diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..1dc3ff6 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,46 @@ +.circleci/ +.azure/ +.vscode/ +.composer/ +.git/ +.gitattributes +.github/ +.gitignore +**/.gitignore +**/.gitkeep +bootstrap/cache/* +database/*.sqlite +.env +CODE_OF_CONDUCT.md +docker-compose.yml +docker-compose.dev.yml +Dockerfile +Dockerfile.dev +.dockerignore +docs/ +fortrabbit.yml +Homestead.* +node_modules/ +npm-debug.log* +persist/ +.phpunit.result.cache +Procfile +public/storage +resources/vendor/ +results/ +.sass-lint.yml +scripts/tests +scripts/vagrant +sonar-project.properties +storage/app/public/* +storage/debugbar/* +storage/framework/cache/* +storage/framework/sessions/* +storage/framework/views/* +storage/logs/* +.styleci.yml +.travis.yml* +vendor/ +yarn-error.log +.sonarlint/ +cypress.env.json diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..876136d --- /dev/null +++ b/.editorconfig @@ -0,0 +1,24 @@ +# This file is for unifying the coding style for different editors and IDEs +# editorconfig.org + +root = true + +[*] +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +indent_style = space +indent_size = 4 + +[*.blade.php] +indent_size = 2 + +[*.{js,vue,scss}] +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false + +[*.yml] +indent_size = 2 diff --git a/.env.dev b/.env.dev new file mode 100644 index 0000000..a826100 --- /dev/null +++ b/.env.dev @@ -0,0 +1,76 @@ +APP_ENV=local +APP_DEBUG=true + +APP_KEY=ChangeMeBy32KeyLengthOrGenerated +HASH_SALT=ChangeMeBy20+KeyLength +HASH_LENGTH=18 + +APP_URL=http://localhost + +DB_CONNECTION=mysql +DB_HOST=mysql +DB_PORT=3306 +DB_DATABASE=monica +DB_USERNAME=homestead +DB_PASSWORD=secret +DB_PREFIX= + +# Mail credentials used to send emails from the application. +MAIL_MAILER=smtp +MAIL_HOST=fake_mail +MAIL_PORT=1025 +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_ENCRYPTION=null +MAIL_FROM_ADDRESS= +MAIL_FROM_NAME="Some Name" +APP_EMAIL_NEW_USERS_NOTIFICATION= + +# Default locale used in the application. +APP_DEFAULT_LOCALE=en + +# Ability to disable signups on your instance. +# Can be true or false. Default to false. +APP_DISABLE_SIGNUP=false +# Enable user email verification. +APP_SIGNUP_DOUBLE_OPTIN=false + +# Set trusted proxy IP addresses. Useful for ssl terminating loadbalancers. +# To trust all proxies that connect directly to your server, use a "*". +# To trust one or more specific proxies that connect directly to your server, use a comma separated list of IP addresses. +APP_TRUSTED_PROXIES= + +# Frequency of creation of new log files. Logs are written when an error occurs. +# Refer to config/logging.php for the possible values. +LOG_CHANNEL=single + +SENTRY_SUPPORT=false +SENTRY_LARAVEL_DSN= + +CHECK_VERSION=false + +REQUIRES_SUBSCRIPTION=false + +# Change this only if you know what you are doing +CACHE_DRIVER=file +SESSION_DRIVER=file +SESSION_LIFETIME=120 +QUEUE_CONNECTION=sync +BROADCAST_DRIVER=log + +# Default filesystem to store uploaded files. +# Possible values: public|s3 +FILESYSTEM_DISK=public + +# AWS keys for S3 when using this storage method +AWS_KEY= +AWS_SECRET= +AWS_REGION=us-east-1 +AWS_BUCKET= +AWS_SERVER= + +# Allow Two Factor Authentication feature on your instance +MFA_ENABLED=true + +# Enable DAV support (beta feature) +DAV_ENABLED=true diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..8304243 --- /dev/null +++ b/.env.example @@ -0,0 +1,176 @@ +# +# Welcome, friend ❤. Thanks for trying out Monica. We hope you'll have fun. +# + +# Two choices: local|production. Use local if you want to install Monica as a +# development version. Use production otherwise. +APP_ENV=local + +# true if you want to show debug information on errors. For production, put this +# to false. +APP_DEBUG=false + +# The encryption key. This is the most important part of the application. Keep +# this secure otherwise, everyone will be able to access your application. +# Must be 32 characters long exactly. +# Use `php artisan key:generate` or `echo -n 'base64:'; openssl rand -base64 32` to generate a random key. +APP_KEY=ChangeMeBy32KeyLengthOrGenerated + +# Prevent information leakage by referring to IDs with hashIds instead of +# the actual IDs used in the database. +HASH_SALT=ChangeMeBy20+KeyLength +HASH_LENGTH=18 + +# The URL of your application. +APP_URL=http://localhost + +# Force using APP_URL as base url of your application. +# You should not need this, unless you are using subdirectory config. +APP_FORCE_URL=false + +# Database information +# To keep this information secure, we urge you to change the default password +# Currently only "mysql" compatible servers are working +DB_CONNECTION=mysql +DB_HOST=127.0.0.1 +DB_PORT=3306 +# You can use mysql unix socket if available, it overrides DB_HOST and DB_PORT values. +#DB_UNIX_SOCKET=/var/run/mysqld/mysqld.sock +DB_DATABASE=monica +DB_USERNAME=homestead +DB_PASSWORD=secret +DB_PREFIX= +DB_TEST_HOST=127.0.0.1 +DB_TEST_PORT=3306 +DB_TEST_DATABASE=monica_test +DB_TEST_USERNAME=homestead +DB_TEST_PASSWORD=secret + +# Use utf8mb4 database charset format to support emoji characters +# ⚠ be sure your DBMS supports utf8mb4 format +DB_USE_UTF8MB4=true + +# Mail credentials used to send emails from the application. +MAIL_MAILER=smtp +MAIL_HOST=mailtrap.io +MAIL_PORT=2525 +MAIL_USERNAME= +MAIL_PASSWORD= +MAIL_ENCRYPTION= +# Outgoing emails will be sent with these identity +MAIL_FROM_ADDRESS= +MAIL_FROM_NAME="Monica instance" +# New registration notification sent to this email +APP_EMAIL_NEW_USERS_NOTIFICATION= + +# Ability to disable signups on your instance. +# Can be true or false. Default to false. +APP_DISABLE_SIGNUP=true + +# Enable user email verification. +APP_SIGNUP_DOUBLE_OPTIN=false + +# Set trusted proxy IP addresses. +# To trust all proxies that connect directly to your server, use a "*". +# To trust one or more specific proxies that connect directly to your server, +# use a comma separated list of IP addresses. +APP_TRUSTED_PROXIES= + +# Enable automatic cloudflare trusted proxy discover +APP_TRUSTED_CLOUDFLARE=false + +# Frequency of creation of new log files. Logs are written when an error occurs. +# Refer to config/logging.php for the possible values. +LOG_CHANNEL=daily + +# Error tracking. Specific to hosted version on .com. You probably don't need +# those. +SENTRY_SUPPORT=false +SENTRY_LARAVEL_DSN= + +# Send a daily ping to https://version.monicahq.com to check if a new version +# is available. When a new version is detected, you will have a message in the +# UI, as well as the release notes for the new changes. Can be true or false. +# Default to true. +CHECK_VERSION=true + +# Cache, session, and queue parameters +# ⚠ Change this only if you know what you are doing +#. Cache: database, file, memcached, redis, dynamodb +#. Session: file, cookie, database, apc, memcached, redis, array +#. Queue: sync, database, beanstalkd, sqs, redis +# If Queue is not set to 'sync', you'll have to set a queue worker +# See https://laravel.com/docs/5.7/queues#running-the-queue-worker +CACHE_DRIVER=database +SESSION_DRIVER=file +SESSION_LIFETIME=120 +QUEUE_CONNECTION=sync + +# If you use redis, set the redis host or ip, like: +#REDIS_HOST=redis + +# Maximum allowed size for uploaded files, in kilobytes. +# Make sure this is an integer, without commas or spaces. +DEFAULT_MAX_UPLOAD_SIZE=10240 + +# Maximum allowed storage size per account, in megabytes. +# Make sure this is an integer, without commas or spaces. +DEFAULT_MAX_STORAGE_SIZE=512 + +# Default filesystem to store uploaded files. +# Possible values: public|s3 +FILESYSTEM_DISK=public + +# AWS keys for S3 when using this storage method +AWS_KEY= +AWS_SECRET= +AWS_REGION=us-east-1 +AWS_BUCKET= +AWS_SERVER= + +# Set to true if you use S3 and need path style URL support for bucket access +# The default is to use virtual-hosted style URLs which may not work everywhere +S3_PATH_STYLE= + +# Allow Two Factor Authentication feature on your instance +MFA_ENABLED=true + +# Enable DAV support +DAV_ENABLED=true + +# CLIENT ID and SECRET used for OAuth authentication +PASSPORT_PASSWORD_GRANT_CLIENT_ID= +PASSPORT_PASSWORD_GRANT_CLIENT_SECRET= + +# Allow to access general statistics about your instance through a public API +# call +ALLOW_STATISTICS_THROUGH_PUBLIC_API_ACCESS=false + +# Indicates that each user in the instance must comply to international policies +# like CASL or GDPR +POLICY_COMPLIANT=true + +# Enable geolocation services +# This is used to translate addresses to GPS coordinates. +ENABLE_GEOLOCATION=false + +# API key for geolocation services +# We use LocationIQ (https://locationiq.com/) to translate addresses to +# latitude/longitude coordinates. We could use Google instead but we don't +# want to give anything to Google, ever. +# LocationIQ offers 5000 free requests per day. +LOCATION_IQ_API_KEY= + +# Enable weather on contact profile page +# Weather can only be fetched if we know longitude/latitude - this is why +# you also need to activate the geolocation service above to make it work +ENABLE_WEATHER=false + +# Access to weather data from darksky api +# https://www.weatherapi.com/signup.aspx +# You need to enable the weather above if you provide an API key here. +WEATHERAPI_KEY= + +# Configure rate limits for RouteService per minute +RATE_LIMIT_PER_MINUTE_API=60 +RATE_LIMIT_PER_MINUTE_OAUTH=5 diff --git a/.env.mischlabs.example b/.env.mischlabs.example new file mode 100644 index 0000000..0f64752 --- /dev/null +++ b/.env.mischlabs.example @@ -0,0 +1,27 @@ +# Copy this file to .env on the NAS and fill the secrets before starting Monica. + +APP_ENV=production +APP_DEBUG=false +APP_URL=https://crm.mischlabs.de +APP_KEY=base64:GENERATE_ME + +DB_CONNECTION=mysql +DB_HOST=db +DB_PORT=3306 +DB_DATABASE=monica +DB_USERNAME=monica +DB_PASSWORD=CHANGE_ME_LONG_RANDOM_PASSWORD +DB_PREFIX= +DB_USE_UTF8MB4=true + +QUEUE_CONNECTION=sync +CACHE_DRIVER=database +SESSION_DRIVER=file + +MAIL_MAILER=log +MAIL_DRIVER=log +MAIL_FROM_ADDRESS=mail.misch@pm.me +MAIL_FROM_NAME="MischCRM" + +APP_TRUSTED_PROXIES=* +MONICA_PORT=38090 diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..309a720 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,96 @@ +module.exports = { + 'env': { + 'browser': true, + 'es6': true, + 'cypress/globals': true + }, + 'extends': [ + 'plugin:vue/recommended' + ], + 'parserOptions': { + 'ecmaVersion': 12, + 'sourceType': 'module' + }, + 'plugins': [ + 'vue', + 'cypress' + ], + 'rules': { + 'array-bracket-spacing': [ + 'error', + 'never' + ], + 'indent': [ + 'error', + 2 + ], + 'linebreak-style': [ + 'error', + 'unix' + ], + 'no-trailing-spaces': [ + 'error', + { + 'ignoreComments': true, + 'skipBlankLines': true + } + ], + 'quotes': [ + 'error', + 'single' + ], + 'semi': [ + 'error', + 'always' + ], + 'semi-spacing': [ + 'error', + { + 'after': true, + 'before': false + } + ], + 'semi-style': [ + 'error', + 'last' + ], + + // strongly recommended + 'vue/component-name-in-template-casing': [ + 'error', + 'kebab-case' + ], + 'vue/component-tags-order': [ + 'error', { + 'order': [ + 'style', + [ + 'template', + 'script' + ] + ] + }], + 'vue/html-end-tags' : 'error', + 'vue/html-self-closing': [ + 'error', + { + 'html': { + 'normal': 'never', + 'void': 'always' + } + } + ], + 'vue/no-v-html' : 0, + 'vue/max-attributes-per-line': [ + // https://vuejs.org/v2/style-guide/#Multi-attribute-elements-strongly-recommended + 'error', + { + 'singleline': 5, + 'multiline': { + 'max': 5, + 'allowFirstLine': true + } + } + ], + } +}; diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8a9d393 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,24 @@ +* text=auto +*.css linguist-vendored +*.scss linguist-vendored +*.js linguist-vendored +/.github export-ignore +/.platform export-ignore +/.vscode export-ignore +/tests export-ignore +/scripts export-ignore +.eslintrc.js export-ignore +.gitattributes export-ignore +.gitignore export-ignore +.platform.app.yaml export-ignore +.releaserc export-ignore +.sass-lint.yml export-ignore +.styleci.yml export-ignore +crowdin.yml export-ignore +cypress.json export-ignore +phpstan.neon export-ignore +phpunit.xml export-ignore +psalm.yml export-ignore +server.php export-ignore +sonar-project.properties export-ignore +template-definition.yaml export-ignore diff --git a/.gitea/workflows/docker-build.yml b/.gitea/workflows/docker-build.yml index 2a59a87..b793f4c 100644 --- a/.gitea/workflows/docker-build.yml +++ b/.gitea/workflows/docker-build.yml @@ -1,4 +1,4 @@ -name: Build & Push Docker Image to Gitea Registry +name: Build & Push Monica Image to Gitea Registry on: push: @@ -55,6 +55,7 @@ jobs: shell: sh run: | docker build \ + -f scripts/docker/Dockerfile \ -t ${{ env.REGISTRY_IMAGE }}:latest \ -t ${{ env.REGISTRY_IMAGE }}:${{ github.sha }} \ . diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..4860ef1 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: [djaiss, asbiin] +patreon: monicahq diff --git a/.github/ISSUE_TEMPLATE/Bug_report.md b/.github/ISSUE_TEMPLATE/Bug_report.md new file mode 100644 index 0000000..042364a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/Bug_report.md @@ -0,0 +1,22 @@ +--- +name: Bug report +about: Create a report to help us improve + +--- + +(Note: you don't need to follow this template, nor to keep headlines or bold sentences - they are just there to guide you. Feel free to delete everything. We review every issue even if we don't immediately respond.) + +Thanks for filing an issue and for your interest in the project. + +**Describe the bug** +A clear and concise description of what the bug is. If your comment is `it doesn't work`, we won't know what to do with it. + +**Screenshots** +If you can, add screenshots to help explain your problem. An image is always helpful. +**Which version are you using:** + - Hosting version on https://app.monicahq.com + - Mobile version + - A server you maintain yourself (if so, please indicate your current version of Monica) + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/Feature_request.md b/.github/ISSUE_TEMPLATE/Feature_request.md new file mode 100644 index 0000000..2c8bdf2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/Feature_request.md @@ -0,0 +1,24 @@ +--- +name: Feature request +about: Suggest an idea for this project + +--- + +(Note: you don't need to follow this template, nor to keep headlines or bold sentences - they are just there to guide you. Feel free to delete everything. We review every issue even if we don't immediately respond.) + +Thanks for filing an issue and for your interest in the project. + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. +* I'm always frustrated when [...] +* Current feature X is awesome but doesn't fill the need of [...] +* Monica doesn't have feature X which is essential as [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered (optional)** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. The more context, the better. diff --git a/.github/ISSUE_TEMPLATE/General_issue.md b/.github/ISSUE_TEMPLATE/General_issue.md new file mode 100644 index 0000000..feac0d5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/General_issue.md @@ -0,0 +1,13 @@ +--- +name: Feedback +about: If you want to leave a general comment + +--- + +(Note: you don't need to follow this template, nor to keep headlines or bold sentences - they are just there to guide you. Feel free to delete everything. We review every issue even if we don't immediately respond.) + +Thanks for filing an issue and for your interest in the project. + +If your issue is about a feature request, tell us why you need it. + +If your issue is about a bug, please be as precise as possible in describing it. \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..982ded7 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,35 @@ +First of all thanks so much for taking the time to open a pull request and help the project. It's because of people like you that we love working on this project. + +Please read the list below. Feel free to delete this text after but we need you to read it so we make sure that the project is consistent and remains of quality. + +### Checklist + +#### Before submitting the PR +- [ ] Read the [CONTRIBUTING document](https://github.com/monicahq/monica/blob/main/CONTRIBUTING.md) before submitting your PR. +- [ ] If the PR is related to an issue or fix one, don't forget to indicate it. +- [ ] Create your PR as draft if it is not final yet. Mark it as ready... when it’s ready. Otherwise the PR will be considered complete and rejected if it's not working. + +### General checks +- [ ] Make sure that the change you propose is the smallest possible. +- [ ] The name of the PR should follow the [conventional commits guideline](https://github.com/monicahq/monica/blob/main/docs/contribute/readme.md#conventional-commits) that the project follows. + +### Front-end changes +- [ ] If you change the UI, make sure to ask repositories administrators first about your changes by pinging djaiss or asbiin in this PR. +- [ ] Screenshots are included if the PR changes the UI. +- [ ] Front-end tests have been written with Cypress. + +#### Backend/models changes +- [ ] The API has been updated. +- [ ] API's documentation has been added by submitting a pull request in the [marketing website repository](https://github.com/monicahq/marketing_site/pulls). +- [ ] Tests have been added for the new code. +- [ ] If you change a model, make sure the SetupTest file is updated. We need seeders to develop locally and generate fake data. + +#### If the code changes the SQL schema +- [ ] Make sure exporting account data as SQL is still working. +- [ ] Make sure your changes do not break importing data with `vCard` and `.csv` files. +- [ ] Make sure account reset and deletion still work. + +#### Other tasks +- [ ] [CONTRIBUTORS](https://github.com/monicahq/monica/blob/main/CONTRIBUTORS) entry added, if necessary. +- [ ] If it's relevant and worth mentioning, create a changelog entry for this change. The changelog entry will appear inside the UI for all users to see. To know if your change is worth the creation of a changelog entry, [read the documentation](https://github.com/monicahq/monica/blob/main/docs/administrators/tips.md#when-is-it-relevant-to-create-a-changelog-entry). +- [ ] Don't forget to [ask for a free account](mailto:regis@monicahq.com) on https://monicahq.com as anyone who contributes can request a free account. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..4c86d15 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,44 @@ +version: 2 + +updates: + # Maintain dependencies for GitHub Actions + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: weekly + time: "11:00" + labels: + - actions + - dependencies + - auto-squash + + # Maintain dependencies for npm + - package-ecosystem: npm + directory: "/" + schedule: + interval: weekly + time: "07:00" + open-pull-requests-limit: 10 + versioning-strategy: lockfile-only + labels: + - javascript + - dependencies + - auto-squash + + # Maintain dependencies for Composer + - package-ecosystem: composer + directory: "/" + schedule: + interval: weekly + time: "07:00" + open-pull-requests-limit: 10 + versioning-strategy: lockfile-only + ignore: + - dependency-name: doctrine/dbal + versions: + - ">= 2.10.a" + - "< 2.11" + labels: + - php + - dependencies + - auto-squash diff --git a/.gitignore b/.gitignore index f06341e..3200b78 100644 --- a/.gitignore +++ b/.gitignore @@ -1,29 +1,30 @@ -# Dependency directories -node_modules/ -jspm_packages/ -web_modules/ - -# SQLite databases -*.db -*.db-journal -*.db-shm -*.db-wal -crm.db -data/ - -# Log files -npm-debug.log* -yarn-debug.log* -yarn-error.log* -*.log - -# Environment variables +.scannerwork/ +/node_modules +/persist +/public/css +/public/fonts +/public/hot +/public/js +/public/storage +/public/mix-manifest.json +/resources/vendor +/results +/storage/oauth-private.key +/storage/oauth-public.key +/tests/cypress/screenshots +/tests/cypress/videos +/vendor +.composer .env -.env.local -.env.development.local -.env.test.local -.env.production.local - -# OS files +.deploy.json .DS_Store -Thumbs.db +.idea +.phpunit.result.cache +.sentry-release +cypress.env.json +Homestead.yaml +Homestead.json +monicadump.sql +npm-debug.log* +php-extensions-*.tar.bz2 +yarn-error.log diff --git a/.platform.app.yaml b/.platform.app.yaml new file mode 100644 index 0000000..8ae5fd2 --- /dev/null +++ b/.platform.app.yaml @@ -0,0 +1,91 @@ +# This file describes an application. You can have multiple applications +# in the same project. + +# The name of this app. Must be unique within a project. +name: app + +# The type of the application to build. +type: php:8.1 +build: + flavor: none + +runtime: + extensions: + - apcu + - gmp + - redis + - sodium + +# The hooks that will be performed when the package is deployed. +hooks: + build: | + set -evx + composer install --no-interaction --no-dev + composer require --update-no-dev platformsh/laravel-bridge + mkdir -p ${SENTRY_ROOT:-/app/vendor/bin} + curl -sL https://sentry.io/get-cli/ | INSTALL_DIR=${SENTRY_ROOT:-/app/vendor/bin} bash + deploy: | + set -evx + rm -f bootstrap/cache/*.php + php artisan monica:update --force --skip-storage-link -vvv + +# The relationships of the application with services or other applications. +# The left-hand side is the name of the relationship as it will be exposed +# to the application in the PLATFORM_RELATIONSHIPS variable. The right-hand +# side is in the form `:`. +relationships: + database: "db:mysql" + rediscache: "cache:redis" + redissession: "cache:redis" + +# The size of the persistent disk of the application (in MB). +disk: 512 + +# The mounts that will be performed when the package is deployed. +mounts: + "storage/app/public": + source: local + source_path: "public" + "storage/app/temp": + source: local + source_path: "temp" + "storage/framework/views": + source: local + source_path: "views" + "storage/framework/sessions": + source: local + source_path: "sessions" + "storage/framework/cache": + source: local + source_path: "cache" + "storage/logs": + source: local + source_path: "logs" + "bootstrap/cache": + source: local + source_path: "cache" + "/.config": + source: local + source_path: "config" + +# The configuration of app when it is exposed to the web. +web: + locations: + "/": + root: "public" + index: + - index.php + allow: true + passthru: "/index.php" + +workers: + queue: + size: S + commands: + start: | + php artisan queue:work --sleep=3 --tries=3 --queue=default,migration + +crons: + scheduler: + spec: '*/5 * * * *' + cmd: 'php artisan schedule:run -v' diff --git a/.platform/routes.yaml b/.platform/routes.yaml new file mode 100644 index 0000000..7b5cdf8 --- /dev/null +++ b/.platform/routes.yaml @@ -0,0 +1,20 @@ +# The routes of the project. +# +# Each route describes how an incoming URL is going +# to be processed by Platform.sh. + +"https://{default}/": + type: upstream + upstream: "app:http" +"https://www.{default}/": + type: redirect + to: "https://{default}/" +"https://{default}/.well-known/carddav": + type: redirect + to: "https://{default}/dav" +"https://{default}/.well-known/caldav": + type: redirect + to: "https://{default}/dav" +"https://{default}/.well-known/security.txt": + type: redirect + to: "https://{default}/security.txt" diff --git a/.platform/services.yaml b/.platform/services.yaml new file mode 100644 index 0000000..59f286e --- /dev/null +++ b/.platform/services.yaml @@ -0,0 +1,6 @@ +db: + type: mariadb:10.4 + disk: 2048 + +cache: + type: redis:5.0 diff --git a/.releaserc b/.releaserc new file mode 100644 index 0000000..919562e --- /dev/null +++ b/.releaserc @@ -0,0 +1,38 @@ +{ + "branches": [ + "main", + "4.x", + "next", + "next-major", + {"name": "beta", "prerelease": true}, + {"name": "alpha", "prerelease": true} + ], + "plugins": [ + [ + "@semantic-release/commit-analyzer", + { + "preset": "conventionalcommits", + "releaseRules": [ + {"scope": "no-release", "release": false} + ] + } + ], + [ + "@semantic-release/release-notes-generator", + { + "preset": "conventionalcommits", + "writerOpts": { + "commitGroupsSort": ["feat"], + "commitsSort": ["scope", "subject"] + } + } + ], + [ + "@semantic-release/changelog", + { + "changelogFile": "CHANGELOG.md" + } + ], + "@semantic-release/github" + ] +} diff --git a/.sass-lint.yml b/.sass-lint.yml new file mode 100644 index 0000000..afcfcc5 --- /dev/null +++ b/.sass-lint.yml @@ -0,0 +1,94 @@ +options: + formatter: stylish +files: + include: '**/*.s+(a|c)ss' +rules: + # Extends + extends-before-mixins: 1 + extends-before-declarations: 1 + placeholder-in-extend: 1 + + # Mixins + mixins-before-declarations: 1 + + # Line Spacing + one-declaration-per-line: 1 + empty-line-between-blocks: 1 + single-line-per-selector: 1 + + # Disallows + no-attribute-selectors: 0 + no-color-hex: 0 + no-color-keywords: 1 + no-color-literals: 0 + no-combinators: 0 + no-css-comments: 1 + no-debug: 1 + no-disallowed-properties: 0 + no-duplicate-properties: 1 + no-empty-rulesets: 1 + no-extends: 0 + no-ids: 1 + no-important: 1 + no-invalid-hex: 1 + no-mergeable-selectors: 1 + no-misspelled-properties: 1 + no-qualifying-elements: 1 + no-trailing-whitespace: 1 + no-trailing-zero: 1 + no-transition-all: 1 + no-universal-selectors: 0 + no-url-protocols: 1 + no-vendor-prefixes: 1 + no-warn: 1 + property-units: 0 + + # Nesting + force-attribute-nesting: 3 + force-element-nesting: 1 + force-pseudo-nesting: 1 + + # Name Formats + class-name-format: 1 + function-name-format: 1 + id-name-format: 0 + mixin-name-format: 1 + placeholder-name-format: 1 + variable-name-format: 1 + + # Style Guide + attribute-quotes: 1 + bem-depth: 0 + border-zero: 1 + brace-style: 1 + clean-import-paths: 1 + empty-args: 1 + hex-length: 1 + hex-notation: 1 + indentation: 1 + leading-zero: 1 + nesting-depth: + - 1 + - + max-depth: 5 + property-sort-order: 1 + pseudo-element: 1 + quotes: 1 + shorthand-values: 1 + url-quotes: 1 + variable-for-property: 1 + zero-unit: 1 + + # Inner Spacing + space-after-comma: 1 + space-before-colon: 1 + space-after-colon: 1 + space-before-brace: 1 + space-before-bang: 1 + space-after-bang: 1 + space-between-parens: 1 + space-around-operator: 1 + + # Final Items + trailing-semicolon: 1 + final-newline: 1 \ No newline at end of file diff --git a/.snyk b/.snyk new file mode 100644 index 0000000..42c707d --- /dev/null +++ b/.snyk @@ -0,0 +1,78 @@ +# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. +version: v1.14.1 +ignore: {} +# patches apply the minimum changes required to fix a vulnerability +patch: + SNYK-JS-AXIOS-174505: + - axios: + patched: '2019-05-06T05:47:15.257Z' + SNYK-JS-LODASH-450202: + - cypress > lodash: + patched: '2019-07-12T16:10:37.161Z' + SNYK-JS-HTTPSPROXYAGENT-469131: + - snyk > proxy-agent > https-proxy-agent: + patched: '2019-10-04T05:47:03.270Z' + - snyk > proxy-agent > pac-proxy-agent > https-proxy-agent: + patched: '2019-10-04T05:47:03.270Z' + SNYK-JS-TREEKILL-536781: + - snyk > snyk-sbt-plugin > tree-kill: + patched: '2019-12-12T05:47:36.247Z' + SNYK-JS-LODASH-567746: + - lodash: + patched: '2020-04-30T15:46:03.231Z' + - cypress > lodash: + patched: '2020-04-30T15:46:03.231Z' + - eslint > lodash: + patched: '2020-04-30T15:46:03.231Z' + - mocha-multi-reporters > lodash: + patched: '2020-04-30T15:46:03.231Z' + - laravel-mix > lodash: + patched: '2020-04-30T15:46:03.231Z' + - eslint > inquirer > lodash: + patched: '2020-04-30T15:46:03.231Z' + - eslint > table > lodash: + patched: '2020-04-30T15:46:03.231Z' + - eslint-plugin-vue > vue-eslint-parser > lodash: + patched: '2020-04-30T15:46:03.231Z' + - mocha > yargs-unparser > lodash: + patched: '2020-04-30T15:46:03.231Z' + - laravel-mix > @babel/core > lodash: + patched: '2020-04-30T15:46:03.231Z' + - laravel-mix > css-loader > lodash: + patched: '2020-04-30T15:46:03.231Z' + - laravel-mix > webpack-merge > lodash: + patched: '2020-04-30T15:46:03.231Z' + - laravel-mix > @babel/preset-env > @babel/plugin-transform-block-scoping > lodash: + patched: '2020-04-30T15:46:03.231Z' + - laravel-mix > babel-merge > @babel/core > lodash: + patched: '2020-04-30T15:46:03.231Z' + - laravel-mix > extract-text-webpack-plugin > async > lodash: + patched: '2020-04-30T15:46:03.231Z' + - laravel-mix > optimize-css-assets-webpack-plugin > last-call-webpack-plugin > lodash: + patched: '2020-04-30T15:46:03.231Z' + - laravel-mix > webpack-dev-server > http-proxy-middleware > lodash: + patched: '2020-04-30T15:46:03.231Z' + - laravel-mix > @babel/preset-env > @babel/plugin-transform-modules-umd > @babel/helper-module-transforms > lodash: + patched: '2020-04-30T15:46:03.231Z' + - laravel-mix > babel-merge > @babel/core > @babel/traverse > lodash: + patched: '2020-04-30T15:46:03.231Z' + - laravel-mix > webpack-dev-server > portfinder > async > lodash: + patched: '2020-04-30T15:46:03.231Z' + - laravel-mix > @babel/preset-env > @babel/plugin-transform-unicode-regex > @babel/helper-create-regexp-features-plugin > @babel/helper-regex > lodash: + patched: '2020-04-30T15:46:03.231Z' + - laravel-mix > babel-merge > @babel/core > @babel/helpers > @babel/traverse > lodash: + patched: '2020-04-30T15:46:03.231Z' + - laravel-mix > @babel/preset-env > @babel/plugin-transform-exponentiation-operator > @babel/helper-builder-binary-assignment-operator-visitor > @babel/helper-explode-assignable-expression > @babel/traverse > lodash: + patched: '2020-04-30T15:46:03.231Z' + - laravel-mix > babel-merge > @babel/core > @babel/helpers > @babel/traverse > @babel/generator > lodash: + patched: '2020-04-30T15:46:03.231Z' + - laravel-mix > babel-merge > @babel/core > @babel/helpers > @babel/traverse > @babel/helper-split-export-declaration > @babel/types > lodash: + patched: '2020-04-30T15:46:03.231Z' + - laravel-mix > @babel/preset-env > @babel/plugin-transform-exponentiation-operator > @babel/helper-builder-binary-assignment-operator-visitor > @babel/helper-explode-assignable-expression > @babel/traverse > @babel/generator > lodash: + patched: '2020-04-30T15:46:03.231Z' + - laravel-mix > @babel/preset-env > @babel/plugin-transform-exponentiation-operator > @babel/helper-builder-binary-assignment-operator-visitor > @babel/helper-explode-assignable-expression > @babel/traverse > @babel/helper-split-export-declaration > @babel/types > lodash: + patched: '2020-04-30T15:46:03.231Z' + - laravel-mix > babel-merge > @babel/core > @babel/helpers > @babel/traverse > @babel/helper-function-name > @babel/helper-get-function-arity > @babel/types > lodash: + patched: '2020-04-30T15:46:03.231Z' + - laravel-mix > @babel/preset-env > @babel/plugin-transform-exponentiation-operator > @babel/helper-builder-binary-assignment-operator-visitor > @babel/helper-explode-assignable-expression > @babel/traverse > @babel/helper-function-name > @babel/helper-get-function-arity > @babel/types > lodash: + patched: '2020-04-30T15:46:03.231Z' diff --git a/.styleci.yml b/.styleci.yml new file mode 100644 index 0000000..33d0075 --- /dev/null +++ b/.styleci.yml @@ -0,0 +1,11 @@ +preset: laravel +enabled: + - length_ordered_imports + - fully_qualified_strict_types +disabled: + - alpha_ordered_imports + - no_useless_return +finder: + not-name: + - index.php + - server.php diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..0349f0f --- /dev/null +++ b/.tool-versions @@ -0,0 +1,3 @@ +php 8.1.0 +nodejs 16.15.0 +yarn 1.22.19 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..54cbc14 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,1381 @@ +## [4.0.0](https://github.com/monicahq/monica/compare/v3.7.0...v4.0.0) (2023-01-30) + + +### ⚠ BREAKING CHANGES + +* switch to php 8.1+ dependency (#6250) +* drop php 7.4 support (#6246) + +### Features + +* add DB_TESTING_PORT in database config ([#6201](https://github.com/monicahq/monica/issues/6201)) ([fefa799](https://github.com/monicahq/monica/commit/fefa79968cc1257372df433e9234a283b75f5b5a)) +* add disallow in robots.txt ([#6268](https://github.com/monicahq/monica/issues/6268)) ([be2e280](https://github.com/monicahq/monica/commit/be2e28070c4f9eaf65bf260a731b606d0de48010)) +* add name to user resource ([#6174](https://github.com/monicahq/monica/issues/6174)) ([8465803](https://github.com/monicahq/monica/commit/84658036a5dad7fc4e68b3d112b7a5cd9c0c6954)) +* check male translation and fall back to generic ([#6039](https://github.com/monicahq/monica/issues/6039)) ([4ba9062](https://github.com/monicahq/monica/commit/4ba9062f95b4c17bef770eb698bf381f9a1de19b)) +* drop php 7.4 support ([#6246](https://github.com/monicahq/monica/issues/6246)) ([84d0232](https://github.com/monicahq/monica/commit/84d0232095c4178be64076d1c52f7dc2e2a8caeb)) +* focus tags input box ([#6392](https://github.com/monicahq/monica/issues/6392)) ([2d75053](https://github.com/monicahq/monica/commit/2d7505368c9e87cdfb5c9ed2301c705b09a562c7)) +* load more activities ([#5973](https://github.com/monicahq/monica/issues/5973)) ([117fe19](https://github.com/monicahq/monica/commit/117fe19545e433163600e45704ea901935a5aa4a)) +* switch to php 8.1+ dependency ([#6250](https://github.com/monicahq/monica/issues/6250)) ([6a7f49f](https://github.com/monicahq/monica/commit/6a7f49fd90becc39cdf480e38e269a67f5b2215f)) + + +### Bug Fixes + +* allow configuring port for test database ([#6236](https://github.com/monicahq/monica/issues/6236)) ([aeffb71](https://github.com/monicahq/monica/commit/aeffb7184e95ce6e5a68187b5a8528fbcc95bac6)), closes [#6200](https://github.com/monicahq/monica/issues/6200) +* allow empty completed_at task date ([#6025](https://github.com/monicahq/monica/issues/6025)) ([d4504e3](https://github.com/monicahq/monica/commit/d4504e3267ceb01e3e46b65a8e09b0d88e461170)) +* change APP_TRUST_PROXIES to APP_TRUSTED_PROXIES ([#6095](https://github.com/monicahq/monica/issues/6095)) ([5f63bed](https://github.com/monicahq/monica/commit/5f63bed75d53b6f35b9b3fc20a24d449a1bcfebf)) +* Continuously pressing enter shows empty tags ([#6314](https://github.com/monicahq/monica/issues/6314)) ([2386096](https://github.com/monicahq/monica/commit/23860966acb284c8bb46be282668932437070865)), closes [#6235](https://github.com/monicahq/monica/issues/6235) +* fix avatar not being loaded on dashboard ([#6224](https://github.com/monicahq/monica/issues/6224)) ([7c8105c](https://github.com/monicahq/monica/commit/7c8105c338e4c1f9a3111c2e22b5974cd13e0736)) +* fix blurry modals from sweet-modal-vue ([#6026](https://github.com/monicahq/monica/issues/6026)) ([4cc1d8f](https://github.com/monicahq/monica/commit/4cc1d8f251fcc5fb23c3db0a03cf436389a140d5)) +* fix Journal sidebar width on mobile ([#6027](https://github.com/monicahq/monica/issues/6027)) ([d690bf6](https://github.com/monicahq/monica/commit/d690bf6019fdf21e3ecbbcc7a1fcef34a2b8ab82)) +* fix laravel cloudflare proxy ([#6264](https://github.com/monicahq/monica/issues/6264)) ([d0b50fe](https://github.com/monicahq/monica/commit/d0b50fec143dee5572c814cc8dbbf6eb8ddc6400)) +* life event creation with unknown month/day ([#6046](https://github.com/monicahq/monica/issues/6046)) ([d81123b](https://github.com/monicahq/monica/commit/d81123b5ac8a9ffc70b01967b81013be5c15384d)) +* only include real contacts in carddav sync ([#6014](https://github.com/monicahq/monica/issues/6014)) ([626f078](https://github.com/monicahq/monica/commit/626f078e73ad65b330b5a35156a9d7885e6ce10f)) +* **php8.1:** deprecated trim with null value ([#6374](https://github.com/monicahq/monica/issues/6374)) ([b4c1c03](https://github.com/monicahq/monica/commit/b4c1c0385059068290c501c63b560b7b411f38f9)) +* skip version check if current version is empty ([#6137](https://github.com/monicahq/monica/issues/6137)) ([4e1e4ee](https://github.com/monicahq/monica/commit/4e1e4ee1e9c0bca573e71b447aedcb2cd019d819)) +* typo in french translation of nephew ([#6074](https://github.com/monicahq/monica/issues/6074)) ([ad11e01](https://github.com/monicahq/monica/commit/ad11e01de971b6cc2bf88322437cab1c454263f9)) +* vcard bday export format with unknown year ([#6087](https://github.com/monicahq/monica/issues/6087)) ([f0db671](https://github.com/monicahq/monica/commit/f0db6716d6f2fc0b74705768e1092bd9516136ab)) + +# [3.7.0](https://github.com/monicahq/monica/compare/v3.6.1...v3.7.0) (2022-02-06) + + +### Bug Fixes + +* fix APP_TRUST_PROXIES ([#5955](https://github.com/monicahq/monica/issues/5955)) ([e930afb](https://github.com/monicahq/monica/commit/e930afb76d95199c131c7417367c7c973fa5868d)) +* fix month reminder view ([#5914](https://github.com/monicahq/monica/issues/5914)) ([503fb36](https://github.com/monicahq/monica/commit/503fb36c8b102e0a372a935bcb0435e907311cd1)) +* fix weather short date ([#5901](https://github.com/monicahq/monica/issues/5901)) ([defcf43](https://github.com/monicahq/monica/commit/defcf43e0a0e6dbe20ae3a1313d5afa1b7cf6a0c)) + + +### Features + +* update laravel-cloudflare ([#5904](https://github.com/monicahq/monica/issues/5904)) ([458642a](https://github.com/monicahq/monica/commit/458642a50be5fad8b0e921c1b427af045d909e93)) + +## [3.6.1](https://github.com/monicahq/monica/compare/v3.6.0...v3.6.1) (2022-01-12) + + +### Bug Fixes + +* fix contact search + adorable return data ([#5881](https://github.com/monicahq/monica/issues/5881)) ([3309785](https://github.com/monicahq/monica/commit/33097850af0fdefa50f06b8423ed40ce051812ff)) +* fix heroku deploy ([#5879](https://github.com/monicahq/monica/issues/5879)) ([2812ed3](https://github.com/monicahq/monica/commit/2812ed318a97120eff6d80f42bfa8eab053a6cc9)) + +# [3.6.0](https://github.com/monicahq/monica/compare/v3.5.0...v3.6.0) (2022-01-11) + + +### Features + +* activate Norwegian and Russian languages ([#5856](https://github.com/monicahq/monica/issues/5856)) ([8bdccbb](https://github.com/monicahq/monica/commit/8bdccbb7f9114600fc5eabecf334ede87a96b4eb)) +* add contact soft delete and prunable ([#5826](https://github.com/monicahq/monica/issues/5826)) ([6f887df](https://github.com/monicahq/monica/commit/6f887dfb7590ee62833a8d99617e0993eb1a83db)) +* add reminders/upcoming API ([#5783](https://github.com/monicahq/monica/issues/5783)) ([a3e9b79](https://github.com/monicahq/monica/commit/a3e9b79236b2e388a4823f0ffb061551de05e406)) +* export data as json format ([#4779](https://github.com/monicahq/monica/issues/4779)) ([8c627a2](https://github.com/monicahq/monica/commit/8c627a28cbc18599e1f56df1a4e943d18d558ec0)) +* implement laravel password strength ([#5821](https://github.com/monicahq/monica/issues/5821)) ([8295be3](https://github.com/monicahq/monica/commit/8295be3f5872302360370acf29df985088320c0f)) +* improve reliability of pingversion ([#5723](https://github.com/monicahq/monica/issues/5723)) ([0c791f6](https://github.com/monicahq/monica/commit/0c791f6c15e048a69b010f7c549220c4ef51d91c)) +* order introductions contact list by first and last name ([#5102](https://github.com/monicahq/monica/issues/5102)) ([6ff0738](https://github.com/monicahq/monica/commit/6ff0738a6e115783003744ef1d6045e1f9957cc4)) +* quick add with email ([#5182](https://github.com/monicahq/monica/issues/5182)) ([80001fc](https://github.com/monicahq/monica/commit/80001fc1dfa8989c4b53d494cb4d697e07668239)) +* re-activate adorable avatars with permanent solution ([#5872](https://github.com/monicahq/monica/issues/5872)) ([ccf6d4f](https://github.com/monicahq/monica/commit/ccf6d4fe2f3657677f7e0de74054f5414c2f6727)) +* sync carddav delete contact requests ([#5835](https://github.com/monicahq/monica/issues/5835)) ([30d97f9](https://github.com/monicahq/monica/commit/30d97f9321ffe82641e16599541bbb2498da56d1)) + + +### Bug Fixes + +* add link to reminders endpoint at api root ([#5801](https://github.com/monicahq/monica/issues/5801)) ([337367a](https://github.com/monicahq/monica/commit/337367a89ec94da758ad1f4d65588640f3c34088)) +* fix Date display with timezone ([#5825](https://github.com/monicahq/monica/issues/5825)) ([d73e3c4](https://github.com/monicahq/monica/commit/d73e3c41eab0c33aef03040d35f55d549c402880)) +* version display on heroku ([#5860](https://github.com/monicahq/monica/issues/5860)) ([0cf965f](https://github.com/monicahq/monica/commit/0cf965fd617664d34b404ba5cc168a70c2c83ee3)) + +# [3.5.0](https://github.com/monicahq/monica/compare/v3.4.0...v3.5.0) (2021-11-19) + + +### Bug Fixes + +* fix display empty weather ([#5685](https://github.com/monicahq/monica/issues/5685)) ([1afa06e](https://github.com/monicahq/monica/commit/1afa06ec1fccd3dbd5c2fe68728e33a70ce2a1c4)) +* fix weather get attribute ([#5705](https://github.com/monicahq/monica/issues/5705)) ([25e5e59](https://github.com/monicahq/monica/commit/25e5e59b56f1f7d63bde5247e821a68341ad4c9e)) + + +### Features + +* use ipdata to get infos from ip ([#5680](https://github.com/monicahq/monica/issues/5680)) ([339b0fe](https://github.com/monicahq/monica/commit/339b0feb6dc1d4c9b447984abc4dd8eb3160ece7)) + +# [3.4.0](https://github.com/monicahq/monica/compare/v3.3.1...v3.4.0) (2021-10-31) + + +### Features + +* add dependencies node and yarn in Dockerfile ([#5635](https://github.com/monicahq/monica/issues/5635)) ([48726b5](https://github.com/monicahq/monica/commit/48726b5edf646ef5fd7c4594847b0315fd800e3c)) +* added URLs to be exported in vCards. ([#5609](https://github.com/monicahq/monica/issues/5609)) ([38429a2](https://github.com/monicahq/monica/commit/38429a25a2b1651f6caa3d739c0876d9d2dfa5b4)) +* get weather from weatherapi ([#5668](https://github.com/monicahq/monica/issues/5668)) ([d19b6ad](https://github.com/monicahq/monica/commit/d19b6adc378acda567058a5054c0d6b89694229c)) +* retry get gps coordinate when rate limited second ([#5615](https://github.com/monicahq/monica/issues/5615)) ([8eed44e](https://github.com/monicahq/monica/commit/8eed44e48ecee3f19e57665a67698cb1df8ae8f0)) +* searchable contacts on introductions form ([#5632](https://github.com/monicahq/monica/issues/5632)) ([cc05552](https://github.com/monicahq/monica/commit/cc05552320114e13aed189bccd5d7b7d11eb0bba)) +* update last called attribute ([#5614](https://github.com/monicahq/monica/issues/5614)) ([83e1d68](https://github.com/monicahq/monica/commit/83e1d680861b9242cf4fcf52e8e8476688f4e93d)) + + +### Bug Fixes + +* fix carddav addressbook add ([#5660](https://github.com/monicahq/monica/issues/5660)) ([ac44cfb](https://github.com/monicahq/monica/commit/ac44cfb4e00cbc2c6223acb7c0bdba9fc5725934)) +* fix creating default gender ([#5607](https://github.com/monicahq/monica/issues/5607)) ([6c5ac48](https://github.com/monicahq/monica/commit/6c5ac48df4eb25ca7da871c2e41d702f25e7630b)) +* fix distant contact etag handle ([#5605](https://github.com/monicahq/monica/issues/5605)) ([1da427f](https://github.com/monicahq/monica/commit/1da427f113e56b3c3c519aaf0ef8b224e35b5d24)) +* fix duplicate reminders on dashboard ([#5569](https://github.com/monicahq/monica/issues/5569)) ([bb97115](https://github.com/monicahq/monica/commit/bb971155d40e5f9f2d85af14e0b0915d67d4a1e5)) +* fix edit an activity with a category ([#5661](https://github.com/monicahq/monica/issues/5661)) ([9128db8](https://github.com/monicahq/monica/commit/9128db8b6f9df6bb1e3c12749c0627b575480311)) +* fix gift api without passport ([#5664](https://github.com/monicahq/monica/issues/5664)) ([7939a5f](https://github.com/monicahq/monica/commit/7939a5f8fcbfd5be671dc02df63e61ac8d4acc63)) +* fix import table layout ([#5662](https://github.com/monicahq/monica/issues/5662)) ([cd138c8](https://github.com/monicahq/monica/commit/cd138c83b41928982d01dae63e9258658bd5dc15)) +* fix vcard company import ([#5616](https://github.com/monicahq/monica/issues/5616)) ([0dd4b23](https://github.com/monicahq/monica/commit/0dd4b23baf799757d0555b07119048571c4a16b1)) + +## [3.3.1](https://github.com/monicahq/monica/compare/v3.3.0...v3.3.1) (2021-10-10) + + +### Bug Fixes + +* allow delete any reminder + fix reminder edit data ([#5582](https://github.com/monicahq/monica/issues/5582)) ([981a639](https://github.com/monicahq/monica/commit/981a639013e4b3124a84450b6aeca97dcf0208c2)) +* fix davclient options call ([#5584](https://github.com/monicahq/monica/issues/5584)) ([9c276aa](https://github.com/monicahq/monica/commit/9c276aaa6e12c10b227f4fe95fbe215f67dcde9b)) + +# [3.3.0](https://github.com/monicahq/monica/compare/v3.2.0...v3.3.0) (2021-10-09) + + +### Bug Fixes + +* :bug: people tags filter link ([#5568](https://github.com/monicahq/monica/issues/5568)) ([0cabd16](https://github.com/monicahq/monica/commit/0cabd166525b56cc290c7a593497790c3703126d)) +* docker dev add version ([#5529](https://github.com/monicahq/monica/issues/5529)) ([781f805](https://github.com/monicahq/monica/commit/781f805da7a727f03d7057790f76229ffc0d6eaa)) +* fix dav client options checks ([#5532](https://github.com/monicahq/monica/issues/5532)) ([3812232](https://github.com/monicahq/monica/commit/38122320e2be0289aa6aa0bec6fbd48cbf71bb65)) +* fix import vcard photo ([#5577](https://github.com/monicahq/monica/issues/5577)) ([741f5a7](https://github.com/monicahq/monica/commit/741f5a7a7570bacf6b13294f58cb755e0e03797a)) +* fix quick contact creation ([#5572](https://github.com/monicahq/monica/issues/5572)) ([bab87db](https://github.com/monicahq/monica/commit/bab87db46611fd1ae1ebbb3f40619356d886f298)) +* nickname label wrong on contact edit ([#5576](https://github.com/monicahq/monica/issues/5576)) ([dd7970c](https://github.com/monicahq/monica/commit/dd7970ca51b2c11694ef0493abb024127c100f04)) +* null reference on gift photo upload ([#5547](https://github.com/monicahq/monica/issues/5547)) ([2c33e0b](https://github.com/monicahq/monica/commit/2c33e0b8ddb0e5f4ac108b75a81b0d2ae8858264)), closes [#5516](https://github.com/monicahq/monica/issues/5516) [#5397](https://github.com/monicahq/monica/issues/5397) +* package.json & yarn.lock to reduce vulnerabilities ([#5580](https://github.com/monicahq/monica/issues/5580)) ([57ae565](https://github.com/monicahq/monica/commit/57ae565abe3c953fb610753b300ebbf5fe36a247)) + + +### Features + +* add a script to build docker dev ([#5531](https://github.com/monicahq/monica/issues/5531)) ([2655231](https://github.com/monicahq/monica/commit/2655231b4fdd3bc44b13e9800282873ecb608062)) +* add configurable rate limit for api and oauth ([#5489](https://github.com/monicahq/monica/issues/5489)) ([bc50181](https://github.com/monicahq/monica/commit/bc50181780332ba79d8c9cc8305981df5932cad0)) +* add new stackerrorlog log channel ([#5578](https://github.com/monicahq/monica/issues/5578)) ([5fd6eb2](https://github.com/monicahq/monica/commit/5fd6eb29554b6a19c5d8c3c73db302b9bbd39ee1)) +* add next reminder date to stayintouch ([#5491](https://github.com/monicahq/monica/issues/5491)) ([c544deb](https://github.com/monicahq/monica/commit/c544deb3d102d1e96dc513e083dcbf36b4bba9a0)) +* add total of participants in activity ([#5474](https://github.com/monicahq/monica/issues/5474)) ([9194eb3](https://github.com/monicahq/monica/commit/9194eb31670612ff4db77bf06677c1854337eecc)) +* carddav client ([#3851](https://github.com/monicahq/monica/issues/3851)) ([e6c92cf](https://github.com/monicahq/monica/commit/e6c92cf00580340c27d5327f6eb88e6e4cc5a61b)) +* import vcard using uuid ([#5533](https://github.com/monicahq/monica/issues/5533)) ([160b36e](https://github.com/monicahq/monica/commit/160b36eed2841902f24553bc071ea7b8a01a144d)) +* use Http facade for DavClient ([#5573](https://github.com/monicahq/monica/issues/5573)) ([a669e98](https://github.com/monicahq/monica/commit/a669e98f83d98bd8997553d133a30c9c0602f80e)) +* use queue to update contacts with carddav ([#5575](https://github.com/monicahq/monica/issues/5575)) ([0e989fe](https://github.com/monicahq/monica/commit/0e989fec3504a084515c8eb4c7ce6cebc30263cf)) + +# [3.2.0](https://github.com/monicahq/monica/compare/v3.1.3...v3.2.0) (2021-08-26) + + +### Features + +* activate greek language ([#5453](https://github.com/monicahq/monica/issues/5453)) ([c571001](https://github.com/monicahq/monica/commit/c571001e066375167e105aa18b7d5ce18bd5e0af)) +* add path style URL support for S3 buckets ([#5362](https://github.com/monicahq/monica/issues/5362)) ([ee6206a](https://github.com/monicahq/monica/commit/ee6206a08064fdf799aa87051ecd2c0bfc48f9fb)) +* add Portuguese-BR language ([#5333](https://github.com/monicahq/monica/issues/5333)) ([aca3c7a](https://github.com/monicahq/monica/commit/aca3c7a5bfc862084799b4de5e9afb4163f6bd31)) +* add Vietnamese language ([#5343](https://github.com/monicahq/monica/issues/5343)) ([bf2d570](https://github.com/monicahq/monica/commit/bf2d570bd71c645f23b393beebd3550fddb993d4)) +* allow to update a subscription frequency ([#5436](https://github.com/monicahq/monica/issues/5436)) ([e298c63](https://github.com/monicahq/monica/commit/e298c63dd2beb1e8f048cbf32a1dc36e66ce4061)) +* revoke session from other browser after a password change ([#5328](https://github.com/monicahq/monica/issues/5328)) ([a4c037f](https://github.com/monicahq/monica/commit/a4c037f539e282491496995925159463edec6629)) + + +### Bug Fixes + +* contact name population on delete confirmation ([#5431](https://github.com/monicahq/monica/issues/5431)) ([ffd0e86](https://github.com/monicahq/monica/commit/ffd0e867361d916d00b4e0d654a537a4d6f8b998)) + +## [3.1.3](https://github.com/monicahq/monica/compare/v3.1.2...v3.1.3) (2021-06-28) + + +### Bug Fixes + +* fix layout selection ([#5313](https://github.com/monicahq/monica/issues/5313)) ([8b4821f](https://github.com/monicahq/monica/commit/8b4821f1393a1fad31d3ba3cf30679d483197664)) +* use post request for exportToSql ([#5314](https://github.com/monicahq/monica/issues/5314)) ([cefeb9b](https://github.com/monicahq/monica/commit/cefeb9bdfa74e30ff77ff692c3d14c05ae081bff)) + +## [3.1.2](https://github.com/monicahq/monica/compare/v3.1.1...v3.1.2) (2021-06-24) + + +### Bug Fixes + +* fix search being extremely slow ([#5306](https://github.com/monicahq/monica/issues/5306)) ([8ba7d98](https://github.com/monicahq/monica/commit/8ba7d983efcef7555b620cb1a0bbc32db3835f00)) + +## [3.1.1](https://github.com/monicahq/monica/compare/v3.1.0...v3.1.1) (2021-06-23) + + +### Bug Fixes + +* fix search with additional info ([#5301](https://github.com/monicahq/monica/issues/5301)) ([13325cc](https://github.com/monicahq/monica/commit/13325cc8c1f32b391905a35abb166f843faef142)) + +# [3.1.0](https://github.com/monicahq/monica/compare/v3.0.1...v3.1.0) (2021-06-22) + + +### Features + +* add a console command to see memcached stats ([#5186](https://github.com/monicahq/monica/issues/5186)) ([b359c90](https://github.com/monicahq/monica/commit/b359c90206bc4bddbd87fe8ad7a11993304a8542)) +* add a rate limiter for locationiq queries ([#5185](https://github.com/monicahq/monica/issues/5185)) ([f8442ba](https://github.com/monicahq/monica/commit/f8442ba507181d425e3dafb6a06555a15518b384)) +* add Indonesian language ([#5190](https://github.com/monicahq/monica/issues/5190)) ([16cd47e](https://github.com/monicahq/monica/commit/16cd47e925144b3ec20c0ad851bb7bd595af4438)) +* add new logging stack for papertrail+errorlog ([#5166](https://github.com/monicahq/monica/issues/5166)) ([744efb0](https://github.com/monicahq/monica/commit/744efb0e6bfbe3f452999285583b290e21c8b61c)) +* add notes when importing vcard ([#5216](https://github.com/monicahq/monica/issues/5216)) ([36912bc](https://github.com/monicahq/monica/commit/36912bc5ef7d06f281e555f93f42d91fbb7ccb63)) +* allow recovery codes when disabling 2FA ([#4970](https://github.com/monicahq/monica/issues/4970)) ([1f4c4c4](https://github.com/monicahq/monica/commit/1f4c4c4b6c2c39dc4917600220d78a44580d1327)) +* datestamp filename of exported SQL file. ([#5136](https://github.com/monicahq/monica/issues/5136)) ([a658fcf](https://github.com/monicahq/monica/commit/a658fcf074b36ba6a8855ecaa7b3c13a3e78888d)) +* download and get storage files as private ([#5192](https://github.com/monicahq/monica/issues/5192)) ([7fdc445](https://github.com/monicahq/monica/commit/7fdc4453b688781a651f09d9b6cbc274ac3fbdbb)) +* email field on add person ([#5097](https://github.com/monicahq/monica/issues/5097)) ([2392afc](https://github.com/monicahq/monica/commit/2392afc0aaa9d5f79d5f3f6357912c70a4d96ca1)) +* make archived contact readonly ([#5285](https://github.com/monicahq/monica/issues/5285)) ([a3fdac9](https://github.com/monicahq/monica/commit/a3fdac949f662d08ced6e02bcf63fd5106600fb0)) +* search notes when searching through contacts ([#5103](https://github.com/monicahq/monica/issues/5103)) ([6378bc1](https://github.com/monicahq/monica/commit/6378bc183df414175a9aee49c94780247ef27b94)) + + +### Bug Fixes + +* fix import vcard stability ([#5160](https://github.com/monicahq/monica/issues/5160)) ([3f2821d](https://github.com/monicahq/monica/commit/3f2821d75a4d2451f397d98d4095547d520f660e)) +* fix importvcard job ([#5151](https://github.com/monicahq/monica/issues/5151)) ([cf8041c](https://github.com/monicahq/monica/commit/cf8041cfe7544889c7d4c28e5b1e27cd1671bbd0)) +* fix name order selection and result ([#5255](https://github.com/monicahq/monica/issues/5255)) ([d3217c0](https://github.com/monicahq/monica/commit/d3217c067e642614c3f2176e963f41d77a1aed29)) +* fix stripe pages stability ([#5161](https://github.com/monicahq/monica/issues/5161)) ([53977cc](https://github.com/monicahq/monica/commit/53977cc9eb24e9a14d0ec8d8419c54595e680857)) +* fix tags list filtering ([#5123](https://github.com/monicahq/monica/issues/5123)) ([99bd8e1](https://github.com/monicahq/monica/commit/99bd8e17f8ac78af45937673a38a15c72f1e0278)) +* fix unarchive on limited account ([#5256](https://github.com/monicahq/monica/issues/5256)) ([8357d0f](https://github.com/monicahq/monica/commit/8357d0f57907d0fe568db1b51defece6d65b4ad0)) +* fix vcard import to generate avatars ([#5193](https://github.com/monicahq/monica/issues/5193)) ([6323a5d](https://github.com/monicahq/monica/commit/6323a5d4cd4207eba9d0d46a3f2f6a425915afbe)) +* left trim url if there is a trailing slash ([#5149](https://github.com/monicahq/monica/issues/5149)) ([56572bb](https://github.com/monicahq/monica/commit/56572bbd576bdc45eaa8ebf5b2f27c7aab6c8a9d)) +* package.json & yarn.lock to reduce vulnerabilities ([#5269](https://github.com/monicahq/monica/issues/5269)) ([9c111c3](https://github.com/monicahq/monica/commit/9c111c3427e9ca2a8e9e84cf336bdd97cb67bee2)) + +## [3.0.1](https://github.com/monicahq/monica/compare/v3.0.0...v3.0.1) (2021-05-02) + + +### Bug Fixes + +* fix deploy on fortrabbit with version number ([#5139](https://github.com/monicahq/monica/issues/5139)) ([c5394af](https://github.com/monicahq/monica/commit/c5394af9bc30207a9158488c7617f3bb265a3c72)) +* fix import job without subscription bypass ([#5147](https://github.com/monicahq/monica/issues/5147)) ([fbac248](https://github.com/monicahq/monica/commit/fbac24891a9ace9f9c88fd4d23b8612243af283a)) + +# [3.0.0](https://github.com/monicahq/monica/compare/v2.22.1...v3.0.0) (2021-04-30) + + +### Features + +* remove assets from repository (see [#4759](https://github.com/monicahq/monica/issues/4759)) ([#5133](https://github.com/monicahq/monica/issues/5133)) ([02ba369](https://github.com/monicahq/monica/commit/02ba3694929154ecdafbe95fa34ad6920680b6b7)) + + +### BREAKING CHANGES + +* The assets are no longer embedded in source code: javascript, css, font files. Run `yarn install` then `yarn run production` to recreate them from sources, or download a [release file](https://github.com/monicahq/monica/releases) that contains compiled files. +* For Heroku users: You'll have to manually go to `Settings` > `Buildpacks` and add buildpack: `nodejs`. See [this doc](https://github.com/monicahq/monica/blob/master/docs/installation/providers/heroku.md#update-from-2x-to-3x). +* See more information about how to install a Monica instance [here](https://github.com/monicahq/monica/tree/master/docs/installation). + +## [2.22.1](https://github.com/monicahq/monica/compare/v2.22.0...v2.22.1) (2021-04-30) + + +### Code Refactoring + +* remove assets from repository ([#4759](https://github.com/monicahq/monica/issues/4759)) ([570dde1](https://github.com/monicahq/monica/commit/570dde1a13096c8e15fa436eae99ddc572486922)) + + +### BREAKING CHANGES + +* The assets are no longer embedded in source code: javascript, css, font files. Run `yarn install` then `yarn run production` to recreate them from sources, or download a [release file](https://github.com/monicahq/monica/releases) that contains compiled files. +* For Heroku users: You'll have to manually go to `Settings` > `Buildpacks` and add buildpack: `nodejs`. See [this doc](https://github.com/monicahq/monica/blob/master/docs/installation/providers/heroku.md#update-from-2x-to-3x). +* See more information about how to install a Monica instance [here](https://github.com/monicahq/monica/tree/master/docs/installation). + +# [2.22.0](https://github.com/monicahq/monica/compare/v2.21.0...v2.22.0) (2021-04-30) + + +### Bug Fixes + +* fix bypass account limitation to create more contacts ([#5125](https://github.com/monicahq/monica/issues/5125)) ([3d66188](https://github.com/monicahq/monica/commit/3d66188350f107094309d3dcd62b8202aad25004)) +* fix bypass invitation ([#5127](https://github.com/monicahq/monica/issues/5127)) ([d889475](https://github.com/monicahq/monica/commit/d88947523094d7159a937033f3a4ab05380fb4a9)) +* fix stripe page ([#5113](https://github.com/monicahq/monica/issues/5113)) ([caa5bef](https://github.com/monicahq/monica/commit/caa5bef93bed33d269ef1260c805e0ffaffd08fa)) + + +### Features + +* create a new stacked log channel ([#5122](https://github.com/monicahq/monica/issues/5122)) ([71c3789](https://github.com/monicahq/monica/commit/71c3789b6013dcea5a3e856b5f6e52c32769b1f4)) +* display gifts date ([#5081](https://github.com/monicahq/monica/issues/5081)) ([a478fd8](https://github.com/monicahq/monica/commit/a478fd8f4394ee650a89bbf62610e973fb98c03a)) + +# [2.21.0](https://github.com/monicahq/monica/compare/v2.20.0...v2.21.0) (2021-04-25) + + +### Features + +* add ability to attach dates to gifts ([#4909](https://github.com/monicahq/monica/issues/4909)) ([da17b8d](https://github.com/monicahq/monica/commit/da17b8d1b48d894443fad165868086c4d04eb94d)) +* add date of creation in journal ([#4949](https://github.com/monicahq/monica/issues/4949)) ([be85cad](https://github.com/monicahq/monica/commit/be85cadd2abf38aba2353f629fac1733dd922e9b)) + + +### Bug Fixes + +* fix udpate maintenance mode message ([#4983](https://github.com/monicahq/monica/issues/4983)) ([225e68e](https://github.com/monicahq/monica/commit/225e68e038349afe1c86631dca04297b8ba72955)) +* sort and group relationships by relationship type ([#4985](https://github.com/monicahq/monica/issues/4985)) ([105b74f](https://github.com/monicahq/monica/commit/105b74f94e7f6b08da3883020670c9b8e3c72df0)) + +# [2.20.0](https://github.com/monicahq/monica/compare/v2.19.1...v2.20.0) (2021-03-18) + + +### Bug Fixes + +* catch fatal error during install hooks ([#4642](https://github.com/monicahq/monica/issues/4642)) ([1c63ea0](https://github.com/monicahq/monica/commit/1c63ea0b3088e7701c4326bce365a8f09491d6f3)) +* fix add gender type ([#4548](https://github.com/monicahq/monica/issues/4548)) ([c0561ce](https://github.com/monicahq/monica/commit/c0561cef7b6c296d41ee42405dec46dae8bb34af)) +* fix broken stay in touch frequency input ([#4969](https://github.com/monicahq/monica/issues/4969)) ([500ecc8](https://github.com/monicahq/monica/commit/500ecc830282c79170121c2666a71025aaa65721)) +* fix checkbox UI issue in invite user page ([#4546](https://github.com/monicahq/monica/issues/4546)) ([827154e](https://github.com/monicahq/monica/commit/827154e9bebb3a545c71eb73ef204feeaf59ed07)) +* fix contact list description display & UI column names ([#4891](https://github.com/monicahq/monica/issues/4891)) ([aa090f8](https://github.com/monicahq/monica/commit/aa090f89846cc5323005636ca77b294953f2c5de)) +* fix date missing on journal api ([#4905](https://github.com/monicahq/monica/issues/4905)) ([8de23ba](https://github.com/monicahq/monica/commit/8de23ba01b5b796cc7c9fbe4d22b4f8b8d2d8cd9)) +* fix date you met update UX ([#4511](https://github.com/monicahq/monica/issues/4511)) ([288e3d0](https://github.com/monicahq/monica/commit/288e3d0af5bcd331ac83836f25d5b347194f11c9)) +* fix docker build ([#4733](https://github.com/monicahq/monica/issues/4733)) ([4fa4561](https://github.com/monicahq/monica/commit/4fa4561c2c4b34d3f39c5266d1581fa2cd9ee75a)) +* fix oauth login bad credentials ([#4688](https://github.com/monicahq/monica/issues/4688)) ([28d4cc9](https://github.com/monicahq/monica/commit/28d4cc94bb339e4345ae1a0d9c1a4f45716707ff)) +* fix passport setup migration ([#4606](https://github.com/monicahq/monica/issues/4606)) ([e17b89b](https://github.com/monicahq/monica/commit/e17b89b656ea6d002f8347af75aebb631c4d0e4f)) +* fix subscriptions list display ([#4967](https://github.com/monicahq/monica/issues/4967)) ([ca21705](https://github.com/monicahq/monica/commit/ca217056bb375d38b4673f6fcfb4db758b775298)) +* fix the adorable url migration ([#4963](https://github.com/monicahq/monica/issues/4963)) ([ed2b3b7](https://github.com/monicahq/monica/commit/ed2b3b7667b8ea2c65bd151b4fe0c75364418eb8)) +* fix the adorable url migration (again) ([#4964](https://github.com/monicahq/monica/issues/4964)) ([5894065](https://github.com/monicahq/monica/commit/5894065059e5877ba4486c64bc746378ac655fdb)) +* update activity with emotions ([#4459](https://github.com/monicahq/monica/issues/4459)) ([d4adb4f](https://github.com/monicahq/monica/commit/d4adb4f206c7637ae5bd21f39009568e0ca639c3)) +* update adorable api to api.hello-avatar.com ([#4778](https://github.com/monicahq/monica/issues/4778)) ([527131e](https://github.com/monicahq/monica/commit/527131e4e72f7a0deea5d4a9d8025a6d1a9d15fa)) + + +### Features + +* add a confirmation to delete a journal entry [#4308](https://github.com/monicahq/monica/issues/4308) ([#4514](https://github.com/monicahq/monica/issues/4514)) ([18fadb7](https://github.com/monicahq/monica/commit/18fadb77ce7fb4b46fd71ae8205127b9e8c9581d)) +* add Android icon for use when bookmarking ([#4798](https://github.com/monicahq/monica/issues/4798)) ([dcee3a9](https://github.com/monicahq/monica/commit/dcee3a943212476f2e96e5c54165daae244c47fc)) +* add Apple icons for use when bookmarking. ([#4743](https://github.com/monicahq/monica/issues/4743)) ([a28adcd](https://github.com/monicahq/monica/commit/a28adcdd7d13330271d198c470b6985eee39df11)) +* add artisan command to create new account ([#4745](https://github.com/monicahq/monica/issues/4745)) ([b9ee793](https://github.com/monicahq/monica/commit/b9ee793669562c8bf44bd57322e9f5126b2af998)) +* add notion of addressbooks ([#3749](https://github.com/monicahq/monica/issues/3749)) ([a18962e](https://github.com/monicahq/monica/commit/a18962ecbf09cb222ac943f8be19362985a7235a)) +* add Swedish language ([#4652](https://github.com/monicahq/monica/issues/4652)) ([e1edcad](https://github.com/monicahq/monica/commit/e1edcad04b5cdee0c61883db0d64cf2ca9e9369c)) +* allow customization of life event types ([#4243](https://github.com/monicahq/monica/issues/4243)) ([657d824](https://github.com/monicahq/monica/commit/657d824273e8eedc01ed099576571d47d0e26017)) +* default gender to unknown ([#4753](https://github.com/monicahq/monica/issues/4753)) ([ebf7c08](https://github.com/monicahq/monica/commit/ebf7c085dd786b174055be389ba5fef35fae861a)) +* set and clear personal description now appears in change log ([#4893](https://github.com/monicahq/monica/issues/4893)) ([686a0a1](https://github.com/monicahq/monica/commit/686a0a1f0b2dbbee91fef41eca318a3b9fbd48ff)) + + +## v2.19.1 - 2020-09-12 + +### Fixes: + +* Fix journal entry XSS vulnerability + + +## v2.19.0 - 2020-08-27 + +### Enhancements: + +* Update tag management on the contact profile +* Add next and previous arrows when viewing photos +* Add dependency to php imagick module +* Renamed MOBILE_CLIENT_ID and MOBILE_CLIENT_SECRET variables to PASSPORT_PERSONAL_ACCESS_CLIENT_ID and PASSPORT_PERSONAL_ACCESS_CLIENT_SECRET + +### Fixes: + +* Fix amount display on subscription account settings +* Fix exception when registering in certain cases +* Fix vue-select usage + + +## v2.18.0 - 2020-05-23 + +### New features: + +* Display age of death to relationship sidebar if the person is dead +* Crop contact photos on upload +* Add new name orders \ (\ \) & \ (\ \) +* Add console command to test email delivery +* Add Traditional Chinese language +* Add Japanese language +* Change title of birthday reminder for deceased people + +### Enhancements: + +* Change docker image sync +* Stores amount as integer-ish values, and fix debts and gifts amount forms +* Use current text from search bar to create a new person +* Always allow to add a new person from search bar +* Use queue to send email verification +* Improve autocomplete fields on signup and login forms +* Add cache for S3 storage, and use new standard variables +* Remove authentication with login+password for carddav +* Add new command monica:passport to generate encryption if needed +* Improve nginx config docker examples +* Remove u2f support (replaced with WebAuthn) +* Serialize photo content in VCard photo value + +### Fixes: + +* Fix life event categories and types are not translated when adding new life event +* Fix subdirectory config url +* Fix google2fa column size +* Fix errors display for api +* Fix currency in double +* Fix authentication with token on basic auth +* Fix editing multiple notes at the same time only edits one note +* Fix countries in fake contact seeder +* Fix docker rsync exclude rules +* Fix docker cron (legacy) on apache variant +* Fix login route already set by Laravel now +* Fix setMe contact controller +* Fix carddav sync-collection reporting wrong syncToken + + +## v2.17.0 - 2020-03-22 + +### New features: + +* Add a weekly job to update gravatars +* Add ability to set 'me' contact +* Add middle name field to new contact and edit contact +* Add backend and api for contact field labels +* Add audit log when setting a contact's description +* Add support for audit logs on a contact page +* Add support for audit logs in the Settings page +* Add vue data validations +* Add ability to edit activities +* Associate a photo to a gift +* New API method: get all the contacts for a given tag + +### Enhancements: + +* Use Carbon v2 library as translator for dates +* Contacts displayed in the activity list are now clickable again +* Gift are now added and updated inline +* Add a link in the downgrade process to archive all contacts in the account + +### Fixes: + +* Fix dates being off by one day +* Fix wrong untagged contacts counter when viewing untagged contacts +* Fix markdown doesn't work on journal activity entries +* Fix markdown doesn't work on Activity entries +* Fix summary of activities showing the same date for every entry +* Fix vcard categories import/export as tags +* Fix resend email verification feature not sending email +* Fix edit conversation date not being editable +* Fix display of the toggle buttons in the Settings page +* Fix how you met date not being deleted upon save +* Fix description not being saved when creating/editing activity +* Markdown is now properly applied for a phone call description +* Fix contacts list UX with 2 tabs opened +* Fix activity mock data seeder +* Fix ordering of contact tags to be alphabetical + + +## v2.16.0 - 2019-12-31 + +### New features: + +* Save contact tags in vCard 'CATEGORIES' field + +### Enhancements: + +* Activities are now added inline +* Improve modals bottom buttons display +* Add foreign keys to all tables +* Add English (UK) locale +* Add API methods to destroy and store documents +* Add API methods to manage photos and avatars +* Add emotions and participants to activities +* Enable API web navigation +* Enhance UI of API's Settings to add comprehension and documentation +* Improve trim string middleware to not trim password text +* Upgrade to Laravel 6.x +* Enhance user invitation mail +* Add job information next to the contact name on profile page +* Use supervisor in docker images +* Use JawsDB by default on heroku instances +* Add pluralization forms for non-english-like-plural languages, for vue.js translations +* Upload master docker image to GitHub packages + +### Fixes: + +* Fix contact list cells link +* Fix birthdate selection UX +* Fix OAuth login process with WebAuthn activated +* Fix journal entry edit +* Fix register in case country is not detected from ip address +* Fix Photo->contact relation +* Fix subscription page +* Fix relationship create and destroy with partial contact +* Fix 2fa route on webauthn page +* Fix tooltip on favorite icon +* Fix icons disappeared on contact information +* Fix CSV uploads with weird photo files +* Ensure disable_signup is checked on form register validation +* Fix password resetting page +* Fix email verification sending on test environments +* Fix contact export +* Fix currencies seeder by accounting for defaults +* Fix search when prefix table is used +* Fix storage page not being displayed if a contact does not exist anymore +* Fix API requests for Reminders failing with internal server error + +## v2.15.2 - 2019-09-26 + +### Enhancements: + +* Revert depends on php7.2+ + + +## v2.15.1 - 2019-09-24 + +### Fixes: + +* Fix people header file +* Fix query and scope searches with table prefix +* Remove monica:clean command confirmation + + +## v2.15.0 - 2019-09-22 + +### New features: + +* Paginate the Contacts page and improve database performance +* Add ability to edit a Journal entry +* Add vcard photo/avatar import +* Add ability to change the avatar of your contacts +* Add the ability to set a 'me' contact (only API for now) +* Add stepparent/stepchild relationship + +### Enhancements: + +* Docker image: create passport keys for OAuth access +* Reduce a lot of queries +* Update to laravel cashier 10.0, and get ready with SCA/PSD2 +* Add stripe webhook +* Depends on php7.3+ +* Use pretty-radio and optimize vue.js components +* Hide stay-in-touch for deceased contacts + +### Fixes: + +* Fix query and scope search +* Reschedule missed stay-in-touch +* Fix tasks 'mark as done' UX +* Fix tattoo or piercing activity locale title +* Fix getting infos about country without providing ip +* Fix migration and contact delete in case a DB prefix is used +* Fix partial/real contact edit on relationship +* Fix same contact selection in multi-search +* Fix conversation creation +* Fix phone call update +* Fix conversation list show +* Fix subscription cancel +* Fix last consulted contact list +* Fix exception in case a user register twice +* Fix vcard export with empty gender +* Fix touch contact's updated_at on stay in touch trigger job +* Fix relationship list view +* Fix relationship id with no gender +* Fix some UX errors +* Fix stripe payment UI +* Fix datepicker for locale usage + + +## v2.14.0 - 2019-05-16 + +### New features: + +* Add WebAuthn Multi-factor authentication +* Add multi factor auth on oauth + +### Enhancements: + +* Add Swiss CHF currency +* Add ability to enable DAV for some users +* Group relationships in create/edit forms +* Rewrite contact search fields +* Use string and array classes instead of helpers + +### Fixes: + +* Fix dav url on dav settings page +* Fix debt direction on debt edit +* Fix schedule run in case cron can't run on fix hours +* Fix contact create with birthdate age 0 +* Fix contact link create on job queue +* Fix /settings/dav route +* Fix display relationship without a ofContact property +* Fix register request validate +* Fix relationship create + + +## v2.13.0 - 2019-04-07 + +### Enhancements: + +* Add ability to update a relationship +* Add a sex type behind the gender +* Make gender optional on a contact profile +* Add a Collection::sortByCollator macro + +### Fixes: + +* Fix destroy relationship +* Fix event dispatch for login (google2fa, u2f) events handle +* Fix address input label mistake +* Fix dashboard crash when reminder is empty +* Fix import vCard with Cyrillic encoding +* Fix import/export vcard with birthday with year unknown +* Fix contact missing create form +* Fix money format for non two "2" minor unit currencies + + +## v2.12.1 - 2019-03-09 + +### Enhancements: + +* Add eloquent relationships touches + +### Fixes: + +* Fix reminders not being sent +* Fix setting deceased information with removing date and reminder +* Fix contact information update +* Fix adding people on activity create and update +* Fix setting a relationship without selecting any birthdate option +* Fix several typos in English language files +* Fix Journal view now includes Activities as intended +* Fix deleting a LifeEvent no longer deletes the associated Contact + + +## v2.12.0 - 2019-02-09 + +### New features: + +* Support CalDAV to export the collection of birthdays (breaking change: url of CardDAV is '/dav' now) +* Add a page in settings to display all DAV resources +* Add notion of instance administrator for a user +* Add ability to name u2f security keys and to delete register ones +* Add ability to add a comment when rating your day in the journal +* Add API methods to manage genders +* Breaking change: rewrite API methods to manage contacts + +### Enhancements: + +* Don't change timestamps on contact number_of_views update +* Redirect to the related real contact when trying to display a partial contact +* Use iterator reader for vcard imports +* Accept last name when using contact search field +* Register all app services as singleton +* Docker image: add sentry-cli and run sentry:release command if sentry is enabled +* Refactor reminders by removing Notifications table and creating two new tables: reminder outbox and reminder sent +* Shorten the value of the contact field if it does not fit into the contact field information +* Add foreign keys to activities table +* Add foreign keys to reminders, reminder rules, contacts and life events tables +* Add number of life events on the contact profile page +* Add base HTML tag and tweak all assets and urls to use relative paths +* Refactor activity types with services +* Refactor activity type categories with services + +### Fixes: + +* Fix addresses and contact fields imports on VCard import +* Remove users without an existing account in the accounts table +* Fix case when schedule date is null +* Add phpstan analyser, and fix a lot of issues +* Fix middleware priority order to always set locale after authenticate +* Accept lastname_firstname name order for VCard imports (FN field) +* Fix vue.js DateTime picker to type a date in other format than en-us one +* Fix DateTime parse when compact format is used +* Fix contact and relationship edit with reminder enabled +* Fix broken migration for the activities table +* Fix VCard import with partial N entry +* Fix using 'label' tag without 'for' attribute +* Fix model binding when it is a guest request (not logged in) +* Fix bug preventing to create life event without day and month +* Fix ability to delete a user with a u2f key activated +* Fix validation fails with Services +* Fix getting birthday reminders about related contacts +* Fix default temperature scale setting +* Fix API methods for Occupation object +* Fix activity date viewed as one day before the event happened +* Fix settags api call with an empty tag + + +## v2.11.2 - 2019-01-01 + +* Carddav: support sync-token (rfc6578) +* Fix premium feature flag appearing on self-hosted version +* Fix exception when user is logged out (again) +* Fix carddav group-member-set propfind call +* Fix contacts view in case birthdate returns null +* Fix conversation without message + + +## v2.11.1 - 2018-12-26 + +* Migrate LinkedIn url from the Contact object to a ContactFieldType object +* Activate eslint to check vue and javascript formatting +* Fix tasks store and update +* Fix error handling in vue components +* Fix exception when user is logged out +* Fix subscription plan display +* Fix dashboard calls display +* Fix tags getting error +* Fix contact getIncompleteName to work with UTF-8 last_name characters +* Fix associate null tags + + +## v2.11.0 - 2018-12-23 + +* Add ability to indicate temperature scale (Fahrenheit/Celsius) on the Settings page +* Add ability to see the current weather on the contact profile page +* Add ability to generate recovery codes in order to bypass 2FA/U2F +* Add ability to indicate latitude and longitude to addresses +* Add ability to upload photos +* Add ability to indicate how you felt when logging a call +* Add information about who initiated a phone call +* Add ability to edit a phone call +* Add ability to create tasks that are not linked to any contacts +* Remove limitation on the date field when creating an activity +* Fix Set Tag api method which deleted existing tags, which it shouldn't +* Fix editing relationship not working +* Fix Storage page not being displayed +* Fix VCard import without firstname +* Fix avatar display in searches +* Fix conversation add/update using contact add/update flash messages +* Fix incompatibility of people search queries with PostgreSQL +* Refactor how contacts are managed +* Add the notion of places + + +## v2.10.2 - 2018-11-14 + +* Fix composer install problems +* Fix editing conversations not working +* Fix deletion of relationships not working + + +## v2.10.1 - 2018-11-13 + +* Fix work information not being able to be edited +* Display contacts for each tag in the Tags view on the Settings page + + +## v2.10.0 - 2018-11-11 + +* Add ability to upload documents +* Add ability to archive a contact +* Add right-click support on contact list +* Add autocompletion on tags +* Add CardDAV support — disabled by default. To enable it, toggle the `CARDDAV_ENABLED` env variable. +* Add a command (export:all) to export all data from an instance in SQL +* New header on a profile page +* Standardize phonenumber format while importing vCard +* Set currency and timezone for new users +* Remove changelogs from the database and manage changelogs from a json file instead +* Highlight buttons when selected using keyboard +* Hide deceased people from dashboard's 'Last Consulted' section +* Improve API methods for tag management +* Fix settings' sidebar links and change security icon +* Fix CSV import +* Filter deceased people from people list by default +* Fix errors during PostgreSQL migration +* Better documentation for PostgreSQL users +* Fix some API methods +* API breaking change: Remove 'POST /contacts/:contact_id/pets' in favor of 'POST /pets/' with a 'contact_id' +* API breaking change: Remove 'PUT /contacts/:contact_id/pets/:id' in favor of 'PUT /pets/:id' with a 'contact_id' +* API breaking change: Every validator fails now send a HTTP 400 code (was 200) with the error 32 +* API breaking change: Every Invald Parameters errors now send a HTTP 400 (was 500 or 200) code with the error 41 +* Use Laravel email verification, and remove the old package used for that +* Prevent submitting an empty form when pressing enter +* Remove Antiflood package on oauth/login and use Laravel throttle + + +## v2.9.0 - 2018-10-14 + +* Allow to define a max file size for uploaded document in an ENV variable (default to 10240kb) +* Add description field for a contact +* Add ability to retrieve all conversations for one contact through the API +* Add all tasks not yet completed on the dashboard +* Fix gravatar not displayed on dashboard view + + +## v2.8.1 - 2018-10-08 + +* Add ability to set a reminder for a life event +* Stop reporting OAuth exceptions +* Replace karakus/laravel-cloudflare with monicahq/laravel-cloudflare to fix dependencies issues +* Fix use of 'json' mysql column type + + +## v2.8.0 - 2018-09-28 + +* Add ability to track life events +* Add ability to define the default email address used for support +* Add sentry:release command +* Add Envoy file template +* Add passport config file +* Add new variable APP_DISPLAY_NAME +* Rename env variable 2FA_ENABLED to MFA_ENABLED (2FA_ENABLED is still functional for compatibility reasons) +* Improve search +* Fix reminders displaying wrong date +* Fix select boxes not working properly anymore +* Fix confirm email sent when signup_double_optin is false +* Fix now() without timezone functions +* Remove notion of events +* Support papertrail logging + + +## v2.7.1 - 2018-09-05 + +* Fix duplication of modules in the Settings page + + +## v2.7.0 - 2018-09-04 + +* Add ability to log conversations made on social networks or SMS +* Add language selector on register page +* Support Arabic language +* Improve automatic route binding +* Split app css in two files for better support of ltr/rtl text direction +* Add helper function htmldir() +* Fix gifts not showing when value was not set +* Fix phpunit not parsing all test files +* Fix login remember with 2fa and u2f enabled +* Fix gender update +* Fix how comparing version is done +* Fix search with wrong search field +* Fix gift recipient relation +* Fix subscription cancel on account deletion +* Fix email maximum size on settings +* Fix reminder link in email sent + + +## v2.6.0 - 2018-08-17 + +* Add ability to set a contact as favorite +* Add ability to search for a contact in the dropdown when creating a relationship +* Add activity reports page, which shows useful statistics about activities with a specific contact +* Fix reminders not being sent for single-digit hours +* Fix accounts with an empty reminder time +* Fix account id get for acceptPolicy +* Use our own docker image (central perk) to run tests +* Add end-2-end testing with Cypress +* Render timezone listbox dynamically +* Use a new formatter to display money (debts), with right locale handle +* Get first existing gravatar if contact has multiple emails +* Display the date and time of the next reminder sent in settings page + + +## v2.5.0 - 2018-08-08 + +* Add ability to define custom activity types and activity type categories +* Add ability to search a contact by job title +* Fix invoice page not showing properly +* Fix translation not being displayed correctly on Subscription page +* Add the TrimStrings middleware to trim all inputs +* Call to monica:ping when updating instance +* Fix idHasher decode function +* Fix storage folder not being linked to public if migrations fail + + +## v2.4.2 - 2018-07-26 + +* Add functional tests for account deletion and account reset +* Fix activities not being displayed in the journal +* Fix food preferences not being able to be updated +* Add functional test for account exporting +* Fix fake content seeder for testing purposes + + +## v2.4.1 - 2018-07-25 + +* Add ability to discover Cloudflare trusted proxies automatically. This adds a new ENV variable. +* Fix avatar link in journal page +* Fix broken migration +* Fix Settings not displaying under some conditions + + +## v2.4.0 - 2018-07-23 + +* Fix account deletion, reset and export +* Fix export feature which exported 'changelog_user' table, which it shouldn't +* Change how dates are stored, from local timezone to UTC +* Remove the APP_TIMEZONE env variable +* Add U2F/yubikey support and refactor MultiFactor Authentication +* Add a script to update assets automatically +* Allow for plus sign search in contacts api (contact_fields_data) +* Fix sonar run for pull requests +* Improve date and datetime parsing + + +## v2.3.1 - 2018-06-21 + +* Fix journal entries not being displayed +* Add ability to click on entire row on the contact list +* Fix first name of a relation which could not be saved +* Fix last name not being reset when set empty + + +## v2.3.0 - 2018-06-13 + +* Add a new variable DB_USE_UTF8MB4. Please read instructions carefully for this one. +* Add support for nicknames +* Fix resetting account not working +* Fix CSV import that can break if dates have the wrong format +* Add default accounts email confirmation in setup:test +* Set the default tooltip delay to 0 so the tooltip does not stay displayed for 200ms by default +* Replace queries with hardcoded "monica" database name to use the current default connection database +* Set the default_avatar_color property before saving a contact model. +* Move docs folder back to the repository + + +## v2.2.1 - 2018-05-31 + +* Fix url of confirmation email resend +* Update translations +* Fix sonar run on release version + + +## v2.2.0 - 2018-05-30 + +* Add debts on the dashboard +* Add support for User and Currency objects in the API +* Add ability to force users to accept privacy and terms of use +* Fix journal entry with date different than today's date not working +* Fix Contact search dropdown showing non-contacts that link to nowhere +* Add ability to sort contact list by untagged contacts +* Allow multiple imported fields and replace existing contacts +* Add ex wife/husband relationship +* Fix duplication of tags when filtering contacts +* Add trusted proxies to run behind a ssl terminating loadbalancer +* Fix reminders for past events are visible on the dashboard +* Add email address verification on register, and email change +* Change table structure to support emojis in texts + + +## v2.1.1 - 2018-05-13 + +* Change file structure inside the People folder (backend change) +* Remove automatic birthday reminder creation when editing a contact +* Set fixed version for MySQL in docker-compose +* Build absolute path to stubs files in UploadVCardTest and UploadVCardsTest (backend) +* Refactor how countries are fetched +* Change address fetching in API +* Add ComposerScripts links +* Fix tests to prepare for foreign keys (backend) +* Fix deploy tagged version +* Fix vagrant box +* Fix notifications being sent even if reminder rule is set to off +* Fix API locale +* Fix update command (backend) + + +## v2.1.0 - 2018-05-03 + +* Refactor vCard import +* Add support for markdown on the Journal +* Add support for markdown for Notes +* Add many unit tests on the API +* Add ability to display contact fields for each contact in the contact list through the API +* Add ability to stay in touch with a contact by sending reminders at a given interval +* Add secure Oauth route for the API login +* Fix removal of tags + + +## v2.0.1 - 2018-04-17 + +* Add ability to set relationships through the API +* Fix ordering of activites in journal +* Fix how you meet section not being shown +* Add a changelog inside the application +* Fix monica:calculatestatistics command + + +## v2.0.0 - 2018-04-12 + +* Add ability to set a journal entry date +* Use UUID instead of actual ID to identify contacts +* Add ability to show/hide sections on the Contact sheet view +* Add many more relationship types to link contacts together +* Fix called_at field in the Call object returned by the API +* Add Linkedin URL in the Contact object returned by the API +* Improve localization: add plural forms, localize every needed messages +* Split app.js in 3 files, and load translations files for Vue in separate files +* Localize update tag message +* Fix some messages syntax and ponctuation +* Add a new monica:update command +* Fix gifts handle +* Remove old documentation from sources +* Fix Bug when editing gift + + +## v1.8.2 - 2018-03-20 + +* Add a Vagrantfile to run Monica on Vagrant +* Add support for Hebrew and Chinese Simplified +* Add bullet points to call lists when rendered from markdown +* Require debugbar on dev only +* Improve heroku integration +* Open register page after a clean installation +* API: Add ability to sort tasks by completed_at attribute +* API: Add sorting capabilities to most models +* Update Czech, Italian, Portuguese, Russian, German, French language files +* Fix docker image creating wrong storage directories +* Fix notification messages + + +## v1.8.1 - 2018-03-02 + +* Fix message in contact edit page +* Fix months list for non english languages in contact edit page +* Fix birthdate calendar for non english languages in contact edit page +* Fix Gravatar support +* Remove partial contacts from search results returned by the API +* Fix reset account deleting default account values +* Fix notifications not working with aysnchronous queue +* Support mysql unix socket + + +## v1.8.0 - 2018-02-26 + +* Add ability to search and sort in the API +* Add ability to define the hour the reminder should be sent +* Add notifications for reminders (30 and 7 days before an event happens) +* Add API calls to associate and remove tags to a contact +* Docker image: use cron to run schedule tasks +* Docker image: reduce size of image +* Docker image: create storage subdirectory in case they not exist +* Docker image: use rewrite rules in .htaccess from public directory instead of apache conf file +* Remove trailing slash from routes + + +## v1.7.2 - 2018-02-20 + +* Fix a bug where POST requests were not working with Apache +* Fix a bug preventing to delete a contact + + +## v1.7.1 - 2018-02-17 + +* Fix a bug that occured when running setup:production command + + +## v1.7.0 - 2018-02-16 + +* Add ability to create custom genders +* Add Annual plan for the .com site +* Fix avatar being invalid in the Contact API call +* DB_PREFIX is now blank in .env.example +* Fix empty message after updating a gift + + +## v1.6.2 - 2018-01-25 + +* Add support for pets in the API +* Add ability to export a contact to vCard +* Add ability to mark a gift idea as being offered +* Add translation for "preferences updated" message in the Settings page +* Add a lot of unit tests + + +## v1.6.1 - 2018-01-14 + +* Add missing journal link to the mobile main menu +* Remove list of events being loaded in the dashboard for no reason +* Remove duplicated code in Addresses.vue file +* Fix reminders not being sent in some cases +* Fix avatars not being displayed in an activity on the journal +* Fix filtering of contacts by tags not taking into account the selected tag from the profile page + + +## v1.6.0 - 2018-01-09 + +* Change the structure of the dashboard +* Add two factor authentication ability +* Add ability to edit a reminder +* Fix vCard import if custom field types are not present +* Fetch Countries in alphabetical order in "Add Address" form in People Profile page +* Display missing page when loading a contact that does not exist +* Add ability to filter contacts by more than one tag +* Change the structure of the dashboard +* Add two factor authentication ability +* Add pet support to API + + +## v1.5.0 - 2018-01-02 + +* Add Webmanifest to create bookmarks on phones +* Add pets management +* Activities made with contact now appears in the Journal +* Add ability to rate how a day went in the Journal +* Add validation when changing email address +* Add ability to change account's password in the settings +* Show a user's avatar when searching +* Fix timezone not being saved in the Settings tab + + +## v1.4.1 - 2017-12-13 + +* Add default user account on setup + + +## v1.4.0 - 2017-12-13 + +* Add ability to add a birthday (or any date) without knowing the year +* Add the artisan command (CLI) `php artisan setup:test` to setup the development environment +* Remove the table `important_dates` which was not used +* Change how resetting an account is achieved +* Add progress bar when generating fake data to populate the dev environment + + +## v1.3.0 - 2017-12-04 + +* Notes can be set as favorites +* Favorite notes are shown on the dashboard +* Notes are now managed inline +* Add dynamic notifications when adding/updating/deleting data from Vue files +* Add ability to change account's owner first and last names + + +## v1.2.0 - 2017-11-29 + +* Add a much better way to manage tasks of a contact +* Tasks can now be mark as completed and can now be edited +* Add more usage statistics to reflect latest changes in the DB + + +## v1.1.0 - 2017-11-26 + +* Add the ability to add multiple contact fields and addresses per contact +* Add a new Personalization tab under Settings + + +## v1.0.0 - 2017-11-09 + +* Add the ability to mark a contact as deceased +* Add a button to `Save and add another contact` straight from the Add contact screen +* Add the ability to indicate how you've met someone +* Replace former front-end build system by mix (which is the new default with Laravel 5.5) +* Add the first part of the API +* Fix the access to upgrade account view +* Add security.txt file +* Upgrade codebase to Laravel 5.5 + + +## v0.7.1 - 2017-10-21 + +* Fix an error in the JS that broke the application + + +## v0.7.0 - 2017-10-21 + +* Add ability to assign a single activity to multiple people +* Improve german translations +* Fix reminders not being sent in case of wrong timezones +* Fix the access to upgrade account view +* Replace the custom RandomHelper by str_random +* Multiple small fixes + + +## v0.6.5 - 2017-08-28 + +* Add a new welcome screen for new users +* Fix typo when displaying message of no existing contact to link when adding a child +* Monicahq.com only: add limitations to free accounts + + +## v0.6.4 - 2017-08-23 + +* Add restriction of 50 characters for a first name, and 100 characters for a last name +* Add support for storing uploaded files on s3 +* Sort contacts by first name, last name when linking significant others and kids +* Remove automatic uppercase of the first name +* Remove beginning / ending spaces in names when adding / saving a contact +* Fix birthday reminder creation bug on vCard import +* Fix search bar being hard to use + + +## v0.6.3 - 2017-08-16 + +* Fix kids not being able to be removed +* Fix some CSRF potential vulnerabilities + + +## v0.6.2 - 2017-08-16 + +* Add support for Markdown for the notes and call logs + + +## v0.6.1 - 2017-08-15 + +* Fix delete account bug +* Fix kid deletion bug +* Fix gift creation + + +## v0.6.0 - 2017-08-14 + +* Add ability to set significant other and kids as contact. +* Add Italian translation +* Add debt total below a contacts debt +* Add world currencies +* Add German translation + + +## v0.5.0 - 2017-07-24 + +* Add version checking. +* Add ability to search various fields in contacts through the top-nav search. +* Fix gift view not being shown. + + +## v0.4.2 - 2017-07-18 + +### New features: +* Add Indian rupee currency. +* Add Danish krone currency. +* Add Czech translation. + +### Improvements: +* Fix https issue on password reset. + + +## v0.4.1 - 2017-07-13 + +* Fix reminders not being sent introduced by previous version. + + +## v0.4.0 - 2017-07-13 + +### New features: +* Add ability to keep track of phone calls. + +### Improvements: +* Fix Google Contact instructions link on the Import screen. +* Input field are now automatically selected when a radio button is checked. +* Many small bug fixes. + + +## v0.3.0 - 2017-07-04 + +### New features: +* Add support for organizing people into tags (requires `bower update` for dev environment). +* Add ability to filter contacts per tags on the contact list. + +### Improvements: +* Fix import translation key on the import reports. +* Settings' sidebar now has better icons. + + +## v0.2.1 - 2017-07-02 + +### Improvements: +* Update the design of the latest actions on the dashboard. +* Change order of first and last names fields on contact add/edit, if the name order is defined as "last name, first name". +* Speed up the display of the contact lists when there is a lot of contacts in the account. +* Remove the search on the list of contacts, which was broken for a while, until a proper solution is found. +* Bug fixes. + + +## v0.2.0 - 2017-06-29 + +### New features: +* Add import from vCard (or .vcf) in the Settings panel. +* Add ability to reset account. Resetting an account will remove everything - but won't close the account like deletion would. + +### Improvements: +* Journal entries now respect new lines. +* Fix name not appearing in the latest actions tab on the dashboard. + + +## v0.1.0 - 2017-06-26 + +* First official release. We'll now follow this structure. If you self host, we highly recommend that you check the latest tag instead of pulling from master. + + +## 2017-06-24 + +### Improvements: +* On the people's tab, filters are now placed above the table. + + +## 2017-06-22 + +### New features: +* Add ability to define name order (Firstname Lastname or Lastname Firstname) in the Settings panel. + +### Improvements: +* Fix the order of the address fields. +* Env variables are now read from config files rather than directly from the .env file. +* Some US typos fix. + + +## 2017-06-20 + +### New features: +* Add support for mutiple users in one account. +* Add subscriptions on .com. This has no effect on self hosted versions. + + +## 2017-06-16 + +### Improvements: +* Add automatic reminders when setting a birthdate When adding a birthdate (contact, kid, significant other). When updating or deleting the person, the reminder will be changed accordingly. + + +## 2017-06-15 + +### New features: +* Add reminder automatically when you set the birthdate of a contact. + +### Improvements: +* Add timezone for Switzerland. +* Major refactoring of how contacts are managed in the codebase. + + +## 2017-06-14 + +### New features: +* Timezone can now be defined in a new ENV variable so every new user of the instance will have this timezone. Set to America/New_York by default. +* Add ability to edit a note. +* Add ability to edit a debt. +* Add support for South African ZAR currency. + +### Improvements: +* Fix Deploy to Heroku button. +* Fix Bern timezone by actually removing it. The Carbon library does not support this timezone. + + +## 2017-06-13 + +### New features: +* You can now add job information and company name for your contacts. + +### Improvements: +* Gifts table now display comments if defined, as well as who the gift is for. + + +## 2017-06-12 + +### New features: +* Add instructions to setup Monica from scratch on Debian Stretch. +* Add Export to SQL feature, under Settings > Export data. +* Add Deploy to Heroku button. Only caveat: you can't upload photos to contacts (Heroku has ephemeral storage). + + +## 2017-06-11 + +### New features: +* Add command line vCard importer + +### Improvements: +* Email address of a contact is now a mailto:// field. +* Phone number of a contact is now a tel:// field. +* Fix debt description on the dashboard +* Fix typos +* Fix Bootstrap tabs on the dashboard + + +## 2017-06-10 + +### New features: +* Add support for other currencies (CAD $, EUR €, GBP £, RUB ₽) for the gifts and debts section. This is set in the User setting. Default is USD $. +* Add ability to define main social network accounts to a contact (Facebook, Twitter, LinkedIn) + +### Improvements: +* Fix counter showing number of gifts on the dashboard +* Docker image now runs the cron to send emails +* Fix Russian translations +* Fix the wrong route after password change + + +## 2017-06-09 + +### New features: +* Add Docker support +* Add Russian language +* Add Portuguese (Brazil) language + +### Improvements: +* Fix emails being sent too often +* Breaking change: Email name and address of the user who sends reminders are now ENV variables (MAIL_FROM_ADDRESS and MAIL_FROM_NAME). + + +## 2017-06-08 + +### New features: +* Add Gravatar automatically when adding an email address to a contact. If no gravatar found, defaults to the initials. + +### Improvements: +* Dramatically reduce the number of queries necessary to load the list of contacts on the People's tab. +* Phone number are now treated like a string and not integers on the front-end side. +* Breaking change: Add a new env variable to define which email address should be used when sending notifications about new user signups. You need to add this new env variable (APP_EMAIL_NEW_USERS_NOTIFICATION) to your `.env` file. +* Fix typos and small bugs + + +## 2017-06-07 + +* Add ability to delete a contact +* Add a changelog diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..6761bca --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,51 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +We want everyone to be able to participate to our project no matter what they are or are from, as long as you don't bring drama or something irrelevant to the project. We are here to discuss Monica, its direction and the code around Monica - that's it. We will never judge someone, no matter the differences. On the contrary, we welcome differences. We will only judge the code that is submitted, and it will never be personal. + +To summarize, don't be an ass - we are here to create something good, and something that is useful for the world. Don't bring your personal story and everything will be alright. + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* Discussion about politics, culture, gender, ethnicity,... +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at regis AT monicahq DOT com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..e2eba90 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,90 @@ +## Contributing + +First off, thank you for considering contributing to Monica. We need people like +you to make Monica the best tool it can be. + +Before you do anything else, please read the [README.md](README.md) of this project first. +This is where we highlight the vision and the strategy. Please make sure you +accept this vision before contributing to this project. + +If you want to contribute to the translation / localization of Monica, please +head over to [Crowdin](https://crowdin.com/project/monicahq) where we manage our +localization files. + +### 1. Where do I go from here? + +If you've noticed a bug or have a question, [make an issue](https://github.com/monicahq/monica/issues/new), +we'll try to answer it as fast as possible. + +### 2. Fork & create a branch + +If this is something you think you can fix, then +[fork Monica](https://help.github.com/articles/fork-a-repo) +and create a branch with a descriptive name. + +A good branch name would be (where issue #325 is the ticket you're working on): + +```sh +git checkout -b 325-add-japanese-translations +``` + +### 3. Get the test suite running + +Make sure you follow the [instructions](https://github.com/monicahq/monica/blob/main/docs/contribute/readme.md#testing-environment) +on how to setup the test suite. + +### 4. Did you find a bug? + +* **Ensure the bug was not already reported** by searching on GitHub under +[Issues](https://github.com/monicahq/monica/issues). + +* If you're unable to find an open issue addressing the problem, +[open a new one](https://github.com/monicahq/monica/issues/new). +Be sure to include a **title and clear description**, as much relevant +information as possible, and a **code sample** or an **executable test case** +demonstrating the expected behavior that is not occurring. + +### 5. Implement your fix or feature + +* At this point, you're ready to make your changes! Feel free to ask for help; +everyone is a beginner at first :smile_cat: +* Write a good commit message. To write good commit messages, please follow +[those recommendations](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). +There are important to maintain an healthy commit logs. +* We follow [conventionalcommits](https://www.conventionalcommits.org/en/v1.0.0/) +guideline in our Pull Request title. Please follow it: use the `feat` type +for new features pull request, and `fix` type to fix a bug. +* If there are multiple commits in your pull request, these commits will be +squashed before merging. Please make sure, if that's the case, that your pull +request has a nice description explaining what it does. +* It's okay to have work-in-progress pull requests. Create a `draft` pull request +if that's the case, otherwise your pull request will be +considered in a state of being able to be merged as is. +* If you wish to appear as a contributor, update the CONTRIBUTORS file and +add your name to it. Include this change in your pull request. + +### 6. Wait for the code to be reviewed + +It can take several days before we can review the code you've submitted. We +all have a lot of work to do and while we truly appreciate pull requests that +are submitted, we can't review them instantly. We'll do our best to review +them as fast as possible, but there are only 24 hours in a day and we can't +sometimes be as fast as we wish we were. Moreover, there are little chances that +the PR will be reviewed over the weekend, a time dedicated to spend time with +friends and families (those you manage with Monica anyway :-)). + +Also, keep in mind that this project is still a side project. Maintainers of +this project are not paid to work on it. Everything they do, is done during +their time off of their "real" job, that means at night, on the weekend and +during holidays. + +### 7. What can I contribute to? + +Even the simplest change is appreciated. It can be a typo error, translating the +application in a new language, fix a bug. No change is too small. + +* If your contribution involves a change in the UI (even if it's very small), +please ping @djaiss in an issue *before* you start working on it, explaining +what you want to achieve, why and how. We want to maintain a high level of +visual quality in the software and we will dismiss all pull requests that change +the front end that have not been discussed before-hand. diff --git a/CONTRIBUTORS b/CONTRIBUTORS new file mode 100644 index 0000000..3c6dfa9 --- /dev/null +++ b/CONTRIBUTORS @@ -0,0 +1,63 @@ +This is the list of all the people that make this great software. +Add yourself at the bottom of the list if you do contribute and wish +to appear as a contributor. At the very minimum, the GitHub username +is required if you do wish to appear. Other fields are optional. + +Format: Firstname Lastname @github-handle + +CONTRIBUTORS +------------ +Maazarin @djaiss +Alexis Saettler @asbiin +Kirk Strauser @kstrauser +Taryn Hill @Phrohdoh +Andreas Zweili @Nebucatnetzer +Kadir Yamamoto @yamakadi +Rocco Palladino @rpalladino +Sebastian Gumprich @rndmh3ro +Brendan Butts @sevenecks +Christian Fratta @hherebus +Jaroslav Lichtblau @svetlemodry +Lee Fenlan @themodem +Aaron Johnson @aejnsn +David Egan @degan6 +Scott Williams @scott-joe +Craig Davison @davisonio +Michael Heap @mheap +Matthew Du Pont @mattdp +Kovah @kovah +Steven Maguire @stevenmaguire +@erdmenchen +Tom Rochette @tomzx +Roland Szabo @rolisz +Theo Mathieu @Mokto +Daniel Pieper @danielpieper +Andrew Paul Smith +Brian Clemens +Christopher Zentgraf @TheZenti +Lorenzo L. Ancora @LorenzoAncora +Ben Dauphinee @bendauphinee +Benjamin Dowson @lattlay +Stuart Johnston @mechanarchy +Bryan Kam @lydgate +Tom Granot @TomGranot +Chris Forrence @chrisforrence +Mohammed Al-Sahaf @Mohammed90 +Ivan Kruchkoff @ivankruchkoff +Joaquim Monserrat @jeremies +Jack Kuo @JackKuo-tw +Russell Ault @RussellAult +Martijn van der Ven @Zegnat +Matthew Fitzgerald @mfitzgerald2 +Simon Van Accoleyen @SimonVanacco +Michael Bianco +Ben Fesili @benfes +Markus Dick @markusdick +Jacek Sawoszczuk @jsawo +Dung Nguyen @nhymxu +Krzysztof Rewak @krzysztofrewak +Geidson Benicio @geidsonc +Maximilian Arzberger @Schlauer-Hax +Gregor Bigalke @GregTCLTK +Julián Garcés Rodríguez +Alberto Cuevas Ocegueda diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 7b9edd1..0000000 --- a/Dockerfile +++ /dev/null @@ -1,32 +0,0 @@ -# Base Image: Lightweight Node.js Alpine -FROM node:18-alpine - -# Set working directory -WORKDIR /app - -# Copy package descriptors first to leverage Docker layer caching -COPY package*.json ./ - -# Install production dependencies only -RUN npm ci --omit=dev - -# Copy server code and public assets -COPY server.js ./ -COPY public/ ./public/ - -# Create persistent storage folder for SQLite database -RUN mkdir -p /app/data - -# Environment configuration variables -ENV PORT=8085 -ENV DATABASE_PATH=/app/data/crm.db -ENV NODE_ENV=production - -# Expose internal Express port -EXPOSE 8085 - -# Define persistent volume directory for crm.db -VOLUME ["/app/data"] - -# Run server -CMD ["npm", "start"] diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..7509eec --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,660 @@ +### GNU AFFERO GENERAL PUBLIC LICENSE + +Version 3, 19 November 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. + + +Everyone is permitted to copy and distribute verbatim copies of this +license document, but changing it is not allowed. + +### Preamble + +The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + +The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains +free software for all its users. + +When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + +Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + +A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + +The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + +An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing +under this license. + +The precise terms and conditions for copying, distribution and +modification follow. + +### TERMS AND CONDITIONS + +#### 0. Definitions. + +"This License" refers to version 3 of the GNU Affero General Public +License. + +"Copyright" also means copyright-like laws that apply to other kinds +of works, such as semiconductor masks. + +"The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + +To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of +an exact copy. The resulting work is called a "modified version" of +the earlier work or a work "based on" the earlier work. + +A "covered work" means either the unmodified Program or a work based +on the Program. + +To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + +To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user +through a computer network, with no transfer of a copy, is not +conveying. + +An interactive user interface displays "Appropriate Legal Notices" to +the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + +#### 1. Source Code. + +The "source code" for a work means the preferred form of the work for +making modifications to it. "Object code" means any non-source form of +a work. + +A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + +The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + +The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + +The Corresponding Source need not include anything that users can +regenerate automatically from other parts of the Corresponding Source. + +The Corresponding Source for a work in source code form is that same +work. + +#### 2. Basic Permissions. + +All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not convey, +without conditions so long as your license otherwise remains in force. +You may convey covered works to others for the sole purpose of having +them make modifications exclusively for you, or provide you with +facilities for running those works, provided that you comply with the +terms of this License in conveying all material for which you do not +control copyright. Those thus making or running the covered works for +you must do so exclusively on your behalf, under your direction and +control, on terms that prohibit them from making any copies of your +copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under the +conditions stated below. Sublicensing is not allowed; section 10 makes +it unnecessary. + +#### 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + +No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + +When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such +circumvention is effected by exercising rights under this License with +respect to the covered work, and you disclaim any intention to limit +operation or modification of the work as a means of enforcing, against +the work's users, your or third parties' legal rights to forbid +circumvention of technological measures. + +#### 4. Conveying Verbatim Copies. + +You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + +#### 5. Conveying Modified Source Versions. + +You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these +conditions: + +- a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. +- b) The work must carry prominent notices stating that it is + released under this License and any conditions added under + section 7. This requirement modifies the requirement in section 4 + to "keep intact all notices". +- c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. +- d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + +A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + +#### 6. Conveying Non-Source Forms. + +You may convey a covered work in object code form under the terms of +sections 4 and 5, provided that you also convey the machine-readable +Corresponding Source under the terms of this License, in one of these +ways: + +- a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. +- b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the Corresponding + Source from a network server at no charge. +- c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. +- d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. +- e) Convey the object code using peer-to-peer transmission, + provided you inform other peers where the object code and + Corresponding Source of the work are being offered to the general + public at no charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + +A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, +family, or household purposes, or (2) anything designed or sold for +incorporation into a dwelling. In determining whether a product is a +consumer product, doubtful cases shall be resolved in favor of +coverage. For a particular product received by a particular user, +"normally used" refers to a typical or common use of that class of +product, regardless of the status of the particular user or of the way +in which the particular user actually uses, or expects or is expected +to use, the product. A product is a consumer product regardless of +whether the product has substantial commercial, industrial or +non-consumer uses, unless such uses represent the only significant +mode of use of the product. + +"Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to +install and execute modified versions of a covered work in that User +Product from a modified version of its Corresponding Source. The +information must suffice to ensure that the continued functioning of +the modified object code is in no case prevented or interfered with +solely because modification has been made. + +If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + +The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or +updates for a work that has been modified or installed by the +recipient, or for the User Product in which it has been modified or +installed. Access to a network may be denied when the modification +itself materially and adversely affects the operation of the network +or violates the rules and protocols for communication across the +network. + +Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + +#### 7. Additional Terms. + +"Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders +of that material) supplement the terms of this License with terms: + +- a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or +- b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or +- c) Prohibiting misrepresentation of the origin of that material, + or requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or +- d) Limiting the use for publicity purposes of names of licensors + or authors of the material; or +- e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or +- f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions + of it) with contractual assumptions of liability to the recipient, + for any liability that these contractual assumptions directly + impose on those licensors and authors. + +All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; the +above requirements apply either way. + +#### 8. Termination. + +You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + +However, if you cease all violation of this License, then your license +from a particular copyright holder is reinstated (a) provisionally, +unless and until the copyright holder explicitly and finally +terminates your license, and (b) permanently, if the copyright holder +fails to notify you of the violation by some reasonable means prior to +60 days after the cessation. + +Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + +Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + +#### 9. Acceptance Not Required for Having Copies. + +You are not required to accept this License in order to receive or run +a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + +#### 10. Automatic Licensing of Downstream Recipients. + +Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + +An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + +#### 11. Patents. + +A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + +A contributor's "essential patent claims" are all patent claims owned +or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + +In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + +If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + +A patent license is "discriminatory" if it does not include within the +scope of its coverage, prohibits the exercise of, or is conditioned on +the non-exercise of one or more of the rights that are specifically +granted under this License. You may not convey a covered work if you +are a party to an arrangement with a third party that is in the +business of distributing software, under which you make payment to the +third party based on the extent of your activity of conveying the +work, and under which the third party grants, to any of the parties +who would receive the covered work from you, a discriminatory patent +license (a) in connection with copies of the covered work conveyed by +you (or copies made from those copies), or (b) primarily for and in +connection with specific products or compilations that contain the +covered work, unless you entered into that arrangement, or that patent +license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + +#### 12. No Surrender of Others' Freedom. + +If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under +this License and any other pertinent obligations, then as a +consequence you may not convey it at all. For example, if you agree to +terms that obligate you to collect a royalty for further conveying +from those to whom you convey the Program, the only way you could +satisfy both those terms and this License would be to refrain entirely +from conveying the Program. + +#### 13. Remote Network Interaction; Use with the GNU General Public License. + +Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your +version supports such interaction) an opportunity to receive the +Corresponding Source of your version by providing access to the +Corresponding Source from a network server at no charge, through some +standard or customary means of facilitating copying of software. This +Corresponding Source shall include the Corresponding Source for any +work covered by version 3 of the GNU General Public License that is +incorporated pursuant to the following paragraph. + +Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + +#### 14. Revised Versions of this License. + +The Free Software Foundation may publish revised and/or new versions +of the GNU Affero General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever +published by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future versions +of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + +Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + +#### 15. Disclaimer of Warranty. + +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT +WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND +PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE +DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR +CORRECTION. + +#### 16. Limitation of Liability. + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR +CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES +ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT +NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR +LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM +TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER +PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +#### 17. Interpretation of Sections 15 and 16. + +If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + +END OF TERMS AND CONDITIONS + +### How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these +terms. + +To do so, attach the following notices to the program. It is safest to +attach them to the start of each source file to most effectively state +the exclusion of warranty; and each file should have at least the +"copyright" line and a pointer to where the full notice is found. + + The software is called Monica and is a personal relationship management system. + Copyright (C) 2016-2022 Maazarin + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as + published by the Free Software Foundation, either version 3 of the + License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper +mail. + +If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for +the specific requirements. + +You should also get your employer (if you work as a programmer) or +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. For more information on this, and how to apply and follow +the GNU AGPL, see . diff --git a/MISCHLABS_DEPLOYMENT.md b/MISCHLABS_DEPLOYMENT.md new file mode 100644 index 0000000..e1a6962 --- /dev/null +++ b/MISCHLABS_DEPLOYMENT.md @@ -0,0 +1,83 @@ +# MischLabs Monica Deployment + +This repository is the MischLabs fork of Monica CRM, based on the upstream `monicahq/monica` `4.x` branch. + +## Repository and Image + +- Gitea repository: `https://git.mischlabs.de/MrDiderot/CRM.git` +- Upstream base: `https://github.com/monicahq/monica`, branch `4.x` +- Registry image: `git.mischlabs.de/mrdiderot/crm:latest` +- NAS path: `/volume2/docker/mischcrm` +- Default NAS port: `38090:80` + +## Architecture + +The NAS does not build the image locally. Gitea Actions builds the Monica image from `scripts/docker/Dockerfile` and pushes it to the local Gitea registry. The NAS runs: + +- `mischcrm`: Monica Apache web container +- `mischcrm_cron`: Monica scheduler container using `cron.sh` +- `mischcrm_db`: MariaDB 11 + +Persistent Docker volumes: + +- `mischcrm_monica_storage`: Monica storage, OAuth keys, uploads, logs +- `mischcrm_monica_db`: MariaDB data + +## First Start on NAS + +```bash +cd /volume2/docker/mischcrm +cp .env.mischlabs.example .env +``` + +Generate secrets: + +```bash +openssl rand -base64 32 +openssl rand -hex 32 +``` + +Edit `.env`: + +- `APP_KEY` must be `base64:` +- `DB_PASSWORD` must be the long random hex password +- `APP_URL` must match the public reverse proxy URL, for example `https://crm.mischlabs.de` + +Then start: + +```bash +docker compose pull +docker compose up -d +``` + +Check: + +```bash +docker ps --filter "name=mischcrm" --format "table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}" +docker logs mischcrm --tail=120 +``` + +## Updates + +Watchtower should update `git.mischlabs.de/mrdiderot/crm:latest` automatically after Gitea Actions pushes a new image. + +Manual update: + +```bash +cd /volume2/docker/mischcrm +git pull --ff-only +docker compose pull +docker compose up -d +``` + +## Important + +Do not commit `.env`. + +Do not delete the Docker volumes unless you explicitly want to remove Monica data. + +If Monica redirects incorrectly behind Cloudflare/Nginx Proxy Manager, check: + +- `APP_URL` +- `APP_TRUSTED_PROXIES=*` +- reverse proxy target port `38090` diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..a6161ab --- /dev/null +++ b/Procfile @@ -0,0 +1,3 @@ +web: vendor/bin/heroku-php-nginx -C nginx_app.conf /public +queue: php artisan queue:work --sleep=3 --tries=3 +release: php artisan monica:update --force -vvv diff --git a/README.md b/README.md index b6d6c2a..c4d94f8 100644 --- a/README.md +++ b/README.md @@ -1,139 +1,318 @@ -# 🤝 MischCRM - Personal Relationship Manager +# MischLabs Monica Fork -Willkommen bei **MischCRM**, deinem eleganten, privaten und vollautomatisierten Beziehungsmanager (Personal CRM). Diese Anwendung wurde entwickelt, um lose Textnotizen (z. B. aus Obsidian) durch eine intuitive, saubere Web-Oberfläche zu ersetzen, mit der du deine Freundschaften, gemeinsamen Treffen, offenen Gesprächsthemen und Meilensteine mühelos verwalten kannst. +This repository is the MischLabs-hosted fork of Monica CRM. For deployment on MischNAS via Gitea Registry and Watchtower, see [`MISCHLABS_DEPLOYMENT.md`](MISCHLABS_DEPLOYMENT.md). -Das gesamte System läuft **vollständig lokal und privat** ohne externe Tracker oder Cloud-Dienste auf deinem Server (z. B. MischNAS). +The upstream Monica README follows below. ---- +

-## ✨ Features +![Monica's Logo](https://user-images.githubusercontent.com/61099/37693034-5783b3d6-2c93-11e8-80ea-bd78438dcd51.png) -* **Premium Glassmorphism Dark Theme**: Ein luxuriöses, modernes Interface mit Outfits-Schriftart, Radial-Glows, Unschärfe-Effekten (`backdrop-filter`) und flüssigen Mikro-Animationen. -* **Interaktives Ehre-Rating**: Ein dynamisches Ehre-Zähler-Modul (+/- Buttons) direkt auf den Profilkarten und im Detailbereich mit spürbarem UI-Feedback. -* **Schnell-Log für Treffen**: Trage mit wenigen Klicks Treffen direkt vom Dashboard oder aus einem Freundesprofil ein (Aktivität, Stimmung/Vibe, detaillierte Gesprächsnotizen). -* **"Lange nicht gesehen"-Warner**: Eine automatische Liste auf dem Dashboard, die Freunde nach dem Datum des letzten Treffens sortiert und dich warnt, wenn du dich länger nicht gemeldet hast. -* **Geburtstags-Tabelle**: Zeigt anstehende Geburtstage in den nächsten 30 Tagen an, berechnet die Tage bis zum Geburtstag und das neue Lebensalter. -* **Dinge zum Ansprechen (Gesprächsthemen-Checkliste)**: Verwalte offene Fragen, D&D-Ideen oder Themen für das nächste Treffen direkt auf dem Profil des Freundes. -* **Obsidian Markdown Import**: Ein intelligenter Parser, der Obsidian `.md`-Notizen einliest, YAML-Frontmatter und Überschriften analysiert und die Profildaten direkt in die SQLite-Datenbank überführt. -* **Dockerized Deployment**: Perfekt vorbereitet für das einfache Hosting auf deinem `MischNAS` mittels Docker und Docker Compose. +

+

Personal Relationship Manager

---- +
-## 🚀 Lokale Installation & Entwicklung +[![Build Status](https://img.shields.io/github/workflow/status/monicahq/monica/Build?style=flat-square&label=Build%20Status)](https://github.com/monicahq/monica/actions) +[![Docker pulls](https://img.shields.io/docker/pulls/library/monica)](https://hub.docker.com/_/monica/) +![Lines of code](https://img.shields.io/tokei/lines/github/monicahq/monica) +[![Code coverage](https://img.shields.io/sonar/coverage/monica?server=https%3A%2F%2Fsonarcloud.io&style=flat-square&label=Coverage%20Status)](https://sonarcloud.io/project/activity?custom_metrics=coverage&graph=custom&id=monica) +[![License](https://img.shields.io/github/license/monicahq/monica)](https://github.com/monicahq/monica/blob/main/LICENSE.md) -### Voraussetzungen -* **Node.js** (v18 oder neuer) -* **npm** oder **pnpm** -### Setup -1. Installiere die Abhängigkeiten im CRM-Verzeichnis: - ```bash - npm install - ``` +
-2. Starte den Server im Entwicklungsmodus (mit automatischem Reload bei Codeänderungen): - ```bash - npm run dev - ``` - Der Server läuft nun auf [http://localhost:8085](http://localhost:8085). +Monica is a great open source personal relationship management system. ---- +- [Introduction](#introduction) + - [Purpose](#purpose) + - [Features](#features) + - [Who is it for?](#who-is-it-for) + - [What Monica isn’t](#what-monica-isnt) + - [Where does this tool come from?](#where-does-this-tool-come-from) +- [Get started](#get-started) + - [Requirements](#requirements) + - [Update your instance](#update-your-instance) +- [Contribute](#contribute) + - [Contribute as a community](#contribute-as-a-community) + - [Contribute as a developer](#contribute-as-a-developer) +- [Principles, vision, goals and strategy](#principles-vision-goals-and-strategy) + - [Principles](#principles) + - [Vision](#vision) + - [Goals](#goals) + - [Strategy](#strategy) + - [Monetization](#monetization) + - [Why Open Source?](#why-open-source) + - [Patreon](#patreon) +- [Contact](#contact) +- [Team](#team) +- [Thank you, open source](#thank-you-open-source) +- [License](#license) -## 🐳 Bereitstellung mit Docker (MischNAS) +## Introduction -MischCRM lässt sich mit einem einzigen Befehl per Docker Compose deployen. +Monica is an open-source web application to organize and record your interactions with your loved ones. We call it a PRM, or Personal Relationship Management. Think of it as a [CRM](https://en.wikipedia.org/wiki/Customer_relationship_management) (a popular tool used by sales teams in the corporate world) for your friends or family. This is what it currently looks like: -### Starten -Führe folgenden Befehl im Verzeichnis aus, in dem sich die `docker-compose.yml` befindet: -```bash -docker compose up -d -``` -Die Anwendung ist anschließend unter **[http://localhost:38090](http://localhost:38090)** erreichbar (Port `38090` auf dem Host wird auf Port `8085` im Container weitergeleitet). +

-### 💾 Daten-Persistenz & Backups -Das CRM verwendet **SQLite** zur Datenhaltung. Die Datenbankdatei `crm.db` wird im Host-Ordner `./data/` abgelegt, welcher in den Container gemountet wird (`/app/data`). +![Screenshot of the application](docs/images/main-app.png) -* **Kein Datenverlust**: Selbst wenn der Container gelöscht, geupdatet oder neu gebaut wird, bleiben alle deine Freunde, Treffen und Notizen in `./data/crm.db` sicher auf deinem Host-System gespeichert. -* **Einfache Backups**: Sichere einfach die Datei `./data/crm.db`, um ein vollständiges Backup deines CRMs zu erstellen. +

---- +### Purpose -## 📂 Obsidian Import Guide +Monica allows people to keep track of everything that’s important about their friends and family. Like the activities with them. When you last called someone and what you talked about. It will help you remember the name and the age of their kids. It can also remind you to call someone you haven’t talked to in a while. -Du kannst bestehende Markdown-Notizen aus deinem Obsidian-Vault direkt in MischCRM importieren. Kopiere dazu einfach den gesamten Text der Notiz in das Feld unter dem Tab **Obsidian Import**. +### Features -### Unterstützte Markdown-Struktur: -Der Parser ist optimiert auf folgende Struktur (siehe z. B. dein Notizenformat für *Aaron Lingel*): +* Add and manage contacts +* Define relationships between contacts +* Reminders +* Automatic reminders for birthdays +* Stay in touch with a contact by sending reminders at a given interval +* Management of debts +* Ability to add notes to a contact +* Ability to record how you met someone +* Management of activities with a contact +* Management of tasks +* Management of gifts given and received and ideas for gifts +* Management of addresses and all the different ways to contact someone +* Management of contact field types +* Management of a contact’s pets +* Basic journal +* Ability to record how your day went +* Upload documents and photos +* Export and import of data +* Export contacts as vCards +* Ability to define custom genders +* Ability to define custom activity types +* Ability to favorite contacts +* Track conversations on social media or SMS +* Multiple users +* Tags to organize contacts +* Ability to define what section should appear on the contact sheet +* Multiple currencies +* Multiple languages +* An API that covers most of the data -```markdown ---- -tags: - - Freund ---- +### Who is it for? -## Allgemeines -- Name: Aaron Lingel -- Geburtstag: 13.08.1999 -- Kontakt: +49 172 1821612 -- Wohnort: Am schlaggraben 18, 71272 renningen -- Beziehungsstatus: FOREVER ALONE (aber hat mich) -- Familie: Mama Lingel und Papa Africano -- Ehre: +32 +This project is **for people who have difficulty remembering details about other people’s lives** – especially those they care about. Yes, you can still use Facebook to achieve this, but you will only be able to see what people do and post, and not add your own notes about them. -## Aktuelle Lebenssituation -- Arbeit/Studium: Studium / Arbeit -- Hobbys/Interessen: Ehrenbruder sein, D&D, Gaming -Auf der Suche nach neuen Abenteuern. +We’ve also received lots of positive feedback from users who suffer from Asperger syndrome, Alzheimer’s disease, or simply introverts who use this application on a daily basis. -## Persönliche Meilensteine -- Hat ein stabiles Freundschaftsprofil bekommen -- Neue Wohnung bezogen +### What Monica isn’t -## Random Infos -- Lieblingsessen/-getränk: Pizza, Kaltgetränke -- Sonstige Infos: Bester Kumpel. Immer am Start. -``` + * Monica is not a social network and **it never will be**. It’s not meant to be social. It’s designed to be the opposite: it’s for your eyes only. + * Monica is not a smart assistant. It won’t guess what you want to do. It’s actually pretty dumb: it will only send you emails for the things you asked to be reminded of. + * Monica is not a tool that will scan your data and do nasty things with it. It’s your data, your server, do whatever you want with it. You’re in control of your data. -### So funktioniert der Import: -1. Der Parser extrahiert den Namen aus dem Dateinamen oder dem Feld `Name:` unter `## Allgemeines`. -2. Geburtstage im Format `DD.MM.YYYY` werden automatisch in das Standardformat `YYYY-MM-DD` konvertiert. -3. Der Zähler für **Ehre** (z. B. `+32`) wird extrahiert und als Startwert festgelegt. -4. Alle Unterabschnitte werden den entsprechenden Datenbankspalten zugeordnet. -5. Nach dem Import kannst du direkt per Klick auf das neu erstellte CRM-Profil springen! +### Where does this tool come from? ---- +I originally built this tool to help me in my private life: I’ve been living outside my own country for a long time now. I want to keep notes and remember the life of my friends in my home country and be able to ask the relevant questions when I email them or talk to them over the phone. -## 🛠️ Technologien +Moreover, as a foreigner in my new country, I met a lot of other foreigners – and most go back to their countries. I still want to remember the names or ages of their kids. You may call it cheating but considering my poor memory, I call it caring. -* **Backend**: Node.js, Express.js, SQLite (`sqlite3`) -* **Frontend**: Vanilla HTML5, CSS3 (luxuriöse HSL-Variablen, Backdrop-Filter), Vanilla JavaScript (SPA, state-driven rendering) -* **Icons**: Lucide Icons (per CDN geladen) +After a few months, I decided to open source Monica so it could help other people as well. ---- +## Get started -## Gitea Registry Deployment auf MischNAS +There are multiple ways of getting started with Monica: -MischCRM soll auf der NAS nicht lokal gebaut werden. Der Build laeuft in Gitea Actions und pusht das fertige Image in die lokale Gitea Container Registry: +1. You can use [our Hosted version](https://monicahq.com "Monica website"). This is the simplest way to use Monica. +1. You can install it on your own server by following the [installation instructions here](/docs/installation/readme.md). There are no limitations on Monica if you install it on your own server. -```text -git.mischlabs.de/mrdiderot/crm:latest -``` + - The downloadable version will always be the most complete version – the same as offered on the paid plan on the Hosted version. + - Self-hosted will always be completely free with no strings attached and you will be in complete control. -Die NAS zieht dieses Image per Docker Compose. Der universelle Watchtower kann es danach regelmaessig pruefen und bei neuen Images automatisch aktualisieren. +1. You can deploy straight on a [PaaS platform](https://en.wikipedia.org/wiki/Platform_as_a_service) like: -### Start und manuelles Update auf der NAS + - Platform.sh [![Deploy on Platform.sh](https://platform.sh/images/deploy/deploy-button-lg-blue.svg)](https://console.platform.sh/projects/create-project/?template=https%3A%2F%2Fraw.githubusercontent.com%2Fmonicahq%2Fmonica%2Fmain%2F.platform.app.yaml&utm_campaign=deploy_on_platform&utm_medium=button&utm_source=affiliate_links&utm_content=https%3A%2F%2Fgithub.com%2Fmonicahq%2Fmonica) -```bash -cd /volume2/docker/mischcrm -git pull --ff-only -docker compose pull -docker compose up -d -``` + - [Heroku](https://heroku.com) [![Deploy to Heroku](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/monicahq/monica/tree/4.x) -Falls `docker compose up -d` lokal zu bauen beginnt, steht in der Compose noch `build:` statt `image:`. Korrekt ist: -```yaml -image: git.mischlabs.de/mrdiderot/crm:latest -``` +### Requirements -Die SQLite-Daten bleiben ueber `./data:/app/data` persistent. Die Datei `./data/crm.db` darf nicht ins Git committed werden. +If you want to host Monica yourself, you will need a server with: + +- PHP 8.1 or newer +- HTTP server with PHP support (eg: Apache, Nginx, Caddy) +- Composer +- MySQL + +To successfully build and host Monica, we recommend a system with at least 1.5 GB for RAM. Monica can run on systems with significantly less memory, but due to the high memory requirements of the build process during updates, you may encounter issues and failed builds. + +### Update your instance + +Once the software is installed, you’ll need to update it from time to time to have access to the latest features. [Read this document](/docs/installation/update.md) to learn how to do it. + +## Contribute + +Do you want to help? That’s awesome. We welcome contributions of all kinds from everyone. + +Here are some of the things you can do to help. + +### Contribute as a community + +- Unlike Fight Club, the best way to help is **to actually talk about Monica** as much as you can in blog posts and articles, or on Twitter and Facebook. + +- You can answer questions in [the issue tracker](https://github.com/monicahq/monica/issues) to help other community members. + +- You can financially support Monica’s development [on Patreon](https://www.patreon.com/monicahq) or by subscribing to [a paid account](https://monicahq.com/pricing). + +### Contribute as a developer + +- Read our [Contribution Guide](/CONTRIBUTING.md). + +- Install [the developer version locally](/docs/contribute/readme.md) so you can start contributing. + +- Look for [issues labelled ‘Bugs’](https://github.com/monicahq/monica/issues?q=is%3Aopen+is%3Aissue+label%3Abug) if you are looking to have an immediate impact on Monica. + +- Look for [issues labelled ‘Help Wanted’](https://github.com/monicahq/monica/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22). These are issues that you can solve relatively easily. + +- Look for [issues labelled ’Good First Issue’](https://github.com/monicahq/monica/labels/good%20first%20issue). These issues are for people who want to contribute, but try to work on a small feature first. + +- If you are an advanced developer, you can try to tackle [issues labelled ‘Feature Requests’](https://github.com/monicahq/monica/issues?q=is%3Aopen+is%3Aissue+label%3A%22feature+request%22). These are harder to do and will require a lot of back-and-forth with the repository administrator to make sure we are going to the right direction with the product. + + +## Principles, vision, goals and strategy + +We want to use technology in a way that does not harm human relationships, like big social networks can do. + +### Principles + +Monica has a few principles. + +- It should help have better relationships. + +- It should be simple to use, simple to contribute to, simple to understand, extremely simple to maintain. + +- It is not a social network and never will be. + +- It is not and never will be ad-supported. + +- Users are not and never will be tracked. + +- It should be transparent. + +- It should be open-source. + +- It should do one thing (documenting social interactions) extremely well, and nothing more. + +- It should be well documented. + +### Vision + +Monica’s vision is to **help people have more meaningful relationships**. + +### Goals + +We want to provide a platform that is: + +- **really easy to use**: we value simplicity over anything else. + +- **open-source**: we believe everyone should be able to contribute to this tool, and see for themselves that nothing nasty is done behind the scenes that would go against the best interests of the users. We also want to leverage the community to build attractive features and do things that would not be possible otherwise. + +- **easy to contribute to**: we want to keep the codebase as simple as possible. This has two big advantages: anyone can contribute, and it’s easily maintainable on the long run. + +- **available everywhere**: Monica should be able to run on any desktop OS or mobile phone easily. This will be made possible by making sure the tool is easily installable by anyone who wants to either contribute or host the platform themselves. + +### Strategy + +We think Monica has to become a platform more than an application, so people can build on it. + +Here what we should do in order to realize our vision: + +- (**done**) Build an API in order to create an ecosystem. The ecosystem is what will make Monica a successful platform. + +- (**done**) Build importers and exporters of data. We don’t want to have any vendor lock-ins. Data is the property of the users and they should be able to do whatever they want with it. + +- (**done**) Be the central point of contact management, by supporting CardDav protocol. + +- (**done**) Be the central point of calendar events, by supporting CalDav protocol. + +- (**partially done**) Build great reports so people can have interesting insights on how they interact with their loved ones. + +- Create a smart recommendation system for gifts. For instance, if my nephew is soon 6 years old in a month, I will be able to receive an email with a list of 5 potential gifts I can offer to a 6 year old boy. + +- Add more ways of being reminded: Telegram, SMS,... + +- Create Chrome extensions to load Monica’s data in a sidebar when viewing a contact on Facebook, letting us take additional notes as we see them on Facebook. + +- Add modules that can be activated on demand. One would be for instance, for the people who wants to use Monica for dating purposes (yes, we’ve received this kind of feedback already). + +### Monetization + +While it’s not the driving force behind Monica, it would be great if the tool could generate money so we could work full time on it and sustain it on the long run. We are big fans of [Sentry](https://sentry.io), Wordpress and GitLab and we believe this kind of business model is an inspiring one where everyone wins. + +If you want to support the development of Monica, consider taking [a paid account](https://www.monicahq.com/pricing), or support us [on Patreon](https://www.patreon.com/monicahq). + +- The [Hosted version of Monica](https://monicahq.com) is offered in two versions: + + * a [free plan](https://app.monicahq.com/register) which includes: + + 10 contacts + + data exporters + + * a [paid plan](https://www.monicahq.com/pricing) which includes: + + unlimited contacts + + email reminders + + data importers + + advanced features + + * We’re still working on the features included in the paid plan, and these may be subject to change while we work out our business model to make Monica’s development sustainable. + + * People who substantially contribute to the GitHub repository (with a pull request that adds value, that gets merged – not a typo fix, for instance) will also have access to the paid version for free. + +- There is a [Patreon account](https://www.patreon.com/monicahq) for those who want to financially support Monica’s development in another way. The best way to support Monica it is to actually talk about it and help grow its userbase. + + +There are no ads on the platform and there never will be. We will never resell your data on [the Hosted version](https://monicahq.com/) and we have no access to it if you self-host. + +We are like you, and this is why we are on GitHub: we hate big corporations that do not have at heart the best interests of their users, even if they say otherwise. We believe that the only way to sustain the development of Monica is to actually make money in a good old-fashioned way. + +### Why Open Source? + +Why is Monica open source? Is it risky? Will someone steal my code and do a for-profit business that will kill my own business? Why reveal my strategy to the world? These are the kind of questions we’ve received by email already. + +The answer to these questions is simple: yes, you can fork Monica and make a competing project, make money out of it (even if the license is not super friendly towards that) and I’ll never know. But it’s okay, I don’t mind. + +I wanted to open source Monica for several reasons: + +- **I believe that this tool can really change people’s lives.** + While I aim to make money out of it, I also want everyone to benefit from it. Open sourcing a project like this will help Monica become much bigger than what I imagine myself. While I strongly believe that this software has to follow the vision I have for it, I need to be humble enough to know that ideas come from everywhere, and people have much better ideas than what I can have. + +- **You can’t make something great alone.** + While Monica could become a company and hire a bunch of super smart people to work on it, you can’t beat the manpower of an entire community. Open sourcing the product means bugs will be fixed faster, features will be developed faster, and more importantly, developers will be able to contribute to a tool that positively changes their own lives and the lives of other people. + +- **Doing things in a transparent way leads to formidable things.** + People respect the project more when they can see how it’s being worked on. You can’t hide nasty things in the code. You can’t do things behind the backs of your users. Doing everything in the open is a major driving force that motivates you to keep doing what’s right. + +- **Once you’ve created a community of passionate developers around your project, you’ve won.** + Because developers are very powerful influencers. Developers will create apps around your product, talk about it on forums, and share the project with their friends, families, and colleagues. Cherish the developers – users will follow. + +### Patreon + +You can support the development of Monica [on Patreon](https://www.patreon.com/monicahq). Thanks for your help. + +## Contact + +## Team + +Our team is made of two core members: + +- [Maazarin (djaiss)](https://github.com/djaiss) + +- [Alexis Saettler (asbiin)](https://github.com/asbiin) + +We are also fortunate to have an amazing [community of developers](https://github.com/monicahq/monica/graphs/contributors) who help us greatly. + +## Thank you, open source + +Monica uses a lot of open source projects and we thank them with all our hearts. We hope that providing Monica as an free, open source project will help other people the same way those softwares have helped us. + +## License + +Copyright © 2016–2022 + +Licensed under [the AGPL License](/LICENSE.md). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..de4a294 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,3 @@ +## Reporting a Vulnerability + +If you discover any security related issues, please email security@monicahq.com instead of using the issue tracker. diff --git a/app.json b/app.json new file mode 100644 index 0000000..6190013 --- /dev/null +++ b/app.json @@ -0,0 +1,74 @@ +{ + "name": "Monica", + "description": "Personal Relationship Manager - a new kind of CRM to manage your friends and family.", + "repository": "https://github.com/monicahq/monica", + "logo": "https://raw.githubusercontent.com/monicahq/monica/main/public/img/monica_60.png", + "keywords": [ + "CRM", + "family", + "friends", + "relationship", + "PRM" + ], + "buildpacks": [ + { + "url": "heroku/php" + }, + { + "url": "heroku/nodejs" + } + ], + "addons": [ + { + "plan": "jawsdb:kitefin" + }, + { + "plan": "scheduler:standard" + } + ], + "scripts": { + "postdeploy": "php artisan setup:production --force -vvv" + }, + "env": { + "APP_KEY": { + "description": "Please change this to a 32-character string. For example run `echo -n 'base64:'; openssl rand -base64 32` and copy/paste the value.", + "value": "change-me-to-a-random-string----" + }, + "APP_URL": { + "description": "Please change this to your Heroku app's domain.", + "value": "https://XXX.herokuapp.com" + }, + "APP_ENV": { + "description": "Use monica in 'production' mode, or set it to 'local' if you want to install Monica as a development version.", + "value": "production" + }, + "APP_DISABLE_SIGNUP": { + "description": "Disable user signup.", + "value": "false" + }, + "APP_DEBUG": { + "description": "Enables or disables debug mode.", + "value": "false" + }, + "APP_EMAIL_NEW_USERS_NOTIFICATION": { + "description": "", + "value": "noreply@example.com" + }, + "MAIL_FROM_ADDRESS": { + "description": "", + "value": "noreply@example.com" + }, + "MAIL_FROM_NAME": { + "description": "", + "value": "Bob Smith" + }, + "DB_CONNECTION": { + "description": "Tells the application to use Heroku's database connection.", + "value": "heroku" + }, + "HEROKU": { + "description": "Tells the application this application is hosted on Heroku.", + "value": "true" + } + } +} diff --git a/app/Console/Commands/CalculateStatistics.php b/app/Console/Commands/CalculateStatistics.php new file mode 100644 index 0000000..235dbb7 --- /dev/null +++ b/app/Console/Commands/CalculateStatistics.php @@ -0,0 +1,69 @@ +number_of_users = DB::table('users')->count(); + $statistic->number_of_contacts = DB::table('contacts')->count(); + $statistic->number_of_notes = DB::table('notes')->count(); + $statistic->number_of_reminders = DB::table('reminders')->count(); + $statistic->number_of_tasks = DB::table('tasks')->count(); + $statistic->number_of_invitations_sent = DB::table('accounts')->sum('number_of_invitations_sent'); + + // number_of_accounts_with_more_than_one_user + $number_of_accounts_with_more_than_one_user = 0; + foreach (Account::all() as $account) { + if ($account->users()->count() > 1) { + $number_of_accounts_with_more_than_one_user = $number_of_accounts_with_more_than_one_user + 1; + } + } + $statistic->number_of_accounts_with_more_than_one_user = $number_of_accounts_with_more_than_one_user; + $statistic->number_of_import_jobs = DB::table('import_jobs')->count(); + $statistic->number_of_tags = DB::table('tags')->count(); + $statistic->number_of_activities = DB::table('activities')->count(); + $statistic->number_of_addresses = DB::table('addresses')->count(); + $statistic->number_of_api_calls = DB::table('api_usage')->count(); + $statistic->number_of_calls = DB::table('calls')->count(); + $statistic->number_of_contact_fields = DB::table('contact_fields')->count(); + $statistic->number_of_contact_field_types = DB::table('contact_field_types')->count(); + $statistic->number_of_debts = DB::table('debts')->count(); + $statistic->number_of_entries = DB::table('entries')->count(); + $statistic->number_of_gifts = DB::table('gifts')->count(); + $statistic->number_of_oauth_access_tokens = DB::table('oauth_access_tokens')->count(); + $statistic->number_of_oauth_clients = DB::table('oauth_clients')->count(); + $statistic->number_of_relationships = DB::table('relationships')->count(); + $statistic->number_of_subscriptions = DB::table('subscriptions')->count(); + $statistic->number_of_conversations = DB::table('conversations')->count(); + $statistic->number_of_messages = DB::table('messages')->count(); + + $statistic->save(); + } +} diff --git a/app/Console/Commands/Clean.php b/app/Console/Commands/Clean.php new file mode 100644 index 0000000..0d8ffca --- /dev/null +++ b/app/Console/Commands/Clean.php @@ -0,0 +1,53 @@ +handleTokenDelete($event->token); + }); + + app(TokenClean::class)->execute([ + 'dryrun' => (bool) $this->option('dry-run'), + ]); + } + + /** + * Handle TokenDeleteEvent event. + * + * @param SyncToken $token + */ + private function handleTokenDelete($token) + { + $this->info('Delete token '.$token->id.' - User '.$token->user_id.' - Type '.$token->name.' - timestamp '.$token->timestamp); + } +} diff --git a/app/Console/Commands/CreateAccount.php b/app/Console/Commands/CreateAccount.php new file mode 100644 index 0000000..97408d2 --- /dev/null +++ b/app/Console/Commands/CreateAccount.php @@ -0,0 +1,70 @@ +option('email'); + if (empty($email)) { + $this->error($this::ERROR_MISSING_EMAIL); + } + + $password = $this->option('password'); + if (empty($password)) { + $this->error($this::ERROR_MISSING_PASSWORD); + } + + $firstName = $this->option('firstname') ?? 'John'; + + $lastName = $this->option('lastname') ?? 'Doe'; + + if (empty($email) || empty($password)) { + return; + } + + if ($this->confirmToProceed('This will create a new user for '.$firstName.' '.$lastName.' with email '.$email)) { + Account::createDefault($firstName, $lastName, $email, $password); + + $this->info('| You can now sign in to your account:'); + $this->line('| username: '.$email); + $this->line('| password: '); + } + } +} diff --git a/app/Console/Commands/DavClientsUpdate.php b/app/Console/Commands/DavClientsUpdate.php new file mode 100644 index 0000000..dacb569 --- /dev/null +++ b/app/Console/Commands/DavClientsUpdate.php @@ -0,0 +1,55 @@ +get(); + + $now = now(); + $subscriptions->filter(function ($subscription) use ($now) { + return $this->isTimeToRunSync($subscription, $now); + })->each(function ($subscription) { + SynchronizeAddressBooks::dispatch($subscription); + }); + } + + /** + * Test if the last synchronized timestamp is older than the subscription's frequency time. + * + * @param AddressBookSubscription $subscription + * @param Carbon $now + * @return bool + */ + private function isTimeToRunSync(AddressBookSubscription $subscription, Carbon $now): bool + { + return is_null($subscription->last_synchronized_at) + || $subscription->last_synchronized_at->addMinutes($subscription->frequency)->lessThan($now); + } +} diff --git a/app/Console/Commands/Deactivate2FA.php b/app/Console/Commands/Deactivate2FA.php new file mode 100644 index 0000000..fbd34ee --- /dev/null +++ b/app/Console/Commands/Deactivate2FA.php @@ -0,0 +1,75 @@ +option('email'); + + // if no email was passed to the option, prompt the user to enter the email + if (! $email) { + $email = $this->ask('what is the user\'s email?'); + } + + // retrieve the user with the specified email + $user = User::where('email', $email)->first(); + + if (! $user) { + // show an error and exist if the user does not exist + $this->error('No user with that email.'); + + return; + } + if (is_null($user->google2fa_secret)) { + // show an error and exist if the user does not exist + $this->error('2FA is currently not activated for this user.'); + + return; + } + + // Print a warning + $this->info('2FA will be deactivated for '.$user->email); + $this->info('This action can\'t be cancelled.'); + + // ask for confirmation if not forced + if (! $this->option('force') && ! $this->confirm('Do you wish to continue?')) { + return; + } + + // remove google2fa_secret key + $user->google2fa_secret = null; + + // save the user + $user->save(); + + // show the new secret key + $this->info('2FA has been deactivated for '.$user->email); + } +} diff --git a/app/Console/Commands/ExportAll.php b/app/Console/Commands/ExportAll.php new file mode 100644 index 0000000..6803f5e --- /dev/null +++ b/app/Console/Commands/ExportAll.php @@ -0,0 +1,34 @@ +info('Exported as '.$job->handle().''); + } +} diff --git a/app/Console/Commands/GetVersion.php b/app/Console/Commands/GetVersion.php new file mode 100644 index 0000000..6b8d470 --- /dev/null +++ b/app/Console/Commands/GetVersion.php @@ -0,0 +1,32 @@ +line(config('monica.app_version')); + } +} diff --git a/app/Console/Commands/Helpers/Command.php b/app/Console/Commands/Helpers/Command.php new file mode 100644 index 0000000..0eb5dcb --- /dev/null +++ b/app/Console/Commands/Helpers/Command.php @@ -0,0 +1,71 @@ +$method(...$args); + } +} diff --git a/app/Console/Commands/Helpers/CommandCaller.php b/app/Console/Commands/Helpers/CommandCaller.php new file mode 100644 index 0000000..c9576ff --- /dev/null +++ b/app/Console/Commands/Helpers/CommandCaller.php @@ -0,0 +1,54 @@ +info($message); + $command->line($commandline, null, OutputInterface::VERBOSITY_VERBOSE); + exec($commandline.' 2>&1', $output); + foreach ($output as $line) { + $command->line($line, null, OutputInterface::VERBOSITY_VERY_VERBOSE); + } + $command->line('', null, OutputInterface::VERBOSITY_VERBOSE); + } + + /** + * Print a message on the console, then execute an artisan command. + * + * @param Command $command Laravel command context + * @param string $message Message to output + * @param string $commandline Artisan command name to execute + * @param array $arguments Optional arguments to pass to the artisan command + * + * @codeCoverageIgnore + */ + public function artisan(Command $command, string $message, string $commandline, array $arguments = []): void + { + $info = ''; + foreach ($arguments as $key => $value) { + if (is_string($key)) { + $info .= ' '.$key.'="'.$value.'"'; + } else { + $info .= ' '.$value; + } + } + $this->exec($command, $message, Application::formatCommandString($commandline.$info)); + } +} diff --git a/app/Console/Commands/Helpers/CommandCallerContract.php b/app/Console/Commands/Helpers/CommandCallerContract.php new file mode 100644 index 0000000..483c306 --- /dev/null +++ b/app/Console/Commands/Helpers/CommandCallerContract.php @@ -0,0 +1,27 @@ +option('ldap_uri') ?? '127.0.0.1'; + $ldap_attr_mail = $this->option('ldap_attr_mail') ?? 'mail'; + $ldap_attr_firstname = $this->option('ldap_attr_firstname') ?? 'givenName'; + $ldap_attr_lastname = $this->option('ldap_attr_lastname') ?? 'sn'; + + $ldap_user = $this->option('ldap_user'); + if (empty($ldap_user)) { + $this->error($this::ERROR_MISSING_LDAP_USER); + } + + $ldap_pass = $this->option('ldap_pass'); + if (empty($ldap_pass)) { + $this->error($this::ERROR_MISSING_LDAP_PASS); + } + + $ldap_base = $this->option('ldap_base'); + if (empty($ldap_base)) { + $this->error($this::ERROR_MISSING_LDAP_BASE); + } + + $ldap_filter = $this->option('ldap_filter'); + if (empty($ldap_filter)) { + $this->error($this::ERROR_MISSING_LDAP_FILTER); + } + + if (empty($ldap_user) || empty($ldap_pass) || empty($ldap_base) || empty($ldap_filter)) { + return; + } + + $ldap_conn = ldap_connect($ldap_uri); + if (! $ldap_conn) { + $this->error('Could not connect to LDAP URI'); + + return; + } + if (! ldap_set_option($ldap_conn, LDAP_OPT_PROTOCOL_VERSION, 3)) { + $this->error('Could not set LDAP protocol v3'); + + return false; + } + + try { + $bind = ldap_bind($ldap_conn, $ldap_user, $ldap_pass); + if (! $bind) { + $this->error('Could not bind with given LDAP credentials'); + + return; + } + } catch (\Exception $e) { + $this->error($e->getMessage()); + + return; + } + + $ldap_res = []; + try { + $ldap_res = ldap_search($ldap_conn, $ldap_base, $ldap_filter, [$ldap_attr_mail, $ldap_attr_firstname, $ldap_attr_lastname]); + } catch (\Exception $e) { + $this->error($e->getMessage()); + + return; + } + + $ldap_data = ldap_get_entries($ldap_conn, $ldap_res); + + for ($i = 0; $i < $ldap_data['count']; $i++) { + if (! (isset($ldap_data[$i][$ldap_attr_mail]) && $ldap_data[$i][$ldap_attr_mail]['count'] > 0)) { + continue; + } + $user_mail = $ldap_data[$i][$ldap_attr_mail][0]; + $user_firstname = 'John'; + $user_lastname = 'Doe'; + $user_password = bin2hex(random_bytes(64)); + if (isset($ldap_data[$i][$ldap_attr_firstname]) && $ldap_data[$i][$ldap_attr_firstname]['count'] > 0) { + $user_firstname = $ldap_data[$i][$ldap_attr_firstname][0]; + } + if (isset($ldap_data[$i][$ldap_attr_lastname]) && $ldap_data[$i][$ldap_attr_lastname]['count'] > 0) { + $user_lastname = $ldap_data[$i][$ldap_attr_lastname][0]; + } + $this->info('Importing user "'.$user_mail.'"'); + try { + Account::createDefault($user_firstname, $user_lastname, $user_mail, $user_password); + } catch (\Exception $import_error) { + $this->warn('Could not import user "'.$user_mail.'": '.$import_error->getMessage()); + } + } + } +} diff --git a/app/Console/Commands/ImportCSV.php b/app/Console/Commands/ImportCSV.php new file mode 100644 index 0000000..0c6d541 --- /dev/null +++ b/app/Console/Commands/ImportCSV.php @@ -0,0 +1,249 @@ +argument('file'); + + if (is_numeric($this->argument('user'))) { + $user = User::find($this->argument('user')); + } else { + $user = User::where('email', $this->argument('user'))->first(); + } + + if (! $user) { + $this->error('You need to provide a valid User ID or email address!'); + + return -1; + } + + if (! file_exists($file)) { + $this->error('You need to provide a valid file path.'); + + return -2; + } + + if (is_string($file)) { + $this->info("Importing CSV file {$file} to user {$user->id}"); + } + + // create special gender for this import + // we don't know which gender all the contacts are, so we need to create a special status for them, as we + // can't guess whether they are men, women or else. + $gender = Gender::where('name', config('dav.default_gender'))->first(); + if (! $gender) { + $gender = new Gender; + $gender->account_id = $user->account_id; + $gender->name = config('dav.default_gender'); + $gender->save(); + } + + $first = true; + $imported = 0; + $handle = fopen($file, 'r'); + try { + while (($data = fgetcsv($handle)) !== false) { /** @phpstan-ignore-line */ + // don't import the columns + if ($first) { + $first = false; + continue; + } + + // if first & last name do not exist skip row + if (empty($data[1]) && empty($data[3])) { + continue; + } + + $this->csvToContact($data, $user->account_id, $gender->id); + + $imported++; + } + } finally { + fclose($handle); + } + + $this->info("Imported {$imported} Contacts"); + } + + /** + * Create contact. + */ + private function csvToContact($data, $account_id, $gender_id) + { + $contact = new Contact(); + $contact->account_id = $account_id; + $contact->gender_id = $gender_id; + + if (! empty($data[1])) { + $contact->first_name = $data[1]; // Given Name + } + + if (! empty($data[2])) { + $contact->middle_name = $data[2]; // Additional Name + } + + if (! empty($data[3])) { + $contact->last_name = $data[3]; // Family Name + } + + $street = null; + if (! empty($data[49])) { + $street = $data[49]; // address 1 street + } + + $city = null; + if (! empty($data[50])) { + $city = $data[50]; // address 1 city + } + + $province = null; + if (! empty($data[52])) { + $province = $data[52]; // address 1 region (state) + } + + $postalCode = null; + if (! empty($data[53])) { + $postalCode = $data[53]; // address 1 postal code (zip) 53 + } + + if (! empty($data[66])) { + $contact->job = $data[66]; // organization 1 name 66 + } + + $contact->setAvatarColor(); + $contact->save(); + + if (! empty($data[28])) { + // Email 1 Value + ContactField::firstOrCreate([ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'data' => $data[28], + 'contact_field_type_id' => $this->contactFieldEmailId(), + ]); + } + + if ($postalCode || $province || $street || $city) { + $request = [ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'street' => $street, + 'city' => $city, + 'province' => $province, + 'postal_code' => $postalCode, + ]; + + app(CreateAddress::class)->execute($request); + } + + if (! empty($data[42])) { + // Phone 1 Value + ContactField::firstOrCreate([ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'data' => $data[42], + 'contact_field_type_id' => $this->contactFieldPhoneId(), + ]); + } + + if (! empty($data[14])) { + $birthdate = DateHelper::parseDate($data[14]); + + $specialDate = $contact->setSpecialDate('birthdate', $birthdate->year, $birthdate->month, $birthdate->day); + + app(CreateReminder::class)->execute([ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'initial_date' => DateHelper::getDate($specialDate), + 'frequency_type' => 'year', + 'frequency_number' => 1, + 'title' => trans( + 'people.people_add_birthday_reminder', + ['name' => $contact->first_name] + ), + 'delible' => false, + ]); + } + + GetAvatarsFromInternet::dispatch($contact); + } + + /** + * Get the default contact field email id for the account. + * + * @return int + */ + private function contactFieldEmailId() + { + if (! $this->contactFieldEmailId) { + $contactFieldType = ContactFieldType::where('type', 'email')->first(); + $this->contactFieldEmailId = $contactFieldType->id; + } + + return $this->contactFieldEmailId; + } + + /** + * Get the default contact field phone id for the account. + * + * @return int + */ + private function contactFieldPhoneId() + { + if (! $this->contactFieldPhoneId) { + $contactFieldType = ContactFieldType::where('type', 'phone')->first(); + $this->contactFieldPhoneId = $contactFieldType->id; + } + + return $this->contactFieldPhoneId; + } +} diff --git a/app/Console/Commands/ImportVCards.php b/app/Console/Commands/ImportVCards.php new file mode 100644 index 0000000..e3902f2 --- /dev/null +++ b/app/Console/Commands/ImportVCards.php @@ -0,0 +1,116 @@ +option('user'); + + // if no email was passed to the option, prompt the user to enter the email + if (! $email) { + $email = $this->ask('what is the user\'s email?'); + } + + // retrieve the user with the specified email + $user = User::where('email', $email)->first(); + + if (! $user) { + // show an error and exist if the user does not exist + $this->error('No user with that email.'); + + return -1; + } + + $path = $this->option('path'); + + // if no email was passed to the option, prompt the user to enter the email + if (! $path) { + $path = $this->ask('what file you want to import?'); + } + + if (! $filesystem->exists($path) || ! $this->acceptedExtensions($filesystem, $path)) { + $this->error('The provided vcard file was not found or is not valid!'); + + return -2; + } + + $importJob = $this->import($path, $user); + + return $this->report($importJob) ? 0 : 1; + } + + private function acceptedExtensions(Filesystem $filesystem, string $path): bool + { + switch ($filesystem->extension($path)) { + case 'vcf': + case 'vcard': + return true; + default: + return false; + } + } + + private function import(string $path, User $user): ImportJob + { + $pathName = Storage::putFile('public', new File($path)); + + $importJob = $user->account->importjobs()->create([ + 'user_id' => $user->id, + 'type' => 'vcard', + 'filename' => $pathName, + ]); + + AddContactFromVCard::dispatchSync($importJob); + + return $importJob; + } + + private function report(ImportJob $importJob) + { + $importJob->refresh(); + + if ($importJob->failed) { + $this->warn('Error: '.$importJob->failed_reason); + + return false; + } + + $this->info('Contacts found: '.$importJob->contacts_found); + $this->info('Contacts skipped: '.$importJob->contacts_skipped); + $this->info('Contacts imported: '.$importJob->contacts_imported); + + return true; + } +} diff --git a/app/Console/Commands/Inspire.php b/app/Console/Commands/Inspire.php new file mode 100644 index 0000000..6f3440c --- /dev/null +++ b/app/Console/Commands/Inspire.php @@ -0,0 +1,33 @@ +comment(PHP_EOL.Inspiring::quote().PHP_EOL); + } +} diff --git a/app/Console/Commands/LangGenerate.php b/app/Console/Commands/LangGenerate.php new file mode 100644 index 0000000..5d988c5 --- /dev/null +++ b/app/Console/Commands/LangGenerate.php @@ -0,0 +1,50 @@ +isDir()) { + continue; + } + + $lang = $dir->getFilename(); + if ($lang == '.' || $lang == '..') { + continue; + } + + $this->call('lang:js', [ + '--json' => true, + '--source' => $dir->getPathname(), + 'target' => 'public/js/langs/'.$lang.'.json', + ]); + } + } +} diff --git a/app/Console/Commands/MigrateDatabaseCollation.php b/app/Console/Commands/MigrateDatabaseCollation.php new file mode 100644 index 0000000..25cdf68 --- /dev/null +++ b/app/Console/Commands/MigrateDatabaseCollation.php @@ -0,0 +1,121 @@ +confirmToProceed()) { + try { + $connection = DBHelper::connection(); + + if ($connection->getDriverName() != 'mysql') { + return; + } + + $databasename = $connection->getDatabaseName(); + + $schemata = $connection->table('information_schema.schemata') + ->select('DEFAULT_CHARACTER_SET_NAME') + ->where('schema_name', '=', $databasename) + ->get(); + + $schema = $schemata->first()->DEFAULT_CHARACTER_SET_NAME; + + if (config('database.use_utf8mb4') && $schema == 'utf8') { + $this->line('Migrate to utf8mb4 schema collation'); + $this->toUtf8mb4($connection, $databasename); + } elseif (! config('database.use_utf8mb4') && $schema == 'utf8mb4') { + $this->line('Migrate to utf8 schema collation'); + $this->toUtf8($connection, $databasename); + } else { + $this->info('Nothing to migrate, everything is ok.'); + } + } catch (\Exception $e) { + $this->error(' '); + $this->error(' Check if the DB_USE_UTF8MB4 variable in .env file is correctly set '); + $this->error(' '); + $this->info(''); + throw $e; + } + } + } + + /** + * Switch to utf8mb4. + * + * @param \Illuminate\Database\Connection $connection + * @param string $databasename + */ + private function toUtf8mb4($connection, $databasename) + { + // Tables + $tables = $connection->table('information_schema.tables') + ->select('table_name') + ->where('table_schema', '=', $databasename) + ->get(); + + foreach ($tables as $table) { + DB::statement('ALTER TABLE `'.$table->table_name.'` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;'); + } + + // Database + $pdo = $connection->getPdo(); + $pdo->setAttribute(\PDO::ATTR_EMULATE_PREPARES, true); + DB::statement('ALTER DATABASE `'.$databasename.'` CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;'); + $pdo->setAttribute(\PDO::ATTR_EMULATE_PREPARES, false); + } + + /** + * Switch to utf8. + * + * @param \Illuminate\Database\Connection $connection + * @param string $databasename + */ + private function toUtf8($connection, $databasename) + { + // Tables + $tables = $connection->table('information_schema.tables') + ->select('table_name') + ->where('table_schema', '=', $databasename) + ->get(); + + foreach ($tables as $table) { + DB::statement('ALTER TABLE `'.$table->table_name.'` CONVERT TO CHARACTER SET utf8 COLLATE utf8_unicode_ci;'); + } + + // Database + $pdo = $connection->getPdo(); + $pdo->setAttribute(\PDO::ATTR_EMULATE_PREPARES, true); + DB::statement('ALTER DATABASE `'.$databasename.'` CHARACTER SET = utf8 COLLATE = utf8_unicode_ci;'); + $pdo->setAttribute(\PDO::ATTR_EMULATE_PREPARES, false); + } +} diff --git a/app/Console/Commands/NewAddressBookSubscription.php b/app/Console/Commands/NewAddressBookSubscription.php new file mode 100644 index 0000000..d67530f --- /dev/null +++ b/app/Console/Commands/NewAddressBookSubscription.php @@ -0,0 +1,62 @@ +option('email'))->firstOrFail(); + + $url = $this->option('url') ?? $this->ask('url', 'CardDAV url of the address book'); + $login = $this->option('login') ?? $this->ask('login', 'Login name'); + $password = $this->option('password') ?? $this->ask('password', 'User password'); + + try { + $addressBookSubscription = app(CreateAddressBookSubscription::class)->execute([ + 'account_id' => $user->account_id, + 'user_id' => $user->id, + 'base_uri' => $url, + 'username' => $login, + 'password' => $password, + ]); + } catch (\Exception $e) { + $this->error($e->getMessage()); + } + + if (! isset($addressBookSubscription)) { + $this->error('Could not add subscription'); + } else { + $this->info('Subscription added'); + SynchronizeAddressBooks::dispatch($addressBookSubscription, true); + } + } +} diff --git a/app/Console/Commands/OneTime/MoveAvatars.php b/app/Console/Commands/OneTime/MoveAvatars.php new file mode 100644 index 0000000..bc63aa6 --- /dev/null +++ b/app/Console/Commands/OneTime/MoveAvatars.php @@ -0,0 +1,137 @@ +confirmToProceed()) { + return; + } + + Contact::where('has_avatar', true) + ->chunk(200, function ($contacts) { + $this->handleContacts($contacts); + }); + } + + private function handleContacts($contacts) + { + foreach ($contacts as $contact) { + if ($contact->avatar_location == $this->newStorage()) { + continue; + } + + try { + $this->handleOneContact($contact); + } catch (FileNotFoundException $e) { + continue; + } + } + } + + private function handleOneContact($contact) + { + // move avatars to new location + $this->moveContactAvatars($contact); + + if (! $this->option('dryrun')) { + $contact->deleteAvatars(); + $this->line(' Files deleted from old location.', null, OutputInterface::VERBOSITY_VERBOSE); + + // Update location. The filename has not changed. + $contact->avatar_location = $this->newStorage(); + $contact->save(); + } + } + + private function moveContactAvatars($contact) + { + $this->line('Contact id:'.$contact->id.' | Avatar location:'.$contact->avatar_location.' | File name:'.$contact->avatar_file_name); + + $avatarFileNames = []; + array_push($avatarFileNames, $this->getFileName($contact)); + array_push($avatarFileNames, $this->getFileName($contact, 110)); + array_push($avatarFileNames, $this->getFileName($contact, 174)); + + $storage = Storage::disk($contact->avatar_location); + $newStorage = Storage::disk($this->newStorage()); + + foreach ($avatarFileNames as $avatarFileName) { + if ($newStorage->exists($avatarFileName)) { + $this->line(' File already pushed: '.$avatarFileName, null, OutputInterface::VERBOSITY_VERBOSE); + continue; + } + if (! $this->option('dryrun')) { + $avatarFile = $storage->get($avatarFileName); + $newStorage->put($avatarFileName, $avatarFile, config('filesystems.default_visibility')); + } + + $this->line(' File pushed: '.$avatarFileName, null, OutputInterface::VERBOSITY_VERBOSE); + } + } + + private function getFileName($contact, $size = null) + { + $filename = pathinfo($contact->avatar_file_name, PATHINFO_FILENAME); + $extension = pathinfo($contact->avatar_file_name, PATHINFO_EXTENSION); + + $avatarFileName = 'avatars/'.$filename.'.'.$extension; + if (! is_null($size)) { + $avatarFileName = 'avatars/'.$filename.'_'.$size.'.'.$extension; + } + + if ($this->fileExists($contact->avatar_location, $avatarFileName)) { + return $avatarFileName; + } + } + + private function fileExists($storage, $avatarFileName): bool + { + $storage = Storage::disk($storage); + + if (! $storage->exists($avatarFileName)) { + $this->line(' ! File not found: '.$avatarFileName, null, OutputInterface::VERBOSITY_VERBOSE); + throw new FileNotFoundException(); + } + + return true; + } + + private function newStorage() + { + return $this->option('storage') ?? config('filesystems.default'); + } +} diff --git a/app/Console/Commands/OneTime/MoveAvatarsToPhotosDirectory.php b/app/Console/Commands/OneTime/MoveAvatarsToPhotosDirectory.php new file mode 100644 index 0000000..5f46083 --- /dev/null +++ b/app/Console/Commands/OneTime/MoveAvatarsToPhotosDirectory.php @@ -0,0 +1,85 @@ +confirmToProceed()) { + return; + } + + Event::listen(MoveAvatarEvent::class, function ($event) { + $this->handleEvent($event->contact); + }); + + $delay = now(); + + Contact::where('has_avatar', true) + ->chunk(100, function ($contacts) use ($delay) { + foreach ($contacts as $contact) { + if ($contact->avatar_source === 'default') { + $this->handleContact($contact, $delay); + } + } + // add some delay, so we treat 100 contacts each minutes + $delay = $delay->addMinutes(1); + }); + } + + private function handleContact($contact, $delay) + { + try { + if ($this->option('dryrun')) { + MoveContactAvatarToPhotosDirectory::dispatchNow($contact, true); + } else { + MoveContactAvatarToPhotosDirectory::dispatch($contact, false) + ->delay($delay); + } + } catch (FileNotFoundException $e) { + $this->warn(' ! File not found: '.$e->fileName, OutputInterface::VERBOSITY_VERBOSE); + } + } + + private function handleEvent($contact) + { + $this->info('Contact id:'.$contact->id.' | Avatar location:'.$contact->avatar_location.' | File name:'.$contact->avatar_file_name); + } +} diff --git a/app/Console/Commands/Passport.php b/app/Console/Commands/Passport.php new file mode 100644 index 0000000..979e1c5 --- /dev/null +++ b/app/Console/Commands/Passport.php @@ -0,0 +1,73 @@ +confirmToProceed()) { + $this->checkEncryptionKeys(); + $this->checkPersonalAccessClient(); + } + } + + private function checkEncryptionKeys() + { + $this->info('Checking encryption keys...', OutputInterface::VERBOSITY_VERBOSE); + + if (! empty(config('passport.private_key')) && ! empty(config('passport.public_key'))) { + $this->info('✓ PASSPORT_PRIVATE_KEY and PASSPORT_PUBLIC_KEY detected.', OutputInterface::VERBOSITY_VERBOSE); + + return; + } + + if (file_exists(base_path('storage/oauth-private.key')) && file_exists(base_path('storage/oauth-public.key'))) { + $this->info('✓ Files storage/oauth-private.key and storage/oauth-public.key detected.', OutputInterface::VERBOSITY_VERBOSE); + + return; + } + + $this->artisan('✓ Creating encryption keys', 'passport:keys', ['--no-interaction']); + $this->warn('! Please be careful to backup '.base_path('storage/oauth-public.key').' and '.base_path('storage/oauth-private.key').' files !', OutputInterface::VERBOSITY_VERBOSE); + } + + private function checkPersonalAccessClient() + { + $this->info('Checking Personal Access Client...', OutputInterface::VERBOSITY_VERBOSE); + + if (PersonalAccessClient::count() > 0) { + $this->info('✓ Personal Access Client already created.', OutputInterface::VERBOSITY_VERBOSE); + + return; + } + + $this->artisan('✓ Creating personal access client', 'passport:client', ['--personal', '--no-interaction']); + } +} diff --git a/app/Console/Commands/PingVersionServer.php b/app/Console/Commands/PingVersionServer.php new file mode 100644 index 0000000..8944854 --- /dev/null +++ b/app/Console/Commands/PingVersionServer.php @@ -0,0 +1,113 @@ +confirmToProceed('Checking version deactivated', function () { + return $this->getLaravel()->environment() === 'production'; + })) { + return false; + } + + $instance = Instance::first(); + $instance->current_version = config('monica.app_version'); + + if ($instance->current_version == '') { + Log::warning('Current instance version is not set, skipping version check.'); + + return; + } + + // Query version.monicahq.com + try { + $this->log('Call url: '.config('monica.weekly_ping_server_url')); + $response = Http::acceptJson() + ->post(config('monica.weekly_ping_server_url'), [ + 'uuid' => $instance->uuid, + 'version' => $instance->current_version, + 'contacts' => Contact::count(), + ]) + ->throw(); + } catch (RequestException $e) { + $this->error('Error calling "'.config('monica.weekly_ping_server_url').'": '.$e->getMessage()); + Log::error(__CLASS__.' Error calling "'.config('monica.weekly_ping_server_url').'": '.$e->getMessage(), [$e]); + + return; + } + + // Receive the JSON + $json = $response->json(); + + $this->log('instance version: '.$instance->current_version); + $currentVersion = $this->getVersion($instance->current_version); + + $this->log('current version: '.$json['latest_version']); + $latestVersion = $this->getVersion($json['latest_version']); + + if ($latestVersion > $currentVersion) { + $instance->latest_version = $json['latest_version']; + $instance->latest_release_notes = $json['notes']; + $instance->number_of_versions_since_current_version = $json['number_of_versions_since_user_version']; + } else { + $instance->latest_release_notes = null; + $instance->number_of_versions_since_current_version = null; + } + + $instance->save(); + } + + public function log($string) + { + $this->info($string, OutputInterface::VERBOSITY_VERBOSE); + } + + private function getVersion(string $version): ?Version + { + try { + return new Version($version); + } catch (\Exception $e) { + $this->error("Error parsing version '$version': ".$e->getMessage()); + } + + return null; + } +} diff --git a/app/Console/Commands/SendReminders.php b/app/Console/Commands/SendReminders.php new file mode 100644 index 0000000..5169d17 --- /dev/null +++ b/app/Console/Commands/SendReminders.php @@ -0,0 +1,56 @@ +addDays(2)) + ->orderBy('planned_date', 'asc') + ->chunk(500, function ($reminderOutboxes) { + $this->send($reminderOutboxes); + }); + } + + /** + * Send the reminder to the user and schedule the future. + * + * @return void + */ + private function send($reminderOutboxes) + { + foreach ($reminderOutboxes as $reminderOutbox) { + if ($reminderOutbox->user->isTheRightTimeToBeReminded($reminderOutbox->planned_date)) { + NotifyUserAboutReminder::dispatch($reminderOutbox); + } + } + } +} diff --git a/app/Console/Commands/SendStayInTouch.php b/app/Console/Commands/SendStayInTouch.php new file mode 100644 index 0000000..6771d98 --- /dev/null +++ b/app/Console/Commands/SendStayInTouch.php @@ -0,0 +1,47 @@ +addDays(2)) + ->whereNotNull('stay_in_touch_frequency') + ->orderBy('stay_in_touch_trigger_date', 'asc') + ->chunk(500, function ($contacts) { + $this->schedule($contacts); + }); + } + + private function schedule($contacts) + { + foreach ($contacts as $contact) { + ScheduleStayInTouch::dispatch($contact); + } + } +} diff --git a/app/Console/Commands/SendTestEmail.php b/app/Console/Commands/SendTestEmail.php new file mode 100644 index 0000000..f08fbd7 --- /dev/null +++ b/app/Console/Commands/SendTestEmail.php @@ -0,0 +1,64 @@ +option('email'); + + // if no email was passed to the option, prompt the user to enter the email + if (! $email) { + $email = (string) $this->ask('What email address should I send the test email to?'); + } + + // Validate user provided email address + if (filter_var($email, FILTER_VALIDATE_EMAIL) === false) { + $this->error("Invalid email address: \"$email\"."); + + return -1; + } + + $this->info("Preparing and sending email to \"$email\""); + + // immediately deliver the test email (bypassing the queue) + Mail::raw( + "Hi $email, you requested a test email from Monica.", + function ($message) use ($email) { + $message->to($email) + ->subject('Monica email delivery test'); + } + ); + + $this->info('Email sent!'); + + return 0; + } +} diff --git a/app/Console/Commands/SentryRelease.php b/app/Console/Commands/SentryRelease.php new file mode 100644 index 0000000..e7f1f5e --- /dev/null +++ b/app/Console/Commands/SentryRelease.php @@ -0,0 +1,135 @@ +check()) { + return; + } + + if ($this->confirmToProceed()) { + $this->install_dir = env('SENTRY_ROOT', getenv('HOME').'/.local/bin'); + + $release = $this->option('release') ?? config('sentry.release'); + $commit = $this->option('commit') ?? + (is_dir(__DIR__.'/../../../.git') ? trim(exec('git log --pretty="%H" -n1 HEAD')) : $release); + + // Sentry update + $this->exec('Update sentry', $this->getSentryCli().' update'); + + // Create a release + $this->execSentryCli('Create a release', 'releases new '.$release.' --finalize --project '.config('sentry-release.project')); + + // Associate commits with the release + $this->execSentryCli('Associate commits with the release', 'releases set-commits '.$release.' --commit "'.config('sentry-release.repo').'@'.$commit.'"'); + + // Create a deploy + $this->execSentryCli('Create a deploy', 'releases deploys '.$release.' new --env '.$this->option('environment').' --name '.config('monica.app_version')); + + if ($this->option('store-release')) { + // Set sentry release + $this->line('Store release in config/.release file', null, OutputInterface::VERBOSITY_VERBOSE); + file_put_contents(__DIR__.'/../../../config/.release', $release); + } + } + } + + private function check(): bool + { + $check = true; + if (empty(config('sentry-release.auth_token'))) { + $this->error('You must provide an auth_token (SENTRY_AUTH_TOKEN)'); + $check = false; + } + if (empty(config('sentry-release.organisation'))) { + $this->error('You must provide an organisation slug (SENTRY_ORG)'); + $check = false; + } + if (empty(config('sentry-release.project'))) { + $this->error('You must set the project (SENTRY_PROJECT)'); + $check = false; + } + if (empty(config('sentry-release.repo'))) { + $this->error('You must set the repository (SENTRY_REPO)'); + $check = false; + } + if (empty($this->option('environment'))) { + $this->error('No environment given'); + $check = false; + } + + return $check; + } + + private function getSentryCli() + { + if (! file_exists($this->install_dir.'/'.self::SENTRY_CLI)) { + mkdir($this->install_dir, 0777, true); + $this->exec('Downloading sentry-cli', 'curl -sL '.self::SENTRY_URL.' | INSTALL_DIR='.$this->install_dir.' bash'); + } + + return $this->install_dir.'/'.self::SENTRY_CLI; + } + + private function execSentryCli($message, $command) + { + $this->exec($message, $this->getSentryCli().' '.$command); + } +} diff --git a/app/Console/Commands/SetPremiumAccount.php b/app/Console/Commands/SetPremiumAccount.php new file mode 100644 index 0000000..39eb787 --- /dev/null +++ b/app/Console/Commands/SetPremiumAccount.php @@ -0,0 +1,36 @@ +argument('accountId')); + $account->update([ + 'has_access_to_paid_version_for_free' => true, + ]); + } +} diff --git a/app/Console/Commands/SetUserAdmin.php b/app/Console/Commands/SetUserAdmin.php new file mode 100644 index 0000000..b2dd5c0 --- /dev/null +++ b/app/Console/Commands/SetUserAdmin.php @@ -0,0 +1,74 @@ +option('email'); + + // if no email was passed to the option, prompt the user to enter the email + if (! $email) { + $email = $this->ask('What is the user’s email?'); + } + + // retrieve the user with the specified email + $user = User::where('email', $email)->first(); + + if (! $user) { + // show an error and exist if the user does not exist + $this->error('No user with that email.'); + + return; + } + + // Print a warning + if ($user->admin) { + $this->warn($user->email.' will be removed from the administrators of this instance'); + } else { + $this->warn($user->email.' will be added to the administrators of this instance'); + } + + // ask for confirmation if not forced + if (! $this->option('force') && ! $this->confirm('Do you wish to continue?')) { + return; + } + + // toglle admin status + $user->admin = ! $user->admin; + $user->save(); + + // Show new status + if ($user->admin) { + $this->info($user->email.' has been added to the administrators of this instance'); + } else { + $this->info($user->email.' has been removed from the administrators of this instance'); + } + } +} diff --git a/app/Console/Commands/SetupProduction.php b/app/Console/Commands/SetupProduction.php new file mode 100644 index 0000000..1a309bc --- /dev/null +++ b/app/Console/Commands/SetupProduction.php @@ -0,0 +1,82 @@ +option('force')) && (! $this->confirm('You are about to setup and configure Monica. Do you wish to continue?'))) { + return; + } + + /* + * If the .env file does not exist, then key generation + * will fail. So we create one if it does not already exist. + */ + if (! file_exists(__DIR__.'/../../../.env')) { + touch(__DIR__.'/../../../.env'); + } + + $this->call('monica:update', ['--force' => true]); + + if (! $this->option('skipSeed')) { + $this->line('✓ Filling database'); + $this->call('db:seed', ['--force' => true]); + } + + $this->line(''); + $this->line('-----------------------------'); + $this->line('|'); + $this->line('| Welcome to Monica v'.config('monica.app_version')); + $this->line('|'); + $this->line('-----------------------------'); + + $email = $this->option('email'); + $password = $this->option('password'); + if (! empty($email) && ! empty($password)) { + Account::createDefault('John', 'Doe', $email, $password); + + $this->info('| You can now sign in to your account:'); + $this->line('| username: '.$email); + $this->line('| password: '); + } elseif (InstanceHelper::hasAtLeastOneAccount()) { + $this->info('| You can now log in to your account'); + } else { + $this->info('| You can now register to the first account by opening the application:'); + } + + $this->line('| URL: '.config('app.url')); + $this->line('-----------------------------'); + + $this->info('Setup is done. Have fun.'); + } +} diff --git a/app/Console/Commands/SetupTest.php b/app/Console/Commands/SetupTest.php new file mode 100644 index 0000000..3d637f4 --- /dev/null +++ b/app/Console/Commands/SetupTest.php @@ -0,0 +1,677 @@ +confirm('Are you sure you want to proceed? This will delete ALL data in your environment.')) { + return; + } + + $this->artisan('✓ Performing migrations', 'migrate:fresh'); + + $this->artisan('✓ Symlink the storage folder', 'storage:link'); + + if (! $this->option('skipSeed')) { + $this->numberOfContacts = $this->ask('How many contacts would you like to have in this test account?'); + $this->info('✓ Filling database with fake data'); + $this->seed(); + } + + $this->line(''); + $this->line('-----------------------------'); + $this->line('|'); + $this->line('| Welcome to Monica v'.config('monica.app_version')); + $this->line('|'); + $this->line('-----------------------------'); + $this->info('| You can now sign in to your account:'); + $this->line('| username: admin@admin.com'); + $this->line('| password: admin0'); + $this->line('| URL: '.config('app.url')); + $this->line('-----------------------------'); + + $this->info('Setup is done. Have fun.'); + } + + public function exec($message, $command) + { + $this->info($message); + $this->line($command); + exec($command, $output); + $this->line(implode('\n', $output)); + $this->line(''); + } + + public function artisan($message, $command, array $arguments = []) + { + $this->info($message); + $this->line(Application::formatCommandString($command)); + $this->callSilent($command, $arguments); + $this->line(''); + } + + /** + * Run the database seeds. + * + * @return void + */ + public function seed() + { + $this->setUpFaker(); + + // Get or create the first account + if (User::where('email', 'admin@admin.com')->exists()) { + $this->user = User::where('email', 'admin@admin.com')->first(); + $userId = $this->user->value('id'); + $this->account = Account::where('id', $userId)->first(); + } else { + $this->account = Account::createDefault('John', 'Doe', 'admin@admin.com', 'admin0'); + + // set default admin account to confirmed + /** @var User */ + $adminUser = $this->account->users()->first(); + $this->confirmUser($adminUser); + $this->user = $adminUser; + } + + // create a random number of contacts + //$this->numberOfContacts = rand(60, 100); + echo 'Generating '.$this->numberOfContacts.' fake contacts'.PHP_EOL; + + $output = new ConsoleOutput(); + $progress = new ProgressBar($output, $this->numberOfContacts); + $progress->start(); + + for ($i = 0; $i < $this->numberOfContacts; $i++) { + $gender = (rand(1, 2) == 1) ? 'male' : 'female'; + + $this->contact = app(CreateContact::class)->execute([ + 'account_id' => $this->account->id, + 'author_id' => $this->user->id, + 'first_name' => $this->faker->firstName($gender), + 'last_name' => (rand(1, 2) == 1) ? $this->faker->lastName : null, + 'nickname' => (rand(1, 2) == 1) ? $this->faker->name : null, + 'gender_id' => $this->getRandomGender()->id, + 'is_partial' => false, + 'is_birthdate_known' => false, + 'is_deceased' => false, + 'is_deceased_date_known' => false, + ]); + + $this->populateTags(); + $this->populateFoodPreferences(); + $this->populateDeceasedDate(); + $this->populateBirthday(); + $this->populateFirstMetInformation(); + $this->populateRelationships(); + $this->populateNotes(); + $this->populateActivities(); + $this->populateTasks(); + $this->populateDebts(); + $this->populateCalls(); + $this->populateConversations(); + $this->populateLifeEvents(); + $this->populateGifts(); + $this->populateAddresses(); + $this->populateContactFields(); + $this->populatePets(); + $this->changeUpdatedAt(); + + $progress->advance(); + } + + $this->populateDayRatings(); + $this->populateEntries(); + + $progress->finish(); + + // create the second test, blank account + if (! User::where('email', 'blank@blank.com')->exists()) { + $blankAccount = Account::createDefault('Blank', 'State', 'blank@blank.com', 'blank0'); + $blankUser = $blankAccount->users()->first(); + $this->confirmUser($blankUser); + } + } + + public function populateTags() + { + if (rand(1, 2) == 1) { + $i = 0; + do { + app(AssociateTag::class)->execute([ + 'account_id' => $this->account->id, + 'contact_id' => $this->contact->id, + 'name' => $this->faker->word, + ]); + $i++; + } while ($i < 10); + } + } + + public function populateFoodPreferences() + { + // add food preferences + if (rand(1, 2) == 1) { + $this->contact->food_preferences = $this->faker->realText(); + $this->contact->save(); + } + } + + public function populateDeceasedDate() + { + // deceased? + if (rand(1, 7) == 1) { + $birthdate = $this->faker->dateTimeThisCentury(); + + app(UpdateDeceasedInformation::class)->execute([ + 'account_id' => $this->account->id, + 'contact_id' => $this->contact->id, + 'is_deceased' => rand(1, 2) == 1, + 'is_date_known' => rand(1, 2) == 1, + 'day' => (int) $birthdate->format('d'), + 'month' => (int) $birthdate->format('m'), + 'year' => (int) $birthdate->format('Y'), + 'add_reminder' => rand(1, 2) == 1, + ]); + } + } + + public function populateBirthday() + { + if (rand(1, 2) == 1) { + $birthdate = $this->faker->dateTimeThisCentury(); + + app(UpdateBirthdayInformation::class)->execute([ + 'account_id' => $this->account->id, + 'contact_id' => $this->contact->id, + 'is_date_known' => rand(1, 2) == 1, + 'day' => (int) $birthdate->format('d'), + 'month' => (int) $birthdate->format('m'), + 'year' => (int) $birthdate->format('Y'), + 'is_age_based' => rand(1, 2) == 1, + 'age' => rand(1, 99), + 'add_reminder' => rand(1, 2) == 1, + 'is_deceased' => $this->contact->is_dead, + ]); + } + } + + public function populateFirstMetInformation() + { + if (rand(1, 2) == 1) { + $this->contact->first_met_where = $this->faker->realText(20); + } + + if (rand(1, 2) == 1) { + $this->contact->first_met_additional_info = $this->faker->realText(20); + $firstMetDate = $this->faker->dateTimeThisCentury(); + + if (rand(1, 2) == 1) { + // add a date where we don't know the year + $specialDate = $this->contact->setSpecialDate('first_met', 0, intval($firstMetDate->format('m')), intval($firstMetDate->format('d'))); + } else { + // add a date where we know the year + $specialDate = $this->contact->setSpecialDate('first_met', intval($firstMetDate->format('Y')), intval($firstMetDate->format('m')), intval($firstMetDate->format('d'))); + } + app(CreateReminder::class)->execute([ + 'account_id' => $this->account->id, + 'contact_id' => $this->contact->id, + 'initial_date' => $specialDate->date->toDateString(), + 'frequency_type' => 'year', + 'frequency_number' => 1, + 'title' => trans( + 'people.introductions_reminder_title', + ['name' => $this->contact->first_name] + ), + ]); + } + + if (rand(1, 2) == 1) { + do { + $rand = rand(1, $this->numberOfContacts); + } while (in_array($rand, [$this->contact->id])); + + $this->contact->first_met_through_contact_id = $rand; + } + + $this->contact->save(); + } + + public function populateRelationships() + { + if (rand(1, 2) == 1) { + foreach (range(1, rand(2, 6)) as $index) { + $gender = (rand(1, 2) == 1) ? 'male' : 'female'; + + $relatedContact = app(CreateContact::class)->execute([ + 'account_id' => $this->account->id, + 'author_id' => $this->user->id, + 'first_name' => $this->faker->firstName($gender), + 'last_name' => (rand(1, 2) == 1) ? $this->faker->lastName : null, + 'nickname' => (rand(1, 2) == 1) ? $this->faker->name : null, + 'gender_id' => $this->getRandomGender()->id, + 'is_partial' => rand(1, 2) == 1, + 'is_birthdate_known' => false, + 'is_deceased' => false, + 'is_deceased_date_known' => false, + ]); + + $relatedContactBirthDate = $this->faker->dateTimeThisCentury(); + app(UpdateBirthdayInformation::class)->execute([ + 'account_id' => $this->account->id, + 'contact_id' => $relatedContact->id, + 'is_date_known' => rand(1, 2) == 1, + 'day' => (int) $relatedContactBirthDate->format('d'), + 'month' => (int) $relatedContactBirthDate->format('m'), + 'year' => (int) $relatedContactBirthDate->format('Y'), + 'is_age_based' => rand(1, 2) == 1, + 'age' => rand(1, 99), + 'add_reminder' => rand(1, 2) == 1, + 'is_deceased' => $relatedContact->is_dead, + ]); + + // set relationship + $relationshipId = $this->contact->account->relationshipTypes->random()->id; + $relationship = app(CreateRelationship::class)->execute([ + 'account_id' => $this->account->id, + 'contact_is' => $this->contact->id, + 'of_contact' => $relatedContact->id, + 'relationship_type_id' => $relationshipId, + ]); + } + } + } + + public function populateNotes() + { + if (rand(1, 2) == 1) { + for ($j = 0; $j < rand(1, 13); $j++) { + $note = $this->contact->notes()->create([ + 'body' => $this->faker->realText(rand(40, 500)), + 'account_id' => $this->account->id, + 'is_favorited' => rand(1, 3) == 1, + 'favorited_at' => $this->faker->dateTimeThisCentury(), + ]); + } + } + } + + public function populateActivities() + { + if (rand(1, 2) == 1) { + for ($j = 0; $j < rand(1, 13); $j++) { + $date = DateHelper::getDate(Carbon::instance($this->faker->dateTimeThisYear($max = 'now'))); + + $request = [ + 'account_id' => $this->account->id, + 'activity_type_id' => rand(1, 13), + 'summary' => $this->faker->realText(rand(40, 100)), + 'description' => (rand(1, 2) == 1 ? $this->faker->realText(rand(100, 1000)) : null), + 'happened_at' => $date, + 'contacts' => [$this->contact->id], + ]; + + $activity = app(CreateActivity::class)->execute($request); + + $request = [ + 'account_id' => $this->account->id, + 'activity_id' => $activity->id, + 'contacts' => [$this->contact->id], + ]; + + app(AttachContactToActivity::class)->execute($request); + + DB::table('journal_entries')->insertGetId([ + 'account_id' => $this->account->id, + 'date' => $date, + 'journalable_id' => $activity->id, + 'journalable_type' => 'App\Models\Account\Activity', + ]); + } + } + } + + public function populateTasks() + { + if (rand(1, 2) == 1) { + for ($j = 0; $j < rand(1, 10); $j++) { + $task = $this->contact->tasks()->create([ + 'title' => $this->faker->realText(rand(40, 100)), + 'description' => $this->faker->realText(rand(100, 1000)), + 'completed' => (rand(1, 2) == 1 ? 0 : 1), + 'completed_at' => (rand(1, 2) == 1 ? $this->faker->dateTimeThisCentury() : null), + 'account_id' => $this->account->id, + ]); + } + } + } + + public function populateDebts() + { + if (rand(1, 2) == 1) { + for ($j = 0; $j < rand(1, 6); $j++) { + $this->contact->debts()->create([ + 'in_debt' => (rand(1, 2) == 1 ? 'yes' : 'no'), + 'amount' => rand(321, 39391), + 'reason' => $this->faker->realText(rand(100, 1000)), + 'status' => 'inprogress', + 'account_id' => $this->account->id, + ]); + } + } + } + + public function populateGifts() + { + if (rand(1, 2) == 1) { + for ($j = 0; $j < rand(1, 31); $j++) { + app(CreateGift::class)->execute([ + 'account_id' => $this->account->id, + 'contact_id' => $this->contact->id, + 'status' => (rand(1, 3) == 1 ? 'offered' : 'idea'), + 'name' => $this->faker->realText(rand(10, 100)), + 'comment' => $this->faker->realText(rand(1000, 5000)), + 'url' => $this->faker->url, + 'amount' => rand(12, 120), + ]); + } + } + } + + public function populateAddresses() + { + if (rand(1, 3) == 1) { + $request = [ + 'account_id' => $this->account->id, + 'contact_id' => $this->contact->id, + 'country' => $this->getRandomCountry(), + 'name' => $this->faker->word, + 'street' => (rand(1, 3) == 1) ? $this->faker->streetAddress : null, + 'city' => (rand(1, 3) == 1) ? $this->faker->city : null, + 'province' => (rand(1, 3) == 1) ? $this->faker->state : null, + 'postal_code' => (rand(1, 3) == 1) ? $this->faker->postcode : null, + ]; + + app(CreateAddress::class)->execute($request); + } + } + + private function getRandomCountry() + { + if ($this->countries == null) { + $this->countries = CountriesHelper::getAll(); + } + + return $this->countries->random()['id']; + } + + public function populateContactFields() + { + if (rand(1, 3) == 1) { + + // Fetch number of types + $numberOfTypes = ContactFieldType::where('account_id', $this->account->id)->count(); + + for ($j = 0; $j < rand(1, $numberOfTypes); $j++) { + // Retrieve random ContactFieldType + $contactFieldType = ContactFieldType::where('account_id', $this->account->id)->orderBy(DB::raw('RAND()'))->firstOrFail(); + + // Fake data according to type + $data = null; + switch ($contactFieldType->name) { + case 'Email': + $data = $this->faker->email; + break; + case 'Phone': + $data = $this->faker->phoneNumber; + break; + case 'Facebook': + $data = 'https://facebook.com/'.$this->faker->userName; + break; + case 'Twitter': + $data = 'https://twitter.com/'.$this->faker->userName; + break; + case 'Whatsapp': + $data = $this->faker->phoneNumber; + break; + case 'Telegram': + $data = $this->faker->phoneNumber; + break; + default: + $data = $this->faker->url; + break; + } + + $this->contact->contactFields()->create([ + 'contact_field_type_id' => $contactFieldType->id, + 'data' => $data, + 'account_id' => $this->account->id, + ]); + } + } + } + + public function populateEntries() + { + for ($j = 0; $j < rand(10, 100); $j++) { + $date = $this->faker->dateTimeThisYear(); + + $entryId = DB::table('entries')->insertGetId([ + 'account_id' => $this->account->id, + 'title' => $this->faker->realText(rand(12, 20)), + 'post' => $this->faker->realText(rand(400, 500)), + 'created_at' => $date, + ]); + + DB::table('journal_entries')->insertGetId([ + 'account_id' => $this->account->id, + 'date' => $date, + 'journalable_id' => $entryId, + 'journalable_type' => 'App\Models\Journal\Entry', + 'created_at' => now(), + ]); + } + } + + public function populatePets() + { + if (rand(1, 3) == 1) { + for ($j = 0; $j < rand(1, 3); $j++) { + $date = $this->faker->dateTimeThisYear(); + + DB::table('pets')->insertGetId([ + 'account_id' => $this->account->id, + 'contact_id' => $this->contact->id, + 'pet_category_id' => rand(1, 11), + 'name' => (rand(1, 3) == 1) ? $this->faker->firstName : null, + 'created_at' => $date, + ]); + } + } + } + + public function populateDayRatings() + { + for ($j = 0; $j < rand(10, 100); $j++) { + $date = $this->faker->dateTimeThisYear(); + + $dayId = DB::table('days')->insertGetId([ + 'account_id' => $this->account->id, + 'rate' => rand(1, 3), + 'date' => $date, + 'created_at' => $date, + ]); + + DB::table('journal_entries')->insertGetId([ + 'account_id' => $this->account->id, + 'date' => $date, + 'journalable_id' => $dayId, + 'journalable_type' => 'App\Models\Journal\Day', + 'created_at' => now(), + ]); + } + } + + public function changeUpdatedAt() + { + $this->contact->last_consulted_at = Carbon::instance($this->faker->dateTimeThisYear()); + $this->contact->save(); + } + + public function populateCalls() + { + if (rand(1, 3) == 1) { + $this->contact->calls()->create([ + 'account_id' => $this->account->id, + 'called_at' => $this->faker->dateTimeThisYear(), + ]); + } + } + + public function populateConversations() + { + if (rand(1, 3) == 1) { + for ($j = 0; $j < rand(1, 20); $j++) { + $contactFieldType = ContactFieldType::where('account_id', $this->account->id)->orderBy(DB::raw('RAND()'))->firstOrFail(); + + $conversation = app(CreateConversation::class)->execute([ + 'happened_at' => $this->faker->dateTimeThisCentury(), + 'contact_id' => $this->contact->id, + 'contact_field_type_id' => $contactFieldType->id, + 'account_id' => $this->account->id, + ]); + + for ($k = 0; $k < rand(1, 20); $k++) { + app(AddMessageToConversation::class)->execute([ + 'account_id' => $this->account->id, + 'contact_id' => $this->contact->id, + 'conversation_id' => $conversation->id, + 'written_at' => $this->faker->dateTimeThisCentury(), + 'written_by_me' => (rand(1, 2) == 1), + 'content' => $this->faker->realText(), + ]); + } + } + } + } + + public function populateLifeEvents() + { + if (rand(1, 3) == 1) { + for ($j = 0; $j < rand(1, 20); $j++) { + $lifeEventType = LifeEventType::where('account_id', $this->account->id)->orderBy(DB::raw('RAND()'))->firstOrFail(); + + app(CreateLifeEvent::class)->execute([ + 'account_id' => $this->account->id, + 'contact_id' => $this->contact->id, + 'life_event_type_id' => $lifeEventType->id, + 'happened_at' => $this->faker->dateTimeThisCentury(), + 'name' => $this->faker->realText(), + 'note' => $this->faker->realText(), + 'has_reminder' => false, + 'happened_at_month_unknown' => false, + 'happened_at_day_unknown' => false, + ]); + } + } + } + + public function getRandomGender() + { + return $this->account->genders->random(); + } + + public function confirmUser($user) + { + $user->markEmailAsVerified(); + } +} diff --git a/app/Console/Commands/Tests/SetupFrontEndTestUser.php b/app/Console/Commands/Tests/SetupFrontEndTestUser.php new file mode 100644 index 0000000..9ce815c --- /dev/null +++ b/app/Console/Commands/Tests/SetupFrontEndTestUser.php @@ -0,0 +1,45 @@ +setConnection($this->option('database')); + + $user = factory(User::class)->create(); + $user->account->populateDefaultFields(); + + app(AcceptPolicy::class)->execute([ + 'account_id' => $user->account->id, + 'user_id' => $user->id, + 'ip_address' => null, + ]); + + $this->info($user->getKey()); + } +} diff --git a/app/Console/Commands/Tests/UpdateLangs.php b/app/Console/Commands/Tests/UpdateLangs.php new file mode 100644 index 0000000..074ae9d --- /dev/null +++ b/app/Console/Commands/Tests/UpdateLangs.php @@ -0,0 +1,64 @@ +replace('-', '_'); + switch ($lang) { + case 'zh': + $locale = 'zh_CN'; + break; + case 'en': + continue 2; + } + if (File::exists($orig_path = lang_path("$lang.json")) + && File::exists($trans_path = base_path("vendor/laravel-lang/lang/locales/$locale/$locale.json"))) { + $lang_orig = json_decode(File::get($orig_path), true); + $lang_trans = json_decode(File::get($trans_path), true); + foreach ($en as $key) { + $lang_orig[$key] = $lang_trans[$key]; + } + File::put($orig_path, json_encode($lang_orig, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)."\n"); + } + } + } +} diff --git a/app/Console/Commands/Update.php b/app/Console/Commands/Update.php new file mode 100644 index 0000000..83eb973 --- /dev/null +++ b/app/Console/Commands/Update.php @@ -0,0 +1,116 @@ +confirmToProceed()) { + try { + $this->artisan('✓ Maintenance mode: on', 'down', [ + '--retry' => '10', + ]); + + // Clear or rebuild all cache + if (config('cache.default') != 'database' || Schema::hasTable(config('cache.stores.database.table'))) { + $this->artisan('✓ Resetting application cache', 'cache:clear'); + } + + if ($this->getLaravel()->environment() == 'production') { + $this->artisan('✓ Clear config cache', 'config:clear'); + $this->artisan('✓ Resetting route cache', 'route:cache'); + if ($this->getLaravel()->version() > '5.6') { + $this->artisan('✓ Resetting view cache', 'view:cache'); + } else { + $this->artisan('✓ Resetting view cache', 'view:clear'); + } + } else { + $this->artisan('✓ Clear config cache', 'config:clear'); + $this->artisan('✓ Clear route cache', 'route:clear'); + $this->artisan('✓ Clear view cache', 'view:clear'); + } + + if ($this->option('composer-install') === true) { + $this->exec('✓ Updating composer dependencies', 'composer install --no-interaction'.($this->option('dev') === false ? ' --no-dev' : '')); + } + + if ($this->option('skip-storage-link') !== true && $this->getLaravel()->environment() != 'testing' && ! file_exists(public_path('storage'))) { + $this->artisan('✓ Symlink the storage folder', 'storage:link'); + } + + if ($this->migrateCollationTest()) { + $this->artisan('✓ Performing collation migrations', 'migrate:collation', ['--force']); + } + + $this->artisan('✓ Performing migrations', 'migrate', ['--force']); + + $this->artisan('✓ Check for encryption keys', 'monica:passport', ['--force']); + + $this->artisan('✓ Ping for new version', 'monica:ping', ['--force']); + + // Cache config + if ($this->getLaravel()->environment() == 'production' + && (config('cache.default') != 'database' || Schema::hasTable(config('cache.stores.database.table')))) { + $this->artisan('✓ Cache configuraton', 'config:cache'); + } + } finally { + $this->artisan('✓ Maintenance mode: off', 'up'); + } + + $this->line('Monica v'.config('monica.app_version').' is set up, enjoy.'); + } + } + + private function migrateCollationTest() + { + $connection = DBHelper::connection(); + + if ($connection->getDriverName() != 'mysql') { + return false; + } + + $databasename = $connection->getDatabaseName(); + + $schemata = DB::select( + 'select DEFAULT_CHARACTER_SET_NAME from information_schema.schemata where schema_name = ?', + [$databasename] + ); + + $schema = $schemata[0]->DEFAULT_CHARACTER_SET_NAME; + + return config('database.use_utf8mb4') && $schema == 'utf8' + || ! config('database.use_utf8mb4') && $schema == 'utf8mb4'; + } +} diff --git a/app/Console/Commands/UpdateGravatars.php b/app/Console/Commands/UpdateGravatars.php new file mode 100644 index 0000000..a8dff1b --- /dev/null +++ b/app/Console/Commands/UpdateGravatars.php @@ -0,0 +1,33 @@ +load(__DIR__.'/Commands'); + $this->load(__DIR__.'/Commands/OneTime'); + + if ($this->app->environment() != 'production') { + $this->load(__DIR__.'/Commands/Tests'); + } + + require base_path('routes/console.php'); + } + + /** + * Define the application's command schedule. + * + * @param \Illuminate\Console\Scheduling\Schedule $schedule + * @return void + * @codeCoverageIgnore + */ + protected function schedule(Schedule $schedule) + { + $this->scheduleCommand($schedule, 'queue:prune-batches', 'daily'); + $this->scheduleCommand($schedule, 'send:reminders', 'hourly'); + $this->scheduleCommand($schedule, 'send:stay_in_touch', 'hourly'); + $this->scheduleCommand($schedule, 'monica:davclients', 'hourly'); + $this->scheduleCommand($schedule, 'monica:calculatestatistics', 'daily'); + $this->scheduleCommand($schedule, 'monica:ping', 'daily'); + $this->scheduleCommand($schedule, 'monica:clean', 'daily'); + $this->scheduleCommand($schedule, 'monica:updategravatars', 'weekly'); + if (config('app.cloudflare')) { + $this->scheduleCommand($schedule, 'cloudflare:reload', 'daily'); + } + $this->scheduleCommand($schedule, 'model:prune', 'daily'); + } + + /** + * Define a new schedule command with a frequency. + * + * @codeCoverageIgnore + */ + private function scheduleCommand(Schedule $schedule, string $command, $frequency) + { + $schedule->command($command)->when(function () use ($command, $frequency) { + $event = CronEvent::command($command); + if ($frequency) { + $event = $event->$frequency(); + } + + return $event->isDue(); + }); + } +} diff --git a/app/Console/Scheduling/CronEvent.php b/app/Console/Scheduling/CronEvent.php new file mode 100644 index 0000000..3245c77 --- /dev/null +++ b/app/Console/Scheduling/CronEvent.php @@ -0,0 +1,128 @@ +cron = $cron; + } + + /** + * Get the command. + * + * @param string $command + * @return self + */ + public static function command(string $command): self + { + /** @var \App\Models\Instance\Cron $cron */ + $cron = Cron::firstOrCreate(['command' => $command]); + + return new self($cron); + } + + /** + * Get current Cron. + * + * @return Cron + */ + public function cron() + { + return $this->cron; + } + + /** + * Run the command once per hour. + * + * @return self + */ + public function hourly(): self + { + $this->minutes = 60; + $this->days = 0; + + return $this; + } + + /** + * Run the command once per day. + * + * @return self + */ + public function daily(): self + { + $this->minutes = 0; + $this->days = 1; + + return $this; + } + + /** + * Run the command once a week. + * + * @return self + */ + public function weekly(): self + { + $this->minutes = 0; + $this->days = 7; + + return $this; + } + + /** + * Test if the command is due to run. + * + * @return bool + */ + public function isDue(): bool + { + $now = now(); + + if ($this->cron->last_run !== null) { + $t = $this->cron->last_run; + + if ($this->minutes !== 0) { + $next_run = Carbon::create($t->year, $t->month, $t->day, $t->hour, (int) floor($t->minute / $this->minutes) * $this->minutes, 0) + ->addMinutes($this->minutes); + } elseif ($this->days !== 0) { + $next_run = Carbon::create($t->year, $t->month, (int) floor($t->day / $this->days) * $this->days, 0, 0, 0) + ->addDays($this->days); + } + + if (! isset($next_run) || $next_run > $now) { + return false; + } + } + + $this->cron->update(['last_run' => $now]); + + return true; + } +} diff --git a/app/Events/Event.php b/app/Events/Event.php new file mode 100644 index 0000000..ba2f888 --- /dev/null +++ b/app/Events/Event.php @@ -0,0 +1,8 @@ +contact = $contact; + } +} diff --git a/app/Events/RecoveryLogin.php b/app/Events/RecoveryLogin.php new file mode 100644 index 0000000..d2b61c3 --- /dev/null +++ b/app/Events/RecoveryLogin.php @@ -0,0 +1,30 @@ +user = $user; + } +} diff --git a/app/Events/TokenDeleteEvent.php b/app/Events/TokenDeleteEvent.php new file mode 100644 index 0000000..fcf54fb --- /dev/null +++ b/app/Events/TokenDeleteEvent.php @@ -0,0 +1,31 @@ +token = $token; + } +} diff --git a/app/Exceptions/FileNotFoundException.php b/app/Exceptions/FileNotFoundException.php new file mode 100644 index 0000000..791e1d3 --- /dev/null +++ b/app/Exceptions/FileNotFoundException.php @@ -0,0 +1,30 @@ +fileName = $fileName; + parent::__construct(); + } + + public function __toString(): string + { + return 'File not found: '.$this->fileName; + } +} diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php new file mode 100644 index 0000000..432ebdc --- /dev/null +++ b/app/Exceptions/Handler.php @@ -0,0 +1,64 @@ +> + */ + protected $dontReport = [ + OAuthServerException::class, + WrongIdException::class, + ]; + + /** + * Register the exception handling callbacks for the application. + * + * @return void + * @codeCoverageIgnore + */ + public function register() + { + if (config('monica.sentry_support') && config('app.env') == 'production') { + $this->reportable(function (Throwable $e) { + if ($this->shouldReport($e) && app()->bound('sentry')) { + app('sentry')->captureException($e); + } + }); + } + } + + /** + * Render an exception into an HTTP response. + * + * @param \Illuminate\Http\Request $request + * @param \Throwable $e + * @return \Illuminate\Http\Response|\Symfony\Component\HttpFoundation\Response + */ + public function render($request, Throwable $e) + { + // hopefully catches those pesky token expiries + // and send them back to login. + if ($e instanceof TokenMismatchException) { + return redirect()->route('loginRedirect'); + } + + // Convert all non-http exceptions to a proper 500 http exception + // if we don't do this exceptions are shown as a default template + // instead of our own view in resources/views/errors/500.blade.php + if ($this->shouldReport($e) && ! $this->isHttpException($e) && ! config('app.debug')) { + $e = new HttpException(500, $e->getMessage()); + } + + return parent::render($request, $e); + } +} diff --git a/app/Exceptions/MissingEnvVariableException.php b/app/Exceptions/MissingEnvVariableException.php new file mode 100644 index 0000000..142ad8c --- /dev/null +++ b/app/Exceptions/MissingEnvVariableException.php @@ -0,0 +1,12 @@ + [ + User::countCollection($this->users), + Contact::countCollection($this->allContacts), + Relationship::countCollection($this->relationships), + Addressbook::countCollection($this->addressBooks), + AddressbookSubscription::countCollection($this->addressBookSubscriptions), + Photo::countCollection($this->photos), + Document::countCollection($this->documents), + Activity::countCollection($this->activities), + ], + 'properties' => [ + 'default_gender' => $this->when($this->default_gender_id !== null, function () { + $defaultGender = Gender::where(['account_id' => $this->id])->find($this->default_gender_id); + + return $defaultGender->uuid; + }), + 'journal_entries' => JournalEntry::collection($this->journalEntries()->entry()->get()), + 'modules' => Module::collection($this->modules), + 'reminder_rules' => ReminderRule::collection($this->reminderRules), + 'audit_logs' => AuditLog::collection($this->auditLogs), + ], + 'instance' => [ + 'activity_types' => ActivityType::collection($this->activityTypes), + 'activity_type_categories' => ActivityTypeCategory::collection($this->activityTypeCategories), + 'contact_field_types' => ContactFieldType::collection($this->contactFieldTypes), + 'genders' => GenderResource::collection($this->genders), + 'life_event_types' => LifeEventType::collection($this->lifeEventTypes), + 'life_event_categories' => LifeEventCategory::collection($this->lifeEventCategories), + ], + ]; + } +} diff --git a/app/ExportResources/Account/Activity.php b/app/ExportResources/Account/Activity.php new file mode 100644 index 0000000..149c188 --- /dev/null +++ b/app/ExportResources/Account/Activity.php @@ -0,0 +1,33 @@ + [ + $this->mergeWhen($this->type !== null, function () { + return [ + 'type' => $this->type->uuid, + ]; + }), + ], + ]; + } +} diff --git a/app/ExportResources/Account/ActivityType.php b/app/ExportResources/Account/ActivityType.php new file mode 100644 index 0000000..83b1cf9 --- /dev/null +++ b/app/ExportResources/Account/ActivityType.php @@ -0,0 +1,33 @@ + [ + $this->mergeWhen($this->category !== null, function () { + return [ + 'category' => $this->category->uuid, + ]; + }), + ], + ]; + } +} diff --git a/app/ExportResources/Account/ActivityTypeCategory.php b/app/ExportResources/Account/ActivityTypeCategory.php new file mode 100644 index 0000000..9f988e3 --- /dev/null +++ b/app/ExportResources/Account/ActivityTypeCategory.php @@ -0,0 +1,19 @@ + $this->user->uuid, + 'contacts' => $this->contacts->mapUuid(), + ]; + } +} diff --git a/app/ExportResources/Account/AddressbookSubscription.php b/app/ExportResources/Account/AddressbookSubscription.php new file mode 100644 index 0000000..5af8685 --- /dev/null +++ b/app/ExportResources/Account/AddressbookSubscription.php @@ -0,0 +1,44 @@ + [ + 'addressbook' => $this->addressBook->uuid, + 'sync_token' => $this->syncToken, + $this->merge(function () { + $localSyncToken = SyncToken::where('account_id', $this->account_id)->find($this->localSyncToken); + + return [ + 'local_sync_token' => new SyncTokenResource($localSyncToken), + ]; + }), + ], + ]; + } +} diff --git a/app/ExportResources/Account/LifeEventCategory.php b/app/ExportResources/Account/LifeEventCategory.php new file mode 100644 index 0000000..708541a --- /dev/null +++ b/app/ExportResources/Account/LifeEventCategory.php @@ -0,0 +1,27 @@ + [ + 'translation_key' => $this->default_life_event_category_key, + ], + ]; + } +} diff --git a/app/ExportResources/Account/LifeEventType.php b/app/ExportResources/Account/LifeEventType.php new file mode 100644 index 0000000..9b217d4 --- /dev/null +++ b/app/ExportResources/Account/LifeEventType.php @@ -0,0 +1,34 @@ + [ + 'translation_key' => $this->default_life_event_type_key, + $this->mergeWhen($this->lifeEventCategory !== null, function () { + return [ + 'category' => $this->lifeEventCategory->uuid, + ]; + }), + ], + ]; + } +} diff --git a/app/ExportResources/Account/Photo.php b/app/ExportResources/Account/Photo.php new file mode 100644 index 0000000..761ce82 --- /dev/null +++ b/app/ExportResources/Account/Photo.php @@ -0,0 +1,29 @@ + [ + 'dataUrl' => $this->dataUrl(), + ], + ]; + } +} diff --git a/app/ExportResources/Account/ReminderRule.php b/app/ExportResources/Account/ReminderRule.php new file mode 100644 index 0000000..3097bb1 --- /dev/null +++ b/app/ExportResources/Account/ReminderRule.php @@ -0,0 +1,18 @@ + [ + 'street' => $this->place->street, + 'city' => $this->place->city, + 'province' => $this->place->province, + 'postal_code' => $this->place->postal_code, + 'latitude' => $this->place->latitude, + 'longitude' => $this->place->longitude, + 'country' => $this->place->country, + ], + ]; + } +} diff --git a/app/ExportResources/Contact/Call.php b/app/ExportResources/Contact/Call.php new file mode 100644 index 0000000..c2d6444 --- /dev/null +++ b/app/ExportResources/Contact/Call.php @@ -0,0 +1,33 @@ + [ + $this->mergeWhen($this->emotions->count() > 0, [ + 'emotions' => $this->emotions->map(function ($emotion) { + return $emotion->name; + })->toArray(), + ]), + ], + ]; + } +} diff --git a/app/ExportResources/Contact/Contact.php b/app/ExportResources/Contact/Contact.php new file mode 100644 index 0000000..359ee79 --- /dev/null +++ b/app/ExportResources/Contact/Contact.php @@ -0,0 +1,106 @@ + [ + 'avatar' => [ + 'avatar_source' => $this->avatar_source, + 'avatar_gravatar_url' => $this->avatar_gravatar_url, + 'avatar_adorable_uuid' => $this->avatar_adorable_uuid, + 'avatar_default_url' => $this->avatar_default_url, + $this->mergeWhen($this->avatarPhoto !== null, function () { + return ['avatar_photo' => $this->avatarPhoto->uuid]; + }), + 'has_avatar' => $this->has_avatar, + 'avatar_external_url' => $this->avatar_external_url, + 'avatar_file_name' => $this->avatar_file_name, + 'avatar_location' => $this->avatar_location, + 'gravatar_url' => $this->gravatar_url, + 'default_avatar_color' => $this->default_avatar_color, + ], + 'tags' => $this->when($this->tags->count() > 0, function () { + return $this->tags->map(function ($tag) { + return $tag->name; + })->toArray(); + }), + $this->mergeWhen($this->gender !== null, function () { + return ['gender' => $this->gender->uuid]; + }), + $this->mergeWhen($this->birthdate, [ + 'birthdate' => new SpecialDate($this->birthdate), + ]), + $this->mergeWhen($this->deceasedDate, [ + 'deceased_date' => new SpecialDate($this->deceasedDate), + ]), + $this->mergeWhen($this->deceased_reminder_id, function () { + return ['deceased_reminder' => new Reminder(ContactReminder::find($this->deceased_reminder_id))]; + }), + $this->mergeWhen($this->firstMetDate, [ + 'first_met_date' => new SpecialDate($this->firstMetDate), + ]), + $this->mergeWhen($this->getIntroducer() !== null, function () { + return ['first_met_through' => $this->getIntroducer()->uuid]; + }), + $this->mergeWhen($this->first_met_reminder_id !== null, function () { + return ['first_met_reminder' => new Reminder(ContactReminder::find($this->first_met_reminder_id))]; + }), + ], + 'data' => [ + Call::countCollection($this->calls), + ContactField::countCollection($this->contactFields), + Debt::countCollection($this->debts), + Gift::countCollection($this->gifts), + Note::countCollection($this->notes), + Reminder::countCollection($this->reminders), + Task::countCollection($this->tasks), + Address::countCollection($this->addresses), + Pet::countCollection($this->pets), + Conversation::countCollection($this->conversations), + LifeEvent::countCollection($this->lifeEvents), + Activity::uuidCollection($this->activities), + Photo::uuidCollection($this->photos), + Document::uuidCollection($this->documents), + // Occupation::collection($this->occupations), + ], + ]; + } +} diff --git a/app/ExportResources/Contact/ContactField.php b/app/ExportResources/Contact/ContactField.php new file mode 100644 index 0000000..8c392c7 --- /dev/null +++ b/app/ExportResources/Contact/ContactField.php @@ -0,0 +1,29 @@ + [ + $this->mergeWhen($this->contactFieldType !== null, function () { + return ['type' => $this->contactFieldType->uuid]; + }), + ], + ]; + } +} diff --git a/app/ExportResources/Contact/ContactFieldType.php b/app/ExportResources/Contact/ContactFieldType.php new file mode 100644 index 0000000..b1e56d3 --- /dev/null +++ b/app/ExportResources/Contact/ContactFieldType.php @@ -0,0 +1,22 @@ + [ + $this->mergeWhen($this->contactFieldType !== null, function () { + return ['contact_field_type' => $this->contactFieldType->uuid]; + }), + 'messages' => Message::collection($this->messages), + ], + ]; + } +} diff --git a/app/ExportResources/Contact/Debt.php b/app/ExportResources/Contact/Debt.php new file mode 100644 index 0000000..5990944 --- /dev/null +++ b/app/ExportResources/Contact/Debt.php @@ -0,0 +1,29 @@ + [ + 'in_debt' => $this->in_debt === 'yes', + ], + ]; + } +} diff --git a/app/ExportResources/Contact/Document.php b/app/ExportResources/Contact/Document.php new file mode 100644 index 0000000..ef146cd --- /dev/null +++ b/app/ExportResources/Contact/Document.php @@ -0,0 +1,33 @@ + [ + $this->mergeWhen(($dataUrl = $this->dataUrl()) !== null, [ + 'dataUrl' => $dataUrl, + ]), + ], + ]; + } +} diff --git a/app/ExportResources/Contact/Gender.php b/app/ExportResources/Contact/Gender.php new file mode 100644 index 0000000..580485b --- /dev/null +++ b/app/ExportResources/Contact/Gender.php @@ -0,0 +1,19 @@ + [ + $this->mergeWhen($this->recipient !== null, function () { + return ['recipient' => $this->recipient->uuid]; + }), + $this->mergeWhen($this->photos->count() > 0, [ + 'photos' => $this->photos->mapUuid(), + ]), + ], + ]; + } +} diff --git a/app/ExportResources/Contact/LifeEvent.php b/app/ExportResources/Contact/LifeEvent.php new file mode 100644 index 0000000..4a997dc --- /dev/null +++ b/app/ExportResources/Contact/LifeEvent.php @@ -0,0 +1,32 @@ + [ + $this->mergeWhen($this->lifeEventType != null, function () { + return ['type' => $this->lifeEventType->uuid]; + }), + ], + ]; + } +} diff --git a/app/ExportResources/Contact/Message.php b/app/ExportResources/Contact/Message.php new file mode 100644 index 0000000..0c14ab8 --- /dev/null +++ b/app/ExportResources/Contact/Message.php @@ -0,0 +1,20 @@ + [ + $this->mergeWhen($this->petCategory !== null, function () { + return ['category' => $this->petCategory->name]; + }), + ], + ]; + } +} diff --git a/app/ExportResources/Contact/Reminder.php b/app/ExportResources/Contact/Reminder.php new file mode 100644 index 0000000..bdf12f1 --- /dev/null +++ b/app/ExportResources/Contact/Reminder.php @@ -0,0 +1,24 @@ + $this->count(), + 'type' => Str::of($this->collects)->afterLast('\\')->kebab()->replace('-', '_'), + 'values' => parent::toArray($request), + ]; + } +} diff --git a/app/ExportResources/ExportResource.php b/app/ExportResources/ExportResource.php new file mode 100644 index 0000000..e7f9a47 --- /dev/null +++ b/app/ExportResources/ExportResource.php @@ -0,0 +1,137 @@ +resource = $resource; + } + + /** + * Create a new anonymous resource collection. + * + * @param mixed $resource + * @return CountResourceCollection|MissingValue + */ + public static function countCollection($resource) + { + if ($resource->count() === 0) { + return new MissingValue(); + } + + return tap(new CountResourceCollection($resource, static::class), function ($collection) { + if (property_exists(static::class, 'preserveKeys')) { + $collection->preserveKeys = (new static([]))->preserveKeys === true; + } + }); + } + + /** + * Create a new anonymous resource collection. + * + * @param mixed $resource + * @return MapUuidResourceCollection|MissingValue + */ + public static function uuidCollection($resource) + { + if ($resource->count() === 0) { + return new MissingValue(); + } + + return tap(new MapUuidResourceCollection($resource, static::class), function ($collection) { + if (property_exists(static::class, 'preserveKeys')) { + $collection->preserveKeys = (new static([]))->preserveKeys === true; + } + }); + } + + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request $request + * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + if (is_null($this->resource)) { + return []; + } + + return is_array($this->resource) + ? $this->resource + : $this->export($this->columns, $this->properties, $this->data()); + } + + /** + * @return array|null + */ + public function data(): ?array + { + return null; + } + + /** + * Create the Insert query for the given table. + * + * @param array $columns + * @param array $properties + * @param array $data + * @return array|null + */ + protected function export(array $columns, array $properties = null, array $data = null): ?array + { + $result = []; + + if (! $this->resource->exists()) { + return null; + } + + foreach ($columns as $column) { + $result[$column] = $this->{$column}; + } + + if ($data !== null) { + foreach ($data as $key => $value) { + if (isset($result[$key]) && is_array($result[$key])) { + $result[$key] = array_merge($result[$key], $value); + } else { + $result[$key] = $value; + } + } + } + + if ($properties !== null) { + $result['properties'] = array_merge(collect($properties)->mapWithKeys(function ($item, $key) { + return ($value = $this->{$item}) !== null ? [$item => $value] : new MissingValue(); + })->toArray(), Arr::get($result, 'properties', [])); + } + + return $result; + } +} diff --git a/app/ExportResources/Instance/AuditLog.php b/app/ExportResources/Instance/AuditLog.php new file mode 100644 index 0000000..4fa5aad --- /dev/null +++ b/app/ExportResources/Instance/AuditLog.php @@ -0,0 +1,35 @@ + [ + 'author' => $this->when($this->author !== null, function () { + return $this->author->uuid; + }), + 'contact' => $this->when($this->contact !== null, function () { + return $this->contact->uuid; + }), + ], + ]; + } +} diff --git a/app/ExportResources/Instance/Emotion/Emotion.php b/app/ExportResources/Instance/Emotion/Emotion.php new file mode 100644 index 0000000..d3e3b82 --- /dev/null +++ b/app/ExportResources/Instance/Emotion/Emotion.php @@ -0,0 +1,27 @@ + [ + 'primary' => $this->primary->name, + 'secondary' => $this->secondary->name, + ], + ]; + } +} diff --git a/app/ExportResources/Instance/SpecialDate.php b/app/ExportResources/Instance/SpecialDate.php new file mode 100644 index 0000000..7331773 --- /dev/null +++ b/app/ExportResources/Instance/SpecialDate.php @@ -0,0 +1,17 @@ +getObjectData(); + if ($data !== null) { + switch ($data['type']) { + case 'entry': + return [ + 'uuid' => $this->journalable->uuid, + 'properties' => [ + 'type' => $data['type'], + 'title' => $data['title'], + 'post' => $data['post'], + 'date' => $data['date'], + ], + ]; + case 'day': + return [ + 'uuid' => $this->journalable->uuid, + 'properties' => [ + 'type' => $data['type'], + 'rate' => $data['rate'], + 'comment' => $data['comment'], + 'day' => $data['day'], + 'month' => $data['month'], + 'year' => $data['year'], + ], + ]; + } + } + + return null; + } +} diff --git a/app/ExportResources/MapUuidResourceCollection.php b/app/ExportResources/MapUuidResourceCollection.php new file mode 100644 index 0000000..c07e112 --- /dev/null +++ b/app/ExportResources/MapUuidResourceCollection.php @@ -0,0 +1,24 @@ + $this->count(), + 'type' => Str::of($this->collects)->afterLast('\\')->kebab()->replace('-', '_'), + 'values' => $this->collection->mapUuid(), + ]; + } +} diff --git a/app/ExportResources/Relationship/Relationship.php b/app/ExportResources/Relationship/Relationship.php new file mode 100644 index 0000000..98d1e29 --- /dev/null +++ b/app/ExportResources/Relationship/Relationship.php @@ -0,0 +1,25 @@ + [ + 'type' => $this->relationshipType->name, + 'contact_is' => $this->contactIs->uuid, + 'of_contact' => $this->ofContact->uuid, + ], + ]; + } +} diff --git a/app/ExportResources/User/Module.php b/app/ExportResources/User/Module.php new file mode 100644 index 0000000..af7fb58 --- /dev/null +++ b/app/ExportResources/User/Module.php @@ -0,0 +1,20 @@ + [ + $this->mergeWhen($this->currency !== null, [ + 'currency' => $this->currency->iso, + ]), + $this->mergeWhen($this->invited_by_user_id !== null, function () { + try { + $invited_by_user = Contact::where('account_id', $this->account_id) + ->findOrFail($this->invited_by_user_id); + + return [ + 'invited_by_user' => $invited_by_user->uuid, + ]; + } catch (\Exception $e) { + return new MissingValue(); + } + }), + $this->mergeWhen($this->me !== null, function () { + return ['me_contact' => $this->me->uuid]; + }), + ], + ]; + } +} diff --git a/app/Helpers/AccountHelper.php b/app/Helpers/AccountHelper.php new file mode 100644 index 0000000..1878837 --- /dev/null +++ b/app/Helpers/AccountHelper.php @@ -0,0 +1,219 @@ +has_access_to_paid_version_for_free) { + return false; + } + + if (! config('monica.requires_subscription')) { + return false; + } + + if ($account->isSubscribed()) { + return false; + } + + return true; + } + + /** + * Indicate whether an account has reached the contact limit if the account + * is on a free trial. + * + * @param Account $account + * @return bool + */ + public static function hasReachedContactLimit(Account $account): bool + { + return $account->allContacts()->real()->active()->count() >= config('monica.number_of_allowed_contacts_free_account'); + } + + /** + * Indicate whether an account has not reached the contact limit of free accounts. + * + * @param Account $account + * @return bool + */ + public static function isBelowContactLimit(Account $account): bool + { + return $account->allContacts()->real()->active()->count() <= config('monica.number_of_allowed_contacts_free_account'); + } + + /** + * Check if the account can be downgraded, based on a set of rules. + * + * @param Account $account + * @return bool + */ + public static function canDowngrade(Account $account): bool + { + $canDowngrade = true; + $numberOfUsers = $account->users()->count(); + $numberPendingInvitations = $account->invitations()->count(); + $numberActiveContacts = $account->allContacts()->active()->count(); + + // number of users in the account should be == 1 + if ($numberOfUsers > 1) { + $canDowngrade = false; + } + + // there should not be any pending user invitations + if ($numberPendingInvitations > 0) { + $canDowngrade = false; + } + + // there should not be more than the number of contacts allowed + if ($numberActiveContacts > config('monica.number_of_allowed_contacts_free_account')) { + $canDowngrade = false; + } + + return $canDowngrade; + } + + /** + * Get the default gender for this account. + * + * @param Account $account + * @return string + */ + public static function getDefaultGender(Account $account): string + { + $defaultGenderType = Gender::UNKNOWN; + + if ($account->default_gender_id) { + $defaultGender = Gender::where([ + 'account_id' => $account->id, + ])->find($account->default_gender_id); + + if ($defaultGender) { + $defaultGenderType = $defaultGender->type; + } + } + + return $defaultGenderType; + } + + /** + * Get the reminders for the month given in parameter. + * - 0 means current month + * - 1 means month+1 + * - 2 means month+2... + * + * @param Account $account + * @param int $month + */ + public static function getUpcomingRemindersForMonth(Account $account, int $month) + { + $startOfMonth = now(DateHelper::getTimezone())->addMonthsNoOverflow($month)->startOfMonth(); + + // don't get reminders for past events: + if ($startOfMonth->isPast()) { + $startOfMonth = now(DateHelper::getTimezone()); + } + + $endOfMonth = now(DateHelper::getTimezone())->addMonthsNoOverflow($month)->endOfMonth(); + + return $account->reminderOutboxes() + ->with(['reminder', 'reminder.contact']) + ->whereBetween('planned_date', [$startOfMonth, $endOfMonth]) + ->where([ + 'user_id' => auth()->user()->id, + 'nature' => 'reminder', + ]) + ->orderBy('planned_date', 'asc') + ->get() + ->filter(function ($reminderOutbox) { + return $reminderOutbox->reminder->contact !== null; + }); + } + + /** + * Get the number of activities grouped by year. + * + * @param Account $account + * @return Collection + */ + public static function getYearlyActivitiesStatistics(Account $account): Collection + { + $activitiesStatistics = collect([]); + $activities = $account->activities() + ->select('happened_at') + ->latest('happened_at') + ->get(); + $years = []; + + foreach ($activities as $activity) { + $yearStatistic = $activity->happened_at->format('Y'); + $foundInYear = false; + + foreach ($years as $year => $number) { + if ($year == $yearStatistic) { + $years[$year] = $number + 1; + $foundInYear = true; + } + } + + if (! $foundInYear) { + $years[$yearStatistic] = 1; + } + } + + foreach ($years as $year => $number) { + $activitiesStatistics->put($year, $number); + } + + return $activitiesStatistics; + } + + /** + * Get the number of calls grouped by year. + * + * @return Collection + */ + public static function getYearlyCallStatistics(Account $account): Collection + { + $callsStatistics = collect([]); + $calls = $account->calls() + ->select('called_at') + ->latest('called_at') + ->get(); + $years = []; + + foreach ($calls as $call) { + $yearStatistic = $call->called_at->format('Y'); + $foundInYear = false; + + foreach ($years as $year => $number) { + if ($year == $yearStatistic) { + $years[$year] = $number + 1; + $foundInYear = true; + } + } + + if (! $foundInYear) { + $years[$yearStatistic] = 1; + } + } + + foreach ($years as $year => $number) { + $callsStatistics->put($year, $number); + } + + return $callsStatistics; + } +} diff --git a/app/Helpers/AuditLogHelper.php b/app/Helpers/AuditLogHelper.php new file mode 100644 index 0000000..6dd6d92 --- /dev/null +++ b/app/Helpers/AuditLogHelper.php @@ -0,0 +1,53 @@ + $logs + * @return Collection + */ + public static function getCollectionOfAudits($logs): Collection + { + $logsCollection = collect(); + + foreach ($logs as $log) { + $object = null; + $link = null; + + // the log is about a contact + if (isset($log->object->{'contact_id'})) { + try { + // check if the contact that the log is about still exists + // in that case, we will display a link to point to this contact + $contact = Contact::findOrFail($log->object->{'contact_id'}); + $object = $contact->name; + $link = route('people.show', ['contact' => $contact]); + } catch (ModelNotFoundException $e) { + // the contact doesn't exist anymore, we don't need a link, we'll only display a name + $object = $log->object->{'contact_name'}; + } + $description = trans('logs.settings_log_'.$log->action.'_with_name', ['name' => $object]); + } else { + $description = trans('logs.settings_log_'.$log->action, ['name' => $log->object->{'name'}]); + } + + $logsCollection->push([ + 'author_name' => ($log->author) ? $log->author->name : $log->author_name, + 'description' => $description, + 'link' => $link, + 'object' => $object, + 'audited_at' => $log->audited_at, + ]); + } + + return $logsCollection; + } +} diff --git a/app/Helpers/CollectionHelper.php b/app/Helpers/CollectionHelper.php new file mode 100644 index 0000000..e6beb2e --- /dev/null +++ b/app/Helpers/CollectionHelper.php @@ -0,0 +1,106 @@ +all() as $key => $value) { + $results[$key] = $callback($value, $key); + } + + // Using Collator to sort the array, with locale-sensitive sort ordering support. + static::getCollator()->asort($results, $options); + if ($descending) { + $results = array_reverse($results); + } + + // Once we have sorted all of the keys in the array, we will loop through them + // and grab the corresponding model so we can set the underlying items list + // to the sorted version. Then we'll just return the collection instance. + foreach (array_keys($results) as $key) { + $results[$key] = $collect->get($key); + } + + return new Collection($results); + } + + /** + * Get a Collator object for the locale or current locale. + * + * @param string $locale + * @return \Collator + */ + public static function getCollator($locale = null) + { + static $collators = []; + + if (! $locale) { + $locale = app()->getLocale(); + } + if (! Arr::has($collators, $locale)) { + $collator = new \Collator($locale); + + if (LocaleHelper::getLang($locale) == 'fr') { + $collator->setAttribute(\Collator::FRENCH_COLLATION, \Collator::ON); + } + + $collators[$locale] = $collator; + + return $collator; + } + + return $collators[$locale]; + } + + /** + * Get a value retrieving callback. + * + * @param string|callable $value + * @return callable + */ + private static function valueRetriever($value) + { + if (! is_string($value) && is_callable($value)) { + return $value; + } + + return function ($item) use ($value) { + return data_get($item, $value); + }; + } + + /** + * Group collection based on a specific property from its items. + * + * @param \Illuminate\Support\Collection $collection + * @param string $property + * @return mixed + */ + public static function groupByItemsProperty($collection, $property) + { + return $collection->mapToGroups(function ($item) use ($property) { + return [data_get($item, $property) => $item]; + }); + } +} diff --git a/app/Helpers/ComplianceHelper.php b/app/Helpers/ComplianceHelper.php new file mode 100644 index 0000000..864d440 --- /dev/null +++ b/app/Helpers/ComplianceHelper.php @@ -0,0 +1,45 @@ +where('user_id', $user->id) + ->where('account_id', $user->account_id) + ->where('term_id', $term->id) + ->first(); + + if (! $termUser) { + return false; + } + + return true; + } + + /** + * Indicate if the user has accepted the most recent terms and privacy. + * This really is a shortcut of the `hasSignedGivenTerm` method. + * + * @param User $user + * @return bool + */ + public static function isCompliantWithCurrentTerm(User $user): bool + { + $latestTerm = Term::latest()->first(); + + return self::hasSignedGivenTerm($user, $latestTerm); + } +} diff --git a/app/Helpers/ComposerScripts.php b/app/Helpers/ComposerScripts.php new file mode 100644 index 0000000..61bec8b --- /dev/null +++ b/app/Helpers/ComposerScripts.php @@ -0,0 +1,58 @@ +map(function (Country $item) { + return [ + 'id' => $item->getIsoAlpha2(), + 'country' => static::getCommonNameLocale($item), + ]; + }); + + return collect($countries->sortByCollator('country')); + } + + /** + * Get country name. + * + * @param string $iso code of the country + * @return string common name (localized) of the country + */ + public static function get($iso): string + { + $country = self::getCountry($iso); + if (is_null($country)) { + return ''; + } + + return static::getCommonNameLocale($country); + } + + /** + * Find a country by the (english) name of the country. + * + * @param string $name Common name of a country + * @return string iso_3166_1_alpha2 code of the country + */ + public static function find($name): string + { + $country = collect(CountryLoader::where('name.common', $name)); + if ($country->count() === 0) { + $country = collect(CountryLoader::where('iso_3166_1_alpha2', mb_strtoupper($name))); + } + if ($country->count() === 0) { + return ''; + } + + return (new Country($country->first()))->getIsoAlpha2(); + } + + /** + * Get the common name of country, in locale version. + * + * @param \Rinvex\Country\Country $country + * @return string + */ + private static function getCommonNameLocale(Country $country): string + { + $locale = App::getLocale(); + $lang = LocaleHelper::getLocaleAlpha($locale); + + return $country->getTranslation($lang)['common']; + } + + /** + * Get country for a specific iso code. + * + * @param string $iso + * @return \Rinvex\Country\Country|null the Country element + */ + public static function getCountry($iso): ?Country + { + $country = collect(CountryLoader::where('iso_3166_1_alpha2', mb_strtoupper($iso))); + if ($country->count() === 0) { + $country = collect(CountryLoader::where('alt_spellings', mb_strtoupper($iso))); + } + if ($country->count() === 0) { + return null; + } + + return new Country($country->first()); + } + + /** + * Get country for a specific language. + * + * @param string $locale language code (iso) + * @return \Rinvex\Country\Country|null the Country element + */ + public static function getCountryFromLocale($locale): ?Country + { + $countryCode = LocaleHelper::extractCountry($locale); + if (empty($countryCode)) { + $countryCode = self::getDefaultCountryFromLocale($locale); + } + + if (is_null($countryCode)) { + $lang = LocaleHelper::getLocaleAlpha($locale); + $country = collect(CountryLoader::where("languages.$lang", '>', '0')); + if ($country->count() === 0) { + return null; + } + } else { + $country = collect(CountryLoader::where('iso_3166_1_alpha2', mb_strtoupper($countryCode))); + } + + return new Country($country->first()); + } + + /** + * Get default country for a language. + * + * @param string $locale language code (iso) + * @return string|null iso_3166_1_alpha2 code + */ + private static function getDefaultCountryFromLocale($locale): ?string + { + switch (mb_strtolower($locale)) { + case 'cs': + $country = 'CZ'; + break; + case 'en': + $country = 'US'; + break; + case 'he': + $country = 'IL'; + break; + case 'zh': + $country = 'CN'; + break; + case 'de': + case 'es': + case 'fr': + case 'hr': + case 'it': + case 'nl': + case 'pt': + case 'ru': + case 'tr': + $country = mb_strtoupper($locale); + break; + default: + $country = null; + break; + } + + return $country; + } + + /** + * Get default timezone for the country. + * + * @param mixed $country Country element + * @return string timezone fo this sountry + */ + public static function getDefaultTimezone($country): string + { + // https://en.wikipedia.org/wiki/List_of_tz_database_time_zones + // https://en.wikipedia.org/wiki/List_of_time_zones_by_country + switch ($country->getIsoAlpha3()) { + case 'AUS': + $timezone = 'Australia/Melbourne'; + break; + case 'CHN': + $timezone = 'Asia/Shanghai'; + break; + case 'ESP': + $timezone = 'Europe/Madrid'; + break; + case 'PRT': + $timezone = 'Europe/Lisbon'; + break; + case 'RUS': + $timezone = 'Europe/Moscow'; + break; + case 'CAN': + $timezone = 'America/Toronto'; + break; + case 'USA': + $timezone = 'America/Chicago'; + break; + default: + $timezone = collect($country->getTimezones())->first(); + break; + } + + return $timezone ?? config('app.timezone'); + } +} diff --git a/app/Helpers/DBHelper.php b/app/Helpers/DBHelper.php new file mode 100644 index 0000000..df894f6 --- /dev/null +++ b/app/Helpers/DBHelper.php @@ -0,0 +1,67 @@ +getPdo()->getAttribute(PDO::ATTR_SERVER_VERSION); + } catch (\Exception $e) { + return null; + } + } + + /** + * Test if db version if greater than $version param. + * + * @param string $version + * @return bool + */ + public static function testVersion($version) + { + return version_compare(static::version(), $version) >= 0; + } + + /** + * Get list of tables on this instance. + * + * @return array + */ + public static function getTables() + { + return DB::select('SELECT table_name as `table_name` + FROM information_schema.tables + WHERE table_schema = :table_schema + AND table_name LIKE :table_prefix', [ + 'table_schema' => static::connection()->getDatabaseName(), + 'table_prefix' => '%'.static::connection()->getTablePrefix().'%', + ]); + } + + public static function getTable($name) + { + return '`'.static::connection()->getTablePrefix().$name.'`'; + } +} diff --git a/app/Helpers/DateHelper.php b/app/Helpers/DateHelper.php new file mode 100644 index 0000000..8af4f8a --- /dev/null +++ b/app/Helpers/DateHelper.php @@ -0,0 +1,366 @@ +timezone !== $appTimezone) { + $date->setTimezone($appTimezone); + } + + return $date; + } + + /** + * Creates a Carbon object from Date format. + * If timezone is given, it parse the date with this timezone. + * Always return a date with default timezone (UTC). + * + * @param Carbon|string $date + * @param string $timezone + * @return Carbon|null + */ + public static function parseDate($date, $timezone = null): ?Carbon + { + if (! $date instanceof Carbon) { + try { + $date = Carbon::parse($date); + } catch (\Exception $e) { + // Parse error + return null; + } + } + + $date = Carbon::create($date->year, $date->month, $date->day, 0, 0, 0, $timezone ?? $date->timezone); + + $appTimezone = config('app.timezone'); + if ($date->timezone !== $appTimezone) { + $date->setTimezone($appTimezone); + } + + return $date === false ? null : $date; + } + + /** + * Return timestamp date format. + * + * @param Carbon|\App\Models\Instance\SpecialDate|string|null $date + * @return string|null + */ + public static function getTimestamp($date): ?string + { + if (is_null($date)) { + return null; + } + if ($date instanceof \App\Models\Instance\SpecialDate) { + $date = $date->date; + } + if (! $date instanceof Carbon) { + $date = Carbon::parse($date); + } + + return $date->translatedFormat(config('api.timestamp_format')); + } + + /** + * Return date timestamp format. + * + * @param Carbon|\App\Models\Instance\SpecialDate|string|null $date + * @return string|null + */ + public static function getDate($date): ?string + { + if (is_null($date)) { + return null; + } + if ($date instanceof \App\Models\Instance\SpecialDate) { + $date = $date->date; + } + if (! $date instanceof Carbon) { + $date = Carbon::parse($date); + } + + return $date->translatedFormat(config('api.date_timestamp_format')); + } + + /** + * Get the timezone of the current user, or null. + * + * @return string|null + */ + public static function getTimezone(): ?string + { + return Auth::check() ? Auth::user()->timezone : null; + } + + /** + * Return a date in a short format like "Oct 29, 1981". + * + * @param Carbon $date + * @return string + */ + public static function getShortDate(Carbon $date): string + { + return self::formatDate($date, 'format.short_date_year'); + } + + /** + * Return a date in a full format like "October 29, 1981". + * + * @param Carbon $date + * @return string + */ + public static function getFullDate(Carbon $date): string + { + return self::formatDate($date, 'format.full_date_year'); + } + + /** + * Return the month of the date like "Oct", or "Dec". + * + * @param Carbon $date + * @return string + */ + public static function getShortMonth(Carbon $date): string + { + return self::formatDate($date, 'format.short_month'); + } + + /** + * Return the month and year of the date like "October 2010", + * or "March 2032". + * + * @param Carbon $date + * @return string + */ + public static function getFullMonthAndDate(Carbon $date): string + { + return self::formatDate($date, 'format.full_month_year'); + } + + /** + * Return the day of the date like "Mon", or "Wed". + * + * @param Carbon $date + * @return string + */ + public static function getShortDay(Carbon $date): string + { + return self::formatDate($date, 'format.short_day'); + } + + /** + * Return a date in a short format + * like "Oct 29". + * + * @param Carbon $date + * @return string + */ + public static function getShortDateWithoutYear(Carbon $date): string + { + return self::formatDate($date, 'format.short_date'); + } + + /** + * Return a date and the time according to the timezone of the user, in a short format + * like "Oct 29, 1981 19:32". + * + * @param Carbon $date + * @return string + */ + public static function getShortDateWithTime(Carbon $date): string + { + return self::formatDate($date, 'format.short_date_year_time', true); + } + + /** + * Return a date in a given format. + * + * @param Carbon $date + * @param string $format + * @param bool $withTimezone + * @return string + */ + private static function formatDate(Carbon $date, string $format, bool $withTimezone = false): string + { + $format = trans($format, [], Carbon::getLocale()); + if ($withTimezone) { + $date = $date->setTimezone(static::getTimezone()); + } + + return $date->translatedFormat($format) ?: ''; + } + + /** + * Add a given number of week/month/year to a date. + * + * @param Carbon $date the start date + * @param string $frequency week/month/year + * @param int $number the number of week/month/year to increment to + * @return Carbon + */ + public static function addTimeAccordingToFrequencyType(Carbon $date, string $frequency, int $number): Carbon + { + switch ($frequency) { + case 'week': + $date = $date->addWeeks($number); + break; + case 'month': + $date = $date->addMonths($number); + break; + default: + $date = $date->addYears($number); + break; + } + + return $date; + } + + /** + * Get the name of the month and year of a given date with a given number + * of months more. + * + * @param int $month + * @return string + */ + public static function getMonthAndYear(int $month): string + { + $date = Carbon::now(static::getTimezone())->addMonthsNoOverflow($month); + $format = trans('format.short_month_year', [], Carbon::getLocale()); + + return $date->translatedFormat($format) ?: ''; + } + + /** + * Gets the next theoritical billing date. + * This is used on the Upgrade page to tell the user when the next billing + * date would be if he subscribed. + * + * @param string $interval + * @return Carbon + */ + public static function getNextTheoriticalBillingDate(string $interval): Carbon + { + if ($interval == 'monthly') { + return now(static::getTimezone())->addMonth(); + } + + return now(static::getTimezone())->addYear(); + } + + /** + * Gets a list of all the year from min to max (0 is the current year). + * + * @param int $max + * @param int $min + * @return Collection + */ + public static function getListOfYears($max = 120, $min = 0): Collection + { + $years = collect([]); + $maxYear = now(static::getTimezone())->subYears($min)->year; + $minYear = now(static::getTimezone())->subYears($max)->year; + + for ($year = $maxYear; $year >= $minYear; $year--) { + $years->push([ + 'id' => $year, + 'name' => $year, + ]); + } + + return $years; + } + + /** + * Gets a list of all the months in a year. + * + * @return Collection + */ + public static function getListOfMonths(): Collection + { + $months = collect([]); + $currentDate = Carbon::parse('2000-01-01'); + $format = trans('format.full_month', [], Carbon::getLocale()); + + for ($month = 1; $month <= 12; $month++) { + $currentDate->month = $month; + $months->push([ + 'id' => $month, + 'name' => mb_convert_case($currentDate->translatedFormat($format), MB_CASE_TITLE, 'UTF-8'), + ]); + } + + return $months; + } + + /** + * Gets a list of all the days in a month. + * + * @return Collection + */ + public static function getListOfDays(): Collection + { + $days = collect([]); + for ($day = 1; $day <= 31; $day++) { + $days->push(['id' => $day, 'name' => $day]); + } + + return $days; + } + + /** + * Gets a list of all the hours in a day. + * + * @return Collection + */ + public static function getListOfHours(): Collection + { + $currentDate = Carbon::parse('2000-01-01 00:00:00'); + $format = trans('format.full_hour', [], Carbon::getLocale()); + + $hours = collect([]); + for ($hour = 1; $hour <= 24; $hour++) { + $currentDate->hour = $hour; + $hours->push([ + 'id' => date('H:i', strtotime("$hour:00")), + 'name' => $currentDate->translatedFormat($format), + ]); + } + + return $hours; + } +} diff --git a/app/Helpers/FormHelper.php b/app/Helpers/FormHelper.php new file mode 100644 index 0000000..aeae932 --- /dev/null +++ b/app/Helpers/FormHelper.php @@ -0,0 +1,31 @@ +name_order) { + case 'lastname_firstname': + case 'lastname_firstname_nickname': + case 'lastname_nickname_firstname': + case 'nickname_lastname_firstname': + $nameOrder = 'lastname'; + break; + } + + return $nameOrder; + } +} diff --git a/app/Helpers/GenderHelper.php b/app/Helpers/GenderHelper.php new file mode 100644 index 0000000..44067f0 --- /dev/null +++ b/app/Helpers/GenderHelper.php @@ -0,0 +1,48 @@ +user()->account->genders->map(function (Gender $gender): array { + return [ + 'id' => $gender->id, + 'name' => $gender->name, + ]; + }); + $genders = CollectionHelper::sortByCollator($genders, 'name'); + $genders->prepend(['id' => '', 'name' => trans('app.gender_no_gender')]); + + return $genders; + } + + /** + * Replaces a specific gender of all the contacts in the account with another + * gender. + * + * @param Account $account + * @param Gender $genderToDelete + * @param Gender $genderToReplaceWith + * @return bool + */ + public static function replace(Account $account, Gender $genderToDelete, Gender $genderToReplaceWith): bool + { + Contact::where('account_id', $account->id) + ->where('gender_id', $genderToDelete->id) + ->update(['gender_id' => $genderToReplaceWith->id]); + + return true; + } +} diff --git a/app/Helpers/InstanceHelper.php b/app/Helpers/InstanceHelper.php new file mode 100644 index 0000000..7c14870 --- /dev/null +++ b/app/Helpers/InstanceHelper.php @@ -0,0 +1,118 @@ +count(); + } + + /** + * Get the plan information for the given time period. + * + * @param string $timePeriod Accepted values: 'monthly', 'annual' + * @return array|null + */ + public static function getPlanInformationFromConfig(string $timePeriod): ?array + { + $timePeriod = strtolower($timePeriod); + + if ($timePeriod != 'monthly' && $timePeriod != 'annual') { + return null; + } + + $currency = Currency::where('iso', strtoupper(config('cashier.currency')))->first(); + $amount = MoneyHelper::format(config('monica.paid_plan_'.$timePeriod.'_price'), $currency); + + return [ + 'type' => $timePeriod, + 'name' => config('monica.paid_plan_'.$timePeriod.'_friendly_name'), + 'id' => config('monica.paid_plan_'.$timePeriod.'_id'), + 'price' => config('monica.paid_plan_'.$timePeriod.'_price'), + 'friendlyPrice' => $amount, + ]; + } + + /** + * Get the plan information for the given time period. + * + * @param \Laravel\Cashier\Subscription $subscription + * @return array|null + */ + public static function getPlanInformationFromSubscription(\Laravel\Cashier\Subscription $subscription): ?array + { + try { + $stripeSubscription = $subscription->asStripeSubscription(); + $plan = $stripeSubscription->plan; + } catch (\Stripe\Exception\ApiErrorException $e) { + $stripeSubscription = null; + $plan = null; + } + + if (is_null($stripeSubscription) || is_null($plan)) { + return [ + 'type' => $subscription->stripe_price, + 'name' => $subscription->name, + 'id' => $subscription->stripe_id, + 'price' => '?', + 'friendlyPrice' => '?', + 'nextBillingDate' => '', + ]; + } + + $currency = Currency::where('iso', strtoupper($plan->currency))->first(); + $amount = MoneyHelper::format($plan->amount, $currency); + + return [ + 'type' => $plan->interval === 'month' ? 'monthly' : 'annual', + 'name' => $subscription->name, + 'id' => $plan->id, + 'price' => $plan->amount, + 'friendlyPrice' => $amount, + 'nextBillingDate' => DateHelper::getFullDate(Carbon::createFromTimestamp($stripeSubscription->current_period_end)), + ]; + } + + /** + * Get changelogs entries. + * + * @param int $limit + * @return array + */ + public static function getChangelogEntries($limit = null) + { + $json = public_path('changelog.json'); + $changelogs = json_decode(file_get_contents($json), true)['entries']; + + if ($limit) { + $changelogs = array_slice($changelogs, 0, $limit); + } + + return $changelogs; + } + + /** + * Check if the instance has at least one account. + * + * @return bool + */ + public static function hasAtLeastOneAccount(): bool + { + return DB::table('accounts')->count() > 0; + } +} diff --git a/app/Helpers/JournalHelper.php b/app/Helpers/JournalHelper.php new file mode 100644 index 0000000..4aa0ef8 --- /dev/null +++ b/app/Helpers/JournalHelper.php @@ -0,0 +1,29 @@ +account_id) + ->where('date', now($user->timezone)->toDateString()) + ->firstOrFail(); + } catch (ModelNotFoundException $e) { + return false; + } + + return true; + } +} diff --git a/app/Helpers/LocaleHelper.php b/app/Helpers/LocaleHelper.php new file mode 100644 index 0000000..6b6d12a --- /dev/null +++ b/app/Helpers/LocaleHelper.php @@ -0,0 +1,210 @@ +locale; + } else { + $locale = app('language.detector')->detect() ?: config('app.locale'); + } + + return $locale; + } + + /** + * Get the current lang from locale. + * + * @return string lang, lowercase form. + */ + public static function getLang($locale = null) + { + if (is_null($locale)) { + $locale = App::getLocale(); + } + if (preg_match(self::LANG_SPLIT, $locale)) { + $locale = preg_split(self::LANG_SPLIT, $locale, 2)[0]; + } + + return mb_strtolower($locale); + } + + /** + * Get the current country from locale. + * + * @return string country, uppercase form. + */ + public static function getCountry($locale = null) + { + $countryCode = self::extractCountry($locale); + + if (is_null($countryCode)) { + $country = CountriesHelper::getCountryFromLocale($locale); + $countryCode = $country->getIsoAlpha2(); + } + + return mb_strtoupper($countryCode); + } + + /** + * Extract the current country from locale, i.e. 'en-US' will return 'US'. + * If no country is present in the locale, it will return null. + * + * @return string|null country, uppercase form. + */ + public static function extractCountry($locale = null): ?string + { + if (is_null($locale)) { + $locale = App::getLocale(); + } + if (preg_match(self::LANG_SPLIT, $locale)) { + $locale = preg_split(self::LANG_SPLIT, $locale, 2)[1]; + + return mb_strtoupper($locale); + } + + return null; + } + + /** + * Get the list of avalaible languages. + * + * @return \Illuminate\Support\Collection + */ + public static function getLocaleList() + { + return collect(config('lang-detector.languages'))->map(function (string $lang): array { + return [ + 'lang' => $lang, + 'name' => self::getLocaleName($lang), + 'name-orig' => self::getLocaleName($lang, $lang), + ]; + }); + } + + /** + * Get the name of one language. + * + * @param string $lang + * @param string $locale + * @return string + */ + private static function getLocaleName($lang, $locale = null): string + { + $name = trans('settings.locale_'.$lang, [], $locale); + if ($name == 'settings.locale_'.$lang) { + // The name of the new language is not already set, even in english + $name = $lang; + } + + return (string) Str::of($name); + } + + /** + * Get the direction: left to right/right to left. + * + * @return string + */ + public static function getDirection() + { + $lang = self::getLang(); + switch ($lang) { + // Source: https://meta.wikimedia.org/wiki/Template:List_of_language_names_ordered_by_code + case 'ar': + case 'arc': + case 'dv': + case 'fa': + case 'ha': + case 'he': + case 'khw': + case 'ks': + case 'ku': + case 'ps': + case 'ur': + case 'yi': + return 'rtl'; + default: + return 'ltr'; + } + } + + /** + * Association ISO-639-1 => ISO-639-2. + * + * @var array + */ + private static $locales = []; + + /** + * Get ISO-639-2/t (three-letter codes) from ISO-639-1 (two-letters code). + * + * @param string $locale + * @return string + */ + public static function getLocaleAlpha($locale) + { + if (Arr::has(static::$locales, $locale)) { + return Arr::get(static::$locales, $locale); + } + $locale = mb_strtolower($locale); + $languages = (new ISO639)->allLanguages(); + $lang = ''; + foreach ($languages as $l) { + if ($l[0] == $locale) { + $lang = $l[1]; + break; + } + } + static::$locales[$locale] = $lang; + + return $lang; + } + + /** + * Format phone number by country. + * + * @param string $tel + * @param string|null $iso + * @param int $format + * @return string + */ + public static function formatTelephoneNumberByISO(string $tel, $iso, int $format = PhoneNumberFormat::INTERNATIONAL): string + { + if (empty($iso)) { + return $tel; + } + + try { + $phoneUtil = PhoneNumberUtil::getInstance(); + + $phoneInstance = $phoneUtil->parse($tel, mb_strtoupper($iso)); + + $tel = $phoneUtil->format($phoneInstance, $format); + } catch (NumberParseException $e) { + // Do nothing if the number cannot be parsed successfully + } + + return $tel; + } +} diff --git a/app/Helpers/MailHelper.php b/app/Helpers/MailHelper.php new file mode 100644 index 0000000..a8b8878 --- /dev/null +++ b/app/Helpers/MailHelper.php @@ -0,0 +1,26 @@ +toMail($user); + $markdown = new \Illuminate\Mail\Markdown(view(), config('mail.markdown')); + + return $markdown->render($message->markdown, $message->toArray()); + } +} diff --git a/app/Helpers/MoneyHelper.php b/app/Helpers/MoneyHelper.php new file mode 100644 index 0000000..0e37653 --- /dev/null +++ b/app/Helpers/MoneyHelper.php @@ -0,0 +1,139 @@ +iso) { + $numberFormatter = new \NumberFormatter(App::getLocale(), \NumberFormatter::DECIMAL); + + return $numberFormatter->format($amount); + } + + $moneyCurrency = new MoneyCurrency($currency->iso); + $money = new Money($amount, $moneyCurrency); + $numberFormatter = new \NumberFormatter(App::getLocale(), \NumberFormatter::CURRENCY); + $moneyFormatter = new IntlMoneyFormatter($numberFormatter, new ISOCurrencies()); + + return $moneyFormatter->format($money); + } + + /** + * Format a monetary amount, without the currency. + * The value is formatted using current langage. + * + * @param int|null $amount Amount value in storable format (ex: 100 for 1,00€). + * @param Currency|int|null $currency + * @return string Formatted amount for display without currency symbol (ex: '1234.50'). + */ + public static function getValue($amount, $currency = null): string + { + $currency = self::getCurrency($currency); + + if (! $currency || ! $currency->iso) { + return (string) ($amount / 100); + } + + $moneyCurrency = new MoneyCurrency($currency->iso); + $money = new Money($amount ?? 0, $moneyCurrency); + $numberFormatter = new \NumberFormatter(App::getLocale(), \NumberFormatter::PATTERN_DECIMAL); + $moneyFormatter = new IntlMoneyFormatter($numberFormatter, new ISOCurrencies()); + + return $moneyFormatter->format($money); + } + + /** + * Parse a monetary exchange value as storable integer. + * Currency is used to know the precision of this currency. + * + * @param mixed|null $exchange Amount value in exchange format (ex: 1.00). + * @param Currency|int|null $currency + * @return int Amount as storable format (ex: 14500). + */ + public static function parseInput($exchange, $currency): int + { + $currency = self::getCurrency($currency); + + if (! $currency || ! $currency->iso) { + return (int) ((float) $exchange * 100); + } + + $moneyParser = new DecimalMoneyParser(new ISOCurrencies()); + $money = $moneyParser->parse((string) $exchange, new MoneyCurrency($currency->iso)); + + return (int) $money->getAmount(); + } + + /** + * Format a monetary value as exchange value. + * Exchange value is the amount to be entered in an input by a user, + * using ordinary format. + * + * @param int|null $amount Amount value in storable format (ex: 100 for 1,00€). + * @param Currency|int|null $currency + * @return string Real value of amount in exchange format (ex: 1.24). + */ + public static function exchangeValue($amount, $currency): string + { + $currency = self::getCurrency($currency); + + if (! $currency || ! $currency->iso) { + return (string) ($amount / 100); + } + + $moneyCurrency = new MoneyCurrency($currency->iso); + $money = new Money($amount ?? 0, $moneyCurrency); + $moneyFormatter = new DecimalMoneyFormatter(new ISOCurrencies()); + + return $moneyFormatter->format($money); + } + + /** + * Get currency object. + * + * @param Currency|int|null $currency + * @return Currency|null + */ + public static function getCurrency($currency): ?Currency + { + if (is_int($currency)) { + $currency = Currency::find($currency); + } + + if (! $currency && Auth::check()) { + $currency = Auth::user()->currency; + } + + return $currency; + } +} diff --git a/app/Helpers/RequestHelper.php b/app/Helpers/RequestHelper.php new file mode 100644 index 0000000..c40e199 --- /dev/null +++ b/app/Helpers/RequestHelper.php @@ -0,0 +1,104 @@ +getValidIpAddress(); + if ($ip === false) { + $ip = Request::header('Cf-Connecting-Ip'); + if (is_null($ip)) { + $ip = Request::ip(); + } + } + + return $ip; + } + + /** + * Get client country. + * + * @param string $ip + * @return string|null + */ + public static function country($ip): ?string + { + $position = Location::get($ip); + + return $position ? $position->countryCode : null; + } + + /** + * Get client country and currency. + * + * @param string|null $ip + * @return array + */ + public static function infos($ip) + { + $ip = $ip ?? static::ip(); + + if (config('location.ipstack_apikey') != null) { + $ipstack = new Ipstack(config('location.ipstack_apikey')); + $position = $ipstack->get($ip, true); + + if ($position !== null && Arr::get($position, 'country_code')) { + return [ + 'country' => Arr::get($position, 'country_code'), + 'currency' => Arr::get($position, 'currency.code'), + 'timezone' => Arr::get($position, 'time_zone.id'), + ]; + } + } + + if (config('location.ipdata.token') != null) { + try { + $position = static::getIpData($ip); + + return [ + 'country' => Arr::get($position, 'country_code'), + 'currency' => Arr::get($position, 'currency.code'), + 'timezone' => Arr::get($position, 'time_zone.name'), + ]; + } catch (\Exception $e) { + // skip + } + } + + return [ + 'country' => static::country($ip), + 'currency' => null, + 'timezone' => null, + ]; + } + + /** + * Get data from ipdata. + * + * @param string $ip + * @return array + */ + private static function getIpData(string $ip): array + { + $token = config('location.ipdata.token', ''); + + $url = "https://api.ipdata.co/{$ip}?api-key=".$token; + + return Http::get($url)->throw()->json(); + } +} diff --git a/app/Helpers/SearchHelper.php b/app/Helpers/SearchHelper.php new file mode 100644 index 0000000..87dc394 --- /dev/null +++ b/app/Helpers/SearchHelper.php @@ -0,0 +1,53 @@ +account_id; + + // match against `field: string` queries + if (preg_match('/(.{1,})[:](.{1,})/', $needle, $matches)) { + $search_field = $matches[1]; + $search_term = $matches[2]; + + $field = ContactFieldType::where('account_id', $accountId) + ->where('name', 'LIKE', $search_field) + ->first(); + + $field_id = is_null($field) ? 0 : $field->id; + + /** @var Builder */ + $builder = Contact::whereHas('contactFields', function ($query) use ($accountId, $field_id, $search_term) { + $query->where([ + ['account_id', $accountId], + ['data', 'like', "$search_term%"], + ['contact_field_type_id', $field_id], + ]); + }); + + return $builder->addressBook($accountId, $addressBookName) + ->orderBy($orderByColumn, $orderByDirection); + } + + return Contact::search($needle, $accountId, $orderByColumn, $orderByDirection) + ->addressBook($accountId, $addressBookName); + } +} diff --git a/app/Helpers/StorageHelper.php b/app/Helpers/StorageHelper.php new file mode 100644 index 0000000..81fccbc --- /dev/null +++ b/app/Helpers/StorageHelper.php @@ -0,0 +1,60 @@ +where('account_id', $account->id) + ->sum('filesize'); + $photosSize = DB::table('photos') + ->where('account_id', $account->id) + ->sum('filesize'); + + return $documentsSize + $photosSize; + } + + /** + * Indicates whether the account has the reached the maximum storage size. + * + * @param Account $account + * @return bool + */ + public static function hasReachedAccountStorageLimit(Account $account): bool + { + if (! config('monica.requires_subscription')) { + return false; + } + + $currentAccountSize = self::getAccountStorageSize($account); + + return $currentAccountSize > (config('monica.max_storage_size') * 1000000); + } +} diff --git a/app/Helpers/StringHelper.php b/app/Helpers/StringHelper.php new file mode 100644 index 0000000..ac2fe9c --- /dev/null +++ b/app/Helpers/StringHelper.php @@ -0,0 +1,17 @@ + $tz, + 'timezone' => $timezone, + 'name' => $name, + ]); + } + } + + $collect = collect($list) + ->groupBy('id') + ->sortKeys(); + + $result = []; + foreach ($collect as $item) { + $values = $item->sortByCollator(function ($value) { + return $value['name']; + }); + foreach ($values as $val) { + array_push($result, $val); + } + } + + return $result; + } + + /** + * Format a timezone to be displayed (english only). + * + * @param string $timezone + * @return array int value of the offset, string formatted timezone + */ + private static function formatTimezone($timezone): array + { + $dtimezone = new DateTimeZone($timezone); + $time = now($timezone); + + $offset = $time->format('P'); + + $loc = $dtimezone->getLocation(); + + if ($timezone == 'UTC') { + $formatted = '(UTC) Universal Time Coordinated'; + } else { + $name = $time->tzName; + $i = strpos($name, '/'); + if ($i > 0) { + $name = substr($name, $i + 1); + } + $name = str_replace(['St_', '/', '_'], ['St. ', ', ', ' '], $name); + + if (empty($loc['comments'])) { + $formatted = '(UTC '.$offset.') '.$name; + } else { + $formatted = '(UTC '.$offset.') '.$loc['comments'].' ('.$name.')'; + } + } + + $tz = str_replace(':', '', $offset); + $tz = intval($tz); + + return [$tz, $formatted]; + } + + /** + * Equivalent timezone to convert deprecated timezone. + * + * @var array + * + * @see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones + */ + protected static $equivalentTimezone = [ + 'Australia/Canberra' => 'Australia/Sydney', + 'Asia/Calcutta' => 'Asia/Kolkata', + 'Asia/Chongqing' => 'Asia/Shanghai', + 'Asia/Katmandu' => 'Asia/Kathmandu', + 'Asia/Rangoon' => 'Asia/Yangon', + 'Asia/Ulan_Bator' => 'Asia/Ulaanbaatar', + 'Canada/Atlantic' => 'America/Halifax', + 'Canada/Newfoundland' => 'America/St_Johns', + 'Canada/Saskatchewan' => 'America/Regina', + 'Etc/Greenwich' => 'UTC', // This is not an equivalent, but it the same zone + 'Pacific/Samoa' => 'Pacific/Pago_Pago', + 'US/Alaska' => 'America/Anchorage', + 'US/Arizona' => 'America/Phoenix', + 'US/Central' => 'America/Chicago', + 'US/East-Indiana' => 'America/Indiana/Indianapolis', + 'US/Eastern' => 'America/New_York', + 'US/Mountain' => 'America/Denver', + ]; + + /** + * Adjust a timezone with equivalent name (remove deprecated). + * + * @param string $timezone + * @return string + */ + public static function adjustEquivalentTimezone($timezone): string + { + if (array_key_exists($timezone, self::$equivalentTimezone)) { + return self::$equivalentTimezone[$timezone]; + } + + return $timezone; + } +} diff --git a/app/Helpers/VCardHelper.php b/app/Helpers/VCardHelper.php new file mode 100644 index 0000000..f5e0cc4 --- /dev/null +++ b/app/Helpers/VCardHelper.php @@ -0,0 +1,28 @@ +ADR; + + if (empty($vCardAddress)) { + return null; + } + + $country = Arr::get($vCardAddress->getParts(), '6'); + + return empty($country) ? null : CountriesHelper::find($country); + } +} diff --git a/app/Helpers/WeatherHelper.php b/app/Helpers/WeatherHelper.php new file mode 100644 index 0000000..da602eb --- /dev/null +++ b/app/Helpers/WeatherHelper.php @@ -0,0 +1,59 @@ +place->weathers() + ->orderBy('created_at', 'desc') + ->first(); + + // only get weather data if weather is either not existant or if is + // more than 6h old + if (is_null($weather) || ! $weather->created_at->between(now()->subHours(6), now())) { + self::callWeatherAPI($address); + } + + return $weather; + } + + /** + * Make the call to the weather service. + * + * @param Address $address + */ + private static function callWeatherAPI(Address $address): void + { + $jobs = []; + + if (is_null($address->place->latitude) + && config('monica.enable_geolocation') && ! is_null(config('monica.location_iq_api_key'))) { + $jobs[] = new GetGPSCoordinate($address->place); + } + + if (config('monica.enable_weather') && ! is_null(config('monica.weatherapi_key'))) { + $jobs[] = new GetWeatherInformation($address->place); + } + + Bus::batch($jobs) + ->dispatch(); + } +} diff --git a/app/Helpers/helpers.php b/app/Helpers/helpers.php new file mode 100644 index 0000000..27b1df6 --- /dev/null +++ b/app/Helpers/helpers.php @@ -0,0 +1,17 @@ +user()->account->activityTypeCategories; + + foreach ($activityTypeCategories as $activityTypeCategory) { + $activityTypesData = collect([]); + $activityTypes = $activityTypeCategory->activityTypes; + + foreach ($activityTypes as $activityType) { + $dataActivityType = [ + 'id' => $activityType->id, + 'name' => $activityType->name, + ]; + $activityTypesData->push($dataActivityType); + } + + $data = [ + 'id' => $activityTypeCategory->id, + 'name' => $activityTypeCategory->name, + 'activityTypes' => $activityTypesData, + ]; + $activityTypeCategoriesData->push($data); + } + + return $activityTypeCategoriesData; + } + + /** + * Store an activity type category. + * + * @param Request $request + * @return ActivityTypeCategoryResource + */ + public function store(Request $request) + { + $type = app(CreateActivityTypeCategory::class)->execute([ + 'account_id' => auth()->user()->account_id, + 'name' => $request->input('name'), + 'translation_key' => $request->input('translation_key'), + ]); + + return new ActivityTypeCategoryResource($type); + } + + /** + * Update an activity type category. + * + * @param Request $request + * @param int $activityTypeCategoryId + * @return ActivityTypeCategoryResource + */ + public function update(Request $request, $activityTypeCategoryId) + { + $data = [ + 'account_id' => auth()->user()->account_id, + 'activity_type_category_id' => $activityTypeCategoryId, + 'name' => $request->input('name'), + 'translation_key' => $request->input('translation_key'), + ]; + + $type = app(UpdateActivityTypeCategory::class)->execute($data); + + return new ActivityTypeCategoryResource($type); + } + + /** + * Delete the activity type category. + * + * @param Request $request + * @param int $activityTypeCategoryId + * @return \Illuminate\Http\JsonResponse + */ + public function destroy(Request $request, $activityTypeCategoryId) + { + $data = [ + 'account_id' => auth()->user()->account_id, + 'activity_type_category_id' => $activityTypeCategoryId, + ]; + + try { + app(DestroyActivityTypeCategory::class)->execute($data); + } catch (\Exception $e) { + return $this->respondNotFound(); + } + + return $this->respondObjectDeleted($activityTypeCategoryId); + } +} diff --git a/app/Http/Controllers/Account/Activity/ActivityTypesController.php b/app/Http/Controllers/Account/Activity/ActivityTypesController.php new file mode 100644 index 0000000..f388bc1 --- /dev/null +++ b/app/Http/Controllers/Account/Activity/ActivityTypesController.php @@ -0,0 +1,79 @@ +execute([ + 'account_id' => auth()->user()->account_id, + 'activity_type_category_id' => $request->input('activity_type_category_id'), + 'name' => $request->input('name'), + 'translation_key' => $request->input('translation_key'), + ]); + + return new ActivityTypeResource($type); + } + + /** + * Update an activity type. + * + * @param Request $request + * @param int $activityTypeId + * @return ActivityTypeResource + */ + public function update(Request $request, $activityTypeId) + { + $data = [ + 'account_id' => auth()->user()->account_id, + 'activity_type_id' => $activityTypeId, + 'activity_type_category_id' => $request->input('activity_type_category_id'), + 'name' => $request->input('name'), + 'translation_key' => $request->input('translation_key'), + ]; + + $type = app(UpdateActivityType::class)->execute($data); + + return new ActivityTypeResource($type); + } + + /** + * Delete the activity type. + * + * @param Request $request + * @param int $activityTypeId + * @return \Illuminate\Http\JsonResponse + */ + public function destroy(Request $request, $activityTypeId) + { + $data = [ + 'account_id' => auth()->user()->account_id, + 'activity_type_id' => $activityTypeId, + ]; + + try { + app(DestroyActivityType::class)->execute($data); + } catch (\Exception $e) { + return $this->respondNotFound(); + } + + return $this->respondObjectDeleted($activityTypeId); + } +} diff --git a/app/Http/Controllers/Account/LifeEvent/LifeEventCategoriesController.php b/app/Http/Controllers/Account/LifeEvent/LifeEventCategoriesController.php new file mode 100644 index 0000000..1a5b6da --- /dev/null +++ b/app/Http/Controllers/Account/LifeEvent/LifeEventCategoriesController.php @@ -0,0 +1,44 @@ +user()->account->lifeEventCategories; + + foreach ($lifeEventCategories as $lifeEventCategory) { + $lifeEventTypesData = collect([]); + $lifeEventTypes = $lifeEventCategory->lifeEventTypes; + + foreach ($lifeEventTypes as $lifeEventType) { + $dataLifeEventType = [ + 'id' => $lifeEventType->id, + 'name' => $lifeEventType->name, + 'default_life_event_type_key' => $lifeEventType->default_life_event_type_key, + ]; + $lifeEventTypesData->push($dataLifeEventType); + } + + $data = [ + 'id' => $lifeEventCategory->id, + 'name' => $lifeEventCategory->name, + 'default_life_event_category_key' => $lifeEventCategory->default_life_event_category_key, + 'lifeEventTypes' => $lifeEventTypesData, + ]; + $lifeEventCategoriesData->push($data); + } + + return $lifeEventCategoriesData; + } +} diff --git a/app/Http/Controllers/Account/LifeEvent/LifeEventTypesController.php b/app/Http/Controllers/Account/LifeEvent/LifeEventTypesController.php new file mode 100644 index 0000000..a07951e --- /dev/null +++ b/app/Http/Controllers/Account/LifeEvent/LifeEventTypesController.php @@ -0,0 +1,77 @@ +execute([ + 'account_id' => auth()->user()->account_id, + 'life_event_category_id' => $request->input('life_event_category_id'), + 'name' => $request->input('name'), + ]); + + return new LifeEventTypeResource($type); + } + + /** + * Update a life event type. + * + * @param Request $request + * @param int $liveEventTypeId + * @return LifeEventTypeResource + */ + public function update(Request $request, $liveEventTypeId) + { + $data = [ + 'account_id' => auth()->user()->account_id, + 'life_event_type_id' => $liveEventTypeId, + 'life_event_category_id' => $request->input('life_event_category_id'), + 'name' => $request->input('name'), + ]; + + $type = app(UpdateLifeEventType::class)->execute($data); + + return new LifeEventTypeResource($type); + } + + /** + * Delete the life event type. + * + * @param Request $request + * @param int $lifeEventTypeId + * @return \Illuminate\Http\JsonResponse + */ + public function destroy(Request $request, $lifeEventTypeId) + { + $data = [ + 'account_id' => auth()->user()->account_id, + 'life_event_type_id' => $lifeEventTypeId, + ]; + + try { + app(DestroyLifeEventType::class)->execute($data); + } catch (\Exception $e) { + return $this->respondNotFound(); + } + + return $this->respondObjectDeleted($lifeEventTypeId); + } +} diff --git a/app/Http/Controllers/Api/Account/Activity/ApiActivityTypeCategoryController.php b/app/Http/Controllers/Api/Account/Activity/ApiActivityTypeCategoryController.php new file mode 100644 index 0000000..ec07994 --- /dev/null +++ b/app/Http/Controllers/Api/Account/Activity/ApiActivityTypeCategoryController.php @@ -0,0 +1,135 @@ +user()->account->activityTypeCategories() + ->orderBy($this->sort, $this->sortDirection) + ->paginate($this->getLimitPerPage()); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return ActivityTypeCategoryResource::collection($activityTypeCategories); + } + + /** + * Get the detail of a given activity type category. + * + * @param Request $request + * @param int $activityTypeCategoryId + * @return ActivityTypeCategoryResource|\Illuminate\Http\JsonResponse + */ + public function show(Request $request, $activityTypeCategoryId) + { + try { + $activityTypeCategory = ActivityTypeCategory::where('account_id', auth()->user()->account_id) + ->where('id', $activityTypeCategoryId) + ->firstOrFail(); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + return new ActivityTypeCategoryResource($activityTypeCategory); + } + + /** + * Store the activity type category. + * + * @param Request $request + * @return ActivityTypeCategoryResource|\Illuminate\Http\JsonResponse + */ + public function store(Request $request) + { + try { + $activityTypeCategory = app(CreateActivityTypeCategory::class)->execute( + $request->except(['account_id']) + + + [ + 'account_id' => auth()->user()->account_id, + ] + ); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return new ActivityTypeCategoryResource($activityTypeCategory); + } + + /** + * Update the activity type category. + * + * @param Request $request + * @param int $activityTypeCategoryId + * @return ActivityTypeCategoryResource|\Illuminate\Http\JsonResponse + */ + public function update(Request $request, $activityTypeCategoryId) + { + try { + $activityTypeCategory = app(UpdateActivityTypeCategory::class)->execute( + $request->except(['account_id', 'activity_type_category_id']) + + + [ + 'account_id' => auth()->user()->account_id, + 'activity_type_category_id' => $activityTypeCategoryId, + ] + ); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return new ActivityTypeCategoryResource($activityTypeCategory); + } + + /** + * Delete an activity type category. + * + * @param Request $request + * @return \Illuminate\Http\JsonResponse + */ + public function destroy(Request $request, int $activityTypeCategoryId) + { + try { + app(DestroyActivityTypeCategory::class)->execute([ + 'account_id' => auth()->user()->account_id, + 'activity_type_category_id' => $activityTypeCategoryId, + ]); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return $this->respondObjectDeleted($activityTypeCategoryId); + } +} diff --git a/app/Http/Controllers/Api/Account/Activity/ApiActivityTypeController.php b/app/Http/Controllers/Api/Account/Activity/ApiActivityTypeController.php new file mode 100644 index 0000000..a8d14e0 --- /dev/null +++ b/app/Http/Controllers/Api/Account/Activity/ApiActivityTypeController.php @@ -0,0 +1,135 @@ +user()->account->activityTypes() + ->orderBy($this->sort, $this->sortDirection) + ->paginate($this->getLimitPerPage()); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return ActivityTypeResource::collection($activityTypes); + } + + /** + * Get the detail of a given activity type. + * + * @param Request $request + * @return ActivityTypeResource|\Illuminate\Http\JsonResponse + */ + public function show(Request $request, $activityTypeId) + { + try { + $activityType = ActivityType::where('account_id', auth()->user()->account_id) + ->where('id', $activityTypeId) + ->firstOrFail(); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + return new ActivityTypeResource($activityType); + } + + /** + * Store the activity type. + * + * @param Request $request + * @return ActivityTypeResource|\Illuminate\Http\JsonResponse + */ + public function store(Request $request) + { + try { + $activityType = app(CreateActivityType::class)->execute( + $request->except(['account_id']) + + + [ + 'account_id' => auth()->user()->account_id, + ] + ); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return new ActivityTypeResource($activityType); + } + + /** + * Update the activity type. + * + * @param Request $request + * @param int $activityTypeId + * @return ActivityTypeResource|\Illuminate\Http\JsonResponse + */ + public function update(Request $request, $activityTypeId) + { + try { + $activityType = app(UpdateActivityType::class)->execute( + $request->except(['account_id', 'activity_type_id']) + + + [ + 'account_id' => auth()->user()->account_id, + 'activity_type_id' => $activityTypeId, + ] + ); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return new ActivityTypeResource($activityType); + } + + /** + * Delete an activity type. + * + * @param Request $request + * @param int $activityTypeId + * @return \Illuminate\Http\JsonResponse + */ + public function destroy(Request $request, int $activityTypeId) + { + try { + app(DestroyActivityType::class)->execute([ + 'account_id' => auth()->user()->account_id, + 'activity_type_id' => $activityTypeId, + ]); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return $this->respondObjectDeleted($activityTypeId); + } +} diff --git a/app/Http/Controllers/Api/Account/ApiCompanyController.php b/app/Http/Controllers/Api/Account/ApiCompanyController.php new file mode 100644 index 0000000..fe0261d --- /dev/null +++ b/app/Http/Controllers/Api/Account/ApiCompanyController.php @@ -0,0 +1,135 @@ +user()->account->companies() + ->orderBy($this->sort, $this->sortDirection) + ->paginate($this->getLimitPerPage()); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return CompanyResource::collection($companies); + } + + /** + * Get the detail of a given company. + * + * @param Request $request + * @return CompanyResource|\Illuminate\Http\JsonResponse + */ + public function show(Request $request, $companyId) + { + try { + $company = Company::where('account_id', auth()->user()->account_id) + ->where('id', $companyId) + ->firstOrFail(); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + return new CompanyResource($company); + } + + /** + * Store the company. + * + * @param Request $request + * @return CompanyResource|\Illuminate\Http\JsonResponse + */ + public function store(Request $request) + { + try { + $company = app(CreateCompany::class)->execute( + $request->except(['account_id']) + + + [ + 'account_id' => auth()->user()->account_id, + 'author_id' => auth()->user()->id, + ] + ); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return new CompanyResource($company); + } + + /** + * Update a company. + * + * @param Request $request + * @param int $companyId + * @return CompanyResource|\Illuminate\Http\JsonResponse + */ + public function update(Request $request, $companyId) + { + try { + $company = app(UpdateCompany::class)->execute( + $request->except(['account_id', 'company_id']) + + + [ + 'account_id' => auth()->user()->account_id, + 'company_id' => $companyId, + ] + ); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return new CompanyResource($company); + } + + /** + * Delete a company. + * + * @param Request $request + * @return \Illuminate\Http\JsonResponse + */ + public function destroy(Request $request, int $companyId) + { + try { + app(DestroyCompany::class)->execute([ + 'account_id' => auth()->user()->account_id, + 'company_id' => $companyId, + ]); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return $this->respondObjectDeleted($companyId); + } +} diff --git a/app/Http/Controllers/Api/Account/ApiGenderController.php b/app/Http/Controllers/Api/Account/ApiGenderController.php new file mode 100644 index 0000000..799bcca --- /dev/null +++ b/app/Http/Controllers/Api/Account/ApiGenderController.php @@ -0,0 +1,134 @@ +user()->account->genders() + ->orderBy($this->sort, $this->sortDirection) + ->paginate($this->getLimitPerPage()); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return GenderResource::collection($genders); + } + + /** + * Get the detail of a given gender. + * + * @param Request $request + * @return GenderResource|\Illuminate\Http\JsonResponse + */ + public function show(Request $request, $genderId) + { + try { + $gender = Gender::where('account_id', auth()->user()->account_id) + ->where('id', $genderId) + ->firstOrFail(); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + return new GenderResource($gender); + } + + /** + * Store the gender. + * + * @param Request $request + * @return GenderResource|\Illuminate\Http\JsonResponse + */ + public function store(Request $request) + { + try { + $gender = app(CreateGender::class)->execute( + $request->except(['account_id']) + + + [ + 'account_id' => auth()->user()->account_id, + ] + ); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return new GenderResource($gender); + } + + /** + * Update a gender. + * + * @param Request $request + * @param int $genderId + * @return GenderResource|\Illuminate\Http\JsonResponse + */ + public function update(Request $request, $genderId) + { + try { + $gender = app(UpdateGender::class)->execute( + $request->except(['account_id', 'gender_id']) + + + [ + 'account_id' => auth()->user()->account_id, + 'gender_id' => $genderId, + ] + ); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return new GenderResource($gender); + } + + /** + * Delete a gender. + * + * @param Request $request + * @return \Illuminate\Http\JsonResponse + */ + public function destroy(Request $request, int $genderId) + { + try { + app(DestroyGender::class)->execute([ + 'account_id' => auth()->user()->account_id, + 'gender_id' => $genderId, + ]); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return $this->respondObjectDeleted($genderId); + } +} diff --git a/app/Http/Controllers/Api/Account/ApiPlaceController.php b/app/Http/Controllers/Api/Account/ApiPlaceController.php new file mode 100644 index 0000000..c7c7ef5 --- /dev/null +++ b/app/Http/Controllers/Api/Account/ApiPlaceController.php @@ -0,0 +1,134 @@ +user()->account->places() + ->orderBy($this->sort, $this->sortDirection) + ->paginate($this->getLimitPerPage()); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return PlaceResource::collection($places); + } + + /** + * Get the detail of a given place. + * + * @param Request $request + * @return PlaceResource|\Illuminate\Http\JsonResponse + */ + public function show(Request $request, $placeId) + { + try { + $place = Place::where('account_id', auth()->user()->account_id) + ->where('id', $placeId) + ->firstOrFail(); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + return new PlaceResource($place); + } + + /** + * Store the place. + * + * @param Request $request + * @return PlaceResource|\Illuminate\Http\JsonResponse + */ + public function store(Request $request) + { + try { + $place = app(CreatePlace::class)->execute( + $request->except(['account_id']) + + + [ + 'account_id' => auth()->user()->account_id, + ] + ); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return new PlaceResource($place); + } + + /** + * Update a place. + * + * @param Request $request + * @param int $placeId + * @return PlaceResource|\Illuminate\Http\JsonResponse + */ + public function update(Request $request, $placeId) + { + try { + $place = app(UpdatePlace::class)->execute( + $request->except(['account_id', 'place_id']) + + + [ + 'account_id' => auth()->user()->account_id, + 'place_id' => $placeId, + ] + ); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return new PlaceResource($place); + } + + /** + * Delete a place. + * + * @param Request $request + * @return \Illuminate\Http\JsonResponse + */ + public function destroy(Request $request, int $placeId) + { + try { + app(DestroyPlace::class)->execute([ + 'account_id' => auth()->user()->account_id, + 'place_id' => $placeId, + ]); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return $this->respondObjectDeleted($placeId); + } +} diff --git a/app/Http/Controllers/Api/Account/ApiUserController.php b/app/Http/Controllers/Api/Account/ApiUserController.php new file mode 100644 index 0000000..3250ad1 --- /dev/null +++ b/app/Http/Controllers/Api/Account/ApiUserController.php @@ -0,0 +1,148 @@ +user()); + } + + /** + * Get the state of a specific term for the user. + * + * @param Request $request + * @param int $termId + * @return JsonResponse + */ + public function get(Request $request, $termId) + { + try { + $term = Term::findOrFail($termId); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + $termUser = DB::table('term_user')->where('user_id', auth()->user()->id) + ->where('account_id', auth()->user()->account_id) + ->where('term_id', $term->id) + ->first(); + + if ($termUser) { + $data = [ + 'signed' => true, + 'signed_date' => DateHelper::getTimestamp($termUser->created_at), + 'ip_address' => $termUser->ip_address, + 'user' => new UserResource(auth()->user()), + 'term' => new ComplianceResource($term), + ]; + } else { + return $this->respondNotFound(); + } + + return $this->respond([ + 'data' => $data, + ]); + } + + /** + * Get all the policies ever signed by the authenticated user. + * + * @param Request $request + * @return JsonResponse + */ + public function getSignedPolicies(Request $request) + { + $terms = collect(); + $termsForUser = DB::table('term_user') + ->where('user_id', auth()->user()->id) + ->get(); + + if ($termsForUser->count() == 0) { + return $this->respondNotFound(); + } + + foreach ($termsForUser as $termUser) { + $term = Term::findOrFail($termUser->term_id); + + $terms->push([ + 'signed' => true, + 'signed_date' => DateHelper::getTimestamp($termUser->created_at), + 'ip_address' => $termUser->ip_address, + 'user' => new UserResource(auth()->user()), + 'term' => new ComplianceResource($term), + ]); + } + + return $this->respond([ + 'data' => $terms, + ]); + } + + /** + * Sign the latest policy for the authenticated user. + * + * @param Request $request + * @return JsonResponse + */ + public function set(Request $request) + { + $validator = Validator::make($request->all(), [ + 'ip_address' => 'required', + ]); + + if ($validator->fails()) { + return $this->respondValidatorFailed($validator); + } + + try { + $term = app(AcceptPolicy::class)->execute([ + 'account_id' => auth()->user()->account_id, + 'user_id' => auth()->user()->id, + 'ip_address' => $request->input('ip_address'), + ]); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + try { + $termUser = DB::table('term_user')->where('user_id', auth()->user()->id) + ->where('account_id', auth()->user()->account_id) + ->where('term_id', $term->id) + ->first(); + } catch (ModelNotFoundException $e) { + return $this->respondInvalidQuery(); + } + + return $this->respond([ + 'data' => [ + 'signed' => true, + 'signed_date' => DateHelper::getTimestamp($termUser->created_at), + 'ip_address' => $termUser->ip_address, + 'user' => new UserResource(auth()->user()), + 'term' => new ComplianceResource($term), + ], + ]); + } +} diff --git a/app/Http/Controllers/Api/ApiActivitiesController.php b/app/Http/Controllers/Api/ApiActivitiesController.php new file mode 100644 index 0000000..a67004f --- /dev/null +++ b/app/Http/Controllers/Api/ApiActivitiesController.php @@ -0,0 +1,161 @@ +user()->account->activities() + ->orderBy($this->sort, $this->sortDirection) + ->paginate($this->getLimitPerPage()); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return ActivityResource::collection($activities)->additional(['meta' => [ + 'statistics' => AccountHelper::getYearlyActivitiesStatistics(auth()->user()->account), + ]]); + } + + /** + * Get the detail of a given activity. + * + * @param Request $request + * @return ActivityResource|\Illuminate\Http\JsonResponse + */ + public function show(Request $request, $activityId) + { + try { + $activity = Activity::where('account_id', auth()->user()->account_id) + ->findOrFail($activityId); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + return new ActivityResource($activity); + } + + /** + * Store the activity. + * + * @param Request $request + * @return ActivityResource|\Illuminate\Http\JsonResponse + */ + public function store(Request $request) + { + try { + $activity = app(CreateActivity::class)->execute( + $request->except(['account_id']) + + + [ + 'account_id' => auth()->user()->account_id, + ] + ); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return new ActivityResource($activity); + } + + /** + * Update the activity. + * + * @param Request $request + * @param int $activityId + * @return ActivityResource|\Illuminate\Http\JsonResponse + */ + public function update(Request $request, $activityId) + { + try { + $activity = app(UpdateActivity::class)->execute( + $request->except(['account_id', 'activity_id']) + + + [ + 'account_id' => auth()->user()->account_id, + 'activity_id' => $activityId, + ] + ); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return new ActivityResource($activity); + } + + /** + * Delete an activity. + * + * @param Request $request + * @return \Illuminate\Http\JsonResponse + */ + public function destroy(Request $request, $activityId) + { + try { + app(DestroyActivity::class)->execute([ + 'account_id' => auth()->user()->account_id, + 'activity_id' => $activityId, + ]); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } + + return $this->respondObjectDeleted($activityId); + } + + /** + * Get the list of activities for the given contact. + * + * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection|\Illuminate\Http\JsonResponse + */ + public function activities(Request $request, $contactId) + { + try { + $contact = Contact::where('account_id', auth()->user()->account_id) + ->findOrFail($contactId); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + try { + $activities = $contact->activities() + ->orderBy($this->sort, $this->sortDirection) + ->paginate($this->getLimitPerPage()); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return ActivityResource::collection($activities)->additional(['meta' => [ + 'statistics' => AccountHelper::getYearlyActivitiesStatistics(auth()->user()->account), + ]]); + } +} diff --git a/app/Http/Controllers/Api/ApiContactController.php b/app/Http/Controllers/Api/ApiContactController.php new file mode 100644 index 0000000..35418eb --- /dev/null +++ b/app/Http/Controllers/Api/ApiContactController.php @@ -0,0 +1,260 @@ +middleware('limitations')->only('setMe'); + parent::__construct(); + } + + /** + * Get the list of the contacts. + * We will only retrieve the contacts that are "real", not the partials + * ones. + * + * @param Request $request + * @return JsonResource|JsonResponse + */ + public function index(Request $request) + { + if ($request->input('query')) { + $needle = rawurldecode($request->input('query')); + + try { + $contacts = SearchHelper::searchContacts( + $needle, + $this->sort, + $this->sortDirection + ) + ->real() + ->paginate($this->getLimitPerPage()); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return ContactResource::collection($contacts)->additional([ + 'meta' => [ + 'query' => $needle, + ], + ]); + } + + try { + $contacts = auth()->user()->account->contacts() + ->real() + ->active() + ->orderBy($this->sort, $this->sortDirection) + ->paginate($this->getLimitPerPage()); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return ContactResource::collection($contacts); + } + + /** + * Get the detail of a given contact. + * + * @param Request $request + * @param int $id + * @return ContactResource|JsonResponse + */ + public function show(Request $request, int $id) + { + try { + $contact = Contact::where('account_id', auth()->user()->account_id) + ->where('id', $id) + ->firstOrFail(); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + UpdateLastConsultedDate::dispatch($contact); + + return new ContactResource($contact); + } + + /** + * Store the contact. + * + * @param Request $request + * @return ContactResource|JsonResponse + */ + public function store(Request $request) + { + try { + $contact = app(CreateContact::class)->execute( + $request->except(['account_id']) + + + [ + 'account_id' => auth()->user()->account_id, + 'author_id' => auth()->user()->id, + ] + ); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return new ContactResource($contact); + } + + /** + * Update the contact. + * + * @param Request $request + * @return ContactResource|JsonResponse + */ + public function update(Request $request, $contactId) + { + try { + $contact = app(UpdateContact::class)->execute( + $request->except(['account_id', 'contact_id']) + + + [ + 'contact_id' => $contactId, + 'account_id' => auth()->user()->account_id, + 'author_id' => auth()->user()->id, + ] + ); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return new ContactResource($contact); + } + + /** + * Delete a contact. + * + * @param Request $request + * @return JsonResponse + */ + public function destroy(Request $request, $contactId) + { + $data = [ + 'account_id' => auth()->user()->account_id, + 'contact_id' => $contactId, + ]; + DestroyContact::dispatch($data); + + return $this->respondObjectDeleted($contactId); + } + + /** + * Set the contact career. + * + * @param Request $request + * @param int $contactId + * @return ContactResource|JsonResponse + */ + public function updateWork(Request $request, $contactId) + { + try { + $contact = app(UpdateWorkInformation::class)->execute( + $request->except(['account_id', 'contact_id']) + + [ + 'contact_id' => $contactId, + 'account_id' => auth()->user()->account_id, + 'author_id' => auth()->user()->id, + ] + ); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return new ContactResource($contact); + } + + /** + * Set the contact food preferences. + * + * @param Request $request + * @param int $contactId + * @return ContactResource|JsonResponse + */ + public function updateFoodPreferences(Request $request, $contactId) + { + try { + $contact = app(UpdateContactFoodPreferences::class)->execute( + $request->except(['account_id', 'contact_id']) + + [ + 'contact_id' => $contactId, + 'account_id' => auth()->user()->account_id, + ] + ); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return new ContactResource($contact); + } + + /** + * Set how you met the contact. + * + * @param Request $request + * @param int $contactId + * @return ContactResource|JsonResponse + */ + public function updateIntroduction(Request $request, $contactId) + { + try { + $contact = app(UpdateContactIntroduction::class)->execute( + $request->except(['account_id', 'contact_id']) + + [ + 'contact_id' => $contactId, + 'account_id' => auth()->user()->account_id, + ] + ); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return new ContactResource($contact); + } +} diff --git a/app/Http/Controllers/Api/ApiContactFieldController.php b/app/Http/Controllers/Api/ApiContactFieldController.php new file mode 100644 index 0000000..7ecac6a --- /dev/null +++ b/app/Http/Controllers/Api/ApiContactFieldController.php @@ -0,0 +1,140 @@ +user()->account_id) + ->findOrFail($contactFieldId); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + return new ContactFieldResource($contactField); + } + + /** + * Store the contactField. + * + * @param Request $request + * @return ContactFieldResource|\Illuminate\Http\JsonResponse + */ + public function store(Request $request) + { + try { + $contactField = app(CreateContactField::class)->execute( + $request->except(['account_id']) + + + [ + 'account_id' => auth()->user()->account_id, + ] + ); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return new ContactFieldResource($contactField); + } + + /** + * Update the contactField. + * + * @param Request $request + * @param int $contactFieldId + * @return ContactFieldResource|\Illuminate\Http\JsonResponse + */ + public function update(Request $request, $contactFieldId) + { + try { + $contactField = app(UpdateContactField::class)->execute( + $request->except(['account_id', 'address_id']) + + + [ + 'account_id' => auth()->user()->account_id, + 'contact_field_id' => $contactFieldId, + ] + ); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return new ContactFieldResource($contactField); + } + + /** + * Delete a contactField. + * + * @param Request $request + * @param int $contactFieldId + * @return \Illuminate\Http\JsonResponse + */ + public function destroy(Request $request, int $contactFieldId) + { + try { + app(DestroyContactField::class)->execute([ + 'account_id' => auth()->user()->account_id, + 'contact_field_id' => $contactFieldId, + ]); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return $this->respondObjectDeleted($contactFieldId); + } + + /** + * Get the list of contact fields for the given contact. + * + * @param Request $request + * @param int $contactId + * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection|\Illuminate\Http\JsonResponse + */ + public function contactFields(Request $request, $contactId) + { + try { + $contact = Contact::where('account_id', auth()->user()->account_id) + ->where('id', $contactId) + ->firstOrFail(); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + $contactFields = $contact->contactFields() + ->paginate($this->getLimitPerPage()); + + return ContactFieldResource::collection($contactFields); + } +} diff --git a/app/Http/Controllers/Api/ApiContactTagController.php b/app/Http/Controllers/Api/ApiContactTagController.php new file mode 100644 index 0000000..2beb426 --- /dev/null +++ b/app/Http/Controllers/Api/ApiContactTagController.php @@ -0,0 +1,129 @@ +validateTag($request, $contactId); + if (! $contact instanceof Contact) { + return $contact; + } + + $tags = collect($request->input('tags')) + ->filter(function ($tag) { + return ! empty($tag); + }); + + foreach ($tags as $tag) { + app(AssociateTag::class)->execute([ + 'account_id' => auth()->user()->account_id, + 'contact_id' => $contact->id, + 'name' => $tag, + ]); + } + + return new ContactResource($contact); + } + + /** + * Remove all the tags associated with the contact. + * + * @param Request $request + * @param int $contactId + */ + public function unsetTags(Request $request, $contactId) + { + try { + $contact = Contact::where('account_id', auth()->user()->account_id) + ->where('id', $contactId) + ->firstOrFail(); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + $contactTags = $contact->tags()->get(); + + foreach ($contactTags as $tag) { + app(DetachTag::class)->execute([ + 'account_id' => auth()->user()->account_id, + 'contact_id' => $contact->id, + 'tag_id' => $tag->id, + ]); + } + + return new ContactResource($contact); + } + + /** + * Remove one or more specific tags associated with the contact. + * + * @param Request $request + * @param int $contactId + */ + public function unsetTag(Request $request, $contactId) + { + $contact = $this->validateTag($request, $contactId); + if (! $contact instanceof Contact) { + return $contact; + } + + $tags = collect($request->input('tags')) + ->filter(function ($tag) { + return ! empty($tag); + }); + + foreach ($tags as $tag) { + app(DetachTag::class)->execute([ + 'account_id' => auth()->user()->account_id, + 'contact_id' => $contact->id, + 'tag_id' => $tag, + ]); + } + + return new ContactResource($contact); + } + + /** + * Validate the request for update tag. + * + * @param Request $request + * @param int $contactId + * @return mixed + */ + private function validateTag(Request $request, $contactId) + { + try { + $contact = Contact::where('account_id', auth()->user()->account_id) + ->findOrFail($contactId); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + $validator = Validator::make($request->all(), [ + 'tags' => 'required|array', + ]); + + if ($validator->fails()) { + return $this->respondValidatorFailed($validator); + } + + return $contact; + } +} diff --git a/app/Http/Controllers/Api/ApiController.php b/app/Http/Controllers/Api/ApiController.php new file mode 100644 index 0000000..9bf90fb --- /dev/null +++ b/app/Http/Controllers/Api/ApiController.php @@ -0,0 +1,207 @@ +middleware(function ($request, $next) { + (new ApiUsage)->log($request); + + if ($request->has('sort')) { + $this->setSortCriteria($request->input('sort')); + + // It has a sort criteria, but is it a valid one? + if (empty($this->getSortCriteria())) { + return $this->setHTTPStatusCode(400) + ->setErrorCode(39) + ->respondWithError(); + } + } + + if ($request->has('limit')) { + if ($request->input('limit') > config('api.max_limit_per_page')) { + return $this->setHTTPStatusCode(400) + ->setErrorCode(30) + ->respondWithError(); + } + + $this->setLimitPerPage($request->input('limit')); + } + + if ($request->has('with')) { + $this->setWithParameter($request->input('with')); + } + + // make sure the JSON is well formatted if the call sends a JSON + // if the call contains a JSON, the call must not be a GET or + // a DELETE + // TODO: there is probably a much better way to do that + try { + if ($request->method() != 'GET' && $request->method() != 'DELETE' + && is_null(json_decode($request->getContent()))) { + return $this->setHTTPStatusCode(400) + ->setErrorCode(37) + ->respondWithError(); + } + } catch (\Safe\Exceptions\JsonException $e) { + // no error + } + + return $next($request); + }); + } + + /** + * Default request to the API. + * + * @return \Illuminate\Http\JsonResponse + */ + public function success() + { + return $this->respond([ + 'success' => [ + 'message' => 'Welcome to Monica', + ], + 'links' => [ + 'activities_url' => route('api.activities'), + 'addresses_url' => route('api.addresses'), + 'calls_url' => route('api.calls'), + 'contacts_url' => route('api.contacts'), + 'conversations_url' => route('api.conversations'), + 'countries_url' => route('api.countries'), + 'currencies_url' => route('api.currencies'), + 'documents_url' => route('api.documents'), + 'journal_url' => route('api.journal'), + 'notes_url' => route('api.notes'), + 'relationships_url' => route('api.relationships', ['contact' => ':contactId']), + 'reminders_url' => route('api.reminders'), + 'statistics_url' => route('api.statistics'), + ], + ]); + } + + /** + * @return string + */ + public function getWithParameter() + { + return $this->withParameter; + } + + /** + * @param string $with + * @return self + */ + public function setWithParameter($with) + { + $this->withParameter = $with; + + return $this; + } + + /** + * @return int + */ + public function getLimitPerPage() + { + return $this->limitPerPage; + } + + /** + * @param int $limit + * @return self + */ + public function setLimitPerPage($limit) + { + $this->limitPerPage = $limit; + + return $this; + } + + /** + * Get the sort direction parameter. + * + * @return string + */ + public function getSortDirection() + { + return $this->sortDirection; + } + + /** + * @return string + */ + public function getSortCriteria() + { + return $this->sort; + } + + /** + * @param string $criteria + * @return self + */ + public function setSortCriteria($criteria) + { + $acceptedCriteria = [ + 'created_at', + 'updated_at', + '-created_at', + '-updated_at', + 'completed_at', + '-completed_at', + 'called_at', + '-called_at', + 'favorited_at', + '-favorited_at', + ]; + + if (in_array($criteria, $acceptedCriteria)) { + $this->setSQLOrderByQuery($criteria); + + return $this; + } + + $this->sort = ''; + + return $this; + } + + /** + * Set both the column and order necessary to perform an orderBy. + */ + public function setSQLOrderByQuery($criteria) + { + $this->sortDirection = $criteria[0] == '-' ? 'desc' : 'asc'; + $this->sort = ltrim($criteria, '-'); + } +} diff --git a/app/Http/Controllers/Api/ApiDebtController.php b/app/Http/Controllers/Api/ApiDebtController.php new file mode 100644 index 0000000..b3856e4 --- /dev/null +++ b/app/Http/Controllers/Api/ApiDebtController.php @@ -0,0 +1,191 @@ +user()->account->debts() + ->orderBy($this->sort, $this->sortDirection) + ->paginate($this->getLimitPerPage()); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return DebtResource::collection($debts); + } + + /** + * Get the detail of a given debt. + * + * @param Request $request + * @return DebtResource|\Illuminate\Http\JsonResponse + */ + public function show(Request $request, $debtId) + { + try { + $debt = Debt::where('account_id', auth()->user()->account_id) + ->where('id', $debtId) + ->firstOrFail(); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + return new DebtResource($debt); + } + + /** + * Store the debt. + * + * @param Request $request + * @return DebtResource|\Illuminate\Http\JsonResponse + */ + public function store(Request $request) + { + $isvalid = $this->validateUpdate($request); + if ($isvalid !== true) { + return $isvalid; + } + + try { + $debt = Debt::create( + $request->except(['account_id']) + + ['account_id' => auth()->user()->account_id] + ); + } catch (QueryException $e) { + return $this->respondNotTheRightParameters(); + } + + return new DebtResource($debt); + } + + /** + * Update the debt. + * + * @param Request $request + * @param int $debtId + * @return DebtResource|\Illuminate\Http\JsonResponse + */ + public function update(Request $request, $debtId) + { + try { + $debt = Debt::where('account_id', auth()->user()->account_id) + ->where('id', $debtId) + ->firstOrFail(); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + $isvalid = $this->validateUpdate($request); + if ($isvalid !== true) { + return $isvalid; + } + + try { + $debt->update($request->only(['in_debt', 'status', 'amount', 'reason', 'contact_id'])); + } catch (QueryException $e) { + return $this->respondNotTheRightParameters(); + } + + return new DebtResource($debt); + } + + /** + * Validate the request for update. + * + * @param Request $request + * @return \Illuminate\Http\JsonResponse|true + */ + private function validateUpdate(Request $request) + { + // Validates basic fields to create the entry + $validator = Validator::make($request->all(), [ + 'in_debt' => [ + 'required', + 'string', + Rule::in(['yes', 'no']), + ], + 'status' => [ + 'required', + 'string', + Rule::in(['inprogress', 'completed']), + ], + 'amount' => 'required|numeric', + 'reason' => 'string|max:1000000|nullable', + 'contact_id' => 'required|integer', + ]); + + if ($validator->fails()) { + return $this->respondValidatorFailed($validator); + } + + try { + Contact::where('account_id', auth()->user()->account_id) + ->where('id', $request->input('contact_id')) + ->firstOrFail(); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + return true; + } + + /** + * Delete a debt. + * + * @param Request $request + * @return \Illuminate\Http\JsonResponse + */ + public function destroy(Request $request, $debtId) + { + try { + $debt = Debt::where('account_id', auth()->user()->account_id) + ->where('id', $debtId) + ->firstOrFail(); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + $debt->delete(); + + return $this->respondObjectDeleted($debt->id); + } + + /** + * Get the list of debts for the given contact. + * + * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection|\Illuminate\Http\JsonResponse + */ + public function debts(Request $request, $contactId) + { + try { + $contact = Contact::where('account_id', auth()->user()->account_id) + ->where('id', $contactId) + ->firstOrFail(); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + $debts = $contact->debts() + ->orderBy($this->sort, $this->sortDirection) + ->paginate($this->getLimitPerPage()); + + return DebtResource::collection($debts); + } +} diff --git a/app/Http/Controllers/Api/ApiGiftController.php b/app/Http/Controllers/Api/ApiGiftController.php new file mode 100644 index 0000000..326e05e --- /dev/null +++ b/app/Http/Controllers/Api/ApiGiftController.php @@ -0,0 +1,174 @@ +user()->account->gifts() + ->orderBy($this->sort, $this->sortDirection) + ->paginate($this->getLimitPerPage()); + + return GiftResource::collection($gifts); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + } + + /** + * Get the detail of a given gift. + * + * @param Request $request + * @return GiftResource|\Illuminate\Http\JsonResponse + */ + public function show(Request $request, $id) + { + try { + $gift = Gift::where('account_id', auth()->user()->account_id) + ->findOrFail($id); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + return new GiftResource($gift); + } + + /** + * Store the gift. + * + * @param Request $request + * @return GiftResource|\Illuminate\Http\JsonResponse + */ + public function store(Request $request) + { + try { + $gift = app(CreateGift::class)->execute( + $request->except(['account_id']) + + ['account_id' => auth()->user()->account_id] + ); + + return new GiftResource($gift); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } + } + + /** + * Update the gift. + * + * @param Request $request + * @param int $giftId + * @return GiftResource|\Illuminate\Http\JsonResponse + */ + public function update(Request $request, $giftId) + { + try { + $gift = app(UpdateGift::class)->execute( + $request->except(['account_id', 'gift_id']) + + [ + 'account_id' => auth()->user()->account_id, + 'gift_id' => $giftId, + ] + ); + + return new GiftResource($gift); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } + } + + /** + * Associate a photo to the gift. + * + * @param Request $request + * @param int $giftId + * @param int $photoId + * @return GiftResource|\Illuminate\Http\JsonResponse + */ + public function associate(Request $request, $giftId, $photoId) + { + try { + $gift = app(AssociatePhotoToGift::class)->execute([ + 'account_id' => auth()->user()->account_id, + 'gift_id' => $giftId, + 'photo_id' => $photoId, + ]); + + return new GiftResource($gift); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } + } + + /** + * Delete a gift. + * + * @param Request $request + * @return \Illuminate\Http\JsonResponse + */ + public function destroy(Request $request, $giftId) + { + try { + app(DestroyGift::class)->execute([ + 'account_id' => auth()->user()->account_id, + 'gift_id' => $giftId, + ]); + + return $this->respondObjectDeleted($giftId); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } + } + + /** + * Get the list of gifts for the given contact. + * + * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection|\Illuminate\Http\JsonResponse + */ + public function gifts(Request $request, $contactId) + { + try { + $contact = Contact::where('account_id', auth()->user()->account_id) + ->findOrFail($contactId); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + try { + $gifts = $contact->gifts() + ->orderBy($this->sort, $this->sortDirection) + ->paginate($this->getLimitPerPage()); + + return GiftResource::collection($gifts); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + } +} diff --git a/app/Http/Controllers/Api/ApiJournalController.php b/app/Http/Controllers/Api/ApiJournalController.php new file mode 100644 index 0000000..46981d9 --- /dev/null +++ b/app/Http/Controllers/Api/ApiJournalController.php @@ -0,0 +1,148 @@ +user()->account->entries() + ->orderBy($this->sort, $this->sortDirection) + ->paginate($this->getLimitPerPage()); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return JournalResource::collection($entries); + } + + /** + * Get the detail of a given journal entry. + * + * @param Request $request + * @return JournalResource|\Illuminate\Http\JsonResponse + */ + public function show(Request $request, $entryId) + { + try { + $entry = Entry::where('account_id', auth()->user()->account_id) + ->where('id', $entryId) + ->firstOrFail(); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + return new JournalResource($entry); + } + + /** + * Store the call. + * + * @param Request $request + * @return JournalResource|\Illuminate\Http\JsonResponse + */ + public function store(Request $request) + { + $isvalid = $this->validateUpdate($request); + if ($isvalid !== true) { + return $isvalid; + } + + try { + $entry = Entry::create( + $request->except(['account_id']) + + ['account_id' => auth()->user()->account_id] + ); + } catch (QueryException $e) { + return $this->respondNotTheRightParameters(); + } + + return new JournalResource($entry); + } + + /** + * Update the note. + * + * @param Request $request + * @param int $entryId + * @return JournalResource|\Illuminate\Http\JsonResponse + */ + public function update(Request $request, $entryId) + { + try { + $entry = Entry::where('account_id', auth()->user()->account_id) + ->where('id', $entryId) + ->firstOrFail(); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + $isvalid = $this->validateUpdate($request); + if ($isvalid !== true) { + return $isvalid; + } + + try { + $entry->update($request->only(['title', 'post'])); + } catch (QueryException $e) { + return $this->respondNotTheRightParameters(); + } + + return new JournalResource($entry); + } + + /** + * Validate the request for update. + * + * @param Request $request + * @return \Illuminate\Http\JsonResponse|true + */ + private function validateUpdate(Request $request) + { + // Validates basic fields to create the entry + $validator = Validator::make($request->all(), [ + 'title' => 'required|max:255', + 'post' => 'required|max:1000000', + ]); + + if ($validator->fails()) { + return $this->respondValidatorFailed($validator); + } + + return true; + } + + /** + * Delete a journal entry. + * + * @param Request $request + * @return \Illuminate\Http\JsonResponse + */ + public function destroy(Request $request, $entryId) + { + try { + $entry = Entry::where('account_id', auth()->user()->account_id) + ->where('id', $entryId) + ->firstOrFail(); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + $entry->delete(); + + return $this->respondObjectDeleted($entry->id); + } +} diff --git a/app/Http/Controllers/Api/ApiMeController.php b/app/Http/Controllers/Api/ApiMeController.php new file mode 100644 index 0000000..65e88f7 --- /dev/null +++ b/app/Http/Controllers/Api/ApiMeController.php @@ -0,0 +1,66 @@ +middleware('limitations')->only('store'); + parent::__construct(); + } + + /** + * Set a contact as 'me'. + * + * @param Request $request + * @return string + */ + public function store(Request $request) + { + $data = [ + 'contact_id' => $request->input('contact_id'), + 'account_id' => auth()->user()->account_id, + 'user_id' => auth()->user()->id, + ]; + + try { + app(SetMeContact::class)->execute($data); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } + + return $this->respond(['true']); + } + + /** + * Removes contact as 'me' association. + * + * @param Request $request + * @return string + */ + public function destroy(Request $request) + { + $data = [ + 'account_id' => auth()->user()->account_id, + 'user_id' => auth()->user()->id, + ]; + + app(DeleteMeContact::class)->execute($data); + + return $this->respond(['true']); + } +} diff --git a/app/Http/Controllers/Api/ApiNoteController.php b/app/Http/Controllers/Api/ApiNoteController.php new file mode 100644 index 0000000..5f87189 --- /dev/null +++ b/app/Http/Controllers/Api/ApiNoteController.php @@ -0,0 +1,192 @@ +user()->account->notes() + ->orderBy($this->sort, $this->sortDirection) + ->paginate($this->getLimitPerPage()); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return NoteResource::collection($notes); + } + + /** + * Get the detail of a given note. + * + * @param Request $request + * @return NoteResource|\Illuminate\Http\JsonResponse + */ + public function show(Request $request, $id) + { + try { + $note = Note::where('account_id', auth()->user()->account_id) + ->where('id', $id) + ->firstOrFail(); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + return new NoteResource($note); + } + + /** + * Store the note. + * + * @param Request $request + * @return NoteResource|\Illuminate\Http\JsonResponse + */ + public function store(Request $request) + { + $isvalid = $this->validateUpdate($request); + if ($isvalid !== true) { + return $isvalid; + } + + try { + $note = Note::create( + $request->except(['account_id']) + + ['account_id' => auth()->user()->account_id] + ); + } catch (QueryException $e) { + return $this->respondNotTheRightParameters(); + } + + if ($request->input('is_favorited')) { + $note->favorited_at = now(); + $note->save(); + } + + return new NoteResource($note); + } + + /** + * Update the note. + * + * @param Request $request + * @param int $noteId + * @return NoteResource|\Illuminate\Http\JsonResponse + */ + public function update(Request $request, $noteId) + { + try { + $note = Note::where('account_id', auth()->user()->account_id) + ->where('id', $noteId) + ->firstOrFail(); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + $isvalid = $this->validateUpdate($request); + if ($isvalid !== true) { + return $isvalid; + } + + try { + $note->update($request->only(['body', 'contact_id', 'is_favorited'])); + } catch (QueryException $e) { + return $this->respondNotTheRightParameters(); + } + + if ($request->input('is_favorited')) { + $note->favorited_at = now(); + } else { + $note->favorited_at = null; + } + $note->save(); + + return new NoteResource($note); + } + + /** + * Validate the request for update. + * + * @param Request $request + * @return \Illuminate\Http\JsonResponse|true + */ + private function validateUpdate(Request $request) + { + // Validates basic fields to create the entry + $validator = Validator::make($request->all(), [ + 'body' => 'required|max:100000', + 'contact_id' => 'required|integer', + 'is_favorited' => 'boolean', + ]); + + if ($validator->fails()) { + return $this->respondValidatorFailed($validator); + } + + try { + Contact::where('account_id', auth()->user()->account_id) + ->where('id', $request->input('contact_id')) + ->firstOrFail(); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + return true; + } + + /** + * Delete a note. + * + * @param Request $request + * @return \Illuminate\Http\JsonResponse + */ + public function destroy(Request $request, $noteId) + { + try { + $note = Note::where('account_id', auth()->user()->account_id) + ->where('id', $noteId) + ->firstOrFail(); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + $note->delete(); + + return $this->respondObjectDeleted($note->id); + } + + /** + * Get the list of notes for the given contact. + * + * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection|\Illuminate\Http\JsonResponse + */ + public function notes(Request $request, $contactId) + { + try { + $contact = Contact::where('account_id', auth()->user()->account_id) + ->where('id', $contactId) + ->firstOrFail(); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + $notes = $contact->notes() + ->orderBy($this->sort, $this->sortDirection) + ->paginate($this->getLimitPerPage()); + + return NoteResource::collection($notes); + } +} diff --git a/app/Http/Controllers/Api/ApiPetController.php b/app/Http/Controllers/Api/ApiPetController.php new file mode 100644 index 0000000..959f785 --- /dev/null +++ b/app/Http/Controllers/Api/ApiPetController.php @@ -0,0 +1,182 @@ +user()->account_id) + ->orderBy($this->sort, $this->sortDirection) + ->paginate($this->getLimitPerPage()); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return PetResource::collection($pets); + } + + /** + * Get the detail of a given pet. + * + * @param Request $request + * @return PetResource|\Illuminate\Http\JsonResponse + */ + public function show(Request $request, $id) + { + try { + $pet = Pet::where('account_id', auth()->user()->account_id) + ->where('id', $id) + ->firstOrFail(); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + return new PetResource($pet); + } + + /** + * Store the pet. + * + * @param Request $request + * @return PetResource|\Illuminate\Http\JsonResponse + */ + public function store(Request $request) + { + $isvalid = $this->validateUpdate($request); + if ($isvalid !== true) { + return $isvalid; + } + + try { + $pet = Pet::create( + $request->except(['account_id']) + + ['account_id' => auth()->user()->account_id] + ); + } catch (QueryException $e) { + return $this->respondNotTheRightParameters(); + } + + return new PetResource($pet); + } + + /** + * Update the pet. + * + * @param Request $request + * @param int $petId + * @return PetResource|\Illuminate\Http\JsonResponse + */ + public function update(Request $request, $petId) + { + try { + $pet = Pet::where('account_id', auth()->user()->account_id) + ->where('id', $petId) + ->firstOrFail(); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + $isvalid = $this->validateUpdate($request); + if ($isvalid !== true) { + return $isvalid; + } + + try { + $pet->update($request->only(['pet_category_id', 'contact_id', 'name'])); + } catch (QueryException $e) { + return $this->respondNotTheRightParameters(); + } + + return new PetResource($pet); + } + + /** + * Validate the request for update. + * + * @param Request $request + * @return \Illuminate\Http\JsonResponse|true + */ + private function validateUpdate(Request $request) + { + // Validates basic fields to create the entry + $validator = Validator::make($request->all(), [ + 'pet_category_id' => 'integer|required|exists:pet_categories,id', + 'contact_id' => 'required|integer', + 'name' => 'max:255', + ]); + + if ($validator->fails()) { + return $this->respondValidatorFailed($validator); + } + + try { + Contact::where('account_id', auth()->user()->account_id) + ->where('id', $request->input('contact_id')) + ->firstOrFail(); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + return true; + } + + /** + * Delete a pet. + * + * @param Request $request + * @param int $petId + * @return \Illuminate\Http\JsonResponse + */ + public function destroy(Request $request, $petId) + { + try { + $pet = Pet::where('account_id', auth()->user()->account_id) + ->where('id', $petId) + ->firstOrFail(); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + $pet->delete(); + + return $this->respondObjectDeleted($pet->id); + } + + /** + * Get the list of pets for the given contact. + * + * @param Request $request + * @param int $contactId + * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection|\Illuminate\Http\JsonResponse + */ + public function pets(Request $request, $contactId) + { + try { + $contact = Contact::where('account_id', auth()->user()->account_id) + ->where('id', $contactId) + ->firstOrFail(); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + $pets = $contact->pets() + ->paginate($this->getLimitPerPage()); + + return PetResource::collection($pets); + } +} diff --git a/app/Http/Controllers/Api/ApiRelationshipController.php b/app/Http/Controllers/Api/ApiRelationshipController.php new file mode 100644 index 0000000..e47d59e --- /dev/null +++ b/app/Http/Controllers/Api/ApiRelationshipController.php @@ -0,0 +1,124 @@ +user()->account_id) + ->where('contact_is', $contactId) + ->get(); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + return RelationshipResource::collection($relationships); + } + + /** + * Get the detail of a given relationship. + * + * @param Request $request + * @return RelationshipResource|\Illuminate\Http\JsonResponse + */ + public function show(Request $request, $id) + { + try { + $relationship = Relationship::where('account_id', auth()->user()->account_id) + ->findOrFail($id); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + return new RelationshipResource($relationship); + } + + /** + * Create a new relationship. + * + * @param Request $request + * @return RelationshipResource|\Illuminate\Http\JsonResponse + */ + public function store(Request $request) + { + try { + $relationship = app(CreateRelationship::class)->execute([ + 'account_id' => auth()->user()->account_id, + 'contact_is' => $request->input('contact_is'), + 'of_contact' => $request->input('of_contact'), + 'relationship_type_id' => $request->input('relationship_type_id'), + ]); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } + + return new RelationshipResource($relationship); + } + + /** + * Update an existing relationship. + * + * @param Request $request + * @return \Illuminate\Http\JsonResponse|RelationshipResource + */ + public function update(Request $request, $relationshipId) + { + try { + $relationship = app(UpdateRelationship::class)->execute([ + 'account_id' => auth()->user()->account_id, + 'relationship_id' => $relationshipId, + 'relationship_type_id' => $request->input('relationship_type_id'), + ]); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } + + $relationship->refresh(); + + return new RelationshipResource($relationship); + } + + /** + * Delete a relationship. + * + * @param Request $request + * @return \Illuminate\Http\JsonResponse + */ + public function destroy(Request $request, $relationshipId) + { + try { + app(DestroyRelationship::class)->execute([ + 'account_id' => auth()->user()->account_id, + 'relationship_id' => $relationshipId, + ]); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } + + return $this->respondObjectDeleted($relationshipId); + } +} diff --git a/app/Http/Controllers/Api/ApiRelationshipTypeController.php b/app/Http/Controllers/Api/ApiRelationshipTypeController.php new file mode 100644 index 0000000..4d496b3 --- /dev/null +++ b/app/Http/Controllers/Api/ApiRelationshipTypeController.php @@ -0,0 +1,47 @@ +user()->account->relationshipTypes() + ->paginate($this->getLimitPerPage()); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + return RelationshipTypeResource::collection($relationshipTypes); + } + + /** + * Get the detail of a given relationship type. + * + * @param Request $request + * @return RelationshipTypeResource|\Illuminate\Http\JsonResponse + */ + public function show(Request $request, $id) + { + try { + $relationshipType = RelationshipType::where('account_id', auth()->user()->account_id) + ->findOrFail($id); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + return new RelationshipTypeResource($relationshipType); + } +} diff --git a/app/Http/Controllers/Api/ApiRelationshipTypeGroupController.php b/app/Http/Controllers/Api/ApiRelationshipTypeGroupController.php new file mode 100644 index 0000000..9bf9872 --- /dev/null +++ b/app/Http/Controllers/Api/ApiRelationshipTypeGroupController.php @@ -0,0 +1,53 @@ +user()->account->relationshipTypeGroups() + ->paginate($this->getLimitPerPage()); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + return RelationshipTypeGroupResource::collection($relationshipTypeGroups); + } + + /** + * Get the detail of a given relationship type group. + * + * @param Request $request + * @return RelationshipTypeGroupResource|\Illuminate\Http\JsonResponse + */ + public function show(Request $request, $id) + { + try { + $relationshipTypeGroup = RelationshipTypeGroup::where(static::ACCOUNT_ID, auth()->user()->account_id) + ->where('id', $id) + ->firstOrFail(); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + return new RelationshipTypeGroupResource($relationshipTypeGroup); + } +} diff --git a/app/Http/Controllers/Api/ApiReminderController.php b/app/Http/Controllers/Api/ApiReminderController.php new file mode 100644 index 0000000..fbbeb27 --- /dev/null +++ b/app/Http/Controllers/Api/ApiReminderController.php @@ -0,0 +1,182 @@ +user()->account->reminders() + ->orderBy($this->sort, $this->sortDirection) + ->paginate($this->getLimitPerPage()); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return ReminderResource::collection($reminders); + } + + /** + * Get the detail of a given reminder. + * + * @param Request $request + * @return ReminderResource|\Illuminate\Http\JsonResponse + */ + public function show(Request $request, $reminderId) + { + try { + $reminder = Reminder::where('account_id', auth()->user()->account_id) + ->where('id', $reminderId) + ->firstOrFail(); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + return new ReminderResource($reminder); + } + + /** + * Store the reminder. + * + * @param Request $request + * @return ReminderResource|\Illuminate\Http\JsonResponse + */ + public function store(Request $request) + { + try { + $reminder = app(CreateReminder::class)->execute( + $request->except(['account_id']) + + + [ + 'account_id' => auth()->user()->account_id, + ] + ); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return new ReminderResource($reminder); + } + + /** + * Update the reminder. + * + * @param Request $request + * @param int $reminderId + * @return ReminderResource|\Illuminate\Http\JsonResponse + */ + public function update(Request $request, $reminderId) + { + try { + $reminder = app(UpdateReminder::class)->execute( + $request->except(['account_id', 'reminder_id']) + + + [ + 'account_id' => auth()->user()->account_id, + 'reminder_id' => $reminderId, + ] + ); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return new ReminderResource($reminder); + } + + /** + * Delete a reminder. + * + * @param Request $request + * @return \Illuminate\Http\JsonResponse + */ + public function destroy(Request $request, int $reminderId) + { + try { + app(DestroyReminder::class)->execute([ + 'account_id' => auth()->user()->account_id, + 'reminder_id' => $reminderId, + ]); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return $this->respondObjectDeleted($reminderId); + } + + /** + * Get the list of reminders for the given contact. + * + * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection|\Illuminate\Http\JsonResponse + */ + public function reminders(Request $request, $contactId) + { + try { + $contact = Contact::where('account_id', auth()->user()->account_id) + ->where('id', $contactId) + ->firstOrFail(); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + $reminders = $contact->reminders() + ->orderBy($this->sort, $this->sortDirection) + ->paginate($this->getLimitPerPage()); + + return ReminderResource::collection($reminders); + } + + /** + * Get the reminders for the month given in parameter. + * - 0 means current month + * - 1 means month+1 + * - 2 means month+2... + * + * @param Request $request + * @param int $month + * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection|\Illuminate\Http\JsonResponse + */ + public function upcoming(Request $request, int $month = 0) + { + try { + $reminders = AccountHelper::getUpcomingRemindersForMonth( + auth()->user()->account, + $month + ); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return ReminderOutboxResource::collection($reminders); + } +} diff --git a/app/Http/Controllers/Api/ApiTagController.php b/app/Http/Controllers/Api/ApiTagController.php new file mode 100644 index 0000000..7c3f8d5 --- /dev/null +++ b/app/Http/Controllers/Api/ApiTagController.php @@ -0,0 +1,157 @@ +user()->account->tags() + ->orderBy($this->sort, $this->sortDirection) + ->paginate($this->getLimitPerPage()); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return TagResource::collection($tags); + } + + /** + * Get the detail of a given tag. + * + * @param Request $request + * @return TagResource|JsonResponse + */ + public function show(Request $request, $id) + { + try { + $tag = Tag::where('account_id', auth()->user()->account_id) + ->where('id', $id) + ->firstOrFail(); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + return new TagResource($tag); + } + + /** + * Store the tag. + * + * @param Request $request + * @return TagResource|JsonResponse + */ + public function store(Request $request) + { + try { + $tag = app(CreateTag::class)->execute( + $request->except(['account_id']) + + + [ + 'account_id' => auth()->user()->account_id, + ] + ); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } + + return new TagResource($tag); + } + + /** + * Update the tag. + * + * @param Request $request + * @param int $id + * @return TagResource|JsonResponse + */ + public function update(Request $request, int $id) + { + try { + $tag = app(UpdateTag::class)->execute( + $request->except(['account_id', 'tag_id']) + + + [ + 'tag_id' => $id, + 'account_id' => auth()->user()->account_id, + ] + ); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } + + return new TagResource($tag); + } + + /** + * Delete a tag. + * + * @param Request $request + * @return JsonResponse + */ + public function destroy(Request $request, $id) + { + try { + app(DestroyTag::class)->execute([ + 'tag_id' => $id, + 'account_id' => auth()->user()->account_id, + ]); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } + + return $this->respondObjectDeleted($id); + } + + /** + * Show all the contacts for a given tag. + * + * @param Request $request + * @param int $tagId + * @return JsonResponse|AnonymousResourceCollection + */ + public function contacts(Request $request, int $tagId) + { + try { + $contacts = auth()->user()->account->contacts() + ->real() + ->active() + ->whereHas('tags', function (Builder $query) use ($tagId) { + $query->where('id', $tagId); + }) + ->paginate($this->getLimitPerPage()); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return ContactWithContactFieldsResource::collection($contacts); + } +} diff --git a/app/Http/Controllers/Api/ApiTaskController.php b/app/Http/Controllers/Api/ApiTaskController.php new file mode 100644 index 0000000..a8d388f --- /dev/null +++ b/app/Http/Controllers/Api/ApiTaskController.php @@ -0,0 +1,149 @@ +user()->account->tasks() + ->orderBy($this->sort, $this->sortDirection) + ->paginate($this->getLimitPerPage()); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return TaskResource::collection($tasks); + } + + /** + * Get the detail of a given task. + * + * @param Request $request + * @return TaskResource|\Illuminate\Http\JsonResponse + */ + public function show(Request $request, $taskId) + { + try { + $task = Task::where('account_id', auth()->user()->account_id) + ->where('id', $taskId) + ->firstOrFail(); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + return new TaskResource($task); + } + + /** + * Store the task. + * + * @param Request $request + * @return TaskResource|\Illuminate\Http\JsonResponse + */ + public function store(Request $request) + { + try { + $task = app(CreateTask::class)->execute([ + 'account_id' => auth()->user()->account_id, + 'contact_id' => ($request->input('contact_id') == '' ? null : $request->input('contact_id')), + 'title' => $request->input('title'), + 'description' => ($request->input('description') == '' ? null : $request->input('description')), + ]); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } + + return new TaskResource($task); + } + + /** + * Update the task. + * + * @param Request $request + * @param int $taskId + * @return TaskResource|\Illuminate\Http\JsonResponse + */ + public function update(Request $request, $taskId) + { + try { + $task = app(UpdateTask::class)->execute( + $request->except(['account_id', 'task_id']) + + + [ + 'task_id' => $taskId, + 'account_id' => auth()->user()->account_id, + ] + ); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } + + return new TaskResource($task); + } + + /** + * Delete a task. + * + * @param Request $request + * @return \Illuminate\Http\JsonResponse + */ + public function destroy(Request $request, $taskId) + { + try { + app(DestroyTask::class)->execute([ + 'task_id' => $taskId, + 'account_id' => auth()->user()->account_id, + ]); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } + + return $this->respondObjectDeleted($taskId); + } + + /** + * Get the list of tasks for the given contact. + * + * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection|\Illuminate\Http\JsonResponse + */ + public function tasks(Request $request, $contactId) + { + try { + $contact = Contact::where('account_id', auth()->user()->account_id) + ->where('id', $contactId) + ->firstOrFail(); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + $tasks = $contact->tasks() + ->orderBy($this->sort, $this->sortDirection) + ->paginate($this->getLimitPerPage()); + + return TaskResource::collection($tasks); + } +} diff --git a/app/Http/Controllers/Api/Auth/OAuthController.php b/app/Http/Controllers/Api/Auth/OAuthController.php new file mode 100644 index 0000000..970b9ce --- /dev/null +++ b/app/Http/Controllers/Api/Auth/OAuthController.php @@ -0,0 +1,212 @@ +encrypter = $encrypter; + + if (config('app.debug')) { + Debugbar::disable(); + } + } + + /** + * Display a log in form for oauth accessToken. + * + * @param Request $request + * @return \Illuminate\View\View + */ + public function index(Request $request) + { + $request->session()->flush(); + + return view('auth.oauthlogin'); + } + + /** + * Log in a user and returns an accessToken. + * + * @param Request $request + * @return \Symfony\Component\HttpFoundation\Response|null + */ + public function login(Request $request): ?Response + { + $isvalid = $this->validateRequest($request); + if ($isvalid !== true) { + return $isvalid; + } + + $email = $request->input('email'); + $password = $request->input('password'); + + if (Auth::attempt(['email' => $email, 'password' => $password])) { + // The user is active, not suspended, and exists. + + $request->session()->put('oauth', true); + $request->session()->put('email', $email); + $request->session()->put('password', $this->encrypter->encrypt($password)); + + $this->fixRequest($request); + + // add intendedUrl for WebAuthn + Redirect::setIntendedUrl(route('oauth.verify')); + + return Route::respondWithRoute('oauth.verify'); + } + + return $this->respondUnauthorized(); + } + + /** + * Fix request parameters. + * + * @param Request $request + * @return void + */ + private function fixRequest(Request $request) + { + $request->setMethod('GET'); + $cookie = $request->cookies->get(config('session.cookie')); + $request->cookies->set(config('session.cookie'), $this->encrypter->encrypt($cookie)); + } + + /** + * Validate the request. + * + * @param Request $request + * @return \Illuminate\Http\JsonResponse|true + */ + private function validateRequest(Request $request) + { + $validator = Validator::make($request->all(), [ + 'email' => 'email|required', + 'password' => 'required', + ]); + + if ($validator->fails()) { + return $this->respondValidatorFailed($validator); + } + + // Check if email exists. If not respond with an Unauthorized, this way a hacker + // doesn't know if the login email exist or not, or if the password is wrong + $count = User::where('email', $request->input('email'))->count(); + if ($count === 0) { + return $this->respondUnauthorized(); + } + + return true; + } + + /** + * Log in a user and returns an accessToken. + * + * @param Request $request + * @return \Illuminate\Http\JsonResponse + */ + public function verify(Request $request): JsonResponse + { + $response = $this->handleVerify($request); + + Auth::logout(); + $request->session()->flush(); + + return $response ?: $this->respondUnauthorized(); + } + + /** + * Handle the verify request. + * + * @param Request $request + * @return \Illuminate\Http\JsonResponse|null + */ + private function handleVerify(Request $request): ?JsonResponse + { + if (! $request->session()->has('email') || ! $request->session()->has('password')) { + return null; + } + + $request->query->set('email', $request->session()->pull('email')); + $request->query->set('password', $this->encrypter->decrypt($request->session()->pull('password'))); + + $isvalid = $this->validateRequest($request); + if ($isvalid !== true) { + return $isvalid; + } + + try { + $token = $this->proxy([ + 'username' => $request->input('email'), + 'password' => $request->input('password'), + 'grantType' => 'password', + ]); + + return $this->respond($token); + } catch (\Exception $e) { + return null; + } + } + + /** + * Proxy a request to the OAuth server. + * + * @param array $data the data to send to the server + * @return array + * + * @throws \Safe\Exceptions\JsonException + */ + private function proxy(array $data = []): array + { + $url = App::runningUnitTests() ? Str::of(config('app.url'))->ltrim('/').'/oauth/token' : route('passport.token'); + /** @var \Illuminate\Http\Response */ + $response = app(Kernel::class)->handle(Request::create($url, 'POST', [ + 'grant_type' => $data['grantType'], + 'client_id' => config('passport.password_grant_client.id'), + 'client_secret' => config('passport.password_grant_client.secret'), + 'username' => $data['username'], + 'password' => $data['password'], + 'scope' => '', + ])); + + $data = json_decode($response->content()); + + return [ + 'access_token' => $data->access_token, + 'expires_in' => $data->expires_in, + ]; + } +} diff --git a/app/Http/Controllers/Api/Contact/ApiAddressController.php b/app/Http/Controllers/Api/Contact/ApiAddressController.php new file mode 100644 index 0000000..0bd1df1 --- /dev/null +++ b/app/Http/Controllers/Api/Contact/ApiAddressController.php @@ -0,0 +1,156 @@ +user()->account->addresses() + ->orderBy($this->sort, $this->sortDirection) + ->paginate($this->getLimitPerPage()); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return AddressResource::collection($addresses); + } + + /** + * Get the detail of a given address. + * + * @param Request $request + * @return AddressResource|\Illuminate\Http\JsonResponse + */ + public function show(Request $request, $id) + { + try { + $address = Address::where('account_id', auth()->user()->account_id) + ->where('id', $id) + ->firstOrFail(); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + return new AddressResource($address); + } + + /** + * Store the address. + * + * @param Request $request + * @return AddressResource|\Illuminate\Http\JsonResponse + */ + public function store(Request $request) + { + try { + $address = app(CreateAddress::class)->execute( + $request->except(['account_id']) + + + [ + 'account_id' => auth()->user()->account_id, + ] + ); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return new AddressResource($address); + } + + /** + * Update the address. + * + * @param Request $request + * @param int $addressId + * @return AddressResource|\Illuminate\Http\JsonResponse + */ + public function update(Request $request, $addressId) + { + try { + $address = app(UpdateAddress::class)->execute( + $request->except(['account_id', 'address_id']) + + + [ + 'account_id' => auth()->user()->account_id, + 'address_id' => $addressId, + ] + ); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return new AddressResource($address); + } + + /** + * Delete an address. + * + * @param Request $request + * @return \Illuminate\Http\JsonResponse + */ + public function destroy(Request $request, int $addressId) + { + try { + app(DestroyAddress::class)->execute([ + 'account_id' => auth()->user()->account_id, + 'address_id' => $addressId, + ]); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return $this->respondObjectDeleted($addressId); + } + + /** + * Get the list of addresses for the given contact. + * + * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection|\Illuminate\Http\JsonResponse + */ + public function addresses(Request $request, $contactId) + { + try { + $contact = Contact::where('account_id', auth()->user()->account_id) + ->where('id', $contactId) + ->firstOrFail(); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + $addresses = $contact->addresses() + ->paginate($this->getLimitPerPage()); + + return AddressResource::collection($addresses); + } +} diff --git a/app/Http/Controllers/Api/Contact/ApiAuditLogController.php b/app/Http/Controllers/Api/Contact/ApiAuditLogController.php new file mode 100644 index 0000000..5f11393 --- /dev/null +++ b/app/Http/Controllers/Api/Contact/ApiAuditLogController.php @@ -0,0 +1,42 @@ +user()->account_id) + ->where('id', $contactId) + ->firstOrFail(); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + try { + $logs = $contact->logs() + ->paginate($this->getLimitPerPage()); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return AuditLogResource::collection($logs); + } +} diff --git a/app/Http/Controllers/Api/Contact/ApiAvatarController.php b/app/Http/Controllers/Api/Contact/ApiAvatarController.php new file mode 100644 index 0000000..28c27c3 --- /dev/null +++ b/app/Http/Controllers/Api/Contact/ApiAvatarController.php @@ -0,0 +1,43 @@ +execute( + $request->except(['account_id', 'contact_id']) + + + [ + 'account_id' => auth()->user()->account_id, + 'contact_id' => $contactId, + ] + ); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return new ContactResource($contact); + } +} diff --git a/app/Http/Controllers/Api/Contact/ApiCallController.php b/app/Http/Controllers/Api/Contact/ApiCallController.php new file mode 100644 index 0000000..1d4cc46 --- /dev/null +++ b/app/Http/Controllers/Api/Contact/ApiCallController.php @@ -0,0 +1,162 @@ +user()->account->calls() + ->orderBy($this->sort, $this->sortDirection) + ->paginate($this->getLimitPerPage()); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return CallResource::collection($calls)->additional(['meta' => [ + 'statistics' => AccountHelper::getYearlyCallStatistics(auth()->user()->account), + ]]); + } + + /** + * Get the detail of a given call. + * + * @param Request $request + * @return CallResource|\Illuminate\Http\JsonResponse + */ + public function show(Request $request, $callId) + { + try { + $call = Call::where('account_id', auth()->user()->account_id) + ->where('id', $callId) + ->firstOrFail(); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + return new CallResource($call); + } + + /** + * Store the call. + * + * @param Request $request + * @return CallResource|\Illuminate\Http\JsonResponse + */ + public function store(Request $request) + { + try { + $call = app(CreateCall::class)->execute( + $request->except(['account_id']) + + + [ + 'account_id' => auth()->user()->account_id, + ] + ); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return new CallResource($call); + } + + /** + * Update a call. + * + * @param Request $request + * @param int $callId + * @return CallResource|\Illuminate\Http\JsonResponse + */ + public function update(Request $request, $callId) + { + try { + $call = app(UpdateCall::class)->execute( + $request->except(['account_id', 'call_id']) + + + [ + 'account_id' => auth()->user()->account_id, + 'call_id' => $callId, + ] + ); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return new CallResource($call); + } + + /** + * Delete a call. + * + * @param Request $request + * @return \Illuminate\Http\JsonResponse + */ + public function destroy(Request $request, int $callId) + { + try { + app(DestroyCall::class)->execute([ + 'account_id' => auth()->user()->account_id, + 'call_id' => $callId, + ]); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return $this->respondObjectDeleted($callId); + } + + /** + * Get the list of calls for a given contact. + * + * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection|\Illuminate\Http\JsonResponse + */ + public function calls(Request $request, $contactId) + { + try { + $contact = Contact::where('account_id', auth()->user()->account_id) + ->where('id', $contactId) + ->firstOrFail(); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + $calls = $contact->calls() + ->orderBy($this->sort, $this->sortDirection) + ->paginate($this->getLimitPerPage()); + + return CallResource::collection($calls)->additional(['meta' => [ + 'statistics' => AccountHelper::getYearlyCallStatistics(auth()->user()->account), + ]]); + } +} diff --git a/app/Http/Controllers/Api/Contact/ApiConversationController.php b/app/Http/Controllers/Api/Contact/ApiConversationController.php new file mode 100644 index 0000000..325447a --- /dev/null +++ b/app/Http/Controllers/Api/Contact/ApiConversationController.php @@ -0,0 +1,162 @@ +user()->account->conversations() + ->orderBy($this->sort, $this->sortDirection) + ->paginate($this->getLimitPerPage()); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return ConversationResource::collection($conversations); + } + + /** + * Get the list of conversations for a specific contact. + * + * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection|\Illuminate\Http\JsonResponse + */ + public function conversations(Request $request, $contactId) + { + try { + Contact::where('account_id', auth()->user()->account_id) + ->where('id', $contactId) + ->firstOrFail(); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + try { + $conversations = auth()->user()->account->conversations() + ->where('contact_id', $contactId) + ->orderBy($this->sort, $this->sortDirection) + ->paginate($this->getLimitPerPage()); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return ConversationResource::collection($conversations); + } + + /** + * Get the detail of a given conversation. + * + * @param Request $request + * @return ConversationResource|\Illuminate\Http\JsonResponse + */ + public function show(Request $request, $conversationId) + { + try { + $conversation = Conversation::where('account_id', auth()->user()->account_id) + ->findOrFail($conversationId); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + return new ConversationResource($conversation); + } + + /** + * Store the conversation. + * + * @param Request $request + * @return ConversationResource|\Illuminate\Http\JsonResponse + */ + public function store(Request $request) + { + try { + $conversation = app(CreateConversation::class)->execute( + $request->except(['account_id']) + + + [ + 'account_id' => auth()->user()->account_id, + ] + ); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return new ConversationResource($conversation); + } + + /** + * Update the conversation. + * + * @param Request $request + * @param int $conversationId + * @return ConversationResource|\Illuminate\Http\JsonResponse + */ + public function update(Request $request, $conversationId) + { + try { + $conversation = app(UpdateConversation::class)->execute( + $request->except(['account_id', 'conversation_id']) + + + [ + 'account_id' => auth()->user()->account_id, + 'conversation_id' => $conversationId, + ] + ); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return new ConversationResource($conversation); + } + + /** + * Destroy the conversation. + * + * @param Request $request + * @param int $conversationId + * @return \Illuminate\Http\JsonResponse + */ + public function destroy(Request $request, int $conversationId) + { + try { + app(DestroyConversation::class)->execute([ + 'account_id' => auth()->user()->account_id, + 'conversation_id' => $conversationId, + ]); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return $this->respondObjectDeleted($conversationId); + } +} diff --git a/app/Http/Controllers/Api/Contact/ApiDocumentController.php b/app/Http/Controllers/Api/Contact/ApiDocumentController.php new file mode 100644 index 0000000..ca04970 --- /dev/null +++ b/app/Http/Controllers/Api/Contact/ApiDocumentController.php @@ -0,0 +1,146 @@ +middleware('limitations')->only('store'); + parent::__construct(); + } + + /** + * Get the list of documents. + * + * @param Request $request + * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection|\Illuminate\Http\JsonResponse + */ + public function index(Request $request) + { + try { + $documents = auth()->user()->account->documents() + ->orderBy($this->sort, $this->sortDirection) + ->paginate($this->getLimitPerPage()); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return DocumentResource::collection($documents); + } + + /** + * Get the list of documents for a specific contact. + * + * @param Request $request + * @param int $contactId + * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection|\Illuminate\Http\JsonResponse + */ + public function contact(Request $request, $contactId) + { + try { + Contact::where('account_id', auth()->user()->account_id) + ->findOrFail($contactId); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + try { + $documents = auth()->user()->account->documents() + ->where('contact_id', $contactId) + ->orderBy($this->sort, $this->sortDirection) + ->paginate($this->getLimitPerPage()); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return DocumentResource::collection($documents); + } + + /** + * Get the detail of a given document. + * + * @param Request $request + * @param int $documentId + * @return DocumentResource|\Illuminate\Http\JsonResponse + */ + public function show(Request $request, $documentId) + { + try { + $document = Document::where('account_id', auth()->user()->account_id) + ->findOrFail($documentId); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + return new DocumentResource($document); + } + + /** + * Store a document. + * + * @param Request $request + * @return DocumentResource|\Illuminate\Http\JsonResponse + */ + public function store(Request $request) + { + try { + $document = app(UploadDocument::class)->execute( + $request->except(['account_id']) + + + [ + 'account_id' => auth()->user()->account_id, + ] + ); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return new DocumentResource($document); + } + + /** + * Destroy a document. + * + * @param Request $request + * @param int $documentId + * @return \Illuminate\Http\JsonResponse + */ + public function destroy(Request $request, int $documentId) + { + try { + app(DestroyDocument::class)->execute([ + 'account_id' => auth()->user()->account_id, + 'document_id' => $documentId, + ]); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return $this->respondObjectDeleted($documentId); + } +} diff --git a/app/Http/Controllers/Api/Contact/ApiLifeEventController.php b/app/Http/Controllers/Api/Contact/ApiLifeEventController.php new file mode 100644 index 0000000..99efea8 --- /dev/null +++ b/app/Http/Controllers/Api/Contact/ApiLifeEventController.php @@ -0,0 +1,121 @@ +user()->account->lifeEvents() + ->orderBy($this->sort, $this->sortDirection) + ->paginate($this->getLimitPerPage()); + + return LifeEventResource::collection($lifeEvents); + } + + /** + * Get the detail of a given life event. + * + * @param Request $request + * @return LifeEventResource|\Illuminate\Http\JsonResponse + */ + public function show(Request $request, $lifeEventId) + { + try { + $lifeEvent = LifeEvent::where('account_id', auth()->user()->account_id) + ->findOrFail($lifeEventId); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + return new LifeEventResource($lifeEvent); + } + + /** + * Store the life event. + * + * @param Request $request + * @return LifeEventResource|\Illuminate\Http\JsonResponse + */ + public function store(Request $request) + { + try { + $lifeEvent = app(CreateLifeEvent::class)->execute( + $request->except(['account_id']) + + + [ + 'account_id' => auth()->user()->account_id, + ] + ); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } + + return new LifeEventResource($lifeEvent); + } + + /** + * Update the life event. + * + * @param Request $request + * @param int $lifeEventId + * @return LifeEventResource|\Illuminate\Http\JsonResponse + */ + public function update(Request $request, $lifeEventId) + { + try { + $lifeEvent = app(UpdateLifeEvent::class)->execute( + $request->except(['account_id', 'life_event_id']) + + + [ + 'account_id' => auth()->user()->account_id, + 'life_event_id' => $lifeEventId, + ] + ); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } + + return new LifeEventResource($lifeEvent); + } + + /** + * Destroy the life event. + * + * @param Request $request + * @param int $lifeEventId + * @return \Illuminate\Http\JsonResponse + */ + public function destroy(Request $request, int $lifeEventId) + { + try { + app(DestroyLifeEvent::class)->execute([ + 'account_id' => auth()->user()->account_id, + 'life_event_id' => $lifeEventId, + ]); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + return $this->respondObjectDeleted($lifeEventId); + } +} diff --git a/app/Http/Controllers/Api/Contact/ApiMessageController.php b/app/Http/Controllers/Api/Contact/ApiMessageController.php new file mode 100644 index 0000000..2821483 --- /dev/null +++ b/app/Http/Controllers/Api/Contact/ApiMessageController.php @@ -0,0 +1,126 @@ +respondNotFound(); + } + + try { + app(AddMessageToConversation::class)->execute( + $request->except(['account_id', 'conversation_id', 'contact_id']) + + + [ + 'account_id' => auth()->user()->account_id, + 'conversation_id' => $conversation->id, + 'contact_id' => $conversation->contact_id, + ] + ); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return new ConversationResource($conversation); + } + + /** + * Update the message. + * + * @param Request $request + * @param int $conversationId + * @param int $messageId + * @return ConversationResource|\Illuminate\Http\JsonResponse + */ + public function update(Request $request, int $conversationId, int $messageId) + { + try { + $conversation = Conversation::findOrFail($conversationId); + $message = Message::findOrFail($messageId); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + try { + app(UpdateMessage::class)->execute( + $request->except(['account_id', 'conversation_id', 'message_id', 'contact_id']) + + + [ + 'account_id' => auth()->user()->account_id, + 'conversation_id' => $conversationId, + 'message_id' => $message->id, + 'contact_id' => $conversation->contact_id, + ] + ); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return new ConversationResource($conversation); + } + + /** + * Destroy the message. + * + * @param Request $request + * @param int $conversationId + * @param int $messageId + * @return \Illuminate\Http\JsonResponse + */ + public function destroy(Request $request, int $conversationId, int $messageId) + { + try { + Conversation::findOrFail($conversationId); + Message::findOrFail($messageId); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + try { + app(DestroyMessage::class)->execute([ + 'account_id' => auth()->user()->account_id, + 'conversation_id' => $conversationId, + 'message_id' => $messageId, + ]); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return $this->respondObjectDeleted($messageId); + } +} diff --git a/app/Http/Controllers/Api/Contact/ApiOccupationController.php b/app/Http/Controllers/Api/Contact/ApiOccupationController.php new file mode 100644 index 0000000..c497123 --- /dev/null +++ b/app/Http/Controllers/Api/Contact/ApiOccupationController.php @@ -0,0 +1,134 @@ +user()->account->occupations() + ->orderBy($this->sort, $this->sortDirection) + ->paginate($this->getLimitPerPage()); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return OccupationResource::collection($occupations); + } + + /** + * Get the detail of a given occupation. + * + * @param Request $request + * @return OccupationResource|\Illuminate\Http\JsonResponse + */ + public function show(Request $request, $occupationId) + { + try { + $occupation = Occupation::where('account_id', auth()->user()->account_id) + ->where('id', $occupationId) + ->firstOrFail(); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + return new OccupationResource($occupation); + } + + /** + * Store the occupation. + * + * @param Request $request + * @return OccupationResource|\Illuminate\Http\JsonResponse + */ + public function store(Request $request) + { + try { + $occupation = app(CreateOccupation::class)->execute( + $request->except(['account_id']) + + + [ + 'account_id' => auth()->user()->account_id, + ] + ); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return new OccupationResource($occupation); + } + + /** + * Update an occupation. + * + * @param Request $request + * @param int $occupationId + * @return OccupationResource|\Illuminate\Http\JsonResponse + */ + public function update(Request $request, $occupationId) + { + try { + $occupation = app(UpdateOccupation::class)->execute( + $request->except(['account_id', 'occupation_id']) + + + [ + 'account_id' => auth()->user()->account_id, + 'occupation_id' => $occupationId, + ] + ); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return new OccupationResource($occupation); + } + + /** + * Delete an occupation. + * + * @param Request $request + * @return \Illuminate\Http\JsonResponse + */ + public function destroy(Request $request, int $occupationId) + { + try { + app(DestroyOccupation::class)->execute([ + 'account_id' => auth()->user()->account_id, + 'occupation_id' => $occupationId, + ]); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return $this->respondObjectDeleted($occupationId); + } +} diff --git a/app/Http/Controllers/Api/Contact/ApiPhotoController.php b/app/Http/Controllers/Api/Contact/ApiPhotoController.php new file mode 100644 index 0000000..8314227 --- /dev/null +++ b/app/Http/Controllers/Api/Contact/ApiPhotoController.php @@ -0,0 +1,134 @@ +user()->account->photos() + ->orderBy($this->sort, $this->sortDirection) + ->paginate($this->getLimitPerPage()); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return PhotoResource::collection($photos); + } + + /** + * Get the list of photos for a specific contact. + * + * @param Request $request + * @param int $contactId + * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection|\Illuminate\Http\JsonResponse + */ + public function contact(Request $request, $contactId) + { + try { + $contact = Contact::where('account_id', auth()->user()->account_id) + ->findOrFail($contactId); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + try { + $photos = $contact->photos() + ->orderBy($this->sort, $this->sortDirection) + ->paginate($this->getLimitPerPage()); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return PhotoResource::collection($photos); + } + + /** + * Get the detail of a given photo. + * + * @param Request $request + * @param int $photoId + * @return PhotoResource|\Illuminate\Http\JsonResponse + */ + public function show(Request $request, $photoId) + { + try { + $photo = Photo::where('account_id', auth()->user()->account_id) + ->findOrFail($photoId); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + return new PhotoResource($photo); + } + + /** + * Store a photo. + * + * @param Request $request + * @return PhotoResource|\Illuminate\Http\JsonResponse + */ + public function store(Request $request) + { + try { + $photo = app(UploadPhoto::class)->execute( + $request->except(['account_id']) + + + [ + 'account_id' => auth()->user()->account_id, + ] + ); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return new PhotoResource($photo); + } + + /** + * Destroy a photo. + * + * @param Request $request + * @param int $photoId + * @return \Illuminate\Http\JsonResponse + */ + public function destroy(Request $request, int $photoId) + { + try { + app(DestroyPhoto::class)->execute([ + 'account_id' => auth()->user()->account_id, + 'photo_id' => $photoId, + ]); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } catch (ValidationException $e) { + return $this->respondValidatorFailed($e->validator); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return $this->respondObjectDeleted($photoId); + } +} diff --git a/app/Http/Controllers/Api/Misc/ApiCountryController.php b/app/Http/Controllers/Api/Misc/ApiCountryController.php new file mode 100644 index 0000000..1a3f0af --- /dev/null +++ b/app/Http/Controllers/Api/Misc/ApiCountryController.php @@ -0,0 +1,29 @@ +user()->account->auditLogs() + ->paginate($this->getLimitPerPage()); + } catch (QueryException $e) { + return $this->respondInvalidQuery(); + } + + return AuditLogResource::collection($logs); + } +} diff --git a/app/Http/Controllers/Api/Settings/ApiComplianceController.php b/app/Http/Controllers/Api/Settings/ApiComplianceController.php new file mode 100644 index 0000000..d266e49 --- /dev/null +++ b/app/Http/Controllers/Api/Settings/ApiComplianceController.php @@ -0,0 +1,42 @@ +paginate($this->getLimitPerPage()); + + return ComplianceResource::collection($terms); + } + + /** + * Get the detail of a given term. + * + * @param Request $request + * @return ComplianceResource|\Illuminate\Http\JsonResponse + */ + public function show(Request $request, $termId) + { + try { + $term = Term::where('id', $termId) + ->firstOrFail(); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + return new ComplianceResource($term); + } +} diff --git a/app/Http/Controllers/Api/Settings/ApiContactFieldTypeController.php b/app/Http/Controllers/Api/Settings/ApiContactFieldTypeController.php new file mode 100644 index 0000000..777b5c6 --- /dev/null +++ b/app/Http/Controllers/Api/Settings/ApiContactFieldTypeController.php @@ -0,0 +1,163 @@ +user()->account->contactFieldTypes() + ->paginate($this->getLimitPerPage()); + + return ContactFieldTypeResource::collection($contactFieldTypes); + } + + /** + * Get the detail of a given contact field type. + * + * @param Request $request + * @return ContactFieldTypeResource|\Illuminate\Http\JsonResponse + */ + public function show(Request $request, $contactFieldTypeId) + { + try { + $contactFieldType = ContactFieldType::where('account_id', auth()->user()->account_id) + ->where('id', $contactFieldTypeId) + ->firstOrFail(); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + return new ContactFieldTypeResource($contactFieldType); + } + + /** + * Store the contactfieldtype. + * + * @param Request $request + * @return ContactFieldTypeResource|\Illuminate\Http\JsonResponse + */ + public function store(Request $request) + { + $isvalid = $this->validateUpdate($request); + if ($isvalid !== true) { + return $isvalid; + } + + try { + $contactFieldType = ContactFieldType::create( + $request->except(['account_id']) + + ['account_id' => auth()->user()->account_id] + ); + } catch (QueryException $e) { + return $this->respondNotTheRightParameters(); + } + + return new ContactFieldTypeResource($contactFieldType); + } + + /** + * Update the contact field type. + * + * @param Request $request + * @param int $contactFieldTypeId + * @return ContactFieldTypeResource|\Illuminate\Http\JsonResponse + */ + public function update(Request $request, $contactFieldTypeId) + { + try { + $contactFieldType = ContactFieldType::where('account_id', auth()->user()->account_id) + ->where('id', $contactFieldTypeId) + ->firstOrFail(); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + $isvalid = $this->validateUpdate($request); + if ($isvalid !== true) { + return $isvalid; + } + + // Update the contactfieldtype itself + try { + $contactFieldType->update( + $request->only([ + 'name', + 'fontawesome_icon', + 'protocol', + 'delible', + 'type', + ]) + ); + } catch (QueryException $e) { + return $this->respondNotTheRightParameters(); + } + + return new ContactFieldTypeResource($contactFieldType); + } + + /** + * Validate the request for update. + * + * @param Request $request + * @return \Illuminate\Http\JsonResponse|true + */ + private function validateUpdate(Request $request) + { + // Validates basic fields to create the entry + $validator = Validator::make($request->all(), [ + 'name' => 'required|max:255', + 'fontawesome_icon' => 'nullable|max:255', + 'protocol' => 'nullable|max:255', + 'delible' => 'integer', + 'type' => 'nullable|max:255', + ]); + + if ($validator->fails()) { + return $this->respondValidatorFailed($validator); + } + + return true; + } + + /** + * Delete an contactfieldtype. + * + * @param Request $request + * @return \Illuminate\Http\JsonResponse + */ + public function destroy(Request $request, $contactFieldTypeId) + { + try { + $contactFieldType = ContactFieldType::where('account_id', auth()->user()->account_id) + ->where('id', $contactFieldTypeId) + ->firstOrFail(); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + $contactFields = auth()->user()->account->contactFields + ->where('contact_field_type_id', $contactFieldTypeId); + + foreach ($contactFields as $contactField) { + $contactField->delete(); + } + + $contactFieldType->delete(); + + return $this->respondObjectDeleted($contactFieldType->id); + } +} diff --git a/app/Http/Controllers/Api/Settings/ApiCurrencyController.php b/app/Http/Controllers/Api/Settings/ApiCurrencyController.php new file mode 100644 index 0000000..e9e7a38 --- /dev/null +++ b/app/Http/Controllers/Api/Settings/ApiCurrencyController.php @@ -0,0 +1,41 @@ +getLimitPerPage()); + + return CurrencyResource::collection($currencies); + } + + /** + * Get the detail of a given currency. + * + * @param Request $request + * @return CurrencyResource|\Illuminate\Http\JsonResponse + */ + public function show(Request $request, $currencyId) + { + try { + $currency = Currency::findOrFail($currencyId); + } catch (ModelNotFoundException $e) { + return $this->respondNotFound(); + } + + return new CurrencyResource($currency); + } +} diff --git a/app/Http/Controllers/Api/Statistics/ApiStatisticsController.php b/app/Http/Controllers/Api/Statistics/ApiStatisticsController.php new file mode 100644 index 0000000..be60d41 --- /dev/null +++ b/app/Http/Controllers/Api/Statistics/ApiStatisticsController.php @@ -0,0 +1,57 @@ +respondNotFound(); + } + + // Collecting statistics + $statistic = Statistic::orderBy('created_at', 'desc')->first(); + $instance = Instance::first(); + + // Get the date of the monday of last week + $dateMondayLastWeek = now()->subDays(7); + $dateMondayLastWeek = $dateMondayLastWeek->startOfWeek(); + + // Get the date of the sunday of last week + $dateSundayLastWeek = now()->subDays(7); + $dateSundayLastWeek = $dateSundayLastWeek->endOfWeek(); + + // Get the number of users last monday + $instanceLastMonday = Statistic::whereDate('created_at', '=', $dateMondayLastWeek->toDateString())->first(); + $instanceLastSunday = Statistic::whereDate('created_at', '=', $dateSundayLastWeek->toDateString())->first(); + + $numberNewUsers = 0; + if ($instanceLastMonday && $instanceLastSunday) { + $numberNewUsers = $instanceLastSunday->number_of_users - $instanceLastMonday->number_of_users; + } + + $statistics = collect(); + $statistics->push([ + 'instance_creation_date' => DateHelper::getTimestamp($instance->created_at), + 'number_of_contacts' => ($statistic ? $statistic->number_of_contacts : 0), + 'number_of_users' => ($statistic ? $statistic->number_of_users : 0), + 'number_of_activities' => ($statistic ? $statistic->number_of_activities : 0), + 'number_of_reminders' => ($statistic ? $statistic->number_of_reminders : 0), + 'number_of_new_users_last_week' => $numberNewUsers, + ]); + + return $statistics; + } +} diff --git a/app/Http/Controllers/Auth/EmailChangeController.php b/app/Http/Controllers/Auth/EmailChangeController.php new file mode 100644 index 0000000..a3bff0d --- /dev/null +++ b/app/Http/Controllers/Auth/EmailChangeController.php @@ -0,0 +1,118 @@ +user(); + if ($user && + $user instanceof User && + ! $user->hasVerifiedEmail()) { + return view('auth.emailchange1') + ->with('email', $user->email); + } + + return redirect()->route('login'); + } + + /** + * Display a listing of the resource. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\View\View + */ + public function index(Request $request): \Illuminate\View\View + { + $user = auth()->user(); + + return view('auth.emailchange2') + ->with('email', $user->email); + } + + /** + * Change user email. + * + * @param EmailChangeRequest $request + * @return \Illuminate\Http\RedirectResponse + */ + public function save(EmailChangeRequest $request) + { + $response = $this->validateAndEmailChange($request); + + return $response == 'auth.email_changed' + ? $this->sendChangedResponse($response) + : $this->sendChangedFailedResponse($response); + } + + /** + * Validate a password change request and update password of the user. + * + * @param EmailChangeRequest $request + * @return mixed + */ + protected function validateAndEmailChange(EmailChangeRequest $request) + { + $user = $request->user(); + + app(EmailChange::class)->execute([ + 'account_id' => $user->account_id, + 'email' => $request->input('newmail'), + 'user_id' => $user->id, + ]); + + // Logout the user + Auth::guard()->logout(); + $request->session()->invalidate(); + + return 'auth.email_changed'; + } + + /** + * Get the response for a successful password changed. + * + * @param string $response + * @return \Illuminate\Http\RedirectResponse + */ + protected function sendChangedResponse($response) + { + return redirect()->route('login') + ->with('status', trans($response)); + } + + /** + * Get the response for a failed password. + * + * @param string $response + * @return \Illuminate\Http\RedirectResponse + */ + protected function sendChangedFailedResponse($response) + { + return redirect()->route('login') + ->withErrors(trans($response)); + } +} diff --git a/app/Http/Controllers/Auth/ForgotPasswordController.php b/app/Http/Controllers/Auth/ForgotPasswordController.php new file mode 100644 index 0000000..465c39c --- /dev/null +++ b/app/Http/Controllers/Auth/ForgotPasswordController.php @@ -0,0 +1,22 @@ +middleware('guest'); + } + + /** + * Display the specified resource. + * + * @param string $key + * @return \Illuminate\View\View|\Illuminate\Contracts\View\Factory|\Illuminate\Http\RedirectResponse + */ + public function show($key) + { + if (Auth::check()) { + return redirect()->route('loginRedirect'); + } + + $invitation = Invitation::where('invitation_key', $key) + ->firstOrFail(); + + return view('settings.users.accept') + ->withKey($key) + ->withEmail($invitation->email); + } + + /** + * Get a validator for an incoming registration request. + * + * @param array $data + * @return \Illuminate\Contracts\Validation\Validator + */ + protected function validator(array $data) + { + return Validator::make($data, [ + 'last_name' => 'required|max:255', + 'first_name' => 'required|max:255', + 'email' => 'required|email|max:255|unique:users', + 'email_security' => 'required', + 'password' => ['required', 'confirmed', PasswordRules::defaults()], + 'policy' => 'required', + ]); + } + + /** + * Store the specified resource. + * + * @param Request $request + * @param string $key + * @return null|\Illuminate\Http\RedirectResponse + */ + public function store(Request $request, $key) + { + $this->validator($request->all())->validate(); + + $invitation = Invitation::where('invitation_key', $key) + ->firstOrFail(); + + // as a security measure, make sure that the new user provides the email + // of the person who has invited him/her. + if ($request->input('email_security') != $invitation->invitedBy->email) { + return redirect()->back()->withErrors(trans('settings.users_error_email_not_similar'))->withInput(); + } + + event(new Registered($user = $this->create($request->all(), $invitation))); + + $invitation->delete(); + + /** @var \Illuminate\Contracts\Auth\StatefulGuard */ + $guard = Auth::guard(); + $guard->login($user); + + $this->registered($request, $user); + + return redirect($this->redirectPath()); + } + + /** + * Create a new user instance after a valid registration. + * + * @param array $data + * @param mixed $invitation + * @return \App\Models\User\User + */ + protected function create(array $data, $invitation) + { + $user = app(CreateUser::class)->execute([ + 'account_id' => $invitation->account_id, + 'first_name' => $data['first_name'], + 'last_name' => $data['last_name'], + 'email' => $data['email'], + 'password' => $data['password'], + 'locale' => $invitation->invitedBy->locale, + 'ip_address' => RequestHelper::ip(), + ]); + $user->invited_by_user_id = $invitation->invited_by_user_id; + $user->save(); + + // send me an alert + SendNewUserAlert::dispatch($user); + + return $user; + } + + /** + * The user has been registered. + * + * @param \Illuminate\Http\Request $request + * @param mixed $user + * @return void + */ + protected function registered(Request $request, $user) + { + if (! config('monica.signup_double_optin')) { + // if signup_double_optin is disabled, skip the confirm email part + $user->markEmailAsVerified(); + } + } +} diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php new file mode 100644 index 0000000..cb33945 --- /dev/null +++ b/app/Http/Controllers/Auth/LoginController.php @@ -0,0 +1,50 @@ +middleware('guest', ['except' => 'logout']); + } + + public function showLoginOrRegister() + { + $first = ! InstanceHelper::hasAtLeastOneAccount(); + if ($first) { + return redirect()->route('register'); + } + + return $this->showLoginForm(); + } +} diff --git a/app/Http/Controllers/Auth/PasswordChangeController.php b/app/Http/Controllers/Auth/PasswordChangeController.php new file mode 100644 index 0000000..97cf3f3 --- /dev/null +++ b/app/Http/Controllers/Auth/PasswordChangeController.php @@ -0,0 +1,157 @@ +only( + 'password_current', 'password', 'password_confirmation' + ); + } + + /** + * Change user password. + * + * @param \App\Http\Requests\PasswordChangeRequest $request + */ + public function passwordChange(PasswordChangeRequest $request) + { + $credentials = $this->credentials($request); + + $response = $this->validateAndPasswordChange($credentials); + + return $response === 'passwords.changed' + ? $this->sendChangedResponse($response) + : $this->sendChangedFailedResponse($response); + } + + /** + * Validate a password change request and update password of the user. + * + * @param array $credentials + * @return string|Authenticatable + */ + protected function validateAndPasswordChange($credentials) + { + $user = $this->validateChange($credentials); + if (! $user instanceof CanResetPassword) { + return $user; + } + + if ($user instanceof User) { + $this->setNewPassword($user, $credentials['password']); + } + + return 'passwords.changed'; + } + + /** + * Validate a password change request with the given credentials. + * + * @param array $credentials + * @return string|Authenticatable + * + * @throws \UnexpectedValueException + */ + protected function validateChange(array $credentials) + { + if (is_null($user = $this->getUser($credentials))) { + return 'passwords.invalid'; + } + + return $user; + } + + /** + * Get the user with the given credentials. + * + * @param array $credentials + * @return null|Authenticatable + */ + protected function getUser(array $credentials): ?Authenticatable + { + /** @var User */ + $user = Auth::user(); + + // Using current email from user, and current password sent with the request to authenticate the user + if (! Auth::attempt([ + 'email' => $user->getEmailForPasswordReset(), + 'password' => $credentials['password_current'], + ])) { + // authentication fails + return null; + } + + return $user; + } + + /** + * Set the new password if all validation has passed. + * + * @param User $user + * @param string $password + * @return void + */ + protected function setNewPassword($user, $password) + { + $user->password = Hash::make($password); + + $user->setRememberToken(Str::random(60)); + + $user->save(); + + event(new PasswordReset($user)); + + Auth::guard()->login($user); + } + + /** + * Get the response for a successful password change. + * + * @param string $response + * @return \Illuminate\Http\RedirectResponse + */ + protected function sendChangedResponse($response) + { + return redirect($this->redirectPath()) + ->with('status', trans($response)); + } + + /** + * Get the response for a failed password change. + * + * @param string $response + * @return \Illuminate\Http\RedirectResponse + */ + protected function sendChangedFailedResponse($response) + { + return redirect($this->redirectPath()) + ->withErrors(['password' => trans($response)]); + } +} diff --git a/app/Http/Controllers/Auth/RecoveryLoginController.php b/app/Http/Controllers/Auth/RecoveryLoginController.php new file mode 100644 index 0000000..489cd79 --- /dev/null +++ b/app/Http/Controllers/Auth/RecoveryLoginController.php @@ -0,0 +1,69 @@ +all(), [ + 'recovery' => 'required', + ])->validate(); + + $user = auth()->user(); + $recovery = $request->input('recovery'); + + if ($user instanceof \App\Models\User\User && + $user->recoveryChallenge($recovery)) { + $this->fireLoginEvent($user); + } else { + abort(403); + } + + return redirect($this->redirectPath()); + } + + /** + * Fire the login event. + * + * @param \Illuminate\Contracts\Auth\Authenticatable $user + * @return void + */ + protected function fireLoginEvent($user) + { + Event::dispatch(new RecoveryLogin($user)); + } +} diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php new file mode 100644 index 0000000..4ba8d14 --- /dev/null +++ b/app/Http/Controllers/Auth/RegisterController.php @@ -0,0 +1,140 @@ +middleware('guest'); + } + + /** + * Show the application registration form. + * + * @return \Illuminate\View\View + */ + public function showRegistrationForm(Request $request) + { + $first = ! InstanceHelper::hasAtLeastOneAccount(); + if (config('monica.disable_signup') == 'true' && ! $first) { + abort(403, trans('auth.signup_disabled')); + } + + return view('auth.register') + ->withFirst($first) + ->withLocales(LocaleHelper::getLocaleList()->sortByCollator('lang')); + } + + /** + * Get a validator for an incoming registration request. + * + * @param array $data + * @return \Illuminate\Contracts\Validation\Validator + */ + protected function validator(array $data) + { + return Validator::make($data, [ + 'last_name' => 'required|max:255', + 'first_name' => 'required|max:255', + 'email' => 'required|email|max:255|unique:users', + 'password' => ['required', 'confirmed', PasswordRules::defaults()], + 'policy' => 'required', + ]); + } + + /** + * Create a new user instance after a valid registration. + * + * @param array $data + * @return User|null + */ + protected function create(array $data): ?User + { + $first = ! InstanceHelper::hasAtLeastOneAccount(); + if (config('monica.disable_signup') == 'true' && ! $first) { + abort(403, trans('auth.signup_disabled')); + } + + try { + $account = Account::createDefault( + $data['first_name'], + $data['last_name'], + $data['email'], + $data['password'], + RequestHelper::ip(), + $data['lang'] + ); + /** @var User */ + $user = $account->users()->first(); + + if (! $first) { + // send me an alert + SendNewUserAlert::dispatch($user); + } + + return $user; + } catch (\Exception $e) { + Log::error($e); + + abort(500, trans('auth.signup_error')); + } + } + + /** + * The user has been registered. + * + * @param \Illuminate\Http\Request $request + * @param mixed $user + * @return mixed + */ + protected function registered(Request $request, $user) + { + if (! is_null($user)) { + /** @var int $count */ + $count = Account::count(); + if (! config('monica.signup_double_optin') || $count == 1) { + // if signup_double_optin is disabled, skip the confirm email part + $user->markEmailAsVerified(); + } + } + } +} diff --git a/app/Http/Controllers/Auth/ResetPasswordController.php b/app/Http/Controllers/Auth/ResetPasswordController.php new file mode 100644 index 0000000..634be7e --- /dev/null +++ b/app/Http/Controllers/Auth/ResetPasswordController.php @@ -0,0 +1,42 @@ + 'required', + 'email' => 'required|email', + 'password' => ['required', 'confirmed', PasswordRules::defaults()], + ]; + } +} diff --git a/app/Http/Controllers/Auth/Validate2faController.php b/app/Http/Controllers/Auth/Validate2faController.php new file mode 100644 index 0000000..5fac163 --- /dev/null +++ b/app/Http/Controllers/Auth/Validate2faController.php @@ -0,0 +1,35 @@ +session()->get('oauth')) { + return Route::respondWithRoute('oauth.verify'); + } + if ($request->has('url')) { + return redirect(urldecode($request->input('url'))); + } + + return redirect()->route('login'); + } + + public static function loginCallback() + { + app('pragmarx.google2fa')->setStateless(false); + Google2FA::login(); + } +} diff --git a/app/Http/Controllers/Auth/VerificationController.php b/app/Http/Controllers/Auth/VerificationController.php new file mode 100644 index 0000000..294a49e --- /dev/null +++ b/app/Http/Controllers/Auth/VerificationController.php @@ -0,0 +1,41 @@ +middleware('auth'); + $this->middleware('signed')->only('verify'); + $this->middleware('throttle:6,1')->only('verify', 'resend'); + } +} diff --git a/app/Http/Controllers/ChangelogController.php b/app/Http/Controllers/ChangelogController.php new file mode 100644 index 0000000..ffa3b54 --- /dev/null +++ b/app/Http/Controllers/ChangelogController.php @@ -0,0 +1,22 @@ +withChangelogs($changelogs); + } +} diff --git a/app/Http/Controllers/ComplianceController.php b/app/Http/Controllers/ComplianceController.php new file mode 100644 index 0000000..d2d4937 --- /dev/null +++ b/app/Http/Controllers/ComplianceController.php @@ -0,0 +1,38 @@ +execute([ + 'account_id' => auth()->user()->account_id, + 'user_id' => auth()->user()->id, + 'ip_address' => \Request::ip(), + ]); + + return redirect()->route('dashboard.index'); + } +} diff --git a/app/Http/Controllers/Contacts/ActivitiesController.php b/app/Http/Controllers/Contacts/ActivitiesController.php new file mode 100644 index 0000000..dbbcc6f --- /dev/null +++ b/app/Http/Controllers/Contacts/ActivitiesController.php @@ -0,0 +1,207 @@ +activities() + ->orderBy('happened_at', 'desc') + ->limit(10) + ->get(); + + return ActivityResource::collection($activities)->additional(['meta' => [ + 'statistics' => AccountHelper::getYearlyActivitiesStatistics($contact->account), + ]]); + } + + /** + * Get the list of contacts available to associate the activity with + * participants. + * We could have chosen to query `/people` to get the full list of contacts + * but some accounts have thousands of contacts. Thus for performance + * purposes we have to create our own collection containing just the + * necessary information. + * Also we need to filter out the current contact from the list. + * + * @param Request $request + * @param Contact $contact + * @return Collection + */ + public function contacts(Request $request, Contact $contact) + { + return auth()->user()->account->contacts + ->filter(function ($c) use ($contact) { + return $contact->id !== $c->id; + }) + ->map(function (Contact $c): array { + return [ + 'id' => $c->id, + 'name' => $c->name, + ]; + }); + } + + /** + * Get the list of activity categories. + * + * @param Request $request + * @return Collection + */ + public function categories(Request $request) + { + $categories = auth()->user()->account->activityTypeCategories; + + $array = collect([]); + foreach ($categories as $category) { + $types = ActivityType::where('activity_type_category_id', $category->id)->get(); + + $typeCollection = collect([]); + foreach ($types as $type) { + $typeCollection->push([ + 'id' => $type->id, + 'name' => $type->name, + ]); + } + + $array->push([ + 'id' => $category->id, + 'name' => $category->name, + 'types' => $typeCollection, + ]); + } + + return $array; + } + + /** + * Display the summary of activities for a given contact. + * + * @param Request $request + * @param Contact $contact + * @return \Illuminate\Http\RedirectResponse + */ + public function summary(Request $request, Contact $contact) + { + // get the year of the most recent activity done with the contact + $year = $contact->activities->sortByDesc('happened_at') + ->first() + ->happened_at + ->year; + + return redirect()->route('people.activities.year', [$contact, $year]); + } + + /** + * Get all the activities for this contact for a specific year. + */ + public function year(ActivityStatisticService $activityStatisticService, Contact $contact, int $year) + { + $startDate = Carbon::create($year, 1, 1); + $endDate = Carbon::create($year, 12, 31); + + $activitiesLastTwelveMonths = $activityStatisticService + ->activitiesWithContactInTimeRange($contact, now()->subMonths(12), now()) + ->count(); + + $uniqueActivityTypes = $activityStatisticService + ->uniqueActivityTypesInTimeRange($contact, $startDate, $endDate); + + $activitiesPerYear = $activityStatisticService->activitiesPerYearWithContact($contact); + + $activitiesPerMonthForYear = $activityStatisticService + ->activitiesPerMonthForYear($contact, $year) + ->sortByDesc('month'); + + return view('people.activities.year') + ->withTotalActivities($contact->activities->count()) + ->withActivitiesLastTwelveMonths($activitiesLastTwelveMonths) + ->withUniqueActivityTypes($uniqueActivityTypes) + ->withActivitiesPerYear($activitiesPerYear) + ->withActivitiesPerMonthForYear($activitiesPerMonthForYear) + ->withYear($year) + ->withContact($contact); + } + + /** + * Store the activity. + * + * @param Request $request + * @return \Illuminate\Contracts\Support\Responsable + */ + public function store(Request $request) + { + $activity = app(CreateActivity::class)->execute( + $request->except(['account_id']) + + + [ + 'account_id' => auth()->user()->account_id, + ] + ); + + return new ActivityResource($activity); + } + + /** + * Update the activity. + * + * @param Request $request + * @param Activity $activity + * @return \Illuminate\Contracts\Support\Responsable + */ + public function update(Request $request, Activity $activity) + { + $activity = app(UpdateActivity::class)->execute( + $request->except(['account_id', 'activity_id']) + + + [ + 'account_id' => auth()->user()->account_id, + 'activity_id' => $activity->id, + ] + ); + + return new ActivityResource($activity); + } + + /** + * Delete an activity. + * + * @param Request $request + * @param Activity $activity + * @return \Illuminate\Http\JsonResponse + */ + public function destroy(Request $request, Activity $activity) + { + app(DestroyActivity::class)->execute([ + 'account_id' => auth()->user()->account_id, + 'activity_id' => $activity->id, + ]); + + return $this->respondObjectDeleted($activity->id); + } +} diff --git a/app/Http/Controllers/Contacts/AddressesController.php b/app/Http/Controllers/Contacts/AddressesController.php new file mode 100644 index 0000000..b566439 --- /dev/null +++ b/app/Http/Controllers/Contacts/AddressesController.php @@ -0,0 +1,144 @@ +addresses as $address) { + $addresses->push($this->addressObject($address)); + } + + return $addresses; + } + + /** + * Get all the countries. + */ + public function getCountries() + { + $key = 'countries.'.App::getLocale(); + + $countries = Cache::rememberForever($key, function () { + return CountriesHelper::getAll(); + }); + + return response()->json($countries->all()); + } + + /** + * Store the address. + */ + public function store(Request $request, Contact $contact) + { + $datas = [ + 'account_id' => auth()->user()->account_id, + 'contact_id' => $contact->id, + ] + $request->only([ + 'name', + 'country', + 'street', + 'city', + 'province', + 'postal_code', + 'latitude', + 'longitude', + ]); + + $address = app(CreateAddress::class)->execute($datas); + + return $this->setHTTPStatusCode(201) + ->respond($this->addressObject($address)); + } + + /** + * Edit the contact field. + */ + public function edit(Request $request, Contact $contact, Address $address) + { + $datas = [ + 'account_id' => auth()->user()->account_id, + 'contact_id' => $contact->id, + 'address_id' => $address->id, + ] + $request->only([ + 'name', + 'country', + 'street', + 'city', + 'province', + 'postal_code', + 'latitude', + 'longitude', + ]); + + $address = app(UpdateAddress::class)->execute($datas); + + return $this->respond($this->addressObject($address)); + } + + /** + * Destroy the address. + * + * @param Request $request + * @param Contact $contact + * @param Address $address + * @return \Illuminate\Http\JsonResponse + */ + public function destroy(Request $request, Contact $contact, Address $address) + { + $datas = [ + 'account_id' => auth()->user()->account_id, + 'address_id' => $address->id, + ]; + + if (app(DestroyAddress::class)->execute($datas)) { + return $this->respondObjectDeleted($address->id); + } + + return $this->setHTTPStatusCode(400) + ->setErrorCode(32) + ->respondWithError(); + } + + private function addressObject($address) + { + $place = $address->place; + + return [ + 'id' => $address->id, + 'name' => $address->name, + 'googleMapAddress' => $place->getGoogleMapAddress(), + 'googleMapAddressLatitude' => $place->getGoogleMapsAddressWithLatitude(), + 'address' => $place->getAddressAsString(), + 'country' => $place->country, + 'country_name' => $place->country_name, + 'street' => $place->street, + 'city' => $place->city, + 'province' => $place->province, + 'postal_code' => $place->postal_code, + 'latitude' => $place->latitude, + 'longitude' => $place->longitude, + 'edit' => false, + ]; + } +} diff --git a/app/Http/Controllers/Contacts/AvatarController.php b/app/Http/Controllers/Contacts/AvatarController.php new file mode 100644 index 0000000..0512f91 --- /dev/null +++ b/app/Http/Controllers/Contacts/AvatarController.php @@ -0,0 +1,92 @@ +throwInactive(); + + return view('people.avatar.edit') + ->withContact($contact); + } + + /** + * Update the avatar of the contact. + * + * @param Request $request + * @param Contact $contact + */ + public function update(Request $request, Contact $contact) + { + // update the avatar + $data = [ + 'account_id' => auth()->user()->account_id, + 'contact_id' => $contact->id, + 'source' => $request->input('avatar'), + ]; + + switch ($request->input('avatar')) { + case 'upload': + // if it's a new photo, we need to upload it + $validator = Validator::make($request->all(), [ + 'file' => 'image|max:'.config('monica.max_upload_size'), + ]); + + if ($validator->fails()) { + return back() + ->withInput() + ->withErrors($validator); + } + + $photo = app(UploadPhoto::class)->execute([ + 'account_id' => auth()->user()->account_id, + 'contact_id' => $contact->id, + 'photo' => $request->photo, + ]); + + $data['photo_id'] = $photo->id; + $data['source'] = 'photo'; + break; + case 'photo': + $data['photo_id'] = $contact->avatar_photo_id; + break; + } + + app(UpdateAvatar::class)->execute($data); + + return redirect()->route('people.show', $contact) + ->with('success', trans('people.information_edit_success')); + } + + /** + * Set the given photo as avatar. + * + * @param Request $request + * @param Contact $contact + * @param int $photoId + */ + public function photo(Request $request, Contact $contact, $photoId) + { + // update the avatar + $data = [ + 'account_id' => auth()->user()->account_id, + 'contact_id' => $contact->id, + 'source' => 'photo', + 'photo_id' => $photoId, + ]; + + return app(UpdateAvatar::class)->execute($data); + } +} diff --git a/app/Http/Controllers/Contacts/CallsController.php b/app/Http/Controllers/Contacts/CallsController.php new file mode 100644 index 0000000..6f408f3 --- /dev/null +++ b/app/Http/Controllers/Contacts/CallsController.php @@ -0,0 +1,111 @@ +calls()->get(); + + return CallResource::collection($calls); + } + + /** + * Display the timestamp of the last phone contact. + * + * @param Contact $contact + * @return JsonResponse + */ + public function lastCalled(Contact $contact): JsonResponse + { + $lastTalkedTo = $contact->last_talked_to; + + if ($lastTalkedTo !== null) { + $lastTalkedTo = DateHelper::getShortDate($contact->last_talked_to); + } + + return $this->respond([ + 'last_talked_to' => $lastTalkedTo, + ]); + } + + /** + * Store a call. + * + * @param Contact $contact + * @return Call + */ + public function store(Request $request, Contact $contact) + { + return app(CreateCall::class)->execute([ + 'account_id' => auth()->user()->account_id, + 'contact_id' => $contact->id, + 'content' => $request->input('content'), + 'called_at' => $request->input('called_at'), + 'contact_called' => $request->input('contact_called'), + 'emotions' => $request->input('emotions'), + ]); + } + + /** + * Update a call. + * + * @param Contact $contact + * @param Call $call + * @return Call + */ + public function update(Request $request, Contact $contact, Call $call) + { + return app(UpdateCall::class)->execute([ + 'account_id' => auth()->user()->account_id, + 'call_id' => $call->id, + 'content' => $request->input('content'), + 'called_at' => $request->input('called_at'), + 'contact_called' => $request->input('contact_called'), + 'emotions' => $request->input('emotions'), + ]); + } + + /** + * Delete the call. + * + * @param Request $request + * @param Contact $contact + * @param Call $call + * @return null|\Illuminate\Http\JsonResponse + */ + public function destroy(Request $request, Contact $contact, Call $call): ?JsonResponse + { + $data = [ + 'account_id' => auth()->user()->account_id, + 'call_id' => $call->id, + ]; + + if (app(DestroyCall::class)->execute($data)) { + return $this->respondObjectDeleted($call->id); + } + + return null; + } +} diff --git a/app/Http/Controllers/Contacts/ContactAuditLogController.php b/app/Http/Controllers/Contacts/ContactAuditLogController.php new file mode 100644 index 0000000..cfae00c --- /dev/null +++ b/app/Http/Controllers/Contacts/ContactAuditLogController.php @@ -0,0 +1,26 @@ +logs() + ->with('author') + ->orderBy('created_at', 'desc') + ->paginate(15); + + return view('people.auditlogs.index') + ->withContact($contact) + ->withLogsCollection(AuditLogHelper::getCollectionOfAudits($logs)) + ->withLogsPagination($logs); + } +} diff --git a/app/Http/Controllers/Contacts/ContactFieldsController.php b/app/Http/Controllers/Contacts/ContactFieldsController.php new file mode 100644 index 0000000..cb2880a --- /dev/null +++ b/app/Http/Controllers/Contacts/ContactFieldsController.php @@ -0,0 +1,92 @@ +contactFields as $contactField) { + $data = [ + 'id' => $contactField->id, + 'data' => $contactField->data, + 'name' => $contactField->contactFieldType->name, + 'fontawesome_icon' => (is_null($contactField->contactFieldType->fontawesome_icon) ? null : $contactField->contactFieldType->fontawesome_icon), + 'protocol' => (is_null($contactField->contactFieldType->protocol) ? null : $contactField->contactFieldType->protocol), + 'contact_field_type_id' => $contactField->contact_field_type_id, + 'edit' => false, + ]; + $contactInformationData->push($data); + } + + return $contactInformationData; + } + + /** + * Get all the contact field types. + * + * @param Contact $contact + */ + public function getContactFieldTypes(Contact $contact) + { + return auth()->user()->account->contactFieldTypes; + } + + /** + * Store the contact field. + */ + public function storeContactField(ContactFieldsRequest $request, Contact $contact) + { + $contactField = $contact->contactFields()->create( + $request->only([ + 'contact_field_type_id', + 'data', + ]) + + [ + 'account_id' => auth()->user()->account_id, + ] + ); + + GetAvatarsFromInternet::dispatch($contact); + + return $contactField; + } + + /** + * Edit the contact field. + */ + public function editContactField(ContactFieldsRequest $request, Contact $contact, ContactField $contactField) + { + $contactField->update( + $request->only([ + 'contact_field_type_id', + 'data', + ]) + + [ + 'account_id' => auth()->user()->account_id, + ] + ); + + GetAvatarsFromInternet::dispatch($contact); + + return $contactField; + } + + public function destroyContactField(Contact $contact, ContactField $contactField) + { + $contactField->delete(); + + GetAvatarsFromInternet::dispatch($contact); + } +} diff --git a/app/Http/Controllers/Contacts/ConversationsController.php b/app/Http/Controllers/Contacts/ConversationsController.php new file mode 100644 index 0000000..5da1b0f --- /dev/null +++ b/app/Http/Controllers/Contacts/ConversationsController.php @@ -0,0 +1,289 @@ +withContact($contact) + ->withContactFieldTypes(auth()->user()->account->contactFieldTypes); + } + + /** + * Display the list of conversations. + * + * @param Contact $contact + * @return Collection + */ + public function index(Request $request, Contact $contact) + { + $conversationsCollection = collect([]); + $conversations = $contact->conversations()->get(); + + foreach ($conversations as $conversation) { + $message = $conversation->messages->last(); + $data = [ + 'id' => $conversation->id, + 'message_count' => $conversation->messages->count(), + 'contact_field_type' => $conversation->contactFieldType->name, + 'icon' => $conversation->contactFieldType->fontawesome_icon, + 'content' => ! is_null($message) ? mb_strimwidth($message->content, 0, 50, '…') : '', + 'happened_at' => DateHelper::getShortDate($conversation->happened_at), + 'route' => route('people.conversations.edit', [$contact, $conversation]), + ]; + $conversationsCollection->push($data); + } + + return $conversationsCollection; + } + + /** + * Store the conversation. + * + * @param Request $request + * @param Contact $contact + * @return \Illuminate\Http\RedirectResponse + */ + public function store(Request $request, Contact $contact) + { + $data = $this->validateAndGetDatas($request); + + if ($data instanceof \Illuminate\Contracts\Validation\Validator) { + return back() + ->withInput() + ->withErrors($data); + } + + $date = $data['happened_at']; + $data['contact_id'] = $contact->id; + + // create the conversation + try { + $conversation = app(CreateConversation::class)->execute($data); + } catch (ValidationException $e) { + return back() + ->withInput() + ->withErrors($e->validator); + } catch (\Exception $e) { + return back() + ->withInput() + ->withErrors(trans('app.error_save')); + } + + // add the messages to the conversation + $result = $this->updateMessages($request, $conversation, $date); + if ($result !== true) { + return back() + ->withInput() + ->withErrors($result); + } + + return redirect()->route('people.show', $contact) + ->with('success', trans('people.conversation_add_success')); + } + + /** + * Display a specific conversation. + * + * @param Contact $contact + * @return \Illuminate\View\View + */ + public function edit(Request $request, Contact $contact, Conversation $conversation) + { + $contact->throwInactive(); + + // preparing the messages for the Vue component + $messages = collect([]); + foreach ($conversation->messages as $message) { + $messages->push([ + 'uid' => $message->id, + 'content' => $message->content, + 'author' => ($message->written_by_me ? 'me' : 'other'), + ]); + } + + return view('people.conversations.edit') + ->withContact($contact) + ->withConversation($conversation) + ->withMessages($messages) + ->withContactFieldTypes(auth()->user()->account->contactFieldTypes); + } + + /** + * Update the conversation. + * + * @param Request $request + * @param Contact $contact + * @param Conversation $conversation + * @return \Illuminate\Http\RedirectResponse + */ + public function update(Request $request, Contact $contact, Conversation $conversation) + { + $data = $this->validateAndGetDatas($request); + + if ($data instanceof \Illuminate\Contracts\Validation\Validator) { + return back() + ->withInput() + ->withErrors($data); + } + + $date = $data['happened_at']; + $data['conversation_id'] = $conversation->id; + + // update the conversation + try { + $conversation = app(UpdateConversation::class)->execute($data); + } catch (ValidationException $e) { + return back() + ->withInput() + ->withErrors($e->validator); + } catch (\Exception $e) { + return back() + ->withInput() + ->withErrors(trans('app.error_save')); + } + + // delete all current messages + foreach ($conversation->messages as $message) { + $data = [ + 'account_id' => auth()->user()->account_id, + 'conversation_id' => $conversation->id, + 'message_id' => $message->id, + ]; + app(DestroyMessage::class)->execute($data); + } + + // and create all new ones + $result = $this->updateMessages($request, $conversation, $date); + if ($result !== true) { + return back() + ->withInput() + ->withErrors($result); + } + + return redirect()->route('people.show', $contact) + ->with('success', trans('people.conversation_edit_success')); + } + + /** + * Validate datas and get an array for create or update a conversation. + * + * @param Request $request + * @return array|\Illuminate\Contracts\Validation\Validator + */ + private function validateAndGetDatas(Request $request) + { + $validator = Validator::make($request->all(), [ + 'conversationDateRadio' => 'required', + 'conversationDate' => 'required_unless:conversationDateRadio,today,yesterday', + 'messages' => 'required', + 'contactFieldTypeId' => 'required|integer|exists:contact_field_types,id', + ], [ + 'messages.required' => trans('people.conversation_add_error'), + ]); + + if ($validator->fails()) { + return $validator; + } + + // find out what the date is + $chosenDate = $request->input('conversationDateRadio'); + if ($chosenDate == 'today') { + $date = DateHelper::getDate(now($request->user()->timezone)); + } elseif ($chosenDate == 'yesterday') { + $date = DateHelper::getDate(now($request->user()->timezone)->subDay()); + } else { + $date = $request->input('conversationDate'); + } + + return [ + 'account_id' => auth()->user()->account_id, + 'happened_at' => $date, + 'contact_field_type_id' => $request->input('contactFieldTypeId'), + ]; + } + + /** + * Update messages for conversation. + * + * @param Request $request + * @param Conversation $conversation + * @param string $date + * @return bool|string|\Illuminate\Contracts\Validation\Validator + * @psalm-return bool|array|string|\Illuminate\Contracts\Validation\Validator + */ + private function updateMessages(Request $request, Conversation $conversation, string $date) + { + $messages = explode(',', $request->input('messages')); + foreach ($messages as $messageId) { + $data = [ + 'account_id' => auth()->user()->account_id, + 'conversation_id' => $conversation->id, + 'contact_id' => $conversation->contact_id, + 'written_at' => $date, + 'written_by_me' => ($request->input('who_wrote_'.$messageId) === 'me'), + 'content' => $request->input('content_'.$messageId), + ]; + + try { + app(AddMessageToConversation::class)->execute($data); + } catch (ValidationException $e) { + return $e->validator; + } catch (\Exception $e) { + return trans('app.error_save'); + } + } + + return true; + } + + /** + * Delete the conversation. + * + * @param Request $request + * @param Contact $contact + * @param Conversation $conversation + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\JsonResponse + */ + public function destroy(Request $request, Contact $contact, Conversation $conversation) + { + $data = [ + 'account_id' => auth()->user()->account_id, + 'conversation_id' => $conversation->id, + ]; + + try { + app(DestroyConversation::class)->execute($data); + } catch (\Exception $e) { + return $this->respondNotFound(); + } + + return redirect()->route('people.show', $contact) + ->with('success', trans('people.conversation_delete_success')); + } +} diff --git a/app/Http/Controllers/Contacts/DebtController.php b/app/Http/Controllers/Contacts/DebtController.php new file mode 100644 index 0000000..56b6742 --- /dev/null +++ b/app/Http/Controllers/Contacts/DebtController.php @@ -0,0 +1,139 @@ +withContact($contact); + } + + /** + * Show the form for creating a new resource. + * + * @param Contact $contact + * @return \Illuminate\View\View + */ + public function create(Contact $contact) + { + return view('people.debt.add') + ->withContact($contact) + ->withAccountHasLimitations(AccountHelper::hasLimitations(auth()->user()->account)) + ->withDebt(new Debt); + } + + /** + * Store a newly created resource in storage. + * + * @param DebtRequest $request + * @param Contact $contact + * @return \Illuminate\Http\RedirectResponse + */ + public function store(DebtRequest $request, Contact $contact) + { + $contact->throwInactive(); + + $contact->debts()->create( + $request->only([ + 'in_debt', + 'amount', + 'reason', + ]) + + [ + 'account_id' => $contact->account_id, + 'status' => 'inprogress', + ] + ); + + return redirect()->route('people.show', $contact) + ->with('success', trans('people.debt_add_success')); + } + + /** + * Display the specified resource. + * + * @param Contact $contact + * @param Debt $debt + * @return void + */ + public function show(Contact $contact, Debt $debt): void + { + // + } + + /** + * Show the form for editing the specified resource. + * + * @param Contact $contact + * @param Debt $debt + * @return \Illuminate\View\View + */ + public function edit(Contact $contact, Debt $debt) + { + $contact->throwInactive(); + + return view('people.debt.edit') + ->withContact($contact) + ->withAccountHasLimitations(AccountHelper::hasLimitations(auth()->user()->account)) + ->withDebt($debt); + } + + /** + * Update the specified resource in storage. + * + * @param DebtRequest $request + * @param Contact $contact + * @param Debt $debt + * @return \Illuminate\Http\RedirectResponse + */ + public function update(DebtRequest $request, Contact $contact, Debt $debt) + { + $contact->throwInactive(); + + $debt->update( + $request->only([ + 'in_debt', + 'amount', + 'reason', + ]) + + [ + 'account_id' => $contact->account_id, + 'status' => 'inprogress', + ] + ); + + return redirect()->route('people.show', $contact) + ->with('success', trans('people.debt_edit_success')); + } + + /** + * Remove the specified resource from storage. + * + * @param Contact $contact + * @param Debt $debt + * @return \Illuminate\Http\RedirectResponse + */ + public function destroy(Contact $contact, Debt $debt) + { + $contact->throwInactive(); + + $debt->delete(); + + return redirect()->route('people.show', $contact) + ->with('success', trans('people.debt_delete_success')); + } +} diff --git a/app/Http/Controllers/Contacts/DocumentsController.php b/app/Http/Controllers/Contacts/DocumentsController.php new file mode 100644 index 0000000..ee631cc --- /dev/null +++ b/app/Http/Controllers/Contacts/DocumentsController.php @@ -0,0 +1,85 @@ +middleware('limitations')->only('store'); + } + + /** + * Display the list of documents. + * + * @param Contact $contact + * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection + */ + public function index(Request $request, Contact $contact) + { + $documents = $contact->documents()->get(); + + return DocumentResource::collection($documents); + } + + /** + * Store the document. + * + * @param Request $request + * @param Contact $contact + * @return Document + */ + public function store(Request $request, Contact $contact): Document + { + $contact->throwInactive(); + + return app(UploadDocument::class)->execute([ + 'account_id' => auth()->user()->account_id, + 'contact_id' => $contact->id, + 'document' => $request->document, + ]); + } + + /** + * Delete the document. + * + * @param Request $request + * @param Contact $contact + * @param Document $document + * @return null|\Illuminate\Http\JsonResponse + */ + public function destroy(Request $request, Contact $contact, Document $document): ?JsonResponse + { + $data = [ + 'account_id' => auth()->user()->account_id, + 'document_id' => $document->id, + ]; + + try { + if (app(DestroyDocument::class)->execute($data)) { + return $this->respondObjectDeleted($document->id); + } + } catch (\Exception $e) { + return $this->respondNotFound(); + } + + return null; + } +} diff --git a/app/Http/Controllers/Contacts/GiftController.php b/app/Http/Controllers/Contacts/GiftController.php new file mode 100644 index 0000000..600c70c --- /dev/null +++ b/app/Http/Controllers/Contacts/GiftController.php @@ -0,0 +1,124 @@ +gifts() + ->orderBy('created_at', 'asc') + ->paginate(); + + return GiftResource::collection($gifts); + } + + /** + * Get the detail of a given gift. + * + * @param Request $request + * @param Gift $gift + * @return GiftResource|\Illuminate\Http\JsonResponse + */ + public function show(Request $request, Contact $contact, Gift $gift) + { + return new GiftResource($gift); + } + + /** + * Store the gift. + * + * @param Request $request + * @return GiftResource|\Illuminate\Http\JsonResponse + */ + public function store(Request $request, Contact $contact) + { + $gift = app(CreateGift::class)->execute( + $request->except(['account_id', 'contact_id']) + + [ + 'account_id' => auth()->user()->account_id, + 'contact_id' => $contact->id, + ] + ); + + return new GiftResource($gift); + } + + /** + * Update the gift. + * + * @param Request $request + * @param Gift $gift + * @return GiftResource|\Illuminate\Http\JsonResponse + */ + public function update(Request $request, Contact $contact, Gift $gift) + { + $gift = app(UpdateGift::class)->execute( + $request->except(['account_id', 'contact_id', 'gift_id']) + + [ + 'account_id' => auth()->user()->account_id, + 'contact_id' => $contact->id, + 'gift_id' => $gift->id, + ] + ); + + return new GiftResource($gift); + } + + /** + * Associate a photo to the gift. + * + * @param Request $request + * @param Gift $gift + * @param Photo $photo + * @return GiftResource|\Illuminate\Http\JsonResponse + */ + public function associate(Request $request, Contact $contact, Gift $gift, Photo $photo) + { + $gift = app(AssociatePhotoToGift::class)->execute([ + 'account_id' => auth()->user()->account_id, + 'gift_id' => $gift->id, + 'photo_id' => $photo->id, + ]); + + return new GiftResource($gift); + } + + /** + * Delete a gift. + * + * @param Request $request + * @param Gift $gift + * @return \Illuminate\Http\JsonResponse + */ + public function destroy(Request $request, Contact $contact, Gift $gift) + { + app(DestroyGift::class)->execute([ + 'account_id' => auth()->user()->account_id, + 'gift_id' => $gift->id, + ]); + + return $this->respondObjectDeleted($gift->id); + } +} diff --git a/app/Http/Controllers/Contacts/IntroductionsController.php b/app/Http/Controllers/Contacts/IntroductionsController.php new file mode 100644 index 0000000..714115e --- /dev/null +++ b/app/Http/Controllers/Contacts/IntroductionsController.php @@ -0,0 +1,69 @@ +throwInactive(); + + $contacts = $contact->siblingContacts() + ->real() + ->active() + ->orderByUserPreference() + ->paginate(20); + + $introducer = $contact->getIntroducer(); + if ($introducer !== null) { + $introducer = new ContactResource($introducer); + } + + return view('people.introductions.edit') + ->withContact($contact) + ->withContacts(ContactResource::collection($contacts)) + ->withIntroducer($introducer); + } + + /** + * Update the specified resource in storage. + * + * @param Request $request + * @param Contact $contact + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\JsonResponse + */ + public function update(Request $request, Contact $contact) + { + $contact->throwInactive(); + + $contact = app(UpdateContactIntroduction::class)->execute([ + 'account_id' => auth()->user()->account_id, + 'contact_id' => $contact->id, + 'met_through_contact_id' => $request->input('metThroughId'), + 'general_information' => $request->input('first_met_additional_info'), + 'is_date_known' => $request->input('is_first_met_date_known') == 'known', + 'day' => $request->input('first_met_day'), + 'month' => $request->input('first_met_month'), + 'year' => $request->input('first_met_year'), + 'add_reminder' => $request->addReminder == 'on', + ]); + + return redirect()->route('people.show', $contact) + ->with('success', trans('people.introductions_update_success')); + } +} diff --git a/app/Http/Controllers/Contacts/LifeEventsController.php b/app/Http/Controllers/Contacts/LifeEventsController.php new file mode 100644 index 0000000..3120383 --- /dev/null +++ b/app/Http/Controllers/Contacts/LifeEventsController.php @@ -0,0 +1,138 @@ +user()->account->lifeEventCategories; + + return LifeEventCategoryResource::collection($lifeEventCategories); + } + + /** + * Get the list of life event types for a given life event category. + * + * @param Request $request + * @param int $lifeEventCategoryId + * @return \Illuminate\Http\Resources\Json\ResourceCollection + */ + public function types(Request $request, int $lifeEventCategoryId) + { + $lifeEventCategory = LifeEventCategory::findOrFail($lifeEventCategoryId); + $lifeEventTypes = $lifeEventCategory->lifeEventTypes; + + return LifeEventTypeResource::collection($lifeEventTypes); + } + + /** + * Display the list of life events. + * + * @param Request $request + * @param Contact $contact + * @return Collection + */ + public function index(Request $request, Contact $contact) + { + $lifeEventsCollection = collect([]); + $lifeEvents = $contact->lifeEvents()->get(); + + foreach ($lifeEvents as $lifeEvent) { + $data = [ + 'id' => $lifeEvent->id, + 'life_event_type' => $lifeEvent->lifeEventType->name, + 'default_life_event_type_key' => $lifeEvent->lifeEventType->default_life_event_type_key, + 'life_event_type_name' => $lifeEvent->lifeEventType->name, + 'name' => $lifeEvent->name, + 'note' => $lifeEvent->note, + 'happened_at' => DateHelper::getShortDate($lifeEvent->happened_at), + ]; + $lifeEventsCollection->push($data); + } + + return $lifeEventsCollection; + } + + /** + * Store the life event. + * + * @param Request $request + * @param Contact $contact + * @return LifeEvent|\Illuminate\Http\RedirectResponse + */ + public function store(Request $request, Contact $contact) + { + $data = [ + 'account_id' => auth()->user()->account_id, + 'contact_id' => $contact->id, + 'life_event_type_id' => $request->input('life_event_type_id'), + 'happened_at' => $request->input('happened_at'), + 'name' => $request->input('name'), + 'note' => $request->input('note'), + 'has_reminder' => $request->input('has_reminder'), + 'happened_at_month_unknown' => $request->input('happened_at_month_unknown'), + 'happened_at_day_unknown' => $request->input('happened_at_day_unknown'), + ]; + + // create the conversation + try { + $lifeEvent = app(CreateLifeEvent::class)->execute($data); + } catch (\Exception $e) { + return back() + ->withInput() + ->withErrors(trans('app.error_save')); + } + + return $lifeEvent; + } + + /** + * Destroy the life event. + * + * @param Request $request + * @param LifeEvent $lifeEvent + * @return \Illuminate\Http\JsonResponse|\Illuminate\Http\RedirectResponse + */ + public function destroy(Request $request, LifeEvent $lifeEvent) + { + $data = [ + 'account_id' => auth()->user()->account_id, + 'life_event_id' => $lifeEvent->id, + ]; + + try { + app(DestroyLifeEvent::class)->execute($data); + } catch (\Exception $e) { + // We have to redirect with HTTP status 303 or the browser will issue a + // DELETE request to the new location. This may result in deleting other + // resources as well. Refer to Github issue #2415 + return back(303) + ->withInput() + ->withErrors(trans('app.error_save')); + } + + return $this->respondObjectDeleted($lifeEvent->id); + } +} diff --git a/app/Http/Controllers/Contacts/NotesController.php b/app/Http/Controllers/Contacts/NotesController.php new file mode 100644 index 0000000..eb8a4f0 --- /dev/null +++ b/app/Http/Controllers/Contacts/NotesController.php @@ -0,0 +1,101 @@ +notes()->latest()->get(); + + foreach ($notes as $note) { + $data = [ + 'id' => $note->id, + 'body' => $note->body, + 'is_favorited' => $note->is_favorited, + 'favorited_at' => $note->favorited_at, + 'favorited_at_short' => $note->favorited_at ? DateHelper::getShortDate($note->favorited_at) : null, + 'created_at' => $note->created_at, + 'created_at_short' => DateHelper::getShortDate($note->created_at), + 'edit' => false, + ]; + $notesCollection->push($data); + } + + return $notesCollection; + } + + /** + * Store the task. + */ + public function store(NotesRequest $request, Contact $contact) + { + $contact->throwInactive(); + + return $contact->notes()->create([ + 'account_id' => auth()->user()->account_id, + 'body' => $request->input('body'), + ]); + } + + public function toggle(NoteToggleRequest $request, Contact $contact, Note $note) + { + // check if the state of the note has changed + if ($note->is_favorited) { + $note->favorited_at = null; + $note->is_favorited = false; + } else { + $note->is_favorited = true; + $note->favorited_at = now(); + } + + $note->save(); + } + + /** + * Update the specified resource in storage. + * + * @param NotesRequest $request + * @param Contact $contact + * @param Note $note + * @return Note + */ + public function update(NotesRequest $request, Contact $contact, Note $note): Note + { + $contact->throwInactive(); + + $note->update( + $request->only([ + 'body', + ]) + + ['account_id' => $contact->account_id] + ); + + return $note; + } + + /** + * Remove the specified resource from storage. + * + * @param Contact $contact + * @param Note $note + * @return void + */ + public function destroy(Contact $contact, Note $note): void + { + $contact->throwInactive(); + + $note->delete(); + } +} diff --git a/app/Http/Controllers/Contacts/PetsController.php b/app/Http/Controllers/Contacts/PetsController.php new file mode 100644 index 0000000..6d8ec3e --- /dev/null +++ b/app/Http/Controllers/Contacts/PetsController.php @@ -0,0 +1,114 @@ + $petCategory->id, + 'name' => $petCategory->name, + 'edit' => false, + ]; + $petCategoriesData->push($data); + } + + return $petCategoriesData; + } + + /** + * Get all the pets for this contact. + * + * @param Contact $contact + */ + public function index(Contact $contact) + { + $petsCollection = collect([]); + $pets = $contact->pets; + + foreach ($pets as $pet) { + $data = [ + 'id' => $pet->id, + 'name' => $pet->name, + 'pet_category_id' => $pet->pet_category_id, + 'category_name' => $pet->petCategory->name, + 'edit' => false, + ]; + $petsCollection->push($data); + } + + return $petsCollection; + } + + /** + * Store the pet. + */ + public function store(PetsRequest $request, Contact $contact) + { + $contact->throwInactive(); + + $pet = $contact->pets()->create( + $request->only([ + 'pet_category_id', + 'name', + ]) + + [ + 'account_id' => auth()->user()->account_id, + ] + ); + + return [ + 'id' => $pet->id, + 'name' => $pet->name, + 'pet_category_id' => $pet->pet_category_id, + 'category_name' => $pet->petCategory->name, + 'edit' => false, + ]; + } + + /** + * Update the pet. + */ + public function update(PetsRequest $request, Contact $contact, Pet $pet) + { + $contact->throwInactive(); + + $pet->update( + $request->only([ + 'pet_category_id', + 'name', + ]) + + [ + 'account_id' => auth()->user()->account_id, + ] + ); + + return [ + 'id' => $pet->id, + 'name' => $pet->name, + 'pet_category_id' => $pet->pet_category_id, + 'category_name' => $pet->petCategory->name, + 'edit' => false, + ]; + } + + public function destroy(Contact $contact, Pet $pet) + { + $pet->delete(); + } +} diff --git a/app/Http/Controllers/Contacts/PhotosController.php b/app/Http/Controllers/Contacts/PhotosController.php new file mode 100644 index 0000000..d093dcd --- /dev/null +++ b/app/Http/Controllers/Contacts/PhotosController.php @@ -0,0 +1,84 @@ +photos()->orderBy('created_at', 'desc')->get(); + + return PhotoResource::collection($photos); + } + + /** + * Store the Photo. + * + * @param Request $request + * @param Contact $contact + * @return PhotoResource + */ + public function store(Request $request, Contact $contact): PhotoResource + { + $photo = app(UploadPhoto::class)->execute([ + 'account_id' => auth()->user()->account_id, + 'contact_id' => $contact->id, + 'photo' => $request->photo, + ]); + + return new PhotoResource($photo); + } + + /** + * Delete the Photo. + * Also, if this photo was the current avatar of the contact, change the + * avatar to the default one. + * + * @param Request $request + * @param Contact $contact + * @param Photo $photo + * @return null|\Illuminate\Http\JsonResponse + */ + public function destroy(Request $request, Contact $contact, Photo $photo) + { + $data = [ + 'account_id' => auth()->user()->account_id, + 'photo_id' => $photo->id, + ]; + + try { + app(DestroyPhoto::class)->execute($data); + } catch (\Exception $e) { + return $this->respondNotFound(); + } + + if ($contact->avatar_source == 'photo' + && $contact->avatar_photo_id == $photo->id) { + app(UpdateAvatar::class)->execute([ + 'account_id' => auth()->user()->account_id, + 'contact_id' => $contact->id, + 'source' => 'adorable', + ]); + } + + return $this->respondObjectDeleted($photo->id); + } +} diff --git a/app/Http/Controllers/Contacts/RelationshipsController.php b/app/Http/Controllers/Contacts/RelationshipsController.php new file mode 100644 index 0000000..14cb522 --- /dev/null +++ b/app/Http/Controllers/Contacts/RelationshipsController.php @@ -0,0 +1,270 @@ +account_id, 'updated_at') + ->whereNotIn('id', [$contact->id]) + ->paginate(20); + + return view('people.relationship.new') + ->withContact($contact) + ->withPartner(new Contact) + ->withGenders(GenderHelper::getGendersInput()) + ->withRelationshipTypes($this->getRelationshipTypesList($contact)) + ->withDefaultGender(auth()->user()->account->default_gender_id) + ->withDays(DateHelper::getListOfDays()) + ->withMonths(DateHelper::getListOfMonths()) + ->withBirthdate(now(DateHelper::getTimezone())->toDateString()) + ->withExistingContacts(ContactResource::collection($existingContacts)) + ->withType($request->input('type')) + ->withFormNameOrder(FormHelper::getNameOrderForForms(auth()->user())); + } + + /** + * Store a newly created resource in storage. + * + * @param Request $request + * @param Contact $contact + * @return RedirectResponse + */ + public function store(Request $request, Contact $contact) + { + // case of linking to an existing contact + if ($request->input('relationship_type') == 'existing') { + $partnerId = $request->input('existing_contact_id'); + } else { + + // case of creating a new contact + $datas = $this->validateAndGetDatas($request); + + if ($datas instanceof \Illuminate\Contracts\Validation\Validator) { + return back() + ->withInput() + ->withErrors($datas); + } + + $partner = app(CreateContact::class)->execute($datas); + $partnerId = $partner->id; + } + + app(CreateRelationship::class)->execute([ + 'account_id' => auth()->user()->account_id, + 'contact_is' => $contact->id, + 'of_contact' => $partnerId, + 'relationship_type_id' => $request->input('relationship_type_id'), + ]); + + return redirect()->route('people.show', $contact) + ->with('success', trans('people.relationship_form_add_success')); + } + + /** + * Show the form for editing the specified resource. + * + * @param Contact $contact + * @param Relationship $relationship + * @return View + */ + public function edit(Contact $contact, Relationship $relationship) + { + $contact->throwInactive(); + + $otherContact = $relationship->ofContact; + + $now = now(); + $age = (string) (! is_null($otherContact->birthdate) ? $otherContact->birthdate->getAge() : 0); + $birthdate = ! is_null($otherContact->birthdate) ? $otherContact->birthdate->date->toDateString() : $now->toDateString(); + $day = ! is_null($otherContact->birthdate) ? $otherContact->birthdate->date->day : $now->day; + $month = ! is_null($otherContact->birthdate) ? $otherContact->birthdate->date->month : $now->month; + + $hasBirthdayReminder = is_null($otherContact->birthday_reminder_id) ? 0 : 1; + + return view('people.relationship.edit') + ->withContact($contact) + ->withPartner($otherContact) + ->withGenders(auth()->user()->account->genders) + ->withRelationshipTypes($this->getRelationshipTypesList($contact)) + ->withDays(DateHelper::getListOfDays()) + ->withMonths(DateHelper::getListOfMonths()) + ->withBirthdate($birthdate) + ->withRelationshipId($relationship->id) + ->withType($relationship->relationship_type_id) + ->withBirthdayState($otherContact->getBirthdayState()) + ->withDay($day) + ->withMonth($month) + ->withAge($age) + ->withGenders(GenderHelper::getGendersInput()) + ->withHasBirthdayReminder($hasBirthdayReminder) + ->withFormNameOrder(FormHelper::getNameOrderForForms(auth()->user())); + } + + /** + * Update the specified resource in storage. + * + * @param Request $request + * @param Contact $contact + * @param Relationship $relationship + * @return RedirectResponse + */ + public function update(Request $request, Contact $contact, Relationship $relationship) + { + $otherContact = $relationship->ofContact; + + if ($otherContact->is_partial) { + $datas = $this->validateAndGetDatas($request); + + if ($datas instanceof \Illuminate\Contracts\Validation\Validator) { + return back() + ->withInput() + ->withErrors($datas); + } + + app(UpdateContact::class)->execute($datas + [ + 'contact_id' => $otherContact->id, + 'author_id' => auth()->user()->id, + ]); + } + + // update the relationship + app(UpdateRelationship::class)->execute([ + 'account_id' => auth()->user()->account_id, + 'relationship_id' => $relationship->id, + 'relationship_type_id' => $request->input('relationship_type_id'), + ]); + + return redirect()->route('people.show', $contact) + ->with('success', trans('people.relationship_form_add_success')); + } + + /** + * Validate datas and get an array for create or update a contact. + * + * @param Request $request + * @return array|\Illuminate\Contracts\Validation\Validator + */ + private function validateAndGetDatas(Request $request) + { + $validator = Validator::make($request->all(), [ + 'first_name' => 'required|max:255', + 'last_name' => 'max:255', + 'gender_id' => 'nullable|integer', + 'birthdayDate' => 'date_format:Y-m-d', + ]); + + if ($validator->fails()) { + return $validator; + } + + // this is really ugly. it should be changed + if ($request->input('birthdate') == 'exact') { + $birthdate = $request->input('birthdayDate'); + $birthdate = DateHelper::parseDate($birthdate); + $day = $birthdate->day; + $month = $birthdate->month; + $year = $birthdate->year; + } else { + $day = $request->input('day'); + $month = $request->input('month'); + $year = $request->input('year'); + } + + return [ + 'account_id' => auth()->user()->account_id, + 'author_id' => auth()->user()->id, + 'first_name' => $request->input('first_name'), + 'last_name' => $request->input('last_name'), + 'gender_id' => $request->input('gender_id'), + 'is_birthdate_known' => ! empty($request->input('birthdate')) && $request->input('birthdate') !== 'unknown', + 'birthdate_day' => $day, + 'birthdate_month' => $month, + 'birthdate_year' => $year, + 'birthdate_is_age_based' => $request->input('birthdate') === 'approximate', + 'birthdate_age' => $request->input('age'), + 'birthdate_add_reminder' => ! empty($request->input('addReminder')), + 'is_partial' => ! $request->input('realContact'), + 'is_deceased' => false, + 'is_deceased_date_known' => false, + ]; + } + + /** + * Remove the specified resource from storage. + * + * @param Contact $contact + * @param Relationship $relationship + * @return RedirectResponse + */ + public function destroy(Contact $contact, Relationship $relationship) + { + if ($contact->account_id != auth()->user()->account_id) { + return redirect()->route('people.index'); + } + + if ($relationship->account_id != auth()->user()->account_id) { + return redirect()->route('people.index'); + } + + app(DestroyRelationship::class)->execute([ + 'account_id' => auth()->user()->account_id, + 'relationship_id' => $relationship->id, + ]); + + return redirect()->route('people.show', $contact) + ->with('success', trans('people.relationship_form_deletion_success')); + } + + /** + * Building the list of relationship types specifically for the dropdown which asks + * for an id and a name. + * + * @return Collection + */ + private function getRelationshipTypesList(Contact $contact) + { + $relationshipTypes = collect(); + foreach (auth()->user()->account->relationshipTypes as $relationshipType) { + $types = $relationshipTypes->get($relationshipType->relationshipTypeGroup->name, [ + 'name' => trans('app.relationship_type_group_'.$relationshipType->relationshipTypeGroup->name), + 'options' => [], + ]); + + $types['options'][] = [ + 'id' => $relationshipType->id, + 'name' => $relationshipType->getLocalizedName($contact, true), + ]; + + $relationshipTypes->put($relationshipType->relationshipTypeGroup->name, $types); + } + + return $relationshipTypes; + } +} diff --git a/app/Http/Controllers/Contacts/RemindersController.php b/app/Http/Controllers/Contacts/RemindersController.php new file mode 100644 index 0000000..c48fdbc --- /dev/null +++ b/app/Http/Controllers/Contacts/RemindersController.php @@ -0,0 +1,127 @@ +withContact($contact) + ->withAccountHasLimitations(AccountHelper::hasLimitations(auth()->user()->account)) + ->withReminder(new Reminder); + } + + /** + * Store a reminder. + * + * @param Request $request + * @param Contact $contact + * @return \Illuminate\Http\RedirectResponse + */ + public function store(Request $request, Contact $contact) + { + $frequency_type = $request->input('frequency_type'); + if ($frequency_type === 'recurrent') { + $frequency_type = $request->input('frequency_number_select'); + } + + $data = [ + 'account_id' => auth()->user()->account_id, + 'contact_id' => $contact->id, + 'initial_date' => $request->input('initial_date'), + 'frequency_type' => $frequency_type, + 'frequency_number' => is_null($request->input('frequency_number')) ? 1 : $request->input('frequency_number'), + 'title' => $request->input('title'), + 'description' => $request->input('description'), + ]; + + app(CreateReminder::class)->execute($data); + + return redirect()->route('people.show', $contact) + ->with('success', trans('people.reminders_create_success')); + } + + /** + * Show the form for editing the specified resource. + * + * @param Contact $contact + * @param Reminder $reminder + * @return \Illuminate\View\View + */ + public function edit(Contact $contact, Reminder $reminder) + { + return view('people.reminders.edit') + ->withContact($contact) + ->withAccountHasLimitations(AccountHelper::hasLimitations(auth()->user()->account)) + ->withReminder($reminder); + } + + /** + * Update the reminder. + * + * @param Request $request + * @param Contact $contact + * @param Reminder $reminder + * @return \Illuminate\Http\RedirectResponse + */ + public function update(Request $request, Contact $contact, Reminder $reminder) + { + $frequency_type = $request->input('frequency_type'); + if ($frequency_type === 'recurrent') { + $frequency_type = $request->input('frequency_number_select'); + } + + $data = [ + 'account_id' => auth()->user()->account_id, + 'contact_id' => $contact->id, + 'reminder_id' => $reminder->id, + 'initial_date' => $request->input('initial_date'), + 'frequency_type' => $frequency_type, + 'frequency_number' => is_null($request->input('frequency_number')) ? 1 : $request->input('frequency_number'), + 'title' => $request->input('title'), + 'description' => $request->input('description'), + ]; + + app(UpdateReminder::class)->execute($data); + + return redirect()->route('people.show', $contact) + ->with('success', trans('people.reminders_update_success')); + } + + /** + * Destroy the reminder. + * + * @param Request $request + * @param Contact $contact + * @param Reminder $reminder + * @return \Illuminate\Http\RedirectResponse + */ + public function destroy(Request $request, Contact $contact, Reminder $reminder) + { + $data = [ + 'account_id' => $reminder->account_id, + 'reminder_id' => $reminder->id, + ]; + + app(DestroyReminder::class)->execute($data); + + return redirect()->route('people.show', $contact) + ->with('success', trans('people.reminders_delete_success')); + } +} diff --git a/app/Http/Controllers/Contacts/TagsController.php b/app/Http/Controllers/Contacts/TagsController.php new file mode 100644 index 0000000..ebd863c --- /dev/null +++ b/app/Http/Controllers/Contacts/TagsController.php @@ -0,0 +1,74 @@ +user()->account->tags()->get(); + + return TagResource::collection($tags); + } + + /** + * Get the list of all the tags for this contact. + * + * @param Request $request + * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection + */ + public function get(Request $request, Contact $contact) + { + $tags = $contact->tags()->get(); + + return TagResource::collection($tags); + } + + /** + * Update the specified resource in storage. + * + * @param Request $request + * @param Contact $contact + * @return void + */ + public function update(Request $request, Contact $contact): void + { + $contact->throwInactive(); + + $tags = $request->all(); + + // detaching all the tags + $contactTags = $contact->tags()->get(); + foreach ($contactTags as $tag) { + app(DetachTag::class)->execute([ + 'account_id' => auth()->user()->account_id, + 'contact_id' => $contact->id, + 'tag_id' => $tag->id, + ]); + } + + // attach all the new/updated tags + foreach ($tags as $tag) { + if (! empty($tag['name'])) { + app(AssociateTag::class)->execute([ + 'account_id' => auth()->user()->account_id, + 'contact_id' => $contact->id, + 'name' => $tag['name'], + ]); + } + } + } +} diff --git a/app/Http/Controllers/Contacts/TasksController.php b/app/Http/Controllers/Contacts/TasksController.php new file mode 100644 index 0000000..d75bc52 --- /dev/null +++ b/app/Http/Controllers/Contacts/TasksController.php @@ -0,0 +1,32 @@ +tasks as $task) { + $data = [ + 'id' => $task->id, + 'title' => $task->title, + 'description' => $task->description, + 'completed' => $task->completed, + 'completed_at' => ($task->completed_at) ? DateHelper::getShortDate($task->completed_at) : null, + 'edit' => false, + ]; + $tasks->push($data); + } + + return $tasks; + } +} diff --git a/app/Http/Controllers/ContactsController.php b/app/Http/Controllers/ContactsController.php new file mode 100644 index 0000000..d2bc821 --- /dev/null +++ b/app/Http/Controllers/ContactsController.php @@ -0,0 +1,755 @@ +contacts($request, true); + } + + /** + * Display a listing of the resource. + * + * @param Request $request + * @return View|RedirectResponse + */ + public function archived(Request $request) + { + return $this->contacts($request, false); + } + + /** + * Display contacts. + * + * @param Request $request + * @param bool $active + * @return View|RedirectResponse + */ + private function contacts(Request $request, bool $active) + { + $user = $request->user(); + $sort = $request->input('sort') ?? $user->contacts_sort_order; + $showDeceased = $request->input('show_dead'); + + if ($user->contacts_sort_order !== $sort) { + app(UpdateViewPreference::class)->execute([ + 'account_id' => $user->account_id, + 'user_id' => $user->id, + 'preference' => $sort, + ]); + } + + $contacts = $user->account->contacts()->real(); + if ($active) { + $archived = (clone $contacts)->notActive(); + $contacts = (clone $contacts)->active(); + $nbArchived = $archived->count(); + } else { + $contacts = $contacts->notActive(); + $nbArchived = $contacts->count(); + } + + $tagsCount = Tag::contactsCount(); + $contactsWithoutTagsCount = (clone $contacts)->doesntHave('tags')->count(); + + $tags = null; + $url = null; + $count = 1; + + if ($request->input('tags')) { + $tagsInput = $request->input('tags'); + + $tags = $tagsCount->filter(function ($tag) use ($tagsInput) { + return in_array($tag->name, $tagsInput); + }); + + $url = $tags->map(function ($tag): string { + return 'tags[]='.urlencode($tag->name); + })->join('&'); + + if ('' !== $url) { + $url .= '&'; + } + + if ($tags->count() === 0) { + return redirect()->route('people.index'); + } else { + $contacts = $contacts->tags($tags); + } + } elseif ($request->input('no_tag')) { + $contacts = $contacts->tags('NONE'); + } + + $contactsCount = (clone $contacts)->alive()->count(); + $deceasedCount = (clone $contacts)->dead()->count(); + + if ($showDeceased === 'true') { + $contactsCount += $deceasedCount; + } + + $accountHasLimitations = AccountHelper::hasLimitations(auth()->user()->account); + + return view('people.index') + ->withAccountHasLimitations($accountHasLimitations) + ->withHidingDeceased($showDeceased !== 'true') + ->withDeceasedCount($deceasedCount) + ->withActive($active) + ->withContactsCount($contactsCount) + ->withHasArchived($nbArchived > 0) + ->withArchivedContacts($nbArchived) + ->withTags($tags) + ->withSort($sort) + ->withTagsCount($tagsCount) + ->withUrl($url) + ->withTagCount($count) + ->withTagLess($request->input('no_tag') ?? false) + ->with('contactsWithoutTagsCount', $contactsWithoutTagsCount); + } + + /** + * Show the form to add a new contact. + * + * @param Request $request + * @return View|Factory|RedirectResponse + */ + public function create(Request $request) + { + return $this->createForm($request, false); + } + + /** + * Show the form in case the contact is missing. + * + * @param Request $request + * @return View|Factory|RedirectResponse + */ + public function missing(Request $request) + { + return $this->createForm($request, true); + } + + /** + * Show the Add user form unless the contact has limitations. + * + * @param Request $request + * @param bool $isContactMissing + * @return View|Factory|RedirectResponse + */ + private function createForm(Request $request, bool $isContactMissing = false) + { + $accountHasLimitations = AccountHelper::hasLimitations(auth()->user()->account); + + if ($accountHasLimitations + && AccountHelper::hasReachedContactLimit(auth()->user()->account) + && ! auth()->user()->account->legacy_free_plan_unlimited_contacts) { + return redirect()->route('settings.subscriptions.index'); + } + + return view('people.create') + ->withAccountHasLimitations($accountHasLimitations) + ->withIsContactMissing($isContactMissing) + ->withGenders(GenderHelper::getGendersInput()) + ->withDefaultGender(auth()->user()->account->default_gender_id) + ->withFormNameOrder(FormHelper::getNameOrderForForms(auth()->user())) + ->withFirstName($request->input('first_name')) + ->withLastName($request->input('last_name')) + ->withEmail($request->input('email')); + } + + /** + * Store the contact. + * + * @param Request $request + * @return RedirectResponse + */ + public function store(Request $request) + { + try { + $contact = app(CreateContact::class)->execute([ + 'account_id' => auth()->user()->account_id, + 'author_id' => auth()->user()->id, + 'first_name' => $request->input('first_name'), + 'middle_name' => $request->input('middle_name', null), + 'last_name' => $request->input('last_name', null), + 'nickname' => $request->input('nickname', null), + 'email' => $request->input('email', null), + 'gender_id' => $request->input('gender'), + 'is_birthdate_known' => false, + 'is_deceased' => false, + 'is_deceased_date_known' => false, + ]); + } catch (ValidationException $e) { + return back() + ->withInput() + ->withErrors($e->validator); + } + + // Did the user press "Save" or "Submit and add another person" + if (! is_null($request->input('save'))) { + return redirect()->route('people.show', $contact); + } else { + return redirect()->route('people.create') + ->with('status', trans('people.people_add_success', ['name' => $contact->name])); + } + } + + /** + * Display the contact profile. + * + * @param Contact $contact + * @return View|RedirectResponse + */ + public function show(Contact $contact) + { + // make sure we don't display a partial contact + if ($contact->is_partial) { + $realContact = $contact->getRelatedRealContact(); + if (is_null($realContact)) { + return redirect()->route('people.index') + ->withErrors(trans('people.people_not_found')); + } + + return redirect()->route('people.show', $realContact); + } + $contact->load(['notes' => function ($query) { + $query->orderBy('updated_at', 'desc'); + }]); + + UpdateLastConsultedDate::dispatch($contact); + + $relationships = $contact->relationships; + // get love relationship type + $loveRelationships = $relationships->filter(function ($item) { + $item->relationshipTypeLocalized = $item->relationshipType->getLocalizedName(null, false, $item->ofContact->gender->type ?? null); + + return $item->relationshipType->relationshipTypeGroup->name == 'love'; + }); + $loveRelationships->sortByCollator('relationshipTypeLocalized'); + + // get family relationship type + $familyRelationships = $relationships->filter(function ($item) { + $item->relationshipTypeLocalized = $item->relationshipType->getLocalizedName(null, false, $item->ofContact->gender->type ?? null); + + return $item->relationshipType->relationshipTypeGroup->name == 'family'; + }); + $familyRelationships->sortByCollator('relationshipTypeLocalized'); + + // get friend relationship type + $friendRelationships = $relationships->filter(function ($item) { + $item->relationshipTypeLocalized = $item->relationshipType->getLocalizedName(null, false, $item->ofContact->gender->type ?? null); + + return $item->relationshipType->relationshipTypeGroup->name == 'friend'; + }); + $friendRelationships->sortByCollator('relationshipTypeLocalized'); + + // get work relationship type + $workRelationships = $relationships->filter(function ($item) { + $item->relationshipTypeLocalized = $item->relationshipType->getLocalizedName(null, false, $item->ofContact->gender->type ?? null); + + return $item->relationshipType->relationshipTypeGroup->name == 'work'; + }); + $workRelationships->sortByCollator('relationshipTypeLocalized'); + + // reminders + $reminders = $contact->reminders()->active()->get(); + $relevantRemindersFromRelatedContacts = $contact->getBirthdayRemindersAboutRelatedContacts(); + $reminders = $reminders->merge($relevantRemindersFromRelatedContacts); + // now we need to sort the reminders by next date they will be triggered + foreach ($reminders as $reminder) { + $next_expected_date = $reminder->calculateNextExpectedDateOnTimezone(); + $reminder->next_expected_date_human_readable = DateHelper::getShortDate($next_expected_date); + $reminder->next_expected_date = DateHelper::getDate($next_expected_date); + } + $reminders = $reminders->sortBy('next_expected_date'); + + // list of active features + $modules = $contact->account->modules()->active()->get(); + + // add `---` at the top of the dropdowns + $days = DateHelper::getListOfDays(); + $days->prepend([ + 'id' => 0, + 'name' => '---', + ]); + + $months = DateHelper::getListOfMonths(); + $months->prepend([ + 'id' => 0, + 'name' => '---', + ]); + + $hasReachedAccountStorageLimit = StorageHelper::hasReachedAccountStorageLimit($contact->account); + $accountHasLimitations = AccountHelper::hasLimitations($contact->account); + + return view('people.profile') + ->withHasReachedAccountStorageLimit($hasReachedAccountStorageLimit) + ->withAccountHasLimitations($accountHasLimitations) + ->withLoveRelationships($loveRelationships) + ->withFamilyRelationships($familyRelationships) + ->withFriendRelationships($friendRelationships) + ->withWorkRelationships($workRelationships) + ->withReminders($reminders) + ->withModules($modules) + ->withContact($contact) + ->withWeather($contact->getWeather()) + ->withDays($days) + ->withMonths($months) + ->withYears(DateHelper::getListOfYears()); + } + + /** + * Display the Edit people's view. + * + * @param Contact $contact + * @return View|RedirectResponse + */ + public function edit(Contact $contact) + { + $contact->throwInactive(); + + $now = now(); + $age = (string) (! is_null($contact->birthdate) ? $contact->birthdate->getAge() : 0); + $birthdate = ! is_null($contact->birthdate) ? $contact->birthdate->date->toDateString() : $now->toDateString(); + $deceaseddate = ! is_null($contact->deceasedDate) ? $contact->deceasedDate->date->toDateString() : ''; + $day = ! is_null($contact->birthdate) ? $contact->birthdate->date->day : $now->day; + $month = ! is_null($contact->birthdate) ? $contact->birthdate->date->month : $now->month; + + $hasBirthdayReminder = ! is_null($contact->birthday_reminder_id); + $hasDeceasedReminder = ! is_null($contact->deceased_reminder_id); + + $accountHasLimitations = AccountHelper::hasLimitations(auth()->user()->account); + + return view('people.edit') + ->withAccountHasLimitations($accountHasLimitations) + ->withContact($contact) + ->withDays(DateHelper::getListOfDays()) + ->withMonths(DateHelper::getListOfMonths()) + ->withBirthdayState($contact->getBirthdayState()) + ->withBirthdate($birthdate) + ->withDeceaseddate($deceaseddate) + ->withDay($day) + ->withMonth($month) + ->withAge($age) + ->withHasBirthdayReminder($hasBirthdayReminder) + ->withHasDeceasedReminder($hasDeceasedReminder) + ->withGenders(GenderHelper::getGendersInput()) + ->withFormNameOrder(FormHelper::getNameOrderForForms(auth()->user())); + } + + /** + * Update the contact. + * + * @param Request $request + * @param Contact $contact + * @return RedirectResponse + */ + public function update(Request $request, Contact $contact) + { + $contact->throwInactive(); + + // process birthday dates + // TODO: remove this part entirely when we redo this whole SpecialDate + // thing + if ($request->input('birthdate') == 'exact') { + $birthdate = $request->input('birthdayDate'); + $birthdate = DateHelper::parseDate($birthdate); + $day = $birthdate->day; + $month = $birthdate->month; + $year = $birthdate->year; + } else { + $day = $request->input('day'); + $month = $request->input('month'); + $year = $request->input('year'); + } + $is_deceased_date_known = false; + if ($request->input('is_deceased_date_known') === 'true' && $request->input('deceased_date')) { + $is_deceased_date_known = true; + $deceased_date = $request->input('deceased_date'); + $deceased_date = DateHelper::parseDate($deceased_date); + $deceased_date_day = $deceased_date->day; + $deceased_date_month = $deceased_date->month; + $deceased_date_year = $deceased_date->year; + } else { + $deceased_date_day = $deceased_date_month = $deceased_date_year = null; + } + if (! empty($request->input('is_deceased'))) { + //if the contact has died, disable StayInTouch + $contact->updateStayInTouchFrequency(0); + $contact->setStayInTouchTriggerDate(0); + } + + $data = [ + 'account_id' => auth()->user()->account_id, + 'author_id' => auth()->user()->id, + 'contact_id' => $contact->id, + 'first_name' => $request->input('firstname'), + 'middle_name' => $request->input('middlename', null), + 'last_name' => $request->input('lastname', null), + 'nickname' => $request->input('nickname', null), + 'gender_id' => $request->input('gender'), + 'description' => $request->input('description', null), + 'is_birthdate_known' => ! empty($request->input('birthdate')) && $request->input('birthdate') !== 'unknown', + 'birthdate_day' => $day, + 'birthdate_month' => $month, + 'birthdate_year' => $year, + 'birthdate_is_age_based' => $request->input('birthdate') === 'approximate', + 'birthdate_age' => $request->input('age'), + 'birthdate_add_reminder' => ! empty($request->input('addReminder')), + 'is_deceased' => ! empty($request->input('is_deceased')), + 'is_deceased_date_known' => $is_deceased_date_known, + 'deceased_date_day' => $deceased_date_day, + 'deceased_date_month' => $deceased_date_month, + 'deceased_date_year' => $deceased_date_year, + 'deceased_date_add_reminder' => ! empty($request->input('add_reminder_deceased')), + ]; + + $contact = app(UpdateContact::class)->execute($data); + + if ($request->file('avatar') != '') { + if ($contact->has_avatar) { + try { + $contact->deleteAvatars(); + } catch (\Exception $e) { + Log::warning(__CLASS__.' update: Failed to delete avatars', [ + 'contact' => $contact, + $e, + ]); + } + } + $contact->has_avatar = true; + $contact->avatar_location = config('filesystems.default'); + $contact->avatar_file_name = $request->file('avatar')->store('avatars', [ + 'disk' => $contact->avatar_location, + 'visibility' => config('filesystems.default_visibility'), + ]); + $contact->save(); + } + + return redirect()->route('people.show', $contact) + ->with('success', trans('people.information_edit_success')); + } + + /** + * Delete the contact. + * + * @param Request $request + * @param Contact $contact + * @return RedirectResponse + */ + public function destroy(Request $request, Contact $contact) + { + if ($contact->account_id != auth()->user()->account_id) { + return redirect()->route('people.index'); + } + + $data = [ + 'account_id' => auth()->user()->account_id, + 'contact_id' => $contact->id, + ]; + + DestroyContact::dispatch($data); + + return redirect()->route('people.index') + ->with('success', trans('people.people_delete_success')); + } + + /** + * Show the Edit work view. + * + * @param Request $request + * @param Contact $contact + * @return View|RedirectResponse + */ + public function editWork(Request $request, Contact $contact) + { + $contact->throwInactive(); + + return view('people.work.edit') + ->withContact($contact); + } + + /** + * Save the work information. + * + * @param Request $request + * @param Contact $contact + * @return RedirectResponse + */ + public function updateWork(Request $request, Contact $contact) + { + $contact->throwInactive(); + + $contact = app(UpdateWorkInformation::class)->execute([ + 'account_id' => auth()->user()->account_id, + 'author_id' => auth()->user()->id, + 'contact_id' => $contact->id, + 'job' => $request->input('job'), + 'company' => $request->input('company'), + ]); + + return redirect()->route('people.show', $contact) + ->with('success', trans('people.work_edit_success')); + } + + /** + * Show the Edit food preferences view. + * + * @param Request $request + * @param Contact $contact + * @return View|RedirectResponse + */ + public function editFoodPreferences(Request $request, Contact $contact) + { + $contact->throwInactive(); + + $accountHasLimitations = AccountHelper::hasLimitations(auth()->user()->account); + + return view('people.food-preferences.edit') + ->withAccountHasLimitations($accountHasLimitations) + ->withContact($contact); + } + + /** + * Save the food preferences. + * + * @param Request $request + * @param Contact $contact + * @return RedirectResponse + */ + public function updateFoodPreferences(Request $request, Contact $contact) + { + $contact->throwInactive(); + + $contact = app(UpdateContactFoodPreferences::class)->execute([ + 'account_id' => auth()->user()->account_id, + 'contact_id' => $contact->id, + 'food_preferences' => $request->input('food'), + ]); + + return redirect()->route('people.show', $contact) + ->with('success', trans('people.food_preferences_add_success')); + } + + /** + * Search used in the header. + * + * @param Request $request + */ + public function search(Request $request) + { + $needle = $request->needle; + + if ($needle == null) { + return; + } + + $results = SearchHelper::searchContacts($needle, 'created_at') + ->paginate(20); + + if ($results->total() > 0) { + return ContactResource::collection($results); + } else { + return ['noResults' => trans('people.people_search_no_results')]; + } + } + + /** + * Download the contact as vCard. + * + * @param Contact $contact + * @return \Illuminate\Http\Response + */ + public function vCard(Contact $contact) + { + if (config('app.debug') && class_exists('\Barryvdh\Debugbar\Facade')) { + Debugbar::disable(); + } + + $vcard = app(ExportVCard::class)->execute([ + 'account_id' => auth()->user()->account_id, + 'contact_id' => $contact->id, + ]); + + return response($vcard->serialize()) + ->header('Content-type', 'text/x-vcard') + ->header('Content-Disposition', 'attachment; filename='.Str::slug($contact->name, '-', LocaleHelper::getLang()).'.vcf'); + } + + /** + * Set or change the frequency of which the user wants to stay in touch with + * the given contact. + * + * @param Request $request + * @param Contact $contact + * @return array + */ + public function stayInTouch(Request $request, Contact $contact) + { + $contact->throwInactive(); + + $frequency = intval($request->input('frequency')); + $state = $request->input('state'); + + if (AccountHelper::hasLimitations(auth()->user()->account)) { + throw new \LogicException(trans('people.stay_in_touch_premium')); + } + + // if not active, set frequency to 0 + if (! $state) { + $frequency = 0; + } + $result = $contact->updateStayInTouchFrequency($frequency); + + if (! $result) { + throw new \LogicException(trans('people.stay_in_touch_invalid')); + } + + $contact->setStayInTouchTriggerDate($frequency); + + return [ + 'frequency' => $frequency, + 'trigger_date' => $contact->stay_in_touch_trigger_date, + ]; + } + + /** + * Toggle favorites of a contact. + * + * @param Request $request + * @param Contact $contact + * @return array + */ + public function favorite(Request $request, Contact $contact) + { + $bool = (bool) $request->input('toggle'); + + $contact->is_starred = $bool; + $contact->save(); + + return [ + 'is_starred' => $bool, + ]; + } + + /** + * Toggle archive state of a contact. + * + * @param Request $request + * @param Contact $contact + * @return array + */ + public function archive(Request $request, Contact $contact) + { + if (! $contact->is_active + && AccountHelper::hasReachedContactLimit(auth()->user()->account) + && AccountHelper::hasLimitations(auth()->user()->account) + && ! auth()->user()->account->legacy_free_plan_unlimited_contacts) { + abort(402); + } + + $contact->is_active = ! $contact->is_active; + $contact->save(); + + return [ + 'is_active' => $contact->is_active, + ]; + } + + /** + * Display the list of contacts. + * + * @param Request $request + * @return array + */ + public function list(Request $request) + { + $accountId = auth()->user()->account_id; + + $user = $request->user(); + $sort = $request->input('sort') ?? $user->contacts_sort_order; + + if ($user->contacts_sort_order !== $sort) { + app(UpdateViewPreference::class)->execute([ + 'account_id' => $user->account_id, + 'user_id' => $user->id, + 'preference' => $sort, + ]); + } + + $tags = null; + + $contacts = $user->account->contacts()->real(); + + // filter out archived contacts if necessary + if ($request->input('show_archived') != 'true') { + $contacts = $contacts->active(); + } else { + $contacts = $contacts->notActive(); + } + + // filter out deceased if necessary + if ($request->input('show_dead') != 'true') { + $contacts = $contacts->alive(); + } + + if ($request->input('tags')) { + $tags = Tag::where('account_id', $accountId) + ->whereIn('name', $request->input('tags')) + ->get(); + + if ($tags->count() > 0) { + $contacts = $contacts->tags($tags); + } + } elseif ($request->input('no_tag')) { + // get tag less contacts + $contacts = $contacts->tags('NONE'); + } + + // get the number of contacts per page + $perPage = $request->has('perPage') ? $request->input('perPage') : config('monica.number_of_contacts_pagination'); + + // search contacts + $contacts = $contacts->search($request->input('search') ?? '', $accountId, 'is_starred', 'desc', $sort) + ->paginate($perPage); + + return [ + 'totalRecords' => $contacts->total(), + 'contacts' => ContactResource::collection($contacts), + ]; + } +} diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php new file mode 100644 index 0000000..03e02a2 --- /dev/null +++ b/app/Http/Controllers/Controller.php @@ -0,0 +1,13 @@ +realm = $realm; + } + + /** + * Check Laravel authentication. + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return array + */ + public function check(RequestInterface $request, ResponseInterface $response) + { + if (! Auth::check()) { + return [false, 'User is not authenticated']; + } + + return [true, PrincipalBackend::getPrincipalUser(Auth::user())]; + } + + /** + * This method is called when a user could not be authenticated, and + * authentication was required for the current request. + * + * This gives you the opportunity to set authentication headers. The 401 + * status code will already be set. + * + * In this case of Bearer Auth, this would for example mean that the + * following header needs to be set: + * + * $response->addHeader('WWW-Authenticate', 'Bearer realm=SabreDAV'); + * + * Keep in mind that in the case of multiple authentication backends, other + * WWW-Authenticate headers may already have been set, and you'll want to + * append your own WWW-Authenticate header instead of overwriting the + * existing one. + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return void + */ + public function challenge(RequestInterface $request, ResponseInterface $response) + { + $auth = new \Sabre\HTTP\Auth\Bearer( + $this->realm, + $request, + $response + ); + $auth->requireLogin(); + } +} diff --git a/app/Http/Controllers/DAV/Backend/CalDAV/AbstractCalDAVBackend.php b/app/Http/Controllers/DAV/Backend/CalDAV/AbstractCalDAVBackend.php new file mode 100644 index 0000000..368c3e7 --- /dev/null +++ b/app/Http/Controllers/DAV/Backend/CalDAV/AbstractCalDAVBackend.php @@ -0,0 +1,42 @@ +refreshSyncToken(null)->id; + + return [ + 'id' => $this->backendUri(), + 'uri' => $this->backendUri(), + 'principaluri' => PrincipalBackend::getPrincipalUser($this->user), + '{DAV:}sync-token' => $token, + '{'.SabreServer::NS_SABREDAV.'}sync-token' => $token, + '{'.CalDAVPlugin::NS_CALENDARSERVER.'}getctag' => $token, + ]; + } + + /** + * Get the new exported version of the object. + * + * @param mixed $obj + * @return string + */ + abstract protected function refreshObject($obj): string; +} diff --git a/app/Http/Controllers/DAV/Backend/CalDAV/CalDAVBackend.php b/app/Http/Controllers/DAV/Backend/CalDAV/CalDAVBackend.php new file mode 100644 index 0000000..b5d8e4d --- /dev/null +++ b/app/Http/Controllers/DAV/Backend/CalDAV/CalDAVBackend.php @@ -0,0 +1,356 @@ +init($this->user), + app(CalDAVTasks::class)->init($this->user), + ]; + } + + /** + * Get the backend for this id. + * + * @return AbstractCalDAVBackend|null + */ + private function getBackend($id) + { + return collect($this->getBackends())->first(function ($backend) use ($id) { + return $backend->backendUri() === $id; + }); + } + + /** + * Returns a list of calendars for a principal. + * + * Every project is an array with the following keys: + * * id, a unique id that will be used by other functions to modify the + * calendar. This can be the same as the uri or a database key. + * * uri, which is the basename of the uri with which the calendar is + * accessed. + * * principaluri. The owner of the calendar. Almost always the same as + * principalUri passed to this method. + * + * Furthermore it can contain webdav properties in clark notation. A very + * common one is '{DAV:}displayname'. + * + * Many clients also require: + * {urn:ietf:params:xml:ns:caldav}supported-calendar-component-set + * For this property, you can just return an instance of + * Sabre\CalDAV\Property\SupportedCalendarComponentSet. + * + * If you return {http://sabredav.org/ns}read-only and set the value to 1, + * ACL will automatically be put in read-only mode. + * + * @param string $principalUri + * @return array + */ + public function getCalendarsForUser($principalUri) + { + return array_map(function ($backend) { + return $backend->getDescription(); + }, $this->getBackends()); + } + + /** + * The getChanges method returns all the changes that have happened, since + * the specified syncToken in the specified calendar. + * + * This function should return an array, such as the following: + * + * [ + * 'syncToken' => 'The current synctoken', + * 'added' => [ + * 'new.txt', + * ], + * 'modified' => [ + * 'modified.txt', + * ], + * 'deleted' => [ + * 'foo.php.bak', + * 'old.txt' + * ] + * ); + * + * The returned syncToken property should reflect the *current* syncToken + * of the calendar, as reported in the {http://sabredav.org/ns}sync-token + * property This is * needed here too, to ensure the operation is atomic. + * + * If the $syncToken argument is specified as null, this is an initial + * sync, and all members should be reported. + * + * The modified property is an array of nodenames that have changed since + * the last token. + * + * The deleted property is an array with nodenames, that have been deleted + * from collection. + * + * The $syncLevel argument is basically the 'depth' of the report. If it's + * 1, you only have to report changes that happened only directly in + * immediate descendants. If it's 2, it should also include changes from + * the nodes below the child collections. (grandchildren) + * + * The $limit argument allows a client to specify how many results should + * be returned at most. If the limit is not specified, it should be treated + * as infinite. + * + * If the limit (infinite or not) is higher than you're willing to return, + * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception. + * + * If the syncToken is expired (due to data cleanup) or unknown, you must + * return null. + * + * The limit is 'suggestive'. You are free to ignore it. + * + * @param string $calendarId + * @param string $syncToken + * @param int $syncLevel + * @param int $limit + * @return array + */ + public function getChangesForCalendar($calendarId, $syncToken, $syncLevel, $limit = null) + { + $backend = $this->getBackend($calendarId); + if ($backend) { + return $backend->getChanges($calendarId, $syncToken); + } + + return []; + } + + /** + * Returns all calendar objects within a calendar. + * + * Every item contains an array with the following keys: + * * calendardata - The iCalendar-compatible calendar data + * * uri - a unique key which will be used to construct the uri. This can + * be any arbitrary string, but making sure it ends with '.ics' is a + * good idea. This is only the basename, or filename, not the full + * path. + * * lastmodified - a timestamp of the last modification time + * * etag - An arbitrary string, surrounded by double-quotes. (e.g.: + * '"abcdef"') + * * size - The size of the calendar objects, in bytes. + * * component - optional, a string containing the type of object, such + * as 'vevent' or 'vtodo'. If specified, this will be used to populate + * the Content-Type header. + * + * Note that the etag is optional, but it's highly encouraged to return for + * speed reasons. + * + * The calendardata is also optional. If it's not returned + * 'getCalendarObject' will be called later, which *is* expected to return + * calendardata. + * + * If neither etag or size are specified, the calendardata will be + * used/fetched to determine these numbers. If both are specified the + * amount of times this is needed is reduced by a great degree. + * + * @param mixed $calendarId + * @return array + */ + public function getCalendarObjects($calendarId) + { + $backend = $this->getBackend($calendarId); + if ($backend) { + $objs = $backend->getObjects($calendarId); + + return $objs + ->map(function ($date) use ($backend) { + return $backend->prepareData($date); + }) + ->filter(function ($event) { + return $event !== null; + }) + ->toArray(); + } + + return []; + } + + /** + * Returns information from a single calendar object, based on it's object + * uri. + * + * The object uri is only the basename, or filename and not a full path. + * + * The returned array must have the same keys as getCalendarObjects. The + * 'calendardata' object is required here though, while it's not required + * for getCalendarObjects. + * + * This method must return null if the object did not exist. + * + * @param mixed $calendarId + * @param string $objectUri + * @return array|null + */ + public function getCalendarObject($calendarId, $objectUri) + { + $backend = $this->getBackend($calendarId); + if ($backend) { + $obj = $backend->getObject($calendarId, $objectUri); + + if ($obj) { + return $backend->prepareData($obj); + } + } + + return []; + } + + /** + * Creates a new calendar object. + * + * The object uri is only the basename, or filename and not a full path. + * + * It is possible to return an etag from this function, which will be used + * in the response to this PUT request. Note that the ETag must be + * surrounded by double-quotes. + * + * However, you should only really return this ETag if you don't mangle the + * calendar-data. If the result of a subsequent GET to this object is not + * the exact same as this request body, you should omit the ETag. + * + * @param mixed $calendarId + * @param string $objectUri + * @param string $calendarData + * @return string|null + */ + public function createCalendarObject($calendarId, $objectUri, $calendarData) + { + return $this->updateCalendarObject($calendarId, $objectUri, $calendarData); + } + + /** + * Updates an existing calendarobject, based on it's uri. + * + * The object uri is only the basename, or filename and not a full path. + * + * It is possible return an etag from this function, which will be used in + * the response to this PUT request. Note that the ETag must be surrounded + * by double-quotes. + * + * However, you should only really return this ETag if you don't mangle the + * calendar-data. If the result of a subsequent GET to this object is not + * the exact same as this request body, you should omit the ETag. + * + * @param mixed $calendarId + * @param string $objectUri + * @param string $calendarData + * @return string|null + */ + public function updateCalendarObject($calendarId, $objectUri, $calendarData): ?string + { + $backend = $this->getBackend($calendarId); + + return $backend ? + $backend->updateOrCreateCalendarObject($calendarId, $objectUri, $calendarData) + : null; + } + + /** + * Deletes an existing calendar object. + * + * The object uri is only the basename, or filename and not a full path. + * + * @param mixed $calendarId + * @param string $objectUri + * @return void + */ + public function deleteCalendarObject($calendarId, $objectUri) + { + $backend = $this->getBackend($calendarId); + if ($backend) { + $backend->deleteCalendarObject($objectUri); + } + } + + /** + * Creates a new calendar for a principal. + * + * If the creation was a success, an id must be returned that can be used to + * reference this calendar in other methods, such as updateCalendar. + * + * The id can be any type, including ints, strings, objects or array. + * + * @param string $principalUri + * @param string $calendarUri + * @param array $properties + * @return void + */ + public function createCalendar($principalUri, $calendarUri, array $properties): void + { + } + + /** + * Delete a calendar and all its objects. + * + * @param mixed $calendarId + * @return void + */ + public function deleteCalendar($calendarId) + { + } + + /** + * Creates a new subscription for a principal. + * + * If the creation was a success, an id must be returned that can be used to reference + * this subscription in other methods, such as updateSubscription. + * + * @param string $principalUri + * @param string $uri + * @param array $properties + * @return mixed + */ + public function createSubscription($principalUri, $uri, array $properties) + { + return false; + } + + /** + * Updates a subscription. + * + * The list of mutations is stored in a Sabre\DAV\PropPatch object. + * To do the actual updates, you must tell this object which properties + * you're going to process with the handle() method. + * + * Calling the handle method is like telling the PropPatch object "I + * promise I can handle updating this property". + * + * Read the PropPatch documentation for more info and examples. + * + * @param mixed $subscriptionId + * @param \Sabre\DAV\PropPatch $propPatch + * @return void + */ + public function updateSubscription($subscriptionId, DAV\PropPatch $propPatch) + { + } + + /** + * Deletes a subscription. + * + * @param mixed $subscriptionId + * @return void + */ + public function deleteSubscription($subscriptionId) + { + } +} diff --git a/app/Http/Controllers/DAV/Backend/CalDAV/CalDAVBirthdays.php b/app/Http/Controllers/DAV/Backend/CalDAV/CalDAVBirthdays.php new file mode 100644 index 0000000..efd8d61 --- /dev/null +++ b/app/Http/Controllers/DAV/Backend/CalDAV/CalDAVBirthdays.php @@ -0,0 +1,168 @@ + trans('app.dav_birthdays'), + '{'.SabreServer::NS_SABREDAV.'}read-only' => true, + '{'.CalDAVPlugin::NS_CALDAV.'}calendar-description' => trans('app.dav_birthdays_description', ['name' => $this->user->name]), + '{'.CalDAVPlugin::NS_CALDAV.'}calendar-timezone' => $this->user->timezone, + '{'.CalDAVPlugin::NS_CALDAV.'}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VEVENT']), + '{'.CalDAVPlugin::NS_CALDAV.'}schedule-calendar-transp' => new ScheduleCalendarTransp(ScheduleCalendarTransp::TRANSPARENT), + ]; + } + + /** + * Extension for Calendar objects. + * + * @return string + */ + public function getExtension() + { + return '.ics'; + } + + /** + * Datas for this date. + * + * @param mixed $obj + * @return array + */ + public function prepareData($obj) + { + $calendardata = null; + if ($obj instanceof SpecialDate) { + try { + $calendardata = $this->refreshObject($obj); + + return [ + 'id' => $obj->id, + 'uri' => $this->encodeUri($obj), + 'calendardata' => $calendardata, + 'etag' => '"'.sha1($calendardata).'"', + 'lastmodified' => $obj->updated_at->timestamp, + ]; + } catch (\Exception $e) { + Log::error(__CLASS__.' '.__FUNCTION__.': '.$e->getMessage(), [ + 'calendardata' => $calendardata, + $e, + ]); + } + } + + return []; + } + + /** + * Get the new exported version of the object. + * + * @param mixed $obj date + * @return string + */ + protected function refreshObject($obj): string + { + $vcal = app(ExportVCalendar::class) + ->execute([ + 'account_id' => $this->user->account_id, + 'special_date_id' => $obj->id, + ]); + + return $vcal->serialize(); + } + + private function hasBirthday($contact) + { + if (! $contact || ! $contact->birthdate) { + return false; + } + $birthdayState = $contact->getBirthdayState(); + if ($birthdayState != 'almost' && $birthdayState != 'exact') { + return false; + } + + return true; + } + + /** + * Returns the date for the specific uuid. + * + * @param string|null $collectionId + * @param string $uuid + * @return mixed + */ + public function getObjectUuid($collectionId, $uuid) + { + return SpecialDate::where([ + 'account_id' => $this->user->account_id, + 'uuid' => $uuid, + ])->first(); + } + + /** + * Returns the collection of contact's birthdays. + * + * @return \Illuminate\Support\Collection + */ + public function getObjects($collectionId) + { + // We only return the birthday of default addressBook + $contacts = $this->user->account->contacts() + ->real() + ->active() + ->get(); + + return $contacts->filter(function ($contact) { + return $this->hasBirthday($contact); + }) + ->map(function ($contact) { + return $contact->birthdate; + }); + } + + /** + * Returns the collection of deleted birthdays. + * + * @param string|null $collectionId + * @return \Illuminate\Support\Collection + */ + public function getDeletedObjects($collectionId) + { + return collect(); + } + + /** + * @return string|null + */ + public function updateOrCreateCalendarObject($calendarId, $objectUri, $calendarData): ?string + { + return null; + } + + public function deleteCalendarObject($objectUri) + { + // Not implemented + } +} diff --git a/app/Http/Controllers/DAV/Backend/CalDAV/CalDAVTasks.php b/app/Http/Controllers/DAV/Backend/CalDAV/CalDAVTasks.php new file mode 100644 index 0000000..c67384b --- /dev/null +++ b/app/Http/Controllers/DAV/Backend/CalDAV/CalDAVTasks.php @@ -0,0 +1,219 @@ + trans('app.dav_tasks'), + '{'.CalDAVPlugin::NS_CALDAV.'}calendar-description' => trans('app.dav_tasks_description', ['name' => $this->user->name]), + '{'.CalDAVPlugin::NS_CALDAV.'}calendar-timezone' => $this->user->timezone, + '{'.CalDAVPlugin::NS_CALDAV.'}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO']), + '{'.CalDAVPlugin::NS_CALDAV.'}schedule-calendar-transp' => new ScheduleCalendarTransp(ScheduleCalendarTransp::TRANSPARENT), + ]; + } + + /** + * Returns the collection of all tasks. + * + * @param mixed|null $collectionId + * @return \Illuminate\Support\Collection + */ + public function getObjects($collectionId) + { + return $this->user->account + ->tasks() + ->get(); + } + + /** + * Returns the collection of deleted tasks. + * + * @param string|null $collectionId + * @return \Illuminate\Support\Collection + */ + public function getDeletedObjects($collectionId) + { + return collect(); + } + + /** + * Returns the contact for the specific uuid. + * + * @param mixed|null $collectionId + * @param string $uuid + * @return mixed + */ + public function getObjectUuid($collectionId, $uuid) + { + return Task::where([ + 'account_id' => $this->user->account_id, + 'uuid' => $uuid, + ])->first(); + } + + /** + * Extension for Calendar objects. + * + * @return string + */ + public function getExtension() + { + return '.ics'; + } + + /** + * Datas for this task. + * + * @param mixed $obj + * @return array + */ + public function prepareData($obj) + { + $calendardata = null; + if ($obj instanceof Task) { + try { + $calendardata = $this->refreshObject($obj); + + return [ + 'id' => $obj->id, + 'uri' => $this->encodeUri($obj), + 'calendardata' => $calendardata, + 'etag' => '"'.sha1($calendardata).'"', + 'lastmodified' => $obj->updated_at->timestamp, + ]; + } catch (\Exception $e) { + Log::error(__CLASS__.' '.__FUNCTION__.': '.$e->getMessage(), [ + 'calendardata' => $calendardata, + $e, + ]); + } + } + + return []; + } + + /** + * Get the new exported version of the object. + * + * @param mixed $obj task + * @return string + */ + protected function refreshObject($obj): string + { + $vcal = app(ExportTask::class) + ->execute([ + 'account_id' => $this->user->account_id, + 'task_id' => $obj->id, + ]); + + return $vcal->serialize(); + } + + /** + * Updates an existing calendarobject, based on it's uri. + * + * The object uri is only the basename, or filename and not a full path. + * + * It is possible return an etag from this function, which will be used in + * the response to this PUT request. Note that the ETag must be surrounded + * by double-quotes. + * + * However, you should only really return this ETag if you don't mangle the + * calendar-data. If the result of a subsequent GET to this object is not + * the exact same as this request body, you should omit the ETag. + * + * @param string $objectUri + * @param string $calendarData + * @return string|null + */ + public function updateOrCreateCalendarObject($calendarId, $objectUri, $calendarData): ?string + { + $task_id = null; + if ($objectUri) { + $task = $this->getObject($this->backendUri(), $objectUri); + + if ($task) { + $task_id = $task->id; + } + } + + try { + $result = app(ImportTask::class) + ->execute([ + 'account_id' => $this->user->account_id, + 'task_id' => $task_id, + 'entry' => $calendarData, + ]); + + if (! Arr::has($result, 'error')) { + $task = Task::where('account_id', $this->user->account_id) + ->find($result['task_id']); + + $calendar = $this->prepareData($task); + + return $calendar['etag']; + } + } catch (\Exception $e) { + Log::error(__CLASS__.' '.__FUNCTION__.': '.$e->getMessage(), [ + 'calendarId' => $calendarId, + 'objectUri' => $objectUri, + 'calendarData' => $calendarData, + $e, + ]); + } + + return null; + } + + /** + * Deletes an existing calendar object. + * + * The object uri is only the basename, or filename and not a full path. + * + * @param string $objectUri + * @return void + */ + public function deleteCalendarObject($objectUri) + { + $task = $this->getObject($this->backendUri(), $objectUri); + + if ($task) { + try { + app(DestroyTask::class) + ->execute([ + 'account_id' => $this->user->account_id, + 'task_id' => $task->id, + ]); + } catch (\Exception $e) { + Log::error(__CLASS__.' '.__FUNCTION__.': '.$e->getMessage(), [ + 'objectUri' => $objectUri, + $e, + ]); + } + } + } +} diff --git a/app/Http/Controllers/DAV/Backend/CalDAV/ICalDAVBackend.php b/app/Http/Controllers/DAV/Backend/CalDAV/ICalDAVBackend.php new file mode 100644 index 0000000..b74b0ac --- /dev/null +++ b/app/Http/Controllers/DAV/Backend/CalDAV/ICalDAVBackend.php @@ -0,0 +1,118 @@ + '{DAV:}read', + 'principal' => '{DAV:}owner', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}write-content', + 'principal' => '{DAV:}owner', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}bind', + 'principal' => '{DAV:}owner', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}unbind', + 'principal' => '{DAV:}owner', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}write-properties', + 'principal' => '{DAV:}owner', + 'protected' => true, + ], + ]; + } + + /** + * This method returns the ACL's for card nodes in this address book. + * The result of this method automatically gets passed to the + * card nodes in this address book. + * + * @return array + */ + public function getChildACL() + { + return $this->getACL(); + } + + /** + * Returns the last modification date. + * + * @return int|null + */ + public function getLastModified(): ?int + { + $carddavBackend = $this->carddavBackend; + if ($carddavBackend instanceof CardDAVBackend) { + $date = $carddavBackend->getLastModified(null); + if (! is_null($date)) { + return (int) $date->timestamp; + } + } + + return null; + } + + /** + * This method returns the current sync-token for this collection. + * This can be any string. + * + * If null is returned from this function, the plugin assumes there's no + * sync information available. + * + * @return string|null + */ + public function getSyncToken(): ?string + { + $carddavBackend = $this->carddavBackend; + if ($carddavBackend instanceof CardDAVBackend) { + return (string) $carddavBackend->refreshSyncToken(null)->id; + } + + return null; + } +} diff --git a/app/Http/Controllers/DAV/Backend/CardDAV/AddressBookHome.php b/app/Http/Controllers/DAV/Backend/CardDAV/AddressBookHome.php new file mode 100644 index 0000000..1215b34 --- /dev/null +++ b/app/Http/Controllers/DAV/Backend/CardDAV/AddressBookHome.php @@ -0,0 +1,45 @@ + '{DAV:}read', + 'principal' => '{DAV:}owner', + 'protected' => true, + ], + ]; + } + + /** + * Returns a list of addressbooks. + * + * @return array + */ + public function getChildren() + { + $addressBooks = $this->carddavBackend->getAddressBooksForUser($this->principalUri); + + return collect($addressBooks)->map(function (array $addressBook): AddressBook { + return new AddressBook($this->carddavBackend, $addressBook); + })->toArray(); + } +} diff --git a/app/Http/Controllers/DAV/Backend/CardDAV/AddressBookRoot.php b/app/Http/Controllers/DAV/Backend/CardDAV/AddressBookRoot.php new file mode 100644 index 0000000..9653fb2 --- /dev/null +++ b/app/Http/Controllers/DAV/Backend/CardDAV/AddressBookRoot.php @@ -0,0 +1,51 @@ + '{DAV:}read', + 'principal' => '{DAV:}authenticated', + 'protected' => true, + ], + ]; + } + + /** + * This method returns a node for a principal. + * + * The passed array contains principal information, and is guaranteed to + * at least contain a uri item. Other properties may or may not be + * supplied by the authentication backend. + * + * @param array $principal + * @return \Sabre\DAV\INode + * @psalm-suppress ParamNameMismatch + */ + public function getChildForPrincipal(array $principal) + { + return new AddressBookHome($this->carddavBackend, $principal['uri']); + } +} diff --git a/app/Http/Controllers/DAV/Backend/CardDAV/CardDAVBackend.php b/app/Http/Controllers/DAV/Backend/CardDAV/CardDAVBackend.php new file mode 100644 index 0000000..c27006e --- /dev/null +++ b/app/Http/Controllers/DAV/Backend/CardDAV/CardDAVBackend.php @@ -0,0 +1,486 @@ +getDefaultAddressBook(); + + $addressBooks = AddressBook::where('account_id', $this->user->account_id) + ->get(); + + foreach ($addressBooks as $addressBook) { + $result[] = $this->getAddressBookDetails($addressBook); + } + + return $result; + } + + private function getDefaultAddressBook() + { + $des = $this->getAddressBookDetails(null); + + $me = auth()->user()->me; + if ($me) { + $des += [ + '{'.CalDAVPlugin::NS_CALENDARSERVER.'}me-card' => '/'.config('laravelsabre.path').'/addressbooks/'.$this->user->email.'/contacts/'.$this->encodeUri($me), + ]; + } + + return $des; + } + + private function getAddressBookDetails($addressBook) + { + $id = $addressBook ? $addressBook->name : $this->backendUri(); + $token = $this->getCurrentSyncToken($addressBook); + + $des = [ + 'id' => $id, + 'uri' => $id, + 'principaluri' => PrincipalBackend::getPrincipalUser($this->user), + '{DAV:}displayname' => trans('app.dav_contacts'), + '{'.CardDAVPlugin::NS_CARDDAV.'}addressbook-description' => $addressBook ? $addressBook->description : trans('app.dav_contacts_description', ['name' => $this->user->name]), + ]; + if ($token) { + $des += [ + '{DAV:}sync-token' => $token->id, + '{'.SabreServer::NS_SABREDAV.'}sync-token' => $token->id, + '{'.CalDAVPlugin::NS_CALENDARSERVER.'}getctag' => DAVSyncPlugin::SYNCTOKEN_PREFIX.$token->id, + ]; + } + + return $des; + } + + /** + * Extension for Calendar objects. + * + * @return string + */ + public function getExtension() + { + return '.vcf'; + } + + /** + * The getChanges method returns all the changes that have happened, since + * the specified syncToken in the specified address book. + * + * This function should return an array, such as the following: + * + * [ + * 'syncToken' => 'The current synctoken', + * 'added' => [ + * 'new.txt', + * ], + * 'modified' => [ + * 'modified.txt', + * ], + * 'deleted' => [ + * 'foo.php.bak', + * 'old.txt' + * ] + * ]; + * + * The returned syncToken property should reflect the *current* syncToken + * of the calendar, as reported in the {http://sabredav.org/ns}sync-token + * property. This is needed here too, to ensure the operation is atomic. + * + * If the $syncToken argument is specified as null, this is an initial + * sync, and all members should be reported. + * + * The modified property is an array of nodenames that have changed since + * the last token. + * + * The deleted property is an array with nodenames, that have been deleted + * from collection. + * + * The $syncLevel argument is basically the 'depth' of the report. If it's + * 1, you only have to report changes that happened only directly in + * immediate descendants. If it's 2, it should also include changes from + * the nodes below the child collections. (grandchildren) + * + * The $limit argument allows a client to specify how many results should + * be returned at most. If the limit is not specified, it should be treated + * as infinite. + * + * If the limit (infinite or not) is higher than you're willing to return, + * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception. + * + * If the syncToken is expired (due to data cleanup) or unknown, you must + * return null. + * + * The limit is 'suggestive'. You are free to ignore it. + * + * @param string $addressBookId + * @param string $syncToken + * @param int $syncLevel + * @param int $limit + * @return array|null + */ + public function getChangesForAddressBook($addressBookId, $syncToken, $syncLevel, $limit = null): ?array + { + return $this->getChanges($addressBookId, $syncToken); + } + + /** + * Prepare datas for this contact. + * + * @param Contact $contact + * @return array + */ + public function prepareCard($contact): array + { + $carddata = $contact->vcard; + try { + if (empty($carddata)) { + $carddata = $this->refreshObject($contact); + } + + $etag = app(GetEtag::class)->execute([ + 'account_id' => $this->user->account_id, + 'contact_id' => $contact->id, + ]); + + return [ + 'contact_id' => $contact->id, + 'uri' => $this->encodeUri($contact), + 'carddata' => $carddata, + 'etag' => $etag, + 'distant_etag' => $contact->distant_etag, + 'lastmodified' => $contact->updated_at->timestamp, + ]; + } catch (\Exception $e) { + Log::error(__CLASS__.' '.__FUNCTION__.': '.$e->getMessage(), [ + 'carddata' => $carddata, + 'contact_id' => $contact->id, + $e, + ]); + throw $e; + } + } + + /** + * Get the new exported version of the object. + * + * @param mixed $obj contact + * @return string + */ + protected function refreshObject($obj): string + { + $vcard = app(ExportVCard::class) + ->execute([ + 'account_id' => $this->user->account_id, + 'contact_id' => $obj->id, + ]); + + return $vcard->serialize(); + } + + /** + * Returns the contact for the specific uuid. + * + * @param mixed|null $collectionId + * @param string $uuid + * @return Contact + */ + public function getObjectUuid($collectionId, $uuid) + { + $addressBook = null; + if ($collectionId && $collectionId != $this->backendUri()) { + $addressBook = AddressBook::where([ + 'account_id' => $this->user->account_id, + 'name' => $collectionId, + ])->first(); + } + + return Contact::where([ + 'account_id' => $this->user->account_id, + 'uuid' => $uuid, + 'address_book_id' => $addressBook ? $addressBook->id : null, + ])->first(); + } + + /** + * Returns the collection of all active contacts. + * + * @param string|null $collectionId + * @return \Illuminate\Support\Collection + */ + public function getObjects($collectionId) + { + return $this->user->account->contacts($collectionId) + ->real() + ->active() + ->get(); + } + + /** + * Returns the collection of deleted contacts. + * + * @param string|null $collectionId + * @return \Illuminate\Support\Collection + */ + public function getDeletedObjects($collectionId) + { + return $this->user->account->contacts($collectionId) + ->onlyTrashed() + ->get(); + } + + /** + * Returns all cards for a specific addressbook id. + * + * This method should return the following properties for each card: + * * carddata - raw vcard data + * * uri - Some unique url + * * lastmodified - A unix timestamp + * + * It's recommended to also return the following properties: + * * etag - A unique etag. This must change every time the card changes. + * * size - The size of the card in bytes. + * + * If these last two properties are provided, less time will be spent + * calculating them. If they are specified, you can also ommit carddata. + * This may speed up certain requests, especially with large cards. + * + * @param mixed $addressbookId + * @return array + */ + public function getCards($addressbookId) + { + $contacts = $this->getObjects($addressbookId); + + return $contacts->map(function ($contact) { + return $this->prepareCard($contact); + })->toArray(); + } + + /** + * Returns a specific card. + * + * The same set of properties must be returned as with getCards. The only + * exception is that 'carddata' is absolutely required. + * + * If the card does not exist, you must return false. + * + * @param mixed $addressBookId + * @param string $cardUri + * @return array|bool + */ + public function getCard($addressBookId, $cardUri) + { + $contact = $this->getObject($addressBookId, $cardUri); + + if ($contact) { + return $this->prepareCard($contact); + } + + return false; + } + + /** + * Creates a new card. + * + * The addressbook id will be passed as the first argument. This is the + * same id as it is returned from the getAddressBooksForUser method. + * + * The cardUri is a base uri, and doesn't include the full path. The + * cardData argument is the vcard body, and is passed as a string. + * + * It is possible to return an ETag from this method. This ETag is for the + * newly created resource, and must be enclosed with double quotes (that + * is, the string itself must contain the double quotes). + * + * You should only return the ETag if you store the carddata as-is. If a + * subsequent GET request on the same card does not have the same body, + * byte-by-byte and you did return an ETag here, clients tend to get + * confused. + * + * If you don't return an ETag, you can just return null. + * + * @param mixed $addressBookId + * @param string $cardUri + * @param string $cardData + * @return string|null + */ + public function createCard($addressBookId, $cardUri, $cardData) + { + return $this->updateCard($addressBookId, $cardUri, $cardData); + } + + /** + * Updates a card. + * + * The addressbook id will be passed as the first argument. This is the + * same id as it is returned from the getAddressBooksForUser method. + * + * The cardUri is a base uri, and doesn't include the full path. The + * cardData argument is the vcard body, and is passed as a string. + * + * It is possible to return an ETag from this method. This ETag should + * match that of the updated resource, and must be enclosed with double + * quotes (that is: the string itself must contain the actual quotes). + * + * You should only return the ETag if you store the carddata as-is. If a + * subsequent GET request on the same card does not have the same body, + * byte-by-byte and you did return an ETag here, clients tend to get + * confused. + * + * If you don't return an ETag, you can just return null. + * + * @param mixed $addressBookId + * @param string $cardUri + * @param string|resource $cardData + * @return string|null + */ + public function updateCard($addressBookId, $cardUri, $cardData): ?string + { + $job = new UpdateVCard($this->user, $addressBookId, new ContactUpdateDto($cardUri, null, $cardData)); + + Bus::batch([$job]) + ->allowFailures() + ->dispatch(); + + return null; + } + + /** + * Deletes a card. + * + * @param mixed $addressBookId + * @param string $cardUri + * @return bool + */ + public function deleteCard($addressBookId, $cardUri) + { + $contact = $this->getObject($addressBookId, $cardUri); + + if ($contact) { + DestroyContact::dispatch([ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + ]); + + return true; + } + + return false; + } + + /** + * Updates properties for an address book. + * + * The list of mutations is stored in a Sabre\DAV\PropPatch object. + * To do the actual updates, you must tell this object which properties + * you're going to process with the handle() method. + * + * Calling the handle method is like telling the PropPatch object "I + * promise I can handle updating this property". + * + * Read the PropPatch documentation for more info and examples. + * + * @param string $addressBookId + * @param \Sabre\DAV\PropPatch $propPatch + * @return bool|null + */ + public function updateAddressBook($addressBookId, DAV\PropPatch $propPatch): ?bool + { + $propPatch->handle('{'.CalDAVPlugin::NS_CALENDARSERVER.'}me-card', function ($props) use ($addressBookId) { + $contact = $this->getObject($addressBookId, $props->getHref()); + + $data = [ + 'contact_id' => $contact->id, + 'account_id' => $this->user->account_id, + 'user_id' => $this->user->id, + ]; + + app(SetMeContact::class)->execute($data); + + return true; + }); + + return null; + } + + /** + * Creates a new address book. + * + * This method should return the id of the new address book. The id can be + * in any format, including ints, strings, arrays or objects. + * + * @param string $principalUri + * @param string $url Just the 'basename' of the url. + * @param array $properties + * @return int|bool + */ + public function createAddressBook($principalUri, $url, array $properties) + { + return false; + } + + /** + * Deletes an entire addressbook and all its contents. + * + * @param mixed $addressBookId + * @return bool|null + */ + public function deleteAddressBook($addressBookId) + { + return false; + } +} diff --git a/app/Http/Controllers/DAV/Backend/IDAVBackend.php b/app/Http/Controllers/DAV/Backend/IDAVBackend.php new file mode 100644 index 0000000..cdbb231 --- /dev/null +++ b/app/Http/Controllers/DAV/Backend/IDAVBackend.php @@ -0,0 +1,37 @@ + $this->user->account_id, + 'user_id' => $this->user->id, + 'name' => $collectionId ?? $this->backendUri(), + ]) + ->orderBy('created_at') + ->get(); + + return $tokens->count() > 0 ? $tokens->last() : null; + } + + /** + * Create or refresh the token if a change happened. + * + * @param string|null $collectionId + * @return SyncToken + */ + public function refreshSyncToken($collectionId): SyncToken + { + $token = $this->getCurrentSyncToken($collectionId); + + if (! $token || $token->timestamp < $this->getLastModified($collectionId)) { + $token = $this->createSyncTokenNow($collectionId); + } + + return $token; + } + + /** + * Get SyncToken by token id. + * + * @param string|null $collectionId + * @param string $syncToken + * @return SyncToken|null + */ + protected function getSyncToken($collectionId, $syncToken) + { + /** @var SyncToken|null */ + return SyncToken::where([ + 'account_id' => $this->user->account_id, + 'user_id' => $this->user->id, + 'name' => $collectionId ?? $this->backendUri(), + ]) + ->find($syncToken); + } + + /** + * Create a token with now timestamp. + * + * @param string|null $collectionId + * @return SyncToken + */ + private function createSyncTokenNow($collectionId) + { + return SyncToken::create([ + 'account_id' => $this->user->account_id, + 'user_id' => $this->user->id, + 'name' => $collectionId ?? $this->backendUri(), + 'timestamp' => now(), + ]); + } + + /** + * Returns the last modification date. + * + * @param string|null $collectionId + * @return \Carbon\Carbon|null + */ + public function getLastModified($collectionId) + { + return $this->getObjects($collectionId) + ->map(function ($object) { + return $object->updated_at; + }) + ->max(); + } + + /** + * The getChanges method returns all the changes that have happened, since + * the specified syncToken. + * + * This function should return an array, such as the following: + * + * [ + * 'syncToken' => 'The current synctoken', + * 'added' => [ + * 'new.txt', + * ], + * 'modified' => [ + * 'modified.txt', + * ], + * 'deleted' => [ + * 'foo.php.bak', + * 'old.txt' + * ] + * ); + * + * The returned syncToken property should reflect the *current* syncToken + * , as reported in the {http://sabredav.org/ns}sync-token + * property This is * needed here too, to ensure the operation is atomic. + * + * If the $syncToken argument is specified as null, this is an initial + * sync, and all members should be reported. + * + * The modified property is an array of nodenames that have changed since + * the last token. + * + * The deleted property is an array with nodenames, that have been deleted + * from collection. + * + * The $syncLevel argument is basically the 'depth' of the report. If it's + * 1, you only have to report changes that happened only directly in + * immediate descendants. If it's 2, it should also include changes from + * the nodes below the child collections. (grandchildren) + * + * The $limit argument allows a client to specify how many results should + * be returned at most. If the limit is not specified, it should be treated + * as infinite. + * + * If the limit (infinite or not) is higher than you're willing to return, + * you should throw a Sabre\DAV\Exception\TooMuchMatches() exception. + * + * If the syncToken is expired (due to data cleanup) or unknown, you must + * return null. + * + * The limit is 'suggestive'. You are free to ignore it. + * + * @param string $calendarId + * @param string $syncToken + * @return array|null + */ + public function getChanges($calendarId, $syncToken): ?array + { + $token = null; + $timestamp = null; + if (! empty($syncToken)) { + $token = $this->getSyncToken($calendarId, $syncToken); + + if (is_null($token)) { + // syncToken is not recognized + return null; + } + + $timestamp = $token->timestamp; + } + + $objs = $this->getObjects($calendarId); + + $modified = $objs->filter(function ($obj) use ($timestamp) { + return ! is_null($timestamp) && + $obj->updated_at > $timestamp && + $obj->created_at < $timestamp; + }); + $added = $objs->filter(function ($obj) use ($timestamp) { + return is_null($timestamp) || + $obj->created_at >= $timestamp; + }); + $deleted = $this->getDeletedObjects($calendarId) + ->filter(function ($obj) use ($timestamp) { + $d = $obj->deleted_at; + + return is_null($timestamp) || + $obj->deleted_at >= $timestamp; + }); + + return [ + 'syncToken' => $this->refreshSyncToken($calendarId)->id, + 'added' => $added->map(function ($obj) { + return $this->encodeUri($obj); + })->values()->toArray(), + 'modified' => $modified->map(function ($obj) { + $this->refreshObject($obj); + + return $this->encodeUri($obj); + })->values()->toArray(), + 'deleted' => $deleted->map(function ($obj) { + return $this->encodeUri($obj); + })->values()->toArray(), + ]; + } + + protected function encodeUri($obj): string + { + if (empty($obj->uuid)) { + // refresh model from database + $obj->refresh(); + + if (empty($obj->uuid)) { + // in case uuid is still not set, do it + $obj->forceFill([ + 'uuid' => Str::uuid(), + ])->save(); + } + } + + return urlencode($obj->uuid.$this->getExtension()); + } + + private function decodeUri($uri): string + { + return pathinfo(urldecode($uri), PATHINFO_FILENAME); + } + + /** + * Returns the contact uuid for the specific uri. + * + * @param string $uri + * @return string + */ + public function getUuid($uri): string + { + return $this->decodeUri($uri); + } + + /** + * Returns the contact for the specific uri. + * + * @param string|null $collectionId + * @param string $uri + * @return mixed + */ + public function getObject($collectionId, $uri) + { + try { + return $this->getObjectUuid($collectionId, $this->getUuid($uri)); + } catch (\Exception $e) { + // Object not found + } + } + + /** + * Returns the object for the specific uuid. + * + * @param string|null $collectionId + * @param string $uuid + * @return mixed + */ + abstract public function getObjectUuid($collectionId, $uuid); + + /** + * Returns the collection of objects. + * + * @param string|null $collectionId + * @return \Illuminate\Support\Collection + */ + abstract public function getObjects($collectionId); + + /** + * Returns the collection of objects. + * + * @param string|null $collectionId + * @return \Illuminate\Support\Collection + */ + abstract public function getDeletedObjects($collectionId); + + abstract public function getExtension(); + + /** + * Get the new exported version of the object. + * + * @param mixed $obj + * @return string + */ + abstract protected function refreshObject($obj): string; +} diff --git a/app/Http/Controllers/DAV/DAVACL/PrincipalBackend.php b/app/Http/Controllers/DAV/DAVACL/PrincipalBackend.php new file mode 100644 index 0000000..58f10e4 --- /dev/null +++ b/app/Http/Controllers/DAV/DAVACL/PrincipalBackend.php @@ -0,0 +1,203 @@ +email; + } + + protected function getPrincipals() + { + return [ + [ + 'uri' => static::getPrincipalUser($this->user), + '{DAV:}displayname' => $this->user->name, + '{'.SabreServer::NS_SABREDAV.'}email-address' => $this->user->email, + ], + ]; + } + + /** + * Returns a list of principals based on a prefix. + * + * This prefix will often contain something like 'principals'. You are only + * expected to return principals that are in this base path. + * + * You are expected to return at least a 'uri' for every user, you can + * return any additional properties if you wish so. Common properties are: + * {DAV:}displayname + * {http://sabredav.org/ns}email-address - This is a custom SabreDAV + * field that's actually injected in a number of other properties. If + * you have an email address, use this property. + * + * @param string $prefixPath + * @return array + */ + public function getPrincipalsByPrefix($prefixPath) + { + $prefixPath = Str::finish($prefixPath, '/'); + + return array_filter($this->getPrincipals(), function ($principal) use ($prefixPath) { + return ! $prefixPath || strpos($principal['uri'], $prefixPath) == 0; + }); + } + + /** + * Returns a specific principal, specified by its path. + * The returned structure should be the exact same as from + * getPrincipalsByPrefix. + * + * @param string $path + * @return array + */ + public function getPrincipalByPath($path) + { + foreach ($this->getPrincipalsByPrefix(static::PRINCIPAL_PREFIX) as $principal) { + if ($principal['uri'] === $path) { + return $principal; + } + } + + return []; + } + + /** + * Updates one ore more webdav properties on a principal. + * + * The list of mutations is stored in a Sabre\DAV\PropPatch object. + * To do the actual updates, you must tell this object which properties + * you're going to process with the handle() method. + * + * Calling the handle method is like telling the PropPatch object "I + * promise I can handle updating this property". + * + * Read the PropPatch documentation for more info and examples. + * + * @param string $path + * @param \Sabre\DAV\PropPatch $propPatch + * @return void + */ + public function updatePrincipal($path, DAV\PropPatch $propPatch) + { + } + + /** + * This method is used to search for principals matching a set of + * properties. + * + * This search is specifically used by RFC3744's principal-property-search + * REPORT. + * + * The actual search should be a unicode-non-case-sensitive search. The + * keys in searchProperties are the WebDAV property names, while the values + * are the property values to search on. + * + * By default, if multiple properties are submitted to this method, the + * various properties should be combined with 'AND'. If $test is set to + * 'anyof', it should be combined using 'OR'. + * + * This method should simply return an array with full principal uri's. + * + * If somebody attempted to search on a property the backend does not + * support, you should simply return 0 results. + * + * You can also just return 0 results if you choose to not support + * searching at all, but keep in mind that this may stop certain features + * from working. + * + * @param string $prefixPath + * @param array $searchProperties + * @param string $test + * @return array + */ + public function searchPrincipals($prefixPath, array $searchProperties, $test = 'allof') + { + $result = []; + $principals = $this->getPrincipalsByPrefix($prefixPath); + if (! $principals) { + return $result; + } + + foreach ($principals as $principal) { + $ok = false; + foreach ($searchProperties as $key => $value) { + if ($principal[$key] == $value) { + $ok = true; + } elseif ($test == 'allof') { + $ok = false; + break; + } + } + if ($ok) { + $result[] = $principal['uri']; + } + } + + return $result; + } + + /** + * Returns the list of members for a group-principal. + * + * @param string $principal + * @return array + */ + public function getGroupMemberSet($principal) + { + $principal = $this->getPrincipalByPath($principal); + if (! $principal) { + return []; + } + + return [ + $principal['uri'], + ]; + } + + /** + * Returns the list of groups a principal is a member of. + * + * @param string $principal + * @return array + */ + public function getGroupMembership($principal) + { + return $this->getGroupMemberSet($principal); + } + + /** + * Updates the list of group members for a group principal. + * + * The principals should be passed as a list of uri's. + * + * @param string $principal + * @param array $members + * @return void + */ + public function setGroupMemberSet($principal, array $members) + { + } +} diff --git a/app/Http/Controllers/DAV/DAVRedirect.php b/app/Http/Controllers/DAV/DAVRedirect.php new file mode 100644 index 0000000..bde0557 --- /dev/null +++ b/app/Http/Controllers/DAV/DAVRedirect.php @@ -0,0 +1,34 @@ +on('method:GET', [$this, 'httpGet'], 500); + } + + /** + * This method intercepts GET requests to collections and returns the html. + * + * @param RequestInterface $request + * @param ResponseInterface $response + * @return bool + */ + public function httpGet(RequestInterface $request, ResponseInterface $response) + { + $response->setStatus(302); + $response->setHeader('Location', route('settings.dav')); + + return false; + } +} diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php new file mode 100644 index 0000000..bf9d3c7 --- /dev/null +++ b/app/Http/Controllers/DashboardController.php @@ -0,0 +1,184 @@ +user()->account() + ->withCount( + 'contacts', 'reminders', 'notes', 'activities', 'gifts', 'tasks' + )->with('debts.contact') + ->first(); + + $numberOfContacts = $account->contacts() + ->real() + ->active() + ->count(); + + if ($numberOfContacts === 0) { + return view('dashboard.blank'); + } + + // Fetch last updated contacts + $lastUpdatedContactsCollection = collect([]); + $lastUpdatedContacts = $account->contacts() + ->real() + ->active() + ->alive() + ->latest('last_consulted_at') + ->limit(10) + ->get(); + foreach ($lastUpdatedContacts as $contact) { + $data = [ + 'id' => $contact->hashID(), + 'has_avatar' => $contact->has_avatar, + 'avatar_url' => $contact->getAvatarURL(), + 'initials' => $contact->getInitials(), + 'default_avatar_color' => $contact->default_avatar_color, + 'complete_name' => $contact->name, + ]; + $lastUpdatedContactsCollection->push(json_encode($data)); + } + + $debts = $account->debts()->inProgress(); + + $debt_due = $debts->due()->get() + ->reduce(function ($totalDueDebt, Debt $debt) { + return $totalDueDebt + $debt->amount; + }, 0); + + $debt_owed = $debts->owed()->get() + ->reduce(function ($totalOwedDebt, Debt $debt) { + return $totalOwedDebt + $debt->amount; + }, 0); + + // get last 3 changelog entries + $changelogs = InstanceHelper::getChangelogEntries(3); + + // Load the reminderOutboxes for the upcoming three months + $reminderOutboxes = [ + 0 => AccountHelper::getUpcomingRemindersForMonth(auth()->user()->account, 0), + 1 => AccountHelper::getUpcomingRemindersForMonth(auth()->user()->account, 1), + 2 => AccountHelper::getUpcomingRemindersForMonth(auth()->user()->account, 2), + ]; + + $data = [ + 'lastUpdatedContacts' => $lastUpdatedContactsCollection, + 'number_of_contacts' => $numberOfContacts, + 'number_of_reminders' => $account->reminders_count, + 'number_of_notes' => $account->notes_count, + 'number_of_activities' => $account->activities_count, + 'number_of_gifts' => $account->gifts_count, + 'number_of_tasks' => $account->tasks_count, + 'debt_due' => $debt_due, + 'debt_owed' => $debt_owed, + 'debts' => $debts, + 'user' => auth()->user(), + 'changelogs' => $changelogs, + 'reminderOutboxes' => $reminderOutboxes, + ]; + + return view('dashboard.index', $data); + } + + /** + * Get calls for the dashboard. + * + * @return Collection + */ + public function calls() + { + $callsCollection = collect([]); + $calls = auth()->user()->account->calls() + ->get() + ->reject(function ($call) { + return $call->contact === null; + }) + ->take(15); + + foreach ($calls as $call) { + $data = [ + 'id' => $call->id, + 'called_at' => DateHelper::getShortDate($call->called_at), + 'name' => $call->contact->getIncompleteName(), + 'contact_id' => $call->contact->hashID(), + ]; + $callsCollection->push($data); + } + + return $callsCollection; + } + + /** + * Get notes for the dashboard. + * + * @return Collection + */ + public function notes() + { + $notesCollection = collect([]); + $notes = auth()->user()->account->notes()->favorited()->get(); + + foreach ($notes as $note) { + $data = [ + 'id' => $note->id, + 'body' => $note->body, + 'created_at' => DateHelper::getShortDate($note->created_at), + 'name' => $note->contact->getIncompleteName(), + 'contact' => [ + 'id' => $note->contact->hashID(), + 'has_avatar' => $note->contact->has_avatar, + 'avatar_url' => $note->contact->getAvatarURL(), + 'initials' => $note->contact->getInitials(), + 'default_avatar_color' => $note->contact->default_avatar_color, + 'complete_name' => $note->contact->name, + ], + ]; + $notesCollection->push($data); + } + + return $notesCollection; + } + + /** + * Get debts for the dashboard. + * + * @return Collection + */ + public function debts() + { + $debtsCollection = collect([]); + $debts = auth()->user()->account->debts()->get(); + + foreach ($debts as $debt) { + $debtsCollection->push(new DebtResource($debt)); + } + + return $debtsCollection; + } + + /** + * Save the current active tab to the User table. + */ + public function setTab(Request $request) + { + auth()->user()->dashboard_active_tab = $request->input('tab'); + auth()->user()->save(); + } +} diff --git a/app/Http/Controllers/EmotionController.php b/app/Http/Controllers/EmotionController.php new file mode 100644 index 0000000..5f6fb46 --- /dev/null +++ b/app/Http/Controllers/EmotionController.php @@ -0,0 +1,48 @@ +get(); + + return EmotionResource::collection($secondaries); + } + + /** + * Get the list of emotions. + * + * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection + */ + public function emotions(Request $request, $primaryEmotionId, $secondaryEmotionId) + { + $emotions = Emotion::where('emotion_secondary_id', $secondaryEmotionId) + ->get(); + + return EmotionResource::collection($emotions); + } +} diff --git a/app/Http/Controllers/JournalController.php b/app/Http/Controllers/JournalController.php new file mode 100644 index 0000000..e14f383 --- /dev/null +++ b/app/Http/Controllers/JournalController.php @@ -0,0 +1,273 @@ +input('start_date'); + $endDate = $request->input('end_date'); + $sortBy = $request->input('sort_by', 'created_at'); + $sortOrder = $request->input('sort_order', 'desc'); + $perPage = $request->input('per_page', 30); + + $entries = collect([]); + + $journalEntriesQuery = auth()->user()->account->journalEntries(); + + if ($startDate && $endDate) { + $journalEntriesQuery->whereDate('date', '>=', $startDate) + ->whereDate('date', '<=', $endDate); + } + $journalEntries = $journalEntriesQuery->orderBy($sortBy, $sortOrder) + ->paginate($perPage); + + + // this is needed to determine if we need to display the calendar + // (month + year) next to the journal entry + $previousEntryMonth = 0; + $previousEntryYear = 0; + $showCalendar = true; + + foreach ($journalEntries->items() as $journalEntry) { + if ($previousEntryMonth == $journalEntry->date->month && $previousEntryYear == $journalEntry->date->year) { + $showCalendar = false; + } + + $data = [ + 'id' => $journalEntry->id, + 'date' => $journalEntry->date, + 'journalable_id' => $journalEntry->journalable_id, + 'journalable_type' => $journalEntry->journalable_type, + 'object' => $journalEntry->getObjectData(), + 'show_calendar' => $showCalendar, + ]; + $entries->push($data); + + $previousEntryMonth = $journalEntry->date->month; + $previousEntryYear = $journalEntry->date->year; + $showCalendar = true; + } + + // I need the pagination items when I send back the array. + // There is probably a simpler way to achieve this. + return [ + 'total' => $journalEntries->total(), + 'per_page' => $journalEntries->perPage(), + 'current_page' => $journalEntries->currentPage(), + 'next_page_url' => $journalEntries->nextPageUrl(), + 'prev_page_url' => $journalEntries->previousPageUrl(), + 'data' => $entries, + ]; + } + + /** + * Gets the details of a single Journal Entry. + * + * @param JournalEntry $journalEntry + * @return array + */ + public function get(JournalEntry $journalEntry) + { + return $journalEntry->getObjectData(); + } + + /** + * Store the day entry. + */ + public function storeDay(DaysRequest $request) + { + $day = auth()->user()->account->days()->create([ + 'date' => now(DateHelper::getTimezone()), + 'rate' => $request->input('rate'), + 'comment' => $request->input('comment'), + ]); + + // Log a journal entry + $journalEntry = JournalEntry::add($day); + + return [ + 'id' => $journalEntry->id, + 'date' => $journalEntry->date, + 'journalable_id' => $journalEntry->journalable_id, + 'journalable_type' => $journalEntry->journalable_type, + 'object' => $journalEntry->getObjectData(), + 'show_calendar' => true, + ]; + } + + /** + * Delete the Day entry. + * + * @return void + */ + public function trashDay(Day $day): void + { + $day->deleteJournalEntry(); + $day->delete(); + } + + /** + * Indicates whether the user has already rated the current day. + * + * @return string + */ + public function hasRated() + { + if (JournalHelper::hasAlreadyRatedToday(auth()->user())) { + return 'true'; + } + + return 'notYet'; + } + + /** + * Display the Create journal entry screen. + * + * @return \Illuminate\View\View|\Illuminate\Contracts\View\Factory + */ + public function create() + { + return view('journal.add'); + } + + /** + * Saves the journal entry. + * + * @param Request $request + * @return \Illuminate\Http\RedirectResponse + */ + public function save(Request $request) + { + $validator = Validator::make($request->all(), [ + 'entry' => 'required|string', + 'date' => 'required|date', + ]); + + if ($validator->fails()) { + return back() + ->withInput() + ->withErrors($validator); + } + + $entry = new Entry; + $entry->account_id = $request->user()->account_id; + $entry->post = $request->input('entry'); + + if ($request->input('title') != '') { + $entry->title = $request->input('title'); + } + + $entry->save(); + + $entry->date = $request->input('date'); + // Log a journal entry + JournalEntry::add($entry); + + return redirect()->route('journal.index'); + } + + /** + * Display the Edit journal entry screen. + * + * @param Entry $entry + * @return \Illuminate\View\View + */ + public function edit(Entry $entry) + { + return view('journal.edit') + ->withEntry($entry); + } + + /** + * Method updateDay + * + * @param Request $request + * @param Day $day + * + */ + public function updateDay(Request $request, Day $day) + { + $validatedData = $request->validate([ + 'comment' => 'required|string', + ]); + + $day->update($validatedData); + + return response()->json(['message' => 'Day updated successfully']); + } + + /** + * Update a journal entry. + * + * @param Request $request + * @return \Illuminate\Http\RedirectResponse + */ + public function update(Request $request, Entry $entry) + { + $validator = Validator::make($request->all(), [ + 'entry' => 'required|string', + 'date' => 'required|date', + ]); + + if ($validator->fails()) { + return back() + ->withInput() + ->withErrors($validator); + } + + $entry->post = $request->input('entry'); + + if ($request->input('title') != '') { + $entry->title = $request->input('title'); + } + + $entry->save(); + + // Update journal entry + $journalEntry = $entry->journalEntry; + if ($journalEntry) { + $entry->date = $request->input('date'); + $journalEntry->edit($entry); + } + + return redirect()->route('journal.index'); + } + + /** + * Delete the reminder. + */ + public function deleteEntry(Request $request, Entry $entry) + { + $entry->deleteJournalEntry(); + $entry->delete(); + + return ['true']; + } +} diff --git a/app/Http/Controllers/MeController.php b/app/Http/Controllers/MeController.php new file mode 100644 index 0000000..fa5ddde --- /dev/null +++ b/app/Http/Controllers/MeController.php @@ -0,0 +1,51 @@ +validate($request, [ + 'contact_id' => 'required|integer|exists:contacts,id', + ]); + + app(SetMeContact::class)->execute([ + 'contact_id' => $request->input('contact_id'), + 'account_id' => $request->user()->account_id, + 'user_id' => $request->user()->id, + ]); + + return $this->respond(['true']); + } + + /** + * Removes contact as 'me' association. + * + * @param Request $request + * @return string + */ + public function destroy(Request $request) + { + app(DeleteMeContact::class)->execute([ + 'account_id' => $request->user()->account_id, + 'user_id' => $request->user()->id, + ]); + + return $this->respond(['true']); + } +} diff --git a/app/Http/Controllers/Settings/AuditLogController.php b/app/Http/Controllers/Settings/AuditLogController.php new file mode 100644 index 0000000..90f67a7 --- /dev/null +++ b/app/Http/Controllers/Settings/AuditLogController.php @@ -0,0 +1,28 @@ +user()->account->auditLogs() + ->with('author') + ->orderBy('created_at', 'desc') + ->paginate(15); + + $accountHasLimitations = AccountHelper::hasLimitations(auth()->user()->account); + + return view('settings.auditlog.index') + ->withLogsCollection(AuditLogHelper::getCollectionOfAudits($logs)) + ->withAccountHasLimitations($accountHasLimitations) + ->withLogsPagination($logs); + } +} diff --git a/app/Http/Controllers/Settings/ExportController.php b/app/Http/Controllers/Settings/ExportController.php new file mode 100644 index 0000000..3f8bbcc --- /dev/null +++ b/app/Http/Controllers/Settings/ExportController.php @@ -0,0 +1,124 @@ + auth()->user()->account_id, + 'user_id' => auth()->user()->id, + ]) + ->orderByDesc('created_at') + ->get(); + + return view('settings.export') + ->withAccountHasLimitations(AccountHelper::hasLimitations(auth()->user()->account)) + ->withExports($exports); + } + + /** + * Exports the data of the account in SQL format. + * + * @return \Illuminate\Http\Response|\Symfony\Component\HttpFoundation\Response|null + */ + public function storeSql() + { + $job = $this->newExport(ExportJob::SQL); + ExportAccount::dispatch($job); + + return redirect()->route('settings.export.index') + ->withStatus(trans('settings.export_submitted')); + } + + /** + * Exports the data of the account in SQL format. + * + * @return \Illuminate\Http\Response|\Symfony\Component\HttpFoundation\Response|null + */ + public function storeJson() + { + $job = $this->newExport(ExportJob::JSON); + ExportAccount::dispatch($job); + + return redirect()->route('settings.export.index') + ->withStatus(trans('settings.export_submitted')); + } + + /** + * Create a new ExportJob. + * + * @param string $type + * @return ExportJob + */ + private function newExport(string $type): ExportJob + { + $exports = ExportJob::where([ + 'account_id' => auth()->user()->account_id, + 'user_id' => auth()->user()->id, + ]) + ->orderBy('created_at') + ->get(); + + if ($exports->count() >= config('monica.export_size')) { + $job = $exports->first(); + try { + if ($job->filename !== null) { + StorageHelper::disk($job->location) + ->delete($job->filename); + } + } finally { + $job->delete(); + } + } + + return ExportJob::create([ + 'account_id' => auth()->user()->account_id, + 'user_id' => auth()->user()->id, + 'type' => $type, + ]); + } + + /** + * Download the generated file. + * + * @param Request $request + * @param string $uuid + * @return \Illuminate\Http\Response|\Symfony\Component\HttpFoundation\Response|null + */ + public function download(Request $request, string $uuid) + { + $job = ExportJob::where([ + 'account_id' => auth()->user()->account_id, + 'user_id' => auth()->user()->id, + 'uuid' => $uuid, + ])->firstOrFail(); + + if ($job->status !== ExportJob::EXPORT_DONE) { + return redirect()->route('settings.export.index') + ->withErrors(trans('settings.export_not_done')); + } + $disk = StorageHelper::disk($job->location); + + return $disk->response($job->filename, + "monica.{$job->type}", + [ + 'Content-Type' => "application/{$job->type}; charset=utf-8", + 'Content-Disposition' => "attachment; filename=monica.{$job->type}", + ] + ); + } +} diff --git a/app/Http/Controllers/Settings/GendersController.php b/app/Http/Controllers/Settings/GendersController.php new file mode 100644 index 0000000..ee455b7 --- /dev/null +++ b/app/Http/Controllers/Settings/GendersController.php @@ -0,0 +1,178 @@ +user()->account->genders; + + foreach ($genders as $gender) { + $gendersData->push($this->formatData($gender)); + } + + return CollectionHelper::sortByCollator($gendersData, 'name'); + } + + /** + * Get all the gender sex types. + */ + public function types() + { + $gendersData = collect([]); + + $types = [ + Gender::MALE, + Gender::FEMALE, + Gender::OTHER, + Gender::UNKNOWN, + Gender::NONE, + ]; + + foreach ($types as $type) { + $gendersData->push([ + 'id' => $type, + 'name' => trans('settings.personalization_genders_'.strtolower($type)), + ]); + } + + return CollectionHelper::sortByCollator($gendersData, 'name'); + } + + /** + * Store the gender. + */ + public function store(Request $request) + { + Validator::make($request->all(), [ + 'name' => 'required|max:255', + 'type' => ['required', Rule::in(Gender::LIST)], + ])->validate(); + + $gender = auth()->user()->account->genders()->create( + $request->only([ + 'name', + 'type', + ]) + + [ + 'account_id' => auth()->user()->account_id, + ] + ); + + if ($request->input('isDefault')) { + $this->updateDefault($gender); + } + + return $this->formatData($gender); + } + + /** + * Update the given gender. + */ + public function update(GendersRequest $request, Gender $gender) + { + $gender->update( + $request->only([ + 'name', + 'type', + ]) + ); + if ($request->input('isDefault')) { + $this->updateDefault($gender); + $gender->refresh(); + } elseif ($gender->isDefault()) { + // Case of this gender was the default one previously + $account = auth()->user()->account; + $account->default_gender_id = null; + $account->save(); + $gender->refresh(); + } + + return $this->formatData($gender); + } + + /** + * Destroy a gender type. + */ + public function destroyAndReplaceGender(Gender $gender, $genderId) + { + $account = auth()->user()->account; + try { + $genderToReplaceWith = Gender::where('account_id', $account->id) + ->findOrFail($genderId); + } catch (ModelNotFoundException $e) { + return response()->json([ + 'message' => trans('settings.personalization_genders_modal_error'), + ], 403); + } + + // We get the new gender to associate the contacts with. + GenderHelper::replace($account, $gender, $genderToReplaceWith); + + if ($gender->isDefault()) { + $account->default_gender_id = $genderToReplaceWith->id; + $account->save(); + } + + $gender->delete(); + + return $this->respondObjectDeleted($gender->id); + } + + /** + * Destroy a gender type. + */ + public function destroy(Gender $gender) + { + $gender->delete(); + + return $this->respondObjectDeleted($gender->id); + } + + /** + * Update the given gender to the default gender. + */ + public function updateDefault(Gender $gender) + { + $account = auth()->user()->account; + $account->default_gender_id = $gender->id; + $account->save(); + + return $this->formatData($gender); + } + + /** + * Format data for output. + * + * @param Gender $gender + * @return array + */ + private function formatData($gender) + { + return [ + 'id' => $gender->id, + 'name' => $gender->name, + 'type' => $gender->type, + 'isDefault' => $gender->isDefault(), + 'numberOfContacts' => $gender->contacts->count(), + ]; + } +} diff --git a/app/Http/Controllers/Settings/ModulesController.php b/app/Http/Controllers/Settings/ModulesController.php new file mode 100644 index 0000000..68495ae --- /dev/null +++ b/app/Http/Controllers/Settings/ModulesController.php @@ -0,0 +1,45 @@ +user()->account->modules; + + return $modules->map(function ($module) { + return $this->format($module); + }); + } + + public function toggle(Request $request, Module $module) + { + $module->active = ! $module->active; + $module->save(); + + return $this->respond([ + 'data' => $this->format($module), + ]); + } + + private function format(Module $module) + { + return [ + 'id' => $module->id, + 'key' => $module->key, + 'name' => trans($module->translation_key), + 'active' => $module->active, + ]; + } +} diff --git a/app/Http/Controllers/Settings/MultiFAController.php b/app/Http/Controllers/Settings/MultiFAController.php new file mode 100644 index 0000000..1687af4 --- /dev/null +++ b/app/Http/Controllers/Settings/MultiFAController.php @@ -0,0 +1,157 @@ +generateSecret(); + + $user = $request->user(); + + //generate image for QR barcode + $imageDataUri = Google2FA::getQRCodeInline( + $request->getHttpHost(), + $user->email, + $secret, + 200 + ); + + $request->session()->put($this->SESSION_TFA_SECRET, $secret); + + return response()->json(['image' => $imageDataUri, 'secret' => $secret]); + } + + /** + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\JsonResponse + */ + public function validateTwoFactor(Request $request) + { + //get user + $user = $request->user(); + + if (! is_null($user->google2fa_secret)) { + return response()->json(['error' => trans('settings.2fa_enable_error_already_set')]); + } + + $this->validate($request, [ + 'one_time_password' => 'required', + ]); + + //retrieve secret + $secret = $request->session()->pull($this->SESSION_TFA_SECRET); + + $authenticator = app(Authenticator::class)->boot($request); + + if ($authenticator->verifyGoogle2FA($secret, $request['one_time_password'])) { + //encrypt and then save secret + $user->google2fa_secret = $secret; + $user->save(); + + $authenticator->login(); + + return response()->json(['success' => true]); + } + + $authenticator->logout(); + + return response()->json(['success' => false]); + } + + /** + * @param \Illuminate\Http\Request $request + * @return \Illuminate\View\View|\Illuminate\Contracts\View\Factory + */ + public function disableTwoFactor(Request $request) + { + return view('settings.security.2fa-disable'); + } + + /** + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\JsonResponse + */ + public function deactivateTwoFactor(Request $request) + { + $this->validate($request, [ + 'one_time_password' => 'required', + ]); + + $user = $request->user(); + + if ($this->validateTwoFactorLogin($request, $user, $request['one_time_password'])) { + //make secret column blank + $user->google2fa_secret = null; + $user->save(); + + return response()->json(['success' => true]); + } + + return response()->json(['success' => false]); + } + + /** + * Validate 2nd factor for user with 2FA code or recovery code. + * + * @param Request $request + * @param User $user + * @param string $oneTimePassword + * @return bool + */ + private function validateTwoFactorLogin(Request $request, User $user, string $oneTimePassword): bool + { + //retrieve secret + $secret = $user->google2fa_secret; + + $authenticator = app(Authenticator::class)->boot($request); + + // try provided token as a 2FA code, or as a recovery code + if ($authenticator->verifyGoogle2FA($secret, $oneTimePassword) + || $user->recoveryChallenge($oneTimePassword)) { + $authenticator->logout(); + + return true; + } + + return false; + } + + /** + * Generate a secret key in Base32 format. + * + * @return string + */ + private function generateSecret() + { + return Google2FA::generateSecretKey(32); + } +} diff --git a/app/Http/Controllers/Settings/PersonalizationController.php b/app/Http/Controllers/Settings/PersonalizationController.php new file mode 100644 index 0000000..257d148 --- /dev/null +++ b/app/Http/Controllers/Settings/PersonalizationController.php @@ -0,0 +1,112 @@ +user()->account); + + return view('settings.personalization.index') + ->withAccountHasLimitations($accountHasLimitations); + } + + /** + * Get all the contact field types. + */ + public function getContactFieldTypes() + { + return auth()->user()->account->contactFieldTypes; + } + + /** + * Store a newly created resource in storage. + * + * @param Request $request + * @return string + */ + public function storeContactFieldType(Request $request) + { + Validator::make($request->all(), [ + 'name' => 'required|max:255', + 'icon' => 'max:255|nullable', + 'protocol' => 'max:255|nullable', + ])->validate(); + + return auth()->user()->account->contactFieldTypes()->create( + $request->only([ + 'name', + 'protocol', + ]) + + [ + 'fontawesome_icon' => $request->input('icon'), + 'account_id' => auth()->user()->account_id, + ] + ); + } + + /** + * Edit a newly created resource in storage. + * + * @param Request $request + * @param ContactFieldType $contactFieldType + * @return ContactFieldType + */ + public function editContactFieldType(Request $request, ContactFieldType $contactFieldType): ContactFieldType + { + Validator::make($request->all(), [ + 'name' => 'required|max:255', + 'icon' => 'max:255|nullable', + 'protocol' => 'max:255|nullable', + ])->validate(); + + $contactFieldType->update( + $request->only([ + 'name', + 'protocol', + ]) + + [ + 'fontawesome_icon' => $request->input('icon'), + ] + ); + + return $contactFieldType; + } + + /** + * Destroy the contact field type. + */ + public function destroyContactFieldType(Request $request, ContactFieldType $contactFieldType) + { + if (! $contactFieldType->delible) { + return $this->respondUnauthorized(); + } + + // find all the contact fields that have this contact field types + $contactFields = auth()->user()->account->contactFields + ->where('contact_field_type_id', $contactFieldType->id); + + foreach ($contactFields as $contactField) { + $contactField->delete(); + } + + $contactFieldType->delete(); + + return $this->respondObjectDeleted($contactFieldType->id); + } +} diff --git a/app/Http/Controllers/Settings/RecoveryCodesController.php b/app/Http/Controllers/Settings/RecoveryCodesController.php new file mode 100644 index 0000000..1048433 --- /dev/null +++ b/app/Http/Controllers/Settings/RecoveryCodesController.php @@ -0,0 +1,103 @@ + + */ + public function store(Request $request) + { + // Remove previous codes + auth()->user()->recoveryCodes() + ->each(function ($code) { + $code->delete(); + }); + + // Generate new codes + $this->generate(); + + $codes = auth()->user()->recoveryCodes()->get(); + + return $this->response($codes); + } + + /** + * Get list of recovery codes. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Support\Collection + */ + public function index(Request $request) + { + $codes = auth()->user()->recoveryCodes()->get(); + + if (count($codes) == 0) { + $this->generate(); + $codes = auth()->user()->recoveryCodes()->get(); + } + + return $this->response($codes); + } + + /** + * Format codes collection for response. + * + * @param \Illuminate\Support\Collection $codes + * @return \Illuminate\Support\Collection + */ + private function response($codes) + { + return $codes->map(function (RecoveryCode $code): array { + return [ + 'id' => $code->id, + 'recovery' => $code->recovery, + 'used' => (bool) $code->used, + ]; + }); + } + + /** + * Generate new recovery codes. + * + * @return void + */ + private function generate() + { + // Generate new codes + $random = new Random(); + $random->uppercase(true); + + $codes = []; + + for ($i = 1; $i <= (int) config('auth.recovery.count'); $i++) { + $blocks = []; + + for ($j = 1; $j <= (int) config('auth.recovery.blocks'); $j++) { + $blocks[] = $random->size(config('auth.recovery.chars'))->get(); + } + + $codes[] = implode('-', $blocks); + } + + foreach ($codes as $code) { + RecoveryCode::create([ + 'account_id' => auth()->user()->account_id, + 'user_id' => auth()->user()->id, + 'recovery' => $code, + ]); + } + } +} diff --git a/app/Http/Controllers/Settings/ReminderRulesController.php b/app/Http/Controllers/Settings/ReminderRulesController.php new file mode 100644 index 0000000..081371f --- /dev/null +++ b/app/Http/Controllers/Settings/ReminderRulesController.php @@ -0,0 +1,44 @@ +user()->account->reminderRules; + + return $reminderRules->map(function ($reminderRule) { + return $this->format($reminderRule); + }); + } + + public function toggle(Request $request, ReminderRule $reminderRule) + { + $reminderRule->active = ! $reminderRule->active; + $reminderRule->save(); + + return $this->respond([ + 'data' => $this->format($reminderRule), + ]); + } + + private function format(ReminderRule $reminderRule) + { + return [ + 'id' => $reminderRule->id, + 'number_of_days_before' => $reminderRule->number_of_days_before, + 'active' => $reminderRule->active, + ]; + } +} diff --git a/app/Http/Controllers/Settings/StorageController.php b/app/Http/Controllers/Settings/StorageController.php new file mode 100644 index 0000000..96becf7 --- /dev/null +++ b/app/Http/Controllers/Settings/StorageController.php @@ -0,0 +1,43 @@ +user()->account_id)->get(); + $photos = Photo::where('account_id', auth()->user()->account_id)->get(); + /** @var \Illuminate\Support\Collection */ + $documents = collect($documents); + $elements = $documents->concat($photos)->sortByDesc('created_at'); + + // size is in bytes in the database + $currentAccountSize = StorageHelper::getAccountStorageSize(auth()->user()->account); + + if ($currentAccountSize != 0) { + $currentAccountSize = round($currentAccountSize / 1000000); + } + + // correspondingPercent + $percentUsage = round($currentAccountSize * 100 / config('monica.max_storage_size')); + + $accountHasLimitations = AccountHelper::hasLimitations(auth()->user()->account); + + return view('settings.storage.index') + ->withAccountHasLimitations($accountHasLimitations) + ->withElements($elements) + ->withCurrentAccountSize($currentAccountSize) + ->withAccountLimit(config('monica.max_storage_size')) + ->withPercentUsage($percentUsage); + } +} diff --git a/app/Http/Controllers/Settings/SubscriptionsController.php b/app/Http/Controllers/Settings/SubscriptionsController.php new file mode 100644 index 0000000..d87816f --- /dev/null +++ b/app/Http/Controllers/Settings/SubscriptionsController.php @@ -0,0 +1,367 @@ +route('settings.index'); + } + + $account = auth()->user()->account; + + $subscription = $account->getSubscribedPlan(); + if (! $account->isSubscribed() && (! $subscription || $subscription->ended())) { + return view('settings.subscriptions.blank', [ + 'numberOfCustomers' => InstanceHelper::getNumberOfPaidSubscribers(), + ]); + } + + $hasInvoices = $account->hasStripeId() && $account->hasInvoices(); + $invoices = null; + if ($hasInvoices) { + $invoices = $account->invoices(); + } + + try { + $planInformation = $this->stripeCall(function () use ($subscription): ?array { + return InstanceHelper::getPlanInformationFromSubscription($subscription); + }); + } catch (StripeException $e) { + $planInformation = null; + } + + return view('settings.subscriptions.account', [ + 'planInformation' => $planInformation, + 'subscription' => $subscription, + 'hasInvoices' => $hasInvoices, + 'invoices' => $invoices, + 'accountHasLimitations' => AccountHelper::hasLimitations($account), + ]); + } + + /** + * Display the upgrade view page. + * + * @param Request $request + * @return View|Factory|RedirectResponse + */ + public function upgrade(Request $request) + { + if (! config('monica.requires_subscription')) { + return redirect()->route('settings.index'); + } + + if (auth()->user()->account->isSubscribed()) { + return redirect()->route('settings.subscriptions.index'); + } + + $plan = $request->query('plan'); + if ($plan !== 'monthly' && $plan !== 'annual') { + abort(404); + } + + $planInformation = InstanceHelper::getPlanInformationFromConfig($plan); + + if ($planInformation === null) { + abort(404); + } + + return view('settings.subscriptions.upgrade', [ + 'planInformation' => $planInformation, + 'nextTheoriticalBillingDate' => DateHelper::getFullDate(DateHelper::getNextTheoriticalBillingDate($plan)), + 'intent' => auth()->user()->account->createSetupIntent(), + ]); + } + + /** + * Display the update view page. + * + * @param Request $request + * @return View|Factory|RedirectResponse + */ + public function update(Request $request) + { + if (! config('monica.requires_subscription')) { + return redirect()->route('settings.index'); + } + + $account = auth()->user()->account; + + $subscription = $account->getSubscribedPlan(); + if (! $account->isSubscribed() && (! $subscription || $subscription->ended())) { + return view('settings.subscriptions.blank', [ + 'numberOfCustomers' => InstanceHelper::getNumberOfPaidSubscribers(), + ]); + } + + $planInformation = InstanceHelper::getPlanInformationFromSubscription($subscription); + + if ($planInformation === null) { + abort(404); + } + + $plans = collect(); + foreach (['monthly', 'annual'] as $plan) { + $plans->push(InstanceHelper::getPlanInformationFromConfig($plan)); + } + + $legacyPlan = null; + if (! $plans->contains(function ($value) use ($planInformation) { + return $value['id'] === $planInformation['id']; + })) { + $legacyPlan = $planInformation; + } + + return view('settings.subscriptions.update', [ + 'planInformation' => $planInformation, + 'plans' => $plans, + 'legacyPlan' => $legacyPlan, + ]); + } + + /** + * Process the update process. + * + * @param Request $request + * @return View|Factory|RedirectResponse + */ + public function processUpdate(Request $request) + { + $account = auth()->user()->account; + + $subscription = $account->getSubscribedPlan(); + if (! $account->isSubscribed() && ! $subscription) { + return redirect()->route('settings.index'); + } + + try { + $account->updateSubscription($request->input('frequency'), $subscription); + } catch (StripeException $e) { + return back() + ->withInput() + ->withErrors($e->getMessage()); + } + + return redirect()->route('settings.subscriptions.index'); + } + + /** + * Display the confirm view page. + * + * @return View|Factory|RedirectResponse + * + * @throws ApiErrorException + */ + public function confirmPayment($id) + { + try { + $payment = $this->stripeCall(function () use ($id): \Stripe\PaymentIntent { + return Cashier::stripe()->paymentIntents->retrieve($id); + }); + } catch (StripeException $e) { + return back()->withErrors($e->getMessage()); + } + + return view('settings.subscriptions.confirm', [ + 'payment' => new Payment($payment), + 'redirect' => request('redirect'), + ]); + } + + /** + * Display the upgrade success page. + * + * @return View|Factory|RedirectResponse + */ + public function upgradeSuccess() + { + if (! config('monica.requires_subscription')) { + return redirect()->route('settings.index'); + } + + return view('settings.subscriptions.success'); + } + + /** + * Display the downgrade success page. + * + * @param Request $request + * @return View|Factory|RedirectResponse + */ + public function downgradeSuccess(Request $request) + { + if (! config('monica.requires_subscription')) { + return redirect()->route('settings.index'); + } + + return view('settings.subscriptions.downgrade-success'); + } + + /** + * Display the archive all your contacts page. + * + * @return View|Factory|RedirectResponse + */ + public function archive() + { + return view('settings.subscriptions.archive'); + } + + /** + * Process the Archive process. + * + * @return RedirectResponse + */ + public function processArchive() + { + app(ArchiveAllContacts::class)->execute([ + 'account_id' => auth()->user()->account_id, + ]); + + return redirect()->route('settings.subscriptions.downgrade'); + } + + /** + * Display the downgrade view page. + * + * @return View|Factory|RedirectResponse + */ + public function downgrade() + { + $account = auth()->user()->account; + + if (! config('monica.requires_subscription')) { + return redirect()->route('settings.index'); + } + + $subscription = $account->getSubscribedPlan(); + if (! $account->isSubscribed() && ! $subscription) { + return redirect()->route('settings.index'); + } + + return view('settings.subscriptions.downgrade-checklist') + ->with('numberOfActiveContacts', $account->allContacts()->active()->count()) + ->with('numberOfPendingInvitations', $account->invitations()->count()) + ->with('numberOfUsers', $account->users()->count()) + ->with('accountHasLimitations', AccountHelper::hasLimitations($account)) + ->with('hasReachedContactLimit', ! AccountHelper::isBelowContactLimit($account)) + ->with('canDowngrade', AccountHelper::canDowngrade($account)); + } + + /** + * Process the downgrade process. + * + * @return RedirectResponse + */ + public function processDowngrade() + { + $account = auth()->user()->account; + + if (! AccountHelper::canDowngrade($account)) { + return redirect()->route('settings.subscriptions.downgrade'); + } + + $subscription = $account->getSubscribedPlan(); + if (! $account->isSubscribed() && ! $subscription) { + return redirect()->route('settings.index'); + } + + try { + $account->subscriptionCancel(); + } catch (StripeException $e) { + return back() + ->withInput() + ->withErrors($e->getMessage()); + } + + return redirect()->route('settings.subscriptions.downgrade.success'); + } + + /** + * Process the upgrade payment. + * + * @param Request $request + * @return RedirectResponse + */ + public function processPayment(Request $request) + { + if (! config('monica.requires_subscription')) { + return redirect()->route('settings.index'); + } + + try { + auth()->user()->account + ->subscribe($request->input('payment_method'), $request->input('plan')); + } catch (IncompletePayment $e) { + return redirect()->route( + 'settings.subscriptions.confirm', + [$e->payment->asStripePaymentIntent()->id, 'redirect' => route('settings.subscriptions.upgrade.success')] + ); + } catch (StripeException $e) { + return back() + ->withInput() + ->withErrors($e->getMessage()); + } + + return redirect()->route('settings.subscriptions.upgrade.success'); + } + + /** + * Download the invoice as PDF. + * + * @param mixed $invoiceId + * @return \Symfony\Component\HttpFoundation\Response + */ + public function downloadInvoice($invoiceId) + { + return auth()->user()->account->downloadInvoice($invoiceId, [ + 'vendor' => 'Monica', + 'product' => trans('settings.subscriptions_pdf_title', ['name' => config('monica.paid_plan_monthly_friendly_name')]), + ]); + } + + /** + * Download the invoice as PDF. + * + * @param Request $request + * @return \Illuminate\Http\RedirectResponse|null + */ + public function forceCompletePaymentOnTesting(Request $request): ?RedirectResponse + { + if (App::environment('production')) { + return null; + } + $subscription = auth()->user()->account->getSubscribedPlan(); + $subscription->stripe_status = 'active'; + $subscription->save(); + + return redirect()->route('settings.subscriptions.index'); + } +} diff --git a/app/Http/Controllers/SettingsController.php b/app/Http/Controllers/SettingsController.php new file mode 100644 index 0000000..eef3e26 --- /dev/null +++ b/app/Http/Controllers/SettingsController.php @@ -0,0 +1,450 @@ +middleware('limitations')->only(['inviteUser', 'storeImport']); + } + + /** + * Display a listing of the resource. + * + * @return \Illuminate\View\View + */ + public function index() + { + $meContact = null; + + $search = auth()->user()->first_name.' '. + auth()->user()->last_name.' '. + auth()->user()->email; + $existingContacts = Contact::search($search, auth()->user()->account_id, 'id') + ->real() + ->whereNotIn('id', [auth()->user()->me_contact_id]) + ->paginate(20); + + if (auth()->user()->me_contact_id) { + $meContact = Contact::where('account_id', auth()->user()->account_id) + ->find(auth()->user()->me_contact_id); + if ($meContact) { + $existingContacts->prepend($meContact); + } + } + + $accountHasLimitations = AccountHelper::hasLimitations(auth()->user()->account); + + return view('settings.index') + ->withAccountHasLimitations($accountHasLimitations) + ->withMeContact($meContact ? new ContactResource($meContact) : null) + ->withExistingContacts(ContactResource::collection($existingContacts)) + ->withNamesOrder(User::NAMES_ORDER) + ->withLocales(LocaleHelper::getLocaleList()->sortByCollator('name-orig')) + ->withHours(DateHelper::getListOfHours()) + ->withSelectedTimezone(TimezoneHelper::adjustEquivalentTimezone(DateHelper::getTimezone())) + ->withTimezones(collect(TimezoneHelper::getListOfTimezones())->map(function (array $timezone): array { + return ['id' => $timezone['timezone'], 'name'=>$timezone['name']]; + })); + } + + /** + * Save user settings. + * + * @param SettingsRequest $request + * @return \Illuminate\Http\RedirectResponse + */ + public function save(SettingsRequest $request) + { + $user = $request->user(); + + $user->update( + $request->only([ + 'first_name', + 'last_name', + 'timezone', + 'locale', + 'currency_id', + 'name_order', + 'fluid_container', + 'temperature_scale', + ]) + ); + + if ($user->email !== $request->input('email')) { + app(EmailChange::class)->execute([ + 'account_id' => $user->account_id, + 'email' => $request->input('email'), + 'user_id' => $user->id, + ]); + } + + if (! AccountHelper::hasLimitations($user->account) && $request->input('me_contact_id')) { + $user->me_contact_id = $request->input('me_contact_id'); + $user->save(); + } + + $user->account->default_time_reminder_is_sent = $request->input('reminder_time'); + $user->account->save(); + + return redirect()->route('settings.index') + ->with('status', trans('settings.settings_success', [], $request['locale'])); + } + + /** + * Delete user account. + * + * @param Request $request + * @return \Illuminate\Http\RedirectResponse + */ + public function delete(Request $request) + { + $account = auth()->user()->account; + + try { + app(DestroyAccount::class)->execute([ + 'account_id' => $account->id, + ]); + } catch (StripeException $e) { + return redirect()->route('settings.index') + ->withErrors($e->getMessage()); + } + + auth('')->logout(); + + return redirect()->route('loginRedirect'); + } + + /** + * Reset user account. + * + * @param Request $request + * @return \Illuminate\Http\RedirectResponse + */ + public function reset(Request $request) + { + $user = $request->user(); + $account = $user->account; + + ResetAccount::dispatch([ + 'account_id' => $account->id, + ]); + + return redirect()->route('settings.index') + ->with('status', trans('settings.reset_success')); + } + + /** + * Display the import view. + * + * @return \Illuminate\View\View|\Illuminate\Contracts\View\Factory + */ + public function import() + { + $accountHasLimitations = AccountHelper::hasLimitations(auth()->user()->account); + + if (auth()->user()->account->importjobs->count() == 0) { + return view('settings.imports.blank') + ->withAccountHasLimitations($accountHasLimitations); + } + + return view('settings.imports.index') + ->withAccountHasLimitations($accountHasLimitations); + } + + /** + * Display the Import people's view. + * + * @return \Illuminate\View\View|\Illuminate\Contracts\View\Factory|\Illuminate\Http\RedirectResponse + */ + public function upload() + { + if (AccountHelper::hasLimitations(auth()->user()->account)) { + return redirect()->route('settings.subscriptions.index'); + } + + return view('settings.imports.upload'); + } + + public function storeImport(ImportsRequest $request) + { + $filename = $request->file('vcard')->store('imports', config('filesystems.default')); + + $importJob = auth()->user()->account->importjobs()->create([ + 'user_id' => auth()->user()->id, + 'type' => 'vcard', + 'filename' => $filename, + ]); + + AddContactFromVCard::dispatch($importJob, $request->input('behaviour')); + + return redirect()->route('settings.import'); + } + + /** + * Display the import report view. + * + * @return \Illuminate\View\View|\Illuminate\Contracts\View\Factory + */ + public function report($importJobId) + { + $importJob = ImportJob::where('account_id', auth()->user()->account_id) + ->findOrFail($importJobId); + + return view('settings.imports.report', compact('importJob')); + } + + /** + * Display the users view. + * + * @return \Illuminate\View\View|\Illuminate\Contracts\View\Factory + */ + public function users() + { + $users = auth()->user()->account->users; + $accountHasLimitations = AccountHelper::hasLimitations(auth()->user()->account); + + if ($users->count() == 1 && auth()->user()->account->invitations()->count() == 0) { + return view('settings.users.blank') + ->withAccountHasLimitations($accountHasLimitations); + } + + return view('settings.users.index', compact('users')) + ->withAccountHasLimitations($accountHasLimitations); + } + + /** + * Show the form for creating a new resource. + * + * @return \Illuminate\View\View|\Illuminate\Contracts\View\Factory|\Illuminate\Http\RedirectResponse + */ + public function addUser() + { + if (AccountHelper::hasLimitations(auth()->user()->account)) { + return redirect()->route('settings.subscriptions.index'); + } + + return view('settings.users.add'); + } + + /** + * Store a newly created resource in storage. + * + * @param InvitationRequest $request + * @return \Illuminate\Http\RedirectResponse + */ + public function inviteUser(InvitationRequest $request) + { + // Make sure the confirmation to invite has not been bypassed + if (! $request->input('confirmation')) { + return redirect()->back()->withErrors(trans('settings.users_error_please_confirm'))->withInput(); + } + + // Is the email address already taken? + $users = User::where('email', $request->only(['email']))->count(); + if ($users > 0) { + return redirect()->back()->withErrors(trans('settings.users_error_email_already_taken'))->withInput(); + } + + // Has this user already been invited? + $invitations = Invitation::where('email', $request->only(['email']))->count(); + if ($invitations > 0) { + return redirect()->back()->withErrors(trans('settings.users_error_already_invited'))->withInput(); + } + + $invitation = auth()->user()->account->invitations()->create( + $request->only([ + 'email', + ]) + + [ + 'invited_by_user_id' => auth()->user()->id, + 'account_id' => auth()->user()->account_id, + 'invitation_key' => Str::random(100), + ] + ); + + $invitation->notify((new InvitationMail())->locale(auth()->user()->locale)); + + auth()->user()->account->update([ + 'number_of_invitations_sent' => auth()->user()->account->number_of_invitations_sent + 1, + ]); + + return redirect()->route('settings.users.index') + ->with('status', trans('settings.settings_success')); + } + + /** + * Remove the specified resource from storage. + * + * @param Invitation $invitation + * @return \Illuminate\Http\RedirectResponse + */ + public function destroyInvitation(Invitation $invitation) + { + $invitation->delete(); + + return redirect()->route('settings.users.index') + ->with('success', trans('settings.users_invitation_deleted_confirmation_message')); + } + + /** + * Delete additional user account. + * + * @param int $userID + * @return \Illuminate\Http\RedirectResponse + */ + public function deleteAdditionalUser($userID) + { + $user = User::where('account_id', auth()->user()->account_id) + ->findOrFail($userID); + + // make sure you don't delete yourself from this screen + if ($user->id == auth()->user()->id) { + return redirect()->route('loginRedirect'); + } + + $user->delete(); + + return redirect()->route('settings.users.index') + ->with('success', trans('settings.users_list_delete_success')); + } + + /** + * Display the list of tags for this account. + */ + public function tags() + { + return view('settings.tags') + ->withAccountHasLimitations(AccountHelper::hasLimitations(auth()->user()->account)); + } + + /** + * Destroy the tag. + * + * @param int $tagId + * @return \Illuminate\Http\RedirectResponse + */ + public function deleteTag($tagId) + { + app(DestroyTag::class)->execute([ + 'tag_id' => $tagId, + 'account_id' => auth()->user()->account_id, + ]); + + return redirect()->route('settings.tags.index') + ->with('success', trans('settings.tags_list_delete_success')); + } + + /** + * Edit a tag name. + * + * @param Tag $tag + * @param Request $request + * + * @return \Illuminate\Http\RedirectResponse + */ + public function editTag(Tag $tag, Request $request): RedirectResponse + { + app(UpdateTag::class)->execute([ + 'tag_id' => $tag->id, + 'account_id' => auth()->user()->account_id, + 'name' => $request->input('name'), + ]); + + return back() + ->with('success', trans('settings.tags_list_edit_success')); + } + + public function api() + { + return view('settings.api.index') + ->withAccountHasLimitations(AccountHelper::hasLimitations(auth()->user()->account)); + } + + public function dav() + { + $davroute = route('sabre.dav'); + $email = auth()->user()->email; + + return view('settings.dav.index') + ->withDavRoute($davroute) + ->withCardDavRoute("{$davroute}/addressbooks/{$email}/contacts") + ->withCalDavBirthdaysRoute("{$davroute}/calendars/{$email}/birthdays") + ->withCalDavTasksRoute("{$davroute}/calendars/{$email}/tasks") + ->withAccountHasLimitations(AccountHelper::hasLimitations(auth()->user()->account)); + } + + public function security() + { + $webauthnKeys = WebauthnKey::where('user_id', auth()->id())->get(); + + return view('settings.security.index') + ->with('is2FAActivated', Google2FA::isActivated()) + ->withWebauthnKeys(WebauthnKeyResource::collection($webauthnKeys)) + ->withAccountHasLimitations(AccountHelper::hasLimitations(auth()->user()->account)); + } + + /** + * Update the default view when viewing a contact. + * The default view can be either the life events feed or the general data + * about the contact (notes, reminders, ...). + * Possible values: life-events | notes. + * + * @param Request $request + * @return string + */ + public function updateDefaultProfileView(Request $request) + { + $allowedValues = ['life-events', 'notes', 'photos']; + /** @var string */ + $view = $request->input('name'); + + if (! in_array($view, $allowedValues)) { + return 'not allowed'; + } + + auth()->user()->profile_active_tab = $view; + + if ($view == 'life-events') { + auth()->user()->profile_new_life_event_badge_seen = true; + } + + auth()->user()->save(); + + return $view; + } +} diff --git a/app/Http/Controllers/StorageController.php b/app/Http/Controllers/StorageController.php new file mode 100644 index 0000000..1c0ec6d --- /dev/null +++ b/app/Http/Controllers/StorageController.php @@ -0,0 +1,136 @@ +middleware(['setEtag', 'ifMatch', 'ifNoneMatch']); + } + + /** + * Download file with authorization. + * + * @param Request $request + * @param string $file + * @return \Illuminate\Http\Response|\Symfony\Component\HttpFoundation\StreamedResponse|null + */ + public function show(Request $request, string $file) + { + $filename = $this->getFilename($request, $file); + + try { + $disk = StorageHelper::disk(config('filesystems.default')); + + $lastModified = Carbon::createFromTimestamp($disk->lastModified($file), 'UTC')->locale('en'); + + $headers = [ + 'Last-Modified' => $lastModified->isoFormat('ddd\, DD MMM YYYY HH\:mm\:ss \G\M\T'), + 'Cache-Control' => config('filesystems.default_cache_control'), + ]; + + if (! $this->checkConditions($request, $lastModified)) { + return Response::noContent(304, $headers)->setNotModified(); + } + + return $disk->response($file, $filename, $headers); + } catch (FilesystemException $e) { + abort(404); + } + } + + /** + * Get the filename for this file. + * + * @param Request $request + * @param string $file + * @return string + */ + private function getFilename(Request $request, string $file): string + { + $accountId = $request->user()->account_id; + $folder = Str::before($file, '/'); + + switch ($folder) { + case 'avatars': + $obj = Contact::where([ + 'account_id' => $accountId, + ['avatar_default_url', 'like', "$file%"], + ])->first(); + $filename = Str::after($file, '/'); + break; + + case 'photos': + $obj = Photo::where([ + 'account_id' => $accountId, + 'new_filename' => $file, + ])->first(); + $filename = $obj ? $obj->original_filename : null; + break; + + case 'documents': + $obj = Document::where([ + 'account_id' => $accountId, + 'new_filename' => $file, + ])->first(); + $filename = $obj ? $obj->original_filename : null; + break; + + default: + $obj = false; + $filename = null; + break; + } + + if ($obj === false || $obj === null || ! $obj->exists) { + abort(404); + } + + return $filename; + } + + /** + * Check for If-Modified-Since and If-Unmodified-Since conditions. + * Return true if the condition does not match. + * + * @param Request $request + * @param Carbon $lastModified Last modified date + * @return bool + */ + private function checkConditions(Request $request, Carbon $lastModified): bool + { + if (! $request->header('If-None-Match') && ($ifModifiedSince = $request->header('If-Modified-Since'))) { + // The If-Modified-Since header contains a date. We will only + // return the entity if it has been changed since that date. + $date = Carbon::parse($ifModifiedSince); + + if ($lastModified->lessThanOrEqualTo($date)) { + return false; + } + } + + if ($ifUnmodifiedSince = $request->header('If-Unmodified-Since')) { + // The If-Unmodified-Since will allow the request if the + // entity has not changed since the specified date. + $date = Carbon::parse($ifUnmodifiedSince); + + // We must only check the date if it's valid + if ($lastModified->greaterThan($date)) { + abort(412, 'An If-Unmodified-Since header was specified, but the entity has been changed since the specified date.'); + } + } + + return true; + } +} diff --git a/app/Http/Controllers/TasksController.php b/app/Http/Controllers/TasksController.php new file mode 100644 index 0000000..62c3a11 --- /dev/null +++ b/app/Http/Controllers/TasksController.php @@ -0,0 +1,84 @@ +user()->account->tasks); + } + + /** + * Store a newly created resource in storage. + * + * @param Request $request + * @return Task + */ + public function store(Request $request): Task + { + return app(CreateTask::class)->execute([ + 'account_id' => auth()->user()->account_id, + 'contact_id' => ($request->input('contact_id') == '' ? null : $request->input('contact_id')), + 'title' => $request->input('title'), + 'description' => ($request->input('description') == '' ? null : $request->input('description')), + ]); + } + + /** + * Update a task. + * + * @param Request $request + * @param Task $task + * @return Task + */ + public function update(Request $request, Task $task): Task + { + return app(UpdateTask::class)->execute([ + 'account_id' => auth()->user()->account_id, + 'task_id' => $task->id, + 'contact_id' => ($request->input('contact_id') == '' ? null : $request->input('contact_id')), + 'title' => $request->input('title'), + 'description' => ($request->input('description') == '' ? null : $request->input('description')), + 'completed' => $request->input('completed'), + ]); + } + + /** + * Destroy the task. + * + * @param Task $task + * @return null|\Illuminate\Http\JsonResponse + */ + public function destroy(Task $task): ?JsonResponse + { + try { + if (app(DestroyTask::class)->execute([ + 'task_id' => $task->id, + 'account_id' => auth()->user()->account_id, + ])) { + return $this->respondObjectDeleted($task->id); + } + } catch (\Exception $e) { + return $this->respondNotFound(); + } + + return null; + } +} diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php new file mode 100644 index 0000000..8a47a3a --- /dev/null +++ b/app/Http/Kernel.php @@ -0,0 +1,110 @@ + + */ + protected $middleware = [ + \App\Http\Middleware\TrustProxies::class, + \App\Http\Middleware\PreventRequestsDuringMaintenance::class, + \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class, + \App\Http\Middleware\TrimStrings::class, + \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, + ]; + + /** + * The application's route middleware groups. + * + * @var array> + */ + protected $middlewareGroups = [ + 'web' => [ + \App\Http\Middleware\EncryptCookies::class, + \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, + \Illuminate\Session\Middleware\StartSession::class, + 'sentry.context', + \Illuminate\View\Middleware\ShareErrorsFromSession::class, + \App\Http\Middleware\VerifyCsrfToken::class, + 'bindings', + 'locale', + \App\Http\Middleware\CheckVersion::class, + \App\Http\Middleware\CheckCompliance::class, + \Laravel\Passport\Http\Middleware\CreateFreshApiToken::class, + ], + + 'api' => [ + 'throttle:api', + 'sentry.context', + 'locale', + ], + + 'oauth' => [ + 'throttle:oauth', + \App\Http\Middleware\EncryptCookies::class, + \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, + \Illuminate\Session\Middleware\StartSession::class, + 'sentry.context', + 'locale', + ], + + 'mfa' => [ + \Illuminate\View\Middleware\ShareErrorsFromSession::class, + 'webauthn', + '2fa', + ], + ]; + + /** + * The application's route middleware. + * + * These middleware may be assigned to groups or used individually. + * + * @var array + */ + protected $routeMiddleware = [ + '2fa' => \PragmaRX\Google2FALaravel\Middleware::class, + 'auth' => \App\Http\Middleware\Authenticate::class, + 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, + 'auth.tokenonbasic' => \App\Http\Middleware\AuthenticateWithTokenOnBasicAuth::class, + 'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class, + 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, + 'can' => \Illuminate\Auth\Middleware\Authorize::class, + 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, + 'limitations' => \App\Http\Middleware\CheckAccountLimitations::class, + 'locale' => \App\Http\Middleware\CheckLocale::class, + 'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class, + 'sentry.context' => \App\Http\Middleware\SentryContext::class, + 'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class, + 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, + 'verified' => \App\Http\Middleware\EnsureEmailIsVerified::class, + 'webauthn' => \LaravelWebauthn\Http\Middleware\WebauthnMiddleware::class, + ]; + + /** + * The priority-sorted list of middleware. + * + * This forces the listed middleware to always be in the given order. + * + * @var array + */ + protected $middlewarePriority = [ + \Illuminate\Session\Middleware\StartSession::class, + \Illuminate\View\Middleware\ShareErrorsFromSession::class, + \App\Http\Middleware\Authenticate::class, + \App\Http\Middleware\AuthenticateWithTokenOnBasicAuth::class, + \Illuminate\Routing\Middleware\ThrottleRequests::class, + \Illuminate\Session\Middleware\AuthenticateSession::class, + \Illuminate\Routing\Middleware\SubstituteBindings::class, + \Illuminate\Auth\Middleware\Authorize::class, + \App\Http\Middleware\CheckLocale::class, + ]; +} diff --git a/app/Http/Location/Drivers/CloudflareDriver.php b/app/Http/Location/Drivers/CloudflareDriver.php new file mode 100644 index 0000000..6b41f45 --- /dev/null +++ b/app/Http/Location/Drivers/CloudflareDriver.php @@ -0,0 +1,49 @@ +countryCode = $location->country_code; + + return $position; + } + + protected function process($ip = null) + { + try { + return $this->getCountry($ip); + } catch (\Exception $e) { + return false; + } + } + + private function getCountry($ip = null) + { + $country = Request::header('Cf-Ipcountry'); + + if (! is_null($country)) { + $response = ['country_code' => $country]; + + return new Fluent($response); + } + + return $this->fallback->get($ip ?: RequestHelper::ip()); + } +} diff --git a/app/Http/Middleware/Authenticate.php b/app/Http/Middleware/Authenticate.php new file mode 100644 index 0000000..16cea19 --- /dev/null +++ b/app/Http/Middleware/Authenticate.php @@ -0,0 +1,23 @@ +expectsJson()) { + return route('loginRedirect'); + } + + return ''; + } +} diff --git a/app/Http/Middleware/AuthenticateWithTokenOnBasicAuth.php b/app/Http/Middleware/AuthenticateWithTokenOnBasicAuth.php new file mode 100644 index 0000000..2b05d96 --- /dev/null +++ b/app/Http/Middleware/AuthenticateWithTokenOnBasicAuth.php @@ -0,0 +1,149 @@ +auth = $auth; + } + + /** + * Handle an incoming request. + * + * @param \Illuminate\Http\Request $request + * @param \Closure $next + * @return mixed + */ + public function handle($request, Closure $next) + { + $this->authenticate($request); + + return $next($request); + } + + /** + * Handle authentication. + * + * @param \Illuminate\Http\Request $request + * @return mixed + */ + private function authenticate($request) + { + if ($this->auth->guard()->check()) { + return; + } + + if (! $this->basicAuth($request)) { + $this->failedBasicResponse(); + } + } + + /** + * Try Bearer authentication, with token in 'password' field on basic auth. + * + * @param \Illuminate\Http\Request $request + */ + private function basicAuth(Request $request) + { + if (! $this->assertToken($request)) { + return false; + } + + $user = $this->authUser($request); + + // match User header if present + if ($user && (! $request->getUser() || $request->getUser() === $user->email)) { + $this->auth->guard()->setUser($user); + + return true; + } + + return false; + } + + /** + * Authenticate user. + * + * @param \Illuminate\Http\Request $request + * @return User|null + */ + private function authUser(Request $request): ?User + { + $headerUser = $request->getUser(); + $user = null; + try { + // Remove User from header request as Laravel auth will not authenticate using Bearer token + $request->headers->set('PHP_AUTH_USER', ''); + + /** @var \Illuminate\Auth\RequestGuard */ + $guard = $this->auth->guard('api'); + + /** @var ?User */ + $user = $guard->setRequest($request) + ->user(); + } finally { + $request->headers->set('PHP_AUTH_USER', $headerUser); + } + + return $user; + } + + /** + * Assert Bearer token is present. + * If not using 'password' field on basic auth as Bearer token. + * + * @param \Illuminate\Http\Request $request + * @return bool + */ + private function assertToken(Request $request): bool + { + if (! $request->bearerToken()) { + $password = $request->getPassword(); + $request->headers->set('Authorization', 'Bearer '.$password); + } + + return true; + } + + /** + * Get the response for basic authentication. + * + * @return void + * + * @throws \Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException + */ + protected function failedBasicResponse() + { + throw new UnauthorizedHttpException('Basic', 'Invalid credentials.'); + } +} diff --git a/app/Http/Middleware/CheckAccountLimitations.php b/app/Http/Middleware/CheckAccountLimitations.php new file mode 100644 index 0000000..40a98e0 --- /dev/null +++ b/app/Http/Middleware/CheckAccountLimitations.php @@ -0,0 +1,26 @@ +user()->account)) { + abort(402); + } + + return $next($request); + } +} diff --git a/app/Http/Middleware/CheckCompliance.php b/app/Http/Middleware/CheckCompliance.php new file mode 100644 index 0000000..5b52499 --- /dev/null +++ b/app/Http/Middleware/CheckCompliance.php @@ -0,0 +1,35 @@ +isMethod('post')) { + return $next($request); + } + + if (Route::currentRouteName() == 'compliance') { + return $next($request); + } + + if (Auth::check() && ! ComplianceHelper::isCompliantWithCurrentTerm(auth()->user())) { + return redirect()->route('compliance'); + } + + return $next($request); + } +} diff --git a/app/Http/Middleware/CheckLocale.php b/app/Http/Middleware/CheckLocale.php new file mode 100644 index 0000000..8a0f8f2 --- /dev/null +++ b/app/Http/Middleware/CheckLocale.php @@ -0,0 +1,30 @@ +query('lang'); + + if (empty($locale)) { + $locale = LocaleHelper::getLocale(); + } + + App::setLocale($locale); + + return $next($request); + } +} diff --git a/app/Http/Middleware/CheckVersion.php b/app/Http/Middleware/CheckVersion.php new file mode 100644 index 0000000..a664734 --- /dev/null +++ b/app/Http/Middleware/CheckVersion.php @@ -0,0 +1,41 @@ +latest_version ?? '0.0.0'); + $currentVersion = new Version($instance->current_version ?? '0.0.0'); + + if ($latestVersion == $appVersion && $currentVersion != $latestVersion) { + + // The instance has been updated to the latest version. We reset + // the ping data. + + $instance->current_version = $instance->latest_version; + $instance->latest_release_notes = null; + $instance->number_of_versions_since_current_version = null; + $instance->save(); + } + } + + return $next($request); + } +} diff --git a/app/Http/Middleware/EncryptCookies.php b/app/Http/Middleware/EncryptCookies.php new file mode 100644 index 0000000..f59cb19 --- /dev/null +++ b/app/Http/Middleware/EncryptCookies.php @@ -0,0 +1,17 @@ + + */ + protected $except = [ + // + ]; +} diff --git a/app/Http/Middleware/EnsureEmailIsVerified.php b/app/Http/Middleware/EnsureEmailIsVerified.php new file mode 100644 index 0000000..8c07209 --- /dev/null +++ b/app/Http/Middleware/EnsureEmailIsVerified.php @@ -0,0 +1,25 @@ + + */ + protected $except = [ + // + ]; +} diff --git a/app/Http/Middleware/RedirectIfAuthenticated.php b/app/Http/Middleware/RedirectIfAuthenticated.php new file mode 100644 index 0000000..3bf765e --- /dev/null +++ b/app/Http/Middleware/RedirectIfAuthenticated.php @@ -0,0 +1,26 @@ +check()) { + return redirect()->route('dashboard.index'); + } + + return $next($request); + } +} diff --git a/app/Http/Middleware/SentryContext.php b/app/Http/Middleware/SentryContext.php new file mode 100644 index 0000000..995cd72 --- /dev/null +++ b/app/Http/Middleware/SentryContext.php @@ -0,0 +1,44 @@ +bound('sentry') && config('monica.sentry_support')) { + // Add user context + if (auth()->check()) { + \Sentry\configureScope(function (Scope $scope): void { + $user = auth()->user(); + $scope->setUser([ + 'id' => $user->id, + 'email' => $user->email, + 'username' => $user->name, + ]); + $scope->setExtra('isSubscribed', $user->account->isSubscribed()); + }); + } else { + \Sentry\configureScope(function (Scope $scope): void { + $scope->setUser([ + 'id' => null, + 'ip_address' => RequestHelper::ip(), + ]); + }); + } + } + + return $next($request); + } +} diff --git a/app/Http/Middleware/TrimStrings.php b/app/Http/Middleware/TrimStrings.php new file mode 100644 index 0000000..26c3f3c --- /dev/null +++ b/app/Http/Middleware/TrimStrings.php @@ -0,0 +1,18 @@ + + */ + protected $except = [ + 'password', + 'password_confirmation', + ]; +} diff --git a/app/Http/Middleware/TrustProxies.php b/app/Http/Middleware/TrustProxies.php new file mode 100644 index 0000000..f88f213 --- /dev/null +++ b/app/Http/Middleware/TrustProxies.php @@ -0,0 +1,18 @@ +proxies; + } +} diff --git a/app/Http/Middleware/VerifyCsrfToken.php b/app/Http/Middleware/VerifyCsrfToken.php new file mode 100644 index 0000000..698877d --- /dev/null +++ b/app/Http/Middleware/VerifyCsrfToken.php @@ -0,0 +1,24 @@ + + */ + protected $except = [ + 'stripe/*', + ]; +} diff --git a/app/Http/Requests/AuthorizedRequest.php b/app/Http/Requests/AuthorizedRequest.php new file mode 100644 index 0000000..c9fb80f --- /dev/null +++ b/app/Http/Requests/AuthorizedRequest.php @@ -0,0 +1,14 @@ + 'required|email|max:255|unique:users,email', + ]; + } +} diff --git a/app/Http/Requests/ImportsRequest.php b/app/Http/Requests/ImportsRequest.php new file mode 100644 index 0000000..5d5e25c --- /dev/null +++ b/app/Http/Requests/ImportsRequest.php @@ -0,0 +1,18 @@ + 'required|file|max:'.config('monica.max_upload_size').'|mimes:vcf,vcard', + ]; + } +} diff --git a/app/Http/Requests/InvitationRequest.php b/app/Http/Requests/InvitationRequest.php new file mode 100644 index 0000000..a335100 --- /dev/null +++ b/app/Http/Requests/InvitationRequest.php @@ -0,0 +1,18 @@ + 'required', + ]; + } +} diff --git a/app/Http/Requests/Journal/DaysRequest.php b/app/Http/Requests/Journal/DaysRequest.php new file mode 100644 index 0000000..d3fc20b --- /dev/null +++ b/app/Http/Requests/Journal/DaysRequest.php @@ -0,0 +1,20 @@ + 'integer|required', + ]; + } +} diff --git a/app/Http/Requests/PasswordChangeRequest.php b/app/Http/Requests/PasswordChangeRequest.php new file mode 100644 index 0000000..da8bd8a --- /dev/null +++ b/app/Http/Requests/PasswordChangeRequest.php @@ -0,0 +1,21 @@ + 'required', + 'password' => ['required', 'confirmed', PasswordRules::defaults()], + ]; + } +} diff --git a/app/Http/Requests/People/ContactFieldsRequest.php b/app/Http/Requests/People/ContactFieldsRequest.php new file mode 100644 index 0000000..076c965 --- /dev/null +++ b/app/Http/Requests/People/ContactFieldsRequest.php @@ -0,0 +1,21 @@ + 'required|integer', + 'data' => 'max:255|required', + ]; + } +} diff --git a/app/Http/Requests/People/ConversationRequest.php b/app/Http/Requests/People/ConversationRequest.php new file mode 100644 index 0000000..385198d --- /dev/null +++ b/app/Http/Requests/People/ConversationRequest.php @@ -0,0 +1,20 @@ + 'required|date', + ]; + } +} diff --git a/app/Http/Requests/People/DebtRequest.php b/app/Http/Requests/People/DebtRequest.php new file mode 100644 index 0000000..45770c7 --- /dev/null +++ b/app/Http/Requests/People/DebtRequest.php @@ -0,0 +1,23 @@ + 'required', + 'amount' => 'required|numeric', + 'reason' => 'string|nullable', + 'status' => '', + ]; + } +} diff --git a/app/Http/Requests/People/GiftsRequest.php b/app/Http/Requests/People/GiftsRequest.php new file mode 100644 index 0000000..55380fb --- /dev/null +++ b/app/Http/Requests/People/GiftsRequest.php @@ -0,0 +1,27 @@ + 'required', + 'comment' => '', + 'url' => '', + 'offered' => 'string', + 'date_offered' => 'date|nullable', + 'value' => 'integer|nullable', + 'has_recipient' => 'boolean', + 'recipient' => 'required_with:has_recipient', + ]; + } +} diff --git a/app/Http/Requests/People/NoteToggleRequest.php b/app/Http/Requests/People/NoteToggleRequest.php new file mode 100644 index 0000000..7aecd83 --- /dev/null +++ b/app/Http/Requests/People/NoteToggleRequest.php @@ -0,0 +1,18 @@ + 'required|string', + 'is_favorited' => 'boolean|required', + ]; + } +} diff --git a/app/Http/Requests/People/PetsRequest.php b/app/Http/Requests/People/PetsRequest.php new file mode 100644 index 0000000..16ad0d2 --- /dev/null +++ b/app/Http/Requests/People/PetsRequest.php @@ -0,0 +1,21 @@ + 'max:255|nullable', + 'pet_category_id' => 'integer', + ]; + } +} diff --git a/app/Http/Requests/Request.php b/app/Http/Requests/Request.php new file mode 100644 index 0000000..76b2ffd --- /dev/null +++ b/app/Http/Requests/Request.php @@ -0,0 +1,10 @@ + ['required', Rule::in(Gender::LIST)], + 'name' => 'max:255', + 'id' => 'integer', + ]; + } +} diff --git a/app/Http/Requests/SettingsRequest.php b/app/Http/Requests/SettingsRequest.php new file mode 100644 index 0000000..97e3da7 --- /dev/null +++ b/app/Http/Requests/SettingsRequest.php @@ -0,0 +1,41 @@ + 'required|max:255', + 'last_name' => 'required|max:255', + 'email' => 'required|email|max:255|unique:users,email,'.$this->id, + 'timezone' => 'required|string', + 'fluid_container' => 'required|bool', + 'temperature_scale' => [ + 'required', + 'string', + Rule::in(['fahrenheit', 'celsius']), + ], + 'locale' => [ + 'required', + 'string', + Rule::In(config('lang-detector.languages')), + ], + 'currency_id' => 'required|int|exists:currencies,id', + 'name_order' => [ + 'required', + 'string', + Rule::In(User::NAMES_ORDER), + ], + ]; + } +} diff --git a/app/Http/Resources/Account/User/User.php b/app/Http/Resources/Account/User/User.php new file mode 100644 index 0000000..e118c66 --- /dev/null +++ b/app/Http/Resources/Account/User/User.php @@ -0,0 +1,43 @@ + + */ +class User extends JsonResource +{ + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request $request + * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return [ + 'id' => $this->id, + 'uuid' => $this->uuid, + 'object' => 'user', + 'first_name' => $this->first_name, + 'last_name' => $this->last_name, + 'name' => $this->name, + 'email' => $this->email, + 'me_contact' => new ContactShortResource($this->me), + 'timezone' => $this->timezone, + 'currency' => new CurrencyResource($this->currency), + 'locale' => $this->locale, + 'is_policy_compliant' => $this->policy_compliant, + 'account' => [ + 'id' => $this->account_id, + ], + 'created_at' => DateHelper::getTimestamp($this->created_at), + 'updated_at' => DateHelper::getTimestamp($this->updated_at), + ]; + } +} diff --git a/app/Http/Resources/Activity/Activity.php b/app/Http/Resources/Activity/Activity.php new file mode 100644 index 0000000..3213321 --- /dev/null +++ b/app/Http/Resources/Activity/Activity.php @@ -0,0 +1,44 @@ + + */ +class Activity extends JsonResource +{ + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request $request + * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return [ + 'id' => $this->id, + 'uuid' => $this->uuid, + 'object' => 'activity', + 'summary' => $this->summary, + 'description' => $this->description, + 'happened_at' => DateHelper::getDate($this->happened_at), + 'activity_type' => new ActivityTypeResource($this->type), + 'attendees' => [ + 'total' => $this->contacts()->count(), + 'contacts' => $this->getContactsForAPI(), + ], + 'emotions' => EmotionResource::collection($this->emotions), + 'url' => route('api.activity', $this->id), + 'account' => [ + 'id' => $this->account_id, + ], + 'created_at' => DateHelper::getTimestamp($this->created_at), + 'updated_at' => DateHelper::getTimestamp($this->updated_at), + ]; + } +} diff --git a/app/Http/Resources/Activity/ActivityType.php b/app/Http/Resources/Activity/ActivityType.php new file mode 100644 index 0000000..cc5004d --- /dev/null +++ b/app/Http/Resources/Activity/ActivityType.php @@ -0,0 +1,36 @@ + + */ +class ActivityType extends JsonResource +{ + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request $request + * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return [ + 'id' => $this->id, + 'uuid' => $this->uuid, + 'object' => 'activityType', + 'name' => $this->name, + 'location_type' => $this->location_type, + 'activity_type_category' => new ActivityTypeCategoryResource($this->category), + 'account' => [ + 'id' => $this->account_id, + ], + 'created_at' => DateHelper::getTimestamp($this->created_at), + 'updated_at' => DateHelper::getTimestamp($this->updated_at), + ]; + } +} diff --git a/app/Http/Resources/Activity/ActivityTypeCategory.php b/app/Http/Resources/Activity/ActivityTypeCategory.php new file mode 100644 index 0000000..5415d5c --- /dev/null +++ b/app/Http/Resources/Activity/ActivityTypeCategory.php @@ -0,0 +1,33 @@ + + */ +class ActivityTypeCategory extends JsonResource +{ + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request $request + * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return [ + 'id' => $this->id, + 'uuid' => $this->uuid, + 'object' => 'activityTypeCategory', + 'name' => $this->name, + 'account' => [ + 'id' => $this->account_id, + ], + 'created_at' => DateHelper::getTimestamp($this->created_at), + 'updated_at' => DateHelper::getTimestamp($this->updated_at), + ]; + } +} diff --git a/app/Http/Resources/Address/Address.php b/app/Http/Resources/Address/Address.php new file mode 100644 index 0000000..aa6e142 --- /dev/null +++ b/app/Http/Resources/Address/Address.php @@ -0,0 +1,44 @@ + + */ +class Address extends JsonResource +{ + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request $request + * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return [ + 'id' => $this->id, + 'uuid' => $this->uuid, + 'object' => 'address', + 'name' => $this->name, + 'street' => $this->place->street, + 'city' => $this->place->city, + 'province' => $this->place->province, + 'postal_code' => $this->place->postal_code, + 'latitude' => $this->place->latitude, + 'longitude' => $this->place->longitude, + 'country' => new CountryResource($this->place->country), + 'url' => route('api.address', $this->id), + 'account' => [ + 'id' => $this->account_id, + ], + 'contact' => new ContactShortResource($this->contact), + 'created_at' => DateHelper::getTimestamp($this->created_at), + 'updated_at' => DateHelper::getTimestamp($this->updated_at), + ]; + } +} diff --git a/app/Http/Resources/AuditLog/AuditLog.php b/app/Http/Resources/AuditLog/AuditLog.php new file mode 100644 index 0000000..665d9d8 --- /dev/null +++ b/app/Http/Resources/AuditLog/AuditLog.php @@ -0,0 +1,38 @@ + + */ +class AuditLog extends JsonResource +{ + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request $request + * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return [ + 'id' => $this->id, + 'object' => 'auditlog', + 'author' => ($this->author) ? [ + 'id' => $this->author->id, + 'name' => $this->author->name, + ] : [ + 'name' => $this->author_name, + ], + 'action' => $this->action, + 'objects' => json_decode($this->objects), + 'audited_at' => DateHelper::getTimestamp($this->audited_at), + 'created_at' => DateHelper::getTimestamp($this->created_at), + 'updated_at' => DateHelper::getTimestamp($this->updated_at), + ]; + } +} diff --git a/app/Http/Resources/Call/Call.php b/app/Http/Resources/Call/Call.php new file mode 100644 index 0000000..dee1b8d --- /dev/null +++ b/app/Http/Resources/Call/Call.php @@ -0,0 +1,40 @@ + + */ +class Call extends JsonResource +{ + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request $request + * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return [ + 'id' => $this->id, + 'uuid' => $this->uuid, + 'object' => 'call', + 'called_at' => DateHelper::getTimestamp($this->called_at), + 'content' => $this->content, + 'contact_called' => $this->contact_called, + 'emotions' => EmotionResource::collection($this->emotions), + 'url' => route('api.call', $this->id), + 'account' => [ + 'id' => $this->account_id, + ], + 'contact' => new ContactShortResource($this->contact), + 'created_at' => DateHelper::getTimestamp($this->created_at), + 'updated_at' => DateHelper::getTimestamp($this->updated_at), + ]; + } +} diff --git a/app/Http/Resources/Company/Company.php b/app/Http/Resources/Company/Company.php new file mode 100644 index 0000000..e8718e2 --- /dev/null +++ b/app/Http/Resources/Company/Company.php @@ -0,0 +1,34 @@ + + */ +class Company extends JsonResource +{ + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request $request + * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return [ + 'id' => $this->id, + 'object' => 'company', + 'name' => $this->name, + 'website' => $this->website, + 'number_of_employees' => $this->number_of_employees, + 'account' => [ + 'id' => $this->account_id, + ], + 'created_at' => DateHelper::getTimestamp($this->created_at), + 'updated_at' => DateHelper::getTimestamp($this->updated_at), + ]; + } +} diff --git a/app/Http/Resources/Contact/Contact.php b/app/Http/Resources/Contact/Contact.php new file mode 100644 index 0000000..4681f72 --- /dev/null +++ b/app/Http/Resources/Contact/Contact.php @@ -0,0 +1,24 @@ + + */ +class Contact extends JsonResource +{ + use ContactBase; + + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request $request + * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return $this->toArrayInternal($request, $request->input('with') == 'contactfields'); + } +} diff --git a/app/Http/Resources/Contact/ContactBase.php b/app/Http/Resources/Contact/ContactBase.php new file mode 100644 index 0000000..9279f7e --- /dev/null +++ b/app/Http/Resources/Contact/ContactBase.php @@ -0,0 +1,126 @@ + $this->id, + 'uuid' => $this->uuid, + 'object' => 'contact', + 'hash_id' => $this->getHashId(), + 'first_name' => $this->first_name, + 'last_name' => $this->last_name, + 'nickname' => $this->nickname, + 'complete_name' => $this->name, + 'initials' => $this->getInitials(), + 'description' => $this->description, + 'gender' => is_null($this->gender) ? null : $this->gender->name, + 'gender_type' => is_null($this->gender) ? null : $this->gender->type, + 'is_starred' => (bool) $this->is_starred, + 'is_partial' => (bool) $this->is_partial, + 'is_active' => (bool) $this->is_active, + 'is_dead' => (bool) $this->is_dead, + 'is_me' => $this->isMe(), + 'last_called' => $this->when(! $this->is_partial, $this->last_talked_to), + 'last_activity_together' => $this->when(! $this->is_partial, $this->getLastActivityDate()), + 'stay_in_touch_frequency' => $this->when(! $this->is_partial, $this->stay_in_touch_frequency), + 'stay_in_touch_trigger_date' => $this->when(! $this->is_partial, DateHelper::getTimestamp($this->stay_in_touch_trigger_date)), + 'information' => [ + 'relationships' => $this->when(! $this->is_partial, [ + 'love' => [ + 'total' => (is_null($this->getRelationshipsByRelationshipTypeGroup('love')) ? 0 : $this->getRelationshipsByRelationshipTypeGroup('love')->count()), + 'contacts' => (is_null($this->getRelationshipsByRelationshipTypeGroup('love')) ? null : RelationshipShortResource::collection($this->getRelationshipsByRelationshipTypeGroup('love'))), + ], + 'family' => [ + 'total' => (is_null($this->getRelationshipsByRelationshipTypeGroup('family')) ? 0 : $this->getRelationshipsByRelationshipTypeGroup('family')->count()), + 'contacts' => (is_null($this->getRelationshipsByRelationshipTypeGroup('family')) ? null : RelationshipShortResource::collection($this->getRelationshipsByRelationshipTypeGroup('family'))), + ], + 'friend' => [ + 'total' => (is_null($this->getRelationshipsByRelationshipTypeGroup('friend')) ? 0 : $this->getRelationshipsByRelationshipTypeGroup('friend')->count()), + 'contacts' => (is_null($this->getRelationshipsByRelationshipTypeGroup('friend')) ? null : RelationshipShortResource::collection($this->getRelationshipsByRelationshipTypeGroup('friend'))), + ], + 'work' => [ + 'total' => (is_null($this->getRelationshipsByRelationshipTypeGroup('work')) ? 0 : $this->getRelationshipsByRelationshipTypeGroup('work')->count()), + 'contacts' => (is_null($this->getRelationshipsByRelationshipTypeGroup('work')) ? null : RelationshipShortResource::collection($this->getRelationshipsByRelationshipTypeGroup('work'))), + ], + ]), + 'dates' => [ + 'birthdate' => [ + 'is_age_based' => (is_null($this->birthdate) ? null : (bool) $this->birthdate->is_age_based), + 'is_year_unknown' => (is_null($this->birthdate) ? null : (bool) $this->birthdate->is_year_unknown), + 'date' => DateHelper::getTimestamp($this->birthdate), + ], + 'deceased_date' => [ + 'is_age_based' => (is_null($this->deceasedDate) ? null : (bool) $this->deceasedDate->is_age_based), + 'is_year_unknown' => (is_null($this->deceasedDate) ? null : (bool) $this->deceasedDate->is_year_unknown), + 'date' => DateHelper::getTimestamp($this->deceasedDate), + ], + ], + 'career' => $this->when(! $this->is_partial, [ + 'job' => $this->job, + 'company' => $this->company, + ]), + 'avatar' => $this->when(! $this->is_partial, [ + 'url' => $this->getAvatarUrl(), + 'source' => $this->avatar_source, + 'default_avatar_color' => $this->default_avatar_color, + ]), + 'food_preferences' => $this->when(! $this->is_partial, $this->food_preferences), + 'how_you_met' => $this->when(! $this->is_partial, [ + 'general_information' => $this->first_met_additional_info, + 'first_met_date' => [ + 'is_age_based' => (is_null($this->firstMetDate) ? null : (bool) $this->firstMetDate->is_age_based), + 'is_year_unknown' => (is_null($this->firstMetDate) ? null : (bool) $this->firstMetDate->is_year_unknown), + 'date' => DateHelper::getTimestamp($this->firstMetDate), + ], + 'first_met_through_contact' => new ContactShortResource($this->getIntroducer()), + ]), + ], + 'addresses' => $this->when(! $this->is_partial, AddressResource::collection($this->addresses)), + 'tags' => $this->when(! $this->is_partial, TagResource::collection($this->tags)), + 'statistics' => $this->when(! $this->is_partial, [ + 'number_of_calls' => $this->calls->count(), + 'number_of_notes' => $this->notes->count(), + 'number_of_activities' => $this->activities->count(), + 'number_of_reminders' => $this->reminders->count(), + 'number_of_tasks' => $this->tasks->count(), + 'number_of_gifts' => $this->gifts->count(), + 'number_of_debts' => $this->debts->count(), + ]), + 'contactFields' => $this->when($withContactField && ! $this->is_partial, ContactFieldResource::collection($this->contactFields)), + 'notes' => $this->when($withContactField && ! $this->is_partial, NoteResource::collection($this->notes()->latest()->limit(3)->get())), + 'url' => $this->when(! $this->is_partial, route('api.contact', $this->id)), + 'account' => [ + 'id' => $this->account_id, + ], + 'created_at' => DateHelper::getTimestamp($this->created_at), + 'updated_at' => DateHelper::getTimestamp($this->updated_at), + ]; + } + + protected function getHashId() + { + $hashid = ''; + if ($this->is_partial) { + $realContact = $this->getRelatedRealContact(); + if ($realContact) { + $hashid = $realContact->hashID(); + } + } else { + $hashid = $this->hashID(); + } + + return $hashid; + } +} diff --git a/app/Http/Resources/Contact/ContactSearch.php b/app/Http/Resources/Contact/ContactSearch.php new file mode 100644 index 0000000..0b647e1 --- /dev/null +++ b/app/Http/Resources/Contact/ContactSearch.php @@ -0,0 +1,45 @@ + + */ +class ContactSearch extends JsonResource +{ + use ContactBase; + + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request $request + * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return [ + 'id' => $this->id, + 'uuid' => $this->uuid, + 'object' => 'contact', + 'route' => route('people.show', $this), + 'complete_name' => $this->name, + 'description' => $this->description, + 'initials' => $this->getInitials(), + 'is_me' => $this->isMe(), + 'is_starred' => $this->is_starred, + 'information' => [ + 'avatar' => [ + 'url' => $this->getAvatarUrl(), + 'source' => $this->avatar_source, + 'default_avatar_color' => $this->default_avatar_color, + ], + ], + 'url' => $this->when(! $this->is_partial, route('api.contact', $this->id)), + 'account' => [ + 'id' => $this->account_id, + ], + ]; + } +} diff --git a/app/Http/Resources/Contact/ContactShort.php b/app/Http/Resources/Contact/ContactShort.php new file mode 100644 index 0000000..b12151d --- /dev/null +++ b/app/Http/Resources/Contact/ContactShort.php @@ -0,0 +1,63 @@ + + */ +class ContactShort extends JsonResource +{ + use ContactBase; + + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request $request + * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return [ + 'id' => $this->id, + 'uuid' => $this->uuid, + 'object' => 'contact', + 'hash_id' => $this->getHashId(), + 'first_name' => $this->first_name, + 'last_name' => $this->last_name, + 'nickname' => $this->nickname, + 'complete_name' => $this->name, + 'initials' => $this->getInitials(), + 'gender' => is_null($this->gender) ? null : $this->gender->name, + 'gender_type' => is_null($this->gender) ? null : $this->gender->type, + 'is_starred' => (bool) $this->is_starred, + 'is_partial' => (bool) $this->is_partial, + 'is_active' => (bool) $this->is_active, + 'is_dead' => (bool) $this->is_dead, + 'is_me' => $this->isMe(), + 'information' => [ + 'birthdate' => [ + 'is_age_based' => (is_null($this->birthdate) ? null : (bool) $this->birthdate->is_age_based), + 'is_year_unknown' => (is_null($this->birthdate) ? null : (bool) $this->birthdate->is_year_unknown), + 'date' => DateHelper::getTimestamp($this->birthdate), + ], + 'deceased_date' => [ + 'is_age_based' => (is_null($this->deceasedDate) ? null : (bool) $this->deceasedDate->is_age_based), + 'is_year_unknown' => (is_null($this->deceasedDate) ? null : (bool) $this->deceasedDate->is_year_unknown), + 'date' => DateHelper::getTimestamp($this->deceasedDate), + ], + 'avatar' => [ + 'url' => $this->getAvatarUrl(), + 'source' => $this->avatar_source, + 'default_avatar_color' => $this->default_avatar_color, + ], + ], + 'url' => $this->when(! $this->is_partial, route('api.contact', $this->id)), + 'account' => [ + 'id' => $this->account_id, + ], + ]; + } +} diff --git a/app/Http/Resources/Contact/ContactWithContactFields.php b/app/Http/Resources/Contact/ContactWithContactFields.php new file mode 100644 index 0000000..4d4b77d --- /dev/null +++ b/app/Http/Resources/Contact/ContactWithContactFields.php @@ -0,0 +1,24 @@ + + */ +class ContactWithContactFields extends JsonResource +{ + use ContactBase; + + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request $request + * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return $this->toArrayInternal($request, true); + } +} diff --git a/app/Http/Resources/ContactField/ContactField.php b/app/Http/Resources/ContactField/ContactField.php new file mode 100644 index 0000000..824312e --- /dev/null +++ b/app/Http/Resources/ContactField/ContactField.php @@ -0,0 +1,38 @@ + + */ +class ContactField extends JsonResource +{ + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request $request + * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return [ + 'id' => $this->id, + 'uuid' => $this->uuid, + 'object' => 'contactfield', + 'content' => $this->data, + 'contact_field_type' => new ContactFieldTypeResource($this->contactFieldType), + 'labels' => ContactFieldLabel::collection($this->labels), + 'account' => [ + 'id' => $this->account_id, + ], + 'contact' => new ContactShortResource($this->contact), + 'created_at' => DateHelper::getTimestamp($this->created_at), + 'updated_at' => DateHelper::getTimestamp($this->updated_at), + ]; + } +} diff --git a/app/Http/Resources/ContactField/ContactFieldLabel.php b/app/Http/Resources/ContactField/ContactFieldLabel.php new file mode 100644 index 0000000..727d951 --- /dev/null +++ b/app/Http/Resources/ContactField/ContactFieldLabel.php @@ -0,0 +1,33 @@ + + */ +class ContactFieldLabel extends JsonResource +{ + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request $request + * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return [ + 'id' => $this->id, + 'object' => 'contactfieldlabel', + 'type' => $this->label_i18n ?: $this->label, + 'label' => $this->label_i18n ? trans('people.contact_field_label_'.$this->label_i18n) : $this->label, + 'account' => [ + 'id' => $this->account_id, + ], + 'created_at' => DateHelper::getTimestamp($this->created_at), + 'updated_at' => DateHelper::getTimestamp($this->updated_at), + ]; + } +} diff --git a/app/Http/Resources/Conversation/Conversation.php b/app/Http/Resources/Conversation/Conversation.php new file mode 100644 index 0000000..8c6e810 --- /dev/null +++ b/app/Http/Resources/Conversation/Conversation.php @@ -0,0 +1,40 @@ + + */ +class Conversation extends JsonResource +{ + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request $request + * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return [ + 'id' => $this->id, + 'uuid' => $this->uuid, + 'object' => 'conversation', + 'happened_at' => DateHelper::getTimestamp($this->happened_at), + 'messages' => MessageResource::collection($this->messages), + 'contact_field_type' => new ContactFieldTypeResource($this->contactFieldType), + 'url' => route('api.conversation', $this->id), + 'account' => [ + 'id' => $this->account_id, + ], + 'contact' => new ContactShortResource($this->contact), + 'created_at' => DateHelper::getTimestamp($this->created_at), + 'updated_at' => DateHelper::getTimestamp($this->updated_at), + ]; + } +} diff --git a/app/Http/Resources/Conversation/Message.php b/app/Http/Resources/Conversation/Message.php new file mode 100644 index 0000000..b25fd8b --- /dev/null +++ b/app/Http/Resources/Conversation/Message.php @@ -0,0 +1,40 @@ + + */ +class Message extends JsonResource +{ + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request $request + * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return [ + 'id' => $this->id, + 'uuid' => $this->uuid, + 'object' => 'message', + 'content' => $this->content, + 'written_at' => DateHelper::getTimestamp($this->written_at), + 'written_by_me' => (bool) $this->written_by_me, + 'account' => [ + 'id' => $this->account_id, + ], + 'contact' => new ContactShortResource($this->contact), + 'conversation' => [ + 'id' => $this->conversation->id, + ], + 'created_at' => DateHelper::getTimestamp($this->created_at), + 'updated_at' => DateHelper::getTimestamp($this->updated_at), + ]; + } +} diff --git a/app/Http/Resources/Country/Country.php b/app/Http/Resources/Country/Country.php new file mode 100644 index 0000000..c655cb9 --- /dev/null +++ b/app/Http/Resources/Country/Country.php @@ -0,0 +1,33 @@ +resource)) { + $id = $this->resource['id']; + $name = $this->resource['country']; + } else { + $id = $this->resource; + $name = CountriesHelper::get($this->resource); + } + + return [ + 'id' => $id, + 'object' => 'country', + 'name' => $name, + 'iso' => $id, + ]; + } +} diff --git a/app/Http/Resources/Debt/Debt.php b/app/Http/Resources/Debt/Debt.php new file mode 100644 index 0000000..818e519 --- /dev/null +++ b/app/Http/Resources/Debt/Debt.php @@ -0,0 +1,40 @@ + + */ +class Debt extends JsonResource +{ + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request $request + * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return [ + 'id' => $this->id, + 'uuid' => $this->uuid, + 'object' => 'debt', + 'in_debt' => $this->in_debt, + 'status' => $this->status, + 'amount' => $this->amount, + 'value' => $this->value, + 'amount_with_currency' => $this->displayValue, + 'reason' => $this->reason, + 'account' => [ + 'id' => $this->account_id, + ], + 'contact' => new ContactShortResource($this->contact), + 'created_at' => DateHelper::getTimestamp($this->created_at), + 'updated_at' => DateHelper::getTimestamp($this->updated_at), + ]; + } +} diff --git a/app/Http/Resources/Document/Document.php b/app/Http/Resources/Document/Document.php new file mode 100644 index 0000000..969ecb1 --- /dev/null +++ b/app/Http/Resources/Document/Document.php @@ -0,0 +1,42 @@ + + */ +class Document extends JsonResource +{ + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request $request + * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return [ + 'id' => $this->id, + 'uuid' => $this->uuid, + 'object' => 'document', + 'original_filename' => $this->original_filename, + 'new_filename' => $this->new_filename, + 'filesize' => $this->filesize, + 'type' => $this->type, + 'mime_type' => $this->mime_type, + 'number_of_downloads' => $this->number_of_downloads, + 'link' => $this->getDownloadLink(), + 'url' => route('api.document', $this->id), + 'account' => [ + 'id' => $this->account_id, + ], + 'contact' => new ContactShortResource($this->contact), + 'created_at' => DateHelper::getTimestamp($this->created_at), + 'updated_at' => DateHelper::getTimestamp($this->updated_at), + ]; + } +} diff --git a/app/Http/Resources/Emotion/Emotion.php b/app/Http/Resources/Emotion/Emotion.php new file mode 100644 index 0000000..a68e0f9 --- /dev/null +++ b/app/Http/Resources/Emotion/Emotion.php @@ -0,0 +1,26 @@ + + */ +class Emotion extends JsonResource +{ + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request $request + * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return [ + 'id' => $this->id, + 'object' => 'emotion', + 'name' => $this->name, + ]; + } +} diff --git a/app/Http/Resources/Gender/Gender.php b/app/Http/Resources/Gender/Gender.php new file mode 100644 index 0000000..dbb19b0 --- /dev/null +++ b/app/Http/Resources/Gender/Gender.php @@ -0,0 +1,33 @@ + + */ +class Gender extends JsonResource +{ + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request $request + * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return [ + 'id' => $this->id, + 'object' => 'gender', + 'name' => $this->name, + 'type' => $this->type, + 'account' => [ + 'id' => $this->account_id, + ], + 'created_at' => DateHelper::getTimestamp($this->created_at), + 'updated_at' => DateHelper::getTimestamp($this->updated_at), + ]; + } +} diff --git a/app/Http/Resources/Gift/Gift.php b/app/Http/Resources/Gift/Gift.php new file mode 100644 index 0000000..86c3de4 --- /dev/null +++ b/app/Http/Resources/Gift/Gift.php @@ -0,0 +1,45 @@ + + */ +class Gift extends JsonResource +{ + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request $request + * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return [ + 'id' => $this->id, + 'uuid' => $this->uuid, + 'object' => 'gift', + 'name' => $this->name, + 'comment' => $this->comment, + 'url' => $this->url, + 'amount' => $this->amount, + 'value' => $this->value, + 'amount_with_currency' => $this->displayValue, + 'status' => $this->status, + 'date' => DateHelper::getDate($this->date), + 'recipient' => new ContactShortResource($this->recipient), + 'photos' => PhotoResource::collection($this->photos), + 'contact' => new ContactShortResource($this->contact), + 'account' => [ + 'id' => $this->account_id, + ], + 'created_at' => DateHelper::getTimestamp($this->created_at), + 'updated_at' => DateHelper::getTimestamp($this->updated_at), + ]; + } +} diff --git a/app/Http/Resources/Journal/Entry.php b/app/Http/Resources/Journal/Entry.php new file mode 100644 index 0000000..69f6b8c --- /dev/null +++ b/app/Http/Resources/Journal/Entry.php @@ -0,0 +1,36 @@ + + */ +class Entry extends JsonResource +{ + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request $request + * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return [ + 'id' => $this->id, + 'uuid' => $this->uuid, + 'object' => 'entry', + 'title' => $this->title, + 'post' => $this->post, + 'date' => $this->date, + 'url' => route('api.entry', $this->id), + 'account' => [ + 'id' => $this->account_id, + ], + 'created_at' => DateHelper::getTimestamp($this->created_at), + 'updated_at' => DateHelper::getTimestamp($this->updated_at), + ]; + } +} diff --git a/app/Http/Resources/LifeEvent/LifeEvent.php b/app/Http/Resources/LifeEvent/LifeEvent.php new file mode 100644 index 0000000..1ca038e --- /dev/null +++ b/app/Http/Resources/LifeEvent/LifeEvent.php @@ -0,0 +1,39 @@ + + */ +class LifeEvent extends JsonResource +{ + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request $request + * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return [ + 'id' => $this->id, + 'uuid' => $this->uuid, + 'object' => 'lifeevent', + 'name' => $this->name, + 'note' => $this->note, + 'happened_at' => DateHelper::getTimestamp($this->happened_at), + 'life_event_type' => new LifeEventTypeResource($this->lifeEventType), + 'account' => [ + 'id' => $this->account_id, + ], + 'contact' => new ContactShortResource($this->contact), + 'created_at' => DateHelper::getTimestamp($this->created_at), + 'updated_at' => DateHelper::getTimestamp($this->updated_at), + ]; + } +} diff --git a/app/Http/Resources/LifeEvent/LifeEventCategory.php b/app/Http/Resources/LifeEvent/LifeEventCategory.php new file mode 100644 index 0000000..f80ac8e --- /dev/null +++ b/app/Http/Resources/LifeEvent/LifeEventCategory.php @@ -0,0 +1,35 @@ + + */ +class LifeEventCategory extends JsonResource +{ + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request $request + * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return [ + 'id' => $this->id, + 'uuid' => $this->uuid, + 'object' => 'lifeeventcategory', + 'name' => $this->name, + 'core_monica_data' => (bool) $this->core_monica_data, + 'default_life_event_category_key' => $this->default_life_event_category_key, + 'account' => [ + 'id' => $this->account_id, + ], + 'created_at' => DateHelper::getTimestamp($this->created_at), + 'updated_at' => DateHelper::getTimestamp($this->updated_at), + ]; + } +} diff --git a/app/Http/Resources/LifeEvent/LifeEventType.php b/app/Http/Resources/LifeEvent/LifeEventType.php new file mode 100644 index 0000000..4d57aa9 --- /dev/null +++ b/app/Http/Resources/LifeEvent/LifeEventType.php @@ -0,0 +1,37 @@ + + */ +class LifeEventType extends JsonResource +{ + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request $request + * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return [ + 'id' => $this->id, + 'uuid' => $this->uuid, + 'object' => 'lifeeventtype', + 'name' => $this->name, + 'core_monica_data' => (bool) $this->core_monica_data, + 'default_life_event_type_key' => $this->default_life_event_type_key, + 'life_event_category' => new LifeEventCategoryResource($this->lifeEventCategory), + 'account' => [ + 'id' => $this->account_id, + ], + 'created_at' => DateHelper::getTimestamp($this->created_at), + 'updated_at' => DateHelper::getTimestamp($this->updated_at), + ]; + } +} diff --git a/app/Http/Resources/Note/Note.php b/app/Http/Resources/Note/Note.php new file mode 100644 index 0000000..0b0b472 --- /dev/null +++ b/app/Http/Resources/Note/Note.php @@ -0,0 +1,38 @@ + + */ +class Note extends JsonResource +{ + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request $request + * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return [ + 'id' => $this->id, + 'uuid' => $this->uuid, + 'object' => 'note', + 'body' => $this->body, + 'is_favorited' => (bool) $this->is_favorited, + 'favorited_at' => DateHelper::getTimestamp($this->favorited_at), + 'url' => route('api.note', $this->id), + 'account' => [ + 'id' => $this->account_id, + ], + 'contact' => new ContactShortResource($this->contact), + 'created_at' => DateHelper::getTimestamp($this->created_at), + 'updated_at' => DateHelper::getTimestamp($this->updated_at), + ]; + } +} diff --git a/app/Http/Resources/Occupation/Occupation.php b/app/Http/Resources/Occupation/Occupation.php new file mode 100644 index 0000000..6e6854c --- /dev/null +++ b/app/Http/Resources/Occupation/Occupation.php @@ -0,0 +1,42 @@ + + */ +class Occupation extends JsonResource +{ + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request $request + * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return [ + 'id' => $this->id, + 'object' => 'occupation', + 'title' => $this->title, + 'description' => $this->description, + 'salary' => $this->salary, + 'salary_unit' => $this->salary_unit, + 'currently_works_here' => (bool) $this->currently_works_here, + 'start_date' => DateHelper::getDate($this->start_date), + 'end_date' => DateHelper::getDate($this->end_date), + 'company' => new CompanyResource($this->company), + 'account' => [ + 'id' => $this->account_id, + ], + 'contact' => new ContactShortResource($this->contact), + 'created_at' => DateHelper::getTimestamp($this->created_at), + 'updated_at' => DateHelper::getTimestamp($this->updated_at), + ]; + } +} diff --git a/app/Http/Resources/Pet/Pet.php b/app/Http/Resources/Pet/Pet.php new file mode 100644 index 0000000..6ff90af --- /dev/null +++ b/app/Http/Resources/Pet/Pet.php @@ -0,0 +1,36 @@ + + */ +class Pet extends JsonResource +{ + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request $request + * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return [ + 'id' => $this->id, + 'uuid' => $this->uuid, + 'object' => 'pet', + 'name' => $this->name, + 'pet_category' => PetCategory::make($this->petCategory), + 'account' => [ + 'id' => $this->account_id, + ], + 'contact' => new ContactShortResource($this->contact), + 'created_at' => DateHelper::getTimestamp($this->created_at), + 'updated_at' => DateHelper::getTimestamp($this->updated_at), + ]; + } +} diff --git a/app/Http/Resources/Pet/PetCategory.php b/app/Http/Resources/Pet/PetCategory.php new file mode 100644 index 0000000..1415d61 --- /dev/null +++ b/app/Http/Resources/Pet/PetCategory.php @@ -0,0 +1,27 @@ + + */ +class PetCategory extends JsonResource +{ + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request $request + * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return [ + 'id' => $this->id, + 'object' => 'pet_category', + 'name' => $this->name, + 'is_common' => (bool) $this->is_common, + ]; + } +} diff --git a/app/Http/Resources/Photo/Photo.php b/app/Http/Resources/Photo/Photo.php new file mode 100644 index 0000000..8f3c4a2 --- /dev/null +++ b/app/Http/Resources/Photo/Photo.php @@ -0,0 +1,40 @@ + + */ +class Photo extends JsonResource +{ + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request $request + * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return [ + 'id' => $this->id, + 'uuid' => $this->uuid, + 'object' => 'photo', + 'original_filename' => $this->original_filename, + 'new_filename' => $this->new_filename, + 'filesize' => $this->filesize, + 'mime_type' => $this->mime_type, + 'dataUrl' => $this->dataUrl(), + 'link' => $this->url(), + 'account' => [ + 'id' => $this->account_id, + ], + 'contact' => new ContactShortResource($this->contact()), + 'created_at' => DateHelper::getTimestamp($this->created_at), + 'updated_at' => DateHelper::getTimestamp($this->updated_at), + ]; + } +} diff --git a/app/Http/Resources/Place/Place.php b/app/Http/Resources/Place/Place.php new file mode 100644 index 0000000..5a5fdf6 --- /dev/null +++ b/app/Http/Resources/Place/Place.php @@ -0,0 +1,39 @@ + + */ +class Place extends JsonResource +{ + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request $request + * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return [ + 'id' => $this->id, + 'object' => 'place', + 'street' => $this->street, + 'city' => $this->city, + 'province' => $this->province, + 'postal_code' => $this->postal_code, + 'latitude' => $this->latitude, + 'longitude' => $this->longitude, + 'country' => new CountryResource($this->country), + 'account' => [ + 'id' => $this->account_id, + ], + 'created_at' => DateHelper::getTimestamp($this->created_at), + 'updated_at' => DateHelper::getTimestamp($this->updated_at), + ]; + } +} diff --git a/app/Http/Resources/Relationship/Relationship.php b/app/Http/Resources/Relationship/Relationship.php new file mode 100644 index 0000000..82f58d6 --- /dev/null +++ b/app/Http/Resources/Relationship/Relationship.php @@ -0,0 +1,38 @@ + + */ +class Relationship extends JsonResource +{ + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request $request + * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return [ + 'id' => $this->id, + 'uuid' => $this->uuid, + 'object' => 'relationship', + 'contact_is' => new ContactShortResource($this->contactIs), + 'relationship_type' => new RelationshipTypeResource($this->relationshipType), + 'of_contact' => new ContactShortResource($this->ofContact), + 'url' => route('api.relationship', $this->id), + 'account' => [ + 'id' => $this->account_id, + ], + 'created_at' => DateHelper::getTimestamp($this->created_at), + 'updated_at' => DateHelper::getTimestamp($this->updated_at), + ]; + } +} diff --git a/app/Http/Resources/Relationship/RelationshipShort.php b/app/Http/Resources/Relationship/RelationshipShort.php new file mode 100644 index 0000000..492f7c1 --- /dev/null +++ b/app/Http/Resources/Relationship/RelationshipShort.php @@ -0,0 +1,30 @@ + + */ +class RelationshipShort extends JsonResource +{ + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request $request + * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return [ + 'relationship' => [ + 'id' => $this->id, + 'uuid' => $this->uuid, + 'name' => $this->relationshipType->name, + ], + 'contact' => new ContactShortResource($this->ofContact), + ]; + } +} diff --git a/app/Http/Resources/RelationshipType/RelationshipType.php b/app/Http/Resources/RelationshipType/RelationshipType.php new file mode 100644 index 0000000..dad0d6e --- /dev/null +++ b/app/Http/Resources/RelationshipType/RelationshipType.php @@ -0,0 +1,35 @@ + + */ +class RelationshipType extends JsonResource +{ + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request $request + * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return [ + 'id' => $this->id, + 'object' => 'relationshiptype', + 'name' => $this->name, + 'name_reverse_relationship' => $this->name_reverse_relationship, + 'relationship_type_group_id' => $this->relationship_type_group_id, + 'delible' => $this->delible, + 'account' => [ + 'id' => $this->account_id, + ], + 'created_at' => DateHelper::getTimestamp($this->created_at), + 'updated_at' => DateHelper::getTimestamp($this->updated_at), + ]; + } +} diff --git a/app/Http/Resources/RelationshipTypeGroup/RelationshipTypeGroup.php b/app/Http/Resources/RelationshipTypeGroup/RelationshipTypeGroup.php new file mode 100644 index 0000000..e43c7c1 --- /dev/null +++ b/app/Http/Resources/RelationshipTypeGroup/RelationshipTypeGroup.php @@ -0,0 +1,33 @@ + + */ +class RelationshipTypeGroup extends JsonResource +{ + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request $request + * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return [ + 'id' => $this->id, + 'object' => 'relationshiptypegroup', + 'name' => $this->name, + 'delible' => (bool) $this->delible, + 'account' => [ + 'id' => $this->account_id, + ], + 'created_at' => DateHelper::getTimestamp($this->created_at), + 'updated_at' => DateHelper::getTimestamp($this->updated_at), + ]; + } +} diff --git a/app/Http/Resources/Reminder/Reminder.php b/app/Http/Resources/Reminder/Reminder.php new file mode 100644 index 0000000..3c6048f --- /dev/null +++ b/app/Http/Resources/Reminder/Reminder.php @@ -0,0 +1,40 @@ + + */ +class Reminder extends JsonResource +{ + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request $request + * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return [ + 'id' => $this->id, + 'uuid' => $this->uuid, + 'object' => 'reminder', + 'title' => $this->title, + 'description' => $this->description, + 'frequency_type' => $this->frequency_type, + 'frequency_number' => $this->frequency_number, + 'initial_date' => DateHelper::getTimestamp($this->initial_date), + 'delible' => (bool) $this->delible, + 'account' => [ + 'id' => $this->account_id, + ], + 'contact' => new ContactShortResource($this->contact), + 'created_at' => DateHelper::getTimestamp($this->created_at), + 'updated_at' => DateHelper::getTimestamp($this->updated_at), + ]; + } +} diff --git a/app/Http/Resources/Reminder/ReminderOutbox.php b/app/Http/Resources/Reminder/ReminderOutbox.php new file mode 100644 index 0000000..6b8ba8f --- /dev/null +++ b/app/Http/Resources/Reminder/ReminderOutbox.php @@ -0,0 +1,41 @@ + + */ +class ReminderOutbox extends JsonResource +{ + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request $request + * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return [ + 'id' => $this->id, + 'reminder_id' => $this->reminder_id, + 'object' => $this->nature, + 'planned_date' => $this->planned_date, + 'title' => $this->reminder->title, + 'description' => $this->reminder->description, + 'frequency_type' => $this->reminder->frequency_type, + 'frequency_number' => $this->reminder->frequency_number, + 'initial_date' => DateHelper::getTimestamp($this->reminder->initial_date), + 'delible' => (bool) $this->reminder->delible, + 'account' => [ + 'id' => $this->account_id, + ], + 'contact' => new ContactShortResource($this->reminder->contact), + 'created_at' => DateHelper::getTimestamp($this->created_at), + 'updated_at' => DateHelper::getTimestamp($this->updated_at), + ]; + } +} diff --git a/app/Http/Resources/Settings/Compliance/Compliance.php b/app/Http/Resources/Settings/Compliance/Compliance.php new file mode 100644 index 0000000..633f7c3 --- /dev/null +++ b/app/Http/Resources/Settings/Compliance/Compliance.php @@ -0,0 +1,32 @@ + + */ +class Compliance extends JsonResource +{ + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request $request + * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return [ + 'id' => $this->id, + 'object' => 'term', + 'term_version' => $this->term_version, + 'term_content' => $this->term_content, + 'privacy_version' => $this->privacy_version, + 'privacy_content' => $this->privacy_content, + 'created_at' => DateHelper::getTimestamp($this->created_at), + 'updated_at' => DateHelper::getTimestamp($this->updated_at), + ]; + } +} diff --git a/app/Http/Resources/Settings/ContactFieldType/ContactFieldType.php b/app/Http/Resources/Settings/ContactFieldType/ContactFieldType.php new file mode 100644 index 0000000..161f22e --- /dev/null +++ b/app/Http/Resources/Settings/ContactFieldType/ContactFieldType.php @@ -0,0 +1,37 @@ + + */ +class ContactFieldType extends JsonResource +{ + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request $request + * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return [ + 'id' => $this->id, + 'uuid' => $this->uuid, + 'object' => 'contactfieldtype', + 'name' => $this->name, + 'fontawesome_icon' => $this->fontawesome_icon, + 'protocol' => $this->protocol, + 'delible' => (bool) $this->delible, + 'type' => $this->type, + 'account' => [ + 'id' => $this->account_id, + ], + 'created_at' => DateHelper::getTimestamp($this->created_at), + 'updated_at' => DateHelper::getTimestamp($this->updated_at), + ]; + } +} diff --git a/app/Http/Resources/Settings/Currency/Currency.php b/app/Http/Resources/Settings/Currency/Currency.php new file mode 100644 index 0000000..57d5763 --- /dev/null +++ b/app/Http/Resources/Settings/Currency/Currency.php @@ -0,0 +1,28 @@ + + */ +class Currency extends JsonResource +{ + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request $request + * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return [ + 'id' => $this->id, + 'object' => 'currency', + 'iso' => $this->iso, + 'name' => $this->name, + 'symbol' => $this->symbol, + ]; + } +} diff --git a/app/Http/Resources/Settings/WebauthnKey/WebauthnKey.php b/app/Http/Resources/Settings/WebauthnKey/WebauthnKey.php new file mode 100644 index 0000000..3dc813f --- /dev/null +++ b/app/Http/Resources/Settings/WebauthnKey/WebauthnKey.php @@ -0,0 +1,30 @@ + + */ +class WebauthnKey extends JsonResource +{ + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request $request + * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return [ + 'id' => $this->id, + 'object' => 'webauthnKey', + 'name' => $this->name, + 'counter' => $this->counter, + 'created_at' => DateHelper::getTimestamp($this->created_at), + 'updated_at' => DateHelper::getTimestamp($this->updated_at), + ]; + } +} diff --git a/app/Http/Resources/Tag/Tag.php b/app/Http/Resources/Tag/Tag.php new file mode 100644 index 0000000..3d0bf31 --- /dev/null +++ b/app/Http/Resources/Tag/Tag.php @@ -0,0 +1,33 @@ + + */ +class Tag extends JsonResource +{ + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request $request + * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return [ + 'id' => $this->id, + 'object' => 'tag', + 'name' => $this->name, + 'name_slug' => $this->name_slug, + 'account' => [ + 'id' => $this->account_id, + ], + 'created_at' => DateHelper::getTimestamp($this->created_at), + 'updated_at' => DateHelper::getTimestamp($this->updated_at), + ]; + } +} diff --git a/app/Http/Resources/Task/Task.php b/app/Http/Resources/Task/Task.php new file mode 100644 index 0000000..df52ac5 --- /dev/null +++ b/app/Http/Resources/Task/Task.php @@ -0,0 +1,38 @@ + + */ +class Task extends JsonResource +{ + /** + * Transform the resource into an array. + * + * @param \Illuminate\Http\Request $request + * @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable + */ + public function toArray($request) + { + return [ + 'id' => $this->id, + 'uuid' => $this->uuid, + 'object' => 'task', + 'title' => $this->title, + 'description' => $this->description, + 'completed' => (bool) $this->completed, + 'completed_at' => DateHelper::getTimestamp($this->completed_at), + 'account' => [ + 'id' => $this->account_id, + ], + 'contact' => new ContactShortResource($this->contact), + 'created_at' => DateHelper::getTimestamp($this->created_at), + 'updated_at' => DateHelper::getTimestamp($this->updated_at), + ]; + } +} diff --git a/app/Http/ViewComposers/CountrySelectViewComposer.php b/app/Http/ViewComposers/CountrySelectViewComposer.php new file mode 100644 index 0000000..2737055 --- /dev/null +++ b/app/Http/ViewComposers/CountrySelectViewComposer.php @@ -0,0 +1,22 @@ +all(); + + $view->with('countries', $countries); + } +} diff --git a/app/Http/ViewComposers/CurrencySelectViewComposer.php b/app/Http/ViewComposers/CurrencySelectViewComposer.php new file mode 100644 index 0000000..86d6021 --- /dev/null +++ b/app/Http/ViewComposers/CurrencySelectViewComposer.php @@ -0,0 +1,21 @@ +get(); + $view->with('currencies', $currencies); + } +} diff --git a/app/Http/ViewComposers/DateSelectViewComposer.php b/app/Http/ViewComposers/DateSelectViewComposer.php new file mode 100644 index 0000000..92f83e8 --- /dev/null +++ b/app/Http/ViewComposers/DateSelectViewComposer.php @@ -0,0 +1,29 @@ +with([ + 'months' => $months, + 'years' => $years, + ]); + } +} diff --git a/app/Http/ViewComposers/InstanceViewComposer.php b/app/Http/ViewComposers/InstanceViewComposer.php new file mode 100644 index 0000000..332ec6a --- /dev/null +++ b/app/Http/ViewComposers/InstanceViewComposer.php @@ -0,0 +1,22 @@ +with('instance', $instance); + } +} diff --git a/app/Interfaces/Hashing.php b/app/Interfaces/Hashing.php new file mode 100644 index 0000000..82e5777 --- /dev/null +++ b/app/Interfaces/Hashing.php @@ -0,0 +1,8 @@ +importJob = $importJob; + $this->behaviour = $behaviour; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $this->importJob->process($this->behaviour); + } +} diff --git a/app/Jobs/AuditLog/LogAccountAudit.php b/app/Jobs/AuditLog/LogAccountAudit.php new file mode 100644 index 0000000..254e059 --- /dev/null +++ b/app/Jobs/AuditLog/LogAccountAudit.php @@ -0,0 +1,44 @@ +auditLog = $auditLog; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + app(LogAccountAction::class)->execute( + $this->auditLog + ); + } +} diff --git a/app/Jobs/Avatars/CreateAvatarsForExistingContacts.php b/app/Jobs/Avatars/CreateAvatarsForExistingContacts.php new file mode 100644 index 0000000..0fd3b50 --- /dev/null +++ b/app/Jobs/Avatars/CreateAvatarsForExistingContacts.php @@ -0,0 +1,63 @@ +orWhere('avatar_default_url', 'not like', 'avatars/%') + ->count(); + + return now()->addSeconds($totalContact / 500); + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $delay = $this->retryUntil(); + + Contact::without(['account', 'avatarPhoto', 'gender']) + ->whereNull('avatar_adorable_url') + ->orWhere('avatar_default_url', 'not like', 'avatars/%') + ->chunk(1000, function ($contacts) use ($delay) { + foreach ($contacts as $contact) { + GetAvatarsFromInternet::dispatch($contact) + ->delay($delay); + GenerateDefaultAvatar::dispatch($contact) + ->delay($delay); + } + $delay = $delay->addMinutes(1); + }); + } +} diff --git a/app/Jobs/Avatars/GenerateDefaultAvatar.php b/app/Jobs/Avatars/GenerateDefaultAvatar.php new file mode 100644 index 0000000..46b8571 --- /dev/null +++ b/app/Jobs/Avatars/GenerateDefaultAvatar.php @@ -0,0 +1,60 @@ +contact = $contact; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + if (StringHelper::isNullOrWhitespace($this->contact->default_avatar_color)) { + $this->contact->setAvatarColor(); + $this->contact->save(); + } + + // generate the default avatar + app(GenerateDefaultAvatarService::class)->execute([ + 'contact_id' => $this->contact->id, + ]); + } +} diff --git a/app/Jobs/Avatars/GetAvatarsFromInternet.php b/app/Jobs/Avatars/GetAvatarsFromInternet.php new file mode 100644 index 0000000..1e8bb7f --- /dev/null +++ b/app/Jobs/Avatars/GetAvatarsFromInternet.php @@ -0,0 +1,54 @@ +contact = $contact; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + // generate the default avatar + app(GetAvatarsFromInternetService::class)->execute([ + 'contact_id' => $this->contact->id, + ]); + } +} diff --git a/app/Jobs/Avatars/MoveContactAvatarToPhotosDirectory.php b/app/Jobs/Avatars/MoveContactAvatarToPhotosDirectory.php new file mode 100644 index 0000000..6f43c9a --- /dev/null +++ b/app/Jobs/Avatars/MoveContactAvatarToPhotosDirectory.php @@ -0,0 +1,196 @@ +contact = $contact; + $this->dryrun = $dryrun; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $this->storage = Storage::disk($this->contact->avatar_location); + + // move avatar to new location + $avatarFileName = $this->moveContactAvatars(); + + if ($this->dryrun) { + return; + } + + // create a Photo object for this avatar + $photo = $this->createPhotoObject($avatarFileName); + + // associate the Photo object to the contact + $this->associatePhotoAsAvatar($photo); + + // delete original avatar + $this->deleteOriginalAvatar($avatarFileName); + + // delete thumbnails of avatars + $this->deleteThumbnails(); + } + + /** + * @return string|null + */ + private function moveContactAvatars(): ?string + { + Event::dispatch(new MoveAvatarEvent($this->contact)); + + $newStorage = Storage::disk(config('filesystems.default')); + $avatarFileName = $this->getAvatarFileName(); + + // $avatarFileName has the format `avatars/XXX.jpg`. We need to remove + // the `avatars/` string to store the new file. + $newAvatarFilename = str_replace('avatars/', 'photos/', $avatarFileName); + + if ($newStorage->exists($newAvatarFilename)) { + return null; + } + + if (! $this->dryrun) { + $avatarFile = $this->storage->get($avatarFileName); + $newStorage->put($newAvatarFilename, $avatarFile, config('filesystems.default_visibility')); + + $this->contact->avatar_location = config('filesystems.default'); + $this->contact->save(); + } + + return $avatarFileName; + } + + /** + * @param string|null $avatarFileName + * @return Photo|null + */ + private function createPhotoObject($avatarFileName): ?Photo + { + if (is_null($avatarFileName)) { + return null; + } + + $newAvatarFilename = str_replace('avatars/', '', $avatarFileName); + + $photo = new Photo; + $photo->account_id = $this->contact->account_id; + $photo->original_filename = $newAvatarFilename; + $photo->new_filename = 'photos/'.$newAvatarFilename; + $photo->filesize = Storage::disk($this->contact->avatar_location)->size('/photos/'.$newAvatarFilename); + $photo->mime_type = 'adfad'; + $photo->save(); + + return $photo; + } + + private function associatePhotoAsAvatar($photo) + { + if (is_null($photo)) { + return; + } + + $data = [ + 'account_id' => $this->contact->account_id, + 'contact_id' => $this->contact->id, + 'source' => 'photo', + 'photo_id' => $photo->id, + ]; + app(UpdateAvatar::class)->execute($data); + } + + private function deleteThumbnails() + { + try { + $smallThumbnail = $this->getAvatarFileName(110); + $this->storage->delete($smallThumbnail); + } catch (FileNotFoundException $e) { + // ignore + } + + try { + $bigThumbnail = $this->getAvatarFileName(174); + $this->storage->delete($bigThumbnail); + } catch (FileNotFoundException $e) { + // ignore + } + } + + private function deleteOriginalAvatar($avatarFileName) + { + $this->storage->delete($avatarFileName); + } + + private function getAvatarFileName($size = null) + { + $filename = pathinfo($this->contact->avatar_file_name, PATHINFO_FILENAME); + $extension = pathinfo($this->contact->avatar_file_name, PATHINFO_EXTENSION); + + $avatarFileName = 'avatars/'.$filename.'.'.$extension; + if (! is_null($size)) { + $avatarFileName = 'avatars/'.$filename.'_'.$size.'.'.$extension; + } + + if (! $this->fileExists($avatarFileName)) { + throw new FileNotFoundException($avatarFileName); + } + + return $avatarFileName; + } + + private function fileExists($avatarFileName): bool + { + return $this->storage->exists($avatarFileName); + } +} diff --git a/app/Jobs/Avatars/UpdateAllGravatars.php b/app/Jobs/Avatars/UpdateAllGravatars.php new file mode 100644 index 0000000..6214bf0 --- /dev/null +++ b/app/Jobs/Avatars/UpdateAllGravatars.php @@ -0,0 +1,31 @@ +orWhere('avatar_gravatar_url', '<>', '') + ->active() + ->get(); + + foreach ($contacts as $contact) { + UpdateGravatar::dispatch($contact); + } + } +} diff --git a/app/Jobs/Avatars/UpdateGravatar.php b/app/Jobs/Avatars/UpdateGravatar.php new file mode 100644 index 0000000..7f98309 --- /dev/null +++ b/app/Jobs/Avatars/UpdateGravatar.php @@ -0,0 +1,54 @@ +contact = $contact; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + // generate the default avatar + app(GetGravatar::class)->execute([ + 'contact_id' => $this->contact->id, + ]); + } +} diff --git a/app/Jobs/Dav/DeleteMultipleVCard.php b/app/Jobs/Dav/DeleteMultipleVCard.php new file mode 100644 index 0000000..330787a --- /dev/null +++ b/app/Jobs/Dav/DeleteMultipleVCard.php @@ -0,0 +1,72 @@ +subscription = $subscription->withoutRelations(); + $this->hrefs = $hrefs; + } + + /** + * Update the Last Consulted At field for the given contact. + * + * @return void + */ + public function handle(): void + { + if (! $this->batching()) { + return; // @codeCoverageIgnore + } + + $batch = $this->batch(); + + collect($this->hrefs) + ->each(function ($href) use ($batch) { + $this->deleteVCard($href, $batch); + }); + } + + /** + * Delete the contact. + * + * @param string $href + * @param \Illuminate\Bus\Batch $batch + * @return void + */ + private function deleteVCard(string $href, Batch $batch): void + { + $batch->add([ + new DeleteVCard($this->subscription, $href), + ]); + } +} diff --git a/app/Jobs/Dav/DeleteVCard.php b/app/Jobs/Dav/DeleteVCard.php new file mode 100644 index 0000000..b2588f2 --- /dev/null +++ b/app/Jobs/Dav/DeleteVCard.php @@ -0,0 +1,56 @@ +subscription = $subscription->withoutRelations(); + $this->uri = $uri; + } + + /** + * Send Delete contact. + * + * @return void + */ + public function handle(): void + { + if (! $this->batching()) { + return; + } + + Log::info(__CLASS__.' '.$this->uri); + + $this->subscription->getClient() + ->request('DELETE', $this->uri); + } +} diff --git a/app/Jobs/Dav/GetMultipleVCard.php b/app/Jobs/Dav/GetMultipleVCard.php new file mode 100644 index 0000000..a4bbeda --- /dev/null +++ b/app/Jobs/Dav/GetMultipleVCard.php @@ -0,0 +1,108 @@ +subscription = $subscription->withoutRelations(); + $this->hrefs = $hrefs; + } + + /** + * Update the Last Consulted At field for the given contact. + * + * @return void + */ + public function handle(): void + { + if (! $this->batching()) { + return; // @codeCoverageIgnore + } + + $datas = $this->subscription->getClient() + ->addressbookMultiget([ + '{DAV:}getetag', + $this->getAddressDataProperty(), + ], $this->hrefs); + + collect($datas) + ->filter(function (array $contact): bool { + return isset($contact[200]); + }) + ->each(function (array $contact, $href) { + $this->updateVCard($contact, $href); + }); + } + + /** + * Update the contact. + * + * @param array $contact + * @param string $href + * @return void + */ + private function updateVCard(array $contact, $href): void + { + $card = Arr::get($contact, '200.{'.CardDAVPlugin::NS_CARDDAV.'}address-data'); + + if ($card !== null) { + $dto = new ContactUpdateDto($href, Arr::get($contact, '200.{DAV:}getetag'), $card); + + if (($batch = $this->batch()) !== null) { + $batch->add([ + new UpdateVCard($this->subscription->user, $this->subscription->addressbook->name, $dto), + ]); + } + } + } + + /** + * Get data for address-data property. + * + * @return array + */ + private function getAddressDataProperty(): array + { + $addressDataAttributes = Arr::get($this->subscription->capabilities, 'addressData', [ + 'content-type' => 'text/vcard', + 'version' => '4.0', + ]); + + return [ + 'name' => '{'.CardDAVPlugin::NS_CARDDAV.'}address-data', + 'value' => null, + 'attributes' => $addressDataAttributes, + ]; + } +} diff --git a/app/Jobs/Dav/GetVCard.php b/app/Jobs/Dav/GetVCard.php new file mode 100644 index 0000000..577416b --- /dev/null +++ b/app/Jobs/Dav/GetVCard.php @@ -0,0 +1,71 @@ +subscription = $subscription->withoutRelations(); + $this->contact = $contact; + } + + /** + * Update the Last Consulted At field for the given contact. + * + * @return void + */ + public function handle(): void + { + if (! $this->batching()) { + return; + } + + Log::info(__CLASS__.' '.$this->contact->uri); + + $response = $this->subscription->getClient() + ->request('GET', $this->contact->uri); + + $this->chainUpdateVCard($response->body()); + } + + private function chainUpdateVCard(string $card): void + { + $dto = new ContactUpdateDto($this->contact->uri, $this->contact->etag, $card); + + if (($batch = $this->batch()) !== null) { + $batch->add([ + new UpdateVCard($this->subscription->user, $this->subscription->addressbook->name, $dto), + ]); + } + } +} diff --git a/app/Jobs/Dav/PushVCard.php b/app/Jobs/Dav/PushVCard.php new file mode 100644 index 0000000..e249dbd --- /dev/null +++ b/app/Jobs/Dav/PushVCard.php @@ -0,0 +1,76 @@ +subscription = $subscription->withoutRelations(); + $this->contact = $contact; + } + + /** + * Update the Last Consulted At field for the given contact. + * + * @return void + */ + public function handle(): void + { + if (! $this->batching()) { + return; + } + + Log::info(__CLASS__.' '.$this->contact->uri); + + $headers = []; + + switch ($this->contact->mode) { + case ContactPushDto::MODE_MATCH_ETAG: + $headers['If-Match'] = $this->contact->etag; + break; + case ContactPushDto::MODE_MATCH_ANY: + $headers['If-Match'] = '*'; + break; + } + + $response = $this->subscription->getClient() + ->request('PUT', $this->contact->uri, $this->contact->card, $headers); + + $etag = $response->header('Etag'); + + $contact = Contact::where('account_id', $this->subscription->account_id) + ->findOrFail($this->contact->contactId); + $contact->distant_etag = empty($etag) ? null : $etag; + $contact->save(); + } +} diff --git a/app/Jobs/Dav/UpdateVCard.php b/app/Jobs/Dav/UpdateVCard.php new file mode 100644 index 0000000..1cbf6c9 --- /dev/null +++ b/app/Jobs/Dav/UpdateVCard.php @@ -0,0 +1,127 @@ +user = $user->withoutRelations(); + $this->addressBookName = $addressBookName; + $this->contact = $contact; + } + + /** + * Update the Last Consulted At field for the given contact. + * + * @return void + */ + public function handle(): void + { + if (! $this->batching()) { + return; + } + + $this->withLocale($this->user->preferredLocale(), function () { + $newtag = $this->updateCard($this->addressBookName, $this->contact->uri, $this->contact->card); + + if (! is_null($this->contact->etag) && $newtag !== $this->contact->etag) { + Log::warning(__CLASS__.' '.__FUNCTION__.' wrong etag when updating contact. Expected '.$this->contact->etag.', get '.$newtag, [ + 'contacturl' => $this->contact->uri, + 'carddata' => $this->contact->card, + ]); + } + }); + } + + /** + * Update the contact with the carddata. + * + * @param mixed $addressBookId + * @param string $cardUri + * @param string $cardData + * @return string|null + */ + private function updateCard($addressBookId, $cardUri, $cardData): ?string + { + $backend = app(CardDAVBackend::class)->init($this->user); + + $contact_id = null; + if ($cardUri) { + $contactObject = $backend->getObject($addressBookId, $cardUri); + + if ($contactObject) { + $contact_id = $contactObject->id; + } + } + + try { + $result = app(ImportVCard::class) + ->execute([ + 'account_id' => $this->user->account_id, + 'user_id' => $this->user->id, + 'contact_id' => $contact_id, + 'entry' => $cardData, + 'etag' => $this->contact->etag, + 'behaviour' => ImportVCard::BEHAVIOUR_REPLACE, + 'addressBookName' => $addressBookId === $backend->backendUri() ? null : $addressBookId, + ]); + + if (! Arr::has($result, 'error')) { + return app(GetEtag::class)->execute([ + 'account_id' => $this->user->account_id, + 'contact_id' => $result['contact_id'], + ]); + } + } catch (\Exception $e) { + Log::error(__CLASS__.' '.__FUNCTION__.': '.$e->getMessage(), [ + 'contacturl' => $cardUri, + 'contact_id' => $contact_id, + 'carddata' => $cardData, + $e, + ]); + throw $e; + } + + return null; + } +} diff --git a/app/Jobs/ExportAccount.php b/app/Jobs/ExportAccount.php new file mode 100644 index 0000000..a1518de --- /dev/null +++ b/app/Jobs/ExportAccount.php @@ -0,0 +1,97 @@ +status = ExportJob::EXPORT_TODO; + $exportJob->save(); + $this->exportJob = $exportJob->withoutRelations(); + $this->path = $path ?? 'exports'; + } + + /** + * Execute the job. + */ + public function handle() + { + $this->exportJob->start(); + + $tempFileName = ''; + $handler = $this->exportJob->type === ExportJob::JSON ? + app(JsonExportAccount::class) : + app(SqlExportAccount::class); + try { + $tempFileName = $handler->execute([ + 'account_id' => $this->exportJob->account_id, + 'user_id' => $this->exportJob->user_id, + ]); + + // get the temp file that we just created + $tempFilePath = StorageHelper::disk('local')->path($tempFileName); + + // move the file to the public storage + $file = StorageHelper::disk(config('filesystems.default')) + ->putFileAs($this->path, new File($tempFilePath), basename($tempFileName)); + + $this->exportJob->location = config('filesystems.default'); + $this->exportJob->filename = $file; + + $this->exportJob->end(); + } catch (Throwable $e) { + $this->fail($e); + } finally { + // delete old file from temp folder + $storage = Storage::disk('local'); + if ($storage->exists($tempFileName)) { + $storage->delete($tempFileName); + } + } + } + + /** + * Handle a job failure. + * + * @param \Throwable $exception + */ + public function failed(Throwable $exception): void + { + $this->exportJob->status = ExportJob::EXPORT_FAILED; + $this->exportJob->save(); + } +} diff --git a/app/Jobs/ExportAllAsSQL.php b/app/Jobs/ExportAllAsSQL.php new file mode 100644 index 0000000..6f45ce3 --- /dev/null +++ b/app/Jobs/ExportAllAsSQL.php @@ -0,0 +1,69 @@ +table_name; + + $tableData = DB::table($tableName)->get(); + + // Looping over the rows + foreach ($tableData as $data) { + $newSQLLine = 'INSERT INTO '.$tableName.' ('; + $tableValues = []; + + // Looping over the column names + $tableColumnNames = []; + foreach ($data as $columnName => $value) { + array_push($tableColumnNames, $columnName); + } + + $newSQLLine .= implode(',', $tableColumnNames).') VALUES ('; + + // Looping over the values + foreach ($data as $columnName => $value) { + if (is_null($value)) { + $value = 'NULL'; + } elseif (! is_numeric($value)) { + $value = "'".addslashes($value)."'"; + } + + array_push($tableValues, $value); + } + + $newSQLLine .= implode(',', $tableValues).');'.PHP_EOL; + $sql .= $newSQLLine; + } + } + $filename = 'export-all-'.time().'.sql'; + Storage::disk('local')->put($filename, $sql); + + return $filename; + } +} diff --git a/app/Jobs/GetGPSCoordinate.php b/app/Jobs/GetGPSCoordinate.php new file mode 100644 index 0000000..abfda6c --- /dev/null +++ b/app/Jobs/GetGPSCoordinate.php @@ -0,0 +1,81 @@ +place = $place->withoutRelations(); + } + + /** + * Get the middleware the job should pass through. + * + * @return array + */ + public function middleware() + { + return [ + new RateLimited('GPSCoordinate'), + ]; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + if (($batch = $this->batch()) !== null && $batch->cancelled()) { + return; + } + + try { + app(GetGPSCoordinateService::class)->execute([ + 'account_id' => $this->place->account_id, + 'place_id' => $this->place->id, + ]); + } catch (RateLimitedSecondException $e) { + $this->release(15); + } + } +} diff --git a/app/Jobs/GetWeatherInformation.php b/app/Jobs/GetWeatherInformation.php new file mode 100644 index 0000000..bd69b70 --- /dev/null +++ b/app/Jobs/GetWeatherInformation.php @@ -0,0 +1,67 @@ +place = $place->withoutRelations(); + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + if (! $this->batching()) { + return; + } + + if (is_null($this->place->latitude)) { + $this->fail(new NoCoordinatesException()); + } else { + app(GetWeatherInformationService::class)->execute([ + 'account_id' => $this->place->account_id, + 'place_id' => $this->place->id, + ]); + } + } +} diff --git a/app/Jobs/Job.php b/app/Jobs/Job.php new file mode 100644 index 0000000..55ece29 --- /dev/null +++ b/app/Jobs/Job.php @@ -0,0 +1,21 @@ +reminderOutbox = $reminderOutbox; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + // prepare the notification to be sent + $message = $this->getMessage(); + + if (! is_null($message)) { + $this->sendNotification($message); + $this->scheduleNextReminder(); + } + + // delete the reminder outbox + $this->reminderOutbox->delete(); + } + + /** + * Send the notification to this user. + * + * @param MailNotification $message + * @return void + */ + private function sendNotification(MailNotification $message): void + { + if ($this->reminderOutbox->reminder->contact !== null) { + $account = $this->reminderOutbox->user->account; + $hasLimitations = AccountHelper::hasLimitations($account); + if (! $hasLimitations) { + Notification::send($this->reminderOutbox->user, $message); + } + } + } + + /** + * Schedule the next reminder for this user. + * + * @return void + */ + private function scheduleNextReminder(): void + { + /** @var \App\Models\Contact\Reminder */ + $reminder = $this->reminderOutbox->reminder; + + if ($reminder->frequency_type == 'one_time') { + $reminder->inactive = true; + $reminder->save(); + } else { + $reminder->schedule($this->reminderOutbox->user); + } + } + + /** + * Get message to send. + * + * @return MailNotification|null + */ + private function getMessage(): ?MailNotification + { + switch ($this->reminderOutbox->nature) { + case 'reminder': + return new UserReminded($this->reminderOutbox->reminder); + case 'notification': + return new UserNotified($this->reminderOutbox->reminder, $this->reminderOutbox->notification_number_days_before); + default: + return null; + } + } +} diff --git a/app/Jobs/ResizeAvatars.php b/app/Jobs/ResizeAvatars.php new file mode 100644 index 0000000..0fad5c8 --- /dev/null +++ b/app/Jobs/ResizeAvatars.php @@ -0,0 +1,68 @@ +contact = $contact; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + if (! $this->contact->has_avatar) { + return; + } + + $storage = Storage::disk($this->contact->avatar_location); + if (! $storage->exists($this->contact->avatar_file_name)) { + return; + } + + try { + $avatarFile = $storage->get($this->contact->avatar_file_name); + $filename = pathinfo($this->contact->avatar_file_name, PATHINFO_FILENAME); + $extension = pathinfo($this->contact->avatar_file_name, PATHINFO_EXTENSION); + } catch (FileNotFoundException $e) { + return; + } + + $this->resize($avatarFile, $filename, $extension, $storage, 110); + $this->resize($avatarFile, $filename, $extension, $storage, 174); + } + + private function resize($avatarFile, $filename, $extension, $storage, $size) + { + $avatarFileName = 'avatars/'.$filename.'_'.$size.'.'.$extension; + + $avatar = Image::make($avatarFile); + $avatar->fit($size); + + $storage->put($avatarFileName, (string) $avatar->stream(), config('filesystems.default_visibility')); + } +} diff --git a/app/Jobs/SendNewUserAlert.php b/app/Jobs/SendNewUserAlert.php new file mode 100644 index 0000000..e84f179 --- /dev/null +++ b/app/Jobs/SendNewUserAlert.php @@ -0,0 +1,43 @@ +user = $user; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $email = config('monica.email_new_user_notification'); + if (! empty($email)) { + Notification::route('mail', $email) + ->notify(new NewUserAlert($this->user)); + } + } +} diff --git a/app/Jobs/SendVerifyEmail.php b/app/Jobs/SendVerifyEmail.php new file mode 100644 index 0000000..5697c8f --- /dev/null +++ b/app/Jobs/SendVerifyEmail.php @@ -0,0 +1,38 @@ +user = $user; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $this->user->notify(new VerifyEmail()); + } +} diff --git a/app/Jobs/ServiceQueue.php b/app/Jobs/ServiceQueue.php new file mode 100644 index 0000000..8b2bdf1 --- /dev/null +++ b/app/Jobs/ServiceQueue.php @@ -0,0 +1,70 @@ +service = $service; + $this->data = $data; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle(): void + { + $this->service->handle($this->data); + } + + /** + * Handle a job failure. + * + * @param \Throwable $exception + * @return void + */ + public function failed(Throwable $exception): void + { + $this->service->failed($exception); + } +} diff --git a/app/Jobs/StayInTouch/ScheduleStayInTouch.php b/app/Jobs/StayInTouch/ScheduleStayInTouch.php new file mode 100644 index 0000000..20b2249 --- /dev/null +++ b/app/Jobs/StayInTouch/ScheduleStayInTouch.php @@ -0,0 +1,61 @@ +contact = $contact; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + $account = $this->contact->account; + + $users = []; + foreach ($account->users as $user) { + if ($user->isTheRightTimeToBeReminded($this->contact->stay_in_touch_trigger_date) + && ! AccountHelper::hasLimitations($account)) { + array_push($users, $user); + } + } + + if (count($users) > 0) { + NotificationFacade::send($users, new StayInTouchEmail($this->contact)); + $this->contact->setStayInTouchTriggerDate($this->contact->stay_in_touch_frequency, $this->contact->stay_in_touch_trigger_date); + + return; + } + + $now = now(); + while ($this->contact->stay_in_touch_trigger_date < $now) { + // If stay in touch was missed, we reschedule it. + $this->contact->setStayInTouchTriggerDate($this->contact->stay_in_touch_frequency, $this->contact->stay_in_touch_trigger_date); + } + } +} diff --git a/app/Jobs/SynchronizeAddressBooks.php b/app/Jobs/SynchronizeAddressBooks.php new file mode 100644 index 0000000..12c0449 --- /dev/null +++ b/app/Jobs/SynchronizeAddressBooks.php @@ -0,0 +1,59 @@ +subscription = $subscription; + $this->force = $force; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + try { + app(SynchronizeAddressBook::class)->execute([ + 'account_id' => $this->subscription->account_id, + 'addressbook_subscription_id' => $this->subscription->id, + 'force' => $this->force, + ]); + } catch (\Exception $e) { + Log::error(__CLASS__.' '.__FUNCTION__.':'.$e->getMessage(), [$e]); + } + $this->subscription->last_synchronized_at = now(); + $this->subscription->save(); + } +} diff --git a/app/Jobs/UpdateLastConsultedDate.php b/app/Jobs/UpdateLastConsultedDate.php new file mode 100644 index 0000000..91b5800 --- /dev/null +++ b/app/Jobs/UpdateLastConsultedDate.php @@ -0,0 +1,45 @@ +contact = $contact; + } + + /** + * Update the Last Consulted At field for the given contact. + * + * @return void + */ + public function handle(): void + { + $timestamps = $this->contact->timestamps; + $this->contact->timestamps = false; + + $this->contact->last_consulted_at = now(); + $this->contact->number_of_views = $this->contact->number_of_views + 1; + + $this->contact->save(); + + $this->contact->timestamps = $timestamps; + } +} diff --git a/app/Listeners/LoginListener.php b/app/Listeners/LoginListener.php new file mode 100644 index 0000000..afe1e0d --- /dev/null +++ b/app/Listeners/LoginListener.php @@ -0,0 +1,111 @@ +listen( + \Illuminate\Auth\Events\Login::class, + '\App\Listeners\LoginListener@onLogin' + ); + $events->listen( + \PragmaRX\Google2FALaravel\Events\LoginSucceeded::class, + '\App\Listeners\LoginListener@onGoogle2faLogin' + ); + $events->listen( + \LaravelWebauthn\Events\WebauthnLogin::class, + '\App\Listeners\LoginListener@onWebauthnLogin' + ); + $events->listen( + \App\Events\RecoveryLogin::class, + '\App\Listeners\LoginListener@onRecoveryLogin' + ); + } + + /** + * Handle the Illuminate login event. + * + * @param Login $event + * @return void + */ + public function onLogin(Login $event) + { + if (Auth::viaRemember()) { + $this->registerGoogle2fa($event->user); + $this->registerWebauthn($event->user); + } + } + + /** + * Handle the Google2fa Login event. + * + * @param LoginSucceeded $event + * @return void + */ + public function onGoogle2faLogin(LoginSucceeded $event) + { + $this->registerWebauthn($event->user); + } + + /** + * Handle the Webauthn login event. + * + * @param WebauthnLogin $event + */ + public function onWebauthnLogin(WebauthnLogin $event) + { + $this->registerGoogle2fa($event->user); + } + + /** + * Handle the recovery login event. + * + * @param RecoveryLogin $event + * @return void + */ + public function onRecoveryLogin(RecoveryLogin $event) + { + $this->registerGoogle2fa($event->user); + $this->registerWebauthn($event->user); + } + + /** + * Force register Google2fa login. + * + * @param User $user + */ + private function registerGoogle2fa(User $user) + { + if (config('google2fa.enabled') && ! empty($user->google2fa_secret)) { + Validate2faController::loginCallback(); + } + } + + /** + * Force register Webauthn login. + * + * @param User $user + */ + private function registerWebauthn(User $user) + { + if (Webauthn::enabled($user)) { + Webauthn::forceAuthenticate(); + } + } +} diff --git a/app/Listeners/LogoutUserDevices.php b/app/Listeners/LogoutUserDevices.php new file mode 100644 index 0000000..501208e --- /dev/null +++ b/app/Listeners/LogoutUserDevices.php @@ -0,0 +1,98 @@ +user instanceof User) { + $this->logoutOtherDevices($event->user); + + $this->deleteOtherSessionRecords($event->user); + } + } + + /** + * Invalidate other sessions for the current user. + * + * The application must be using the AuthenticateSession middleware. + * + * @param User $user + * + * @throws \Illuminate\Auth\AuthenticationException + */ + public function logoutOtherDevices($user) + { + $guard = $this->guard(); + $cookieJar = $guard->getCookieJar(); + + if ($this->recaller($guard) || + $cookieJar->hasQueued($guard->getRecallerName())) { + $cookieJar->queue($cookieJar->forever($guard->getRecallerName(), + $user->getAuthIdentifier().'|'.$user->getRememberToken().'|'.$user->getAuthPassword() + )); + } + } + + /** + * Get the guard. + * + * @return SessionGuard + */ + protected function guard(): SessionGuard + { + $guard = Auth::guard('web'); + if (! $guard instanceof SessionGuard) { + throw new \LogicException('guard is not a SessionGuard kind'); + } + + return $guard; + } + + /** + * Get the decrypted recaller cookie for the request. + * + * @param SessionGuard $guard + * @return \Illuminate\Auth\Recaller|null + */ + protected function recaller($guard): ?Recaller + { + if ($recaller = request()->cookies->get($guard->getRecallerName())) { + return new Recaller($recaller); + } + + return null; + } + + /** + * Delete the other browser session records from storage. + * + * @param User $user + * @return void + */ + protected function deleteOtherSessionRecords($user) + { + if (config('session.driver') !== 'database') { + return; + } + + DB::connection(config('session.connection'))->table(config('session.table', 'sessions')) + ->where('user_id', $user->id) + ->where('id', '!=', request()->session()->getId()) + ->delete(); + } +} diff --git a/app/Models/Account/Account.php b/app/Models/Account/Account.php new file mode 100644 index 0000000..5dc12a7 --- /dev/null +++ b/app/Models/Account/Account.php @@ -0,0 +1,751 @@ +|bool + */ + protected $guarded = ['id']; + + /** + * The attributes that are mass assignable. + * + * @var array + */ + protected $fillable = [ + 'number_of_invitations_sent', + 'api_key', + 'default_time_reminder_is_sent', + 'default_gender_id', + ]; + + /** + * The attributes that should be cast to native types. + * + * @var array + */ + protected $casts = [ + 'has_access_to_paid_version_for_free' => 'boolean', + ]; + + /** + * Get the activity records associated with the account. + * + * @return HasMany + */ + public function activities() + { + return $this->hasMany(Activity::class); + } + + /** + * Get the contact records associated with the account. + * + * @return HasMany + */ + public function allContacts() + { + return $this->hasMany(Contact::class); + } + + /** + * Get the addressBook's contacts. + * + * @param string|null $addressBookName + * @return HasMany + */ + public function contacts(string $addressBookName = null) + { + $contacts = $this->allContacts(); + + return $addressBookName + ? $contacts->addressBook($this->id, $addressBookName) + : $contacts->addressBook(); + } + + /** + * Get the invitations associated with the account. + * + * @return HasMany + */ + public function invitations() + { + return $this->hasMany(Invitation::class); + } + + /** + * Get the debt records associated with the account. + * + * @return HasMany + */ + public function debts() + { + return $this->hasMany(Debt::class); + } + + /** + * Get the gift records associated with the account. + * + * @return HasMany + */ + public function gifts() + { + return $this->hasMany(Gift::class); + } + + /** + * Get the note records associated with the account. + * + * @return HasMany + */ + public function notes() + { + return $this->hasMany(Note::class)->orderBy('created_at', 'desc'); + } + + /** + * Get the reminder records associated with the account. + * + * @return HasMany + */ + public function reminders() + { + return $this->hasMany(Reminder::class); + } + + /** + * Get the reminder outboxes records associated with the account. + * + * @return HasMany + */ + public function reminderOutboxes() + { + return $this->hasMany(ReminderOutbox::class); + } + + /** + * Get the task records associated with the account. + * + * @return HasMany + */ + public function tasks() + { + return $this->hasMany(Task::class); + } + + /** + * Get the user records associated with the account. + * + * @return HasMany + */ + public function users() + { + return $this->hasMany(User::class); + } + + /** + * Get the relationship records associated with the account. + * + * @return HasMany + */ + public function relationships() + { + return $this->hasMany(Relationship::class); + } + + /** + * Get the activity statistics record associated with the account. + * + * @return HasMany + */ + public function activityStatistics() + { + return $this->hasMany(ActivityStatistic::class); + } + + /** + * Get the activity type records associated with the account. + * + * @return HasMany + */ + public function activityTypes() + { + return $this->hasMany(ActivityType::class); + } + + /** + * Get the activity type category records associated with the account. + * + * @return HasMany + */ + public function activityTypeCategories() + { + return $this->hasMany(ActivityTypeCategory::class); + } + + /** + * Get the task records associated with the account. + * + * @return HasMany + */ + public function entries() + { + return $this->hasMany(Entry::class); + } + + /** + * Get the import jobs records associated with the account. + * + * @return HasMany + */ + public function importjobs() + { + return $this->hasMany(ImportJob::class)->orderBy('created_at', 'desc'); + } + + /** + * Get the import job reports records associated with the account. + * + * @return HasMany + */ + public function importJobReports() + { + return $this->hasMany(ImportJobReport::class); + } + + /** + * Get the tags records associated with the account. + * + * @return HasMany + */ + public function tags() + { + return $this->hasMany(Tag::class)->orderBy('name', 'asc'); + } + + /** + * Get the calls records associated with the account. + * + * @return HasMany + */ + public function calls() + { + return $this->hasMany(Call::class)->orderBy('called_at', 'desc'); + } + + /** + * Get the Contact Field types records associated with the account. + * + * @return HasMany + */ + public function contactFieldTypes() + { + return $this->hasMany(ContactFieldType::class); + } + + /** + * Get the Contact Field records associated with the contact. + * + * @return HasMany + */ + public function contactFields() + { + return $this->hasMany(ContactField::class); + } + + /** + * Get the Journal Entries records associated with the account. + * + * @return HasMany + */ + public function journalEntries() + { + return $this->hasMany(JournalEntry::class)->orderBy('date', 'desc'); + } + + /** + * Get the special dates records associated with the account. + * + * @return HasMany + */ + public function specialDates() + { + return $this->hasMany(SpecialDate::class); + } + + /** + * Get the Days records associated with the account. + * + * @return HasMany + */ + public function days() + { + return $this->hasMany(Day::class); + } + + /** + * Get the Genders records associated with the account. + * + * @return HasMany + */ + public function genders() + { + return $this->hasMany(Gender::class); + } + + /** + * Get the Reminder Rules records associated with the account. + * + * @return HasMany + */ + public function reminderRules() + { + return $this->hasMany(ReminderRule::class); + } + + /** + * Get the relationship types records associated with the account. + * + * @return HasMany + */ + public function relationshipTypes() + { + return $this->hasMany(RelationshipType::class); + } + + /** + * Get the relationship type groups records associated with the account. + * + * @return HasMany + */ + public function relationshipTypeGroups() + { + return $this->hasMany(RelationshipTypeGroup::class); + } + + /** + * Get the modules records associated with the account. + * + * @return HasMany + */ + public function modules() + { + return $this->hasMany(Module::class); + } + + /** + * Get the Conversation records associated with the account. + * + * @return HasMany + */ + public function conversations() + { + return $this->hasMany(Conversation::class); + } + + /** + * Get the Message records associated with the account. + * + * @return HasMany + */ + public function messages() + { + return $this->hasMany(Message::class); + } + + /** + * Get the Document records associated with the account. + * + * @return HasMany + */ + public function documents() + { + return $this->hasMany(Document::class); + } + + /** + * Get the Life Event Category records associated with the account. + * + * @return HasMany + */ + public function lifeEventCategories() + { + return $this->hasMany(LifeEventCategory::class); + } + + /** + * Get the Life Event Type records associated with the account. + * + * @return HasMany + */ + public function lifeEventTypes() + { + return $this->hasMany(LifeEventType::class); + } + + /** + * Get the Life Event records associated with the account. + * + * @return HasMany + */ + public function lifeEvents() + { + return $this->hasMany(LifeEvent::class); + } + + /** + * Get the Photos records associated with the account. + * + * @return HasMany + */ + public function photos() + { + return $this->hasMany(Photo::class); + } + + /** + * Get the Weather records associated with the account. + * + * @return HasMany + */ + public function weathers() + { + return $this->hasMany(Weather::class); + } + + /** + * Get the Places records associated with the account. + * + * @return HasMany + */ + public function places() + { + return $this->hasMany(Place::class); + } + + /** + * Get the Addresses records associated with the account. + * + * @return HasMany + */ + public function addresses() + { + return $this->hasMany(Address::class); + } + + /** + * Get the Address Books records associated with the account. + * + * @return HasMany + */ + public function addressBooks() + { + return $this->hasMany(AddressBook::class); + } + + /** + * Get the Address Book Subscriptions records associated with the account. + * + * @return HasMany + */ + public function addressBookSubscriptions() + { + return $this->hasMany(AddressBookSubscription::class); + } + + /** + * Get the Company records associated with the account. + * + * @return HasMany + */ + public function companies() + { + return $this->hasMany(Company::class); + } + + /** + * Get the Occupation records associated with the account. + * + * @return HasMany + */ + public function occupations() + { + return $this->hasMany(Occupation::class); + } + + /** + * * Get the Audit log records associated with the account. + * + * @return HasMany + */ + public function auditLogs() + { + return $this->hasMany(AuditLog::class); + } + + /** + * Populates the Activity Type table right after an account is + * created. + */ + public function populateActivityTypeTable() + { + $defaultActivityTypeCategories = DB::table('default_activity_type_categories')->get(); + + foreach ($defaultActivityTypeCategories as $defaultActivityTypeCategory) { + $activityTypeCategoryId = DB::table('activity_type_categories')->insertGetId([ + 'account_id' => $this->id, + 'translation_key' => $defaultActivityTypeCategory->translation_key, + ]); + + $defaultActivityTypes = DB::table('default_activity_types') + ->where('default_activity_type_category_id', $defaultActivityTypeCategory->id) + ->get(); + + foreach ($defaultActivityTypes as $defaultActivityType) { + DB::table('activity_types')->insert([ + 'account_id' => $this->id, + 'activity_type_category_id' => $activityTypeCategoryId, + 'translation_key' => $defaultActivityType->translation_key, + ]); + } + } + } + + /** + * Populates the default genders in a new account. + * + * @return void + */ + public function populateDefaultGendersTable() + { + Gender::create(['type' => Gender::MALE, 'name' => trans('app.gender_male'), 'account_id' => $this->id]); + Gender::create(['type' => Gender::FEMALE, 'name' => trans('app.gender_female'), 'account_id' => $this->id]); + Gender::create(['type' => Gender::OTHER, 'name' => trans('app.gender_none'), 'account_id' => $this->id]); + } + + /** + * Populates the default reminder rules in a new account. + * + * @return void + */ + public function populateDefaultReminderRulesTable() + { + ReminderRule::create(['number_of_days_before' => 7, 'account_id' => $this->id, 'active' => 1]); + ReminderRule::create(['number_of_days_before' => 30, 'account_id' => $this->id, 'active' => 1]); + } + + /** + * Populates the default relationship types in a new account. + * + * @return void + */ + public function populateRelationshipTypeGroupsTable($ignoreTableAlreadyMigrated = false) + { + $defaultRelationshipTypeGroups = DB::table('default_relationship_type_groups')->get(); + foreach ($defaultRelationshipTypeGroups as $defaultRelationshipTypeGroup) { + if (! $ignoreTableAlreadyMigrated || $defaultRelationshipTypeGroup->migrated == 0) { + DB::table('relationship_type_groups')->insert([ + 'account_id' => $this->id, + 'name' => $defaultRelationshipTypeGroup->name, + 'delible' => $defaultRelationshipTypeGroup->delible, + ]); + } + } + } + + /** + * Populate the relationship types table based on the default ones. + * + * @return void + */ + public function populateRelationshipTypesTable($migrateOnlyNewTypes = false) + { + if ($migrateOnlyNewTypes) { + $defaultRelationshipTypes = DB::table('default_relationship_types')->where('migrated', 0)->get(); + } else { + $defaultRelationshipTypes = DB::table('default_relationship_types')->get(); + } + + foreach ($defaultRelationshipTypes as $defaultRelationshipType) { + $defaultRelationshipTypeGroup = DB::table('default_relationship_type_groups') + ->where('id', $defaultRelationshipType->relationship_type_group_id) + ->first(); + + $relationshipTypeGroup = $this->getRelationshipTypeGroupByType($defaultRelationshipTypeGroup->name); + + if ($relationshipTypeGroup) { + RelationshipType::create([ + 'account_id' => $this->id, + 'name' => $defaultRelationshipType->name, + 'name_reverse_relationship' => $defaultRelationshipType->name_reverse_relationship, + 'relationship_type_group_id' => $relationshipTypeGroup->id, + 'delible' => $defaultRelationshipType->delible, + ]); + } + } + } + + /** + * Create a new account and associate a new User. + * + * @param string $first_name + * @param string $last_name + * @param string $email + * @param string $password + * @param string $ipAddress + * @return self + */ + public static function createDefault($first_name, $last_name, $email, $password, $ipAddress = null, $lang = null) + { + // create new account + $account = new self; + $account->api_key = Str::random(30); + $account->created_at = now(); + $account->save(); + + try { + // create the first user for this account + $user = app(CreateUser::class)->execute([ + 'account_id' => $account->id, + 'first_name' => $first_name, + 'last_name' => $last_name, + 'email' => $email, + 'password' => $password, + 'locale' => $lang, + 'ip_address' => $ipAddress, + ]); + } catch (\Exception $e) { + $account->delete(); + throw $e; + } + + $account->populateDefaultFields(); + + return $account; + } + + /** + * Populates all the default column that should be there when a new account + * is created or reset. + */ + public function populateDefaultFields() + { + app(PopulateContactFieldTypesTable::class)->execute([ + 'account_id' => $this->id, + 'migrate_existing_data' => true, + ]); + + $this->populateDefaultGendersTable(); + $this->populateDefaultReminderRulesTable(); + $this->populateRelationshipTypeGroupsTable(); + $this->populateRelationshipTypesTable(); + $this->populateActivityTypeTable(); + + app(PopulateLifeEventsTable::class)->execute([ + 'account_id' => $this->id, + 'migrate_existing_data' => true, + ]); + + app(PopulateModulesTable::class)->execute([ + 'account_id' => $this->id, + 'migrate_existing_data' => true, + ]); + } + + /** + * Gets the RelationshipType object matching the given type. + * + * @param string $relationshipTypeName + * @return RelationshipType|null + */ + public function getRelationshipTypeByType(string $relationshipTypeName) + { + return $this->relationshipTypes->where('name', $relationshipTypeName)->first(); + } + + /** + * Gets the RelationshipType object matching the given type. + * + * @param string $relationshipTypeGroupName + * @return RelationshipTypeGroup|null + */ + public function getRelationshipTypeGroupByType(string $relationshipTypeGroupName) + { + return $this->relationshipTypeGroups->where('name', $relationshipTypeGroupName)->first(); + } + + /** + * Get the first available locale in an account. This gets the first user + * in the account and reads his locale. + * + * @return string|null + * + * @throws ModelNotFoundException + */ + public function getFirstLocale(): ?string + { + try { + $user = $this->users()->firstOrFail(); + } catch (ModelNotFoundException $e) { + return null; + } + + return $user->locale; + } +} diff --git a/app/Models/Account/Activity.php b/app/Models/Account/Activity.php new file mode 100644 index 0000000..121b8e6 --- /dev/null +++ b/app/Models/Account/Activity.php @@ -0,0 +1,160 @@ +|bool + */ + protected $guarded = ['id']; + + /** + * The attributes that should be mutated to dates. + * + * @var array + */ + protected $dates = ['happened_at']; + + /** + * The relations to eager load on every query. + * + * @var array + */ + protected $with = [ + 'account', + 'type', + 'contacts', + ]; + + /** + * Get the account record associated with the activity. + * + * @return BelongsTo + */ + public function account() + { + return $this->belongsTo(Account::class); + } + + /** + * Get the contact record associated with the activity. + * + * @return BelongsToMany + */ + public function contacts() + { + return $this->belongsToMany(Contact::class); + } + + /** + * Get the activity type record associated with the activity. + * + * @return BelongsTo + */ + public function type() + { + return $this->belongsTo(ActivityType::class, 'activity_type_id'); + } + + /** + * Get all of the activities journal entries. + */ + public function journalEntries() + { + return $this->morphMany(JournalEntry::class, 'journalable'); + } + + /** + * Get the emotion records associated with the activity. + * + * @return BelongsToMany + */ + public function emotions() + { + return $this->belongsToMany(Emotion::class, 'emotion_activity', 'activity_id', 'emotion_id') + ->withPivot('account_id') + ->withTimestamps(); + } + + /** + * Get the summary for this activity. + * + * @return string or null + */ + public function getSummary() + { + return $this->summary; + } + + /** + * Get the key of the title of the activity. + * + * @return string or null + */ + public function getTitle() + { + return $this->type ? $this->type->translation_key : null; + } + + /** + * Get all the contacts this activity is associated with. + */ + public function getContactsForAPI() + { + $attendees = $this->contacts->filter(function ($contact) { + // This should not be possible! + return $contact->account_id === $this->account_id; + }); + + return ContactShortResource::collection($attendees); + } + + /** + * Gets the information about the activity for the journal. + * + * @return array + */ + public function getInfoForJournalEntry() + { + return [ + 'type' => 'activity', + 'id' => $this->id, + 'activity_type' => (! is_null($this->type) ? $this->type->name : null), + 'summary' => $this->summary, + 'description' => $this->description, + 'day' => $this->happened_at->day, + 'day_name' => mb_convert_case(DateHelper::getShortDay($this->happened_at), MB_CASE_TITLE, 'UTF-8'), + 'month' => $this->happened_at->month, + 'month_name' => mb_convert_case(DateHelper::getShortMonth($this->happened_at), MB_CASE_UPPER, 'UTF-8'), + 'year' => $this->happened_at->year, + 'attendees' => $this->getContactsForAPI(), + ]; + } +} diff --git a/app/Models/Account/ActivityStatistic.php b/app/Models/Account/ActivityStatistic.php new file mode 100644 index 0000000..15f588d --- /dev/null +++ b/app/Models/Account/ActivityStatistic.php @@ -0,0 +1,39 @@ + + */ + protected $fillable = [ + 'account_id', + 'contact_id', + 'year', + 'count', + ]; + + /** + * Get the account record associated with the activity statistic. + */ + public function account() + { + return $this->belongsTo(Account::class); + } + + /** + * Get the contact record associated with the activity statistic. + */ + public function contact() + { + return $this->belongsTo(Contact::class); + } +} diff --git a/app/Models/Account/ActivityType.php b/app/Models/Account/ActivityType.php new file mode 100644 index 0000000..f3782ec --- /dev/null +++ b/app/Models/Account/ActivityType.php @@ -0,0 +1,82 @@ + + */ + protected $fillable = [ + 'name', + 'activity_type_category_id', + 'account_id', + 'translation_key', + ]; + + /** + * Get the account record associated with the activity type. + * + * @return BelongsTo + */ + public function account() + { + return $this->belongsTo(Account::class); + } + + /** + * Get the activity type category record associated with the activity types. + * + * @return BelongsTo + */ + public function category() + { + return $this->belongsTo(ActivityTypeCategory::class, 'activity_type_category_id'); + } + + /** + * Get the activity records associated with the activity type. + * + * @return HasMany + */ + public function activities() + { + return $this->hasMany(Activity::class); + } + + /** + * Get the activity type's attribute. + */ + public function getNameAttribute($value) + { + if ($this->translation_key && ! $value) { + return trans('people.activity_type_'.$this->translation_key); + } + + return $value; + } + + /** + * Reset all associated activities with this category type. + * + * @return void + */ + public function resetAssociationWithActivities() + { + foreach ($this->activities as $activity) { + $activity->activity_type_id = null; + $activity->save(); + } + } +} diff --git a/app/Models/Account/ActivityTypeCategory.php b/app/Models/Account/ActivityTypeCategory.php new file mode 100644 index 0000000..c745a1d --- /dev/null +++ b/app/Models/Account/ActivityTypeCategory.php @@ -0,0 +1,63 @@ + + */ + protected $fillable = [ + 'name', + 'translation_key', + 'account_id', + ]; + + /** + * Get the account record associated with the activity type group. + * + * @return BelongsTo + */ + public function account() + { + return $this->belongsTo(Account::class); + } + + /** + * Get the activity type records associated with the category. + * + * @return HasMany + */ + public function activityTypes() + { + return $this->hasMany(ActivityType::class); + } + + /** + * Get the activity type category's attribute. + * + * @return string + * @psalm-suppress InvalidReturnStatement + */ + public function getNameAttribute($value) + { + if ($this->translation_key && ! $value) { + return trans('people.activity_type_category_'.$this->translation_key); + } + + return $value; + } +} diff --git a/app/Models/Account/AddressBook.php b/app/Models/Account/AddressBook.php new file mode 100644 index 0000000..94d5207 --- /dev/null +++ b/app/Models/Account/AddressBook.php @@ -0,0 +1,75 @@ + + */ + protected $fillable = [ + 'account_id', + 'user_id', + 'name', + 'description', + ]; + + /** + * The attributes that aren't mass assignable. + * + * @var array|bool + */ + protected $guarded = ['id']; + + /** + * The attributes that should be cast to native types. + * + * @var array + */ + protected $casts = [ + ]; + + /** + * Get the account record associated with the address book. + * + * @return BelongsTo + */ + public function account() + { + return $this->belongsTo(Account::class); + } + + /** + * Get the user record associated with the address book. + * + * @return BelongsTo + */ + public function user() + { + return $this->belongsTo(User::class); + } + + /** + * Get all contacts for this address book. + * + * @return HasMany + */ + public function contacts() + { + return $this->hasMany(Contact::class); + } +} diff --git a/app/Models/Account/AddressBookSubscription.php b/app/Models/Account/AddressBookSubscription.php new file mode 100644 index 0000000..987f245 --- /dev/null +++ b/app/Models/Account/AddressBookSubscription.php @@ -0,0 +1,175 @@ + + */ + protected $fillable = [ + 'account_id', + 'user_id', + 'address_book_id', + 'name', + 'uri', + 'capabilities', + 'username', + 'password', + 'readonly', + 'syncToken', + 'localSyncToken', + 'frequency', + 'last_synchronized_at', + 'active', + ]; + + /** + * The attributes that aren't mass assignable. + * + * @var array|bool + */ + protected $guarded = ['id']; + + /** + * The attributes that should be mutated to dates. + * + * @var array + */ + protected $dates = [ + 'last_synchronized_at', + ]; + + /** + * The attributes that should be cast to native types. + * + * @var array + */ + protected $casts = [ + 'readonly' => 'boolean', + 'active' => 'boolean', + 'localSyncToken' => 'integer', + ]; + + /** + * Eager load account with every contact. + * + * @var array + */ + protected $with = [ + 'user', + ]; + + /** + * Get the account record associated with the subscription. + * + * @return BelongsTo + */ + public function account() + { + return $this->belongsTo(Account::class); + } + + /** + * Get the user record associated with the subscription. + * + * @return BelongsTo + */ + public function user() + { + return $this->belongsTo(User::class); + } + + /** + * Get the addressbook record associated with the subscription. + * + * @return BelongsTo + */ + public function addressBook() + { + return $this->belongsTo(AddressBook::class); + } + + /** + * Get capabilities. + * + * @param string $value + * @return array + */ + public function getCapabilitiesAttribute($value) + { + return json_decode($value, true); + } + + /** + * Set capabilities. + * + * @param string $value + * @return void + */ + public function setCapabilitiesAttribute($value) + { + $this->attributes['capabilities'] = json_encode($value); + } + + /** + * Get password. + * + * @param string $value + * @return string + */ + public function getPasswordAttribute($value) + { + return decrypt($value); + } + + /** + * Set password. + * + * @param string $value + * @return void + */ + public function setPasswordAttribute($value) + { + $this->attributes['password'] = encrypt($value); + } + + /** + * Scope a query to only include active subscriptions. + * + * @param Builder $query + * @return Builder + */ + public function scopeActive($query) + { + return $query->where('active', 1); + } + + /** + * Get a new client. + * + * @return DavClient + */ + public function getClient(): DavClient + { + return app(DavClient::class) + ->setBaseUri($this->uri) + ->setCredentials($this->username, $this->password); + } +} diff --git a/app/Models/Account/ApiUsage.php b/app/Models/Account/ApiUsage.php new file mode 100644 index 0000000..2285e0b --- /dev/null +++ b/app/Models/Account/ApiUsage.php @@ -0,0 +1,21 @@ +url = $request->fullUrl(); + $this->method = $request->getMethod(); + $this->client_ip = $request->getClientIp(); + $this->save(); + } +} diff --git a/app/Models/Account/Company.php b/app/Models/Account/Company.php new file mode 100644 index 0000000..60f1a5a --- /dev/null +++ b/app/Models/Account/Company.php @@ -0,0 +1,53 @@ + + */ + protected $fillable = [ + 'weather_json', + 'account_id', + 'name', + 'website', + 'number_of_employees', + ]; + + /** + * The attributes that aren't mass assignable. + * + * @var array|bool + */ + protected $guarded = ['id']; + + /** + * Get the account record associated with the company. + * + * @return BelongsTo + */ + public function account() + { + return $this->belongsTo(Account::class); + } + + /** + * Get the Occupation records associated with the contact. + * + * @return HasMany + */ + public function occupations() + { + return $this->hasMany(Occupation::class); + } +} diff --git a/app/Models/Account/ExportJob.php b/app/Models/Account/ExportJob.php new file mode 100644 index 0000000..22f2e82 --- /dev/null +++ b/app/Models/Account/ExportJob.php @@ -0,0 +1,114 @@ + + */ + protected $fillable = [ + 'uuid', + 'account_id', + 'user_id', + 'type', + 'status', + 'filesystem', + 'filename', + 'started_at', + 'ended_at', + ]; + + /** + * The attributes that aren't mass assignable. + * + * @var array|bool + */ + protected $guarded = ['id']; + + /** + * The attributes that should be mutated to dates. + * + * @var array + */ + protected $dates = [ + 'started_at', + 'ended_at', + ]; + + /** + * Get the account record associated with the import job. + * + * @return BelongsTo + */ + public function account() + { + return $this->belongsTo(Account::class); + } + + /** + * Get the user record associated with the import job. + * + * @return BelongsTo + */ + public function user() + { + return $this->belongsTo(User::class); + } + + /** + * Start the export job. + * + * @return void + */ + public function start(): void + { + $this->status = self::EXPORT_DOING; + $this->started_at = now(); + $this->save(); + } + + /** + * End the export job. + * + * @return void + */ + public function end(): void + { + $this->status = self::EXPORT_DONE; + $this->ended_at = now(); + $this->save(); + + $this->user->notify(new ExportAccountDone($this)); + } +} diff --git a/app/Models/Account/ImportJob.php b/app/Models/Account/ImportJob.php new file mode 100644 index 0000000..8ce1d0f --- /dev/null +++ b/app/Models/Account/ImportJob.php @@ -0,0 +1,311 @@ +|bool + */ + protected $guarded = ['id']; + + /** + * The attributes that should be mutated to dates. + * + * @var array + */ + protected $dates = ['started_at', 'ended_at']; + + /** + * Get the account record associated with the import job. + * + * @return BelongsTo + */ + public function account() + { + return $this->belongsTo(Account::class); + } + + /** + * Get the user record associated with the import job. + * + * @return BelongsTo + */ + public function user() + { + return $this->belongsTo(User::class); + } + + /** + * Get the import jobs reports records associated with the account. + * + * @return HasMany + */ + public function importJobReports() + { + return $this->hasMany(ImportJobReport::class); + } + + /** + * Process an import job. + * + * @return void + */ + public function process($behaviour = ImportVCard::BEHAVIOUR_ADD) + { + $this->initJob(); + + if (! $this->failed && $this->getPhysicalFile()) { + $this->getEntries(); + + $this->processEntries($behaviour); + } + + $this->deletePhysicalFile(); + + if (! $this->failed) { + $this->endJob(); + } + } + + /** + * Perform preliminary steps to start the import job. + * + * @return void + */ + private function initJob(): void + { + if (AccountHelper::hasLimitations($this->account)) { + $this->fail(trans('auth.not_authorized')); + } + + $this->started_at = now(); + $this->contacts_imported = 0; + $this->contacts_skipped = 0; + $this->save(); + } + + /** + * Perform the steps to finalize the import job. + * + * @return void + */ + private function endJob(): void + { + $this->ended_at = now(); + $this->save(); + } + + /** + * Mark the import job as failed. + * + * @param string $reason + * @return void + */ + private function fail(string $reason): void + { + $this->failed = true; + if (! $this->failed_reason) { + $this->failed_reason = $reason; + } + $this->endJob(); + } + + /** + * Get the physical file (the vCard file). + * + * @return bool + */ + private function getPhysicalFile(): bool + { + try { + $this->physicalFile = Storage::disk(config('filesystems.default'))->readStream($this->filename); + } catch (UnableToReadFile $exception) { + $this->fail(trans('settings.import_vcard_file_not_found')); + + return false; + } + + return true; + } + + /** + * Delete the physical file from the disk. + * + * @return bool + */ + private function deletePhysicalFile(): bool + { + try { + if (Storage::disk(config('filesystems.default'))->delete($this->filename) === false) { + $this->fail(trans('settings.import_vcard_file_not_found')); + + return false; + } + } catch (UnableToDeleteFile $exception) { + $this->fail(trans('settings.import_vcard_file_not_found')); + + return false; + } + + return true; + } + + /** + * Get the number of matches in the vCard file. + * + * @return void + */ + private function getEntries() + { + if ($this->physicalFile !== null) { + $this->entries = new VCardReader($this->physicalFile, Reader::OPTION_FORGIVING + Reader::OPTION_IGNORE_INVALID_LINES); + } + } + + /** + * Process all entries contained in the vCard file. + * + * @param string $behaviour + * @return void + */ + private function processEntries($behaviour = ImportVCard::BEHAVIOUR_ADD) + { + while (true) { + try { + /** @var VCard|null */ + $entry = $this->entries !== null ? $this->entries->getNext() : null; + if (! $entry) { + // file end + break; + } + $this->contacts_found++; + } catch (\Throwable $e) { + $this->skipEntry('?', (string) $e); + continue; + } + + $this->processSingleEntry($entry, $behaviour); + } + + if ($this->contacts_found == 0) { + $this->fail(trans('settings.import_vcard_file_no_entries')); + } + } + + /** + * Process a single vCard entry. + * + * @param string|VCard $entry + * @param string $behaviour + * @return void + */ + private function processSingleEntry($entry, $behaviour = ImportVCard::BEHAVIOUR_ADD): void + { + try { + $result = app(ImportVCard::class)->execute([ + 'account_id' => $this->account_id, + 'user_id' => $this->user_id, + 'entry' => $entry, + 'behaviour' => $behaviour, + ]); + } catch (ValidationException $e) { + $this->fail(implode(',', $e->validator->errors()->all())); + + return; + } + + if (Arr::has($result, 'error') && ! empty($result['error'])) { + $this->skipEntry($result['name'], $result['reason']); + + return; + } + + $this->contacts_imported++; + $this->fileImportJobReport($result['name'], self::VCARD_IMPORTED); + } + + /** + * Skip the current entry. + * + * @param string $name + * @param string $reason + * @return void + */ + private function skipEntry($name, $reason = null): void + { + $this->fileImportJobReport($name, self::VCARD_SKIPPED, $reason); + $this->contacts_skipped++; + } + + /** + * File an import job report for the current entry. + * + * @param string $name + * @param bool $status + * @param string $reason + * @return void + */ + private function fileImportJobReport($name, $status, $reason = null): void + { + $importJobReport = new ImportJobReport; + $importJobReport->account_id = $this->account_id; + $importJobReport->user_id = $this->user_id; + $importJobReport->import_job_id = $this->id; + $importJobReport->contact_information = trim($name); + $importJobReport->skipped = $status; + $importJobReport->skip_reason = $reason; + $importJobReport->save(); + } +} diff --git a/app/Models/Account/ImportJobReport.php b/app/Models/Account/ImportJobReport.php new file mode 100644 index 0000000..f634669 --- /dev/null +++ b/app/Models/Account/ImportJobReport.php @@ -0,0 +1,59 @@ +|bool + */ + protected $guarded = ['id']; + + /** + * Get the account record associated with the import job report. + * + * @return BelongsTo + */ + public function account() + { + return $this->belongsTo(Account::class); + } + + /** + * Get the user record associated with the import job report. + * + * @return BelongsTo + */ + public function user() + { + return $this->belongsTo(User::class); + } + + /** + * Get the import job record associated with the gift. + * + * @return BelongsTo + */ + public function importJob() + { + return $this->belongsTo(ImportJob::class); + } +} diff --git a/app/Models/Account/Invitation.php b/app/Models/Account/Invitation.php new file mode 100644 index 0000000..25de4cc --- /dev/null +++ b/app/Models/Account/Invitation.php @@ -0,0 +1,45 @@ +|bool + */ + protected $guarded = ['id']; + + /** + * Get the account record associated with the invitation. + * + * @return BelongsTo + */ + public function account() + { + return $this->belongsTo(Account::class); + } + + /** + * Get the contact record associated with the task. + * + * @return BelongsTo + */ + public function invitedBy() + { + return $this->belongsTo(User::class, 'invited_by_user_id'); + } +} diff --git a/app/Models/Account/Photo.php b/app/Models/Account/Photo.php new file mode 100644 index 0000000..b1adc8e --- /dev/null +++ b/app/Models/Account/Photo.php @@ -0,0 +1,110 @@ +|bool + */ + protected $guarded = ['id']; + + /** + * Get the account record associated with the photo. + * + * @return BelongsTo + */ + public function account() + { + return $this->belongsTo(Account::class); + } + + /** + * Get the contacts record associated with the photo. + * + * @return BelongsToMany + */ + public function contacts() + { + return $this->belongsToMany(Contact::class)->withTimestamps(); + } + + /** + * Get the first contact record associated with the photo. + * + * @return Contact + */ + public function contact() + { + return $this->contacts->first(); + } + + /** + * Gets the full path of the photo. + * + * @return string + */ + public function url() + { + if (config('filesystems.default_visibility') === 'public') { + return asset(StorageHelper::disk(config('filesystems.default'))->url($this->new_filename)); + } + + return route('storage', ['file' => $this->new_filename]); + } + + /** + * Gets the data-url format of the photo. + * + * @return string|null + */ + public function dataUrl(): ?string + { + try { + $url = $this->new_filename; + $file = StorageHelper::disk(config('filesystems.default'))->get($url); + + return (string) Image::make($file)->encode('data-url'); + } catch (FileNotFoundException $e) { + return null; + } + } + + /** + * Delete the model from the database. + * + * @return bool|null + */ + public function delete() + { + try { + Storage::disk(config('filesystems.default')) + ->delete($this->new_filename); + } catch (FileNotFoundException $e) { + // continue + } + + return parent::delete(); + } +} diff --git a/app/Models/Account/Place.php b/app/Models/Account/Place.php new file mode 100644 index 0000000..bb1bde8 --- /dev/null +++ b/app/Models/Account/Place.php @@ -0,0 +1,126 @@ +|bool + */ + protected $guarded = ['id']; + + /** + * Get the account record associated with the place. + * + * @return BelongsTo + */ + public function account() + { + return $this->belongsTo(Account::class); + } + + /** + * Get the Weather record associated with the place. + * + * @return HasMany + */ + public function weathers() + { + return $this->hasMany(Weather::class); + } + + /** + * Get the address as a sentence. + * + * @return string|null + */ + public function getAddressAsString(): ?string + { + $address = ''; + + if (! is_null($this->street)) { + $address = $this->street; + } + + if (! is_null($this->city)) { + $address .= ' '.$this->city; + } + + if (! is_null($this->province)) { + $address .= ' '.$this->province; + } + + if (! is_null($this->postal_code)) { + $address .= ' '.$this->postal_code; + } + + if (! is_null($this->country)) { + $address .= ' '.$this->getCountryName(); + } + + if (empty($address)) { + return null; + } + + // trim extra whitespaces inside the address + return Str::of($address)->replaceMatches('/\s+/', ' '); + } + + /** + * Get the country of the place. + * + * @return string|null + */ + public function getCountryName(): ?string + { + if ($this->country) { + return CountriesHelper::get($this->country); + } + + return null; + } + + /** + * Get an URL for Google Maps for the place. + * + * @return string + */ + public function getGoogleMapAddress() + { + $place = $this->getAddressAsString(); + $place = urlencode($place); + + return "https://www.google.com/maps/place/{$place}"; + } + + /** + * Get the Google Maps url for the latitude/longitude. + * + * @return string + */ + public function getGoogleMapsAddressWithLatitude() + { + return 'http://maps.google.com/maps?q='.$this->latitude.','.+$this->longitude; + } +} diff --git a/app/Models/Account/Weather.php b/app/Models/Account/Weather.php new file mode 100644 index 0000000..b696ecd --- /dev/null +++ b/app/Models/Account/Weather.php @@ -0,0 +1,245 @@ + + */ + protected $fillable = [ + 'account_id', + 'place_id', + 'weather_json', + ]; + + /** + * The attributes that should be cast to native types. + * + * @var array + */ + protected $casts = [ + 'weather_json' => 'array', + ]; + + /** + * The attributes that aren't mass assignable. + * + * @var array|bool + */ + protected $guarded = ['id']; + + /** + * Get the account record associated with the weather data. + * + * @return BelongsTo + */ + public function account() + { + return $this->belongsTo(Account::class); + } + + /** + * Get the place record associated with the weather data. + * + * @return BelongsTo + */ + public function place() + { + return $this->belongsTo(Place::class); + } + + /** + * Get the weather code. + * + * @return string|null + */ + public function getSummaryCodeAttribute(): ?string + { + $json = $this->weather_json; + + // currently.icon: Darksky version + if (! ($icon = Arr::get($json, 'currently.icon'))) { + if (($text = Arr::get($json, 'current.condition.text')) === 'Partly cloudy') { + $icon = ((bool) Arr::get($json, 'current.is_day')) ? 'partly-cloudy-day' : 'partly-cloudy-night'; + } else { + $icon = (string) Str::of($text)->lower()->replace(' ', '-'); + } + } + + return $icon; + } + + /** + * Get the weather summary. + * + * @return string|null + */ + public function getSummaryAttribute(): ?string + { + $summary_code = $this->summary_code; + if (empty($summary_code)) { + return null; + } + + return (string) Str::of(trans('app.weather_'.$summary_code)); + } + + /** + * Get the weather location. + * + * @return string|null + */ + public function getLocationAttribute(): ?string + { + return Arr::get($this->weather_json, 'location.name'); + } + + /** + * Get the weather update date. + * + * @return Carbon + */ + public function getDateAttribute(): ?Carbon + { + if (($timestamp = Arr::get($this->weather_json, 'current.last_updated_epoch')) !== null) { + return Carbon::createFromTimestamp($timestamp); + } + + return null; + } + + /** + * Get the weather icon. + * + * @return string + * + * @codeCoverageIgnore + */ + public function getEmojiAttribute(): string + { + switch ($this->summary_code) { + case 'sunny': + case 'clear-day': + $string = '🌞'; + break; + case 'clear': + case 'clear-night': + $string = '🌃'; + break; + case 'light-drizzle': + case 'patchy-light-drizzle': + case 'patchy-light-rain': + case 'light-rain': + case 'moderate-rain-at-times': + case 'moderate-rain': + case 'patchy-rain-possible': + case 'heavy-rain-at-times': + case 'heavy-rain': + case 'light-freezing-rain': + case 'moderate-or-heavy-freezing-rain': + case 'light-sleet': + case 'moderate-or-heavy-rain-shower': + case 'light-rain-shower': + case 'torrential-rain-shower': + case 'rain': + $string = '🌧️'; + break; + case 'snow': + case 'blowing-snow': + case 'patchy-light-snow': + case 'light-snow': + case 'patchy-moderate-snow': + case 'moderate-snow': + case 'patchy-heavy-snow': + case 'heavy-snow': + case 'light-snow-showers': + case 'moderate-or-heavy-snow-showers': + $string = '❄️'; + break; + case 'patchy-snow-possible': + case 'patchy-sleet-possible': + case 'moderate-or-heavy-sleet': + case 'light-sleet-showers': + case 'moderate-or-heavy-sleet-showers': + case 'sleet': + $string = '🌨️'; + break; + case 'wind': + $string = '💨'; + break; + case 'fog': + case 'mist': + case 'blizzard': + case 'freezing-fog': + $string = '🌫️'; + break; + case 'overcast': + case 'cloudy': + $string = '☁️'; + break; + case 'partly-cloudy-day': + $string = '⛅'; + break; + case 'partly-cloudy-night': + $string = '🎑'; + break; + case 'freezing-drizzle': + case 'heavy-freezing-drizzle': + case 'patchy-freezing-drizzle-possible': + case 'ice-pellets': + case 'light-showers-of-ice-pellets': + case 'moderate-or-heavy-showers-of-ice-pellets': + $string = '🧊'; + break; + case 'thundery-outbreaks-possible': + case 'patchy-light-rain-with-thunder': + case 'moderate-or-heavy-rain-with-thunder': + case 'patchy-light-snow-with-thunder': + case 'moderate-or-heavy-snow-with-thunder': + $string = '⛈️'; + break; + default: + $string = '🌈'; + break; + } + + return $string; + } + + /** + * Get the temperature attribute. + * Temperature is fetched in Celsius. It needs to be + * converted to Fahrenheit depending on the user. + * + * @param string $scale + * @return string + */ + public function temperature($scale = 'celsius') + { + $json = $this->weather_json; + + $temperature = Arr::get($json, 'currently.temperature') ?? Arr::get($json, 'current.temp_c'); + + if ($scale === 'fahrenheit') { + $temperature = Arr::get($json, 'current.temp_f', 9 / 5 * $temperature + 32); + } + + $temperature = round($temperature, 1); + + $numberFormatter = new \NumberFormatter(App::getLocale(), \NumberFormatter::DECIMAL); + + return $numberFormatter->format($temperature); + } +} diff --git a/app/Models/Contact/Address.php b/app/Models/Contact/Address.php new file mode 100644 index 0000000..dbf2bc6 --- /dev/null +++ b/app/Models/Contact/Address.php @@ -0,0 +1,76 @@ +|bool + */ + protected $guarded = ['id']; + + /** + * All of the relationships to be touched. + * + * @var array + */ + protected $touches = ['contact']; + + protected $table = 'addresses'; + + /** + * Get the account record associated with the address. + * + * @return BelongsTo + */ + public function account() + { + return $this->belongsTo(Account::class); + } + + /** + * Get the contact record associated with the address. + * + * @return BelongsTo + */ + public function contact() + { + return $this->belongsTo(Contact::class); + } + + /** + * Get the place record associated with the address. + * + * @return BelongsTo + */ + public function place() + { + return $this->belongsTo(Place::class); + } + + /** + * Get the label associated with the contact. + * + * @return BelongsToMany + */ + public function labels() + { + return $this->belongsToMany(ContactFieldLabel::class); + } +} diff --git a/app/Models/Contact/Call.php b/app/Models/Contact/Call.php new file mode 100644 index 0000000..851d7ad --- /dev/null +++ b/app/Models/Contact/Call.php @@ -0,0 +1,81 @@ +|bool + */ + protected $guarded = ['id']; + + /** + * The attributes that should be mutated to dates. + * + * @var array + */ + protected $dates = ['called_at']; + + /** + * The attributes that should be cast to native types. + * + * @var array + */ + protected $casts = [ + 'contact_called' => 'boolean', + ]; + + /** + * Eager load with every call. + */ + protected $with = [ + 'account', + 'contact', + ]; + + /** + * Get the account record associated with the call. + * + * @return BelongsTo + */ + public function account() + { + return $this->belongsTo(Account::class); + } + + /** + * Get the contact record associated with the call. + * + * @return BelongsTo + */ + public function contact() + { + return $this->belongsTo(Contact::class); + } + + /** + * Get the emotion records associated with the call. + * + * @return BelongsToMany + */ + public function emotions() + { + return $this->belongsToMany(Emotion::class, 'emotion_call', 'call_id', 'emotion_id') + ->withPivot('account_id', 'contact_id') + ->withTimestamps(); + } +} diff --git a/app/Models/Contact/Contact.php b/app/Models/Contact/Contact.php new file mode 100644 index 0000000..0b27265 --- /dev/null +++ b/app/Models/Contact/Contact.php @@ -0,0 +1,1600 @@ + */ + protected $dates = [ + 'last_talked_to', + 'last_consulted_at', + 'stay_in_touch_trigger_date', + 'created_at', + 'updated_at', + ]; + + /** + * The list of columns we want the Searchable trait to use. + * + * @var array + */ + protected $searchable_columns = [ + 'first_name', + 'middle_name', + 'last_name', + 'nickname', + 'description', + 'job', + ]; + + /** + * The list of columns we want the Searchable trait to select. + * + * @var array + */ + protected $return_from_search = [ + 'id', + 'uuid', + 'first_name', + 'middle_name', + 'last_name', + 'nickname', + 'description', + 'gender_id', + 'account_id', + 'created_at', + 'updated_at', + 'is_partial', + 'is_starred', + 'avatar_source', + 'avatar_adorable_uuid', + 'avatar_gravatar_url', + 'avatar_default_url', + 'avatar_photo_id', + 'default_avatar_color', + ]; + + /** + * The attributes that are mass assignable. + * + * @var array + */ + protected $fillable = [ + 'uuid', + 'first_name', + 'middle_name', + 'last_name', + 'nickname', + 'gender_id', + 'description', + 'account_id', + 'is_partial', + 'job', + 'company', + 'food_preferences', + 'birthday_reminder_id', + 'birthday_special_date_id', + 'is_dead', + 'last_consulted_at', + 'created_at', + 'first_met_additional_info', + 'address_book_id', + 'vcard', + 'avatar_gravatar_url', + 'avatar_source', + ]; + + /** + * The attributes that aren't mass assignable. + * + * @var array|bool + */ + protected $guarded = ['id']; + + /** + * Eager load account with every contact. + * + * @var array + */ + protected $with = [ + 'account', + 'avatarPhoto', + 'gender', + ]; + + /** + * The attributes that should be cast to native types. + * + * @var array + */ + protected $casts = [ + 'is_partial' => 'boolean', + 'is_dead' => 'boolean', + 'has_avatar' => 'boolean', + 'is_starred' => 'boolean', + 'is_active' => 'boolean', + 'stay_in_touch_frequency' => 'integer', + ]; + + /** + * The name order attribute that indicates how to format the name of the + * contact. + * + * @var string + */ + protected $nameOrder = 'firstname_lastname'; + + /** + * Get the user associated with the contact. + * + * @return BelongsTo + */ + public function account() + { + return $this->belongsTo(Account::class); + } + + /** + * Get the address book associated with the contact. + * + * @return BelongsTo + */ + public function addressBook() + { + return $this->belongsTo(AddressBook::class); + } + + /** + * Get the list of contacts from the same address book as this contact. + * + * @return HasMany|null + */ + public function siblingContacts(): ?HasMany + { + if ($this->account) { + if ($this->addressBook) { + return $this->account->contacts($this->addressBook->name); + } + + return $this->account->contacts(); + } + + return null; + } + + /** + * Get the gender of the contact. + * + * @return BelongsTo + */ + public function gender() + { + return $this->belongsTo(Gender::class); + } + + /** + * Get the activity records associated with the contact. + * + * @return BelongsToMany + */ + public function activities() + { + return $this->belongsToMany(Activity::class)->orderBy('happened_at', 'desc'); + } + + /** + * Get the activity records associated with the contact. + * + * @return HasMany + */ + public function activityStatistics() + { + return $this->hasMany(ActivityStatistic::class)->orderBy('year', 'desc'); + } + + /** + * Get the debt records associated with the contact. + * + * @return HasMany + */ + public function debts() + { + return $this->hasMany(Debt::class); + } + + /** + * Get the gift records associated with the contact. + * + * @return HasMany + */ + public function gifts() + { + return $this->hasMany(Gift::class); + } + + /** + * Get the note records associated with the contact. + * + * @return HasMany + */ + public function notes() + { + return $this->hasMany(Note::class); + } + + /** + * Get the reminder records associated with the contact. + * + * @return HasMany + */ + public function reminders() + { + return $this->hasMany(Reminder::class); + } + + /** + * Get the task records associated with the contact. + * + * @return HasMany + */ + public function tasks() + { + return $this->hasMany(Task::class); + } + + /** + * Get the tags records associated with the contact. + * + * @return BelongsToMany + */ + public function tags() + { + return $this->belongsToMany(Tag::class)->withPivot('account_id')->withTimestamps(); + } + + /** + * Get the calls records associated with the contact. + * + * @return HasMany + */ + public function calls() + { + return $this->hasMany(Call::class)->orderBy('called_at', 'desc'); + } + + /** + * Get the entries records associated with the contact. + * + * @return HasMany + */ + public function entries() + { + return $this->hasMany(Entry::class); + } + + /** + * Get the Relationships records associated with the contact. + * + * @return HasMany + */ + public function relationships() + { + return $this->hasMany(Relationship::class, 'contact_is'); + } + + /** + * Get the Contact Field records associated with the contact. + * + * @return HasMany + */ + public function contactFields() + { + return $this->hasMany(ContactField::class); + } + + /** + * Get the Address Field records associated with the contact. + * + * @return HasMany + */ + public function addresses() + { + return $this->hasMany(Address::class); + } + + /** + * Get the Pets records associated with the contact. + * + * @return HasMany + */ + public function pets() + { + return $this->hasMany(Pet::class); + } + + /** + * Get the contact records associated with the account. + * + * @return HasMany + */ + public function specialDates() + { + return $this->hasMany(SpecialDate::class); + } + + /** + * Get the Special date represented the birthdate. + * + * @return HasOne + */ + public function birthdate() + { + return $this->hasOne(SpecialDate::class, 'id', 'birthday_special_date_id'); + } + + /** + * Get the Special date represented the deceased date. + * + * @return HasOne + */ + public function deceasedDate() + { + return $this->hasOne(SpecialDate::class, 'id', 'deceased_special_date_id'); + } + + /** + * Get the Special date represented the date first met. + * + * @return HasOne + */ + public function firstMetDate() + { + return $this->hasOne(SpecialDate::class, 'id', 'first_met_special_date_id'); + } + + /** + * Get the Conversation records associated with the contact. + * + * @return HasMany + */ + public function conversations() + { + return $this->hasMany(Conversation::class)->orderBy('conversations.happened_at', 'desc'); + } + + /** + * Get the Message records associated with the contact. + * + * @return HasMany + */ + public function messages() + { + return $this->hasMany(Message::class); + } + + /** + * Get the Document records associated with the contact. + * + * @return HasMany + */ + public function documents() + { + return $this->hasMany(Document::class); + } + + /** + * Get the Photo records associated with the contact. + * + * @return BelongsToMany + */ + public function photos() + { + return $this->belongsToMany(Photo::class)->withTimestamps(); + } + + /** + * Get the Life event records associated with the contact. + * + * @return HasMany + */ + public function lifeEvents() + { + return $this->hasMany(LifeEvent::class)->orderBy('life_events.happened_at', 'desc'); + } + + /** + * Get the Occupation records associated with the contact. + * + * @return HasMany + */ + public function occupations() + { + return $this->hasMany(Occupation::class); + } + + /** + * Get the Avatar Photo records associated with the contact. + * + * @return HasOne + */ + public function avatarPhoto() + { + return $this->hasOne(Photo::class, 'id', 'avatar_photo_id'); + } + + /** + * Get the Audot log records associated with the contact. + * + * @return HasMany + */ + public function logs() + { + return $this->hasMany(AuditLog::class, 'about_contact_id', 'id'); + } + + /** + * Test if this is the 'me' contact. + * + * @return bool + */ + public function isMe() + { + return $this->id == auth()->user()->me_contact_id; + } + + /** + * Sort the contacts according a given criteria. + * + * @param Builder $builder + * @param string $criteria + * @return Builder + */ + public function scopeSortedBy(Builder $builder, string $criteria): Builder + { + switch ($criteria) { + case 'firstnameAZ': + return $builder->orderBy('first_name'); + case 'firstnameZA': + return $builder->orderByDesc('first_name'); + case 'lastnameAZ': + return $builder->orderBy('last_name'); + case 'lastnameZA': + return $builder->orderByDesc('last_name'); + case 'lastactivitydateNewtoOld': + return $this->sortedByLastActivity($builder, 'desc'); + case 'lastactivitydateOldtoNew': + return $this->sortedByLastActivity($builder, 'asc'); + default: + return $builder; + } + } + + /** + * Sort the contacts using last activity. + * + * @param Builder $builder + * @param string $order + * @return Builder + */ + private function sortedByLastActivity(Builder $builder, string $order): Builder + { + $builder->leftJoin('activity_contact', 'contacts.id', '=', 'activity_contact.contact_id'); + $builder->leftJoin('activities', 'activity_contact.activity_id', '=', 'activities.id'); + $builder->groupBy('contacts.id'); + $builder->orderBy('activities.happened_at', $order); + $builder->select(['*', 'contacts.id as id']); + + return $builder; + } + + /** + * Scope a query to only include contacts who are not only a kid or a + * significant other without being a contact. + * + * @param Builder $query + * @return Builder + */ + public function scopeReal($query) + { + return $query->where('is_partial', 0); + } + + /** + * Scope a query to only include contacts who are active. + * + * @param Builder $query + * @return Builder + */ + public function scopeActive($query) + { + return $query->where('is_active', 1); + } + + /** + * Scope a query to only include contacts who are alive. + * + * @param Builder $query + * @return Builder + */ + public function scopeAlive($query) + { + return $query->where('is_dead', 0); + } + + /** + * Scope a query to only include contacts who are dead. + * + * @param Builder $query + * @return Builder + */ + public function scopeDead($query) + { + return $query->where('is_dead', 1); + } + + /** + * Scope a query to only include contacts who are not active. + * + * @param Builder $query + * @return Builder + */ + public function scopeNotActive($query) + { + return $query->where('is_active', 0); + } + + /** + * Scope a query to include contacts whose notes contain the search phrase. + * + * @param Builder $query + * @return Builder + */ + public function scopeNotes($query, int $accountId = null, string $needle) + { + $maccountId = $accountId ?? Auth::user()->account_id; + + return $query->orWhereHas('notes', function ($query) use ($maccountId, $needle) { + return $query->where([ + ['account_id', $maccountId], + ['body', 'like', "%$needle%"], + ]); + }); + } + + /** + * Scope a query to include contacts whose introduction notes contain the search phrase. + * + * @param Builder $query + * @return Builder + */ + public function scopeIntroductionAdditionalInformation($query, int $accountId = null, string $needle) + { + $maccountId = $accountId ?? Auth::user()->account_id; + + return $query->orWhere([ + ['account_id', $maccountId], + ['first_met_additional_info', 'like', "%$needle%"], + ]); + } + + /** + * Scope a query to only include contacts from given address book. + * 'null' value for address book is the default address book. + * + * @param Builder $query + * @param int|null $accountId + * @param string|null $addressBookName + * @return Builder + */ + public function scopeAddressBook($query, int $accountId = null, string $addressBookName = null) + { + $addressBook = null; + if ($accountId && $addressBookName) { + $addressBook = AddressBook::where([ + 'account_id' => $accountId, + 'name' => $addressBookName, + ])->first(); + } + + return $query->where('address_book_id', $addressBook ? $addressBook->id : null); + } + + /** + * Get contacts ordered by user preferences. + * + * @param Builder $query + * @return Builder + */ + public function scopeOrderByUserPreference(Builder $query): Builder + { + switch (Auth::user()->name_order) { + case 'firstname_lastname': + $query = $query->orderBy('first_name') + ->orderBy('last_name'); + break; + case 'firstname_lastname_nickname': + $query = $query->orderBy('first_name') + ->orderBy('last_name') + ->orderBy('nickname'); + break; + case 'firstname_nickname_lastname': + $query = $query->orderBy('first_name') + ->orderBy('nickname') + ->orderBy('last_name'); + break; + case 'nickname': + $query = $query->orderBy('nickname'); + break; + case 'lastname_firstname': + $query = $query->orderBy('last_name') + ->orderby('first_name'); + break; + case 'lastname_firstname_nickname': + $query = $query->orderBy('last_name') + ->orderby('first_name') + ->orderby('nickname'); + break; + case 'lastname_nickname_firstname': + $query = $query->orderBy('last_name') + ->orderby('nickname') + ->orderby('first_name'); + break; + } + + return $query; + } + + /** + * Mutator first_name. + * Get the first name of the contact. + * + * @param string|null $value + */ + public function setFirstNameAttribute($value) + { + $this->attributes['first_name'] = trim($value); + } + + /** + * Mutator last_name. + * + * It doesn't run ucfirst on purpose. + * + * @param string|null $value + */ + public function setLastNameAttribute($value) + { + $value = $value ? trim($value) : null; + $this->attributes['last_name'] = $value; + } + + /** + * Set the name order attribute. + * + * @param string $value + * @return void + */ + public function nameOrder($value) + { + $this->nameOrder = $value; + } + + /** + * Mutator last_name. + * + * @param string|null $value + */ + public function setNicknameAttribute($value) + { + $value = $value ? trim($value) : null; + $this->attributes['nickname'] = $value; + } + + /** + * Get user's initials. + * + * @return string + */ + public function getInitialsAttribute() + { + $name = Str::ascii($this->name, LocaleHelper::getLang()); + preg_match_all('/(?<=\s|^)[a-zA-Z0-9]/i', $name, $initials); + + return implode('', $initials[0]); + } + + /** + * Get the full name of the contact. + * + * @return string + */ + public function getNameAttribute() + { + $completeName = ''; + + if (Auth::check()) { + $this->nameOrder = auth()->user()->name_order; + } + + switch ($this->nameOrder) { + case 'firstname_lastname': + $completeName = $this->first_name; + + if (! is_null($this->middle_name)) { + $completeName = $completeName.' '.$this->middle_name; + } + + if (! is_null($this->last_name)) { + $completeName = $completeName.' '.$this->last_name; + } + break; + case 'lastname_firstname': + $completeName = ''; + if (! is_null($this->last_name)) { + $completeName = $completeName.' '.$this->last_name; + } + + if (! is_null($this->middle_name)) { + $completeName = $completeName.' '.$this->middle_name; + } + + $completeName .= ' '.$this->first_name; + break; + case 'firstname_lastname_nickname': + $completeName = $this->first_name; + + if (! is_null($this->middle_name)) { + $completeName = $completeName.' '.$this->middle_name; + } + + if (! is_null($this->last_name)) { + $completeName = $completeName.' '.$this->last_name; + } + + if (! is_null($this->nickname)) { + $completeName = $completeName.' ('.$this->nickname.')'; + } + break; + case 'firstname_nickname_lastname': + $completeName = $this->first_name; + + if (! is_null($this->middle_name)) { + $completeName = $completeName.' '.$this->middle_name; + } + + if (! is_null($this->nickname)) { + $completeName = $completeName.' ('.$this->nickname.')'; + } + + if (! is_null($this->last_name)) { + $completeName = $completeName.' '.$this->last_name; + } + + break; + case 'lastname_firstname_nickname': + $completeName = ''; + if (! is_null($this->last_name)) { + $completeName = $this->last_name; + } + + $completeName = $completeName.' '.$this->first_name; + + if (! is_null($this->middle_name)) { + $completeName = $completeName.' '.$this->middle_name; + } + + if (! is_null($this->nickname)) { + $completeName = $completeName.' ('.$this->nickname.')'; + } + break; + case 'nickname_firstname_lastname': + $completeName = $this->first_name; + + if (! is_null($this->middle_name)) { + $completeName = $completeName.' '.$this->middle_name; + } + + if (! is_null($this->last_name)) { + $completeName = $completeName.' '.$this->last_name; + } + + if (! is_null($this->nickname)) { + $completeName = $this->nickname.' ('.$completeName.')'; + } + break; + case 'nickname_lastname_firstname': + $completeName = ''; + if (! is_null($this->last_name)) { + $completeName = $this->last_name.' '; + } + + $completeName = $completeName.$this->first_name; + + if (! is_null($this->middle_name)) { + $completeName = $completeName.' '.$this->middle_name; + } + + if (! is_null($this->nickname)) { + $completeName = $this->nickname.' ('.$completeName.')'; + } + break; + case 'lastname_nickname_firstname': + $completeName = ''; + if (! is_null($this->last_name)) { + $completeName = $this->last_name; + } + + if (! is_null($this->nickname)) { + $completeName = $completeName.' ('.$this->nickname.')'; + } + + $completeName = $completeName.' '.$this->first_name; + + if (! is_null($this->middle_name)) { + $completeName = $completeName.' '.$this->middle_name; + } + break; + case 'nickname_bracketed_firstname_lastname': + $completeName = $this->first_name; + + if (! is_null($this->middle_name)) { + $completeName = $completeName.' '.$this->middle_name; + } + + if (! is_null($this->nickname)) { + $completeName = $this->nickname.' ('.$completeName.')'; + } + + if (! is_null($this->last_name)) { + $completeName = $completeName.' '.$this->last_name; + } + break; + case 'nickname': + if (! is_null($this->nickname)) { + $completeName = $this->nickname; + } + + if ($completeName == '') { + $completeName = $this->first_name; + + if (! is_null($this->last_name)) { + $completeName = $completeName.' '.$this->last_name; + } + } + break; + } + + if ($this->is_dead) { + $completeName .= ' ⚰'; + } + + return trim($completeName); + } + + /** + * Get the incomplete name of the contact, like `John D.`. + * + * @return string + */ + public function getIncompleteName() + { + $incompleteName = ''; + $incompleteName = $this->first_name; + + if ($this->nameOrder == 'nickname_bracketed_firstname_lastname' && ! is_null($this->nickname)) { + $incompleteName = $this->nickname; + } + + if (! is_null($this->last_name)) { + $incompleteName .= ' '.mb_substr($this->last_name, 0, 1); + } + + if ($this->is_dead) { + $incompleteName .= ' ⚰'; + } + + return trim($incompleteName); + } + + /** + * Get the initials of the contact, used for avatars. + * + * @return string + */ + public function getInitials() + { + return $this->initials; + } + + /** + * Get the date of the last activity done by this contact. + * + * @return \DateTime|null + */ + public function getLastActivityDate(): ?DateTime + { + if ($this->activities->count() === 0) { + return null; + } + + $lastActivity = $this->activities->sortByDesc('happened_at')->first(); + + return $lastActivity->happened_at; + } + + /** + * Get all the contacts related to the current contact by a specific + * relationship type group. + * + * @param string $type + * @return Collection|null + */ + public function getRelationshipsByRelationshipTypeGroup(string $type): ?Collection + { + $relationshipTypeGroup = $this->account->getRelationshipTypeGroupByType($type); + + if (! $relationshipTypeGroup) { + return null; + } + + return $this->relationships->filter(function ($item) use ($type) { + return $item->relationshipType->relationshipTypeGroup->name == $type; + }); + } + + /** + * Set the default avatar color for this object. + * + * @param string|null $color + * @return void + */ + public function setAvatarColor($color = null) + { + $colors = [ + '#fdb660', + '#93521e', + '#bd5067', + '#b3d5fe', + '#ff9807', + '#709512', + '#5f479a', + '#e5e5cd', + ]; + + $this->default_avatar_color = $color ?? $colors[mt_rand(0, count($colors) - 1)]; + } + + /** + * Set the name of the contact. + * + * @param string $firstName + * @param string $middleName + * @param string $lastName + * @return bool + */ + public function setName(string $firstName, string $lastName = null, string $middleName = null) + { + if ($firstName === '') { + return false; + } + + $this->first_name = $firstName; + $this->middle_name = $middleName; + $this->last_name = $lastName; + + return true; + } + + /** + * Returns the state of the birthday. + * As it's a Special Date, the date can have several states. We need this + * info when we populate the Edit contact sheet. + * + * @return string + */ + public function getBirthdayState() + { + if (! $this->birthday_special_date_id) { + return 'unknown'; + } + + if ($this->birthdate->is_age_based) { + return 'approximate'; + } + + // we know at least the day and month + if ($this->birthdate->is_year_unknown) { + return 'almost'; + } + + return 'exact'; + } + + /** + * Refresh statistics about activities. + * + * @return void + */ + public function calculateActivitiesStatistics() + { + // Delete the Activities statistics table for this contact + $this->activityStatistics->each(function ($activityStatistic) { + $activityStatistic->delete(); + }); + + // Create the statistics again + $this->activities->groupBy('happened_at.year') + ->map(function (Collection $activities, $year) { + ActivityStatistic::create([ + 'account_id' => $this->account_id, + 'contact_id' => $this->id, + 'year' => $year, + 'count' => $activities->count(), + ]); + }); + } + + /** + * Get all the gifts offered, if any. + */ + public function getGiftsOffered() + { + return $this->gifts()->offered()->get(); + } + + /** + * Get all the gift ideas, if any. + */ + public function getGiftIdeas() + { + return $this->gifts()->isIdea()->get(); + } + + /** + * Get all the tasks in the in progress state, if any. + */ + public function getTasksInProgress() + { + return $this->tasks()->inProgress()->get(); + } + + /** + * Get all the tasks in the in completed state, if any. + */ + public function getCompletedTasks() + { + return $this->tasks()->completed()->get(); + } + + /** + * Get the default avatar URL. + * + * @return string + */ + public function getAvatarDefaultURL() + { + if (empty($this->avatar_default_url)) { + return ''; + } + + if (config('filesystems.default_visibility') === 'public') { + $matches = Str::of($this->avatar_default_url)->split('/\?/'); + + $url = asset(StorageHelper::disk(config('filesystems.default'))->url($matches[0])); + if ($matches->count() > 1) { + $url .= '?'.$matches[1]; + } + + return $url; + } + + return route('storage', ['file' => $this->avatar_default_url]); + } + + /** + * Get the adorable avatar URL. + * + * @param string|null $value + * @return string|null + */ + public function getAvatarAdorableDataUrlAttribute(?string $value): ?string + { + if (isset($this->avatar_adorable_uuid) && $this->avatar_adorable_uuid !== '') { + return LaravelAdorable::get(config('monica.avatar_size'), $this->avatar_adorable_uuid); + } + + return null; + } + + /** + * Returns the URL of the avatar, properly sized. + * The avatar can come from 4 sources: + * - default, + * - Adorable avatar, + * - Gravatar + * - or a photo that has been uploaded. + * + * @return string|null + */ + public function getAvatarURL() + { + $avatarURL = ''; + + switch ($this->avatar_source) { + case 'adorable': + $avatarURL = $this->avatar_adorable_data_url; + break; + case 'gravatar': + $avatarURL = $this->avatar_gravatar_url; + break; + case 'photo': + if ($this->avatarPhoto) { + $avatarURL = $this->avatarPhoto()->first()->url(); + } else { + $avatarURL = $this->getAvatarDefaultURL(); + } + break; + case 'default': + default: + $avatarURL = $this->getAvatarDefaultURL(); + break; + } + + return $avatarURL; + } + + /** + * Delete avatars files. + * This does not touch avatar_location or avatar_file_name properties of the contact. + * + * @param bool $force + */ + public function deleteAvatars(bool $force = false) + { + if (! $force && (! $this->has_avatar || $this->avatar_location == 'external')) { + return; + } + + $storage = Storage::disk($this->avatar_location); + $this->deleteAvatarSize($storage); + $this->deleteAvatarSize($storage, 110); + $this->deleteAvatarSize($storage, 174); + } + + /** + * Delete avatar file for one size. + * + * @param Filesystem $storage + * @param int $size + */ + private function deleteAvatarSize(Filesystem $storage, int $size = null) + { + $avatarFileName = $this->avatar_file_name; + + if (! is_null($size)) { + $filename = pathinfo($avatarFileName, PATHINFO_FILENAME); + $extension = pathinfo($avatarFileName, PATHINFO_EXTENSION); + $avatarFileName = 'avatars/'.$filename.'_'.$size.'.'.$extension; + } + + try { + if ($storage->exists($avatarFileName)) { + $storage->delete($avatarFileName); + } + } catch (FileNotFoundException $e) { + return; + } + } + + /** + * Check if the contact has debt (by the contact or the user for this contact). + * + * @return bool + */ + public function hasDebt() + { + return $this->debts()->count() !== 0; + } + + /** + * Get the list of tags as a string to populate the tags form. + */ + public function getTagsAsString() + { + return $this->tags->map(function (Tag $tag): string { + return $tag->name; + })->join(','); + } + + /** + * Is this contact owed money? + * + * @return bool + */ + public function isOwedMoney() + { + return $this->totalOutstandingDebtAmount() > 0; + } + + /** + * How much is the debt. + * + * @return int amount in storage value + */ + public function totalOutstandingDebtAmount(): int + { + return $this + ->debts() + ->inProgress() + ->getResults() + ->filter(fn ($d) => Arr::has($d->attributes, 'amount')) + ->sum(function ($d) { + $amount = $d->attributes['amount']; + + return $d->in_debt === 'yes' ? -$amount : $amount; + }); + } + + /** + * Indicates whether the contact has information about how they first met. + * + * @return bool + */ + public function hasFirstMetInformation() + { + return ! is_null($this->first_met_additional_info) || ! is_null($this->firstMetDate) || ! is_null($this->first_met_through_contact_id); + } + + /** + * Gets the contact who introduced this person to the user. + * + * @return Contact|null + */ + public function getIntroducer(): ?self + { + if (! $this->first_met_through_contact_id) { + return null; + } + + try { + /** @var Contact $contact */ + $contact = self::where('account_id', $this->account_id) + ->findOrFail($this->first_met_through_contact_id); + } catch (ModelNotFoundException $e) { + return null; + } + + return $contact; + } + + /** + * Sets a Special Date for this contact, for a specific occasion (birthday, + * decease date,...) of which we know the date. + * + * @param string $occasion + * @param int $year + * @param int $month + * @param int $day + * @return SpecialDate|null + */ + public function setSpecialDate($occasion, int $year, int $month, int $day): ?SpecialDate + { + if (empty($occasion)) { + return null; + } + + $specialDate = new SpecialDate; + $specialDate->setToContact($this)->createFromDate($year, $month, $day); + + switch ($occasion) { + case 'birthdate': + $this->birthday_special_date_id = $specialDate->id; + break; + case 'deceased_date': + $this->deceased_special_date_id = $specialDate->id; + break; + case 'first_met': + $this->first_met_special_date_id = $specialDate->id; + break; + default: + break; + } + + $this->save(); + + return $specialDate; + } + + /** + * Sets a Special Date for this contact, for a specific occasion (birthday, + * decease date,...) of which we know only the age (meaning it's going to + * be approximate). + */ + public function setSpecialDateFromAge($occasion, int $age) + { + if (is_null($occasion)) { + return; + } + + $specialDate = new SpecialDate; + $specialDate->setToContact($this)->createFromAge($age); + + switch ($occasion) { + case 'birthdate': + $this->birthday_special_date_id = $specialDate->id; + break; + case 'deceased_date': + $this->deceased_special_date_id = $specialDate->id; + break; + case 'first_met': + $this->first_met_special_date_id = $specialDate->id; + break; + default: + break; + } + + $this->save(); + + return $specialDate; + } + + /** + * Get all the reminders regarding the birthdays of the contacts who have a + * relationships with the current contact. + * + * @return Collection + */ + public function getBirthdayRemindersAboutRelatedContacts() + { + $relationships = $this->relationships->filter(function ($item) { + return ! is_null($item->ofContact) && + $item->ofContact->birthday_special_date_id > 0; + }); + + $reminders = collect(); + foreach ($relationships as $relationship) { + $reminder = Reminder::where('account_id', $this->account_id) + ->find($relationship->ofContact->birthday_reminder_id); + + if ($reminder) { + $reminders->push($reminder); + } + } + + return $reminders; + } + + /** + * Gets the first contact related to this contact if the current contact is + * partial. + * + * @return self|null + */ + public function getRelatedRealContact() + { + $contact = $this; + + return self::setEagerLoads([])->where('account_id', $this->account_id) + ->where('id', function ($query) use ($contact) { + $query->select('of_contact') + ->from('relationships') + ->where([ + 'account_id' => $contact->account_id, + 'contact_is' => $contact->id, + ]) + ->first(); + }) + ->first(); + } + + /** + * Get the link to this contact, or the related real contact. + * + * @return string + */ + public function getLink() + { + $contact = $this->is_partial ? $this->getRelatedRealContact() : $this; + if (is_null($contact)) { + $contact = $this; + } + + return route('people.show', $contact); + } + + /** + * Get the contacts that have all the provided $tags + * or if $tags is NONE get contacts that have no tags. + * + * @param Builder $query + * @param mixed $tags string or Tag + * @return Builder $query + */ + public function scopeTags($query, $tags) + { + if ($tags == 'NONE') { + // get tagless contacts + $query = $query->has('tags', '<', 1); + } elseif (! empty($tags)) { + // gets users who have all the tags + foreach ($tags as $tag) { + $query = $query->whereHas('tags', function (Builder $query) use ($tag) { + $query->where('id', $tag->id); + }); + } + } + + return $query; + } + + /** + * Indicates the age of the contact at death. + * + * @return int|null + */ + public function getAgeAtDeath(): ?int + { + if (! $this->deceasedDate) { + return null; + } + + if ($this->deceasedDate->is_year_unknown == 1) { + return null; + } + + if (! $this->birthdate) { + return null; + } + + return $this->birthdate->date->diffInYears($this->deceasedDate->date); + } + + /** + * Update the frequency for which user has to be warned to stay in touch + * with the contact. + * + * @param int $frequency + * @return bool + */ + public function updateStayInTouchFrequency($frequency) + { + if (! is_int($frequency)) { + return false; + } + + $this->stay_in_touch_frequency = $frequency; + + if ($frequency == 0) { + $this->stay_in_touch_frequency = null; + } + + $this->save(); + + return true; + } + + /** + * Update the date the notification about staying in touch should be sent. + * + * @param int $frequency + * @param Carbon|null $triggerDate + */ + public function setStayInTouchTriggerDate($frequency, $triggerDate = null) + { + // prevent timestamp update + $timestamps = $this->timestamps; + $this->timestamps = false; + + if ($frequency === 0) { + $this->stay_in_touch_trigger_date = null; + } else { + $triggerDate = $triggerDate ?? now(); + $newTriggerDate = $triggerDate->addDays($frequency); + $this->stay_in_touch_trigger_date = $newTriggerDate; + } + + $this->save(); + + $this->timestamps = $timestamps; + } + + /** + * Get the weather information for this contact, based on the first address + * on the profile. + * + * @return Weather|null + */ + public function getWeather(): ?Weather + { + return WeatherHelper::getWeatherForAddress($this->addresses()->first()); + } + + public function updateConsulted() + { + // prevent timestamp update + $timestamps = $this->timestamps; + $this->timestamps = false; + + $this->last_consulted_at = now(); + $this->number_of_views = $this->number_of_views + 1; + + $this->save(); + + $this->timestamps = $timestamps; + } + + public function throwInactive() + { + if (! $this->is_active) { + throw ValidationException::withMessages([ + trans('people.archived_contact_readonly'), + ]); + } + } + + /** + * Get the prunable model query. + * + * @return \Illuminate\Database\Eloquent\Builder + * @codeCoverageIgnore + */ + public function prunable() + { + return static::where('deleted_at', '<=', now()->subWeek()); + } + + /** + * Prepare the model for pruning. + * + * @return void + * @codeCoverageIgnore + */ + protected function pruning() + { + $this->deleteAvatars(true); + } +} diff --git a/app/Models/Contact/ContactField.php b/app/Models/Contact/ContactField.php new file mode 100644 index 0000000..5d2b0f3 --- /dev/null +++ b/app/Models/Contact/ContactField.php @@ -0,0 +1,95 @@ +|bool + */ + protected $guarded = ['id']; + + /** + * All of the relationships to be touched. + * + * @var array + */ + protected $touches = ['contact']; + + /** + * Get the account record associated with the contact field. + * + * @return BelongsTo + */ + public function account() + { + return $this->belongsTo(Account::class); + } + + /** + * Get the contact record associated with the contact field. + * + * @return BelongsTo + */ + public function contact() + { + return $this->belongsTo(Contact::class); + } + + /** + * Get the label associated with the contact. + * + * @return BelongsToMany + */ + public function labels() + { + return $this->belongsToMany(ContactFieldLabel::class); + } + + /** + * Get the type associated with the contact field. + * + * @return BelongsTo + */ + public function contactFieldType() + { + return $this->belongsTo(ContactFieldType::class); + } + + /** + * Scope a query to only include contact field of email type. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeEmail($query) + { + return $query->whereHas('contactFieldType', function ($query) { + $query->where('type', '=', ContactFieldType::EMAIL); + }); + } + + /** + * Scope a query to only include contact field of phone type. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopePhone($query) + { + return $query->whereHas('contactFieldType', function ($query) { + $query->where('type', '=', ContactFieldType::PHONE); + }); + } +} diff --git a/app/Models/Contact/ContactFieldLabel.php b/app/Models/Contact/ContactFieldLabel.php new file mode 100644 index 0000000..53544aa --- /dev/null +++ b/app/Models/Contact/ContactFieldLabel.php @@ -0,0 +1,44 @@ +|bool + */ + protected $guarded = ['id']; + + protected $table = 'contact_field_labels'; + + /** @var array */ + public static $standardLabels = [ + 'home', + 'work', + 'cell', + 'fax', + 'pager', + 'main', + 'other', + ]; + + /** + * Get the account record associated with the contact field type. + * + * @return BelongsTo + */ + public function account() + { + return $this->belongsTo(Account::class); + } +} diff --git a/app/Models/Contact/ContactFieldType.php b/app/Models/Contact/ContactFieldType.php new file mode 100644 index 0000000..b5eb98a --- /dev/null +++ b/app/Models/Contact/ContactFieldType.php @@ -0,0 +1,66 @@ +|bool + */ + protected $guarded = ['id']; + + protected $table = 'contact_field_types'; + + /** + * The attributes that should be cast to native types. + * + * @var array + */ + protected $casts = [ + 'delible' => 'boolean', + ]; + + /** + * Email type contact field. + * + * @var string + */ + public const EMAIL = 'email'; + + /** + * Phone type contact field. + * + * @var string + */ + public const PHONE = 'phone'; + + /** + * Get the account record associated with the contact field type. + * + * @return BelongsTo + */ + public function account() + { + return $this->belongsTo(Account::class); + } + + /** + * Get the conversations associated with the contact field type. + * + * @return HasMany + */ + public function conversations() + { + return $this->hasMany(Conversation::class); + } +} diff --git a/app/Models/Contact/Conversation.php b/app/Models/Contact/Conversation.php new file mode 100644 index 0000000..5982946 --- /dev/null +++ b/app/Models/Contact/Conversation.php @@ -0,0 +1,68 @@ +|bool + */ + protected $guarded = ['id']; + + /** + * The attributes that should be mutated to dates. + * + * @var array + */ + protected $dates = ['happened_at']; + + /** + * Get the account record associated with the conversation. + * + * @return BelongsTo + */ + public function account() + { + return $this->belongsTo(Account::class); + } + + /** + * Get the contact record associated with the conversation. + * + * @return BelongsTo + */ + public function contact() + { + return $this->belongsTo(Contact::class); + } + + /** + * Get the contact field type record associated with the conversation. + * + * @return BelongsTo + */ + public function contactFieldType() + { + return $this->belongsTo(ContactFieldType::class); + } + + /** + * Get the Message records associated with the conversation. + * + * @return HasMany + */ + public function messages() + { + return $this->hasMany(Message::class); + } +} diff --git a/app/Models/Contact/Debt.php b/app/Models/Contact/Debt.php new file mode 100644 index 0000000..710e261 --- /dev/null +++ b/app/Models/Contact/Debt.php @@ -0,0 +1,103 @@ +|bool + */ + protected $guarded = ['id']; + + /** + * Eager load with every debt. + */ + protected $with = [ + 'account', + 'contact', + ]; + + /** + * Get the account record associated with the debt. + * + * @return BelongsTo + */ + public function account() + { + return $this->belongsTo(Account::class); + } + + /** + * Get the contact record associated with the debt. + * + * @return BelongsTo + */ + public function contact() + { + return $this->belongsTo(Contact::class); + } + + /** + * Get the currency record associated with the debt. + * + * @return BelongsTo + */ + public function currency() + { + return $this->belongsTo(Currency::class); + } + + /** + * Limit results to unpaid/unreceived debt. + * + * @param Builder $query + * @return Builder + */ + public function scopeInProgress(Builder $query) + { + return $query->where('status', 'inprogress'); + } + + /** + * Limit results to due debt. + * + * @param Builder $query + * @return Builder + */ + public function scopeDue(Builder $query) + { + return $query->where('in_debt', 'yes'); + } + + /** + * Limit results to owed debt. + * + * @param Builder $query + * @return Builder + */ + public function scopeOwed(Builder $query) + { + return $query->where('in_debt', 'no'); + } +} diff --git a/app/Models/Contact/Document.php b/app/Models/Contact/Document.php new file mode 100644 index 0000000..b54e9e9 --- /dev/null +++ b/app/Models/Contact/Document.php @@ -0,0 +1,110 @@ +|bool + */ + protected $guarded = ['id']; + + /** + * The attributes that should be cast to native types. + * + * @var array + */ + protected $casts = [ + 'number_of_downloads' => 'integer', + ]; + + /** + * Get the account record associated with the document. + * + * @return BelongsTo + */ + public function account() + { + return $this->belongsTo(Account::class); + } + + /** + * Get the contact record associated with the document. + * + * @return BelongsTo + */ + public function contact() + { + return $this->belongsTo(Contact::class); + } + + /** + * Get the download link. + * + * @return string + */ + public function getDownloadLink(): string + { + if (config('filesystems.default_visibility') === 'public') { + return asset(StorageHelper::disk(config('filesystems.default'))->url($this->new_filename)); + } + + return route('storage', ['file' => $this->new_filename]); + } + + /** + * Gets the data-url format of the document. + * + * @return string|null + */ + public function dataUrl(): ?string + { + try { + $url = $this->new_filename; + $file = StorageHelper::disk(config('filesystems.default'))->get($url); + + return sprintf('data:%s;base64,%s', + $this->mime_type, + base64_encode($file) + ); + } catch (FileNotFoundException $e) { + return null; + } + } + + /** + * Delete the model from the database. + * + * @return bool|null + */ + public function delete() + { + try { + Storage::disk(config('filesystems.default')) + ->delete($this->new_filename); + } catch (FileNotFoundException $e) { + // continue + } + + return parent::delete(); + } +} diff --git a/app/Models/Contact/Gender.php b/app/Models/Contact/Gender.php new file mode 100644 index 0000000..a7b892b --- /dev/null +++ b/app/Models/Contact/Gender.php @@ -0,0 +1,99 @@ +|bool + */ + protected $guarded = ['id']; + + /** + * The attributes that are mass assignable. + * + * @var array + */ + protected $fillable = [ + 'name', + 'type', + 'account_id', + ]; + + /** + * Male type gender. + * + * @var string + */ + public const MALE = 'M'; + + /** + * Female type gender. + * + * @var string + */ + public const FEMALE = 'F'; + + /** + * Other type gender. + * + * @var string + */ + public const OTHER = 'O'; + + /** + * Unknown type gender. + * + * @var string + */ + public const UNKNOWN = 'U'; + + /** + * None type gender. + * + * @var string + */ + public const NONE = 'N'; + + public const LIST = ['M', 'F', 'O', 'U', 'N']; + + /** + * Get the account record associated with the gender. + * + * @return BelongsTo + */ + public function account() + { + return $this->belongsTo(Account::class); + } + + /** + * Get the contact records associated with the gender. + * + * @return HasMany + */ + public function contacts() + { + return $this->hasMany(Contact::class); + } + + /** + * Is this gender the default account one?. + * + * @return bool + */ + public function isDefault(): bool + { + return $this->account->default_gender_id === $this->id; + } +} diff --git a/app/Models/Contact/Gift.php b/app/Models/Contact/Gift.php new file mode 100644 index 0000000..7d98f96 --- /dev/null +++ b/app/Models/Contact/Gift.php @@ -0,0 +1,155 @@ +|bool + */ + protected $guarded = ['id']; + + /** + * The attributes that should be mutated to dates. + * + * @var array + */ + protected $dates = [ + 'date', + ]; + + /** + * The attributes that should be cast to native types. + * + * @var array + */ + protected $casts = [ + ]; + + /** + * Get the account record associated with the gift. + * + * @return BelongsTo + */ + public function account() + { + return $this->belongsTo(Account::class); + } + + /** + * Get the contact record associated with the gift. + * + * @return BelongsTo + */ + public function contact() + { + return $this->belongsTo(Contact::class); + } + + /** + * Get the contact record associated with the gift. + * + * @return HasOne + */ + public function recipient() + { + return $this->hasOne(Contact::class, 'id', 'is_for'); + } + + /** + * Get the photos record associated with the gift. + * + * @return BelongsToMany + */ + public function photos() + { + return $this->belongsToMany(Photo::class)->withTimestamps(); + } + + /** + * Limit results to already offered gifts. + * + * @param Builder $query + * @return Builder + */ + public function scopeOffered(Builder $query) + { + return $query->where('status', 'offered'); + } + + /** + * Limit results to gifts at the idea stage. + * + * @param Builder $query + * @return Builder + */ + public function scopeIsIdea(Builder $query) + { + return $query->where('status', 'idea'); + } + + /** + * Check whether the gift is meant for a particular member + * of the contact's family. + * + * @return bool + */ + public function hasParticularRecipient() + { + return $this->is_for !== null && $this->is_for !== 0; + } + + /** + * Set the recipient for the gift. + * + * @param int $value + * @return void + */ + public function setRecipientAttribute($value): void + { + $this->attributes['is_for'] = $value; + } + + /** + * Get the name of the recipient for this gift. + * + * @return string|null + */ + public function getRecipientNameAttribute(): ?string + { + if ($this->hasParticularRecipient()) { + $recipient = $this->recipient; + if (! is_null($recipient)) { + return $recipient->first_name; + } + } + + return null; + } +} diff --git a/app/Models/Contact/LifeEvent.php b/app/Models/Contact/LifeEvent.php new file mode 100644 index 0000000..c71afbd --- /dev/null +++ b/app/Models/Contact/LifeEvent.php @@ -0,0 +1,96 @@ +|bool + */ + protected $guarded = ['id']; + + /** + * The attributes that are mass assignable. + * + * @var array + */ + protected $fillable = [ + 'name', + 'note', + 'happened_at', + 'account_id', + 'contact_id', + 'reminder_id', + 'life_event_type_id', + 'happened_at_month_unknown', + 'happened_at_day_unknown', + ]; + + /** + * The attributes that should be mutated to dates. + * + * @var array + */ + protected $dates = ['happened_at']; + + /** + * The attributes that should be cast to native types. + * + * @var array + */ + protected $casts = [ + 'happened_at_month_unknown' => 'boolean', + 'happened_at_day_unknown' => 'boolean', + ]; + + /** + * Get the account record associated with the life event. + * + * @return BelongsTo + */ + public function account() + { + return $this->belongsTo(Account::class); + } + + /** + * Get the contact record associated with the life event. + * + * @return BelongsTo + */ + public function contact() + { + return $this->belongsTo(Contact::class); + } + + /** + * Get the life event type record associated with the life event. + * + * @return BelongsTo + */ + public function lifeEventType() + { + return $this->belongsTo(LifeEventType::class, 'life_event_type_id'); + } + + /** + * Get the reminder record associated with the life event. + * + * @return BelongsTo + */ + public function reminder() + { + return $this->belongsTo(Reminder::class); + } +} diff --git a/app/Models/Contact/LifeEventCategory.php b/app/Models/Contact/LifeEventCategory.php new file mode 100644 index 0000000..3442eca --- /dev/null +++ b/app/Models/Contact/LifeEventCategory.php @@ -0,0 +1,57 @@ + + */ + protected $fillable = [ + 'name', + 'account_id', + 'default_life_event_category_key', + 'core_monica_data', + ]; + + /** + * The attributes that should be cast to native types. + * + * @var array + */ + protected $casts = [ + 'core_monica_data' => 'boolean', + ]; + + /** + * Get the account record associated with the life event category. + * + * @return BelongsTo + */ + public function account() + { + return $this->belongsTo(Account::class); + } + + /** + * Get the life event type records associated with the category. + * + * @return HasMany + */ + public function lifeEventTypes() + { + return $this->hasMany(LifeEventType::class); + } +} diff --git a/app/Models/Contact/LifeEventType.php b/app/Models/Contact/LifeEventType.php new file mode 100644 index 0000000..e6a6814 --- /dev/null +++ b/app/Models/Contact/LifeEventType.php @@ -0,0 +1,68 @@ + + */ + protected $fillable = [ + 'name', + 'account_id', + 'life_event_category_id', + 'default_life_event_type_key', + 'core_monica_data', + ]; + + /** + * The attributes that should be cast to native types. + * + * @var array + */ + protected $casts = [ + 'core_monica_data' => 'boolean', + ]; + + /** + * Get the account record associated with the life event type. + * + * @return BelongsTo + */ + public function account() + { + return $this->belongsTo(Account::class); + } + + /** + * Get the life event category record associated with the life event type. + * + * @return BelongsTo + */ + public function lifeEventCategory() + { + return $this->belongsTo(LifeEventCategory::class, 'life_event_category_id'); + } + + /** + * Get the Life event records associated with the life event Type. + * + * @return HasMany + */ + public function lifeEvents() + { + return $this->hasMany(LifeEvent::class); + } +} diff --git a/app/Models/Contact/Message.php b/app/Models/Contact/Message.php new file mode 100644 index 0000000..8c4d505 --- /dev/null +++ b/app/Models/Contact/Message.php @@ -0,0 +1,66 @@ +|bool + */ + protected $guarded = ['id']; + + /** + * The attributes that should be mutated to dates. + * + * @var array + */ + protected $dates = ['written_at']; + + /** + * The attributes that should be cast to native types. + * + * @var array + */ + protected $casts = [ + 'written_by_me' => 'boolean', + ]; + + /** + * Get the account record associated with the message. + * + * @return BelongsTo + */ + public function account() + { + return $this->belongsTo(Account::class); + } + + /** + * Get the contact record associated with the message. + * + * @return BelongsTo + */ + public function contact() + { + return $this->belongsTo(Contact::class); + } + + /** + * Get the Conversation records associated with the message. + * + * @return BelongsTo + */ + public function conversation() + { + return $this->belongsTo(Conversation::class); + } +} diff --git a/app/Models/Contact/Note.php b/app/Models/Contact/Note.php new file mode 100644 index 0000000..06435db --- /dev/null +++ b/app/Models/Contact/Note.php @@ -0,0 +1,124 @@ +|bool + */ + protected $guarded = ['id']; + + /** + * The attributes that should be cast to native types. + * + * @var array + */ + protected $casts = [ + 'is_favorited' => 'boolean', + ]; + + protected $dates = [ + 'favorited_at', + ]; + + /** + * The attributes that are mass assignable. + * + * @var array + */ + protected $fillable = [ + 'account_id', + 'contact_id', + 'body', + 'is_favorited', + ]; + + /** + * Eager load with every note. + */ + protected $with = [ + 'account', + 'contact', + ]; + + /** + * Get the account record associated with the note. + * + * @return BelongsTo + */ + public function account() + { + return $this->belongsTo(Account::class); + } + + /** + * Get the contact record associated with the note. + * + * @return BelongsTo + */ + public function contact() + { + return $this->belongsTo(Contact::class); + } + + /** + * Limit notes to favorited ones. + * + * @param Builder $query + * @return Builder + */ + public function scopeFavorited(Builder $query) + { + return $query->where('is_favorited', true); + } + + /** + * Get the description of a note. + * + * @return string + */ + public function getBody() + { + return $this->body; + } + + /** + * Gets the activity date for this note. + * + * @return string + */ + public function getCreatedAt() + { + return DateHelper::getShortDate($this->created_at); + } + + /** + * Gets the content of the activity and formats it for the email. + * + * @return string + */ + public function getContent() + { + return wordwrap($this->getBody(), 75); + } +} diff --git a/app/Models/Contact/Occupation.php b/app/Models/Contact/Occupation.php new file mode 100644 index 0000000..ea44c85 --- /dev/null +++ b/app/Models/Contact/Occupation.php @@ -0,0 +1,87 @@ + + */ + protected $fillable = [ + 'account_id', + 'contact_id', + 'company_id', + 'title', + 'description', + 'salary', + 'salary_unit', + 'currently_works_here', + 'start_date', + 'end_date', + ]; + + /** + * Valid value for salary unit. + * + * @var array + */ + public static $salaryUnits = [ + 'year', 'month', 'week', 'day', 'hour', + ]; + + /** + * The attributes that should be cast to native types. + * + * @var array + */ + protected $casts = [ + 'start_date' => 'datetime:Y-m-d', + 'end_date' => 'datetime:Y-m-d', + ]; + + /** + * The attributes that aren't mass assignable. + * + * @var array|bool + */ + protected $guarded = ['id']; + + /** + * Get the account record associated with the occupation. + * + * @return BelongsTo + */ + public function account() + { + return $this->belongsTo(Account::class); + } + + /** + * Get the contact record associated with the occupation. + * + * @return BelongsTo + */ + public function contact() + { + return $this->belongsTo(Contact::class); + } + + /** + * Get the company record associated with the occupation. + * + * @return BelongsTo + */ + public function company() + { + return $this->belongsTo(Company::class); + } +} diff --git a/app/Models/Contact/Pet.php b/app/Models/Contact/Pet.php new file mode 100644 index 0000000..fdbb9db --- /dev/null +++ b/app/Models/Contact/Pet.php @@ -0,0 +1,61 @@ +|bool + */ + protected $guarded = ['id']; + + /** + * Get the account record associated with the pet. + * + * @return BelongsTo + */ + public function account() + { + return $this->belongsTo(Account::class); + } + + /** + * Get the contact record associated with the pet. + * + * @return BelongsTo + */ + public function contact() + { + return $this->belongsTo(Contact::class); + } + + /** + * Get the contact record associated with the pet. + * + * @return BelongsTo + */ + public function petCategory() + { + return $this->belongsTo(PetCategory::class); + } + + /** + * Set the name to null if it's an empty string. + * + * @param string $value + * @return void + */ + public function setNameAttribute($value) + { + $this->attributes['name'] = $value ?: null; + } +} diff --git a/app/Models/Contact/PetCategory.php b/app/Models/Contact/PetCategory.php new file mode 100644 index 0000000..536b087 --- /dev/null +++ b/app/Models/Contact/PetCategory.php @@ -0,0 +1,28 @@ +|bool + */ + protected $guarded = ['id']; + + protected $table = 'pet_categories'; + + /** + * Scope a query to only include pet categories that are considered `common`. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeCommon($query) + { + return $query->where('is_common', 1); + } +} diff --git a/app/Models/Contact/Reminder.php b/app/Models/Contact/Reminder.php new file mode 100644 index 0000000..a38e54c --- /dev/null +++ b/app/Models/Contact/Reminder.php @@ -0,0 +1,199 @@ +|bool + */ + protected $guarded = ['id']; + + /** + * The attributes that should be cast to native types. + * + * @var array + */ + protected $casts = [ + 'is_birthday' => 'boolean', + 'delible' => 'boolean', + 'inactive' => 'boolean', + 'initial_date' => 'date:Y-m-d', + ]; + + /** + * Valid value for frequency type. + * + * @var array + */ + public static $frequencyTypes = [ + 'one_time', 'week', 'month', 'year', + ]; + + /** + * Get the account record associated with the reminder. + * + * @return BelongsTo + */ + public function account() + { + return $this->belongsTo(Account::class); + } + + /** + * Get the contact record associated with the reminder. + * + * @return BelongsTo + */ + public function contact() + { + return $this->belongsTo(Contact::class); + } + + /** + * Get the Reminder Outbox records associated with the account. + * + * @return HasMany + */ + public function reminderOutboxes() + { + return $this->hasMany(ReminderOutbox::class); + } + + /** + * Scope a query to only include active reminders. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeActive($query) + { + return $query->where('inactive', false); + } + + /** + * Test if this reminder is the contact's birthday reminder. + * + * @return bool + */ + public function isBirthdayReminder(): bool + { + return $this->contact !== null + && $this->contact->birthday_reminder_id === $this->id; + } + + /** + * Calculate the next expected date for this reminder. + * + * @return Carbon + */ + public function calculateNextExpectedDate($date = null) + { + if (is_null($date)) { + $date = $this->initial_date; + } + + while ($date->isPast()) { + $date = DateHelper::addTimeAccordingToFrequencyType($date, $this->frequency_type, $this->frequency_number); + } + + if ($date->isToday()) { + $date = DateHelper::addTimeAccordingToFrequencyType($date, $this->frequency_type, $this->frequency_number); + } + + return $date; + } + + /** + * Calculate the next expected date using user timezone for this reminder. + * + * @return Carbon + */ + public function calculateNextExpectedDateOnTimezone() + { + $date = $this->initial_date; + $date = Carbon::create($date->year, $date->month, $date->day, 0, 0, 0, + DateHelper::getTimezone() ?? config('app.timezone')); + + return $this->calculateNextExpectedDate($date); + } + + /** + * Schedule the reminder to be sent. + * + * @param User $user + * @return void + */ + public function schedule(User $user) + { + // remove any existing scheduled reminders + $this->reminderOutboxes->each->delete(); + + // when should we send this reminder? + $triggerDate = $this->calculateNextExpectedDate(); + + // schedule the reminder in the outbox, one for each user of the account + ReminderOutbox::create([ + 'account_id' => $this->account_id, + 'reminder_id' => $this->id, + 'user_id' => $user->id, + 'planned_date' => $triggerDate, + 'nature' => 'reminder', + ]); + + $this->scheduleNotifications($triggerDate, $user); + } + + /** + * Create all the notifications that are supposed to be sent + * 30 and 7 days prior to the actual reminder. + * + * @param Carbon $triggerDate + * @param User $user + * @return void + */ + public function scheduleNotifications(Carbon $triggerDate, User $user) + { + $date = $triggerDate->toDateString(); + $reminderRules = $this->account->reminderRules()->where('active', 1)->get(); + + foreach ($reminderRules as $reminderRule) { + $datePrior = Carbon::createFromFormat('Y-m-d', $date) + ->subDays($reminderRule->number_of_days_before); + + if ($datePrior->lessThanOrEqualTo(now())) { + continue; + } + + ReminderOutbox::create([ + 'account_id' => $this->account_id, + 'reminder_id' => $this->id, + 'user_id' => $user->id, + 'planned_date' => $datePrior->toDateString(), + 'nature' => 'notification', + 'notification_number_days_before' => $reminderRule->number_of_days_before, + ]); + } + } +} diff --git a/app/Models/Contact/ReminderOutbox.php b/app/Models/Contact/ReminderOutbox.php new file mode 100644 index 0000000..515bcc3 --- /dev/null +++ b/app/Models/Contact/ReminderOutbox.php @@ -0,0 +1,71 @@ +|bool + */ + protected $guarded = ['id']; + + /** + * The attributes that should be mutated to dates. + * + * @var array + */ + protected $dates = [ + 'planned_date', + ]; + + /** + * Get the account record associated with the reminder. + * + * @return BelongsTo + */ + public function account() + { + return $this->belongsTo(Account::class); + } + + /** + * Get the reminder record associated with the reminder. + * + * @return BelongsTo + */ + public function reminder() + { + return $this->belongsTo(Reminder::class); + } + + /** + * Get the user record associated with the reminder. + * + * @return BelongsTo + */ + public function user() + { + return $this->belongsTo(User::class); + } +} diff --git a/app/Models/Contact/ReminderRule.php b/app/Models/Contact/ReminderRule.php new file mode 100644 index 0000000..5d1223c --- /dev/null +++ b/app/Models/Contact/ReminderRule.php @@ -0,0 +1,38 @@ +|bool + */ + protected $guarded = ['id']; + + protected $table = 'reminder_rules'; + + /** + * The attributes that should be cast to native types. + * + * @var array + */ + protected $casts = [ + 'active' => 'boolean', + ]; + + /** + * Get the account record associated with the reminder. + * + * @return BelongsTo + */ + public function account() + { + return $this->belongsTo(Account::class); + } +} diff --git a/app/Models/Contact/Tag.php b/app/Models/Contact/Tag.php new file mode 100644 index 0000000..0ffb8a6 --- /dev/null +++ b/app/Models/Contact/Tag.php @@ -0,0 +1,67 @@ +|bool + */ + protected $guarded = ['id']; + + /** + * The attributes that are mass assignable. + * + * @var array + */ + protected $fillable = [ + 'name', + 'name_slug', + 'account_id', + ]; + + /** + * Get the account record associated with the tag. + */ + public function account() + { + return $this->belongsTo(Account::class); + } + + /** + * Get the contacts record associated with the tag. + */ + public function contacts() + { + return $this->belongsToMany(Contact::class)->withPivot('account_id')->withTimestamps(); + } + + /** + * Get the tags with the contact count. + */ + public static function contactsCount() + { + return DB::table('contact_tag')->selectRaw('COUNT(tag_id) AS contact_count, name, tag_id AS id') + ->join('tags', function ($join) { + $join->on('tags.id', '=', 'contact_tag.tag_id') + ->on('tags.account_id', '=', 'contact_tag.account_id'); + }) + ->join('contacts', function ($join) { + $join->on('contacts.id', '=', 'contact_tag.contact_id') + ->on('contacts.account_id', '=', 'contact_tag.account_id'); + }) + ->where([ + 'tags.account_id' => auth()->user()->account_id, + 'contacts.address_book_id' => null, + ]) + ->groupBy('tag_id') + ->get() + ->sortByCollator('name'); + } +} diff --git a/app/Models/Contact/Task.php b/app/Models/Contact/Task.php new file mode 100644 index 0000000..1be25be --- /dev/null +++ b/app/Models/Contact/Task.php @@ -0,0 +1,101 @@ +|bool + */ + protected $guarded = ['id']; + + /** + * The attributes that should be mutated to dates. + * + * @var array + */ + protected $dates = [ + 'completed_at', + 'archived_at', + ]; + + /** + * The attributes that should be cast to native types. + * + * @var array + */ + protected $casts = [ + 'completed' => 'boolean', + 'archived' => 'boolean', + ]; + + /** + * Eager load with every task. + */ + protected $with = [ + 'account', + 'contact', + ]; + + /** + * Get the account record associated with the task. + * + * @return BelongsTo + */ + public function account() + { + return $this->belongsTo(Account::class); + } + + /** + * Get the contact record associated with the task. + * + * @return BelongsTo + */ + public function contact() + { + return $this->belongsTo(Contact::class); + } + + /** + * Limit tasks to completed ones. + * + * @param Builder $query + * @return Builder + */ + public function scopeCompleted(Builder $query) + { + return $query->where('completed', true); + } + + /** + * Limit tasks to in-progress ones. + * + * @param Builder $query + * @return Builder + */ + public function scopeInProgress(Builder $query) + { + return $query->where('completed', false); + } +} diff --git a/app/Models/Instance/AuditLog.php b/app/Models/Instance/AuditLog.php new file mode 100644 index 0000000..05dcc84 --- /dev/null +++ b/app/Models/Instance/AuditLog.php @@ -0,0 +1,90 @@ + + */ + protected $fillable = [ + 'account_id', + 'author_id', + 'about_contact_id', + 'author_name', + 'action', + 'objects', + 'should_appear_on_dashboard', + 'audited_at', + ]; + + /** + * The attributes that should be mutated to dates. + * + * @var array + */ + protected $dates = [ + 'audited_at', + ]; + + /** + * The attributes that should be cast to native types. + * + * @var array + */ + protected $casts = [ + 'should_appear_on_dashboard' => 'boolean', + ]; + + /** + * Get the Account record associated with the audit log. + * + * @return BelongsTo + */ + public function account() + { + return $this->belongsTo(Account::class); + } + + /** + * Get the User record associated with the audit log. + * + * @return BelongsTo + */ + public function author() + { + return $this->belongsTo(User::class); + } + + /** + * Get the Contact record associated with the audit log. + * + * @return BelongsTo + */ + public function contact() + { + return $this->belongsTo(Contact::class, 'about_contact_id'); + } + + /** + * Get the JSON object. + * + * @param mixed $value + * @return mixed + */ + public function getObjectAttribute($value) + { + return json_decode($this->objects); + } +} diff --git a/app/Models/Instance/Cron.php b/app/Models/Instance/Cron.php new file mode 100644 index 0000000..615631e --- /dev/null +++ b/app/Models/Instance/Cron.php @@ -0,0 +1,27 @@ + + */ + protected $fillable = [ + 'command', + 'last_run', + ]; + + /** + * The attributes that should be cast to native types. + * + * @var array + */ + protected $casts = [ + 'last_run' => 'datetime', + ]; +} diff --git a/app/Models/Instance/Emotion/Emotion.php b/app/Models/Instance/Emotion/Emotion.php new file mode 100644 index 0000000..92d3f4f --- /dev/null +++ b/app/Models/Instance/Emotion/Emotion.php @@ -0,0 +1,71 @@ +|bool + */ + protected $guarded = ['id']; + + /** + * Get the primary emotion record associated with the emotion. + * + * @return BelongsTo + */ + public function primary() + { + return $this->belongsTo(PrimaryEmotion::class, 'emotion_primary_id'); + } + + /** + * Get the secondary emotion record associated with the emotion. + * + * @return BelongsTo + */ + public function secondary() + { + return $this->belongsTo(SecondaryEmotion::class, 'emotion_secondary_id'); + } + + /** + * Get the call records associated with the emotion. + * + * @return BelongsToMany + */ + public function calls() + { + return $this->belongsToMany(Call::class, 'emotion_call', 'emotion_id', 'call_id') + ->withPivot('account_id', 'contact_id') + ->withTimestamps(); + } + + /** + * Get the activity records associated with the emotion. + * + * @return BelongsToMany + */ + public function activities() + { + return $this->belongsToMany(Activity::class, 'emotion_activity', 'emotion_id', 'activity_id') + ->withPivot('account_id') + ->withTimestamps(); + } +} diff --git a/app/Models/Instance/Emotion/PrimaryEmotion.php b/app/Models/Instance/Emotion/PrimaryEmotion.php new file mode 100644 index 0000000..fd26022 --- /dev/null +++ b/app/Models/Instance/Emotion/PrimaryEmotion.php @@ -0,0 +1,44 @@ +|bool + */ + protected $guarded = ['id']; + + /** + * Get the emotion records associated with the primary emotion. + * + * @return HasMany + */ + public function emotions() + { + return $this->hasMany(Emotion::class, 'emotion_primary_id'); + } + + /** + * Get the secondary records associated with the primary emotion. + * + * @return HasMany + */ + public function secondaries() + { + return $this->hasMany(SecondaryEmotion::class, 'emotion_primary_id'); + } +} diff --git a/app/Models/Instance/Emotion/SecondaryEmotion.php b/app/Models/Instance/Emotion/SecondaryEmotion.php new file mode 100644 index 0000000..1b6f8d9 --- /dev/null +++ b/app/Models/Instance/Emotion/SecondaryEmotion.php @@ -0,0 +1,45 @@ +|bool + */ + protected $guarded = ['id']; + + /** + * Get the primary emotion record associated with the secondary emotion. + * + * @return BelongsTo + */ + public function primary() + { + return $this->belongsTo(PrimaryEmotion::class, 'emotion_primary_id'); + } + + /** + * Get the emotion records associated with the secondary emotion. + * + * @return HasMany + */ + public function emotions() + { + return $this->hasMany(Emotion::class); + } +} diff --git a/app/Models/Instance/Instance.php b/app/Models/Instance/Instance.php new file mode 100644 index 0000000..be6fcc0 --- /dev/null +++ b/app/Models/Instance/Instance.php @@ -0,0 +1,21 @@ +update(['migrated' => 1]); + } +} diff --git a/app/Models/Instance/SpecialDate.php b/app/Models/Instance/SpecialDate.php new file mode 100644 index 0000000..24c17e3 --- /dev/null +++ b/app/Models/Instance/SpecialDate.php @@ -0,0 +1,184 @@ +|bool + */ + protected $guarded = ['id']; + + /** + * All of the relationships to be touched. + * + * @var array + */ + protected $touches = ['contact']; + + /** + * The attributes that should be mutated to dates. + * + * @var array + */ + protected $dates = ['date']; + + /** + * The attributes that are mass assignable. + * + * @var array + */ + protected $fillable = [ + 'contact_id', + 'account_id', + ]; + + /** + * The attributes that should be cast to native types. + * + * @var array + */ + protected $casts = [ + 'is_age_based' => 'boolean', + 'is_year_unknown' => 'boolean', + ]; + + /** + * Get the account record associated with the special date. + * + * @return BelongsTo + */ + public function account() + { + return $this->belongsTo(Account::class); + } + + /** + * Get the contact record associated with the special date. + * + * @return BelongsTo + */ + public function contact() + { + return $this->belongsTo(Contact::class); + } + + /** + * Returns a short version of the date, taking into account if the year is + * unknown or not. This will return either `July 21` or `July 21, 2017`. + */ + public function toShortString() + { + if ($this->is_year_unknown) { + return DateHelper::getShortDateWithoutYear($this->date); + } + + return DateHelper::getShortDate($this->date); + } + + /** + * Returns the age that the date represents, if the date is set and if it's + * not based on a year we don't know. + * + * @return int|null + */ + public function getAge(): ?int + { + if (is_null($this->date)) { + return null; + } + + if ($this->is_year_unknown) { + return null; + } + + return $this->date->diffInYears(now()); + } + + /** + * Create a SpecialDate from an age. + * + * @param int $age + */ + public function createFromAge(int $age) + { + $this->is_age_based = true; + $this->date = now(DateHelper::getTimezone())->subYears($age)->month(1)->day(1); + $this->save(); + + return $this; + } + + /** + * Create a SpecialDate from an actual date, that might not contain a year. + * + * @param int $year + * @param int $month + * @param int $day + */ + public function createFromDate(int $year, int $month, int $day) + { + // year 0 represents the `unknown` choice in the dropdown representing + // the years + if ($year != 0) { + $date = Carbon::createFromDate($year, $month, $day); + $this->is_year_unknown = false; + } else { + $date = Carbon::createFromDate(now()->year, $month, $day); + $this->is_year_unknown = true; + } + + $this->date = $date; + $this->save(); + + return $this; + } + + /** + * Associates a special date to a contact. + * + * @param Contact $contact + */ + public function setToContact(Contact $contact) + { + $this->account_id = $contact->account_id; + $this->contact_id = $contact->id; + $this->save(); + + return $this; + } +} diff --git a/app/Models/Instance/Statistic.php b/app/Models/Instance/Statistic.php new file mode 100644 index 0000000..e856d86 --- /dev/null +++ b/app/Models/Instance/Statistic.php @@ -0,0 +1,9 @@ +|bool + */ + protected $guarded = ['id']; + + protected $dates = [ + 'date', + ]; + + /** + * Get the account record associated with the debt. + */ + public function account() + { + return $this->belongsTo(Account::class); + } + + /** + * Get all the information of the Entry for the journal. + * + * @return array + */ + public function getInfoForJournalEntry() + { + return [ + 'type' => 'day', + 'id' => $this->id, + 'rate' => $this->rate, + 'comment' => $this->comment, + 'date' => $this->date, + 'day' => $this->date->day, + 'day_name' => mb_convert_case(DateHelper::getShortDay($this->date), MB_CASE_TITLE, 'UTF-8'), + 'month' => $this->date->month, + 'month_name' => mb_convert_case(DateHelper::getShortMonth($this->date), MB_CASE_UPPER, 'UTF-8'), + 'year' => $this->date->year, + 'happens_today' => $this->date->isToday(), + ]; + } +} diff --git a/app/Models/Journal/Entry.php b/app/Models/Journal/Entry.php new file mode 100644 index 0000000..fd124b3 --- /dev/null +++ b/app/Models/Journal/Entry.php @@ -0,0 +1,83 @@ +|bool + */ + protected $guarded = ['id']; + + /** + * The attributes that are mass assignable. + * + * @var array + */ + protected $fillable = [ + 'account_id', + 'title', + 'post', + ]; + + /** + * Get the account record associated with the entry. + * + * @return BelongsTo + */ + public function account() + { + return $this->belongsTo(Account::class); + } + + /** + * Get the Entry date. + * + * @param string $value + * @return \Carbon\Carbon + */ + public function getDateAttribute($value) + { + // Default to created_at, but show journalEntry->date if the entry type is JournalEntry + return $this->journalEntry ? $this->journalEntry->date : $this->created_at; + } + + /** + * Get all the information of the Entry for the journal. + * + * @return array + */ + public function getInfoForJournalEntry() + { + return [ + 'type' => 'entry', + 'id' => $this->id, + 'title' => $this->title, + 'post' => $this->post, + 'day' => $this->date->day, + 'day_name' => mb_convert_case(DateHelper::getShortDay($this->date), MB_CASE_TITLE, 'UTF-8'), + 'month' => $this->date->month, + 'month_name' => mb_convert_case(DateHelper::getShortMonth($this->date), MB_CASE_UPPER, 'UTF-8'), + 'year' => $this->date->year, + 'date' => $this->date, + 'created_at' => DateHelper::getShortDateWithTime($this->created_at), + ]; + } +} diff --git a/app/Models/Journal/JournalEntry.php b/app/Models/Journal/JournalEntry.php new file mode 100644 index 0000000..f3f10e9 --- /dev/null +++ b/app/Models/Journal/JournalEntry.php @@ -0,0 +1,127 @@ +|bool + */ + protected $guarded = ['id']; + + protected $table = 'journal_entries'; + + protected $dates = [ + 'date', + ]; + + /** + * Eager load with every entry. + */ + protected $with = [ + 'journalable', + ]; + + /** + * Get all of the owning "journal-able" models. + * + * @return MorphTo + */ + public function journalable() + { + return $this->morphTo(); + } + + /** + * Get the account record associated with the journal entry. + * + * @return BelongsTo + */ + public function account() + { + return $this->belongsTo(Account::class); + } + + /** + * Adds a new entry in the journal. + * + * @param \App\Interfaces\IsJournalableInterface $resourceToLog + * @return self + */ + public static function add(IsJournalableInterface $resourceToLog): self + { + $journal = new self; + $journal->account_id = $resourceToLog->account_id; + $journal->date = now(DateHelper::getTimezone()); + if ($resourceToLog instanceof \App\Models\Account\Activity) { + $journal->date = $resourceToLog->happened_at; + } elseif ($resourceToLog instanceof \App\Models\Journal\Entry) { + $journal->date = $resourceToLog->attributes['date']; + } + $journal->save(); + $resourceToLog->journalEntries()->save($journal); + + return $journal; + } + + /** + * Update an entry in the journal. + * + * @param \App\Interfaces\IsJournalableInterface $resourceToLog + * @return self + */ + public function edit(IsJournalableInterface $resourceToLog): self + { + if ($resourceToLog instanceof \App\Models\Journal\Entry) { + $this->date = $resourceToLog->attributes['date']; + } + $this->save(); + + return $this; + } + + /** + * Get the information about the object represented by the Journal Entry. + * + * @return array + */ + public function getObjectData() + { + // Instantiating the object + /** @var IsJournalableInterface */ + $correspondingObject = $this->journalable; + + return $correspondingObject->getInfoForJournalEntry(); + } + + /** + * Filter by real entry (day rate or journal entry). + * + * @param Builder $query + * @return Builder + */ + public function scopeEntry(Builder $query): Builder + { + return $query->where('journalable_type', '!=', 'App\Models\Account\Activity'); + } +} diff --git a/app/Models/ModelBinding.php b/app/Models/ModelBinding.php new file mode 100644 index 0000000..b9e12c6 --- /dev/null +++ b/app/Models/ModelBinding.php @@ -0,0 +1,27 @@ +where('account_id', Auth::user()->account_id) + ->where($this->getRouteKeyName(), $value) + ->firstOrFail(); + } +} diff --git a/app/Models/ModelBindingHasher.php b/app/Models/ModelBindingHasher.php new file mode 100644 index 0000000..2771e0f --- /dev/null +++ b/app/Models/ModelBindingHasher.php @@ -0,0 +1,11 @@ +parameter('contact'); + + if (Auth::guest() || is_null($contact)) { + return null; + } + + return $this->where('account_id', Auth::user()->account_id) + ->where('contact_id', $contact->id) + ->where($this->getRouteKeyName(), $value) + ->firstOrFail(); + } +} diff --git a/app/Models/Relationship/Relationship.php b/app/Models/Relationship/Relationship.php new file mode 100644 index 0000000..590d570 --- /dev/null +++ b/app/Models/Relationship/Relationship.php @@ -0,0 +1,99 @@ + + */ + protected $fillable = [ + 'account_id', + 'contact_is', + 'of_contact', + 'relationship_type_id', + ]; + + /** + * Get the account record associated with the relationship. + * + * @return BelongsTo + */ + public function account() + { + return $this->belongsTo(Account::class); + } + + /** + * Get the contact record associated with the relationship. + * + * @return BelongsTo + */ + public function contactIs() + { + return $this->belongsTo(Contact::class, 'contact_is'); + } + + /** + * Get the contact record connected with the relationship. + * + * @return BelongsTo + */ + public function ofContact() + { + return $this->belongsTo(Contact::class, 'of_contact'); + } + + /** + * Get the relationship type record associated with the relationship. + * + * @return BelongsTo + */ + public function relationshipType() + { + return $this->belongsTo(RelationshipType::class, 'relationship_type_id'); + } + + /** + * Get the reverser relationship of this one. + * + * @return self|null + */ + public function reverseRelationship(): ?self + { + $reverseRelationshipType = $this->relationshipType->reverseRelationshipType(); + if ($reverseRelationshipType) { + return self::where([ + 'account_id'=> $this->account_id, + 'contact_is' => $this->of_contact, + 'of_contact' => $this->contact_is, + 'relationship_type_id' => $reverseRelationshipType->id, + ])->first(); + } + + return null; + } +} diff --git a/app/Models/Relationship/RelationshipType.php b/app/Models/Relationship/RelationshipType.php new file mode 100644 index 0000000..e608c32 --- /dev/null +++ b/app/Models/Relationship/RelationshipType.php @@ -0,0 +1,125 @@ +|bool + */ + protected $guarded = ['id']; + + protected $table = 'relationship_types'; + + /** + * The attributes that should be cast to native types. + * + * @var array + */ + protected $casts = [ + 'delible' => 'boolean', + ]; + + /** + * Get the account record associated with the reminder. + * + * @return BelongsTo + */ + public function account() + { + return $this->belongsTo(Account::class); + } + + /** + * Get the relationship type group record associated with the reminder. + * + * @return BelongsTo + */ + public function relationshipTypeGroup() + { + return $this->belongsTo(RelationshipTypeGroup::class); + } + + /** + * Get the reverser relationship type of this one. + * + * @return self|null + */ + public function reverseRelationshipType() + { + return $this->account->getRelationshipTypeByType($this->name_reverse_relationship); + } + + /** + * Get the i18n version of the name attribute, like "Significant other". + * + * @psalm-suppress InvalidReturnType + * @psalm-suppress InvalidReturnStatement + * + * @param Contact $contact + * @param bool $includeOpposite + * @param string $gender + * @return string|null|\Illuminate\Contracts\Translation\Translator + */ + public function getLocalizedName(Contact $contact = null, bool $includeOpposite = false, string $gender = null) + { + $defaultGender = AccountHelper::getDefaultGender($this->account); + + if (is_null($gender)) { + $gender = $defaultGender; + } + + $femaleVersion = trans('app.relationship_type_'.$this->name.'_female'); + $maleVersion = trans('app.relationship_type_'.$this->name.'_male'); + if ($maleVersion === 'app.relationship_type_'.$this->name.'_male') { + $maleVersion = trans('app.relationship_type_'.$this->name); + } + + if (! is_null($contact)) { + $maleVersionWithName = trans('app.relationship_type_'.$this->name.'_male_with_name', ['name' => $contact->name]); + if ($maleVersionWithName === 'app.relationship_type_'.$this->name.'_male_with_name') { + $maleVersionWithName = trans('app.relationship_type_'.$this->name.'_with_name'); + } + $femaleVersionWithName = trans('app.relationship_type_'.$this->name.'_female_with_name', ['name' => $contact->name]); + + // include the reverse of the relation in the string (masculine/feminine) + // this is used in the dropdown of the relationship types when creating + // or deleting a relationship. + if ($includeOpposite) { + // in some language, masculine and feminine version of a relationship type is the same. + // we need to keep just one version in that case. + if ($femaleVersion === $maleVersion) { + // `Maazarin's significant other` + return $maleVersionWithName; + } + + return $defaultGender === Gender::FEMALE ? + // `Maazarin's aunt/uncle` + $femaleVersionWithName.'/'.$maleVersion : + // `Maazarin's uncle/aunt` + $maleVersionWithName.'/'.$femaleVersion; + } else { + return $gender === Gender::FEMALE ? + // `Maazarin's aunt` + $femaleVersionWithName : + // `Maazarin's uncle` + $maleVersionWithName; + } + } + + return $gender === Gender::FEMALE ? + // `aunt` + $femaleVersion : + // `uncle` + $maleVersion; + } +} diff --git a/app/Models/Relationship/RelationshipTypeGroup.php b/app/Models/Relationship/RelationshipTypeGroup.php new file mode 100644 index 0000000..b27ac74 --- /dev/null +++ b/app/Models/Relationship/RelationshipTypeGroup.php @@ -0,0 +1,38 @@ +|bool + */ + protected $guarded = ['id']; + + protected $table = 'relationship_type_groups'; + + /** + * The attributes that should be cast to native types. + * + * @var array + */ + protected $casts = [ + 'delible' => 'boolean', + ]; + + /** + * Get the account record associated with the reminder. + * + * @return BelongsTo + */ + public function account() + { + return $this->belongsTo(Account::class); + } +} diff --git a/app/Models/Settings/Currency.php b/app/Models/Settings/Currency.php new file mode 100644 index 0000000..bc050ab --- /dev/null +++ b/app/Models/Settings/Currency.php @@ -0,0 +1,12 @@ +|bool + */ + protected $guarded = ['id']; + + /** + * Get the user record associated with the term. + */ + public function users() + { + return $this->belongsToMany(User::class)->withPivot('user_id')->withTimestamps(); + } +} diff --git a/app/Models/User/Changelog.php b/app/Models/User/Changelog.php new file mode 100644 index 0000000..06f0202 --- /dev/null +++ b/app/Models/User/Changelog.php @@ -0,0 +1,54 @@ +|bool + */ + protected $guarded = ['id']; + + /** + * The attributes that should be mutated to dates. + * + * @var array + */ + protected $dates = [ + 'created_at', + ]; + + /** + * Get the user records associated with the tag. + */ + public function users() + { + return $this->belongsToMany(User::class)->withPivot('read', 'upvote')->withTimestamps(); + } + + /** + * Return the markdown parsed description. + * + * @return string + */ + public function getDescriptionAttribute($value) + { + return (new Parsedown())->text($value); + } + + /** + * Return the created_at date in a friendly format. + * + * @return string + */ + public function getCreatedAtAttribute($value) + { + return DateHelper::getShortDate($value); + } +} diff --git a/app/Models/User/Module.php b/app/Models/User/Module.php new file mode 100644 index 0000000..807ecb0 --- /dev/null +++ b/app/Models/User/Module.php @@ -0,0 +1,52 @@ +|bool + */ + protected $guarded = ['id']; + + /** + * The attributes that should be cast to native types. + * + * @var array + */ + protected $casts = [ + 'active' => 'boolean', + 'delible' => 'boolean', + ]; + + /** + * Get the account record associated with the module. + * + * @return BelongsTo + */ + public function account() + { + return $this->belongsTo(Account::class); + } + + /** + * Scope a query to only include modules that are active. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @return \Illuminate\Database\Eloquent\Builder + */ + public function scopeActive($query) + { + return $query->where('active', true); + } +} diff --git a/app/Models/User/RecoveryCode.php b/app/Models/User/RecoveryCode.php new file mode 100644 index 0000000..1a5adbf --- /dev/null +++ b/app/Models/User/RecoveryCode.php @@ -0,0 +1,33 @@ + + */ + protected $fillable = [ + 'account_id', + 'user_id', + 'recovery', + ]; + + /** + * Scope a query to only include unused code. + * + * @param Builder $query + * @return Builder + */ + public function scopeUnused($query) + { + return $query->where('used', 0); + } +} diff --git a/app/Models/User/SyncToken.php b/app/Models/User/SyncToken.php new file mode 100644 index 0000000..6416fde --- /dev/null +++ b/app/Models/User/SyncToken.php @@ -0,0 +1,29 @@ +|bool + */ + protected $guarded = ['id']; + + /** + * The attributes that are mass assignable. + * + * @var array + */ + protected $fillable = [ + 'account_id', + 'user_id', + 'name', + 'timestamp', + ]; +} diff --git a/app/Models/User/User.php b/app/Models/User/User.php new file mode 100644 index 0000000..d614f27 --- /dev/null +++ b/app/Models/User/User.php @@ -0,0 +1,308 @@ +|bool + */ + protected $guarded = ['id']; + + /** + * The attributes that are mass assignable. + * + * @var array + */ + protected $fillable = [ + 'first_name', + 'last_name', + 'email', + 'password', + 'timezone', + 'locale', + 'currency_id', + 'fluid_container', + 'temperature_scale', + 'name_order', + 'google2fa_secret', + ]; + + /** + * Eager load account with every user. + */ + protected $with = ['account']; + + /** + * The attributes that should be hidden for arrays. + * + * @var array + */ + protected $hidden = [ + 'password', 'remember_token', 'google2fa_secret', + ]; + + /** + * The attributes that should be cast to native types. + * + * @var array + */ + protected $casts = [ + 'profile_new_life_event_badge_seen' => 'boolean', + 'admin' => 'boolean', + 'fluid_container' => 'boolean', + ]; + + /** + * Available names order. + * + * @var array + */ + public const NAMES_ORDER = [ + 'firstname_lastname', + 'lastname_firstname', + 'firstname_lastname_nickname', + 'firstname_nickname_lastname', + 'lastname_firstname_nickname', + 'lastname_nickname_firstname', + 'nickname_firstname_lastname', + 'nickname_lastname_firstname', + 'nickname_bracketed_firstname_lastname', + 'nickname', + ]; + + /** + * Get the account record associated with the user. + * + * @return BelongsTo + */ + public function account() + { + return $this->belongsTo(Account::class); + } + + /** + * Get the contact record associated with the 'me' contact. + * + * @return HasOne + */ + public function me() + { + return $this->hasOne(Contact::class, 'id', 'me_contact_id'); + } + + /** + * Get the term records associated with the user. + * + * @return BelongsToMany + */ + public function terms() + { + return $this->belongsToMany(Term::class)->withPivot('ip_address')->withTimestamps(); + } + + /** + * Get the recovery codes associated with the user. + * + * @return HasMany + */ + public function recoveryCodes() + { + return $this->hasMany(RecoveryCode::class); + } + + /** + * Gets the currency for this user. + * + * @return BelongsTo + */ + public function currency() + { + return $this->belongsTo(Currency::class); + } + + /** + * Assigns a default value just in case the sort order is empty. + * + * @param string $value + * @return string + */ + public function getContactsSortOrderAttribute($value): string + { + return ! empty($value) ? $value : 'firstnameAZ'; + } + + /** + * Indicates if the layout is fluid or not for the UI. + * + * @return string + */ + public function getFluidLayout(): string + { + if ($this->fluid_container) { + return 'container-fluid'; + } else { + return 'container'; + } + } + + /** + * Get users's full name. The name is formatted according to the user's + * preference, either "Firstname Lastname", or "Lastname Firstname". + * + * @return string + */ + public function getNameAttribute(): string + { + $completeName = ''; + + if (FormHelper::getNameOrderForForms($this) === 'firstname') { + $completeName = $this->first_name; + + if ($this->last_name !== '') { + $completeName = $completeName.' '.$this->last_name; + } + } else { + if ($this->last_name !== '') { + $completeName = $this->last_name; + } + + $completeName = $completeName.' '.$this->first_name; + } + + return $completeName; + } + + /** + * Ecrypt the user's google_2fa secret. + * + * @param string $value + * @return void + */ + public function setGoogle2faSecretAttribute($value): void + { + $this->attributes['google2fa_secret'] = encrypt($value); + } + + /** + * Decrypt the user's google_2fa secret. + * + * @param string|null $value + * @return string|null + */ + public function getGoogle2faSecretAttribute($value): ?string + { + return is_null($value) ? null : decrypt($value); + } + + /** + * Indicate if the user has accepted the most current terms and privacy. + * + * @param string|null $value + * @return bool + */ + public function getPolicyCompliantAttribute($value): bool + { + return ComplianceHelper::isCompliantWithCurrentTerm($this); + } + + /** + * Indicate whether the user should be reminded at this time. + * This is affected by the user settings regarding the hour of the day he + * wants to be reminded. + * + * @param Carbon|null $date + * @return bool + */ + public function isTheRightTimeToBeReminded($date) + { + if (is_null($date)) { + return false; + } + + $now = now($this->timezone); + $isTheRightTime = true; + + // compare date with current date for the user + if (! $date->isSameDay($now)) { + $isTheRightTime = false; + } + + // compare current hour for the user with the hour they want to be + // reminded as per the hour set on the profile + if (! $now->isSameHour($this->account->default_time_reminder_is_sent)) { + $isTheRightTime = false; + } + + return $isTheRightTime; + } + + /** + * Send the email verification notification. + * + * @return void + */ + public function sendEmailVerificationNotification(): void + { + /** @var int $count */ + $count = Account::count(); + if (config('monica.signup_double_optin') && $count > 1) { + SendVerifyEmail::dispatch($this); + } + } + + /** + * Get the preferred locale of the entity. + * + * @return string|null + */ + public function preferredLocale() + { + return $this->locale; + } + + /** + * Try using a recovery code. + * + * @param string $recovery + * @return bool + */ + public function recoveryChallenge(string $recovery): bool + { + $recoveryCodes = $this->recoveryCodes()->unused()->get(); + + foreach ($recoveryCodes as $recoveryCode) { + if ($recoveryCode->recovery === $recovery) { + $recoveryCode->used = true; + $recoveryCode->save(); + + return true; + } + } + + return false; + } +} diff --git a/app/Notifications/EmailMessaging.php b/app/Notifications/EmailMessaging.php new file mode 100644 index 0000000..ab71082 --- /dev/null +++ b/app/Notifications/EmailMessaging.php @@ -0,0 +1,48 @@ +locale); + + return (new MailMessage) + ->subject(trans('mail.confirmation_email_title')) + ->line(trans('mail.confirmation_email_title')) + ->line(trans('mail.confirmation_email_intro')) + ->action(trans('mail.confirmation_email_button'), $verificationUrl) + ->line(trans('mail.confirmation_email_bottom')); + } + + /** + * Get the mail representation to reset a password. + * + * @param User $user + * @return MailMessage + */ + public static function resetPasswordMail(User $user, $token): MailMessage + { + App::setLocale($user->locale); + + return (new MailMessage) + ->subject(trans('mail.password_reset_title')) + ->line(trans('mail.password_reset_title')) + ->line(trans('mail.password_reset_intro')) + ->action(trans('mail.password_reset_button'), url(Str::of(config('app.url'))->ltrim('/').route('password.reset', ['token' => $token, 'email' => $user->getEmailForPasswordReset()], false))) + ->line(trans('mail.password_reset_expiration', ['count' => config('auth.passwords.'.config('auth.defaults.passwords').'.expire')])) + ->line(trans('mail.password_reset_bottom')); + } +} diff --git a/app/Notifications/ExportAccountDone.php b/app/Notifications/ExportAccountDone.php new file mode 100644 index 0000000..9db6fc0 --- /dev/null +++ b/app/Notifications/ExportAccountDone.php @@ -0,0 +1,65 @@ +exportJob = $exportJob->withoutRelations(); + $this->afterCommit(); + } + + /** + * Get the notification's delivery channels. + * + * @return array + */ + public function via() + { + return ['mail']; + } + + /** + * Get the mail representation of the notification. + * + * @param User $user + * @return \Illuminate\Notifications\Messages\MailMessage + */ + public function toMail(User $user): MailMessage + { + $date = Carbon::parse($this->exportJob->created_at) + ->setTimezone($user->timezone); + + return (new MailMessage) + ->success() + ->subject(trans('mail.export_title')) + ->greeting(trans('mail.greetings', ['username' => $user->first_name])) + ->line(trans('mail.export_description', ['date' => DateHelper::getShortDate($date)])) + ->action(trans('mail.export_download'), route('settings.export.index')); + } +} diff --git a/app/Notifications/InvitationMail.php b/app/Notifications/InvitationMail.php new file mode 100644 index 0000000..7f98c9d --- /dev/null +++ b/app/Notifications/InvitationMail.php @@ -0,0 +1,62 @@ +invitedBy; + $acceptInvitationUrl = $this->acceptInvitationUrl($invitation); + + return (new MailMessage) + ->subject(trans('mail.invitation_title', ['name' => $user->name])) + ->line(trans('mail.invitation_intro', ['name' => $user->name, 'email' => $user->email])) + ->line(trans('mail.invitation_link')) + ->action(trans('mail.invitation_button'), $acceptInvitationUrl) + ->line(trans('mail.invitation_expiration', ['count' => Config::get('auth.invitation.expire', 2)])); + } + + /** + * Get the verification URL for the given notifiable. + * + * @param Invitation $invitation + * @return string + */ + protected function acceptInvitationUrl(Invitation $invitation) + { + return URL::temporarySignedRoute( + 'invitations.accept', + now()->addDays(Config::get('auth.invitation.expire', 2)), + ['key' => $invitation->invitation_key] + ); + } +} diff --git a/app/Notifications/NewUserAlert.php b/app/Notifications/NewUserAlert.php new file mode 100644 index 0000000..284cbdf --- /dev/null +++ b/app/Notifications/NewUserAlert.php @@ -0,0 +1,53 @@ +user = $user; + } + + /** + * Get the notification's delivery channels. + * + * @return array + */ + public function via() + { + return ['mail']; + } + + /** + * Get the mail representation of the notification. + * + * @return MailMessage + */ + public function toMail(): MailMessage + { + return (new MailMessage) + ->subject("New registration: {$this->user->first_name} {$this->user->last_name}") + ->greeting('New registration') + ->line("User: {$this->user->first_name} {$this->user->last_name}") + ->line("ID: {$this->user->id}") + ->line("Email: {$this->user->email}"); + } +} diff --git a/app/Notifications/StayInTouchEmail.php b/app/Notifications/StayInTouchEmail.php new file mode 100644 index 0000000..ecf7c70 --- /dev/null +++ b/app/Notifications/StayInTouchEmail.php @@ -0,0 +1,73 @@ +contact = $contact; + } + + /** + * Get the notification's delivery channels. + * + * @return array + */ + public function via() + { + return ['mail']; + } + + /** + * Get the mail representation of the notification. + * + * @param User $user + * @return MailMessage + */ + public function toMail(User $user): MailMessage + { + return (new MailMessage) + ->subject(trans('mail.stay_in_touch_subject_line', ['name' => $this->contact->name])) + ->greeting(trans('mail.greetings', ['username' => $user->first_name])) + ->line(trans_choice('mail.stay_in_touch_subject_description', $this->contact->stay_in_touch_frequency, [ + 'name' => $this->contact->name, + 'frequency' => $this->contact->stay_in_touch_frequency, + ])) + ->action(trans('mail.footer_contact_info2', ['name' => $this->contact->name]), $this->contact->getLink()); + } + + /** + * Use in test to check the parameter notification. + * + * @param Contact $contact + * @return bool + */ + public function assertSentFor(Contact $contact): bool + { + return $contact->id == $this->contact->id; + } +} diff --git a/app/Notifications/UserNotified.php b/app/Notifications/UserNotified.php new file mode 100644 index 0000000..614c14e --- /dev/null +++ b/app/Notifications/UserNotified.php @@ -0,0 +1,83 @@ +reminder = $reminder; + $this->numberDaysBefore = $numberDaysBefore; + } + + /** + * Get the notification's delivery channels. + * + * @return array + */ + public function via() + { + return ['mail']; + } + + /** + * Get the mail representation of the notification. + * + * @param User $user + * @return \Illuminate\Notifications\Messages\MailMessage + */ + public function toMail(User $user): MailMessage + { + $contact = Contact::where('account_id', $user->account_id) + ->findOrFail($this->reminder->contact_id); + + $message = (new MailMessage) + ->subject(trans('mail.subject_line', ['contact' => $contact->name])) + ->greeting(trans('mail.greetings', ['username' => $user->first_name])) + ->line(trans_choice('mail.notification_description', $this->numberDaysBefore, [ + 'count' => $this->numberDaysBefore, + 'date' => DateHelper::getShortDate($this->reminder->calculateNextExpectedDate()), + ])) + ->line($this->reminder->title) + ->line(trans('mail.for', ['name' => $contact->name])) + ->action(trans('mail.footer_contact_info2', ['name' => $contact->name]), $contact->getLink()); + + if (! is_null($this->reminder->description)) { + $message = $message + ->line(trans('mail.comment', ['comment' => $this->reminder->description])); + } + + return $message; + } +} diff --git a/app/Notifications/UserReminded.php b/app/Notifications/UserReminded.php new file mode 100644 index 0000000..f1764bb --- /dev/null +++ b/app/Notifications/UserReminded.php @@ -0,0 +1,71 @@ +reminder = $reminder; + } + + /** + * Get the notification's delivery channels. + * + * @return array + */ + public function via() + { + return ['mail']; + } + + /** + * Get the mail representation of the notification. + * + * @param User $user + * @return \Illuminate\Notifications\Messages\MailMessage + */ + public function toMail(User $user): MailMessage + { + $contact = Contact::where('account_id', $user->account_id) + ->findOrFail($this->reminder->contact_id); + + $message = (new MailMessage) + ->subject(trans('mail.subject_line', ['contact' => $contact->name])) + ->greeting(trans('mail.greetings', ['username' => $user->first_name])) + ->line(trans('mail.want_reminded_of', ['reason' => $this->reminder->title])) + ->line(trans('mail.for', ['name' => $contact->name])) + ->action(trans('mail.footer_contact_info2', ['name' => $contact->name]), $contact->getLink()); + + if (! is_null($this->reminder->description)) { + $message = $message + ->line(trans('mail.comment', ['comment' => $this->reminder->description])); + } + + return $message; + } +} diff --git a/app/Policies/.gitkeep b/app/Policies/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/app/Policies/.gitkeep @@ -0,0 +1 @@ + diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php new file mode 100644 index 0000000..76604af --- /dev/null +++ b/app/Providers/AppServiceProvider.php @@ -0,0 +1,238 @@ +app->environment('production')) { + return Password::min(6); + } + $rules = Password::min(config('app.password_min')); + $config = explode(',', config('app.password_rules')); + if (in_array('mixedCase', $config)) { + $rules = $rules->mixedCase(); + } + if (in_array('letters', $config)) { + $rules = $rules->letters(); + } + if (in_array('numbers', $config)) { + $rules = $rules->numbers(); + } + if (in_array('symbols', $config)) { + $rules = $rules->symbols(); + } + if (in_array('uncompromised', $config)) { + $rules = $rules->uncompromised(); + } + + return $rules; + }); + + if (config('database.use_utf8mb4') + && DBHelper::connection()->getDriverName() == 'mysql' + && ! DBHelper::testVersion('5.7.7')) { + Schema::defaultStringLength(191); + } + + Cashier::useCustomerModel(\App\Models\Account\Account::class); + + VerifyEmail::toMailUsing(function ($user, $verificationUrl) { + return EmailMessaging::verifyEmailMail($user, $verificationUrl); + }); + ResetPassword::toMailUsing(function ($user, $token) { + return EmailMessaging::resetPasswordMail($user, $token); + }); + + Paginator::defaultView('vendor.pagination.default'); + + RateLimiter::for('GPSCoordinate', function () { + return [ + Limit::perMinute(60), + Limit::perDay(5000), + ]; + }); + + EtagConditionals::etagGenerateUsing(function (\Illuminate\Http\Request $request, \Symfony\Component\HttpFoundation\Response $response) { + $url = $request->getRequestUri(); + + return Cache::rememberForever('etag.'.$url, function () use ($url) { + return sha1($url); + }); + }); + } + + /** + * Register any application services. + * + * @return void + */ + public function register() + { + Passport::ignoreMigrations(); + Cashier::ignoreMigrations(); + Cashier::formatCurrencyUsing(function ($amount, $currency) { + $currency = \App\Models\Settings\Currency::where('iso', strtoupper($currency ?? config('cashier.currency')))->first(); + + return \App\Helpers\MoneyHelper::format($amount, $currency); + }); + } + + /** + * All of the container singletons that should be registered. + * + * @var array + */ + public $singletons = [ + \App\Services\Account\Activity\Activity\AttachContactToActivity::class => \App\Services\Account\Activity\Activity\AttachContactToActivity::class, + \App\Services\Account\Activity\Activity\CreateActivity::class => \App\Services\Account\Activity\Activity\CreateActivity::class, + \App\Services\Account\Activity\Activity\DestroyActivity::class => \App\Services\Account\Activity\Activity\DestroyActivity::class, + \App\Services\Account\Activity\Activity\UpdateActivity::class => \App\Services\Account\Activity\Activity\UpdateActivity::class, + \App\Services\Account\Activity\ActivityStatisticService::class => \App\Services\Account\Activity\ActivityStatisticService::class, + \App\Services\Account\Activity\ActivityTypeCategory\CreateActivityTypeCategory::class => \App\Services\Account\Activity\ActivityTypeCategory\CreateActivityTypeCategory::class, + \App\Services\Account\Activity\ActivityTypeCategory\DestroyActivityTypeCategory::class => \App\Services\Account\Activity\ActivityTypeCategory\DestroyActivityTypeCategory::class, + \App\Services\Account\Activity\ActivityTypeCategory\UpdateActivityTypeCategory::class => \App\Services\Account\Activity\ActivityTypeCategory\UpdateActivityTypeCategory::class, + \App\Services\Account\Activity\ActivityType\CreateActivityType::class => \App\Services\Account\Activity\ActivityType\CreateActivityType::class, + \App\Services\Account\Activity\ActivityType\DestroyActivityType::class => \App\Services\Account\Activity\ActivityType\DestroyActivityType::class, + \App\Services\Account\Activity\ActivityType\UpdateActivityType::class => \App\Services\Account\Activity\ActivityType\UpdateActivityType::class, + \App\Services\Account\Company\CreateCompany::class => \App\Services\Account\Company\CreateCompany::class, + \App\Services\Account\Company\DestroyCompany::class => \App\Services\Account\Company\DestroyCompany::class, + \App\Services\Account\Company\UpdateCompany::class => \App\Services\Account\Company\UpdateCompany::class, + \App\Services\Account\Settings\DestroyAllDocuments::class => \App\Services\Account\Settings\DestroyAllDocuments::class, + \App\Services\Account\Gender\CreateGender::class => \App\Services\Account\Gender\CreateGender::class, + \App\Services\Account\Gender\DestroyGender::class => \App\Services\Account\Gender\DestroyGender::class, + \App\Services\Account\Gender\UpdateGender::class => \App\Services\Account\Gender\UpdateGender::class, + \App\Services\Account\Photo\DestroyPhoto::class => \App\Services\Account\Photo\DestroyPhoto::class, + \App\Services\Account\Photo\UploadPhoto::class => \App\Services\Account\Photo\UploadPhoto::class, + \App\Services\Account\Place\CreatePlace::class => \App\Services\Account\Place\CreatePlace::class, + \App\Services\Account\Place\DestroyPlace::class => \App\Services\Account\Place\DestroyPlace::class, + \App\Services\Account\Place\UpdatePlace::class => \App\Services\Account\Place\UpdatePlace::class, + \App\Services\User\CreateUser::class => \App\Services\User\CreateUser::class, + \App\Services\Auth\Population\PopulateContactFieldTypesTable::class => \App\Services\Auth\Population\PopulateContactFieldTypesTable::class, + \App\Services\Auth\Population\PopulateLifeEventsTable::class => \App\Services\Auth\Population\PopulateLifeEventsTable::class, + \App\Services\Auth\Population\PopulateModulesTable::class => \App\Services\Auth\Population\PopulateModulesTable::class, + \App\Services\Contact\Avatar\GenerateDefaultAvatar::class => \App\Services\Contact\Avatar\GenerateDefaultAvatar::class, + \App\Services\Contact\Avatar\GetAdorableAvatarURL::class => \App\Services\Contact\Avatar\GetAdorableAvatarURL::class, + \App\Services\Contact\Avatar\GetAvatarsFromInternet::class => \App\Services\Contact\Avatar\GetAvatarsFromInternet::class, + \App\Services\Contact\Avatar\GetGravatar::class => \App\Services\Contact\Avatar\GetGravatar::class, + \App\Services\Contact\Avatar\GetGravatarURL::class => \App\Services\Contact\Avatar\GetGravatarURL::class, + \App\Services\Contact\Avatar\UpdateAvatar::class => \App\Services\Contact\Avatar\UpdateAvatar::class, + \App\Services\Contact\Address\CreateAddress::class => \App\Services\Contact\Address\CreateAddress::class, + \App\Services\Contact\Address\DestroyAddress::class => \App\Services\Contact\Address\DestroyAddress::class, + \App\Services\Contact\Address\UpdateAddress::class => \App\Services\Contact\Address\UpdateAddress::class, + \App\Services\Contact\Call\CreateCall::class => \App\Services\Contact\Call\CreateCall::class, + \App\Services\Contact\Call\DestroyCall::class => \App\Services\Contact\Call\DestroyCall::class, + \App\Services\Contact\Call\UpdateCall::class => \App\Services\Contact\Call\UpdateCall::class, + \App\Services\Contact\Contact\CreateContact::class => \App\Services\Contact\Contact\CreateContact::class, + \App\Services\Contact\Contact\DeleteMeContact::class => \App\Services\Contact\Contact\DeleteMeContact::class, + \App\Services\Contact\Contact\DestroyContact::class => \App\Services\Contact\Contact\DestroyContact::class, + \App\Services\Contact\Contact\SetMeContact::class => \App\Services\Contact\Contact\SetMeContact::class, + \App\Services\Contact\Contact\UpdateBirthdayInformation::class => \App\Services\Contact\Contact\UpdateBirthdayInformation::class, + \App\Services\Contact\Contact\UpdateContact::class => \App\Services\Contact\Contact\UpdateContact::class, + \App\Services\Contact\Contact\UpdateContactFoodPreferences::class => \App\Services\Contact\Contact\UpdateContactFoodPreferences::class, + \App\Services\Contact\Contact\UpdateContactIntroduction::class => \App\Services\Contact\Contact\UpdateContactIntroduction::class, + \App\Services\Contact\Contact\UpdateWorkInformation::class => \App\Services\Contact\Contact\UpdateWorkInformation::class, + \App\Services\Contact\Contact\UpdateDeceasedInformation::class => \App\Services\Contact\Contact\UpdateDeceasedInformation::class, + \App\Services\Contact\Conversation\AddMessageToConversation::class => \App\Services\Contact\Conversation\AddMessageToConversation::class, + \App\Services\Contact\Conversation\CreateConversation::class => \App\Services\Contact\Conversation\CreateConversation::class, + \App\Services\Contact\Conversation\DestroyConversation::class => \App\Services\Contact\Conversation\DestroyConversation::class, + \App\Services\Contact\Conversation\DestroyMessage::class => \App\Services\Contact\Conversation\DestroyMessage::class, + \App\Services\Contact\Conversation\UpdateConversation::class => \App\Services\Contact\Conversation\UpdateConversation::class, + \App\Services\Contact\Conversation\UpdateMessage::class => \App\Services\Contact\Conversation\UpdateMessage::class, + \App\Services\Contact\Document\DestroyDocument::class => \App\Services\Contact\Document\DestroyDocument::class, + \App\Services\Contact\Document\UploadDocument::class => \App\Services\Contact\Document\UploadDocument::class, + \App\Services\Contact\Gift\AssociatePhotoToGift::class => \App\Services\Contact\Gift\AssociatePhotoToGift::class, + \App\Services\Contact\Gift\CreateGift::class => \App\Services\Contact\Gift\CreateGift::class, + \App\Services\Contact\Gift\DestroyGift::class => \App\Services\Contact\Gift\DestroyGift::class, + \App\Services\Contact\Gift\UpdateGift::class => \App\Services\Contact\Gift\UpdateGift::class, + \App\Services\Contact\Label\UpdateAddressLabels::class => \App\Services\Contact\Label\UpdateAddressLabels::class, + \App\Services\Contact\Label\UpdateContactFieldLabels::class => \App\Services\Contact\Label\UpdateContactFieldLabels::class, + \App\Services\Contact\LifeEvent\CreateLifeEvent::class => \App\Services\Contact\LifeEvent\CreateLifeEvent::class, + \App\Services\Contact\LifeEvent\DestroyLifeEvent::class => \App\Services\Contact\LifeEvent\DestroyLifeEvent::class, + \App\Services\Contact\LifeEvent\UpdateLifeEvent::class => \App\Services\Contact\LifeEvent\UpdateLifeEvent::class, + \App\Services\Contact\Occupation\CreateOccupation::class => \App\Services\Contact\Occupation\CreateOccupation::class, + \App\Services\Contact\Occupation\DestroyOccupation::class => \App\Services\Contact\Occupation\DestroyOccupation::class, + \App\Services\Contact\Occupation\UpdateOccupation::class => \App\Services\Contact\Occupation\UpdateOccupation::class, + \App\Services\Contact\Relationship\CreateRelationship::class => \App\Services\Contact\Relationship\CreateRelationship::class, + \App\Services\Contact\Relationship\DestroyRelationship::class => \App\Services\Contact\Relationship\DestroyRelationship::class, + \App\Services\Contact\Relationship\UpdateRelationship::class => \App\Services\Contact\Relationship\UpdateRelationship::class, + \App\Services\Contact\Reminder\CreateReminder::class => \App\Services\Contact\Reminder\CreateReminder::class, + \App\Services\Contact\Reminder\DestroyReminder::class => \App\Services\Contact\Reminder\DestroyReminder::class, + \App\Services\Contact\Reminder\UpdateReminder::class => \App\Services\Contact\Reminder\UpdateReminder::class, + \App\Services\Contact\Tag\AssociateTag::class => \App\Services\Contact\Tag\AssociateTag::class, + \App\Services\Contact\Tag\CreateTag::class => \App\Services\Contact\Tag\CreateTag::class, + \App\Services\Contact\Tag\DestroyTag::class => \App\Services\Contact\Tag\DestroyTag::class, + \App\Services\Contact\Tag\DetachTag::class => \App\Services\Contact\Tag\DetachTag::class, + \App\Services\Contact\Tag\UpdateTag::class => \App\Services\Contact\Tag\UpdateTag::class, + \App\Services\Instance\IdHasher::class => \App\Services\Instance\IdHasher::class, + \App\Services\Instance\Geolocalization\GetGPSCoordinate::class => \App\Services\Instance\Geolocalization\GetGPSCoordinate::class, + \App\Services\Instance\Weather\GetWeatherInformation::class => \App\Services\Instance\Weather\GetWeatherInformation::class, + \App\Services\Task\CreateTask::class => \App\Services\Task\CreateTask::class, + \App\Services\Task\DestroyTask::class => \App\Services\Task\DestroyTask::class, + \App\Services\Task\UpdateTask::class => \App\Services\Task\UpdateTask::class, + \App\Services\User\EmailChange::class => \App\Services\User\EmailChange::class, + \App\Services\VCalendar\ExportTask::class => \App\Services\VCalendar\ExportTask::class, + \App\Services\VCalendar\ExportVCalendar::class => \App\Services\VCalendar\ExportVCalendar::class, + \App\Services\VCalendar\ImportTask::class => \App\Services\VCalendar\ImportTask::class, + \App\Services\VCard\ExportVCard::class => \App\Services\VCard\ExportVCard::class, + \App\Services\VCard\ImportVCard::class => \App\Services\VCard\ImportVCard::class, + \App\Services\Account\Settings\SqlExportAccount::class => \App\Services\Account\Settings\SqlExportAccount::class, + \App\Services\Account\Settings\ResetAccount::class => \App\Services\Account\Settings\ResetAccount::class, + \App\Services\Account\Settings\DestroyAccount::class => \App\Services\Account\Settings\DestroyAccount::class, + \App\Services\Instance\AuditLog\LogAccountAction::class => \App\Services\Instance\AuditLog\LogAccountAction::class, + \App\Services\User\UpdateViewPreference::class => \App\Services\User\UpdateViewPreference::class, + \App\Services\User\AcceptPolicy::class => \App\Services\User\AcceptPolicy::class, + ]; +} diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php new file mode 100644 index 0000000..d713781 --- /dev/null +++ b/app/Providers/AuthServiceProvider.php @@ -0,0 +1,31 @@ + + */ + protected $policies = [ + // 'App\Models\Model' => 'App\Policies\ModelPolicy', + ]; + + /** + * Register any application authentication / authorization services. + * + * @return void + */ + public function boot(Request $request) + { + $this->registerPolicies(); + + Passport::ignoreCsrfToken(in_array($request->method(), ['HEAD', 'GET', 'OPTIONS'])); + } +} diff --git a/app/Providers/BroadcastServiceProvider.php b/app/Providers/BroadcastServiceProvider.php new file mode 100644 index 0000000..6c7434f --- /dev/null +++ b/app/Providers/BroadcastServiceProvider.php @@ -0,0 +1,19 @@ +nodes(); + }); + LaravelSabre::plugins(function () { + return $this->plugins(); + }); + LaravelSabre::auth(function (\Illuminate\Http\Request $request): bool { + if (! $request->user()) { + return false; + } + + if ($request->user()->admin || + config('laravelsabre.users') == null) { + return true; + } + + $users = explode(',', config('laravelsabre.users')); + $filtered = Arr::where($users, function ($value, $key) use ($request) { + return $value === $request->user()->email; + }); + + return count($filtered) > 0; + }); + } + + /** + * List of nodes for DAV Collection. + */ + private function nodes(): array + { + $user = Auth::user(); + + // Initiate custom backends for link between Sabre and Monica + $principalBackend = app(PrincipalBackend::class)->init($user); // User rights + $carddavBackend = app(CardDAVBackend::class)->init($user); // Contacts + $caldavBackend = app(CalDAVBackend::class)->init($user); // Calendar + + return [ + new PrincipalCollection($principalBackend), + new AddressBookRoot($principalBackend, $carddavBackend), + new CalendarRoot($principalBackend, $caldavBackend), + ]; + } + + /** + * List of Sabre plugins. + */ + private function plugins() + { + // Authentication backend + $authBackend = new AuthBackend(); + yield new AuthPlugin($authBackend); + + // CardDAV plugin + yield new CardDAVPlugin(); + yield new VCFExportPlugin(); + + // CalDAV plugin + yield new CalDAVPlugin(); + yield new ICSExportPlugin(); + + // Sync Plugin - rfc6578 + yield new SyncPlugin(); + + // ACL plugnin + $aclPlugin = new AclPlugin(); + $aclPlugin->allowUnauthenticatedAccess = false; + $aclPlugin->hideNodesFromListings = true; + yield $aclPlugin; + + // In local environment add browser plugin + if (App::environment('local')) { + yield new BrowserPlugin(false); + } else { + yield new DAVRedirect(); + } + } +} diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php new file mode 100644 index 0000000..9bcc63b --- /dev/null +++ b/app/Providers/EventServiceProvider.php @@ -0,0 +1,31 @@ +> + */ + protected $listen = [ + \Illuminate\Auth\Events\Registered::class => [ + \Illuminate\Auth\Listeners\SendEmailVerificationNotification::class, + ], + \Illuminate\Auth\Events\PasswordReset::class => [ + \App\Listeners\LogoutUserDevices::class, + ], + ]; + + /** + * The subscriber classes to register. + * + * @var array + */ + protected $subscribe = [ + \App\Listeners\LoginListener::class, + ]; +} diff --git a/app/Providers/MacroServiceProvider.php b/app/Providers/MacroServiceProvider.php new file mode 100644 index 0000000..d4b3615 --- /dev/null +++ b/app/Providers/MacroServiceProvider.php @@ -0,0 +1,47 @@ +map(function ($item) { + return $item->uuid; + })->toArray(); + }); + } + } +} diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php new file mode 100644 index 0000000..168d6a3 --- /dev/null +++ b/app/Providers/RouteServiceProvider.php @@ -0,0 +1,123 @@ +ltrim('/')); + } + + if (App::environment('production')) { + URL::forceScheme('https'); + } + + $this->configureRateLimiting(); + + Route::bind('contact', function ($value) { + // In case the user is logged out + if (! Auth::check()) { + redirect()->route('loginRedirect')->send(); + + return; + } + + try { + $id = app(IdHasher::class)->decodeId($value); + + return Contact::where('account_id', auth()->user()->account_id) + ->findOrFail($id); + } catch (WrongIdException $ex) { + redirect()->route('people.missing')->send(); + } catch (ModelNotFoundException $ex) { + redirect()->route('people.missing')->send(); + } + }); + + Route::model('otherContact', Contact::class); + } + + /** + * Define the routes for the application. + */ + public function map(): void + { + Route::prefix('api') + ->middleware('api') + ->namespace($this->namespace.'\Api') + ->group(base_path('routes/api.php')); + + Route::prefix('oauth') + ->namespace($this->namespace.'\Api') + ->group(base_path('routes/oauth.php')); + + Route::middleware('web') + ->namespace($this->namespace) + ->group(base_path('routes/web.php')); + + Route::middleware('web') + ->namespace($this->namespace) + ->group(base_path('routes/special.php')); + } + + /** + * Configure the rate limiters for the application. + * + * @return void + */ + protected function configureRateLimiting() + { + RateLimiter::for('api', function (Request $request) { + return Limit::perMinute(config('monica.rate_limit_api')) + ->by(optional($request->user())->id ?: RequestHelper::ip()) + ->response(function (Request $request, array $headers) { + $message = [ + 'error' => [ + 'message' => config('api.error_codes.34'), + 'error_code' => 34, + ], + ]; + + return new JsonResponse($message, 429, $headers); + }); + }); + RateLimiter::for('oauth', function (Request $request) { + return Limit::perMinute(config('monica.rate_limit_oauth'))->by($request->input('email') ?: RequestHelper::ip()); + }); + } +} diff --git a/app/Services/Account/Activity/Activity/AttachContactToActivity.php b/app/Services/Account/Activity/Activity/AttachContactToActivity.php new file mode 100644 index 0000000..a4d3356 --- /dev/null +++ b/app/Services/Account/Activity/Activity/AttachContactToActivity.php @@ -0,0 +1,88 @@ + 'required|integer|exists:accounts,id', + 'activity_id' => 'required|integer|exists:activities,id', + 'contacts' => 'required|array', + ]; + } + + /** + * Validate all datas to execute the service. + * + * @param array $data + * @return bool + */ + public function validate(array $data): bool + { + parent::validate($data); + + Activity::where('account_id', $data['account_id']) + ->findOrFail($data['activity_id']); + + foreach ($data['contacts'] as $contactId) { + Contact::where('account_id', $data['account_id']) + ->findOrFail($contactId); + } + + return true; + } + + /** + * Attach contacts to an activity. + * + * @param array $data + * @return Activity + */ + public function execute(array $data): Activity + { + $this->validate($data); + + /** @var Activity */ + $activity = Activity::find($data['activity_id']); + + $this->attach($data, $activity); + + return $activity; + } + + /** + * Create the association. + * + * @param array $data + * @param Activity $activity + * @return void + */ + private function attach(array $data, Activity $activity) + { + $attendees = []; + foreach ($data['contacts'] as $contact) { + $attendees[$contact] = ['account_id' => $activity->account_id]; + } + + // sync attendees: old contacts will be detached automatically + $changes = $activity->contacts()->sync($attendees); + + foreach ($changes as $change) { + // detached, attached, and updated attendees + foreach ($change as $contactId) { + Contact::find($contactId)->calculateActivitiesStatistics(); + } + } + } +} diff --git a/app/Services/Account/Activity/Activity/CreateActivity.php b/app/Services/Account/Activity/Activity/CreateActivity.php new file mode 100644 index 0000000..27392aa --- /dev/null +++ b/app/Services/Account/Activity/Activity/CreateActivity.php @@ -0,0 +1,127 @@ + 'required|integer|exists:accounts,id', + 'activity_type_id' => 'nullable|integer|exists:activity_types,id', + 'summary' => 'required|string:255', + 'description' => 'nullable|string:1000000', + 'happened_at' => 'required|date|date_format:Y-m-d', + 'emotions' => 'nullable|array', + 'contacts' => 'required|array', + ]; + } + + /** + * Validate all datas to execute the service. + * + * @param array $data + * @return bool + */ + public function validate(array $data): bool + { + parent::validate($data); + + if (count($data['contacts']) > 0) { + foreach ($data['contacts'] as $contactId) { + Contact::where('account_id', $data['account_id']) + ->findOrFail($contactId); + } + } + + if (! empty($data['activity_type_id']) && $data['activity_type_id'] != '') { + ActivityType::where('account_id', $data['account_id']) + ->findOrFail($data['activity_type_id']); + } + + if (! empty($data['emotions']) && $data['emotions'] != '') { + foreach ($data['emotions'] as $emotionId) { + Emotion::findOrFail($emotionId); + } + } + + return true; + } + + /** + * Create an activity. + * + * @param array $data + * @return Activity + */ + public function execute(array $data): Activity + { + $this->validate($data); + + $activity = $this->create($data); + + // Log a journal entry + JournalEntry::add($activity); + + // Now we associate the activity with each one of the attendees + app(AttachContactToActivity::class)->execute([ + 'account_id' => $data['account_id'], + 'activity_id' => $activity->id, + 'contacts' => $data['contacts'], + ]); + + return $activity; + } + + /** + * Create the activity. + * + * @param array $data + * @return Activity + */ + private function create(array $data): Activity + { + $activity = Activity::create([ + 'account_id' => $data['account_id'], + 'activity_type_id' => $this->nullOrValue($data, 'activity_type_id'), + 'summary' => $data['summary'], + 'description' => $this->nullOrValue($data, 'description'), + 'happened_at' => $data['happened_at'], + ]); + + if (! empty($data['emotions']) && $data['emotions'] != '') { + $this->addEmotions($data['emotions'], $activity); + } + + return $activity; + } + + /** + * Add emotions to the activity. + * + * @param array $emotions + * @param Activity $activity + * @return void + */ + private function addEmotions(array $emotions, Activity $activity) + { + $emotionsSync = []; + foreach ($emotions as $emotion) { + $emotionsSync[$emotion] = ['account_id' => $activity->account_id]; + } + + $activity->emotions()->sync($emotionsSync); + } +} diff --git a/app/Services/Account/Activity/Activity/DestroyActivity.php b/app/Services/Account/Activity/Activity/DestroyActivity.php new file mode 100644 index 0000000..abf43bf --- /dev/null +++ b/app/Services/Account/Activity/Activity/DestroyActivity.php @@ -0,0 +1,57 @@ + 'required|integer|exists:accounts,id', + 'activity_id' => 'required|integer|exists:activities,id', + ]; + } + + /** + * Validate all datas to execute the service. + * + * @param array $data + * @return bool + */ + public function validate(array $data): bool + { + parent::validate($data); + + Activity::where('account_id', $data['account_id']) + ->findOrFail($data['activity_id']); + + return true; + } + + /** + * Destroy an activity. + * + * @param array $data + * @return bool + */ + public function execute(array $data): bool + { + $this->validate($data); + + $activity = Activity::find($data['activity_id']); + + $activity->deleteJournalEntry(); + + $activity->delete(); + + return true; + } +} diff --git a/app/Services/Account/Activity/Activity/UpdateActivity.php b/app/Services/Account/Activity/Activity/UpdateActivity.php new file mode 100644 index 0000000..0ddc065 --- /dev/null +++ b/app/Services/Account/Activity/Activity/UpdateActivity.php @@ -0,0 +1,131 @@ + 'required|integer|exists:accounts,id', + 'activity_id' => 'required|integer|exists:activities,id', + 'activity_type_id' => 'nullable|integer|exists:activity_types,id', + 'summary' => 'required|string:255', + 'description' => 'nullable|string:1000000', + 'happened_at' => 'required|date|date_format:Y-m-d', + 'emotions' => 'nullable|array', + 'contacts' => 'required|array', + ]; + } + + /** + * Validate all datas to execute the service. + * + * @param array $data + * @return bool + */ + public function validate(array $data): bool + { + parent::validate($data); + + Activity::where('account_id', $data['account_id']) + ->findOrFail($data['activity_id']); + + foreach ($data['contacts'] as $contactId) { + Contact::where('account_id', $data['account_id']) + ->findOrFail($contactId); + } + + if (! empty($data['activity_type_id']) && $data['activity_type_id'] != '') { + ActivityType::where('account_id', $data['account_id']) + ->findOrFail($data['activity_type_id']); + } + + if (! empty($data['emotions']) && $data['emotions'] != '') { + foreach ($data['emotions'] as $emotionId) { + Emotion::findOrFail($emotionId); + } + } + + return true; + } + + /** + * Update an activity. + * + * @param array $data + * @return Activity + */ + public function execute(array $data): Activity + { + $this->validate($data); + + /** @var Activity */ + $activity = Activity::find($data['activity_id']); + + $this->update($data, $activity); + + // Log a journal entry but need to delete the previous one first + $activity->deleteJournalEntry(); + JournalEntry::add($activity); + + // Now we update the activity with each one of the attendees + app(AttachContactToActivity::class)->execute([ + 'account_id' => $data['account_id'], + 'activity_id' => $data['activity_id'], + 'contacts' => $data['contacts'], + ]); + + return $activity->refresh(); + } + + /** + * Update the activity. + * + * @param array $data + * @param Activity $activity + * @return void + */ + private function update(array $data, Activity $activity) + { + $activity->update([ + 'activity_type_id' => $this->nullOrValue($data, 'activity_type_id'), + 'summary' => $data['summary'], + 'description' => $this->nullOrValue($data, 'description'), + 'happened_at' => $data['happened_at'], + ]); + + if (! empty($data['emotions']) && $data['emotions'] != '') { + $this->updateEmotions($data['emotions'], $activity); + } + } + + /** + * Update activity's emotions. + * + * @param array $emotions + * @param Activity $activity + * @return void + */ + private function updateEmotions(array $emotions, Activity $activity) + { + $emotionsSync = []; + foreach ($emotions as $emotion) { + $emotionsSync[$emotion] = ['account_id' => $activity->account_id]; + } + + $activity->emotions()->sync($emotionsSync); + } +} diff --git a/app/Services/Account/Activity/ActivityStatisticService.php b/app/Services/Account/Activity/ActivityStatisticService.php new file mode 100644 index 0000000..a6ca1ac --- /dev/null +++ b/app/Services/Account/Activity/ActivityStatisticService.php @@ -0,0 +1,125 @@ +activities() + ->where('happened_at', '>=', $startDate) + ->where('happened_at', '<=', $endDate) + ->orderBy('happened_at', 'desc') + ->get(); + } + + /** + * Get the list of number of activities per year in total done with + * the contact. + * + * @param Contact $contact + * @return \Illuminate\Database\Eloquent\Collection + */ + public function activitiesPerYearWithContact(Contact $contact) + { + return $contact->activityStatistics()->get(); + } + + /** + * Get the list of activities per month for a given year. + * + * @param Contact $contact + * @param int $year + * @return Collection + */ + public function activitiesPerMonthForYear(Contact $contact, int $year) + { + $startDate = Carbon::create($year, 1, 1, 0, 0, 0); + $endDate = Carbon::create($year, 12, 31); + + $activities = $this->activitiesWithContactInTimeRange($contact, $startDate, $endDate); + + $activitiesPerMonth = collect([]); + for ($month = 1; $month < 13; $month++) { + $activitiesInMonth = collect([]); + + foreach ($activities as $activity) { + if ($activity->happened_at->month === $month) { + $activitiesInMonth->push($activity); + } + } + + $activitiesPerMonth->push([ + 'month' => $month, + 'occurences' => $activitiesInMonth->count(), + 'activities' => $activitiesInMonth, + ]); + } + + $maxActivitiesInAMonth = $activitiesPerMonth->max('occurences'); + + $activitiesPerMonth->transform(function ($activity) use ($maxActivitiesInAMonth) { + if ($activity['occurences'] != 0) { + $activity['percent'] = ($activity['occurences'] * 100 / $maxActivitiesInAMonth); + } else { + $activity['percent'] = 0; + } + + return $activity; + }); + + return $activitiesPerMonth; + } + + /** + * Get the list of unique activity types for activities done with + * a contact in a given timeframe, along with the number of occurences. + * + * @param Contact $contact + * @param Carbon $startDate + * @param Carbon $endDate + * @return Collection + */ + public function uniqueActivityTypesInTimeRange(Contact $contact, Carbon $startDate, Carbon $endDate) + { + $activities = $this->activitiesWithContactInTimeRange($contact, $startDate, $endDate); + + // group activities by activity type id + $grouped = $activities->groupBy(function ($item, $key) { + return $item['activity_type_id']; + }); + + // remove activity type id that are null + $grouped = $grouped->reject(function ($value, $key) { + return $key == ''; + }); + + // calculate how many occurences of unique activity type id + $activities = $grouped->map(function ($item) { + return collect($item)->count(); + }); + + $activityTypes = collect([]); + foreach ($activities as $key => $value) { + $activityTypes->push([ + 'object' => ActivityType::find($key), + 'occurences' => $value, + ]); + } + + return $activityTypes; + } +} diff --git a/app/Services/Account/Activity/ActivityType/CreateActivityType.php b/app/Services/Account/Activity/ActivityType/CreateActivityType.php new file mode 100644 index 0000000..02268e5 --- /dev/null +++ b/app/Services/Account/Activity/ActivityType/CreateActivityType.php @@ -0,0 +1,48 @@ + 'required|integer|exists:accounts,id', + 'activity_type_category_id' => 'required|integer|exists:activity_type_categories,id', + 'name' => 'nullable|string|max:255', + 'translation_key' => 'nullable|string|max:255', + ]; + } + + /** + * Create an activity type. + * + * @param array $data + * @return ActivityType + */ + public function execute(array $data): ActivityType + { + $this->validate($data); + + ActivityTypeCategory::where('account_id', $data['account_id']) + ->findOrFail($data['activity_type_category_id']); + + $activityType = ActivityType::create([ + 'account_id' => $data['account_id'], + 'activity_type_category_id' => $data['activity_type_category_id'], + 'name' => $this->nullOrValue($data, 'name'), + 'translation_key' => $this->nullOrValue($data, 'translation_key'), + ]); + + return ActivityType::find($activityType->id); + } +} diff --git a/app/Services/Account/Activity/ActivityType/DestroyActivityType.php b/app/Services/Account/Activity/ActivityType/DestroyActivityType.php new file mode 100644 index 0000000..2bc9b5d --- /dev/null +++ b/app/Services/Account/Activity/ActivityType/DestroyActivityType.php @@ -0,0 +1,40 @@ + 'required|integer|exists:accounts,id', + 'activity_type_id' => 'required|integer|exists:activity_types,id', + ]; + } + + /** + * Destroy a activity type. + * + * @param array $data + * @return bool + */ + public function execute(array $data): bool + { + $this->validate($data); + + $activityType = ActivityType::where('account_id', $data['account_id']) + ->findOrFail($data['activity_type_id']); + + $activityType->delete(); + + return true; + } +} diff --git a/app/Services/Account/Activity/ActivityType/UpdateActivityType.php b/app/Services/Account/Activity/ActivityType/UpdateActivityType.php new file mode 100644 index 0000000..52d5927 --- /dev/null +++ b/app/Services/Account/Activity/ActivityType/UpdateActivityType.php @@ -0,0 +1,52 @@ + 'required|integer|exists:accounts,id', + 'activity_type_category_id' => 'required|integer|exists:activity_type_categories,id', + 'activity_type_id' => 'required|integer|exists:activity_types,id', + 'name' => 'nullable|string|max:255', + 'translation_key' => 'nullable|string|max:255', + ]; + } + + /** + * Update an activity type. + * + * @param array $data + * @return ActivityType + */ + public function execute(array $data): ActivityType + { + $this->validate($data); + + ActivityTypeCategory::where('account_id', $data['account_id']) + ->findOrFail($data['activity_type_category_id']); + + /** @var ActivityType */ + $activityType = ActivityType::where('account_id', $data['account_id']) + ->findOrFail($data['activity_type_id']); + + $activityType->update([ + 'activity_type_category_id' => $data['activity_type_category_id'], + 'name' => $this->nullOrValue($data, 'name'), + 'translation_key' => $this->nullOrValue($data, 'translation_key'), + ]); + + return $activityType; + } +} diff --git a/app/Services/Account/Activity/ActivityTypeCategory/CreateActivityTypeCategory.php b/app/Services/Account/Activity/ActivityTypeCategory/CreateActivityTypeCategory.php new file mode 100644 index 0000000..2eb4670 --- /dev/null +++ b/app/Services/Account/Activity/ActivityTypeCategory/CreateActivityTypeCategory.php @@ -0,0 +1,42 @@ + 'required|integer|exists:accounts,id', + 'name' => 'nullable|string|max:255', + 'translation_key' => 'nullable|string|max:255', + ]; + } + + /** + * Create an activity type category. + * + * @param array $data + * @return ActivityTypeCategory + */ + public function execute(array $data): ActivityTypeCategory + { + $this->validate($data); + + $activityTypeCategory = ActivityTypeCategory::create([ + 'account_id' => $data['account_id'], + 'name' => $this->nullOrValue($data, 'name'), + 'translation_key' => $this->nullOrValue($data, 'translation_key'), + ]); + + return ActivityTypeCategory::find($activityTypeCategory->id); + } +} diff --git a/app/Services/Account/Activity/ActivityTypeCategory/DestroyActivityTypeCategory.php b/app/Services/Account/Activity/ActivityTypeCategory/DestroyActivityTypeCategory.php new file mode 100644 index 0000000..47036a9 --- /dev/null +++ b/app/Services/Account/Activity/ActivityTypeCategory/DestroyActivityTypeCategory.php @@ -0,0 +1,40 @@ + 'required|integer|exists:accounts,id', + 'activity_type_category_id' => 'required|integer|exists:activity_type_categories,id', + ]; + } + + /** + * Destroy a activity type category. + * + * @param array $data + * @return bool + */ + public function execute(array $data): bool + { + $this->validate($data); + + $activityTypeCategory = ActivityTypeCategory::where('account_id', $data['account_id']) + ->findOrFail($data['activity_type_category_id']); + + $activityTypeCategory->delete(); + + return true; + } +} diff --git a/app/Services/Account/Activity/ActivityTypeCategory/UpdateActivityTypeCategory.php b/app/Services/Account/Activity/ActivityTypeCategory/UpdateActivityTypeCategory.php new file mode 100644 index 0000000..5b7ff95 --- /dev/null +++ b/app/Services/Account/Activity/ActivityTypeCategory/UpdateActivityTypeCategory.php @@ -0,0 +1,46 @@ + 'required|integer|exists:accounts,id', + 'activity_type_category_id' => 'required|integer|exists:activity_type_categories,id', + 'name' => 'nullable|string|max:255', + 'translation_key' => 'nullable|string|max:255', + ]; + } + + /** + * Update an activity type category. + * + * @param array $data + * @return ActivityTypeCategory + */ + public function execute(array $data): ActivityTypeCategory + { + $this->validate($data); + + /** @var ActivityTypeCategory */ + $activityTypeCategory = ActivityTypeCategory::where('account_id', $data['account_id']) + ->findOrFail($data['activity_type_category_id']); + + $activityTypeCategory->update([ + 'name' => $this->nullOrValue($data, 'name'), + 'translation_key' => $this->nullOrValue($data, 'translation_key'), + ]); + + return $activityTypeCategory; + } +} diff --git a/app/Services/Account/Company/CreateCompany.php b/app/Services/Account/Company/CreateCompany.php new file mode 100644 index 0000000..8d00648 --- /dev/null +++ b/app/Services/Account/Company/CreateCompany.php @@ -0,0 +1,75 @@ + 'required|integer|exists:accounts,id', + 'author_id' => 'required|integer|exists:users,id', + 'name' => 'required|string|max:255', + 'website' => 'nullable|string|max:255', + 'number_of_employees' => 'nullable|integer', + ]; + } + + /** + * Create a company. + * + * @param array $data + * @return Company + */ + public function execute(array $data): Company + { + $this->validate($data); + + $this->log($data); + + return Company::create([ + 'account_id' => $data['account_id'], + 'name' => $data['name'], + 'website' => $this->nullOrValue($data, 'website'), + 'number_of_employees' => $this->nullOrValue($data, 'number_of_employees'), + ]); + } + + /** + * Add an audit log. + * + * @param array $data + * @return void + * + * @throws JsonException + */ + private function log(array $data): void + { + $author = User::find($data['author_id']); + + LogAccountAudit::dispatch([ + 'action' => 'company_created', + 'account_id' => $data['account_id'], + 'about_contact_id' => null, + 'author_id' => $author->id, + 'author_name' => $author->name, + 'audited_at' => now(), + 'should_appear_on_dashboard' => true, + 'objects' => json_encode([ + 'name' => $data['name'], + ]), + ]); + } +} diff --git a/app/Services/Account/Company/DestroyCompany.php b/app/Services/Account/Company/DestroyCompany.php new file mode 100644 index 0000000..804786f --- /dev/null +++ b/app/Services/Account/Company/DestroyCompany.php @@ -0,0 +1,40 @@ + 'required|integer|exists:accounts,id', + 'company_id' => 'required|integer|exists:companies,id', + ]; + } + + /** + * Destroy a company. + * + * @param array $data + * @return bool + */ + public function execute(array $data): bool + { + $this->validate($data); + + $company = Company::where('account_id', $data['account_id']) + ->findOrFail($data['company_id']); + + $company->delete(); + + return true; + } +} diff --git a/app/Services/Account/Company/UpdateCompany.php b/app/Services/Account/Company/UpdateCompany.php new file mode 100644 index 0000000..b8a6459 --- /dev/null +++ b/app/Services/Account/Company/UpdateCompany.php @@ -0,0 +1,48 @@ + 'required|integer|exists:accounts,id', + 'company_id' => 'required|integer|exists:companies,id', + 'name' => 'required|string|max:255', + 'website' => 'nullable|string|max:255', + 'number_of_employees' => 'nullable|integer', + ]; + } + + /** + * Update a company. + * + * @param array $data + * @return Company + */ + public function execute(array $data): Company + { + $this->validate($data); + + /** @var Company */ + $company = Company::where('account_id', $data['account_id']) + ->findOrFail($data['company_id']); + + $company->update([ + 'name' => $data['name'], + 'website' => $this->nullOrValue($data, 'website'), + 'number_of_employees' => $this->nullOrValue($data, 'number_of_employees'), + ]); + + return $company; + } +} diff --git a/app/Services/Account/Gender/CreateGender.php b/app/Services/Account/Gender/CreateGender.php new file mode 100644 index 0000000..b597ba7 --- /dev/null +++ b/app/Services/Account/Gender/CreateGender.php @@ -0,0 +1,44 @@ + 'required|integer|exists:accounts,id', + 'name' => 'required|string|max:255', + 'type' => [ + 'required', + Rule::in([Gender::MALE, Gender::FEMALE, Gender::OTHER]), + ], + ]; + } + + /** + * Create a gender. + * + * @param array $data + * @return Gender + */ + public function execute(array $data): Gender + { + $this->validate($data); + + return Gender::create([ + 'account_id' => $data['account_id'], + 'name' => $data['name'], + 'type' => $data['type'], + ]); + } +} diff --git a/app/Services/Account/Gender/DestroyGender.php b/app/Services/Account/Gender/DestroyGender.php new file mode 100644 index 0000000..fa4cf4b --- /dev/null +++ b/app/Services/Account/Gender/DestroyGender.php @@ -0,0 +1,40 @@ + 'required|integer|exists:accounts,id', + 'gender_id' => 'required|integer|exists:genders,id', + ]; + } + + /** + * Destroy a gender. + * + * @param array $data + * @return bool + */ + public function execute(array $data): bool + { + $this->validate($data); + + $gender = Gender::where('account_id', $data['account_id']) + ->findOrFail($data['gender_id']); + + $gender->delete(); + + return true; + } +} diff --git a/app/Services/Account/Gender/UpdateGender.php b/app/Services/Account/Gender/UpdateGender.php new file mode 100644 index 0000000..3a5c591 --- /dev/null +++ b/app/Services/Account/Gender/UpdateGender.php @@ -0,0 +1,50 @@ + 'required|integer|exists:accounts,id', + 'gender_id' => 'required|integer|exists:genders,id', + 'name' => 'required|string|max:255', + 'type' => [ + 'required', + Rule::in([Gender::MALE, Gender::FEMALE, Gender::OTHER]), + ], + ]; + } + + /** + * Update a gender. + * + * @param array $data + * @return Gender + */ + public function execute(array $data): Gender + { + $this->validate($data); + + /** @var Gender */ + $gender = Gender::where('account_id', $data['account_id']) + ->findOrFail($data['gender_id']); + + $gender->update([ + 'name' => $data['name'], + 'type' => $data['type'], + ]); + + return $gender; + } +} diff --git a/app/Services/Account/LifeEvent/LifeEventType/CreateLifeEventType.php b/app/Services/Account/LifeEvent/LifeEventType/CreateLifeEventType.php new file mode 100644 index 0000000..af4a73b --- /dev/null +++ b/app/Services/Account/LifeEvent/LifeEventType/CreateLifeEventType.php @@ -0,0 +1,49 @@ + 'required|integer|exists:accounts,id', + 'life_event_category_id' => 'required|integer|exists:life_event_categories,id', + 'name' => 'required|string|max:255', + ]; + } + + /** + * Create a life event type. + * + * @param array $data + * @return LifeEventType + */ + public function execute(array $data): LifeEventType + { + $this->validate($data); + + LifeEventCategory::where('account_id', $data['account_id']) + ->findOrFail($data['life_event_category_id']); + + $lifeEventType = LifeEventType::create([ + 'account_id' => $data['account_id'], + 'life_event_category_id' => $data['life_event_category_id'], + 'name' => $data['name'], + 'default_life_event_type_key' => null, + 'core_monica_data' => false, + 'specific_information_structure' => null, + ]); + + return LifeEventType::find($lifeEventType->id); + } +} diff --git a/app/Services/Account/LifeEvent/LifeEventType/DestroyLifeEventType.php b/app/Services/Account/LifeEvent/LifeEventType/DestroyLifeEventType.php new file mode 100644 index 0000000..7fb456f --- /dev/null +++ b/app/Services/Account/LifeEvent/LifeEventType/DestroyLifeEventType.php @@ -0,0 +1,40 @@ + 'required|integer|exists:accounts,id', + 'life_event_type_id' => 'required|integer|exists:life_event_types,id', + ]; + } + + /** + * Destroy a life event type. + * + * @param array $data + * @return bool + */ + public function execute(array $data): bool + { + $this->validate($data); + + $lifeEventType = LifeEventType::where('account_id', $data['account_id']) + ->findOrFail($data['life_event_type_id']); + + $lifeEventType->delete(); + + return true; + } +} diff --git a/app/Services/Account/LifeEvent/LifeEventType/UpdateLifeEventType.php b/app/Services/Account/LifeEvent/LifeEventType/UpdateLifeEventType.php new file mode 100644 index 0000000..1b70480 --- /dev/null +++ b/app/Services/Account/LifeEvent/LifeEventType/UpdateLifeEventType.php @@ -0,0 +1,50 @@ + 'required|integer|exists:accounts,id', + 'life_event_category_id' => 'required|integer|exists:life_event_categories,id', + 'life_event_type_id' => 'required|integer|exists:life_event_types,id', + 'name' => 'required|string|max:255', + ]; + } + + /** + * Update a life event type. + * + * @param array $data + * @return LifeEventType + */ + public function execute(array $data): LifeEventType + { + $this->validate($data); + + LifeEventCategory::where('account_id', $data['account_id']) + ->findOrFail($data['life_event_category_id']); + + /** @var LifeEventType */ + $lifeEventType = LifeEventType::where('account_id', $data['account_id']) + ->findOrFail($data['life_event_type_id']); + + $lifeEventType->update([ + 'life_event_category_id' => $data['life_event_category_id'], + 'name' => $data['name'], + ]); + + return $lifeEventType; + } +} diff --git a/app/Services/Account/Photo/DestroyPhoto.php b/app/Services/Account/Photo/DestroyPhoto.php new file mode 100644 index 0000000..1309dbd --- /dev/null +++ b/app/Services/Account/Photo/DestroyPhoto.php @@ -0,0 +1,46 @@ + 'required|integer|exists:accounts,id', + 'photo_id' => 'required|integer|exists:photos,id', + ]; + } + + /** + * Destroy a photo. + * + * @param array $data + * @return bool + */ + public function execute(array $data): bool + { + $this->validate($data); + + $photo = Photo::where('account_id', $data['account_id']) + ->findOrFail($data['photo_id']); + + // Delete the physical photo + // Throws FileNotFoundException + Storage::delete($photo->new_filename); + + // Delete the object in the DB + $photo->delete(); + + return true; + } +} diff --git a/app/Services/Account/Photo/UploadPhoto.php b/app/Services/Account/Photo/UploadPhoto.php new file mode 100644 index 0000000..b2241cc --- /dev/null +++ b/app/Services/Account/Photo/UploadPhoto.php @@ -0,0 +1,219 @@ +isValidPhoto($value); + }); + } + + /** + * Get the validation rules that apply to the service. + * + * @return array + */ + public function rules() + { + return [ + 'account_id' => 'required|integer|exists:accounts,id', + 'contact_id' => 'required|integer|exists:contacts,id', + 'photo' => 'required_without:data|file|image', + 'data' => 'required_without:photo|string|photo', + 'extension' => 'nullable|string', + ]; + } + + /** + * Upload a photo. + * + * @param array $data + * @return Photo|null + */ + public function execute(array $data): ?Photo + { + $this->validate($data); + + $contact = Contact::where('account_id', $data['account_id']) + ->findOrFail($data['contact_id']); + + $contact->throwInactive(); + + $array = null; + if (Arr::has($data, 'photo')) { + $array = $this->importPhoto($data); + } else { + $array = $this->importFile($data); + } + + if (! $array) { + return null; + } + + return tap(Photo::create($array), function ($photo) use ($contact): void { + $contact->photos()->syncWithoutDetaching([$photo->id]); + }); + } + + /** + * Create an array with the necessary fields to create the photo object. + * + * @return array + */ + private function importPhoto($data): array + { + $photo = $data['photo']; + + return [ + 'account_id' => $data['account_id'], + 'original_filename' => $photo->getClientOriginalName(), + 'filesize' => $photo->getSize(), + 'mime_type' => (new \Mimey\MimeTypes)->getMimeType($photo->guessClientExtension()), + 'new_filename' => $photo->store('photos', [ + 'disk' => config('filesystems.default'), + 'visibility' => config('filesystems.default_visibility'), + ]), + ]; + } + + /** + * Upload the photo. + * + * @return array|null + */ + private function importFile(array $data): ?array + { + $filename = Str::random(40); + + try { + $image = Image::make($data['data']); + } catch (NotReadableException $e) { + return null; + } + + $tempfile = $this->storeImage('local', $image, 'temp/'.$filename); + + try { + $storagePath = StorageHelper::disk('local')->path($tempfile); + // This sets the basePath to get the filesize later + $image = $image->setFileInfoFromPath($storagePath); + $extension = (new \Mimey\MimeTypes)->getExtension($image->mime()); + if (empty($extension)) { + $extension = str_replace(' ', '', Arr::get($data, 'extension')); + } + if (! empty($extension)) { + $filename .= '.'.$extension; + } + + $array = [ + 'account_id' => $data['account_id'], + 'original_filename' => $filename, + 'filesize' => $image->filesize(), + 'mime_type' => $image->mime(), + ]; + + $array['new_filename'] = $this->storeImage(config('filesystems.default'), $image, 'photos/'.$filename); + } finally { + $storage = Storage::disk('local'); + if ($storage->exists($tempfile)) { + $storage->delete($tempfile); + } + } + + return $array; + } + + /** + * Store the decoded image in the temp file. + * + * @param string $disk + * @param \Intervention\Image\Image $image + * @param string $filename + * @return string|null + */ + private function storeImage(string $disk, $image, string $filename): ?string + { + $result = Storage::disk($disk) + ->put($path = $filename, (string) $image->stream(), config('filesystems.default_visibility')); + + return $result ? $path : null; + } + + /** + * Determines if the source photo is a valid encoded photo. + * + * @param string $data + * @return bool + */ + private function isValidPhoto(string $data): bool + { + return $this->isBinary($data) || $this->isDataUrl($data) || $this->isBase64($data); + } + + /** + * Determines if source data is binary data. + * + * @param string $data + * @return bool + */ + private function isBinary(string $data): bool + { + $mime = finfo_buffer(finfo_open(FILEINFO_MIME_TYPE), $data); // @phpstan-ignore-line + + return substr($mime, 0, 4) != 'text' && $mime != 'application/x-empty'; + } + + /** + * Determines if source data is data-url format. + * + * @param string $data + * @return bool + */ + private function isDataUrl(string $data): bool + { + if (! is_string($data)) { + return false; + } + + $pattern = "/^data:(?:image\/[a-zA-Z\-\.]+)(?:charset=\".+\")?;base64,(?P.+)$/"; + preg_match($pattern, $data, $matches); + + if (is_array($matches) && Arr::has($matches, 'data')) { + return ! empty(base64_decode($matches['data'])); + } + + return false; + } + + /** + * Determines if source data is base64 encoded. + * + * @param string $data + * @return bool + */ + private function isBase64(string $data): bool + { + if (! is_string($data)) { + return false; + } + + return base64_encode(base64_decode($data)) === str_replace(["\n", "\r"], '', $data); + } +} diff --git a/app/Services/Account/Place/CreatePlace.php b/app/Services/Account/Place/CreatePlace.php new file mode 100644 index 0000000..50dedea --- /dev/null +++ b/app/Services/Account/Place/CreatePlace.php @@ -0,0 +1,70 @@ + 'required|integer|exists:accounts,id', + 'street' => 'nullable|string|max:255', + 'city' => 'nullable|string|max:255', + 'province' => 'nullable|string|max:255', + 'postal_code' => 'nullable|string|max:255', + 'country' => 'nullable|string|max:3', + 'latitude' => 'nullable|numeric', + 'longitude' => 'nullable|numeric', + ]; + } + + /** + * Create a place. + * + * @param array $data + * @return Place + */ + public function execute(array $data): Place + { + $this->validate($data); + + $place = Place::create([ + 'account_id' => $data['account_id'], + 'street' => $this->nullOrValue($data, 'street'), + 'city' => $this->nullOrValue($data, 'city'), + 'province' => $this->nullOrValue($data, 'province'), + 'postal_code' => $this->nullOrValue($data, 'postal_code'), + 'country' => $this->nullOrValue($data, 'country'), + 'latitude' => $this->nullOrValue($data, 'latitude'), + 'longitude' => $this->nullOrValue($data, 'longitude'), + ]); + + if (is_null($place->latitude) || is_null($place->longitude)) { + $this->getGeocodingInfo($place); + } + + return $place; + } + + /** + * Get geocoding information about the place (lat/longitude). + * + * @param Place $place + * @return void + */ + private function getGeocodingInfo(Place $place) + { + if (config('monica.enable_geolocation') && ! is_null(config('monica.location_iq_api_key'))) { + GetGPSCoordinate::dispatch($place); + } + } +} diff --git a/app/Services/Account/Place/DestroyPlace.php b/app/Services/Account/Place/DestroyPlace.php new file mode 100644 index 0000000..aaa1fe8 --- /dev/null +++ b/app/Services/Account/Place/DestroyPlace.php @@ -0,0 +1,40 @@ + 'required|integer|exists:accounts,id', + 'place_id' => 'required|integer|exists:places,id', + ]; + } + + /** + * Destroy a place. + * + * @param array $data + * @return bool + */ + public function execute(array $data): bool + { + $this->validate($data); + + $place = Place::where('account_id', $data['account_id']) + ->findOrFail($data['place_id']); + + $place->delete(); + + return true; + } +} diff --git a/app/Services/Account/Place/UpdatePlace.php b/app/Services/Account/Place/UpdatePlace.php new file mode 100644 index 0000000..6afeff1 --- /dev/null +++ b/app/Services/Account/Place/UpdatePlace.php @@ -0,0 +1,74 @@ + 'required|integer|exists:accounts,id', + 'place_id' => 'required|integer|exists:places,id', + 'street' => 'nullable|string|max:255', + 'city' => 'nullable|string|max:255', + 'province' => 'nullable|string|max:255', + 'postal_code' => 'nullable|string|max:255', + 'country' => 'nullable|string|max:3', + 'latitude' => 'nullable|numeric', + 'longitude' => 'nullable|numeric', + ]; + } + + /** + * Update a place. + * + * @param array $data + * @return Place + */ + public function execute(array $data): Place + { + $this->validate($data); + + /** @var Place */ + $place = Place::where('account_id', $data['account_id']) + ->findOrFail($data['place_id']); + + $place->update([ + 'street' => $this->nullOrValue($data, 'street'), + 'city' => $this->nullOrValue($data, 'city'), + 'province' => $this->nullOrValue($data, 'province'), + 'postal_code' => $this->nullOrValue($data, 'postal_code'), + 'country' => $this->nullOrValue($data, 'country'), + 'latitude' => $this->nullOrValue($data, 'latitude'), + 'longitude' => $this->nullOrValue($data, 'longitude'), + ]); + + if (is_null($place->latitude) || is_null($place->longitude)) { + $this->getGeocodingInfo($place); + } + + return $place; + } + + /** + * Get geocoding information about the place (lat/longitude). + * + * @param Place $place + * @return void + */ + private function getGeocodingInfo(Place $place) + { + if (config('monica.enable_geolocation') && ! is_null(config('monica.location_iq_api_key'))) { + GetGPSCoordinate::dispatch($place); + } + } +} diff --git a/app/Services/Account/Settings/ArchiveAllContacts.php b/app/Services/Account/Settings/ArchiveAllContacts.php new file mode 100644 index 0000000..7145a41 --- /dev/null +++ b/app/Services/Account/Settings/ArchiveAllContacts.php @@ -0,0 +1,46 @@ + 'required|integer|exists:accounts,id', + ]; + } + + /** + * Archive all the contacts in the account. + * + * This method is used by a user who wants to downgrade his plan. + * + * @param array $data + * @return bool + */ + public function execute(array $data): bool + { + $this->validate($data); + + try { + DB::table('contacts') + ->where('account_id', $data['account_id']) + ->update(['is_active' => 0]); + } catch (QueryException $e) { + return false; + } + + return true; + } +} diff --git a/app/Services/Account/Settings/DestroyAccount.php b/app/Services/Account/Settings/DestroyAccount.php new file mode 100644 index 0000000..4627b58 --- /dev/null +++ b/app/Services/Account/Settings/DestroyAccount.php @@ -0,0 +1,86 @@ + 'required|integer|exists:accounts,id', + ]; + } + + /** + * Completely delete an account. + * + * @param array $data + * @return void + * + * @throws StripeException + */ + public function execute(array $data): void + { + $this->validate($data); + + $account = Account::find($data['account_id']); + + $this->destroyDocuments($account); + + $this->destroyPhotos($account); + + $this->cancelStripe($account); + + $account->delete(); + } + + /** + * Destroy the documents. + * + * @param Account $account + * @return void + */ + private function destroyDocuments(Account $account) + { + app(DestroyAllDocuments::class)->execute([ + 'account_id' => $account->id, + ]); + } + + /** + * Destroy the photos. + * + * @param Account $account + * @return void + */ + private function destroyPhotos(Account $account) + { + app(DestroyAllPhotos::class)->execute([ + 'account_id' => $account->id, + ]); + } + + /** + * Cancel Stripe subscription. + * + * @param Account $account + * @return void + * + * @throws StripeException + */ + private function cancelStripe(Account $account) + { + if ($account->isSubscribed() && ! $account->has_access_to_paid_version_for_free) { + $account->subscriptionCancel(); + } + } +} diff --git a/app/Services/Account/Settings/DestroyAllDocuments.php b/app/Services/Account/Settings/DestroyAllDocuments.php new file mode 100644 index 0000000..6610fe9 --- /dev/null +++ b/app/Services/Account/Settings/DestroyAllDocuments.php @@ -0,0 +1,41 @@ + 'required|integer|exists:accounts,id', + ]; + } + + /** + * Destroy all documents in an account. + * + * @param array $data + * @return bool + */ + public function execute(array $data): bool + { + $this->validate($data); + + $documents = Document::where('account_id', $data['account_id']) + ->get(); + + foreach ($documents as $document) { + $document->delete(); + } + + return true; + } +} diff --git a/app/Services/Account/Settings/DestroyAllPhotos.php b/app/Services/Account/Settings/DestroyAllPhotos.php new file mode 100644 index 0000000..a1a21db --- /dev/null +++ b/app/Services/Account/Settings/DestroyAllPhotos.php @@ -0,0 +1,41 @@ + 'required|integer|exists:accounts,id', + ]; + } + + /** + * Destroy all photos in an account. + * + * @param array $data + * @return bool + */ + public function execute(array $data): bool + { + $this->validate($data); + + $photos = Photo::where('account_id', $data['account_id']) + ->get(); + + foreach ($photos as $photo) { + $photo->delete(); + } + + return true; + } +} diff --git a/app/Services/Account/Settings/JsonExportAccount.php b/app/Services/Account/Settings/JsonExportAccount.php new file mode 100644 index 0000000..b20bcca --- /dev/null +++ b/app/Services/Account/Settings/JsonExportAccount.php @@ -0,0 +1,94 @@ + 'required|integer|exists:accounts,id', + 'user_id' => 'required|integer|exists:users,id', + ]; + } + + /** + * Export account as Json. + * + * @param array $data + * @return string + */ + public function execute(array $data): string + { + $this->validate($data); + + $user = User::findOrFail($data['user_id']); + + $this->tempFileName = 'temp/'.Str::random(40).'.json'; + + $this->writeExport($data, $user); + + return $this->tempFileName; + } + + /** + * Export data in temp file. + * + * @param array $data + * @param User $user + */ + private function writeExport(array $data, User $user) + { + $result = []; + $result['version'] = '1.0-preview.1'; + $result['app_version'] = config('monica.app_version'); + $result['export_date'] = now(); + $result['url'] = config('app.url'); + $result['exported_by'] = $user->uuid; + $result['account'] = $this->exportAccount($data); + + $this->writeToTempFile(json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_IGNORE | JSON_UNESCAPED_SLASHES)); + } + + /** + * Write to a temp file. + * + * @return void + */ + private function writeToTempFile(string $sql) + { + Storage::disk('local') + ->append($this->tempFileName, $sql); + } + + /** + * Export the Account table. + * + * @param array $data + * @return mixed + */ + private function exportAccount(array $data) + { + $account = Account::find($data['account_id']); + + $exporter = new AccountResource($account); + + return $exporter->resolve(); + } +} diff --git a/app/Services/Account/Settings/ResetAccount.php b/app/Services/Account/Settings/ResetAccount.php new file mode 100644 index 0000000..b0dc333 --- /dev/null +++ b/app/Services/Account/Settings/ResetAccount.php @@ -0,0 +1,180 @@ + 'required|integer|exists:accounts,id', + ]; + } + + /** + * Reset the account. + * + * @param array $data + * @return void + */ + public function handle(array $data): void + { + $this->validate($data); + + $account = Account::find($data['account_id']); + + $this->destroyCompanies($account); + + $this->destroyDays($account); + + $this->destroyPlaces($account); + + $this->destroyDocuments($account); + + $this->destroyPhotos($account); + + $this->destroyJournalEntries($account); + + $this->destroyImportJobs($account); + + $this->destroyContacts($account); + } + + /** + * Destroy the companies. + * + * @param Account $account + * @return void + */ + private function destroyCompanies(Account $account) + { + $companies = $account->companies; + foreach ($companies as $company) { + $company->delete(); + } + } + + /** + * Destroy the days. + * + * @param Account $account + * @return void + */ + private function destroyDays(Account $account) + { + $days = $account->days; + foreach ($days as $day) { + $day->delete(); + } + } + + /** + * Destroy the places. + * + * @param Account $account + * @return void + */ + private function destroyPlaces(Account $account) + { + $places = $account->places; + foreach ($places as $place) { + $place->delete(); + } + } + + /** + * Destroy the documents. + * + * @param Account $account + * @return void + */ + private function destroyDocuments(Account $account) + { + app(DestroyAllDocuments::class)->execute([ + 'account_id' => $account->id, + ]); + } + + /** + * Destroy the photos. + * + * @param Account $account + * @return void + */ + private function destroyPhotos(Account $account) + { + app(DestroyAllPhotos::class)->execute([ + 'account_id' => $account->id, + ]); + } + + /** + * Destroy the journal entries associated with all the contacts of this + * account. + * + * @param Account $account + * @return void + */ + private function destroyJournalEntries(Account $account) + { + $entries = $account->entries; + foreach ($entries as $entry) { + $entry->delete(); + } + + $activities = $account->activities; + foreach ($activities as $activity) { + $entries = $activity->journalEntries; + foreach ($entries as $entry) { + $entry->delete(); + } + + $activity->delete(); + } + } + + /** + * Destroy the import jobs. + * + * @param Account $account + * @return void + */ + private function destroyImportJobs(Account $account) + { + $importjobs = $account->importjobs; + foreach ($importjobs as $importjob) { + $importjob->delete(); + } + } + + /** + * Destroy all the contacts associated with this account. + * + * @param Account $account + * @return void + */ + private function destroyContacts(Account $account) + { + $contacts = $account->contacts; + foreach ($contacts as $contact) { + DestroyContact::dispatchSync([ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'force_delete' => true, + ]); + } + } +} diff --git a/app/Services/Account/Settings/SqlExportAccount.php b/app/Services/Account/Settings/SqlExportAccount.php new file mode 100644 index 0000000..5aeda3f --- /dev/null +++ b/app/Services/Account/Settings/SqlExportAccount.php @@ -0,0 +1,1373 @@ + 'required|integer|exists:accounts,id', + 'user_id' => 'required|integer|exists:users,id', + ]; + } + + /** + * Export account as SQL. + * + * @param array $data + * @return string + */ + public function execute(array $data): string + { + $this->validate($data); + + $user = User::findOrFail($data['user_id']); + + $this->tempFileName = 'temp/'.Str::random(40).'.sql'; + + $this->writeExport($data, $user); + + return $this->tempFileName; + } + + /** + * Export data in temp file. + * + * @param array $data + * @param User $user + */ + private function writeExport(array $data, User $user) + { + $sql = '# ************************************************************ +# '.$user->first_name.' '.$user->last_name.' dump of data +# Export date: '.now().' +# How to use: +# * create a fresh database +# * run migrations (`php artisan migrate`) +# * import this sql file +# ************************************************************ + +SET FOREIGN_KEY_CHECKS=0; +'.PHP_EOL; + + $this->writeToTempFile($sql); + + $this->exportAccount($data); + $this->exportActivity($data); + $this->exportContact($data); + $this->exportActivityContact($data); + $this->exportActivityStatistic($data); + $this->exportActivityTypeCategory($data); + $this->exportActivityType($data); + $this->exportAddress($data); + $this->exportCall($data); + $this->exportCompany($data); + $this->exportContactFieldType($data); + $this->exportContactField($data); + $this->exportContactTag($data); + $this->exportConversation($data); + $this->exportDays($data); + $this->exportDebt($data); + $this->exportDocument($data); + $this->exportEmotionCall($data); + $this->exportEntries($data); + $this->exportGender($data); + $this->exportGift($data); + $this->exportInvitation($data); + $this->exportJournalEntry($data); + $this->exportLifeEventCategory($data); + $this->exportLifeEventType($data); + $this->exportLifeEvent($data); + $this->exportMessage($data); + $this->exportMetaDataLoveRelationship($data); + $this->exportModule($data); + $this->exportNote($data); + $this->exportOccupation($data); + $this->exportPet($data); + $this->exportPhoto($data); + $this->exportPlace($data); + $this->exportRecoveryCode($data); + $this->exportRelationTypeGroup($data); + $this->exportRelationType($data); + $this->exportRelationship($data); + $this->exportReminderOutbox($data); + $this->exportReminderRule($data); + $this->exportReminder($data); + $this->exportSpecialDate($data); + $this->exportTag($data); + $this->exportTask($data); + $this->exportTermUser($data); + $this->exportUser($data); + $this->exportWeather($data); + $this->exportContactPhoto($data); + $this->exportAuditLogs($data); + + $sql = 'SET FOREIGN_KEY_CHECKS=1;'; + $this->writeToTempFile($sql); + } + + /** + * Create the Insert query for the given table. + * + * @param string $tableName + * @param string $foreignKey + * @param array $columns + * @param array $data + * @return void + */ + private function buildInsertSQLQuery(string $tableName, string $foreignKey, array $columns, array $data) + { + $accountData = DB::table($tableName) + ->select($columns) + ->where($foreignKey, $data['account_id']) + ->get(); + + if ($accountData->count() == 0) { + return; + } + + // adding a ` for each column + $listOfColumns = array_map(function ($column) { + return '`'.$column.'`'; + }, $columns); + $listOfColumns = implode(',', $listOfColumns); + + $sql = 'INSERT IGNORE INTO '.DBHelper::getTable($tableName).' ('.$listOfColumns.') VALUES'.PHP_EOL; + + $insertValues = []; + foreach ($accountData as $singleSQLData) { + $columnValues = []; + + // build an array of values + foreach ($columns as $value) { + $value = $singleSQLData->{$value}; + + if (is_null($value)) { + $value = 'NULL'; + } elseif (! is_numeric($value)) { + $value = "'".addslashes($value)."'"; + } + + array_push($columnValues, $value); + } + + array_push($insertValues, ' ('.implode(',', $columnValues).')'); + } + $sql .= implode(','.PHP_EOL, $insertValues); + $this->writeToTempFile($sql.';'.PHP_EOL); + } + + /** + * Write to a temp file. + * + * @return void + */ + private function writeToTempFile(string $sql) + { + Storage::disk('local') + ->append($this->tempFileName, $sql); + } + + /** + * Export the Account table. + * + * @param array $data + */ + private function exportAccount(array $data) + { + $columns = [ + 'id', + 'api_key', + 'number_of_invitations_sent', + ]; + + $foreignKey = 'id'; + + $this->buildInsertSQLQuery('accounts', $foreignKey, $columns, $data); + } + + /** + * Export the Activity table. + * + * @param array $data + */ + private function exportActivity(array $data) + { + $columns = [ + 'id', + 'account_id', + 'activity_type_id', + 'summary', + 'description', + 'happened_at', + 'created_at', + 'updated_at', + ]; + + $foreignKey = 'account_id'; + + $this->buildInsertSQLQuery('activities', $foreignKey, $columns, $data); + } + + /** + * Export the Activity Contact table. + * + * @param array $data + */ + private function exportActivityContact(array $data) + { + $columns = [ + 'activity_id', + 'account_id', + 'contact_id', + ]; + + $foreignKey = 'account_id'; + + $this->buildInsertSQLQuery('activity_contact', $foreignKey, $columns, $data); + } + + /** + * Export the Activity Statistic table. + * + * @param array $data + */ + private function exportActivityStatistic(array $data) + { + $columns = [ + 'id', + 'account_id', + 'contact_id', + 'year', + 'count', + 'created_at', + 'updated_at', + ]; + + $foreignKey = 'account_id'; + + $this->buildInsertSQLQuery('activity_statistics', $foreignKey, $columns, $data); + } + + /** + * Export the Activity Type Category table. + * + * @param array $data + */ + private function exportActivityTypeCategory(array $data) + { + $columns = [ + 'id', + 'account_id', + 'name', + 'translation_key', + 'created_at', + 'updated_at', + ]; + + $foreignKey = 'account_id'; + + $this->buildInsertSQLQuery('activity_type_categories', $foreignKey, $columns, $data); + } + + /** + * Export the Activity Type table. + * + * @param array $data + */ + private function exportActivityType(array $data) + { + $columns = [ + 'id', + 'account_id', + 'activity_type_category_id', + 'name', + 'translation_key', + 'location_type', + 'created_at', + 'updated_at', + ]; + + $foreignKey = 'account_id'; + + $this->buildInsertSQLQuery('activity_types', $foreignKey, $columns, $data); + } + + /** + * Export the Address table. + * + * @param array $data + */ + private function exportAddress(array $data) + { + $columns = [ + 'id', + 'account_id', + 'place_id', + 'contact_id', + 'name', + 'created_at', + 'updated_at', + ]; + + $foreignKey = 'account_id'; + + $this->buildInsertSQLQuery('addresses', $foreignKey, $columns, $data); + } + + /** + * Export the Call table. + * + * @param array $data + */ + private function exportCall(array $data) + { + $columns = [ + 'id', + 'account_id', + 'contact_id', + 'called_at', + 'content', + 'contact_called', + 'created_at', + 'updated_at', + ]; + + $foreignKey = 'account_id'; + + $this->buildInsertSQLQuery('calls', $foreignKey, $columns, $data); + } + + /** + * Export the Company table. + * + * @param array $data + */ + private function exportCompany(array $data) + { + $columns = [ + 'id', + 'account_id', + 'name', + 'website', + 'number_of_employees', + 'created_at', + 'updated_at', + ]; + + $foreignKey = 'account_id'; + + $this->buildInsertSQLQuery('companies', $foreignKey, $columns, $data); + } + + /** + * Export the Contact Field Type table. + * + * @param array $data + */ + private function exportContactFieldType(array $data) + { + $columns = [ + 'id', + 'account_id', + 'name', + 'fontawesome_icon', + 'protocol', + 'delible', + 'type', + 'created_at', + 'updated_at', + ]; + + $foreignKey = 'account_id'; + + $this->buildInsertSQLQuery('contact_field_types', $foreignKey, $columns, $data); + } + + /** + * Export the Contact Field table. + * + * @param array $data + */ + private function exportContactField(array $data) + { + $columns = [ + 'id', + 'account_id', + 'contact_id', + 'contact_field_type_id', + 'data', + 'created_at', + 'updated_at', + ]; + + $foreignKey = 'account_id'; + + $this->buildInsertSQLQuery('contact_fields', $foreignKey, $columns, $data); + } + + /** + * Export the Contact Tag table. + * + * @param array $data + */ + private function exportContactTag(array $data) + { + $columns = [ + 'contact_id', + 'tag_id', + 'account_id', + 'created_at', + 'updated_at', + ]; + + $foreignKey = 'account_id'; + + $this->buildInsertSQLQuery('contact_tag', $foreignKey, $columns, $data); + } + + /** + * Export the Contact table. + * + * @param array $data + */ + private function exportContact(array $data) + { + $columns = [ + 'id', + 'account_id', + 'first_name', + 'middle_name', + 'last_name', + 'nickname', + 'gender_id', + 'description', + 'uuid', + 'is_starred', + 'is_partial', + 'is_active', + 'is_dead', + 'deceased_special_date_id', + 'deceased_reminder_id', + 'last_talked_to', + 'stay_in_touch_frequency', + 'stay_in_touch_trigger_date', + 'birthday_special_date_id', + 'birthday_reminder_id', + 'first_met_through_contact_id', + 'first_met_special_date_id', + 'first_met_reminder_id', + 'first_met_where', + 'first_met_additional_info', + 'job', + 'company', + 'food_preferences', + 'avatar_source', + 'avatar_gravatar_url', + 'avatar_adorable_uuid', + 'avatar_adorable_url', + 'avatar_default_url', + 'avatar_photo_id', + 'has_avatar', + 'avatar_external_url', + 'avatar_file_name', + 'avatar_location', + 'gravatar_url', + 'last_consulted_at', + 'number_of_views', + 'created_at', + 'updated_at', + 'default_avatar_color', + 'has_avatar_bool', + ]; + + $foreignKey = 'account_id'; + + $this->buildInsertSQLQuery('contacts', $foreignKey, $columns, $data); + } + + /** + * Export the Conversation table. + * + * @param array $data + */ + private function exportConversation(array $data) + { + $columns = [ + 'id', + 'account_id', + 'contact_id', + 'contact_field_type_id', + 'created_at', + 'updated_at', + ]; + + $foreignKey = 'account_id'; + + $this->buildInsertSQLQuery('conversations', $foreignKey, $columns, $data); + } + + /** + * Export the Day table. + * + * @param array $data + */ + private function exportDays(array $data) + { + $columns = [ + 'id', + 'account_id', + 'date', + 'rate', + 'comment', + 'created_at', + 'updated_at', + ]; + + $foreignKey = 'account_id'; + + $this->buildInsertSQLQuery('days', $foreignKey, $columns, $data); + } + + /** + * Export the Debt table. + * + * @param array $data + */ + private function exportDebt(array $data) + { + $columns = [ + 'id', + 'account_id', + 'contact_id', + 'in_debt', + 'status', + 'amount', + 'currency_id', + 'reason', + 'created_at', + 'updated_at', + ]; + + $foreignKey = 'account_id'; + + $this->buildInsertSQLQuery('debts', $foreignKey, $columns, $data); + } + + /** + * Export the Document table. + * + * @param array $data + */ + private function exportDocument(array $data) + { + $columns = [ + 'id', + 'account_id', + 'contact_id', + 'original_filename', + 'new_filename', + 'filesize', + 'type', + 'mime_type', + 'number_of_downloads', + 'created_at', + 'updated_at', + ]; + + $foreignKey = 'account_id'; + + $this->buildInsertSQLQuery('documents', $foreignKey, $columns, $data); + } + + /** + * Export the Emotion Call table. + * + * @param array $data + */ + private function exportEmotionCall(array $data) + { + $columns = [ + 'account_id', + 'call_id', + 'emotion_id', + 'contact_id', + 'created_at', + 'updated_at', + ]; + + $foreignKey = 'account_id'; + + $this->buildInsertSQLQuery('emotion_call', $foreignKey, $columns, $data); + } + + /** + * Export the Entries table. + * + * @param array $data + */ + private function exportEntries(array $data) + { + $columns = [ + 'id', + 'account_id', + 'title', + 'post', + 'created_at', + 'updated_at', + ]; + + $foreignKey = 'account_id'; + + $this->buildInsertSQLQuery('entries', $foreignKey, $columns, $data); + } + + /** + * Export the Gender table. + * + * @param array $data + */ + private function exportGender(array $data) + { + $columns = [ + 'id', + 'account_id', + 'name', + 'type', + 'created_at', + 'updated_at', + ]; + + $foreignKey = 'account_id'; + + $this->buildInsertSQLQuery('genders', $foreignKey, $columns, $data); + } + + /** + * Export the Gift table. + * + * @param array $data + */ + private function exportGift(array $data) + { + $columns = [ + 'id', + 'account_id', + 'contact_id', + 'name', + 'comment', + 'url', + 'amount', + 'currency_id', + 'status', + 'date', + 'created_at', + 'updated_at', + ]; + + $foreignKey = 'account_id'; + + $this->buildInsertSQLQuery('gifts', $foreignKey, $columns, $data); + } + + /** + * Export the Invitation table. + * + * @param array $data + */ + private function exportInvitation(array $data) + { + $columns = [ + 'id', + 'account_id', + 'invited_by_user_id', + 'email', + 'invitation_key', + 'created_at', + 'updated_at', + ]; + + $foreignKey = 'account_id'; + + $this->buildInsertSQLQuery('invitations', $foreignKey, $columns, $data); + } + + /** + * Export the Journal Entry table. + * + * @param array $data + */ + private function exportJournalEntry(array $data) + { + $columns = [ + 'id', + 'account_id', + 'date', + 'journalable_id', + 'journalable_type', + 'created_at', + 'updated_at', + ]; + + $foreignKey = 'account_id'; + + $this->buildInsertSQLQuery('journal_entries', $foreignKey, $columns, $data); + } + + /** + * Export the Life Event Category table. + * + * @param array $data + */ + private function exportLifeEventCategory(array $data) + { + $columns = [ + 'id', + 'account_id', + 'name', + 'default_life_event_category_key', + 'core_monica_data', + 'created_at', + 'updated_at', + ]; + + $foreignKey = 'account_id'; + + $this->buildInsertSQLQuery('life_event_categories', $foreignKey, $columns, $data); + } + + /** + * Export the Life Event Type table. + * + * @param array $data + */ + private function exportLifeEventType(array $data) + { + $columns = [ + 'id', + 'account_id', + 'life_event_category_id', + 'name', + 'default_life_event_type_key', + 'core_monica_data', + 'specific_information_structure', + 'created_at', + 'updated_at', + ]; + + $foreignKey = 'account_id'; + + $this->buildInsertSQLQuery('life_event_types', $foreignKey, $columns, $data); + } + + /** + * Export the Life Event table. + * + * @param array $data + */ + private function exportLifeEvent(array $data) + { + $columns = [ + 'id', + 'account_id', + 'contact_id', + 'life_event_type_id', + 'reminder_id', + 'name', + 'note', + 'happened_at', + 'happened_at_month_unknown', + 'happened_at_day_unknown', + 'specific_information', + 'created_at', + 'updated_at', + ]; + + $foreignKey = 'account_id'; + + $this->buildInsertSQLQuery('life_events', $foreignKey, $columns, $data); + } + + /** + * Export the Message table. + * + * @param array $data + */ + private function exportMessage(array $data) + { + $columns = [ + 'id', + 'account_id', + 'contact_id', + 'conversation_id', + 'content', + 'written_at', + 'written_by_me', + 'created_at', + 'updated_at', + ]; + + $foreignKey = 'account_id'; + + $this->buildInsertSQLQuery('messages', $foreignKey, $columns, $data); + } + + /** + * Export the Metadata love relationship table. + * + * @param array $data + */ + private function exportMetaDataLoveRelationship(array $data) + { + $columns = [ + 'id', + 'account_id', + 'relationship_id', + 'is_active', + 'notes', + 'meet_date', + 'official_date', + 'breakup_date', + 'breakup_reason', + 'created_at', + 'updated_at', + ]; + + $foreignKey = 'account_id'; + + $this->buildInsertSQLQuery('metadata_love_relationships', $foreignKey, $columns, $data); + } + + /** + * Export the Module table. + * + * @param array $data + */ + private function exportModule(array $data) + { + $columns = [ + 'id', + 'account_id', + 'key', + 'translation_key', + 'active', + 'delible', + 'created_at', + 'updated_at', + ]; + + $foreignKey = 'account_id'; + + $this->buildInsertSQLQuery('modules', $foreignKey, $columns, $data); + } + + /** + * Export the Note table. + * + * @param array $data + */ + private function exportNote(array $data) + { + $columns = [ + 'id', + 'account_id', + 'contact_id', + 'body', + 'is_favorited', + 'favorited_at', + 'created_at', + 'updated_at', + ]; + + $foreignKey = 'account_id'; + + $this->buildInsertSQLQuery('notes', $foreignKey, $columns, $data); + } + + /** + * Export the Occupation table. + * + * @param array $data + */ + private function exportOccupation(array $data) + { + $columns = [ + 'id', + 'account_id', + 'contact_id', + 'company_id', + 'title', + 'description', + 'salary', + 'salary_unit', + 'currently_works_here', + 'start_date', + 'end_date', + 'created_at', + 'updated_at', + ]; + + $foreignKey = 'account_id'; + + $this->buildInsertSQLQuery('occupations', $foreignKey, $columns, $data); + } + + /** + * Export the Pet table. + * + * @param array $data + */ + private function exportPet(array $data) + { + $columns = [ + 'id', + 'account_id', + 'contact_id', + 'pet_category_id', + 'name', + 'created_at', + 'updated_at', + ]; + + $foreignKey = 'account_id'; + + $this->buildInsertSQLQuery('pets', $foreignKey, $columns, $data); + } + + /** + * Export the Photo table. + * + * @param array $data + */ + private function exportPhoto(array $data) + { + $columns = [ + 'id', + 'account_id', + 'original_filename', + 'new_filename', + 'filesize', + 'mime_type', + 'created_at', + 'updated_at', + ]; + + $foreignKey = 'account_id'; + + $this->buildInsertSQLQuery('photos', $foreignKey, $columns, $data); + } + + /** + * Export the Place table. + * + * @param array $data + */ + private function exportPlace(array $data) + { + $columns = [ + 'id', + 'account_id', + 'street', + 'city', + 'province', + 'postal_code', + 'country', + 'latitude', + 'longitude', + 'created_at', + 'updated_at', + ]; + + $foreignKey = 'account_id'; + + $this->buildInsertSQLQuery('places', $foreignKey, $columns, $data); + } + + /** + * Export the Recovery Code table. + * + * @param array $data + */ + private function exportRecoveryCode(array $data) + { + $columns = [ + 'id', + 'account_id', + 'user_id', + 'recovery', + 'used', + 'created_at', + 'updated_at', + ]; + + $foreignKey = 'account_id'; + + $this->buildInsertSQLQuery('recovery_codes', $foreignKey, $columns, $data); + } + + /** + * Export the Relationship Type Group table. + * + * @param array $data + */ + private function exportRelationTypeGroup(array $data) + { + $columns = [ + 'id', + 'account_id', + 'name', + 'delible', + 'created_at', + 'updated_at', + ]; + + $foreignKey = 'account_id'; + + $this->buildInsertSQLQuery('relationship_type_groups', $foreignKey, $columns, $data); + } + + /** + * Export the Relationship Type table. + * + * @param array $data + */ + private function exportRelationType(array $data) + { + $columns = [ + 'id', + 'account_id', + 'name', + 'name_reverse_relationship', + 'relationship_type_group_id', + 'delible', + 'created_at', + 'updated_at', + ]; + + $foreignKey = 'account_id'; + + $this->buildInsertSQLQuery('relationship_types', $foreignKey, $columns, $data); + } + + /** + * Export the Relationship. + * + * @param array $data + */ + private function exportRelationship(array $data) + { + $columns = [ + 'id', + 'account_id', + 'relationship_type_id', + 'contact_is', + 'of_contact', + 'created_at', + 'updated_at', + ]; + + $foreignKey = 'account_id'; + + $this->buildInsertSQLQuery('relationships', $foreignKey, $columns, $data); + } + + /** + * Export the Reminder Outbox table. + * + * @param array $data + */ + private function exportReminderOutbox(array $data) + { + $columns = [ + 'id', + 'account_id', + 'reminder_id', + 'user_id', + 'planned_date', + 'nature', + 'notification_number_days_before', + 'created_at', + 'updated_at', + ]; + + $foreignKey = 'account_id'; + + $this->buildInsertSQLQuery('reminder_outbox', $foreignKey, $columns, $data); + } + + /** + * Export the Reminder Rule table. + * + * @param array $data + */ + private function exportReminderRule(array $data) + { + $columns = [ + 'id', + 'account_id', + 'number_of_days_before', + 'active', + 'created_at', + 'updated_at', + ]; + + $foreignKey = 'account_id'; + + $this->buildInsertSQLQuery('reminder_rules', $foreignKey, $columns, $data); + } + + /** + * Export the Reminder table. + * + * @param array $data + */ + private function exportReminder(array $data) + { + $columns = [ + 'id', + 'account_id', + 'contact_id', + 'initial_date', + 'title', + 'description', + 'frequency_type', + 'frequency_number', + 'delible', + 'inactive', + 'created_at', + 'updated_at', + ]; + + $foreignKey = 'account_id'; + + $this->buildInsertSQLQuery('reminders', $foreignKey, $columns, $data); + } + + /** + * Export the Special Date table. + * + * @param array $data + */ + private function exportSpecialDate(array $data) + { + $columns = [ + 'id', + 'account_id', + 'contact_id', + 'uuid', + 'is_age_based', + 'is_year_unknown', + 'date', + 'created_at', + 'updated_at', + ]; + + $foreignKey = 'account_id'; + + $this->buildInsertSQLQuery('special_dates', $foreignKey, $columns, $data); + } + + /** + * Export the Tag table. + * + * @param array $data + */ + private function exportTag(array $data) + { + $columns = [ + 'id', + 'account_id', + 'name', + 'name_slug', + 'description', + 'created_at', + 'updated_at', + ]; + + $foreignKey = 'account_id'; + + $this->buildInsertSQLQuery('tags', $foreignKey, $columns, $data); + } + + /** + * Export the Task table. + * + * @param array $data + */ + private function exportTask(array $data) + { + $columns = [ + 'id', + 'account_id', + 'contact_id', + 'uuid', + 'title', + 'description', + 'completed', + 'completed_at', + 'created_at', + 'updated_at', + ]; + + $foreignKey = 'account_id'; + + $this->buildInsertSQLQuery('tasks', $foreignKey, $columns, $data); + } + + /** + * Export the Term User table. + * + * @param array $data + */ + private function exportTermUser(array $data) + { + $columns = [ + 'account_id', + 'user_id', + 'term_id', + 'ip_address', + 'created_at', + 'updated_at', + ]; + + $foreignKey = 'account_id'; + + $this->buildInsertSQLQuery('term_user', $foreignKey, $columns, $data); + } + + /** + * Export the User table. + * + * @param array $data + */ + private function exportUser(array $data) + { + $columns = [ + 'id', + 'first_name', + 'last_name', + 'email', + 'me_contact_id', + 'admin', + 'email_verified_at', + 'password', + 'remember_token', + 'google2fa_secret', + 'account_id', + 'timezone', + 'currency_id', + 'locale', + 'metric', + 'fluid_container', + 'contacts_sort_order', + 'name_order', + 'invited_by_user_id', + 'dashboard_active_tab', + 'gifts_active_tab', + 'profile_active_tab', + 'profile_new_life_event_badge_seen', + 'temperature_scale', + 'created_at', + 'updated_at', + ]; + + $foreignKey = 'account_id'; + + $this->buildInsertSQLQuery('users', $foreignKey, $columns, $data); + } + + /** + * Export the Weather table. + * + * @param array $data + */ + private function exportWeather(array $data) + { + $columns = [ + 'id', + 'account_id', + 'place_id', + 'weather_json', + 'created_at', + 'updated_at', + ]; + + $foreignKey = 'account_id'; + + $this->buildInsertSQLQuery('weather', $foreignKey, $columns, $data); + } + + /** + * Export the Contact Photo table. + * This is custom as we need to loop on the contacts for this account. + * + * @param array $data + */ + private function exportContactPhoto(array $data) + { + $contacts = DB::table('contacts') + ->select('id') + ->where('account_id', $data['account_id']) + ->get(); + + if ($contacts->count() == 0) { + return; + } + + $sql = 'INSERT IGNORE INTO '.DBHelper::getTable('contact_photo').' (`contact_id`, `photo_id`, `created_at`, `updated_at`) VALUES'.PHP_EOL; + $insertValues = []; + foreach ($contacts as $contact) { + $photos = DB::table('contact_photo') + ->where('contact_id', $contact->id) + ->get(); + + foreach ($photos as $photo) { + array_push($insertValues, ' ('.$photo->contact_id.','.$photo->photo_id.",'".$photo->created_at."','".$photo->updated_at."')"); + } + } + $sql .= implode(','.PHP_EOL, $insertValues); + $this->writeToTempFile($sql.';'.PHP_EOL); + } + + /** + * Export the Audit logs table. + * + * @param array $data + */ + private function exportAuditLogs(array $data) + { + $columns = [ + 'id', + 'account_id', + 'author_id', + 'about_contact_id', + 'author_name', + 'action', + 'objects', + 'should_appear_on_dashboard', + 'audited_at', + 'created_at', + 'updated_at', + ]; + + $foreignKey = 'account_id'; + + $this->buildInsertSQLQuery('audit_logs', $foreignKey, $columns, $data); + } +} diff --git a/app/Services/Auth/Population/PopulateContactFieldTypesTable.php b/app/Services/Auth/Population/PopulateContactFieldTypesTable.php new file mode 100644 index 0000000..e98ff05 --- /dev/null +++ b/app/Services/Auth/Population/PopulateContactFieldTypesTable.php @@ -0,0 +1,99 @@ + 'required|integer|exists:accounts,id', + 'migrate_existing_data' => 'required|boolean', + ]; + } + + /** + * Execute the service. + * + * @param array $data + * @return bool + */ + public function execute(array $data): bool + { + $this->createEntries($data); + + return true; + } + + /** + * Create contact field type entries. + * + * @param array $data + * @return void + */ + private function createEntries($data) + { + $defaultContactFieldTypes = $this->getDefaultContactFieldTypes($data); + + foreach ($defaultContactFieldTypes as $defaultContactFieldType) { + $this->createEntry($defaultContactFieldType, $data); + } + } + + /** + * Get the default contact field types. + * + * @param array $data + * @return Collection + * + * @throws QueryException if the query does not run for some reasons. + */ + private function getDefaultContactFieldTypes($data) + { + if ($data['migrate_existing_data'] == 1) { + $defaultContactFieldTypes = DB::table('default_contact_field_types') + ->get(); + } else { + $defaultContactFieldTypes = DB::table('default_contact_field_types') + ->where('migrated', 0) + ->get(); + } + + return $defaultContactFieldTypes; + } + + /** + * Create an entry in the life event category table. + * + * @param object $defaultContactFieldType + * @param array $data + * @return void + */ + private function createEntry($defaultContactFieldType, $data) + { + ContactFieldType::create([ + 'account_id' => $data['account_id'], + 'name' => $defaultContactFieldType->name, + 'fontawesome_icon' => (is_null($defaultContactFieldType->fontawesome_icon) ? null : $defaultContactFieldType->fontawesome_icon), + 'protocol' => (is_null($defaultContactFieldType->protocol) ? null : $defaultContactFieldType->protocol), + 'delible' => $defaultContactFieldType->delible, + 'type' => (is_null($defaultContactFieldType->type) ? null : $defaultContactFieldType->type), + ]); + } +} diff --git a/app/Services/Auth/Population/PopulateLifeEventsTable.php b/app/Services/Auth/Population/PopulateLifeEventsTable.php new file mode 100644 index 0000000..a9955d7 --- /dev/null +++ b/app/Services/Auth/Population/PopulateLifeEventsTable.php @@ -0,0 +1,170 @@ + 'required|integer|exists:accounts,id', + 'migrate_existing_data' => 'required|boolean', + ]; + } + + /** + * The data needed for the query to be executed. + * + * @var array + */ + private $data; + + /** + * Execute the service. + * + * @param array $givenData + * @return bool + */ + public function execute(array $givenData): bool + { + $this->data = $givenData; + + if (! $this->validate($this->data)) { + return false; + } + + $locale = $this->getLocaleOfAccount($this->data['account_id']); + if (is_null($locale)) { + return false; + } + + $this->createEntries($locale); + + $this->markTableAsMigrated(); + + return true; + } + + /** + * Get the locale associated with the account. + * + * @return string|null + */ + private function getLocaleOfAccount($accountId) + { + // get the account + $account = Account::findOrFail($accountId); + + return $account->getFirstLocale(); + } + + /** + * Create life event category and life event type entries. + * + * @return void + */ + private function createEntries($locale) + { + App::setLocale($locale); + + $defaultLifeEventCategories = $this->getDefaultLifeEventCategories(); + + foreach ($defaultLifeEventCategories as $defaultLifeEventCategory) { + $lifeEventCategory = $this->feedLifeEventCategory($defaultLifeEventCategory); + + $defaultLifeEventTypes = DB::table('default_life_event_types') + ->where('default_life_event_category_id', $defaultLifeEventCategory->id) + ->get(); + + foreach ($defaultLifeEventTypes as $defaultLifeEventType) { + $this->feedLifeEventType($defaultLifeEventType, $lifeEventCategory); + } + } + } + + /** + * Get the default life event categories. + * + * @return Collection + * + * @throws QueryException if the query does not run for some reasons. + */ + private function getDefaultLifeEventCategories() + { + if ($this->data['migrate_existing_data'] == 1) { + $defaultLifeEventCategories = DB::table('default_life_event_categories') + ->get(); + } else { + $defaultLifeEventCategories = DB::table('default_life_event_categories') + ->where('migrated', 0) + ->get(); + } + + return $defaultLifeEventCategories; + } + + /** + * Create an entry in the life event category table. + * + * @param object $defaultLifeEventCategory + * @return LifeEventCategory + */ + private function feedLifeEventCategory($defaultLifeEventCategory): LifeEventCategory + { + return LifeEventCategory::create([ + 'account_id' => $this->data['account_id'], + 'name' => trans('settings.personalization_life_event_category_'.$defaultLifeEventCategory->translation_key), + 'core_monica_data' => true, + 'default_life_event_category_key' => $defaultLifeEventCategory->translation_key, + ]); + } + + /** + * Create an entry in the life event type table. + * + * @param object $defaultLifeEventType + * @return void + */ + private function feedLifeEventType($defaultLifeEventType, $lifeEventCategory) + { + LifeEventType::create([ + 'account_id' => $this->data['account_id'], + 'life_event_category_id' => $lifeEventCategory->id, + 'core_monica_data' => true, + 'specific_information_structure' => $defaultLifeEventType->specific_information_structure, + 'default_life_event_type_key' => $defaultLifeEventType->translation_key, + ]); + } + + /** + * Mark the table as migrated. + * + * @return void + */ + private function markTableAsMigrated() + { + DB::table('default_life_event_categories') + ->update(['migrated' => 1]); + + DB::table('default_life_event_types') + ->update(['migrated' => 1]); + } +} diff --git a/app/Services/Auth/Population/PopulateModulesTable.php b/app/Services/Auth/Population/PopulateModulesTable.php new file mode 100644 index 0000000..c901e4e --- /dev/null +++ b/app/Services/Auth/Population/PopulateModulesTable.php @@ -0,0 +1,107 @@ + 'required|integer|exists:accounts,id', + 'migrate_existing_data' => 'required|boolean', + ]; + } + + /** + * The data needed for the query to be executed. + * + * @var array + */ + private $data; + + /** + * Execute the service. + * + * @param array $givenData + * @return bool + */ + public function execute(array $givenData): bool + { + $this->data = $givenData; + + if (! $this->validate($this->data)) { + return false; + } + + $this->createEntries(); + + return true; + } + + /** + * Create modules entries. + * + * @return void + */ + private function createEntries() + { + $defaultModules = $this->getDefaultModules(); + + foreach ($defaultModules as $defaultModule) { + $this->feedModule($defaultModule); + } + } + + /** + * Get the default modules. + * + * @return Collection + * + * @throws QueryException if the query does not run for some reasons. + */ + private function getDefaultModules() + { + if ($this->data['migrate_existing_data'] == 1) { + $defaultModules = DB::table('default_contact_modules') + ->get(); + } else { + $defaultModules = DB::table('default_contact_modules') + ->where('migrated', 0) + ->get(); + } + + return $defaultModules; + } + + /** + * Create an entry in the module table. + * + * @param object $defaultModule + * @return void + */ + private function feedModule($defaultModule) + { + Module::create([ + 'account_id' => $this->data['account_id'], + 'key' => $defaultModule->key, + 'translation_key' => $defaultModule->translation_key, + 'delible' => $defaultModule->delible, + 'active' => $defaultModule->active, + ]); + } +} diff --git a/app/Services/BaseService.php b/app/Services/BaseService.php new file mode 100644 index 0000000..09ee9fd --- /dev/null +++ b/app/Services/BaseService.php @@ -0,0 +1,78 @@ +rules()) + ->validate(); + + return true; + } + + /** + * Checks if the value is empty or null. + * + * @param mixed $data + * @param mixed $index + * @return mixed + */ + public function nullOrValue($data, $index) + { + $value = Arr::get($data, $index, null); + + return is_null($value) || $value === '' ? null : $value; + } + + /** + * Checks if the value is empty or null and returns a date from a string. + * + * @param mixed $data + * @param mixed $index + * @return mixed + */ + public function nullOrDate($data, $index) + { + $value = Arr::get($data, $index, null); + + return is_null($value) || $value === '' ? null : Carbon::parse($value); + } + + /** + * Returns the value if it's defined, or false otherwise. + * + * @param mixed $data + * @param mixed $index + * @return mixed + */ + public function valueOrFalse($data, $index) + { + if (empty($data[$index])) { + return false; + } + + return $data[$index]; + } +} diff --git a/app/Services/Contact/Address/CreateAddress.php b/app/Services/Contact/Address/CreateAddress.php new file mode 100644 index 0000000..b7740ec --- /dev/null +++ b/app/Services/Contact/Address/CreateAddress.php @@ -0,0 +1,92 @@ + 'required|integer|exists:accounts,id', + 'contact_id' => 'required|integer|exists:contacts,id', + 'name' => 'nullable|string|max:255', + 'street' => 'nullable|string|max:255', + 'city' => 'nullable|string|max:255', + 'province' => 'nullable|string|max:255', + 'postal_code' => 'nullable|string|max:255', + 'country' => 'nullable|string|max:3', + 'latitude' => 'nullable|numeric', + 'longitude' => 'nullable|numeric', + 'labels' => 'nullable|array', + ]; + } + + /** + * Create an address. + * + * @param array $data + * @return Address + */ + public function execute(array $data): Address + { + $this->validate($data); + + $contact = Contact::where('account_id', $data['account_id']) + ->findOrFail($data['contact_id']); + + $contact->throwInactive(); + + $place = $this->createPlace($data); + + $address = Address::create([ + 'account_id' => $data['account_id'], + 'contact_id' => $data['contact_id'], + 'place_id' => $place->id, + 'name' => $this->nullOrValue($data, 'name'), + ]); + + if ($labels = $this->nullOrValue($data, 'labels')) { + app(UpdateAddressLabels::class)->execute([ + 'account_id' => $data['account_id'], + 'address_id' => $address->id, + 'labels' => $labels, + ]); + } + + return $address; + } + + /** + * Create a place for the given address. + * + * @param array $data + * @return Place + */ + private function createPlace(array $data) + { + $request = [ + 'account_id' => $data['account_id'], + 'street' => $this->nullOrValue($data, 'street'), + 'city' => $this->nullOrValue($data, 'city'), + 'province' => $this->nullOrValue($data, 'province'), + 'postal_code' => $this->nullOrValue($data, 'postal_code'), + 'country' => $this->nullOrValue($data, 'country'), + 'latitude' => $this->nullOrValue($data, 'latitude'), + 'longitude' => $this->nullOrValue($data, 'longitude'), + ]; + + return app(CreatePlace::class)->execute($request); + } +} diff --git a/app/Services/Contact/Address/DestroyAddress.php b/app/Services/Contact/Address/DestroyAddress.php new file mode 100644 index 0000000..6f35694 --- /dev/null +++ b/app/Services/Contact/Address/DestroyAddress.php @@ -0,0 +1,48 @@ + 'required|integer|exists:accounts,id', + 'address_id' => 'required|integer|exists:addresses,id', + ]; + } + + /** + * Destroy an address. + * + * @param array $data + * @return bool + */ + public function execute(array $data): bool + { + $this->validate($data); + + $address = Address::where('account_id', $data['account_id']) + ->findOrFail($data['address_id']); + + $address->contact->throwInactive(); + + app(DestroyPlace::class)->execute([ + 'account_id' => $data['account_id'], + 'place_id' => $address->place_id, + ]); + + $address->delete(); + + return true; + } +} diff --git a/app/Services/Contact/Address/UpdateAddress.php b/app/Services/Contact/Address/UpdateAddress.php new file mode 100644 index 0000000..443ef6a --- /dev/null +++ b/app/Services/Contact/Address/UpdateAddress.php @@ -0,0 +1,97 @@ + 'required|integer|exists:accounts,id', + 'contact_id' => 'required|integer|exists:contacts,id', + 'address_id' => 'required|integer|exists:addresses,id', + 'name' => 'nullable|string|max:255', + 'street' => 'nullable|string|max:255', + 'city' => 'nullable|string|max:255', + 'province' => 'nullable|string|max:255', + 'postal_code' => 'nullable|string|max:255', + 'country' => 'nullable|string|max:3', + 'latitude' => 'nullable|numeric', + 'longitude' => 'nullable|numeric', + 'labels' => 'nullable|array', + ]; + } + + /** + * Update an address. + * + * @param array $data + * @return Address + */ + public function execute(array $data): Address + { + $this->validate($data); + + /** @var Address */ + $address = Address::where('account_id', $data['account_id']) + ->where('contact_id', $data['contact_id']) + ->findOrFail($data['address_id']); + + $contact = Contact::where('account_id', $data['account_id']) + ->findOrFail($data['contact_id']); + + $contact->throwInactive(); + + $this->updatePlace($data, $address); + + $address->update([ + 'name' => $this->nullOrValue($data, 'name'), + ]); + + if ($labels = $this->nullOrValue($data, 'labels')) { + app(UpdateAddressLabels::class)->execute([ + 'account_id' => $data['account_id'], + 'address_id' => $address->id, + 'labels' => $labels, + ]); + } + + return $address; + } + + /** + * Create a place for the given address. + * + * @param array $data + * @param Address $address + * @return Place + */ + private function updatePlace(array $data, Address $address) + { + $request = [ + 'account_id' => $data['account_id'], + 'place_id' => $address->place_id, + 'street' => $this->nullOrValue($data, 'street'), + 'city' => $this->nullOrValue($data, 'city'), + 'province' => $this->nullOrValue($data, 'province'), + 'postal_code' => $this->nullOrValue($data, 'postal_code'), + 'country' => $this->nullOrValue($data, 'country'), + 'latitude' => $this->nullOrValue($data, 'latitude'), + 'longitude' => $this->nullOrValue($data, 'longitude'), + ]; + + return app(UpdatePlace::class)->execute($request); + } +} diff --git a/app/Services/Contact/Avatar/GenerateDefaultAvatar.php b/app/Services/Contact/Avatar/GenerateDefaultAvatar.php new file mode 100644 index 0000000..addd9da --- /dev/null +++ b/app/Services/Contact/Avatar/GenerateDefaultAvatar.php @@ -0,0 +1,120 @@ + 'required|integer|exists:contacts,id', + ]; + } + + /** + * Generate the default image for the avatar, based on the initals of the + * contact and returns the filename. + * + * @param array $data + * @return Contact + */ + public function execute(array $data) + { + $this->validate($data); + + $contact = Contact::find($data['contact_id']); + + $contact = $this->generateContactUUID($contact); + + // delete existing default avatar + $contact = $this->deleteExistingDefaultAvatar($contact); + + // create new avatar + $filename = $this->createNewAvatar($contact); + + $contact->avatar_default_url = $filename; + $contact->save(); + + Cache::forget('etag'.Str::before('?', $filename)); + + return $contact; + } + + /** + * Create an uuid for the contact if it does not exist. + * + * @param Contact $contact + * @return Contact + */ + private function generateContactUUID(Contact $contact) + { + if (! $contact->uuid) { + $contact->uuid = Str::uuid()->toString(); + $contact->save(); + } + + return $contact; + } + + /** + * Create a new avatar for the contact based on the name of the contact. + * + * @param Contact $contact + * @return string + */ + private function createNewAvatar(Contact $contact) + { + $img = null; + try { + $img = Avatar::create($contact->name) + ->setBackground($contact->default_avatar_color) + ->getImageObject() + ->encode('jpg'); + + $filename = 'avatars/'.$contact->uuid.'.jpg'; + Storage::disk(config('filesystems.default')) + ->put($filename, $img, config('filesystems.default_visibility')); + + // This will force the browser to reload the new avatar + return $filename.'?'.now()->format('U'); + } finally { + if ($img) { + $img->destroy(); + } + } + } + + /** + * Delete the existing default avatar. + * + * @param Contact $contact + * @return Contact + */ + private function deleteExistingDefaultAvatar(Contact $contact) + { + if ($contact->avatar_default_url !== null) { + try { + Storage::disk(config('filesystems.default')) + ->delete($contact->avatar_default_url); + $contact->avatar_default_url = null; + } catch (FileNotFoundException $e) { + // ignore + } + } + + return $contact; + } +} diff --git a/app/Services/Contact/Avatar/GetAdorableAvatarURL.php b/app/Services/Contact/Avatar/GetAdorableAvatarURL.php new file mode 100644 index 0000000..d3c9db4 --- /dev/null +++ b/app/Services/Contact/Avatar/GetAdorableAvatarURL.php @@ -0,0 +1,53 @@ + 'required|string', + 'size' => 'nullable|integer|between:1,2000', + ]; + } + + /** + * Get an url for an adorable avatar. + * - http://avatars.adorable.io/ gives avatars based on a random string. + * + * @param array $data + * @return string|null + */ + public function execute(array $data) + { + $this->validate($data); + + $size = $this->size($data); + + return $size.'/'.$data['uuid'].'.png'; + } + + /** + * Get the size for the avatar, based on a given parameter. Provides a + * default otherwise. + * + * @param array $data + * @return int + */ + private function size(array $data) + { + if (isset($data['size'])) { + return $data['size']; + } + + return (int) config('monica.avatar_size'); + } +} diff --git a/app/Services/Contact/Avatar/GetAvatarsFromInternet.php b/app/Services/Contact/Avatar/GetAvatarsFromInternet.php new file mode 100644 index 0000000..047ff53 --- /dev/null +++ b/app/Services/Contact/Avatar/GetAvatarsFromInternet.php @@ -0,0 +1,99 @@ + 'required|integer|exists:contacts,id', + ]; + } + + /** + * Query both Gravatar and Adorable Avatars based on the email address of + * the contact. + * + * - http://avatars.adorable.io/ gives avatars based on a random string. + * This random string comes from the `avatar_adorable_uuid` field in the + * Contact object. + * - Gravatar only gives an avatar only if it's set. + * + * @param array $data + * @return Contact + */ + public function execute(array $data): Contact + { + $this->validate($data); + + $contact = Contact::findOrFail($data['contact_id']); + + $contact = $this->getAdorable($contact); + $contact = $this->getGravatar($contact); + + return $contact; + } + + /** + * Generate the UUID used to identify the contact in the Adorable service. + * + * @param Contact $contact + * @return Contact + */ + private function generateUUID(Contact $contact) + { + if (empty($contact->avatar_adorable_uuid)) { + $contact->avatar_adorable_uuid = Str::uuid()->toString(); + $contact->save(); + } + + return $contact; + } + + /** + * Get the adorable avatar. + * + * @param Contact $contact + * @return Contact + */ + private function getAdorable(Contact $contact) + { + // prevent timestamp update + $timestamps = $contact->timestamps; + $contact->timestamps = false; + + $contact = $this->generateUUID($contact); + $contact->avatar_adorable_url = app(GetAdorableAvatarURL::class)->execute([ + 'uuid' => $contact->avatar_adorable_uuid, + 'size' => 200, + ]); + $contact->save(); + + $contact->timestamps = $timestamps; + + return $contact; + } + + /** + * Query Gravatar (if it exists) for the contact's email address. + * + * @param Contact $contact + * @return Contact + */ + private function getGravatar(Contact $contact) + { + return app(GetGravatar::class)->execute([ + 'contact_id' => $contact->id, + ]); + } +} diff --git a/app/Services/Contact/Avatar/GetGravatar.php b/app/Services/Contact/Avatar/GetGravatar.php new file mode 100644 index 0000000..a413976 --- /dev/null +++ b/app/Services/Contact/Avatar/GetGravatar.php @@ -0,0 +1,110 @@ + 'required|integer|exists:contacts,id', + ]; + } + + public function execute(array $data): Contact + { + $this->validate($data); + + /** @var Contact */ + $contact = Contact::findOrFail($data['contact_id']); + + // prevent timestamp update + $timestamps = $contact->timestamps; + $contact->timestamps = false; + + $contact = $this->getGravatar($contact); + $contact->save(); + + $contact->timestamps = $timestamps; + + return $contact; + } + + /** + * Get the emails of the contact, based on the contact fields. + * + * @param Contact $contact + * @return Collection + */ + private function getEmails(Contact $contact) + { + $emails = collect(); + + $contactFields = $contact->contactFields() + ->email() + ->get(); + foreach ($contactFields as $contactField) { + try { + $email = $contactField->data; + + Validator::make(['email' => $email], ['email' => 'email']) + ->validate(); + + $emails->push($email); + } catch (ModelNotFoundException $e) { + // Not found + } catch (ValidationException $e) { + // Not an email + } + } + + return $emails; + } + + /** + * Query Gravatar (if it exists) for the contact's email address. + * + * @param Contact $contact + * @return Contact + */ + private function getGravatar(Contact $contact) + { + $emails = $this->getEmails($contact); + $gravatarUrl = null; + + foreach ($emails as $email) { + $gravatarUrl = app(GetGravatarURL::class)->execute([ + 'email' => $email, + 'size' => config('monica.avatar_size'), + ]); + if ($gravatarUrl) { + break; + } + } + + if ($gravatarUrl) { + $contact->avatar_gravatar_url = $gravatarUrl; + } else { + // in this case we need to make sure that we reset the gravatar URL + $contact->avatar_gravatar_url = null; + + if ($contact->avatar_source == 'gravatar') { + $contact->avatar_source = 'adorable'; + } + } + + return $contact; + } +} diff --git a/app/Services/Contact/Avatar/GetGravatarURL.php b/app/Services/Contact/Avatar/GetGravatarURL.php new file mode 100644 index 0000000..e3048f7 --- /dev/null +++ b/app/Services/Contact/Avatar/GetGravatarURL.php @@ -0,0 +1,77 @@ + 'required|email', + 'size' => 'nullable|integer|between:1,2000', + ]; + } + + /** + * Get Gravatar, if it exists. + * + * @param array $data + * @return string|null + */ + public function execute(array $data): ?string + { + $this->validate($data); + + if ($this->exists($data)) { + $size = $this->size($data); + + return Gravatar::get($data['email'], [ + 'size' => $size, + 'secure' => App::environment('production'), + ]); + } + + return null; + } + + /** + * Test given email. + * + * @param array $data + * @return bool + */ + private function exists(array $data) + { + try { + return Gravatar::exists($data['email']); + } catch (\Exception $e) { + // catch invalid email + return false; + } + } + + /** + * Get the size for the gravatar, based on a given parameter. Provides a + * default otherwise. + * + * @param array $data + * @return int + */ + private function size(array $data) + { + if (isset($data['size'])) { + return $data['size']; + } + + return (int) config('monica.avatar_size'); + } +} diff --git a/app/Services/Contact/Avatar/UpdateAvatar.php b/app/Services/Contact/Avatar/UpdateAvatar.php new file mode 100644 index 0000000..028505f --- /dev/null +++ b/app/Services/Contact/Avatar/UpdateAvatar.php @@ -0,0 +1,75 @@ + 'required|integer|exists:accounts,id', + 'contact_id' => 'required|integer|exists:contacts,id', + 'source' => [ + 'required', + Rule::in([ + 'default', + 'adorable', + 'gravatar', + 'photo', + ]), + ], + 'photo_id' => 'required_if:source,photo|integer|exists:photos,id', + ]; + } + + /** + * Update message in a conversation. + * + * @param array $data + * @return Contact + */ + public function execute(array $data): Contact + { + $this->validate($data); + + /** @var Contact */ + $contact = Contact::where('account_id', $data['account_id']) + ->findOrFail($data['contact_id']); + + $contact->throwInactive(); + + if (isset($data['photo_id'])) { + Photo::where('account_id', $data['account_id']) + ->findOrFail($data['photo_id']); + } + + $contact->avatar_source = $data['source']; + switch ($contact->avatar_source) { + case 'photo': + // in case of a photo, set the photo as the avatar + $contact->avatar_photo_id = $this->nullOrValue($data, 'photo_id'); + $contact->photos()->syncWithoutDetaching([$this->nullOrValue($data, 'photo_id')]); + break; + default: + $contact->avatar_photo_id = null; + break; + } + + $contact->save(); + + return $contact; + } +} diff --git a/app/Services/Contact/Call/CreateCall.php b/app/Services/Contact/Call/CreateCall.php new file mode 100644 index 0000000..64a56e6 --- /dev/null +++ b/app/Services/Contact/Call/CreateCall.php @@ -0,0 +1,94 @@ + 'required|integer|exists:accounts,id', + 'contact_id' => 'required|integer', + 'called_at' => 'required|date', + 'content' => 'nullable|string', + 'contact_called' => 'nullable|boolean', + 'emotions' => 'nullable|array', + ]; + } + + /** + * Create a call. + * + * @param array $data + * @return Call + */ + public function execute(array $data): Call + { + $this->validate($data); + + $contact = Contact::where('account_id', $data['account_id']) + ->findOrFail($data['contact_id']); + + $contact->throwInactive(); + + // emotions array is left out as they are not attached during this call + $call = Call::create(Arr::except($data, ['emotions'])); + + $this->updateLastCallInfo($contact, $call); + + if (! empty($data['emotions'])) { + if ($data['emotions'] != '') { + $this->addEmotions($data['emotions'], $call); + } + } + + return $call; + } + + /** + * Add emotions to the call. + * + * @param array $emotions + * @param Call $call + * @return void + */ + private function addEmotions(array $emotions, Call $call) + { + foreach ($emotions as $emotionId) { + $emotion = Emotion::findOrFail($emotionId); + $call->emotions()->syncWithoutDetaching([$emotion->id => [ + 'account_id' => $call->account_id, + 'contact_id' => $call->contact_id, + ]]); + } + } + + /** + * Update last call information of the contact. + * + * @param Contact $contact + * @param Call $call + * @return void + */ + private function updateLastCallInfo(Contact $contact, Call $call) + { + if (is_null($contact->last_talked_to)) { + $contact->last_talked_to = $call->called_at; + } else { + $contact->last_talked_to = $contact->last_talked_to->max($call->called_at); + } + + $contact->save(); + } +} diff --git a/app/Services/Contact/Call/DestroyCall.php b/app/Services/Contact/Call/DestroyCall.php new file mode 100644 index 0000000..5b0133c --- /dev/null +++ b/app/Services/Contact/Call/DestroyCall.php @@ -0,0 +1,64 @@ + 'required|integer|exists:accounts,id', + 'call_id' => 'required|integer', + ]; + } + + /** + * Destroy a call. + * + * @param array $data + * @return bool + */ + public function execute(array $data): bool + { + $this->validate($data); + + $call = Call::where('account_id', $data['account_id']) + ->findOrFail($data['call_id']); + + $contact = $call->contact; + + $contact->throwInactive(); + + // delete all associations with emotions + $call->emotions()->sync([]); + + $call->delete(); + + $this->updateLastCallInfo($contact); + + return true; + } + + /** + * Update last call information of the contact. + * + * @param Contact $contact + * @return void + */ + private function updateLastCallInfo(Contact $contact) + { + // look for all the calls of the contact and take the most recent call + // as the one we just deleted could have been the most recent call + $contact->last_talked_to = optional($contact->calls->first())->called_at; + $contact->save(); + } +} diff --git a/app/Services/Contact/Call/UpdateCall.php b/app/Services/Contact/Call/UpdateCall.php new file mode 100644 index 0000000..58d15d7 --- /dev/null +++ b/app/Services/Contact/Call/UpdateCall.php @@ -0,0 +1,102 @@ + 'required|integer|exists:accounts,id', + 'call_id' => 'required|integer|exists:calls,id', + 'called_at' => 'required|date', + 'content' => 'nullable|string', + 'contact_called' => 'nullable|boolean', + 'emotions' => 'nullable|array', + ]; + } + + /** + * Update a call. + * + * @param array $data + * @return Call + */ + public function execute(array $data): Call + { + $this->validate($data); + + /** @var Call */ + $call = Call::where('account_id', $data['account_id']) + ->findOrFail($data['call_id']); + + $call->contact->throwInactive(); + + $call->update([ + 'called_at' => $data['called_at'], + 'content' => (empty($data['content']) ? null : $data['content']), + 'contact_called' => (empty($data['contact_called']) ? null : $data['contact_called']), + ]); + + // emotions array is left out as they are not attached during this call + if (! empty($data['emotions'])) { + if ($data['emotions'] != '') { + $this->addEmotions($data['emotions'], $call); + } + } + + $this->updateLastCallInfo($call); + + return $call; + } + + /** + * Add emotions to the call. + * + * @param array $emotions + * @param Call $call + * @return void + */ + private function addEmotions(array $emotions, Call $call) + { + // reset current emotions + $call->emotions()->sync([]); + + // saving new emotions + foreach ($emotions as $emotionId) { + $emotion = Emotion::findOrFail($emotionId); + $call->emotions()->syncWithoutDetaching([$emotion->id => [ + 'account_id' => $call->account_id, + 'contact_id' => $call->contact_id, + ]]); + } + } + + /** + * Update last call information of the contact. + * + * @param Call $call + * @return void + */ + private function updateLastCallInfo(Call $call) + { + /** @var \App\Models\Contact\Contact */ + $contact = $call->contact; + if (is_null($contact->last_talked_to)) { + $contact->last_talked_to = $call->called_at; + } else { + $contact->last_talked_to = $contact->last_talked_to->max($call->called_at); + } + + $contact->save(); + } +} diff --git a/app/Services/Contact/Contact/CreateContact.php b/app/Services/Contact/Contact/CreateContact.php new file mode 100644 index 0000000..556b759 --- /dev/null +++ b/app/Services/Contact/Contact/CreateContact.php @@ -0,0 +1,262 @@ + 'required|integer|exists:accounts,id', + 'author_id' => 'required|integer|exists:users,id', + 'uuid' => 'nullable|string', + 'address_book_id' => 'nullable|integer|exists:addressbooks,id', + 'first_name' => 'required|string|max:255', + 'middle_name' => 'nullable|string|max:255', + 'last_name' => 'nullable|string|max:255', + 'nickname' => 'nullable|string|max:255', + 'email' => 'nullable|string|max:255', + 'gender_id' => 'nullable|integer|exists:genders,id', + 'description' => 'nullable|string|max:255', + 'is_partial' => 'nullable|boolean', + 'is_birthdate_known' => 'required|boolean', + 'birthdate_day' => 'nullable|integer', + 'birthdate_month' => 'nullable|integer', + 'birthdate_year' => 'nullable|integer', + 'birthdate_is_age_based' => 'nullable|boolean', + 'birthdate_age' => 'nullable|integer', + 'birthdate_add_reminder' => 'nullable|boolean', + 'is_deceased' => 'required|boolean', + 'is_deceased_date_known' => 'required|boolean', + 'deceased_date_day' => 'nullable|integer', + 'deceased_date_month' => 'nullable|integer', + 'deceased_date_year' => 'nullable|integer', + 'deceased_date_add_reminder' => 'nullable|boolean', + ]; + } + + /** + * Create a contact. + * + * @param array $data + * @return Contact + */ + public function execute(array $data): Contact + { + $this->validate($data); + + $account = Account::find($data['account_id']); + if (AccountHelper::hasReachedContactLimit($account) + && AccountHelper::hasLimitations($account) + && ! $account->legacy_free_plan_unlimited_contacts) { + abort(402); + } + + if (Arr::get($data, 'address_book_id')) { + AddressBook::where('account_id', $data['account_id']) + ->findOrFail($data['address_book_id']); + } + + $contact = $this->create($data); + + $this->updateBirthDayInformation($data, $contact); + $this->updateDeceasedInformation($data, $contact); + $this->updateEmail($data, $contact); + $this->generateUUID($contact); + $this->addAvatars($contact); + + $this->log($data, $contact); + + // we query the DB again to fill the object with all the new properties + $contact->refresh(); + + return $contact; + } + + /** + * Create the contact. + * + * @param array $data + * @return Contact + */ + private function create(array $data): Contact + { + // filter out the data that shall not be updated here + $dataOnly = Arr::except( + $data, + [ + 'author_id', + 'email', + 'is_birthdate_known', + 'birthdate_day', + 'birthdate_month', + 'birthdate_year', + 'birthdate_is_age_based', + 'birthdate_age', + 'birthdate_add_reminder', + 'is_deceased', + 'is_deceased_date_known', + 'deceased_date_day', + 'deceased_date_month', + 'deceased_date_year', + 'deceased_date_add_reminder', + ] + ); + + if (! empty($uuid = Arr::get($data, 'uuid')) && Uuid::isValid($uuid)) { + $dataOnly['uuid'] = $uuid; + } + + return Contact::create($dataOnly); + } + + /** + * Generates a UUID for this contact. + * + * @param Contact $contact + * @return void + */ + private function generateUUID(Contact $contact) + { + if (empty($contact->uuid)) { + $contact->uuid = Str::uuid()->toString(); + $contact->save(); + } + } + + /** + * Add the different default avatars. + * + * @param Contact $contact + * @return void + */ + private function addAvatars(Contact $contact) + { + // set the default avatar color + $contact->setAvatarColor(); + $contact->save(); + + // populate the avatar from Adorable and grab the Gravatar + GetAvatarsFromInternet::dispatch($contact); + + // also generate the default avatar + GenerateDefaultAvatar::dispatch($contact); + } + + /** + * Update the information about the birthday. + * + * @param array $data + * @param Contact $contact + * @return void + */ + private function updateBirthDayInformation(array $data, Contact $contact) + { + app(UpdateBirthdayInformation::class)->execute([ + 'account_id' => $data['account_id'], + 'contact_id' => $contact->id, + 'is_date_known' => $data['is_birthdate_known'], + 'day' => $this->nullOrvalue($data, 'birthdate_day'), + 'month' => $this->nullOrvalue($data, 'birthdate_month'), + 'year' => $this->nullOrvalue($data, 'birthdate_year'), + 'is_age_based' => $this->nullOrvalue($data, 'birthdate_is_age_based'), + 'age' => $this->nullOrvalue($data, 'birthdate_age'), + 'add_reminder' => $this->nullOrvalue($data, 'birthdate_add_reminder'), + 'is_deceased' => $data['is_deceased'], + ]); + } + + /** + * Adds a contact field containing the email address. + * + * @param array $data + * @param Contact $contact + * @return void + */ + private function updateEmail(array $data, Contact $contact) + { + $contactFieldType = ContactFieldType::where([ + 'account_id' => $data['account_id'], + 'type' => ContactFieldType::EMAIL, + ])->first(); + + if (is_null($contactFieldType) || is_null($this->nullOrvalue($data, 'email'))) { + return; + } + + app(CreateContactField::class)->execute([ + 'account_id' => $data['account_id'], + 'contact_id' => $contact->id, + 'contact_field_type_id' => $contactFieldType->id, + 'data' => $data['email'], + ]); + } + + /** + * Update the information about the date of death. + * + * @param array $data + * @param Contact $contact + * @return void + */ + private function updateDeceasedInformation(array $data, Contact $contact) + { + app(UpdateDeceasedInformation::class)->execute([ + 'account_id' => $data['account_id'], + 'contact_id' => $contact->id, + 'is_deceased' => $data['is_deceased'], + 'is_date_known' => $data['is_deceased_date_known'], + 'day' => $this->nullOrValue($data, 'deceased_date_day'), + 'month' => $this->nullOrValue($data, 'deceased_date_month'), + 'year' => $this->nullOrValue($data, 'deceased_date_year'), + 'add_reminder' => $this->nullOrValue($data, 'deceased_date_add_reminder'), + ]); + } + + /** + * Add an audit log. + * + * @param array $data + * @param Contact $contact + * @return void + */ + private function log(array $data, Contact $contact): void + { + $author = User::find($data['author_id']); + + LogAccountAudit::dispatch([ + 'action' => 'contact_created', + 'account_id' => $author->account_id, + 'about_contact_id' => $contact->id, + 'author_id' => $author->id, + 'author_name' => $author->name, + 'audited_at' => now(), + 'should_appear_on_dashboard' => true, + 'objects' => json_encode([ + 'contact_name' => $contact->name, + 'contact_id' => $contact->id, + ]), + ]); + } +} diff --git a/app/Services/Contact/Contact/DeleteMeContact.php b/app/Services/Contact/Contact/DeleteMeContact.php new file mode 100644 index 0000000..b4214d3 --- /dev/null +++ b/app/Services/Contact/Contact/DeleteMeContact.php @@ -0,0 +1,43 @@ + 'required|integer|exists:accounts,id', + 'user_id' => 'required|integer|exists:users,id', + ]; + } + + /** + * Set a contact as 'me' contact. + * + * @param array $data + * @return User + */ + public function execute(array $data): User + { + $this->validate($data); + + /** @var User */ + $user = User::where('account_id', $data['account_id']) + ->findOrFail($data['user_id']); + + $user->me_contact_id = null; + $user->save(); + + return $user; + } +} diff --git a/app/Services/Contact/Contact/DestroyContact.php b/app/Services/Contact/Contact/DestroyContact.php new file mode 100644 index 0000000..6438d61 --- /dev/null +++ b/app/Services/Contact/Contact/DestroyContact.php @@ -0,0 +1,89 @@ + 'required|integer|exists:accounts,id', + 'contact_id' => 'required|integer|exists:contacts,id', + 'force_delete' => 'nullable|boolean', + ]; + } + + /** + * Destroy a contact. + * + * @param array $data + * @return void + */ + public function handle(array $data): void + { + $this->validate($data); + + $contact = Contact::where('account_id', $data['account_id']) + ->findOrFail($data['contact_id']); + + $contact->throwInactive(); + + $this->destroyRelationships($data, $contact); + + $contact->deleteAvatars(); + + if ($this->valueOrFalse($data, 'force_delete') === true) { + $contact->forceDelete(); + } else { + $contact->delete(); + } + } + + /** + * Destroy all associated relationships. + * + * @param array $data + * @param Contact $contact + * @return void + */ + private function destroyRelationships(array $data, Contact $contact) + { + $relationships = Relationship::where('contact_is', $contact->id)->get(); + $this->destroySpecificRelationships($data, $relationships); + + $relationships = Relationship::where('of_contact', $contact->id)->get(); + $this->destroySpecificRelationships($data, $relationships); + } + + /** + * Delete specific relationships. + * + * @param array $data + * @param \Illuminate\Support\Collection $relationships + * @return void + */ + private function destroySpecificRelationships(array $data, $relationships) + { + foreach ($relationships as $relationship) { + app(DestroyRelationship::class) + ->execute([ + 'account_id' => $data['account_id'], + 'relationship_id' => $relationship->id, + ]); + } + } +} diff --git a/app/Services/Contact/Contact/SetMeContact.php b/app/Services/Contact/Contact/SetMeContact.php new file mode 100644 index 0000000..9c0f7c9 --- /dev/null +++ b/app/Services/Contact/Contact/SetMeContact.php @@ -0,0 +1,53 @@ + 'required|integer|exists:accounts,id', + 'user_id' => 'required|integer|exists:users,id', + 'contact_id' => 'required|integer|exists:contacts,id', + ]; + } + + /** + * Set a contact as 'me' contact. + * + * @param array $data + * @return User + */ + public function execute(array $data): User + { + $this->validate($data); + + /** @var User */ + $user = User::where('account_id', $data['account_id']) + ->findOrFail($data['user_id']); + + if (AccountHelper::hasLimitations($user->account)) { + abort(402); + } + + /** @var Contact */ + $contact = Contact::where('account_id', $data['account_id']) + ->findOrFail($data['contact_id']); + + $user->me_contact_id = $contact->id; + $user->save(); + + return $user; + } +} diff --git a/app/Services/Contact/Contact/UpdateBirthdayInformation.php b/app/Services/Contact/Contact/UpdateBirthdayInformation.php new file mode 100644 index 0000000..c3ffc1a --- /dev/null +++ b/app/Services/Contact/Contact/UpdateBirthdayInformation.php @@ -0,0 +1,201 @@ + 'required|integer|exists:accounts,id', + 'contact_id' => 'required|integer|exists:contacts,id', + 'is_date_known' => 'required|boolean', + 'is_age_based' => 'nullable|boolean', + 'day' => [ + 'integer', + 'nullable', + Rule::requiredIf(function () { + return Arr::get($this->data, 'is_date_known', false) && ! Arr::get($this->data, 'is_age_based', false); + }), + ], + 'month' => [ + 'integer', + 'nullable', + Rule::requiredIf(function () { + return Arr::get($this->data, 'is_date_known', false) && ! Arr::get($this->data, 'is_age_based', false); + }), + ], + 'year' => 'nullable|integer', + 'age' => [ + 'integer', + 'nullable', + Rule::requiredIf(function () { + return Arr::get($this->data, 'is_date_known', false) && Arr::get($this->data, 'is_age_based', false); + }), + ], + 'add_reminder' => 'nullable|boolean', + ]; + } + + /** + * Update the information about the birthday. + * + * @param array $data + * @return Contact + */ + public function execute(array $data) + { + $this->data = $data; + $this->validate($data); + + /** @var Contact */ + $contact = Contact::where('account_id', $data['account_id']) + ->findOrFail($data['contact_id']); + + $contact->throwInactive(); + + $this->clearRelatedReminder($contact); + + $this->clearRelatedSpecialDate($contact); + + $this->manageBirthday($data, $contact); + + return $contact; + } + + /** + * Delete related reminder. + * + * @param Contact $contact + * @return void + */ + private function clearRelatedReminder(Contact $contact) + { + if (is_null($contact->birthday_reminder_id)) { + return; + } + + app(DestroyReminder::class)->execute([ + 'account_id' => $contact->account_id, + 'reminder_id' => $contact->birthday_reminder_id, + ]); + } + + /** + * Delete related special date. + * + * @param Contact $contact + * @return void + */ + private function clearRelatedSpecialDate(Contact $contact) + { + $specialDate = SpecialDate::find($contact->birthday_special_date_id); + if (! is_null($specialDate)) { + $specialDate->delete(); + } + } + + /** + * Update birthday information depending on the type of information. + * + * @param array $data + * @param Contact $contact + * @return void + */ + private function manageBirthday(array $data, Contact $contact): void + { + if (! $data['is_date_known']) { + return; + } + + if ($data['is_age_based']) { + $this->approximate($data, $contact); + } else { + $this->exact($data, $contact); + } + } + + /** + * Case where the birthday is approximate. That means the birthdate is based + * on the estimated age of the contact. + * + * @param array $data + * @param Contact $contact + * @return void + */ + private function approximate(array $data, Contact $contact) + { + $contact->setSpecialDateFromAge('birthdate', $data['age']); + } + + /** + * Case where we have a year, month and day for the birthday. + * + * @param array $data + * @param Contact $contact + * @return void + */ + private function exact(array $data, Contact $contact) + { + $specialDate = $contact->setSpecialDate( + 'birthdate', + (is_null($data['year']) ? 0 : $data['year']), + $data['month'], + $data['day'] + ); + + $this->setReminder($data, $contact, $specialDate); + } + + /** + * Set a reminder for the given special date, if required. + * + * @param array $data + * @param Contact $contact + * @param SpecialDate $specialDate + * @return void + */ + private function setReminder(array $data, Contact $contact, SpecialDate $specialDate) + { + if (empty($data['add_reminder'])) { + return; + } + + $reminder = app(CreateReminder::class)->execute([ + 'account_id' => $data['account_id'], + 'contact_id' => $data['contact_id'], + 'initial_date' => DateHelper::getDate($specialDate), + 'frequency_type' => 'year', + 'frequency_number' => 1, + 'title' => trans( + ($data['is_deceased'] ? + 'people.people_add_birthday_reminder_deceased' : 'people.people_add_birthday_reminder'), + ['name' => $contact->first_name] + ), + 'delible' => false, + ]); + + $contact->birthday_reminder_id = $reminder->id; + $contact->save(); + } +} diff --git a/app/Services/Contact/Contact/UpdateContact.php b/app/Services/Contact/Contact/UpdateContact.php new file mode 100644 index 0000000..c33cd90 --- /dev/null +++ b/app/Services/Contact/Contact/UpdateContact.php @@ -0,0 +1,177 @@ + 'required|integer|exists:accounts,id', + 'author_id' => 'required|integer|exists:users,id', + 'contact_id' => 'required|integer', + 'uuid' => 'nullable|string', + 'first_name' => 'required|string|max:255', + 'middle_name' => 'nullable|string|max:255', + 'last_name' => 'nullable|string|max:255', + 'nickname' => 'nullable|string|max:255', + 'gender_id' => 'nullable|integer|exists:genders,id', + 'description' => 'nullable|string|max:255', + 'is_partial' => 'nullable|boolean', + 'is_birthdate_known' => 'required|boolean', + 'birthdate_day' => 'nullable|integer', + 'birthdate_month' => 'nullable|integer', + 'birthdate_year' => 'nullable|integer', + 'birthdate_is_age_based' => 'nullable|boolean', + 'birthdate_age' => 'nullable|integer', + 'birthdate_add_reminder' => 'nullable|boolean', + 'is_deceased' => 'nullable|boolean', + 'is_deceased_date_known' => 'required|boolean', + 'deceased_date_day' => 'nullable|integer', + 'deceased_date_month' => 'nullable|integer', + 'deceased_date_year' => 'nullable|integer', + 'deceased_date_add_reminder' => 'nullable|boolean', + ]; + } + + /** + * Update a contact. + * + * @param array $data + * @return Contact + */ + public function execute(array $data): Contact + { + $this->data = $data; + $this->validate($this->data); + + /* @var Contact */ + $this->contact = Contact::where('account_id', $data['account_id']) + ->findOrFail($data['contact_id']); + + $this->contact->throwInactive(); + + // Test is the account is limited and the contact should be updated as real contact + $account = Account::find($data['account_id']); + if ($this->contact->is_partial + && ! $this->valueOrFalse($this->data, 'is_partial') + && AccountHelper::hasReachedContactLimit($account) + && AccountHelper::hasLimitations($account) + && ! $account->legacy_free_plan_unlimited_contacts) { + abort(402); + } + + $this->updateGeneralInformation(); + $this->updateDescription(); + $this->updateBirthDayInformation(); + $this->updateDeceasedInformation(); + + return $this->contact->refresh(); + } + + private function updateGeneralInformation(): void + { + // filter out the data that shall not be updated here + $dataOnly = Arr::except( + $this->data, + [ + 'author_id', + 'uuid', + 'is_birthdate_known', + 'birthdate_day', + 'birthdate_month', + 'birthdate_year', + 'birthdate_is_age_based', + 'birthdate_age', + 'birthdate_add_reminder', + 'is_deceased', + 'is_deceased_date_known', + 'deceased_date_day', + 'deceased_date_month', + 'deceased_date_year', + 'deceased_date_add_reminder', + 'description', + ] + ); + + if (! empty($uuid = Arr::get($this->data, 'uuid')) && Uuid::isValid($uuid)) { + $dataOnly['uuid'] = $uuid; + } + + $oldName = $this->contact->name; + $this->contact->update($dataOnly); + + // only update the avatar if the name has changed + if ($oldName != $this->contact->name) { + GenerateDefaultAvatar::dispatch($this->contact); + } + } + + private function updateDescription(): void + { + if (is_null($this->nullOrValue($this->data, 'description'))) { + app(ClearPersonalDescription::class)->execute([ + 'account_id' => $this->data['account_id'], + 'contact_id' => $this->data['contact_id'], + 'author_id' => $this->data['author_id'], + ]); + } else { + if ($this->contact->description != $this->data['description']) { + app(SetPersonalDescription::class)->execute([ + 'account_id' => $this->data['account_id'], + 'contact_id' => $this->data['contact_id'], + 'author_id' => $this->data['author_id'], + 'description' => $this->data['description'], + ]); + } + } + } + + private function updateBirthDayInformation(): void + { + app(UpdateBirthdayInformation::class)->execute([ + 'account_id' => $this->data['account_id'], + 'contact_id' => $this->contact->id, + 'is_date_known' => $this->data['is_birthdate_known'], + 'day' => $this->nullOrvalue($this->data, 'birthdate_day'), + 'month' => $this->nullOrvalue($this->data, 'birthdate_month'), + 'year' => $this->nullOrvalue($this->data, 'birthdate_year'), + 'is_age_based' => $this->nullOrvalue($this->data, 'birthdate_is_age_based'), + 'age' => $this->nullOrvalue($this->data, 'birthdate_age'), + 'add_reminder' => $this->nullOrvalue($this->data, 'birthdate_add_reminder'), + 'is_deceased' => $this->data['is_deceased'], + ]); + } + + private function updateDeceasedInformation(): void + { + app(UpdateDeceasedInformation::class)->execute([ + 'account_id' => $this->data['account_id'], + 'contact_id' => $this->contact->id, + 'is_deceased' => $this->data['is_deceased'], + 'is_date_known' => $this->data['is_deceased_date_known'], + 'day' => $this->nullOrvalue($this->data, 'deceased_date_day'), + 'month' => $this->nullOrvalue($this->data, 'deceased_date_month'), + 'year' => $this->nullOrvalue($this->data, 'deceased_date_year'), + 'add_reminder' => $this->nullOrvalue($this->data, 'deceased_date_add_reminder'), + ]); + } +} diff --git a/app/Services/Contact/Contact/UpdateContactFoodPreferences.php b/app/Services/Contact/Contact/UpdateContactFoodPreferences.php new file mode 100644 index 0000000..3fec1f5 --- /dev/null +++ b/app/Services/Contact/Contact/UpdateContactFoodPreferences.php @@ -0,0 +1,54 @@ + 'required|integer|exists:accounts,id', + 'contact_id' => 'required|integer|exists:contacts,id', + 'food_preferences' => 'nullable|string|max:65535', + ]; + } + + /** + * Update the food preferences of the given contact. + * + * @param array $data + * @return Contact + */ + public function execute(array $data): Contact + { + $this->validate($data); + + /** @var Contact */ + $contact = Contact::where('account_id', $data['account_id']) + ->findOrFail($data['contact_id']); + + $contact->throwInactive(); + if ($contact->is_partial) { + throw ValidationException::withMessages([ + 'contact_id' => 'The contact can\'t be a partial contact', + ]); + } + + $contact->food_preferences = ! empty($data['food_preferences']) ? $data['food_preferences'] : null; + $contact->save(); + + // we query the DB again to fill the object with all the new properties + $contact->refresh(); + + return $contact; + } +} diff --git a/app/Services/Contact/Contact/UpdateContactIntroduction.php b/app/Services/Contact/Contact/UpdateContactIntroduction.php new file mode 100644 index 0000000..9e0a811 --- /dev/null +++ b/app/Services/Contact/Contact/UpdateContactIntroduction.php @@ -0,0 +1,214 @@ + 'required|integer|exists:accounts,id', + 'contact_id' => 'required|integer|exists:contacts,id', + 'met_through_contact_id' => 'nullable|integer|exists:contacts,id', + 'general_information' => 'nullable|string|max:65535', + 'where' => 'nullable|string|max:255', + 'is_date_known' => 'required|boolean', + 'is_age_based' => 'nullable|boolean', + 'day' => [ + 'integer', + 'nullable', + Rule::requiredIf(function () { + return Arr::get($this->data, 'is_date_known', false) && ! Arr::get($this->data, 'is_age_based', false); + }), + ], + 'month' => [ + 'integer', + 'nullable', + Rule::requiredIf(function () { + return Arr::get($this->data, 'is_date_known', false) && ! Arr::get($this->data, 'is_age_based', false); + }), + ], + 'year' => 'nullable|integer', + 'age' => [ + 'integer', + 'nullable', + Rule::requiredIf(function () { + return Arr::get($this->data, 'is_date_known', false) && Arr::get($this->data, 'is_age_based', false); + }), + ], + 'add_reminder' => 'nullable|boolean', + ]; + } + + /** + * Update the information about how a contact was introduced. + * + * @param array $data + * @return Contact + * + * @throws ValidationException + */ + public function execute(array $data): Contact + { + $this->data = $data; + $this->validate($data); + + /** @var Contact */ + $contact = Contact::where('account_id', $data['account_id']) + ->findOrFail($data['contact_id']); + + $contact->throwInactive(); + + if ($contact->is_partial) { + throw ValidationException::withMessages([ + 'contact_id' => 'The contact can\'t be a partial contact', + ]); + } + + if ($metContactId = Arr::get($data, 'met_through_contact_id')) { + Contact::where('account_id', $data['account_id']) + ->findOrFail($metContactId); + } + + $this->setMetThroughContact($data, $contact); + $this->clearRelatedReminder($contact); + $this->manageDate($data, $contact); + $this->setInformation($data, $contact); + + // we query the DB again to fill the object with all the new properties + $contact->refresh(); + + return $contact; + } + + private function setMetThroughContact(array $data, Contact $contact): void + { + $contact->first_met_through_contact_id = Arr::get($data, 'met_through_contact_id'); + $contact->save(); + } + + private function setInformation(array $data, Contact $contact): void + { + $contact->first_met_additional_info = Arr::get($data, 'general_information'); + $contact->first_met_where = Arr::get($data, 'where'); + $contact->save(); + } + + private function clearRelatedReminder(Contact $contact): void + { + try { + app(DestroyReminder::class)->execute([ + 'account_id' => $contact->account_id, + 'reminder_id' => $contact->first_met_reminder_id, + ]); + } catch (\Exception $e) { + // Ignore this error + } + } + + /** + * Update date information depending on the type of information. + * + * @param array $data + * @param Contact $contact + * @return void + */ + private function manageDate(array $data, Contact $contact): void + { + if (! $data['is_date_known']) { + $contact->firstMetDate()->delete(); + + return; + } + + if (Arr::get($data, 'is_age_based')) { + $this->approximate($data, $contact); + } else { + $this->exact($data, $contact); + } + } + + /** + * Case where the date is approximate. That means the date is based + * on the estimated age of the contact. + * + * @param array $data + * @param Contact $contact + * @return void + */ + private function approximate(array $data, Contact $contact): void + { + $contact->setSpecialDateFromAge('first_met', $data['age']); + } + + /** + * Case where we have a year, month and day for the date. + * + * @param array $data + * @param Contact $contact + * @return void + */ + private function exact(array $data, Contact $contact): void + { + $specialDate = $contact->setSpecialDate( + 'first_met', + (is_null($data['year']) ? 0 : $data['year']), + $data['month'], + $data['day'] + ); + + $this->setReminder($data, $contact, $specialDate); + } + + /** + * Set a reminder for the given special date, if required. + * + * @param array $data + * @param Contact $contact + * @param SpecialDate $specialDate + * @return void + */ + private function setReminder(array $data, Contact $contact, SpecialDate $specialDate): void + { + if (empty($data['add_reminder'])) { + return; + } + + $reminder = app(CreateReminder::class)->execute([ + 'account_id' => $data['account_id'], + 'contact_id' => $data['contact_id'], + 'initial_date' => DateHelper::getDate($specialDate), + 'frequency_type' => 'year', + 'frequency_number' => 1, + 'title' => trans( + 'people.introductions_reminder_title', + ['name' => $contact->first_name] + ), + 'delible' => false, + ]); + + $contact->first_met_reminder_id = $reminder->id; + $contact->save(); + } +} diff --git a/app/Services/Contact/Contact/UpdateDeceasedInformation.php b/app/Services/Contact/Contact/UpdateDeceasedInformation.php new file mode 100644 index 0000000..09e7851 --- /dev/null +++ b/app/Services/Contact/Contact/UpdateDeceasedInformation.php @@ -0,0 +1,164 @@ + 'required|integer|exists:accounts,id', + 'contact_id' => 'required|integer', + 'is_deceased' => 'required|boolean', + 'is_date_known' => 'required|boolean', + 'day' => 'nullable|integer', + 'month' => 'nullable|integer', + 'year' => 'nullable|integer', + 'add_reminder' => 'nullable|boolean', + ]; + } + + /** + * Update the information about the deceased date. + * + * @param array $data + * @return Contact + */ + public function execute(array $data) + { + $this->validate($data); + + /** @var Contact */ + $contact = Contact::where('account_id', $data['account_id']) + ->findOrFail($data['contact_id']); + + $contact->throwInactive(); + + $this->clearRelatedReminder($contact); + + $this->clearRelatedSpecialDate($contact); + + $this->manageDeceasedDate($data, $contact); + + return $contact; + } + + /** + * Delete related reminder. + * + * @param Contact $contact + * @return void + */ + private function clearRelatedReminder(Contact $contact) + { + if (is_null($contact->deceased_reminder_id)) { + return; + } + + app(DestroyReminder::class)->execute([ + 'account_id' => $contact->account_id, + 'reminder_id' => $contact->deceased_reminder_id, + ]); + } + + /** + * Delete related special date. + * + * @param Contact $contact + * @return void + */ + private function clearRelatedSpecialDate(Contact $contact) + { + $specialDate = SpecialDate::find($contact->deceased_special_date_id); + if (! is_null($specialDate)) { + $specialDate->delete(); + } + } + + /** + * Update deceased date information depending on the type of information. + * + * @param array $data + * @param Contact $contact + * @return void + */ + private function manageDeceasedDate(array $data, Contact $contact): void + { + if (! $data['is_deceased']) { + // remove all information about deceased date in the DB + $contact->is_dead = false; + $contact->deceased_special_date_id = null; + $contact->save(); + + return; + } + + $contact->is_dead = true; + $contact->save(); + + if ($data['is_date_known']) { + $this->exact($data, $contact); + } + } + + /** + * Case where we have a year, month and day for the date. + * + * @param array $data + * @param Contact $contact + * @return void + */ + private function exact(array $data, Contact $contact) + { + $specialDate = $contact->setSpecialDate( + 'deceased_date', + (is_null($data['year']) ? 0 : $data['year']), + $data['month'], + $data['day'] + ); + + $this->setReminder($data, $contact, $specialDate); + } + + /** + * Set a reminder for the given special date, if required. + * + * @param array $data + * @param Contact $contact + * @param SpecialDate $specialDate + * @return void + */ + private function setReminder(array $data, Contact $contact, SpecialDate $specialDate) + { + if (empty($data['add_reminder'])) { + return; + } + + $reminder = app(CreateReminder::class)->execute([ + 'account_id' => $data['account_id'], + 'contact_id' => $data['contact_id'], + 'initial_date' => DateHelper::getDate($specialDate), + 'frequency_type' => 'year', + 'frequency_number' => 1, + 'title' => trans( + 'people.deceased_reminder_title', + ['name' => $contact->first_name] + ), + ]); + + $contact->deceased_reminder_id = $reminder->id; + $contact->save(); + } +} diff --git a/app/Services/Contact/Contact/UpdateWorkInformation.php b/app/Services/Contact/Contact/UpdateWorkInformation.php new file mode 100644 index 0000000..4bccce6 --- /dev/null +++ b/app/Services/Contact/Contact/UpdateWorkInformation.php @@ -0,0 +1,87 @@ + 'required|integer|exists:accounts,id', + 'author_id' => 'required|integer|exists:users,id', + 'contact_id' => 'required|integer|exists:contacts,id', + 'job' => 'nullable|string|max:255', + 'company' => 'nullable|string|max:255', + ]; + } + + /** + * Update a contact. + * + * @param array $data + * @return Contact + */ + public function execute(array $data): Contact + { + $this->validate($data); + + /** @var Contact */ + $contact = Contact::where('account_id', $data['account_id']) + ->findOrFail($data['contact_id']); + + $contact->throwInactive(); + if ($contact->is_partial) { + throw ValidationException::withMessages([ + 'contact_id' => 'The contact can\'t be a partial contact', + ]); + } + + $contact->job = empty($data['job']) ? null : $data['job']; + $contact->company = empty($data['company']) ? null : $data['company']; + $contact->save(); + + $this->log($data, $contact); + + $contact->refresh(); + + return $contact; + } + + /** + * Add an audit log. + * + * @param array $data + * @param Contact $contact + * @return void + */ + private function log(array $data, Contact $contact): void + { + $author = User::find($data['author_id']); + + LogAccountAudit::dispatch([ + 'action' => 'contact_work_updated', + 'account_id' => $author->account_id, + 'about_contact_id' => $contact->id, + 'author_id' => $author->id, + 'author_name' => $author->name, + 'audited_at' => now(), + 'should_appear_on_dashboard' => true, + 'objects' => json_encode([ + 'contact_name' => $contact->name, + 'contact_id' => $contact->id, + ]), + ]); + } +} diff --git a/app/Services/Contact/ContactField/CreateContactField.php b/app/Services/Contact/ContactField/CreateContactField.php new file mode 100644 index 0000000..0bbfa04 --- /dev/null +++ b/app/Services/Contact/ContactField/CreateContactField.php @@ -0,0 +1,64 @@ + 'required|integer|exists:accounts,id', + 'contact_id' => 'required|integer|exists:contacts,id', + 'contact_field_type_id' => 'required|integer|exists:contact_field_types,id', + 'data' => 'required|string|max:255', + 'labels' => 'nullable|array', + ]; + } + + /** + * Create a contact field. + * + * @param array $data + * @return ContactField + */ + public function execute(array $data): ContactField + { + $this->validate($data); + + $contact = Contact::where('account_id', $data['account_id']) + ->findOrFail($data['contact_id']); + + $contact->throwInactive(); + + ContactFieldType::where('account_id', $data['account_id']) + ->findOrFail($data['contact_field_type_id']); + + $contactField = ContactField::create([ + 'account_id' => $data['account_id'], + 'contact_id' => $data['contact_id'], + 'contact_field_type_id' => $data['contact_field_type_id'], + 'data' => $this->nullOrValue($data, 'data'), + ]); + + if ($labels = $this->nullOrValue($data, 'labels')) { + app(UpdateContactFieldLabels::class)->execute([ + 'account_id' => $data['account_id'], + 'contact_field_id' => $contactField->id, + 'labels' => $labels, + ]); + } + + return $contactField; + } +} diff --git a/app/Services/Contact/ContactField/DestroyContactField.php b/app/Services/Contact/ContactField/DestroyContactField.php new file mode 100644 index 0000000..f47e8e6 --- /dev/null +++ b/app/Services/Contact/ContactField/DestroyContactField.php @@ -0,0 +1,42 @@ + 'required|integer|exists:accounts,id', + 'contact_field_id' => 'required|integer|exists:contact_fields,id', + ]; + } + + /** + * Destroy an address. + * + * @param array $data + * @return bool + */ + public function execute(array $data): bool + { + $this->validate($data); + + $contactField = ContactField::where('account_id', $data['account_id']) + ->findOrFail($data['contact_field_id']); + + $contactField->contact->throwInactive(); + + $contactField->delete(); + + return true; + } +} diff --git a/app/Services/Contact/ContactField/UpdateContactField.php b/app/Services/Contact/ContactField/UpdateContactField.php new file mode 100644 index 0000000..e306a95 --- /dev/null +++ b/app/Services/Contact/ContactField/UpdateContactField.php @@ -0,0 +1,68 @@ + 'required|integer|exists:accounts,id', + 'contact_field_id' => 'required|integer|exists:contact_fields,id', + 'contact_id' => 'required|integer|exists:contacts,id', + 'contact_field_type_id' => 'required|integer|exists:contact_field_types,id', + 'data' => 'required|string|max:255', + 'labels' => 'nullable|array', + ]; + } + + /** + * Update a contact field. + * + * @param array $data + * @return ContactField + */ + public function execute(array $data): ContactField + { + $this->validate($data); + + /** @var ContactField */ + $contactField = ContactField::where('account_id', $data['account_id']) + ->findOrFail($data['contact_field_id']); + + $contact = Contact::where('account_id', $data['account_id']) + ->findOrFail($data['contact_id']); + + $contact->throwInactive(); + + ContactFieldType::where('account_id', $data['account_id']) + ->findOrFail($data['contact_field_type_id']); + + $contactField->update([ + 'contact_id' => $data['contact_id'], + 'contact_field_type_id' => $data['contact_field_type_id'], + 'data' => $this->nullOrValue($data, 'data'), + ]); + + if ($labels = $this->nullOrValue($data, 'labels')) { + app(UpdateContactFieldLabels::class)->execute([ + 'account_id' => $data['account_id'], + 'contact_field_id' => $data['contact_field_id'], + 'labels' => $labels, + ]); + } + + return $contactField; + } +} diff --git a/app/Services/Contact/Conversation/AddMessageToConversation.php b/app/Services/Contact/Conversation/AddMessageToConversation.php new file mode 100644 index 0000000..ed8f701 --- /dev/null +++ b/app/Services/Contact/Conversation/AddMessageToConversation.php @@ -0,0 +1,55 @@ + 'required|integer|exists:accounts,id', + 'contact_id' => 'required|integer|exists:contacts,id', + 'conversation_id' => 'required|integer|exists:conversations,id', + 'written_at' => 'required|date', + 'written_by_me' => 'required|boolean', + 'content' => 'required|string', + ]; + } + + /** + * Add message to a conversation. + * + * @param array $data + * @return Message + */ + public function execute(array $data): Message + { + $this->validate($data); + + $contact = Contact::where('account_id', $data['account_id']) + ->findOrFail($data['contact_id']); + + $contact->throwInactive(); + + Conversation::where('contact_id', $data['contact_id']) + ->where('account_id', $data['account_id']) + ->findOrFail($data['conversation_id']); + + return Message::create($data); + } +} diff --git a/app/Services/Contact/Conversation/CreateConversation.php b/app/Services/Contact/Conversation/CreateConversation.php new file mode 100644 index 0000000..736c227 --- /dev/null +++ b/app/Services/Contact/Conversation/CreateConversation.php @@ -0,0 +1,52 @@ + 'required|date', + 'account_id' => 'required|integer|exists:accounts,id', + 'contact_id' => 'required|integer|exists:contacts,id', + 'contact_field_type_id' => 'required|integer|exists:contact_field_types,id', + ]; + } + + /** + * Create a conversation. + * + * @param array $data + * @return Conversation + */ + public function execute(array $data): Conversation + { + $this->validate($data); + + $contact = Contact::where('account_id', $data['account_id']) + ->findOrFail($data['contact_id']); + + $contact->throwInactive(); + + ContactFieldType::where('account_id', $data['account_id']) + ->findOrFail($data['contact_field_type_id']); + + return Conversation::create($data); + } +} diff --git a/app/Services/Contact/Conversation/DestroyConversation.php b/app/Services/Contact/Conversation/DestroyConversation.php new file mode 100644 index 0000000..0ff1fa3 --- /dev/null +++ b/app/Services/Contact/Conversation/DestroyConversation.php @@ -0,0 +1,47 @@ + 'required|integer|exists:accounts,id', + 'conversation_id' => 'required|integer|exists:conversations,id', + ]; + } + + /** + * Destroy a conversation. + * + * @param array $data + * @return bool + */ + public function execute(array $data): bool + { + $this->validate($data); + + $conversation = Conversation::where('account_id', $data['account_id']) + ->findOrFail($data['conversation_id']); + + $conversation->contact->throwInactive(); + + $conversation->delete(); + + return true; + } +} diff --git a/app/Services/Contact/Conversation/DestroyMessage.php b/app/Services/Contact/Conversation/DestroyMessage.php new file mode 100644 index 0000000..b73ad42 --- /dev/null +++ b/app/Services/Contact/Conversation/DestroyMessage.php @@ -0,0 +1,53 @@ + 'required|integer|exists:accounts,id', + 'conversation_id' => 'required|integer|exists:conversations,id', + 'message_id' => 'required|integer|exists:messages,id', + ]; + } + + /** + * Destroy a message. + * + * @param array $data + * @return bool + */ + public function execute(array $data): bool + { + $this->validate($data); + + Conversation::where('account_id', $data['account_id']) + ->findOrFail($data['conversation_id']); + + $message = Message::where('account_id', $data['account_id']) + ->where('conversation_id', $data['conversation_id']) + ->findOrFail($data['message_id']); + + $message->contact->throwInactive(); + + $message->delete(); + + return true; + } +} diff --git a/app/Services/Contact/Conversation/UpdateConversation.php b/app/Services/Contact/Conversation/UpdateConversation.php new file mode 100644 index 0000000..d1861d8 --- /dev/null +++ b/app/Services/Contact/Conversation/UpdateConversation.php @@ -0,0 +1,57 @@ + 'required|integer|exists:accounts,id', + 'happened_at' => 'required|date', + 'contact_field_type_id' => 'required|integer', + 'conversation_id' => 'required|integer|exists:conversations,id', + ]; + } + + /** + * Update a conversation. + * + * @param array $data + * @return Conversation + */ + public function execute(array $data): Conversation + { + $this->validate($data); + + /** @var Conversation */ + $conversation = Conversation::where('account_id', $data['account_id']) + ->findOrFail($data['conversation_id']); + + $conversation->contact->throwInactive(); + + ContactFieldType::where('account_id', $data['account_id']) + ->findOrFail($data['contact_field_type_id']); + + $conversation->update([ + 'happened_at' => $data['happened_at'], + 'contact_field_type_id' => $data['contact_field_type_id'], + ]); + + return $conversation; + } +} diff --git a/app/Services/Contact/Conversation/UpdateMessage.php b/app/Services/Contact/Conversation/UpdateMessage.php new file mode 100644 index 0000000..f1ce425 --- /dev/null +++ b/app/Services/Contact/Conversation/UpdateMessage.php @@ -0,0 +1,68 @@ + 'required|integer|exists:accounts,id', + 'contact_id' => 'required|integer|exists:contacts,id', + 'conversation_id' => 'required|integer|exists:conversations,id', + 'message_id' => 'required|integer|exists:messages,id', + 'written_at' => 'required|date', + 'written_by_me' => 'required|boolean', + 'content' => 'required|string', + ]; + } + + /** + * Update message in a conversation. + * + * @param array $data + * @return Message + */ + public function execute(array $data): Message + { + $this->validate($data); + + $contact = Contact::where('account_id', $data['account_id']) + ->findOrFail($data['contact_id']); + + $contact->throwInactive(); + + Conversation::where('contact_id', $data['contact_id']) + ->where('account_id', $data['account_id']) + ->findOrFail($data['conversation_id']); + + /** @var Message */ + $message = Message::where('contact_id', $data['contact_id']) + ->where('conversation_id', $data['conversation_id']) + ->where('account_id', $data['account_id']) + ->findOrFail($data['message_id']); + + $message->update([ + 'written_at' => $data['written_at'], + 'written_by_me' => $data['written_by_me'], + 'content' => $data['content'], + ]); + + return $message; + } +} diff --git a/app/Services/Contact/Description/ClearPersonalDescription.php b/app/Services/Contact/Description/ClearPersonalDescription.php new file mode 100644 index 0000000..5dc3ffd --- /dev/null +++ b/app/Services/Contact/Description/ClearPersonalDescription.php @@ -0,0 +1,76 @@ + 'required|integer|exists:accounts,id', + 'contact_id' => 'required|integer|exists:contacts,id', + 'author_id' => 'required|integer|exists:users,id', + ]; + } + + /** + * Clear a contact's description. + * + * @param array $data + * @return Contact + */ + public function execute(array $data): Contact + { + $this->validate($data); + + /** @var Contact */ + $contact = Contact::where('account_id', $data['account_id']) + ->findOrFail($data['contact_id']); + + $contact->throwInactive(); + + $contact->description = null; + $contact->save(); + + $this->log($data, $contact); + + return $contact; + } + + /** + * Add an audit log. + * + * @param array $data + * @param Contact $contact + * @return void + */ + private function log(array $data, Contact $contact): void + { + $author = User::find($data['author_id']); + + LogAccountAudit::dispatch([ + 'action' => 'contact_description_cleared', + 'account_id' => $author->account_id, + 'about_contact_id' => $contact->id, + 'author_id' => $author->id, + 'author_name' => $author->name, + 'audited_at' => now(), + 'should_appear_on_dashboard' => true, + 'objects' => json_encode([ + 'contact_name' => $contact->name, + 'contact_id' => $contact->id, + ]), + ]); + } +} diff --git a/app/Services/Contact/Description/SetPersonalDescription.php b/app/Services/Contact/Description/SetPersonalDescription.php new file mode 100644 index 0000000..47b5ffa --- /dev/null +++ b/app/Services/Contact/Description/SetPersonalDescription.php @@ -0,0 +1,80 @@ + 'required|integer|exists:accounts,id', + 'contact_id' => 'required|integer|exists:contacts,id', + 'author_id' => 'required|integer|exists:users,id', + 'description' => 'nullable|string|max:255', + ]; + } + + /** + * Set a contact's description. + * The description should be saved as unparsed markdown content, and fetched + * as unparsed markdown content. The UI is responsible for parsing and + * displaying the proper content. + * + * @param array $data + * @return Contact + */ + public function execute(array $data): Contact + { + $this->validate($data); + + /** @var Contact $contact */ + $contact = Contact::where('account_id', $data['account_id']) + ->findOrFail($data['contact_id']); + + $contact->throwInactive(); + + $contact->description = $data['description']; + $contact->save(); + + $this->log($data, $contact); + + return $contact->refresh(); + } + + /** + * Add an audit log. + * + * @param array $data + * @param Contact $contact + * @return void + */ + private function log(array $data, Contact $contact): void + { + $author = User::find($data['author_id']); + + LogAccountAudit::dispatch([ + 'action' => 'contact_description_updated', + 'account_id' => $author->account_id, + 'about_contact_id' => $contact->id, + 'author_id' => $author->id, + 'author_name' => $author->name, + 'audited_at' => now(), + 'should_appear_on_dashboard' => true, + 'objects' => json_encode([ + 'contact_name' => $contact->name, + 'contact_id' => $contact->id, + ]), + ]); + } +} diff --git a/app/Services/Contact/Document/DestroyDocument.php b/app/Services/Contact/Document/DestroyDocument.php new file mode 100644 index 0000000..9af9bab --- /dev/null +++ b/app/Services/Contact/Document/DestroyDocument.php @@ -0,0 +1,46 @@ + 'required|integer|exists:accounts,id', + 'document_id' => 'required|integer', + ]; + } + + /** + * Destroy a document. + * + * @param array $data + * @return bool + */ + public function execute(array $data): bool + { + $this->validate($data); + + $document = Document::where('account_id', $data['account_id']) + ->findOrFail($data['document_id']); + + // Delete the physical document + // Throws FileNotFoundException + Storage::delete($document->new_filename); + + // Delete the object in the DB + $document->delete(); + + return true; + } +} diff --git a/app/Services/Contact/Document/UploadDocument.php b/app/Services/Contact/Document/UploadDocument.php new file mode 100644 index 0000000..b5300d9 --- /dev/null +++ b/app/Services/Contact/Document/UploadDocument.php @@ -0,0 +1,79 @@ + 'required|integer|exists:accounts,id', + 'contact_id' => 'required|integer', + 'document' => 'required|file', + ]; + } + + /** + * Upload a document. + * + * @param array $data + * @return Document + */ + public function execute(array $data): Document + { + $this->validate($data); + + $account = Account::find($data['account_id']); + if (AccountHelper::hasLimitations($account)) { + abort(402); + } + + $contact = Contact::where('account_id', $data['account_id']) + ->findOrFail($data['contact_id']); + + $contact->throwInactive(); + + $array = $this->populateData($data); + + return Document::create($array); + } + + /** + * Create an array with the necessary fields to create the document object. + * + * @return array + */ + private function populateData($data) + { + $document = $data['document']; + + $data = [ + 'account_id' => $data['account_id'], + 'contact_id' => $data['contact_id'], + 'original_filename' => $document->getClientOriginalName(), + 'filesize' => $document->getSize(), + 'type' => $document->guessClientExtension(), + 'mime_type' => (new \Mimey\MimeTypes)->getMimeType($document->guessClientExtension()), + ]; + + $filename = $document->store('documents', [ + 'disk' => config('filesystems.default'), + 'visibility' => config('filesystems.default_visibility'), + ]); + + return array_merge($data, [ + 'new_filename' => $filename, + ]); + } +} diff --git a/app/Services/Contact/Gift/AssociatePhotoToGift.php b/app/Services/Contact/Gift/AssociatePhotoToGift.php new file mode 100644 index 0000000..5951734 --- /dev/null +++ b/app/Services/Contact/Gift/AssociatePhotoToGift.php @@ -0,0 +1,46 @@ + 'required|integer|exists:accounts,id', + 'photo_id' => 'required|integer|exists:photos,id', + 'gift_id' => 'required|integer|exists:gifts,id', + ]; + } + + /** + * Link a photo to a gift. + * + * @param array $data + */ + public function execute(array $data) + { + $this->validate($data); + + $photo = Photo::where('account_id', $data['account_id']) + ->findOrFail($data['photo_id']); + + $gift = Gift::where('account_id', $data['account_id']) + ->findOrFail($data['gift_id']); + + $gift->contact->throwInactive(); + + $gift->photos()->syncWithoutDetaching([$photo->id]); + + return $gift; + } +} diff --git a/app/Services/Contact/Gift/CreateGift.php b/app/Services/Contact/Gift/CreateGift.php new file mode 100644 index 0000000..95492f6 --- /dev/null +++ b/app/Services/Contact/Gift/CreateGift.php @@ -0,0 +1,80 @@ + 'required|integer|exists:accounts,id', + 'contact_id' => 'required|integer|exists:contacts,id', + 'name' => 'required|string|max:255', + 'status' => [ + 'required', + Rule::in([ + 'idea', + 'offered', + 'received', + ]), + ], + 'comment' => 'string|max:1000000|nullable', + 'url' => 'string|max:1000000|nullable', + 'amount' => 'numeric|nullable', + 'date' => 'date|nullable', + 'recipient_id' => 'integer|nullable|exists:contacts,id', + ]; + } + + /** + * Create a tag. + * + * @param array $data + * @return Gift + */ + public function execute(array $data): Gift + { + $this->validate($data); + + $contact = Contact::where('account_id', $data['account_id']) + ->findOrFail($data['contact_id']); + + $contact->throwInactive(); + + if (isset($data['recipient_id'])) { + Contact::where('account_id', $data['account_id']) + ->findOrFail($data['recipient_id']); + } + + $array = [ + 'account_id' => $data['account_id'], + 'contact_id' => $data['contact_id'], + 'name' => $data['name'], + 'status' => $data['status'], + 'comment' => $this->nullOrvalue($data, 'comment'), + 'url' => $this->nullOrvalue($data, 'url'), + 'amount' => $this->nullOrvalue($data, 'amount'), + 'date' => $this->nullOrvalue($data, 'date'), + ]; + + if (Auth::check()) { + $array['currency_id'] = Auth::user()->currency->id; + } + + return tap(Gift::create($array), function ($gift) use ($data): void { + $gift->recipient = $this->nullOrvalue($data, 'recipient_id'); + $gift->save(); + }); + } +} diff --git a/app/Services/Contact/Gift/DestroyGift.php b/app/Services/Contact/Gift/DestroyGift.php new file mode 100644 index 0000000..a85a46e --- /dev/null +++ b/app/Services/Contact/Gift/DestroyGift.php @@ -0,0 +1,44 @@ + 'required|integer|exists:accounts,id', + 'gift_id' => 'required|integer|exists:gifts,id', + ]; + } + + /** + * Destroy a gift. + * + * @param array $data + * @return bool + */ + public function execute(array $data) + { + $this->validate($data); + + $gift = Gift::where('account_id', $data['account_id']) + ->findOrFail($data['gift_id']); + + $gift->contact->throwInactive(); + + $gift->photos()->detach(); + + $gift->delete(); + + return true; + } +} diff --git a/app/Services/Contact/Gift/UpdateGift.php b/app/Services/Contact/Gift/UpdateGift.php new file mode 100644 index 0000000..194d515 --- /dev/null +++ b/app/Services/Contact/Gift/UpdateGift.php @@ -0,0 +1,85 @@ + 'required|integer|exists:accounts,id', + 'gift_id' => 'required|integer|exists:gifts,id', + 'contact_id' => 'required|integer|exists:contacts,id', + 'name' => 'required|string|max:255', + 'status' => [ + 'required', + Rule::in([ + 'idea', + 'offered', + 'received', + ]), + ], + 'comment' => 'string|max:1000000|nullable', + 'url' => 'string|max:1000000|nullable', + 'amount' => 'numeric|nullable', + 'date' => 'date|nullable', + 'recipient_id' => 'integer|nullable|exists:contacts,id', + ]; + } + + /** + * Update a gift. + * + * @param array $data + * @return Gift + */ + public function execute(array $data): Gift + { + $this->validate($data); + + $gift = Gift::where('account_id', $data['account_id']) + ->findOrFail((int) $data['gift_id']); + + $contact = Contact::where('account_id', $data['account_id']) + ->findOrFail($data['contact_id']); + + $contact->throwInactive(); + + if (isset($data['recipient_id'])) { + Contact::where('account_id', $data['account_id']) + ->findOrFail($data['recipient_id']); + } + + $array = [ + 'contact_id' => $data['contact_id'], + 'name' => $data['name'], + 'status' => $data['status'], + 'comment' => $this->nullOrvalue($data, 'comment'), + 'url' => $this->nullOrvalue($data, 'url'), + 'amount' => $this->nullOrvalue($data, 'amount'), + 'date' => $this->nullOrvalue($data, 'date'), + ]; + + if (Auth::check()) { + $array['currency_id'] = Auth::user()->currency->id; + } + + $gift->update($array); + + return tap($gift, function ($gift) use ($data): void { + $gift->recipient = $this->nullOrvalue($data, 'recipient_id'); + $gift->save(); + }); + } +} diff --git a/app/Services/Contact/Label/UpdateAddressLabels.php b/app/Services/Contact/Label/UpdateAddressLabels.php new file mode 100644 index 0000000..384f5c2 --- /dev/null +++ b/app/Services/Contact/Label/UpdateAddressLabels.php @@ -0,0 +1,88 @@ + 'required|integer|exists:accounts,id', + 'address_id' => 'required|integer|exists:addresses,id', + 'labels' => 'required|array', + ]; + } + + /** + * Update address' labels. + * + * @param array $data + * @return void + */ + public function execute(array $data) + { + $this->validate($data); + + $address = Address::where('account_id', $data['account_id']) + ->findOrFail($data['address_id']); + + $address->contact->throwInactive(); + + $labelsId = $this->getLabelsId($data); + + $this->updateLabels($labelsId, $address); + } + + /** + * Get ContactFieldLabel ids. + * + * @param array $data + * @return array + */ + private function getLabelsId(array $data): array + { + $labelsId = []; + foreach ($data['labels'] as $label) { + $label2 = mb_strtolower($label); + if (in_array($label2, ContactFieldLabel::$standardLabels)) { + $labelsId[] = (ContactFieldLabel::firstOrCreate([ + 'account_id' => $data['account_id'], + 'label_i18n' => $label2, + ]))->id; + } else { + $labelsId[] = (ContactFieldLabel::firstOrCreate([ + 'account_id' => $data['account_id'], + 'label' => $label, + ]))->id; + } + } + + return $labelsId; + } + + /** + * Update contactField's labels. + * + * @param array $labelsId + * @param Address $address + * @return void + */ + private function updateLabels(array $labelsId, Address $address) + { + $labelsSync = []; + foreach ($labelsId as $labelId) { + $labelsSync[$labelId] = ['account_id' => $address->account_id]; + } + + $address->labels()->sync($labelsSync); + } +} diff --git a/app/Services/Contact/Label/UpdateContactFieldLabels.php b/app/Services/Contact/Label/UpdateContactFieldLabels.php new file mode 100644 index 0000000..5a1d5b4 --- /dev/null +++ b/app/Services/Contact/Label/UpdateContactFieldLabels.php @@ -0,0 +1,89 @@ + 'required|integer|exists:accounts,id', + 'contact_field_id' => 'required|integer|exists:contact_fields,id', + 'labels' => 'required|array', + ]; + } + + /** + * Update contact field's labels. + * + * @param array $data + * @return void + */ + public function execute(array $data) + { + $this->validate($data); + + $contactField = ContactField::where('account_id', $data['account_id']) + ->findOrFail($data['contact_field_id']); + + $contactField->contact->throwInactive(); + + $labelsId = $this->getLabelsId($data); + + $this->updateLabels($labelsId, $contactField); + } + + /** + * Get ContactFieldLabel ids. + * + * @param array $data + * @return array + */ + private function getLabelsId(array $data): array + { + $labelsId = []; + foreach ($data['labels'] as $label) { + $label2 = mb_strtolower($label); + $s = ContactFieldLabel::$standardLabels; + if (in_array($label2, ContactFieldLabel::$standardLabels)) { + $labelsId[] = (ContactFieldLabel::firstOrCreate([ + 'account_id' => $data['account_id'], + 'label_i18n' => $label2, + ]))->id; + } else { + $labelsId[] = (ContactFieldLabel::firstOrCreate([ + 'account_id' => $data['account_id'], + 'label' => $label, + ]))->id; + } + } + + return $labelsId; + } + + /** + * Update contactField's labels. + * + * @param array $labelsId + * @param ContactField $contactField + * @return void + */ + private function updateLabels(array $labelsId, ContactField $contactField) + { + $labelsSync = []; + foreach ($labelsId as $labelId) { + $labelsSync[$labelId] = ['account_id' => $contactField->account_id]; + } + + $contactField->labels()->sync($labelsSync); + } +} diff --git a/app/Services/Contact/LifeEvent/CreateLifeEvent.php b/app/Services/Contact/LifeEvent/CreateLifeEvent.php new file mode 100644 index 0000000..ac65df3 --- /dev/null +++ b/app/Services/Contact/LifeEvent/CreateLifeEvent.php @@ -0,0 +1,97 @@ + 'required|integer|exists:accounts,id', + 'contact_id' => 'required|integer', + 'life_event_type_id' => 'required|integer', + 'happened_at' => 'required|date', + 'name' => 'nullable|string', + 'note' => 'nullable|string', + 'has_reminder' => 'required|boolean', + 'happened_at_month_unknown' => 'required|boolean', + 'happened_at_day_unknown' => 'required|boolean', + ]; + } + + /** + * Create a life event. + * + * @param array $data + * @return LifeEvent + */ + public function execute(array $data): LifeEvent + { + $this->validate($data); + + $contact = Contact::where('account_id', $data['account_id']) + ->findOrFail($data['contact_id']); + + $contact->throwInactive(); + + LifeEventType::where('account_id', $data['account_id']) + ->findOrFail($data['life_event_type_id']); + + $lifeEvent = new LifeEvent; + $lifeEvent->account_id = $data['account_id']; + $lifeEvent->contact_id = $data['contact_id']; + $lifeEvent->life_event_type_id = $data['life_event_type_id']; + $lifeEvent->happened_at = $data['happened_at']; + $lifeEvent->name = $data['name']; + $lifeEvent->note = $data['note']; + $lifeEvent->happened_at_month_unknown = $data['happened_at_month_unknown']; + $lifeEvent->happened_at_day_unknown = $data['happened_at_day_unknown']; + $lifeEvent->save(); + + $this->addYearlyReminder($data, $lifeEvent); + + // Get the newly created object as the Create method doesn't return all + // fields by default + return LifeEvent::find($lifeEvent->id); + } + + /** + * Add yearly reminder if necessary. + * + * @param array $data + * @param LifeEvent $lifeEvent + */ + private function addYearlyReminder($data, $lifeEvent) + { + if ($data['has_reminder']) { + $date = Carbon::parse($data['happened_at']); + + $data = [ + 'contact_id' => $data['contact_id'], + 'account_id' => $data['account_id'], + 'initial_date' => $date->toDateString(), + 'frequency_type' => 'year', + 'frequency_number' => 1, + 'title' => $lifeEvent->lifeEventType->name, + 'description' => null, + ]; + + $reminder = app(CreateReminder::class)->execute($data); + + $lifeEvent->reminder_id = $reminder->id; + $lifeEvent->save(); + } + } +} diff --git a/app/Services/Contact/LifeEvent/DestroyLifeEvent.php b/app/Services/Contact/LifeEvent/DestroyLifeEvent.php new file mode 100644 index 0000000..8b94afa --- /dev/null +++ b/app/Services/Contact/LifeEvent/DestroyLifeEvent.php @@ -0,0 +1,55 @@ + 'required|integer|exists:accounts,id', + 'life_event_id' => 'required|integer', + ]; + } + + /** + * Destroy a life event. + * + * @param array $data + * @return bool + */ + public function execute(array $data): bool + { + $this->validate($data); + + $lifeEvent = LifeEvent::where('account_id', $data['account_id']) + ->findOrFail($data['life_event_id']); + + $lifeEvent->contact->throwInactive(); + + $this->deleteAssociatedReminder($lifeEvent); + + $lifeEvent->delete(); + + return true; + } + + /** + * Delete the associated reminder, if it's set. + */ + private function deleteAssociatedReminder($lifeEvent) + { + if ($lifeEvent->reminder_id) { + Reminder::where('id', $lifeEvent->reminder_id)->delete(); + } + } +} diff --git a/app/Services/Contact/LifeEvent/UpdateLifeEvent.php b/app/Services/Contact/LifeEvent/UpdateLifeEvent.php new file mode 100644 index 0000000..edb3c9d --- /dev/null +++ b/app/Services/Contact/LifeEvent/UpdateLifeEvent.php @@ -0,0 +1,56 @@ + 'required|integer|exists:accounts,id', + 'life_event_id' => 'required|integer', + 'life_event_type_id' => 'required|integer', + 'happened_at' => 'required|date', + 'name' => 'nullable|string', + 'note' => 'nullable|string', + ]; + } + + /** + * Update a life event. + * + * @param array $data + * @return LifeEvent + */ + public function execute(array $data): LifeEvent + { + $this->validate($data); + + /** @var LifeEvent */ + $lifeEvent = LifeEvent::where('account_id', $data['account_id']) + ->findOrFail($data['life_event_id']); + + $lifeEvent->contact->throwInactive(); + + LifeEventType::where('account_id', $data['account_id']) + ->findOrFail($data['life_event_type_id']); + + $lifeEvent->update([ + 'happened_at' => $data['happened_at'], + 'life_event_type_id' => $data['life_event_type_id'], + 'name' => $data['name'], + 'note' => $data['note'], + ]); + + return $lifeEvent; + } +} diff --git a/app/Services/Contact/Occupation/CreateOccupation.php b/app/Services/Contact/Occupation/CreateOccupation.php new file mode 100644 index 0000000..c0b4961 --- /dev/null +++ b/app/Services/Contact/Occupation/CreateOccupation.php @@ -0,0 +1,64 @@ + 'required|integer|exists:accounts,id', + 'contact_id' => 'required|integer|exists:contacts,id', + 'company_id' => 'required|integer|exists:companies,id', + 'title' => 'required|string|max:255', + 'description' => 'nullable|string|max:1000', + 'salary' => 'nullable|integer', + 'salary_unit' => [ + 'nullable', + Rule::in(Occupation::$salaryUnits), + ], + 'currently_works_here' => 'nullable|boolean', + 'start_date' => 'nullable|date_format:Y-m-d', + 'end_date' => 'nullable|date_format:Y-m-d', + ]; + } + + /** + * Create a occupation. + * + * @param array $data + * @return Occupation + */ + public function execute(array $data): Occupation + { + $this->validate($data); + + $contact = Contact::where('account_id', $data['account_id']) + ->findOrFail($data['contact_id']); + + $contact->throwInactive(); + + return Occupation::create([ + 'account_id' => $data['account_id'], + 'contact_id' => $data['contact_id'], + 'company_id' => $data['company_id'], + 'title' => $data['title'], + 'description' => $this->nullOrValue($data, 'description'), + 'salary' => $this->nullOrValue($data, 'salary'), + 'salary_unit' => $this->nullOrValue($data, 'salary_unit'), + 'currently_works_here' => $this->nullOrValue($data, 'currently_works_here'), + 'start_date' => $this->nullOrDate($data, 'start_date'), + 'end_date' => $this->nullOrDate($data, 'end_date'), + ]); + } +} diff --git a/app/Services/Contact/Occupation/DestroyOccupation.php b/app/Services/Contact/Occupation/DestroyOccupation.php new file mode 100644 index 0000000..2ea93fa --- /dev/null +++ b/app/Services/Contact/Occupation/DestroyOccupation.php @@ -0,0 +1,40 @@ + 'required|integer|exists:accounts,id', + 'occupation_id' => 'required|integer|exists:occupations,id', + ]; + } + + /** + * Destroy an occupation. + * + * @param array $data + * @return bool + */ + public function execute(array $data): bool + { + $this->validate($data); + + $occupation = Occupation::where('account_id', $data['account_id']) + ->findOrFail($data['occupation_id']); + + $occupation->delete(); + + return true; + } +} diff --git a/app/Services/Contact/Occupation/UpdateOccupation.php b/app/Services/Contact/Occupation/UpdateOccupation.php new file mode 100644 index 0000000..0546548 --- /dev/null +++ b/app/Services/Contact/Occupation/UpdateOccupation.php @@ -0,0 +1,66 @@ + 'required|integer|exists:accounts,id', + 'contact_id' => 'required|integer|exists:contacts,id', + 'company_id' => 'required|integer|exists:companies,id', + 'occupation_id' => 'required|integer|exists:occupations,id', + 'title' => 'required|string|max:255', + 'description' => 'nullable|string|max:1000', + 'salary' => 'nullable|integer', + 'salary_unit' => [ + 'nullable', + Rule::in(Occupation::$salaryUnits), + ], + 'currently_works_here' => 'nullable|boolean', + 'start_date' => 'nullable|date_format:Y-m-d', + 'end_date' => 'nullable|date_format:Y-m-d', + ]; + } + + /** + * Update a occupation. + * + * @param array $data + * @return Occupation + */ + public function execute(array $data): Occupation + { + $this->validate($data); + + /** @var Occupation */ + $occupation = Occupation::where('account_id', $data['account_id']) + ->where('contact_id', $data['contact_id']) + ->where('company_id', $data['company_id']) + ->findOrFail($data['occupation_id']); + + $occupation->contact->throwInactive(); + + $occupation->update([ + 'title' => $data['title'], + 'description' => $this->nullOrValue($data, 'description'), + 'salary' => $this->nullOrValue($data, 'salary'), + 'salary_unit' => $this->nullOrValue($data, 'salary_unit'), + 'currently_works_here' => $this->nullOrValue($data, 'currently_works_here'), + 'start_date' => $this->nullOrDate($data, 'start_date'), + 'end_date' => $this->nullOrDate($data, 'end_date'), + ]); + + return $occupation; + } +} diff --git a/app/Services/Contact/Relationship/CreateRelationship.php b/app/Services/Contact/Relationship/CreateRelationship.php new file mode 100644 index 0000000..aadabb8 --- /dev/null +++ b/app/Services/Contact/Relationship/CreateRelationship.php @@ -0,0 +1,77 @@ + 'required|integer|exists:accounts,id', + 'contact_is' => 'required|integer|exists:contacts,id', + 'of_contact' => 'required|integer|exists:contacts,id', + 'relationship_type_id' => 'required|integer|exists:relationship_types,id', + ]; + } + + /** + * Set a relationship between two contacts. + * + * @param array $data + * @return Relationship + */ + public function execute(array $data): Relationship + { + $this->validate($data); + + $contact = Contact::where('account_id', $data['account_id']) + ->findOrFail($data['contact_is']); + + $contact->throwInactive(); + + $otherContact = Contact::where('account_id', $data['account_id']) + ->findOrFail($data['of_contact']); + + $relationshipType = RelationshipType::where('account_id', $data['account_id']) + ->findOrFail($data['relationship_type_id']); + + // create the relationship + $relationship = $this->setRelationship($contact, $otherContact, $relationshipType); + + $reverseRelationshipType = $relationshipType->reverseRelationshipType(); + if ($reverseRelationshipType) { + // create the reverse relationship + $this->setRelationship($otherContact, $contact, $reverseRelationshipType); + } + + return $relationship; + } + + /** + * Set a relationship between two contacts. + * + * @param Contact $contact + * @param Contact $otherContact + * @param RelationshipType $relationshipType + * @return Relationship + */ + public function setRelationship(Contact $contact, Contact $otherContact, RelationshipType $relationshipType): Relationship + { + return Relationship::create([ + 'account_id' => $relationshipType->account_id, + 'relationship_type_id' => $relationshipType->id, + 'contact_is' => $contact->id, + 'of_contact' => $otherContact->id, + ]); + } +} diff --git a/app/Services/Contact/Relationship/DestroyRelationship.php b/app/Services/Contact/Relationship/DestroyRelationship.php new file mode 100644 index 0000000..f113fd2 --- /dev/null +++ b/app/Services/Contact/Relationship/DestroyRelationship.php @@ -0,0 +1,90 @@ + 'required|integer|exists:accounts,id', + 'relationship_id' => 'required|integer|exists:relationships,id', + ]; + } + + /** + * Destroy a relationship. + * + * @param array $data + * @return bool + */ + public function execute(array $data): bool + { + $this->validate($data); + + $relationship = Relationship::where('account_id', $data['account_id']) + ->findOrFail($data['relationship_id']); + + $relationship->contactIs->throwInactive(); + + $otherContact = $relationship->ofContact; + + $this->deleteRelationship($relationship); + + $this->deletePartialContact($otherContact); + + return true; + } + + /** + * Delete relationship. + * + * @param Relationship $relationship + */ + private function deleteRelationship(Relationship $relationship) + { + $reverseRelationship = $relationship->reverseRelationship(); + if ($reverseRelationship) { + $reverseRelationship->delete(); + } + + $relationship->delete(); + } + + /** + * Delete partial contact. + * + * @param Contact $contact + */ + private function deletePartialContact(Contact $contact) + { + // the contact is partial - if the relationship is deleted, the partial + // contact has no reason to exist anymore + if ($contact->is_partial) { + $otherRelations = Relationship::where('account_id', $contact->account_id) + ->where(function (Builder $query) use ($contact) { + return $query->where('of_contact', $contact->id) + ->orWhere('contact_is', $contact->id); + }) + ->count(); + + if ($otherRelations == 0) { + DestroyContact::dispatch([ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + ]); + } + } + } +} diff --git a/app/Services/Contact/Relationship/UpdateRelationship.php b/app/Services/Contact/Relationship/UpdateRelationship.php new file mode 100644 index 0000000..7de5b95 --- /dev/null +++ b/app/Services/Contact/Relationship/UpdateRelationship.php @@ -0,0 +1,69 @@ + 'required|integer|exists:accounts,id', + 'relationship_id' => 'required|integer|exists:relationships,id', + 'relationship_type_id' => 'required|integer|exists:relationship_types,id', + ]; + } + + /** + * Update a relationship. + * + * @param array $data + * @return Relationship + */ + public function execute(array $data): Relationship + { + $this->validate($data); + + $relationship = Relationship::where('account_id', $data['account_id']) + ->findOrFail($data['relationship_id']); + + $relationship->contactIs->throwInactive(); + + $newRelationshipType = RelationshipType::where('account_id', $data['account_id']) + ->findOrFail($data['relationship_type_id']); + + $reverseRelationship = $relationship->reverseRelationship(); + if ($reverseRelationship) { + $newReverseRelationshipType = $newRelationshipType->reverseRelationshipType(); + if ($newReverseRelationshipType) { + $this->updateRelationship($reverseRelationship, $newReverseRelationshipType); + } + } + + return $this->updateRelationship($relationship, $newRelationshipType); + } + + /** + * Update one relationship. + * + * @param Relationship $relationship + * @param RelationshipType $relationshipType + * @return Relationship + */ + private function updateRelationship(Relationship $relationship, RelationshipType $relationshipType): Relationship + { + $relationship->update([ + 'relationship_type_id' => $relationshipType->id, + ]); + + return $relationship; + } +} diff --git a/app/Services/Contact/Reminder/CreateReminder.php b/app/Services/Contact/Reminder/CreateReminder.php new file mode 100644 index 0000000..e984bd1 --- /dev/null +++ b/app/Services/Contact/Reminder/CreateReminder.php @@ -0,0 +1,66 @@ + 'required|integer|exists:accounts,id', + 'contact_id' => 'required|integer|exists:contacts,id', + 'initial_date' => 'required|date_format:Y-m-d', + 'frequency_type' => [ + 'required', + Rule::in(Reminder::$frequencyTypes), + ], + 'frequency_number' => 'required|integer', + 'title' => 'required|string|max:100000', + 'description' => 'nullable|max:1000000', + 'delible' => 'nullable|boolean', + ]; + } + + /** + * Create a reminder. + * + * @param array $data + * @return Reminder + */ + public function execute(array $data): Reminder + { + $this->validate($data); + + $contact = Contact::where('account_id', $data['account_id']) + ->findOrFail($data['contact_id']); + + $contact->throwInactive(); + + $reminder = Reminder::create([ + 'account_id' => $data['account_id'], + 'contact_id' => $data['contact_id'], + 'title' => $data['title'], + 'description' => $this->nullOrValue($data, 'description'), + 'initial_date' => $data['initial_date'], + 'frequency_type' => $data['frequency_type'], + 'frequency_number' => $data['frequency_number'], + 'delible' => (isset($data['delible']) ? $data['delible'] : true), + ]); + + foreach ($contact->account->users as $user) { + $reminder->schedule($user); + } + + return $reminder; + } +} diff --git a/app/Services/Contact/Reminder/DestroyReminder.php b/app/Services/Contact/Reminder/DestroyReminder.php new file mode 100644 index 0000000..1870e31 --- /dev/null +++ b/app/Services/Contact/Reminder/DestroyReminder.php @@ -0,0 +1,43 @@ + 'required|integer|exists:accounts,id', + 'reminder_id' => 'required|integer|exists:reminders,id', + ]; + } + + /** + * Destroy a reminder and all scheduled reminders that are associated with + * it (in ReminderOutbox table) thanks to foreign keys. + * + * @param array $data + * @return bool + */ + public function execute(array $data): bool + { + $this->validate($data); + + $reminder = Reminder::where('account_id', $data['account_id']) + ->findOrFail($data['reminder_id']); + + $reminder->contact->throwInactive(); + + $reminder->delete(); + + return true; + } +} diff --git a/app/Services/Contact/Reminder/UpdateReminder.php b/app/Services/Contact/Reminder/UpdateReminder.php new file mode 100644 index 0000000..cb7a472 --- /dev/null +++ b/app/Services/Contact/Reminder/UpdateReminder.php @@ -0,0 +1,70 @@ + 'required|integer|exists:accounts,id', + 'contact_id' => 'required|integer|exists:contacts,id', + 'reminder_id' => 'required|integer|exists:reminders,id', + 'initial_date' => 'required|date_format:Y-m-d', + 'frequency_type' => [ + 'required', + Rule::in(Reminder::$frequencyTypes), + ], + 'frequency_number' => 'nullable|integer', + 'title' => 'required|string|max:100000', + 'description' => 'nullable|max:1000000', + 'delible' => 'nullable|boolean', + ]; + } + + /** + * Update a reminder. + * + * @param array $data + * @return Reminder + */ + public function execute(array $data): Reminder + { + $this->validate($data); + + /** @var Reminder */ + $reminder = Reminder::where('account_id', $data['account_id']) + ->where('contact_id', $data['contact_id']) + ->findOrFail($data['reminder_id']); + + $contact = Contact::where('account_id', $data['account_id']) + ->findOrFail($data['contact_id']); + + $contact->throwInactive(); + + $reminder->update([ + 'title' => $data['title'], + 'description' => $this->nullOrValue($data, 'description'), + 'initial_date' => $data['initial_date'], + 'frequency_type' => $data['frequency_type'], + 'frequency_number' => $this->nullOrValue($data, 'frequency_number'), + 'delible' => (isset($data['delible']) ? $data['delible'] : true), + ]); + + foreach ($reminder->account->users as $user) { + $reminder->schedule($user); + } + + return $reminder; + } +} diff --git a/app/Services/Contact/Tag/AssociateTag.php b/app/Services/Contact/Tag/AssociateTag.php new file mode 100644 index 0000000..f3d5611 --- /dev/null +++ b/app/Services/Contact/Tag/AssociateTag.php @@ -0,0 +1,100 @@ + 'required|integer|exists:accounts,id', + 'contact_id' => 'required|integer', + 'name' => 'required|string', + ]; + } + + /** + * Associate a tag to a contact. + * + * @param array $data + * @return Tag + */ + public function execute(array $data): Tag + { + $this->validate($data); + + $contact = Contact::where('account_id', $data['account_id']) + ->findOrFail($data['contact_id']); + + $contact->throwInactive(); + + // check if the tag already exists in the account + $tag = $this->tagExistOrCreate($data); + + // associate the tag to the contact + $this->associateToContact($tag, $contact); + + return $tag; + } + + /** + * Check if the tag already exists in the account. + * If it does, returns it. + * If it doesn't, create it. + * + * @return Tag + */ + private function tagExistOrCreate(array $data): Tag + { + $tag = Tag::where([ + 'account_id' => $data['account_id'], + 'name' => $data['name'], + ]) + ->first(); + + if (! $tag) { + return $this->createTag($data); + } + + return $tag; + } + + /** + * Creates the tag. + * + * @return Tag + */ + private function createTag(array $data): Tag + { + return app(CreateTag::class)->execute([ + 'account_id' => $data['account_id'], + 'name' => $data['name'], + ]); + } + + /** + * Associate the tag to the contact. + * + * @return void + */ + private function associateToContact(Tag $tag, Contact $contact) + { + // make sure the tag is not associated with the contact already + $contact->tags()->detach($tag->id); + + $contact->tags()->syncWithoutDetaching([ + $tag->id => [ + 'account_id' => $contact->account_id, + ], + ]); + } +} diff --git a/app/Services/Contact/Tag/CreateTag.php b/app/Services/Contact/Tag/CreateTag.php new file mode 100644 index 0000000..96b3be7 --- /dev/null +++ b/app/Services/Contact/Tag/CreateTag.php @@ -0,0 +1,47 @@ + 'required|integer|exists:accounts,id', + 'name' => 'required|string', + ]; + } + + /** + * Create a tag. + * + * @param array $data + * @return Tag + */ + public function execute(array $data): Tag + { + $this->validate($data); + + $array = [ + 'account_id' => $data['account_id'], + 'name' => $data['name'], + 'name_slug' => Str::slug($data['name'], '-', LocaleHelper::getLang()), + ]; + + if (empty($array['name_slug'])) { + $array['name_slug'] = htmlentities($data['name']); + } + + return Tag::create($array); + } +} diff --git a/app/Services/Contact/Tag/DestroyTag.php b/app/Services/Contact/Tag/DestroyTag.php new file mode 100644 index 0000000..a1d177e --- /dev/null +++ b/app/Services/Contact/Tag/DestroyTag.php @@ -0,0 +1,42 @@ + 'required|integer|exists:accounts,id', + 'tag_id' => 'required|integer', + ]; + } + + /** + * Destroy a tag. + * + * @param array $data + * @return bool + */ + public function execute(array $data) + { + $this->validate($data); + + $tag = Tag::where('account_id', $data['account_id']) + ->findOrFail($data['tag_id']); + + $tag->contacts()->detach(); + + $tag->delete(); + + return true; + } +} diff --git a/app/Services/Contact/Tag/DetachTag.php b/app/Services/Contact/Tag/DetachTag.php new file mode 100644 index 0000000..7494d1c --- /dev/null +++ b/app/Services/Contact/Tag/DetachTag.php @@ -0,0 +1,45 @@ + 'required|integer|exists:accounts,id', + 'contact_id' => 'required|integer', + 'tag_id' => 'required|integer', + ]; + } + + /** + * Detach the tag associated with a contact. + * + * @param array $data + * @return void + */ + public function execute(array $data) + { + $this->validate($data); + + $contact = Contact::where('account_id', $data['account_id']) + ->findOrFail($data['contact_id']); + + $contact->throwInactive(); + + Tag::where('account_id', $data['account_id']) + ->findOrFail($data['tag_id']); + + $contact->tags()->detach($data['tag_id']); + } +} diff --git a/app/Services/Contact/Tag/UpdateTag.php b/app/Services/Contact/Tag/UpdateTag.php new file mode 100644 index 0000000..14be3d8 --- /dev/null +++ b/app/Services/Contact/Tag/UpdateTag.php @@ -0,0 +1,46 @@ + 'required|integer|exists:accounts,id', + 'tag_id' => 'required|integer', + 'name' => 'required|string', + ]; + } + + /** + * Update a tag. + * + * @param array $data + * @return Tag + */ + public function execute(array $data): Tag + { + $this->validate($data); + + /** @var Tag */ + $tag = Tag::where('account_id', $data['account_id']) + ->findOrFail($data['tag_id']); + + $tag->name = $data['name']; + $tag->name_slug = Str::slug($data['name'], '-', LocaleHelper::getLang()); + $tag->save(); + + return $tag; + } +} diff --git a/app/Services/DavClient/CreateAddressBookSubscription.php b/app/Services/DavClient/CreateAddressBookSubscription.php new file mode 100644 index 0000000..326abf5 --- /dev/null +++ b/app/Services/DavClient/CreateAddressBookSubscription.php @@ -0,0 +1,91 @@ + 'required|integer|exists:accounts,id', + 'user_id' => 'required|integer|exists:users,id', + 'base_uri' => 'required|string|url', + 'username' => 'required|string', + 'password' => 'required|string', + ]; + } + + /** + * Add a new Adress Book. + * + * @param array $data + * @return AddressBookSubscription|null + */ + public function execute(array $data): ?AddressBookSubscription + { + $this->validate($data); + + $addressBookData = $this->getAddressBookData($data); + if (! $addressBookData) { + throw new DavClientException(__('Could not get address book data.')); + } + + $lastAddressBook = AddressBook::where('account_id', $data['account_id']) + ->orderBy('id', 'desc') + ->first(); + + $lastId = 0; + if ($lastAddressBook) { + $lastId = intval(preg_replace('/\w+(\d+)/i', '$1', $lastAddressBook->name)); + } + $nextAddressBookName = 'contacts'.($lastId + 1); + + $addressBook = AddressBook::create([ + 'account_id' => $data['account_id'], + 'user_id' => $data['user_id'], + 'name' => $nextAddressBookName, + 'description' => $addressBookData['name'], + ]); + $subscription = AddressBookSubscription::create([ + 'account_id' => $data['account_id'], + 'user_id' => $data['user_id'], + 'username' => $data['username'], + 'address_book_id' => $addressBook->id, + 'uri' => $addressBookData['uri'], + 'capabilities' => $addressBookData['capabilities'], + ]); + $subscription->password = $data['password']; + $subscription->save(); + + return $subscription; + } + + private function getAddressBookData(array $data): ?array + { + $client = $this->getClient($data); + + return app(AddressBookGetter::class) + ->execute($client); + } + + private function getClient(array $data): DavClient + { + return app(DavClient::class) + ->setBaseUri(Arr::get($data, 'base_uri')) + ->setCredentials(Arr::get($data, 'username'), Arr::get($data, 'password')); + } +} diff --git a/app/Services/DavClient/SynchronizeAddressBook.php b/app/Services/DavClient/SynchronizeAddressBook.php new file mode 100644 index 0000000..3b327f0 --- /dev/null +++ b/app/Services/DavClient/SynchronizeAddressBook.php @@ -0,0 +1,76 @@ + 'required|integer|exists:accounts,id', + 'addressbook_subscription_id' => 'required|integer|exists:addressbook_subscriptions,id', + 'force' => 'nullable|boolean', + ]; + } + + /** + * @param array $data + * @return void + */ + public function execute(array $data) + { + $this->validate($data); + + $account = Account::find($data['account_id']); + if (AccountHelper::hasReachedContactLimit($account) + && AccountHelper::hasLimitations($account) + && ! $account->legacy_free_plan_unlimited_contacts) { + abort(402); + } + + $subscription = AddressBookSubscription::where('account_id', $data['account_id']) + ->findOrFail($data['addressbook_subscription_id']); + + try { + $this->sync($data, $subscription); + } catch (ClientException $e) { + Log::error(__CLASS__.' '.__FUNCTION__.': '.$e->getMessage(), [ + 'body' => $e->hasResponse() ? $e->getResponse()->getBody() : null, + $e, + ]); + } + } + + private function sync(array $data, AddressBookSubscription $subscription) + { + $client = $this->getDavClient($subscription); + $sync = new SyncDto($subscription, $client); + $force = Arr::get($data, 'force', false); + + app(AddressBookSynchronizer::class) + ->execute($sync, $force); + } + + private function getDavClient(AddressBookSubscription $subscription): DavClient + { + return app(DavClient::class) + ->setBaseUri($subscription->uri) + ->setCredentials($subscription->username, $subscription->password); + } +} diff --git a/app/Services/DavClient/UpdateSubscriptionLocalSyncToken.php b/app/Services/DavClient/UpdateSubscriptionLocalSyncToken.php new file mode 100644 index 0000000..43977ed --- /dev/null +++ b/app/Services/DavClient/UpdateSubscriptionLocalSyncToken.php @@ -0,0 +1,55 @@ + 'required|integer|exists:accounts,id', + 'addressbook_subscription_id' => 'required|integer|exists:addressbook_subscriptions,id', + ]; + } + + /** + * @param array $data + * @return void + */ + public function execute(array $data): void + { + $this->validate($data); + + $subscription = AddressBookSubscription::where('account_id', $data['account_id']) + ->findOrFail($data['addressbook_subscription_id']); + + $this->updateSyncToken($subscription); + } + + /** + * Update the synctoken. + * + * @return void + */ + private function updateSyncToken(AddressBookSubscription $subscription): void + { + $backend = app(CardDAVBackend::class) + ->init($subscription->user); + + $token = $backend->getCurrentSyncToken($subscription->addressbook->name); + + if ($token !== null) { + $subscription->localSyncToken = $token->id; + $subscription->save(); + } + } +} diff --git a/app/Services/DavClient/Utils/AddressBookContactsPush.php b/app/Services/DavClient/Utils/AddressBookContactsPush.php new file mode 100644 index 0000000..72f5583 --- /dev/null +++ b/app/Services/DavClient/Utils/AddressBookContactsPush.php @@ -0,0 +1,118 @@ + $changes + * @param array|null $localChanges + * @return Collection + */ + public function execute(SyncDto $sync, Collection $changes, ?array $localChanges): Collection + { + $this->sync = $sync; + + $changes = $this->preparePushChangedContacts($changes, Arr::get($localChanges, 'modified', [])); + $added = $this->preparePushAddedContacts(Arr::get($localChanges, 'added', [])); + $deleted = $this->prepareDeletedContacts(Arr::get($localChanges, 'deleted', [])); + + return $changes + ->union($added) + ->union($deleted) + ->filter(function ($c) { + return $c !== null; + }); + } + + /** + * Get list of requests to push new contacts. + * + * @param array $contacts + * @return Collection + */ + private function preparePushAddedContacts(array $contacts): Collection + { + // All added contact must be pushed + return collect($contacts) + ->map(function (string $uri): ?PushVCard { + $card = $this->backend()->getCard($this->sync->addressBookName(), $uri); + + return $card === false ? null + : new PushVCard($this->sync->subscription, + new ContactPushDto( + $uri, + $card['distant_etag'], + $card['carddata'], + $card['contact_id'] + ) + ); + }); + } + + /** + * Get list of requests to delete contacts. + * + * @param array $contacts + * @return Collection + */ + private function prepareDeletedContacts(array $contacts): Collection + { + // All removed contact must be deleted + return collect($contacts) + ->map(function (string $uri): DeleteVCard { + return new DeleteVCard($this->sync->subscription, $uri); + }); + } + + /** + * Get list of requests to push modified contacts. + * + * @param Collection $changes + * @param array $contacts + * @return Collection + */ + private function preparePushChangedContacts(Collection $changes, array $contacts): Collection + { + $backend = $this->backend(); + + $refreshIds = $changes->map(function (ContactDto $contact) use ($backend) { + return $backend->getUuid($contact->uri); + }); + + // We don't push contact that have just been pulled + return collect($contacts) + ->reject(function (string $uri) use ($refreshIds, $backend): bool { + $uuid = $backend->getUuid($uri); + + return $refreshIds->contains($uuid); + })->map(function (string $uri) use ($backend): ?PushVCard { + $card = $backend->getCard($this->sync->addressBookName(), $uri); + + return $card === false ? null + : new PushVCard($this->sync->subscription, + new ContactPushDto( + $uri, + $card['distant_etag'], + $card['carddata'], + $card['contact_id'], + $card['distant_etag'] !== null ? ContactPushDto::MODE_MATCH_ETAG : ContactPushDto::MODE_MATCH_ANY + ) + ); + }); + } +} diff --git a/app/Services/DavClient/Utils/AddressBookContactsPushMissed.php b/app/Services/DavClient/Utils/AddressBookContactsPushMissed.php new file mode 100644 index 0000000..8f8d4a2 --- /dev/null +++ b/app/Services/DavClient/Utils/AddressBookContactsPushMissed.php @@ -0,0 +1,75 @@ +|null $localChanges + * @param Collection $distContacts + * @param Collection $localContacts + * @return Collection + */ + public function execute(SyncDto $sync, ?array $localChanges, Collection $distContacts, Collection $localContacts): Collection + { + $this->sync = $sync; + + $missings = $this->preparePushMissedContacts(Arr::get($localChanges, 'added', []), $distContacts, $localContacts); + + return app(AddressBookContactsPush::class) + ->execute($sync, collect(), $localChanges) + ->union($missings); + } + + /** + * Get list of requests of missed contacts. + * + * @param array $added + * @param Collection $distContacts + * @param Collection $localContacts + * @return Collection + */ + private function preparePushMissedContacts(array $added, Collection $distContacts, Collection $localContacts): Collection + { + $backend = $this->backend(); + + $distUuids = $distContacts->map(function (ContactDto $contact) use ($backend): string { + return $backend->getUuid($contact->uri); + }); + $addedUuids = collect($added)->map(function (string $uri) use ($backend): string { + return $backend->getUuid($uri); + }); + + return collect($localContacts) + ->filter(function (Contact $contact) use ($distUuids, $addedUuids) { + return ! $distUuids->contains($contact->uuid) + && ! $addedUuids->contains($contact->uuid); + })->map(function (Contact $contact) use ($backend): PushVCard { + $card = $backend->prepareCard($contact); + + return new PushVCard($this->sync->subscription, + new ContactPushDto( + $card['uri'], + $contact->distant_etag, + $card['carddata'], + $contact->id, + ContactPushDto::MODE_MATCH_ANY + ) + ); + }); + } +} diff --git a/app/Services/DavClient/Utils/AddressBookContactsUpdater.php b/app/Services/DavClient/Utils/AddressBookContactsUpdater.php new file mode 100644 index 0000000..378a411 --- /dev/null +++ b/app/Services/DavClient/Utils/AddressBookContactsUpdater.php @@ -0,0 +1,79 @@ + $refresh + * @return Collection + */ + public function execute(SyncDto $sync, Collection $refresh): Collection + { + $this->sync = $sync; + + return $this->hasCapability('addressbookMultiget') + ? $this->refreshMultigetContacts($refresh) + : $this->refreshSimpleGetContacts($refresh); + } + + /** + * Get contacts data with addressbook-multiget request. + * + * @param Collection $refresh + * @return Collection + */ + private function refreshMultigetContacts(Collection $refresh): Collection + { + $updated = $refresh + ->filter(function ($item): bool { + return ! ($item instanceof ContactDeleteDto); + }) + ->pluck('uri')->toArray(); + + $deleted = $refresh + ->filter(function ($item): bool { + return $item instanceof ContactDeleteDto; + }) + ->pluck('uri')->toArray(); + + return collect([ + new GetMultipleVCard($this->sync->subscription, $updated), + new DeleteMultipleVCard($this->sync->subscription, $deleted), + ]); + } + + /** + * Get contacts data with request. + * + * @param Collection $refresh + * @return Collection + */ + private function refreshSimpleGetContacts(Collection $refresh): Collection + { + return $refresh + ->map(function (ContactDto $contact) { + if ($contact instanceof ContactDeleteDto) { + return new DeleteVCard($this->sync->subscription, $contact->uri); + } else { + return new GetVCard($this->sync->subscription, $contact); + } + }); + } +} diff --git a/app/Services/DavClient/Utils/AddressBookContactsUpdaterMissed.php b/app/Services/DavClient/Utils/AddressBookContactsUpdaterMissed.php new file mode 100644 index 0000000..eb65c7b --- /dev/null +++ b/app/Services/DavClient/Utils/AddressBookContactsUpdaterMissed.php @@ -0,0 +1,35 @@ + $localContacts + * @param Collection $distContacts + * @return Collection + */ + public function execute(SyncDto $sync, Collection $localContacts, Collection $distContacts): Collection + { + $this->sync = $sync; + + $uuids = $localContacts->pluck('uuid'); + + $missed = $distContacts->reject(function (ContactDto $contact) use ($uuids): bool { + return $uuids->contains($this->backend()->getUuid($contact->uri)); + }); + + return app(AddressBookContactsUpdater::class) + ->execute($this->sync, $missed); + } +} diff --git a/app/Services/DavClient/Utils/AddressBookGetter.php b/app/Services/DavClient/Utils/AddressBookGetter.php new file mode 100644 index 0000000..6297806 --- /dev/null +++ b/app/Services/DavClient/Utils/AddressBookGetter.php @@ -0,0 +1,272 @@ +client = $client; + + try { + return $this->getAddressBookData(); + } catch (ClientException $e) { + Log::error(__CLASS__.' '.__FUNCTION__.': '.$e->getMessage(), [$e]); + throw $e; + } + } + + /** + * Get address book data: uri, capabilities, and name. + * + * @return array + */ + private function getAddressBookData(): array + { + $uri = $this->getAddressBookBaseUri(); + + $this->client->setBaseUri($uri); + + if (Str::startsWith($uri, 'https://www.googleapis.com')) { + // Google API sucks + $capabilities = [ + 'addressbookMultiget' => true, + 'addressbookQuery' => true, + 'syncCollection' => true, + 'addressData' => [ + 'content-type' => 'text/vcard', + 'version' => '3.0', + ], + ]; + } else { + $capabilities = $this->getCapabilities(); + } + + $name = $this->client->getProperty('{DAV:}displayname'); + + return [ + 'uri' => $uri, + 'capabilities' => $capabilities, + 'name' => $name, + ]; + } + + /** + * Calculate address book base uri. + * + * @return string + */ + private function getAddressBookBaseUri(): string + { + try { + // Get the principal of this account + $principal = $this->getCurrentUserPrincipal(); + $baseUri = $this->client->path($principal); + } catch (\Exception $e) { + $baseUri = $this->client->getServiceUrl(); + } + + if ($baseUri) { + $this->client->setBaseUri($baseUri); + } + + if (! Str::contains($baseUri, 'https://www.googleapis.com')) { + // Google API does not follow rfc2518 section-15 ! + + // Check the OPTIONS of the server + $this->checkOptions(); + } + + // Get the principal of this account + $principal = $this->getCurrentUserPrincipal(); + + // Get the AddressBook of this principal + $addressBook = $this->getAddressBookUrl($principal); + $addressBookUrl = $this->client->path($addressBook); + + if (! Str::contains($addressBookUrl, 'https://www.googleapis.com')) { + // Check the OPTIONS of the server + $this->checkOptions(true, $addressBookUrl); + } + + if ($addressBook === null) { + throw new DavClientException('No address book found'); + } + + return $addressBookUrl; + } + + /** + * Check options of the server. + * + * @param bool $addressbook + * @param string $url + * @return void + * + * @see https://datatracker.ietf.org/doc/html/rfc2518#section-15 + * @see https://datatracker.ietf.org/doc/html/rfc6352#section-6.1 + * + * @throws DavServerNotCompliantException + */ + private function checkOptions(bool $addressbook = false, string $url = '') + { + $options = $this->client->options($url); + + if (! in_array('1', $options) || ! in_array('3', $options) || ($addressbook && ! in_array('addressbook', $options))) { + throw new DavServerNotCompliantException('server is not compliant with rfc2518 section 15.1, or rfc6352 section 6.1'); + } + } + + /** + * Get principal name. + * + * @return string + * + * @see https://datatracker.ietf.org/doc/html/rfc5397#section-3 + * + * @throws DavServerNotCompliantException + */ + private function getCurrentUserPrincipal(): string + { + $prop = $this->client->getProperty('{DAV:}current-user-principal'); + + if (is_null($prop) || empty($prop)) { + throw new DavServerNotCompliantException('Server does not support rfc 5397 section 3 (DAV:current-user-principal)'); + } elseif (is_string($prop)) { + return $prop; + } + + return $prop[0]['value']; + } + + /** + * Get addressbook url. + * + * @return string + * + * @see https://datatracker.ietf.org/doc/html/rfc6352#section-7.1.1 + * + * @throws DavServerNotCompliantException + */ + private function getAddressBookHome(string $principal): string + { + $prop = $this->client->getProperty('{'.CardDAVPlugin::NS_CARDDAV.'}addressbook-home-set', $principal); + + if (is_null($prop) || empty($prop)) { + throw new DavServerNotCompliantException('Server does not support rfc 6352 section 7.1.1 (CARD:addressbook-home-set)'); + } elseif (is_string($prop)) { + return $prop; + } + + return $prop[0]['value']; + } + + /** + * Get Url fro address book. + * + * @return string|null + */ + private function getAddressBookUrl(string $principal): ?string + { + $home = $this->getAddressBookHome($principal); + + $books = $this->client->propfind('{DAV:}resourcetype', 1, [], $home); + + foreach ($books as $book => $properties) { + if ($book == $home) { + continue; + } + + if (($resources = Arr::get($properties, '{DAV:}resourcetype', null)) && + $resources->is('{'.CardDAVPlugin::NS_CARDDAV.'}addressbook')) { + return $book; + } + } + + return null; + } + + /** + * Get capabilities properties. + * + * @return array + */ + private function getCapabilities() + { + return $this->getSupportedReportSet() + + + $this->getSupportedAddressData(); + } + + /** + * Get supported-report-set property. + * + * @return array + */ + private function getSupportedReportSet(): array + { + $supportedReportSet = $this->client->getSupportedReportSet(); + + $addressbookMultiget = in_array('{'.CardDAVPlugin::NS_CARDDAV.'}addressbook-multiget', $supportedReportSet); + $addressbookQuery = in_array('{'.CardDAVPlugin::NS_CARDDAV.'}addressbook-query', $supportedReportSet); + $syncCollection = in_array('{DAV:}sync-collection', $supportedReportSet); + + return [ + 'addressbookMultiget' => $addressbookMultiget, + 'addressbookQuery' => $addressbookQuery, + 'syncCollection' => $syncCollection, + ]; + } + + /** + * Get supported-address-data property. + * + * @return array + */ + private function getSupportedAddressData(): array + { + // get the supported card format + $addressData = collect($this->client->getProperty('{'.CardDAVPlugin::NS_CARDDAV.'}supported-address-data')); + $datas = $addressData->firstWhere('attributes.version', '4.0'); + if (! $datas) { + $datas = $addressData->firstWhere('attributes.version', '3.0'); + } + + if (! $datas) { + // It should not happen ! + $datas = [ + 'attributes' => [ + 'content-type' => 'text/vcard', + 'version' => '4.0', + ], + ]; + } + + return [ + 'addressData' => [ + 'content-type' => Arr::get($datas, 'attributes.content-type'), + 'version' => Arr::get($datas, 'attributes.version'), + ], + ]; + } +} diff --git a/app/Services/DavClient/Utils/AddressBookSynchronizer.php b/app/Services/DavClient/Utils/AddressBookSynchronizer.php new file mode 100644 index 0000000..1b0ba2b --- /dev/null +++ b/app/Services/DavClient/Utils/AddressBookSynchronizer.php @@ -0,0 +1,245 @@ +sync = $sync; + + $force + ? $this->forcesync() + : $this->sync(); + } + + /** + * Sync the address book. + */ + private function sync() + { + // Get changes to sync + $localChanges = $this->backend()->getChangesForAddressBook($this->sync->addressBookName(), (string) $this->sync->subscription->localSyncToken, 1); + + // Get distant changes to sync + $changes = $this->getDistantChanges(); + + // Get distant contacts + $batch = app(AddressBookContactsUpdater::class) + ->execute($this->sync, $changes); + + if (! $this->sync->subscription->readonly) { + $batch->union( + app(AddressBookContactsPush::class) + ->execute($this->sync, $changes, $localChanges) + ); + } + + $accountId = $this->sync->subscription->account_id; + $subscriptionId = $this->sync->subscription->id; + Bus::batch($batch) + ->then(function (Batch $batch) use ($accountId, $subscriptionId) { + app(UpdateSubscriptionLocalSyncToken::class)->execute([ + 'account_id' => $accountId, + 'addressbook_subscription_id' => $subscriptionId, + ]); + }) + ->allowFailures() + ->dispatch(); + } + + /** + * Sync the address book. + */ + private function forcesync() + { + $backend = $this->backend(); + + // Get changes to sync + $localChanges = $backend->getChangesForAddressBook($this->sync->addressBookName(), (string) $this->sync->subscription->localSyncToken, 1); + + // Get current list of contacts + $localContacts = $backend->getObjects($this->sync->addressBookName()); + + // Get distant changes to sync + $distContacts = $this->getAllContactsEtag(); + + // Get missed contacts + $batch = app(AddressBookContactsUpdaterMissed::class) + ->execute($this->sync, $localContacts, $distContacts); + + if (! $this->sync->subscription->readonly) { + $batch->union( + app(AddressBookContactsPushMissed::class) + ->execute($this->sync, $localChanges, $distContacts, $localContacts) + ); + } + + $accountId = $this->sync->subscription->account_id; + $subscriptionId = $this->sync->subscription->id; + Bus::batch($batch) + ->then(function (Batch $batch) use ($accountId, $subscriptionId) { + app(UpdateSubscriptionLocalSyncToken::class)->execute([ + 'account_id' => $accountId, + 'addressbook_subscription_id' => $subscriptionId, + ]); + }) + ->allowFailures() + ->dispatch(); + } + + /** + * Get distant changes to sync. + * + * @return Collection + */ + private function getDistantChanges(): Collection + { + $etags = collect($this->getDistantEtags()); + $contacts = $etags->filter(function ($contact, $href): bool { + return $this->filterDistantContacts($contact, $href); + }) + ->map(function (array $contact, string $href): ContactDto { + return new ContactDto($href, Arr::get($contact, 'properties.200.{DAV:}getetag')); + }); + + $deleted = $etags->filter(function ($contact): bool { + return is_array($contact) && $contact['status'] === '404'; + }) + ->map(function (array $contact, string $href): ContactDto { + return new ContactDeleteDto($href); + }); + + return $contacts->union($deleted); + } + + /** + * Filter contacts to only return vcards type and new contacts or contacts with matching etags. + * + * @param mixed $contact + * @param string $href + * @return bool + */ + private function filterDistantContacts($contact, $href): bool + { + // only return vcards + if (! is_array($contact) || ! Str::contains(Arr::get($contact, 'properties.200.{DAV:}getcontenttype'), 'text/vcard')) { + return false; + } + + // only new contact or contact with etag that match + $card = $this->backend()->getCard($this->sync->addressBookName(), $href); + + return $card === false || $card['etag'] !== Arr::get($contact, 'properties.200.{DAV:}getetag'); + } + + /** + * Get refreshed etags. + * + * @return array + */ + private function getDistantEtags(): array + { + if ($this->hasCapability('syncCollection')) { + // With sync-collection + return $this->callSyncCollectionWhenNeeded(); + } else { + // With PROPFIND + return $this->sync->propFind([ + '{DAV:}getcontenttype', + '{DAV:}getetag', + ], 1); + } + } + + /** + * Make sync-collection request if sync-token has changed. + * + * @return array + */ + private function callSyncCollectionWhenNeeded(): array + { + // get the current distant syncToken + $distantSyncToken = $this->sync->getProperty('{DAV:}sync-token'); + + if (($this->sync->subscription->syncToken ?? '') === $distantSyncToken) { + // no change at all + return []; + } + + return $this->callSyncCollection(); + } + + /** + * Make sync-collection request. + * + * @return array + */ + private function callSyncCollection(): array + { + $syncToken = $this->sync->subscription->syncToken ?? ''; + + // get sync + $collection = $this->sync->syncCollection([ + '{DAV:}getcontenttype', + '{DAV:}getetag', + ], $syncToken); + + // save the new syncToken as current one + if ($newSyncToken = Arr::get($collection, 'synctoken')) { + $this->sync->subscription->syncToken = $newSyncToken; + $this->sync->subscription->save(); + } + + return $collection; + } + + /** + * Get all contacts etag. + * + * @return Collection + */ + private function getAllContactsEtag(): Collection + { + if (! $this->hasCapability('addressbookQuery')) { + return collect(); + } + + $data = $this->sync->addressbookQuery('{DAV:}getetag'); + $data = collect($data); + + $updated = $data->filter(function ($contact): bool { + return is_array($contact) && $contact['status'] === '200'; + }) + ->map(function (array $contact, string $href): ContactDto { + return new ContactDto($href, Arr::get($contact, 'properties.200.{DAV:}getetag')); + }); + $deleted = $data->filter(function ($contact): bool { + return is_array($contact) && $contact['status'] === '404'; + }) + ->map(function (array $contact, string $href): ContactDto { + return new ContactDeleteDto($href); + }); + + return $updated->union($deleted); + } +} diff --git a/app/Services/DavClient/Utils/Dav/DavClient.php b/app/Services/DavClient/Utils/Dav/DavClient.php new file mode 100644 index 0000000..3594353 --- /dev/null +++ b/app/Services/DavClient/Utils/Dav/DavClient.php @@ -0,0 +1,602 @@ +baseUri = $uri; + + return $this; + } + + /** + * Set credentials. + * + * @param string $username + * @param string $password + * @return self + */ + public function setCredentials(string $username, string $password): self + { + $this->username = $username; + $this->password = $password; + + return $this; + } + + /** + * Get current uri. + * + * @param string|null $path + * @return string + */ + public function path(?string $path = null): string + { + $uri = GuzzleUtils::uriFor($this->baseUri); + + return (string) (is_null($path) || empty($path) ? $uri : $uri->withPath((string) Str::of($path)->start('/'))); + } + + /** + * Get a PendingRequest. + * + * @return PendingRequest + */ + public function getRequest(): PendingRequest + { + $request = Http::withUserAgent('Monica DavClient '.config('monica.app_version').'/Guzzle'); + + if (! is_null($this->username) && ! is_null($this->password)) { + $request = $request->withBasicAuth($this->username, $this->password); + } + + return $request; + } + + /** + * Follow rfc6764 to get carddav service url. + * + * @see https://datatracker.ietf.org/doc/html/rfc6764 + */ + public function getServiceUrl() + { + // first attempt on relative url + $target = $this->standardServiceUrl('.well-known/carddav'); + + if (! $target) { + // second attempt on absolute root url + $target = $this->standardServiceUrl('/.well-known/carddav'); + } + + if (! $target) { + // third attempt for non standard server, like Google API + $target = $this->nonStandardServiceUrl('/.well-known/carddav'); + } + + if (! $target) { + // Get service name register (section 9.2) + $target = app(ServiceUrlQuery::class)->execute('_carddavs._tcp', true, $this->path(), $this); + if (is_null($target)) { + $target = app(ServiceUrlQuery::class)->execute('_carddav._tcp', false, $this->path(), $this); + } + } + + return $target; + } + + private function standardServiceUrl(string $url): ?string + { + // Get well-known register (section 9.1) + $response = $this->getRequest() + ->withoutRedirecting() + ->get($this->path($url)); + + $code = $response->status(); + if ($code === 301 || $code === 302) { + return $response->header('Location'); + } + + if ($response->serverError()) { + $response->throw(); + } + + return null; + } + + private function nonStandardServiceUrl($url): ?string + { + $response = $this->getRequest() + ->withoutRedirecting() + ->send('PROPFIND', $this->path($url)); + + $code = $response->status(); + if ($code === 301 || $code === 302) { + return $this->path($response->header('Location')); + } + + return null; + } + + /** + * Do a PROPFIND request. + * + * The list of requested properties must be specified as an array, in clark + * notation. + * + * The returned array will contain a list of filenames as keys, and + * properties as values. + * + * The properties array will contain the list of properties. Only properties + * that are actually returned from the server (without error) will be + * returned, anything else is discarded. + * + * Depth should be either 0 or 1. A depth of 1 will cause a request to be + * made to the server to also return all child resources. + * + * @param string $url + * @param array|string $properties + * @param int $depth + * @return array + */ + public function propFind($properties, int $depth = 0, array $options = [], string $url = ''): array + { + $dom = new \DOMDocument('1.0', 'UTF-8'); + $root = self::addElementNS($dom, 'DAV:', 'd:propfind'); + $prop = self::addElement($dom, $root, 'd:prop'); + + $namespaces = ['DAV:' => 'd']; + + self::fetchProperties($dom, $prop, $properties, $namespaces); + + $body = $dom->saveXML(); + + $response = $this->request('PROPFIND', $url, $body, ['Depth' => $depth], $options); + + $result = self::parseMultiStatus($response->body()); + + // If depth was 0, we only return the top item value + if ($depth === 0) { + reset($result); + $result = current($result); + + return Arr::get($result, 'properties.200', []); + } + + return array_map(function ($statusList) { + return Arr::get($statusList, 'properties.200', []); + }, $result); + } + + /** + * Run a REPORT {DAV:}sync-collection. + * + * @param string $url + * @param array|string $properties + * @param string $syncToken + * @return array + * + * @see https://datatracker.ietf.org/doc/html/rfc6578 + */ + public function syncCollection($properties, string $syncToken, array $options = [], string $url = ''): array + { + $dom = new \DOMDocument('1.0', 'UTF-8'); + $root = self::addElementNS($dom, 'DAV:', 'd:sync-collection'); + + self::addElement($dom, $root, 'd:sync-token', $syncToken); + self::addElement($dom, $root, 'd:sync-level', '1'); + + $prop = self::addElement($dom, $root, 'd:prop'); + + $namespaces = ['DAV:' => 'd']; + + self::fetchProperties($dom, $prop, $properties, $namespaces); + + $body = $dom->saveXML(); + + $response = $this->request('REPORT', $url, $body, ['Depth' => '0'], $options); + + return self::parseMultiStatus($response->body()); + } + + /** + * Run a REPORT card:addressbook-multiget. + * + * @param array|string $properties + * @param iterable $contacts + * @param string $url + * @param array $options + * @return array + * + * @see https://datatracker.ietf.org/doc/html/rfc6352#section-8.7 + */ + public function addressbookMultiget($properties, iterable $contacts, array $options = [], string $url = ''): array + { + $dom = new \DOMDocument('1.0', 'UTF-8'); + $root = self::addElementNS($dom, CardDAVPlugin::NS_CARDDAV, 'card:addressbook-multiget'); + $dom->createAttributeNS('DAV:', 'd:e'); + + $prop = self::addElement($dom, $root, 'd:prop'); + + $namespaces = [ + 'DAV:' => 'd', + CardDAVPlugin::NS_CARDDAV => 'card', + ]; + + self::fetchProperties($dom, $prop, $properties, $namespaces); + + foreach ($contacts as $contact) { + self::addElement($dom, $root, 'd:href', $contact); + } + + $body = $dom->saveXML(); + + $response = $this->request('REPORT', $url, $body, ['Depth' => '1'], $options); + + return self::parseMultiStatus($response->body()); + } + + /** + * Run a REPORT card:addressbook-query. + * + * @param string $url + * @param array|string $properties + * @return array + * + * @see https://datatracker.ietf.org/doc/html/rfc6352#section-8.6 + */ + public function addressbookQuery($properties, array $options = [], string $url = ''): array + { + $dom = new \DOMDocument('1.0', 'UTF-8'); + $root = self::addElementNS($dom, CardDAVPlugin::NS_CARDDAV, 'card:addressbook-query'); + $dom->createAttributeNS('DAV:', 'd:e'); + + $prop = self::addElement($dom, $root, 'd:prop'); + + $namespaces = [ + 'DAV:' => 'd', + CardDAVPlugin::NS_CARDDAV => 'card', + ]; + + self::fetchProperties($dom, $prop, $properties, $namespaces); + + $body = $dom->saveXML(); + + $response = $this->request('REPORT', $url, $body, ['Depth' => '1'], $options); + + return self::parseMultiStatus($response->body()); + } + + /** + * Add properties to the prop object. + * + * Properties must follow: + * - for a simple value + * [ + * '{namespace}value', + * ] + * + * - for a more complex value element + * [ + * [ + * 'name' => '{namespace}value', + * 'value' => 'content element', + * 'attributes' => ['name' => 'value', ...], + * ] + * ] + * + * @param \DOMDocument $dom + * @param \DOMNode $prop + * @param array|string $properties + * @param array $namespaces + * @return void + */ + private static function fetchProperties(\DOMDocument $dom, \DOMNode $prop, $properties, array $namespaces) + { + if (is_string($properties)) { + $properties = [$properties]; + } + + foreach ($properties as $property) { + if (is_array($property)) { + $propertyExt = $property; + $property = $propertyExt['name']; + } + [$namespace, $elementName] = Service::parseClarkNotation($property); + + $ns = Arr::get($namespaces, $namespace); + $element = $ns !== null + ? $dom->createElement("$ns:$elementName") + : $dom->createElementNS($namespace, "x:$elementName"); + + $child = $prop->appendChild($element); + + if (isset($propertyExt)) { + if (($nodeValue = Arr::get($propertyExt, 'value')) !== null) { + $child->nodeValue = $nodeValue; + } + if (($attributes = Arr::get($propertyExt, 'attributes')) !== null) { + foreach ($attributes as $name => $property) { + $child->appendChild($dom->createAttribute($name))->nodeValue = $property; + } + } + } + } + } + + /** + * Get a WebDAV property. + * + * @param string $property + * @param string $url + * @return array|string|null + */ + public function getProperty(string $property, string $url = '', array $options = []) + { + $properties = $this->propfind($property, 0, $options, $url); + + if (($prop = Arr::get($properties, $property)) && is_array($prop)) { + $value = $prop[0]; + + if (is_string($value)) { + $prop = $value; + } + } + + return $prop; + } + + /** + * Get a {DAV:}supported-report-set propfind. + * + * @param array $options + * @return array + * + * @see https://datatracker.ietf.org/doc/html/rfc3253#section-3.1.5 + */ + public function getSupportedReportSet(array $options = []): array + { + $propName = '{DAV:}supported-report-set'; + + $properties = $this->propFind($propName, 0, $options); + + if (($prop = Arr::get($properties, $propName)) && is_array($prop)) { + $prop = array_map(function ($supportedReport) { + return $this->iterateOver($supportedReport, '{DAV:}supported-report', function ($report) { + return $this->iterateOver($report, '{DAV:}report', function ($type) { + return Arr::get($type, 'name'); + }); + }); + }, $prop); + } + + return $prop; + } + + /** + * Iterate over the list, if it contains an item name that match with $name. + * + * @param array $list + * @param string $name + * @param callable $callback + * @return mixed + */ + private function iterateOver(array $list, string $name, callable $callback) + { + if (Arr::get($list, 'name') === $name + && ($value = Arr::get($list, 'value'))) { + foreach ($value as $item) { + return $callback($item); + } + } + } + + /** + * Updates a list of properties on the server. + * + * The list of properties must have clark-notation properties for the keys, + * and the actual (string) value for the value. If the value is null, an + * attempt is made to delete the property. + * + * @param string $url + * @param array $properties + * @return bool + * + * @see https://datatracker.ietf.org/doc/html/rfc2518#section-12.13 + */ + public function propPatch(array $properties, string $url = ''): bool + { + $propPatch = new PropPatch(); + $propPatch->properties = $properties; + $body = (new Service())->write( + '{DAV:}propertyupdate', + $propPatch + ); + + $response = $this->request('PROPPATCH', $url, $body); + + if ($response->status() === 207) { + // If it's a 207, the request could still have failed, but the + // information is hidden in the response body. + $result = self::parseMultiStatus($response->body()); + + $errorProperties = []; + foreach ($result as $statusList) { + foreach ($statusList['properties'] as $status => $properties) { + if ($status >= 400) { + foreach ($properties as $propName => $propValue) { + $errorProperties[] = $propName.' ('.$status.')'; + } + } + } + } + if (! empty($errorProperties)) { + throw new DavClientException('PROPPATCH failed. The following properties errored: '.implode(', ', $errorProperties)); + } + } + + return true; + } + + /** + * Performs an HTTP options request. + * + * This method returns all the features from the 'DAV:' header as an array. + * If there was no DAV header, or no contents this method will return an + * empty array. + * + * @param string $url + * @return array + */ + public function options(string $url = ''): array + { + $response = $this->request('OPTIONS', $url); + + $dav = $response->header('Dav'); + if (empty($dav)) { + return []; + } + $davs = explode(', ', $dav); + + return array_map(function ($header) { + return trim($header); + }, $davs); + } + + /** + * Performs an actual HTTP request, and returns the result. + * + * @param string $method + * @param string $url + * @param string|null|resource|\Psr\Http\Message\StreamInterface $body + * @param array $headers + * @return Response + */ + public function request(string $method, string $url = '', $body = null, array $headers = [], array $options = []): Response + { + $request = $this->getRequest() + ->withHeaders($headers); + + if ($body !== null) { + $request = $request->withBody($body, 'application/xml; charset=utf-8'); + } + + $url = Str::startsWith($url, 'http') ? $url : $this->path($url); + + return $request + ->send($method, $url, $options) + ->throw(); + } + + /** + * Parses a WebDAV multistatus response body. + * + * This method returns an array with the following structure + * + * [ + * 'url/to/resource' => [ + * 'properties' => [ + * '200' => [ + * '{DAV:}property1' => 'value1', + * '{DAV:}property2' => 'value2', + * ], + * '404' => [ + * '{DAV:}property1' => null, + * '{DAV:}property2' => null, + * ], + * ], + * 'status' => 200, + * ], + * 'url/to/resource2' => [ + * .. etc .. + * ] + * ] + * + * @param string $body xml body + * @return array + * + * @see https://datatracker.ietf.org/doc/html/rfc4918#section-9.2.1 + */ + private static function parseMultiStatus(string $body): array + { + $multistatus = (new Service()) + ->expect('{DAV:}multistatus', $body); + + $result = []; + + if (is_object($multistatus)) { + foreach ($multistatus->getResponses() as $response) { + $result[$response->getHref()] = [ + 'properties' => $response->getResponseProperties(), + 'status' => $response->getHttpStatus() ?? '200', + ]; + } + + $synctoken = $multistatus->getSyncToken(); + if (! empty($synctoken)) { + $result['synctoken'] = $synctoken; + } + } + + return $result; + } + + /** + * Create a new Element Namespace and add it as document's child. + * + * @param \DOMDocument $dom + * @param string|null $namespace + * @param string $qualifiedName + * @return \DOMNode + */ + private static function addElementNS(\DOMDocument $dom, ?string $namespace, string $qualifiedName): \DOMNode + { + return $dom->appendChild($dom->createElementNS($namespace, $qualifiedName)); + } + + /** + * Create a new Element and add it as root's child. + * + * @param \DOMDocument $dom + * @param \DOMNode $root + * @param string $name + * @param string|null $value + * @return \DOMNode + */ + private static function addElement(\DOMDocument $dom, \DOMNode $root, string $name, ?string $value = null): \DOMNode + { + return $root->appendChild($dom->createElement($name, $value)); + } +} diff --git a/app/Services/DavClient/Utils/Dav/DavClientException.php b/app/Services/DavClient/Utils/Dav/DavClientException.php new file mode 100644 index 0000000..5c0c785 --- /dev/null +++ b/app/Services/DavClient/Utils/Dav/DavClientException.php @@ -0,0 +1,9 @@ +dns_get_record($name.'.'.$host, DNS_SRV); + + if ($entries && $entries->count() > 0) { + $entries = collect($entries) + ->groupBy('pri') + ->sortKeys() + ->first() + ->sortByDesc('weight'); + + foreach ($entries as $entry) { + try { + return $this->getUri($entry, $https, $client); + } catch (RequestException $e) { + // no exception + } + } + } + + return null; + } + + /** + * Get uri from entry. + * + * @param array $entry + * @param bool $https + * @param DavClient $client + * @return string + * + * @throws \Http\Client\Exception\RequestException + */ + private function getUri(array $entry, bool $https, DavClient $client): string + { + $uri = (new Uri()) + ->withScheme($https ? 'https' : 'http') + ->withPort($entry['port']) + ->withHost($entry['target']); + + // Test connection + $client->request('GET', $uri); + + return (string) $uri; + } + + private function dns_get_record(string $hostname, int $type = DNS_ANY, ?array &$authns = null, ?array &$addtl = null, bool $raw = false): ?Collection + { + error_clear_last(); + $result = \dns_get_record($hostname, $type, $authns, $addtl, $raw); + if ($result === false) { + return null; + } + + return collect($result); + } +} diff --git a/app/Services/DavClient/Utils/Model/ContactDeleteDto.php b/app/Services/DavClient/Utils/Model/ContactDeleteDto.php new file mode 100644 index 0000000..4c686fd --- /dev/null +++ b/app/Services/DavClient/Utils/Model/ContactDeleteDto.php @@ -0,0 +1,7 @@ +uri = $uri; + $this->etag = $etag; + } +} diff --git a/app/Services/DavClient/Utils/Model/ContactPushDto.php b/app/Services/DavClient/Utils/Model/ContactPushDto.php new file mode 100644 index 0000000..7135b88 --- /dev/null +++ b/app/Services/DavClient/Utils/Model/ContactPushDto.php @@ -0,0 +1,37 @@ +mode = $mode; + $this->contactId = $contact_id; + } +} diff --git a/app/Services/DavClient/Utils/Model/ContactUpdateDto.php b/app/Services/DavClient/Utils/Model/ContactUpdateDto.php new file mode 100644 index 0000000..9484ef0 --- /dev/null +++ b/app/Services/DavClient/Utils/Model/ContactUpdateDto.php @@ -0,0 +1,44 @@ +card = self::transformCard($card); + } + + /** + * Transform card. + * + * @param string|resource $card + * @return string + */ + protected static function transformCard($card): string + { + if (is_resource($card)) { + $card = tap(stream_get_contents($card), function () use ($card) { + fclose($card); + }); + } + + return $card; + } +} diff --git a/app/Services/DavClient/Utils/Model/SyncDto.php b/app/Services/DavClient/Utils/Model/SyncDto.php new file mode 100644 index 0000000..19bf08d --- /dev/null +++ b/app/Services/DavClient/Utils/Model/SyncDto.php @@ -0,0 +1,77 @@ +subscription = $subscription; + $this->client = $client; + } + + /** + * Get address book name. + * + * @return string + */ + public function addressBookName(): string + { + return $this->subscription->addressbook->name; + } + + /** + * Get carddav backend. + * + * @return CardDAVBackend + */ + public function backend(): CardDAVBackend + { + return app(CardDAVBackend::class)->init($this->subscription->user); + } + + /** + * Execute a method against a new dav client instance. + * + * @param string $method + * @param array $parameters + * @return mixed + */ + public function __call($method, $parameters) + { + if (static::hasMacro($method)) { + return $this->macroCall($method, $parameters); + } + + return $this->subscription->getClient() + ->{$method}(...$parameters); + } +} diff --git a/app/Services/DavClient/Utils/Traits/HasCapability.php b/app/Services/DavClient/Utils/Traits/HasCapability.php new file mode 100644 index 0000000..13e2589 --- /dev/null +++ b/app/Services/DavClient/Utils/Traits/HasCapability.php @@ -0,0 +1,19 @@ +sync->subscription->capabilities, $capability, false); + } +} diff --git a/app/Services/DavClient/Utils/Traits/WithSyncDto.php b/app/Services/DavClient/Utils/Traits/WithSyncDto.php new file mode 100644 index 0000000..1046890 --- /dev/null +++ b/app/Services/DavClient/Utils/Traits/WithSyncDto.php @@ -0,0 +1,24 @@ +sync->backend(); + } +} diff --git a/app/Services/DispatchableService.php b/app/Services/DispatchableService.php new file mode 100644 index 0000000..eb4a3a4 --- /dev/null +++ b/app/Services/DispatchableService.php @@ -0,0 +1,50 @@ + 'required|integer|exists:accounts,id', + 'author_id' => 'required|integer|exists:users,id', + 'about_contact_id' => 'nullable|integer|exists:contacts,id', + 'author_name' => 'required|string|max:255', + 'audited_at' => 'required|date', + 'action' => 'required|string|max:255', + 'should_appear_on_dashboard' => 'nullable|boolean', + 'objects' => 'required|json', + ]; + } + + /** + * Log an action that happened in an account. + * + * @param array $data + * @return AuditLog + */ + public function execute(array $data): AuditLog + { + $this->validate($data); + + return AuditLog::create([ + 'account_id' => $data['account_id'], + 'author_id' => $data['author_id'], + 'about_contact_id' => $this->nullOrValue($data, 'about_contact_id'), + 'author_name' => $data['author_name'], + 'audited_at' => $data['audited_at'], + 'action' => $data['action'], + 'objects' => $data['objects'], + 'should_appear_on_dashboard' => $this->valueOrFalse($data, 'should_appear_on_dashboard'), + ]); + } +} diff --git a/app/Services/Instance/Geolocalization/GetGPSCoordinate.php b/app/Services/Instance/Geolocalization/GetGPSCoordinate.php new file mode 100644 index 0000000..28bec7b --- /dev/null +++ b/app/Services/Instance/Geolocalization/GetGPSCoordinate.php @@ -0,0 +1,115 @@ + 'required|integer|exists:accounts,id', + 'place_id' => 'required|integer|exists:places,id', + ]; + } + + /** + * Get the latitude and longitude from a place. + * This method uses LocationIQ to process the geocoding. + * + * @param array $data + * @return Place|null + */ + public function execute(array $data) + { + $this->validateWeatherEnvVariables(); + + $this->validate($data); + + $place = Place::where('account_id', $data['account_id']) + ->findOrFail($data['place_id']); + + return $this->query($place); + } + + /** + * Make sure that geolocation env variables are set. + * + * @return void + */ + private function validateWeatherEnvVariables() + { + if (! config('monica.enable_geolocation') || is_null(config('monica.location_iq_api_key'))) { + throw new MissingEnvVariableException(); + } + } + + /** + * Build the query to send with the API call. + * + * @param Place $place + * @return string|null + */ + private function buildQuery(Place $place): ?string + { + if (($q = $place->getAddressAsString()) === null) { + return null; + } + + $query = http_build_query([ + 'format' => 'json', + 'key' => config('monica.location_iq_api_key'), + 'q' => $q, + ]); + + return Str::finish(config('location.location_iq_url'), '/').'search.php?'.$query; + } + + /** + * Actually make the call to the reverse geocoding API. + * + * @param Place $place + * @return Place|null + */ + private function query(Place $place): ?Place + { + if (($query = $this->buildQuery($place)) === null) { + return null; + } + + try { + $response = Http::get($query); + $response->throw(); + + $place->latitude = $response->json('0.lat'); + $place->longitude = $response->json('0.lon'); + $place->save(); + + return $place; + } catch (RequestException $e) { + if ($e->response->status() === 429 && ($error = $e->response->json('error')) && $error === 'Rate Limited Second') { + throw new RateLimitedSecondException($e); + } elseif ($e->response->status() !== 404 && $e->response->status() !== 400) { + Log::error(__CLASS__.' '.__FUNCTION__.': Error making the call: '.$e->getMessage(), [ + 'query' => Str::of($query)->replace(config('monica.location_iq_api_key'), '******'), + $e, + ]); + } + } + + return null; + } +} diff --git a/app/Services/Instance/IdHasher.php b/app/Services/Instance/IdHasher.php new file mode 100644 index 0000000..6a84279 --- /dev/null +++ b/app/Services/Instance/IdHasher.php @@ -0,0 +1,45 @@ +prefix = $prefix ?? config('hashids.default_prefix'); + } + + public function encodeId($id) + { + return $this->prefix.Hashids::encode($id); + } + + public function decodeId($hash) + { + if (Str::startsWith($hash, $this->prefix)) { + $result = Hashids::decode(Str::after($hash, $this->prefix)); + + if (count($result) > 0) { + return $result[0]; // result is always an array due to quirk in Hashids libary + } + } + + throw new WrongIdException(); + } +} diff --git a/app/Services/Instance/TokenClean.php b/app/Services/Instance/TokenClean.php new file mode 100644 index 0000000..e1945e6 --- /dev/null +++ b/app/Services/Instance/TokenClean.php @@ -0,0 +1,75 @@ + 'boolean', + ]; + } + + /** + * @var Carbon + */ + private $timefix; + + /** + * Clean token list. + * + * @param array $data + */ + public function execute(array $data) + { + $this->timefix = now()->addDays(-7); + + DB::table('synctoken') + ->orderBy('user_id') + ->groupBy('user_id', 'name') + ->select(DB::raw('user_id, name, max(timestamp) as timestamp')) + ->chunk(200, function ($tokens) use ($data) { + foreach ($tokens as $token) { + $this->handleUserToken($data, $token->user_id, $token->name, $token->timestamp); + } + }); + } + + /** + * Handle tokens for a user. + * + * @param array $data + * @param int $userId + * @param string $tokenName + * @param string $timestamp + */ + private function handleUserToken(array $data, int $userId, string $tokenName, string $timestamp) + { + $tokens = SyncToken::where([ + ['user_id', $userId], + ['name', $tokenName], + ['timestamp', '<', Carbon::parse($timestamp)], + ['timestamp', '<', $this->timefix], + ]) + ->orderByDesc('timestamp') + ->get(); + + foreach ($tokens as $token) { + event(new TokenDeleteEvent($token)); + if (! $data['dryrun']) { + $token->delete(); + } + } + } +} diff --git a/app/Services/Instance/Weather/GetWeatherInformation.php b/app/Services/Instance/Weather/GetWeatherInformation.php new file mode 100644 index 0000000..d88374a --- /dev/null +++ b/app/Services/Instance/Weather/GetWeatherInformation.php @@ -0,0 +1,120 @@ + 'required|integer|exists:accounts,id', + 'place_id' => 'required|integer|exists:places,id', + ]; + } + + /** + * Get the weather information. + * + * @param array $data + * @return Weather|null + * + * @throws \Illuminate\Validation\ValidationException if the array that is given in parameter is not valid + * @throws \App\Exceptions\MissingEnvVariableException if the weather services are not enabled + * @throws \Illuminate\Database\Eloquent\ModelNotFoundException if the Place object is not found + */ + public function execute(array $data): ?Weather + { + $this->validateWeatherEnvVariables(); + + $this->validate($data); + + $place = Place::where('account_id', $data['account_id']) + ->findOrFail($data['place_id']); + + if (is_null($place->latitude)) { + throw new NoCoordinatesException(); + } + + return $this->query($place, 'en'); + } + + /** + * Make sure that weather env variables are set. + * + * @return void + */ + private function validateWeatherEnvVariables() + { + if (! config('monica.enable_weather') || is_null(config('monica.weatherapi_key'))) { + throw new MissingEnvVariableException(); + } + } + + /** + * Actually make the call to Darksky. + * + * @param Place $place + * @return Weather|null + * + * @throws \Exception + */ + private function query(Place $place, ?string $lang = null): ?Weather + { + $query = $this->buildQuery($place, $lang); + + try { + $response = Http::get($query); + $response->throw(); + + return Weather::create([ + 'account_id' => $place->account_id, + 'place_id' => $place->id, + 'weather_json' => $response->object(), + ]); + } catch (HttpClientException $e) { + Log::error(__CLASS__.' '.__FUNCTION__.': Error making the call: '.$e->getMessage(), [ + 'query' => Str::of($query)->replace(config('monica.weatherapi_key'), '******'), + $e, + ]); + } + + return null; + } + + /** + * Prepare the query that will be send to Darksky. + * + * @param Place $place + * @return string + */ + private function buildQuery(Place $place, ?string $lang = null) + { + $coords = $place->latitude.','.$place->longitude; + + $query = [ + 'key' => config('monica.weatherapi_key'), + 'q' => $coords, + 'lang' => $lang ?? 'en', + ]; + if ($lang !== null && $lang !== 'en') { + $query['lang'] = $lang; + } + + return Str::of(config('location.weatherapi_url'))->rtrim('/').'?'.http_build_query($query); + } +} diff --git a/app/Services/QueuableService.php b/app/Services/QueuableService.php new file mode 100644 index 0000000..c8f5378 --- /dev/null +++ b/app/Services/QueuableService.php @@ -0,0 +1,27 @@ + 'required|integer|exists:accounts,id', + 'contact_id' => 'nullable|integer', + 'title' => 'required|string:255', + 'description' => 'nullable|string:400000000', + ]; + } + + /** + * Create a task. + * + * @param array $data + * @return Task + */ + public function execute(array $data): Task + { + $this->validate($data); + + if (! empty($data['contact_id'])) { + Contact::where('account_id', $data['account_id']) + ->findOrFail($data['contact_id']); + } + + $task = Task::create($data); + + return Task::find($task->id); + } +} diff --git a/app/Services/Task/DestroyTask.php b/app/Services/Task/DestroyTask.php new file mode 100644 index 0000000..b8cbe91 --- /dev/null +++ b/app/Services/Task/DestroyTask.php @@ -0,0 +1,41 @@ + 'required|integer|exists:accounts,id', + 'task_id' => 'required|integer', + ]; + } + + /** + * Destroy a task. + * + * @param array $data + * @return bool + */ + public function execute(array $data): bool + { + $this->validate($data); + + $task = Task::where('account_id', $data['account_id']) + ->findOrFail($data['task_id']); + + // Delete the object in the DB + $task->delete(); + + return true; + } +} diff --git a/app/Services/Task/UpdateTask.php b/app/Services/Task/UpdateTask.php new file mode 100644 index 0000000..f88d74d --- /dev/null +++ b/app/Services/Task/UpdateTask.php @@ -0,0 +1,57 @@ + 'required|integer|exists:accounts,id', + 'contact_id' => 'nullable|integer', + 'task_id' => 'required|integer|exists:tasks,id', + 'title' => 'required|string:255', + 'description' => 'nullable|string:400000000', + 'completed' => 'required|boolean', + ]; + } + + /** + * Update a task. + * + * @param array $data + * @return Task + */ + public function execute(array $data): Task + { + $this->validate($data); + + if (! empty($data['contact_id'])) { + /** @var Task */ + $task = Task::where('account_id', $data['account_id']) + ->where('contact_id', $data['contact_id']) + ->findOrFail($data['task_id']); + } else { + /** @var Task */ + $task = Task::where('account_id', $data['account_id']) + ->findOrFail($data['task_id']); + } + + $task->update([ + 'title' => $data['title'], + 'description' => (! empty($data['description']) ? $data['description'] : null), + 'completed' => $data['completed'], + 'completed_at' => ($data['completed'] == true ? now() : null), + ]); + + return $task; + } +} diff --git a/app/Services/User/AcceptPolicy.php b/app/Services/User/AcceptPolicy.php new file mode 100644 index 0000000..16f904e --- /dev/null +++ b/app/Services/User/AcceptPolicy.php @@ -0,0 +1,58 @@ + 'required|integer|exists:accounts,id', + 'user_id' => 'required|integer|exists:users,id', + 'ip_address' => 'nullable|string|max:255', + ]; + } + + /** + * Accept the latest user policy. + * + * @param array $data + * @return Term + * + * @throws \Exception + */ + public function execute(array $data): Term + { + $this->validate($data); + + try { + $user = User::where('account_id', $data['account_id']) + ->findOrFail($data['user_id']); + } catch (ModelNotFoundException $e) { + throw new ModelNotFoundException(trans('app.error_user_account')); + } + + $latestTerm = Term::latest()->first(); + + if (! $latestTerm) { + throw new \Exception(trans('app.error_no_term')); + } + + $user->terms()->syncWithoutDetaching([$latestTerm->id => [ + 'account_id' => $user->account_id, + 'ip_address' => $this->nullOrValue($data, 'ip_address'), + ]]); + + return $latestTerm; + } +} diff --git a/app/Services/User/CreateUser.php b/app/Services/User/CreateUser.php new file mode 100644 index 0000000..eb9025a --- /dev/null +++ b/app/Services/User/CreateUser.php @@ -0,0 +1,154 @@ + 'required|integer|exists:accounts,id', + 'first_name' => 'required|max:255', + 'last_name' => 'required|max:255', + 'email' => 'required|email|max:255|unique:users', + 'password' => 'required|min:6', + 'locale' => 'nullable', + 'ip_address' => 'nullable', + ]; + } + + /** + * Create a user. + * + * @param array $data + * @return User + */ + public function execute(array $data): User + { + $this->validate($data); + + $ipAddress = $data['ip_address'] ?? RequestHelper::ip(); + + $user = $this->createUser($data); + $user = $this->setRegionalParameters($user, $ipAddress); + $user->save(); + + app(AcceptPolicy::class)->execute([ + 'account_id' => $user->account_id, + 'user_id' => $user->id, + 'ip_address' => $ipAddress, + ]); + + return $user; + } + + /** + * Create a user. + * + * @param array $data + * @return User + */ + private function createUser($data): User + { + // create the user + $user = new User(); + $user->account_id = $data['account_id']; + $user->first_name = $data['first_name']; + $user->last_name = $data['last_name']; + $user->email = $data['email']; + $user->password = bcrypt($data['password']); + $user->locale = $data['locale'] ?? App::getLocale(); + + return $user; + } + + /** + * Set the regional default parameters. + * + * @param User $user + * @param string|null $ipAddress + * @return User + */ + private function setRegionalParameters($user, $ipAddress): User + { + $infos = RequestHelper::infos($ipAddress); + + // Associate timezone and currency + $currencyCode = $infos['currency']; + $timezone = $infos['timezone']; + if ($infos['country']) { + $country = CountriesHelper::getCountry($infos['country']); + } else { + $country = CountriesHelper::getCountryFromLocale($user->locale); + } + + // Timezone + if (! is_null($timezone)) { + $user->timezone = $timezone; + } elseif (! is_null($country)) { + $user->timezone = CountriesHelper::getDefaultTimezone($country); + } else { + $user->timezone = config('app.timezone'); + } + + // Currency + if ((! is_null($currencyCode) + && ! $this->associateCurrency($user, $currencyCode)) + || ! is_null($country)) { + foreach ($country->getCurrencies() as $currency) { + if ($this->associateCurrency($user, $currency['iso_4217_code'])) { + break; + } + } + } + + // Temperature scale + if (! is_null($country)) { + switch ($country->getIsoAlpha2()) { + case 'US': + case 'BZ': + case 'KY': + $user->temperature_scale = 'fahrenheit'; + break; + default: + $user->temperature_scale = 'celsius'; + break; + } + } else { + $user->temperature_scale = 'celsius'; + } + + return $user; + } + + /** + * Associate currency with the User. + * + * @param User $user + * @param string $currency + * @return bool + */ + private function associateCurrency($user, $currency): bool + { + $currencyObj = Currency::where('iso', $currency)->first(); + if (! is_null($currencyObj)) { + $user->currency()->associate($currencyObj); + + return true; + } + + return false; + } +} diff --git a/app/Services/User/EmailChange.php b/app/Services/User/EmailChange.php new file mode 100644 index 0000000..08bd165 --- /dev/null +++ b/app/Services/User/EmailChange.php @@ -0,0 +1,57 @@ + 'required|integer|exists:accounts,id', + 'email' => 'required|email|unique:users', + 'user_id' => 'required|integer', + ]; + } + + /** + * Update email of the user. + * + * @param array $data + * @return User + */ + public function execute(array $data): User + { + $this->validate($data); + + /** @var User */ + $user = User::where('account_id', $data['account_id']) + ->findOrFail($data['user_id']); + + // Change email of the user + $user->email = $data['email']; + + /** @var int $count */ + $count = Account::count(); + if (config('monica.signup_double_optin') && $count > 1) { + // Resend validation token + $user->email_verified_at = null; + $user->save(); + + $user->sendEmailVerificationNotification(); + } else { + $user->save(); + $user->markEmailAsVerified(); + } + + return $user; + } +} diff --git a/app/Services/User/UpdateViewPreference.php b/app/Services/User/UpdateViewPreference.php new file mode 100644 index 0000000..2dfe1d8 --- /dev/null +++ b/app/Services/User/UpdateViewPreference.php @@ -0,0 +1,48 @@ + 'required|integer|exists:accounts,id', + 'user_id' => 'required|integer|exists:users,id', + 'preference' => 'required|string|max:255', + ]; + } + + /** + * Set the contact view preference. + * + * @param array $data + * @return User + */ + public function execute(array $data): User + { + $this->validate($data); + + try { + /** @var User */ + $user = User::where('account_id', $data['account_id']) + ->findOrFail($data['user_id']); + } catch (ModelNotFoundException $e) { + throw new ModelNotFoundException(trans('app.error_user_account')); + } + + $user->contacts_sort_order = $data['preference']; + $user->save(); + + return $user; + } +} diff --git a/app/Services/VCalendar/ExportTask.php b/app/Services/VCalendar/ExportTask.php new file mode 100644 index 0000000..5c128cc --- /dev/null +++ b/app/Services/VCalendar/ExportTask.php @@ -0,0 +1,105 @@ + 'required|integer|exists:accounts,id', + 'task_id' => 'required|integer|exists:tasks,id', + ]; + } + + /** + * Export one VCalendar. + * + * @param array $data + * @return VCalendar + */ + public function execute(array $data): VCalendar + { + $this->validate($data); + + $task = Task::where('account_id', $data['account_id']) + ->findOrFail($data['task_id']); + + return $this->export($task); + } + + /** + * @param Task $task + * @return VCalendar + */ + private function export(Task $task): VCalendar + { + // The standard for most of these fields can be found on https://datatracker.ietf.org/doc/html/rfc5545 + if (! $task->uuid) { + $task->forceFill([ + 'uuid' => Str::uuid(), + ])->save(); + } + + // Basic information + $vcal = new VCalendar(); + $vtodo = $vcal->create('VTODO'); + $vcal->add($vtodo); + + $this->exportTimezone($vcal); + $this->exportVTodo($task, $vtodo); + + return $vcal; + } + + /** + * @param VCalendar $vcal + */ + private function exportTimezone(VCalendar $vcal) + { + $vcal->add('VTIMEZONE', [ + 'TZID' => Auth::user()->timezone, + ]); + } + + /** + * @param Task $task + * @param VTodo $vtodo + */ + private function exportVTodo(Task $task, VTodo $vtodo) + { + $contact = $task->contact; + + $vtodo->UID = $task->uuid; + $vtodo->SUMMARY = $task->title; + + if ($task->created_at) { + $vtodo->DTSTAMP = $task->created_at; + $vtodo->CREATED = $task->created_at; + } + if (! empty($task->description)) { + $vtodo->DESCRIPTION = $task->description; + } + if ($contact) { + $vtodo->ATTACH = $contact->getLink(); + } + if ($task->completed) { + $vtodo->STATUS = 'COMPLETED'; + } + if ($task->completed_at) { + $vtodo->COMPLETED = $task->completed_at; + } + } +} diff --git a/app/Services/VCalendar/ExportVCalendar.php b/app/Services/VCalendar/ExportVCalendar.php new file mode 100644 index 0000000..5550af4 --- /dev/null +++ b/app/Services/VCalendar/ExportVCalendar.php @@ -0,0 +1,108 @@ + 'required|integer|exists:accounts,id', + 'special_date_id' => 'required|integer|exists:special_dates,id', + ]; + } + + /** + * Export one VCalendar. + * + * @param array $data + * @return VCalendar + */ + public function execute(array $data): VCalendar + { + $this->validate($data); + + $date = SpecialDate::where('account_id', $data['account_id']) + ->findOrFail($data['special_date_id']); + + return $this->export($date); + } + + /** + * @param SpecialDate $date + * @return VCalendar + */ + private function export(SpecialDate $date): VCalendar + { + // The standard for most of these fields can be found on https://datatracker.ietf.org/doc/html/rfc5545 + if (! $date->uuid) { + $date->forceFill([ + 'uuid' => Str::uuid(), + ])->save(); + } + + // Basic information + $vcal = new VCalendar(); + $vevent = $vcal->create('VEVENT'); + $vcal->add($vevent); + + $this->exportTimezone($vcal); + $this->exportBirthday($date, $vevent); + + return $vcal; + } + + /** + * @param VCalendar $vcal + * @return void + */ + private function exportTimezone(VCalendar $vcal) + { + $vcal->add('VTIMEZONE', [ + 'TZID' => Auth::user()->timezone, + ]); + } + + /** + * @param SpecialDate $date + * @param VEvent $vevent + * @return void + */ + private function exportBirthday(SpecialDate $date, VEvent $vevent) + { + $contact = $date->contact; + + $vevent->UID = $date->uuid; + $vevent->DTSTART = $date->date->format('Ymd'); + $vevent->DTSTART['VALUE'] = 'DATE'; + $vevent->DTEND = $date->date->addDays(1)->format('Ymd'); + $vevent->DTEND['VALUE'] = 'DATE'; + $vevent->RRULE = 'FREQ=YEARLY'; + + if ($date->created_at) { + $vevent->DTSTAMP = $date->created_at; + $vevent->CREATED = $date->created_at; + } + if ($contact) { + $name = $contact->name; + $vevent->SUMMARY = trans('people.reminders_birthday', ['name' => $name]); + $vevent->ATTACH = $contact->getLink(); + $vevent->DESCRIPTION = trans('mail.footer_contact_info2_link', [ + 'name' => $name, + 'url' => $contact->getLink(), + ]); + } + } +} diff --git a/app/Services/VCalendar/ImportTask.php b/app/Services/VCalendar/ImportTask.php new file mode 100644 index 0000000..321334b --- /dev/null +++ b/app/Services/VCalendar/ImportTask.php @@ -0,0 +1,189 @@ + 'required|integer|exists:accounts,id', + 'task_id' => 'nullable|integer|exists:tasks,id', + 'entry' => 'required|string', + ]; + } + + /** + * Export one VCalendar. + * + * @param array $data + * @return array + */ + public function execute(array $data): array + { + $this->validate($data); + + if (Arr::has($data, 'task_id') && ! is_null($data['task_id'])) { + $task = Task::where('account_id', $data['account_id']) + ->findOrFail($data['task_id']); + } else { + $task = new Task(['account_id' => $data['account_id']]); + } + + return $this->process($data, $task); + } + + /** + * Import one VCalendar. + * + * @param array $data + * @param Task $task + * @return array + */ + private function process(array $data, Task $task): array + { + $entry = $this->getEntry($data); + + if (! $entry) { + return [ + 'error' => '0', + ]; + } + + if (! $this->canImportCurrentEntry($entry)) { + return [ + 'error' => '1', + ]; + } + + $task = $this->importEntry($task, $entry); + + return [ + 'task_id' => $task->id, + ]; + } + + /** + * Check whether this entry contains a VTODO. If not, it + * can not be imported. + * + * @param VCalendar $entry + * @return bool + */ + private function canImportCurrentEntry(VCalendar $entry): bool + { + return ! is_null($entry->VTODO); + } + + /** + * Create the Task object matching the current entry. + * + * @param Task $task + * @param VCalendar $entry + * @return Task + */ + private function importEntry($task, VCalendar $entry): Task + { + $this->importUid($task, $entry); + $this->importSummary($task, $entry); + $this->importCompleted($task, $entry); + $this->importTimestamp($task, $entry); + + $task->save(); + + return $task; + } + + /** + * @param array $data + * @return VCalendar|null + */ + private function getEntry($data): ?VCalendar + { + try { + $entry = Reader::read($data['entry'], Reader::OPTION_FORGIVING + Reader::OPTION_IGNORE_INVALID_LINES); + if ($entry instanceof VCalendar) { + return $entry; + } + } catch (ParseException $e) { + // catch parse errors + } + + return null; + } + + /** + * Import uid. + * + * @param Task $task + * @param VCalendar $entry + * @return void + */ + private function importUid(Task $task, VCalendar $entry): void + { + if (empty($task->uuid) && Uuid::isValid((string) $entry->VTODO->UID)) { + $task->uuid = (string) $entry->VTODO->UID; + } + } + + /** + * Import uid. + * + * @param Task $task + * @param VCalendar $entry + * @return void + */ + private function importTimestamp(Task $task, VCalendar $entry): void + { + if (empty($task->created_at)) { + if ($entry->VTODO->DTSTAMP) { + $task->created_at = Carbon::parse($entry->VTODO->DTSTAMP->getDateTime()); + } elseif ($entry->VTODO->CREATED) { + $task->created_at = Carbon::parse($entry->VTODO->CREATED->getDateTime()); + } + } + } + + /** + * @param Task $task + * @param VCalendar $entry + */ + private function importSummary(Task $task, VCalendar $entry) + { + $task->title = $this->formatValue($entry->VTODO->SUMMARY); + if ($entry->VTODO->DESCRIPTION) { + $task->description = $this->formatValue($entry->VTODO->DESCRIPTION); + } + } + + /** + * @param Task $task + * @param VCalendar $entry + */ + private function importCompleted(Task $task, VCalendar $entry) + { + $task->completed = ((string) $entry->VTODO->STATUS) == 'COMPLETED'; + if (! $task->completed) { + $task->completed_at = null; + } elseif ($entry->VTODO->COMPLETED) { + $task->completed_at = Carbon::parse($entry->VTODO->COMPLETED->getDateTime()); + } + } +} diff --git a/app/Services/VCard/ExportVCard.php b/app/Services/VCard/ExportVCard.php new file mode 100644 index 0000000..c6f1224 --- /dev/null +++ b/app/Services/VCard/ExportVCard.php @@ -0,0 +1,340 @@ + 'required|integer|exists:accounts,id', + 'contact_id' => 'required|integer|exists:contacts,id', + ]; + } + + /** + * Export one VCard. + * + * @param array $data + * @return VCard + */ + public function execute(array $data): VCard + { + $this->validate($data); + + /** @var Contact */ + $contact = Contact::where('account_id', $data['account_id']) + ->findOrFail($data['contact_id']); + + $vcard = $this->export($contact); + + $contact->timestamps = false; + $contact->vcard = $vcard->serialize(); + $contact->save(); + + return $vcard; + } + + private function escape($value): string + { + return ! empty((string) $value) ? trim((string) $value) : (string) null; + } + + /** + * @param Contact $contact + * @return VCard + */ + private function export(Contact $contact): VCard + { + // The standard for most of these fields can be found on https://datatracker.ietf.org/doc/html/rfc6350 + if (! $contact->uuid) { + $contact->forceFill([ + 'uuid' => Str::uuid(), + ])->save(); + } + + if ($contact->vcard) { + try { + /** @var VCard */ + $vcard = Reader::read($contact->vcard, Reader::OPTION_FORGIVING + Reader::OPTION_IGNORE_INVALID_LINES); + if (! $vcard->UID) { + $vcard->UID = $contact->uuid; + } + } catch (ParseException $e) { + // Ignore error + } + } + if (! isset($vcard)) { + // Basic information + $vcard = new VCard([ + 'UID' => $contact->uuid, + 'SOURCE' => $contact->getLink(), + 'VERSION' => '4.0', + ]); + } + + $this->exportNames($contact, $vcard); + $this->exportGender($contact, $vcard); + $this->exportPhoto($contact, $vcard); + $this->exportWorkInformation($contact, $vcard); + $this->exportBirthday($contact, $vcard); + $this->exportAddress($contact, $vcard); + $this->exportContactFields($contact, $vcard); + $this->exportTimestamp($contact, $vcard); + $this->exportTags($contact, $vcard); + + return $vcard; + } + + /** + * @param Contact $contact + * @param VCard $vcard + */ + private function exportNames(Contact $contact, VCard $vcard) + { + $vcard->remove('FN'); + $vcard->remove('N'); + $vcard->remove('NICKNAME'); + + $vcard->add('FN', $this->escape($contact->name)); + + $vcard->add('N', [ + $this->escape($contact->last_name), + $this->escape($contact->first_name), + $this->escape($contact->middle_name), + ]); + + if (! empty($contact->nickname)) { + $vcard->add('NICKNAME', $this->escape($contact->nickname)); + } + } + + /** + * @param Contact $contact + * @param VCard $vcard + */ + private function exportGender(Contact $contact, VCard $vcard) + { + $vcard->remove('GENDER'); + + if (is_null($contact->gender)) { + return; + } + + $gender = $contact->gender->type; + if (empty($gender)) { + switch ($contact->gender->name) { + case trans('app.gender_male'): + $gender = Gender::MALE; + break; + case trans('app.gender_female'): + $gender = Gender::FEMALE; + break; + default: + $gender = Gender::OTHER; + break; + } + } + $vcard->add('GENDER', $gender); + } + + /** + * @param Contact $contact + * @param VCard $vcard + */ + private function exportPhoto(Contact $contact, VCard $vcard) + { + $vcard->remove('PHOTO'); + + if ($contact->avatar_source == 'photo') { + $photo = $contact->avatarPhoto; + + $vcard->add('PHOTO', $photo->dataUrl()); + } else { + $picture = $contact->getAvatarURL(); + + if (! empty($picture)) { + $vcard->add('PHOTO', $picture); + } + } + } + + /** + * @param Contact $contact + * @param VCard $vcard + */ + private function exportWorkInformation(Contact $contact, VCard $vcard) + { + $vcard->remove('ORG'); + $vcard->remove('TITLE'); + + if (! empty($contact->company)) { + $vcard->add('ORG', $this->escape($contact->company)); + } + + if (! empty($contact->job)) { + $vcard->add('TITLE', $this->escape($contact->job)); + } + } + + /** + * @param Contact $contact + * @param VCard $vcard + * + * @see https://datatracker.ietf.org/doc/html/rfc6350#section-6.2.5 + */ + private function exportBirthday(Contact $contact, VCard $vcard) + { + $vcard->remove('BDAY'); + + if (! is_null($contact->birthdate)) { + if ($contact->birthdate->is_year_unknown) { + $date = $contact->birthdate->date->format('--md'); + } else { + $date = $contact->birthdate->date->format('Ymd'); + } + $vcard->add('BDAY', $date); + } + } + + /** + * @param Contact $contact + * @param VCard $vcard + * + * @see https://datatracker.ietf.org/doc/html/rfc6350#section-6.3.1 + */ + private function exportAddress(Contact $contact, VCard $vcard) + { + $vcard->remove('ADR'); + + foreach ($contact->addresses as $address) { + $type = $this->getContactFieldLabel($address); + $arguments = []; + if ($type != '') { + $arguments['TYPE'] = $type; + } + $vcard->add('ADR', [ + '', + '', + $address->place->street, + $address->place->city, + $address->place->province, + $address->place->postal_code, + $address->place->country, + ], + $arguments + ); + } + } + + /** + * @param Contact $contact + * @param VCard $vcard + */ + private function exportContactFields(Contact $contact, VCard $vcard) + { + $vcard->remove('TEL'); + $vcard->remove('EMAIL'); + $vcard->remove('socialProfile'); + $vcard->remove('URL'); + + foreach ($contact->contactFields as $contactField) { + $type = $this->getContactFieldLabel($contactField); + switch ($contactField->contactFieldType->type) { + case ContactFieldType::PHONE: + $vcard->add('TEL', $this->escape($contactField->data), $type); + break; + case ContactFieldType::EMAIL: + $vcard->add('EMAIL', $this->escape($contactField->data), $type); + break; + default: + switch ($contactField->contactFieldType->name) { + // See https://tools.ietf.org/id/draft-george-vcarddav-vcard-extension-02.html + case 'Facebook': + $vcard->add('socialProfile', $this->escape('https://www.facebook.com/'.$contactField->data), ['type' => 'facebook']); + break; + case 'Twitter': + $vcard->add('socialProfile', $this->escape('https://twitter.com/'.$contactField->data), ['type' => 'twitter']); + break; + case 'Whatsapp': + $vcard->add('socialProfile', $this->escape('https://wa.me/'.$contactField->data), ['type' => 'whatsapp']); + break; + case 'Telegram': + $vcard->add('socialProfile', $this->escape('http://t.me/'.$contactField->data), ['type' => 'telegram']); + break; + case 'LinkedIn': + $vcard->add('socialProfile', $this->escape('http://www.linkedin.com/in/'.$contactField->data), ['type' => 'linkedin']); + break; + default: + // If field isn't a supported social profile, but still has a protocol, then export it as a url. + if (! empty($contactField->contactFieldType->protocol)) { + $vcard->add('URL', $this->escape($contactField->contactFieldType->protocol.$contactField->data)); + } + break; + } + break; + } + } + } + + /** + * @param LabelInterface $labelProvider + * @return array|null + */ + private function getContactFieldLabel(LabelInterface $labelProvider): ?array + { + $type = null; + /** @var \Illuminate\Support\Collection */ + $labels = $labelProvider->labels()->get(); + if ($labels->count() > 0) { + $type = []; + $type['type'] = $labels->map(function (ContactFieldLabel $label): string { + return mb_strtoupper($label->label_i18n) ?: $label->label; + })->join(','); + } + + return $type; + } + + /** + * @param Contact $contact + * @param VCard $vcard + */ + private function exportTimestamp(Contact $contact, VCard $vcard) + { + $vcard->remove('REV'); + $vcard->REV = $contact->updated_at->format('Ymd\\THis\\Z'); + } + + /** + * @param Contact $contact + * @param VCard $vcard + */ + private function exportTags(Contact $contact, VCard $vcard) + { + $vcard->remove('CATEGORIES'); + + if ($contact->tags->count() > 0) { + $vcard->CATEGORIES = $contact->tags->map(function (Tag $tag): string { + return $tag->name; + })->toArray(); + } + } +} diff --git a/app/Services/VCard/GetEtag.php b/app/Services/VCard/GetEtag.php new file mode 100644 index 0000000..776f795 --- /dev/null +++ b/app/Services/VCard/GetEtag.php @@ -0,0 +1,38 @@ + 'required|integer|exists:accounts,id', + 'contact_id' => 'required|integer|exists:contacts,id', + ]; + } + + /** + * Export etag of the VCard. + * + * @param array $data + * @return string + */ + public function execute(array $data): string + { + $this->validate($data); + + $contact = Contact::where('account_id', $data['account_id']) + ->findOrFail($data['contact_id']); + + return $contact->distant_etag ?? '"'.sha1($contact->vcard).'"'; + } +} diff --git a/app/Services/VCard/ImportVCard.php b/app/Services/VCard/ImportVCard.php new file mode 100644 index 0000000..20dce0b --- /dev/null +++ b/app/Services/VCard/ImportVCard.php @@ -0,0 +1,1231 @@ + */ + protected $errorResults = [ + 'ERROR_PARSER' => 'import_vcard_parse_error', + 'ERROR_CONTACT_EXIST' => 'import_vcard_contact_exist', + 'ERROR_CONTACT_DOESNT_HAVE_FIRSTNAME' => 'import_vcard_contact_no_firstname', + ]; + + /** + * Valids value for frequency type. + * + * @var array + */ + public static $behaviourTypes = [ + self::BEHAVIOUR_ADD, self::BEHAVIOUR_REPLACE, + ]; + + /** + * The Account id. + * + * @var int + */ + public $accountId; + + /** + * The User id. + * + * @var int + */ + public $userId; + + /** + * The contact fields ids. + * + * @var array + */ + protected $contactFields; + + /** + * The genders that will be associated with imported contacts. + * + * @var array + */ + protected $genders; + + /** + * @var AddressBook|null + */ + protected $addressBook; + + /** + * Get the validation rules that apply to the service. + * + * + * @return array + */ + public function rules() + { + return [ + 'account_id' => 'required|integer|exists:accounts,id', + 'user_id' => 'required|integer|exists:users,id', + 'contact_id' => 'nullable|integer|exists:contacts,id', + 'entry' => [ + 'required', + function ($attribute, $value, $fail) { + if (! is_string($value) && ! is_resource($value) && ! $value instanceof VCard) { + $fail($attribute.' must be a string, a resource, or a VCard object.'); + } + }, + ], + 'behaviour' => [ + 'required', + Rule::in(self::$behaviourTypes), + ], + 'addressBookName' => 'nullable|string|exists:addressbooks,name', + 'etag' => 'nullable|string', + ]; + } + + /** + * Import one VCard. + * + * @param array $data + * @return array + */ + public function execute(array $data): array + { + $this->validate($data); + + $account = Account::find($data['account_id']); + if (AccountHelper::hasReachedContactLimit($account) + && AccountHelper::hasLimitations($account) + && ! $account->legacy_free_plan_unlimited_contacts) { + abort(402); + } + + User::where('account_id', $data['account_id']) + ->findOrFail($data['user_id']); + + if ($contactId = Arr::get($data, 'contact_id')) { + Contact::where('account_id', $data['account_id']) + ->findOrFail($contactId); + } + + if ($addressBookName = Arr::get($data, 'addressBookName')) { + AddressBook::where([ + 'account_id' => $data['account_id'], + 'name' => $addressBookName, + ])->firstOrFail(); + } + + return $this->process($data); + } + + private function clear() + { + $this->contactFields = []; + $this->genders = []; + $this->accountId = 0; + $this->userId = 0; + $this->addressBook = null; + } + + /** + * Process data importation. + * + * @param array $data + * @return array + */ + private function process(array $data): array + { + if ($this->accountId !== $data['account_id']) { + $this->clear(); + $this->accountId = $data['account_id']; + } + $this->userId = $data['user_id']; + + if ($addressBookName = Arr::get($data, 'addressBookName')) { + $this->addressBook = AddressBook::where([ + 'account_id' => $data['account_id'], + 'name' => $addressBookName, + ])->first(); + } + + /** + * @var VCard|null $entry + * @var string $vcard + */ + ['entry' => $entry, 'vcard' => $vcard] = $this->getEntry($data); + + if ($entry === null) { + return [ + 'error' => 'ERROR_PARSER', + 'reason' => $this->errorResults['ERROR_PARSER'], + 'name' => '(unknow)', + ]; + } + + return $this->processEntry($data, $entry, $vcard); + } + + /** + * Process entry importation. + * + * @param array $data + * @param VCard $entry + * @param string $vcard + * @return array + */ + private function processEntry(array $data, VCard $entry, string $vcard): array + { + if (! $this->canImportCurrentEntry($entry)) { + return [ + 'error' => 'ERROR_CONTACT_DOESNT_HAVE_FIRSTNAME', + 'reason' => $this->errorResults['ERROR_CONTACT_DOESNT_HAVE_FIRSTNAME'], + 'name' => $this->name($entry), + ]; + } + + $contactId = Arr::get($data, 'contact_id'); + $contact = $this->getExistingContact($entry, $contactId); + + return $this->processEntryContact($data, $entry, $vcard, $contact); + } + + /** + * Process entry importation. + * + * @param array $data + * @param VCard $entry + * @param string $vcard + * @param Contact|null $contact + * @return array + */ + private function processEntryContact(array $data, VCard $entry, string $vcard, ?Contact $contact): array + { + $behaviour = $data['behaviour'] ?: self::BEHAVIOUR_ADD; + if ($contact && $behaviour === self::BEHAVIOUR_ADD) { + return [ + 'contact_id' => $contact->id, + 'error' => 'ERROR_CONTACT_EXIST', + 'reason' => $this->errorResults['ERROR_CONTACT_EXIST'], + 'name' => $this->name($entry), + ]; + } + + if ($contact) { + $timestamps = $contact->timestamps; + $contact->timestamps = false; + } + + $contact = $this->importEntry($contact, $entry, $vcard, Arr::get($data, 'etag')); + + if (isset($timestamps)) { + $contact->timestamps = $timestamps; + } + + return [ + 'contact_id' => $contact->id, + 'name' => $this->name($entry), + ]; + } + + /** + * @param array $data + * @return array + */ + private function getEntry($data): array + { + $entry = $vcard = $data['entry']; + + if (! $entry instanceof VCard) { + try { + $entry = Reader::read($entry, Reader::OPTION_FORGIVING + Reader::OPTION_IGNORE_INVALID_LINES); + } catch (ParseException $e) { + return [ + 'entry' => null, + 'vcard' => $vcard, + ]; + } + } + + if ($vcard instanceof VCard) { + $vcard = $entry->serialize(); + } + + return [ + 'entry' => $entry, + 'vcard' => $vcard, + ]; + } + + /** + * Get or create the gender called "Vcard" that is associated with all + * imported contacts. + * + * @param string $genderCode + * @return Gender + */ + private function getGender($genderCode): Gender + { + if (! Arr::has($this->genders, $genderCode)) { + $gender = $this->getGenderByType($genderCode); + if (! $gender) { + switch ($genderCode) { + case 'M': + $gender = $this->getGenderByName(trans('app.gender_male')) ?? $this->getGenderByName(config('dav.default_gender')); + break; + case 'F': + $gender = $this->getGenderByName(trans('app.gender_female')) ?? $this->getGenderByName(config('dav.default_gender')); + break; + default: + $gender = $this->getGenderByName(config('dav.default_gender')); + break; + } + } + + if (! $gender) { + $gender = new Gender; + $gender->account_id = $this->accountId; + $gender->name = config('dav.default_gender'); + $gender->type = Gender::UNKNOWN; + $gender->save(); + } + + Arr::set($this->genders, $genderCode, $gender); + } + + return Arr::get($this->genders, $genderCode); + } + + /** + * Get the gender by name. + * + * @param string $name + * @return Gender|null + */ + private function getGenderByName($name) + { + return Gender::where([ + 'account_id' => $this->accountId, + 'name' => $name, + ])->first(); + } + + /** + * Get the gender by type. + * + * @param string $type + * @return Gender|null + */ + private function getGenderByType($type) + { + return Gender::where([ + 'account_id' => $this->accountId, + 'type' => $type, + ])->first(); + } + + /** + * Check whether a contact has a first name or a nickname. If not, contact + * can not be imported. + * + * @param VCard $entry + * @return bool + */ + private function canImportCurrentEntry(VCard $entry): bool + { + return + $this->hasFirstnameInN($entry) || + $this->hasNickname($entry) || + $this->hasFN($entry); + } + + /** + * @param VCard $entry + * @return bool + */ + private function hasFirstnameInN(VCard $entry): bool + { + return $entry->N !== null && ! empty(Arr::get($entry->N->getParts(), '1')); + } + + /** + * @param VCard $entry + * @return bool + */ + private function hasNICKNAME(VCard $entry): bool + { + return ! empty((string) $entry->NICKNAME); + } + + /** + * @param VCard $entry + * @return bool + */ + private function hasFN(VCard $entry): bool + { + return ! empty((string) $entry->FN); + } + + /** + * Check whether the email is valid. + * + * @param string $email + */ + private function isValidEmail(string $email): bool + { + return (bool) filter_var($email, FILTER_VALIDATE_EMAIL); + } + + /** + * Check whether the contact already exists in the database. + * + * @param VCard $entry + * @param int $contact_id + * @return Contact|null + */ + private function getExistingContact(VCard $entry, $contact_id = null) + { + $contact = null; + if (! is_null($contact_id)) { + $contact = Contact::where([ + 'account_id' => $this->accountId, + 'address_book_id' => $this->addressBook ? $this->addressBook->id : null, + ]) + ->find($contact_id); + } + + if (! $contact) { + $contact = $this->existingUuid($entry); + } + + if (! $contact) { + $contact = $this->existingContactWithEmail($entry); + } + + if (! $contact) { + $contact = $this->existingContactWithName($entry); + } + + if ($contact) { + $contact->timestamps = false; + } + + return $contact; + } + + /** + * Search with email field. + * + * @param VCard $entry + * @return Contact|null + */ + private function existingContactWithEmail(VCard $entry): ?Contact + { + if (empty($entry->EMAIL)) { + return null; + } + + if ($this->isValidEmail((string) $entry->EMAIL)) { + $contactField = ContactField::where([ + 'account_id' => $this->accountId, + 'contact_field_type_id' => $this->getContactFieldTypeId(ContactFieldType::EMAIL), + ])->whereIn('data', iterator_to_array($entry->EMAIL))->first(); + + // filter contact field + // - if no address book selected + // - if the address book match the contact's contact field address book + if ($contactField && ( + ! $this->addressBook + || $contactField->contact->address_book_id === $this->addressBook->id + )) { + return $contactField->contact; + } + } + + return null; + } + + /** + * Search with names fields. + * + * @param VCard $entry + * @return Contact|null + */ + private function existingContactWithName(VCard $entry) + { + $contact = []; + $this->importNames($contact, $entry); + + return Contact::where([ + 'account_id' => $this->accountId, + 'first_name' => Arr::get($contact, 'first_name'), + 'middle_name' => Arr::get($contact, 'middle_name'), + 'last_name' => Arr::get($contact, 'last_name'), + 'address_book_id' => $this->addressBook ? $this->addressBook->id : null, + ])->first(); + } + + /** + * Search with uuid. + * + * @param VCard $entry + * @return Contact|null + */ + private function existingUuid(VCard $entry): ?Contact + { + return ! empty($uuid = (string) $entry->UID) && Uuid::isValid($uuid) + ? Contact::where([ + 'account_id' => $this->accountId, + 'uuid' => $uuid, + 'address_book_id' => $this->addressBook ? $this->addressBook->id : null, + ])->first() + : null; + } + + /** + * Create the Contact object matching the current entry. + * + * @param Contact|null $contact + * @param VCard $entry + * @param string $vcard + * @param string|null $etag + * @return Contact + */ + private function importEntry(?Contact $contact, VCard $entry, string $vcard, ?string $etag): Contact + { + $contact = $this->importGeneralInformation($contact, $entry); + + $this->importPhoto($contact, $entry); + $this->importWorkInformation($contact, $entry); + $this->importAddress($contact, $entry); + $this->importEmail($contact, $entry); + $this->importTel($contact, $entry); + $this->importSocialProfile($contact, $entry); + $this->importCategories($contact, $entry); + $this->importNote($contact, $entry); + + // Save vcard content + if ($contact->address_book_id) { + $contact->vcard = $vcard; + $contact->distant_etag = $etag; + } + + $contact->save(); + + return $contact; + } + + /** + * Import general contact information. + * + * @param Contact|null $contact + * @param VCard $entry + * @return Contact + */ + private function importGeneralInformation(?Contact $contact, VCard $entry): Contact + { + $contactData = $this->getContactData($contact); + $original = $contactData; + + $contactData = $this->importUid($contactData, $entry); + $contactData = $this->importNames($contactData, $entry); + $contactData = $this->importGender($contactData, $entry); + $contactData = $this->importBirthday($contactData, $entry); + + if ($contact !== null && $contactData !== $original) { + $contact = app(UpdateContact::class)->execute($contactData); + } else { + $contact = app(CreateContact::class)->execute($contactData); + } + + return $contact; + } + + /** + * Get contact data. + * + * @param Contact|null $contact + * @return array + */ + private function getContactData(?Contact $contact): array + { + $result = [ + 'account_id' => $contact ? $contact->account_id : $this->accountId, + 'uuid' => $contact ? $contact->uuid : null, + 'address_book_id' => $this->addressBook ? $this->addressBook->id : null, + 'first_name' => $contact ? $contact->first_name : null, + 'middle_name' => $contact ? $contact->middle_name : null, + 'last_name' => $contact ? $contact->last_name : null, + 'nickname' => $contact ? $contact->nickname : null, + 'gender_id' => $contact ? $contact->gender_id : $this->getGender('O')->id, + 'description' => $contact ? $contact->description : null, + 'is_partial' => $contact ? $contact->is_partial : false, + 'is_birthdate_known' => $contact ? $contact->birthdate !== null : false, + 'is_deceased' => $contact && $contact->is_dead !== null ? $contact->is_dead : false, + 'is_deceased_date_known' => $contact ? $contact->deceasedDate !== null : false, + 'author_id' => $this->userId, + ]; + + if ($contact) { + $result['contact_id'] = $contact->id; + } + + if ($result['is_birthdate_known']) { + if ($result['birthdate_is_age_based'] = $contact->birthdate->is_age_based) { + $result['birthdate_age'] = now()->diffInYears($contact->birthdate->date, true); + } else { + $result['birthdate_day'] = $contact->birthdate->date->day; + $result['birthdate_month'] = $contact->birthdate->date->month; + if (! $contact->birthdate->is_year_unknown) { + $result['birthdate_year'] = $contact->birthdate->date->year; + } + } + } + + if ($result['is_deceased_date_known'] && + ! ($result['birthdate_is_age_based'] = $contact->deceasedDate->is_age_based)) { + $result['deceased_date_day'] = $contact->deceasedDate->date->day; + $result['deceased_date_month'] = $contact->deceasedDate->date->month; + if (! $contact->deceasedDate->is_year_unknown) { + $result['deceased_date_year'] = $contact->deceasedDate->date->year; + } + } + + return $result; + } + + /** + * Import names of the contact. + * + * @param array $contactData + * @param VCard $entry + * @return array + */ + private function importNames(array $contactData, VCard $entry): array + { + if ($this->hasFirstnameInN($entry)) { + $contactData = $this->importFromN($contactData, $entry); + } elseif ($this->hasFN($entry)) { + $contactData = $this->importFromFN($contactData, $entry); + } elseif ($this->hasNICKNAME($entry)) { + $contactData = $this->importFromNICKNAME($contactData, $entry); + } else { + throw new \LogicException('Check if you can import entry!'); + } + + return $contactData; + } + + /** + * Return the name and email address of the current entry. + * John Doe Johnny john@doe.com. + * Only used for report display. + * + * @psalm-suppress InvalidReturnStatement + * @psalm-suppress InvalidReturnType + * + * @param VCard $entry + * @return array|string + */ + private function name($entry) + { + if ($this->hasFirstnameInN($entry)) { + $parts = $entry->N->getParts(); + + $name = ''; + if (! empty(Arr::get($parts, '1'))) { + $name .= $this->formatValue($parts[1]); + } + if (! empty(Arr::get($parts, '2'))) { + $name .= ' '.$this->formatValue($parts[2]); + } + if (! empty(Arr::get($parts, '0'))) { + $name .= ' '.$this->formatValue($parts[0]); + } + $name .= ' '.$this->formatValue($entry->EMAIL); + } elseif ($this->hasNICKNAME($entry)) { + $name = $this->formatValue($entry->NICKNAME); + $name .= ' '.$this->formatValue($entry->EMAIL); + } elseif ($this->hasFN($entry)) { + $name = $this->formatValue($entry->FN); + $name .= ' '.$this->formatValue($entry->EMAIL); + } else { + $name = trans('settings.import_vcard_unknown_entry'); + } + + return $name; + } + + /** + * @param array $contactData + * @param VCard $entry + * @return array + */ + private function importFromN(array $contactData, VCard $entry): array + { + $parts = $entry->N->getParts(); + + $contactData['last_name'] = $this->formatValue(Arr::get($parts, '0')); + $contactData['first_name'] = $this->formatValue(Arr::get($parts, '1')); + $contactData['middle_name'] = $this->formatValue(Arr::get($parts, '2')); + // prefix [3] + // suffix [4] + + if (! empty($entry->NICKNAME)) { + $contactData['nickname'] = $this->formatValue($entry->NICKNAME); + } + + return $contactData; + } + + /** + * @param array $contactData + * @param VCard $entry + * @return array + */ + private function importFromNICKNAME(array $contactData, VCard $entry): array + { + $contactData['first_name'] = $this->formatValue($entry->NICKNAME); + + return $contactData; + } + + /** + * @param array $contactData + * @param VCard $entry + * @return array + */ + private function importFromFN(array $contactData, VCard $entry): array + { + $fullnameParts = preg_split('/\s+/', $entry->FN, 2); + + $user = User::where('account_id', $this->accountId) + ->findOrFail($this->userId); + + if (FormHelper::getNameOrderForForms($user) === 'firstname') { + $contactData['first_name'] = $this->formatValue($fullnameParts[0]); + if (count($fullnameParts) > 1) { + $contactData['last_name'] = $this->formatValue($fullnameParts[1]); + } + } elseif (count($fullnameParts) > 1) { + $contactData['last_name'] = $this->formatValue($fullnameParts[0]); + $contactData['first_name'] = $this->formatValue($fullnameParts[1]); + } else { + $contactData['first_name'] = $this->formatValue($fullnameParts[0]); + } + + if (! empty($entry->NICKNAME)) { + $contactData['nickname'] = $this->formatValue($entry->NICKNAME); + } + + return $contactData; + } + + /** + * Import uid of the contact. + * + * @param array $contactData + * @param VCard $entry + * @return array + */ + private function importUid(array $contactData, VCard $entry): array + { + if (! empty($uuid = (string) $entry->UID) && Uuid::isValid($uuid)) { + $contactData['uuid'] = $uuid; + } + + return $contactData; + } + + /** + * Import gender of the contact. + * + * @param array $contactData + * @param VCard $entry + * @return array + */ + private function importGender(array $contactData, VCard $entry): array + { + if ($entry->GENDER) { + $contactData['gender_id'] = $this->getGender((string) $entry->GENDER)->id; + } + + return $contactData; + } + + /** + * Import photo of the contact. + * + * @param Contact $contact + * @param VCard $entry + * @return void + */ + private function importPhoto(Contact $contact, VCard $entry): void + { + if ($entry->PHOTO) { + if (Str::startsWith((string) $entry->PHOTO, 'https://secure.gravatar.com') || Str::startsWith((string) $entry->PHOTO, 'https://www.gravatar.com')) { + // Gravatar + $contact->avatar_gravatar_url = (string) $entry->PHOTO; + } elseif (! Str::startsWith((string) $entry->PHOTO, 'https://') + && ! Str::startsWith((string) $entry->PHOTO, 'http://') + && ($contact->avatar_source != 'photo' || empty($contact->avatar_photo_id))) { + // Import photo image + // Skipping in case a photo avatar is already set + + $array = [ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'data' => (string) $entry->PHOTO, + ]; + if (! is_null($entry->PHOTO['TYPE'])) { + /** @var \Sabre\VObject\Parameter */ + $type = $entry->PHOTO['TYPE']; + $array['extension'] = $type->getValue(); + } + + try { + $photo = app(UploadPhoto::class) + ->execute($array); + if (! $photo) { + return; + } + + app(UpdateAvatar::class)->execute([ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'source' => 'photo', + 'photo_id' => $photo->id, + ]); + } catch (ValidationException $e) { + // wrong data + Log::error(__CLASS__.' '.__FUNCTION__.': ERROR when UploadPhoto: '.implode(', ', $e->validator->errors()->all()).', PHOTO='.$array['data'], [ + 'data' => $array, + 'contact_id' => $contact->id, + $e, + ]); + } + } + } + } + + /** + * @param Contact $contact + * @param VCard $entry + * @return void + */ + private function importWorkInformation(Contact $contact, VCard $entry): void + { + $request = [ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'author_id' => $this->userId, + ]; + + if ($entry->ORG) { + $parts = $entry->ORG->getParts(); + if ($company = Arr::get($parts, '0')) { + $request['company'] = $this->formatValue($company); + } + } + + if ($entry->ROLE) { + $request['job'] = $this->formatValue($entry->ROLE); + } + + if ($entry->TITLE) { + $request['job'] = $this->formatValue($entry->TITLE); + } + + if (array_key_exists('job', $request) || array_key_exists('company', $request)) { + app(UpdateWorkInformation::class)->execute($request); + } + } + + /** + * @param array $contactData + * @param VCard $entry + * @return array + */ + private function importBirthday(array $contactData, VCard $entry): array + { + if ($entry->BDAY && ! empty((string) $entry->BDAY)) { + $bday = (string) $entry->BDAY; + $is_year_unknown = false; + + if (Str::startsWith($bday, '--')) { + $bday = '0'.substr($bday, 1); + $is_year_unknown = true; + } + + $birthdate = null; + try { + $birthdate = DateHelper::parseDate($bday); + } catch (\Exception $e) { + // catch any date parse exception + } + + if (! is_null($birthdate)) { + $contactData['is_birthdate_known'] = true; + $contactData['birthdate_is_age_based'] = false; + $contactData['birthdate_day'] = $birthdate->day; + $contactData['birthdate_month'] = $birthdate->month; + $contactData['birthdate_year'] = $is_year_unknown ? null : $birthdate->year; + $contactData['birthdate_add_reminder'] = true; + $contactData['is_deceased'] = false; + } + } + + return $contactData; + } + + /** + * @param Contact $contact + * @param VCard $entry + * @return void + */ + private function importAddress(Contact $contact, VCard $entry): void + { + if (! $entry->ADR) { + return; + } + + $addresses = $contact->addresses() + ->get() + ->sortBy('id'); + + foreach ($entry->ADR as $adr) { + $parts = $adr->getParts(); + $addressContent = [ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'street' => $this->formatValue(Arr::get($parts, '2')), + 'city' => $this->formatValue(Arr::get($parts, '3')), + 'province' => $this->formatValue(Arr::get($parts, '4')), + 'postal_code' => $this->formatValue(Arr::get($parts, '5')), + 'country' => CountriesHelper::find(Arr::get($parts, '6')), + 'labels' => preg_split('/,/', (string) $adr['TYPE']), + ]; + + // We assume addresses are in the same order + $address = $addresses->shift(); + + if (is_null($address)) { + // Address does not exist + app(CreateAddress::class)->execute($addressContent); + } else { + // Address has to be updated + $address = app(UpdateAddress::class)->execute([ + 'address_id' => $address->id, + 'name' => $address->name, + ] + + $addressContent + ); + } + } + + foreach ($addresses as $address) { + // Remaining addresses have to be removed + app(DestroyAddress::class)->execute([ + 'account_id' => $contact->account_id, + 'address_id' => $address->id, + ]); + } + } + + /** + * @param Contact $contact + * @param VCard $entry + * @return void + */ + private function importEmail(Contact $contact, VCard $entry): void + { + if (is_null($entry->EMAIL)) { + return; + } + + $contactFieldTypeId = $this->getContactFieldTypeId(ContactFieldType::EMAIL); + if (! $contactFieldTypeId) { + // Case of contact field type email does not exist + return; + } + + $emails = $contact->contactFields() + ->email() + ->get() + ->sortBy('id'); + + foreach ($entry->EMAIL as $email) { + $contactFieldContent = [ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'contact_field_type_id' => $contactFieldTypeId, + 'data' => $this->formatValue((string) $email), + 'labels' => preg_split('/,/', (string) $email['TYPE']), + ]; + + // We assume contact fields are in the same order + $contactField = $emails->shift(); + + if (is_null($contactField)) { + // Address does not exist + app(CreateContactField::class)->execute($contactFieldContent); + } else { + // Address has to be updated + app(UpdateContactField::class)->execute([ + 'contact_field_id' => $contactField->id, + ] + + $contactFieldContent + ); + } + } + + foreach ($emails as $email) { + // Remaining emails have to be removed + app(DestroyContactField::class)->execute([ + 'account_id' => $contact->account_id, + 'contact_field_id' => $email->id, + ]); + } + } + + /** + * @param Contact $contact + * @param VCard $entry + * @return void + */ + private function importNote(Contact $contact, VCard $entry): void + { + if (is_null($entry->NOTE)) { + return; + } + + $note = Note::create([ + 'contact_id' => $contact->id, + 'account_id' => $contact->account_id, + 'body' => $entry->NOTE, + ]); + } + + /** + * @param Contact $contact + * @param VCard $entry + * @return void + */ + private function importTel(Contact $contact, VCard $entry): void + { + if (is_null($entry->TEL)) { + return; + } + + $contactFieldTypeId = $this->getContactFieldTypeId(ContactFieldType::PHONE); + if (! $contactFieldTypeId) { + // Case of contact field type phone does not exist + return; + } + + $phones = $contact->contactFields() + ->phone() + ->get() + ->sortBy('id'); + + $countryISO = VCardHelper::getCountryISOFromSabreVCard($entry); + + foreach ($entry->TEL as $tel) { + $data = (string) $tel; + $data = LocaleHelper::formatTelephoneNumberByISO($data, $countryISO, Str::startsWith($data, '+') ? \libphonenumber\PhoneNumberFormat::INTERNATIONAL : \libphonenumber\PhoneNumberFormat::NATIONAL); + + $contactFieldContent = [ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'contact_field_type_id' => $contactFieldTypeId, + 'data' => $this->formatValue($data), + 'labels' => preg_split('/,/', (string) $tel['TYPE']), + ]; + + // We assume contact fields are in the same order + $phone = $phones->shift(); + + if (is_null($phone)) { + // Address does not exist + app(CreateContactField::class)->execute($contactFieldContent); + } else { + // Address has to be updated + app(UpdateContactField::class)->execute([ + 'contact_field_id' => $phone->id, + ] + + $contactFieldContent + ); + } + } + + foreach ($phones as $phone) { + // Remaining phones have to be removed + app(DestroyContactField::class)->execute([ + 'account_id' => $contact->account_id, + 'contact_field_id' => $phone->id, + ]); + } + } + + /** + * @param Contact $contact + * @param VCard $entry + * @return void + */ + private function importSocialProfile(Contact $contact, VCard $entry): void + { + if (is_null($entry->socialProfile)) { + return; + } + + foreach ($entry->socialProfile as $socialProfile) { + $type = $socialProfile['type']; + $contactFieldTypeId = null; + $data = null; + switch ((string) $type) { + case 'facebook': + $contactFieldTypeId = $this->getContactFieldTypeId('Facebook'); + $data = str_replace('https://www.facebook.com/', '', $this->formatValue((string) $socialProfile)); + break; + case 'twitter': + $contactFieldTypeId = $this->getContactFieldTypeId('Twitter'); + $data = str_replace('https://twitter.com/', '', $this->formatValue((string) $socialProfile)); + break; + case 'whatsapp': + $contactFieldTypeId = $this->getContactFieldTypeId('Whatsapp'); + $data = str_replace('https://wa.me/', '', $this->formatValue((string) $socialProfile)); + break; + case 'telegram': + $contactFieldTypeId = $this->getContactFieldTypeId('Telegram'); + $data = str_replace('http://t.me/', '', $this->formatValue((string) $socialProfile)); + break; + case 'linkedin': + $contactFieldTypeId = $this->getContactFieldTypeId('LinkedIn'); + $data = str_replace('http://www.linkedin.com/in/', '', $this->formatValue((string) $socialProfile)); + break; + default: + // Not supported + break; + } + + if (! is_null($contactFieldTypeId) && ! is_null($data)) { + ContactField::firstOrCreate([ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'data' => $data, + 'contact_field_type_id' => $contactFieldTypeId, + ]); + } + } + } + + /** + * Get the contact field type id for the $type. + * + * @param string $type The type of the ContactFieldType, or the name + * @return int|null + */ + private function getContactFieldTypeId(string $type) + { + if (! Arr::has($this->contactFields, $type)) { + $contactFieldType = ContactFieldType::where([ + 'account_id' => $this->accountId, + 'type' => $type, + ])->first(); + + if (is_null($contactFieldType)) { + $contactFieldType = ContactFieldType::where([ + 'account_id' => $this->accountId, + 'name' => $type, + ])->first(); + } + + Arr::set($this->contactFields, $type, $contactFieldType != null ? $contactFieldType->id : null); + } + + return Arr::get($this->contactFields, $type); + } + + /** + * Import the categories as tags. + * + * @param Contact $contact + * @param VCard $entry + * @return void + */ + private function importCategories(Contact $contact, VCard $entry) + { + $tags = []; + foreach ($contact->tags as $tag) { + $tags[$tag->name] = $tag->id; + } + + if (! is_null($entry->CATEGORIES)) { + $categories = preg_split('/,/', $entry->CATEGORIES); + + foreach ($categories as $category) { + $name = (string) $category; + if (isset($tags[$name])) { + unset($tags[$name]); + } else { + app(AssociateTag::class)->execute([ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'name' => $name, + ]); + } + } + } + + foreach ($tags as $tag) { + app(DetachTag::class)->execute([ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'tag_id' => $tag, + ]); + } + } +} diff --git a/app/Traits/AmountFormatter.php b/app/Traits/AmountFormatter.php new file mode 100644 index 0000000..b86d455 --- /dev/null +++ b/app/Traits/AmountFormatter.php @@ -0,0 +1,73 @@ +belongsTo(Currency::class); + } + + /** + * Set exchange value. + * + * @return void + */ + public function setAmountAttribute($value) + { + $this->attributes['amount'] = MoneyHelper::parseInput($value, $this->currency); + } + + /** + * Get exchange value. + * + * @return string|null + */ + public function getAmountAttribute(): ?string + { + if (! ($amount = Arr::get($this->attributes, 'amount', null))) { + return null; + } + + return MoneyHelper::exchangeValue($amount, $this->currency); + } + + /** + * Get value of amount (without currency). + * + * @return string + */ + public function getValueAttribute(): string + { + if (! ($amount = Arr::get($this->attributes, 'amount', null))) { + return ''; + } + + return MoneyHelper::getValue($amount, $this->currency); + } + + /** + * Get display value: amount with currency. + * + * @return string + */ + public function getDisplayValueAttribute(): string + { + if (! ($amount = Arr::get($this->attributes, 'amount', null))) { + return ''; + } + + return MoneyHelper::format($amount, $this->currency); + } +} diff --git a/app/Traits/DAVFormat.php b/app/Traits/DAVFormat.php new file mode 100644 index 0000000..de768a4 --- /dev/null +++ b/app/Traits/DAVFormat.php @@ -0,0 +1,17 @@ +attributes['uuid']) || empty($this->attributes['uuid']) || $this->attributes['uuid'] == null) { + return (string) tap(Str::uuid()->toString(), function ($uuid) { + $this->forceFill([ + 'uuid' => $uuid, + ]); + $this->save(['timestamps' => false]); + }); + } + + return (string) $this->attributes['uuid']; + } +} diff --git a/app/Traits/Hasher.php b/app/Traits/Hasher.php new file mode 100644 index 0000000..9f276db --- /dev/null +++ b/app/Traits/Hasher.php @@ -0,0 +1,37 @@ +encodeId(parent::getRouteKey()); + } + + public function resolveRouteBinding($value, $field = null): ?Model + { + $id = $this->decodeId($value); + + return parent::resolveRouteBinding($id, $field); + } + + protected function decodeId($value) + { + return app(IdHasher::class)->decodeId($value); + } + + public function hashID() + { + return $this->getRouteKey(); + } +} diff --git a/app/Traits/Journalable.php b/app/Traits/Journalable.php new file mode 100644 index 0000000..6699a8a --- /dev/null +++ b/app/Traits/Journalable.php @@ -0,0 +1,46 @@ +morphMany(JournalEntry::class, 'journalable'); + } + + /** + * Get the journal record associated. + * + * @return MorphOne + */ + public function journalEntry() + { + return $this->morphOne(JournalEntry::class, 'journalable'); + } + + /** + * Delete the Journal Entry associated with the given object. + * + * @return bool + */ + public function deleteJournalEntry() + { + if ($this->journalEntry) { + $this->journalEntry->delete(); + + return true; + } + + return false; + } +} diff --git a/app/Traits/JsonRespondController.php b/app/Traits/JsonRespondController.php new file mode 100644 index 0000000..2d3a287 --- /dev/null +++ b/app/Traits/JsonRespondController.php @@ -0,0 +1,193 @@ +httpStatusCode; + } + + /** + * Set HTTP status code of the response. + * + * @param int $statusCode + * @return self + */ + public function setHTTPStatusCode($statusCode) + { + $this->httpStatusCode = $statusCode; + + return $this; + } + + /** + * Get error code of the response. + * + * @return int + */ + public function getErrorCode() + { + return $this->errorCode; + } + + /** + * Set error code of the response. + * + * @param int $errorCode + * @return self + */ + public function setErrorCode($errorCode) + { + $this->errorCode = $errorCode; + + return $this; + } + + /** + * Sends a JSON to the consumer. + * + * @param array $data + * @param array $headers [description] + * @return JsonResponse + */ + public function respond($data, $headers = []) + { + return response()->json($data, $this->getHTTPStatusCode(), $headers); + } + + /** + * Sends a response not found (404) to the request. + * Error Code = 31. + * + * @return JsonResponse + */ + public function respondNotFound() + { + return $this->setHTTPStatusCode(404) + ->setErrorCode(31) + ->respondWithError(); + } + + /** + * Sends an error when the validator failed. + * Error Code = 32. + * + * @param Validator $validator + * @return JsonResponse + */ + public function respondValidatorFailed(Validator $validator) + { + return $this->setHTTPStatusCode(422) + ->setErrorCode(32) + ->respondWithError($validator->errors()->all()); + } + + /** + * Sends an error when the query didn't have the right parameters for + * creating an object. + * Error Code = 33. + * + * @param string $message + * @return JsonResponse + */ + public function respondNotTheRightParameters($message = null) + { + return $this->setHTTPStatusCode(500) + ->setErrorCode(33) + ->respondWithError($message); + } + + /** + * Sends a response invalid query (http 500) to the request. + * Error Code = 40. + * + * @param string $message + * @return JsonResponse + */ + public function respondInvalidQuery($message = null) + { + return $this->setHTTPStatusCode(500) + ->setErrorCode(40) + ->respondWithError($message); + } + + /** + * Sends an error when the query contains invalid parameters. + * Error Code = 41. + * + * @param string $message + * @return JsonResponse + */ + public function respondInvalidParameters($message = null) + { + return $this->setHTTPStatusCode(422) + ->setErrorCode(41) + ->respondWithError($message); + } + + /** + * Sends a response unauthorized (401) to the request. + * Error Code = 42. + * + * @param string $message + * @return JsonResponse + */ + public function respondUnauthorized($message = null) + { + return $this->setHTTPStatusCode(401) + ->setErrorCode(42) + ->respondWithError($message); + } + + /** + * Sends a response with error. + * + * @param string|array $message + * @return JsonResponse + */ + public function respondWithError($message = null) + { + return $this->respond([ + 'error' => [ + 'message' => $message ?? config('api.error_codes.'.$this->getErrorCode()), + 'error_code' => $this->getErrorCode(), + ], + ]); + } + + /** + * Sends a response that the object has been deleted, and also indicates + * the id of the object that has been deleted. + * + * @param int $id + * @return JsonResponse + */ + public function respondObjectDeleted($id) + { + return $this->respond([ + 'deleted' => true, + 'id' => $id, + ]); + } +} diff --git a/app/Traits/Searchable.php b/app/Traits/Searchable.php new file mode 100644 index 0000000..ee22ff8 --- /dev/null +++ b/app/Traits/Searchable.php @@ -0,0 +1,76 @@ +searchable_columns == null) { + return null; + } + + $searchableColumns = array_map(function ($column) { + return DBHelper::getTable($this->getTable()).".`$column`"; + }, $this->searchable_columns); + + $queryString = $this->buildQuery($searchableColumns, $needle); + + $builder->whereRaw(DBHelper::getTable($this->getTable()).".`account_id` = $accountId") + ->whereRaw("( $queryString )") + ->orderBy($orderByColumn, $orderByDirection); + + if ($sortOrder) { + $builder->sortedBy($sortOrder); + } + + $builder->select(array_map(function ($column) { + return "{$this->getTable()}.$column"; + }, $this->return_from_search)); + + return $builder; + } + + /** + * Build a query based on the array that contains column names. + * + * @param array $array + * @param string $searchTerm + * @return string + */ + private function buildQuery(array $array, string $searchTerm): string + { + $first = true; + $queryString = ''; + $searchTerms = explode(' ', $searchTerm); + + foreach ($searchTerms as $searchTerm) { + $searchTerm = DBHelper::connection()->getPdo()->quote('%'.$searchTerm.'%'); + + foreach ($array as $column) { + if ($first) { + $first = false; + } else { + $queryString .= ' OR '; + } + $queryString .= $column.' LIKE '.$searchTerm; + } + } + + return $queryString; + } +} diff --git a/app/Traits/StripeCall.php b/app/Traits/StripeCall.php new file mode 100644 index 0000000..32ec472 --- /dev/null +++ b/app/Traits/StripeCall.php @@ -0,0 +1,57 @@ +getJsonBody(); + $err = $body['error']; + $errorMessage = trans('settings.stripe_error_card', ['message' => $err['message']]); + Log::error(__CLASS__.' '.__FUNCTION__.': Stripe card decline error: '.$e->getMessage(), ['body' => $e->getJsonBody(), $e]); + } catch (\Stripe\Exception\RateLimitException $e) { + // Too many requests made to the API too quickly + $errorMessage = trans('settings.stripe_error_rate_limit'); + Log::error(__CLASS__.' '.__FUNCTION__.': Stripe RateLimit error: '.$e->getMessage(), ['body' => $e->getJsonBody(), $e]); + } catch (\Stripe\Exception\InvalidRequestException $e) { + // Invalid parameters were supplied to Stripe's API + $errorMessage = trans('settings.stripe_error_invalid_request'); + Log::error(__CLASS__.' '.__FUNCTION__.': Stripe InvalidRequest error: '.$e->getMessage(), ['body' => $e->getJsonBody(), $e]); + } catch (\Stripe\Exception\AuthenticationException $e) { + // Authentication with Stripe's API failed + // (maybe you changed API keys recently) + $errorMessage = trans('settings.stripe_error_authentication'); + Log::error(__CLASS__.' '.__FUNCTION__.': Stripe Authentication error: '.$e->getMessage(), ['body' => $e->getJsonBody(), $e]); + } catch (\Stripe\Exception\ApiConnectionException $e) { + // Network communication with Stripe failed + $errorMessage = trans('settings.stripe_error_api_connection_error'); + Log::error(__CLASS__.' '.__FUNCTION__.': Stripe ApiConnection error: '.$e->getMessage(), ['body' => $e->getJsonBody(), $e]); + } catch (\Stripe\Exception\ApiErrorException $e) { + $errorMessage = $e->getMessage(); + Log::error(__CLASS__.' '.__FUNCTION__.': Stripe error: '.$e->getMessage(), ['body' => $e->getJsonBody(), $e]); + } catch (\Laravel\Cashier\Exceptions\IncompletePayment $e) { + throw $e; + } catch (\Exception $e) { + $errorMessage = $e->getMessage(); + Log::error(__CLASS__.' '.__FUNCTION__.': Stripe error: '.$e->getMessage(), [$e]); + } + + throw new StripeException($errorMessage); + } +} diff --git a/app/Traits/Subscription.php b/app/Traits/Subscription.php new file mode 100644 index 0000000..3d0d2b1 --- /dev/null +++ b/app/Traits/Subscription.php @@ -0,0 +1,146 @@ +stripeCall(function () use ($payment_method, $plan) { + $this->newSubscription($plan['name'], $plan['id']) + ->create($payment_method, [ + 'email' => auth()->user()->email, + ]); + + return true; + }); + } + + /** + * Update an existing subscription. + * + * @param string $planName + * @param \Laravel\Cashier\Subscription $subscription + * @return \Laravel\Cashier\Subscription + */ + public function updateSubscription(string $planName, \Laravel\Cashier\Subscription $subscription) + { + $oldPlan = $subscription->stripe_price; + $plan = InstanceHelper::getPlanInformationFromConfig($planName); + if ($plan === null) { + abort(404); + } + + if ($oldPlan === $planName) { + // No change + return $subscription; + } + + $subscription = $this->stripeCall(function () use ($subscription, $plan) { + return $subscription->swap($plan['id']); + }); + + if ($subscription->stripe_price !== $oldPlan && $subscription->stripe_price === $plan['id']) { + $subscription->forceFill([ + 'name' => $plan['name'], + ])->save(); + } + + return $subscription; + } + + /** + * Check if the account is currently subscribed to a plan. + * + * @return bool + */ + public function isSubscribed() + { + if ($this->has_access_to_paid_version_for_free) { + return true; + } + + return $this->getSubscribedPlan() !== null; + } + + /** + * Get the subscription the account is subscribed to. + * + * @return \Laravel\Cashier\Subscription|null + */ + public function getSubscribedPlan() + { + return $this->subscriptions()->recurring()->first(); + } + + /** + * Get the id of the plan the account is subscribed to. + * + * @return string + */ + public function getSubscribedPlanId() + { + $plan = $this->getSubscribedPlan(); + + return is_null($plan) ? '' : $plan->stripe_price; + } + + /** + * Get the friendly name of the plan the account is subscribed to. + * + * @return string|null + */ + public function getSubscribedPlanName(): ?string + { + $plan = $this->getSubscribedPlan(); + + return is_null($plan) ? null : $plan->name; + } + + /** + * Cancel the plan the account is subscribed to. + * + * @return bool|string + */ + public function subscriptionCancel() + { + $plan = $this->getSubscribedPlan(); + + if (! is_null($plan)) { + return $this->stripeCall(function () use ($plan) { + $plan->cancelNow(); + + return true; + }); + } + + return false; + } + + /** + * Check if the account has invoices linked to this account. + * + * @return bool + */ + public function hasInvoices() + { + return $this->subscriptions()->count() > 0; + } +} diff --git a/app/Traits/WithUser.php b/app/Traits/WithUser.php new file mode 100644 index 0000000..52e097a --- /dev/null +++ b/app/Traits/WithUser.php @@ -0,0 +1,26 @@ +user = $user; + + return $this; + } +} diff --git a/app/ViewHelpers/ContactHelper.php b/app/ViewHelpers/ContactHelper.php new file mode 100644 index 0000000..fe844af --- /dev/null +++ b/app/ViewHelpers/ContactHelper.php @@ -0,0 +1,41 @@ +action); + + $logsCollection->push([ + 'author_name' => ($log->author) ? $log->author->name : $log->author_name, + 'description' => $description, + 'audited_at' => DateHelper::getShortDateWithTime($log->audited_at), + ]); + } + + return $logsCollection; + } +} diff --git a/artisan b/artisan new file mode 100644 index 0000000..f80e641 --- /dev/null +++ b/artisan @@ -0,0 +1,53 @@ +#!/usr/bin/env php +make(Illuminate\Contracts\Console\Kernel::class); + +$status = $kernel->handle( + $input = new Symfony\Component\Console\Input\ArgvInput, + new Symfony\Component\Console\Output\ConsoleOutput +); + +/* +|-------------------------------------------------------------------------- +| Shutdown The Application +|-------------------------------------------------------------------------- +| +| Once Artisan has finished running. We will fire off the shutdown events +| so that any final work may be done by the application before we shut +| down the process. This is the last thing to happen to the request. +| +*/ + +$kernel->terminate($input, $status); + +exit($status); diff --git a/bootstrap/app.php b/bootstrap/app.php new file mode 100644 index 0000000..f2801ad --- /dev/null +++ b/bootstrap/app.php @@ -0,0 +1,55 @@ +singleton( + Illuminate\Contracts\Http\Kernel::class, + App\Http\Kernel::class +); + +$app->singleton( + Illuminate\Contracts\Console\Kernel::class, + App\Console\Kernel::class +); + +$app->singleton( + Illuminate\Contracts\Debug\ExceptionHandler::class, + App\Exceptions\Handler::class +); + +/* +|-------------------------------------------------------------------------- +| Return The Application +|-------------------------------------------------------------------------- +| +| This script returns the application instance. The instance is given to +| the calling script so we can separate the building of the instances +| from the actual running of the application and sending responses. +| +*/ + +return $app; diff --git a/bootstrap/cache/.gitignore b/bootstrap/cache/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/bootstrap/cache/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..2cc26a9 --- /dev/null +++ b/composer.json @@ -0,0 +1,149 @@ +{ + "name": "djaiss/monica", + "type": "project", + "description": "Monica is a personal CRM.", + "keywords": [ + "prm", + "crm", + "social" + ], + "license": "AGPL", + "require": { + "php": "^8.1", + "ext-bcmath": "*", + "ext-gd": "*", + "ext-gmp": "*", + "ext-intl": "*", + "ext-redis": "*", + "asbiin/laravel-adorable": "^1.0", + "asbiin/laravel-webauthn": "^4.0", + "bacon/bacon-qr-code": "^2.0", + "creativeorange/gravatar": "^1.0", + "doctrine/dbal": "^3.0", + "erusev/parsedown": "^1.7", + "giggsey/libphonenumber-for-php": "^8.9", + "guzzlehttp/guzzle": "^7.2", + "guzzlehttp/psr7": "^2.1", + "intervention/image": "^2.3", + "laravel/cashier": "^13.0", + "laravel/framework": "^9.0", + "laravel/passport": "^11.0", + "laravel/socialite": "^5.0", + "laravel/ui": "^4.0", + "laravolt/avatar": "^4.0", + "lcobucci/clock": "^3.0.0", + "league/flysystem-aws-s3-v3": "^3.0", + "mariuzzo/laravel-js-localization": "^1.7", + "matriphe/iso-639": "^1.0", + "moneyphp/money": "^4.0", + "monicahq/laravel-cloudflare": "^3.0", + "monicahq/laravel-sabre": "^1.2", + "ok/ipstack-client": "^2.0", + "phar-io/version": "^3.1", + "pragmarx/google2fa": "^8.0", + "pragmarx/google2fa-laravel": "^2.0", + "pragmarx/random": "^0", + "predis/predis": "^2.0", + "rinvex/countries": "^8.1", + "sabre/dav": "^4.0", + "sentry/sentry-laravel": "^2.0", + "spatie/macroable": "^2.0", + "stevebauman/location": "^6.1", + "symfony/http-client": "^6.0", + "symfony/mailgun-mailer": "^6.0", + "symfony/translation": "^6.0", + "thecodingmachine/safe": "^2.0", + "vectorface/whip": "^0.4", + "vinkla/hashids": "^10.0", + "vluzrmos/language-detector": "^2.2", + "web-token/jwt-key-mgmt": "^3.0", + "web-token/jwt-signature-algorithm-ecdsa": "^3.0", + "web-token/jwt-signature-algorithm-eddsa": "^3.0", + "web-token/jwt-signature-algorithm-rsa": "^3.0", + "werk365/etagconditionals": "dev-master", + "xantios/mimey": "^2.0" + }, + "require-dev": { + "barryvdh/laravel-debugbar": "^3", + "fakerphp/faker": "^1.10", + "khanamiryan/qrcode-detector-decoder": "^2.0", + "laravel/dusk": "^7.11", + "laravel/legacy-factories": "^1.0", + "laravel/tinker": "^2.6", + "matthiasnoback/live-code-coverage": "^1", + "mockery/mockery": "^1.0", + "nunomaduro/collision": "^6.1", + "nunomaduro/larastan": "^2.2", + "phpunit/phpcov": "^8.0", + "phpunit/phpunit": "^9.0", + "psalm/plugin-laravel": "^2.0", + "roave/security-advisories": "dev-master", + "spatie/laravel-ignition": "^1.0", + "thecodingmachine/phpstan-safe-rule": "^1.0", + "vimeo/psalm": "^5.5" + }, + "suggest": { + "ext-apcu": "*" + }, + "config": { + "apcu-autoloader": true, + "optimize-autoloader": true, + "preferred-install": "dist", + "sort-packages": true, + "allow-plugins": { + "composer/package-versions-deprecated": true, + "php-http/discovery": true + } + }, + "extra": { + "laravel": { + "dont-discover": [] + } + }, + "autoload": { + "psr-4": { + "App\\": "app/" + }, + "classmap": [ + "database/seeds", + "database/factories" + ], + "files": [ + "app/Helpers/helpers.php" + ] + }, + "autoload-dev": { + "classmap": [ + "tests/TestCase.php" + ], + "psr-4": { + "Tests\\": "tests/", + "Database\\Factories\\": "database/factories/", + "Database\\Seeders\\": "database/seeders/" + } + }, + "scripts": { + "pre-install-cmd": [ + "App\\Helpers\\ComposerScripts::preInstall" + ], + "post-install-cmd": [ + "Illuminate\\Foundation\\ComposerScripts::postInstall" + ], + "pre-update-cmd": [ + "App\\Helpers\\ComposerScripts::preUpdate" + ], + "post-update-cmd": [ + "Illuminate\\Foundation\\ComposerScripts::postUpdate" + ], + "post-autoload-dump": [ + "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", + "@php artisan package:discover" + ], + "post-root-package-install": [ + "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" + ], + "post-create-project-cmd": [ + "@php artisan key:generate" + ] + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..1d786f5 --- /dev/null +++ b/composer.lock @@ -0,0 +1,19684 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "cf135499df1575ba5a5747a5fcf4289c", + "packages": [ + { + "name": "asbiin/laravel-adorable", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/asbiin/laravel-adorable.git", + "reference": "fabfa239bacbce8895e9e1017c16465b8efef5f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/asbiin/laravel-adorable/zipball/fabfa239bacbce8895e9e1017c16465b8efef5f9", + "reference": "fabfa239bacbce8895e9e1017c16465b8efef5f9", + "shasum": "" + }, + "require": { + "illuminate/support": "^8.0 || ^9.0 || ^10.0 || ^11.0", + "intervention/image": "^2.7", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "larastan/larastan": "^1.0 || ^2.0", + "mockery/mockery": "^1.4", + "orchestra/testbench": "^6.0 || ^7.0 || ^8.0 || ^9.0", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^9.5 || ^10.0 || ^11.0", + "psalm/plugin-laravel": "^2.0", + "vimeo/psalm": "^4.0 || ^5.6" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "LaravelAdorable": "LaravelAdorable\\Facades\\LaravelAdorable" + }, + "providers": [ + "LaravelAdorable\\LaravelAdorableServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "LaravelAdorable\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alexis Saettler", + "email": "alexis@saettler.org" + } + ], + "description": "Generate an Adorable Avatar for Laravel", + "keywords": [ + "adorable", + "avatar", + "laravel", + "php" + ], + "support": { + "issues": "https://github.com/monicahq/laravel-adorable/issues", + "source": "https://github.com/monicahq/laravel-adorable" + }, + "funding": [ + { + "url": "https://github.com/asbiin", + "type": "github" + } + ], + "time": "2024-03-13T19:55:01+00:00" + }, + { + "name": "asbiin/laravel-webauthn", + "version": "4.4.1", + "source": { + "type": "git", + "url": "https://github.com/asbiin/laravel-webauthn.git", + "reference": "a47afc4bf20e9fd63749f08d6fa877748fe49faf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/asbiin/laravel-webauthn/zipball/a47afc4bf20e9fd63749f08d6fa877748fe49faf", + "reference": "a47afc4bf20e9fd63749f08d6fa877748fe49faf", + "shasum": "" + }, + "require": { + "illuminate/support": "^9.0 || ^10.0 || ^11.0", + "php": ">=8.1", + "phpdocumentor/reflection-docblock": "^5.3", + "psr/http-factory-implementation": "1.0", + "symfony/property-access": "^6.4 || ^7.0", + "symfony/property-info": "^6.4 || ^7.0", + "symfony/serializer": "^6.4 || ^7.0", + "web-auth/cose-lib": "^4.0", + "web-auth/webauthn-lib": "^4.8", + "web-token/jwt-library": "^3.0" + }, + "conflict": { + "web-auth/webauthn-lib": "4.7.0" + }, + "require-dev": { + "ext-sqlite3": "*", + "guzzlehttp/psr7": "^2.1", + "jschaedl/composer-git-hooks": "^4.0", + "larastan/larastan": "^2.0", + "laravel/legacy-factories": "^1.0", + "laravel/pint": "^1.13", + "ocramius/package-versions": "^2.0", + "orchestra/testbench": "^7.0 || ^8.0 || ^9.0", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.0", + "phpstan/phpstan-strict-rules": "^1.0", + "phpunit/phpunit": "^9.5 || ^10.0 || ^11.0", + "psalm/plugin-laravel": "^2.8" + }, + "suggest": { + "guzzlehttp/psr7": "To provide a psr/http-factory-implementation implementation", + "php-http/discovery": "To find a psr/http-factory-implementation implementation", + "psr/http-client-implementation": "Required for the AndroidSafetyNet Attestation Statement support", + "symfony/psr-http-message-bridge": "To find a psr/http-factory-implementation implementation" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "LaravelWebauthn\\WebauthnServiceProvider" + ] + }, + "hooks": { + "config": { + "stop-on-failure": [ + "pre-commit" + ] + }, + "pre-commit": [ + "files=$(git diff --staged --name-only);\"$(dirname \"$0\")/../../vendor/bin/pint\" $files; git add $files" + ] + } + }, + "autoload": { + "psr-4": { + "LaravelWebauthn\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alexis Saettler", + "email": "alexis@saettler.org" + } + ], + "description": "Laravel Webauthn support", + "keywords": [ + "laravel", + "php", + "security", + "webauthn" + ], + "support": { + "issues": "https://github.com/asbiin/laravel-webauthn/issues", + "source": "https://github.com/asbiin/laravel-webauthn" + }, + "funding": [ + { + "url": "https://github.com/asbiin", + "type": "github" + } + ], + "time": "2024-04-08T20:50:21+00:00" + }, + { + "name": "aws/aws-crt-php", + "version": "v1.2.5", + "source": { + "type": "git", + "url": "https://github.com/awslabs/aws-crt-php.git", + "reference": "0ea1f04ec5aa9f049f97e012d1ed63b76834a31b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/0ea1f04ec5aa9f049f97e012d1ed63b76834a31b", + "reference": "0ea1f04ec5aa9f049f97e012d1ed63b76834a31b", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35||^5.6.3||^9.5", + "yoast/phpunit-polyfills": "^1.0" + }, + "suggest": { + "ext-awscrt": "Make sure you install awscrt native extension to use any of the functionality." + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "AWS SDK Common Runtime Team", + "email": "aws-sdk-common-runtime@amazon.com" + } + ], + "description": "AWS Common Runtime for PHP", + "homepage": "https://github.com/awslabs/aws-crt-php", + "keywords": [ + "amazon", + "aws", + "crt", + "sdk" + ], + "support": { + "issues": "https://github.com/awslabs/aws-crt-php/issues", + "source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.5" + }, + "time": "2024-04-19T21:30:56+00:00" + }, + { + "name": "aws/aws-sdk-php", + "version": "3.305.7", + "source": { + "type": "git", + "url": "https://github.com/aws/aws-sdk-php.git", + "reference": "f4108b0222fdc0f0d96c5dbc8055b957d06f1cae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/f4108b0222fdc0f0d96c5dbc8055b957d06f1cae", + "reference": "f4108b0222fdc0f0d96c5dbc8055b957d06f1cae", + "shasum": "" + }, + "require": { + "aws/aws-crt-php": "^1.2.3", + "ext-json": "*", + "ext-pcre": "*", + "ext-simplexml": "*", + "guzzlehttp/guzzle": "^6.5.8 || ^7.4.5", + "guzzlehttp/promises": "^1.4.0 || ^2.0", + "guzzlehttp/psr7": "^1.9.1 || ^2.4.5", + "mtdowling/jmespath.php": "^2.6", + "php": ">=7.2.5", + "psr/http-message": "^1.0 || ^2.0" + }, + "require-dev": { + "andrewsville/php-token-reflection": "^1.4", + "aws/aws-php-sns-message-validator": "~1.0", + "behat/behat": "~3.0", + "composer/composer": "^1.10.22", + "dms/phpunit-arraysubset-asserts": "^0.4.0", + "doctrine/cache": "~1.4", + "ext-dom": "*", + "ext-openssl": "*", + "ext-pcntl": "*", + "ext-sockets": "*", + "nette/neon": "^2.3", + "paragonie/random_compat": ">= 2", + "phpunit/phpunit": "^5.6.3 || ^8.5 || ^9.5", + "psr/cache": "^1.0", + "psr/simple-cache": "^1.0", + "sebastian/comparator": "^1.2.3 || ^4.0", + "yoast/phpunit-polyfills": "^1.0" + }, + "suggest": { + "aws/aws-php-sns-message-validator": "To validate incoming SNS notifications", + "doctrine/cache": "To use the DoctrineCacheAdapter", + "ext-curl": "To send requests using cURL", + "ext-openssl": "Allows working with CloudFront private distributions and verifying received SNS messages", + "ext-sockets": "To use client-side monitoring" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Aws\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Amazon Web Services", + "homepage": "http://aws.amazon.com" + } + ], + "description": "AWS SDK for PHP - Use Amazon Web Services in your PHP project", + "homepage": "http://aws.amazon.com/sdkforphp", + "keywords": [ + "amazon", + "aws", + "cloud", + "dynamodb", + "ec2", + "glacier", + "s3", + "sdk" + ], + "support": { + "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80", + "issues": "https://github.com/aws/aws-sdk-php/issues", + "source": "https://github.com/aws/aws-sdk-php/tree/3.305.7" + }, + "time": "2024-05-01T18:05:51+00:00" + }, + { + "name": "bacon/bacon-qr-code", + "version": "2.0.8", + "source": { + "type": "git", + "url": "https://github.com/Bacon/BaconQrCode.git", + "reference": "8674e51bb65af933a5ffaf1c308a660387c35c22" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/8674e51bb65af933a5ffaf1c308a660387c35c22", + "reference": "8674e51bb65af933a5ffaf1c308a660387c35c22", + "shasum": "" + }, + "require": { + "dasprid/enum": "^1.0.3", + "ext-iconv": "*", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "phly/keep-a-changelog": "^2.1", + "phpunit/phpunit": "^7 | ^8 | ^9", + "spatie/phpunit-snapshot-assertions": "^4.2.9", + "squizlabs/php_codesniffer": "^3.4" + }, + "suggest": { + "ext-imagick": "to generate QR code images" + }, + "type": "library", + "autoload": { + "psr-4": { + "BaconQrCode\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Ben Scholzen 'DASPRiD'", + "email": "mail@dasprids.de", + "homepage": "https://dasprids.de/", + "role": "Developer" + } + ], + "description": "BaconQrCode is a QR code generator for PHP.", + "homepage": "https://github.com/Bacon/BaconQrCode", + "support": { + "issues": "https://github.com/Bacon/BaconQrCode/issues", + "source": "https://github.com/Bacon/BaconQrCode/tree/2.0.8" + }, + "time": "2022-12-07T17:46:57+00:00" + }, + { + "name": "brick/math", + "version": "0.11.0", + "source": { + "type": "git", + "url": "https://github.com/brick/math.git", + "reference": "0ad82ce168c82ba30d1c01ec86116ab52f589478" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/brick/math/zipball/0ad82ce168c82ba30d1c01ec86116ab52f589478", + "reference": "0ad82ce168c82ba30d1c01ec86116ab52f589478", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.2", + "phpunit/phpunit": "^9.0", + "vimeo/psalm": "5.0.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Brick\\Math\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Arbitrary-precision arithmetic library", + "keywords": [ + "Arbitrary-precision", + "BigInteger", + "BigRational", + "arithmetic", + "bigdecimal", + "bignum", + "brick", + "math" + ], + "support": { + "issues": "https://github.com/brick/math/issues", + "source": "https://github.com/brick/math/tree/0.11.0" + }, + "funding": [ + { + "url": "https://github.com/BenMorel", + "type": "github" + } + ], + "time": "2023-01-15T23:15:59+00:00" + }, + { + "name": "carbonphp/carbon-doctrine-types", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon-doctrine-types.git", + "reference": "99f76ffa36cce3b70a4a6abce41dba15ca2e84cb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon-doctrine-types/zipball/99f76ffa36cce3b70a4a6abce41dba15ca2e84cb", + "reference": "99f76ffa36cce3b70a4a6abce41dba15ca2e84cb", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "doctrine/dbal": "<3.7.0 || >=4.0.0" + }, + "require-dev": { + "doctrine/dbal": "^3.7.0", + "nesbot/carbon": "^2.71.0 || ^3.0.0", + "phpunit/phpunit": "^10.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Carbon\\Doctrine\\": "src/Carbon/Doctrine/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "KyleKatarn", + "email": "kylekatarnls@gmail.com" + } + ], + "description": "Types to use Carbon in Doctrine", + "keywords": [ + "carbon", + "date", + "datetime", + "doctrine", + "time" + ], + "support": { + "issues": "https://github.com/CarbonPHP/carbon-doctrine-types/issues", + "source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/2.1.0" + }, + "funding": [ + { + "url": "https://github.com/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/nesbot/carbon", + "type": "tidelift" + } + ], + "time": "2023-12-11T17:09:12+00:00" + }, + { + "name": "clue/stream-filter", + "version": "v1.7.0", + "source": { + "type": "git", + "url": "https://github.com/clue/stream-filter.git", + "reference": "049509fef80032cb3f051595029ab75b49a3c2f7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/clue/stream-filter/zipball/049509fef80032cb3f051595029ab75b49a3c2f7", + "reference": "049509fef80032cb3f051595029ab75b49a3c2f7", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "Clue\\StreamFilter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + } + ], + "description": "A simple and modern approach to stream filtering in PHP", + "homepage": "https://github.com/clue/stream-filter", + "keywords": [ + "bucket brigade", + "callback", + "filter", + "php_user_filter", + "stream", + "stream_filter_append", + "stream_filter_register" + ], + "support": { + "issues": "https://github.com/clue/stream-filter/issues", + "source": "https://github.com/clue/stream-filter/tree/v1.7.0" + }, + "funding": [ + { + "url": "https://clue.engineering/support", + "type": "custom" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2023-12-20T15:40:13+00:00" + }, + { + "name": "composer/ca-bundle", + "version": "1.5.0", + "source": { + "type": "git", + "url": "https://github.com/composer/ca-bundle.git", + "reference": "0c5ccfcfea312b5c5a190a21ac5cef93f74baf99" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/0c5ccfcfea312b5c5a190a21ac5cef93f74baf99", + "reference": "0c5ccfcfea312b5c5a190a21ac5cef93f74baf99", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "ext-pcre": "*", + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.10", + "psr/log": "^1.0", + "symfony/phpunit-bridge": "^4.2 || ^5", + "symfony/process": "^4.0 || ^5.0 || ^6.0 || ^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\CaBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Lets you find a path to the system CA bundle, and includes a fallback to the Mozilla CA bundle.", + "keywords": [ + "cabundle", + "cacert", + "certificate", + "ssl", + "tls" + ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/ca-bundle/issues", + "source": "https://github.com/composer/ca-bundle/tree/1.5.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-03-15T14:00:32+00:00" + }, + { + "name": "creativeorange/gravatar", + "version": "v1.0.24", + "source": { + "type": "git", + "url": "https://github.com/creativeorange/gravatar.git", + "reference": "ec0d78c7d4ef6d66c3cc09b6a03ab8d53e44379f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/creativeorange/gravatar/zipball/ec0d78c7d4ef6d66c3cc09b6a03ab8d53e44379f", + "reference": "ec0d78c7d4ef6d66c3cc09b6a03ab8d53e44379f", + "shasum": "" + }, + "require": { + "illuminate/support": "^5|^6|^7|^8|^9|^10.0|^11.0", + "php": ">=5.4.0" + }, + "require-dev": { + "nunomaduro/larastan": "^0.6.2|^2.4", + "orchestra/testbench": "^5.4|^8.0|^9.0", + "php": ">=7.2" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Creativeorange\\Gravatar\\GravatarServiceProvider" + ], + "aliases": { + "Gravatar": "Creativeorange\\Gravatar\\Facades\\Gravatar" + } + } + }, + "autoload": { + "psr-4": { + "Creativeorange\\Gravatar\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaco Tijssen", + "email": "jaco@creativeorange.nl", + "homepage": "https://www.creativeorange.nl", + "role": "Developer" + } + ], + "description": "A Laravel Gravatar package for retrieving gravatar image URLs or checking the existance of an image.", + "keywords": [ + "avatar", + "gravatar", + "laravel" + ], + "support": { + "issues": "https://github.com/creativeorange/gravatar/issues", + "source": "https://github.com/creativeorange/gravatar/tree/v1.0.24" + }, + "time": "2024-02-28T09:23:55+00:00" + }, + { + "name": "dasprid/enum", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/DASPRiD/Enum.git", + "reference": "6faf451159fb8ba4126b925ed2d78acfce0dc016" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/6faf451159fb8ba4126b925ed2d78acfce0dc016", + "reference": "6faf451159fb8ba4126b925ed2d78acfce0dc016", + "shasum": "" + }, + "require": { + "php": ">=7.1 <9.0" + }, + "require-dev": { + "phpunit/phpunit": "^7 | ^8 | ^9", + "squizlabs/php_codesniffer": "*" + }, + "type": "library", + "autoload": { + "psr-4": { + "DASPRiD\\Enum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Ben Scholzen 'DASPRiD'", + "email": "mail@dasprids.de", + "homepage": "https://dasprids.de/", + "role": "Developer" + } + ], + "description": "PHP 7.1 enum implementation", + "keywords": [ + "enum", + "map" + ], + "support": { + "issues": "https://github.com/DASPRiD/Enum/issues", + "source": "https://github.com/DASPRiD/Enum/tree/1.0.5" + }, + "time": "2023-08-25T16:18:39+00:00" + }, + { + "name": "defuse/php-encryption", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/defuse/php-encryption.git", + "reference": "f53396c2d34225064647a05ca76c1da9d99e5828" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/defuse/php-encryption/zipball/f53396c2d34225064647a05ca76c1da9d99e5828", + "reference": "f53396c2d34225064647a05ca76c1da9d99e5828", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "paragonie/random_compat": ">= 2", + "php": ">=5.6.0" + }, + "require-dev": { + "phpunit/phpunit": "^5|^6|^7|^8|^9|^10", + "yoast/phpunit-polyfills": "^2.0.0" + }, + "bin": [ + "bin/generate-defuse-key" + ], + "type": "library", + "autoload": { + "psr-4": { + "Defuse\\Crypto\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Hornby", + "email": "taylor@defuse.ca", + "homepage": "https://defuse.ca/" + }, + { + "name": "Scott Arciszewski", + "email": "info@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "Secure PHP Encryption Library", + "keywords": [ + "aes", + "authenticated encryption", + "cipher", + "crypto", + "cryptography", + "encrypt", + "encryption", + "openssl", + "security", + "symmetric key cryptography" + ], + "support": { + "issues": "https://github.com/defuse/php-encryption/issues", + "source": "https://github.com/defuse/php-encryption/tree/v2.4.0" + }, + "time": "2023-06-19T06:10:36+00:00" + }, + { + "name": "dflydev/dot-access-data", + "version": "v3.0.2", + "source": { + "type": "git", + "url": "https://github.com/dflydev/dflydev-dot-access-data.git", + "reference": "f41715465d65213d644d3141a6a93081be5d3549" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/f41715465d65213d644d3141a6a93081be5d3549", + "reference": "f41715465d65213d644d3141a6a93081be5d3549", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.42", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.3", + "scrutinizer/ocular": "1.6.0", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^4.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Dflydev\\DotAccessData\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Dragonfly Development Inc.", + "email": "info@dflydev.com", + "homepage": "http://dflydev.com" + }, + { + "name": "Beau Simensen", + "email": "beau@dflydev.com", + "homepage": "http://beausimensen.com" + }, + { + "name": "Carlos Frutos", + "email": "carlos@kiwing.it", + "homepage": "https://github.com/cfrutos" + }, + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com" + } + ], + "description": "Given a deep data structure, access data by dot notation.", + "homepage": "https://github.com/dflydev/dflydev-dot-access-data", + "keywords": [ + "access", + "data", + "dot", + "notation" + ], + "support": { + "issues": "https://github.com/dflydev/dflydev-dot-access-data/issues", + "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.2" + }, + "time": "2022-10-27T11:44:00+00:00" + }, + { + "name": "doctrine/cache", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/cache.git", + "reference": "1ca8f21980e770095a31456042471a57bc4c68fb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/cache/zipball/1ca8f21980e770095a31456042471a57bc4c68fb", + "reference": "1ca8f21980e770095a31456042471a57bc4c68fb", + "shasum": "" + }, + "require": { + "php": "~7.1 || ^8.0" + }, + "conflict": { + "doctrine/common": ">2.2,<2.4" + }, + "require-dev": { + "cache/integration-tests": "dev-master", + "doctrine/coding-standard": "^9", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "psr/cache": "^1.0 || ^2.0 || ^3.0", + "symfony/cache": "^4.4 || ^5.4 || ^6", + "symfony/var-exporter": "^4.4 || ^5.4 || ^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Cache\\": "lib/Doctrine/Common/Cache" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Cache library is a popular cache implementation that supports many different drivers such as redis, memcache, apc, mongodb and others.", + "homepage": "https://www.doctrine-project.org/projects/cache.html", + "keywords": [ + "abstraction", + "apcu", + "cache", + "caching", + "couchdb", + "memcached", + "php", + "redis", + "xcache" + ], + "support": { + "issues": "https://github.com/doctrine/cache/issues", + "source": "https://github.com/doctrine/cache/tree/2.2.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcache", + "type": "tidelift" + } + ], + "time": "2022-05-20T20:07:39+00:00" + }, + { + "name": "doctrine/dbal", + "version": "3.8.4", + "source": { + "type": "git", + "url": "https://github.com/doctrine/dbal.git", + "reference": "b05e48a745f722801f55408d0dbd8003b403dbbd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/b05e48a745f722801f55408d0dbd8003b403dbbd", + "reference": "b05e48a745f722801f55408d0dbd8003b403dbbd", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2", + "doctrine/cache": "^1.11|^2.0", + "doctrine/deprecations": "^0.5.3|^1", + "doctrine/event-manager": "^1|^2", + "php": "^7.4 || ^8.0", + "psr/cache": "^1|^2|^3", + "psr/log": "^1|^2|^3" + }, + "require-dev": { + "doctrine/coding-standard": "12.0.0", + "fig/log-test": "^1", + "jetbrains/phpstorm-stubs": "2023.1", + "phpstan/phpstan": "1.10.58", + "phpstan/phpstan-strict-rules": "^1.5", + "phpunit/phpunit": "9.6.16", + "psalm/plugin-phpunit": "0.18.4", + "slevomat/coding-standard": "8.13.1", + "squizlabs/php_codesniffer": "3.9.0", + "symfony/cache": "^5.4|^6.0|^7.0", + "symfony/console": "^4.4|^5.4|^6.0|^7.0", + "vimeo/psalm": "4.30.0" + }, + "suggest": { + "symfony/console": "For helpful console commands such as SQL execution and import of files." + }, + "bin": [ + "bin/doctrine-dbal" + ], + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\DBAL\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + } + ], + "description": "Powerful PHP database abstraction layer (DBAL) with many features for database schema introspection and management.", + "homepage": "https://www.doctrine-project.org/projects/dbal.html", + "keywords": [ + "abstraction", + "database", + "db2", + "dbal", + "mariadb", + "mssql", + "mysql", + "oci8", + "oracle", + "pdo", + "pgsql", + "postgresql", + "queryobject", + "sasql", + "sql", + "sqlite", + "sqlserver", + "sqlsrv" + ], + "support": { + "issues": "https://github.com/doctrine/dbal/issues", + "source": "https://github.com/doctrine/dbal/tree/3.8.4" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fdbal", + "type": "tidelift" + } + ], + "time": "2024-04-25T07:04:44+00:00" + }, + { + "name": "doctrine/deprecations", + "version": "1.1.3", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "dfbaa3c2d2e9a9df1118213f3b8b0c597bb99fab" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/dfbaa3c2d2e9a9df1118213f3b8b0c597bb99fab", + "reference": "dfbaa3c2d2e9a9df1118213f3b8b0c597bb99fab", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9", + "phpstan/phpstan": "1.4.10 || 1.10.15", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "psalm/plugin-phpunit": "0.18.4", + "psr/log": "^1 || ^2 || ^3", + "vimeo/psalm": "4.30.0 || 5.12.0" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "lib/Doctrine/Deprecations" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/1.1.3" + }, + "time": "2024-01-30T19:34:25+00:00" + }, + { + "name": "doctrine/event-manager", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/event-manager.git", + "reference": "750671534e0241a7c50ea5b43f67e23eb5c96f32" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/event-manager/zipball/750671534e0241a7c50ea5b43f67e23eb5c96f32", + "reference": "750671534e0241a7c50ea5b43f67e23eb5c96f32", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "conflict": { + "doctrine/common": "<2.9" + }, + "require-dev": { + "doctrine/coding-standard": "^10", + "phpstan/phpstan": "^1.8.8", + "phpunit/phpunit": "^9.5", + "vimeo/psalm": "^4.28" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "The Doctrine Event Manager is a simple PHP event system that was built to be used with the various Doctrine projects.", + "homepage": "https://www.doctrine-project.org/projects/event-manager.html", + "keywords": [ + "event", + "event dispatcher", + "event manager", + "event system", + "events" + ], + "support": { + "issues": "https://github.com/doctrine/event-manager/issues", + "source": "https://github.com/doctrine/event-manager/tree/2.0.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fevent-manager", + "type": "tidelift" + } + ], + "time": "2022-10-12T20:59:15+00:00" + }, + { + "name": "doctrine/inflector", + "version": "2.0.10", + "source": { + "type": "git", + "url": "https://github.com/doctrine/inflector.git", + "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/5817d0659c5b50c9b950feb9af7b9668e2c436bc", + "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^11.0", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.1", + "phpstan/phpstan-strict-rules": "^1.3", + "phpunit/phpunit": "^8.5 || ^9.5", + "vimeo/psalm": "^4.25 || ^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Inflector\\": "lib/Doctrine/Inflector" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.", + "homepage": "https://www.doctrine-project.org/projects/inflector.html", + "keywords": [ + "inflection", + "inflector", + "lowercase", + "manipulation", + "php", + "plural", + "singular", + "strings", + "uppercase", + "words" + ], + "support": { + "issues": "https://github.com/doctrine/inflector/issues", + "source": "https://github.com/doctrine/inflector/tree/2.0.10" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector", + "type": "tidelift" + } + ], + "time": "2024-02-18T20:23:39+00:00" + }, + { + "name": "doctrine/lexer", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5", + "psalm/plugin-phpunit": "^0.18.3", + "vimeo/psalm": "^5.21" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/3.0.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], + "time": "2024-02-05T11:56:58+00:00" + }, + { + "name": "dompdf/dompdf", + "version": "v2.0.8", + "source": { + "type": "git", + "url": "https://github.com/dompdf/dompdf.git", + "reference": "c20247574601700e1f7c8dab39310fca1964dc52" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dompdf/dompdf/zipball/c20247574601700e1f7c8dab39310fca1964dc52", + "reference": "c20247574601700e1f7c8dab39310fca1964dc52", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "masterminds/html5": "^2.0", + "phenx/php-font-lib": ">=0.5.4 <1.0.0", + "phenx/php-svg-lib": ">=0.5.2 <1.0.0", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "ext-json": "*", + "ext-zip": "*", + "mockery/mockery": "^1.3", + "phpunit/phpunit": "^7.5 || ^8 || ^9", + "squizlabs/php_codesniffer": "^3.5" + }, + "suggest": { + "ext-gd": "Needed to process images", + "ext-gmagick": "Improves image processing performance", + "ext-imagick": "Improves image processing performance", + "ext-zlib": "Needed for pdf stream compression" + }, + "type": "library", + "autoload": { + "psr-4": { + "Dompdf\\": "src/" + }, + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1" + ], + "authors": [ + { + "name": "The Dompdf Community", + "homepage": "https://github.com/dompdf/dompdf/blob/master/AUTHORS.md" + } + ], + "description": "DOMPDF is a CSS 2.1 compliant HTML to PDF converter", + "homepage": "https://github.com/dompdf/dompdf", + "support": { + "issues": "https://github.com/dompdf/dompdf/issues", + "source": "https://github.com/dompdf/dompdf/tree/v2.0.8" + }, + "time": "2024-04-29T13:06:17+00:00" + }, + { + "name": "dragonmantank/cron-expression", + "version": "v3.3.3", + "source": { + "type": "git", + "url": "https://github.com/dragonmantank/cron-expression.git", + "reference": "adfb1f505deb6384dc8b39804c5065dd3c8c8c0a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/adfb1f505deb6384dc8b39804c5065dd3c8c8c0a", + "reference": "adfb1f505deb6384dc8b39804c5065dd3c8c8c0a", + "shasum": "" + }, + "require": { + "php": "^7.2|^8.0", + "webmozart/assert": "^1.0" + }, + "replace": { + "mtdowling/cron-expression": "^1.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-webmozart-assert": "^1.0", + "phpunit/phpunit": "^7.0|^8.0|^9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Cron\\": "src/Cron/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Tankersley", + "email": "chris@ctankersley.com", + "homepage": "https://github.com/dragonmantank" + } + ], + "description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due", + "keywords": [ + "cron", + "schedule" + ], + "support": { + "issues": "https://github.com/dragonmantank/cron-expression/issues", + "source": "https://github.com/dragonmantank/cron-expression/tree/v3.3.3" + }, + "funding": [ + { + "url": "https://github.com/dragonmantank", + "type": "github" + } + ], + "time": "2023-08-10T19:36:49+00:00" + }, + { + "name": "egulias/email-validator", + "version": "4.0.2", + "source": { + "type": "git", + "url": "https://github.com/egulias/EmailValidator.git", + "reference": "ebaaf5be6c0286928352e054f2d5125608e5405e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/ebaaf5be6c0286928352e054f2d5125608e5405e", + "reference": "ebaaf5be6c0286928352e054f2d5125608e5405e", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^2.0 || ^3.0", + "php": ">=8.1", + "symfony/polyfill-intl-idn": "^1.26" + }, + "require-dev": { + "phpunit/phpunit": "^10.2", + "vimeo/psalm": "^5.12" + }, + "suggest": { + "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Egulias\\EmailValidator\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eduardo Gulias Davis" + } + ], + "description": "A library for validating emails against several RFCs", + "homepage": "https://github.com/egulias/EmailValidator", + "keywords": [ + "email", + "emailvalidation", + "emailvalidator", + "validation", + "validator" + ], + "support": { + "issues": "https://github.com/egulias/EmailValidator/issues", + "source": "https://github.com/egulias/EmailValidator/tree/4.0.2" + }, + "funding": [ + { + "url": "https://github.com/egulias", + "type": "github" + } + ], + "time": "2023-10-06T06:47:41+00:00" + }, + { + "name": "erusev/parsedown", + "version": "1.7.4", + "source": { + "type": "git", + "url": "https://github.com/erusev/parsedown.git", + "reference": "cb17b6477dfff935958ba01325f2e8a2bfa6dab3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/erusev/parsedown/zipball/cb17b6477dfff935958ba01325f2e8a2bfa6dab3", + "reference": "cb17b6477dfff935958ba01325f2e8a2bfa6dab3", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35" + }, + "type": "library", + "autoload": { + "psr-0": { + "Parsedown": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Emanuil Rusev", + "email": "hello@erusev.com", + "homepage": "http://erusev.com" + } + ], + "description": "Parser for Markdown.", + "homepage": "http://parsedown.org", + "keywords": [ + "markdown", + "parser" + ], + "support": { + "issues": "https://github.com/erusev/parsedown/issues", + "source": "https://github.com/erusev/parsedown/tree/1.7.x" + }, + "time": "2019-12-30T22:54:17+00:00" + }, + { + "name": "firebase/php-jwt", + "version": "v6.10.0", + "source": { + "type": "git", + "url": "https://github.com/firebase/php-jwt.git", + "reference": "a49db6f0a5033aef5143295342f1c95521b075ff" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/a49db6f0a5033aef5143295342f1c95521b075ff", + "reference": "a49db6f0a5033aef5143295342f1c95521b075ff", + "shasum": "" + }, + "require": { + "php": "^7.4||^8.0" + }, + "require-dev": { + "guzzlehttp/guzzle": "^6.5||^7.4", + "phpspec/prophecy-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", + "psr/cache": "^1.0||^2.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0" + }, + "suggest": { + "ext-sodium": "Support EdDSA (Ed25519) signatures", + "paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present" + }, + "type": "library", + "autoload": { + "psr-4": { + "Firebase\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Neuman Vong", + "email": "neuman+pear@twilio.com", + "role": "Developer" + }, + { + "name": "Anant Narayanan", + "email": "anant@php.net", + "role": "Developer" + } + ], + "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", + "homepage": "https://github.com/firebase/php-jwt", + "keywords": [ + "jwt", + "php" + ], + "support": { + "issues": "https://github.com/firebase/php-jwt/issues", + "source": "https://github.com/firebase/php-jwt/tree/v6.10.0" + }, + "time": "2023-12-01T16:26:39+00:00" + }, + { + "name": "fruitcake/php-cors", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/fruitcake/php-cors.git", + "reference": "3d158f36e7875e2f040f37bc0573956240a5a38b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/3d158f36e7875e2f040f37bc0573956240a5a38b", + "reference": "3d158f36e7875e2f040f37bc0573956240a5a38b", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0", + "symfony/http-foundation": "^4.4|^5.4|^6|^7" + }, + "require-dev": { + "phpstan/phpstan": "^1.4", + "phpunit/phpunit": "^9", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2-dev" + } + }, + "autoload": { + "psr-4": { + "Fruitcake\\Cors\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fruitcake", + "homepage": "https://fruitcake.nl" + }, + { + "name": "Barryvdh", + "email": "barryvdh@gmail.com" + } + ], + "description": "Cross-origin resource sharing library for the Symfony HttpFoundation", + "homepage": "https://github.com/fruitcake/php-cors", + "keywords": [ + "cors", + "laravel", + "symfony" + ], + "support": { + "issues": "https://github.com/fruitcake/php-cors/issues", + "source": "https://github.com/fruitcake/php-cors/tree/v1.3.0" + }, + "funding": [ + { + "url": "https://fruitcake.nl", + "type": "custom" + }, + { + "url": "https://github.com/barryvdh", + "type": "github" + } + ], + "time": "2023-10-12T05:21:21+00:00" + }, + { + "name": "geoip2/geoip2", + "version": "v2.13.0", + "source": { + "type": "git", + "url": "https://github.com/maxmind/GeoIP2-php.git", + "reference": "6a41d8fbd6b90052bc34dff3b4252d0f88067b23" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/maxmind/GeoIP2-php/zipball/6a41d8fbd6b90052bc34dff3b4252d0f88067b23", + "reference": "6a41d8fbd6b90052bc34dff3b4252d0f88067b23", + "shasum": "" + }, + "require": { + "ext-json": "*", + "maxmind-db/reader": "~1.8", + "maxmind/web-service-common": "~0.8", + "php": ">=7.2" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "3.*", + "phpstan/phpstan": "*", + "phpunit/phpunit": "^8.0 || ^9.0", + "squizlabs/php_codesniffer": "3.*" + }, + "type": "library", + "autoload": { + "psr-4": { + "GeoIp2\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Gregory J. Oschwald", + "email": "goschwald@maxmind.com", + "homepage": "https://www.maxmind.com/" + } + ], + "description": "MaxMind GeoIP2 PHP API", + "homepage": "https://github.com/maxmind/GeoIP2-php", + "keywords": [ + "IP", + "geoip", + "geoip2", + "geolocation", + "maxmind" + ], + "support": { + "issues": "https://github.com/maxmind/GeoIP2-php/issues", + "source": "https://github.com/maxmind/GeoIP2-php/tree/v2.13.0" + }, + "time": "2022-08-05T20:32:58+00:00" + }, + { + "name": "giggsey/libphonenumber-for-php", + "version": "8.13.35", + "source": { + "type": "git", + "url": "https://github.com/giggsey/libphonenumber-for-php.git", + "reference": "cd52d7b27572ee45d31ca0d61b394638ed9a6bae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/giggsey/libphonenumber-for-php/zipball/cd52d7b27572ee45d31ca0d61b394638ed9a6bae", + "reference": "cd52d7b27572ee45d31ca0d61b394638ed9a6bae", + "shasum": "" + }, + "require": { + "giggsey/locale": "^1.7|^2.0", + "php": ">=5.3.2", + "symfony/polyfill-mbstring": "^1.17" + }, + "replace": { + "giggsey/libphonenumber-for-php-lite": "self.version" + }, + "require-dev": { + "pear/pear-core-minimal": "^1.9", + "pear/pear_exception": "^1.0", + "pear/versioncontrol_git": "^0.5", + "phing/phing": "^2.7", + "php-coveralls/php-coveralls": "^1.0|^2.0", + "symfony/console": "^2.8|^3.0|^v4.4|^v5.2", + "symfony/phpunit-bridge": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "8.x-dev" + } + }, + "autoload": { + "psr-4": { + "libphonenumber\\": "src/" + }, + "exclude-from-classmap": [ + "/src/data/", + "/src/carrier/data/", + "/src/geocoding/data/", + "/src/timezone/data/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Joshua Gigg", + "email": "giggsey@gmail.com", + "homepage": "https://giggsey.com/" + } + ], + "description": "PHP Port of Google's libphonenumber", + "homepage": "https://github.com/giggsey/libphonenumber-for-php", + "keywords": [ + "geocoding", + "geolocation", + "libphonenumber", + "mobile", + "phonenumber", + "validation" + ], + "support": { + "issues": "https://github.com/giggsey/libphonenumber-for-php/issues", + "source": "https://github.com/giggsey/libphonenumber-for-php" + }, + "time": "2024-04-19T12:41:30+00:00" + }, + { + "name": "giggsey/locale", + "version": "2.6", + "source": { + "type": "git", + "url": "https://github.com/giggsey/Locale.git", + "reference": "37874fa473131247c348059fb7b8985efc18b5ea" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/giggsey/Locale/zipball/37874fa473131247c348059fb7b8985efc18b5ea", + "reference": "37874fa473131247c348059fb7b8985efc18b5ea", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "require-dev": { + "ext-json": "*", + "pear/pear-core-minimal": "^1.9", + "pear/pear_exception": "^1.0", + "pear/versioncontrol_git": "^0.5", + "phing/phing": "^2.7", + "php-coveralls/php-coveralls": "^2.0", + "phpunit/phpunit": "^8.5|^9.5", + "symfony/console": "^5.0|^6.0", + "symfony/filesystem": "^5.0|^6.0", + "symfony/finder": "^5.0|^6.0", + "symfony/process": "^5.0|^6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Giggsey\\Locale\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Joshua Gigg", + "email": "giggsey@gmail.com", + "homepage": "https://giggsey.com/" + } + ], + "description": "Locale functions required by libphonenumber-for-php", + "support": { + "issues": "https://github.com/giggsey/Locale/issues", + "source": "https://github.com/giggsey/Locale/tree/2.6" + }, + "time": "2024-04-18T19:31:19+00:00" + }, + { + "name": "graham-campbell/manager", + "version": "v4.7.0", + "source": { + "type": "git", + "url": "https://github.com/GrahamCampbell/Laravel-Manager.git", + "reference": "b4cafa6491b9c92ecf7ce17521580050a27b8308" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/GrahamCampbell/Laravel-Manager/zipball/b4cafa6491b9c92ecf7ce17521580050a27b8308", + "reference": "b4cafa6491b9c92ecf7ce17521580050a27b8308", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^5.5 || ^6.0 || ^7.0 || ^8.0 || ^9.0", + "illuminate/support": "^5.5 || ^6.0 || ^7.0 || ^8.0 || ^9.0", + "php": "^7.1.3 || ^8.0" + }, + "require-dev": { + "graham-campbell/analyzer": "^2.4 || ^3.0", + "graham-campbell/testbench-core": "^3.4", + "mockery/mockery": "^1.3.1", + "phpunit/phpunit": "^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "GrahamCampbell\\Manager\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "Manager Provides Some Manager Functionality For Laravel", + "keywords": [ + "Graham Campbell", + "GrahamCampbell", + "Laravel Manager", + "Laravel-Manager", + "connector", + "framework", + "interface", + "laravel", + "manager" + ], + "support": { + "issues": "https://github.com/GrahamCampbell/Laravel-Manager/issues", + "source": "https://github.com/GrahamCampbell/Laravel-Manager/tree/v4.7.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/graham-campbell/manager", + "type": "tidelift" + } + ], + "time": "2022-01-24T01:59:19+00:00" + }, + { + "name": "graham-campbell/result-type", + "version": "v1.1.2", + "source": { + "type": "git", + "url": "https://github.com/GrahamCampbell/Result-Type.git", + "reference": "fbd48bce38f73f8a4ec8583362e732e4095e5862" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/fbd48bce38f73f8a4ec8583362e732e4095e5862", + "reference": "fbd48bce38f73f8a4ec8583362e732e4095e5862", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.2" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "GrahamCampbell\\ResultType\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "An Implementation Of The Result Type", + "keywords": [ + "Graham Campbell", + "GrahamCampbell", + "Result Type", + "Result-Type", + "result" + ], + "support": { + "issues": "https://github.com/GrahamCampbell/Result-Type/issues", + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.2" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type", + "type": "tidelift" + } + ], + "time": "2023-11-12T22:16:48+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "7.8.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "41042bc7ab002487b876a0683fc8dce04ddce104" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/41042bc7ab002487b876a0683fc8dce04ddce104", + "reference": "41042bc7ab002487b876a0683fc8dce04ddce104", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^1.5.3 || ^2.0.1", + "guzzlehttp/psr7": "^1.9.1 || ^2.5.1", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "php-http/client-integration-tests": "dev-master#2c025848417c1135031fdf9c728ee53d0a7ceaee as 3.0.999", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.36 || ^9.6.15", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.8.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2023-12-03T20:35:24+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "bbff78d96034045e58e13dedd6ad91b5d1253223" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/bbff78d96034045e58e13dedd6ad91b5d1253223", + "reference": "bbff78d96034045e58e13dedd6ad91b5d1253223", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.36 || ^9.6.15" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.0.2" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2023-12-03T20:19:20+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.6.2", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/45b30f99ac27b5ca93cb4831afe16285f57b8221", + "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "^0.9", + "phpunit/phpunit": "^8.5.36 || ^9.6.15" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.6.2" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2023-12-03T20:05:35+00:00" + }, + { + "name": "guzzlehttp/uri-template", + "version": "v1.0.3", + "source": { + "type": "git", + "url": "https://github.com/guzzle/uri-template.git", + "reference": "ecea8feef63bd4fef1f037ecb288386999ecc11c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/ecea8feef63bd4fef1f037ecb288386999ecc11c", + "reference": "ecea8feef63bd4fef1f037ecb288386999ecc11c", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "symfony/polyfill-php80": "^1.24" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.36 || ^9.6.15", + "uri-template/tests": "1.0.0" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\UriTemplate\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + } + ], + "description": "A polyfill class for uri_template of PHP", + "keywords": [ + "guzzlehttp", + "uri-template" + ], + "support": { + "issues": "https://github.com/guzzle/uri-template/issues", + "source": "https://github.com/guzzle/uri-template/tree/v1.0.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/uri-template", + "type": "tidelift" + } + ], + "time": "2023-12-03T19:50:20+00:00" + }, + { + "name": "hashids/hashids", + "version": "4.1.0", + "source": { + "type": "git", + "url": "https://github.com/vinkla/hashids.git", + "reference": "8cab111f78e0bd9c76953b082919fc9e251761be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vinkla/hashids/zipball/8cab111f78e0bd9c76953b082919fc9e251761be", + "reference": "8cab111f78e0bd9c76953b082919fc9e251761be", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^8.0 || ^9.4", + "squizlabs/php_codesniffer": "^3.5" + }, + "suggest": { + "ext-bcmath": "Required to use BC Math arbitrary precision mathematics (*).", + "ext-gmp": "Required to use GNU multiple precision mathematics (*)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.1-dev" + } + }, + "autoload": { + "psr-4": { + "Hashids\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ivan Akimov", + "email": "ivan@barreleye.com" + }, + { + "name": "Vincent Klaiber", + "email": "hello@doubledip.se" + } + ], + "description": "Generate short, unique, non-sequential ids (like YouTube and Bitly) from numbers", + "homepage": "https://hashids.org/php", + "keywords": [ + "bitly", + "decode", + "encode", + "hash", + "hashid", + "hashids", + "ids", + "obfuscate", + "youtube" + ], + "support": { + "issues": "https://github.com/vinkla/hashids/issues", + "source": "https://github.com/vinkla/hashids/tree/4.1.0" + }, + "time": "2020-11-26T19:24:33+00:00" + }, + { + "name": "http-interop/http-factory-guzzle", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/http-interop/http-factory-guzzle.git", + "reference": "8f06e92b95405216b237521cc64c804dd44c4a81" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/http-interop/http-factory-guzzle/zipball/8f06e92b95405216b237521cc64c804dd44c4a81", + "reference": "8f06e92b95405216b237521cc64c804dd44c4a81", + "shasum": "" + }, + "require": { + "guzzlehttp/psr7": "^1.7||^2.0", + "php": ">=7.3", + "psr/http-factory": "^1.0" + }, + "provide": { + "psr/http-factory-implementation": "^1.0" + }, + "require-dev": { + "http-interop/http-factory-tests": "^0.9", + "phpunit/phpunit": "^9.5" + }, + "suggest": { + "guzzlehttp/psr7": "Includes an HTTP factory starting in version 2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Factory\\Guzzle\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "An HTTP Factory using Guzzle PSR7", + "keywords": [ + "factory", + "http", + "psr-17", + "psr-7" + ], + "support": { + "issues": "https://github.com/http-interop/http-factory-guzzle/issues", + "source": "https://github.com/http-interop/http-factory-guzzle/tree/1.2.0" + }, + "time": "2021-07-21T13:50:14+00:00" + }, + { + "name": "intervention/image", + "version": "2.7.2", + "source": { + "type": "git", + "url": "https://github.com/Intervention/image.git", + "reference": "04be355f8d6734c826045d02a1079ad658322dad" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Intervention/image/zipball/04be355f8d6734c826045d02a1079ad658322dad", + "reference": "04be355f8d6734c826045d02a1079ad658322dad", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "guzzlehttp/psr7": "~1.1 || ^2.0", + "php": ">=5.4.0" + }, + "require-dev": { + "mockery/mockery": "~0.9.2", + "phpunit/phpunit": "^4.8 || ^5.7 || ^7.5.15" + }, + "suggest": { + "ext-gd": "to use GD library based image processing.", + "ext-imagick": "to use Imagick based image processing.", + "intervention/imagecache": "Caching extension for the Intervention Image library" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.4-dev" + }, + "laravel": { + "providers": [ + "Intervention\\Image\\ImageServiceProvider" + ], + "aliases": { + "Image": "Intervention\\Image\\Facades\\Image" + } + } + }, + "autoload": { + "psr-4": { + "Intervention\\Image\\": "src/Intervention/Image" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Oliver Vogel", + "email": "oliver@intervention.io", + "homepage": "https://intervention.io/" + } + ], + "description": "Image handling and manipulation library with support for Laravel integration", + "homepage": "http://image.intervention.io/", + "keywords": [ + "gd", + "image", + "imagick", + "laravel", + "thumbnail", + "watermark" + ], + "support": { + "issues": "https://github.com/Intervention/image/issues", + "source": "https://github.com/Intervention/image/tree/2.7.2" + }, + "funding": [ + { + "url": "https://paypal.me/interventionio", + "type": "custom" + }, + { + "url": "https://github.com/Intervention", + "type": "github" + } + ], + "time": "2022-05-21T17:30:32+00:00" + }, + { + "name": "jean85/pretty-package-versions", + "version": "2.0.6", + "source": { + "type": "git", + "url": "https://github.com/Jean85/pretty-package-versions.git", + "reference": "f9fdd29ad8e6d024f52678b570e5593759b550b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/f9fdd29ad8e6d024f52678b570e5593759b550b4", + "reference": "f9fdd29ad8e6d024f52678b570e5593759b550b4", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.0.0", + "php": "^7.1|^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.2", + "jean85/composer-provided-replaced-stub-package": "^1.0", + "phpstan/phpstan": "^1.4", + "phpunit/phpunit": "^7.5|^8.5|^9.4", + "vimeo/psalm": "^4.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Jean85\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alessandro Lai", + "email": "alessandro.lai85@gmail.com" + } + ], + "description": "A library to get pretty versions strings of installed dependencies", + "keywords": [ + "composer", + "package", + "release", + "versions" + ], + "support": { + "issues": "https://github.com/Jean85/pretty-package-versions/issues", + "source": "https://github.com/Jean85/pretty-package-versions/tree/2.0.6" + }, + "time": "2024-03-08T09:58:59+00:00" + }, + { + "name": "laravel/cashier", + "version": "v13.17.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/cashier-stripe.git", + "reference": "cae3a62e1819a0429ead6567ee26c049bf0c2677" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/cashier-stripe/zipball/cae3a62e1819a0429ead6567ee26c049bf0c2677", + "reference": "cae3a62e1819a0429ead6567ee26c049bf0c2677", + "shasum": "" + }, + "require": { + "dompdf/dompdf": "^1.2.1|^2.0", + "ext-json": "*", + "illuminate/console": "^8.37|^9.0|^10.0", + "illuminate/contracts": "^8.37|^9.0|^10.0", + "illuminate/database": "^8.37|^9.0|^10.0", + "illuminate/http": "^8.37|^9.0|^10.0", + "illuminate/log": "^8.37|^9.0|^10.0", + "illuminate/notifications": "^8.37|^9.0|^10.0", + "illuminate/routing": "^8.37|^9.0|^10.0", + "illuminate/support": "^8.37|^9.0|^10.0", + "illuminate/view": "^8.37|^9.0|^10.0", + "moneyphp/money": "^3.2|^4.0", + "nesbot/carbon": "^2.0", + "php": "^7.3|^8.0", + "stripe/stripe-php": "^7.39|^8.0|^9.0", + "symfony/http-kernel": "^5.0|^6.0", + "symfony/polyfill-intl-icu": "^1.22.1" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "orchestra/testbench": "^6.0|^7.0|^8.0", + "phpunit/phpunit": "^9.0" + }, + "suggest": { + "ext-intl": "Allows for more locales besides the default \"en\" when formatting money values." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "13.x-dev" + }, + "laravel": { + "providers": [ + "Laravel\\Cashier\\CashierServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Cashier\\": "src/", + "Laravel\\Cashier\\Database\\Factories\\": "database/factories/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Dries Vints", + "email": "dries@laravel.com" + } + ], + "description": "Laravel Cashier provides an expressive, fluent interface to Stripe's subscription billing services.", + "keywords": [ + "billing", + "laravel", + "stripe" + ], + "support": { + "issues": "https://github.com/laravel/cashier/issues", + "source": "https://github.com/laravel/cashier" + }, + "time": "2023-03-01T09:33:20+00:00" + }, + { + "name": "laravel/framework", + "version": "v9.52.16", + "source": { + "type": "git", + "url": "https://github.com/laravel/framework.git", + "reference": "082345d76fc6a55b649572efe10b11b03e279d24" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/framework/zipball/082345d76fc6a55b649572efe10b11b03e279d24", + "reference": "082345d76fc6a55b649572efe10b11b03e279d24", + "shasum": "" + }, + "require": { + "brick/math": "^0.9.3|^0.10.2|^0.11", + "doctrine/inflector": "^2.0.5", + "dragonmantank/cron-expression": "^3.3.2", + "egulias/email-validator": "^3.2.1|^4.0", + "ext-ctype": "*", + "ext-filter": "*", + "ext-hash": "*", + "ext-mbstring": "*", + "ext-openssl": "*", + "ext-session": "*", + "ext-tokenizer": "*", + "fruitcake/php-cors": "^1.2", + "guzzlehttp/uri-template": "^1.0", + "laravel/serializable-closure": "^1.2.2", + "league/commonmark": "^2.2.1", + "league/flysystem": "^3.8.0", + "monolog/monolog": "^2.0", + "nesbot/carbon": "^2.62.1", + "nunomaduro/termwind": "^1.13", + "php": "^8.0.2", + "psr/container": "^1.1.1|^2.0.1", + "psr/log": "^1.0|^2.0|^3.0", + "psr/simple-cache": "^1.0|^2.0|^3.0", + "ramsey/uuid": "^4.7", + "symfony/console": "^6.0.9", + "symfony/error-handler": "^6.0", + "symfony/finder": "^6.0", + "symfony/http-foundation": "^6.0", + "symfony/http-kernel": "^6.0", + "symfony/mailer": "^6.0", + "symfony/mime": "^6.0", + "symfony/process": "^6.0", + "symfony/routing": "^6.0", + "symfony/uid": "^6.0", + "symfony/var-dumper": "^6.0", + "tijsverkoyen/css-to-inline-styles": "^2.2.5", + "vlucas/phpdotenv": "^5.4.1", + "voku/portable-ascii": "^2.0" + }, + "conflict": { + "tightenco/collect": "<5.5.33" + }, + "provide": { + "psr/container-implementation": "1.1|2.0", + "psr/simple-cache-implementation": "1.0|2.0|3.0" + }, + "replace": { + "illuminate/auth": "self.version", + "illuminate/broadcasting": "self.version", + "illuminate/bus": "self.version", + "illuminate/cache": "self.version", + "illuminate/collections": "self.version", + "illuminate/conditionable": "self.version", + "illuminate/config": "self.version", + "illuminate/console": "self.version", + "illuminate/container": "self.version", + "illuminate/contracts": "self.version", + "illuminate/cookie": "self.version", + "illuminate/database": "self.version", + "illuminate/encryption": "self.version", + "illuminate/events": "self.version", + "illuminate/filesystem": "self.version", + "illuminate/hashing": "self.version", + "illuminate/http": "self.version", + "illuminate/log": "self.version", + "illuminate/macroable": "self.version", + "illuminate/mail": "self.version", + "illuminate/notifications": "self.version", + "illuminate/pagination": "self.version", + "illuminate/pipeline": "self.version", + "illuminate/queue": "self.version", + "illuminate/redis": "self.version", + "illuminate/routing": "self.version", + "illuminate/session": "self.version", + "illuminate/support": "self.version", + "illuminate/testing": "self.version", + "illuminate/translation": "self.version", + "illuminate/validation": "self.version", + "illuminate/view": "self.version" + }, + "require-dev": { + "ably/ably-php": "^1.0", + "aws/aws-sdk-php": "^3.235.5", + "doctrine/dbal": "^2.13.3|^3.1.4", + "ext-gmp": "*", + "fakerphp/faker": "^1.21", + "guzzlehttp/guzzle": "^7.5", + "league/flysystem-aws-s3-v3": "^3.0", + "league/flysystem-ftp": "^3.0", + "league/flysystem-path-prefixing": "^3.3", + "league/flysystem-read-only": "^3.3", + "league/flysystem-sftp-v3": "^3.0", + "mockery/mockery": "^1.5.1", + "orchestra/testbench-core": "^7.24", + "pda/pheanstalk": "^4.0", + "phpstan/phpdoc-parser": "^1.15", + "phpstan/phpstan": "^1.4.7", + "phpunit/phpunit": "^9.5.8", + "predis/predis": "^1.1.9|^2.0.2", + "symfony/cache": "^6.0", + "symfony/http-client": "^6.0" + }, + "suggest": { + "ably/ably-php": "Required to use the Ably broadcast driver (^1.0).", + "aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage, and SES mail driver (^3.235.5).", + "brianium/paratest": "Required to run tests in parallel (^6.0).", + "doctrine/dbal": "Required to rename columns and drop SQLite columns (^2.13.3|^3.1.4).", + "ext-apcu": "Required to use the APC cache driver.", + "ext-fileinfo": "Required to use the Filesystem class.", + "ext-ftp": "Required to use the Flysystem FTP driver.", + "ext-gd": "Required to use Illuminate\\Http\\Testing\\FileFactory::image().", + "ext-memcached": "Required to use the memcache cache driver.", + "ext-pcntl": "Required to use all features of the queue worker and console signal trapping.", + "ext-pdo": "Required to use all database features.", + "ext-posix": "Required to use all features of the queue worker.", + "ext-redis": "Required to use the Redis cache and queue drivers (^4.0|^5.0).", + "fakerphp/faker": "Required to use the eloquent factory builder (^1.9.1).", + "filp/whoops": "Required for friendly error pages in development (^2.14.3).", + "guzzlehttp/guzzle": "Required to use the HTTP Client and the ping methods on schedules (^7.5).", + "laravel/tinker": "Required to use the tinker console command (^2.0).", + "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.0).", + "league/flysystem-ftp": "Required to use the Flysystem FTP driver (^3.0).", + "league/flysystem-path-prefixing": "Required to use the scoped driver (^3.3).", + "league/flysystem-read-only": "Required to use read-only disks (^3.3)", + "league/flysystem-sftp-v3": "Required to use the Flysystem SFTP driver (^3.0).", + "mockery/mockery": "Required to use mocking (^1.5.1).", + "nyholm/psr7": "Required to use PSR-7 bridging features (^1.2).", + "pda/pheanstalk": "Required to use the beanstalk queue driver (^4.0).", + "phpunit/phpunit": "Required to use assertions and run tests (^9.5.8).", + "predis/predis": "Required to use the predis connector (^1.1.9|^2.0.2).", + "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", + "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0|^7.0).", + "symfony/cache": "Required to PSR-6 cache bridge (^6.0).", + "symfony/filesystem": "Required to enable support for relative symbolic links (^6.0).", + "symfony/http-client": "Required to enable support for the Symfony API mail transports (^6.0).", + "symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^6.0).", + "symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^6.0).", + "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^2.0)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.x-dev" + } + }, + "autoload": { + "files": [ + "src/Illuminate/Collections/helpers.php", + "src/Illuminate/Events/functions.php", + "src/Illuminate/Foundation/helpers.php", + "src/Illuminate/Support/helpers.php" + ], + "psr-4": { + "Illuminate\\": "src/Illuminate/", + "Illuminate\\Support\\": [ + "src/Illuminate/Macroable/", + "src/Illuminate/Collections/", + "src/Illuminate/Conditionable/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Laravel Framework.", + "homepage": "https://laravel.com", + "keywords": [ + "framework", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2023-10-03T13:02:30+00:00" + }, + { + "name": "laravel/passport", + "version": "v11.10.6", + "source": { + "type": "git", + "url": "https://github.com/laravel/passport.git", + "reference": "2642f360c51dfde3a6ea60f86ae5d9a8c0caf3cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/passport/zipball/2642f360c51dfde3a6ea60f86ae5d9a8c0caf3cf", + "reference": "2642f360c51dfde3a6ea60f86ae5d9a8c0caf3cf", + "shasum": "" + }, + "require": { + "ext-json": "*", + "firebase/php-jwt": "^6.4", + "illuminate/auth": "^9.0|^10.0", + "illuminate/console": "^9.0|^10.0", + "illuminate/container": "^9.0|^10.0", + "illuminate/contracts": "^9.0|^10.0", + "illuminate/cookie": "^9.0|^10.0", + "illuminate/database": "^9.0|^10.0", + "illuminate/encryption": "^9.0|^10.0", + "illuminate/http": "^9.0|^10.0", + "illuminate/support": "^9.0|^10.0", + "lcobucci/jwt": "^4.3|^5.0", + "league/oauth2-server": "^8.5.3", + "nyholm/psr7": "^1.5", + "php": "^8.0", + "phpseclib/phpseclib": "^2.0|^3.0", + "symfony/psr-http-message-bridge": "^2.1" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "orchestra/testbench": "^7.31|^8.11", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "11.x-dev" + }, + "laravel": { + "providers": [ + "Laravel\\Passport\\PassportServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Passport\\": "src/", + "Laravel\\Passport\\Database\\Factories\\": "database/factories/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Laravel Passport provides OAuth2 server support to Laravel.", + "keywords": [ + "laravel", + "oauth", + "passport" + ], + "support": { + "issues": "https://github.com/laravel/passport/issues", + "source": "https://github.com/laravel/passport" + }, + "time": "2024-03-01T11:11:18+00:00" + }, + { + "name": "laravel/serializable-closure", + "version": "v1.3.3", + "source": { + "type": "git", + "url": "https://github.com/laravel/serializable-closure.git", + "reference": "3dbf8a8e914634c48d389c1234552666b3d43754" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/3dbf8a8e914634c48d389c1234552666b3d43754", + "reference": "3dbf8a8e914634c48d389c1234552666b3d43754", + "shasum": "" + }, + "require": { + "php": "^7.3|^8.0" + }, + "require-dev": { + "nesbot/carbon": "^2.61", + "pestphp/pest": "^1.21.3", + "phpstan/phpstan": "^1.8.2", + "symfony/var-dumper": "^5.4.11" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\SerializableClosure\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "nuno@laravel.com" + } + ], + "description": "Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP.", + "keywords": [ + "closure", + "laravel", + "serializable" + ], + "support": { + "issues": "https://github.com/laravel/serializable-closure/issues", + "source": "https://github.com/laravel/serializable-closure" + }, + "time": "2023-11-08T14:08:06+00:00" + }, + { + "name": "laravel/socialite", + "version": "v5.13.2", + "source": { + "type": "git", + "url": "https://github.com/laravel/socialite.git", + "reference": "278d4615f68205722b3a129135774b3764b28a90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/socialite/zipball/278d4615f68205722b3a129135774b3764b28a90", + "reference": "278d4615f68205722b3a129135774b3764b28a90", + "shasum": "" + }, + "require": { + "ext-json": "*", + "firebase/php-jwt": "^6.4", + "guzzlehttp/guzzle": "^6.0|^7.0", + "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", + "illuminate/http": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", + "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", + "league/oauth1-client": "^1.10.1", + "php": "^7.2|^8.0", + "phpseclib/phpseclib": "^3.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "orchestra/testbench": "^4.0|^5.0|^6.0|^7.0|^8.0|^9.0", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^8.0|^9.3|^10.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + }, + "laravel": { + "providers": [ + "Laravel\\Socialite\\SocialiteServiceProvider" + ], + "aliases": { + "Socialite": "Laravel\\Socialite\\Facades\\Socialite" + } + } + }, + "autoload": { + "psr-4": { + "Laravel\\Socialite\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Laravel wrapper around OAuth 1 & OAuth 2 libraries.", + "homepage": "https://laravel.com", + "keywords": [ + "laravel", + "oauth" + ], + "support": { + "issues": "https://github.com/laravel/socialite/issues", + "source": "https://github.com/laravel/socialite" + }, + "time": "2024-04-26T13:48:16+00:00" + }, + { + "name": "laravel/ui", + "version": "v4.5.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/ui.git", + "reference": "a3562953123946996a503159199d6742d5534e61" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/ui/zipball/a3562953123946996a503159199d6742d5534e61", + "reference": "a3562953123946996a503159199d6742d5534e61", + "shasum": "" + }, + "require": { + "illuminate/console": "^9.21|^10.0|^11.0", + "illuminate/filesystem": "^9.21|^10.0|^11.0", + "illuminate/support": "^9.21|^10.0|^11.0", + "illuminate/validation": "^9.21|^10.0|^11.0", + "php": "^8.0", + "symfony/console": "^6.0|^7.0" + }, + "require-dev": { + "orchestra/testbench": "^7.35|^8.15|^9.0", + "phpunit/phpunit": "^9.3|^10.4|^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.x-dev" + }, + "laravel": { + "providers": [ + "Laravel\\Ui\\UiServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Ui\\": "src/", + "Illuminate\\Foundation\\Auth\\": "auth-backend/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Laravel UI utilities and presets.", + "keywords": [ + "laravel", + "ui" + ], + "support": { + "source": "https://github.com/laravel/ui/tree/v4.5.1" + }, + "time": "2024-03-21T18:12:29+00:00" + }, + { + "name": "laravolt/avatar", + "version": "4.1.7", + "source": { + "type": "git", + "url": "https://github.com/laravolt/avatar.git", + "reference": "2c11878524e19032793effa67f09df0682e5775b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravolt/avatar/zipball/2c11878524e19032793effa67f09df0682e5775b", + "reference": "2c11878524e19032793effa67f09df0682e5775b", + "shasum": "" + }, + "require": { + "illuminate/cache": "^6.0|^7.0|^8.0|^9.0", + "illuminate/support": "^6.0|^7.0|^8.0|^9.0", + "intervention/image": "^2.5", + "php": ">=7.3" + }, + "require-dev": { + "mockery/mockery": "~1.3", + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "~9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + }, + "laravel": { + "providers": [ + "Laravolt\\Avatar\\ServiceProvider" + ], + "aliases": { + "Avatar": "Laravolt\\Avatar\\Facade" + } + } + }, + "autoload": { + "psr-4": { + "Laravolt\\Avatar\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bayu Hendra Winata", + "email": "uyab.exe@gmail.com", + "homepage": "https://laravolt.dev", + "role": "Developer" + } + ], + "description": "Turn name, email, and any other string into initial-based avatar or gravatar.", + "homepage": "https://github.com/laravolt/avatar", + "keywords": [ + "avatar", + "gravatar", + "laravel", + "laravolt" + ], + "support": { + "issues": "https://github.com/laravolt/avatar/issues", + "source": "https://github.com/laravolt/avatar/tree/4.1.7" + }, + "funding": [ + { + "url": "https://paypal.me/bayuhendra", + "type": "custom" + }, + { + "url": "https://ko-fi.com/bayuhendra", + "type": "ko_fi" + }, + { + "url": "https://www.patreon.com/uyab", + "type": "patreon" + } + ], + "time": "2022-03-07T23:07:16+00:00" + }, + { + "name": "lcobucci/clock", + "version": "3.2.0", + "source": { + "type": "git", + "url": "https://github.com/lcobucci/clock.git", + "reference": "6f28b826ea01306b07980cb8320ab30b966cd715" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lcobucci/clock/zipball/6f28b826ea01306b07980cb8320ab30b966cd715", + "reference": "6f28b826ea01306b07980cb8320ab30b966cd715", + "shasum": "" + }, + "require": { + "php": "~8.2.0 || ~8.3.0", + "psr/clock": "^1.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "require-dev": { + "infection/infection": "^0.27", + "lcobucci/coding-standard": "^11.0.0", + "phpstan/extension-installer": "^1.3.1", + "phpstan/phpstan": "^1.10.25", + "phpstan/phpstan-deprecation-rules": "^1.1.3", + "phpstan/phpstan-phpunit": "^1.3.13", + "phpstan/phpstan-strict-rules": "^1.5.1", + "phpunit/phpunit": "^10.2.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Lcobucci\\Clock\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Luís Cobucci", + "email": "lcobucci@gmail.com" + } + ], + "description": "Yet another clock abstraction", + "support": { + "issues": "https://github.com/lcobucci/clock/issues", + "source": "https://github.com/lcobucci/clock/tree/3.2.0" + }, + "funding": [ + { + "url": "https://github.com/lcobucci", + "type": "github" + }, + { + "url": "https://www.patreon.com/lcobucci", + "type": "patreon" + } + ], + "time": "2023-11-17T17:00:27+00:00" + }, + { + "name": "lcobucci/jwt", + "version": "5.3.0", + "source": { + "type": "git", + "url": "https://github.com/lcobucci/jwt.git", + "reference": "08071d8d2c7f4b00222cc4b1fb6aa46990a80f83" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/lcobucci/jwt/zipball/08071d8d2c7f4b00222cc4b1fb6aa46990a80f83", + "reference": "08071d8d2c7f4b00222cc4b1fb6aa46990a80f83", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "ext-sodium": "*", + "php": "~8.1.0 || ~8.2.0 || ~8.3.0", + "psr/clock": "^1.0" + }, + "require-dev": { + "infection/infection": "^0.27.0", + "lcobucci/clock": "^3.0", + "lcobucci/coding-standard": "^11.0", + "phpbench/phpbench": "^1.2.9", + "phpstan/extension-installer": "^1.2", + "phpstan/phpstan": "^1.10.7", + "phpstan/phpstan-deprecation-rules": "^1.1.3", + "phpstan/phpstan-phpunit": "^1.3.10", + "phpstan/phpstan-strict-rules": "^1.5.0", + "phpunit/phpunit": "^10.2.6" + }, + "suggest": { + "lcobucci/clock": ">= 3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Lcobucci\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Luís Cobucci", + "email": "lcobucci@gmail.com", + "role": "Developer" + } + ], + "description": "A simple library to work with JSON Web Token and JSON Web Signature", + "keywords": [ + "JWS", + "jwt" + ], + "support": { + "issues": "https://github.com/lcobucci/jwt/issues", + "source": "https://github.com/lcobucci/jwt/tree/5.3.0" + }, + "funding": [ + { + "url": "https://github.com/lcobucci", + "type": "github" + }, + { + "url": "https://www.patreon.com/lcobucci", + "type": "patreon" + } + ], + "time": "2024-04-11T23:07:54+00:00" + }, + { + "name": "league/commonmark", + "version": "2.4.2", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/commonmark.git", + "reference": "91c24291965bd6d7c46c46a12ba7492f83b1cadf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/91c24291965bd6d7c46c46a12ba7492f83b1cadf", + "reference": "91c24291965bd6d7c46c46a12ba7492f83b1cadf", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "league/config": "^1.1.1", + "php": "^7.4 || ^8.0", + "psr/event-dispatcher": "^1.0", + "symfony/deprecation-contracts": "^2.1 || ^3.0", + "symfony/polyfill-php80": "^1.16" + }, + "require-dev": { + "cebe/markdown": "^1.0", + "commonmark/cmark": "0.30.3", + "commonmark/commonmark.js": "0.30.0", + "composer/package-versions-deprecated": "^1.8", + "embed/embed": "^4.4", + "erusev/parsedown": "^1.0", + "ext-json": "*", + "github/gfm": "0.29.0", + "michelf/php-markdown": "^1.4 || ^2.0", + "nyholm/psr7": "^1.5", + "phpstan/phpstan": "^1.8.2", + "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0", + "scrutinizer/ocular": "^1.8.1", + "symfony/finder": "^5.3 | ^6.0 || ^7.0", + "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 || ^7.0", + "unleashedtech/php-coding-standard": "^3.1.1", + "vimeo/psalm": "^4.24.0 || ^5.0.0" + }, + "suggest": { + "symfony/yaml": "v2.3+ required if using the Front Matter extension" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.5-dev" + } + }, + "autoload": { + "psr-4": { + "League\\CommonMark\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Highly-extensible PHP Markdown parser which fully supports the CommonMark spec and GitHub-Flavored Markdown (GFM)", + "homepage": "https://commonmark.thephpleague.com", + "keywords": [ + "commonmark", + "flavored", + "gfm", + "github", + "github-flavored", + "markdown", + "md", + "parser" + ], + "support": { + "docs": "https://commonmark.thephpleague.com/", + "forum": "https://github.com/thephpleague/commonmark/discussions", + "issues": "https://github.com/thephpleague/commonmark/issues", + "rss": "https://github.com/thephpleague/commonmark/releases.atom", + "source": "https://github.com/thephpleague/commonmark" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/commonmark", + "type": "tidelift" + } + ], + "time": "2024-02-02T11:59:32+00:00" + }, + { + "name": "league/config", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/config.git", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/config/zipball/754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "shasum": "" + }, + "require": { + "dflydev/dot-access-data": "^3.0.1", + "nette/schema": "^1.2", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.8.2", + "phpunit/phpunit": "^9.5.5", + "scrutinizer/ocular": "^1.8.1", + "unleashedtech/php-coding-standard": "^3.1", + "vimeo/psalm": "^4.7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.2-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Config\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Define configuration arrays with strict schemas and access values with dot notation", + "homepage": "https://config.thephpleague.com", + "keywords": [ + "array", + "config", + "configuration", + "dot", + "dot-access", + "nested", + "schema" + ], + "support": { + "docs": "https://config.thephpleague.com/", + "issues": "https://github.com/thephpleague/config/issues", + "rss": "https://github.com/thephpleague/config/releases.atom", + "source": "https://github.com/thephpleague/config" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + } + ], + "time": "2022-12-11T20:36:23+00:00" + }, + { + "name": "league/event", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/event.git", + "reference": "d2cc124cf9a3fab2bb4ff963307f60361ce4d119" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/event/zipball/d2cc124cf9a3fab2bb4ff963307f60361ce4d119", + "reference": "d2cc124cf9a3fab2bb4ff963307f60361ce4d119", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "henrikbjorn/phpspec-code-coverage": "~1.0.1", + "phpspec/phpspec": "^2.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.2-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Event\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frenky.net" + } + ], + "description": "Event package", + "keywords": [ + "emitter", + "event", + "listener" + ], + "support": { + "issues": "https://github.com/thephpleague/event/issues", + "source": "https://github.com/thephpleague/event/tree/master" + }, + "time": "2018-11-26T11:52:41+00:00" + }, + { + "name": "league/flysystem", + "version": "3.27.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem.git", + "reference": "4729745b1ab737908c7d055148c9a6b3e959832f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/4729745b1ab737908c7d055148c9a6b3e959832f", + "reference": "4729745b1ab737908c7d055148c9a6b3e959832f", + "shasum": "" + }, + "require": { + "league/flysystem-local": "^3.0.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "conflict": { + "async-aws/core": "<1.19.0", + "async-aws/s3": "<1.14.0", + "aws/aws-sdk-php": "3.209.31 || 3.210.0", + "guzzlehttp/guzzle": "<7.0", + "guzzlehttp/ringphp": "<1.1.1", + "phpseclib/phpseclib": "3.0.15", + "symfony/http-client": "<5.2" + }, + "require-dev": { + "async-aws/s3": "^1.5 || ^2.0", + "async-aws/simple-s3": "^1.1 || ^2.0", + "aws/aws-sdk-php": "^3.295.10", + "composer/semver": "^3.0", + "ext-fileinfo": "*", + "ext-ftp": "*", + "ext-zip": "*", + "friendsofphp/php-cs-fixer": "^3.5", + "google/cloud-storage": "^1.23", + "microsoft/azure-storage-blob": "^1.1", + "phpseclib/phpseclib": "^3.0.36", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.5.11|^10.0", + "sabre/dav": "^4.6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "File storage abstraction for PHP", + "keywords": [ + "WebDAV", + "aws", + "cloud", + "file", + "files", + "filesystem", + "filesystems", + "ftp", + "s3", + "sftp", + "storage" + ], + "support": { + "issues": "https://github.com/thephpleague/flysystem/issues", + "source": "https://github.com/thephpleague/flysystem/tree/3.27.0" + }, + "funding": [ + { + "url": "https://ecologi.com/frankdejonge", + "type": "custom" + }, + { + "url": "https://github.com/frankdejonge", + "type": "github" + } + ], + "time": "2024-04-07T19:17:50+00:00" + }, + { + "name": "league/flysystem-aws-s3-v3", + "version": "3.27.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git", + "reference": "3e6ce2f972f1470db779f04d29c289dcd2c32837" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/3e6ce2f972f1470db779f04d29c289dcd2c32837", + "reference": "3e6ce2f972f1470db779f04d29c289dcd2c32837", + "shasum": "" + }, + "require": { + "aws/aws-sdk-php": "^3.295.10", + "league/flysystem": "^3.10.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "conflict": { + "guzzlehttp/guzzle": "<7.0", + "guzzlehttp/ringphp": "<1.1.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\AwsS3V3\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "AWS S3 filesystem adapter for Flysystem.", + "keywords": [ + "Flysystem", + "aws", + "file", + "files", + "filesystem", + "s3", + "storage" + ], + "support": { + "source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/3.27.0" + }, + "funding": [ + { + "url": "https://ecologi.com/frankdejonge", + "type": "custom" + }, + { + "url": "https://github.com/frankdejonge", + "type": "github" + } + ], + "time": "2024-04-07T19:16:54+00:00" + }, + { + "name": "league/flysystem-local", + "version": "3.25.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-local.git", + "reference": "61a6a90d6e999e4ddd9ce5adb356de0939060b92" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/61a6a90d6e999e4ddd9ce5adb356de0939060b92", + "reference": "61a6a90d6e999e4ddd9ce5adb356de0939060b92", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "league/flysystem": "^3.0.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\Local\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Local filesystem adapter for Flysystem.", + "keywords": [ + "Flysystem", + "file", + "files", + "filesystem", + "local" + ], + "support": { + "source": "https://github.com/thephpleague/flysystem-local/tree/3.25.1" + }, + "funding": [ + { + "url": "https://ecologi.com/frankdejonge", + "type": "custom" + }, + { + "url": "https://github.com/frankdejonge", + "type": "github" + } + ], + "time": "2024-03-15T19:58:44+00:00" + }, + { + "name": "league/mime-type-detection", + "version": "1.15.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/mime-type-detection.git", + "reference": "ce0f4d1e8a6f4eb0ddff33f57c69c50fd09f4301" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/ce0f4d1e8a6f4eb0ddff33f57c69c50fd09f4301", + "reference": "ce0f4d1e8a6f4eb0ddff33f57c69c50fd09f4301", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.2", + "phpstan/phpstan": "^0.12.68", + "phpunit/phpunit": "^8.5.8 || ^9.3 || ^10.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\MimeTypeDetection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Mime-type detection for Flysystem", + "support": { + "issues": "https://github.com/thephpleague/mime-type-detection/issues", + "source": "https://github.com/thephpleague/mime-type-detection/tree/1.15.0" + }, + "funding": [ + { + "url": "https://github.com/frankdejonge", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/flysystem", + "type": "tidelift" + } + ], + "time": "2024-01-28T23:22:08+00:00" + }, + { + "name": "league/oauth1-client", + "version": "v1.10.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/oauth1-client.git", + "reference": "d6365b901b5c287dd41f143033315e2f777e1167" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/oauth1-client/zipball/d6365b901b5c287dd41f143033315e2f777e1167", + "reference": "d6365b901b5c287dd41f143033315e2f777e1167", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-openssl": "*", + "guzzlehttp/guzzle": "^6.0|^7.0", + "guzzlehttp/psr7": "^1.7|^2.0", + "php": ">=7.1||>=8.0" + }, + "require-dev": { + "ext-simplexml": "*", + "friendsofphp/php-cs-fixer": "^2.17", + "mockery/mockery": "^1.3.3", + "phpstan/phpstan": "^0.12.42", + "phpunit/phpunit": "^7.5||9.5" + }, + "suggest": { + "ext-simplexml": "For decoding XML-based responses." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev", + "dev-develop": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "League\\OAuth1\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ben Corlett", + "email": "bencorlett@me.com", + "homepage": "http://www.webcomm.com.au", + "role": "Developer" + } + ], + "description": "OAuth 1.0 Client Library", + "keywords": [ + "Authentication", + "SSO", + "authorization", + "bitbucket", + "identity", + "idp", + "oauth", + "oauth1", + "single sign on", + "trello", + "tumblr", + "twitter" + ], + "support": { + "issues": "https://github.com/thephpleague/oauth1-client/issues", + "source": "https://github.com/thephpleague/oauth1-client/tree/v1.10.1" + }, + "time": "2022-04-15T14:02:14+00:00" + }, + { + "name": "league/oauth2-server", + "version": "8.5.4", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/oauth2-server.git", + "reference": "ab7714d073844497fd222d5d0a217629089936bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/oauth2-server/zipball/ab7714d073844497fd222d5d0a217629089936bc", + "reference": "ab7714d073844497fd222d5d0a217629089936bc", + "shasum": "" + }, + "require": { + "defuse/php-encryption": "^2.3", + "ext-openssl": "*", + "lcobucci/clock": "^2.2 || ^3.0", + "lcobucci/jwt": "^4.3 || ^5.0", + "league/event": "^2.2", + "league/uri": "^6.7 || ^7.0", + "php": "^8.0", + "psr/http-message": "^1.0.1 || ^2.0" + }, + "replace": { + "league/oauth2server": "*", + "lncd/oauth2": "*" + }, + "require-dev": { + "laminas/laminas-diactoros": "^3.0.0", + "phpstan/phpstan": "^0.12.57", + "phpstan/phpstan-phpunit": "^0.12.16", + "phpunit/phpunit": "^9.6.6", + "roave/security-advisories": "dev-master" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\OAuth2\\Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alex Bilbie", + "email": "hello@alexbilbie.com", + "homepage": "http://www.alexbilbie.com", + "role": "Developer" + }, + { + "name": "Andy Millington", + "email": "andrew@noexceptions.io", + "homepage": "https://www.noexceptions.io", + "role": "Developer" + } + ], + "description": "A lightweight and powerful OAuth 2.0 authorization and resource server library with support for all the core specification grants. This library will allow you to secure your API with OAuth and allow your applications users to approve apps that want to access their data from your API.", + "homepage": "https://oauth2.thephpleague.com/", + "keywords": [ + "Authentication", + "api", + "auth", + "authorisation", + "authorization", + "oauth", + "oauth 2", + "oauth 2.0", + "oauth2", + "protect", + "resource", + "secure", + "server" + ], + "support": { + "issues": "https://github.com/thephpleague/oauth2-server/issues", + "source": "https://github.com/thephpleague/oauth2-server/tree/8.5.4" + }, + "funding": [ + { + "url": "https://github.com/sephster", + "type": "github" + } + ], + "time": "2023-08-25T22:35:12+00:00" + }, + { + "name": "league/uri", + "version": "7.4.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri.git", + "reference": "bedb6e55eff0c933668addaa7efa1e1f2c417cc4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/bedb6e55eff0c933668addaa7efa1e1f2c417cc4", + "reference": "bedb6e55eff0c933668addaa7efa1e1f2c417cc4", + "shasum": "" + }, + "require": { + "league/uri-interfaces": "^7.3", + "php": "^8.1" + }, + "conflict": { + "league/uri-schemes": "^1.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-fileinfo": "to create Data URI from file contennts", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "jeremykendall/php-domain-parser": "to resolve Public Suffix and Top Level Domain", + "league/uri-components": "Needed to easily manipulate URI objects components", + "php-64bit": "to improve IPV4 host parsing", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "URI manipulation library", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "middleware", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc3986", + "rfc3987", + "rfc6570", + "uri", + "uri-template", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri/tree/7.4.1" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2024-03-23T07:42:40+00:00" + }, + { + "name": "league/uri-interfaces", + "version": "7.4.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri-interfaces.git", + "reference": "8d43ef5c841032c87e2de015972c06f3865ef718" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/8d43ef5c841032c87e2de015972c06f3865ef718", + "reference": "8d43ef5c841032c87e2de015972c06f3865ef718", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "php": "^8.1", + "psr/http-factory": "^1", + "psr/http-message": "^1.1 || ^2.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "php-64bit": "to improve IPV4 host parsing", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "Common interfaces and classes for URI representation and interaction", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc3986", + "rfc3987", + "rfc6570", + "uri", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.4.1" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2024-03-23T07:42:40+00:00" + }, + { + "name": "mariuzzo/laravel-js-localization", + "version": "v1.11.1", + "source": { + "type": "git", + "url": "https://github.com/rmariuzzo/Laravel-JS-Localization.git", + "reference": "beda21f30ad8b3160ad17146fab6dcb79fe38eac" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/rmariuzzo/Laravel-JS-Localization/zipball/beda21f30ad8b3160ad17146fab6dcb79fe38eac", + "reference": "beda21f30ad8b3160ad17146fab6dcb79fe38eac", + "shasum": "" + }, + "require": { + "illuminate/config": "^4.2 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.0 || ^10.0 || ^11.0", + "illuminate/console": "^4.2 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.0 || ^10.0 || ^11.0", + "illuminate/filesystem": "^4.2 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.0 || ^10.0 || ^11.0", + "php": "^5.4 || ^7.0 || ^8.0", + "tedivm/jshrink": "~1.0" + }, + "require-dev": { + "orchestra/testbench": "^2.2 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.0", + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.0 || ^10.0 || ^11.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Mariuzzo\\LaravelJsLocalization\\LaravelJsLocalizationServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Mariuzzo\\LaravelJsLocalization\\": "src/Mariuzzo/LaravelJsLocalization/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Rubens Mariuzzo", + "email": "rubens@mariuzzo.com", + "homepage": "https://github.com/rmariuzzo", + "role": "Developer" + }, + { + "name": "German Popoter", + "email": "me@gpopoteur.com", + "homepage": "https://github.com/gpopoteur", + "role": "Developer" + }, + { + "name": "Galievskiy Dmitriy", + "homepage": "https://github.com/xAockd", + "role": "Developer" + }, + { + "name": "Ramon Ackermann", + "homepage": "https://github.com/sboo", + "role": "Developer" + }, + { + "name": "Anton Komarev", + "homepage": "https://github.com/antonkomarev", + "role": "Developer" + }, + { + "name": "Pascal Baljet", + "homepage": "https://github.com/pascalbaljetmedia", + "role": "Developer" + } + ], + "description": "Laravel Localization in JavaScript", + "homepage": "https://github.com/rmariuzzo/laravel-js-localization", + "keywords": [ + "JS", + "i18n", + "javascript", + "lang", + "laravel", + "laravel 5", + "localization" + ], + "support": { + "issues": "https://github.com/rmariuzzo/laravel-js-localization/issues", + "source": "https://github.com/rmariuzzo/laravel-js-localization" + }, + "time": "2024-03-31T11:24:22+00:00" + }, + { + "name": "masterminds/html5", + "version": "2.9.0", + "source": { + "type": "git", + "url": "https://github.com/Masterminds/html5-php.git", + "reference": "f5ac2c0b0a2eefca70b2ce32a5809992227e75a6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/f5ac2c0b0a2eefca70b2ce32a5809992227e75a6", + "reference": "f5ac2c0b0a2eefca70b2ce32a5809992227e75a6", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Masterminds\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Matt Butcher", + "email": "technosophos@gmail.com" + }, + { + "name": "Matt Farina", + "email": "matt@mattfarina.com" + }, + { + "name": "Asmir Mustafic", + "email": "goetas@gmail.com" + } + ], + "description": "An HTML5 parser and serializer.", + "homepage": "http://masterminds.github.io/html5-php", + "keywords": [ + "HTML5", + "dom", + "html", + "parser", + "querypath", + "serializer", + "xml" + ], + "support": { + "issues": "https://github.com/Masterminds/html5-php/issues", + "source": "https://github.com/Masterminds/html5-php/tree/2.9.0" + }, + "time": "2024-03-31T07:05:07+00:00" + }, + { + "name": "matriphe/iso-639", + "version": "1.3", + "source": { + "type": "git", + "url": "https://github.com/matriphe/php-iso-639.git", + "reference": "9a4a5823147890e70e0e0f60f3baea95e8d3b5f1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/matriphe/php-iso-639/zipball/9a4a5823147890e70e0e0f60f3baea95e8d3b5f1", + "reference": "9a4a5823147890e70e0e0f60f3baea95e8d3b5f1", + "shasum": "" + }, + "require-dev": { + "phpunit/phpunit": "^4.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "Matriphe\\ISO639\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Muhammad Zamroni", + "email": "halo@matriphe.com" + } + ], + "description": "PHP library to convert ISO-639-1 code to language name.", + "keywords": [ + "639", + "iso", + "iso-639", + "lang", + "language", + "laravel" + ], + "support": { + "issues": "https://github.com/matriphe/php-iso-639/issues", + "source": "https://github.com/matriphe/php-iso-639/tree/1.3" + }, + "time": "2024-03-17T21:30:14+00:00" + }, + { + "name": "maxmind-db/reader", + "version": "v1.11.1", + "source": { + "type": "git", + "url": "https://github.com/maxmind/MaxMind-DB-Reader-php.git", + "reference": "1e66f73ffcf25e17c7a910a1317e9720a95497c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/maxmind/MaxMind-DB-Reader-php/zipball/1e66f73ffcf25e17c7a910a1317e9720a95497c7", + "reference": "1e66f73ffcf25e17c7a910a1317e9720a95497c7", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "conflict": { + "ext-maxminddb": "<1.11.1,>=2.0.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "3.*", + "php-coveralls/php-coveralls": "^2.1", + "phpstan/phpstan": "*", + "phpunit/phpcov": ">=6.0.0", + "phpunit/phpunit": ">=8.0.0,<10.0.0", + "squizlabs/php_codesniffer": "3.*" + }, + "suggest": { + "ext-bcmath": "bcmath or gmp is required for decoding larger integers with the pure PHP decoder", + "ext-gmp": "bcmath or gmp is required for decoding larger integers with the pure PHP decoder", + "ext-maxminddb": "A C-based database decoder that provides significantly faster lookups" + }, + "type": "library", + "autoload": { + "psr-4": { + "MaxMind\\Db\\": "src/MaxMind/Db" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Gregory J. Oschwald", + "email": "goschwald@maxmind.com", + "homepage": "https://www.maxmind.com/" + } + ], + "description": "MaxMind DB Reader API", + "homepage": "https://github.com/maxmind/MaxMind-DB-Reader-php", + "keywords": [ + "database", + "geoip", + "geoip2", + "geolocation", + "maxmind" + ], + "support": { + "issues": "https://github.com/maxmind/MaxMind-DB-Reader-php/issues", + "source": "https://github.com/maxmind/MaxMind-DB-Reader-php/tree/v1.11.1" + }, + "time": "2023-12-02T00:09:23+00:00" + }, + { + "name": "maxmind/web-service-common", + "version": "v0.9.0", + "source": { + "type": "git", + "url": "https://github.com/maxmind/web-service-common-php.git", + "reference": "4dc5a3e8df38aea4ca3b1096cee3a038094e9b53" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/maxmind/web-service-common-php/zipball/4dc5a3e8df38aea4ca3b1096cee3a038094e9b53", + "reference": "4dc5a3e8df38aea4ca3b1096cee3a038094e9b53", + "shasum": "" + }, + "require": { + "composer/ca-bundle": "^1.0.3", + "ext-curl": "*", + "ext-json": "*", + "php": ">=7.2" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "3.*", + "phpstan/phpstan": "*", + "phpunit/phpunit": "^8.0 || ^9.0", + "squizlabs/php_codesniffer": "3.*" + }, + "type": "library", + "autoload": { + "psr-4": { + "MaxMind\\Exception\\": "src/Exception", + "MaxMind\\WebService\\": "src/WebService" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Gregory Oschwald", + "email": "goschwald@maxmind.com" + } + ], + "description": "Internal MaxMind Web Service API", + "homepage": "https://github.com/maxmind/web-service-common-php", + "support": { + "issues": "https://github.com/maxmind/web-service-common-php/issues", + "source": "https://github.com/maxmind/web-service-common-php/tree/v0.9.0" + }, + "time": "2022-03-28T17:43:20+00:00" + }, + { + "name": "moneyphp/money", + "version": "v4.5.0", + "source": { + "type": "git", + "url": "https://github.com/moneyphp/money.git", + "reference": "a1daa7daf159b4044e3d0c34c41fe2be5860e850" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/moneyphp/money/zipball/a1daa7daf159b4044e3d0c34c41fe2be5860e850", + "reference": "a1daa7daf159b4044e3d0c34c41fe2be5860e850", + "shasum": "" + }, + "require": { + "ext-bcmath": "*", + "ext-filter": "*", + "ext-json": "*", + "php": "~8.1.0 || ~8.2.0 || ~8.3.0" + }, + "require-dev": { + "cache/taggable-cache": "^1.1.0", + "doctrine/coding-standard": "^12.0", + "doctrine/instantiator": "^1.5.0 || ^2.0", + "ext-gmp": "*", + "ext-intl": "*", + "florianv/exchanger": "^2.8.1", + "florianv/swap": "^4.3.0", + "moneyphp/crypto-currencies": "^1.1.0", + "moneyphp/iso-currencies": "^3.4", + "php-http/message": "^1.16.0", + "php-http/mock-client": "^1.6.0", + "phpbench/phpbench": "^1.2.5", + "phpunit/phpunit": "^10.5.9", + "psalm/plugin-phpunit": "^0.18.4", + "psr/cache": "^1.0.1 || ^2.0 || ^3.0", + "vimeo/psalm": "~5.20.0" + }, + "suggest": { + "ext-gmp": "Calculate without integer limits", + "ext-intl": "Format Money objects with intl", + "florianv/exchanger": "Exchange rates library for PHP", + "florianv/swap": "Exchange rates library for PHP", + "psr/cache-implementation": "Used for Currency caching" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Money\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mathias Verraes", + "email": "mathias@verraes.net", + "homepage": "http://verraes.net" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + }, + { + "name": "Frederik Bosch", + "email": "f.bosch@genkgo.nl" + } + ], + "description": "PHP implementation of Fowler's Money pattern", + "homepage": "http://moneyphp.org", + "keywords": [ + "Value Object", + "money", + "vo" + ], + "support": { + "issues": "https://github.com/moneyphp/money/issues", + "source": "https://github.com/moneyphp/money/tree/v4.5.0" + }, + "time": "2024-02-15T19:47:21+00:00" + }, + { + "name": "monicahq/laravel-cloudflare", + "version": "3.7.1", + "source": { + "type": "git", + "url": "https://github.com/monicahq/laravel-cloudflare.git", + "reference": "5d18e60d5d772466c931e4e73377f0493e104211" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/monicahq/laravel-cloudflare/zipball/5d18e60d5d772466c931e4e73377f0493e104211", + "reference": "5d18e60d5d772466c931e4e73377f0493e104211", + "shasum": "" + }, + "require": { + "illuminate/support": "^8.0 || ^9.0 || ^10.0 || ^11.0", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "guzzlehttp/guzzle": "^6.3 || ^7.0", + "larastan/larastan": "^1.0 || ^2.4", + "mockery/mockery": "^1.4", + "ocramius/package-versions": "^1.5 || ^2.1", + "orchestra/testbench": "^6.0 || ^7.0 || ^8.0 || ^9.0", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.0", + "phpstan/phpstan-strict-rules": "^1.0", + "phpunit/phpunit": "^9.5 || ^10.0 || ^11.0", + "vimeo/psalm": "^4.0 || ^5.6" + }, + "suggest": { + "guzzlehttp/guzzle": "Required to get cloudflares ip addresses (^6.5.5|^7.0)." + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Monicahq\\Cloudflare\\TrustedProxyServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Monicahq\\Cloudflare\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alexis Saettler", + "email": "alexis@saettler.org" + } + ], + "description": "Add Cloudflare ip addresses to trusted proxies for Laravel.", + "keywords": [ + "cloudflare", + "laravel", + "php", + "proxies" + ], + "support": { + "issues": "https://github.com/monicahq/laravel-cloudflare/issues", + "source": "https://github.com/monicahq/laravel-cloudflare" + }, + "funding": [ + { + "url": "https://github.com/asbiin", + "type": "github" + } + ], + "time": "2024-03-30T09:59:45+00:00" + }, + { + "name": "monicahq/laravel-sabre", + "version": "1.8.0", + "source": { + "type": "git", + "url": "https://github.com/monicahq/laravel-sabre.git", + "reference": "f2133745c133b7103fdc3c9f5fb2d02737714588" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/monicahq/laravel-sabre/zipball/f2133745c133b7103fdc3c9f5fb2d02737714588", + "reference": "f2133745c133b7103fdc3c9f5fb2d02737714588", + "shasum": "" + }, + "require": { + "illuminate/support": "^8.0 || ^9.0 || ^10.0 || ^11.0", + "sabre/dav": "^4.0", + "thecodingmachine/safe": "^2.0" + }, + "require-dev": { + "larastan/larastan": "^1.0 || ^2.0", + "mockery/mockery": "^1.4", + "ocramius/package-versions": "^1.9 || ^2.0", + "orchestra/testbench": "^6.0 || ^7.0 || ^8.0 || ^9.0", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.0", + "phpstan/phpstan-strict-rules": "^1.0", + "phpunit/phpunit": "^9.0 || ^10.0 || ^11.0", + "roave/security-advisories": "dev-master", + "thecodingmachine/phpstan-safe-rule": "^1.0", + "vimeo/psalm": "^4.0 || ^5.6" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "LaravelSabre\\LaravelSabreServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "LaravelSabre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alexis Saettler", + "email": "alexis@saettler.org" + } + ], + "description": "Sabre DAV server adapter for Laravel.", + "keywords": [ + "dav", + "laravel", + "php", + "sabre" + ], + "support": { + "issues": "https://github.com/monicahq/laravel-sabre/issues", + "source": "https://github.com/monicahq/laravel-sabre" + }, + "funding": [ + { + "url": "https://github.com/asbiin", + "type": "github" + } + ], + "time": "2024-03-01T23:13:56+00:00" + }, + { + "name": "monolog/monolog", + "version": "2.9.3", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "a30bfe2e142720dfa990d0a7e573997f5d884215" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/a30bfe2e142720dfa990d0a7e573997f5d884215", + "reference": "a30bfe2e142720dfa990d0a7e573997f5d884215", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "psr/log": "^1.0.1 || ^2.0 || ^3.0" + }, + "provide": { + "psr/log-implementation": "1.0.0 || 2.0.0 || 3.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^2.4.9 || ^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7 || ^8", + "ext-json": "*", + "graylog2/gelf-php": "^1.4.2 || ^2@dev", + "guzzlehttp/guzzle": "^7.4", + "guzzlehttp/psr7": "^2.2", + "mongodb/mongodb": "^1.8", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "phpspec/prophecy": "^1.15", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^8.5.38 || ^9.6.19", + "predis/predis": "^1.1 || ^2.0", + "rollbar/rollbar": "^1.3 || ^2 || ^3", + "ruflin/elastica": "^7", + "swiftmailer/swiftmailer": "^5.3|^6.0", + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/2.9.3" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2024-04-12T20:52:51+00:00" + }, + { + "name": "mtdowling/jmespath.php", + "version": "2.7.0", + "source": { + "type": "git", + "url": "https://github.com/jmespath/jmespath.php.git", + "reference": "bbb69a935c2cbb0c03d7f481a238027430f6440b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/bbb69a935c2cbb0c03d7f481a238027430f6440b", + "reference": "bbb69a935c2cbb0c03d7f481a238027430f6440b", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "symfony/polyfill-mbstring": "^1.17" + }, + "require-dev": { + "composer/xdebug-handler": "^3.0.3", + "phpunit/phpunit": "^8.5.33" + }, + "bin": [ + "bin/jp.php" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "files": [ + "src/JmesPath.php" + ], + "psr-4": { + "JmesPath\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Declaratively specify how to extract elements from a JSON document", + "keywords": [ + "json", + "jsonpath" + ], + "support": { + "issues": "https://github.com/jmespath/jmespath.php/issues", + "source": "https://github.com/jmespath/jmespath.php/tree/2.7.0" + }, + "time": "2023-08-25T10:54:48+00:00" + }, + { + "name": "nesbot/carbon", + "version": "2.72.3", + "source": { + "type": "git", + "url": "https://github.com/briannesbitt/Carbon.git", + "reference": "0c6fd108360c562f6e4fd1dedb8233b423e91c83" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/0c6fd108360c562f6e4fd1dedb8233b423e91c83", + "reference": "0c6fd108360c562f6e4fd1dedb8233b423e91c83", + "shasum": "" + }, + "require": { + "carbonphp/carbon-doctrine-types": "*", + "ext-json": "*", + "php": "^7.1.8 || ^8.0", + "psr/clock": "^1.0", + "symfony/polyfill-mbstring": "^1.0", + "symfony/polyfill-php80": "^1.16", + "symfony/translation": "^3.4 || ^4.0 || ^5.0 || ^6.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "require-dev": { + "doctrine/dbal": "^2.0 || ^3.1.4 || ^4.0", + "doctrine/orm": "^2.7 || ^3.0", + "friendsofphp/php-cs-fixer": "^3.0", + "kylekatarnls/multi-tester": "^2.0", + "ondrejmirtes/better-reflection": "*", + "phpmd/phpmd": "^2.9", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^0.12.99 || ^1.7.14", + "phpunit/php-file-iterator": "^2.0.5 || ^3.0.6", + "phpunit/phpunit": "^7.5.20 || ^8.5.26 || ^9.5.20", + "squizlabs/php_codesniffer": "^3.4" + }, + "bin": [ + "bin/carbon" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-3.x": "3.x-dev", + "dev-master": "2.x-dev" + }, + "laravel": { + "providers": [ + "Carbon\\Laravel\\ServiceProvider" + ] + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + } + }, + "autoload": { + "psr-4": { + "Carbon\\": "src/Carbon/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Nesbitt", + "email": "brian@nesbot.com", + "homepage": "https://markido.com" + }, + { + "name": "kylekatarnls", + "homepage": "https://github.com/kylekatarnls" + } + ], + "description": "An API extension for DateTime that supports 281 different languages.", + "homepage": "https://carbon.nesbot.com", + "keywords": [ + "date", + "datetime", + "time" + ], + "support": { + "docs": "https://carbon.nesbot.com/docs", + "issues": "https://github.com/briannesbitt/Carbon/issues", + "source": "https://github.com/briannesbitt/Carbon" + }, + "funding": [ + { + "url": "https://github.com/sponsors/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon#sponsor", + "type": "opencollective" + }, + { + "url": "https://tidelift.com/subscription/pkg/packagist-nesbot-carbon?utm_source=packagist-nesbot-carbon&utm_medium=referral&utm_campaign=readme", + "type": "tidelift" + } + ], + "time": "2024-01-25T10:35:09+00:00" + }, + { + "name": "nette/schema", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/nette/schema.git", + "reference": "a6d3a6d1f545f01ef38e60f375d1cf1f4de98188" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/schema/zipball/a6d3a6d1f545f01ef38e60f375d1cf1f4de98188", + "reference": "a6d3a6d1f545f01ef38e60f375d1cf1f4de98188", + "shasum": "" + }, + "require": { + "nette/utils": "^4.0", + "php": "8.1 - 8.3" + }, + "require-dev": { + "nette/tester": "^2.4", + "phpstan/phpstan-nette": "^1.0", + "tracy/tracy": "^2.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "📐 Nette Schema: validating data structures against a given Schema.", + "homepage": "https://nette.org", + "keywords": [ + "config", + "nette" + ], + "support": { + "issues": "https://github.com/nette/schema/issues", + "source": "https://github.com/nette/schema/tree/v1.3.0" + }, + "time": "2023-12-11T11:54:22+00:00" + }, + { + "name": "nette/utils", + "version": "v4.0.4", + "source": { + "type": "git", + "url": "https://github.com/nette/utils.git", + "reference": "d3ad0aa3b9f934602cb3e3902ebccf10be34d218" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/utils/zipball/d3ad0aa3b9f934602cb3e3902ebccf10be34d218", + "reference": "d3ad0aa3b9f934602cb3e3902ebccf10be34d218", + "shasum": "" + }, + "require": { + "php": ">=8.0 <8.4" + }, + "conflict": { + "nette/finder": "<3", + "nette/schema": "<1.2.2" + }, + "require-dev": { + "jetbrains/phpstorm-attributes": "dev-master", + "nette/tester": "^2.5", + "phpstan/phpstan": "^1.0", + "tracy/tracy": "^2.9" + }, + "suggest": { + "ext-gd": "to use Image", + "ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()", + "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()", + "ext-json": "to use Nette\\Utils\\Json", + "ext-mbstring": "to use Strings::lower() etc...", + "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.", + "homepage": "https://nette.org", + "keywords": [ + "array", + "core", + "datetime", + "images", + "json", + "nette", + "paginator", + "password", + "slugify", + "string", + "unicode", + "utf-8", + "utility", + "validation" + ], + "support": { + "issues": "https://github.com/nette/utils/issues", + "source": "https://github.com/nette/utils/tree/v4.0.4" + }, + "time": "2024-01-17T16:50:36+00:00" + }, + { + "name": "nunomaduro/termwind", + "version": "v1.15.1", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/termwind.git", + "reference": "8ab0b32c8caa4a2e09700ea32925441385e4a5dc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/8ab0b32c8caa4a2e09700ea32925441385e4a5dc", + "reference": "8ab0b32c8caa4a2e09700ea32925441385e4a5dc", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^8.0", + "symfony/console": "^5.3.0|^6.0.0" + }, + "require-dev": { + "ergebnis/phpstan-rules": "^1.0.", + "illuminate/console": "^8.0|^9.0", + "illuminate/support": "^8.0|^9.0", + "laravel/pint": "^1.0.0", + "pestphp/pest": "^1.21.0", + "pestphp/pest-plugin-mock": "^1.0", + "phpstan/phpstan": "^1.4.6", + "phpstan/phpstan-strict-rules": "^1.1.0", + "symfony/var-dumper": "^5.2.7|^6.0.0", + "thecodingmachine/phpstan-strict-rules": "^1.0.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Termwind\\Laravel\\TermwindServiceProvider" + ] + } + }, + "autoload": { + "files": [ + "src/Functions.php" + ], + "psr-4": { + "Termwind\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Its like Tailwind CSS, but for the console.", + "keywords": [ + "cli", + "console", + "css", + "package", + "php", + "style" + ], + "support": { + "issues": "https://github.com/nunomaduro/termwind/issues", + "source": "https://github.com/nunomaduro/termwind/tree/v1.15.1" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://github.com/xiCO2k", + "type": "github" + } + ], + "time": "2023-02-08T01:06:31+00:00" + }, + { + "name": "nyholm/psr7", + "version": "1.8.1", + "source": { + "type": "git", + "url": "https://github.com/Nyholm/psr7.git", + "reference": "aa5fc277a4f5508013d571341ade0c3886d4d00e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Nyholm/psr7/zipball/aa5fc277a4f5508013d571341ade0c3886d4d00e", + "reference": "aa5fc277a4f5508013d571341ade0c3886d4d00e", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0" + }, + "provide": { + "php-http/message-factory-implementation": "1.0", + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "http-interop/http-factory-tests": "^0.9", + "php-http/message-factory": "^1.0", + "php-http/psr7-integration-tests": "^1.0", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.4", + "symfony/error-handler": "^4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.8-dev" + } + }, + "autoload": { + "psr-4": { + "Nyholm\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + }, + { + "name": "Martijn van der Ven", + "email": "martijn@vanderven.se" + } + ], + "description": "A fast PHP7 implementation of PSR-7", + "homepage": "https://tnyholm.se", + "keywords": [ + "psr-17", + "psr-7" + ], + "support": { + "issues": "https://github.com/Nyholm/psr7/issues", + "source": "https://github.com/Nyholm/psr7/tree/1.8.1" + }, + "funding": [ + { + "url": "https://github.com/Zegnat", + "type": "github" + }, + { + "url": "https://github.com/nyholm", + "type": "github" + } + ], + "time": "2023-11-13T09:31:12+00:00" + }, + { + "name": "ok/ipstack-client", + "version": "2.1", + "source": { + "type": "git", + "url": "https://github.com/GitHubHubus/ipstack-client.git", + "reference": "07313ced7feebc16204555d6da0ec876dfb858fd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/GitHubHubus/ipstack-client/zipball/07313ced7feebc16204555d6da0ec876dfb858fd", + "reference": "07313ced7feebc16204555d6da0ec876dfb858fd", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-simplexml": "*", + "php": ">=7.4" + }, + "require-dev": { + "phpunit/php-code-coverage": "^9", + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "autoload": { + "psr-0": { + "OK\\Ipstack\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Oleg Kochetkov", + "email": "oleg.kochetkov999@yandex.ru" + } + ], + "description": "A PHP wrapper for using Ipstack API", + "homepage": "https://github.com/GitHubHubus/ipstack-client", + "keywords": [ + "IP", + "api", + "client", + "geocode", + "ipstack", + "php" + ], + "support": { + "issues": "https://github.com/GitHubHubus/ipstack-client/issues", + "source": "https://github.com/GitHubHubus/ipstack-client/tree/2.1" + }, + "time": "2021-11-17T14:10:03+00:00" + }, + { + "name": "paragonie/constant_time_encoding", + "version": "v2.6.3", + "source": { + "type": "git", + "url": "https://github.com/paragonie/constant_time_encoding.git", + "reference": "58c3f47f650c94ec05a151692652a868995d2938" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/58c3f47f650c94ec05a151692652a868995d2938", + "reference": "58c3f47f650c94ec05a151692652a868995d2938", + "shasum": "" + }, + "require": { + "php": "^7|^8" + }, + "require-dev": { + "phpunit/phpunit": "^6|^7|^8|^9", + "vimeo/psalm": "^1|^2|^3|^4" + }, + "type": "library", + "autoload": { + "psr-4": { + "ParagonIE\\ConstantTime\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com", + "role": "Maintainer" + }, + { + "name": "Steve 'Sc00bz' Thomas", + "email": "steve@tobtu.com", + "homepage": "https://www.tobtu.com", + "role": "Original Developer" + } + ], + "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)", + "keywords": [ + "base16", + "base32", + "base32_decode", + "base32_encode", + "base64", + "base64_decode", + "base64_encode", + "bin2hex", + "encoding", + "hex", + "hex2bin", + "rfc4648" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/constant_time_encoding/issues", + "source": "https://github.com/paragonie/constant_time_encoding" + }, + "time": "2022-06-14T06:56:20+00:00" + }, + { + "name": "paragonie/random_compat", + "version": "v9.99.100", + "source": { + "type": "git", + "url": "https://github.com/paragonie/random_compat.git", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a", + "shasum": "" + }, + "require": { + "php": ">= 7" + }, + "require-dev": { + "phpunit/phpunit": "4.*|5.*", + "vimeo/psalm": "^1" + }, + "suggest": { + "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", + "keywords": [ + "csprng", + "polyfill", + "pseudorandom", + "random" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/random_compat/issues", + "source": "https://github.com/paragonie/random_compat" + }, + "time": "2020-10-15T08:29:30+00:00" + }, + { + "name": "paragonie/sodium_compat", + "version": "v1.21.1", + "source": { + "type": "git", + "url": "https://github.com/paragonie/sodium_compat.git", + "reference": "bb312875dcdd20680419564fe42ba1d9564b9e37" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/sodium_compat/zipball/bb312875dcdd20680419564fe42ba1d9564b9e37", + "reference": "bb312875dcdd20680419564fe42ba1d9564b9e37", + "shasum": "" + }, + "require": { + "paragonie/random_compat": ">=1", + "php": "^5.2.4|^5.3|^5.4|^5.5|^5.6|^7|^8" + }, + "require-dev": { + "phpunit/phpunit": "^3|^4|^5|^6|^7|^8|^9" + }, + "suggest": { + "ext-libsodium": "PHP < 7.0: Better performance, password hashing (Argon2i), secure memory management (memzero), and better security.", + "ext-sodium": "PHP >= 7.0: Better performance, password hashing (Argon2i), secure memory management (memzero), and better security." + }, + "type": "library", + "autoload": { + "files": [ + "autoload.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com" + }, + { + "name": "Frank Denis", + "email": "jedisct1@pureftpd.org" + } + ], + "description": "Pure PHP implementation of libsodium; uses the PHP extension if it exists", + "keywords": [ + "Authentication", + "BLAKE2b", + "ChaCha20", + "ChaCha20-Poly1305", + "Chapoly", + "Curve25519", + "Ed25519", + "EdDSA", + "Edwards-curve Digital Signature Algorithm", + "Elliptic Curve Diffie-Hellman", + "Poly1305", + "Pure-PHP cryptography", + "RFC 7748", + "RFC 8032", + "Salpoly", + "Salsa20", + "X25519", + "XChaCha20-Poly1305", + "XSalsa20-Poly1305", + "Xchacha20", + "Xsalsa20", + "aead", + "cryptography", + "ecdh", + "elliptic curve", + "elliptic curve cryptography", + "encryption", + "libsodium", + "php", + "public-key cryptography", + "secret-key cryptography", + "side-channel resistant" + ], + "support": { + "issues": "https://github.com/paragonie/sodium_compat/issues", + "source": "https://github.com/paragonie/sodium_compat/tree/v1.21.1" + }, + "time": "2024-04-22T22:05:04+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phenx/php-font-lib", + "version": "0.5.6", + "source": { + "type": "git", + "url": "https://github.com/dompdf/php-font-lib.git", + "reference": "a1681e9793040740a405ac5b189275059e2a9863" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dompdf/php-font-lib/zipball/a1681e9793040740a405ac5b189275059e2a9863", + "reference": "a1681e9793040740a405ac5b189275059e2a9863", + "shasum": "" + }, + "require": { + "ext-mbstring": "*" + }, + "require-dev": { + "symfony/phpunit-bridge": "^3 || ^4 || ^5 || ^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "FontLib\\": "src/FontLib" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-or-later" + ], + "authors": [ + { + "name": "Fabien Ménager", + "email": "fabien.menager@gmail.com" + } + ], + "description": "A library to read, parse, export and make subsets of different types of font files.", + "homepage": "https://github.com/PhenX/php-font-lib", + "support": { + "issues": "https://github.com/dompdf/php-font-lib/issues", + "source": "https://github.com/dompdf/php-font-lib/tree/0.5.6" + }, + "time": "2024-01-29T14:45:26+00:00" + }, + { + "name": "phenx/php-svg-lib", + "version": "0.5.4", + "source": { + "type": "git", + "url": "https://github.com/dompdf/php-svg-lib.git", + "reference": "46b25da81613a9cf43c83b2a8c2c1bdab27df691" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dompdf/php-svg-lib/zipball/46b25da81613a9cf43c83b2a8c2c1bdab27df691", + "reference": "46b25da81613a9cf43c83b2a8c2c1bdab27df691", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^7.1 || ^8.0", + "sabberworm/php-css-parser": "^8.4" + }, + "require-dev": { + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Svg\\": "src/Svg" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Fabien Ménager", + "email": "fabien.menager@gmail.com" + } + ], + "description": "A library to read, parse and export to PDF SVG files.", + "homepage": "https://github.com/PhenX/php-svg-lib", + "support": { + "issues": "https://github.com/dompdf/php-svg-lib/issues", + "source": "https://github.com/dompdf/php-svg-lib/tree/0.5.4" + }, + "time": "2024-04-08T12:52:34+00:00" + }, + { + "name": "php-http/client-common", + "version": "2.7.1", + "source": { + "type": "git", + "url": "https://github.com/php-http/client-common.git", + "reference": "1e19c059b0e4d5f717bf5d524d616165aeab0612" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/client-common/zipball/1e19c059b0e4d5f717bf5d524d616165aeab0612", + "reference": "1e19c059b0e4d5f717bf5d524d616165aeab0612", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "php-http/httplug": "^2.0", + "php-http/message": "^1.6", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0 || ^2.0", + "symfony/options-resolver": "~4.0.15 || ~4.1.9 || ^4.2.1 || ^5.0 || ^6.0 || ^7.0", + "symfony/polyfill-php80": "^1.17" + }, + "require-dev": { + "doctrine/instantiator": "^1.1", + "guzzlehttp/psr7": "^1.4", + "nyholm/psr7": "^1.2", + "phpspec/phpspec": "^5.1 || ^6.3 || ^7.1", + "phpspec/prophecy": "^1.10.2", + "phpunit/phpunit": "^7.5.20 || ^8.5.33 || ^9.6.7" + }, + "suggest": { + "ext-json": "To detect JSON responses with the ContentTypePlugin", + "ext-libxml": "To detect XML responses with the ContentTypePlugin", + "php-http/cache-plugin": "PSR-6 Cache plugin", + "php-http/logger-plugin": "PSR-3 Logger plugin", + "php-http/stopwatch-plugin": "Symfony Stopwatch plugin" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Client\\Common\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Common HTTP Client implementations and tools for HTTPlug", + "homepage": "http://httplug.io", + "keywords": [ + "client", + "common", + "http", + "httplug" + ], + "support": { + "issues": "https://github.com/php-http/client-common/issues", + "source": "https://github.com/php-http/client-common/tree/2.7.1" + }, + "time": "2023-11-30T10:31:25+00:00" + }, + { + "name": "php-http/discovery", + "version": "1.19.4", + "source": { + "type": "git", + "url": "https://github.com/php-http/discovery.git", + "reference": "0700efda8d7526335132360167315fdab3aeb599" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/discovery/zipball/0700efda8d7526335132360167315fdab3aeb599", + "reference": "0700efda8d7526335132360167315fdab3aeb599", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0|^2.0", + "php": "^7.1 || ^8.0" + }, + "conflict": { + "nyholm/psr7": "<1.0", + "zendframework/zend-diactoros": "*" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "*", + "psr/http-factory-implementation": "*", + "psr/http-message-implementation": "*" + }, + "require-dev": { + "composer/composer": "^1.0.2|^2.0", + "graham-campbell/phpspec-skip-example-extension": "^5.0", + "php-http/httplug": "^1.0 || ^2.0", + "php-http/message-factory": "^1.0", + "phpspec/phpspec": "^5.1 || ^6.1 || ^7.3", + "sebastian/comparator": "^3.0.5 || ^4.0.8", + "symfony/phpunit-bridge": "^6.4.4 || ^7.0.1" + }, + "type": "composer-plugin", + "extra": { + "class": "Http\\Discovery\\Composer\\Plugin", + "plugin-optional": true + }, + "autoload": { + "psr-4": { + "Http\\Discovery\\": "src/" + }, + "exclude-from-classmap": [ + "src/Composer/Plugin.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Finds and installs PSR-7, PSR-17, PSR-18 and HTTPlug implementations", + "homepage": "http://php-http.org", + "keywords": [ + "adapter", + "client", + "discovery", + "factory", + "http", + "message", + "psr17", + "psr7" + ], + "support": { + "issues": "https://github.com/php-http/discovery/issues", + "source": "https://github.com/php-http/discovery/tree/1.19.4" + }, + "time": "2024-03-29T13:00:05+00:00" + }, + { + "name": "php-http/httplug", + "version": "2.4.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/httplug.git", + "reference": "625ad742c360c8ac580fcc647a1541d29e257f67" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/httplug/zipball/625ad742c360c8ac580fcc647a1541d29e257f67", + "reference": "625ad742c360c8ac580fcc647a1541d29e257f67", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "php-http/promise": "^1.1", + "psr/http-client": "^1.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "require-dev": { + "friends-of-phpspec/phpspec-code-coverage": "^4.1 || ^5.0 || ^6.0", + "phpspec/phpspec": "^5.1 || ^6.0 || ^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eric GELOEN", + "email": "geloen.eric@gmail.com" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "HTTPlug, the HTTP client abstraction for PHP", + "homepage": "http://httplug.io", + "keywords": [ + "client", + "http" + ], + "support": { + "issues": "https://github.com/php-http/httplug/issues", + "source": "https://github.com/php-http/httplug/tree/2.4.0" + }, + "time": "2023-04-14T15:10:03+00:00" + }, + { + "name": "php-http/message", + "version": "1.16.1", + "source": { + "type": "git", + "url": "https://github.com/php-http/message.git", + "reference": "5997f3289332c699fa2545c427826272498a2088" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/message/zipball/5997f3289332c699fa2545c427826272498a2088", + "reference": "5997f3289332c699fa2545c427826272498a2088", + "shasum": "" + }, + "require": { + "clue/stream-filter": "^1.5", + "php": "^7.2 || ^8.0", + "psr/http-message": "^1.1 || ^2.0" + }, + "provide": { + "php-http/message-factory-implementation": "1.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.6", + "ext-zlib": "*", + "guzzlehttp/psr7": "^1.0 || ^2.0", + "laminas/laminas-diactoros": "^2.0 || ^3.0", + "php-http/message-factory": "^1.0.2", + "phpspec/phpspec": "^5.1 || ^6.3 || ^7.1", + "slim/slim": "^3.0" + }, + "suggest": { + "ext-zlib": "Used with compressor/decompressor streams", + "guzzlehttp/psr7": "Used with Guzzle PSR-7 Factories", + "laminas/laminas-diactoros": "Used with Diactoros Factories", + "slim/slim": "Used with Slim Framework PSR-7 implementation" + }, + "type": "library", + "autoload": { + "files": [ + "src/filters.php" + ], + "psr-4": { + "Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "HTTP Message related tools", + "homepage": "http://php-http.org", + "keywords": [ + "http", + "message", + "psr-7" + ], + "support": { + "issues": "https://github.com/php-http/message/issues", + "source": "https://github.com/php-http/message/tree/1.16.1" + }, + "time": "2024-03-07T13:22:09+00:00" + }, + { + "name": "php-http/message-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/message-factory.git", + "reference": "4d8778e1c7d405cbb471574821c1ff5b68cc8f57" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/message-factory/zipball/4d8778e1c7d405cbb471574821c1ff5b68cc8f57", + "reference": "4d8778e1c7d405cbb471574821c1ff5b68cc8f57", + "shasum": "" + }, + "require": { + "php": ">=5.4", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Factory interfaces for PSR-7 HTTP Message", + "homepage": "http://php-http.org", + "keywords": [ + "factory", + "http", + "message", + "stream", + "uri" + ], + "support": { + "issues": "https://github.com/php-http/message-factory/issues", + "source": "https://github.com/php-http/message-factory/tree/1.1.0" + }, + "abandoned": "psr/http-factory", + "time": "2023-04-14T14:16:17+00:00" + }, + { + "name": "php-http/promise", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/php-http/promise.git", + "reference": "fc85b1fba37c169a69a07ef0d5a8075770cc1f83" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/promise/zipball/fc85b1fba37c169a69a07ef0d5a8075770cc1f83", + "reference": "fc85b1fba37c169a69a07ef0d5a8075770cc1f83", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "friends-of-phpspec/phpspec-code-coverage": "^4.3.2 || ^6.3", + "phpspec/phpspec": "^5.1.2 || ^6.2 || ^7.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Joel Wurtz", + "email": "joel.wurtz@gmail.com" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Promise used for asynchronous HTTP requests", + "homepage": "http://httplug.io", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/php-http/promise/issues", + "source": "https://github.com/php-http/promise/tree/1.3.1" + }, + "time": "2024-03-15T13:55:21+00:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" + }, + "time": "2020-06-27T09:03:43+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "5.4.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "298d2febfe79d03fe714eb871d5538da55205b1a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/298d2febfe79d03fe714eb871d5538da55205b1a", + "reference": "298d2febfe79d03fe714eb871d5538da55205b1a", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.1", + "ext-filter": "*", + "php": "^7.4 || ^8.0", + "phpdocumentor/reflection-common": "^2.2", + "phpdocumentor/type-resolver": "^1.7", + "phpstan/phpdoc-parser": "^1.7", + "webmozart/assert": "^1.9.1" + }, + "require-dev": { + "mockery/mockery": "~1.3.5", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-webmozart-assert": "^1.2", + "phpunit/phpunit": "^9.5", + "vimeo/psalm": "^5.13" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + }, + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.4.0" + }, + "time": "2024-04-09T21:13:58+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "1.8.2", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "153ae662783729388a584b4361f2545e4d841e3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/153ae662783729388a584b4361f2545e4d841e3c", + "reference": "153ae662783729388a584b4361f2545e4d841e3c", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.0", + "php": "^7.3 || ^8.0", + "phpdocumentor/reflection-common": "^2.0", + "phpstan/phpdoc-parser": "^1.13" + }, + "require-dev": { + "ext-tokenizer": "*", + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.1", + "phpunit/phpunit": "^9.5", + "rector/rector": "^0.13.9", + "vimeo/psalm": "^4.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "support": { + "issues": "https://github.com/phpDocumentor/TypeResolver/issues", + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.8.2" + }, + "time": "2024-02-23T11:10:43+00:00" + }, + { + "name": "phpoption/phpoption", + "version": "1.9.2", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-option.git", + "reference": "80735db690fe4fc5c76dfa7f9b770634285fa820" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/80735db690fe4fc5c76dfa7f9b770634285fa820", + "reference": "80735db690fe4fc5c76dfa7f9b770634285fa820", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": true + }, + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpOption\\": "src/PhpOption/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com", + "homepage": "https://github.com/schmittjoh" + }, + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "Option Type for PHP", + "keywords": [ + "language", + "option", + "php", + "type" + ], + "support": { + "issues": "https://github.com/schmittjoh/php-option/issues", + "source": "https://github.com/schmittjoh/php-option/tree/1.9.2" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption", + "type": "tidelift" + } + ], + "time": "2023-11-12T21:59:55+00:00" + }, + { + "name": "phpseclib/phpseclib", + "version": "3.0.37", + "source": { + "type": "git", + "url": "https://github.com/phpseclib/phpseclib.git", + "reference": "cfa2013d0f68c062055180dd4328cc8b9d1f30b8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/cfa2013d0f68c062055180dd4328cc8b9d1f30b8", + "reference": "cfa2013d0f68c062055180dd4328cc8b9d1f30b8", + "shasum": "" + }, + "require": { + "paragonie/constant_time_encoding": "^1|^2", + "paragonie/random_compat": "^1.4|^2.0|^9.99.99", + "php": ">=5.6.1" + }, + "require-dev": { + "phpunit/phpunit": "*" + }, + "suggest": { + "ext-dom": "Install the DOM extension to load XML formatted public keys.", + "ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.", + "ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.", + "ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.", + "ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations." + }, + "type": "library", + "autoload": { + "files": [ + "phpseclib/bootstrap.php" + ], + "psr-4": { + "phpseclib3\\": "phpseclib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jim Wigginton", + "email": "terrafrost@php.net", + "role": "Lead Developer" + }, + { + "name": "Patrick Monnerat", + "email": "pm@datasphere.ch", + "role": "Developer" + }, + { + "name": "Andreas Fischer", + "email": "bantu@phpbb.com", + "role": "Developer" + }, + { + "name": "Hans-Jürgen Petrich", + "email": "petrich@tronic-media.com", + "role": "Developer" + }, + { + "name": "Graham Campbell", + "email": "graham@alt-three.com", + "role": "Developer" + } + ], + "description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.", + "homepage": "http://phpseclib.sourceforge.net", + "keywords": [ + "BigInteger", + "aes", + "asn.1", + "asn1", + "blowfish", + "crypto", + "cryptography", + "encryption", + "rsa", + "security", + "sftp", + "signature", + "signing", + "ssh", + "twofish", + "x.509", + "x509" + ], + "support": { + "issues": "https://github.com/phpseclib/phpseclib/issues", + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.37" + }, + "funding": [ + { + "url": "https://github.com/terrafrost", + "type": "github" + }, + { + "url": "https://www.patreon.com/phpseclib", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpseclib/phpseclib", + "type": "tidelift" + } + ], + "time": "2024-03-03T02:14:58+00:00" + }, + { + "name": "phpstan/phpdoc-parser", + "version": "1.28.0", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "cd06d6b1a1b3c75b0b83f97577869fd85a3cd4fb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/cd06d6b1a1b3c75b0b83f97577869fd85a3cd4fb", + "reference": "cd06d6b1a1b3c75b0b83f97577869fd85a3cd4fb", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^4.15", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^1.5", + "phpstan/phpstan-phpunit": "^1.1", + "phpstan/phpstan-strict-rules": "^1.0", + "phpunit/phpunit": "^9.5", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.28.0" + }, + "time": "2024-04-03T18:51:33+00:00" + }, + { + "name": "pragmarx/google2fa", + "version": "v8.0.1", + "source": { + "type": "git", + "url": "https://github.com/antonioribeiro/google2fa.git", + "reference": "80c3d801b31fe165f8fe99ea085e0a37834e1be3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/80c3d801b31fe165f8fe99ea085e0a37834e1be3", + "reference": "80c3d801b31fe165f8fe99ea085e0a37834e1be3", + "shasum": "" + }, + "require": { + "paragonie/constant_time_encoding": "^1.0|^2.0", + "php": "^7.1|^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.18", + "phpunit/phpunit": "^7.5.15|^8.5|^9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "PragmaRX\\Google2FA\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Antonio Carlos Ribeiro", + "email": "acr@antoniocarlosribeiro.com", + "role": "Creator & Designer" + } + ], + "description": "A One Time Password Authentication package, compatible with Google Authenticator.", + "keywords": [ + "2fa", + "Authentication", + "Two Factor Authentication", + "google2fa" + ], + "support": { + "issues": "https://github.com/antonioribeiro/google2fa/issues", + "source": "https://github.com/antonioribeiro/google2fa/tree/v8.0.1" + }, + "time": "2022-06-13T21:57:56+00:00" + }, + { + "name": "pragmarx/google2fa-laravel", + "version": "v2.2.0", + "source": { + "type": "git", + "url": "https://github.com/antonioribeiro/google2fa-laravel.git", + "reference": "0c3f5ee764d86fbb0af9f662d6ab927162199fc1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/antonioribeiro/google2fa-laravel/zipball/0c3f5ee764d86fbb0af9f662d6ab927162199fc1", + "reference": "0c3f5ee764d86fbb0af9f662d6ab927162199fc1", + "shasum": "" + }, + "require": { + "laravel/framework": "^5.4.36|^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", + "php": ">=7.0", + "pragmarx/google2fa-qrcode": "^1.0|^2.0|^3.0" + }, + "require-dev": { + "bacon/bacon-qr-code": "^2.0", + "orchestra/testbench": "3.4.*|3.5.*|3.6.*|3.7.*|4.*|5.*|6.*|7.*|8.*|9.*", + "phpunit/phpunit": "~5|~6|~7|~8|~9|~10" + }, + "suggest": { + "bacon/bacon-qr-code": "Required to generate inline QR Codes.", + "pragmarx/recovery": "Generate recovery codes." + }, + "type": "library", + "extra": { + "component": "package", + "frameworks": [ + "Laravel" + ], + "branch-alias": { + "dev-master": "0.2-dev" + }, + "laravel": { + "providers": [ + "PragmaRX\\Google2FALaravel\\ServiceProvider" + ], + "aliases": { + "Google2FA": "PragmaRX\\Google2FALaravel\\Facade" + } + } + }, + "autoload": { + "psr-4": { + "PragmaRX\\Google2FALaravel\\": "src/", + "PragmaRX\\Google2FALaravel\\Tests\\": "tests/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Antonio Carlos Ribeiro", + "email": "acr@antoniocarlosribeiro.com", + "role": "Creator & Designer" + } + ], + "description": "A One Time Password Authentication package, compatible with Google Authenticator.", + "keywords": [ + "Authentication", + "Two Factor Authentication", + "google2fa", + "laravel" + ], + "support": { + "issues": "https://github.com/antonioribeiro/google2fa-laravel/issues", + "source": "https://github.com/antonioribeiro/google2fa-laravel/tree/v2.2.0" + }, + "time": "2024-03-26T22:27:18+00:00" + }, + { + "name": "pragmarx/google2fa-qrcode", + "version": "v3.0.0", + "source": { + "type": "git", + "url": "https://github.com/antonioribeiro/google2fa-qrcode.git", + "reference": "ce4d8a729b6c93741c607cfb2217acfffb5bf76b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/antonioribeiro/google2fa-qrcode/zipball/ce4d8a729b6c93741c607cfb2217acfffb5bf76b", + "reference": "ce4d8a729b6c93741c607cfb2217acfffb5bf76b", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "pragmarx/google2fa": ">=4.0" + }, + "require-dev": { + "bacon/bacon-qr-code": "^2.0", + "chillerlan/php-qrcode": "^1.0|^2.0|^3.0|^4.0", + "khanamiryan/qrcode-detector-decoder": "^1.0", + "phpunit/phpunit": "~4|~5|~6|~7|~8|~9" + }, + "suggest": { + "bacon/bacon-qr-code": "For QR Code generation, requires imagick", + "chillerlan/php-qrcode": "For QR Code generation" + }, + "type": "library", + "extra": { + "component": "package", + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "PragmaRX\\Google2FAQRCode\\": "src/", + "PragmaRX\\Google2FAQRCode\\Tests\\": "tests/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Antonio Carlos Ribeiro", + "email": "acr@antoniocarlosribeiro.com", + "role": "Creator & Designer" + } + ], + "description": "QR Code package for Google2FA", + "keywords": [ + "2fa", + "Authentication", + "Two Factor Authentication", + "google2fa", + "qr code", + "qrcode" + ], + "support": { + "issues": "https://github.com/antonioribeiro/google2fa-qrcode/issues", + "source": "https://github.com/antonioribeiro/google2fa-qrcode/tree/v3.0.0" + }, + "time": "2021-08-15T12:53:48+00:00" + }, + { + "name": "pragmarx/random", + "version": "v0.2.2", + "source": { + "type": "git", + "url": "https://github.com/antonioribeiro/random.git", + "reference": "daf08a189c5d2d40d1a827db46364d3a741a51b7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/antonioribeiro/random/zipball/daf08a189c5d2d40d1a827db46364d3a741a51b7", + "reference": "daf08a189c5d2d40d1a827db46364d3a741a51b7", + "shasum": "" + }, + "require": { + "php": ">=7.0" + }, + "require-dev": { + "fzaninotto/faker": "~1.7", + "phpunit/phpunit": "~6.4", + "pragmarx/trivia": "~0.1", + "squizlabs/php_codesniffer": "^2.3" + }, + "suggest": { + "fzaninotto/faker": "Allows you to get dozens of randomized types", + "pragmarx/trivia": "For the trivia database" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "psr-4": { + "PragmaRX\\Random\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Antonio Carlos Ribeiro", + "email": "acr@antoniocarlosribeiro.com", + "homepage": "https://antoniocarlosribeiro.com", + "role": "Developer" + } + ], + "description": "Create random chars, numbers, strings", + "homepage": "https://github.com/antonioribeiro/random", + "keywords": [ + "Randomize", + "faker", + "pragmarx", + "random", + "random number", + "random pattern", + "random string" + ], + "support": { + "issues": "https://github.com/antonioribeiro/random/issues", + "source": "https://github.com/antonioribeiro/random/tree/master" + }, + "time": "2017-11-21T05:26:22+00:00" + }, + { + "name": "predis/predis", + "version": "v2.2.2", + "source": { + "type": "git", + "url": "https://github.com/predis/predis.git", + "reference": "b1d3255ed9ad4d7254f9f9bba386c99f4bb983d1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/predis/predis/zipball/b1d3255ed9ad4d7254f9f9bba386c99f4bb983d1", + "reference": "b1d3255ed9ad4d7254f9f9bba386c99f4bb983d1", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.3", + "phpstan/phpstan": "^1.9", + "phpunit/phpunit": "^8.0 || ~9.4.4" + }, + "suggest": { + "ext-relay": "Faster connection with in-memory caching (>=0.6.2)" + }, + "type": "library", + "autoload": { + "psr-4": { + "Predis\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Till Krüss", + "homepage": "https://till.im", + "role": "Maintainer" + } + ], + "description": "A flexible and feature-complete Redis client for PHP.", + "homepage": "http://github.com/predis/predis", + "keywords": [ + "nosql", + "predis", + "redis" + ], + "support": { + "issues": "https://github.com/predis/predis/issues", + "source": "https://github.com/predis/predis/tree/v2.2.2" + }, + "funding": [ + { + "url": "https://github.com/sponsors/tillkruss", + "type": "github" + } + ], + "time": "2023-09-13T16:42:03+00:00" + }, + { + "name": "psr/cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "support": { + "source": "https://github.com/php-fig/cache/tree/3.0.0" + }, + "time": "2021-02-03T23:26:27+00:00" + }, + { + "name": "psr/clock", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "time": "2022-11-25T14:36:26+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "e616d01114759c4c489f93b099585439f795fe35" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/e616d01114759c4c489f93b099585439f795fe35", + "reference": "e616d01114759c4c489f93b099585439f795fe35", + "shasum": "" + }, + "require": { + "php": ">=7.0.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory/tree/1.0.2" + }, + "time": "2023-04-10T20:10:41+00:00" + }, + { + "name": "psr/http-message", + "version": "1.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/cb6ce4845ce34a8ad9e68117c10ee90a29919eba", + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/1.1" + }, + "time": "2023-04-04T09:50:52+00:00" + }, + { + "name": "psr/log", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/fe5ea303b0887d5caefd3d431c3e61ad47037001", + "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.0" + }, + "time": "2021-07-14T16:46:02+00:00" + }, + { + "name": "psr/simple-cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "support": { + "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" + }, + "time": "2021-10-29T13:26:27+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "ramsey/collection", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/ramsey/collection.git", + "reference": "a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/collection/zipball/a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5", + "reference": "a4b48764bfbb8f3a6a4d1aeb1a35bb5e9ecac4a5", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "captainhook/plugin-composer": "^5.3", + "ergebnis/composer-normalize": "^2.28.3", + "fakerphp/faker": "^1.21", + "hamcrest/hamcrest-php": "^2.0", + "jangregor/phpstan-prophecy": "^1.0", + "mockery/mockery": "^1.5", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpcsstandards/phpcsutils": "^1.0.0-rc1", + "phpspec/prophecy-phpunit": "^2.0", + "phpstan/extension-installer": "^1.2", + "phpstan/phpstan": "^1.9", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^9.5", + "psalm/plugin-mockery": "^1.1", + "psalm/plugin-phpunit": "^0.18.4", + "ramsey/coding-standard": "^2.0.3", + "ramsey/conventional-commits": "^1.3", + "vimeo/psalm": "^5.4" + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + }, + "ramsey/conventional-commits": { + "configFile": "conventional-commits.json" + } + }, + "autoload": { + "psr-4": { + "Ramsey\\Collection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ben Ramsey", + "email": "ben@benramsey.com", + "homepage": "https://benramsey.com" + } + ], + "description": "A PHP library for representing and manipulating collections.", + "keywords": [ + "array", + "collection", + "hash", + "map", + "queue", + "set" + ], + "support": { + "issues": "https://github.com/ramsey/collection/issues", + "source": "https://github.com/ramsey/collection/tree/2.0.0" + }, + "funding": [ + { + "url": "https://github.com/ramsey", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/ramsey/collection", + "type": "tidelift" + } + ], + "time": "2022-12-31T21:50:55+00:00" + }, + { + "name": "ramsey/uuid", + "version": "4.7.6", + "source": { + "type": "git", + "url": "https://github.com/ramsey/uuid.git", + "reference": "91039bc1faa45ba123c4328958e620d382ec7088" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/91039bc1faa45ba123c4328958e620d382ec7088", + "reference": "91039bc1faa45ba123c4328958e620d382ec7088", + "shasum": "" + }, + "require": { + "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12", + "ext-json": "*", + "php": "^8.0", + "ramsey/collection": "^1.2 || ^2.0" + }, + "replace": { + "rhumsaa/uuid": "self.version" + }, + "require-dev": { + "captainhook/captainhook": "^5.10", + "captainhook/plugin-composer": "^5.3", + "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", + "doctrine/annotations": "^1.8", + "ergebnis/composer-normalize": "^2.15", + "mockery/mockery": "^1.3", + "paragonie/random-lib": "^2", + "php-mock/php-mock": "^2.2", + "php-mock/php-mock-mockery": "^1.3", + "php-parallel-lint/php-parallel-lint": "^1.1", + "phpbench/phpbench": "^1.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-phpunit": "^1.1", + "phpunit/phpunit": "^8.5 || ^9", + "ramsey/composer-repl": "^1.4", + "slevomat/coding-standard": "^8.4", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^4.9" + }, + "suggest": { + "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", + "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.", + "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.", + "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", + "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Ramsey\\Uuid\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).", + "keywords": [ + "guid", + "identifier", + "uuid" + ], + "support": { + "issues": "https://github.com/ramsey/uuid/issues", + "source": "https://github.com/ramsey/uuid/tree/4.7.6" + }, + "funding": [ + { + "url": "https://github.com/ramsey", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/ramsey/uuid", + "type": "tidelift" + } + ], + "time": "2024-04-27T21:32:50+00:00" + }, + { + "name": "rinvex/countries", + "version": "v8.1.2", + "source": { + "type": "git", + "url": "https://github.com/rinvex/countries.git", + "reference": "b012697307453e7d9d2df25bd385904b3e2e9baf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/rinvex/countries/zipball/b012697307453e7d9d2df25bd385904b3e2e9baf", + "reference": "b012697307453e7d9d2df25bd385904b3e2e9baf", + "shasum": "" + }, + "require": { + "php": "^8.0.0" + }, + "require-dev": { + "codedungeon/phpunit-result-printer": "^0.31.0", + "phpunit/phpunit": "^9.5.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Rinvex\\Country\\Providers\\CountryServiceProvider" + ] + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Rinvex\\Country\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Rinvex LLC", + "email": "help@rinvex.com", + "homepage": "https://rinvex.com" + }, + { + "name": "Abdelrahman Omran", + "email": "me@omranic.com", + "homepage": "https://omranic.com", + "role": "Project Lead" + }, + { + "name": "The Generous PHP Community", + "homepage": "https://github.com/rinvex/countries/contributors" + } + ], + "description": "Rinvex Countries is a simple and lightweight package for retrieving country details with flexibility. A whole bunch of data including name, demonym, capital, iso codes, dialling codes, geo data, currencies, flags, emoji, and other attributes for all 250 countries worldwide at your fingertips.", + "homepage": "https://rinvex.com", + "keywords": [ + "Flexible", + "Simple", + "countries", + "country", + "currencies", + "demonym", + "dialling", + "emoji", + "flags", + "geographic", + "languages", + "rinvex", + "svg" + ], + "support": { + "docs": "https://github.com/rinvex/countries/blob/master/README.md", + "email": "help@rinvex.com", + "issues": "https://github.com/rinvex/countries/issues", + "source": "https://github.com/rinvex/countries" + }, + "time": "2022-12-29T20:30:43+00:00" + }, + { + "name": "sabberworm/php-css-parser", + "version": "v8.5.1", + "source": { + "type": "git", + "url": "https://github.com/MyIntervals/PHP-CSS-Parser.git", + "reference": "4a3d572b0f8b28bb6fd016ae8bbfc445facef152" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/4a3d572b0f8b28bb6fd016ae8bbfc445facef152", + "reference": "4a3d572b0f8b28bb6fd016ae8bbfc445facef152", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=5.6.20" + }, + "require-dev": { + "phpunit/phpunit": "^5.7.27" + }, + "suggest": { + "ext-mbstring": "for parsing UTF-8 CSS" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "9.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Sabberworm\\CSS\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Raphael Schweikert" + }, + { + "name": "Oliver Klee", + "email": "github@oliverklee.de" + }, + { + "name": "Jake Hotson", + "email": "jake.github@qzdesign.co.uk" + } + ], + "description": "Parser for CSS Files written in PHP", + "homepage": "https://www.sabberworm.com/blog/2010/6/10/php-css-parser", + "keywords": [ + "css", + "parser", + "stylesheet" + ], + "support": { + "issues": "https://github.com/MyIntervals/PHP-CSS-Parser/issues", + "source": "https://github.com/MyIntervals/PHP-CSS-Parser/tree/v8.5.1" + }, + "time": "2024-02-15T16:41:13+00:00" + }, + { + "name": "sabre/dav", + "version": "4.6.0", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/dav.git", + "reference": "554145304b4a026477d130928d16e626939b0b2a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/dav/zipball/554145304b4a026477d130928d16e626939b0b2a", + "reference": "554145304b4a026477d130928d16e626939b0b2a", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-date": "*", + "ext-dom": "*", + "ext-iconv": "*", + "ext-json": "*", + "ext-mbstring": "*", + "ext-pcre": "*", + "ext-simplexml": "*", + "ext-spl": "*", + "lib-libxml": ">=2.7.0", + "php": "^7.1.0 || ^8.0", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "sabre/event": "^5.0", + "sabre/http": "^5.0.5", + "sabre/uri": "^2.0", + "sabre/vobject": "^4.2.1", + "sabre/xml": "^2.0.1" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.19", + "monolog/monolog": "^1.27 || ^2.0", + "phpstan/phpstan": "^0.12 || ^1.0", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6" + }, + "suggest": { + "ext-curl": "*", + "ext-imap": "*", + "ext-pdo": "*" + }, + "bin": [ + "bin/sabredav", + "bin/naturalselection" + ], + "type": "library", + "autoload": { + "psr-4": { + "Sabre\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + } + ], + "description": "WebDAV Framework for PHP", + "homepage": "http://sabre.io/", + "keywords": [ + "CalDAV", + "CardDAV", + "WebDAV", + "framework", + "iCalendar" + ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/dav/issues", + "source": "https://github.com/fruux/sabre-dav" + }, + "time": "2023-12-11T13:01:23+00:00" + }, + { + "name": "sabre/event", + "version": "5.1.4", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/event.git", + "reference": "d7da22897125d34d7eddf7977758191c06a74497" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/event/zipball/d7da22897125d34d7eddf7977758191c06a74497", + "reference": "d7da22897125d34d7eddf7977758191c06a74497", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.17.1", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.0" + }, + "type": "library", + "autoload": { + "files": [ + "lib/coroutine.php", + "lib/Loop/functions.php", + "lib/Promise/functions.php" + ], + "psr-4": { + "Sabre\\Event\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + } + ], + "description": "sabre/event is a library for lightweight event-based programming", + "homepage": "http://sabre.io/event/", + "keywords": [ + "EventEmitter", + "async", + "coroutine", + "eventloop", + "events", + "hooks", + "plugin", + "promise", + "reactor", + "signal" + ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/event/issues", + "source": "https://github.com/fruux/sabre-event" + }, + "time": "2021-11-04T06:51:17+00:00" + }, + { + "name": "sabre/http", + "version": "5.1.10", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/http.git", + "reference": "f9f3d1fba8916fa2f4ec25636c4fedc26cb94e02" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/http/zipball/f9f3d1fba8916fa2f4ec25636c4fedc26cb94e02", + "reference": "f9f3d1fba8916fa2f4ec25636c4fedc26cb94e02", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-curl": "*", + "ext-mbstring": "*", + "php": "^7.1 || ^8.0", + "sabre/event": ">=4.0 <6.0", + "sabre/uri": "^2.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.17.1", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.0" + }, + "suggest": { + "ext-curl": " to make http requests with the Client class" + }, + "type": "library", + "autoload": { + "files": [ + "lib/functions.php" + ], + "psr-4": { + "Sabre\\HTTP\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + } + ], + "description": "The sabre/http library provides utilities for dealing with http requests and responses. ", + "homepage": "https://github.com/fruux/sabre-http", + "keywords": [ + "http" + ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/http/issues", + "source": "https://github.com/fruux/sabre-http" + }, + "time": "2023-08-18T01:55:28+00:00" + }, + { + "name": "sabre/uri", + "version": "2.3.3", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/uri.git", + "reference": "7e0e7dfd0b7e14346a27eabd66e843a6e7f1812b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/uri/zipball/7e0e7dfd0b7e14346a27eabd66e843a6e7f1812b", + "reference": "7e0e7dfd0b7e14346a27eabd66e843a6e7f1812b", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.17", + "phpstan/extension-installer": "^1.3", + "phpstan/phpstan": "^1.10", + "phpstan/phpstan-phpunit": "^1.3", + "phpstan/phpstan-strict-rules": "^1.5", + "phpunit/phpunit": "^9.6" + }, + "type": "library", + "autoload": { + "files": [ + "lib/functions.php" + ], + "psr-4": { + "Sabre\\Uri\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + } + ], + "description": "Functions for making sense out of URIs.", + "homepage": "http://sabre.io/uri/", + "keywords": [ + "rfc3986", + "uri", + "url" + ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/uri/issues", + "source": "https://github.com/fruux/sabre-uri" + }, + "time": "2023-06-09T06:54:04+00:00" + }, + { + "name": "sabre/vobject", + "version": "4.5.4", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/vobject.git", + "reference": "a6d53a3e5bec85ed3dd78868b7de0f5b4e12f772" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/vobject/zipball/a6d53a3e5bec85ed3dd78868b7de0f5b4e12f772", + "reference": "a6d53a3e5bec85ed3dd78868b7de0f5b4e12f772", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^7.1 || ^8.0", + "sabre/xml": "^2.1 || ^3.0 || ^4.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.17.1", + "phpstan/phpstan": "^0.12", + "phpunit/php-invoker": "^2.0 || ^3.1", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.0" + }, + "suggest": { + "hoa/bench": "If you would like to run the benchmark scripts" + }, + "bin": [ + "bin/vobject", + "bin/generate_vcards" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Sabre\\VObject\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + }, + { + "name": "Dominik Tobschall", + "email": "dominik@fruux.com", + "homepage": "http://tobschall.de/", + "role": "Developer" + }, + { + "name": "Ivan Enderlin", + "email": "ivan.enderlin@hoa-project.net", + "homepage": "http://mnt.io/", + "role": "Developer" + } + ], + "description": "The VObject library for PHP allows you to easily parse and manipulate iCalendar and vCard objects", + "homepage": "http://sabre.io/vobject/", + "keywords": [ + "availability", + "freebusy", + "iCalendar", + "ical", + "ics", + "jCal", + "jCard", + "recurrence", + "rfc2425", + "rfc2426", + "rfc2739", + "rfc4770", + "rfc5545", + "rfc5546", + "rfc6321", + "rfc6350", + "rfc6351", + "rfc6474", + "rfc6638", + "rfc6715", + "rfc6868", + "vCalendar", + "vCard", + "vcf", + "xCal", + "xCard" + ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/vobject/issues", + "source": "https://github.com/fruux/sabre-vobject" + }, + "time": "2023-11-09T12:54:37+00:00" + }, + { + "name": "sabre/xml", + "version": "2.2.7", + "source": { + "type": "git", + "url": "https://github.com/sabre-io/xml.git", + "reference": "f1d53d55976bbd4cf3e640dda6ebc31120c71a4e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sabre-io/xml/zipball/f1d53d55976bbd4cf3e640dda6ebc31120c71a4e", + "reference": "f1d53d55976bbd4cf3e640dda6ebc31120c71a4e", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-xmlreader": "*", + "ext-xmlwriter": "*", + "lib-libxml": ">=2.6.20", + "php": "^7.1 || ^8.0", + "sabre/uri": ">=1.0,<3.0.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.17.1", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.0" + }, + "type": "library", + "autoload": { + "files": [ + "lib/Deserializer/functions.php", + "lib/Serializer/functions.php" + ], + "psr-4": { + "Sabre\\Xml\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Evert Pot", + "email": "me@evertpot.com", + "homepage": "http://evertpot.com/", + "role": "Developer" + }, + { + "name": "Markus Staab", + "email": "markus.staab@redaxo.de", + "role": "Developer" + } + ], + "description": "sabre/xml is an XML library that you may not hate.", + "homepage": "https://sabre.io/xml/", + "keywords": [ + "XMLReader", + "XMLWriter", + "dom", + "xml" + ], + "support": { + "forum": "https://groups.google.com/group/sabredav-discuss", + "issues": "https://github.com/sabre-io/xml/issues", + "source": "https://github.com/fruux/sabre-xml" + }, + "time": "2024-04-18T10:15:43+00:00" + }, + { + "name": "sentry/sdk", + "version": "3.6.0", + "source": { + "type": "git", + "url": "https://github.com/getsentry/sentry-php-sdk.git", + "reference": "24c235ff2027401cbea099bf88689e1a1f197c7a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/getsentry/sentry-php-sdk/zipball/24c235ff2027401cbea099bf88689e1a1f197c7a", + "reference": "24c235ff2027401cbea099bf88689e1a1f197c7a", + "shasum": "" + }, + "require": { + "http-interop/http-factory-guzzle": "^1.0", + "sentry/sentry": "^3.22", + "symfony/http-client": "^4.3|^5.0|^6.0|^7.0" + }, + "type": "metapackage", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sentry", + "email": "accounts@sentry.io" + } + ], + "description": "This is a metapackage shipping sentry/sentry with a recommended HTTP client.", + "homepage": "http://sentry.io", + "keywords": [ + "crash-reporting", + "crash-reports", + "error-handler", + "error-monitoring", + "log", + "logging", + "sentry" + ], + "support": { + "issues": "https://github.com/getsentry/sentry-php-sdk/issues", + "source": "https://github.com/getsentry/sentry-php-sdk/tree/3.6.0" + }, + "funding": [ + { + "url": "https://sentry.io/", + "type": "custom" + }, + { + "url": "https://sentry.io/pricing/", + "type": "custom" + } + ], + "time": "2023-12-04T10:49:33+00:00" + }, + { + "name": "sentry/sentry", + "version": "3.22.1", + "source": { + "type": "git", + "url": "https://github.com/getsentry/sentry-php.git", + "reference": "8859631ba5ab15bc1af420b0eeed19ecc6c9d81d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/getsentry/sentry-php/zipball/8859631ba5ab15bc1af420b0eeed19ecc6c9d81d", + "reference": "8859631ba5ab15bc1af420b0eeed19ecc6c9d81d", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "guzzlehttp/promises": "^1.5.3|^2.0", + "jean85/pretty-package-versions": "^1.5|^2.0.4", + "php": "^7.2|^8.0", + "php-http/async-client-implementation": "^1.0", + "php-http/client-common": "^1.5|^2.0", + "php-http/discovery": "^1.15", + "php-http/httplug": "^1.1|^2.0", + "php-http/message": "^1.5", + "php-http/message-factory": "^1.1", + "psr/http-factory": "^1.0", + "psr/http-factory-implementation": "^1.0", + "psr/log": "^1.0|^2.0|^3.0", + "symfony/options-resolver": "^3.4.43|^4.4.30|^5.0.11|^6.0|^7.0", + "symfony/polyfill-php80": "^1.17" + }, + "conflict": { + "php-http/client-common": "1.8.0", + "raven/raven": "*" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.19|3.4.*", + "guzzlehttp/psr7": "^1.8.4|^2.1.1", + "http-interop/http-factory-guzzle": "^1.0", + "monolog/monolog": "^1.6|^2.0|^3.0", + "nikic/php-parser": "^4.10.3", + "php-http/mock-client": "^1.3", + "phpbench/phpbench": "^1.0", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^1.3", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^8.5.14|^9.4", + "symfony/phpunit-bridge": "^5.2|^6.0", + "vimeo/psalm": "^4.17" + }, + "suggest": { + "monolog/monolog": "Allow sending log messages to Sentry by using the included Monolog handler." + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Sentry\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sentry", + "email": "accounts@sentry.io" + } + ], + "description": "A PHP SDK for Sentry (http://sentry.io)", + "homepage": "http://sentry.io", + "keywords": [ + "crash-reporting", + "crash-reports", + "error-handler", + "error-monitoring", + "log", + "logging", + "sentry" + ], + "support": { + "issues": "https://github.com/getsentry/sentry-php/issues", + "source": "https://github.com/getsentry/sentry-php/tree/3.22.1" + }, + "funding": [ + { + "url": "https://sentry.io/", + "type": "custom" + }, + { + "url": "https://sentry.io/pricing/", + "type": "custom" + } + ], + "time": "2023-11-13T11:47:28+00:00" + }, + { + "name": "sentry/sentry-laravel", + "version": "2.14.2", + "source": { + "type": "git", + "url": "https://github.com/getsentry/sentry-laravel.git", + "reference": "4538ed31d77868dd3b6d72ad6e5e68b572beeb9f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/getsentry/sentry-laravel/zipball/4538ed31d77868dd3b6d72ad6e5e68b572beeb9f", + "reference": "4538ed31d77868dd3b6d72ad6e5e68b572beeb9f", + "shasum": "" + }, + "require": { + "illuminate/support": "5.0 - 5.8 | ^6.0 | ^7.0 | ^8.0 | ^9.0", + "nyholm/psr7": "^1.0", + "php": "^7.2 | ^8.0", + "sentry/sdk": "^3.1", + "sentry/sentry": "^3.3", + "symfony/psr-http-message-bridge": "^1.0 | ^2.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.11", + "laravel/framework": "5.0 - 5.8 | ^6.0 | ^7.0 | ^8.0 | ^9.0", + "mockery/mockery": "^1.3", + "orchestra/testbench": "3.1 - 3.8 | ^4.7 | ^5.1 | ^6.0 | ^7.0", + "phpunit/phpunit": "^5.7 | ^6.5 | ^7.5 | ^8.4 | ^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev", + "dev-0.x": "0.x-dev" + }, + "laravel": { + "providers": [ + "Sentry\\Laravel\\ServiceProvider", + "Sentry\\Laravel\\Tracing\\ServiceProvider" + ], + "aliases": { + "Sentry": "Sentry\\Laravel\\Facade" + } + } + }, + "autoload": { + "psr-0": { + "Sentry\\Laravel\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Sentry", + "email": "accounts@sentry.io" + } + ], + "description": "Laravel SDK for Sentry (https://sentry.io)", + "homepage": "https://sentry.io", + "keywords": [ + "crash-reporting", + "crash-reports", + "error-handler", + "error-monitoring", + "laravel", + "log", + "logging", + "sentry" + ], + "support": { + "issues": "https://github.com/getsentry/sentry-laravel/issues", + "source": "https://github.com/getsentry/sentry-laravel/tree/2.14.2" + }, + "funding": [ + { + "url": "https://sentry.io/", + "type": "custom" + }, + { + "url": "https://sentry.io/pricing/", + "type": "custom" + } + ], + "time": "2022-10-13T09:21:29+00:00" + }, + { + "name": "spatie/macroable", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/macroable.git", + "reference": "ec2c320f932e730607aff8052c44183cf3ecb072" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/macroable/zipball/ec2c320f932e730607aff8052c44183cf3ecb072", + "reference": "ec2c320f932e730607aff8052c44183cf3ecb072", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^8.0|^9.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\Macroable\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + } + ], + "description": "A trait to dynamically add methods to a class", + "homepage": "https://github.com/spatie/macroable", + "keywords": [ + "macroable", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/macroable/issues", + "source": "https://github.com/spatie/macroable/tree/2.0.0" + }, + "time": "2021-03-26T22:39:02+00:00" + }, + { + "name": "spomky-labs/cbor-php", + "version": "3.0.4", + "source": { + "type": "git", + "url": "https://github.com/Spomky-Labs/cbor-php.git", + "reference": "658ed12a85a6b31fa312b89cd92f3a4ce6df4c6b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Spomky-Labs/cbor-php/zipball/658ed12a85a6b31fa312b89cd92f3a4ce6df4c6b", + "reference": "658ed12a85a6b31fa312b89cd92f3a4ce6df4c6b", + "shasum": "" + }, + "require": { + "brick/math": "^0.9|^0.10|^0.11|^0.12", + "ext-mbstring": "*", + "php": ">=8.0" + }, + "require-dev": { + "ekino/phpstan-banned-code": "^1.0", + "ext-json": "*", + "infection/infection": "^0.27", + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-beberlei-assert": "^1.0", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.0", + "phpstan/phpstan-strict-rules": "^1.0", + "phpunit/phpunit": "^10.1", + "qossmic/deptrac-shim": "^1.0", + "rector/rector": "^0.19", + "roave/security-advisories": "dev-latest", + "symfony/var-dumper": "^6.0|^7.0", + "symplify/easy-coding-standard": "^12.0" + }, + "suggest": { + "ext-bcmath": "GMP or BCMath extensions will drastically improve the library performance. BCMath extension needed to handle the Big Float and Decimal Fraction Tags", + "ext-gmp": "GMP or BCMath extensions will drastically improve the library performance" + }, + "type": "library", + "autoload": { + "psr-4": { + "CBOR\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/Spomky-Labs/cbor-php/contributors" + } + ], + "description": "CBOR Encoder/Decoder for PHP", + "keywords": [ + "Concise Binary Object Representation", + "RFC7049", + "cbor" + ], + "support": { + "issues": "https://github.com/Spomky-Labs/cbor-php/issues", + "source": "https://github.com/Spomky-Labs/cbor-php/tree/3.0.4" + }, + "funding": [ + { + "url": "https://github.com/Spomky", + "type": "github" + }, + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2024-01-29T20:33:48+00:00" + }, + { + "name": "spomky-labs/pki-framework", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/Spomky-Labs/pki-framework.git", + "reference": "0b10c8b53366729417d6226ae89a665f9e2d61b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Spomky-Labs/pki-framework/zipball/0b10c8b53366729417d6226ae89a665f9e2d61b6", + "reference": "0b10c8b53366729417d6226ae89a665f9e2d61b6", + "shasum": "" + }, + "require": { + "brick/math": "^0.10|^0.11|^0.12", + "ext-mbstring": "*", + "php": ">=8.1" + }, + "require-dev": { + "ekino/phpstan-banned-code": "^1.0", + "ext-gmp": "*", + "ext-openssl": "*", + "infection/infection": "^0.28", + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpstan/extension-installer": "^1.3", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-beberlei-assert": "^1.0", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.1", + "phpstan/phpstan-strict-rules": "^1.3", + "phpunit/phpunit": "^10.1|^11.0", + "rector/rector": "^1.0", + "roave/security-advisories": "dev-latest", + "symfony/phpunit-bridge": "^6.4|^7.0", + "symfony/string": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0", + "symplify/easy-coding-standard": "^12.0" + }, + "suggest": { + "ext-bcmath": "For better performance (or GMP)", + "ext-gmp": "For better performance (or BCMath)", + "ext-openssl": "For OpenSSL based cyphering" + }, + "type": "library", + "autoload": { + "psr-4": { + "SpomkyLabs\\Pki\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Joni Eskelinen", + "email": "jonieske@gmail.com", + "role": "Original developer" + }, + { + "name": "Florent Morselli", + "email": "florent.morselli@spomky-labs.com", + "role": "Spomky-Labs PKI Framework developer" + } + ], + "description": "A PHP framework for managing Public Key Infrastructures. It comprises X.509 public key certificates, attribute certificates, certification requests and certification path validation.", + "homepage": "https://github.com/spomky-labs/pki-framework", + "keywords": [ + "DER", + "Private Key", + "ac", + "algorithm identifier", + "asn.1", + "asn1", + "attribute certificate", + "certificate", + "certification request", + "cryptography", + "csr", + "decrypt", + "ec", + "encrypt", + "pem", + "pkcs", + "public key", + "rsa", + "sign", + "signature", + "verify", + "x.509", + "x.690", + "x509", + "x690" + ], + "support": { + "issues": "https://github.com/Spomky-Labs/pki-framework/issues", + "source": "https://github.com/Spomky-Labs/pki-framework/tree/1.2.1" + }, + "funding": [ + { + "url": "https://github.com/Spomky", + "type": "github" + }, + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2024-03-30T18:03:49+00:00" + }, + { + "name": "stevebauman/location", + "version": "v6.6.2", + "source": { + "type": "git", + "url": "https://github.com/stevebauman/location.git", + "reference": "49f28e58daa0382bdc571b20f27b3c57c8691f1c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/stevebauman/location/zipball/49f28e58daa0382bdc571b20f27b3c57c8691f1c", + "reference": "49f28e58daa0382bdc571b20f27b3c57c8691f1c", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "geoip2/geoip2": "^2.0", + "illuminate/support": "^5.0|^6.0|^7.0|^8.0|^9.0|^10.0", + "php": ">=7.3" + }, + "require-dev": { + "mockery/mockery": "~0.9|^1.0", + "orchestra/testbench": "~3.2|~4.0|^6.0|^7.0|^8.0", + "pestphp/pest": "^1.21" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Stevebauman\\Location\\LocationServiceProvider" + ], + "aliases": { + "Location": "Stevebauman\\Location\\Facades\\Location" + } + } + }, + "autoload": { + "psr-4": { + "Stevebauman\\Location\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Steve Bauman", + "email": "steven_bauman@outlook.com" + } + ], + "description": "Retrieve a user's location by their IP Address", + "keywords": [ + "IP", + "geo", + "geo-location", + "geoip", + "laravel", + "location", + "php" + ], + "support": { + "issues": "https://github.com/stevebauman/location/issues", + "source": "https://github.com/stevebauman/location/tree/v6.6.2" + }, + "time": "2023-02-27T14:26:23+00:00" + }, + { + "name": "stripe/stripe-php", + "version": "v9.9.0", + "source": { + "type": "git", + "url": "https://github.com/stripe/stripe-php.git", + "reference": "479b5c2136fde0debb93d290ceaf20dd161c358f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/stripe/stripe-php/zipball/479b5c2136fde0debb93d290ceaf20dd161c358f", + "reference": "479b5c2136fde0debb93d290ceaf20dd161c358f", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-mbstring": "*", + "php": ">=5.6.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "3.5.0", + "php-coveralls/php-coveralls": "^2.5", + "phpstan/phpstan": "^1.2", + "phpunit/phpunit": "^5.7 || ^9.0", + "squizlabs/php_codesniffer": "^3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Stripe\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Stripe and contributors", + "homepage": "https://github.com/stripe/stripe-php/contributors" + } + ], + "description": "Stripe PHP Library", + "homepage": "https://stripe.com/", + "keywords": [ + "api", + "payment processing", + "stripe" + ], + "support": { + "issues": "https://github.com/stripe/stripe-php/issues", + "source": "https://github.com/stripe/stripe-php/tree/v9.9.0" + }, + "time": "2022-11-08T20:25:52+00:00" + }, + { + "name": "symfony/console", + "version": "v6.4.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "a170e64ae10d00ba89e2acbb590dc2e54da8ad8f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/a170e64ae10d00ba89e2acbb590dc2e54da8ad8f", + "reference": "a170e64ae10d00ba89e2acbb590dc2e54da8ad8f", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^5.4|^6.0|^7.0" + }, + "conflict": { + "symfony/dependency-injection": "<5.4", + "symfony/dotenv": "<5.4", + "symfony/event-dispatcher": "<5.4", + "symfony/lock": "<5.4", + "symfony/process": "<5.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/lock": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/stopwatch": "^5.4|^6.0|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v6.4.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-04-18T09:22:46+00:00" + }, + { + "name": "symfony/css-selector", + "version": "v7.0.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/css-selector.git", + "reference": "b08a4ad89e84b29cec285b7b1f781a7ae51cf4bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/b08a4ad89e84b29cec285b7b1f781a7ae51cf4bc", + "reference": "b08a4ad89e84b29cec285b7b1f781a7ae51cf4bc", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\CssSelector\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Jean-François Simon", + "email": "jeanfrancois.simon@sensiolabs.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Converts CSS selectors to XPath expressions", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/css-selector/tree/v7.0.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-04-18T09:29:19+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/7c3aff79d10325257a001fcf92d991f24fc967cf", + "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.4.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-05-23T14:45:45+00:00" + }, + { + "name": "symfony/error-handler", + "version": "v6.4.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/error-handler.git", + "reference": "667a072466c6a53827ed7b119af93806b884cbb3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/667a072466c6a53827ed7b119af93806b884cbb3", + "reference": "667a072466c6a53827ed7b119af93806b884cbb3", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^1|^2|^3", + "symfony/var-dumper": "^5.4|^6.0|^7.0" + }, + "conflict": { + "symfony/deprecation-contracts": "<2.5", + "symfony/http-kernel": "<6.4" + }, + "require-dev": { + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/serializer": "^5.4|^6.0|^7.0" + }, + "bin": [ + "Resources/bin/patch-type-declarations" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\ErrorHandler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to manage errors and ease debugging PHP code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/error-handler/tree/v6.4.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-04-18T09:22:46+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v7.0.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "db2a7fab994d67d92356bb39c367db115d9d30f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/db2a7fab994d67d92356bb39c367db115d9d30f9", + "reference": "db2a7fab994d67d92356bb39c367db115d9d30f9", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/event-dispatcher-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/error-handler": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v7.0.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-04-18T09:29:19+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.4.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "4e64b49bf370ade88e567de29465762e316e4224" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/4e64b49bf370ade88e567de29465762e316e4224", + "reference": "4e64b49bf370ade88e567de29465762e316e4224", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.4.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-23T14:51:35+00:00" + }, + { + "name": "symfony/finder", + "version": "v6.4.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "511c48990be17358c23bf45c5d71ab85d40fb764" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/511c48990be17358c23bf45c5d71ab85d40fb764", + "reference": "511c48990be17358c23bf45c5d71ab85d40fb764", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "symfony/filesystem": "^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v6.4.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-04-23T10:36:43+00:00" + }, + { + "name": "symfony/http-client", + "version": "v6.4.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client.git", + "reference": "3683d8107cf1efdd24795cc5f7482be1eded34ac" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client/zipball/3683d8107cf1efdd24795cc5f7482be1eded34ac", + "reference": "3683d8107cf1efdd24795cc5f7482be1eded34ac", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-client-contracts": "^3.4.1", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "php-http/discovery": "<1.15", + "symfony/http-foundation": "<6.3" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "1.0", + "symfony/http-client-implementation": "3.0" + }, + "require-dev": { + "amphp/amp": "^2.5", + "amphp/http-client": "^4.2.1", + "amphp/http-tunnel": "^1.0", + "amphp/socket": "^1.1", + "guzzlehttp/promises": "^1.4|^2.0", + "nyholm/psr7": "^1.0", + "php-http/httplug": "^1.0|^2.0", + "psr/http-client": "^1.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/stopwatch": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpClient\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously", + "homepage": "https://symfony.com", + "keywords": [ + "http" + ], + "support": { + "source": "https://github.com/symfony/http-client/tree/v6.4.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-04-18T09:22:46+00:00" + }, + { + "name": "symfony/http-client-contracts", + "version": "v3.4.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client-contracts.git", + "reference": "b6b5c876b3a4ed74460e2c5ac53bbce2f12e2a7e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/b6b5c876b3a4ed74460e2c5ac53bbce2f12e2a7e", + "reference": "b6b5c876b3a4ed74460e2c5ac53bbce2f12e2a7e", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\HttpClient\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to HTTP clients", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/http-client-contracts/tree/v3.4.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-04-01T18:51:09+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v6.4.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "b4db6b833035477cb70e18d0ae33cb7c2b521759" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/b4db6b833035477cb70e18d0ae33cb7c2b521759", + "reference": "b4db6b833035477cb70e18d0ae33cb7c2b521759", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.1", + "symfony/polyfill-php83": "^1.27" + }, + "conflict": { + "symfony/cache": "<6.3" + }, + "require-dev": { + "doctrine/dbal": "^2.13.1|^3|^4", + "predis/predis": "^1.1|^2.0", + "symfony/cache": "^6.3|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4|^7.0", + "symfony/mime": "^5.4|^6.0|^7.0", + "symfony/rate-limiter": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Defines an object-oriented layer for the HTTP specification", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-foundation/tree/v6.4.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-04-18T09:22:46+00:00" + }, + { + "name": "symfony/http-kernel", + "version": "v6.4.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-kernel.git", + "reference": "b7b5e6cdef670a0c82d015a966ffc7e855861a98" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/b7b5e6cdef670a0c82d015a966ffc7e855861a98", + "reference": "b7b5e6cdef670a0c82d015a966ffc7e855861a98", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/error-handler": "^6.4|^7.0", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/browser-kit": "<5.4", + "symfony/cache": "<5.4", + "symfony/config": "<6.1", + "symfony/console": "<5.4", + "symfony/dependency-injection": "<6.4", + "symfony/doctrine-bridge": "<5.4", + "symfony/form": "<5.4", + "symfony/http-client": "<5.4", + "symfony/http-client-contracts": "<2.5", + "symfony/mailer": "<5.4", + "symfony/messenger": "<5.4", + "symfony/translation": "<5.4", + "symfony/translation-contracts": "<2.5", + "symfony/twig-bridge": "<5.4", + "symfony/validator": "<6.4", + "symfony/var-dumper": "<6.3", + "twig/twig": "<2.13" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/browser-kit": "^5.4|^6.0|^7.0", + "symfony/clock": "^6.2|^7.0", + "symfony/config": "^6.1|^7.0", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/css-selector": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/dom-crawler": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/finder": "^5.4|^6.0|^7.0", + "symfony/http-client-contracts": "^2.5|^3", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/property-access": "^5.4.5|^6.0.5|^7.0", + "symfony/routing": "^5.4|^6.0|^7.0", + "symfony/serializer": "^6.4.4|^7.0.4", + "symfony/stopwatch": "^5.4|^6.0|^7.0", + "symfony/translation": "^5.4|^6.0|^7.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/uid": "^5.4|^6.0|^7.0", + "symfony/validator": "^6.4|^7.0", + "symfony/var-dumper": "^5.4|^6.4|^7.0", + "symfony/var-exporter": "^6.2|^7.0", + "twig/twig": "^2.13|^3.0.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpKernel\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a structured process for converting a Request into a Response", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-kernel/tree/v6.4.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-04-29T11:24:44+00:00" + }, + { + "name": "symfony/mailer", + "version": "v6.4.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/mailer.git", + "reference": "2c446d4e446995bed983c0b5bb9ff837e8de7dbd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mailer/zipball/2c446d4e446995bed983c0b5bb9ff837e8de7dbd", + "reference": "2c446d4e446995bed983c0b5bb9ff837e8de7dbd", + "shasum": "" + }, + "require": { + "egulias/email-validator": "^2.1.10|^3|^4", + "php": ">=8.1", + "psr/event-dispatcher": "^1", + "psr/log": "^1|^2|^3", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/mime": "^6.2|^7.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<5.4", + "symfony/messenger": "<6.2", + "symfony/mime": "<6.2", + "symfony/twig-bridge": "<6.2.1" + }, + "require-dev": { + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/http-client": "^5.4|^6.0|^7.0", + "symfony/messenger": "^6.2|^7.0", + "symfony/twig-bridge": "^6.2|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mailer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps sending emails", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/mailer/tree/v6.4.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-04-18T09:22:46+00:00" + }, + { + "name": "symfony/mailgun-mailer", + "version": "v6.4.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/mailgun-mailer.git", + "reference": "044eede71c3eb5fbe7192042b8c0d04987b5653d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mailgun-mailer/zipball/044eede71c3eb5fbe7192042b8c0d04987b5653d", + "reference": "044eede71c3eb5fbe7192042b8c0d04987b5653d", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/mailer": "^5.4.21|^6.2.7|^7.0" + }, + "conflict": { + "symfony/http-foundation": "<6.2" + }, + "require-dev": { + "symfony/http-client": "^6.3|^7.0", + "symfony/webhook": "^6.3|^7.0" + }, + "type": "symfony-mailer-bridge", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mailer\\Bridge\\Mailgun\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Mailgun Mailer Bridge", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/mailgun-mailer/tree/v6.4.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-04-18T09:22:46+00:00" + }, + { + "name": "symfony/mime", + "version": "v6.4.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/mime.git", + "reference": "decadcf3865918ecfcbfa90968553994ce935a5e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mime/zipball/decadcf3865918ecfcbfa90968553994ce935a5e", + "reference": "decadcf3865918ecfcbfa90968553994ce935a5e", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "egulias/email-validator": "~3.0.0", + "phpdocumentor/reflection-docblock": "<3.2.2", + "phpdocumentor/type-resolver": "<1.4.0", + "symfony/mailer": "<5.4", + "symfony/serializer": "<6.3.2" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3.1|^4", + "league/html-to-markdown": "^5.0", + "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.4|^7.0", + "symfony/property-access": "^5.4|^6.0|^7.0", + "symfony/property-info": "^5.4|^6.0|^7.0", + "symfony/serializer": "^6.3.2|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mime\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows manipulating MIME messages", + "homepage": "https://symfony.com", + "keywords": [ + "mime", + "mime-type" + ], + "support": { + "source": "https://github.com/symfony/mime/tree/v6.4.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-04-18T09:22:46+00:00" + }, + { + "name": "symfony/options-resolver", + "version": "v7.0.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "23cc173858776ad451e31f053b1c9f47840b2cfa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/23cc173858776ad451e31f053b1c9f47840b2cfa", + "reference": "23cc173858776ad451e31f053b1c9f47840b2cfa", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\OptionsResolver\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an improved replacement for the array_replace PHP function", + "homepage": "https://symfony.com", + "keywords": [ + "config", + "configuration", + "options" + ], + "support": { + "source": "https://github.com/symfony/options-resolver/tree/v7.0.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-04-18T09:29:19+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.29.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/ef4d7e442ca910c4764bce785146269b30cb5fc4", + "reference": "ef4d7e442ca910c4764bce785146269b30cb5fc4", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.29.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-29T20:11:03+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.29.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/32a9da87d7b3245e09ac426c83d334ae9f06f80f", + "reference": "32a9da87d7b3245e09ac426c83d334ae9f06f80f", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.29.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-29T20:11:03+00:00" + }, + { + "name": "symfony/polyfill-intl-icu", + "version": "v1.29.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-icu.git", + "reference": "07094a28851a49107f3ab4f9120ca2975a64b6e1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-icu/zipball/07094a28851a49107f3ab4f9120ca2975a64b6e1", + "reference": "07094a28851a49107f3ab4f9120ca2975a64b6e1", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance and support of other locales than \"en\"" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Icu\\": "" + }, + "classmap": [ + "Resources/stubs" + ], + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's ICU-related data and classes", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "icu", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-icu/tree/v1.29.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-29T20:12:16+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.29.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "a287ed7475f85bf6f61890146edbc932c0fff919" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/a287ed7475f85bf6f61890146edbc932c0fff919", + "reference": "a287ed7475f85bf6f61890146edbc932c0fff919", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "symfony/polyfill-intl-normalizer": "^1.10", + "symfony/polyfill-php72": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.29.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-29T20:11:03+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.29.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "bc45c394692b948b4d383a08d7753968bed9a83d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/bc45c394692b948b4d383a08d7753968bed9a83d", + "reference": "bc45c394692b948b4d383a08d7753968bed9a83d", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.29.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-29T20:11:03+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.29.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9773676c8a1bb1f8d4340a62efe641cf76eda7ec", + "reference": "9773676c8a1bb1f8d4340a62efe641cf76eda7ec", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.29.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-29T20:11:03+00:00" + }, + { + "name": "symfony/polyfill-php72", + "version": "v1.29.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php72.git", + "reference": "861391a8da9a04cbad2d232ddd9e4893220d6e25" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/861391a8da9a04cbad2d232ddd9e4893220d6e25", + "reference": "861391a8da9a04cbad2d232ddd9e4893220d6e25", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php72\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php72/tree/v1.29.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-29T20:11:03+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.29.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", + "reference": "87b68208d5c1188808dd7839ee1e6c8ec3b02f1b", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.29.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-29T20:11:03+00:00" + }, + { + "name": "symfony/polyfill-php83", + "version": "v1.29.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "86fcae159633351e5fd145d1c47de6c528f8caff" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/86fcae159633351e5fd145d1c47de6c528f8caff", + "reference": "86fcae159633351e5fd145d1c47de6c528f8caff", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "symfony/polyfill-php80": "^1.14" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php83\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php83/tree/v1.29.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-29T20:11:03+00:00" + }, + { + "name": "symfony/polyfill-uuid", + "version": "v1.29.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-uuid.git", + "reference": "3abdd21b0ceaa3000ee950097bc3cf9efc137853" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/3abdd21b0ceaa3000ee950097bc3cf9efc137853", + "reference": "3abdd21b0ceaa3000ee950097bc3cf9efc137853", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-uuid": "*" + }, + "suggest": { + "ext-uuid": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Uuid\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for uuid functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/polyfill-uuid/tree/v1.29.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-29T20:11:03+00:00" + }, + { + "name": "symfony/process", + "version": "v6.4.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "cdb1c81c145fd5aa9b0038bab694035020943381" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/cdb1c81c145fd5aa9b0038bab694035020943381", + "reference": "cdb1c81c145fd5aa9b0038bab694035020943381", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v6.4.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-04-18T09:22:46+00:00" + }, + { + "name": "symfony/property-access", + "version": "v7.0.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/property-access.git", + "reference": "8661b861480d2807eb2789ff99d034c0c71ab955" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/property-access/zipball/8661b861480d2807eb2789ff99d034c0c71ab955", + "reference": "8661b861480d2807eb2789ff99d034c0c71ab955", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/property-info": "^6.4|^7.0" + }, + "require-dev": { + "symfony/cache": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\PropertyAccess\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides functions to read and write from/to an object or array using a simple string notation", + "homepage": "https://symfony.com", + "keywords": [ + "access", + "array", + "extraction", + "index", + "injection", + "object", + "property", + "property-path", + "reflection" + ], + "support": { + "source": "https://github.com/symfony/property-access/tree/v7.0.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-04-18T09:29:19+00:00" + }, + { + "name": "symfony/property-info", + "version": "v7.0.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/property-info.git", + "reference": "f0bdb46e19ab308527b324b7ec36161f6880a532" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/property-info/zipball/f0bdb46e19ab308527b324b7ec36161f6880a532", + "reference": "f0bdb46e19ab308527b324b7ec36161f6880a532", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/string": "^6.4|^7.0" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "<5.2", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/dependency-injection": "<6.4", + "symfony/serializer": "<6.4" + }, + "require-dev": { + "phpdocumentor/reflection-docblock": "^5.2", + "phpstan/phpdoc-parser": "^1.0", + "symfony/cache": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/serializer": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\PropertyInfo\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kévin Dunglas", + "email": "dunglas@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Extracts information about PHP class' properties using metadata of popular sources", + "homepage": "https://symfony.com", + "keywords": [ + "doctrine", + "phpdoc", + "property", + "symfony", + "type", + "validator" + ], + "support": { + "source": "https://github.com/symfony/property-info/tree/v7.0.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-04-28T11:44:19+00:00" + }, + { + "name": "symfony/psr-http-message-bridge", + "version": "v2.3.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/psr-http-message-bridge.git", + "reference": "581ca6067eb62640de5ff08ee1ba6850a0ee472e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/psr-http-message-bridge/zipball/581ca6067eb62640de5ff08ee1ba6850a0ee472e", + "reference": "581ca6067eb62640de5ff08ee1ba6850a0ee472e", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "psr/http-message": "^1.0 || ^2.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0", + "symfony/http-foundation": "^5.4 || ^6.0" + }, + "require-dev": { + "nyholm/psr7": "^1.1", + "psr/log": "^1.1 || ^2 || ^3", + "symfony/browser-kit": "^5.4 || ^6.0", + "symfony/config": "^5.4 || ^6.0", + "symfony/event-dispatcher": "^5.4 || ^6.0", + "symfony/framework-bundle": "^5.4 || ^6.0", + "symfony/http-kernel": "^5.4 || ^6.0", + "symfony/phpunit-bridge": "^6.2" + }, + "suggest": { + "nyholm/psr7": "For a super lightweight PSR-7/17 implementation" + }, + "type": "symfony-bridge", + "extra": { + "branch-alias": { + "dev-main": "2.3-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Bridge\\PsrHttpMessage\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "http://symfony.com/contributors" + } + ], + "description": "PSR HTTP message bridge", + "homepage": "http://symfony.com", + "keywords": [ + "http", + "http-message", + "psr-17", + "psr-7" + ], + "support": { + "issues": "https://github.com/symfony/psr-http-message-bridge/issues", + "source": "https://github.com/symfony/psr-http-message-bridge/tree/v2.3.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-07-26T11:53:26+00:00" + }, + { + "name": "symfony/routing", + "version": "v6.4.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/routing.git", + "reference": "276e06398f71fa2a973264d94f28150f93cfb907" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/routing/zipball/276e06398f71fa2a973264d94f28150f93cfb907", + "reference": "276e06398f71fa2a973264d94f28150f93cfb907", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "doctrine/annotations": "<1.12", + "symfony/config": "<6.2", + "symfony/dependency-injection": "<5.4", + "symfony/yaml": "<5.4" + }, + "require-dev": { + "doctrine/annotations": "^1.12|^2", + "psr/log": "^1|^2|^3", + "symfony/config": "^6.2|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/yaml": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Routing\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Maps an HTTP request to a set of configuration variables", + "homepage": "https://symfony.com", + "keywords": [ + "router", + "routing", + "uri", + "url" + ], + "support": { + "source": "https://github.com/symfony/routing/tree/v6.4.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-04-18T09:22:46+00:00" + }, + { + "name": "symfony/serializer", + "version": "v7.0.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/serializer.git", + "reference": "08f0c517acf4b12dfc0d3963cd12f7b8023aea31" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/serializer/zipball/08f0c517acf4b12dfc0d3963cd12f7b8023aea31", + "reference": "08f0c517acf4b12dfc0d3963cd12f7b8023aea31", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "<3.2.2", + "phpdocumentor/type-resolver": "<1.4.0", + "symfony/dependency-injection": "<6.4", + "symfony/property-access": "<6.4", + "symfony/property-info": "<6.4", + "symfony/uid": "<6.4", + "symfony/validator": "<6.4", + "symfony/yaml": "<6.4" + }, + "require-dev": { + "phpdocumentor/reflection-docblock": "^3.2|^4.0|^5.0", + "seld/jsonlint": "^1.10", + "symfony/cache": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/error-handler": "^6.4|^7.0", + "symfony/filesystem": "^6.4|^7.0", + "symfony/form": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/mime": "^6.4|^7.0", + "symfony/property-access": "^6.4|^7.0", + "symfony/property-info": "^6.4|^7.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/uid": "^6.4|^7.0", + "symfony/validator": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0", + "symfony/var-exporter": "^6.4|^7.0", + "symfony/yaml": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Serializer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Handles serializing and deserializing data structures, including object graphs, into array structures or other formats like XML and JSON.", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/serializer/tree/v7.0.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-04-28T11:44:19+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.4.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "11bbf19a0fb7b36345861e85c5768844c552906e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/11bbf19a0fb7b36345861e85c5768844c552906e", + "reference": "11bbf19a0fb7b36345861e85c5768844c552906e", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.4.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2023-12-19T21:51:00+00:00" + }, + { + "name": "symfony/string", + "version": "v7.0.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "e405b5424dc2528e02e31ba26b83a79fd4eb8f63" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/e405b5424dc2528e02e31ba26b83a79fd4eb8f63", + "reference": "e405b5424dc2528e02e31ba26b83a79fd4eb8f63", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/error-handler": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v7.0.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-04-18T09:29:19+00:00" + }, + { + "name": "symfony/translation", + "version": "v6.4.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "7495687c58bfd88b7883823747b0656d90679123" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/7495687c58bfd88b7883823747b0656d90679123", + "reference": "7495687c58bfd88b7883823747b0656d90679123", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/translation-contracts": "^2.5|^3.0" + }, + "conflict": { + "symfony/config": "<5.4", + "symfony/console": "<5.4", + "symfony/dependency-injection": "<5.4", + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<5.4", + "symfony/service-contracts": "<2.5", + "symfony/twig-bundle": "<5.4", + "symfony/yaml": "<5.4" + }, + "provide": { + "symfony/translation-implementation": "2.3|3.0" + }, + "require-dev": { + "nikic/php-parser": "^4.18|^5.0", + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/finder": "^5.4|^6.0|^7.0", + "symfony/http-client-contracts": "^2.5|^3.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/intl": "^5.4|^6.0|^7.0", + "symfony/polyfill-intl-icu": "^1.21", + "symfony/routing": "^5.4|^6.0|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to internationalize your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/translation/tree/v6.4.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-04-18T09:22:46+00:00" + }, + { + "name": "symfony/translation-contracts", + "version": "v3.4.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "43810bdb2ddb5400e5c5e778e27b210a0ca83b6b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/43810bdb2ddb5400e5c5e778e27b210a0ca83b6b", + "reference": "43810bdb2ddb5400e5c5e778e27b210a0ca83b6b", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.4-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/translation-contracts/tree/v3.4.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-23T14:51:35+00:00" + }, + { + "name": "symfony/uid", + "version": "v6.4.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/uid.git", + "reference": "a66efcb71d8bc3a207d9d78e0bd67f3321510355" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/uid/zipball/a66efcb71d8bc3a207d9d78e0bd67f3321510355", + "reference": "a66efcb71d8bc3a207d9d78e0bd67f3321510355", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/polyfill-uuid": "^1.15" + }, + "require-dev": { + "symfony/console": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Uid\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to generate and represent UIDs", + "homepage": "https://symfony.com", + "keywords": [ + "UID", + "ulid", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/uid/tree/v6.4.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-04-18T09:22:46+00:00" + }, + { + "name": "symfony/var-dumper", + "version": "v6.4.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "7a9cd977cd1c5fed3694bee52990866432af07d7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/7a9cd977cd1c5fed3694bee52990866432af07d7", + "reference": "7a9cd977cd1c5fed3694bee52990866432af07d7", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/console": "<5.4" + }, + "require-dev": { + "ext-iconv": "*", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/error-handler": "^6.3|^7.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/uid": "^5.4|^6.0|^7.0", + "twig/twig": "^2.13|^3.0.4" + }, + "bin": [ + "Resources/bin/var-dump-server" + ], + "type": "library", + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "support": { + "source": "https://github.com/symfony/var-dumper/tree/v6.4.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-04-18T09:22:46+00:00" + }, + { + "name": "tedivm/jshrink", + "version": "v1.7.0", + "source": { + "type": "git", + "url": "https://github.com/tedious/JShrink.git", + "reference": "7a35f5a4651ca2ce77295eb8a3b4e133ba47e19e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tedious/JShrink/zipball/7a35f5a4651ca2ce77295eb8a3b4e133ba47e19e", + "reference": "7a35f5a4651ca2ce77295eb8a3b4e133ba47e19e", + "shasum": "" + }, + "require": { + "php": "^7.0|^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.14", + "php-coveralls/php-coveralls": "^2.5.0", + "phpunit/phpunit": "^9|^10" + }, + "type": "library", + "autoload": { + "psr-0": { + "JShrink": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Robert Hafner", + "email": "tedivm@tedivm.com" + } + ], + "description": "Javascript Minifier built in PHP", + "homepage": "http://github.com/tedious/JShrink", + "keywords": [ + "javascript", + "minifier" + ], + "support": { + "issues": "https://github.com/tedious/JShrink/issues", + "source": "https://github.com/tedious/JShrink/tree/v1.7.0" + }, + "funding": [ + { + "url": "https://github.com/tedivm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/tedivm/jshrink", + "type": "tidelift" + } + ], + "time": "2023-10-04T17:23:23+00:00" + }, + { + "name": "thecodingmachine/safe", + "version": "v2.5.0", + "source": { + "type": "git", + "url": "https://github.com/thecodingmachine/safe.git", + "reference": "3115ecd6b4391662b4931daac4eba6b07a2ac1f0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/3115ecd6b4391662b4931daac4eba6b07a2ac1f0", + "reference": "3115ecd6b4391662b4931daac4eba6b07a2ac1f0", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.5", + "phpunit/phpunit": "^9.5", + "squizlabs/php_codesniffer": "^3.2", + "thecodingmachine/phpstan-strict-rules": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.2.x-dev" + } + }, + "autoload": { + "files": [ + "deprecated/apc.php", + "deprecated/array.php", + "deprecated/datetime.php", + "deprecated/libevent.php", + "deprecated/misc.php", + "deprecated/password.php", + "deprecated/mssql.php", + "deprecated/stats.php", + "deprecated/strings.php", + "lib/special_cases.php", + "deprecated/mysqli.php", + "generated/apache.php", + "generated/apcu.php", + "generated/array.php", + "generated/bzip2.php", + "generated/calendar.php", + "generated/classobj.php", + "generated/com.php", + "generated/cubrid.php", + "generated/curl.php", + "generated/datetime.php", + "generated/dir.php", + "generated/eio.php", + "generated/errorfunc.php", + "generated/exec.php", + "generated/fileinfo.php", + "generated/filesystem.php", + "generated/filter.php", + "generated/fpm.php", + "generated/ftp.php", + "generated/funchand.php", + "generated/gettext.php", + "generated/gmp.php", + "generated/gnupg.php", + "generated/hash.php", + "generated/ibase.php", + "generated/ibmDb2.php", + "generated/iconv.php", + "generated/image.php", + "generated/imap.php", + "generated/info.php", + "generated/inotify.php", + "generated/json.php", + "generated/ldap.php", + "generated/libxml.php", + "generated/lzf.php", + "generated/mailparse.php", + "generated/mbstring.php", + "generated/misc.php", + "generated/mysql.php", + "generated/network.php", + "generated/oci8.php", + "generated/opcache.php", + "generated/openssl.php", + "generated/outcontrol.php", + "generated/pcntl.php", + "generated/pcre.php", + "generated/pgsql.php", + "generated/posix.php", + "generated/ps.php", + "generated/pspell.php", + "generated/readline.php", + "generated/rpminfo.php", + "generated/rrd.php", + "generated/sem.php", + "generated/session.php", + "generated/shmop.php", + "generated/sockets.php", + "generated/sodium.php", + "generated/solr.php", + "generated/spl.php", + "generated/sqlsrv.php", + "generated/ssdeep.php", + "generated/ssh2.php", + "generated/stream.php", + "generated/strings.php", + "generated/swoole.php", + "generated/uodbc.php", + "generated/uopz.php", + "generated/url.php", + "generated/var.php", + "generated/xdiff.php", + "generated/xml.php", + "generated/xmlrpc.php", + "generated/yaml.php", + "generated/yaz.php", + "generated/zip.php", + "generated/zlib.php" + ], + "classmap": [ + "lib/DateTime.php", + "lib/DateTimeImmutable.php", + "lib/Exceptions/", + "deprecated/Exceptions/", + "generated/Exceptions/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHP core functions that throw exceptions instead of returning FALSE on error", + "support": { + "issues": "https://github.com/thecodingmachine/safe/issues", + "source": "https://github.com/thecodingmachine/safe/tree/v2.5.0" + }, + "time": "2023-04-05T11:54:14+00:00" + }, + { + "name": "tijsverkoyen/css-to-inline-styles", + "version": "v2.2.7", + "source": { + "type": "git", + "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", + "reference": "83ee6f38df0a63106a9e4536e3060458b74ccedb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/83ee6f38df0a63106a9e4536e3060458b74ccedb", + "reference": "83ee6f38df0a63106a9e4536e3060458b74ccedb", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "php": "^5.5 || ^7.0 || ^8.0", + "symfony/css-selector": "^2.7 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0 || ^7.5 || ^8.5.21 || ^9.5.10" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.2.x-dev" + } + }, + "autoload": { + "psr-4": { + "TijsVerkoyen\\CssToInlineStyles\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Tijs Verkoyen", + "email": "css_to_inline_styles@verkoyen.eu", + "role": "Developer" + } + ], + "description": "CssToInlineStyles is a class that enables you to convert HTML-pages/files into HTML-pages/files with inline styles. This is very useful when you're sending emails.", + "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", + "support": { + "issues": "https://github.com/tijsverkoyen/CssToInlineStyles/issues", + "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.2.7" + }, + "time": "2023-12-08T13:03:43+00:00" + }, + { + "name": "vectorface/whip", + "version": "v0.4.0", + "source": { + "type": "git", + "url": "https://github.com/Vectorface/whip.git", + "reference": "daa06bad325cff3fca5b870a5f167173ac88f4ad" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Vectorface/whip/zipball/daa06bad325cff3fca5b870a5f167173ac88f4ad", + "reference": "daa06bad325cff3fca5b870a5f167173ac88f4ad", + "shasum": "" + }, + "require": { + "php": ">=5.6.0", + "psr/http-message": "^1.0" + }, + "require-dev": { + "codeclimate/php-test-reporter": "dev-master", + "phpunit/phpunit": "^4.8", + "squizlabs/php_codesniffer": "~2.0", + "vectorface/dunit": "~2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Vectorface\\Whip\\": "./src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Bruce", + "email": "dbruce@vectorface.com", + "role": "Developer" + }, + { + "name": "Cory Darby", + "email": "ckdarby@vectorface.com", + "role": "Developer" + } + ], + "description": "A PHP class for retrieving accurate IP address information for the client.", + "homepage": "https://github.com/Vectorface/whip", + "keywords": [ + "IP", + "cdn", + "cloudflare" + ], + "support": { + "issues": "https://github.com/Vectorface/whip/issues", + "source": "https://github.com/Vectorface/whip" + }, + "time": "2020-08-25T13:45:06+00:00" + }, + { + "name": "vinkla/hashids", + "version": "10.0.1", + "source": { + "type": "git", + "url": "https://github.com/vinkla/laravel-hashids.git", + "reference": "9dbcfc1b20ecc25e73bba6e8c724d1648fa15fdd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vinkla/laravel-hashids/zipball/9dbcfc1b20ecc25e73bba6e8c724d1648fa15fdd", + "reference": "9dbcfc1b20ecc25e73bba6e8c724d1648fa15fdd", + "shasum": "" + }, + "require": { + "graham-campbell/manager": "^4.7", + "hashids/hashids": "^4.1", + "illuminate/contracts": "^9.0", + "illuminate/support": "^9.0", + "php": "^8.0" + }, + "require-dev": { + "graham-campbell/analyzer": "^3.0", + "graham-campbell/testbench": "^5.7", + "mockery/mockery": "^1.3", + "phpunit/phpunit": "^9.3", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "10.0-dev" + }, + "laravel": { + "aliases": { + "Hashids": "Vinkla\\Hashids\\Facades\\Hashids" + }, + "providers": [ + "Vinkla\\Hashids\\HashidsServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Vinkla\\Hashids\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Vincent Klaiber", + "email": "hello@doubledip.se" + } + ], + "description": "A Hashids bridge for Laravel", + "keywords": [ + "hashids", + "laravel" + ], + "support": { + "issues": "https://github.com/vinkla/laravel-hashids/issues", + "source": "https://github.com/vinkla/laravel-hashids/tree/10.0.1" + }, + "time": "2022-04-10T18:38:38+00:00" + }, + { + "name": "vlucas/phpdotenv", + "version": "v5.6.0", + "source": { + "type": "git", + "url": "https://github.com/vlucas/phpdotenv.git", + "reference": "2cf9fb6054c2bb1d59d1f3817706ecdb9d2934c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/2cf9fb6054c2bb1d59d1f3817706ecdb9d2934c4", + "reference": "2cf9fb6054c2bb1d59d1f3817706ecdb9d2934c4", + "shasum": "" + }, + "require": { + "ext-pcre": "*", + "graham-campbell/result-type": "^1.1.2", + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.2", + "symfony/polyfill-ctype": "^1.24", + "symfony/polyfill-mbstring": "^1.24", + "symfony/polyfill-php80": "^1.24" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-filter": "*", + "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" + }, + "suggest": { + "ext-filter": "Required to use the boolean validator." + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": true + }, + "branch-alias": { + "dev-master": "5.6-dev" + } + }, + "autoload": { + "psr-4": { + "Dotenv\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Vance Lucas", + "email": "vance@vancelucas.com", + "homepage": "https://github.com/vlucas" + } + ], + "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "support": { + "issues": "https://github.com/vlucas/phpdotenv/issues", + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv", + "type": "tidelift" + } + ], + "time": "2023-11-12T22:43:29+00:00" + }, + { + "name": "vluzrmos/language-detector", + "version": "v2.3.4", + "source": { + "type": "git", + "url": "https://github.com/vluzrmos/laravel-language-detector.git", + "reference": "25bf9011f34660ed7c03ed4c0b031c205f3d6050" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vluzrmos/laravel-language-detector/zipball/25bf9011f34660ed7c03ed4c0b031c205f3d6050", + "reference": "25bf9011f34660ed7c03ed4c0b031c205f3d6050", + "shasum": "" + }, + "require": { + "illuminate/support": "~6.0 || ~7.0 || ~8.0 || ~9.0 || ~10.0", + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.16", + "orchestra/testbench": "^5.0 || ^6.0 || ^7.0 || ^8.0", + "phpunit/phpunit": "^8.5 || ^9.0" + }, + "type": "package", + "extra": { + "laravel": { + "providers": [ + "Vluzrmos\\LanguageDetector\\Providers\\LanguageDetectorServiceProvider" + ] + } + }, + "autoload": { + "files": [ + "src/Support/helpers.php" + ], + "psr-4": { + "Vluzrmos\\LanguageDetector\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Vagner do Carmo", + "email": "vluzrmos@gmail.com" + } + ], + "description": "Detect the language for your application using browser preferences, subdomains or route prefixes.", + "keywords": [ + "i18n", + "language", + "laravel", + "locale", + "lumen" + ], + "support": { + "issues": "https://github.com/vluzrmos/laravel-language-detector/issues", + "source": "https://github.com/vluzrmos/laravel-language-detector/tree/v2.3.4" + }, + "time": "2023-02-28T17:04:53+00:00" + }, + { + "name": "voku/portable-ascii", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/voku/portable-ascii.git", + "reference": "b56450eed252f6801410d810c8e1727224ae0743" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/voku/portable-ascii/zipball/b56450eed252f6801410d810c8e1727224ae0743", + "reference": "b56450eed252f6801410d810c8e1727224ae0743", + "shasum": "" + }, + "require": { + "php": ">=7.0.0" + }, + "require-dev": { + "phpunit/phpunit": "~6.0 || ~7.0 || ~9.0" + }, + "suggest": { + "ext-intl": "Use Intl for transliterator_transliterate() support" + }, + "type": "library", + "autoload": { + "psr-4": { + "voku\\": "src/voku/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Lars Moelleken", + "homepage": "http://www.moelleken.org/" + } + ], + "description": "Portable ASCII library - performance optimized (ascii) string functions for php.", + "homepage": "https://github.com/voku/portable-ascii", + "keywords": [ + "ascii", + "clean", + "php" + ], + "support": { + "issues": "https://github.com/voku/portable-ascii/issues", + "source": "https://github.com/voku/portable-ascii/tree/2.0.1" + }, + "funding": [ + { + "url": "https://www.paypal.me/moelleken", + "type": "custom" + }, + { + "url": "https://github.com/voku", + "type": "github" + }, + { + "url": "https://opencollective.com/portable-ascii", + "type": "open_collective" + }, + { + "url": "https://www.patreon.com/voku", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/voku/portable-ascii", + "type": "tidelift" + } + ], + "time": "2022-03-08T17:03:00+00:00" + }, + { + "name": "web-auth/cose-lib", + "version": "4.3.0", + "source": { + "type": "git", + "url": "https://github.com/web-auth/cose-lib.git", + "reference": "e5c417b3b90e06c84638a18d350e438d760cb955" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-auth/cose-lib/zipball/e5c417b3b90e06c84638a18d350e438d760cb955", + "reference": "e5c417b3b90e06c84638a18d350e438d760cb955", + "shasum": "" + }, + "require": { + "brick/math": "^0.9|^0.10|^0.11|^0.12", + "ext-json": "*", + "ext-mbstring": "*", + "ext-openssl": "*", + "php": ">=8.1", + "spomky-labs/pki-framework": "^1.0" + }, + "require-dev": { + "ekino/phpstan-banned-code": "^1.0", + "infection/infection": "^0.27", + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpstan/extension-installer": "^1.3", + "phpstan/phpstan": "^1.7", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.1", + "phpstan/phpstan-strict-rules": "^1.2", + "phpunit/phpunit": "^10.1", + "qossmic/deptrac-shim": "^1.0", + "rector/rector": "^0.19", + "symfony/phpunit-bridge": "^6.4|^7.0", + "symplify/easy-coding-standard": "^12.0" + }, + "suggest": { + "ext-bcmath": "For better performance, please install either GMP (recommended) or BCMath extension", + "ext-gmp": "For better performance, please install either GMP (recommended) or BCMath extension" + }, + "type": "library", + "autoload": { + "psr-4": { + "Cose\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/web-auth/cose/contributors" + } + ], + "description": "CBOR Object Signing and Encryption (COSE) For PHP", + "homepage": "https://github.com/web-auth", + "keywords": [ + "COSE", + "RFC8152" + ], + "support": { + "issues": "https://github.com/web-auth/cose-lib/issues", + "source": "https://github.com/web-auth/cose-lib/tree/4.3.0" + }, + "funding": [ + { + "url": "https://github.com/Spomky", + "type": "github" + }, + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2024-02-05T21:00:39+00:00" + }, + { + "name": "web-auth/metadata-service", + "version": "4.8.6", + "source": { + "type": "git", + "url": "https://github.com/web-auth/webauthn-metadata-service.git", + "reference": "fb7c1f107639285fab90f870aab38360252c82f5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-auth/webauthn-metadata-service/zipball/fb7c1f107639285fab90f870aab38360252c82f5", + "reference": "fb7c1f107639285fab90f870aab38360252c82f5", + "shasum": "" + }, + "require": { + "ext-json": "*", + "lcobucci/clock": "^2.2|^3.0", + "paragonie/constant_time_encoding": "^2.6", + "php": ">=8.1", + "psr/clock": "^1.0", + "psr/event-dispatcher": "^1.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "psr/log": "^1.0|^2.0|^3.0", + "spomky-labs/pki-framework": "^1.0", + "symfony/deprecation-contracts": "^3.2" + }, + "suggest": { + "phpdocumentor/reflection-docblock": "As of 4.5.x, the phpdocumentor/reflection-docblock component will become mandatory for converting objects such as the Metadata Statement", + "psr/clock-implementation": "As of 4.5.x, the PSR Clock implementation will replace lcobucci/clock", + "psr/log-implementation": "Recommended to receive logs from the library", + "symfony/property-access": "As of 4.5.x, the symfony/serializer component will become mandatory for converting objects such as the Metadata Statement", + "symfony/property-info": "As of 4.5.x, the symfony/serializer component will become mandatory for converting objects such as the Metadata Statement", + "symfony/serializer": "As of 4.5.x, the symfony/serializer component will become mandatory for converting objects such as the Metadata Statement", + "web-token/jwt-library": "Mandatory for fetching Metadata Statement from distant sources" + }, + "type": "library", + "extra": { + "thanks": { + "name": "web-auth/webauthn-framework", + "url": "https://github.com/web-auth/webauthn-framework" + } + }, + "autoload": { + "psr-4": { + "Webauthn\\MetadataService\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/web-auth/metadata-service/contributors" + } + ], + "description": "Metadata Service for FIDO2/Webauthn", + "homepage": "https://github.com/web-auth", + "keywords": [ + "FIDO2", + "fido", + "webauthn" + ], + "support": { + "source": "https://github.com/web-auth/webauthn-metadata-service/tree/4.8.6" + }, + "funding": [ + { + "url": "https://github.com/Spomky", + "type": "github" + }, + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2024-03-13T07:16:02+00:00" + }, + { + "name": "web-auth/webauthn-lib", + "version": "4.8.6", + "source": { + "type": "git", + "url": "https://github.com/web-auth/webauthn-lib.git", + "reference": "925873eb504a1db8a77dc2b4d2b578334736fa16" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-auth/webauthn-lib/zipball/925873eb504a1db8a77dc2b4d2b578334736fa16", + "reference": "925873eb504a1db8a77dc2b4d2b578334736fa16", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "ext-openssl": "*", + "paragonie/constant_time_encoding": "^2.6", + "php": ">=8.1", + "psr/event-dispatcher": "^1.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "psr/log": "^1.0|^2.0|^3.0", + "spomky-labs/cbor-php": "^3.0", + "symfony/uid": "^6.1|^7.0", + "web-auth/cose-lib": "^4.2.3", + "web-auth/metadata-service": "self.version" + }, + "suggest": { + "phpdocumentor/reflection-docblock": "As of 4.5.x, the phpdocumentor/reflection-docblock component will become mandatory for converting objects such as the Metadata Statement", + "psr/log-implementation": "Recommended to receive logs from the library", + "symfony/event-dispatcher": "Recommended to use dispatched events", + "symfony/property-access": "As of 4.5.x, the symfony/serializer component will become mandatory for converting objects such as the Metadata Statement", + "symfony/property-info": "As of 4.5.x, the symfony/serializer component will become mandatory for converting objects such as the Metadata Statement", + "symfony/serializer": "As of 4.5.x, the symfony/serializer component will become mandatory for converting objects such as the Metadata Statement", + "web-token/jwt-library": "Mandatory for the AndroidSafetyNet Attestation Statement support" + }, + "type": "library", + "extra": { + "thanks": { + "name": "web-auth/webauthn-framework", + "url": "https://github.com/web-auth/webauthn-framework" + } + }, + "autoload": { + "psr-4": { + "Webauthn\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/web-auth/webauthn-library/contributors" + } + ], + "description": "FIDO2/Webauthn Support For PHP", + "homepage": "https://github.com/web-auth", + "keywords": [ + "FIDO2", + "fido", + "webauthn" + ], + "support": { + "source": "https://github.com/web-auth/webauthn-lib/tree/4.8.6" + }, + "funding": [ + { + "url": "https://github.com/Spomky", + "type": "github" + }, + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2024-04-08T10:04:23+00:00" + }, + { + "name": "web-token/jwt-key-mgmt", + "version": "3.4.3", + "source": { + "type": "git", + "url": "https://github.com/web-token/jwt-key-mgmt.git", + "reference": "4d2a5a1a86477dd50b89aff76962816ddbd64590" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-token/jwt-key-mgmt/zipball/4d2a5a1a86477dd50b89aff76962816ddbd64590", + "reference": "4d2a5a1a86477dd50b89aff76962816ddbd64590", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "php": ">=8.1", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "web-token/jwt-library": "^3.3" + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/web-token/jwt-framework/contributors" + } + ], + "description": "[DEPRECATED] Please use web-token/jwt-library instead.", + "homepage": "https://github.com/web-token", + "keywords": [ + "JOSE", + "JWE", + "JWK", + "JWKSet", + "JWS", + "Jot", + "RFC7515", + "RFC7516", + "RFC7517", + "RFC7518", + "RFC7519", + "RFC7520", + "bundle", + "jwa", + "jwt", + "symfony" + ], + "support": { + "source": "https://github.com/web-token/jwt-key-mgmt/tree/3.4.3" + }, + "funding": [ + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "abandoned": "web-token/jwt-library", + "time": "2024-02-22T07:19:34+00:00" + }, + { + "name": "web-token/jwt-library", + "version": "3.4.3", + "source": { + "type": "git", + "url": "https://github.com/web-token/jwt-library.git", + "reference": "4b09510eec25c328525048cbdf6042a39a7c28d8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-token/jwt-library/zipball/4b09510eec25c328525048cbdf6042a39a7c28d8", + "reference": "4b09510eec25c328525048cbdf6042a39a7c28d8", + "shasum": "" + }, + "require": { + "brick/math": "^0.9|^0.10|^0.11|^0.12", + "ext-json": "*", + "ext-mbstring": "*", + "paragonie/constant_time_encoding": "^2.6", + "paragonie/sodium_compat": "^1.20", + "php": ">=8.1", + "psr/cache": "^3.0", + "psr/clock": "^1.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "spomky-labs/pki-framework": "^1.2.1", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/http-client": "^5.4|^6.0|^7.0", + "symfony/polyfill-mbstring": "^1.12" + }, + "conflict": { + "spomky-labs/jose": "*" + }, + "suggest": { + "ext-bcmath": "GMP or BCMath is highly recommended to improve the library performance", + "ext-gmp": "GMP or BCMath is highly recommended to improve the library performance", + "ext-openssl": "For key management (creation, optimization, etc.) and some algorithms (AES, RSA, ECDSA, etc.)", + "ext-sodium": "Sodium is required for OKP key creation, EdDSA signature algorithm and ECDH-ES key encryption with OKP keys", + "paragonie/sodium_compat": "Sodium is required for OKP key creation, EdDSA signature algorithm and ECDH-ES key encryption with OKP keys", + "spomky-labs/aes-key-wrap": "For all Key Wrapping algorithms (A128KW, A192KW, A256KW, A128GCMKW, A192GCMKW, A256GCMKW, PBES2-HS256+A128KW, PBES2-HS384+A192KW, PBES2-HS512+A256KW...)", + "symfony/http-client": "To enable JKU/X5U support." + }, + "type": "library", + "autoload": { + "psr-4": { + "Jose\\Component\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/web-token/jwt-framework/contributors" + } + ], + "description": "JWT library", + "homepage": "https://github.com/web-token", + "keywords": [ + "JOSE", + "JWE", + "JWK", + "JWKSet", + "JWS", + "Jot", + "RFC7515", + "RFC7516", + "RFC7517", + "RFC7518", + "RFC7519", + "RFC7520", + "bundle", + "jwa", + "jwt", + "symfony" + ], + "support": { + "issues": "https://github.com/web-token/jwt-library/issues", + "source": "https://github.com/web-token/jwt-library/tree/3.4.3" + }, + "funding": [ + { + "url": "https://github.com/Spomky", + "type": "github" + }, + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "time": "2024-04-17T17:41:33+00:00" + }, + { + "name": "web-token/jwt-signature-algorithm-ecdsa", + "version": "3.4.3", + "source": { + "type": "git", + "url": "https://github.com/web-token/jwt-signature-algorithm-ecdsa.git", + "reference": "28516e170f6ee6d13766d9e2b912c2853e1ac5e4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-token/jwt-signature-algorithm-ecdsa/zipball/28516e170f6ee6d13766d9e2b912c2853e1ac5e4", + "reference": "28516e170f6ee6d13766d9e2b912c2853e1ac5e4", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "php": ">=8.1", + "web-token/jwt-library": "^3.3" + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/web-token/jwt-framework/contributors" + } + ], + "description": "[DEPRECATED] Please use web-token/jwt-library instead.", + "homepage": "https://github.com/web-token", + "keywords": [ + "JOSE", + "JWE", + "JWK", + "JWKSet", + "JWS", + "Jot", + "RFC7515", + "RFC7516", + "RFC7517", + "RFC7518", + "RFC7519", + "RFC7520", + "bundle", + "jwa", + "jwt", + "symfony" + ], + "support": { + "source": "https://github.com/web-token/jwt-signature-algorithm-ecdsa/tree/3.4.3" + }, + "funding": [ + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "abandoned": "web-token/jwt-library", + "time": "2024-02-22T07:19:34+00:00" + }, + { + "name": "web-token/jwt-signature-algorithm-eddsa", + "version": "3.4.3", + "source": { + "type": "git", + "url": "https://github.com/web-token/jwt-signature-algorithm-eddsa.git", + "reference": "488327e6344d5504993951990eb572837e0909d9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-token/jwt-signature-algorithm-eddsa/zipball/488327e6344d5504993951990eb572837e0909d9", + "reference": "488327e6344d5504993951990eb572837e0909d9", + "shasum": "" + }, + "require": { + "ext-sodium": "*", + "paragonie/sodium_compat": "^1.20", + "php": ">=8.1", + "web-token/jwt-library": "^3.3" + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/web-token/jwt-framework/contributors" + } + ], + "description": "[DEPRECATED] Please use web-token/jwt-library instead.", + "homepage": "https://github.com/web-token", + "keywords": [ + "JOSE", + "JWE", + "JWK", + "JWKSet", + "JWS", + "Jot", + "RFC7515", + "RFC7516", + "RFC7517", + "RFC7518", + "RFC7519", + "RFC7520", + "bundle", + "jwa", + "jwt", + "symfony" + ], + "support": { + "source": "https://github.com/web-token/jwt-signature-algorithm-eddsa/tree/3.4.3" + }, + "funding": [ + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "abandoned": "web-token/jwt-library", + "time": "2024-02-22T07:19:34+00:00" + }, + { + "name": "web-token/jwt-signature-algorithm-rsa", + "version": "3.4.3", + "source": { + "type": "git", + "url": "https://github.com/web-token/jwt-signature-algorithm-rsa.git", + "reference": "4408e41671294f0390731e2f84065a03a8089ace" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/web-token/jwt-signature-algorithm-rsa/zipball/4408e41671294f0390731e2f84065a03a8089ace", + "reference": "4408e41671294f0390731e2f84065a03a8089ace", + "shasum": "" + }, + "require": { + "brick/math": "^0.9|^0.10|^0.11|^0.12", + "ext-openssl": "*", + "php": ">=8.1", + "web-token/jwt-library": "^3.3" + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Florent Morselli", + "homepage": "https://github.com/Spomky" + }, + { + "name": "All contributors", + "homepage": "https://github.com/web-token/jwt-framework/contributors" + } + ], + "description": "[DEPRECATED] Please use web-token/jwt-library instead.", + "homepage": "https://github.com/web-token", + "keywords": [ + "JOSE", + "JWE", + "JWK", + "JWKSet", + "JWS", + "Jot", + "RFC7515", + "RFC7516", + "RFC7517", + "RFC7518", + "RFC7519", + "RFC7520", + "bundle", + "jwa", + "jwt", + "symfony" + ], + "support": { + "source": "https://github.com/web-token/jwt-signature-algorithm-rsa/tree/3.4.3" + }, + "funding": [ + { + "url": "https://www.patreon.com/FlorentMorselli", + "type": "patreon" + } + ], + "abandoned": "web-token/jwt-library", + "time": "2024-02-22T07:19:34+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.11.0", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "php": "^7.2 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<0.12.20", + "vimeo/psalm": "<4.6.1 || 4.6.2" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.13" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/1.11.0" + }, + "time": "2022-06-03T18:03:27+00:00" + }, + { + "name": "werk365/etagconditionals", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/365Werk/etagconditionals.git", + "reference": "2ce75f2efda1f58560c44252f8b281834b132237" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/365Werk/etagconditionals/zipball/2ce75f2efda1f58560c44252f8b281834b132237", + "reference": "2ce75f2efda1f58560c44252f8b281834b132237", + "shasum": "" + }, + "require": { + "illuminate/support": "~7|~8|~9|~10" + }, + "require-dev": { + "orchestra/testbench": "~5|~6|~7|~8", + "phpunit/phpunit": "~8.0|~9.0" + }, + "default-branch": true, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Werk365\\EtagConditionals\\EtagConditionalsServiceProvider" + ], + "aliases": { + "EtagConditionals": "Werk365\\EtagConditionals\\Facades\\EtagConditionals" + } + } + }, + "autoload": { + "psr-4": { + "Werk365\\EtagConditionals\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Hergen Dillema", + "email": "h.dillema@365werk.nl", + "homepage": "https://365werk.nl" + } + ], + "description": "Laravel package to enable support for ETags and handling If-Match and If-None-Match conditional requests", + "homepage": "https://github.com/werk365/etagconditionals", + "keywords": [ + "EtagConditionals", + "laravel" + ], + "support": { + "issues": "https://github.com/365Werk/etagconditionals/issues", + "source": "https://github.com/365Werk/etagconditionals/tree/1.4.2" + }, + "time": "2023-03-22T10:32:46+00:00" + }, + { + "name": "xantios/mimey", + "version": "v2.2.0", + "source": { + "type": "git", + "url": "https://github.com/Xantios/mimey.git", + "reference": "8cb6f0c29b8eadde38777ed947847f4253c00b60" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Xantios/mimey/zipball/8cb6f0c29b8eadde38777ed947847f4253c00b60", + "reference": "8cb6f0c29b8eadde38777ed947847f4253c00b60", + "shasum": "" + }, + "require": { + "php": ">=7.0" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.4", + "phpunit/phpunit": "^9.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Mimey\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Xantios Krugor", + "email": "git@xantios.nl" + }, + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "PHP package for converting file extensions to MIME types and vice versa.", + "support": { + "issues": "https://github.com/Xantios/mimey/issues", + "source": "https://github.com/Xantios/mimey/tree/v2.2.0" + }, + "time": "2021-06-12T14:33:14+00:00" + } + ], + "packages-dev": [ + { + "name": "amphp/amp", + "version": "v2.6.4", + "source": { + "type": "git", + "url": "https://github.com/amphp/amp.git", + "reference": "ded3d9be08f526089eb7ee8d9f16a9768f9dec2d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/amp/zipball/ded3d9be08f526089eb7ee8d9f16a9768f9dec2d", + "reference": "ded3d9be08f526089eb7ee8d9f16a9768f9dec2d", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "dev-master", + "amphp/phpunit-util": "^1", + "ext-json": "*", + "jetbrains/phpstorm-stubs": "^2019.3", + "phpunit/phpunit": "^7 | ^8 | ^9", + "react/promise": "^2", + "vimeo/psalm": "^3.12" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "files": [ + "lib/functions.php", + "lib/Internal/functions.php" + ], + "psr-4": { + "Amp\\": "lib" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Lowrey", + "email": "rdlowrey@php.net" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "A non-blocking concurrency framework for PHP applications.", + "homepage": "https://amphp.org/amp", + "keywords": [ + "async", + "asynchronous", + "awaitable", + "concurrency", + "event", + "event-loop", + "future", + "non-blocking", + "promise" + ], + "support": { + "irc": "irc://irc.freenode.org/amphp", + "issues": "https://github.com/amphp/amp/issues", + "source": "https://github.com/amphp/amp/tree/v2.6.4" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-03-21T18:52:26+00:00" + }, + { + "name": "amphp/byte-stream", + "version": "v1.8.2", + "source": { + "type": "git", + "url": "https://github.com/amphp/byte-stream.git", + "reference": "4f0e968ba3798a423730f567b1b50d3441c16ddc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/byte-stream/zipball/4f0e968ba3798a423730f567b1b50d3441c16ddc", + "reference": "4f0e968ba3798a423730f567b1b50d3441c16ddc", + "shasum": "" + }, + "require": { + "amphp/amp": "^2", + "php": ">=7.1" + }, + "require-dev": { + "amphp/php-cs-fixer-config": "dev-master", + "amphp/phpunit-util": "^1.4", + "friendsofphp/php-cs-fixer": "^2.3", + "jetbrains/phpstorm-stubs": "^2019.3", + "phpunit/phpunit": "^6 || ^7 || ^8", + "psalm/phar": "^3.11.4" + }, + "type": "library", + "autoload": { + "files": [ + "lib/functions.php" + ], + "psr-4": { + "Amp\\ByteStream\\": "lib" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + } + ], + "description": "A stream abstraction to make working with non-blocking I/O simple.", + "homepage": "https://amphp.org/byte-stream", + "keywords": [ + "amp", + "amphp", + "async", + "io", + "non-blocking", + "stream" + ], + "support": { + "issues": "https://github.com/amphp/byte-stream/issues", + "source": "https://github.com/amphp/byte-stream/tree/v1.8.2" + }, + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2024-04-13T18:00:56+00:00" + }, + { + "name": "barryvdh/laravel-debugbar", + "version": "v3.13.5", + "source": { + "type": "git", + "url": "https://github.com/barryvdh/laravel-debugbar.git", + "reference": "92d86be45ee54edff735e46856f64f14b6a8bb07" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/92d86be45ee54edff735e46856f64f14b6a8bb07", + "reference": "92d86be45ee54edff735e46856f64f14b6a8bb07", + "shasum": "" + }, + "require": { + "illuminate/routing": "^9|^10|^11", + "illuminate/session": "^9|^10|^11", + "illuminate/support": "^9|^10|^11", + "maximebf/debugbar": "~1.22.0", + "php": "^8.0", + "symfony/finder": "^6|^7" + }, + "require-dev": { + "mockery/mockery": "^1.3.3", + "orchestra/testbench-dusk": "^5|^6|^7|^8|^9", + "phpunit/phpunit": "^9.6|^10.5", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.13-dev" + }, + "laravel": { + "providers": [ + "Barryvdh\\Debugbar\\ServiceProvider" + ], + "aliases": { + "Debugbar": "Barryvdh\\Debugbar\\Facades\\Debugbar" + } + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Barryvdh\\Debugbar\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Barry vd. Heuvel", + "email": "barryvdh@gmail.com" + } + ], + "description": "PHP Debugbar integration for Laravel", + "keywords": [ + "debug", + "debugbar", + "laravel", + "profiler", + "webprofiler" + ], + "support": { + "issues": "https://github.com/barryvdh/laravel-debugbar/issues", + "source": "https://github.com/barryvdh/laravel-debugbar/tree/v3.13.5" + }, + "funding": [ + { + "url": "https://fruitcake.nl", + "type": "custom" + }, + { + "url": "https://github.com/barryvdh", + "type": "github" + } + ], + "time": "2024-04-12T11:20:37+00:00" + }, + { + "name": "barryvdh/laravel-ide-helper", + "version": "v2.15.1", + "source": { + "type": "git", + "url": "https://github.com/barryvdh/laravel-ide-helper.git", + "reference": "77831852bb7bc54f287246d32eb91274eaf87f8b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/77831852bb7bc54f287246d32eb91274eaf87f8b", + "reference": "77831852bb7bc54f287246d32eb91274eaf87f8b", + "shasum": "" + }, + "require": { + "barryvdh/reflection-docblock": "^2.0.6", + "composer/class-map-generator": "^1.0", + "doctrine/dbal": "^2.6 || ^3.1.4", + "ext-json": "*", + "illuminate/console": "^9 || ^10", + "illuminate/filesystem": "^9 || ^10", + "illuminate/support": "^9 || ^10", + "nikic/php-parser": "^4.18 || ^5", + "php": "^8.0", + "phpdocumentor/type-resolver": "^1.1.0" + }, + "require-dev": { + "ext-pdo_sqlite": "*", + "friendsofphp/php-cs-fixer": "^3", + "illuminate/config": "^9 || ^10", + "illuminate/view": "^9 || ^10", + "mockery/mockery": "^1.4", + "orchestra/testbench": "^7 || ^8", + "phpunit/phpunit": "^9", + "spatie/phpunit-snapshot-assertions": "^4", + "vimeo/psalm": "^5.4" + }, + "suggest": { + "illuminate/events": "Required for automatic helper generation (^6|^7|^8|^9|^10)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.15-dev" + }, + "laravel": { + "providers": [ + "Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Barryvdh\\LaravelIdeHelper\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Barry vd. Heuvel", + "email": "barryvdh@gmail.com" + } + ], + "description": "Laravel IDE Helper, generates correct PHPDocs for all Facade classes, to improve auto-completion.", + "keywords": [ + "autocomplete", + "codeintel", + "helper", + "ide", + "laravel", + "netbeans", + "phpdoc", + "phpstorm", + "sublime" + ], + "support": { + "issues": "https://github.com/barryvdh/laravel-ide-helper/issues", + "source": "https://github.com/barryvdh/laravel-ide-helper/tree/v2.15.1" + }, + "funding": [ + { + "url": "https://fruitcake.nl", + "type": "custom" + }, + { + "url": "https://github.com/barryvdh", + "type": "github" + } + ], + "time": "2024-02-15T14:23:20+00:00" + }, + { + "name": "barryvdh/reflection-docblock", + "version": "v2.1.1", + "source": { + "type": "git", + "url": "https://github.com/barryvdh/ReflectionDocBlock.git", + "reference": "e6811e927f0ecc37cc4deaa6627033150343e597" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/barryvdh/ReflectionDocBlock/zipball/e6811e927f0ecc37cc4deaa6627033150343e597", + "reference": "e6811e927f0ecc37cc4deaa6627033150343e597", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.14|^9" + }, + "suggest": { + "dflydev/markdown": "~1.0", + "erusev/parsedown": "~1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-0": { + "Barryvdh": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "mike.vanriel@naenius.com" + } + ], + "support": { + "source": "https://github.com/barryvdh/ReflectionDocBlock/tree/v2.1.1" + }, + "time": "2023-06-14T05:06:27+00:00" + }, + { + "name": "composer/class-map-generator", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/composer/class-map-generator.git", + "reference": "8286a62d243312ed99b3eee20d5005c961adb311" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/class-map-generator/zipball/8286a62d243312ed99b3eee20d5005c961adb311", + "reference": "8286a62d243312ed99b3eee20d5005c961adb311", + "shasum": "" + }, + "require": { + "composer/pcre": "^2.1 || ^3.1", + "php": "^7.2 || ^8.0", + "symfony/finder": "^4.4 || ^5.3 || ^6 || ^7" + }, + "require-dev": { + "phpstan/phpstan": "^1.6", + "phpstan/phpstan-deprecation-rules": "^1", + "phpstan/phpstan-phpunit": "^1", + "phpstan/phpstan-strict-rules": "^1.1", + "symfony/filesystem": "^5.4 || ^6", + "symfony/phpunit-bridge": "^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\ClassMapGenerator\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Utilities to scan PHP code and generate class maps.", + "keywords": [ + "classmap" + ], + "support": { + "issues": "https://github.com/composer/class-map-generator/issues", + "source": "https://github.com/composer/class-map-generator/tree/1.1.1" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-03-15T12:53:41+00:00" + }, + { + "name": "composer/pcre", + "version": "3.1.3", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "5b16e25a5355f1f3afdfc2f954a0a80aec4826a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/5b16e25a5355f1f3afdfc2f954a0a80aec4826a8", + "reference": "5b16e25a5355f1f3afdfc2f954a0a80aec4826a8", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.3", + "phpstan/phpstan-strict-rules": "^1.1", + "symfony/phpunit-bridge": "^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.1.3" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-03-19T10:26:25+00:00" + }, + { + "name": "composer/semver", + "version": "3.4.0", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "35e8d0af4486141bc745f23a29cc2091eb624a32" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/35e8d0af4486141bc745f23a29cc2091eb624a32", + "reference": "35e8d0af4486141bc745f23a29cc2091eb624a32", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.4", + "symfony/phpunit-bridge": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.4.0" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2023-08-31T09:50:34+00:00" + }, + { + "name": "composer/xdebug-handler", + "version": "3.0.4", + "source": { + "type": "git", + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "4f988f8fdf580d53bdb2d1278fe93d1ed5462255" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/4f988f8fdf580d53bdb2d1278fe93d1ed5462255", + "reference": "4f988f8fdf580d53bdb2d1278fe93d1ed5462255", + "shasum": "" + }, + "require": { + "composer/pcre": "^1 || ^2 || ^3", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1 || ^2 || ^3" + }, + "require-dev": { + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Composer\\XdebugHandler\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" + } + ], + "description": "Restarts a process without Xdebug.", + "keywords": [ + "Xdebug", + "performance" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/3.0.4" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-03-26T18:29:49+00:00" + }, + { + "name": "dnoegel/php-xdg-base-dir", + "version": "v0.1.1", + "source": { + "type": "git", + "url": "https://github.com/dnoegel/php-xdg-base-dir.git", + "reference": "8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dnoegel/php-xdg-base-dir/zipball/8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd", + "reference": "8f8a6e48c5ecb0f991c2fdcf5f154a47d85f9ffd", + "shasum": "" + }, + "require": { + "php": ">=5.3.2" + }, + "require-dev": { + "phpunit/phpunit": "~7.0|~6.0|~5.0|~4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "XdgBaseDir\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "implementation of xdg base directory specification for php", + "support": { + "issues": "https://github.com/dnoegel/php-xdg-base-dir/issues", + "source": "https://github.com/dnoegel/php-xdg-base-dir/tree/v0.1.1" + }, + "time": "2019-12-04T15:06:13+00:00" + }, + { + "name": "doctrine/instantiator", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^11", + "ext-pdo": "*", + "ext-phar": "*", + "phpbench/phpbench": "^1.2", + "phpstan/phpstan": "^1.9.4", + "phpstan/phpstan-phpunit": "^1.3", + "phpunit/phpunit": "^9.5.27", + "vimeo/psalm": "^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "https://ocramius.github.io/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "keywords": [ + "constructor", + "instantiate" + ], + "support": { + "issues": "https://github.com/doctrine/instantiator/issues", + "source": "https://github.com/doctrine/instantiator/tree/2.0.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], + "time": "2022-12-30T00:23:10+00:00" + }, + { + "name": "fakerphp/faker", + "version": "v1.23.1", + "source": { + "type": "git", + "url": "https://github.com/FakerPHP/Faker.git", + "reference": "bfb4fe148adbf78eff521199619b93a52ae3554b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/bfb4fe148adbf78eff521199619b93a52ae3554b", + "reference": "bfb4fe148adbf78eff521199619b93a52ae3554b", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "psr/container": "^1.0 || ^2.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "conflict": { + "fzaninotto/faker": "*" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "doctrine/persistence": "^1.3 || ^2.0", + "ext-intl": "*", + "phpunit/phpunit": "^9.5.26", + "symfony/phpunit-bridge": "^5.4.16" + }, + "suggest": { + "doctrine/orm": "Required to use Faker\\ORM\\Doctrine", + "ext-curl": "Required by Faker\\Provider\\Image to download images.", + "ext-dom": "Required by Faker\\Provider\\HtmlLorem for generating random HTML.", + "ext-iconv": "Required by Faker\\Provider\\ru_RU\\Text::realText() for generating real Russian text.", + "ext-mbstring": "Required for multibyte Unicode string functionality." + }, + "type": "library", + "autoload": { + "psr-4": { + "Faker\\": "src/Faker/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "François Zaninotto" + } + ], + "description": "Faker is a PHP library that generates fake data for you.", + "keywords": [ + "data", + "faker", + "fixtures" + ], + "support": { + "issues": "https://github.com/FakerPHP/Faker/issues", + "source": "https://github.com/FakerPHP/Faker/tree/v1.23.1" + }, + "time": "2024-01-02T13:46:09+00:00" + }, + { + "name": "felixfbecker/advanced-json-rpc", + "version": "v3.2.1", + "source": { + "type": "git", + "url": "https://github.com/felixfbecker/php-advanced-json-rpc.git", + "reference": "b5f37dbff9a8ad360ca341f3240dc1c168b45447" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/felixfbecker/php-advanced-json-rpc/zipball/b5f37dbff9a8ad360ca341f3240dc1c168b45447", + "reference": "b5f37dbff9a8ad360ca341f3240dc1c168b45447", + "shasum": "" + }, + "require": { + "netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0", + "php": "^7.1 || ^8.0", + "phpdocumentor/reflection-docblock": "^4.3.4 || ^5.0.0" + }, + "require-dev": { + "phpunit/phpunit": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "AdvancedJsonRpc\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Felix Becker", + "email": "felix.b@outlook.com" + } + ], + "description": "A more advanced JSONRPC implementation", + "support": { + "issues": "https://github.com/felixfbecker/php-advanced-json-rpc/issues", + "source": "https://github.com/felixfbecker/php-advanced-json-rpc/tree/v3.2.1" + }, + "time": "2021-06-11T22:34:44+00:00" + }, + { + "name": "felixfbecker/language-server-protocol", + "version": "v1.5.2", + "source": { + "type": "git", + "url": "https://github.com/felixfbecker/php-language-server-protocol.git", + "reference": "6e82196ffd7c62f7794d778ca52b69feec9f2842" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/felixfbecker/php-language-server-protocol/zipball/6e82196ffd7c62f7794d778ca52b69feec9f2842", + "reference": "6e82196ffd7c62f7794d778ca52b69feec9f2842", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "phpstan/phpstan": "*", + "squizlabs/php_codesniffer": "^3.1", + "vimeo/psalm": "^4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "LanguageServerProtocol\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Felix Becker", + "email": "felix.b@outlook.com" + } + ], + "description": "PHP classes for the Language Server Protocol", + "keywords": [ + "language", + "microsoft", + "php", + "server" + ], + "support": { + "issues": "https://github.com/felixfbecker/php-language-server-protocol/issues", + "source": "https://github.com/felixfbecker/php-language-server-protocol/tree/v1.5.2" + }, + "time": "2022-03-02T22:36:06+00:00" + }, + { + "name": "fidry/cpu-core-counter", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/theofidry/cpu-core-counter.git", + "reference": "f92996c4d5c1a696a6a970e20f7c4216200fcc42" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/f92996c4d5c1a696a6a970e20f7c4216200fcc42", + "reference": "f92996c4d5c1a696a6a970e20f7c4216200fcc42", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "fidry/makefile": "^0.2.0", + "fidry/php-cs-fixer-config": "^1.1.2", + "phpstan/extension-installer": "^1.2.0", + "phpstan/phpstan": "^1.9.2", + "phpstan/phpstan-deprecation-rules": "^1.0.0", + "phpstan/phpstan-phpunit": "^1.2.2", + "phpstan/phpstan-strict-rules": "^1.4.4", + "phpunit/phpunit": "^8.5.31 || ^9.5.26", + "webmozarts/strict-phpunit": "^7.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Fidry\\CpuCoreCounter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Théo FIDRY", + "email": "theo.fidry@gmail.com" + } + ], + "description": "Tiny utility to get the number of CPU cores.", + "keywords": [ + "CPU", + "core" + ], + "support": { + "issues": "https://github.com/theofidry/cpu-core-counter/issues", + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.1.0" + }, + "funding": [ + { + "url": "https://github.com/theofidry", + "type": "github" + } + ], + "time": "2024-02-07T09:43:46+00:00" + }, + { + "name": "filp/whoops", + "version": "2.15.4", + "source": { + "type": "git", + "url": "https://github.com/filp/whoops.git", + "reference": "a139776fa3f5985a50b509f2a02ff0f709d2a546" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/filp/whoops/zipball/a139776fa3f5985a50b509f2a02ff0f709d2a546", + "reference": "a139776fa3f5985a50b509f2a02ff0f709d2a546", + "shasum": "" + }, + "require": { + "php": "^5.5.9 || ^7.0 || ^8.0", + "psr/log": "^1.0.1 || ^2.0 || ^3.0" + }, + "require-dev": { + "mockery/mockery": "^0.9 || ^1.0", + "phpunit/phpunit": "^4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.3", + "symfony/var-dumper": "^2.6 || ^3.0 || ^4.0 || ^5.0" + }, + "suggest": { + "symfony/var-dumper": "Pretty print complex values better with var-dumper available", + "whoops/soap": "Formats errors as SOAP responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Whoops\\": "src/Whoops/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Filipe Dobreira", + "homepage": "https://github.com/filp", + "role": "Developer" + } + ], + "description": "php error handling for cool kids", + "homepage": "https://filp.github.io/whoops/", + "keywords": [ + "error", + "exception", + "handling", + "library", + "throwable", + "whoops" + ], + "support": { + "issues": "https://github.com/filp/whoops/issues", + "source": "https://github.com/filp/whoops/tree/2.15.4" + }, + "funding": [ + { + "url": "https://github.com/denis-sokolov", + "type": "github" + } + ], + "time": "2023-11-03T12:00:00+00:00" + }, + { + "name": "hamcrest/hamcrest-php", + "version": "v2.0.1", + "source": { + "type": "git", + "url": "https://github.com/hamcrest/hamcrest-php.git", + "reference": "8c3d0a3f6af734494ad8f6fbbee0ba92422859f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/8c3d0a3f6af734494ad8f6fbbee0ba92422859f3", + "reference": "8c3d0a3f6af734494ad8f6fbbee0ba92422859f3", + "shasum": "" + }, + "require": { + "php": "^5.3|^7.0|^8.0" + }, + "replace": { + "cordoval/hamcrest-php": "*", + "davedevelopment/hamcrest-php": "*", + "kodova/hamcrest-php": "*" + }, + "require-dev": { + "phpunit/php-file-iterator": "^1.4 || ^2.0", + "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1-dev" + } + }, + "autoload": { + "classmap": [ + "hamcrest" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "This is the PHP port of Hamcrest Matchers", + "keywords": [ + "test" + ], + "support": { + "issues": "https://github.com/hamcrest/hamcrest-php/issues", + "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.0.1" + }, + "time": "2020-07-09T08:09:16+00:00" + }, + { + "name": "khanamiryan/qrcode-detector-decoder", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/khanamiryan/php-qrcode-detector-decoder.git", + "reference": "8d53cbecaa32f1e56a3be58bb3055ac31774ecd0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/khanamiryan/php-qrcode-detector-decoder/zipball/8d53cbecaa32f1e56a3be58bb3055ac31774ecd0", + "reference": "8d53cbecaa32f1e56a3be58bb3055ac31774ecd0", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.5 | ^8.0 | ^9.0", + "rector/rector": "^0.13.6", + "symplify/easy-coding-standard": "^11.0", + "vimeo/psalm": "^4.24" + }, + "type": "library", + "autoload": { + "files": [ + "lib/Common/customFunctions.php" + ], + "psr-4": { + "Zxing\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT", + "Apache-2.0" + ], + "authors": [ + { + "name": "Ashot Khanamiryan", + "email": "a.khanamiryan@gmail.com", + "homepage": "https://github.com/khanamiryan", + "role": "Developer" + } + ], + "description": "QR code decoder / reader", + "homepage": "https://github.com/khanamiryan/php-qrcode-detector-decoder/", + "keywords": [ + "barcode", + "qr", + "zxing" + ], + "support": { + "issues": "https://github.com/khanamiryan/php-qrcode-detector-decoder/issues", + "source": "https://github.com/khanamiryan/php-qrcode-detector-decoder/tree/2.0.2" + }, + "time": "2022-11-17T10:54:53+00:00" + }, + { + "name": "laravel/dusk", + "version": "v7.13.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/dusk.git", + "reference": "dce7c4cc1c308bb18e95b2b3bf7d06d3f040a1f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/dusk/zipball/dce7c4cc1c308bb18e95b2b3bf7d06d3f040a1f6", + "reference": "dce7c4cc1c308bb18e95b2b3bf7d06d3f040a1f6", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-zip": "*", + "guzzlehttp/guzzle": "^7.2", + "illuminate/console": "^9.0|^10.0", + "illuminate/support": "^9.0|^10.0", + "nesbot/carbon": "^2.0", + "php": "^8.0", + "php-webdriver/webdriver": "^1.9.0", + "symfony/console": "^6.0", + "symfony/finder": "^6.0", + "symfony/process": "^6.0", + "vlucas/phpdotenv": "^5.2" + }, + "require-dev": { + "mockery/mockery": "^1.4.2", + "orchestra/testbench": "^7.33|^8.13", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.5.10|^10.0.1", + "psy/psysh": "^0.11.12" + }, + "suggest": { + "ext-pcntl": "Used to gracefully terminate Dusk when tests are running." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + }, + "laravel": { + "providers": [ + "Laravel\\Dusk\\DuskServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Dusk\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Laravel Dusk provides simple end-to-end testing and browser automation.", + "keywords": [ + "laravel", + "testing", + "webdriver" + ], + "support": { + "issues": "https://github.com/laravel/dusk/issues", + "source": "https://github.com/laravel/dusk/tree/v7.13.0" + }, + "time": "2024-02-23T22:29:53+00:00" + }, + { + "name": "laravel/legacy-factories", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/legacy-factories.git", + "reference": "6cb79f668fc36b8b396ada1da3ba45867889c30f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/legacy-factories/zipball/6cb79f668fc36b8b396ada1da3ba45867889c30f", + "reference": "6cb79f668fc36b8b396ada1da3ba45867889c30f", + "shasum": "" + }, + "require": { + "illuminate/macroable": "^8.0|^9.0|^10.0|^11.0", + "php": "^7.3|^8.0", + "symfony/finder": "^3.4|^4.0|^5.0|^6.0|^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + }, + "laravel": { + "providers": [ + "Illuminate\\Database\\Eloquent\\LegacyFactoryServiceProvider" + ] + } + }, + "autoload": { + "files": [ + "helpers.php" + ], + "psr-4": { + "Illuminate\\Database\\Eloquent\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The legacy version of the Laravel Eloquent factories.", + "homepage": "http://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2024-01-15T13:55:14+00:00" + }, + { + "name": "laravel/tinker", + "version": "v2.9.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/tinker.git", + "reference": "502e0fe3f0415d06d5db1f83a472f0f3b754bafe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/tinker/zipball/502e0fe3f0415d06d5db1f83a472f0f3b754bafe", + "reference": "502e0fe3f0415d06d5db1f83a472f0f3b754bafe", + "shasum": "" + }, + "require": { + "illuminate/console": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", + "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", + "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0", + "php": "^7.2.5|^8.0", + "psy/psysh": "^0.11.1|^0.12.0", + "symfony/var-dumper": "^4.3.4|^5.0|^6.0|^7.0" + }, + "require-dev": { + "mockery/mockery": "~1.3.3|^1.4.2", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^8.5.8|^9.3.3" + }, + "suggest": { + "illuminate/database": "The Illuminate Database package (^6.0|^7.0|^8.0|^9.0|^10.0|^11.0)." + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Tinker\\TinkerServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Tinker\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Powerful REPL for the Laravel framework.", + "keywords": [ + "REPL", + "Tinker", + "laravel", + "psysh" + ], + "support": { + "issues": "https://github.com/laravel/tinker/issues", + "source": "https://github.com/laravel/tinker/tree/v2.9.0" + }, + "time": "2024-01-04T16:10:04+00:00" + }, + { + "name": "matthiasnoback/live-code-coverage", + "version": "v1.6.0", + "source": { + "type": "git", + "url": "https://github.com/matthiasnoback/live-code-coverage.git", + "reference": "23d04096c01379f16770d96c0cdcf40d2b421743" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/matthiasnoback/live-code-coverage/zipball/23d04096c01379f16770d96c0cdcf40d2b421743", + "reference": "23d04096c01379f16770d96c0cdcf40d2b421743", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0", + "phpunit/php-code-coverage": "^9.0", + "phpunit/phpunit": "^9.3", + "webmozart/assert": "^1.2" + }, + "require-dev": { + "symfony/filesystem": "^3.3", + "symfony/finder": "^3.3", + "symfony/process": "^3.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "LiveCodeCoverage\\": "src/LiveCodeCoverage/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Generate code coverage reports on a live server", + "support": { + "issues": "https://github.com/matthiasnoback/live-code-coverage/issues", + "source": "https://github.com/matthiasnoback/live-code-coverage/tree/v1.6.0" + }, + "time": "2021-03-08T07:44:10+00:00" + }, + { + "name": "maximebf/debugbar", + "version": "v1.22.3", + "source": { + "type": "git", + "url": "https://github.com/maximebf/php-debugbar.git", + "reference": "7aa9a27a0b1158ed5ad4e7175e8d3aee9a818b96" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/7aa9a27a0b1158ed5ad4e7175e8d3aee9a818b96", + "reference": "7aa9a27a0b1158ed5ad4e7175e8d3aee9a818b96", + "shasum": "" + }, + "require": { + "php": "^7.2|^8", + "psr/log": "^1|^2|^3", + "symfony/var-dumper": "^4|^5|^6|^7" + }, + "require-dev": { + "dbrekelmans/bdi": "^1", + "phpunit/phpunit": "^8|^9", + "symfony/panther": "^1|^2.1", + "twig/twig": "^1.38|^2.7|^3.0" + }, + "suggest": { + "kriswallsmith/assetic": "The best way to manage assets", + "monolog/monolog": "Log using Monolog", + "predis/predis": "Redis storage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.22-dev" + } + }, + "autoload": { + "psr-4": { + "DebugBar\\": "src/DebugBar/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Maxime Bouroumeau-Fuseau", + "email": "maxime.bouroumeau@gmail.com", + "homepage": "http://maximebf.com" + }, + { + "name": "Barry vd. Heuvel", + "email": "barryvdh@gmail.com" + } + ], + "description": "Debug bar in the browser for php application", + "homepage": "https://github.com/maximebf/php-debugbar", + "keywords": [ + "debug", + "debugbar" + ], + "support": { + "issues": "https://github.com/maximebf/php-debugbar/issues", + "source": "https://github.com/maximebf/php-debugbar/tree/v1.22.3" + }, + "time": "2024-04-03T19:39:26+00:00" + }, + { + "name": "mockery/mockery", + "version": "1.6.11", + "source": { + "type": "git", + "url": "https://github.com/mockery/mockery.git", + "reference": "81a161d0b135df89951abd52296adf97deb0723d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mockery/mockery/zipball/81a161d0b135df89951abd52296adf97deb0723d", + "reference": "81a161d0b135df89951abd52296adf97deb0723d", + "shasum": "" + }, + "require": { + "hamcrest/hamcrest-php": "^2.0.1", + "lib-pcre": ">=7.0", + "php": ">=7.3" + }, + "conflict": { + "phpunit/phpunit": "<8.0" + }, + "require-dev": { + "phpunit/phpunit": "^8.5 || ^9.6.17", + "symplify/easy-coding-standard": "^12.1.14" + }, + "type": "library", + "autoload": { + "files": [ + "library/helpers.php", + "library/Mockery.php" + ], + "psr-4": { + "Mockery\\": "library/Mockery" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Pádraic Brady", + "email": "padraic.brady@gmail.com", + "homepage": "https://github.com/padraic", + "role": "Author" + }, + { + "name": "Dave Marshall", + "email": "dave.marshall@atstsolutions.co.uk", + "homepage": "https://davedevelopment.co.uk", + "role": "Developer" + }, + { + "name": "Nathanael Esayeas", + "email": "nathanael.esayeas@protonmail.com", + "homepage": "https://github.com/ghostwriter", + "role": "Lead Developer" + } + ], + "description": "Mockery is a simple yet flexible PHP mock object framework", + "homepage": "https://github.com/mockery/mockery", + "keywords": [ + "BDD", + "TDD", + "library", + "mock", + "mock objects", + "mockery", + "stub", + "test", + "test double", + "testing" + ], + "support": { + "docs": "https://docs.mockery.io/", + "issues": "https://github.com/mockery/mockery/issues", + "rss": "https://github.com/mockery/mockery/releases.atom", + "security": "https://github.com/mockery/mockery/security/advisories", + "source": "https://github.com/mockery/mockery" + }, + "time": "2024-03-21T18:34:15+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.11.1", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "reference": "7284c22080590fb39f2ffa3e9057f10a4ddd0e0c", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3,<3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.11.1" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2023-03-08T13:26:56+00:00" + }, + { + "name": "netresearch/jsonmapper", + "version": "v4.4.1", + "source": { + "type": "git", + "url": "https://github.com/cweiske/jsonmapper.git", + "reference": "132c75c7dd83e45353ebb9c6c9f591952995bbf0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cweiske/jsonmapper/zipball/132c75c7dd83e45353ebb9c6c9f591952995bbf0", + "reference": "132c75c7dd83e45353ebb9c6c9f591952995bbf0", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-spl": "*", + "php": ">=7.1" + }, + "require-dev": { + "phpunit/phpunit": "~7.5 || ~8.0 || ~9.0 || ~10.0", + "squizlabs/php_codesniffer": "~3.5" + }, + "type": "library", + "autoload": { + "psr-0": { + "JsonMapper": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "OSL-3.0" + ], + "authors": [ + { + "name": "Christian Weiske", + "email": "cweiske@cweiske.de", + "homepage": "http://github.com/cweiske/jsonmapper/", + "role": "Developer" + } + ], + "description": "Map nested JSON structures onto PHP classes", + "support": { + "email": "cweiske@cweiske.de", + "issues": "https://github.com/cweiske/jsonmapper/issues", + "source": "https://github.com/cweiske/jsonmapper/tree/v4.4.1" + }, + "time": "2024-01-31T06:18:54+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v4.19.1", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "4e1b88d21c69391150ace211e9eaf05810858d0b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/4e1b88d21c69391150ace211e9eaf05810858d0b", + "reference": "4e1b88d21c69391150ace211e9eaf05810858d0b", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=7.1" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v4.19.1" + }, + "time": "2024-03-17T08:10:35+00:00" + }, + { + "name": "nunomaduro/collision", + "version": "v6.4.0", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/collision.git", + "reference": "f05978827b9343cba381ca05b8c7deee346b6015" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/f05978827b9343cba381ca05b8c7deee346b6015", + "reference": "f05978827b9343cba381ca05b8c7deee346b6015", + "shasum": "" + }, + "require": { + "filp/whoops": "^2.14.5", + "php": "^8.0.0", + "symfony/console": "^6.0.2" + }, + "require-dev": { + "brianium/paratest": "^6.4.1", + "laravel/framework": "^9.26.1", + "laravel/pint": "^1.1.1", + "nunomaduro/larastan": "^1.0.3", + "nunomaduro/mock-final-classes": "^1.1.0", + "orchestra/testbench": "^7.7", + "phpunit/phpunit": "^9.5.23", + "spatie/ignition": "^1.4.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-develop": "6.x-dev" + }, + "laravel": { + "providers": [ + "NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "NunoMaduro\\Collision\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Cli error handling for console/command-line PHP applications.", + "keywords": [ + "artisan", + "cli", + "command-line", + "console", + "error", + "handling", + "laravel", + "laravel-zero", + "php", + "symfony" + ], + "support": { + "issues": "https://github.com/nunomaduro/collision/issues", + "source": "https://github.com/nunomaduro/collision" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://www.patreon.com/nunomaduro", + "type": "patreon" + } + ], + "time": "2023-01-03T12:54:54+00:00" + }, + { + "name": "nunomaduro/larastan", + "version": "v2.9.5", + "source": { + "type": "git", + "url": "https://github.com/larastan/larastan.git", + "reference": "101f1a4470f87326f4d3995411d28679d8800abe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/larastan/larastan/zipball/101f1a4470f87326f4d3995411d28679d8800abe", + "reference": "101f1a4470f87326f4d3995411d28679d8800abe", + "shasum": "" + }, + "require": { + "ext-json": "*", + "illuminate/console": "^9.52.16 || ^10.28.0 || ^11.0", + "illuminate/container": "^9.52.16 || ^10.28.0 || ^11.0", + "illuminate/contracts": "^9.52.16 || ^10.28.0 || ^11.0", + "illuminate/database": "^9.52.16 || ^10.28.0 || ^11.0", + "illuminate/http": "^9.52.16 || ^10.28.0 || ^11.0", + "illuminate/pipeline": "^9.52.16 || ^10.28.0 || ^11.0", + "illuminate/support": "^9.52.16 || ^10.28.0 || ^11.0", + "php": "^8.0.2", + "phpmyadmin/sql-parser": "^5.9.0", + "phpstan/phpstan": "^1.10.66" + }, + "require-dev": { + "doctrine/coding-standard": "^12.0", + "nikic/php-parser": "^4.19.1", + "orchestra/canvas": "^7.11.1 || ^8.11.0 || ^9.0.2", + "orchestra/testbench": "^7.33.0 || ^8.13.0 || ^9.0.3", + "phpunit/phpunit": "^9.6.13 || ^10.5.16" + }, + "suggest": { + "orchestra/testbench": "Using Larastan for analysing a package needs Testbench" + }, + "type": "phpstan-extension", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + } + }, + "autoload": { + "psr-4": { + "Larastan\\Larastan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Can Vural", + "email": "can9119@gmail.com" + }, + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Larastan - Discover bugs in your code without running it. A phpstan/phpstan wrapper for Laravel", + "keywords": [ + "PHPStan", + "code analyse", + "code analysis", + "larastan", + "laravel", + "package", + "php", + "static analysis" + ], + "support": { + "issues": "https://github.com/larastan/larastan/issues", + "source": "https://github.com/larastan/larastan/tree/v2.9.5" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/canvural", + "type": "github" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://www.patreon.com/nunomaduro", + "type": "patreon" + } + ], + "abandoned": "larastan/larastan", + "time": "2024-04-16T19:13:34+00:00" + }, + { + "name": "orchestra/canvas", + "version": "v7.11.1", + "source": { + "type": "git", + "url": "https://github.com/orchestral/canvas.git", + "reference": "ccfbf44bfd2b959fa05b6ad5c770c89641991edc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/orchestral/canvas/zipball/ccfbf44bfd2b959fa05b6ad5c770c89641991edc", + "reference": "ccfbf44bfd2b959fa05b6ad5c770c89641991edc", + "shasum": "" + }, + "require": { + "illuminate/database": "^9.52.15", + "illuminate/support": "^9.52.15", + "orchestra/canvas-core": "^7.7", + "orchestra/testbench-core": "^7.31", + "php": "^8.0", + "symfony/yaml": "^5.4 || ^6.0" + }, + "require-dev": { + "laravel/framework": "^9.52.15", + "laravel/pint": "^1.4", + "mockery/mockery": "^1.5.1", + "phpstan/phpstan": "^1.10.5", + "phpunit/phpunit": "^9.5.10", + "spatie/laravel-ray": "^1.32.4" + }, + "bin": [ + "canvas" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "8.0-dev" + }, + "laravel": { + "providers": [ + "Orchestra\\Canvas\\LaravelServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Orchestra\\Canvas\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Mior Muhammad Zaki", + "email": "crynobone@gmail.com" + } + ], + "description": "Code Generators for Laravel Applications and Packages", + "support": { + "issues": "https://github.com/orchestral/canvas/issues", + "source": "https://github.com/orchestral/canvas/tree/v7.11.1" + }, + "time": "2023-09-25T08:18:28+00:00" + }, + { + "name": "orchestra/canvas-core", + "version": "v7.7.0", + "source": { + "type": "git", + "url": "https://github.com/orchestral/canvas-core.git", + "reference": "7e1bc8933fd0bd40464e4119060065000fc2ab2f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/orchestral/canvas-core/zipball/7e1bc8933fd0bd40464e4119060065000fc2ab2f", + "reference": "7e1bc8933fd0bd40464e4119060065000fc2ab2f", + "shasum": "" + }, + "require": { + "illuminate/console": "^9.52.15", + "illuminate/filesystem": "^9.52.15", + "php": "^8.0" + }, + "conflict": { + "orchestra/canvas": "<7.10.0", + "orchestra/testbench-core": "<7.25.0" + }, + "require-dev": { + "fakerphp/faker": "^1.21", + "laravel/framework": "^9.52.15", + "laravel/pint": "^1.1", + "mockery/mockery": "^1.5.1", + "orchestra/testbench-core": "^7.31", + "orchestra/workbench": "^0.3", + "phpstan/phpstan": "^1.10.6", + "phpunit/phpunit": "^9.6", + "symfony/yaml": "^6.0.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "8.0-dev" + }, + "laravel": { + "providers": [ + "Orchestra\\Canvas\\Core\\LaravelServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Orchestra\\Canvas\\Core\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Mior Muhammad Zaki", + "email": "crynobone@gmail.com" + } + ], + "description": "Code Generators Builder for Laravel Applications and Packages", + "support": { + "issues": "https://github.com/orchestral/canvas/issues", + "source": "https://github.com/orchestral/canvas-core/tree/v7.7.0" + }, + "time": "2023-09-19T04:21:54+00:00" + }, + { + "name": "orchestra/testbench", + "version": "v7.41.3", + "source": { + "type": "git", + "url": "https://github.com/orchestral/testbench.git", + "reference": "a2b39ae75bca6b3078254242c0bed0fdd55b6aa4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/orchestral/testbench/zipball/a2b39ae75bca6b3078254242c0bed0fdd55b6aa4", + "reference": "a2b39ae75bca6b3078254242c0bed0fdd55b6aa4", + "shasum": "" + }, + "require": { + "fakerphp/faker": "^1.21", + "laravel/framework": "^9.52.15", + "mockery/mockery": "^1.5.1", + "orchestra/testbench-core": "^7.42.6", + "orchestra/workbench": "^1.4 || ^7.4", + "php": "^8.0", + "phpunit/phpunit": "^9.5.10", + "symfony/process": "^6.0.9", + "symfony/yaml": "^6.0.9", + "vlucas/phpdotenv": "^5.4.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.0-dev" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mior Muhammad Zaki", + "email": "crynobone@gmail.com", + "homepage": "https://github.com/crynobone" + } + ], + "description": "Laravel Testing Helper for Packages Development", + "homepage": "https://packages.tools/testbench/", + "keywords": [ + "BDD", + "TDD", + "dev", + "laravel", + "laravel-packages", + "testing" + ], + "support": { + "issues": "https://github.com/orchestral/testbench/issues", + "source": "https://github.com/orchestral/testbench/tree/v7.41.3" + }, + "time": "2024-04-16T09:17:54+00:00" + }, + { + "name": "orchestra/testbench-core", + "version": "v7.42.7", + "source": { + "type": "git", + "url": "https://github.com/orchestral/testbench-core.git", + "reference": "0120f5428e6ea9654a7ffe9de1b7d3b48db73375" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/orchestral/testbench-core/zipball/0120f5428e6ea9654a7ffe9de1b7d3b48db73375", + "reference": "0120f5428e6ea9654a7ffe9de1b7d3b48db73375", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "conflict": { + "brianium/paratest": "<6.4.0 || >=7.0.0", + "laravel/framework": "<9.52.9 || >=10.0.0", + "nunomaduro/collision": "<6.2.0 || >=7.0.0", + "orchestra/testbench-dusk": "<7.39.0 || >=8.0.0", + "orchestra/workbench": "<1.0.0", + "phpunit/phpunit": "<9.5.10 || >=10.0.0" + }, + "require-dev": { + "composer-runtime-api": "^2.2", + "fakerphp/faker": "^1.21", + "laravel/framework": "^9.52.9", + "laravel/pint": "^1.4", + "mockery/mockery": "^1.5.1", + "phpstan/phpstan": "^1.10.7", + "phpunit/phpunit": "^9.5.10", + "spatie/laravel-ray": "^1.32.4", + "symfony/process": "^6.0.9", + "symfony/yaml": "^6.0.9", + "vlucas/phpdotenv": "^5.4.1" + }, + "suggest": { + "brianium/paratest": "Allow using parallel testing (^6.4).", + "ext-pcntl": "Required to use all features of the console signal trapping.", + "fakerphp/faker": "Allow using Faker for testing (^1.21).", + "laravel/framework": "Required for testing (^9.52.9).", + "mockery/mockery": "Allow using Mockery for testing (^1.5.1).", + "nunomaduro/collision": "Allow using Laravel style tests output and parallel testing (^6.2).", + "orchestra/testbench-browser-kit": "Allow using legacy Laravel BrowserKit for testing (^7.0).", + "orchestra/testbench-dusk": "Allow using Laravel Dusk for testing (^7.0).", + "phpunit/phpunit": "Allow using PHPUnit for testing (^9.5.10).", + "symfony/process": "Required to use Orchestra\\Testbench\\remote function (^6.0.9).", + "symfony/yaml": "Required for Testbench CLI (^6.0.9).", + "vlucas/phpdotenv": "Required for Testbench CLI (^5.4.1)." + }, + "bin": [ + "testbench" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.0-dev" + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Orchestra\\Testbench\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mior Muhammad Zaki", + "email": "crynobone@gmail.com", + "homepage": "https://github.com/crynobone" + } + ], + "description": "Testing Helper for Laravel Development", + "homepage": "https://packages.tools/testbench", + "keywords": [ + "BDD", + "TDD", + "dev", + "laravel", + "laravel-packages", + "testing" + ], + "support": { + "issues": "https://github.com/orchestral/testbench/issues", + "source": "https://github.com/orchestral/testbench-core" + }, + "time": "2024-04-21T07:55:51+00:00" + }, + { + "name": "orchestra/workbench", + "version": "v7.4.0", + "source": { + "type": "git", + "url": "https://github.com/orchestral/workbench.git", + "reference": "06a0ccf09f07245753703ad406a4d3572788f375" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/orchestral/workbench/zipball/06a0ccf09f07245753703ad406a4d3572788f375", + "reference": "06a0ccf09f07245753703ad406a4d3572788f375", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.2", + "fakerphp/faker": "^1.21", + "laravel/framework": "^9.52.15", + "laravel/tinker": "^2.8.2", + "orchestra/canvas": "^7.11.1", + "orchestra/testbench-core": "^7.38", + "php": "^8.0", + "spatie/laravel-ray": "^1.32.4", + "symfony/polyfill-php83": "^1.28", + "symfony/yaml": "^6.0.9" + }, + "require-dev": { + "laravel/pint": "^1.4", + "mockery/mockery": "^1.5.1", + "phpstan/phpstan": "^1.10.7", + "phpunit/phpunit": "^9.6", + "symfony/process": "^6.0.9" + }, + "suggest": { + "ext-pcntl": "Required to use all features of the console signal trapping." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.5.x-dev" + } + }, + "autoload": { + "psr-4": { + "Orchestra\\Workbench\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mior Muhammad Zaki", + "email": "crynobone@gmail.com" + } + ], + "description": "Workbench Companion for Laravel Packages Development", + "keywords": [ + "dev", + "laravel", + "laravel-packages", + "testing" + ], + "support": { + "issues": "https://github.com/orchestral/workbench/issues", + "source": "https://github.com/orchestral/workbench/tree/v7.4.0" + }, + "time": "2024-03-13T05:58:30+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "php-webdriver/webdriver", + "version": "1.15.1", + "source": { + "type": "git", + "url": "https://github.com/php-webdriver/php-webdriver.git", + "reference": "cd52d9342c5aa738c2e75a67e47a1b6df97154e8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-webdriver/php-webdriver/zipball/cd52d9342c5aa738c2e75a67e47a1b6df97154e8", + "reference": "cd52d9342c5aa738c2e75a67e47a1b6df97154e8", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-zip": "*", + "php": "^7.3 || ^8.0", + "symfony/polyfill-mbstring": "^1.12", + "symfony/process": "^5.0 || ^6.0 || ^7.0" + }, + "replace": { + "facebook/webdriver": "*" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.20.0", + "ondram/ci-detector": "^4.0", + "php-coveralls/php-coveralls": "^2.4", + "php-mock/php-mock-phpunit": "^2.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpunit/phpunit": "^9.3", + "squizlabs/php_codesniffer": "^3.5", + "symfony/var-dumper": "^5.0 || ^6.0" + }, + "suggest": { + "ext-SimpleXML": "For Firefox profile creation" + }, + "type": "library", + "autoload": { + "files": [ + "lib/Exception/TimeoutException.php" + ], + "psr-4": { + "Facebook\\WebDriver\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A PHP client for Selenium WebDriver. Previously facebook/webdriver.", + "homepage": "https://github.com/php-webdriver/php-webdriver", + "keywords": [ + "Chromedriver", + "geckodriver", + "php", + "selenium", + "webdriver" + ], + "support": { + "issues": "https://github.com/php-webdriver/php-webdriver/issues", + "source": "https://github.com/php-webdriver/php-webdriver/tree/1.15.1" + }, + "time": "2023-10-20T12:21:20+00:00" + }, + { + "name": "phpmyadmin/sql-parser", + "version": "5.9.0", + "source": { + "type": "git", + "url": "https://github.com/phpmyadmin/sql-parser.git", + "reference": "011fa18a4e55591fac6545a821921dd1d61c6984" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpmyadmin/sql-parser/zipball/011fa18a4e55591fac6545a821921dd1d61c6984", + "reference": "011fa18a4e55591fac6545a821921dd1d61c6984", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0", + "symfony/polyfill-mbstring": "^1.3", + "symfony/polyfill-php80": "^1.16" + }, + "conflict": { + "phpmyadmin/motranslator": "<3.0" + }, + "require-dev": { + "phpbench/phpbench": "^1.1", + "phpmyadmin/coding-standard": "^3.0", + "phpmyadmin/motranslator": "^4.0 || ^5.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.9.12", + "phpstan/phpstan-phpunit": "^1.3.3", + "phpunit/php-code-coverage": "*", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "psalm/plugin-phpunit": "^0.16.1", + "vimeo/psalm": "^4.11", + "zumba/json-serializer": "~3.0.2" + }, + "suggest": { + "ext-mbstring": "For best performance", + "phpmyadmin/motranslator": "Translate messages to your favorite locale" + }, + "bin": [ + "bin/highlight-query", + "bin/lint-query", + "bin/sql-parser", + "bin/tokenize-query" + ], + "type": "library", + "autoload": { + "psr-4": { + "PhpMyAdmin\\SqlParser\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "authors": [ + { + "name": "The phpMyAdmin Team", + "email": "developers@phpmyadmin.net", + "homepage": "https://www.phpmyadmin.net/team/" + } + ], + "description": "A validating SQL lexer and parser with a focus on MySQL dialect.", + "homepage": "https://github.com/phpmyadmin/sql-parser", + "keywords": [ + "analysis", + "lexer", + "parser", + "query linter", + "sql", + "sql lexer", + "sql linter", + "sql parser", + "sql syntax highlighter", + "sql tokenizer" + ], + "support": { + "issues": "https://github.com/phpmyadmin/sql-parser/issues", + "source": "https://github.com/phpmyadmin/sql-parser" + }, + "funding": [ + { + "url": "https://www.phpmyadmin.net/donate/", + "type": "other" + } + ], + "time": "2024-01-20T20:34:02+00:00" + }, + { + "name": "phpstan/phpstan", + "version": "1.10.67", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan.git", + "reference": "16ddbe776f10da6a95ebd25de7c1dbed397dc493" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/16ddbe776f10da6a95ebd25de7c1dbed397dc493", + "reference": "16ddbe776f10da6a95ebd25de7c1dbed397dc493", + "shasum": "" + }, + "require": { + "php": "^7.2|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "keywords": [ + "dev", + "static analysis" + ], + "support": { + "docs": "https://phpstan.org/user-guide/getting-started", + "forum": "https://github.com/phpstan/phpstan/discussions", + "issues": "https://github.com/phpstan/phpstan/issues", + "security": "https://github.com/phpstan/phpstan/security/policy", + "source": "https://github.com/phpstan/phpstan-src" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://github.com/phpstan", + "type": "github" + } + ], + "time": "2024-04-16T07:22:02+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "9.2.31", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/48c34b5d8d983006bd2adc2d0de92963b9155965", + "reference": "48c34b5d8d983006bd2adc2d0de92963b9155965", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=7.3", + "phpunit/php-file-iterator": "^3.0.3", + "phpunit/php-text-template": "^2.0.2", + "sebastian/code-unit-reverse-lookup": "^2.0.2", + "sebastian/complexity": "^2.0", + "sebastian/environment": "^5.1.2", + "sebastian/lines-of-code": "^1.0.3", + "sebastian/version": "^3.0.1", + "theseer/tokenizer": "^1.2.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.31" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:37:42+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "3.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2021-12-02T12:48:52+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:58:55+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T05:33:50+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "5.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:16:10+00:00" + }, + { + "name": "phpunit/phpcov", + "version": "8.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpcov.git", + "reference": "8ec45dde34a84914a0ace355fbd6d7af2242c9a4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpcov/zipball/8ec45dde34a84914a0ace355fbd6d7af2242c9a4", + "reference": "8ec45dde34a84914a0ace355fbd6d7af2242c9a4", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "phpunit/php-code-coverage": "^9.2", + "phpunit/php-file-iterator": "^3.0", + "phpunit/phpunit": "^9.3", + "sebastian/cli-parser": "^1.0", + "sebastian/diff": "^4.0", + "sebastian/version": "^3.0" + }, + "bin": [ + "phpcov" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "8.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "CLI frontend for php-code-coverage", + "homepage": "https://github.com/sebastianbergmann/phpcov", + "support": { + "issues": "https://github.com/sebastianbergmann/phpcov/issues", + "source": "https://github.com/sebastianbergmann/phpcov/tree/8.2.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-03-24T12:07:05+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "9.6.19", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "a1a54a473501ef4cdeaae4e06891674114d79db8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a1a54a473501ef4cdeaae4e06891674114d79db8", + "reference": "a1a54a473501ef4cdeaae4e06891674114d79db8", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.3.1 || ^2", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.10.1", + "phar-io/manifest": "^2.0.3", + "phar-io/version": "^3.0.2", + "php": ">=7.3", + "phpunit/php-code-coverage": "^9.2.28", + "phpunit/php-file-iterator": "^3.0.5", + "phpunit/php-invoker": "^3.1.1", + "phpunit/php-text-template": "^2.0.3", + "phpunit/php-timer": "^5.0.2", + "sebastian/cli-parser": "^1.0.1", + "sebastian/code-unit": "^1.0.6", + "sebastian/comparator": "^4.0.8", + "sebastian/diff": "^4.0.3", + "sebastian/environment": "^5.1.3", + "sebastian/exporter": "^4.0.5", + "sebastian/global-state": "^5.0.1", + "sebastian/object-enumerator": "^4.0.3", + "sebastian/resource-operations": "^3.0.3", + "sebastian/type": "^3.2", + "sebastian/version": "^3.0.2" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "9.6-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.19" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2024-04-05T04:35:58+00:00" + }, + { + "name": "pimple/pimple", + "version": "v3.5.0", + "source": { + "type": "git", + "url": "https://github.com/silexphp/Pimple.git", + "reference": "a94b3a4db7fb774b3d78dad2315ddc07629e1bed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/silexphp/Pimple/zipball/a94b3a4db7fb774b3d78dad2315ddc07629e1bed", + "reference": "a94b3a4db7fb774b3d78dad2315ddc07629e1bed", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "psr/container": "^1.1 || ^2.0" + }, + "require-dev": { + "symfony/phpunit-bridge": "^5.4@dev" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4.x-dev" + } + }, + "autoload": { + "psr-0": { + "Pimple": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "Pimple, a simple Dependency Injection Container", + "homepage": "https://pimple.symfony.com", + "keywords": [ + "container", + "dependency injection" + ], + "support": { + "source": "https://github.com/silexphp/Pimple/tree/v3.5.0" + }, + "time": "2021-10-28T11:13:42+00:00" + }, + { + "name": "psalm/plugin-laravel", + "version": "v2.9.0", + "source": { + "type": "git", + "url": "https://github.com/psalm/psalm-plugin-laravel.git", + "reference": "98e1a875358cc2e25ae377f8f42c5e4ebcb6d076" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/psalm/psalm-plugin-laravel/zipball/98e1a875358cc2e25ae377f8f42c5e4ebcb6d076", + "reference": "98e1a875358cc2e25ae377f8f42c5e4ebcb6d076", + "shasum": "" + }, + "require": { + "barryvdh/laravel-ide-helper": "^2.13 || ^3.0", + "ext-simplexml": "*", + "illuminate/config": "^9.48 || ^10.0 || ^11.0", + "illuminate/container": "^9.48 || ^10.0 || ^11.0", + "illuminate/contracts": "^9.48 || ^10.0 || ^11.0", + "illuminate/database": "^9.48 || ^10.0 || ^11.0", + "illuminate/events": "^9.48 || ^10.0 || ^11.0", + "illuminate/http": "^9.48 || ^10.0 || ^11.0", + "illuminate/routing": "^9.48 || ^10.0 || ^11.0", + "illuminate/support": "^9.48 || ^10.0 || ^11.0", + "illuminate/view": "^9.48 || ^10.0 || ^11.0", + "nikic/php-parser": "^4.13", + "orchestra/testbench": "^7.19 || ^8.0 || ^9.0", + "php": "^8.0.2", + "symfony/console": "^6.0 || ^7.0", + "vimeo/psalm": "^4.30 || ^5.1" + }, + "require-dev": { + "codeception/codeception": "^5.0", + "codeception/module-asserts": "^3.0", + "codeception/module-cli": "^2.0", + "codeception/module-filesystem": "^3.0", + "codeception/module-phpbrowser": "^3.0", + "phpunit/phpunit": "^9.6 || ^10.0", + "ramsey/collection": "^1.3", + "slevomat/coding-standard": "^8.8", + "squizlabs/php_codesniffer": "*", + "symfony/http-foundation": "^6.0 || ^7.0" + }, + "type": "psalm-plugin", + "extra": { + "psalm": { + "pluginClass": "Psalm\\LaravelPlugin\\Plugin" + } + }, + "autoload": { + "psr-4": { + "Psalm\\LaravelPlugin\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Matthew Brown", + "email": "github@muglug.com" + } + ], + "description": "A Laravel plugin for Psalm", + "homepage": "https://github.com/psalm/psalm-plugin-laravel", + "support": { + "issues": "https://github.com/psalm/psalm-plugin-laravel/issues", + "source": "https://github.com/psalm/psalm-plugin-laravel/tree/v2.9.0" + }, + "time": "2024-03-12T20:51:12+00:00" + }, + { + "name": "psy/psysh", + "version": "v0.12.3", + "source": { + "type": "git", + "url": "https://github.com/bobthecow/psysh.git", + "reference": "b6b6cce7d3ee8fbf31843edce5e8f5a72eff4a73" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/b6b6cce7d3ee8fbf31843edce5e8f5a72eff4a73", + "reference": "b6b6cce7d3ee8fbf31843edce5e8f5a72eff4a73", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-tokenizer": "*", + "nikic/php-parser": "^5.0 || ^4.0", + "php": "^8.0 || ^7.4", + "symfony/console": "^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4", + "symfony/var-dumper": "^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4" + }, + "conflict": { + "symfony/console": "4.4.37 || 5.3.14 || 5.3.15 || 5.4.3 || 5.4.4 || 6.0.3 || 6.0.4" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.2" + }, + "suggest": { + "ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)", + "ext-pdo-sqlite": "The doc command requires SQLite to work.", + "ext-posix": "If you have PCNTL, you'll want the POSIX extension as well." + }, + "bin": [ + "bin/psysh" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "0.12.x-dev" + }, + "bamarni-bin": { + "bin-links": false, + "forward-command": false + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Psy\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Justin Hileman", + "email": "justin@justinhileman.info", + "homepage": "http://justinhileman.com" + } + ], + "description": "An interactive shell for modern PHP.", + "homepage": "http://psysh.org", + "keywords": [ + "REPL", + "console", + "interactive", + "shell" + ], + "support": { + "issues": "https://github.com/bobthecow/psysh/issues", + "source": "https://github.com/bobthecow/psysh/tree/v0.12.3" + }, + "time": "2024-04-02T15:57:53+00:00" + }, + { + "name": "rector/rector", + "version": "1.0.4", + "source": { + "type": "git", + "url": "https://github.com/rectorphp/rector.git", + "reference": "6e04d0eb087aef707fa0c5686d33d6ff61f4a555" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/6e04d0eb087aef707fa0c5686d33d6ff61f4a555", + "reference": "6e04d0eb087aef707fa0c5686d33d6ff61f4a555", + "shasum": "" + }, + "require": { + "php": "^7.2|^8.0", + "phpstan/phpstan": "^1.10.57" + }, + "conflict": { + "rector/rector-doctrine": "*", + "rector/rector-downgrade-php": "*", + "rector/rector-phpunit": "*", + "rector/rector-symfony": "*" + }, + "suggest": { + "ext-dom": "To manipulate phpunit.xml via the custom-rule command" + }, + "bin": [ + "bin/rector" + ], + "type": "library", + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Instant Upgrade and Automated Refactoring of any PHP code", + "keywords": [ + "automation", + "dev", + "migration", + "refactoring" + ], + "support": { + "issues": "https://github.com/rectorphp/rector/issues", + "source": "https://github.com/rectorphp/rector/tree/1.0.4" + }, + "funding": [ + { + "url": "https://github.com/tomasvotruba", + "type": "github" + } + ], + "time": "2024-04-05T09:01:07+00:00" + }, + { + "name": "roave/security-advisories", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/Roave/SecurityAdvisories.git", + "reference": "a6cc84fe50abd91fdbfa06fa0e7b93386aa2193c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/a6cc84fe50abd91fdbfa06fa0e7b93386aa2193c", + "reference": "a6cc84fe50abd91fdbfa06fa0e7b93386aa2193c", + "shasum": "" + }, + "conflict": { + "3f/pygmentize": "<1.2", + "admidio/admidio": "<4.2.13", + "adodb/adodb-php": "<=5.20.20|>=5.21,<=5.21.3", + "aheinze/cockpit": "<2.2", + "aimeos/aimeos-typo3": "<19.10.12|>=20,<20.10.5", + "airesvsg/acf-to-rest-api": "<=3.1", + "akaunting/akaunting": "<2.1.13", + "akeneo/pim-community-dev": "<5.0.119|>=6,<6.0.53", + "alextselegidis/easyappointments": "<1.5", + "alterphp/easyadmin-extension-bundle": ">=1.2,<1.2.11|>=1.3,<1.3.1", + "amazing/media2click": ">=1,<1.3.3", + "amphp/artax": "<1.0.6|>=2,<2.0.6", + "amphp/http": "<=1.7.2|>=2,<=2.1", + "amphp/http-client": ">=4,<4.4", + "anchorcms/anchor-cms": "<=0.12.7", + "andreapollastri/cipi": "<=3.1.15", + "andrewhaine/silverstripe-form-capture": ">=0.2,<=0.2.3|>=1,<1.0.2|>=2,<2.2.5", + "apache-solr-for-typo3/solr": "<2.8.3", + "apereo/phpcas": "<1.6", + "api-platform/core": ">=2.2,<2.2.10|>=2.3,<2.3.6|>=2.6,<2.7.10|>=3,<3.0.12|>=3.1,<3.1.3", + "appwrite/server-ce": "<=1.2.1", + "arc/web": "<3", + "area17/twill": "<1.2.5|>=2,<2.5.3", + "artesaos/seotools": "<0.17.2", + "asymmetricrypt/asymmetricrypt": "<9.9.99", + "athlon1600/php-proxy": "<=5.1", + "athlon1600/php-proxy-app": "<=3", + "austintoddj/canvas": "<=3.4.2", + "automad/automad": "<=1.10.9", + "automattic/jetpack": "<9.8", + "awesome-support/awesome-support": "<=6.0.7", + "aws/aws-sdk-php": "<3.288.1", + "azuracast/azuracast": "<0.18.3", + "backdrop/backdrop": "<1.24.2", + "backpack/crud": "<3.4.9", + "bacula-web/bacula-web": "<8.0.0.0-RC2-dev", + "badaso/core": "<2.7", + "bagisto/bagisto": "<2.1", + "barrelstrength/sprout-base-email": "<1.2.7", + "barrelstrength/sprout-forms": "<3.9", + "barryvdh/laravel-translation-manager": "<0.6.2", + "barzahlen/barzahlen-php": "<2.0.1", + "baserproject/basercms": "<5.0.9", + "bassjobsen/bootstrap-3-typeahead": ">4.0.2", + "bbpress/bbpress": "<2.6.5", + "bcosca/fatfree": "<3.7.2", + "bedita/bedita": "<4", + "bigfork/silverstripe-form-capture": ">=3,<3.1.1", + "billz/raspap-webgui": "<2.9.5", + "bk2k/bootstrap-package": ">=7.1,<7.1.2|>=8,<8.0.8|>=9,<9.0.4|>=9.1,<9.1.3|>=10,<10.0.10|>=11,<11.0.3", + "blueimp/jquery-file-upload": "==6.4.4", + "bmarshall511/wordpress_zero_spam": "<5.2.13", + "bolt/bolt": "<3.7.2", + "bolt/core": "<=4.2", + "bottelet/flarepoint": "<2.2.1", + "bref/bref": "<2.1.17", + "brightlocal/phpwhois": "<=4.2.5", + "brotkrueml/codehighlight": "<2.7", + "brotkrueml/schema": "<1.13.1|>=2,<2.5.1", + "brotkrueml/typo3-matomo-integration": "<1.3.2", + "buddypress/buddypress": "<7.2.1", + "bugsnag/bugsnag-laravel": ">=2,<2.0.2", + "bytefury/crater": "<6.0.2", + "cachethq/cachet": "<2.5.1", + "cakephp/cakephp": "<3.10.3|>=4,<4.0.10|>=4.1,<4.1.4|>=4.2,<4.2.12|>=4.3,<4.3.11|>=4.4,<4.4.10", + "cakephp/database": ">=4.2,<4.2.12|>=4.3,<4.3.11|>=4.4,<4.4.10", + "cardgate/magento2": "<2.0.33", + "cardgate/woocommerce": "<=3.1.15", + "cart2quote/module-quotation": ">=4.1.6,<=4.4.5|>=5,<5.4.4", + "cartalyst/sentry": "<=2.1.6", + "catfan/medoo": "<1.7.5", + "causal/oidc": "<2.1", + "cecil/cecil": "<7.47.1", + "centreon/centreon": "<22.10.15", + "cesnet/simplesamlphp-module-proxystatistics": "<3.1", + "chriskacerguis/codeigniter-restserver": "<=2.7.1", + "civicrm/civicrm-core": ">=4.2,<4.2.9|>=4.3,<4.3.3", + "ckeditor/ckeditor": "<4.24", + "cockpit-hq/cockpit": "<=2.6.3|==2.7", + "codeception/codeception": "<3.1.3|>=4,<4.1.22", + "codeigniter/framework": "<3.1.9", + "codeigniter4/framework": "<4.4.7", + "codeigniter4/shield": "<1.0.0.0-beta8", + "codiad/codiad": "<=2.8.4", + "composer/composer": "<1.10.27|>=2,<2.2.23|>=2.3,<2.7", + "concrete5/concrete5": "<9.2.8", + "concrete5/core": "<8.5.8|>=9,<9.1", + "contao-components/mediaelement": ">=2.14.2,<2.21.1", + "contao/comments-bundle": ">=2,<4.13.40|>=5.0.0.0-RC1-dev,<5.3.4", + "contao/contao": ">=3,<3.5.37|>=4,<4.4.56|>=4.5,<4.9.40|>=4.10,<4.11.7|>=4.13,<4.13.21|>=5.1,<5.1.4", + "contao/core": "<3.5.39", + "contao/core-bundle": "<4.13.40|>=5,<5.3.4", + "contao/listing-bundle": ">=3,<=3.5.30|>=4,<4.4.8", + "contao/managed-edition": "<=1.5", + "corveda/phpsandbox": "<1.3.5", + "cosenary/instagram": "<=2.3", + "craftcms/cms": "<4.6.2", + "croogo/croogo": "<4", + "cuyz/valinor": "<0.12", + "czproject/git-php": "<4.0.3", + "dapphp/securimage": "<3.6.6", + "darylldoyle/safe-svg": "<1.9.10", + "datadog/dd-trace": ">=0.30,<0.30.2", + "datatables/datatables": "<1.10.10", + "david-garcia/phpwhois": "<=4.3.1", + "dbrisinajumi/d2files": "<1", + "dcat/laravel-admin": "<=2.1.3.0-beta", + "derhansen/fe_change_pwd": "<2.0.5|>=3,<3.0.3", + "derhansen/sf_event_mgt": "<4.3.1|>=5,<5.1.1|>=7,<7.4", + "desperado/xml-bundle": "<=0.1.7", + "devgroup/dotplant": "<2020.09.14-dev", + "directmailteam/direct-mail": "<6.0.3|>=7,<7.0.3|>=8,<9.5.2", + "doctrine/annotations": "<1.2.7", + "doctrine/cache": ">=1,<1.3.2|>=1.4,<1.4.2", + "doctrine/common": "<2.4.3|>=2.5,<2.5.1", + "doctrine/dbal": ">=2,<2.0.8|>=2.1,<2.1.2|>=3,<3.1.4", + "doctrine/doctrine-bundle": "<1.5.2", + "doctrine/doctrine-module": "<=0.7.1", + "doctrine/mongodb-odm": "<1.0.2", + "doctrine/mongodb-odm-bundle": "<3.0.1", + "doctrine/orm": ">=2,<2.4.8|>=2.5,<2.5.1|>=2.8.3,<2.8.4", + "dolibarr/dolibarr": "<=19", + "dompdf/dompdf": "<2.0.4", + "doublethreedigital/guest-entries": "<3.1.2", + "drupal/core": ">=6,<6.38|>=7,<7.96|>=8,<10.1.8|>=10.2,<10.2.2", + "drupal/drupal": ">=5,<5.11|>=6,<6.38|>=7,<7.80|>=8,<8.9.16|>=9,<9.1.12|>=9.2,<9.2.4", + "duncanmcclean/guest-entries": "<3.1.2", + "dweeves/magmi": "<=0.7.24", + "ec-cube/ec-cube": "<2.4.4|>=2.11,<=2.17.1|>=3,<=3.0.18.0-patch4|>=4,<=4.1.2", + "ecodev/newsletter": "<=4", + "ectouch/ectouch": "<=2.7.2", + "egroupware/egroupware": "<16.1.20170922", + "elefant/cms": "<2.0.7", + "elgg/elgg": "<3.3.24|>=4,<4.0.5", + "elijaa/phpmemcacheadmin": "<=1.3", + "encore/laravel-admin": "<=1.8.19", + "endroid/qr-code-bundle": "<3.4.2", + "enhavo/enhavo-app": "<=0.13.1", + "enshrined/svg-sanitize": "<0.15", + "erusev/parsedown": "<1.7.2", + "ether/logs": "<3.0.4", + "evolutioncms/evolution": "<=3.2.3", + "exceedone/exment": "<4.4.3|>=5,<5.0.3", + "exceedone/laravel-admin": "<2.2.3|==3", + "ezsystems/demobundle": ">=5.4,<5.4.6.1-dev", + "ezsystems/ez-support-tools": ">=2.2,<2.2.3", + "ezsystems/ezdemo-ls-extension": ">=5.4,<5.4.2.1-dev", + "ezsystems/ezfind-ls": ">=5.3,<5.3.6.1-dev|>=5.4,<5.4.11.1-dev|>=2017.12,<2017.12.0.1-dev", + "ezsystems/ezplatform": "<=1.13.6|>=2,<=2.5.24", + "ezsystems/ezplatform-admin-ui": ">=1.3,<1.3.5|>=1.4,<1.4.6|>=1.5,<1.5.29|>=2.3,<2.3.26", + "ezsystems/ezplatform-admin-ui-assets": ">=4,<4.2.1|>=5,<5.0.1|>=5.1,<5.1.1", + "ezsystems/ezplatform-graphql": ">=1.0.0.0-RC1-dev,<1.0.13|>=2.0.0.0-beta1,<2.3.12", + "ezsystems/ezplatform-kernel": "<1.2.5.1-dev|>=1.3,<1.3.35", + "ezsystems/ezplatform-rest": ">=1.2,<=1.2.2|>=1.3,<1.3.8", + "ezsystems/ezplatform-richtext": ">=2.3,<2.3.7.1-dev", + "ezsystems/ezplatform-solr-search-engine": ">=1.7,<1.7.12|>=2,<2.0.2|>=3.3,<3.3.15", + "ezsystems/ezplatform-user": ">=1,<1.0.1", + "ezsystems/ezpublish-kernel": "<6.13.8.2-dev|>=7,<7.5.31", + "ezsystems/ezpublish-legacy": "<=2017.12.7.3|>=2018.06,<=2019.03.5.1", + "ezsystems/platform-ui-assets-bundle": ">=4.2,<4.2.3", + "ezsystems/repository-forms": ">=2.3,<2.3.2.1-dev|>=2.5,<2.5.15", + "ezyang/htmlpurifier": "<4.1.1", + "facade/ignition": "<1.16.15|>=2,<2.4.2|>=2.5,<2.5.2", + "facturascripts/facturascripts": "<=2022.08", + "fastly/magento2": "<1.2.26", + "feehi/cms": "<=2.1.1", + "feehi/feehicms": "<=2.1.1", + "fenom/fenom": "<=2.12.1", + "filegator/filegator": "<7.8", + "filp/whoops": "<2.1.13", + "fineuploader/php-traditional-server": "<=1.2.2", + "firebase/php-jwt": "<6", + "fixpunkt/fp-masterquiz": "<2.2.1|>=3,<3.5.2", + "fixpunkt/fp-newsletter": "<1.1.1|>=2,<2.1.2|>=2.2,<3.2.6", + "flarum/core": "<1.8.5", + "flarum/flarum": "<0.1.0.0-beta8", + "flarum/framework": "<1.8.5", + "flarum/mentions": "<1.6.3", + "flarum/sticky": ">=0.1.0.0-beta14,<=0.1.0.0-beta15", + "flarum/tags": "<=0.1.0.0-beta13", + "floriangaerber/magnesium": "<0.3.1", + "fluidtypo3/vhs": "<5.1.1", + "fof/byobu": ">=0.3.0.0-beta2,<1.1.7", + "fof/upload": "<1.2.3", + "foodcoopshop/foodcoopshop": ">=3.2,<3.6.1", + "fooman/tcpdf": "<6.2.22", + "forkcms/forkcms": "<5.11.1", + "fossar/tcpdf-parser": "<6.2.22", + "francoisjacquet/rosariosis": "<=11.5.1", + "frappant/frp-form-answers": "<3.1.2|>=4,<4.0.2", + "friendsofsymfony/oauth2-php": "<1.3", + "friendsofsymfony/rest-bundle": ">=1.2,<1.2.2", + "friendsofsymfony/user-bundle": ">=1.2,<1.3.5", + "friendsofsymfony1/swiftmailer": ">=4,<5.4.13|>=6,<6.2.5", + "friendsofsymfony1/symfony1": ">=1.1,<1.15.19", + "friendsoftypo3/mediace": ">=7.6.2,<7.6.5", + "friendsoftypo3/openid": ">=4.5,<4.5.31|>=4.7,<4.7.16|>=6,<6.0.11|>=6.1,<6.1.6", + "froala/wysiwyg-editor": "<3.2.7|>=4.0.1,<=4.1.3", + "froxlor/froxlor": "<=2.1.1", + "frozennode/administrator": "<=5.0.12", + "fuel/core": "<1.8.1", + "funadmin/funadmin": "<=3.2|>=3.3.2,<=3.3.3", + "gaoming13/wechat-php-sdk": "<=1.10.2", + "genix/cms": "<=1.1.11", + "getgrav/grav": "<1.7.45", + "getkirby/cms": "<4.1.1", + "getkirby/kirby": "<=2.5.12", + "getkirby/panel": "<2.5.14", + "getkirby/starterkit": "<=3.7.0.2", + "gilacms/gila": "<=1.15.4", + "gleez/cms": "<=1.2|==2", + "globalpayments/php-sdk": "<2", + "gogentooss/samlbase": "<1.2.7", + "google/protobuf": "<3.15", + "gos/web-socket-bundle": "<1.10.4|>=2,<2.6.1|>=3,<3.3", + "gree/jose": "<2.2.1", + "gregwar/rst": "<1.0.3", + "grumpydictator/firefly-iii": "<6.1.7", + "gugoan/economizzer": "<=0.9.0.0-beta1", + "guzzlehttp/guzzle": "<6.5.8|>=7,<7.4.5", + "guzzlehttp/psr7": "<1.9.1|>=2,<2.4.5", + "haffner/jh_captcha": "<=2.1.3|>=3,<=3.0.2", + "harvesthq/chosen": "<1.8.7", + "helloxz/imgurl": "<=2.31", + "hhxsv5/laravel-s": "<3.7.36", + "hillelcoren/invoice-ninja": "<5.3.35", + "himiklab/yii2-jqgrid-widget": "<1.0.8", + "hjue/justwriting": "<=1", + "hov/jobfair": "<1.0.13|>=2,<2.0.2", + "httpsoft/http-message": "<1.0.12", + "hyn/multi-tenant": ">=5.6,<5.7.2", + "ibexa/admin-ui": ">=4.2,<4.2.3", + "ibexa/core": ">=4,<4.0.7|>=4.1,<4.1.4|>=4.2,<4.2.3|>=4.5,<4.5.6|>=4.6,<4.6.2", + "ibexa/graphql": ">=2.5,<2.5.31|>=3.3,<3.3.28|>=4.2,<4.2.3", + "ibexa/post-install": "<=1.0.4", + "ibexa/solr": ">=4.5,<4.5.4", + "ibexa/user": ">=4,<4.4.3", + "icecoder/icecoder": "<=8.1", + "idno/known": "<=1.3.1", + "ilicmiljan/secure-props": ">=1.2,<1.2.2", + "illuminate/auth": "<5.5.10", + "illuminate/cookie": ">=4,<=4.0.11|>=4.1,<=4.1.99999|>=4.2,<=4.2.99999|>=5,<=5.0.99999|>=5.1,<=5.1.99999|>=5.2,<=5.2.99999|>=5.3,<=5.3.99999|>=5.4,<=5.4.99999|>=5.5,<=5.5.49|>=5.6,<=5.6.99999|>=5.7,<=5.7.99999|>=5.8,<=5.8.99999|>=6,<6.18.31|>=7,<7.22.4", + "illuminate/database": "<6.20.26|>=7,<7.30.5|>=8,<8.40", + "illuminate/encryption": ">=4,<=4.0.11|>=4.1,<=4.1.31|>=4.2,<=4.2.22|>=5,<=5.0.35|>=5.1,<=5.1.46|>=5.2,<=5.2.45|>=5.3,<=5.3.31|>=5.4,<=5.4.36|>=5.5,<5.5.40|>=5.6,<5.6.15", + "illuminate/view": "<6.20.42|>=7,<7.30.6|>=8,<8.75", + "imdbphp/imdbphp": "<=5.1.1", + "impresscms/impresscms": "<=1.4.5", + "impresspages/impresspages": "<=1.0.12", + "in2code/femanager": "<5.5.3|>=6,<6.3.4|>=7,<7.2.3", + "in2code/ipandlanguageredirect": "<5.1.2", + "in2code/lux": "<17.6.1|>=18,<24.0.2", + "innologi/typo3-appointments": "<2.0.6", + "intelliants/subrion": "<4.2.2", + "inter-mediator/inter-mediator": "==5.5", + "islandora/islandora": ">=2,<2.4.1", + "ivankristianto/phpwhois": "<=4.3", + "jackalope/jackalope-doctrine-dbal": "<1.7.4", + "james-heinrich/getid3": "<1.9.21", + "james-heinrich/phpthumb": "<1.7.12", + "jasig/phpcas": "<1.3.3", + "jcbrand/converse.js": "<3.3.3", + "johnbillion/wp-crontrol": "<1.16.2", + "joomla/application": "<1.0.13", + "joomla/archive": "<1.1.12|>=2,<2.0.1", + "joomla/filesystem": "<1.6.2|>=2,<2.0.1", + "joomla/filter": "<1.4.4|>=2,<2.0.1", + "joomla/framework": "<1.5.7|>=2.5.4,<=3.8.12", + "joomla/input": ">=2,<2.0.2", + "joomla/joomla-cms": ">=2.5,<3.9.12", + "joomla/session": "<1.3.1", + "joyqi/hyper-down": "<=2.4.27", + "jsdecena/laracom": "<2.0.9", + "jsmitty12/phpwhois": "<5.1", + "juzaweb/cms": "<=3.4", + "kazist/phpwhois": "<=4.2.6", + "kelvinmo/simplexrd": "<3.1.1", + "kevinpapst/kimai2": "<1.16.7", + "khodakhah/nodcms": "<=3", + "kimai/kimai": "<2.13", + "kitodo/presentation": "<3.2.3|>=3.3,<3.3.4", + "klaviyo/magento2-extension": ">=1,<3", + "knplabs/knp-snappy": "<=1.4.2", + "kohana/core": "<3.3.3", + "krayin/laravel-crm": "<1.2.2", + "kreait/firebase-php": ">=3.2,<3.8.1", + "kumbiaphp/kumbiapp": "<=1.1.1", + "la-haute-societe/tcpdf": "<6.2.22", + "laminas/laminas-diactoros": "<2.18.1|==2.19|==2.20|==2.21|==2.22|==2.23|>=2.24,<2.24.2|>=2.25,<2.25.2", + "laminas/laminas-form": "<2.17.1|>=3,<3.0.2|>=3.1,<3.1.1", + "laminas/laminas-http": "<2.14.2", + "laravel/fortify": "<1.11.1", + "laravel/framework": "<6.20.44|>=7,<7.30.6|>=8,<8.75", + "laravel/laravel": ">=5.4,<5.4.22", + "laravel/socialite": ">=1,<1.0.99|>=2,<2.0.10", + "latte/latte": "<2.10.8", + "lavalite/cms": "<=9|==10.1", + "lcobucci/jwt": ">=3.4,<3.4.6|>=4,<4.0.4|>=4.1,<4.1.5", + "league/commonmark": "<0.18.3", + "league/flysystem": "<1.1.4|>=2,<2.1.1", + "league/oauth2-server": ">=8.3.2,<8.4.2|>=8.5,<8.5.3", + "lexik/jwt-authentication-bundle": "<2.10.7|>=2.11,<2.11.3", + "libreform/libreform": ">=2,<=2.0.8", + "librenms/librenms": "<2017.08.18", + "liftkit/database": "<2.13.2", + "lightsaml/lightsaml": "<1.3.5", + "limesurvey/limesurvey": "<3.27.19", + "livehelperchat/livehelperchat": "<=3.91", + "livewire/livewire": ">2.2.4,<2.2.6|>=3.3.5,<3.4.9", + "lms/routes": "<2.1.1", + "localizationteam/l10nmgr": "<7.4|>=8,<8.7|>=9,<9.2", + "luyadev/yii-helpers": "<1.2.1", + "magento/community-edition": "<2.4.3.0-patch3|>=2.4.4,<2.4.5", + "magento/core": "<=1.9.4.5", + "magento/magento1ce": "<1.9.4.3-dev", + "magento/magento1ee": ">=1,<1.14.4.3-dev", + "magento/product-community-edition": ">=2,<2.2.10|>=2.3,<2.3.2.0-patch2", + "magneto/core": "<1.9.4.4-dev", + "maikuolan/phpmussel": ">=1,<1.6", + "mainwp/mainwp": "<=4.4.3.3", + "mantisbt/mantisbt": "<2.26.1", + "marcwillmann/turn": "<0.3.3", + "matyhtf/framework": "<3.0.6", + "mautic/core": "<4.4.12|>=5.0.0.0-alpha,<5.0.4", + "mdanter/ecc": "<2", + "mediawiki/core": "<1.36.2", + "mediawiki/matomo": "<2.4.3", + "mediawiki/semantic-media-wiki": "<4.0.2", + "melisplatform/melis-asset-manager": "<5.0.1", + "melisplatform/melis-cms": "<5.0.1", + "melisplatform/melis-front": "<5.0.1", + "mezzio/mezzio-swoole": "<3.7|>=4,<4.3", + "mgallegos/laravel-jqgrid": "<=1.3", + "microsoft/microsoft-graph": ">=1.16,<1.109.1|>=2,<2.0.1", + "microsoft/microsoft-graph-beta": "<2.0.1", + "microsoft/microsoft-graph-core": "<2.0.2", + "microweber/microweber": "<=2.0.4", + "mikehaertl/php-shellcommand": "<1.6.1", + "miniorange/miniorange-saml": "<1.4.3", + "mittwald/typo3_forum": "<1.2.1", + "mobiledetect/mobiledetectlib": "<2.8.32", + "modx/revolution": "<=2.8.3.0-patch", + "mojo42/jirafeau": "<4.4", + "mongodb/mongodb": ">=1,<1.9.2", + "monolog/monolog": ">=1.8,<1.12", + "moodle/moodle": "<=4.3.3", + "mos/cimage": "<0.7.19", + "movim/moxl": ">=0.8,<=0.10", + "movingbytes/social-network": "<=1.2.1", + "mpdf/mpdf": "<=7.1.7", + "munkireport/comment": "<4.1", + "munkireport/managedinstalls": "<2.6", + "munkireport/munki_facts": "<1.5", + "munkireport/munkireport": ">=2.5.3,<5.6.3", + "munkireport/reportdata": "<3.5", + "munkireport/softwareupdate": "<1.6", + "mustache/mustache": ">=2,<2.14.1", + "namshi/jose": "<2.2", + "neoan3-apps/template": "<1.1.1", + "neorazorx/facturascripts": "<2022.04", + "neos/flow": ">=1,<1.0.4|>=1.1,<1.1.1|>=2,<2.0.1|>=2.3,<2.3.16|>=3,<3.0.12|>=3.1,<3.1.10|>=3.2,<3.2.13|>=3.3,<3.3.13|>=4,<4.0.6", + "neos/form": ">=1.2,<4.3.3|>=5,<5.0.9|>=5.1,<5.1.3", + "neos/media-browser": "<7.3.19|>=8,<8.0.16|>=8.1,<8.1.11|>=8.2,<8.2.11|>=8.3,<8.3.9", + "neos/neos": ">=1.1,<1.1.3|>=1.2,<1.2.13|>=2,<2.0.4|>=2.3,<2.9.99|>=3,<3.0.20|>=3.1,<3.1.18|>=3.2,<3.2.14|>=3.3,<5.3.10|>=7,<7.0.9|>=7.1,<7.1.7|>=7.2,<7.2.6|>=7.3,<7.3.4|>=8,<8.0.2", + "neos/swiftmailer": ">=4.1,<4.1.99|>=5.4,<5.4.5", + "netgen/tagsbundle": ">=3.4,<3.4.11|>=4,<4.0.15", + "nette/application": ">=2,<2.0.19|>=2.1,<2.1.13|>=2.2,<2.2.10|>=2.3,<2.3.14|>=2.4,<2.4.16|>=3,<3.0.6", + "nette/nette": ">=2,<2.0.19|>=2.1,<2.1.13", + "nilsteampassnet/teampass": "<3.0.10", + "nonfiction/nterchange": "<4.1.1", + "notrinos/notrinos-erp": "<=0.7", + "noumo/easyii": "<=0.9", + "nukeviet/nukeviet": "<4.5.02", + "nyholm/psr7": "<1.6.1", + "nystudio107/craft-seomatic": "<3.4.12", + "nzedb/nzedb": "<0.8", + "nzo/url-encryptor-bundle": ">=4,<4.3.2|>=5,<5.0.1", + "october/backend": "<1.1.2", + "october/cms": "<1.0.469|==1.0.469|==1.0.471|==1.1.1", + "october/october": "<=3.4.4", + "october/rain": "<1.0.472|>=1.1,<1.1.2", + "october/system": "<1.0.476|>=1.1,<1.1.12|>=2,<2.2.34|>=3,<3.5.2", + "omeka/omeka-s": "<4.0.3", + "onelogin/php-saml": "<2.10.4", + "oneup/uploader-bundle": ">=1,<1.9.3|>=2,<2.1.5", + "open-web-analytics/open-web-analytics": "<1.7.4", + "opencart/opencart": "<=3.0.3.7|>=4,<4.0.2.3-dev", + "openid/php-openid": "<2.3", + "openmage/magento-lts": "<20.5", + "opensolutions/vimbadmin": "<=3.0.15", + "opensource-workshop/connect-cms": "<1.7.2|>=2,<2.3.2", + "orchid/platform": ">=9,<9.4.4|>=14.0.0.0-alpha4,<14.5", + "oro/calendar-bundle": ">=4.2,<=4.2.6|>=5,<=5.0.6|>=5.1,<5.1.1", + "oro/commerce": ">=4.1,<5.0.11|>=5.1,<5.1.1", + "oro/crm": ">=1.7,<1.7.4|>=3.1,<4.1.17|>=4.2,<4.2.7", + "oro/crm-call-bundle": ">=4.2,<=4.2.5|>=5,<5.0.4|>=5.1,<5.1.1", + "oro/customer-portal": ">=4.1,<=4.1.13|>=4.2,<=4.2.10|>=5,<=5.0.11|>=5.1,<=5.1.3", + "oro/platform": ">=1.7,<1.7.4|>=3.1,<3.1.29|>=4.1,<4.1.17|>=4.2,<=4.2.10|>=5,<=5.0.12|>=5.1,<=5.1.3", + "oxid-esales/oxideshop-ce": "<4.5", + "oxid-esales/paymorrow-module": ">=1,<1.0.2|>=2,<2.0.1", + "packbackbooks/lti-1-3-php-library": "<5", + "padraic/humbug_get_contents": "<1.1.2", + "pagarme/pagarme-php": "<3", + "pagekit/pagekit": "<=1.0.18", + "paragonie/random_compat": "<2", + "passbolt/passbolt_api": "<4.6.2", + "paypal/adaptivepayments-sdk-php": "<=3.9.2", + "paypal/invoice-sdk-php": "<=3.9", + "paypal/merchant-sdk-php": "<3.12", + "paypal/permissions-sdk-php": "<=3.9.1", + "pear/archive_tar": "<1.4.14", + "pear/auth": "<1.2.4", + "pear/crypt_gpg": "<1.6.7", + "pear/pear": "<=1.10.1", + "pegasus/google-for-jobs": "<1.5.1|>=2,<2.1.1", + "personnummer/personnummer": "<3.0.2", + "phanan/koel": "<5.1.4", + "phenx/php-svg-lib": "<0.5.2", + "php-mod/curl": "<2.3.2", + "phpbb/phpbb": "<3.2.10|>=3.3,<3.3.1", + "phpems/phpems": ">=6,<=6.1.3", + "phpfastcache/phpfastcache": "<6.1.5|>=7,<7.1.2|>=8,<8.0.7", + "phpmailer/phpmailer": "<6.5", + "phpmussel/phpmussel": ">=1,<1.6", + "phpmyadmin/phpmyadmin": "<5.2.1", + "phpmyfaq/phpmyfaq": "<3.2.5|==3.2.5", + "phpoffice/common": "<0.2.9", + "phpoffice/phpexcel": "<1.8", + "phpoffice/phpspreadsheet": "<1.16", + "phpseclib/phpseclib": "<2.0.47|>=3,<3.0.36", + "phpservermon/phpservermon": "<3.6", + "phpsysinfo/phpsysinfo": "<3.4.3", + "phpunit/phpunit": ">=4.8.19,<4.8.28|>=5.0.10,<5.6.3", + "phpwhois/phpwhois": "<=4.2.5", + "phpxmlrpc/extras": "<0.6.1", + "phpxmlrpc/phpxmlrpc": "<4.9.2", + "pi/pi": "<=2.5", + "pimcore/admin-ui-classic-bundle": "<1.3.4", + "pimcore/customer-management-framework-bundle": "<4.0.6", + "pimcore/data-hub": "<1.2.4", + "pimcore/demo": "<10.3", + "pimcore/ecommerce-framework-bundle": "<1.0.10", + "pimcore/perspective-editor": "<1.5.1", + "pimcore/pimcore": "<11.2.3", + "pixelfed/pixelfed": "<0.11.11", + "plotly/plotly.js": "<2.25.2", + "pocketmine/bedrock-protocol": "<8.0.2", + "pocketmine/pocketmine-mp": "<5.11.2", + "pocketmine/raklib": ">=0.14,<0.14.6|>=0.15,<0.15.1", + "pressbooks/pressbooks": "<5.18", + "prestashop/autoupgrade": ">=4,<4.10.1", + "prestashop/blockreassurance": "<=5.1.3", + "prestashop/blockwishlist": ">=2,<2.1.1", + "prestashop/contactform": ">=1.0.1,<4.3", + "prestashop/gamification": "<2.3.2", + "prestashop/prestashop": "<8.1.4", + "prestashop/productcomments": "<5.0.2", + "prestashop/ps_emailsubscription": "<2.6.1", + "prestashop/ps_facetedsearch": "<3.4.1", + "prestashop/ps_linklist": "<3.1", + "privatebin/privatebin": "<1.4", + "processwire/processwire": "<=3.0.210", + "propel/propel": ">=2.0.0.0-alpha1,<=2.0.0.0-alpha7", + "propel/propel1": ">=1,<=1.7.1", + "pterodactyl/panel": "<1.7", + "ptheofan/yii2-statemachine": ">=2.0.0.0-RC1-dev,<=2", + "ptrofimov/beanstalk_console": "<1.7.14", + "pubnub/pubnub": "<6.1", + "pusher/pusher-php-server": "<2.2.1", + "pwweb/laravel-core": "<=0.3.6.0-beta", + "pyrocms/pyrocms": "<=3.9.1", + "qcubed/qcubed": "<=3.1.1", + "quickapps/cms": "<=2.0.0.0-beta2", + "rainlab/blog-plugin": "<1.4.1", + "rainlab/debugbar-plugin": "<3.1", + "rainlab/user-plugin": "<=1.4.5", + "rankmath/seo-by-rank-math": "<=1.0.95", + "rap2hpoutre/laravel-log-viewer": "<0.13", + "react/http": ">=0.7,<1.9", + "really-simple-plugins/complianz-gdpr": "<6.4.2", + "redaxo/source": "<=5.15.1", + "remdex/livehelperchat": "<4.29", + "reportico-web/reportico": "<=8.1", + "rhukster/dom-sanitizer": "<1.0.7", + "rmccue/requests": ">=1.6,<1.8", + "robrichards/xmlseclibs": ">=1,<3.0.4", + "roots/soil": "<4.1", + "rudloff/alltube": "<3.0.3", + "s-cart/core": "<6.9", + "s-cart/s-cart": "<6.9", + "sabberworm/php-css-parser": ">=1,<1.0.1|>=2,<2.0.1|>=3,<3.0.1|>=4,<4.0.1|>=5,<5.0.9|>=5.1,<5.1.3|>=5.2,<5.2.1|>=6,<6.0.2|>=7,<7.0.4|>=8,<8.0.1|>=8.1,<8.1.1|>=8.2,<8.2.1|>=8.3,<8.3.1", + "sabre/dav": ">=1.6,<1.7.11|>=1.8,<1.8.9", + "scheb/two-factor-bundle": "<3.26|>=4,<4.11", + "sensiolabs/connect": "<4.2.3", + "serluck/phpwhois": "<=4.2.6", + "sfroemken/url_redirect": "<=1.2.1", + "sheng/yiicms": "<=1.2", + "shopware/core": "<6.5.8.8-dev|>=6.6.0.0-RC1-dev,<6.6.1", + "shopware/platform": "<6.5.8.8-dev|>=6.6.0.0-RC1-dev,<6.6.1", + "shopware/production": "<=6.3.5.2", + "shopware/shopware": "<6.2.3", + "shopware/storefront": "<=6.4.8.1|>=6.5.8,<6.5.8.7-dev", + "shopxo/shopxo": "<2.2.6", + "showdoc/showdoc": "<2.10.4", + "silverstripe-australia/advancedreports": ">=1,<=2", + "silverstripe/admin": "<1.13.19|>=2,<2.1.8", + "silverstripe/assets": ">=1,<1.11.1", + "silverstripe/cms": "<4.11.3", + "silverstripe/comments": ">=1.3,<1.9.99|>=2,<2.9.99|>=3,<3.1.1", + "silverstripe/forum": "<=0.6.1|>=0.7,<=0.7.3", + "silverstripe/framework": "<4.13.39|>=5,<5.1.11", + "silverstripe/graphql": ">=2,<2.0.5|>=3,<3.8.2|>=4,<4.3.7|>=5,<5.1.3", + "silverstripe/hybridsessions": ">=1,<2.4.1|>=2.5,<2.5.1", + "silverstripe/recipe-cms": ">=4.5,<4.5.3", + "silverstripe/registry": ">=2.1,<2.1.2|>=2.2,<2.2.1", + "silverstripe/restfulserver": ">=1,<1.0.9|>=2,<2.0.4|>=2.1,<2.1.2", + "silverstripe/silverstripe-omnipay": "<2.5.2|>=3,<3.0.2|>=3.1,<3.1.4|>=3.2,<3.2.1", + "silverstripe/subsites": ">=2,<2.6.1", + "silverstripe/taxonomy": ">=1.3,<1.3.1|>=2,<2.0.1", + "silverstripe/userforms": "<3|>=5,<5.4.2", + "silverstripe/versioned-admin": ">=1,<1.11.1", + "simple-updates/phpwhois": "<=1", + "simplesamlphp/saml2": "<1.10.6|>=2,<2.3.8|>=3,<3.1.4|==5.0.0.0-alpha12", + "simplesamlphp/simplesamlphp": "<1.18.6", + "simplesamlphp/simplesamlphp-module-infocard": "<1.0.1", + "simplesamlphp/simplesamlphp-module-openid": "<1", + "simplesamlphp/simplesamlphp-module-openidprovider": "<0.9", + "simplesamlphp/xml-security": "==1.6.11", + "simplito/elliptic-php": "<1.0.6", + "sitegeist/fluid-components": "<3.5", + "sjbr/sr-freecap": "<2.4.6|>=2.5,<2.5.3", + "slim/psr7": "<1.4.1|>=1.5,<1.5.1|>=1.6,<1.6.1", + "slim/slim": "<2.6", + "slub/slub-events": "<3.0.3", + "smarty/smarty": "<3.1.48|>=4,<4.3.1", + "snipe/snipe-it": "<=6.2.2", + "socalnick/scn-social-auth": "<1.15.2", + "socialiteproviders/steam": "<1.1", + "spatie/browsershot": "<3.57.4", + "spipu/html2pdf": "<5.2.8", + "spoon/library": "<1.4.1", + "spoonity/tcpdf": "<6.2.22", + "squizlabs/php_codesniffer": ">=1,<2.8.1|>=3,<3.0.1", + "ssddanbrown/bookstack": "<22.02.3", + "statamic/cms": "<4.46", + "stormpath/sdk": "<9.9.99", + "studio-42/elfinder": "<2.1.62", + "subhh/libconnect": "<7.0.8|>=8,<8.1", + "sukohi/surpass": "<1", + "sulu/sulu": "<1.6.44|>=2,<2.4.17|>=2.5,<2.5.13", + "sumocoders/framework-user-bundle": "<1.4", + "superbig/craft-audit": "<3.0.2", + "swag/paypal": "<5.4.4", + "swiftmailer/swiftmailer": "<6.2.5", + "swiftyedit/swiftyedit": "<1.2", + "sylius/admin-bundle": ">=1,<1.0.17|>=1.1,<1.1.9|>=1.2,<1.2.2", + "sylius/grid": ">=1,<1.1.19|>=1.2,<1.2.18|>=1.3,<1.3.13|>=1.4,<1.4.5|>=1.5,<1.5.1", + "sylius/grid-bundle": "<1.10.1", + "sylius/paypal-plugin": ">=1,<1.2.4|>=1.3,<1.3.1", + "sylius/resource-bundle": ">=1,<1.3.14|>=1.4,<1.4.7|>=1.5,<1.5.2|>=1.6,<1.6.4", + "sylius/sylius": "<=1.12.13", + "symbiote/silverstripe-multivaluefield": ">=3,<3.0.99", + "symbiote/silverstripe-queuedjobs": ">=3,<3.0.2|>=3.1,<3.1.4|>=4,<4.0.7|>=4.1,<4.1.2|>=4.2,<4.2.4|>=4.3,<4.3.3|>=4.4,<4.4.3|>=4.5,<4.5.1|>=4.6,<4.6.4", + "symbiote/silverstripe-seed": "<6.0.3", + "symbiote/silverstripe-versionedfiles": "<=2.0.3", + "symfont/process": ">=0", + "symfony/cache": ">=3.1,<3.4.35|>=4,<4.2.12|>=4.3,<4.3.8", + "symfony/dependency-injection": ">=2,<2.0.17|>=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7", + "symfony/error-handler": ">=4.4,<4.4.4|>=5,<5.0.4", + "symfony/form": ">=2.3,<2.3.35|>=2.4,<2.6.12|>=2.7,<2.7.50|>=2.8,<2.8.49|>=3,<3.4.20|>=4,<4.0.15|>=4.1,<4.1.9|>=4.2,<4.2.1", + "symfony/framework-bundle": ">=2,<2.3.18|>=2.4,<2.4.8|>=2.5,<2.5.2|>=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7|>=5.3.14,<5.3.15|>=5.4.3,<5.4.4|>=6.0.3,<6.0.4", + "symfony/http-foundation": ">=2,<2.8.52|>=3,<3.4.35|>=4,<4.2.12|>=4.3,<4.3.8|>=4.4,<4.4.7|>=5,<5.0.7", + "symfony/http-kernel": ">=2,<4.4.50|>=5,<5.4.20|>=6,<6.0.20|>=6.1,<6.1.12|>=6.2,<6.2.6", + "symfony/intl": ">=2.7,<2.7.38|>=2.8,<2.8.31|>=3,<3.2.14|>=3.3,<3.3.13", + "symfony/maker-bundle": ">=1.27,<1.29.2|>=1.30,<1.31.1", + "symfony/mime": ">=4.3,<4.3.8", + "symfony/phpunit-bridge": ">=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7", + "symfony/polyfill": ">=1,<1.10", + "symfony/polyfill-php55": ">=1,<1.10", + "symfony/proxy-manager-bridge": ">=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7", + "symfony/routing": ">=2,<2.0.19", + "symfony/security": ">=2,<2.7.51|>=2.8,<3.4.49|>=4,<4.4.24|>=5,<5.2.8", + "symfony/security-bundle": ">=2,<4.4.50|>=5,<5.4.20|>=6,<6.0.20|>=6.1,<6.1.12|>=6.2,<6.2.6", + "symfony/security-core": ">=2.4,<2.6.13|>=2.7,<2.7.9|>=2.7.30,<2.7.32|>=2.8,<3.4.49|>=4,<4.4.24|>=5,<5.2.9", + "symfony/security-csrf": ">=2.4,<2.7.48|>=2.8,<2.8.41|>=3,<3.3.17|>=3.4,<3.4.11|>=4,<4.0.11", + "symfony/security-guard": ">=2.8,<3.4.48|>=4,<4.4.23|>=5,<5.2.8", + "symfony/security-http": ">=2.3,<2.3.41|>=2.4,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.2.12|>=4.3,<4.3.8|>=4.4,<4.4.7|>=5,<5.0.7|>=5.1,<5.2.8|>=5.3,<5.3.2|>=5.4,<5.4.31|>=6,<6.3.8", + "symfony/serializer": ">=2,<2.0.11|>=4.1,<4.4.35|>=5,<5.3.12", + "symfony/symfony": ">=2,<4.4.51|>=5,<5.4.31|>=6,<6.3.8", + "symfony/translation": ">=2,<2.0.17", + "symfony/twig-bridge": ">=2,<4.4.51|>=5,<5.4.31|>=6,<6.3.8", + "symfony/ux-autocomplete": "<2.11.2", + "symfony/validator": ">=2,<2.0.24|>=2.1,<2.1.12|>=2.2,<2.2.5|>=2.3,<2.3.3", + "symfony/var-exporter": ">=4.2,<4.2.12|>=4.3,<4.3.8", + "symfony/web-profiler-bundle": ">=2,<2.3.19|>=2.4,<2.4.9|>=2.5,<2.5.4", + "symfony/webhook": ">=6.3,<6.3.8", + "symfony/yaml": ">=2,<2.0.22|>=2.1,<2.1.7|>=2.2.0.0-beta1,<2.2.0.0-beta2", + "symphonycms/symphony-2": "<2.6.4", + "t3/dce": "<0.11.5|>=2.2,<2.6.2", + "t3g/svg-sanitizer": "<1.0.3", + "t3s/content-consent": "<1.0.3|>=2,<2.0.2", + "tastyigniter/tastyigniter": "<3.3", + "tcg/voyager": "<=1.4", + "tecnickcom/tcpdf": "<=6.7.4", + "terminal42/contao-tablelookupwizard": "<3.3.5", + "thelia/backoffice-default-template": ">=2.1,<2.1.2", + "thelia/thelia": ">=2.1,<2.1.3", + "theonedemon/phpwhois": "<=4.2.5", + "thinkcmf/thinkcmf": "<6.0.8", + "thorsten/phpmyfaq": "<3.2.2", + "tikiwiki/tiki-manager": "<=17.1", + "timber/timber": ">=0.16.6,<1.23.1|>=1.24,<1.24.1|>=2,<2.1", + "tinymce/tinymce": "<7", + "tinymighty/wiki-seo": "<1.2.2", + "titon/framework": "<9.9.99", + "tobiasbg/tablepress": "<=2.0.0.0-RC1", + "topthink/framework": "<6.0.14", + "topthink/think": "<=6.1.1", + "topthink/thinkphp": "<=3.2.3", + "torrentpier/torrentpier": "<=2.4.1", + "tpwd/ke_search": "<4.0.3|>=4.1,<4.6.6|>=5,<5.0.2", + "tribalsystems/zenario": "<=9.4.59197", + "truckersmp/phpwhois": "<=4.3.1", + "ttskch/pagination-service-provider": "<1", + "twig/twig": "<1.44.7|>=2,<2.15.3|>=3,<3.4.3", + "typo3/cms": "<9.5.29|>=10,<10.4.35|>=11,<11.5.23|>=12,<12.2", + "typo3/cms-backend": "<4.1.14|>=4.2,<4.2.15|>=4.3,<4.3.7|>=4.4,<4.4.4|>=7,<=7.6.50|>=8,<=8.7.39|>=9,<=9.5.24|>=10,<=10.4.13|>=11,<=11.1", + "typo3/cms-core": "<=8.7.56|>=9,<=9.5.45|>=10,<=10.4.42|>=11,<=11.5.34|>=12,<=12.4.10|==13", + "typo3/cms-extbase": "<6.2.24|>=7,<7.6.8|==8.1.1", + "typo3/cms-fluid": "<4.3.4|>=4.4,<4.4.1", + "typo3/cms-form": ">=8,<=8.7.39|>=9,<=9.5.24|>=10,<=10.4.13|>=11,<=11.1", + "typo3/cms-frontend": "<4.3.9|>=4.4,<4.4.5", + "typo3/cms-install": "<4.1.14|>=4.2,<4.2.16|>=4.3,<4.3.9|>=4.4,<4.4.5|>=12.2,<12.4.8", + "typo3/cms-rte-ckeditor": ">=9.5,<9.5.42|>=10,<10.4.39|>=11,<11.5.30", + "typo3/flow": ">=1,<1.0.4|>=1.1,<1.1.1|>=2,<2.0.1|>=2.3,<2.3.16|>=3,<3.0.12|>=3.1,<3.1.10|>=3.2,<3.2.13|>=3.3,<3.3.13|>=4,<4.0.6", + "typo3/html-sanitizer": ">=1,<=1.5.2|>=2,<=2.1.3", + "typo3/neos": ">=1.1,<1.1.3|>=1.2,<1.2.13|>=2,<2.0.4|>=2.3,<2.3.99|>=3,<3.0.20|>=3.1,<3.1.18|>=3.2,<3.2.14|>=3.3,<3.3.23|>=4,<4.0.17|>=4.1,<4.1.16|>=4.2,<4.2.12|>=4.3,<4.3.3", + "typo3/phar-stream-wrapper": ">=1,<2.1.1|>=3,<3.1.1", + "typo3/swiftmailer": ">=4.1,<4.1.99|>=5.4,<5.4.5", + "typo3fluid/fluid": ">=2,<2.0.8|>=2.1,<2.1.7|>=2.2,<2.2.4|>=2.3,<2.3.7|>=2.4,<2.4.4|>=2.5,<2.5.11|>=2.6,<2.6.10", + "ua-parser/uap-php": "<3.8", + "uasoft-indonesia/badaso": "<=2.9.7", + "unisharp/laravel-filemanager": "<2.6.4", + "userfrosting/userfrosting": ">=0.3.1,<4.6.3", + "usmanhalalit/pixie": "<1.0.3|>=2,<2.0.2", + "uvdesk/community-skeleton": "<=1.1.1", + "uvdesk/core-framework": "<=1.1.1", + "vanilla/safecurl": "<0.9.2", + "verbb/comments": "<1.5.5", + "verbb/image-resizer": "<2.0.9", + "verbb/knock-knock": "<1.2.8", + "verot/class.upload.php": "<=2.1.6", + "villagedefrance/opencart-overclocked": "<=1.11.1", + "vova07/yii2-fileapi-widget": "<0.1.9", + "vrana/adminer": "<4.8.1", + "waldhacker/hcaptcha": "<2.1.2", + "wallabag/tcpdf": "<6.2.22", + "wallabag/wallabag": "<2.6.7", + "wanglelecc/laracms": "<=1.0.3", + "web-auth/webauthn-framework": ">=3.3,<3.3.4", + "web-feet/coastercms": "==5.5", + "webbuilders-group/silverstripe-kapost-bridge": "<0.4", + "webcoast/deferred-image-processing": "<1.0.2", + "webklex/laravel-imap": "<5.3", + "webklex/php-imap": "<5.3", + "webpa/webpa": "<3.1.2", + "wikibase/wikibase": "<=1.39.3", + "wikimedia/parsoid": "<0.12.2", + "willdurand/js-translation-bundle": "<2.1.1", + "winter/wn-backend-module": "<1.2.4", + "winter/wn-dusk-plugin": "<2.1", + "winter/wn-system-module": "<1.2.4", + "wintercms/winter": "<=1.2.3", + "woocommerce/woocommerce": "<6.6", + "wp-cli/wp-cli": ">=0.12,<2.5", + "wp-graphql/wp-graphql": "<=1.14.5", + "wp-premium/gravityforms": "<2.4.21", + "wpanel/wpanel4-cms": "<=4.3.1", + "wpcloud/wp-stateless": "<3.2", + "wpglobus/wpglobus": "<=1.9.6", + "wwbn/avideo": "<=12.4", + "xataface/xataface": "<3", + "xpressengine/xpressengine": "<3.0.15", + "yab/quarx": "<2.4.5", + "yeswiki/yeswiki": "<4.1", + "yetiforce/yetiforce-crm": "<=6.4", + "yidashi/yii2cmf": "<=2", + "yii2mod/yii2-cms": "<1.9.2", + "yiisoft/yii": "<1.1.29", + "yiisoft/yii2": "<2.0.38", + "yiisoft/yii2-authclient": "<2.2.15", + "yiisoft/yii2-bootstrap": "<2.0.4", + "yiisoft/yii2-dev": "<2.0.43", + "yiisoft/yii2-elasticsearch": "<2.0.5", + "yiisoft/yii2-gii": "<=2.2.4", + "yiisoft/yii2-jui": "<2.0.4", + "yiisoft/yii2-redis": "<2.0.8", + "yikesinc/yikes-inc-easy-mailchimp-extender": "<6.8.6", + "yoast-seo-for-typo3/yoast_seo": "<7.2.3", + "yourls/yourls": "<=1.8.2", + "yuan1994/tpadmin": "<=1.3.12", + "zencart/zencart": "<=1.5.7.0-beta", + "zendesk/zendesk_api_client_php": "<2.2.11", + "zendframework/zend-cache": ">=2.4,<2.4.8|>=2.5,<2.5.3", + "zendframework/zend-captcha": ">=2,<2.4.9|>=2.5,<2.5.2", + "zendframework/zend-crypt": ">=2,<2.4.9|>=2.5,<2.5.2", + "zendframework/zend-db": "<2.2.10|>=2.3,<2.3.5", + "zendframework/zend-developer-tools": ">=1.2.2,<1.2.3", + "zendframework/zend-diactoros": "<1.8.4", + "zendframework/zend-feed": "<2.10.3", + "zendframework/zend-form": ">=2,<2.2.7|>=2.3,<2.3.1", + "zendframework/zend-http": "<2.8.1", + "zendframework/zend-json": ">=2.1,<2.1.6|>=2.2,<2.2.6", + "zendframework/zend-ldap": ">=2,<2.0.99|>=2.1,<2.1.99|>=2.2,<2.2.8|>=2.3,<2.3.3", + "zendframework/zend-mail": "<2.4.11|>=2.5,<2.7.2", + "zendframework/zend-navigation": ">=2,<2.2.7|>=2.3,<2.3.1", + "zendframework/zend-session": ">=2,<2.0.99|>=2.1,<2.1.99|>=2.2,<2.2.9|>=2.3,<2.3.4", + "zendframework/zend-validator": ">=2.3,<2.3.6", + "zendframework/zend-view": ">=2,<2.2.7|>=2.3,<2.3.1", + "zendframework/zend-xmlrpc": ">=2.1,<2.1.6|>=2.2,<2.2.6", + "zendframework/zendframework": "<=3", + "zendframework/zendframework1": "<1.12.20", + "zendframework/zendopenid": "<2.0.2", + "zendframework/zendrest": "<2.0.2", + "zendframework/zendservice-amazon": "<2.0.3", + "zendframework/zendservice-api": "<1", + "zendframework/zendservice-audioscrobbler": "<2.0.2", + "zendframework/zendservice-nirvanix": "<2.0.2", + "zendframework/zendservice-slideshare": "<2.0.2", + "zendframework/zendservice-technorati": "<2.0.2", + "zendframework/zendservice-windowsazure": "<2.0.2", + "zendframework/zendxml": ">=1,<1.0.1", + "zenstruck/collection": "<0.2.1", + "zetacomponents/mail": "<1.8.2", + "zf-commons/zfc-user": "<1.2.2", + "zfcampus/zf-apigility-doctrine": ">=1,<1.0.3", + "zfr/zfr-oauth2-server-module": "<0.1.2", + "zoujingli/thinkadmin": "<=6.1.53" + }, + "type": "metapackage", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "role": "maintainer" + }, + { + "name": "Ilya Tribusean", + "email": "slash3b@gmail.com", + "role": "maintainer" + } + ], + "description": "Prevents installation of composer packages with known security vulnerabilities: no API, simply require it", + "keywords": [ + "dev" + ], + "support": { + "issues": "https://github.com/Roave/SecurityAdvisories/issues", + "source": "https://github.com/Roave/SecurityAdvisories/tree/latest" + }, + "funding": [ + { + "url": "https://github.com/Ocramius", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/roave/security-advisories", + "type": "tidelift" + } + ], + "time": "2024-04-30T09:04:31+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:27:43+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "1.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", + "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:08:54+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T05:30:19+00:00" + }, + { + "name": "sebastian/comparator", + "version": "4.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", + "reference": "fa0f136dd2334583309d32b62544682ee972b51a", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/diff": "^4.0", + "sebastian/exporter": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2022-09-14T12:41:17+00:00" + }, + { + "name": "sebastian/complexity", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a", + "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-22T06:19:30+00:00" + }, + { + "name": "sebastian/diff", + "version": "4.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", + "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:30:58+00:00" + }, + { + "name": "sebastian/environment", + "version": "5.1.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:03:51+00:00" + }, + { + "name": "sebastian/exporter", + "version": "4.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72", + "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:33:00+00:00" + }, + { + "name": "sebastian/global-state", + "version": "5.0.7", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^9.3" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.7" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T06:35:11+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "1.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-22T06:20:34+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", + "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "sebastian/object-reflector": "^2.0", + "sebastian/recursion-context": "^4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:12:34+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-10-26T13:14:26+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "4.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:07:39+00:00" + }, + { + "name": "sebastian/resource-operations", + "version": "3.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/resource-operations.git", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides a list of PHP built-in functions that operate on resources", + "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "support": { + "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-14T16:00:52+00:00" + }, + { + "name": "sebastian/type", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:13:03+00:00" + }, + { + "name": "sebastian/version", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c6c1022351a901512170118436c764e473f6de8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", + "reference": "c6c1022351a901512170118436c764e473f6de8c", + "shasum": "" + }, + "require": { + "php": ">=7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-09-28T06:39:44+00:00" + }, + { + "name": "spatie/array-to-xml", + "version": "3.3.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/array-to-xml.git", + "reference": "f56b220fe2db1ade4c88098d83413ebdfc3bf876" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/array-to-xml/zipball/f56b220fe2db1ade4c88098d83413ebdfc3bf876", + "reference": "f56b220fe2db1ade4c88098d83413ebdfc3bf876", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "php": "^8.0" + }, + "require-dev": { + "mockery/mockery": "^1.2", + "pestphp/pest": "^1.21", + "spatie/pest-plugin-snapshots": "^1.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Spatie\\ArrayToXml\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "https://freek.dev", + "role": "Developer" + } + ], + "description": "Convert an array to xml", + "homepage": "https://github.com/spatie/array-to-xml", + "keywords": [ + "array", + "convert", + "xml" + ], + "support": { + "source": "https://github.com/spatie/array-to-xml/tree/3.3.0" + }, + "funding": [ + { + "url": "https://spatie.be/open-source/support-us", + "type": "custom" + }, + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2024-05-01T10:20:27+00:00" + }, + { + "name": "spatie/backtrace", + "version": "1.6.1", + "source": { + "type": "git", + "url": "https://github.com/spatie/backtrace.git", + "reference": "8373b9d51638292e3bfd736a9c19a654111b4a23" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/backtrace/zipball/8373b9d51638292e3bfd736a9c19a654111b4a23", + "reference": "8373b9d51638292e3bfd736a9c19a654111b4a23", + "shasum": "" + }, + "require": { + "php": "^7.3|^8.0" + }, + "require-dev": { + "ext-json": "*", + "laravel/serializable-closure": "^1.3", + "phpunit/phpunit": "^9.3", + "spatie/phpunit-snapshot-assertions": "^4.2", + "symfony/var-dumper": "^5.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\Backtrace\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van de Herten", + "email": "freek@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + } + ], + "description": "A better backtrace", + "homepage": "https://github.com/spatie/backtrace", + "keywords": [ + "Backtrace", + "spatie" + ], + "support": { + "source": "https://github.com/spatie/backtrace/tree/1.6.1" + }, + "funding": [ + { + "url": "https://github.com/sponsors/spatie", + "type": "github" + }, + { + "url": "https://spatie.be/open-source/support-us", + "type": "other" + } + ], + "time": "2024-04-24T13:22:11+00:00" + }, + { + "name": "spatie/flare-client-php", + "version": "1.4.4", + "source": { + "type": "git", + "url": "https://github.com/spatie/flare-client-php.git", + "reference": "17082e780752d346c2db12ef5d6bee8e835e399c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/flare-client-php/zipball/17082e780752d346c2db12ef5d6bee8e835e399c", + "reference": "17082e780752d346c2db12ef5d6bee8e835e399c", + "shasum": "" + }, + "require": { + "illuminate/pipeline": "^8.0|^9.0|^10.0|^11.0", + "php": "^8.0", + "spatie/backtrace": "^1.5.2", + "symfony/http-foundation": "^5.2|^6.0|^7.0", + "symfony/mime": "^5.2|^6.0|^7.0", + "symfony/process": "^5.2|^6.0|^7.0", + "symfony/var-dumper": "^5.2|^6.0|^7.0" + }, + "require-dev": { + "dms/phpunit-arraysubset-asserts": "^0.5.0", + "pestphp/pest": "^1.20|^2.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.0", + "spatie/phpunit-snapshot-assertions": "^4.0|^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.3.x-dev" + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Spatie\\FlareClient\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Send PHP errors to Flare", + "homepage": "https://github.com/spatie/flare-client-php", + "keywords": [ + "exception", + "flare", + "reporting", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/flare-client-php/issues", + "source": "https://github.com/spatie/flare-client-php/tree/1.4.4" + }, + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2024-01-31T14:18:45+00:00" + }, + { + "name": "spatie/ignition", + "version": "1.14.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/ignition.git", + "reference": "80385994caed328f6f9c9952926932e65b9b774c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/ignition/zipball/80385994caed328f6f9c9952926932e65b9b774c", + "reference": "80385994caed328f6f9c9952926932e65b9b774c", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "php": "^8.0", + "spatie/backtrace": "^1.5.3", + "spatie/flare-client-php": "^1.4.0", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0" + }, + "require-dev": { + "illuminate/cache": "^9.52|^10.0|^11.0", + "mockery/mockery": "^1.4", + "pestphp/pest": "^1.20|^2.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.0", + "psr/simple-cache-implementation": "*", + "symfony/cache": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "vlucas/phpdotenv": "^5.5" + }, + "suggest": { + "openai-php/client": "Require get solutions from OpenAI", + "simple-cache-implementation": "To cache solutions from OpenAI" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.5.x-dev" + } + }, + "autoload": { + "psr-4": { + "Spatie\\Ignition\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Spatie", + "email": "info@spatie.be", + "role": "Developer" + } + ], + "description": "A beautiful error page for PHP applications.", + "homepage": "https://flareapp.io/ignition", + "keywords": [ + "error", + "flare", + "laravel", + "page" + ], + "support": { + "docs": "https://flareapp.io/docs/ignition-for-laravel/introduction", + "forum": "https://twitter.com/flareappio", + "issues": "https://github.com/spatie/ignition/issues", + "source": "https://github.com/spatie/ignition" + }, + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2024-04-26T08:45:51+00:00" + }, + { + "name": "spatie/laravel-ignition", + "version": "1.6.4", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-ignition.git", + "reference": "1a2b4bd3d48c72526c0ba417687e5c56b5cf49bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-ignition/zipball/1a2b4bd3d48c72526c0ba417687e5c56b5cf49bc", + "reference": "1a2b4bd3d48c72526c0ba417687e5c56b5cf49bc", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-mbstring": "*", + "illuminate/support": "^8.77|^9.27", + "monolog/monolog": "^2.3", + "php": "^8.0", + "spatie/flare-client-php": "^1.0.1", + "spatie/ignition": "^1.4.1", + "symfony/console": "^5.0|^6.0", + "symfony/var-dumper": "^5.0|^6.0" + }, + "require-dev": { + "filp/whoops": "^2.14", + "livewire/livewire": "^2.8|dev-develop", + "mockery/mockery": "^1.4", + "nunomaduro/larastan": "^1.0", + "orchestra/testbench": "^6.23|^7.0", + "pestphp/pest": "^1.20", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-phpunit": "^1.0", + "spatie/laravel-ray": "^1.27" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Spatie\\LaravelIgnition\\IgnitionServiceProvider" + ], + "aliases": { + "Flare": "Spatie\\LaravelIgnition\\Facades\\Flare" + } + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Spatie\\LaravelIgnition\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Spatie", + "email": "info@spatie.be", + "role": "Developer" + } + ], + "description": "A beautiful error page for Laravel applications.", + "homepage": "https://flareapp.io/ignition", + "keywords": [ + "error", + "flare", + "laravel", + "page" + ], + "support": { + "docs": "https://flareapp.io/docs/ignition-for-laravel/introduction", + "forum": "https://twitter.com/flareappio", + "issues": "https://github.com/spatie/laravel-ignition/issues", + "source": "https://github.com/spatie/laravel-ignition" + }, + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2023-01-03T19:28:04+00:00" + }, + { + "name": "spatie/laravel-ray", + "version": "1.36.1", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-ray.git", + "reference": "799eb881d5ede337f373b5fe9722c92b787890f4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-ray/zipball/799eb881d5ede337f373b5fe9722c92b787890f4", + "reference": "799eb881d5ede337f373b5fe9722c92b787890f4", + "shasum": "" + }, + "require": { + "ext-json": "*", + "illuminate/contracts": "^7.20|^8.19|^9.0|^10.0|^11.0", + "illuminate/database": "^7.20|^8.19|^9.0|^10.0|^11.0", + "illuminate/queue": "^7.20|^8.19|^9.0|^10.0|^11.0", + "illuminate/support": "^7.20|^8.19|^9.0|^10.0|^11.0", + "php": "^7.4|^8.0", + "rector/rector": "^0.19.2|^1.0", + "spatie/backtrace": "^1.0", + "spatie/ray": "^1.41.1", + "symfony/stopwatch": "4.2|^5.1|^6.0|^7.0", + "zbateson/mail-mime-parser": "^1.3.1|^2.0" + }, + "require-dev": { + "guzzlehttp/guzzle": "^7.3", + "laravel/framework": "^7.20|^8.19|^9.0|^10.0|^11.0", + "orchestra/testbench-core": "^5.0|^6.0|^7.0|^8.0|^9.0", + "pestphp/pest": "^1.22|^2.0", + "phpstan/phpstan": "^1.10.57", + "phpunit/phpunit": "^9.3|^10.1", + "spatie/pest-plugin-snapshots": "^1.1|^2.0", + "symfony/var-dumper": "^4.2|^5.1|^6.0|^7.0.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + }, + "laravel": { + "providers": [ + "Spatie\\LaravelRay\\RayServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Spatie\\LaravelRay\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + } + ], + "description": "Easily debug Laravel apps", + "homepage": "https://github.com/spatie/laravel-ray", + "keywords": [ + "laravel-ray", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/laravel-ray/issues", + "source": "https://github.com/spatie/laravel-ray/tree/1.36.1" + }, + "funding": [ + { + "url": "https://github.com/sponsors/spatie", + "type": "github" + }, + { + "url": "https://spatie.be/open-source/support-us", + "type": "other" + } + ], + "time": "2024-04-12T12:15:59+00:00" + }, + { + "name": "spatie/ray", + "version": "1.41.2", + "source": { + "type": "git", + "url": "https://github.com/spatie/ray.git", + "reference": "c44f8cfbf82c69909b505de61d8d3f2d324e93fc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/ray/zipball/c44f8cfbf82c69909b505de61d8d3f2d324e93fc", + "reference": "c44f8cfbf82c69909b505de61d8d3f2d324e93fc", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "php": "^7.3|^8.0", + "ramsey/uuid": "^3.0|^4.1", + "spatie/backtrace": "^1.1", + "spatie/macroable": "^1.0|^2.0", + "symfony/stopwatch": "^4.0|^5.1|^6.0|^7.0", + "symfony/var-dumper": "^4.2|^5.1|^6.0|^7.0.3" + }, + "require-dev": { + "illuminate/support": "6.x|^8.18|^9.0", + "nesbot/carbon": "^2.63", + "pestphp/pest": "^1.22", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.5", + "rector/rector": "^0.19.2", + "spatie/phpunit-snapshot-assertions": "^4.2", + "spatie/test-time": "^1.2" + }, + "bin": [ + "bin/remove-ray.sh" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Spatie\\Ray\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + } + ], + "description": "Debug with Ray to fix problems faster", + "homepage": "https://github.com/spatie/ray", + "keywords": [ + "ray", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/ray/issues", + "source": "https://github.com/spatie/ray/tree/1.41.2" + }, + "funding": [ + { + "url": "https://github.com/sponsors/spatie", + "type": "github" + }, + { + "url": "https://spatie.be/open-source/support-us", + "type": "other" + } + ], + "time": "2024-04-24T14:21:46+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v7.0.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "cc168be6fbdcdf3401f50ae863ee3818ed4338f5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/cc168be6fbdcdf3401f50ae863ee3818ed4338f5", + "reference": "cc168be6fbdcdf3401f50ae863ee3818ed4338f5", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8", + "symfony/process": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v7.0.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-04-18T09:29:19+00:00" + }, + { + "name": "symfony/polyfill-iconv", + "version": "v1.29.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-iconv.git", + "reference": "cd4226d140ecd3d0f13d32ed0a4a095ffe871d2f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-iconv/zipball/cd4226d140ecd3d0f13d32ed0a4a095ffe871d2f", + "reference": "cd4226d140ecd3d0f13d32ed0a4a095ffe871d2f", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-iconv": "*" + }, + "suggest": { + "ext-iconv": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Iconv\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Iconv extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "iconv", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-iconv/tree/v1.29.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-01-29T20:11:03+00:00" + }, + { + "name": "symfony/stopwatch", + "version": "v7.0.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/stopwatch.git", + "reference": "41a7a24aa1dc82adf46a06bc292d1923acfe6b84" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/41a7a24aa1dc82adf46a06bc292d1923acfe6b84", + "reference": "41a7a24aa1dc82adf46a06bc292d1923acfe6b84", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/service-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Stopwatch\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a way to profile code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/stopwatch/tree/v7.0.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-04-18T09:29:19+00:00" + }, + { + "name": "symfony/yaml", + "version": "v6.4.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "53e8b1ef30a65f78eac60fddc5ee7ebbbdb1dee0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/53e8b1ef30a65f78eac60fddc5ee7ebbbdb1dee0", + "reference": "53e8b1ef30a65f78eac60fddc5ee7ebbbdb1dee0", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/console": "<5.4" + }, + "require-dev": { + "symfony/console": "^5.4|^6.0|^7.0" + }, + "bin": [ + "Resources/bin/yaml-lint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Loads and dumps YAML files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v6.4.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-04-28T10:28:08+00:00" + }, + { + "name": "thecodingmachine/phpstan-safe-rule", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/thecodingmachine/phpstan-safe-rule.git", + "reference": "8a7b88e0d54f209a488095085f183e9174c40e1e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thecodingmachine/phpstan-safe-rule/zipball/8a7b88e0d54f209a488095085f183e9174c40e1e", + "reference": "8a7b88e0d54f209a488095085f183e9174c40e1e", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "phpstan/phpstan": "^1.0", + "thecodingmachine/safe": "^1.0 || ^2.0" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^7.5.2 || ^8.0", + "squizlabs/php_codesniffer": "^3.4" + }, + "type": "phpstan-extension", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + }, + "phpstan": { + "includes": [ + "phpstan-safe-rule.neon" + ] + } + }, + "autoload": { + "psr-4": { + "TheCodingMachine\\Safe\\PHPStan\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "David Négrier", + "email": "d.negrier@thecodingmachine.com" + } + ], + "description": "A PHPStan rule to detect safety issues. Must be used in conjunction with thecodingmachine/safe", + "support": { + "issues": "https://github.com/thecodingmachine/phpstan-safe-rule/issues", + "source": "https://github.com/thecodingmachine/phpstan-safe-rule/tree/v1.2.0" + }, + "time": "2022-01-17T10:12:29+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.2.3", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.3" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:36:25+00:00" + }, + { + "name": "vimeo/psalm", + "version": "5.24.0", + "source": { + "type": "git", + "url": "https://github.com/vimeo/psalm.git", + "reference": "462c80e31c34e58cc4f750c656be3927e80e550e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vimeo/psalm/zipball/462c80e31c34e58cc4f750c656be3927e80e550e", + "reference": "462c80e31c34e58cc4f750c656be3927e80e550e", + "shasum": "" + }, + "require": { + "amphp/amp": "^2.4.2", + "amphp/byte-stream": "^1.5", + "composer-runtime-api": "^2", + "composer/semver": "^1.4 || ^2.0 || ^3.0", + "composer/xdebug-handler": "^2.0 || ^3.0", + "dnoegel/php-xdg-base-dir": "^0.1.1", + "ext-ctype": "*", + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-simplexml": "*", + "ext-tokenizer": "*", + "felixfbecker/advanced-json-rpc": "^3.1", + "felixfbecker/language-server-protocol": "^1.5.2", + "fidry/cpu-core-counter": "^0.4.1 || ^0.5.1 || ^1.0.0", + "netresearch/jsonmapper": "^1.0 || ^2.0 || ^3.0 || ^4.0", + "nikic/php-parser": "^4.16", + "php": "^7.4 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0", + "sebastian/diff": "^4.0 || ^5.0 || ^6.0", + "spatie/array-to-xml": "^2.17.0 || ^3.0", + "symfony/console": "^4.1.6 || ^5.0 || ^6.0 || ^7.0", + "symfony/filesystem": "^5.4 || ^6.0 || ^7.0" + }, + "conflict": { + "nikic/php-parser": "4.17.0" + }, + "provide": { + "psalm/psalm": "self.version" + }, + "require-dev": { + "amphp/phpunit-util": "^2.0", + "bamarni/composer-bin-plugin": "^1.4", + "brianium/paratest": "^6.9", + "ext-curl": "*", + "mockery/mockery": "^1.5", + "nunomaduro/mock-final-classes": "^1.1", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/phpdoc-parser": "^1.6", + "phpunit/phpunit": "^9.6", + "psalm/plugin-mockery": "^1.1", + "psalm/plugin-phpunit": "^0.18", + "slevomat/coding-standard": "^8.4", + "squizlabs/php_codesniffer": "^3.6", + "symfony/process": "^4.4 || ^5.0 || ^6.0 || ^7.0" + }, + "suggest": { + "ext-curl": "In order to send data to shepherd", + "ext-igbinary": "^2.0.5 is required, used to serialize caching data" + }, + "bin": [ + "psalm", + "psalm-language-server", + "psalm-plugin", + "psalm-refactor", + "psalter" + ], + "type": "project", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev", + "dev-4.x": "4.x-dev", + "dev-3.x": "3.x-dev", + "dev-2.x": "2.x-dev", + "dev-1.x": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psalm\\": "src/Psalm/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Matthew Brown" + } + ], + "description": "A static analysis tool for finding errors in PHP applications", + "keywords": [ + "code", + "inspection", + "php", + "static analysis" + ], + "support": { + "docs": "https://psalm.dev/docs", + "issues": "https://github.com/vimeo/psalm/issues", + "source": "https://github.com/vimeo/psalm" + }, + "time": "2024-05-01T19:32:08+00:00" + }, + { + "name": "zbateson/mail-mime-parser", + "version": "2.4.1", + "source": { + "type": "git", + "url": "https://github.com/zbateson/mail-mime-parser.git", + "reference": "ff49e02f6489b38f7cc3d1bd3971adc0f872569c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zbateson/mail-mime-parser/zipball/ff49e02f6489b38f7cc3d1bd3971adc0f872569c", + "reference": "ff49e02f6489b38f7cc3d1bd3971adc0f872569c", + "shasum": "" + }, + "require": { + "guzzlehttp/psr7": "^1.7.0|^2.0", + "php": ">=7.1", + "pimple/pimple": "^3.0", + "zbateson/mb-wrapper": "^1.0.1", + "zbateson/stream-decorators": "^1.0.6" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "*", + "mikey179/vfsstream": "^1.6.0", + "phpstan/phpstan": "*", + "phpunit/phpunit": "<10" + }, + "suggest": { + "ext-iconv": "For best support/performance", + "ext-mbstring": "For best support/performance" + }, + "type": "library", + "autoload": { + "psr-4": { + "ZBateson\\MailMimeParser\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Zaahid Bateson" + }, + { + "name": "Contributors", + "homepage": "https://github.com/zbateson/mail-mime-parser/graphs/contributors" + } + ], + "description": "MIME email message parser", + "homepage": "https://mail-mime-parser.org", + "keywords": [ + "MimeMailParser", + "email", + "mail", + "mailparse", + "mime", + "mimeparse", + "parser", + "php-imap" + ], + "support": { + "docs": "https://mail-mime-parser.org/#usage-guide", + "issues": "https://github.com/zbateson/mail-mime-parser/issues", + "source": "https://github.com/zbateson/mail-mime-parser" + }, + "funding": [ + { + "url": "https://github.com/zbateson", + "type": "github" + } + ], + "time": "2024-04-28T00:58:54+00:00" + }, + { + "name": "zbateson/mb-wrapper", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/zbateson/mb-wrapper.git", + "reference": "09a8b77eb94af3823a9a6623dcc94f8d988da67f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zbateson/mb-wrapper/zipball/09a8b77eb94af3823a9a6623dcc94f8d988da67f", + "reference": "09a8b77eb94af3823a9a6623dcc94f8d988da67f", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "symfony/polyfill-iconv": "^1.9", + "symfony/polyfill-mbstring": "^1.9" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "*", + "phpstan/phpstan": "*", + "phpunit/phpunit": "<10.0" + }, + "suggest": { + "ext-iconv": "For best support/performance", + "ext-mbstring": "For best support/performance" + }, + "type": "library", + "autoload": { + "psr-4": { + "ZBateson\\MbWrapper\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Zaahid Bateson" + } + ], + "description": "Wrapper for mbstring with fallback to iconv for encoding conversion and string manipulation", + "keywords": [ + "charset", + "encoding", + "http", + "iconv", + "mail", + "mb", + "mb_convert_encoding", + "mbstring", + "mime", + "multibyte", + "string" + ], + "support": { + "issues": "https://github.com/zbateson/mb-wrapper/issues", + "source": "https://github.com/zbateson/mb-wrapper/tree/1.2.1" + }, + "funding": [ + { + "url": "https://github.com/zbateson", + "type": "github" + } + ], + "time": "2024-03-18T04:31:04+00:00" + }, + { + "name": "zbateson/stream-decorators", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/zbateson/stream-decorators.git", + "reference": "783b034024fda8eafa19675fb2552f8654d3a3e9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/zbateson/stream-decorators/zipball/783b034024fda8eafa19675fb2552f8654d3a3e9", + "reference": "783b034024fda8eafa19675fb2552f8654d3a3e9", + "shasum": "" + }, + "require": { + "guzzlehttp/psr7": "^1.9 | ^2.0", + "php": ">=7.2", + "zbateson/mb-wrapper": "^1.0.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "*", + "phpstan/phpstan": "*", + "phpunit/phpunit": "<10.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "ZBateson\\StreamDecorators\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Zaahid Bateson" + } + ], + "description": "PHP psr7 stream decorators for mime message part streams", + "keywords": [ + "base64", + "charset", + "decorators", + "mail", + "mime", + "psr7", + "quoted-printable", + "stream", + "uuencode" + ], + "support": { + "issues": "https://github.com/zbateson/stream-decorators/issues", + "source": "https://github.com/zbateson/stream-decorators/tree/1.2.1" + }, + "funding": [ + { + "url": "https://github.com/zbateson", + "type": "github" + } + ], + "time": "2023-05-30T22:51:52+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": { + "werk365/etagconditionals": 20, + "roave/security-advisories": 20 + }, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": "^8.1", + "ext-bcmath": "*", + "ext-gd": "*", + "ext-gmp": "*", + "ext-intl": "*", + "ext-redis": "*" + }, + "platform-dev": [], + "plugin-api-version": "2.2.0" +} diff --git a/config/api.php b/config/api.php new file mode 100644 index 0000000..85a824f --- /dev/null +++ b/config/api.php @@ -0,0 +1,65 @@ + env('MAX_API_LIMIT_PER_PAGE', 100), + + /* + |-------------------------------------------------------------------------- + | Format of the timestamp + |-------------------------------------------------------------------------- + | + | This defines the format of the timestamp that is returned on most API + | calls. + | + */ + 'timestamp_format' => env('API_TIMESTAMP_FORMAT', 'Y-m-d\TH:i:s\Z'), + + /* + |-------------------------------------------------------------------------- + | Format of the date timestamp + |-------------------------------------------------------------------------- + | + | This defines the format of the date that is returned on some API calls + | when it requires a date only. + | + */ + 'date_timestamp_format' => env('API_DATE_TIMESTAMP_FORMAT', 'Y-m-d'), + + /* + |-------------------------------------------------------------------------- + | Error codes for the API + |-------------------------------------------------------------------------- + */ + 'error_codes' => [ + '30' => 'The limit parameter is too big', + '31' => 'The resource has not been found', + '32' => 'Error while trying to save the data', + '33' => 'Too many parameters', + '34' => 'Too many attempts, please slow down the request', + '35' => 'This email address is already taken', + '36' => 'You can\'t set a partner or a child to a partial contact', + '37' => 'Problems parsing JSON', + '38' => 'Date should be in the future', + '39' => 'The sorting criteria is invalid', + '40' => 'Invalid query', + '41' => 'Invalid parameters.', + '42' => 'Not authorized', + ], + + /* + |-------------------------------------------------------------------------- + | The API documentation link + |-------------------------------------------------------------------------- + */ + 'help' => 'https://www.monicahq.com/api', + +]; diff --git a/config/app.php b/config/app.php new file mode 100644 index 0000000..523c28e --- /dev/null +++ b/config/app.php @@ -0,0 +1,265 @@ + env('APP_NAME', 'Monica'), + + /* + |-------------------------------------------------------------------------- + | Application Display Name + |-------------------------------------------------------------------------- + | + | This is the name of the application that will be displayed in the notification emails. + | + */ + + 'display_name' => env('APP_DISPLAY_NAME', env('APP_NAME', 'Monica')), + + /* + |-------------------------------------------------------------------------- + | Application Environment + |-------------------------------------------------------------------------- + | + | This value determines the "environment" your application is currently + | running in. This may determine how you prefer to configure various + | services your application utilizes. Set this in your ".env" file. + | + */ + + 'env' => env('APP_ENV', 'production'), + + /* + |-------------------------------------------------------------------------- + | Application Debug Mode + |-------------------------------------------------------------------------- + | + | When your application is in debug mode, detailed error messages with + | stack traces will be shown on every error that occurs within your + | application. If disabled, a simple generic error page is shown. + | + */ + + 'debug' => (bool) env('APP_DEBUG', false), + + /* + |-------------------------------------------------------------------------- + | Application URL + |-------------------------------------------------------------------------- + | + | This URL is used by the console to properly generate URLs when using + | the Artisan command line tool. You should set this to the root of + | your application so that it is used when running Artisan tasks. + | + */ + + 'url' => env('APP_URL', 'http://localhost'), + + 'asset_url' => env('ASSET_URL', null), + + 'force_url' => (bool) env('APP_FORCE_URL', false), + + /* + |-------------------------------------------------------------------------- + | TIMEZONE + |-------------------------------------------------------------------------- + | + | Timezone is not configurable in the .env file as everything is stored in + | UTC. + | + */ + + 'timezone' => 'UTC', + + /* + |-------------------------------------------------------------------------- + | Application Locale Configuration + |-------------------------------------------------------------------------- + | + | The application locale determines the default locale that will be used + | by the translation service provider. You are free to set this value + | to any of the locales which will be supported by the application. + | + */ + + 'locale' => env('APP_DEFAULT_LOCALE', 'en'), + + /* + |-------------------------------------------------------------------------- + | Application Fallback Locale + |-------------------------------------------------------------------------- + | + | The fallback locale determines the locale to use when the current one + | is not available. You may change the value to correspond to any of + | the language folders that are provided through your application. + | + */ + + 'fallback_locale' => 'en', + + /* + |-------------------------------------------------------------------------- + | Encryption Key + |-------------------------------------------------------------------------- + | + | This key is used by the Illuminate encrypter service and should be set + | to a random, 32 character string, otherwise these encrypted strings + | will not be safe. Please do this before deploying an application! + | + */ + + 'key' => env('APP_KEY'), + + 'cipher' => 'AES-256-CBC', + + /* + |-------------------------------------------------------------------------- + | Password strength + |-------------------------------------------------------------------------- + | + | You can configure password strength requirements here. + | - password_min is the minimum length of the password. + | - password_rules are requirements that can be added on the password: + | mixedCase, letters, numbers, symbols, uncompromised. + | See https://laravel.com/docs/8.x/validation#validating-passwords + | + */ + + 'password_min' => (int) env('APP_PASSWORD_MIN', 8), + + 'password_rules' => env('APP_PASSWORD_RULES', 'mixedCase,letters,numbers,symbols,uncompromised'), + + /* + |-------------------------------------------------------------------------- + | Trust proxies + |-------------------------------------------------------------------------- + | + | List of trusted proxies. + | Example: set it to '*' to allow any proxy. + | + */ + + 'trust_proxies' => env('APP_TRUSTED_PROXIES', env('APP_TRUST_PROXIES')), + + /* + |-------------------------------------------------------------------------- + | Autoloaded Service Providers + |-------------------------------------------------------------------------- + | + | The service providers listed here will be automatically loaded on the + | request to your application. Feel free to add your own services to + | this array to grant expanded functionality to your applications. + | + */ + 'providers' => [ + + /* + * Laravel Framework Service Providers... + */ + Illuminate\Auth\AuthServiceProvider::class, + Illuminate\Broadcasting\BroadcastServiceProvider::class, + Illuminate\Bus\BusServiceProvider::class, + Illuminate\Cache\CacheServiceProvider::class, + Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class, + Illuminate\Cookie\CookieServiceProvider::class, + Illuminate\Database\DatabaseServiceProvider::class, + Illuminate\Encryption\EncryptionServiceProvider::class, + Illuminate\Filesystem\FilesystemServiceProvider::class, + Illuminate\Foundation\Providers\FoundationServiceProvider::class, + Illuminate\Hashing\HashServiceProvider::class, + Illuminate\Mail\MailServiceProvider::class, + Illuminate\Pagination\PaginationServiceProvider::class, + Illuminate\Pipeline\PipelineServiceProvider::class, + Illuminate\Queue\QueueServiceProvider::class, + Illuminate\Redis\RedisServiceProvider::class, + Illuminate\Auth\Passwords\PasswordResetServiceProvider::class, + Illuminate\Session\SessionServiceProvider::class, + Illuminate\Translation\TranslationServiceProvider::class, + Illuminate\Validation\ValidationServiceProvider::class, + Illuminate\View\ViewServiceProvider::class, + Illuminate\Notifications\NotificationServiceProvider::class, + + /* + * Application Service Providers... + */ + App\Providers\AppServiceProvider::class, + App\Providers\AuthServiceProvider::class, + App\Providers\EventServiceProvider::class, + App\Providers\MacroServiceProvider::class, + Vluzrmos\LanguageDetector\Providers\LanguageDetectorServiceProvider::class, + App\Providers\RouteServiceProvider::class, + Laravel\Socialite\SocialiteServiceProvider::class, + Intervention\Image\ImageServiceProvider::class, + Laravel\Cashier\CashierServiceProvider::class, + Laravel\Passport\PassportServiceProvider::class, + Creativeorange\Gravatar\GravatarServiceProvider::class, + App\Providers\DAVServiceProvider::class, + ], + + /* + |-------------------------------------------------------------------------- + | Class Aliases + |-------------------------------------------------------------------------- + | + | This array of class aliases will be registered when this application + | is started. However, feel free to register as many as you wish as + | the aliases are "lazy" loaded so they don't hinder performance. + | + */ + + 'aliases' => [ + + 'App' => Illuminate\Support\Facades\App::class, + 'Arr' => Illuminate\Support\Arr::class, + 'Artisan' => Illuminate\Support\Facades\Artisan::class, + 'Auth' => Illuminate\Support\Facades\Auth::class, + 'Blade' => Illuminate\Support\Facades\Blade::class, + 'Broadcast' => Illuminate\Support\Facades\Broadcast::class, + 'Bus' => Illuminate\Support\Facades\Bus::class, + 'Cache' => Illuminate\Support\Facades\Cache::class, + 'Config' => Illuminate\Support\Facades\Config::class, + 'Cookie' => Illuminate\Support\Facades\Cookie::class, + 'Crypt' => Illuminate\Support\Facades\Crypt::class, + 'DateHelper' => App\Helpers\DateHelper::class, + 'DB' => Illuminate\Support\Facades\DB::class, + 'Eloquent' => Illuminate\Database\Eloquent\Model::class, + 'Event' => Illuminate\Support\Facades\Event::class, + 'File' => Illuminate\Support\Facades\File::class, + 'Gate' => Illuminate\Support\Facades\Gate::class, + 'Hash' => Illuminate\Support\Facades\Hash::class, + 'Lang' => Illuminate\Support\Facades\Lang::class, + 'Log' => Illuminate\Support\Facades\Log::class, + 'Mail' => Illuminate\Support\Facades\Mail::class, + 'Notification' => Illuminate\Support\Facades\Notification::class, + 'Password' => Illuminate\Support\Facades\Password::class, + 'Queue' => Illuminate\Support\Facades\Queue::class, + 'Redirect' => Illuminate\Support\Facades\Redirect::class, + 'Redis' => Illuminate\Support\Facades\Redis::class, + 'Request' => Illuminate\Support\Facades\Request::class, + 'Response' => Illuminate\Support\Facades\Response::class, + 'Route' => Illuminate\Support\Facades\Route::class, + 'Schema' => Illuminate\Support\Facades\Schema::class, + 'Session' => Illuminate\Support\Facades\Session::class, + 'Storage' => Illuminate\Support\Facades\Storage::class, + 'Str' => Illuminate\Support\Str::class, + 'URL' => Illuminate\Support\Facades\URL::class, + 'Validator' => Illuminate\Support\Facades\Validator::class, + 'View' => Illuminate\Support\Facades\View::class, + 'Debugbar' => Barryvdh\Debugbar\Facade::class, + 'Notification' => Illuminate\Support\Facades\Notification::class, + 'Socialite' => Laravel\Socialite\Facades\Socialite::class, + 'Image' => Intervention\Image\Facades\Image::class, + 'Gravatar' => Creativeorange\Gravatar\Facades\Gravatar::class, + ], + +]; diff --git a/config/auth.php b/config/auth.php new file mode 100644 index 0000000..211cb54 --- /dev/null +++ b/config/auth.php @@ -0,0 +1,154 @@ + [ + 'guard' => 'web', + 'passwords' => 'users', + ], + + /* + |-------------------------------------------------------------------------- + | Authentication Guards + |-------------------------------------------------------------------------- + | + | Next, you may define every authentication guard for your application. + | Of course, a great default configuration has been defined for you + | here which uses session storage and the Eloquent user provider. + | + | All authentication drivers have a user provider. This defines how the + | users are actually retrieved out of your database or other storage + | mechanisms used by this application to persist your user's data. + | + | Supported: "session", "token" + | + */ + + 'guards' => [ + 'web' => [ + 'driver' => 'session', + 'provider' => 'users', + ], + + 'api' => [ + 'driver' => 'passport', + 'provider' => 'users', + ], + ], + + /* + |-------------------------------------------------------------------------- + | User Providers + |-------------------------------------------------------------------------- + | + | All authentication drivers have a user provider. This defines how the + | users are actually retrieved out of your database or other storage + | mechanisms used by this application to persist your user's data. + | + | If you have multiple user tables or models you may configure multiple + | sources which represent each model / table. These sources may then + | be assigned to any extra authentication guards you have defined. + | + | Supported: "database", "eloquent" + | + */ + + 'providers' => [ + 'users' => [ + 'driver' => 'eloquent', + 'model' => App\Models\User\User::class, + ], + + // 'users' => [ + // 'driver' => 'database', + // 'table' => 'users', + // ], + ], + + /* + |-------------------------------------------------------------------------- + | Resetting Passwords + |-------------------------------------------------------------------------- + | + | Here you may set the options for resetting passwords including the view + | that is your password reset e-mail. You may also set the name of the + | table that maintains all of the reset tokens for your application. + | + | You may specify multiple password reset configurations if you have more + | than one user table or model in the application and you want to have + | separate password reset settings based on the specific user types. + | + | The expire time is the number of minutes that the reset token should be + | considered valid. This security feature keeps tokens short-lived so + | they have less time to be guessed. You may change this as needed. + | + */ + + 'passwords' => [ + 'users' => [ + 'provider' => 'users', + 'email' => 'auth.emails.password', + 'table' => 'password_resets', + 'expire' => 60, + 'throttle' => 60, + ], + ], + + /* + |-------------------------------------------------------------------------- + | Password Confirmation Timeout + |-------------------------------------------------------------------------- + | + | Here you may define the amount of seconds before a password confirmation + | times out and the user is prompted to re-enter their password via the + | confirmation screen. By default, the timeout lasts for three hours. + | + */ + + 'password_timeout' => 10800, + + /* + | Invitation Email + |-------------------------------------------------------------------------- + | + | Parameters for the invitation email send, requested by a user. + | Expire is in days. + | + */ + + 'invitation' => [ + 'expire' => 2, + ], + + /* + |-------------------------------------------------------------------------- + | Recovery codes generation + |-------------------------------------------------------------------------- + | + | Recovery codes can be used to access your account in the event you lost + | access to the device used to generate or receive two-factor + | authentication codes. + | + | * count: number of recovery codes to generate + | * blocks: number of blocks in one code + | * chars: number of characters per block + | + */ + + 'recovery' => [ + 'count' => 8, + 'blocks' => 2, + 'chars' => 4, + ], +]; diff --git a/config/broadcasting.php b/config/broadcasting.php new file mode 100644 index 0000000..abaaac3 --- /dev/null +++ b/config/broadcasting.php @@ -0,0 +1,52 @@ + env('BROADCAST_DRIVER', 'pusher'), + + /* + |-------------------------------------------------------------------------- + | Broadcast Connections + |-------------------------------------------------------------------------- + | + | Here you may define all of the broadcast connections that will be used + | to broadcast events to other systems or over websockets. Samples of + | each available type of connection are provided inside this array. + | + */ + + 'connections' => [ + + 'pusher' => [ + 'driver' => 'pusher', + 'key' => env('PUSHER_KEY'), + 'secret' => env('PUSHER_SECRET'), + 'app_id' => env('PUSHER_APP_ID'), + 'options' => [ + // + ], + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => 'default', + ], + + 'log' => [ + 'driver' => 'log', + ], + + ], + +]; diff --git a/config/cache.php b/config/cache.php new file mode 100644 index 0000000..640144c --- /dev/null +++ b/config/cache.php @@ -0,0 +1,158 @@ + env('CACHE_DRIVER', 'file'), + + /* + |-------------------------------------------------------------------------- + | Cache Stores + |-------------------------------------------------------------------------- + | + | Here you may define all of the cache "stores" for your application as + | well as their drivers. You may even define multiple stores for the + | same cache driver to group types of items stored in your caches. + | + */ + + 'stores' => [ + + 'apc' => [ + 'driver' => 'apc', + ], + + 'array' => [ + 'driver' => 'array', + ], + + 'database' => [ + 'driver' => 'database', + 'table' => 'cache', + 'connection' => null, + ], + + 'file' => [ + 'driver' => 'file', + 'path' => storage_path('framework/cache/data'), + ], + + 'memcached' => [ + 'driver' => 'memcached', + 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'), + 'sasl' => [ + env('MEMCACHED_USERNAME'), + env('MEMCACHED_PASSWORD'), + ], + 'options' => [ + // Memcached::OPT_CONNECT_TIMEOUT => 2000, + ], + 'servers' => [ + [ + 'host' => env('MEMCACHED_HOST', '127.0.0.1'), + 'port' => env('MEMCACHED_PORT', 11211), + 'weight' => 100, + ], + ], + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => 'cache', + ], + + 'dynamodb' => [ + 'driver' => 'dynamodb', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + 'table' => env('DYNAMODB_CACHE_TABLE', 'cache'), + 'endpoint' => env('DYNAMODB_ENDPOINT'), + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Cache Key Prefix + |-------------------------------------------------------------------------- + | + | When utilizing a RAM based store such as APC or Memcached, there might + | be other applications utilizing the same cache. So, we'll specify a + | value to get prefixed to all our keys so we can avoid collisions. + | + */ + + 'prefix' => env( + 'CACHE_PREFIX', + Str::slug(env('APP_NAME', 'laravel'), '_').'_cache' + ), + +]; + +// on fortrabbit: construct credentials from App secrets +if (env('APP_SECRETS')) { + $secrets = json_decode(file_get_contents(env('APP_SECRETS')), true); + if (array_key_exists('MEMCACHE', $secrets)) { + $servers = [[ + 'host' => $secrets['MEMCACHE']['HOST1'], + 'port' => $secrets['MEMCACHE']['PORT1'], + 'weight' => 100, + ]]; + if ($secrets['MEMCACHE']['COUNT'] > 1) { + $servers [] = [ + 'host' => $secrets['MEMCACHE']['HOST2'], + 'port' => $secrets['MEMCACHE']['PORT2'], + 'weight' => 100, + ]; + } + Arr::set($config, 'stores.memcached.servers', $servers); + } +} + +if (extension_loaded('memcached')) { + $timeout_ms = 50; + $options = [ + // Assure that dead servers are properly removed and ... + \Memcached::OPT_REMOVE_FAILED_SERVERS => true, + + // ... retried after a short while (here: 2 seconds) + \Memcached::OPT_RETRY_TIMEOUT => 2, + + // KETAMA must be enabled so that replication can be used + \Memcached::OPT_LIBKETAMA_COMPATIBLE => true, + + // Replicate the data, write it to both memcached servers + \Memcached::OPT_NUMBER_OF_REPLICAS => 1, + + // Those values assure that a dead (due to increased latency or + // really unresponsive) memcached server is dropped fast + \Memcached::OPT_POLL_TIMEOUT => $timeout_ms, // milliseconds + \Memcached::OPT_SEND_TIMEOUT => $timeout_ms * 1000, // microseconds + \Memcached::OPT_RECV_TIMEOUT => $timeout_ms * 1000, // microseconds + \Memcached::OPT_CONNECT_TIMEOUT => $timeout_ms, // milliseconds + + // Further performance tuning + \Memcached::OPT_NO_BLOCK => true, + ]; + Arr::set($config, 'stores.memcached.options', $options); +} + +return $config; diff --git a/config/cashier.php b/config/cashier.php new file mode 100644 index 0000000..2825285 --- /dev/null +++ b/config/cashier.php @@ -0,0 +1,88 @@ + env('STRIPE_KEY'), + + 'secret' => env('STRIPE_SECRET'), + + /* + |-------------------------------------------------------------------------- + | Cashier Path + |-------------------------------------------------------------------------- + | + | This is the base URI path where Cashier's views, such as the payment + | verification screen, will be available from. You're free to tweak + | this path according to your preferences and application design. + | + */ + + 'path' => env('CASHIER_PATH', 'stripe'), + + /* + |-------------------------------------------------------------------------- + | Stripe Webhooks + |-------------------------------------------------------------------------- + | + | Your Stripe webhook secret is used to prevent unauthorized requests to + | your Stripe webhook handling controllers. The tolerance setting will + | check the drift between the current time and the signed request's. + | + */ + + 'webhook' => [ + 'secret' => env('STRIPE_WEBHOOK_SECRET'), + 'tolerance' => env('STRIPE_WEBHOOK_TOLERANCE', 300), + ], + + /* + |-------------------------------------------------------------------------- + | Currency + |-------------------------------------------------------------------------- + | + | This is the default currency that will be used when generating charges + | from your application. Of course, you are welcome to use any of the + | various world currencies that are currently supported via Stripe. + | + */ + + 'currency' => env('CASHIER_CURRENCY', 'usd'), + + /* + |-------------------------------------------------------------------------- + | Currency Locale + |-------------------------------------------------------------------------- + | + | This is the default locale in which your money values are formatted in + | for display. To utilize other locales besides the default en locale + | verify you have the "intl" PHP extension installed on the system. + | + */ + + 'currency_locale' => env('CASHIER_CURRENCY_LOCALE', 'en'), + + /* + |-------------------------------------------------------------------------- + | Payment Confirmation Notification + |-------------------------------------------------------------------------- + | + | If this setting is enabled, Cashier will automatically notify customers + | whose payments require additional verification. You should listen to + | Stripe's webhooks in order for this feature to function correctly. + | + */ + + 'payment_notification' => env('CASHIER_PAYMENT_NOTIFICATION'), + +]; diff --git a/config/compile.php b/config/compile.php new file mode 100644 index 0000000..04807ea --- /dev/null +++ b/config/compile.php @@ -0,0 +1,35 @@ + [ + // + ], + + /* + |-------------------------------------------------------------------------- + | Compiled File Providers + |-------------------------------------------------------------------------- + | + | Here you may list service providers which define a "compiles" function + | that returns additional files that should be compiled, providing an + | easy way to get common files from any packages you are utilizing. + | + */ + + 'providers' => [ + // + ], + +]; diff --git a/config/cors.php b/config/cors.php new file mode 100644 index 0000000..5c9de89 --- /dev/null +++ b/config/cors.php @@ -0,0 +1,34 @@ + ['api/*'], + + 'allowed_methods' => ['*'], + + 'allowed_origins' => ['*'], + + 'allowed_origins_patterns' => [], + + 'allowed_headers' => ['*'], + + 'exposed_headers' => false, + + 'max_age' => false, + + 'supports_credentials' => false, + +]; diff --git a/config/database.php b/config/database.php new file mode 100644 index 0000000..199fa14 --- /dev/null +++ b/config/database.php @@ -0,0 +1,231 @@ + PDO::FETCH_CLASS, + + /* + |-------------------------------------------------------------------------- + | Default Database Connection Name + |-------------------------------------------------------------------------- + | + | Here you may specify which of the database connections below you wish + | to use as your default connection for all database work. Of course + | you may use many connections at once using the Database library. + | + | PostgreSQL users: insert 'pgsql' and edit the 'pgsql' section below. + | + */ + + 'default' => env('DB_CONNECTION', 'mysql'), + + /* + |-------------------------------------------------------------------------- + | Use utf8mb4 charset format + |-------------------------------------------------------------------------- + | + | Use the new utf8mb4 charset format + | ⚠ be sure your DBMS supports utf8mb4 format + | See https://dev.mysql.com/doc/refman/5.5/en/charset-unicode-utf8mb4.html + | MySQL > 5.7.7 fully support it. + | + */ + + 'use_utf8mb4' => env('DB_USE_UTF8MB4', true), + + /* + |-------------------------------------------------------------------------- + | Database Connections + |-------------------------------------------------------------------------- + | + | Here are each of the database connections setup for your application. + | Of course, examples of configuring each database platform that is + | supported by Laravel is shown below to make development simple. + | + | + | All database work in Laravel is done through the PHP PDO facilities + | so make sure you have the driver for your particular database of + | choice installed on your machine before you begin development. + | + | PostgreSQL users: comment out host and port for UNIX domain socket + | connections (local file-based connection without the need to edit the + | firewall settings). + | + */ + + 'connections' => [ + + 'sqlite' => [ + 'driver' => 'sqlite', + 'url' => env('DATABASE_URL'), + 'database' => env('DB_DATABASE', database_path('database.sqlite')), + 'prefix' => env('DB_PREFIX', ''), + 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), + ], + + 'mysql' => [ + 'driver' => 'mysql', + 'url' => env('DATABASE_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'forge'), + 'username' => env('DB_USERNAME', 'forge'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_UNIX_SOCKET', ''), + 'charset' => env('DB_USE_UTF8MB4', true) ? 'utf8mb4' : 'utf8', + 'collation' => env('DB_USE_UTF8MB4', true) ? 'utf8mb4_unicode_ci' : 'utf8_unicode_ci', + 'prefix' => env('DB_PREFIX', ''), + 'prefix_indexes' => true, + 'strict' => false, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + + 'testing' => [ + 'driver' => env('DB_TEST_DRIVER', 'mysql'), + 'host' => env('DB_TEST_HOST'), + 'port' => env('DB_TEST_PORT', '3306'), + 'unix_socket' => env('DB_TEST_UNIX_SOCKET', ''), + 'database' => env('DB_TEST_DATABASE'), + 'username' => env('DB_TEST_USERNAME'), + 'password' => env('DB_TEST_PASSWORD'), + 'charset' => env('DB_USE_UTF8MB4', true) ? 'utf8mb4' : 'utf8', + 'collation' => env('DB_USE_UTF8MB4', true) ? 'utf8mb4_unicode_ci' : 'utf8_unicode_ci', + 'prefix' => env('DB_TEST_PREFIX', ''), + 'prefix_indexes' => true, + 'strict' => false, + ], + + 'pgsql' => [ + 'driver' => 'pgsql', + 'host' => env('DB_HOST', 'localhost'), + 'port' => env('DB_PORT', '5432'), + 'database' => env('DB_DATABASE', 'forge'), + 'username' => env('DB_USERNAME', 'forge'), + 'password' => env('DB_PASSWORD', ''), + 'prefix' => env('DB_PREFIX', ''), + 'charset' => 'utf8', + 'schema' => 'public', + ], + + 'pgsqltesting' => [ + 'driver' => 'pgsql', + 'host' => env('DB_TEST_HOST'), + 'port' => env('DB_PORT', '5432'), + 'database' => env('DB_TEST_DATABASE'), + 'username' => env('DB_TEST_USERNAME'), + 'password' => env('DB_TEST_PASSWORD'), + 'charset' => 'utf8', + 'prefix' => '', + 'schema' => 'public', + ], + + 'sqlsrv' => [ + 'driver' => 'sqlsrv', + 'url' => env('DATABASE_URL'), + 'host' => env('DB_HOST', 'localhost'), + 'port' => env('DB_PORT', '1433'), + 'database' => env('DB_DATABASE', 'forge'), + 'username' => env('DB_USERNAME', 'forge'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => 'utf8', + 'prefix' => '', + 'prefix_indexes' => true, + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Migration Repository Table + |-------------------------------------------------------------------------- + | + | This table keeps track of all the migrations that have already run for + | your application. Using this information, we can determine which of + | the migrations on disk haven't actually been run in the database. + | + */ + + 'migrations' => 'migrations', + + /* + |-------------------------------------------------------------------------- + | Redis Databases + |-------------------------------------------------------------------------- + | + | Redis is an open source, fast, and advanced key-value store that also + | provides a richer set of commands than a typical key-value systems + | such as APC or Memcached. Laravel makes it easy to dig right in. + | + */ + + 'redis' => [ + + 'client' => env('REDIS_CLIENT', 'phpredis'), + + 'options' => [ + 'cluster' => env('REDIS_CLUSTER', 'redis'), + 'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'), + ], + + 'default' => [ + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'password' => env('REDIS_PASSWORD', null), + 'port' => (int) env('REDIS_PORT', 6379), + 'database' => env('REDIS_DB', env('REDIS_DATABASE', 0)), + ], + + 'cache' => [ + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'password' => env('REDIS_PASSWORD', null), + 'port' => (int) env('REDIS_PORT', 6379), + 'database' => env('REDIS_CACHE_DB', 1), + ], + + ], + +]; + +/* + * If the instance is hosted on Heroku, then the database information + * needs to be parsed from the environment variable provided by Heroku. + * This is done below, added to the $db variable and then returned. + */ +if (env('HEROKU')) { + $url = parse_url(env('JAWSDB_URL', env('CLEARDB_DATABASE_URL'))); + + $db['connections']['heroku'] = [ + 'driver' => 'mysql', + 'host' => $url['host'], + 'database' => Str::startsWith($url['path'], '/') ? Str::after($url['path'], '/') : $url['path'], + 'username' => $url['user'], + 'password' => $url['pass'], + 'charset' => env('DB_USE_UTF8MB4', true) ? 'utf8mb4' : 'utf8', + 'collation' => env('DB_USE_UTF8MB4', true) ? 'utf8mb4_unicode_ci' : 'utf8_unicode_ci', + 'prefix' => env('DB_PREFIX', ''), + 'strict' => false, + 'schema' => 'public', + ]; + if (array_key_exists('port', $url)) { + $db['connections']['heroku']['port'] = $url['port']; + } +} + +return $db; diff --git a/config/dav.php b/config/dav.php new file mode 100644 index 0000000..56c6d4a --- /dev/null +++ b/config/dav.php @@ -0,0 +1,13 @@ + 'vCard', + +]; diff --git a/config/filesystems.php b/config/filesystems.php new file mode 100644 index 0000000..e7cc4cb --- /dev/null +++ b/config/filesystems.php @@ -0,0 +1,108 @@ + env('FILESYSTEM_DISK', env('DEFAULT_FILESYSTEM', 'public')), + + /* + |-------------------------------------------------------------------------- + | Filesystem Disks + |-------------------------------------------------------------------------- + | + | Here you may configure as many filesystem "disks" as you wish, and you + | may even configure multiple disks of the same driver. Defaults have + | been setup for each driver as an example of the required options. + | + */ + + 'disks' => [ + + 'local' => [ + 'driver' => 'local', + 'root' => storage_path('app'), + 'throw' => true, + ], + + 'public' => [ + 'driver' => 'local', + 'root' => storage_path('app/public'), + 'url' => env('APP_URL').'/storage', + 'visibility' => 'public', + 'throw' => true, + ], + + 's3' => [ + 'driver' => 's3', + 'key' => env('AWS_ACCESS_KEY_ID', env('AWS_KEY')), + 'secret' => env('AWS_SECRET_ACCESS_KEY', env('AWS_SECRET')), + 'region' => env('AWS_DEFAULT_REGION', env('AWS_REGION')), + 'bucket' => env('AWS_BUCKET'), + 'url' => env('AWS_URL'), + 'endpoint' => env('AWS_ENDPOINT', env('AWS_SERVER', '') ? 'https://'.env('AWS_SERVER') : null), + 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', env('S3_PATH_STYLE', false)), + 'throw' => true, + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Symbolic Links + |-------------------------------------------------------------------------- + | + | Here you may configure the symbolic links that will be created when the + | `storage:link` Artisan command is executed. The array keys should be + | the locations of the links and the values should be their targets. + | + */ + + 'links' => [ + public_path('storage') => storage_path('app/public'), + ], + + /* + |-------------------------------------------------------------------------- + | Filesystem visibility + |-------------------------------------------------------------------------- + | + | If this is set to private, all files are stored privately, and are + | delivered using a proxy-url by monica, providing access of the files in + | the storage. + | This means only the authenticated user will be able to open the files. + | + | You might store the files publicly if you're on a private instance and if + | you want to make files accessible from the outside - the url files are + | still private and not easy to guess. + | + | Supported: "private", "public" + | + */ + + 'default_visibility' => env('FILESYSTEM_DEFAULT_VISIBILITY', 'private'), + + /* + |-------------------------------------------------------------------------- + | Cache control for files + |-------------------------------------------------------------------------- + | + | Defines the Cache-Control header used to serve files. + | Default: 'max-age=2628000' for 1 month cache. + | + */ + + 'default_cache_control' => env('DEFAULT_CACHE_CONTROL', 'private, max-age=2628000'), + +]; diff --git a/config/google2fa.php b/config/google2fa.php new file mode 100644 index 0000000..952fe46 --- /dev/null +++ b/config/google2fa.php @@ -0,0 +1,84 @@ + env('MFA_ENABLED', env('2FA_ENABLED', true)), + + /* + * Lifetime in minutes. + * + * In case you need your users to be asked for a new one time passwords from time to time. + */ + 'lifetime' => env('OTP_LIFETIME', 0), // 0 = eternal + + /* + * Renew lifetime at every new request. + */ + 'keep_alive' => env('OTP_KEEP_ALIVE', true), + + /* + * Auth container binding. + */ + 'auth' => 'auth', + + /* + * Guard. + */ + 'guard' => '', + + /* + * 2FA verified session var. + */ + + 'session_var' => 'google2fa', + + /* + * One Time Password request input name. + */ + 'otp_input' => 'one_time_password', + + /* + * One Time Password Window. + */ + 'window' => 8, + + /* + * Forbid user to reuse One Time Passwords. + */ + 'forbid_old_passwords' => false, + + /* + * User's table column for google2fa secret. + */ + 'otp_secret_column' => 'google2fa_secret', + + /* + * One Time Password View. + */ + 'view' => 'auth/validate2fa', + + /* + * One Time Password error message. + */ + 'error_messages' => [ + 'wrong_otp' => "The 'One Time Password' typed was wrong.", + 'cannot_be_empty' => 'One Time Password cannot be empty.', + 'unknown' => 'An unknown error has occurred. Please try again.', + ], + + /* + * Throw exceptions or just fire events? + */ + 'throw_exceptions' => env('OTP_THROW_EXCEPTION', true), + + /* + * Which image backend to use for generating QR codes? + * + * Supports imagemagick, svg and eps + */ + 'qrcode_image_backend' => \PragmaRX\Google2FALaravel\Support\Constants::QRCODE_IMAGE_BACKEND_SVG, + +]; diff --git a/config/hashids.php b/config/hashids.php new file mode 100644 index 0000000..35b83fc --- /dev/null +++ b/config/hashids.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +return [ + + /* + |-------------------------------------------------------------------------- + | Default Connection Name + |-------------------------------------------------------------------------- + | + | Here you may specify which of the connections below you wish to use as + | your default connection for all work. Of course, you may use many + | connections at once using the manager class. + | + */ + + 'default' => 'main', + + /* + |-------------------------------------------------------------------------- + | Hashids Connections + |-------------------------------------------------------------------------- + | + | Here are each of the connections setup for your application. Example + | configuration has been included, but you may add as many connections as + | you would like. + | + */ + + 'connections' => [ + + 'main' => [ + 'salt' => env('HASH_SALT', 'your-salt-string'), + 'length' => env('HASH_LENGTH', 18), + ], + + 'alternative' => [ + 'salt' => 'your-salt-string', + 'length' => 'your-length-integer', + ], + + ], + + /* + * Default prefix for ids + */ + 'default_prefix' => 'h:', + +]; diff --git a/config/hashing.php b/config/hashing.php new file mode 100644 index 0000000..d3c8e2f --- /dev/null +++ b/config/hashing.php @@ -0,0 +1,52 @@ + 'bcrypt', + + /* + |-------------------------------------------------------------------------- + | Bcrypt Options + |-------------------------------------------------------------------------- + | + | Here you may specify the configuration options that should be used when + | passwords are hashed using the Bcrypt algorithm. This will allow you + | to control the amount of time it takes to hash the given password. + | + */ + + 'bcrypt' => [ + 'rounds' => env('BCRYPT_ROUNDS', 10), + ], + + /* + |-------------------------------------------------------------------------- + | Argon Options + |-------------------------------------------------------------------------- + | + | Here you may specify the configuration options that should be used when + | passwords are hashed using the Argon algorithm. These will allow you + | to control the amount of time it takes to hash the given password. + | + */ + + 'argon' => [ + 'memory' => 1024, + 'threads' => 2, + 'time' => 2, + ], + +]; diff --git a/config/image.php b/config/image.php new file mode 100644 index 0000000..6798381 --- /dev/null +++ b/config/image.php @@ -0,0 +1,20 @@ + 'gd', + +]; diff --git a/config/lang-detector.php b/config/lang-detector.php new file mode 100644 index 0000000..4ad77da --- /dev/null +++ b/config/lang-detector.php @@ -0,0 +1,68 @@ + env('LANG_DETECTOR_AUTODETECT', false), + + /* + * Default driver to use to detect the request language. + * + * Available: browser, subdomain, uri. + */ + 'driver' => env('LANG_DETECTOR_DRIVER', 'browser'), + + /* + * Used on subdomain and uri drivers. That indicates which segment should be used + * to verify the language. + */ + 'segment' => env('LANG_DETECTOR_SEGMENT', 0), + + /** + * Languages available on the application. + * + * You could use parse_langs_to_array to use the string syntax + * or just use the array of languages with its aliases. + * + * @see https://github.com/monicahq/monica/blob/main/docs/contribute/translate.md for translations. + */ + 'languages' => parse_langs_to_array( + env('LANG_DETECTOR_LANGUAGES', [ + 'en', + 'ar', + 'de', + 'el', + 'en-GB' => 'en-GB', + 'es', + 'fr', + 'he', + 'id', + 'it', + 'nl', + 'no', + 'pt-BR' => 'pt-BR', + 'ru', + 'sv', + 'tr', + 'vi', + 'zh', + 'zh-TW' => 'zh-TW', + ]) + ), + + /* + * Indicates if should store detected locale on cookies + */ + 'cookie' => (bool) env('LANG_DETECTOR_COOKIE', true), + + /* + * Indicates if should encrypt cookie + */ + 'cookie_encrypt' => (bool) env('LANG_DETECTOR_COOKIE_ENCRYPT', false), + + /* + * Cookie name + */ + 'cookie_name' => env('LANG_DETECTOR_COOKIE', 'locale'), +]; diff --git a/config/laravelcloudflare.php b/config/laravelcloudflare.php new file mode 100644 index 0000000..7770d19 --- /dev/null +++ b/config/laravelcloudflare.php @@ -0,0 +1,62 @@ + (bool) env('LARAVEL_CLOUDFLARE_ENABLED', env('APP_TRUSTED_CLOUDFLARE', false)), + + /* + |-------------------------------------------------------------------------- + | Name of the cache to store values of the proxies + |-------------------------------------------------------------------------- + | + | This value is the key used in the cache (table, redis, etc.) to store the + | values. + | + */ + + 'cache' => 'cloudflare.proxies', + + /* + |-------------------------------------------------------------------------- + | Cloudflare main url + |-------------------------------------------------------------------------- + | + | This is the url for the cloudflare api. + | + */ + + 'url' => 'https://www.cloudflare.com', + + /* + |-------------------------------------------------------------------------- + | Cloudflare uri for ipv4 ips response + |-------------------------------------------------------------------------- + | + | This is the path to get the values of ipv4 ips from Cloudflare. + | + */ + + 'ipv4-path' => 'ips-v4', + + /* + |-------------------------------------------------------------------------- + | Cloudflare uri for ipv6 ips response + |-------------------------------------------------------------------------- + | + | This is the path to get the values of ipv6 ips from Cloudflare. + | + */ + + 'ipv6-path' => 'ips-v6', + +]; diff --git a/config/laravelsabre.php b/config/laravelsabre.php new file mode 100644 index 0000000..c4bc918 --- /dev/null +++ b/config/laravelsabre.php @@ -0,0 +1,72 @@ + null, + + /* + |-------------------------------------------------------------------------- + | LaravelSabre Path + |-------------------------------------------------------------------------- + | + | This is the URI path where LaravelSabre will be accessible from. Feel free + | to change this path to anything you like. + | + */ + + 'path' => 'dav', + + /* + |-------------------------------------------------------------------------- + | LaravelSabre Master Switch + |-------------------------------------------------------------------------- + | + | This option may be used to disable LaravelSabre. + | + */ + + 'enabled' => (bool) env('DAV_ENABLED', (bool) env('CARDDAV_ENABLED', false)), + + /* + |-------------------------------------------------------------------------- + | LaravelSabre Route Middleware + |-------------------------------------------------------------------------- + | + | These middleware will be assigned to every LaravelSabre route, giving you + | the chance to add your own middleware to this list or change any of + | the existing middleware. Or, you can simply stick with this list. + | + */ + + 'middleware' => [ + 'api', + 'auth.tokenonbasic', + 'limitations', + Authorize::class, + ], + + /* + |-------------------------------------------------------------------------- + | Enable access only to these users + |-------------------------------------------------------------------------- + | + | A comma-separated list of user's email to enable dav for. + | If null or empty, there will be no restriction. + | + */ + 'users' => env('DAV_USERS', null), + +]; diff --git a/config/laravolt/avatar.php b/config/laravolt/avatar.php new file mode 100644 index 0000000..2b68d6c --- /dev/null +++ b/config/laravolt/avatar.php @@ -0,0 +1,82 @@ + 'gd', + + // Initial generator class + 'generator' => \Laravolt\Avatar\Generator\DefaultGenerator::class, + + // Whether all characters supplied must be replaced with their closest ASCII counterparts + 'ascii' => true, + + // Image shape: circle or square + 'shape' => 'square', + + // Image width, in pixel + 'width' => 150, + + // Image height, in pixel + 'height' => 150, + + // Number of characters used as initials. If name consists of single word, the first N character will be used + 'chars' => 2, + + // font size + 'fontSize' => 48, + + // convert initial letter in uppercase + 'uppercase' => false, + + // Fonts used to render text. + // If contains more than one fonts, randomly selected based on name supplied + 'fonts' => [__DIR__.'/../../vendor/laravolt/avatar/fonts/OpenSans-Bold.ttf'], + + // List of foreground colors to be used, randomly selected based on name supplied + 'foregrounds' => [ + '#FFFFFF', + ], + + // List of background colors to be used, randomly selected based on name supplied + 'backgrounds' => [ + '#f44336', + '#E91E63', + '#9C27B0', + '#673AB7', + '#3F51B5', + '#2196F3', + '#03A9F4', + '#00BCD4', + '#009688', + '#4CAF50', + '#8BC34A', + '#CDDC39', + '#FFC107', + '#FF9800', + '#FF5722', + ], + + 'border' => [ + 'size' => 1, + + // border color, available value are: + // 'foreground' (same as foreground color) + // 'background' (same as background color) + // or any valid hex ('#aabbcc') + 'color' => 'foreground', + ], +]; diff --git a/config/location.php b/config/location.php new file mode 100644 index 0000000..8846d04 --- /dev/null +++ b/config/location.php @@ -0,0 +1,153 @@ + env('IPSTACK_APIKEY', null), + + /* + |-------------------------------------------------------------------------- + | Driver + |-------------------------------------------------------------------------- + | + | The default driver you would like to use for location retrieval. + | + */ + 'driver' => App\Http\Location\Drivers\CloudflareDriver::class, + + /* + |-------------------------------------------------------------------------- + | Driver Fallbacks + |-------------------------------------------------------------------------- + | + | The drivers you want to use to retrieve the users location + | if the above selected driver is unavailable. + | + | These will be called upon in order (first to last). + | + */ + 'fallbacks' => [ + Stevebauman\Location\Drivers\IpInfo::class, + Stevebauman\Location\Drivers\GeoPlugin::class, + Stevebauman\Location\Drivers\MaxMind::class, + ], + + /* + |-------------------------------------------------------------------------- + | Position + |-------------------------------------------------------------------------- + | + | Here you may configure the position instance that is created + | and returned from the above drivers. The instance you + | create must extend the built-in Position class. + | + */ + 'position' => Stevebauman\Location\Position::class, + + /* + |-------------------------------------------------------------------------- + | MaxMind Configuration + |-------------------------------------------------------------------------- + | + | The configuration for the MaxMind driver. + | + | If web service is enabled, you must fill in your user ID and license key. + | + | If web service is disabled, it will try and retrieve the users location + | from the MaxMind database file located in the local path below. + | + */ + 'maxmind' => [ + 'web' => [ + 'enabled' => false, + 'user_id' => '', + 'license_key' => '', + 'options' => [ + 'host' => 'geoip.maxmind.com', + ], + ], + 'local' => [ + 'path' => database_path('maxmind/GeoLite2-City.mmdb'), + ], + ], + + /* + |-------------------------------------------------------------------------- + | IP API Pro Configuration + |-------------------------------------------------------------------------- + | + | The configuration for the IP API Pro driver. + | + */ + 'ip_api' => [ + 'token' => env('IP_API_TOKEN'), + ], + + /* + |-------------------------------------------------------------------------- + | IPInfo Configuration + |-------------------------------------------------------------------------- + | + | The configuration for the IPInfo driver. + | + */ + 'ipinfo' => [ + 'token' => env('IPINFO_TOKEN'), + ], + + /* + |-------------------------------------------------------------------------- + | IPData Configuration + |-------------------------------------------------------------------------- + | + | The configuration for the IPData driver. + | + */ + 'ipdata' => [ + 'token' => env('IPDATA_TOKEN'), + ], + + /* + |-------------------------------------------------------------------------- + | Localhost Testing + |-------------------------------------------------------------------------- + | + | If your running your website locally and want to test different + | IP addresses to see location detection, set 'enabled' to true. + | + | The testing IP address is a Google host in the United-States. + | + */ + 'testing' => [ + 'enabled' => env('LOCATION_TESTING', false), + 'ip' => '66.102.0.0', + ], + + /* + |-------------------------------------------------------------------------- + | Locationiq API Url + |-------------------------------------------------------------------------- + | + | Url to call Locationiq api. See https://locationiq.com/docs + | + */ + 'location_iq_url' => env('LOCATIONIQ_URL', 'https://us1.locationiq.com/v1/'), + + /* + |-------------------------------------------------------------------------- + | Weatherapi Url + |-------------------------------------------------------------------------- + | + | Url to call Weatherapi. + | + */ + 'weatherapi_url' => env('WEATHERAPI_URL', 'https://api.weatherapi.com/v1/current.json'), + +]; diff --git a/config/logging.php b/config/logging.php new file mode 100644 index 0000000..94c463b --- /dev/null +++ b/config/logging.php @@ -0,0 +1,106 @@ + env('LOG_CHANNEL', 'stack'), + + /* + |-------------------------------------------------------------------------- + | Log Channels + |-------------------------------------------------------------------------- + | + | Here you may configure the log channels for your application. Out of + | the box, Laravel uses the Monolog PHP logging library. This gives + | you a variety of powerful log handlers / formatters to utilize. + | + | Available Drivers: "single", "daily", "slack", "syslog", + | "errorlog", "custom", "stack" + | + */ + 'channels' => [ + 'stack' => [ + 'driver' => 'stack', + 'channels' => ['single'], + 'ignore_exceptions' => false, + ], + 'stackerrorlog' => [ + 'driver' => 'stack', + 'channels' => ['errorlog', 'papertrail', 'sentry'], + ], + 'single' => [ + 'driver' => 'single', + 'path' => storage_path('logs/laravel.log'), + 'level' => 'debug', + ], + 'daily' => [ + 'driver' => 'daily', + 'path' => storage_path('logs/laravel.log'), + 'level' => 'debug', + 'days' => 7, + ], + 'slack' => [ + 'driver' => 'slack', + 'url' => env('LOG_SLACK_WEBHOOK_URL'), + 'username' => 'Laravel Log', + 'emoji' => ':boom:', + 'level' => 'critical', + ], + 'papertrailerrorlog' => [ + 'driver' => 'stack', + 'channels' => ['papertrail', 'errorlog'], + ], + 'papertrail' => [ + 'driver' => 'monolog', + 'level' => 'debug', + 'handler' => SyslogUdpHandler::class, + 'handler_with' => [ + 'host' => env('PAPERTRAIL_URL'), + 'port' => env('PAPERTRAIL_PORT'), + ], + ], + 'sentry' => [ + 'driver' => 'sentry', + 'level' => 'debug', + 'bubble' => true, + ], + 'stderr' => [ + 'driver' => 'monolog', + 'handler' => StreamHandler::class, + 'with' => [ + 'stream' => 'php://stderr', + ], + ], + 'syslog' => [ + 'driver' => 'syslog', + 'level' => 'debug', + ], + 'errorlog' => [ + 'driver' => 'errorlog', + 'level' => 'debug', + ], + 'testing' => [ + 'driver' => 'errorlog', + 'level' => 'emergency', + ], + 'null' => [ + 'driver' => 'monolog', + 'handler' => NullHandler::class, + ], + 'emergency' => [ + 'path' => storage_path('logs/laravel.log'), + ], + ], +]; diff --git a/config/mail.php b/config/mail.php new file mode 100644 index 0000000..7547756 --- /dev/null +++ b/config/mail.php @@ -0,0 +1,107 @@ + env('MAIL_MAILER', env('MAIL_DRIVER', 'smtp')), + + /* + |-------------------------------------------------------------------------- + | Mailer Configurations + |-------------------------------------------------------------------------- + | + | Here you may configure all of the mailers used by your application plus + | their respective settings. Several examples have been configured for + | you and you are free to add your own as your application requires. + | + | Laravel supports a variety of mail "transport" drivers to be used while + | sending an e-mail. You will specify which one you are using for your + | mailers below. You are free to add additional mailers as required. + | + | Supported: "smtp", "sendmail", "mailgun", "ses", + | "postmark", "log", "array" + | + */ + + 'mailers' => [ + 'smtp' => [ + 'transport' => 'smtp', + 'host' => env('MAIL_HOST', 'smtp.mailgun.org'), + 'port' => env('MAIL_PORT', 587), + 'encryption' => env('MAIL_ENCRYPTION', 'tls'), + 'username' => env('MAIL_USERNAME'), + 'password' => env('MAIL_PASSWORD'), + 'verify_peer' => env('MAIL_VERIFY_PEER', true), + ], + + 'ses' => [ + 'transport' => 'ses', + ], + + 'mailgun' => [ + 'transport' => 'mailgun', + ], + + 'postmark' => [ + 'transport' => 'postmark', + ], + + 'sendmail' => [ + 'transport' => 'sendmail', + 'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'), + ], + + 'log' => [ + 'transport' => 'log', + 'channel' => env('MAIL_LOG_CHANNEL'), + ], + + 'array' => [ + 'transport' => 'array', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Global "From" Address + |-------------------------------------------------------------------------- + | + | You may wish for all e-mails sent by your application to be sent from + | the same address. Here, you may specify a name and address that is + | used globally for all e-mails that are sent by your application. + | + */ + + 'from' => [ + 'address' => env('MAIL_FROM_ADDRESS'), + 'name' => env('MAIL_FROM_NAME'), + ], + + /* + |-------------------------------------------------------------------------- + | Markdown Mail Settings + |-------------------------------------------------------------------------- + | + | If you are using Markdown based email rendering, you may configure your + | theme and component paths here, allowing you to customize the design + | of the emails. Or, you may simply stick with the Laravel defaults! + | + */ + 'markdown' => [ + 'theme' => 'default', + 'paths' => [ + resource_path('views/vendor/mail'), + ], + ], + +]; diff --git a/config/monica.php b/config/monica.php new file mode 100644 index 0000000..855cd17 --- /dev/null +++ b/config/monica.php @@ -0,0 +1,287 @@ + trim(trim(is_file(__DIR__.'/.version') ? file_get_contents(__DIR__.'/.version') : (is_dir(__DIR__.'/../.git') ? exec('git --git-dir '.base_path('.git').' describe --abbrev=0 --tags') : ''), 'v')), + + /* + |-------------------------------------------------------------------------- + | Disable User registration + |-------------------------------------------------------------------------- + | + | Disables registration of new users + | + */ + 'disable_signup' => env('APP_DISABLE_SIGNUP', false), + + /* + |-------------------------------------------------------------------------- + | Activate double optin on signup + |-------------------------------------------------------------------------- + | + | Activates double optin on signup + | + */ + 'signup_double_optin' => env('APP_SIGNUP_DOUBLE_OPTIN', false), + + /* + |-------------------------------------------------------------------------- + | New User Email Notification + |-------------------------------------------------------------------------- + | + | Email to notify when new user registers. + | + */ + 'email_new_user_notification' => env('APP_EMAIL_NEW_USERS_NOTIFICATION'), + + /* + |-------------------------------------------------------------------------- + | User and error tracking + |-------------------------------------------------------------------------- + | + | We provide placeholders for Sentry. + | + */ + 'sentry_support' => env('SENTRY_SUPPORT', false), + + /* + |-------------------------------------------------------------------------- + | Ping that checks if a new version is available + |-------------------------------------------------------------------------- + | + | This is used to indicate if you allow the ping to be sent to + | version.monicahq.com to check if a new version is available. + | + */ + 'check_version' => env('CHECK_VERSION', true), + + /* + |-------------------------------------------------------------------------- + | Allow access through the API of the public statistics + |-------------------------------------------------------------------------- + | + | Your Monica instance has some statistics (number of users, number of + | contacts,...). Those data can be public (they are on MonicaHQ.com). + | This setting lets you access those data through a public API call. + | + */ + 'allow_statistics_through_public_api_access' => env('ALLOW_STATISTICS_THROUGH_PUBLIC_API_ACCESS', false), + + /* + |-------------------------------------------------------------------------- + | URL of the server for the version check + |-------------------------------------------------------------------------- + | + | This is the server that is used to ping if a new version is avaialble. + | Do not change this manually. + | + */ + 'weekly_ping_server_url' => 'https://version.monicahq.com/ping', + + /* + |-------------------------------------------------------------------------- + | List of default relationship type group + |-------------------------------------------------------------------------- + | + | This is used to populate the relationship type groups table. + | + */ + 'default_relationship_type_group' => [ + 'love', + 'family', + 'friend', + 'work', + ], + + /* + |-------------------------------------------------------------------------- + | Compliance to various international policies. + |-------------------------------------------------------------------------- + | + | Indicates whether we should comply to international policies like GDPR or + | CASL. Defaults to false, but if you do, it's at your own risk. + | + */ + 'policy_compliant' => env('POLICY_COMPLIANT', true), + + /* + |-------------------------------------------------------------------------- + | Access to paid features + |-------------------------------------------------------------------------- + | + | This value determines if the instance can access the paid features that + | are available on https://monicahq.com, for free. + | If set to false, the instance won't have access to the paid features. + | + | Available Settings: true, false + | + */ + 'requires_subscription' => env('REQUIRES_SUBSCRIPTION', false), + + /* + |-------------------------------------------------------------------------- + | Paid plan settings + |-------------------------------------------------------------------------- + | + | This value determines the name and the cost of the paid plan offered + | on https://monicahq.com. These settings make sense only if you do activate + | the `unlock_paid_features` above. + | + | + */ + 'paid_plan_monthly_friendly_name' => env('PAID_PLAN_MONTHLY_FRIENDLY_NAME', null), + 'paid_plan_monthly_id' => env('PAID_PLAN_MONTHLY_ID', null), + 'paid_plan_monthly_price' => env('PAID_PLAN_MONTHLY_PRICE', null), + 'paid_plan_annual_friendly_name' => env('PAID_PLAN_ANNUAL_FRIENDLY_NAME', null), + 'paid_plan_annual_id' => env('PAID_PLAN_ANNUAL_ID', null), + 'paid_plan_annual_price' => env('PAID_PLAN_ANNUAL_PRICE', null), + + /* + |-------------------------------------------------------------------------- + | Number of allowed contacts + |-------------------------------------------------------------------------- + | + | This value determines the number of contacts allowed on a free account. + | + */ + 'number_of_allowed_contacts_free_account' => env('NUMBER_OF_ALLOWED_CONTACTS_FREE_ACCOUNT', 10), + + /* + |-------------------------------------------------------------------------- + | Number of contacts to paginate + |-------------------------------------------------------------------------- + | + | This value determines the number of contacts to paginate on the contacts page by default. + | + */ + 'number_of_contacts_pagination' => env('NUMBER_OF_CONTACTS_PAGINATION', 30), + + /* + |-------------------------------------------------------------------------- + | Email address to contact for support + |-------------------------------------------------------------------------- + | + | This value will be the email address used in the footer of the application + | to contact support. + | + */ + 'support_email_address' => env('SUPPORT_EMAIL_ADDRESS', 'support@monicahq.com'), + + /* + |-------------------------------------------------------------------------- + | Twitter account for support + |-------------------------------------------------------------------------- + | + | This value determines the twitter account shown in case of maintenance in + | progress. + | + */ + 'twitter_account' => env('SUPPORT_TWITTER', 'monicaHQ_app'), + + /* + |-------------------------------------------------------------------------- + | Maximum allowed size for uploaded files, in kilobytes. + |-------------------------------------------------------------------------- + | + | This value determines the maximum size when uploading a file, in kilobytes. + | + */ + 'max_upload_size' => env('DEFAULT_MAX_UPLOAD_SIZE', 10240), + + /* + |-------------------------------------------------------------------------- + | Maximum allowed storage size per account, in megabytes. + |-------------------------------------------------------------------------- + | + | This the default limit for each new account. Default value: 512Mb. + | + */ + 'max_storage_size' => env('DEFAULT_MAX_STORAGE_SIZE', 512), + + /* + |-------------------------------------------------------------------------- + | Enable geolocation service. + |-------------------------------------------------------------------------- + | + | For some features, we need to translate addresses to latitude/longitude + | coordinates. Like getting weather, for instance. + | If you do enable geolocation, you also need to provide a geolocation + | api key as shown below. + | + */ + 'enable_geolocation' => env('ENABLE_GEOLOCATION', false), + + /* + |-------------------------------------------------------------------------- + | API key for geolocation service. + |-------------------------------------------------------------------------- + | + | We use LocationIQ (https://locationiq.com/) to translate addresses to + | latitude/longitude coordinates. We could use Google instead but we don't + | want to give anything to Google, ever. + | LocationIQ offers 10,000 free requests per day. + | + */ + 'location_iq_api_key' => env('LOCATION_IQ_API_KEY', null), + + /* + |-------------------------------------------------------------------------- + | Enable weather to be displayed on the contact profile page. + |-------------------------------------------------------------------------- + | + | Geolocation needs to be enabled for this feature to work. We need to it + | to translate addresses to long/latitude coordinates. + */ + 'enable_weather' => env('ENABLE_WEATHER', false), + + /* + |-------------------------------------------------------------------------- + | API key for weather data. + |-------------------------------------------------------------------------- + | + | To provide weather information, we use WeatherAPI. + | See https://www.weatherapi.com/ + */ + 'weatherapi_key' => env('WEATHERAPI_KEY', null), + + /* + |-------------------------------------------------------------------------- + | Configure default rate limit for route services per minute + |-------------------------------------------------------------------------- + | + | Configure rate limit for route services per minute + */ + 'rate_limit_api' => env('RATE_LIMIT_PER_MINUTE_API', 60), + 'rate_limit_oauth' => env('RATE_LIMIT_PER_MINUTE_OAUTH', 5), + + /* + |-------------------------------------------------------------------------- + | Default avatar size + |-------------------------------------------------------------------------- + | + | The default avatar size. + */ + 'avatar_size' => 200, + + /* + |-------------------------------------------------------------------------- + | Export size count + |-------------------------------------------------------------------------- + | + | Number of exports available. When the number of exports is reached, the + | oldest export will be deleted. + | + */ + 'export_size' => (int) env('EXPORT_SIZE', 5), + +]; diff --git a/config/passport.php b/config/passport.php new file mode 100644 index 0000000..60d7ee2 --- /dev/null +++ b/config/passport.php @@ -0,0 +1,96 @@ + env('PASSPORT_PRIVATE_KEY'), + + 'public_key' => env('PASSPORT_PUBLIC_KEY'), + + /* + |-------------------------------------------------------------------------- + | Client UUIDs + |-------------------------------------------------------------------------- + | + | By default, Passport uses auto-incrementing primary keys when assigning + | IDs to clients. However, if Passport is installed using the provided + | --uuids switch, this will be set to "true" and UUIDs will be used. + | + */ + + 'client_uuids' => false, + + /* + |-------------------------------------------------------------------------- + | Personal Access Client + |-------------------------------------------------------------------------- + | + | If you enable client hashing, you should set the personal access client + | ID and unhashed secret within your environment file. The values will + | get used while issuing fresh personal access tokens to your users. + | + */ + + 'personal_access_client' => [ + 'id' => env('PASSPORT_PERSONAL_ACCESS_CLIENT_ID'), + 'secret' => env('PASSPORT_PERSONAL_ACCESS_CLIENT_SECRET'), + ], + + /* + |-------------------------------------------------------------------------- + | Password Grant Client + |-------------------------------------------------------------------------- + | + | Password grant client used for oauth login, and mobile application. + | + */ + + 'password_grant_client' => [ + 'id' => env('PASSPORT_PASSWORD_GRANT_CLIENT_ID', env('MOBILE_CLIENT_ID')), + 'secret' => env('PASSPORT_PASSWORD_GRANT_CLIENT_SECRET', env('MOBILE_CLIENT_SECRET')), + ], + + /* + |-------------------------------------------------------------------------- + | Passport Storage Driver + |-------------------------------------------------------------------------- + | + | This configuration value allows you to customize the storage options + | for Passport, such as the database connection that should be used + | by Passport's internal database models which store tokens, etc. + | + */ + + 'storage' => [ + 'database' => [ + 'connection' => env('DB_CONNECTION', 'mysql'), + ], + ], + +]; + +// Use fortrabbit secrets +if (env('APP_SECRETS')) { + $secrets = json_decode(file_get_contents(env('APP_SECRETS')), true); + + if (isset($secrets['CUSTOM']['PASSPORT_PRIVATE_KEY'])) { + $passports['private_key'] = str_replace('\\\\n', '\\n', $secrets['CUSTOM']['PASSPORT_PRIVATE_KEY']); + } + if (isset($secrets['CUSTOM']['PASSPORT_PUBLIC_KEY'])) { + $passports['public_key'] = str_replace('\\\\n', '\\n', $secrets['CUSTOM']['PASSPORT_PUBLIC_KEY']); + } +} + +return $passports; diff --git a/config/queue.php b/config/queue.php new file mode 100644 index 0000000..23b31e7 --- /dev/null +++ b/config/queue.php @@ -0,0 +1,89 @@ + env('QUEUE_CONNECTION', env('QUEUE_DRIVER', 'sync')), + + /* + |-------------------------------------------------------------------------- + | Queue Connections + |-------------------------------------------------------------------------- + | + | Here you may configure the connection information for each server that + | is used by your application. A default configuration has been added + | for each back-end shipped with Laravel. You are free to add more. + | + | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null" + | + */ + + 'connections' => [ + + 'sync' => [ + 'driver' => 'sync', + ], + + 'database' => [ + 'driver' => 'database', + 'table' => 'jobs', + 'queue' => 'default', + 'retry_after' => 90, + ], + + 'beanstalkd' => [ + 'driver' => 'beanstalkd', + 'host' => 'localhost', + 'queue' => 'default', + 'retry_after' => 90, + 'block_for' => 0, + ], + + 'sqs' => [ + 'driver' => 'sqs', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'), + 'queue' => env('SQS_QUEUE', 'your-queue-name'), + 'suffix' => env('SQS_SUFFIX'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => 'default', + 'queue' => env('REDIS_QUEUE', 'default'), + 'retry_after' => 90, + 'block_for' => null, + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Failed Queue Jobs + |-------------------------------------------------------------------------- + | + | These options configure the behavior of failed queue job logging so you + | can control which database and table are used to store the jobs that + | have failed. You may change them to any database / table you wish. + | + */ + + 'failed' => [ + 'driver' => env('QUEUE_FAILED_DRIVER', 'database'), + 'database' => env('DB_CONNECTION', 'mysql'), + 'table' => 'failed_jobs', + ], + +]; diff --git a/config/sentry-release.php b/config/sentry-release.php new file mode 100644 index 0000000..52077a6 --- /dev/null +++ b/config/sentry-release.php @@ -0,0 +1,37 @@ + env('SENTRY_AUTH_TOKEN'), + + /* + |-------------------------------------------------------------------------- + | Organisation slug + |-------------------------------------------------------------------------- + */ + 'organisation' => env('SENTRY_ORG'), + + /* + |-------------------------------------------------------------------------- + | Project + |-------------------------------------------------------------------------- + */ + 'project' => env('SENTRY_PROJECT'), + + /* + |-------------------------------------------------------------------------- + | Git repository set in sentry + |-------------------------------------------------------------------------- + | + | See https://sentry.io/settings/{slug}/repos/ + | + */ + 'repo' => env('SENTRY_REPO', 'monicahq/monica'), +]; diff --git a/config/sentry.php b/config/sentry.php new file mode 100644 index 0000000..fec9b04 --- /dev/null +++ b/config/sentry.php @@ -0,0 +1,37 @@ + env('SENTRY_LARAVEL_DSN', env('SENTRY_DSN')), + + // capture release as git sha + 'release' => is_file(__DIR__.'/.release') ? trim(file_get_contents(__DIR__.'/.release')) : (is_dir(__DIR__.'/../.git') ? trim(exec('git --git-dir '.base_path('.git').' log --pretty="%h" -n1 HEAD')) : null), + + // When left empty or `null` the Laravel environment will be used + 'environment' => env('SENTRY_ENVIRONMENT'), + + 'breadcrumbs' => [ + // Capture Laravel logs in breadcrumbs + 'logs' => true, + + // Capture SQL queries in breadcrumbs + 'sql_queries' => true, + + // Capture bindings on SQL queries logged in breadcrumbs + 'sql_bindings' => true, + + // Capture queue job information in breadcrumbs + 'queue_info' => true, + + // Capture command information in breadcrumbs + 'command_info' => true, + ], + + // @see: https://docs.sentry.io/platforms/php/data-management/sensitive-data/#personally-identifiable-information-pii + 'send_default_pii' => env('SENTRY_DEFAULT_PII', false), + + 'traces_sample_rate' => (float) env('SENTRY_TRACES_SAMPLE_RATE', 0.0), + + 'controllers_base_namespace' => env('SENTRY_CONTROLLERS_BASE_NAMESPACE', 'App\\Http\\Controllers'), + +]; diff --git a/config/services.php b/config/services.php new file mode 100644 index 0000000..589e6cc --- /dev/null +++ b/config/services.php @@ -0,0 +1,43 @@ + [ + 'domain' => env('MAILGUN_DOMAIN'), + 'secret' => env('MAILGUN_SECRET'), + 'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'), + ], + + 'postmark' => [ + 'token' => env('POSTMARK_TOKEN'), + ], + + 'ses' => [ + 'key' => env('AWS_ACCESS_KEY_ID', env('SES_KEY')), + 'secret' => env('AWS_SECRET_ACCESS_KEY', env('SES_SECRET')), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + ], + + 'stripe' => [ + 'model' => App\Models\Account\Account::class, + 'key' => env('STRIPE_KEY', null), + 'secret' => env('STRIPE_SECRET', null), + 'webhook' => [ + 'secret' => env('STRIPE_WEBHOOK_SECRET', null), + 'tolerance' => env('STRIPE_WEBHOOK_TOLERANCE', 300), + ], + ], + +]; diff --git a/config/session.php b/config/session.php new file mode 100644 index 0000000..d285898 --- /dev/null +++ b/config/session.php @@ -0,0 +1,194 @@ + env('SESSION_DRIVER', 'file'), + + /* + |-------------------------------------------------------------------------- + | Session Lifetime + |-------------------------------------------------------------------------- + | + | Here you may specify the number of minutes that you wish the session + | to be allowed to remain idle before it expires. If you want them + | to immediately expire on the browser closing, set that option. + | + */ + + 'lifetime' => env('SESSION_LIFETIME', 120), + + 'expire_on_close' => false, + + /* + |-------------------------------------------------------------------------- + | Session Encryption + |-------------------------------------------------------------------------- + | + | This option allows you to easily specify that all of your session data + | should be encrypted before it is stored. All encryption will be run + | automatically by Laravel and you can use the Session like normal. + | + */ + + 'encrypt' => false, + + /* + |-------------------------------------------------------------------------- + | Session File Location + |-------------------------------------------------------------------------- + | + | When using the native session driver, we need a location where session + | files may be stored. A default has been set for you but a different + | location may be specified. This is only needed for file sessions. + | + */ + + 'files' => storage_path('framework/sessions'), + + /* + |-------------------------------------------------------------------------- + | Session Database Connection + |-------------------------------------------------------------------------- + | + | When using the "database" or "redis" session drivers, you may specify a + | connection that should be used to manage these sessions. This should + | correspond to a connection in your database configuration options. + | + */ + + 'connection' => env('SESSION_CONNECTION', null), + + /* + |-------------------------------------------------------------------------- + | Session Database Table + |-------------------------------------------------------------------------- + | + | When using the "database" session driver, you may specify the table we + | should use to manage the sessions. Of course, a sensible default is + | provided for you; however, you are free to change this as needed. + | + */ + + 'table' => 'sessions', + + /* + |-------------------------------------------------------------------------- + | Session Cache Store | Session Cache Store + |-------------------------------------------------------------------------- + | + | When using the "apc" or "memcached" session drivers, you may specify a + | cache store that should be used for these sessions. This value must + | correspond with one of the application's configured cache stores. + | + */ + + 'store' => env('SESSION_STORE', null), + + /* + |-------------------------------------------------------------------------- + | Session Sweeping Lottery + |-------------------------------------------------------------------------- + | + | Some session drivers must manually sweep their storage location to get + | rid of old sessions from storage. Here are the chances that it will + | happen on a given request. By default, the odds are 2 out of 100. + | + */ + + 'lottery' => [2, 100], + + /* + |-------------------------------------------------------------------------- + | Session Cookie Name + |-------------------------------------------------------------------------- + | + | Here you may change the name of the cookie used to identify a session + | instance by ID. The name specified here will get used every time a + | new session cookie is created by the framework for every driver. + | + */ + + 'cookie' => env('SESSION_COOKIE', 'laravel_session'), + + /* + |-------------------------------------------------------------------------- + | Session Cookie Path + |-------------------------------------------------------------------------- + | + | The session cookie path determines the path for which the cookie will + | be regarded as available. Typically, this will be the root path of + | your application but you are free to change this when necessary. + | + */ + + 'path' => '/', + + /* + |-------------------------------------------------------------------------- + | Session Cookie Domain + |-------------------------------------------------------------------------- + | + | Here you may change the domain of the cookie used to identify a session + | in your application. This will determine which domains the cookie is + | available to in your application. A sensible default has been set. + | + */ + + 'domain' => env('SESSION_DOMAIN', null), + + /* + |-------------------------------------------------------------------------- + | HTTPS Only Cookies + |-------------------------------------------------------------------------- + | + | By setting this option to true, session cookies will only be sent back + | to the server if the browser has a HTTPS connection. This will keep + | the cookie from being sent to you if it can not be done securely. + | + */ + + 'secure' => env('SESSION_SECURE_COOKIE', null), + + /* + |-------------------------------------------------------------------------- + | HTTP Access Only + |-------------------------------------------------------------------------- + | + | Setting this value to true will prevent JavaScript from accessing the + | value of the cookie and the cookie will only be accessible through + | the HTTP protocol. You are free to modify this option if needed. + | + */ + + 'http_only' => true, + + /* + |-------------------------------------------------------------------------- + | Same-Site Cookies + |-------------------------------------------------------------------------- + | + | This option determines how your cookies behave when cross-site requests + | take place, and can be used to mitigate CSRF attacks. By default, we + | do not enable this as other CSRF protection services are in place. + | + | Supported: "lax", "strict" + | + */ + + 'same_site' => 'lax', + +]; diff --git a/config/view.php b/config/view.php new file mode 100644 index 0000000..22b8a18 --- /dev/null +++ b/config/view.php @@ -0,0 +1,36 @@ + [ + resource_path('views'), + ], + + /* + |-------------------------------------------------------------------------- + | Compiled View Path + |-------------------------------------------------------------------------- + | + | This option determines where all the compiled Blade templates will be + | stored for your application. Typically, this is within the storage + | directory. However, as usual, you are free to change this value. + | + */ + + 'compiled' => env( + 'VIEW_COMPILED_PATH', + realpath(storage_path('framework/views')) + ), + +]; diff --git a/config/webauthn.php b/config/webauthn.php new file mode 100644 index 0000000..280c439 --- /dev/null +++ b/config/webauthn.php @@ -0,0 +1,306 @@ + env('MFA_ENABLED', true), + + /* + |-------------------------------------------------------------------------- + | Webauthn Guard + |-------------------------------------------------------------------------- + | + | Here you may specify which authentication guard Webauthn will use while + | authenticating users. This value should correspond with one of your + | guards that is already present in your "auth" configuration file. + | + */ + + 'guard' => 'web', + + /* + |-------------------------------------------------------------------------- + | Username / Email + |-------------------------------------------------------------------------- + | + | This value defines which model attribute should be considered as your + | application's "username" field. Typically, this might be the email + | address of the users but you are free to change this value here. + | + */ + + 'username' => 'email', + + /* + |-------------------------------------------------------------------------- + | Webauthn Routes Prefix / Subdomain + |-------------------------------------------------------------------------- + | + | Here you may specify which prefix Webauthn will assign to all the routes + | that it registers with the application. If necessary, you may change + | subdomain under which all of the Webauthn routes will be available. + | + */ + + 'prefix' => 'webauthn', + + 'domain' => null, + + /* + |-------------------------------------------------------------------------- + | Webauthn Routes Middleware + |-------------------------------------------------------------------------- + | + | Here you may specify which middleware Webauthn will assign to the routes + | that it registers with the application. If necessary, you may change + | these middleware but typically this provided default is preferred. + | + */ + + 'middleware' => [ + 'web', + 'auth', + 'verified', + ], + + /* + |-------------------------------------------------------------------------- + | Webauthn key model + |-------------------------------------------------------------------------- + | + | Here you may specify the model used to create Webauthn keys. + | + */ + + 'model' => WebauthnKey::class, + + /* + |-------------------------------------------------------------------------- + | Rate Limiting + |-------------------------------------------------------------------------- + | + | By default, Laravel Webauthn will throttle logins to five requests per + | minute for every email and IP address combination. However, if you would + | like to specify a custom rate limiter to call then you may specify it here. + | + */ + + 'limiters' => [ + 'login' => null, + ], + + /* + |-------------------------------------------------------------------------- + | Redirect routes + |-------------------------------------------------------------------------- + | + | When using navigation, redirects to these url on success: + | - login: after a successfull login. + | - register: after a successfull Webauthn key creation. + | + | Redirects are not used in case of application/json requests. + | + */ + + 'redirects' => [ + 'login' => '/dashboard', + 'register' => '/settings/security', + ], + + /* + |-------------------------------------------------------------------------- + | View to load after middleware login request. + |-------------------------------------------------------------------------- + | + | The name of blade template to load: + | - authenticate: when a user login, and has to validate Webauthn 2nd factor. + | - register: when a user request to create a Webauthn key. + | + | If the views are empty or null, then the route will not be registered. + | + */ + + 'views' => [ + 'authenticate' => 'auth.validatewebauthn', + 'register' => null, + ], + + /* + |-------------------------------------------------------------------------- + | Webauthn logging + |-------------------------------------------------------------------------- + | + | Here you may specify the channel to which Webauthn will log messages. + | This value should correspond with one of your loggers that is already + | present in your "logging" configuration file. If left as null, it will + | use the default logger for the application. + | + */ + + 'log' => null, + + /* + |-------------------------------------------------------------------------- + | Session name + |-------------------------------------------------------------------------- + | + | Name of the session parameter to store the successful login. + | + */ + + 'session_name' => 'webauthn_auth', + + /* + |-------------------------------------------------------------------------- + | Webauthn challenge length + |-------------------------------------------------------------------------- + | + | Length of the random string used in the challenge request. + | + */ + + 'challenge_length' => 32, + + /* + |-------------------------------------------------------------------------- + | Webauthn timeout (milliseconds) + |-------------------------------------------------------------------------- + | + | Time that the caller is willing to wait for the call to complete. + | + */ + + 'timeout' => 60000, + + /* + |-------------------------------------------------------------------------- + | Webauthn extension client input + |-------------------------------------------------------------------------- + | + | Optional authentication extension. + | See https://www.w3.org/TR/webauthn/#client-extension-input + | + */ + + 'extensions' => [], + + /* + |-------------------------------------------------------------------------- + | Webauthn icon + |-------------------------------------------------------------------------- + | + | Url which resolves to an image associated with the entity. + | See https://www.w3.org/TR/webauthn/#dom-publickeycredentialentity-icon + | + */ + + 'icon' => env('WEBAUTHN_ICON'), + + /* + |-------------------------------------------------------------------------- + | Webauthn Attestation Conveyance + |-------------------------------------------------------------------------- + | + | This parameter specify the preference regarding the attestation conveyance + | during credential generation. + | See https://www.w3.org/TR/webauthn/#enum-attestation-convey + | + | Supported: "none", "indirect", "direct", "enterprise". + */ + + 'attestation_conveyance' => 'none', + + /* + |-------------------------------------------------------------------------- + | Google Safetynet ApiKey + |-------------------------------------------------------------------------- + | + | Api key to use Google Safetynet. + | See https://developer.android.com/training/safetynet/attestation + | + */ + + 'google_safetynet_api_key' => env('GOOGLE_SAFETYNET_API_KEY'), + + /* + |-------------------------------------------------------------------------- + | Webauthn Public Key Credential Parameters + |-------------------------------------------------------------------------- + | + | List of allowed Cryptographic Algorithm Identifier. + | See https://www.w3.org/TR/webauthn/#sctn-alg-identifier + | + */ + + 'public_key_credential_parameters' => [ + \Cose\Algorithms::COSE_ALGORITHM_ES256, // ECDSA with SHA-256 + \Cose\Algorithms::COSE_ALGORITHM_ES512, // ECDSA with SHA-512 + \Cose\Algorithms::COSE_ALGORITHM_RS256, // RSASSA-PKCS1-v1_5 with SHA-256 + \Cose\Algorithms::COSE_ALGORITHM_EDDSA, // EDDSA + \Cose\Algorithms::COSE_ALGORITHM_ES384, // ECDSA with SHA-384 + ], + + /* + |-------------------------------------------------------------------------- + | Credentials Attachment. + |-------------------------------------------------------------------------- + | + | Authentication can be tied to the current device (like when using Windows + | Hello or Touch ID) or a cross-platform device (like USB Key). When this + | is "null" the user will decide where to store his authentication info. + | + | See https://www.w3.org/TR/webauthn/#enum-attachment + | + | Supported: "null", "cross-platform", "platform". + | + */ + + 'attachment_mode' => null, + + /* + |-------------------------------------------------------------------------- + | User presence and verification + |-------------------------------------------------------------------------- + | + | Most authenticators and smartphones will ask the user to actively verify + | themselves for log in. Use "required" to always ask verify, "preferred" + | to ask when possible, and "discouraged" to just ask for user presence. + | + | See https://www.w3.org/TR/webauthn/#enum-userVerificationRequirement + | + | Supported: "required", "preferred", "discouraged". + | + */ + + 'user_verification' => 'preferred', + + /* + |-------------------------------------------------------------------------- + | Userless (One touch, Typeless) login + |-------------------------------------------------------------------------- + | + | By default, users must input their email to receive a list of credentials + | ID to use for authentication, but they can also login without specifying + | one if the device can remember them, allowing for true one-touch login. + | + | If required or preferred, login verification will be always required. + | + | See https://www.w3.org/TR/webauthn/#enum-residentKeyRequirement + | + | Supported: "null", "required", "preferred", "discouraged". + | + */ + + 'userless' => null, + +]; diff --git a/crowdin.yml b/crowdin.yml new file mode 100644 index 0000000..07ab307 --- /dev/null +++ b/crowdin.yml @@ -0,0 +1,36 @@ +commit_message: '[skip ci]' +project_id_env: CROWDIN_PROJECT_ID +api_token_env: CROWDIN_PERSONAL_TOKEN +files: + - source: /resources/lang/en/*.php + translation: /resources/lang/%two_letters_code%/%original_file_name% + languages_mapping: + two_letters_code: + en-GB: en-GB + pt-BR: pt-BR + zh-CN: zh + zh-TW: zh-TW + - source: /resources/lang/vendor/confirmation/en/*.php + translation: /resources/lang/vendor/confirmation/%two_letters_code%/%original_file_name% + languages_mapping: + two_letters_code: + en-GB: en-GB + pt-BR: pt-BR + zh-CN: zh + zh-TW: zh-TW + - source: /resources/lang/vendor/webauthn/en/*.php + translation: /resources/lang/vendor/webauthn/%two_letters_code%/%original_file_name% + languages_mapping: + two_letters_code: + en-GB: en-GB + pt-BR: pt-BR + zh-CN: zh + zh-TW: zh-TW + - source: /resources/lang/en.json + translation: /resources/lang/%two_letters_code%.json + languages_mapping: + two_letters_code: + en-GB: en-GB + pt-BR: pt-BR + zh-CN: zh + zh-TW: zh-TW diff --git a/cypress.json b/cypress.json new file mode 100644 index 0000000..84e5616 --- /dev/null +++ b/cypress.json @@ -0,0 +1,16 @@ +{ + "baseUrl": "http://localhost:8000", + "videosFolder": "tests/cypress/videos", + "screenshotsFolder": "tests/cypress/screenshots", + "supportFile": "tests/cypress/support/index.js", + "fixturesFolder": "tests/cypress/fixtures", + "integrationFolder": "tests/cypress/integration", + "pluginsFile": "tests/cypress/plugins/index.js", + "video": false, + "reporter": "junit", + "reporterOptions": { + "mochaFile": "results/junit/cypress/results-[hash].xml", + "toConsole": true + }, + "projectId": "q8h6k9" +} diff --git a/database/.gitignore b/database/.gitignore new file mode 100644 index 0000000..9b1dffd --- /dev/null +++ b/database/.gitignore @@ -0,0 +1 @@ +*.sqlite diff --git a/database/factories/Account/AddressBookFactory.php b/database/factories/Account/AddressBookFactory.php new file mode 100644 index 0000000..3ef8ac9 --- /dev/null +++ b/database/factories/Account/AddressBookFactory.php @@ -0,0 +1,37 @@ + + */ + protected $model = AddressBook::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition() + { + return [ + 'account_id' => factory(Account::class)->create(), + 'user_id' => function (array $attributes) { + return factory(User::class)->create([ + 'account_id' => $attributes['account_id'], + ]); + }, + 'name' => 'contacts1', + 'description' => $this->faker->sentence, + ]; + } +} diff --git a/database/factories/Account/AddressBookSubscriptionFactory.php b/database/factories/Account/AddressBookSubscriptionFactory.php new file mode 100644 index 0000000..419d339 --- /dev/null +++ b/database/factories/Account/AddressBookSubscriptionFactory.php @@ -0,0 +1,56 @@ + + */ + protected $model = AddressBookSubscription::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition() + { + return [ + 'account_id' => factory(Account::class)->create(), + 'user_id' => function (array $attributes) { + return factory(User::class)->create([ + 'account_id' => $attributes['account_id'], + ]); + }, + 'address_book_id' => function (array $attributes) { + return AddressBook::factory()->create([ + 'account_id' => $attributes['account_id'], + 'user_id' => $attributes['user_id'], + ]); + }, + 'name' => $this->faker->word, + 'uri' => $this->faker->url, + 'capabilities' => [ + 'addressbookMultiget' => true, + 'addressbookQuery' => true, + 'syncCollection' => true, + 'addressData' => [ + 'content-type' => 'text/vcard', + 'version' => '4.0', + ], + ], + 'username' => $this->faker->email, + 'password' => 'password', + 'syncToken' => '"test"', + ]; + } +} diff --git a/database/factories/Account/ExportJobFactory.php b/database/factories/Account/ExportJobFactory.php new file mode 100644 index 0000000..bc1fe82 --- /dev/null +++ b/database/factories/Account/ExportJobFactory.php @@ -0,0 +1,36 @@ + + */ + protected $model = ExportJob::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition() + { + return [ + 'account_id' => factory(Account::class)->create(), + 'user_id' => function (array $attributes) { + return factory(User::class)->create([ + 'account_id' => $attributes['account_id'], + ]); + }, + 'type' => 'json', + ]; + } +} diff --git a/database/factories/AccountFactory.php b/database/factories/AccountFactory.php new file mode 100644 index 0000000..197a010 --- /dev/null +++ b/database/factories/AccountFactory.php @@ -0,0 +1,156 @@ +define(App\Models\Account\Account::class, function (Faker\Generator $faker) { + return [ + 'api_key' => Str::random(30), + ]; +}); + +$factory->define(App\Models\Account\Activity::class, function (Faker\Generator $faker) { + return [ + 'account_id' => factory(App\Models\Account\Account::class)->create()->id, + 'activity_type_id' => function (array $data) { + return factory(App\Models\Account\ActivityType::class)->create([ + 'account_id' => $data['account_id'], + ])->id; + }, + 'description' => $faker->sentence, + 'summary' => $faker->sentence, + 'happened_at' => \App\Helpers\DateHelper::parseDateTime($faker->dateTimeThisCentury()), + ]; +}); + +$factory->define(App\Models\Account\ActivityType::class, function (Faker\Generator $faker) { + return [ + 'account_id' => factory(App\Models\Account\Account::class)->create()->id, + 'activity_type_category_id' => function (array $data) { + return factory(App\Models\Account\ActivityTypeCategory::class)->create([ + 'account_id' => $data['account_id'], + ])->id; + }, + 'translation_key' => $faker->sentence, + 'location_type' => $faker->word, + ]; +}); + +$factory->define(App\Models\Account\ActivityTypeCategory::class, function (Faker\Generator $faker) { + return [ + 'account_id' => factory(App\Models\Account\Account::class)->create()->id, + 'translation_key' => $faker->sentence, + 'name' => $faker->sentence, + ]; +}); + +$factory->define(App\Models\Account\Company::class, function (Faker\Generator $faker) { + return [ + 'account_id' => factory(App\Models\Account\Account::class)->create()->id, + 'name' => 'Central Perk', + 'website' => 'https://centralperk.com', + 'number_of_employees' => 4, + ]; +}); + +$factory->define(App\Models\Account\ImportJob::class, function (Faker\Generator $faker) { + return [ + 'account_id' => factory(App\Models\Account\Account::class)->create()->id, + 'user_id' => function (array $data) { + return factory(App\Models\User\User::class)->create([ + 'account_id' => $data['account_id'], + ])->id; + }, + ]; +}); + +$factory->define(App\Models\Account\ImportJobReport::class, function (Faker\Generator $faker) { + return []; +}); + +$factory->define(App\Models\Account\Invitation::class, function (Faker\Generator $faker) { + return [ + 'account_id' => factory(App\Models\Account\Account::class)->create()->id, + 'invited_by_user_id' => function (array $data) { + return factory(App\Models\User\User::class)->create([ + 'account_id' => $data['account_id'], + ])->id; + }, + 'invitation_key' => Str::random(100), + ]; +}); + +$factory->define(App\Models\Account\Photo::class, function (Faker\Generator $faker) { + $account = factory(App\Models\Account\Account::class)->create(); + + return [ + 'account_id' => $account->id, + 'original_filename' => 'file.jpg', + 'new_filename' => 'file.jpg', + ]; +}); + +$factory->define(App\Models\Account\Place::class, function (Faker\Generator $faker) { + return [ + 'account_id' => factory(App\Models\Account\Account::class)->create()->id, + 'country' => 'US', + 'street' => '12', + 'city' => 'beverly hills', + 'province' => null, + 'postal_code' => '90210', + ]; +}); + +$factory->define(App\Models\Account\Weather::class, function (Faker\Generator $faker) { + return [ + 'account_id' => factory(App\Models\Account\Account::class)->create()->id, + 'place_id' => function (array $data) { + return factory(App\Models\Account\Place::class)->create([ + 'account_id' => $data['account_id'], + ])->id; + }, + 'weather_json' => json_decode(' + { + "location": { + "name": "Le Pre-Saint-Gervais", + "region": "Ile-de-France", + "country": "France", + "lat": 48.89, + "lon": 2.39, + "tz_id": "Europe\/Paris", + "localtime_epoch": 1635605324, + "localtime": "2021-10-30 16:48" + }, + "current": { + "last_updated_epoch": 1635605100, + "last_updated": "2021-10-30 16:45", + "temp_c": 13, + "temp_f": 55.4, + "is_day": 0, + "condition": { + "text": "Partly cloudy", + "icon": "\/\/cdn.weatherapi.com\/weather\/64x64\/night\/116.png", + "code": 1003 + }, + "wind_mph": 5.6, + "wind_kph": 9, + "wind_degree": 210, + "wind_dir": "SSW", + "pressure_mb": 1001, + "pressure_in": 29.56, + "precip_mm": 0, + "precip_in": 0, + "humidity": 94, + "cloud": 75, + "feelslike_c": 11.2, + "feelslike_f": 52.1, + "vis_km": 10, + "vis_miles": 6, + "uv": 4, + "gust_mph": 17.9, + "gust_kph": 28.8 + } + }'), + 'created_at' => now(), + ]; +}); diff --git a/database/factories/ContactFactory.php b/database/factories/ContactFactory.php new file mode 100644 index 0000000..d8dea9f --- /dev/null +++ b/database/factories/ContactFactory.php @@ -0,0 +1,322 @@ +define(App\Models\Contact\Contact::class, function (Faker\Generator $faker) { + return [ + 'account_id' => factory(App\Models\Account\Account::class)->create()->id, + 'first_name' => 'John', + 'last_name' => 'Doe', + 'has_avatar' => false, + 'gender_id' => function (array $data) { + return factory(App\Models\Contact\Gender::class)->create([ + 'account_id' => $data['account_id'], + ])->id; + }, + 'uuid' => Str::uuid(), + 'default_avatar_color' => '#ffffff', + 'avatar_default_url' => 'avatars/img.png', + ]; +}); + +$factory->state(App\Models\Contact\Contact::class, 'partial', [ + 'is_partial' => 1, +]); + +$factory->state(App\Models\Contact\Contact::class, 'archived', [ + 'is_active' => 0, +]); + +$factory->state(App\Models\Contact\Contact::class, 'named', function (Faker\Generator $faker) { + return [ + 'first_name' => $faker->firstName, + 'last_name' => $faker->lastName, + ]; +}); + +$factory->state(App\Models\Contact\Contact::class, 'no_gender', function (Faker\Generator $faker) { + return [ + 'gender_id' => null, + ]; +}); + +$factory->define(App\Models\Contact\ContactFieldType::class, function (Faker\Generator $faker) { + return [ + 'account_id' => factory(App\Models\Account\Account::class)->create()->id, + 'name' => 'Email', + 'protocol' => 'mailto:', + 'type' => 'email', + ]; +}); + +$factory->define(App\Models\Contact\ContactField::class, function (Faker\Generator $faker) { + return [ + 'account_id' => factory(App\Models\Account\Account::class)->create()->id, + 'contact_id' => function (array $data) { + return factory(App\Models\Contact\Contact::class)->create([ + 'account_id' => $data['account_id'], + ])->id; + }, + 'contact_field_type_id' => function (array $data) { + return factory(App\Models\Contact\ContactFieldType::class)->create([ + 'account_id' => $data['account_id'], + ])->id; + }, + 'data' => 'john@doe.com', + ]; +}); + +$factory->define(App\Models\Contact\ContactFieldLabel::class, function (Faker\Generator $faker) { + return [ + 'account_id' => factory(App\Models\Account\Account::class)->create()->id, + 'label_i18n' => 'work', + ]; +}); + +$factory->define(App\Models\Contact\Conversation::class, function (Faker\Generator $faker) { + return [ + 'account_id' => factory(App\Models\Account\Account::class)->create()->id, + 'contact_id' => function (array $data) { + return factory(App\Models\Contact\Contact::class)->create([ + 'account_id' => $data['account_id'], + ])->id; + }, + 'contact_field_type_id' => function (array $data) { + return factory(App\Models\Contact\ContactFieldType::class)->create([ + 'account_id' => $data['account_id'], + ])->id; + }, + ]; +}); + +$factory->define(App\Models\Contact\Reminder::class, function (Faker\Generator $faker) { + return [ + 'account_id' => factory(App\Models\Account\Account::class)->create()->id, + 'contact_id' => function (array $data) { + return factory(App\Models\Contact\Contact::class)->create([ + 'account_id' => $data['account_id'], + ])->id; + }, + ]; +}); + +$factory->define(App\Models\Contact\ReminderOutbox::class, function (Faker\Generator $faker) { + return [ + 'account_id' => factory(App\Models\Account\Account::class)->create()->id, + 'user_id' => function (array $data) { + return factory(App\Models\User\User::class)->create([ + 'account_id' => $data['account_id'], + ])->id; + }, + 'reminder_id' => function (array $data) { + return factory(App\Models\Contact\Reminder::class)->create([ + 'account_id' => $data['account_id'], + ])->id; + }, + 'planned_date' => \App\Helpers\DateHelper::parseDateTime($faker->dateTimeThisCentury()), + ]; +}); + +$factory->define(App\Models\Contact\Gift::class, function (Faker\Generator $faker) { + return [ + 'account_id' => factory(App\Models\Account\Account::class)->create()->id, + 'contact_id' => function (array $data) { + return factory(App\Models\Contact\Contact::class)->create([ + 'account_id' => $data['account_id'], + ])->id; + }, + 'status' => 'idea', + 'created_at' => \App\Helpers\DateHelper::parseDateTime($faker->dateTimeThisCentury()), + ]; +}); + +$factory->define(App\Models\Contact\Call::class, function (Faker\Generator $faker) { + return [ + 'account_id' => factory(App\Models\Account\Account::class)->create()->id, + 'contact_id' => function (array $data) { + return factory(App\Models\Contact\Contact::class)->create([ + 'account_id' => $data['account_id'], + ])->id; + }, + 'created_at' => \App\Helpers\DateHelper::parseDateTime($faker->dateTimeThisCentury()), + ]; +}); + +$factory->define(App\Models\Contact\Task::class, function (Faker\Generator $faker) { + return [ + 'account_id' => factory(App\Models\Account\Account::class)->create()->id, + 'contact_id' => function (array $data) { + return factory(App\Models\Contact\Contact::class)->create([ + 'account_id' => $data['account_id'], + ])->id; + }, + 'title' => $faker->word, + 'description' => $faker->word, + 'completed' => 0, + 'created_at' => \App\Helpers\DateHelper::parseDateTime($faker->dateTimeThisCentury()), + 'uuid' => Str::uuid(), + ]; +}); + +$factory->define(App\Models\Contact\Note::class, function (Faker\Generator $faker) { + return [ + 'account_id' => factory(App\Models\Account\Account::class)->create()->id, + 'contact_id' => function (array $data) { + return factory(App\Models\Contact\Contact::class)->create([ + 'account_id' => $data['account_id'], + ])->id; + }, + 'body' => encrypt($faker->text(200)), + ]; +}); + +$factory->define(App\Models\Contact\Address::class, function (Faker\Generator $faker) { + return [ + 'account_id' => factory(App\Models\Account\Account::class)->create()->id, + 'contact_id' => function (array $data) { + return factory(App\Models\Contact\Contact::class)->create([ + 'account_id' => $data['account_id'], + ])->id; + }, + 'place_id' => function (array $data) { + return factory(App\Models\Account\Place::class)->create([ + 'account_id' => $data['account_id'], + ])->id; + }, + ]; +}); + +$factory->define(App\Models\Contact\Gender::class, function (Faker\Generator $faker) { + return [ + 'account_id' => factory(App\Models\Account\Account::class)->create()->id, + 'type' => 'M', + 'name' => 'Man', + ]; +}); + +$factory->define(App\Models\Contact\Debt::class, function (Faker\Generator $faker) { + return [ + 'account_id' => factory(App\Models\Account\Account::class)->create()->id, + 'contact_id' => function (array $data) { + return factory(App\Models\Contact\Contact::class)->create([ + 'account_id' => $data['account_id'], + ])->id; + }, + ]; +}); + +$factory->define(App\Models\Contact\Tag::class, function (Faker\Generator $faker) { + return [ + 'account_id' => factory(App\Models\Account\Account::class)->create()->id, + 'name' => $faker->word, + 'name_slug' => Str::slug($faker->word), + ]; +}); + +$factory->define(App\Models\Contact\Pet::class, function (Faker\Generator $faker) { + return [ + 'account_id' => factory(App\Models\Account\Account::class)->create()->id, + 'contact_id' => function (array $data) { + return factory(App\Models\Contact\Contact::class)->create([ + 'account_id' => $data['account_id'], + ])->id; + }, + 'pet_category_id' => factory(App\Models\Contact\PetCategory::class)->create()->id, + ]; +}); + +$factory->define(App\Models\Contact\PetCategory::class, function (Faker\Generator $faker) { + return []; +}); + +$factory->define(App\Models\Contact\ReminderRule::class, function (Faker\Generator $faker) { + return [ + 'account_id' => factory(App\Models\Account\Account::class)->create()->id, + ]; +}); + +$factory->define(App\Models\Contact\Message::class, function (Faker\Generator $faker) { + return [ + 'account_id' => factory(App\Models\Account\Account::class)->create()->id, + 'contact_id' => function (array $data) { + return factory(App\Models\Contact\Contact::class)->create([ + 'account_id' => $data['account_id'], + ])->id; + }, + 'conversation_id' => function (array $data) { + return factory(App\Models\Contact\Conversation::class)->create([ + 'account_id' => $data['account_id'], + ])->id; + }, + ]; +}); + +$factory->define(App\Models\Contact\Document::class, function (Faker\Generator $faker) { + $contact = factory(App\Models\Contact\Contact::class)->create(); + + return [ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'original_filename' => 'file.jpg', + 'new_filename' => 'file.jpg', + ]; +}); + +$factory->define(App\Models\Contact\Occupation::class, function (Faker\Generator $faker) { + return [ + 'account_id' => factory(App\Models\Account\Account::class)->create()->id, + 'contact_id' => function (array $data) { + return factory(App\Models\Contact\Contact::class)->create([ + 'account_id' => $data['account_id'], + ])->id; + }, + 'company_id' => function (array $data) { + return factory(App\Models\Account\Company::class)->create([ + 'account_id' => $data['account_id'], + ])->id; + }, + 'title' => 'Waiter', + 'salary' => '10000', + 'salary_unit' => 'year', + ]; +}); + +$factory->define(App\Models\Contact\LifeEventCategory::class, function (Faker\Generator $faker) { + return [ + 'account_id' => factory(App\Models\Account\Account::class)->create()->id, + 'name' => $faker->text(100), + 'core_monica_data' => true, + ]; +}); + +$factory->define(App\Models\Contact\LifeEventType::class, function (Faker\Generator $faker) { + return [ + 'account_id' => factory(App\Models\Account\Account::class)->create()->id, + 'life_event_category_id' => function (array $data) { + return factory(App\Models\Contact\LifeEventCategory::class)->create([ + 'account_id' => $data['account_id'], + ])->id; + }, + 'name' => $faker->text(100), + 'core_monica_data' => true, + ]; +}); + +$factory->define(App\Models\Contact\LifeEvent::class, function (Faker\Generator $faker) { + return [ + 'account_id' => factory(App\Models\Account\Account::class)->create()->id, + 'contact_id' => function (array $data) { + return factory(App\Models\Contact\Contact::class)->create([ + 'account_id' => $data['account_id'], + ])->id; + }, + 'life_event_type_id' => function (array $data) { + return factory(App\Models\Contact\LifeEventType::class)->create([ + 'account_id' => $data['account_id'], + ])->id; + }, + 'name' => $faker->text(100), + 'note' => $faker->text(100), + 'happened_at' => \App\Helpers\DateHelper::parseDateTime($faker->dateTimeThisCentury()), + ]; +}); diff --git a/database/factories/InstanceFactory.php b/database/factories/InstanceFactory.php new file mode 100644 index 0000000..daffc27 --- /dev/null +++ b/database/factories/InstanceFactory.php @@ -0,0 +1,69 @@ +define(\App\Models\Instance\Cron::class, function (Faker\Generator $faker) { + return [ + 'command' => $faker->word, + 'last_run' => now(), + ]; +}); + +$factory->define(App\Models\Instance\SpecialDate::class, function (Faker\Generator $faker) { + return [ + 'account_id' => factory(App\Models\Account\Account::class)->create()->id, + 'contact_id' => function (array $data) { + return factory(App\Models\Contact\Contact::class)->create([ + 'account_id' => $data['account_id'], + ])->id; + }, + 'date' => \App\Helpers\DateHelper::parseDateTime($faker->dateTimeThisCentury()), + 'created_at' => \App\Helpers\DateHelper::parseDateTime($faker->dateTimeThisCentury()), + ]; +}); + +$factory->define(App\Models\Instance\Instance::class, function (Faker\Generator $faker) { + return [ + 'uuid' => $faker->uuid, + 'latest_version' => '1.0.0', + 'current_version' => '1.0.0', + ]; +}); + +$factory->define(App\Models\Instance\Emotion\Emotion::class, function (Faker\Generator $faker) { + return [ + 'emotion_primary_id' => factory(App\Models\Instance\Emotion\PrimaryEmotion::class)->create()->id, + 'emotion_secondary_id' => function (array $data) { + return factory(App\Models\Instance\Emotion\SecondaryEmotion::class)->create([ + 'emotion_primary_id' => $data['emotion_primary_id'], + ])->id; + }, + 'name' => $faker->text(5), + ]; +}); + +$factory->define(App\Models\Instance\Emotion\SecondaryEmotion::class, function (Faker\Generator $faker) { + return [ + 'emotion_primary_id' => factory(App\Models\Instance\Emotion\PrimaryEmotion::class)->create()->id, + 'name' => $faker->text(5), + ]; +}); + +$factory->define(App\Models\Instance\Emotion\PrimaryEmotion::class, function (Faker\Generator $faker) { + return [ + 'name' => $faker->text(5), + ]; +}); + +$factory->define(App\Models\Instance\AuditLog::class, function (Faker\Generator $faker) { + return [ + 'account_id' => factory(App\Models\Account\Account::class)->create()->id, + 'author_id' => function (array $data) { + return factory(App\Models\User\User::class)->create([ + 'account_id' => $data['account_id'], + ])->id; + }, + 'action' => 'account_created', + 'author_name' => 'Dwight Schrute', + 'audited_at' => $faker->dateTimeThisCentury(), + 'objects' => '{"user": 1}', + ]; +}); diff --git a/database/factories/JournalFactory.php b/database/factories/JournalFactory.php new file mode 100644 index 0000000..0cca819 --- /dev/null +++ b/database/factories/JournalFactory.php @@ -0,0 +1,19 @@ +define(App\Models\Journal\Entry::class, function (Faker\Generator $faker) { + return [ + 'account_id' => factory(App\Models\Account\Account::class)->create()->id, + ]; +}); + +$factory->define(App\Models\Journal\Day::class, function (Faker\Generator $faker) { + return [ + 'account_id' => factory(App\Models\Account\Account::class)->create()->id, + ]; +}); + +$factory->define(App\Models\Journal\JournalEntry::class, function (Faker\Generator $faker) { + return [ + 'account_id' => factory(App\Models\Account\Account::class)->create()->id, + ]; +}); diff --git a/database/factories/RelationshipFactory.php b/database/factories/RelationshipFactory.php new file mode 100644 index 0000000..a725ac0 --- /dev/null +++ b/database/factories/RelationshipFactory.php @@ -0,0 +1,39 @@ +define(App\Models\Relationship\Relationship::class, function (Faker\Generator $faker) { + return [ + 'account_id' => factory(App\Models\Account\Account::class)->create()->id, + 'relationship_type_id' => function (array $data) { + return factory(App\Models\Relationship\RelationshipType::class)->create([ + 'account_id' => $data['account_id'], + ])->id; + }, + 'contact_is' => function (array $data) { + return factory(App\Models\Contact\Contact::class)->create([ + 'account_id' => $data['account_id'], + ])->id; + }, + 'of_contact' => function (array $data) { + return factory(App\Models\Contact\Contact::class)->create([ + 'account_id' => $data['account_id'], + ])->id; + }, + ]; +}); + +$factory->define(App\Models\Relationship\RelationshipType::class, function (Faker\Generator $faker) { + return [ + 'account_id' => factory(App\Models\Account\Account::class)->create()->id, + 'relationship_type_group_id' => function (array $data) { + return factory(App\Models\Relationship\RelationshipTypeGroup::class)->create([ + 'account_id' => $data['account_id'], + ])->id; + }, + ]; +}); + +$factory->define(App\Models\Relationship\RelationshipTypeGroup::class, function (Faker\Generator $faker) { + return [ + 'account_id' => factory(App\Models\Account\Account::class)->create()->id, + ]; +}); diff --git a/database/factories/SettingsFactory.php b/database/factories/SettingsFactory.php new file mode 100644 index 0000000..4701c28 --- /dev/null +++ b/database/factories/SettingsFactory.php @@ -0,0 +1,29 @@ +define(App\Models\Settings\Term::class, function (Faker\Generator $faker) { + return [ + 'term_version' => $faker->realText(50), + 'term_content' => $faker->realText(50), + 'privacy_version' => $faker->realText(50), + 'privacy_content' => $faker->realText(50), + ]; +}); + +$factory->define(App\Models\Settings\Currency::class, function (Faker\Generator $faker) { + return [ + 'iso' => $faker->realText(10), + 'name' => $faker->realText(10), + 'symbol' => $faker->realText(10), + ]; +}); + +$factory->define(\Laravel\Cashier\Subscription::class, function (Faker\Generator $faker) { + return [ + 'account_id' => factory(App\Models\Account\Account::class)->create()->id, + 'name' => $faker->word(), + 'stripe_id' => $faker->word(), + 'stripe_price' => $faker->randomElement(['plan-1', 'plan-2', 'plan-3']), + 'quantity' => 1, + 'created_at' => now(), + ]; +}); diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php new file mode 100644 index 0000000..dfdef13 --- /dev/null +++ b/database/factories/UserFactory.php @@ -0,0 +1,45 @@ +define(App\Models\User\User::class, function (Faker\Generator $faker) { + return [ + 'account_id' => factory(App\Models\Account\Account::class)->create()->id, + 'first_name' => $faker->firstName, + 'last_name' => $faker->lastName, + 'email' => $faker->unique()->safeEmail, + 'email_verified_at' => \App\Helpers\DateHelper::parseDateTime($faker->dateTimeThisCentury()), + 'password' => bcrypt(Str::random(10)), + 'remember_token' => Str::random(10), + 'timezone' => config('app.timezone'), + 'name_order' => 'firstname_lastname', + 'locale' => 'en', + 'currency_id' => function (array $data) { + return factory(App\Models\Settings\Currency::class)->create([ + 'iso' => 'USD', + ])->id; + }, + ]; +}); + +$factory->define(App\Models\User\Changelog::class, function (Faker\Generator $faker) { + return []; +}); + +$factory->define(App\Models\User\Module::class, function (Faker\Generator $faker) { + return [ + 'account_id' => factory(App\Models\Account\Account::class)->create()->id, + ]; +}); + +$factory->define(App\Models\User\SyncToken::class, function (Faker\Generator $faker) { + return [ + 'account_id' => factory(App\Models\Account\Account::class)->create()->id, + 'user_id' => function (array $data) { + return factory(App\Models\User\User::class)->create([ + 'account_id' => $data['account_id'], + ])->id; + }, + 'timestamp' => \App\Helpers\DateHelper::parseDateTime($faker->dateTimeThisCentury()), + ]; +}); diff --git a/database/factories/WebauthnFactory.php b/database/factories/WebauthnFactory.php new file mode 100644 index 0000000..a7d63dc --- /dev/null +++ b/database/factories/WebauthnFactory.php @@ -0,0 +1,27 @@ +define(\LaravelWebauthn\Models\WebauthnKey::class, function (Faker\Generator $faker) { + return [ + 'user_id' => '0', + 'name' => $faker->word, + 'counter' => 0, + 'credentialId' => 'MA==', + 'type' => 'public-key', + 'transports' => [], + 'attestationType' => 'none', + 'trustPath' => new \Webauthn\TrustPath\EmptyTrustPath, + 'aaguid' => '0000000000000000', + 'credentialPublicKey' => 'oWNrZXlldmFsdWU=', + ]; +}); diff --git a/database/migrations/.gitkeep b/database/migrations/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/database/migrations/.gitkeep @@ -0,0 +1 @@ + diff --git a/database/migrations/2014_10_12_000000_create_users_table.php b/database/migrations/2014_10_12_000000_create_users_table.php new file mode 100644 index 0000000..4ea8de5 --- /dev/null +++ b/database/migrations/2014_10_12_000000_create_users_table.php @@ -0,0 +1,37 @@ +increments('id'); + $table->string('first_name'); + $table->string('last_name'); + $table->enum('gender', ['male', 'female']); + $table->string('email')->unique(); + $table->string('password'); + $table->rememberToken(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::drop('users'); + } +} diff --git a/database/migrations/2014_10_12_100000_create_password_resets_table.php b/database/migrations/2014_10_12_100000_create_password_resets_table.php new file mode 100644 index 0000000..cc91fcc --- /dev/null +++ b/database/migrations/2014_10_12_100000_create_password_resets_table.php @@ -0,0 +1,32 @@ +string('email')->index(); + $table->string('token')->index(); + $table->timestamp('created_at'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::drop('password_resets'); + } +} diff --git a/database/migrations/2016_06_01_000001_create_oauth_auth_codes_table.php b/database/migrations/2016_06_01_000001_create_oauth_auth_codes_table.php new file mode 100644 index 0000000..e20203f --- /dev/null +++ b/database/migrations/2016_06_01_000001_create_oauth_auth_codes_table.php @@ -0,0 +1,35 @@ +string('id', 100)->primary(); + $table->unsignedBigInteger('user_id')->index(); + $table->unsignedBigInteger('client_id'); + $table->text('scopes')->nullable(); + $table->boolean('revoked'); + $table->dateTime('expires_at')->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('oauth_auth_codes'); + } +} diff --git a/database/migrations/2016_06_01_000002_create_oauth_access_tokens_table.php b/database/migrations/2016_06_01_000002_create_oauth_access_tokens_table.php new file mode 100644 index 0000000..232e7ce --- /dev/null +++ b/database/migrations/2016_06_01_000002_create_oauth_access_tokens_table.php @@ -0,0 +1,37 @@ +string('id', 100)->primary(); + $table->unsignedBigInteger('user_id')->nullable()->index(); + $table->unsignedBigInteger('client_id'); + $table->string('name')->nullable(); + $table->text('scopes')->nullable(); + $table->boolean('revoked'); + $table->timestamps(); + $table->dateTime('expires_at')->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('oauth_access_tokens'); + } +} diff --git a/database/migrations/2016_06_01_000003_create_oauth_refresh_tokens_table.php b/database/migrations/2016_06_01_000003_create_oauth_refresh_tokens_table.php new file mode 100644 index 0000000..f26abb1 --- /dev/null +++ b/database/migrations/2016_06_01_000003_create_oauth_refresh_tokens_table.php @@ -0,0 +1,33 @@ +string('id', 100)->primary(); + $table->string('access_token_id', 100)->index(); + $table->boolean('revoked'); + $table->dateTime('expires_at')->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('oauth_refresh_tokens'); + } +} diff --git a/database/migrations/2016_06_01_000004_create_oauth_clients_table.php b/database/migrations/2016_06_01_000004_create_oauth_clients_table.php new file mode 100644 index 0000000..3fb8c90 --- /dev/null +++ b/database/migrations/2016_06_01_000004_create_oauth_clients_table.php @@ -0,0 +1,39 @@ +bigIncrements('id'); + $table->unsignedBigInteger('user_id')->nullable()->index(); + $table->string('name'); + $table->string('secret', 100)->nullable(); + $table->string('provider')->nullable(); + $table->text('redirect'); + $table->boolean('personal_access_client'); + $table->boolean('password_client'); + $table->boolean('revoked'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('oauth_clients'); + } +} diff --git a/database/migrations/2016_06_01_000005_create_oauth_personal_access_clients_table.php b/database/migrations/2016_06_01_000005_create_oauth_personal_access_clients_table.php new file mode 100644 index 0000000..f37ea5e --- /dev/null +++ b/database/migrations/2016_06_01_000005_create_oauth_personal_access_clients_table.php @@ -0,0 +1,32 @@ +bigIncrements('id'); + $table->unsignedBigInteger('client_id'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('oauth_personal_access_clients'); + } +} diff --git a/database/migrations/2016_06_07_234741_create_account_table.php b/database/migrations/2016_06_07_234741_create_account_table.php new file mode 100644 index 0000000..4e93008 --- /dev/null +++ b/database/migrations/2016_06_07_234741_create_account_table.php @@ -0,0 +1,32 @@ +increments('id'); + $table->string('api_key'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::drop('accounts'); + } +} diff --git a/database/migrations/2016_06_08_003006_add_account_info_table.php b/database/migrations/2016_06_08_003006_add_account_info_table.php new file mode 100644 index 0000000..93ca727 --- /dev/null +++ b/database/migrations/2016_06_08_003006_add_account_info_table.php @@ -0,0 +1,37 @@ +integer('account_id')->after('remember_token'); + $table->string('send_sms_alert')->default('false')->after('account_id'); + $table->integer('phone_number')->nullable()->after('send_sms_alert'); + $table->integer('amazon_store_country_id')->nullable()->after('phone_number'); + $table->string('timezone')->nullable()->after('amazon_store_country_id'); + $table->string('locale')->default('en')->after('timezone'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('users', function ($table) { + $table->dropColumn(['account_id', 'send_sms_alert', 'phone_number', 'amazon_store_country_id']); + }); + } +} diff --git a/database/migrations/2016_06_08_005413_create_contacts_table.php b/database/migrations/2016_06_08_005413_create_contacts_table.php new file mode 100644 index 0000000..388c7ff --- /dev/null +++ b/database/migrations/2016_06_08_005413_create_contacts_table.php @@ -0,0 +1,135 @@ +increments('id'); + $table->integer('account_id'); + $table->integer('entity_id')->nullable(); + $table->enum('status', ['adult', 'parent', 'kid']); + $table->string('first_name'); + $table->string('middle_name')->nullable(); + $table->string('last_name')->nullable(); + $table->string('surname')->nullable(); + $table->enum('gender', ['male', 'female']); + $table->enum('nature_of_relationship', ['friend', 'family', 'friend_of_friend', 'business'])->nullable(); + $table->enum('couple_status', ['married', 'engaged', 'complicated', 'dates', 'single'])->nullable(); + $table->string('is_birthdate_approximate')->default('false')->nullable(); + $table->dateTime('birthdate')->nullable(); + $table->string('warned_about_birthdate')->default('true'); + $table->string('email')->unique()->nullable(); + $table->string('phone_number')->nullable(); + $table->string('twitter_id')->nullable(); + $table->string('instagram_id')->nullable(); + $table->string('is_first_met_date_approximate')->default('false')->nullable(); + $table->dateTime('first_met')->nullable(); + $table->string('first_met_where')->nullable(); + $table->longText('first_met_additional_info')->nullable(); + $table->string('job')->nullable(); + $table->dateTime('last_talked_to')->nullable(); + $table->string('street')->nullable(); + $table->string('city')->nullable(); + $table->string('province')->nullable(); + $table->string('postal_code')->nullable(); + $table->integer('country_id')->nullable(); + $table->longText('food_preferencies')->nullable(); + $table->string('has_kids')->default('false')->nullable(); + $table->integer('first_parent_id')->nullable; + $table->integer('second_parent_id')->nullable; + $table->dateTime('viewed_at')->nullable(); + $table->softDeletes(); + $table->timestamps(); + }); + + Schema::create('kids', function (Blueprint $table) { + $table->increments('id'); + $table->integer('account_id'); + $table->integer('contact_id_first_parent'); + $table->integer('contact_id_second_parent')->nullable(); + $table->string('was_part_of_entity_id'); + $table->timestamps(); + }); + + Schema::create('entities', function (Blueprint $table) { + $table->increments('id'); + $table->integer('account_id'); + $table->string('name'); + $table->softDeletes(); + $table->timestamps(); + }); + + Schema::create('important_dates', function (Blueprint $table) { + $table->increments('id'); + $table->integer('account_id'); + $table->enum('type', ['entity', 'contact']); + $table->integer('contact_id'); + $table->dateTime('date_to_remember'); + $table->string('description'); + $table->timestamps(); + }); + + Schema::create('gifts', function (Blueprint $table) { + $table->increments('id'); + $table->integer('account_id'); + $table->string('people_id'); + $table->enum('type_of_people', ['parent', 'kid']); + $table->enum('nature', ['amazon', 'other']); + $table->integer('amazon_gift_id')->nullable(); + $table->string('occasion'); + $table->string('giving_date')->nullable(); + $table->timestamps(); + }); + + Schema::create('countries', function (Blueprint $table) { + $table->increments('id'); + $table->string('iso'); + $table->string('country'); + }); + + Schema::create('peoples', function (Blueprint $table) { + $table->increments('id'); + $table->string('api_id'); + $table->integer('account_id'); + $table->enum('type', ['entity', 'contact']); + $table->integer('object_id'); + $table->dateTime('viewed_at')->nullable(); + $table->softDeletes(); + $table->timestamps(); + }); + + Schema::create('reminders', function (Blueprint $table) { + $table->increments('id'); + $table->integer('account_id'); + $table->integer('people_id'); + $table->string('title')->nullable(); + $table->longText('description')->nullable(); + $table->string('frequency_type'); + $table->integer('frequency_number')->nullable(); + $table->dateTime('last_triggered')->nullable(); + $table->dateTime('next_expected_date'); + $table->softDeletes(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::drop('contacts', 'kids', 'important_dates', 'entities', 'gifts', 'note_object', 'notes', 'countries', 'peoples', 'reminders'); + } +} diff --git a/database/migrations/2016_06_25_224219_create_reminder_type_table.php b/database/migrations/2016_06_25_224219_create_reminder_type_table.php new file mode 100644 index 0000000..1183087 --- /dev/null +++ b/database/migrations/2016_06_25_224219_create_reminder_type_table.php @@ -0,0 +1,41 @@ +increments('id'); + $table->string('description'); + $table->string('translation_key'); + $table->timestamps(); + }); + + Schema::table('reminders', function (Blueprint $table) { + $table->integer('reminder_type_id')->after('people_id')->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::drop('reminder_types'); + + Schema::table('reminders', function ($table) { + $table->dropColumn(['reminder_type_id']); + }); + } +} diff --git a/database/migrations/2016_06_28_191025_create_tasks_table.php b/database/migrations/2016_06_28_191025_create_tasks_table.php new file mode 100644 index 0000000..2a17ea2 --- /dev/null +++ b/database/migrations/2016_06_28_191025_create_tasks_table.php @@ -0,0 +1,38 @@ +increments('id'); + $table->integer('account_id'); + $table->integer('people_id'); + $table->string('title'); + $table->longText('description')->nullable(); + $table->enum('status', ['completed', 'inprogress', 'archived']); + $table->dateTime('completed_at')->nullable(); + $table->softDeletes(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::drop('tasks'); + } +} diff --git a/database/migrations/2016_06_30_185050_create_notes_table.php b/database/migrations/2016_06_30_185050_create_notes_table.php new file mode 100644 index 0000000..8deca07 --- /dev/null +++ b/database/migrations/2016_06_30_185050_create_notes_table.php @@ -0,0 +1,42 @@ +increments('id'); + $table->integer('account_id'); + $table->integer('people_id'); + $table->enum('type', ['activity', 'phone_call', 'note', 'gift_idea']); + $table->string('title')->nullable(); + $table->mediumText('body'); + $table->integer('activity_type_id')->nullable(); + $table->dateTime('activity_date')->nullable(); + $table->string('sticky')->default('false'); + $table->string('remind_for_next_call_or_email')->default('false'); + $table->integer('author_id'); + $table->softDeletes(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::drop('notes'); + } +} diff --git a/database/migrations/2016_07_25_133835_add_width_field.php b/database/migrations/2016_07_25_133835_add_width_field.php new file mode 100644 index 0000000..f0704c9 --- /dev/null +++ b/database/migrations/2016_07_25_133835_add_width_field.php @@ -0,0 +1,32 @@ +string('fluid_container')->default('false')->after('locale'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('users', function ($table) { + $table->dropColumn('fluid_container'); + }); + } +} diff --git a/database/migrations/2016_08_28_122938_create_kids_table.php b/database/migrations/2016_08_28_122938_create_kids_table.php new file mode 100644 index 0000000..72d8225 --- /dev/null +++ b/database/migrations/2016_08_28_122938_create_kids_table.php @@ -0,0 +1,54 @@ +increments('id'); + $table->integer('account_id'); + $table->integer('child_of_people_id'); + $table->enum('gender', ['male', 'female']); + $table->string('first_name'); + $table->string('is_birthdate_approximate')->default('false')->nullable(); + $table->dateTime('birthdate')->nullable(); + $table->longText('food_preferencies')->nullable(); + $table->softDeletes(); + $table->timestamps(); + }); + + Schema::table('peoples', function ($table) { + $table->string('has_kids')->default('false')->after('object_id')->nullable(); + $table->integer('number_of_kids')->after('has_kids')->nullable(); + }); + + Schema::table('contacts', function ($table) { + $table->dropColumn(['first_parent_id', 'status', 'has_kids', 'warned_about_birthdate']); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::drop('kids'); + + Schema::table('peoples', function ($table) { + $table->dropColumn('has_kids'); + }); + } +} diff --git a/database/migrations/2016_08_28_215159_create_relations_table.php b/database/migrations/2016_08_28_215159_create_relations_table.php new file mode 100644 index 0000000..b937382 --- /dev/null +++ b/database/migrations/2016_08_28_215159_create_relations_table.php @@ -0,0 +1,43 @@ +dropColumn(['second_parent_id', 'couple_status']); + }); + + Schema::create('significant_others', function ($table) { + $table->increments('id'); + $table->integer('account_id'); + $table->integer('people_id'); + $table->enum('status', ['active', 'past']); + $table->string('first_name'); + $table->string('last_name')->nullable(); + $table->enum('gender', ['male', 'female']); + $table->string('is_birthdate_approximate')->default('false')->nullable(); + $table->dateTime('birthdate')->nullable(); + $table->softDeletes(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::drop('significant_others'); + } +} diff --git a/database/migrations/2016_09_03_202027_add_reminder_id_to_contacts.php b/database/migrations/2016_09_03_202027_add_reminder_id_to_contacts.php new file mode 100644 index 0000000..d3e1894 --- /dev/null +++ b/database/migrations/2016_09_03_202027_add_reminder_id_to_contacts.php @@ -0,0 +1,48 @@ +integer('birthday_reminder_id')->nullable()->after('birthdate'); + }); + + Schema::table('kids', function (Blueprint $table) { + $table->integer('birthday_reminder_id')->nullable()->after('birthdate'); + }); + + Schema::table('significant_others', function (Blueprint $table) { + $table->integer('birthday_reminder_id')->nullable()->after('birthdate'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('contacts', function ($table) { + $table->dropColumn('fluid_container'); + }); + + Schema::table('kids', function ($table) { + $table->dropColumn('fluid_container'); + }); + + Schema::table('significant_others', function ($table) { + $table->dropColumn('fluid_container'); + }); + } +} diff --git a/database/migrations/2016_09_05_134937_add_last_talked_to_field.php b/database/migrations/2016_09_05_134937_add_last_talked_to_field.php new file mode 100644 index 0000000..50f2caa --- /dev/null +++ b/database/migrations/2016_09_05_134937_add_last_talked_to_field.php @@ -0,0 +1,34 @@ +dropColumn('last_talked_to'); + }); + + Schema::table('peoples', function (Blueprint $table) { + $table->dateTime('last_talked_to')->nullable()->after('number_of_kids'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + // + } +} diff --git a/database/migrations/2016_09_05_135927_add_people_id_to_contacts.php b/database/migrations/2016_09_05_135927_add_people_id_to_contacts.php new file mode 100644 index 0000000..5bea0be --- /dev/null +++ b/database/migrations/2016_09_05_135927_add_people_id_to_contacts.php @@ -0,0 +1,30 @@ +integer('people_id')->nullable()->after('entity_id'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + // + } +} diff --git a/database/migrations/2016_09_05_145111_add_name_info_to_peoples.php b/database/migrations/2016_09_05_145111_add_name_info_to_peoples.php new file mode 100644 index 0000000..1a3a752 --- /dev/null +++ b/database/migrations/2016_09_05_145111_add_name_info_to_peoples.php @@ -0,0 +1,30 @@ +string('sortable_name')->nullable()->after('object_id'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + // + } +} diff --git a/database/migrations/2016_09_06_213550_create_activity_type_table.php b/database/migrations/2016_09_06_213550_create_activity_type_table.php new file mode 100644 index 0000000..e9cf78d --- /dev/null +++ b/database/migrations/2016_09_06_213550_create_activity_type_table.php @@ -0,0 +1,38 @@ +increments('id'); + $table->integer('activity_type_group_id'); + $table->string('key'); + $table->timestamps(); + }); + + Schema::create('activity_type_groups', function ($table) { + $table->increments('id'); + $table->string('key'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + // + } +} diff --git a/database/migrations/2016_09_10_164406_create_jobs_table.php b/database/migrations/2016_09_10_164406_create_jobs_table.php new file mode 100644 index 0000000..3f57234 --- /dev/null +++ b/database/migrations/2016_09_10_164406_create_jobs_table.php @@ -0,0 +1,37 @@ +bigIncrements('id'); + $table->string('queue'); + $table->longText('payload'); + $table->unsignedTinyInteger('attempts'); + $table->unsignedInteger('reserved_at')->nullable(); + $table->unsignedInteger('available_at'); + $table->unsignedInteger('created_at'); + $table->index(['queue', 'reserved_at']); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('jobs'); + } +} diff --git a/database/migrations/2016_09_10_170122_create_notifications_table.php b/database/migrations/2016_09_10_170122_create_notifications_table.php new file mode 100644 index 0000000..059cc30 --- /dev/null +++ b/database/migrations/2016_09_10_170122_create_notifications_table.php @@ -0,0 +1,37 @@ +increments('id'); + $table->integer('account_id'); + $table->integer('people_id'); + $table->enum('for', ['reminder']); + $table->enum('how', ['email']); + $table->string('address'); + $table->longText('content'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + // + } +} diff --git a/database/migrations/2016_09_12_014120_create_failed_jobs_table.php b/database/migrations/2016_09_12_014120_create_failed_jobs_table.php new file mode 100644 index 0000000..843b93d --- /dev/null +++ b/database/migrations/2016_09_12_014120_create_failed_jobs_table.php @@ -0,0 +1,35 @@ +increments('id'); + $table->text('connection'); + $table->text('queue'); + $table->longText('payload'); + $table->longText('exception'); + $table->timestamp('failed_at')->useCurrent(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('failed_jobs'); + } +} diff --git a/database/migrations/2016_09_30_014720_add_kid_to_reminder.php b/database/migrations/2016_09_30_014720_add_kid_to_reminder.php new file mode 100644 index 0000000..df9a2c6 --- /dev/null +++ b/database/migrations/2016_09_30_014720_add_kid_to_reminder.php @@ -0,0 +1,30 @@ +integer('kid_id')->nullable()->after('reminder_type_id'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + // + } +} diff --git a/database/migrations/2016_10_15_024156_add_deleted_at_to_users.php b/database/migrations/2016_10_15_024156_add_deleted_at_to_users.php new file mode 100644 index 0000000..9b543df --- /dev/null +++ b/database/migrations/2016_10_15_024156_add_deleted_at_to_users.php @@ -0,0 +1,30 @@ +softDeletes(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + // + } +} diff --git a/database/migrations/2016_10_19_155139_create_cache_table.php b/database/migrations/2016_10_19_155139_create_cache_table.php new file mode 100644 index 0000000..1f7761c --- /dev/null +++ b/database/migrations/2016_10_19_155139_create_cache_table.php @@ -0,0 +1,32 @@ +string('key')->unique(); + $table->text('value'); + $table->integer('expiration'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('cache'); + } +} diff --git a/database/migrations/2016_10_19_155800_create_sessions_table.php b/database/migrations/2016_10_19_155800_create_sessions_table.php new file mode 100644 index 0000000..56e76d6 --- /dev/null +++ b/database/migrations/2016_10_19_155800_create_sessions_table.php @@ -0,0 +1,35 @@ +string('id')->unique(); + $table->integer('user_id')->nullable(); + $table->string('ip_address', 45)->nullable(); + $table->text('user_agent')->nullable(); + $table->text('payload'); + $table->integer('last_activity'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('sessions'); + } +} diff --git a/database/migrations/2016_10_21_022941_add_statistics_table.php b/database/migrations/2016_10_21_022941_add_statistics_table.php new file mode 100644 index 0000000..11d4532 --- /dev/null +++ b/database/migrations/2016_10_21_022941_add_statistics_table.php @@ -0,0 +1,37 @@ +increments('id'); + $table->integer('number_of_users'); + $table->integer('number_of_contacts'); + $table->integer('number_of_notes'); + $table->integer('number_of_reminders'); + $table->integer('number_of_tasks'); + $table->integer('number_of_kids'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + // + } +} diff --git a/database/migrations/2016_10_24_013543_add_journal_setting_to_users.php b/database/migrations/2016_10_24_013543_add_journal_setting_to_users.php new file mode 100644 index 0000000..b948a25 --- /dev/null +++ b/database/migrations/2016_10_24_013543_add_journal_setting_to_users.php @@ -0,0 +1,30 @@ +string('onboarding_journal_dismissed')->default('false')->after('fluid_container'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + // + } +} diff --git a/database/migrations/2016_10_24_014257_create_journal_tables.php b/database/migrations/2016_10_24_014257_create_journal_tables.php new file mode 100644 index 0000000..452152c --- /dev/null +++ b/database/migrations/2016_10_24_014257_create_journal_tables.php @@ -0,0 +1,17 @@ +string('metric')->default('fahrenheit')->after('locale'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + // + } +} diff --git a/database/migrations/2016_11_01_014353_create_activities_table.php b/database/migrations/2016_11_01_014353_create_activities_table.php new file mode 100644 index 0000000..a288f00 --- /dev/null +++ b/database/migrations/2016_11_01_014353_create_activities_table.php @@ -0,0 +1,38 @@ +increments('id'); + $table->integer('account_id'); + $table->integer('people_id'); + $table->integer('activity_type_id'); + $table->longText('description')->nullable(); + $table->dateTime('date_it_happened'); + $table->integer('user_id_of_the_writer'); + $table->softDeletes(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + // + } +} diff --git a/database/migrations/2016_11_01_015957_add_icon_column.php b/database/migrations/2016_11_01_015957_add_icon_column.php new file mode 100644 index 0000000..c010da2 --- /dev/null +++ b/database/migrations/2016_11_01_015957_add_icon_column.php @@ -0,0 +1,29 @@ +string('icon')->after('key'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + // + } +} diff --git a/database/migrations/2016_11_03_150307_add_activity_location_to_activities.php b/database/migrations/2016_11_03_150307_add_activity_location_to_activities.php new file mode 100644 index 0000000..a9e9d30 --- /dev/null +++ b/database/migrations/2016_11_03_150307_add_activity_location_to_activities.php @@ -0,0 +1,30 @@ +string('location_type')->after('key'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + // + } +} diff --git a/database/migrations/2016_11_09_013049_add_events_table.php b/database/migrations/2016_11_09_013049_add_events_table.php new file mode 100644 index 0000000..1ca29a2 --- /dev/null +++ b/database/migrations/2016_11_09_013049_add_events_table.php @@ -0,0 +1,36 @@ +increments('id'); + $table->integer('account_id'); + $table->integer('people_id'); + $table->string('object_type'); + $table->integer('object_id'); + $table->string('nature_of_operation'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + // + } +} diff --git a/database/migrations/2016_12_08_011555_remove_type_from_notes.php b/database/migrations/2016_12_08_011555_remove_type_from_notes.php new file mode 100644 index 0000000..43100bc --- /dev/null +++ b/database/migrations/2016_12_08_011555_remove_type_from_notes.php @@ -0,0 +1,31 @@ +dropColumn('type'); + $table->dropColumn('activity_type_id'); + $table->dropColumn('sticky'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + // + } +} diff --git a/database/migrations/2016_12_13_133945_add_gifts_table.php b/database/migrations/2016_12_13_133945_add_gifts_table.php new file mode 100644 index 0000000..82c6a78 --- /dev/null +++ b/database/migrations/2016_12_13_133945_add_gifts_table.php @@ -0,0 +1,42 @@ +increments('id'); + $table->integer('account_id'); + $table->integer('people_id'); + $table->string('about_object_type')->nullable(); + $table->string('about_object_id')->nullable(); + $table->string('title'); + $table->longText('description')->nullable(); + $table->longText('url')->nullable(); + $table->string('value_in_dollars')->nullable(); + $table->string('is_an_idea')->default('true'); + $table->string('has_been_offered')->default('false'); + $table->dateTime('date_offered')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + // + } +} diff --git a/database/migrations/2016_12_28_150831_change_title_column.php b/database/migrations/2016_12_28_150831_change_title_column.php new file mode 100644 index 0000000..a7c0ce6 --- /dev/null +++ b/database/migrations/2016_12_28_150831_change_title_column.php @@ -0,0 +1,29 @@ +string('title', 60000)->change(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + // + } +} diff --git a/database/migrations/2017_01_14_200815_add_facebook_columns_to_users_table.php b/database/migrations/2017_01_14_200815_add_facebook_columns_to_users_table.php new file mode 100644 index 0000000..3504123 --- /dev/null +++ b/database/migrations/2017_01_14_200815_add_facebook_columns_to_users_table.php @@ -0,0 +1,36 @@ +unsignedBigInteger('facebook_user_id')->index(); + $table->string('access_token')->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn( + 'facebook_user_id', + 'access_token' + ); + }); + } +} diff --git a/database/migrations/2017_01_15_045025_add_colors_to_users.php b/database/migrations/2017_01_15_045025_add_colors_to_users.php new file mode 100644 index 0000000..53e897b --- /dev/null +++ b/database/migrations/2017_01_15_045025_add_colors_to_users.php @@ -0,0 +1,34 @@ +string('default_avatar_color'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('contacts', function (Blueprint $table) { + $table->dropColumn( + 'default_avatar_color' + ); + }); + } +} diff --git a/database/migrations/2017_01_22_142645_add_fields_to_contacts.php b/database/migrations/2017_01_22_142645_add_fields_to_contacts.php new file mode 100644 index 0000000..82371b3 --- /dev/null +++ b/database/migrations/2017_01_22_142645_add_fields_to_contacts.php @@ -0,0 +1,71 @@ +string('has_kids')->default('false')->after('gender'); + $table->integer('number_of_kids')->default('0')->after('has_kids'); + $table->date('last_talked_to')->nullable()->after('number_of_kids'); + $table->integer('number_of_reminders')->default('0')->after('last_talked_to'); + }); + + Schema::table('contacts', function (Blueprint $table) { + $table->dropColumn( + 'entity_id', 'people_id', 'twitter_id', 'instagram_id' + ); + }); + + Schema::table('activities', function (Blueprint $table) { + $table->integer('contact_id')->after('account_id'); + }); + + Schema::table('activities', function (Blueprint $table) { + $table->dropColumn( + 'people_id', 'user_id_of_the_writer' + ); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('contacts', function (Blueprint $table) { + $table->dropColumn( + 'has_kids', 'number_of_kids', 'last_talked_to', 'number_of_reminders' + ); + }); + + Schema::table('activities', function (Blueprint $table) { + $table->dropColumn( + 'contact_id' + ); + }); + + Schema::table('contacts', function (Blueprint $table) { + $table->integer('entity_id')->nullable(); + $table->integer('people_id')->nullable(); + $table->string('twitter_id')->nullable(); + $table->string('instagram_id')->nullable(); + }); + + Schema::table('activities', function (Blueprint $table) { + $table->integer('people_id')->after('account_id'); + $table->integer('user_id_of_the_writer'); + }); + } +} diff --git a/database/migrations/2017_01_23_043831_change_people_to_contact_for_kids.php b/database/migrations/2017_01_23_043831_change_people_to_contact_for_kids.php new file mode 100644 index 0000000..50204b5 --- /dev/null +++ b/database/migrations/2017_01_23_043831_change_people_to_contact_for_kids.php @@ -0,0 +1,44 @@ +integer('child_of_contact_id')->after('account_id'); + }); + + Schema::table('kids', function (Blueprint $table) { + $table->dropColumn( + 'child_of_people_id' + ); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('kids', function (Blueprint $table) { + $table->integer('child_of_people_id')->after('account_id'); + }); + + Schema::table('kids', function (Blueprint $table) { + $table->dropColumn( + 'child_of_contact_id' + ); + }); + } +} diff --git a/database/migrations/2017_01_26_013524_change_people_to_significantother.php b/database/migrations/2017_01_26_013524_change_people_to_significantother.php new file mode 100644 index 0000000..a207719 --- /dev/null +++ b/database/migrations/2017_01_26_013524_change_people_to_significantother.php @@ -0,0 +1,44 @@ +dropColumn( + 'people_id' + ); + }); + + Schema::table('significant_others', function (Blueprint $table) { + $table->integer('contact_id')->after('account_id'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('significant_others', function (Blueprint $table) { + $table->integer('people_id')->after('account_id'); + }); + + Schema::table('significant_others', function (Blueprint $table) { + $table->dropColumn( + 'contact_id' + ); + }); + } +} diff --git a/database/migrations/2017_01_26_022852_change_notes_to_contact.php b/database/migrations/2017_01_26_022852_change_notes_to_contact.php new file mode 100644 index 0000000..d54b3d4 --- /dev/null +++ b/database/migrations/2017_01_26_022852_change_notes_to_contact.php @@ -0,0 +1,26 @@ +dropColumn( + 'people_id' + ); + }); + + Schema::table('notes', function (Blueprint $table) { + $table->integer('contact_id')->after('account_id'); + }); + } +} diff --git a/database/migrations/2017_01_26_034553_add_notes_count_to_contact.php b/database/migrations/2017_01_26_034553_add_notes_count_to_contact.php new file mode 100644 index 0000000..17b01b5 --- /dev/null +++ b/database/migrations/2017_01_26_034553_add_notes_count_to_contact.php @@ -0,0 +1,34 @@ +integer('number_of_notes')->default(0)->after('number_of_reminders'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('contacts', function (Blueprint $table) { + $table->dropColumn( + 'number_of_reminders' + ); + }); + } +} diff --git a/database/migrations/2017_01_27_024356_change_people_in_events.php b/database/migrations/2017_01_27_024356_change_people_in_events.php new file mode 100644 index 0000000..706f2f6 --- /dev/null +++ b/database/migrations/2017_01_27_024356_change_people_in_events.php @@ -0,0 +1,44 @@ +dropColumn( + 'people_id' + ); + }); + + Schema::table('events', function (Blueprint $table) { + $table->integer('contact_id')->after('account_id'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('events', function (Blueprint $table) { + $table->dropColumn( + 'contact_id' + ); + }); + + Schema::table('events', function (Blueprint $table) { + $table->integer('people_id')->after('account_id'); + }); + } +} diff --git a/database/migrations/2017_01_28_180156_remove_deleted_at_from_significant_others.php b/database/migrations/2017_01_28_180156_remove_deleted_at_from_significant_others.php new file mode 100644 index 0000000..74b1af3 --- /dev/null +++ b/database/migrations/2017_01_28_180156_remove_deleted_at_from_significant_others.php @@ -0,0 +1,34 @@ +dropColumn( + 'deleted_at' + ); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('events', function (Blueprint $table) { + $table->softDeletes(); + }); + } +} diff --git a/database/migrations/2017_01_28_184901_remove_deleted_at_from_kids.php b/database/migrations/2017_01_28_184901_remove_deleted_at_from_kids.php new file mode 100644 index 0000000..f526ac9 --- /dev/null +++ b/database/migrations/2017_01_28_184901_remove_deleted_at_from_kids.php @@ -0,0 +1,34 @@ +dropColumn( + 'deleted_at' + ); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('kids', function (Blueprint $table) { + $table->softDeletes(); + }); + } +} diff --git a/database/migrations/2017_01_28_193913_remove_deleted_at_from_notes.php b/database/migrations/2017_01_28_193913_remove_deleted_at_from_notes.php new file mode 100644 index 0000000..0cc5c13 --- /dev/null +++ b/database/migrations/2017_01_28_193913_remove_deleted_at_from_notes.php @@ -0,0 +1,38 @@ +dropColumn( + 'deleted_at', 'title', 'activity_date', 'remind_for_next_call_or_email', 'author_id' + ); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('notes', function (Blueprint $table) { + $table->string('title')->nullable(); + $table->dateTime('activity_date')->nullable(); + $table->string('remind_for_next_call_or_email')->default('false'); + $table->integer('author_id'); + $table->softDeletes(); + }); + } +} diff --git a/database/migrations/2017_01_28_222114_remove_viewed_at_from_contacts.php b/database/migrations/2017_01_28_222114_remove_viewed_at_from_contacts.php new file mode 100644 index 0000000..e8c0971 --- /dev/null +++ b/database/migrations/2017_01_28_222114_remove_viewed_at_from_contacts.php @@ -0,0 +1,36 @@ +dropColumn('viewed_at'); + }); + } + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + if (! Schema::hasColumn('contacts', 'viewed_at')) { + Schema::table('contacts', function (Blueprint $table) { + $table->dateTime('viewed_at')->nullable(); + }); + } + } +} diff --git a/database/migrations/2017_01_29_175146_remove_delete_at_from_activities.php b/database/migrations/2017_01_29_175146_remove_delete_at_from_activities.php new file mode 100644 index 0000000..7d59b74 --- /dev/null +++ b/database/migrations/2017_01_29_175146_remove_delete_at_from_activities.php @@ -0,0 +1,34 @@ +dropColumn( + 'deleted_at' + ); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('activities', function (Blueprint $table) { + $table->dateTime('deleted_at')->nullable(); + }); + } +} diff --git a/database/migrations/2017_01_29_175629_add_number_activities_to_contacts.php b/database/migrations/2017_01_29_175629_add_number_activities_to_contacts.php new file mode 100644 index 0000000..2901eaa --- /dev/null +++ b/database/migrations/2017_01_29_175629_add_number_activities_to_contacts.php @@ -0,0 +1,34 @@ +integer('number_of_activities')->after('number_of_notes')->default(0); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('contacts', function (Blueprint $table) { + $table->dropColumn( + 'number_of_activities' + ); + }); + } +} diff --git a/database/migrations/2017_01_31_025849_add_activity_statistics_table.php b/database/migrations/2017_01_31_025849_add_activity_statistics_table.php new file mode 100644 index 0000000..c239ffe --- /dev/null +++ b/database/migrations/2017_01_31_025849_add_activity_statistics_table.php @@ -0,0 +1,35 @@ +increments('id'); + $table->integer('account_id'); + $table->integer('contact_id'); + $table->integer('year'); + $table->integer('count'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::drop('activity_statistics'); + } +} diff --git a/database/migrations/2017_02_02_232450_add_confirmation.php b/database/migrations/2017_02_02_232450_add_confirmation.php new file mode 100644 index 0000000..78b0879 --- /dev/null +++ b/database/migrations/2017_02_02_232450_add_confirmation.php @@ -0,0 +1,37 @@ +boolean('confirmed')->default(false); + $table->string('confirmation_code')->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('confirmed'); + }); + + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('confirmation_code'); + }); + } +} diff --git a/database/migrations/2017_02_04_225618_change_reminders_table.php b/database/migrations/2017_02_04_225618_change_reminders_table.php new file mode 100644 index 0000000..2bfe8bd --- /dev/null +++ b/database/migrations/2017_02_04_225618_change_reminders_table.php @@ -0,0 +1,45 @@ +dropColumn( + 'deleted_at', 'people_id' + ); + }); + + Schema::table('reminders', function (Blueprint $table) { + $table->integer('contact_id')->after('account_id'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('reminders', function (Blueprint $table) { + $table->dropColumn( + 'contact_id' + ); + }); + + Schema::table('reminders', function (Blueprint $table) { + $table->integer('people_id')->after('account_id'); + $table->softDeletes(); + }); + } +} diff --git a/database/migrations/2017_02_05_035925_add_gifts_metrics_to_contacts.php b/database/migrations/2017_02_05_035925_add_gifts_metrics_to_contacts.php new file mode 100644 index 0000000..9b0a565 --- /dev/null +++ b/database/migrations/2017_02_05_035925_add_gifts_metrics_to_contacts.php @@ -0,0 +1,36 @@ +integer('number_of_gifts_ideas')->default(0)->after('number_of_activities'); + $table->integer('number_of_gifts_received')->default(0)->after('number_of_gifts_ideas'); + $table->integer('number_of_gifts_offered')->default(0)->after('number_of_gifts_received'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('contacts', function (Blueprint $table) { + $table->dropColumn( + 'number_of_gifts_ideas', 'number_of_gifts_received', 'number_of_gifts_offered' + ); + }); + } +} diff --git a/database/migrations/2017_02_05_041740_change_gifts_table.php b/database/migrations/2017_02_05_041740_change_gifts_table.php new file mode 100644 index 0000000..0108e9d --- /dev/null +++ b/database/migrations/2017_02_05_041740_change_gifts_table.php @@ -0,0 +1,34 @@ +renameColumn('title', 'name'); + $table->renameColumn('description', 'comment'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('gifts', function (Blueprint $table) { + $table->renameColumn('name', 'title'); + $table->renameColumn('comment', 'description'); + }); + } +} diff --git a/database/migrations/2017_02_05_042122_change_people_to_contact_for_gifts.php b/database/migrations/2017_02_05_042122_change_people_to_contact_for_gifts.php new file mode 100644 index 0000000..5bce65e --- /dev/null +++ b/database/migrations/2017_02_05_042122_change_people_to_contact_for_gifts.php @@ -0,0 +1,44 @@ +dropColumn( + 'people_id' + ); + }); + + Schema::table('gifts', function (Blueprint $table) { + $table->integer('contact_id')->after('account_id'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('gifts', function (Blueprint $table) { + $table->dropColumn( + 'contact_id' + ); + }); + + Schema::table('gifts', function (Blueprint $table) { + $table->integer('people_id')->after('account_id'); + }); + } +} diff --git a/database/migrations/2017_02_07_041607_change_tasks_table.php b/database/migrations/2017_02_07_041607_change_tasks_table.php new file mode 100644 index 0000000..e23d0ba --- /dev/null +++ b/database/migrations/2017_02_07_041607_change_tasks_table.php @@ -0,0 +1,45 @@ +dropColumn( + 'people_id', 'deleted_at' + ); + }); + + Schema::table('tasks', function (Blueprint $table) { + $table->integer('contact_id')->after('account_id'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('tasks', function (Blueprint $table) { + $table->dropColumn( + 'contact_id' + ); + }); + + Schema::table('tasks', function (Blueprint $table) { + $table->integer('people_id')->after('account_id'); + $table->softDeletes(); + }); + } +} diff --git a/database/migrations/2017_02_07_051355_add_number_tasks_to_contact.php b/database/migrations/2017_02_07_051355_add_number_tasks_to_contact.php new file mode 100644 index 0000000..42797db --- /dev/null +++ b/database/migrations/2017_02_07_051355_add_number_tasks_to_contact.php @@ -0,0 +1,34 @@ +integer('number_of_tasks')->after('number_of_gifts_offered')->default(0); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('contacts', function (Blueprint $table) { + $table->dropColumn( + 'number_of_tasks' + ); + }); + } +} diff --git a/database/migrations/2017_02_08_002251_change_number_tasks_contact.php b/database/migrations/2017_02_08_002251_change_number_tasks_contact.php new file mode 100644 index 0000000..b36b657 --- /dev/null +++ b/database/migrations/2017_02_08_002251_change_number_tasks_contact.php @@ -0,0 +1,45 @@ +dropColumn( + 'number_of_tasks' + ); + }); + + Schema::table('contacts', function (Blueprint $table) { + $table->integer('number_of_tasks_in_progress')->after('number_of_gifts_offered'); + $table->integer('number_of_tasks_completed')->after('number_of_tasks_in_progress'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('contacts', function (Blueprint $table) { + $table->dropColumn( + 'number_of_tasks_completed', 'number_of_gifts_offered' + ); + }); + + Schema::table('contacts', function (Blueprint $table) { + $table->integer('number_of_tasks'); + }); + } +} diff --git a/database/migrations/2017_02_08_025358_add_sort_preferences_to_users.php b/database/migrations/2017_02_08_025358_add_sort_preferences_to_users.php new file mode 100644 index 0000000..bcb4445 --- /dev/null +++ b/database/migrations/2017_02_08_025358_add_sort_preferences_to_users.php @@ -0,0 +1,34 @@ +string('contacts_sort_order')->after('onboarding_journal_dismissed')->default('firstnameAZ'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn( + 'contacts_sort_order' + ); + }); + } +} diff --git a/database/migrations/2017_02_10_195613_remove_notifications_table.php b/database/migrations/2017_02_10_195613_remove_notifications_table.php new file mode 100644 index 0000000..f2e14f7 --- /dev/null +++ b/database/migrations/2017_02_10_195613_remove_notifications_table.php @@ -0,0 +1,37 @@ +increments('id'); + $table->integer('account_id'); + $table->integer('people_id'); + $table->enum('for', ['reminder']); + $table->enum('how', ['email']); + $table->string('address'); + $table->longText('content'); + $table->timestamps(); + }); + } +} diff --git a/database/migrations/2017_02_10_214714_remove_people_table.php b/database/migrations/2017_02_10_214714_remove_people_table.php new file mode 100644 index 0000000..e27abb9 --- /dev/null +++ b/database/migrations/2017_02_10_214714_remove_people_table.php @@ -0,0 +1,41 @@ +increments('id'); + $table->string('api_id'); + $table->integer('account_id'); + $table->enum('type', ['entity', 'contact']); + $table->integer('object_id'); + $table->string('sortable_name')->nullable()->after('object_id'); + $table->string('has_kids')->default('false')->after('object_id')->nullable(); + $table->integer('number_of_kids')->after('has_kids')->nullable(); + $table->dateTime('last_talked_to')->nullable(); + $table->dateTime('viewed_at')->nullable(); + $table->softDeletes(); + $table->timestamps(); + }); + } +} diff --git a/database/migrations/2017_02_10_215405_remove_entities_table.php b/database/migrations/2017_02_10_215405_remove_entities_table.php new file mode 100644 index 0000000..aa43adb --- /dev/null +++ b/database/migrations/2017_02_10_215405_remove_entities_table.php @@ -0,0 +1,34 @@ +increments('id'); + $table->integer('account_id'); + $table->string('name'); + $table->softDeletes(); + $table->timestamps(); + }); + } +} diff --git a/database/migrations/2017_02_10_224355_calculate_statistics.php b/database/migrations/2017_02_10_224355_calculate_statistics.php new file mode 100644 index 0000000..1fa2d8f --- /dev/null +++ b/database/migrations/2017_02_10_224355_calculate_statistics.php @@ -0,0 +1,33 @@ +number_of_reminders = Reminder::where('contact_id', $contact->id)->count(); + $contact->number_of_notes = Note::where('contact_id', $contact->id)->count(); + $contact->number_of_activities = Activity::where('contact_id', $contact->id)->count(); + $contact->number_of_gifts_ideas = Gift::where('contact_id', $contact->id)->where('is_an_idea', 'true')->count(); + $contact->number_of_gifts_offered = Gift::where('contact_id', $contact->id)->where('has_been_offered', 'true')->count(); + $contact->number_of_tasks_in_progress = Task::where('contact_id', $contact->id)->where('status', 'inprogress')->count(); + $contact->number_of_tasks_completed = Task::where('contact_id', $contact->id)->where('status', 'completed')->count(); + $contact->save(); + } + } +} diff --git a/database/migrations/2017_02_11_154900_add_avatars_to_contacts.php b/database/migrations/2017_02_11_154900_add_avatars_to_contacts.php new file mode 100644 index 0000000..574aece --- /dev/null +++ b/database/migrations/2017_02_11_154900_add_avatars_to_contacts.php @@ -0,0 +1,35 @@ +string('has_avatar')->after('food_preferencies')->default('false'); + $table->string('avatar_file_name')->after('has_avatar')->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('contacts', function (Blueprint $table) { + $table->dropColumn( + 'has_avatar', 'avatar_file_name' + ); + }); + } +} diff --git a/database/migrations/2017_02_12_134220_create_entries_table.php b/database/migrations/2017_02_12_134220_create_entries_table.php new file mode 100644 index 0000000..58644ef --- /dev/null +++ b/database/migrations/2017_02_12_134220_create_entries_table.php @@ -0,0 +1,34 @@ +increments('id'); + $table->integer('account_id'); + $table->string('title')->nullable(); + $table->longText('post'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::drop('entries'); + } +} diff --git a/database/migrations/2017_05_03_155254_move_significant_other_data.php b/database/migrations/2017_05_03_155254_move_significant_other_data.php new file mode 100644 index 0000000..9bb5c75 --- /dev/null +++ b/database/migrations/2017_05_03_155254_move_significant_other_data.php @@ -0,0 +1,20 @@ +dropColumn('last_name'); + }); + } +} diff --git a/database/migrations/2017_05_04_164723_remove_contact_encryption.php b/database/migrations/2017_05_04_164723_remove_contact_encryption.php new file mode 100644 index 0000000..23347b0 --- /dev/null +++ b/database/migrations/2017_05_04_164723_remove_contact_encryption.php @@ -0,0 +1,53 @@ +id; + if (! is_null($contact->email)) { + $contact->email = decrypt($contact->email); + } + + if (! is_null($contact->phone_number)) { + $contact->phone_number = decrypt($contact->phone_number); + } + + if (! is_null($contact->street)) { + $contact->street = decrypt($contact->street); + } + + if (! is_null($contact->city)) { + $contact->city = decrypt($contact->city); + } + + if (! is_null($contact->province)) { + $contact->province = decrypt($contact->province); + } + + if (! is_null($contact->postal_code)) { + $contact->postal_code = decrypt($contact->postal_code); + } + + if ($contact->is_birthdate_approximate == 'true') { + $contact->is_birthdate_approximate = 'approximate'; + } + + if ($contact->is_birthdate_approximate == 'false') { + $contact->is_birthdate_approximate = 'exact'; + } + $contact->save(); + } + } +} diff --git a/database/migrations/2017_05_04_185921_add_title_to_activities.php b/database/migrations/2017_05_04_185921_add_title_to_activities.php new file mode 100644 index 0000000..f2cc501 --- /dev/null +++ b/database/migrations/2017_05_04_185921_add_title_to_activities.php @@ -0,0 +1,20 @@ +string('summary')->after('activity_type_id'); + }); + } +} diff --git a/database/migrations/2017_05_04_193252_alter_activity_nullable.php b/database/migrations/2017_05_04_193252_alter_activity_nullable.php new file mode 100644 index 0000000..c645ffb --- /dev/null +++ b/database/migrations/2017_05_04_193252_alter_activity_nullable.php @@ -0,0 +1,20 @@ +unsignedInteger('activity_type_id')->nullable()->change(); + }); + } +} diff --git a/database/migrations/2017_05_08_164514_remove_encryption_tasks.php b/database/migrations/2017_05_08_164514_remove_encryption_tasks.php new file mode 100644 index 0000000..4fe2028 --- /dev/null +++ b/database/migrations/2017_05_08_164514_remove_encryption_tasks.php @@ -0,0 +1,29 @@ +id.' '; + if (! is_null($task->title)) { + $task->title = decrypt($task->title); + } + + if (! is_null($task->description)) { + $task->description = decrypt($task->description); + } + + $task->save(); + } + } +} diff --git a/database/migrations/2017_05_30_002239_remove_predefined_reminders.php b/database/migrations/2017_05_30_002239_remove_predefined_reminders.php new file mode 100644 index 0000000..16bea04 --- /dev/null +++ b/database/migrations/2017_05_30_002239_remove_predefined_reminders.php @@ -0,0 +1,39 @@ +id.' '; + if (! is_null($reminder->title)) { + $reminder->title = decrypt($reminder->title); + } + + if (! is_null($reminder->description)) { + $reminder->description = decrypt($reminder->description); + } + + $reminder->save(); + } + + Schema::table('reminders', function (Blueprint $table) { + $table->dropColumn( + 'reminder_type_id' + ); + }); + + Schema::drop('reminder_types'); + } +} diff --git a/database/migrations/2017_05_30_023116_create_money_table.php b/database/migrations/2017_05_30_023116_create_money_table.php new file mode 100644 index 0000000..f972892 --- /dev/null +++ b/database/migrations/2017_05_30_023116_create_money_table.php @@ -0,0 +1,26 @@ +increments('id'); + $table->integer('account_id'); + $table->integer('contact_id'); + $table->string('in_debt')->default('no'); + $table->string('status')->default('inprogress'); + $table->integer('amount'); + $table->longText('reason')->nullable(); + $table->timestamps(); + }); + } +} diff --git a/database/migrations/2017_06_07_173437_add_multiple_genders_choices.php b/database/migrations/2017_06_07_173437_add_multiple_genders_choices.php new file mode 100644 index 0000000..f6860ba --- /dev/null +++ b/database/migrations/2017_06_07_173437_add_multiple_genders_choices.php @@ -0,0 +1,54 @@ +getDriverName(); + switch ($driverName) { + case 'mysql': + DB::statement('ALTER TABLE '.DBHelper::getTable('contacts')." CHANGE COLUMN gender gender ENUM('male', 'female', 'none')"); + DB::statement('ALTER TABLE '.DBHelper::getTable('significant_others')." CHANGE COLUMN gender gender ENUM('male', 'female', 'none')"); + DB::statement('ALTER TABLE '.DBHelper::getTable('kids')." CHANGE COLUMN gender gender ENUM('male', 'female', 'none')"); + break; + case 'pgsql': + $this->alterEnum(DBHelper::getTable('contacts'), 'gender', ['male', 'female', 'none']); + $this->alterEnum(DBHelper::getTable('significant_others'), 'gender', ['male', 'female', 'none']); + $this->alterEnum(DBHelper::getTable('kids'), 'gender', ['male', 'female', 'none']); + break; + default: + throw new \Exception("Driver {$driverName} not supported."); + break; + } + } + + /** + * Alter an enum field constraints. Source: https://stackoverflow.com/a/36198549. + * + * @param $table + * @param $field + * @param array $options + */ + protected function alterEnum($table, $field, array $options) + { + $check = "${table}_${field}_check"; + $enumList = []; + foreach ($options as $option) { + $enumList[] = sprintf("'%s'::CHARACTER VARYING", $option); + } + $enumString = implode(', ', $enumList); + DB::transaction(function () use ($table, $field, $check, $enumString) { + DB::statement(sprintf('ALTER TABLE %s DROP CONSTRAINT %s;', $table, $check)); + DB::statement(sprintf('ALTER TABLE %s ADD CONSTRAINT %s CHECK (%s::TEXT = ANY (ARRAY[%s]::TEXT[]))', $table, $check, $field, $enumString)); + }); + } +} diff --git a/database/migrations/2017_06_10_152945_add_social_networks_to_contacts.php b/database/migrations/2017_06_10_152945_add_social_networks_to_contacts.php new file mode 100644 index 0000000..2119bd9 --- /dev/null +++ b/database/migrations/2017_06_10_152945_add_social_networks_to_contacts.php @@ -0,0 +1,22 @@ +string('facebook_profile_url')->after('avatar_file_name')->nullable(); + $table->string('twitter_profile_url')->after('facebook_profile_url')->nullable(); + $table->string('linkedin_profile_url')->after('twitter_profile_url')->nullable(); + }); + } +} diff --git a/database/migrations/2017_06_10_155349_create_currencies_data.php b/database/migrations/2017_06_10_155349_create_currencies_data.php new file mode 100644 index 0000000..b262113 --- /dev/null +++ b/database/migrations/2017_06_10_155349_create_currencies_data.php @@ -0,0 +1,38 @@ +increments('id'); + $table->string('iso'); + $table->string('name'); + $table->string('symbol'); + }); + + //defaults + DB::table('currencies')->insert(['iso' => 'CAD', 'name' => 'Canadian Dollar', 'symbol'=>'$']); + DB::table('currencies')->insert(['iso' => 'USD', 'name' => 'US Dollar', 'symbol'=>'$']); + DB::table('currencies')->insert(['iso' => 'GBP', 'name' => 'British Pound', 'symbol'=>'£']); + DB::table('currencies')->insert(['iso' => 'EUR', 'name' => 'Euro', 'symbol'=>'€']); + DB::table('currencies')->insert(['iso' => 'RUB', 'name' => 'Russian Ruble', 'symbol'=>'₽']); + + Schema::table('users', function (Blueprint $table) { + $dollarResult = DB::table('currencies')->select('id')->where('iso', '=', 'USD')->value('id'); + $table->integer('currency_id')->after('timezone')->default( + $dollarResult + ); + }); + } +} diff --git a/database/migrations/2017_06_11_025227_remove_encryption_journal.php b/database/migrations/2017_06_11_025227_remove_encryption_journal.php new file mode 100644 index 0000000..b0fe3e8 --- /dev/null +++ b/database/migrations/2017_06_11_025227_remove_encryption_journal.php @@ -0,0 +1,29 @@ +id.' '; + if (! is_null($entry->title)) { + $entry->title = decrypt($entry->title); + } + + if (! is_null($entry->post)) { + $entry->post = decrypt($entry->post); + } + + $entry->save(); + } + } +} diff --git a/database/migrations/2017_06_11_110735_change_unique_constraint_for_contacts.php b/database/migrations/2017_06_11_110735_change_unique_constraint_for_contacts.php new file mode 100644 index 0000000..13507c8 --- /dev/null +++ b/database/migrations/2017_06_11_110735_change_unique_constraint_for_contacts.php @@ -0,0 +1,36 @@ +dropUnique(['email']); + + $table->unique(['account_id', 'email'], 'unique_for_each_account_email_pair'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('contacts', function (Blueprint $table) { + $table->dropUnique('unique_for_each_account_email_pair'); + + $table->unique('email'); + }); + } +} diff --git a/database/migrations/2017_06_13_035059_remove_gifts_encryption.php b/database/migrations/2017_06_13_035059_remove_gifts_encryption.php new file mode 100644 index 0000000..d3af93c --- /dev/null +++ b/database/migrations/2017_06_13_035059_remove_gifts_encryption.php @@ -0,0 +1,35 @@ +id; + if (! is_null($gift->name)) { + $gift->name = decrypt($gift->name); + } + + if (! is_null($gift->comment)) { + $gift->comment = decrypt($gift->comment); + } + + if (! is_null($gift->url)) { + $gift->url = decrypt($gift->url); + } + + $gift->save(); + } + } +} diff --git a/database/migrations/2017_06_13_195740_add_company_to_contacts.php b/database/migrations/2017_06_13_195740_add_company_to_contacts.php new file mode 100644 index 0000000..1f6be04 --- /dev/null +++ b/database/migrations/2017_06_13_195740_add_company_to_contacts.php @@ -0,0 +1,20 @@ +string('company')->after('job')->nullable(); + }); + } +} diff --git a/database/migrations/2017_06_14_131803_remove_bern_timezone.php b/database/migrations/2017_06_14_131803_remove_bern_timezone.php new file mode 100644 index 0000000..ee305b4 --- /dev/null +++ b/database/migrations/2017_06_14_131803_remove_bern_timezone.php @@ -0,0 +1,23 @@ +timezone == 'Europe/Bern') { + $user->timezone = 'Europe/Berlin'; + $user->save(); + } + } + } +} diff --git a/database/migrations/2017_06_14_132911_add_zar_currency_to_currencies_table.php b/database/migrations/2017_06_14_132911_add_zar_currency_to_currencies_table.php new file mode 100644 index 0000000..4078de7 --- /dev/null +++ b/database/migrations/2017_06_14_132911_add_zar_currency_to_currencies_table.php @@ -0,0 +1,17 @@ +insert(['iso' => 'ZAR', 'name' => 'South African Rand', 'symbol'=>'R ']); + } +} diff --git a/database/migrations/2017_06_16_215256_add_about_who_to_reminders.php b/database/migrations/2017_06_16_215256_add_about_who_to_reminders.php new file mode 100644 index 0000000..e5c961f --- /dev/null +++ b/database/migrations/2017_06_16_215256_add_about_who_to_reminders.php @@ -0,0 +1,40 @@ +string('is_birthday')->after('contact_id')->default('false'); + $table->string('about_object')->after('is_birthday')->nullable(); + $table->string('about_object_id')->after('about_object')->nullable(); + }); + + // Migrate all kids birthdays to the new system to track birthdays reminders + foreach (Reminder::all() as $reminder) { + if ($reminder->kid_id) { + $reminder->is_birthday = 'true'; + $reminder->about_object = 'kid'; + $reminder->about_object_id = $reminder->kid_id; + $reminder->save(); + } + } + + // Get rid of the kid_id field + Schema::table('reminders', function (Blueprint $table) { + $table->dropColumn( + ['kid_id'] + ); + }); + } +} diff --git a/database/migrations/2017_06_17_010900_fix_contacts_table.php b/database/migrations/2017_06_17_010900_fix_contacts_table.php new file mode 100644 index 0000000..a21f4b6 --- /dev/null +++ b/database/migrations/2017_06_17_010900_fix_contacts_table.php @@ -0,0 +1,22 @@ +is_birthdate_approximate == 'exact' and is_null($contact->birthday_reminder_id)) { + $contact->is_birthdate_approximate = 'approximate'; + $contact->save(); + } + } + } +} diff --git a/database/migrations/2017_06_17_153814_refactor_user_table.php b/database/migrations/2017_06_17_153814_refactor_user_table.php new file mode 100644 index 0000000..f4fbd9b --- /dev/null +++ b/database/migrations/2017_06_17_153814_refactor_user_table.php @@ -0,0 +1,40 @@ +dropColumn([ + 'facebook_user_id', + 'access_token', + 'amazon_store_country_id', + 'onboarding_journal_dismissed', + 'send_sms_alert', + 'phone_number', + 'gender', + 'deleted_at', + ]); + + $table->integer('invited_by_user_id')->after('contacts_sort_order')->nullable(); + }); + + Schema::create('invitations', function (Blueprint $table) { + $table->increments('id'); + $table->integer('account_id'); + $table->integer('invited_by_user_id'); + $table->string('email'); + $table->string('invitation_key'); + $table->timestamps(); + }); + } +} diff --git a/database/migrations/2017_06_19_105842_add_stripe_fields_to_users.php b/database/migrations/2017_06_19_105842_add_stripe_fields_to_users.php new file mode 100644 index 0000000..1f20e65 --- /dev/null +++ b/database/migrations/2017_06_19_105842_add_stripe_fields_to_users.php @@ -0,0 +1,34 @@ +string('stripe_id')->after('api_key')->nullable(); + $table->string('card_brand')->after('stripe_id')->nullable(); + $table->string('card_last_four')->after('card_brand')->nullable(); + $table->timestamp('trial_ends_at')->after('card_last_four')->nullable(); + }); + + Schema::create('subscriptions', function ($table) { + $table->increments('id'); + $table->integer('account_id'); + $table->string('name'); + $table->string('stripe_id'); + $table->string('stripe_plan'); + $table->integer('quantity'); + $table->timestamp('trial_ends_at')->nullable(); + $table->timestamp('ends_at')->nullable(); + $table->timestamps(); + }); + } +} diff --git a/database/migrations/2017_06_20_121345_add_invitations_statistics.php b/database/migrations/2017_06_20_121345_add_invitations_statistics.php new file mode 100644 index 0000000..42f4c71 --- /dev/null +++ b/database/migrations/2017_06_20_121345_add_invitations_statistics.php @@ -0,0 +1,24 @@ +integer('number_of_invitations_sent')->after('api_key')->nullable(); + }); + + Schema::table('statistics', function ($table) { + $table->integer('number_of_invitations_sent')->after('number_of_kids')->nullable(); + $table->integer('number_of_accounts_with_more_than_one_user')->after('number_of_invitations_sent')->nullable(); + }); + } +} diff --git a/database/migrations/2017_06_22_210813_add_name_order_to_users.php b/database/migrations/2017_06_22_210813_add_name_order_to_users.php new file mode 100644 index 0000000..81810a1 --- /dev/null +++ b/database/migrations/2017_06_22_210813_add_name_order_to_users.php @@ -0,0 +1,20 @@ +string('name_order')->default('firstname_first')->after('contacts_sort_order'); + }); + } +} diff --git a/database/migrations/2017_06_27_134704_create_import_table.php b/database/migrations/2017_06_27_134704_create_import_table.php new file mode 100644 index 0000000..63a45d2 --- /dev/null +++ b/database/migrations/2017_06_27_134704_create_import_table.php @@ -0,0 +1,43 @@ +increments('id'); + $table->integer('account_id'); + $table->integer('user_id'); + $table->string('type')->default('vcard'); + $table->integer('contacts_found')->nullable(); + $table->integer('contacts_skipped')->nullable(); + $table->integer('contacts_imported')->nullable(); + $table->string('filename')->nullable(); + $table->date('started_at')->nullable(); + $table->date('ended_at')->nullable(); + $table->boolean('failed')->default(0); + $table->mediumText('failed_reason')->nullable(); + $table->timestamps(); + }); + + Schema::create('import_job_reports', function (Blueprint $table) { + $table->increments('id'); + $table->integer('account_id'); + $table->integer('user_id'); + $table->integer('import_job_id'); + $table->mediumText('contact_information'); + $table->boolean('skipped'); + $table->string('skip_reason')->nullable(); + $table->timestamps(); + }); + } +} diff --git a/database/migrations/2017_06_29_211725_add_import_job_to_statistics.php b/database/migrations/2017_06_29_211725_add_import_job_to_statistics.php new file mode 100644 index 0000000..6b9127c --- /dev/null +++ b/database/migrations/2017_06_29_211725_add_import_job_to_statistics.php @@ -0,0 +1,19 @@ +integer('number_of_import_jobs')->after('number_of_accounts_with_more_than_one_user')->nullable(); + }); + } +} diff --git a/database/migrations/2017_06_29_230523_add_gravatar_url_to_users.php b/database/migrations/2017_06_29_230523_add_gravatar_url_to_users.php new file mode 100644 index 0000000..30697b5 --- /dev/null +++ b/database/migrations/2017_06_29_230523_add_gravatar_url_to_users.php @@ -0,0 +1,20 @@ +string('gravatar_url')->nullable()->after('avatar_file_name'); + }); + } +} diff --git a/database/migrations/2017_07_02_155736_create_tags_table.php b/database/migrations/2017_07_02_155736_create_tags_table.php new file mode 100644 index 0000000..02a874e --- /dev/null +++ b/database/migrations/2017_07_02_155736_create_tags_table.php @@ -0,0 +1,32 @@ +increments('id'); + $table->integer('account_id'); + $table->string('name'); + $table->string('name_slug'); + $table->mediumText('description')->nullable(); + $table->timestamps(); + }); + + Schema::create('contact_tag', function (Blueprint $table) { + $table->integer('contact_id'); + $table->integer('tag_id'); + $table->integer('account_id'); + $table->timestamps(); + }); + } +} diff --git a/database/migrations/2017_07_04_132743_add_tags_to_statistics.php b/database/migrations/2017_07_04_132743_add_tags_to_statistics.php new file mode 100644 index 0000000..214e707 --- /dev/null +++ b/database/migrations/2017_07_04_132743_add_tags_to_statistics.php @@ -0,0 +1,19 @@ +integer('number_of_tags')->after('number_of_accounts_with_more_than_one_user')->nullable(); + }); + } +} diff --git a/database/migrations/2017_07_09_164312_update_bad_translation_key.php b/database/migrations/2017_07_09_164312_update_bad_translation_key.php new file mode 100644 index 0000000..afd8756 --- /dev/null +++ b/database/migrations/2017_07_09_164312_update_bad_translation_key.php @@ -0,0 +1,19 @@ +where('id', 1) + ->update(['key' => 'just_hung_out']); + } +} diff --git a/database/migrations/2017_07_12_014244_create_calls_table.php b/database/migrations/2017_07_12_014244_create_calls_table.php new file mode 100644 index 0000000..e7941f0 --- /dev/null +++ b/database/migrations/2017_07_12_014244_create_calls_table.php @@ -0,0 +1,25 @@ +increments('id'); + $table->integer('account_id'); + $table->integer('contact_id'); + $table->dateTime('called_at'); + $table->mediumText('content')->nullable(); + $table->timestamps(); + }); + } +} diff --git a/database/migrations/2017_07_17_005012_drop_reminders_count_from_contacts.php b/database/migrations/2017_07_17_005012_drop_reminders_count_from_contacts.php new file mode 100644 index 0000000..5e1ba1c --- /dev/null +++ b/database/migrations/2017_07_17_005012_drop_reminders_count_from_contacts.php @@ -0,0 +1,20 @@ +dropColumn('number_of_reminders'); + }); + } +} diff --git a/database/migrations/2017_07_18_215312_add_danish_kroner_to_currencies_table.php b/database/migrations/2017_07_18_215312_add_danish_kroner_to_currencies_table.php new file mode 100644 index 0000000..fc2aaa2 --- /dev/null +++ b/database/migrations/2017_07_18_215312_add_danish_kroner_to_currencies_table.php @@ -0,0 +1,17 @@ +insert(['iso' => 'DKK', 'name' => 'Danish krone', 'symbol'=>'kr.']); + } +} diff --git a/database/migrations/2017_07_18_215758_add_indian_rupee_to_currencies_table.php b/database/migrations/2017_07_18_215758_add_indian_rupee_to_currencies_table.php new file mode 100644 index 0000000..78d0ab7 --- /dev/null +++ b/database/migrations/2017_07_18_215758_add_indian_rupee_to_currencies_table.php @@ -0,0 +1,17 @@ +insert(['iso' => 'INR', 'name' => 'Indian rupee', 'symbol'=>'₹']); + } +} diff --git a/database/migrations/2017_07_19_094503_add_brazilian_real_to_currencies.php b/database/migrations/2017_07_19_094503_add_brazilian_real_to_currencies.php new file mode 100644 index 0000000..37c9cf0 --- /dev/null +++ b/database/migrations/2017_07_19_094503_add_brazilian_real_to_currencies.php @@ -0,0 +1,17 @@ +insert(['iso' => 'BRL', 'name' => 'Brazilian Real', 'symbol' => 'R$']); + } +} diff --git a/database/migrations/2017_07_22_153209_create_instance_table.php b/database/migrations/2017_07_22_153209_create_instance_table.php new file mode 100644 index 0000000..004d05e --- /dev/null +++ b/database/migrations/2017_07_22_153209_create_instance_table.php @@ -0,0 +1,34 @@ +increments('id'); + $table->string('uuid'); + $table->string('current_version'); + $table->string('latest_version')->nullable(); + $table->mediumText('latest_release_notes')->nullable(); + $table->integer('number_of_versions_since_current_version')->nullable(); + $table->timestamps(); + }); + + $instance = new Instance; + $instance->current_version = config('monica.app_version'); + $instance->latest_version = config('monica.app_version'); + $instance->uuid = Str::uuid(); + $instance->save(); + } +} diff --git a/database/migrations/2017_07_26_220021_change_contacts_table.php b/database/migrations/2017_07_26_220021_change_contacts_table.php new file mode 100644 index 0000000..597a616 --- /dev/null +++ b/database/migrations/2017_07_26_220021_change_contacts_table.php @@ -0,0 +1,72 @@ +boolean('is_significant_other')->after('gender')->default(0); + $table->boolean('is_kid')->after('is_significant_other')->default(0); + $table->dropColumn( + 'has_kids', 'number_of_kids', 'nature_of_relationship' + ); + }); + + Schema::table('significant_others', function ($table) { + $table->integer('temp_contact_id'); + }); + + $significantOthers = DB::table('significant_others')->get(); + + foreach ($significantOthers as $significantOther) { + $contact = new Contact; + $contact->account_id = $significantOther->account_id; + $contact->first_name = $significantOther->first_name; + $contact->gender = $significantOther->gender; + $contact->is_birthdate_approximate = $significantOther->is_birthdate_approximate; + $contact->birthdate = $significantOther->birthdate; + $contact->is_significant_other = 1; + $contact->created_at = $significantOther->created_at; + $contact->updated_at = $significantOther->updated_at; + $contact->save(); + + DB::table('significant_others') + ->where('id', $significantOther->id) + ->update(['temp_contact_id' => $contact->id]); + } + + Schema::create('relationships', function (Blueprint $table) { + $table->increments('id'); + $table->integer('account_id'); + $table->integer('contact_id'); + $table->integer('with_contact_id'); + $table->dateTime('anniversary')->nullable(); + $table->boolean('is_active')->default(1); + $table->string('breakup_reason', 1000)->nullable(); + $table->timestamps(); + }); + + $significantOthers = DB::table('significant_others')->get(); + + foreach ($significantOthers as $significantOther) { + DB::table('relationships')->insert([ + 'account_id' => $significantOther->account_id, + 'contact_id' => $significantOther->contact_id, + 'with_contact_id' => $significantOther->temp_contact_id, + ]); + } + + Schema::drop('significant_others'); + } +} diff --git a/database/migrations/2017_08_02_152838_change_string_to_boolean_for_reminders.php b/database/migrations/2017_08_02_152838_change_string_to_boolean_for_reminders.php new file mode 100644 index 0000000..a648c08 --- /dev/null +++ b/database/migrations/2017_08_02_152838_change_string_to_boolean_for_reminders.php @@ -0,0 +1,44 @@ +boolean('is_a_birthday')->after('is_birthday'); + }); + + $reminders = DB::table('reminders')->get(); + + foreach ($reminders as $reminder) { + if ($reminder->is_birthday == 'true') { + DB::table('reminders') + ->where('id', $reminder->id) + ->update(['is_a_birthday' => 1]); + } else { + DB::table('reminders') + ->where('id', $reminder->id) + ->update(['is_a_birthday' => 0]); + } + } + + Schema::table('reminders', function (Blueprint $table) { + $table->dropColumn('is_birthday'); + }); + + Schema::table('reminders', function ($table) { + $table->renameColumn('is_a_birthday', 'is_birthday'); + }); + } +} diff --git a/database/migrations/2017_08_06_085629_change_events_data.php b/database/migrations/2017_08_06_085629_change_events_data.php new file mode 100644 index 0000000..4b37252 --- /dev/null +++ b/database/migrations/2017_08_06_085629_change_events_data.php @@ -0,0 +1,31 @@ +where('object_type', 'significantother') + ->get(); + + foreach ($events as $event) { + DB::table('events')->where('id', $event->id)->delete(); + } + + $events = DB::table('events') + ->where('object_type', 'kid') + ->get(); + + foreach ($events as $event) { + DB::table('events')->where('id', $event->id)->delete(); + } + } +} diff --git a/database/migrations/2017_08_06_153253_move_kids_to_contacts.php b/database/migrations/2017_08_06_153253_move_kids_to_contacts.php new file mode 100644 index 0000000..2354bed --- /dev/null +++ b/database/migrations/2017_08_06_153253_move_kids_to_contacts.php @@ -0,0 +1,84 @@ +integer('temp_contact_id'); + }); + + Schema::create('offsprings', function (Blueprint $table) { + $table->increments('id'); + $table->integer('account_id'); + $table->integer('contact_id'); + $table->integer('is_the_child_of'); + $table->timestamps(); + }); + + Schema::create('progenitors', function (Blueprint $table) { + $table->increments('id'); + $table->integer('account_id'); + $table->integer('contact_id'); + $table->integer('is_the_parent_of'); + $table->timestamps(); + }); + + // Kids are now contacts - they need to be moved to the contacts table + $kids = DB::table('kids')->get(); + foreach ($kids as $kid) { + $contact = new Contact; + $contact->account_id = $kid->account_id; + $contact->first_name = $kid->first_name; + $contact->gender = $kid->gender; + $contact->is_birthdate_approximate = $kid->is_birthdate_approximate; + $contact->birthdate = $kid->birthdate; + $contact->is_kid = 1; + $contact->created_at = $kid->created_at; + $contact->updated_at = $kid->updated_at; + $contact->save(); + + DB::table('kids') + ->where('id', $kid->id) + ->update(['temp_contact_id' => $contact->id]); + + $reminders = DB::table('reminders') + ->where('about_object_id', $kid->id) + ->where('about_object', 'kid') + ->get(); + + foreach ($reminders as $reminder) { + DB::table('reminders') + ->where('id', $reminder->id) + ->update(['contact_id' => $contact->id]); + } + + DB::table('offsprings')->insert([ + 'account_id' => $kid->account_id, + 'contact_id' => $contact->id, + 'is_the_child_of' => $kid->child_of_contact_id, + ]); + } + + Schema::drop('kids'); + + Schema::table('reminders', function ($table) { + $table->dropColumn([ + 'about_object', + 'about_object_id', + ]); + }); + } +} diff --git a/database/migrations/2017_08_16_041431_add_contact_avatar_location.php b/database/migrations/2017_08_16_041431_add_contact_avatar_location.php new file mode 100644 index 0000000..381ea4c --- /dev/null +++ b/database/migrations/2017_08_16_041431_add_contact_avatar_location.php @@ -0,0 +1,32 @@ +string('avatar_location')->after('avatar_file_name')->default('local'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('contacts', function (Blueprint $table) { + $table->dropColumn('avatar_location'); + }); + } +} diff --git a/database/migrations/2017_08_21_224835_remove_paid_limitations_for_current_users.php b/database/migrations/2017_08_21_224835_remove_paid_limitations_for_current_users.php new file mode 100644 index 0000000..1d8e061 --- /dev/null +++ b/database/migrations/2017_08_21_224835_remove_paid_limitations_for_current_users.php @@ -0,0 +1,24 @@ +boolean('has_access_to_paid_version_for_free')->after('id')->default(false); + }); + + DB::table('accounts') + ->update(['has_access_to_paid_version_for_free' => true]); + } +} diff --git a/database/migrations/2017_09_10_125918_remove_unusued_counters.php b/database/migrations/2017_09_10_125918_remove_unusued_counters.php new file mode 100644 index 0000000..433fab1 --- /dev/null +++ b/database/migrations/2017_09_10_125918_remove_unusued_counters.php @@ -0,0 +1,28 @@ +dropColumn( + 'number_of_notes', + 'number_of_activities', + 'number_of_gifts_ideas', + 'number_of_gifts_received', + 'number_of_gifts_offered', + 'number_of_tasks_in_progress', + 'number_of_tasks_completed' + ); + }); + } +} diff --git a/database/migrations/2017_09_13_095923_add_tracking_table.php b/database/migrations/2017_09_13_095923_add_tracking_table.php new file mode 100644 index 0000000..72b97ac --- /dev/null +++ b/database/migrations/2017_09_13_095923_add_tracking_table.php @@ -0,0 +1,24 @@ +increments('id'); + $table->string('url'); + $table->string('method'); + $table->string('client_ip'); + $table->timestamps(); + }); + } +} diff --git a/database/migrations/2017_09_13_191714_add_partial_notion.php b/database/migrations/2017_09_13_191714_add_partial_notion.php new file mode 100644 index 0000000..85a94eb --- /dev/null +++ b/database/migrations/2017_09_13_191714_add_partial_notion.php @@ -0,0 +1,37 @@ +boolean('is_partial')->after('gender')->default(0); + }); + + $contacts = DB::table('contacts')->get(); + foreach ($contacts as $contact) { + if ($contact->is_kid == 1 or $contact->is_significant_other == 1) { + DB::table('contacts') + ->where('id', $contact->id) + ->update(['is_partial' => 1]); + } + } + + Schema::table('contacts', function (Blueprint $table) { + $table->dropColumn( + 'is_significant_other', + 'is_kid' + ); + }); + } +} diff --git a/database/migrations/2017_10_14_083556_change_gift_column_structure.php b/database/migrations/2017_10_14_083556_change_gift_column_structure.php new file mode 100644 index 0000000..c014f9b --- /dev/null +++ b/database/migrations/2017_10_14_083556_change_gift_column_structure.php @@ -0,0 +1,72 @@ +getDriverName() == 'pgsql') { + //Postgresql does not implicitly convert varchar's to integers, therefore add USING ... + DB::statement('ALTER TABLE gifts ALTER about_object_id TYPE INT USING about_object_id::integer'); + } else { + $table->integer('about_object_id')->change(); + } + }); + + Schema::table('gifts', function ($table) { + $table->dropColumn([ + 'about_object_type', + ]); + }); + + Schema::table('gifts', function ($table) { + $table->boolean('is_is_an_idea')->after('is_an_idea'); + $table->boolean('is_has_been_offered')->after('has_been_offered'); + }); + + $gifts = DB::table('gifts')->get(); + + foreach ($gifts as $gift) { + if ($gift->is_an_idea == 'true') { + DB::table('gifts') + ->where('id', $gift->id) + ->update(['is_is_an_idea' => 1]); + } else { + DB::table('gifts') + ->where('id', $gift->id) + ->update(['is_is_an_idea' => 0]); + } + + if ($gift->has_been_offered == 'true') { + DB::table('gifts') + ->where('id', $gift->id) + ->update(['is_has_been_offered' => 1]); + } else { + DB::table('gifts') + ->where('id', $gift->id) + ->update(['is_has_been_offered' => 0]); + } + } + + Schema::table('gifts', function (Blueprint $table) { + $table->dropColumn('is_an_idea'); + $table->dropColumn('has_been_offered'); + }); + + Schema::table('gifts', function ($table) { + $table->renameColumn('is_is_an_idea', 'is_an_idea'); + $table->renameColumn('is_has_been_offered', 'has_been_offered'); + }); + } +} diff --git a/database/migrations/2017_10_17_170803_change_gift_structure.php b/database/migrations/2017_10_17_170803_change_gift_structure.php new file mode 100644 index 0000000..d975cb4 --- /dev/null +++ b/database/migrations/2017_10_17_170803_change_gift_structure.php @@ -0,0 +1,21 @@ +renameColumn('about_object_id', 'is_for'); + $table->renameColumn('value_in_dollars', 'value'); + }); + } +} diff --git a/database/migrations/2017_10_19_134816_create_activity_contact_table.php b/database/migrations/2017_10_19_134816_create_activity_contact_table.php new file mode 100644 index 0000000..4122501 --- /dev/null +++ b/database/migrations/2017_10_19_134816_create_activity_contact_table.php @@ -0,0 +1,34 @@ +unsignedInteger('activity_id'); + $table->unsignedInteger('contact_id'); + + $table->foreign('activity_id')->references('id')->on('activities')->onDelete('cascade'); + $table->foreign('contact_id')->references('id')->on('contacts')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::drop('activity_contact'); + } +} diff --git a/database/migrations/2017_10_19_135215_move_activities_to_pivot_table.php b/database/migrations/2017_10_19_135215_move_activities_to_pivot_table.php new file mode 100644 index 0000000..2a0419e --- /dev/null +++ b/database/migrations/2017_10_19_135215_move_activities_to_pivot_table.php @@ -0,0 +1,34 @@ +select('id', 'contact_id')->get(); + + foreach ($activities as $activity) { + DB::table('activity_contact')->insert( + ['contact_id' => $activity->contact_id, 'activity_id' => $activity->id] + ); + } + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + DB::table('activity_contact')->truncate(); + } +} diff --git a/database/migrations/2017_10_25_102923_remove_contact_id_activities_table.php b/database/migrations/2017_10_25_102923_remove_contact_id_activities_table.php new file mode 100644 index 0000000..03d1a88 --- /dev/null +++ b/database/migrations/2017_10_25_102923_remove_contact_id_activities_table.php @@ -0,0 +1,21 @@ +dropColumn([ + 'contact_id', + ]); + }); + } +} diff --git a/database/migrations/2017_11_01_122541_add_met_through_to_contacts.php b/database/migrations/2017_11_01_122541_add_met_through_to_contacts.php new file mode 100644 index 0000000..119d1ea --- /dev/null +++ b/database/migrations/2017_11_01_122541_add_met_through_to_contacts.php @@ -0,0 +1,24 @@ +dropColumn('is_first_met_date_approximate'); + }); + + Schema::table('contacts', function (Blueprint $table) { + $table->integer('first_met_through_contact_id')->after('phone_number')->nullable(); + }); + } +} diff --git a/database/migrations/2017_11_02_202601_add_is_dead_to_contacts.php b/database/migrations/2017_11_02_202601_add_is_dead_to_contacts.php new file mode 100644 index 0000000..2084d31 --- /dev/null +++ b/database/migrations/2017_11_02_202601_add_is_dead_to_contacts.php @@ -0,0 +1,21 @@ +boolean('is_dead')->after('is_partial')->default(0); + $table->date('deceased_date')->after('is_dead')->nullable(); + }); + } +} diff --git a/database/migrations/2017_11_10_174654_create_contact_fields_table.php b/database/migrations/2017_11_10_174654_create_contact_fields_table.php new file mode 100644 index 0000000..2582ad3 --- /dev/null +++ b/database/migrations/2017_11_10_174654_create_contact_fields_table.php @@ -0,0 +1,107 @@ +increments('id'); + $table->unsignedInteger('account_id'); + $table->string('name'); + $table->string('fontawesome_icon')->nullable(); + $table->string('protocol')->nullable(); + $table->boolean('delible')->default(1); + $table->string('type')->nullable(); + $table->timestamps(); + + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + }); + + Schema::create('contact_fields', function (Blueprint $table) { + $table->increments('id'); + $table->unsignedInteger('account_id'); + $table->unsignedInteger('contact_id'); + $table->unsignedInteger('contact_field_type_id'); + $table->string('data'); + $table->timestamps(); + + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + $table->foreign('contact_id')->references('id')->on('contacts')->onDelete('cascade'); + $table->foreign('contact_field_type_id')->references('id')->on('contact_field_types')->onDelete('cascade'); + }); + + Schema::create('addresses', function (Blueprint $table) { + $table->increments('id'); + $table->unsignedInteger('account_id'); + $table->unsignedInteger('contact_id'); + $table->string('name')->nullable(); + $table->string('street')->nullable(); + $table->string('city')->nullable(); + $table->string('province')->nullable(); + $table->string('postal_code')->nullable(); + $table->integer('country_id')->nullable(); + $table->timestamps(); + + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + $table->foreign('contact_id')->references('id')->on('contacts')->onDelete('cascade'); + }); + + Schema::create('default_contact_field_types', function (Blueprint $table) { + $table->increments('id'); + $table->string('name'); + $table->string('fontawesome_icon')->nullable(); + $table->string('protocol')->nullable(); + $table->boolean('migrated')->default(0); + $table->boolean('delible')->default(1); + $table->string('type')->nullable(); + $table->timestamps(); + }); + + $id = DB::table('default_contact_field_types')->insertGetId([ + 'name' => 'Email', + 'fontawesome_icon' => 'fa fa-envelope-open-o', + 'protocol' => 'mailto:', + 'delible' => false, + 'type' => 'email', + ]); + + $id = DB::table('default_contact_field_types')->insertGetId([ + 'name' => 'Phone', + 'fontawesome_icon' => 'fa fa-volume-control-phone', + 'protocol' => 'tel:', + 'delible' => false, + 'type' => 'phone', + ]); + + $id = DB::table('default_contact_field_types')->insertGetId([ + 'name' => 'Facebook', + 'fontawesome_icon' => 'fa fa-facebook-official', + ]); + + $id = DB::table('default_contact_field_types')->insertGetId([ + 'name' => 'Twitter', + 'fontawesome_icon' => 'fa fa-twitter-square', + ]); + + $id = DB::table('default_contact_field_types')->insertGetId([ + 'name' => 'Whatsapp', + 'fontawesome_icon' => 'fa fa-whatsapp', + ]); + + $id = DB::table('default_contact_field_types')->insertGetId([ + 'name' => 'Telegram', + 'fontawesome_icon' => 'fa fa-telegram', + 'protocol' => 'telegram:', + ]); + } +} diff --git a/database/migrations/2017_11_10_181043_migrate_contacts_information.php b/database/migrations/2017_11_10_181043_migrate_contacts_information.php new file mode 100644 index 0000000..745bc66 --- /dev/null +++ b/database/migrations/2017_11_10_181043_migrate_contacts_information.php @@ -0,0 +1,90 @@ +get(); + + foreach ($accounts as $account) { + $contacts = DB::table('contacts')->where('account_id', $account->id)->get(); + + $account->populateContactFieldTypeTable(); + + // EMAIL + $emailId = DB::table('contact_field_types')->where('account_id', $account->id) + ->where('type', 'email') + ->first(); + + // PHONE NUMBER + $idPhoneNumber = DB::table('contact_field_types')->where('account_id', $account->id) + ->where('type', 'phone') + ->first(); + + // FACEBOOK + $idFacebook = DB::table('contact_field_types')->where('account_id', $account->id) + ->where('name', 'Facebook') + ->first(); + + // TWITTER + $idTwitter = DB::table('contact_field_types')->where('account_id', $account->id) + ->where('name', 'Twitter') + ->first(); + + foreach ($contacts as $contact) { + if (! is_null($contact->email)) { + DB::table('contact_fields')->insert([ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + 'contact_field_type_id' => $emailId->id, + 'data' => $contact->email, + 'created_at' => now(), + ]); + } + + if (! is_null($contact->phone_number)) { + DB::table('contact_fields')->insert([ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + 'contact_field_type_id' => $idPhoneNumber->id, + 'data' => $contact->phone_number, + 'created_at' => now(), + ]); + } + + if (! is_null($contact->facebook_profile_url)) { + DB::table('contact_fields')->insert([ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + 'contact_field_type_id' => $idFacebook->id, + 'data' => $contact->facebook_profile_url, + 'created_at' => now(), + ]); + } + + if (! is_null($contact->twitter_profile_url)) { + DB::table('contact_fields')->insert([ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + 'contact_field_type_id' => $idTwitter->id, + 'data' => $contact->twitter_profile_url, + 'created_at' => now(), + ]); + } + } + } + + $instance = Instance::first(); + $instance->markDefaultContactFieldTypeAsMigrated(); + } +} diff --git a/database/migrations/2017_11_10_202620_move_addresses_from_contact_to_addresses.php b/database/migrations/2017_11_10_202620_move_addresses_from_contact_to_addresses.php new file mode 100644 index 0000000..772f726 --- /dev/null +++ b/database/migrations/2017_11_10_202620_move_addresses_from_contact_to_addresses.php @@ -0,0 +1,31 @@ +select('account_id', 'id', 'street', 'city', 'province', 'postal_code', 'country_id')->get(); + foreach ($contacts as $contact) { + if (! is_null($contact->street) or ! is_null($contact->city) or ! is_null($contact->province) or ! is_null($contact->postal_code) or ! is_null($contact->country_id)) { + $id = DB::table('addresses')->insertGetId([ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'name' => 'default', + 'street' => (is_null($contact->street) ? null : $contact->street), + 'city' => (is_null($contact->city) ? null : $contact->city), + 'province' => (is_null($contact->province) ? null : $contact->province), + 'postal_code' => (is_null($contact->postal_code) ? null : $contact->postal_code), + 'country_id' => (is_null($contact->country_id) ? null : $contact->country_id), + ]); + } + } + } +} diff --git a/database/migrations/2017_11_10_204035_delete_contact_fields_from_contacts.php b/database/migrations/2017_11_10_204035_delete_contact_fields_from_contacts.php new file mode 100644 index 0000000..c761df0 --- /dev/null +++ b/database/migrations/2017_11_10_204035_delete_contact_fields_from_contacts.php @@ -0,0 +1,29 @@ +dropUnique('unique_for_each_account_email_pair'); + $table->dropColumn('email'); + $table->dropColumn('phone_number'); + $table->dropColumn('street'); + $table->dropColumn('city'); + $table->dropColumn('province'); + $table->dropColumn('postal_code'); + $table->dropColumn('country_id'); + $table->dropColumn('facebook_profile_url'); + $table->dropColumn('twitter_profile_url'); + }); + } +} diff --git a/database/migrations/2017_11_20_115635_change-amount-to-double-on-debts.php b/database/migrations/2017_11_20_115635_change-amount-to-double-on-debts.php new file mode 100644 index 0000000..376b479 --- /dev/null +++ b/database/migrations/2017_11_20_115635_change-amount-to-double-on-debts.php @@ -0,0 +1,32 @@ +decimal('amount')->change(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('debts', function (Blueprint $table) { + $table->integer('amount')->change(); + }); + } +} diff --git a/database/migrations/2017_11_27_083043_add_more_statistics.php b/database/migrations/2017_11_27_083043_add_more_statistics.php new file mode 100644 index 0000000..8d34f20 --- /dev/null +++ b/database/migrations/2017_11_27_083043_add_more_statistics.php @@ -0,0 +1,34 @@ +integer('number_of_activities')->after('number_of_kids'); + $table->integer('number_of_addresses')->after('number_of_activities'); + $table->integer('number_of_api_calls')->after('number_of_addresses'); + $table->integer('number_of_calls')->after('number_of_api_calls'); + $table->integer('number_of_contact_fields')->after('number_of_calls'); + $table->integer('number_of_contact_field_types')->after('number_of_contact_fields'); + $table->integer('number_of_debts')->after('number_of_contact_field_types'); + $table->integer('number_of_entries')->after('number_of_debts'); + $table->integer('number_of_gifts')->after('number_of_entries'); + $table->integer('number_of_oauth_access_tokens')->after('number_of_notes'); + $table->integer('number_of_oauth_clients')->after('number_of_oauth_access_tokens'); + $table->integer('number_of_offsprings')->after('number_of_oauth_clients'); + $table->integer('number_of_progenitors')->after('number_of_offsprings'); + $table->integer('number_of_relationships')->after('number_of_progenitors'); + $table->integer('number_of_subscriptions')->after('number_of_relationships'); + }); + } +} diff --git a/database/migrations/2017_11_27_134403_add_new_avatar_to_contacts.php b/database/migrations/2017_11_27_134403_add_new_avatar_to_contacts.php new file mode 100644 index 0000000..02a305a --- /dev/null +++ b/database/migrations/2017_11_27_134403_add_new_avatar_to_contacts.php @@ -0,0 +1,44 @@ +boolean('has_avatar_bool')->default(0); + }); + + DB::table('contacts') + ->where('has_avatar', 'true') + ->update(['has_avatar_bool' => 1]); + + // dropping the non boolean column + // cant rename the column because there is an enum field in this table + Schema::table('contacts', function (Blueprint $table) { + $table->dropColumn('has_avatar'); + }); + + // change the column to boolean + Schema::table('contacts', function (Blueprint $table) { + $table->boolean('has_avatar')->default(0)->after('food_preferencies'); + }); + + DB::table('contacts') + ->where('has_avatar_bool', 1) + ->update(['has_avatar' => 1]); + + Schema::table('contacts', function (Blueprint $table) { + $table->string('avatar_external_url', 400)->nullable()->after('has_avatar'); + }); + } +} diff --git a/database/migrations/2017_11_27_202857_change_tasks_table_structure.php b/database/migrations/2017_11_27_202857_change_tasks_table_structure.php new file mode 100644 index 0000000..570f9a3 --- /dev/null +++ b/database/migrations/2017_11_27_202857_change_tasks_table_structure.php @@ -0,0 +1,29 @@ +boolean('completed')->default(0)->after('description'); + }); + + DB::table('tasks') + ->where('status', 'completed') + ->update(['completed' => 1]); + + Schema::table('tasks', function (Blueprint $table) { + $table->dropColumn('status'); + }); + } +} diff --git a/database/migrations/2017_12_01_113748_update_notes.php b/database/migrations/2017_12_01_113748_update_notes.php new file mode 100644 index 0000000..d6ddab7 --- /dev/null +++ b/database/migrations/2017_12_01_113748_update_notes.php @@ -0,0 +1,21 @@ +boolean('is_favorited')->default(0)->after('body'); + $table->date('favorited_at')->nullable()->after('is_favorited'); + }); + } +} diff --git a/database/migrations/2017_12_04_164831_create_ages_table.php b/database/migrations/2017_12_04_164831_create_ages_table.php new file mode 100644 index 0000000..b6b7238 --- /dev/null +++ b/database/migrations/2017_12_04_164831_create_ages_table.php @@ -0,0 +1,37 @@ +increments('id'); + $table->integer('account_id'); + $table->integer('contact_id'); + $table->boolean('is_age_based')->default(0); + $table->boolean('is_year_unknown')->default(0); + $table->date('date'); + $table->integer('reminder_id')->nullable(); + $table->timestamps(); + }); + + Schema::table('contacts', function (Blueprint $table) { + $table->integer('birthday_special_date_id')->nullable()->after('last_talked_to'); + $table->integer('deceased_special_date_id')->nullable()->after('is_dead'); + $table->integer('first_met_special_date_id')->nullable()->after('first_met_through_contact_id'); + }); + + Schema::table('reminders', function (Blueprint $table) { + $table->integer('special_date_id')->nullable()->after('contact_id'); + }); + } +} diff --git a/database/migrations/2017_12_04_165421_move_ages_data.php b/database/migrations/2017_12_04_165421_move_ages_data.php new file mode 100644 index 0000000..a3d075d --- /dev/null +++ b/database/migrations/2017_12_04_165421_move_ages_data.php @@ -0,0 +1,123 @@ +select('account_id', 'id', 'first_name', 'is_birthdate_approximate', 'birthdate', 'birthday_reminder_id', 'first_met', 'deceased_date')->get(); + + foreach ($contacts as $contact) { + $specialDateDeceasedDateId = null; + $specialDateBirthdateId = null; + $specialDateFirstMetDateId = null; + + if ($contact->deceased_date) { + $specialDateDeceasedDateId = DB::table('special_dates')->insertGetId([ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'is_age_based' => false, + 'date' => $contact->deceased_date, + 'reminder_id' => null, + 'created_at' => now(), + ]); + } + + $isBirthdayApproximate = $contact->is_birthdate_approximate; + + if ($contact->birthdate) { + switch ($isBirthdayApproximate) { + case 'unknown': + break; + case 'approximate': + $specialDateBirthdateId = DB::table('special_dates')->insertGetId([ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'is_age_based' => true, + 'date' => $contact->birthdate, + 'reminder_id' => $contact->birthday_reminder_id, + 'created_at' => now(), + ]); + + break; + case 'exact': + $specialDateBirthdateId = DB::table('special_dates')->insertGetId([ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'is_age_based' => false, + 'date' => $contact->birthdate, + 'reminder_id' => $contact->birthday_reminder_id, + 'created_at' => now(), + ]); + + break; + } + } + + if ($contact->first_met) { + $specialDateFirstMetDateId = DB::table('special_dates')->insertGetId([ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'is_age_based' => false, + 'date' => $contact->first_met, + 'reminder_id' => null, + 'created_at' => now(), + ]); + } + + if ($contact->birthdate && $specialDateBirthdateId) { + // is title field null? If so, that means it's a birthdate and we need to populate the title field with a title + $reminder = DB::table('reminders')->where('id', $contact->birthday_reminder_id) + ->select('title') + ->get(); + + if ($reminder->isEmpty()) { + DB::table('reminders') + ->where('id', $contact->birthday_reminder_id) + ->update([ + 'special_date_id' => $specialDateBirthdateId, + ]); + } else { + DB::table('reminders') + ->where('id', $contact->birthday_reminder_id) + ->update([ + 'special_date_id' => $specialDateBirthdateId, + 'title' => 'Wish happy birthday to '.$contact->first_name, + ]); + } + } + + DB::table('contacts') + ->where('id', $contact->id) + ->update([ + 'deceased_special_date_id' => $specialDateDeceasedDateId, + 'birthday_special_date_id' => $specialDateBirthdateId, + 'first_met_special_date_id' => $specialDateFirstMetDateId, + ]); + } + + Schema::table('contacts', function ($table) { + $table->dropColumn([ + 'deceased_date', + 'first_met', + 'birthdate', + 'is_birthdate_approximate', + 'birthday_reminder_id', + ]); + }); + + Schema::table('reminders', function ($table) { + $table->dropColumn([ + 'is_birthday', + ]); + }); + } +} diff --git a/database/migrations/2017_12_10_181535_remove_important_dates_table.php b/database/migrations/2017_12_10_181535_remove_important_dates_table.php new file mode 100644 index 0000000..e9c358b --- /dev/null +++ b/database/migrations/2017_12_10_181535_remove_important_dates_table.php @@ -0,0 +1,17 @@ +integer('account_id')->after('contact_id'); + }); + + $activitiesContacts = DB::table('activity_contact')->get(); + + foreach ($activitiesContacts as $activityContact) { + $contact = Contact::find($activityContact->contact_id); + + DB::table('activity_contact') + ->where('activity_id', $activityContact->activity_id) + ->where('contact_id', $activityContact->contact_id) + ->update(['account_id' => $contact->account_id]); + } + } +} diff --git a/database/migrations/2017_12_10_214545_add_last_consulted_at_to_contacts.php b/database/migrations/2017_12_10_214545_add_last_consulted_at_to_contacts.php new file mode 100644 index 0000000..c495016 --- /dev/null +++ b/database/migrations/2017_12_10_214545_add_last_consulted_at_to_contacts.php @@ -0,0 +1,35 @@ +timestamp('last_consulted_at')->nullable()->after('linkedin_profile_url'); + }); + + $contacts = DB::table('contacts')->select('id', 'updated_at')->get(); + + foreach ($contacts as $contact) { + if ($contact->updated_at) { + DB::table('contacts') + ->where('id', $contact->id) + ->update(['last_consulted_at' => $contact->updated_at]); + } else { + DB::table('contacts') + ->where('id', $contact->id) + ->update(['last_consulted_at' => $contact->created_at]); + } + } + } +} diff --git a/database/migrations/2017_12_13_115857_create_day_table.php b/database/migrations/2017_12_13_115857_create_day_table.php new file mode 100644 index 0000000..ed16382 --- /dev/null +++ b/database/migrations/2017_12_13_115857_create_day_table.php @@ -0,0 +1,34 @@ +increments('id'); + $table->integer('account_id'); + $table->date('date'); + $table->integer('rate'); + $table->mediumText('comment')->nullable(); + $table->timestamps(); + }); + + Schema::create('journal_entries', function (Blueprint $table) { + $table->increments('id'); + $table->integer('account_id'); + $table->dateTime('date'); + $table->integer('journalable_id'); + $table->string('journalable_type'); + $table->timestamps(); + }); + } +} diff --git a/database/migrations/2017_12_21_163616_update_journal_entries_with_existing_activities.php b/database/migrations/2017_12_21_163616_update_journal_entries_with_existing_activities.php new file mode 100644 index 0000000..a72e4c6 --- /dev/null +++ b/database/migrations/2017_12_21_163616_update_journal_entries_with_existing_activities.php @@ -0,0 +1,39 @@ +select('account_id', 'date_it_happened', 'id', 'created_at')->get(); + + foreach ($activities as $activity) { + $journalEntryID = DB::table('journal_entries')->insertGetId([ + 'account_id' => $activity->account_id, + 'date' => $activity->date_it_happened, + 'journalable_id' => $activity->id, + 'journalable_type' => 'App\Models\Account\Activity', + 'created_at' => $activity->created_at, + ]); + } + + $entries = DB::table('entries')->select('account_id', 'created_at', 'id')->get(); + + foreach ($entries as $entry) { + $journalEntryID = DB::table('journal_entries')->insertGetId([ + 'account_id' => $entry->account_id, + 'date' => $entry->created_at, + 'journalable_id' => $entry->id, + 'journalable_type' => 'App\Models\Journal\Entry', + 'created_at' => $entry->created_at, + ]); + } + } +} diff --git a/database/migrations/2017_12_21_170327_add_google2fa_secret_to_users.php b/database/migrations/2017_12_21_170327_add_google2fa_secret_to_users.php new file mode 100644 index 0000000..71f0969 --- /dev/null +++ b/database/migrations/2017_12_21_170327_add_google2fa_secret_to_users.php @@ -0,0 +1,31 @@ +string('google2fa_secret')->after('remember_token')->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('users', function ($table) { + $table->dropColumn('google2fa_secret'); + }); + } +} diff --git a/database/migrations/2017_12_24_115641_create_pets_table.php b/database/migrations/2017_12_24_115641_create_pets_table.php new file mode 100644 index 0000000..72f77df --- /dev/null +++ b/database/migrations/2017_12_24_115641_create_pets_table.php @@ -0,0 +1,45 @@ +increments('id'); + $table->integer('account_id'); + $table->integer('contact_id'); + $table->integer('pet_category_id'); + $table->string('name')->nullable(); + $table->timestamps(); + }); + + Schema::create('pet_categories', function (Blueprint $table) { + $table->increments('id'); + $table->string('name'); + $table->boolean('is_common'); + $table->timestamps(); + }); + + DB::table('pet_categories')->insert(['name' => 'reptile', 'is_common' => false]); + DB::table('pet_categories')->insert(['name' => 'bird', 'is_common' => false]); + DB::table('pet_categories')->insert(['name' => 'cat', 'is_common' => true]); + DB::table('pet_categories')->insert(['name' => 'dog', 'is_common' => true]); + DB::table('pet_categories')->insert(['name' => 'fish', 'is_common' => true]); + DB::table('pet_categories')->insert(['name' => 'hamster', 'is_common' => false]); + DB::table('pet_categories')->insert(['name' => 'horse', 'is_common' => false]); + DB::table('pet_categories')->insert(['name' => 'rabbit', 'is_common' => false]); + DB::table('pet_categories')->insert(['name' => 'rat', 'is_common' => false]); + DB::table('pet_categories')->insert(['name' => 'small_animal', 'is_common' => false]); + DB::table('pet_categories')->insert(['name' => 'other', 'is_common' => false]); + } +} diff --git a/database/migrations/2017_12_31_114224_add_dashboard_tab_to_users.php b/database/migrations/2017_12_31_114224_add_dashboard_tab_to_users.php new file mode 100644 index 0000000..6e42bd9 --- /dev/null +++ b/database/migrations/2017_12_31_114224_add_dashboard_tab_to_users.php @@ -0,0 +1,20 @@ +string('dashboard_active_tab')->default('calls')->after('invited_by_user_id'); + }); + } +} diff --git a/database/migrations/2018_01_15_105858_create_additional_reminders_table.php b/database/migrations/2018_01_15_105858_create_additional_reminders_table.php new file mode 100644 index 0000000..e9f771d --- /dev/null +++ b/database/migrations/2018_01_15_105858_create_additional_reminders_table.php @@ -0,0 +1,68 @@ +increments('id'); + $table->integer('account_id'); + $table->integer('contact_id'); + $table->integer('reminder_id')->nullable(); + $table->datetime('trigger_date'); + $table->integer('scheduled_number_days_before')->nullable(); + $table->timestamps(); + }); + + Schema::create('reminders_sent', function (Blueprint $table) { + $table->increments('id'); + $table->integer('account_id'); + $table->integer('contact_id'); + $table->integer('reminder_id')->nullable(); + $table->mediumText('title'); + $table->longText('description'); + $table->longText('html_sent_content'); + $table->datetime('sent_date'); + $table->integer('scheduled_number_days_before')->nullable(); + $table->timestamps(); + }); + + Schema::create('reminder_rules', function (Blueprint $table) { + $table->increments('id'); + $table->integer('account_id'); + $table->integer('number_of_days_before'); + $table->boolean('active')->default(true); + $table->timestamps(); + }); + + Schema::table('accounts', function (Blueprint $table) { + $table->string('default_time_reminder_is_sent')->after('number_of_invitations_sent')->default('12:00'); + }); + + $accounts = DB::table('accounts')->select('id')->get(); + foreach ($accounts as $account) { + DB::table('reminder_rules')->insert([ + ['account_id' => $account->id, 'number_of_days_before' => 7], + ['account_id' => $account->id, 'number_of_days_before' => 30], + ]); + } + + // Create notifications for existing reminders + // Only create notifications for reminders that are not weekly based + $reminders = Reminder::where('frequency_type', '!=', 'week')->get(); + foreach ($reminders as $reminder) { + $reminder->scheduleNotifications($reminder->calculateNextExpectedDate(), $reminder->account->users()->first()); + } + } +} diff --git a/database/migrations/2018_01_16_203358_add_gift_received.php b/database/migrations/2018_01_16_203358_add_gift_received.php new file mode 100644 index 0000000..acbf878 --- /dev/null +++ b/database/migrations/2018_01_16_203358_add_gift_received.php @@ -0,0 +1,21 @@ +boolean('has_been_received')->nullable()->after('has_been_offered'); + $table->datetime('date_received')->nullable()->after('date_offered'); + }); + } +} diff --git a/database/migrations/2018_01_16_212320_rename_gift_columns.php b/database/migrations/2018_01_16_212320_rename_gift_columns.php new file mode 100644 index 0000000..7f4da90 --- /dev/null +++ b/database/migrations/2018_01_16_212320_rename_gift_columns.php @@ -0,0 +1,21 @@ +renameColumn('date_offered', 'offered_at'); + $table->renameColumn('date_received', 'received_at'); + }); + } +} diff --git a/database/migrations/2018_01_17_230820_add_gift_tab_view_to_users.php b/database/migrations/2018_01_17_230820_add_gift_tab_view_to_users.php new file mode 100644 index 0000000..9063d8e --- /dev/null +++ b/database/migrations/2018_01_17_230820_add_gift_tab_view_to_users.php @@ -0,0 +1,20 @@ +string('gifts_active_tab')->default('ideas')->after('dashboard_active_tab'); + }); + } +} diff --git a/database/migrations/2018_01_27_014146_add_custom_gender.php b/database/migrations/2018_01_27_014146_add_custom_gender.php new file mode 100644 index 0000000..99ae82b --- /dev/null +++ b/database/migrations/2018_01_27_014146_add_custom_gender.php @@ -0,0 +1,62 @@ +increments('id'); + $table->integer('account_id'); + $table->string('name'); + $table->timestamps(); + }); + + Schema::table('contacts', function (Blueprint $table) { + $table->integer('gender_id')->after('gender'); + }); + + $accounts = DB::table('accounts')->select('id')->get(); + foreach ($accounts as $account) { + $user = DB::table('users')->select('locale')->where('account_id', $account->id)->first(); + + if (! $user) { + continue; + } + App::setLocale($user->locale); + + $male = DB::table('genders')->insertGetId(['account_id' => $account->id, 'name' => trans('app.gender_male')]); + $female = DB::table('genders')->insertGetId(['account_id' => $account->id, 'name' => trans('app.gender_female')]); + $none = DB::table('genders')->insertGetId(['account_id' => $account->id, 'name' => trans('app.gender_none')]); + + $contacts = DB::table('contacts')->select('id', 'gender')->where('account_id', $account->id)->get(); + foreach ($contacts as $contact) { + if ($contact->gender == 'male') { + DB::table('contacts')->where('id', $contact->id)->update(['gender_id' => $male]); + } + + if ($contact->gender == 'female') { + DB::table('contacts')->where('id', $contact->id)->update(['gender_id' => $female]); + } + + if ($contact->gender == 'none') { + DB::table('contacts')->where('id', $contact->id)->update(['gender_id' => $none]); + } + } + } + + Schema::table('contacts', function (Blueprint $table) { + $table->dropColumn('gender'); + }); + } +} diff --git a/database/migrations/2018_02_25_202752_change_locale_in_db.php b/database/migrations/2018_02_25_202752_change_locale_in_db.php new file mode 100644 index 0000000..0bcced8 --- /dev/null +++ b/database/migrations/2018_02_25_202752_change_locale_in_db.php @@ -0,0 +1,23 @@ +where('locale', 'pt-br') + ->update(['locale' => 'pt']); + + DB::table('users') + ->where('locale', 'cz') + ->update(['locale' => 'cs']); + } +} diff --git a/database/migrations/2018_02_28_223747_update_notification_table.php b/database/migrations/2018_02_28_223747_update_notification_table.php new file mode 100644 index 0000000..4a6a9b1 --- /dev/null +++ b/database/migrations/2018_02_28_223747_update_notification_table.php @@ -0,0 +1,21 @@ +integer('delete_after_number_of_emails_sent')->default(0)->after('reminder_id'); + $table->integer('number_of_emails_sent')->default(0)->after('delete_after_number_of_emails_sent'); + }); + } +} diff --git a/database/migrations/2018_03_03_204440_create_relationship_type_table.php b/database/migrations/2018_03_03_204440_create_relationship_type_table.php new file mode 100644 index 0000000..4662637 --- /dev/null +++ b/database/migrations/2018_03_03_204440_create_relationship_type_table.php @@ -0,0 +1,63 @@ +increments('id'); + $table->string('name'); + $table->boolean('delible')->default(0); + $table->boolean('migrated')->default(0); + $table->timestamps(); + }); + + Schema::create('default_relationship_types', function (Blueprint $table) { + $table->increments('id'); + $table->string('name'); + $table->string('name_reverse_relationship'); + $table->integer('relationship_type_group_id'); + $table->boolean('delible')->default(0); + $table->boolean('migrated')->default(0); + $table->timestamps(); + }); + + // Create new table structure + Schema::create('relationship_types', function (Blueprint $table) { + $table->increments('id'); + $table->integer('account_id'); + $table->string('name'); + $table->string('name_reverse_relationship'); + $table->integer('relationship_type_group_id'); + $table->boolean('delible')->default(0); + $table->timestamps(); + }); + + Schema::create('relationship_type_groups', function (Blueprint $table) { + $table->increments('id'); + $table->integer('account_id'); + $table->string('name'); + $table->boolean('delible')->default(0); + $table->timestamps(); + }); + + Schema::create('temp_relationships_table', function (Blueprint $table) { + $table->increments('id'); + $table->integer('account_id'); + $table->integer('relationship_type_id'); + $table->integer('contact_is'); + $table->string('relationship_type_name'); + $table->integer('of_contact'); + $table->timestamps(); + }); + } +} diff --git a/database/migrations/2018_03_18_085815_populate_default_relationship_type_tables.php b/database/migrations/2018_03_18_085815_populate_default_relationship_type_tables.php new file mode 100644 index 0000000..498cbcc --- /dev/null +++ b/database/migrations/2018_03_18_085815_populate_default_relationship_type_tables.php @@ -0,0 +1,166 @@ +insertGetId([ + 'name' => 'love', + ]); + + DB::table('default_relationship_types')->insert([ + [ + 'name' => 'partner', + 'name_reverse_relationship' => 'partner', + 'relationship_type_group_id' => $id, + ], + [ + 'name' => 'spouse', + 'name_reverse_relationship' => 'spouse', + 'relationship_type_group_id' => $id, + ], + [ + 'name' => 'date', + 'name_reverse_relationship' => 'date', + 'relationship_type_group_id' => $id, + ], + [ + 'name' => 'lover', + 'name_reverse_relationship' => 'lover', + 'relationship_type_group_id' => $id, + ], + [ + 'name' => 'inlovewith', + 'name_reverse_relationship' => 'lovedby', + 'relationship_type_group_id' => $id, + ], + [ + 'name' => 'lovedby', + 'name_reverse_relationship' => 'inlovewith', + 'relationship_type_group_id' => $id, + ], ]); + + DB::table('default_relationship_types')->insertGetId([ + 'name' => 'ex', + 'name_reverse_relationship' => 'ex', + 'relationship_type_group_id' => $id, + ]); + + // Family type + $id = DB::table('default_relationship_type_groups')->insertGetId([ + 'name' => 'family', + ]); + + DB::table('default_relationship_types')->insert([ + [ + 'name' => 'parent', + 'name_reverse_relationship' => 'child', + 'relationship_type_group_id' => $id, + ], + [ + 'name' => 'child', + 'name_reverse_relationship' => 'parent', + 'relationship_type_group_id' => $id, + ], + [ + 'name' => 'sibling', + 'name_reverse_relationship' => 'sibling', + 'relationship_type_group_id' => $id, + ], + [ + 'name' => 'grandparent', + 'name_reverse_relationship' => 'grandchild', + 'relationship_type_group_id' => $id, + ], + [ + 'name' => 'grandchild', + 'name_reverse_relationship' => 'grandparent', + 'relationship_type_group_id' => $id, + ], + [ + 'name' => 'uncle', + 'name_reverse_relationship' => 'nephew', + 'relationship_type_group_id' => $id, + ], + [ + 'name' => 'nephew', + 'name_reverse_relationship' => 'uncle', + 'relationship_type_group_id' => $id, + ], + [ + 'name' => 'cousin', + 'name_reverse_relationship' => 'cousin', + 'relationship_type_group_id' => $id, + ], + [ + 'name' => 'godfather', + 'name_reverse_relationship' => 'godson', + 'relationship_type_group_id' => $id, + ], + [ + 'name' => 'godson', + 'name_reverse_relationship' => 'godfather', + 'relationship_type_group_id' => $id, + ], ]); + + // Friend + $id = DB::table('default_relationship_type_groups')->insertGetId([ + 'name' => 'friend', + ]); + + DB::table('default_relationship_types')->insert([ + [ + 'name' => 'friend', + 'name_reverse_relationship' => 'friend', + 'relationship_type_group_id' => $id, + ], + [ + 'name' => 'bestfriend', + 'name_reverse_relationship' => 'bestfriend', + 'relationship_type_group_id' => $id, + ], ]); + + // Work + $id = DB::table('default_relationship_type_groups')->insertGetId([ + 'name' => 'work', + ]); + + DB::table('default_relationship_types')->insert([ + [ + 'name' => 'colleague', + 'name_reverse_relationship' => 'colleague', + 'relationship_type_group_id' => $id, + ], + [ + 'name' => 'boss', + 'name_reverse_relationship' => 'subordinate', + 'relationship_type_group_id' => $id, + ], + [ + 'name' => 'subordinate', + 'name_reverse_relationship' => 'boss', + 'relationship_type_group_id' => $id, + ], + [ + 'name' => 'mentor', + 'name_reverse_relationship' => 'protege', + 'relationship_type_group_id' => $id, + ], + [ + 'name' => 'protege', + 'name_reverse_relationship' => 'mentor', + 'relationship_type_group_id' => $id, + ], ]); + } +} diff --git a/database/migrations/2018_03_18_090209_populate_relationship_type_tables_with_default_values.php b/database/migrations/2018_03_18_090209_populate_relationship_type_tables_with_default_values.php new file mode 100644 index 0000000..fa978d3 --- /dev/null +++ b/database/migrations/2018_03_18_090209_populate_relationship_type_tables_with_default_values.php @@ -0,0 +1,29 @@ +populateRelationshipTypeGroupsTable(); + $account->populateRelationshipTypesTable(); + } + }); + + DB::table('default_relationship_types') + ->update(['migrated' => 1]); + + DB::table('default_relationship_type_groups') + ->update(['migrated' => 1]); + } +} diff --git a/database/migrations/2018_03_18_090345_migrate_current_relationship_table_to_new_relationship_structure.php b/database/migrations/2018_03_18_090345_migrate_current_relationship_table_to_new_relationship_structure.php new file mode 100644 index 0000000..eaccb24 --- /dev/null +++ b/database/migrations/2018_03_18_090345_migrate_current_relationship_table_to_new_relationship_structure.php @@ -0,0 +1,57 @@ +getRelationshipTypeByType('partner')->id; + $itemsToDelete = []; + + $relationships = Relationship::where('account_id', $account->id)->get()->keyBy('id'); + + foreach ($relationships as $relationship) { + foreach ($relationships as $bilateralRelationship) { + if ($relationship->contact_id == $bilateralRelationship->with_contact_id + && $relationship->with_contact_id == $bilateralRelationship->contact_id) { + $relationships->forget($bilateralRelationship->id); + } + } + + DB::table('temp_relationships_table')->insert([ + [ + 'account_id' => $account->id, + 'contact_is' => $relationship->contact_id, + 'relationship_type_name' => 'partner', + 'of_contact' => $relationship->with_contact_id, + 'relationship_type_id' => $relationshipTypeId, + ], + [ + 'account_id' => $account->id, + 'contact_is' => $relationship->with_contact_id, + 'relationship_type_name' => 'partner', + 'of_contact' => $relationship->contact_id, + 'relationship_type_id' => $relationshipTypeId, + ], + ]); + } + } + }); + + Schema::dropIfExists('relationships'); + + Schema::rename('temp_relationships_table', 'relationships'); + } +} diff --git a/database/migrations/2018_03_24_083258_migrate_offsprings.php b/database/migrations/2018_03_24_083258_migrate_offsprings.php new file mode 100644 index 0000000..4320e5c --- /dev/null +++ b/database/migrations/2018_03_24_083258_migrate_offsprings.php @@ -0,0 +1,47 @@ +getRelationshipTypeByType('child')->id; + $relationshipParentTypeId = $account->getRelationshipTypeByType('parent')->id; + $offsprings = DB::table('offsprings')->where('account_id', $account->id)->get(); + + foreach ($offsprings as $offspring) { + DB::table('relationships')->insert([ + [ + 'account_id' => $account->id, + 'contact_is' => $offspring->is_the_child_of, + 'relationship_type_name' => 'child', + 'of_contact' => $offspring->contact_id, + 'relationship_type_id' => $relationshipChildTypeId, + ], + [ + 'account_id' => $account->id, + 'contact_is' => $offspring->contact_id, + 'relationship_type_name' => 'parent', + 'of_contact' => $offspring->is_the_child_of, + 'relationship_type_id' => $relationshipParentTypeId, + ], + ]); + } + } + }); + + Schema::dropIfExists('offsprings'); + Schema::dropIfExists('progenitors'); + } +} diff --git a/database/migrations/2018_04_04_220850_create_default_modules_table.php b/database/migrations/2018_04_04_220850_create_default_modules_table.php new file mode 100644 index 0000000..a3de482 --- /dev/null +++ b/database/migrations/2018_04_04_220850_create_default_modules_table.php @@ -0,0 +1,93 @@ +increments('id'); + $table->string('key'); + $table->string('translation_key'); + $table->boolean('delible')->default(0); + $table->boolean('active')->default(1); + $table->boolean('migrated')->default(0); + $table->timestamps(); + }); + + DB::table('default_contact_modules')->insert([ + [ + 'key' => 'love_relationships', + 'translation_key' => 'app.relationship_type_group_love', + ], + [ + 'key' => 'family_relationships', + 'translation_key' => 'app.relationship_type_group_family', + ], + [ + 'key' => 'other_relationships', + 'translation_key' => 'app.relationship_type_group_other', + ], + [ + 'key' => 'pets', + 'translation_key' => 'people.pets_title', + ], + [ + 'key' => 'contact_information', + 'translation_key' => 'people.section_contact_information', + ], + [ + 'key' => 'addresses', + 'translation_key' => 'people.contact_address_title', + ], + [ + 'key' => 'how_you_met', + 'translation_key' => 'people.introductions_sidebar_title', + ], + [ + 'key' => 'work_information', + 'translation_key' => 'people.work_information', + ], + [ + 'key' => 'food_preferences', + 'translation_key' => 'people.food_preferencies_title', + ], + [ + 'key' => 'notes', + 'translation_key' => 'people.section_personal_notes', + ], + [ + 'key' => 'phone_calls', + 'translation_key' => 'people.call_title', + ], + [ + 'key' => 'activities', + 'translation_key' => 'people.activity_title', + ], + [ + 'key' => 'reminders', + 'translation_key' => 'people.section_personal_reminders', + ], + [ + 'key' => 'tasks', + 'translation_key' => 'people.section_personal_tasks', + ], + [ + 'key' => 'gifts', + 'translation_key' => 'people.gifts_title', + ], + [ + 'key' => 'debts', + 'translation_key' => 'people.debt_title', + ], ]); + } +} diff --git a/database/migrations/2018_04_04_222608_create_account_modules_table.php b/database/migrations/2018_04_04_222608_create_account_modules_table.php new file mode 100644 index 0000000..19b8138 --- /dev/null +++ b/database/migrations/2018_04_04_222608_create_account_modules_table.php @@ -0,0 +1,26 @@ +increments('id'); + $table->integer('account_id'); + $table->string('key'); + $table->string('translation_key'); + $table->boolean('active')->default(1); + $table->boolean('delible')->default(0); + $table->timestamps(); + }); + } +} diff --git a/database/migrations/2018_04_10_205655_fix_production_error.php b/database/migrations/2018_04_10_205655_fix_production_error.php new file mode 100644 index 0000000..733a964 --- /dev/null +++ b/database/migrations/2018_04_10_205655_fix_production_error.php @@ -0,0 +1,40 @@ +account; + if (is_null($account)) { + $usersWithoutAccount->push($user); + } + } + }); + + foreach ($usersWithoutAccount as $user) { + // creation of a new account + $account = new Account; + $account->api_key = Str::random(30); + $account->created_at = now(); + $account->save(); + + $user->account_id = $account->id; + $user->save(); + } + } +} diff --git a/database/migrations/2018_04_10_222515_migrate-modules.php b/database/migrations/2018_04_10_222515_migrate-modules.php new file mode 100644 index 0000000..4ed7e6d --- /dev/null +++ b/database/migrations/2018_04_10_222515_migrate-modules.php @@ -0,0 +1,28 @@ +execute([ + 'account_id' => $account->id, + 'migrate_existing_data' => false, + ]); + } + }); + + DB::table('default_contact_modules')->update(['migrated' => 1]); + } +} diff --git a/database/migrations/2018_04_13_131008_fix-contacts-data.php b/database/migrations/2018_04_13_131008_fix-contacts-data.php new file mode 100644 index 0000000..5cb87c2 --- /dev/null +++ b/database/migrations/2018_04_13_131008_fix-contacts-data.php @@ -0,0 +1,43 @@ +get(); + $lineContactIsToDelete = collect([]); + $lineOfContactToDelete = collect([]); + + foreach ($relationships as $relationship) { + $contact = DB::table('contacts')->where('id', $relationship->contact_is)->first(); + if (! $contact) { + $lineContactIsToDelete->push($relationship); + } + + $contact = DB::table('contacts')->where('id', $relationship->of_contact)->first(); + if (! $contact) { + $lineOfContactToDelete->push($relationship); + } + } + + foreach ($lineContactIsToDelete as $relationship) { + DB::table('relationships')->where('id', $relationship->id)->delete(); + } + + foreach ($lineOfContactToDelete as $relationship) { + DB::table('relationships')->where('id', $relationship->id)->delete(); + } + } +} diff --git a/database/migrations/2018_04_13_205231_create_changes_table.php b/database/migrations/2018_04_13_205231_create_changes_table.php new file mode 100644 index 0000000..1f2caf0 --- /dev/null +++ b/database/migrations/2018_04_13_205231_create_changes_table.php @@ -0,0 +1,30 @@ +increments('id'); + $table->mediumText('description'); + $table->timestamps(); + }); + + Schema::create('changelog_user', function (Blueprint $table) { + $table->integer('changelog_id'); + $table->integer('user_id'); + $table->boolean('read')->default(0); + $table->boolean('upvote')->default(0); + $table->timestamps(); + }); + } +} diff --git a/database/migrations/2018_04_14_081052_fix_wrong_gender.php b/database/migrations/2018_04_14_081052_fix_wrong_gender.php new file mode 100644 index 0000000..ad90005 --- /dev/null +++ b/database/migrations/2018_04_14_081052_fix_wrong_gender.php @@ -0,0 +1,25 @@ +where('account_id', '!=', 0)->get(); + foreach ($contacts as $contact) { + $account = Account::find($contact->account_id); + $firstGender = Gender::where('account_id', $account->id)->first(); + $contact->gender_id = $firstGender->id; + $contact->save(); + } + } +} diff --git a/database/migrations/2018_04_19_190239_stay_in_touch.php b/database/migrations/2018_04_19_190239_stay_in_touch.php new file mode 100644 index 0000000..d156a93 --- /dev/null +++ b/database/migrations/2018_04_19_190239_stay_in_touch.php @@ -0,0 +1,21 @@ +integer('stay_in_touch_frequency')->nullable()->after('last_talked_to'); + $table->datetime('stay_in_touch_trigger_date')->nullable()->after('stay_in_touch_frequency'); + }); + } +} diff --git a/database/migrations/2018_05_06_061227_external_countries.php b/database/migrations/2018_05_06_061227_external_countries.php new file mode 100644 index 0000000..c9315f4 --- /dev/null +++ b/database/migrations/2018_05_06_061227_external_countries.php @@ -0,0 +1,75 @@ +char('country', 3)->after('country_id')->nullable(); + }); + + Address::chunk(200, function ($addresses) { + foreach ($addresses as $addresse) { + $iso = DB::table('countries')->where('id', $addresse->country_id)->value('iso'); + $addresse->update(['country' => mb_strtoupper($this->fixIso($iso))]); + } + }); + + Schema::table('addresses', function (Blueprint $table) { + $table->dropColumn('country_id'); + }); + Schema::dropIfExists('countries'); + } + + private function fixIso($iso) + { + switch ($iso) { + case 'ct': + // Cyprus + return 'CY'; + break; + } + + return $iso; + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('addresses', function (Blueprint $table) { + $table->integer('country_id')->after('country')->nullable(); + }); + + Schema::create('countries', function (Blueprint $table) { + $table->increments('id'); + $table->string('iso'); + $table->string('country'); + }); + + Address::chunk(200, function ($addresses) { + foreach ($addresses as $addresse) { + $id = DB::table('countries')->where('iso', mb_strtolower($addresse->country))->value('id'); + $addresse->update(['country_id' => $id]); + } + }); + + Schema::table('addresses', function (Blueprint $table) { + $table->dropColumn('country'); + }); + } +} diff --git a/database/migrations/2018_05_07_070458_create_terms_table.php b/database/migrations/2018_05_07_070458_create_terms_table.php new file mode 100644 index 0000000..4142c29 --- /dev/null +++ b/database/migrations/2018_05_07_070458_create_terms_table.php @@ -0,0 +1,148 @@ +increments('id'); + $table->string('term_version'); + $table->mediumText('term_content'); + $table->string('privacy_version'); + $table->mediumText('privacy_content'); + $table->timestamps(); + }); + + Schema::create('term_user', function (Blueprint $table) { + $table->unsignedInteger('account_id'); + $table->unsignedInteger('user_id'); + $table->unsignedInteger('term_id'); + $table->string('ip_address')->nullable(); + $table->timestamps(); + }); + + $privacy = ' +Monica is an open source project. The hosted version has a premium plan that let us collect money so we can pay for the servers and additional servers, but the main goal is not to make money (otherwise we wouldn’t have opened source it). + +Monica comes in two flavors: you can either use our hosted version, or download it and run it yourself. In the latter case, we do not track anything at all. We don’t know that you’ve even downloaded the product. Do whatever you want with it (but respect your local laws). + +When you create your account on our hosted version, you are giving the site information about yourself that we collect. This includes your name, your email address and your password, that is encrypted before being stored. We do not store any other personal information. + +When you login to the service, we are using cookies to remember your login credentials. This is the only use we do with the cookies. + +Monica runs on Linode and we are the only ones, apart from Linode’s employees, who have access to those servers. + +We do hourly backups of the database. + +Your password is encrypted with bcrypt, a password hashing algorithm that is highly secure. You can also activate two factor authentication on your account if you need an extra layer of security. Apart from those encryptions mechanism, your data is not encrypted in the database. If someone gets access to the database, they will be able to read your data. We do our best to make sure that this will never happen, but it can happen. + +If a data breach happens, we will contact the users who are affected to warn them about the breach. + +Transactional emails are dserved through Postmark. + +We use an open source tool called Sentry to track errors that happen in production. Their service records the errors, but they don’t have access to any information apart the account ID, which lets me debug what’s going on. + +The site does not currently and will never show ads. It also does not, and don’t intend to, sell data to a third party, with or without your consent. We are just against this. Fuck ads. + +We do no use any tracking third parties, like Google Analytics or Intercom, that track user behaviours or data, neither on the marketing site or the hosted version. We are deeply against their principles as they would use those data to profile you, which we are totally against. + +All the data you put on Monica belongs to you. We do not have any rights on it. Please don’t put illegal stuff on it, otherwise we’d be in trouble. + +All the information about the contacts you put on Monica are private to you. We do not cross link information between accounts or use one information in an account to populate another account (unlike Facebook for instance). + +We use Stripe to collect payments made to access the paid version. We do not store credit card information or anything concerning the transactions themselves on our servers. However, as per the open source library we use to process the payments (Laravel Cashier), we store the last 4 digits of the credit card, the brand name (VISA or MasterCard). As a user, you are identified on Stripe by a random number that they generate and use. + +Regarding the payments, you can downgrade to the free plan whenever you like. When you do, Stripe is automatically updated and we have no way to charge you again, even if we would like to. The less we deal with payment information, the happier we are. + +You can export your data at any time. You can also use the API to export all your data if you know how to do it. You can also request that we process this ourselves and send it to you. Your data will be exported in the SQL format. + +When you close your account, we immediately destroy all your personal information and don’t keep any backup. While you have control over this, we can delete an account for you if you ask us. + +In certain situations, we may be required to disclose peronal data in response to lawful requests by public authorities, including to met national security or law enforcements requirements. We just hope that this never happens. + +If you violate the terms of use we will terminate your account and notify you about it. However if you follow the "don’t be a dick" policy, nothing should ever happen to you and we’ll all be happy. + +Monica uses only open-source projects that are mainly hosted on Github. + +We will update this privacy policy as soon as we introduce new information practices. If we do, we will send an email to the email address specified in your account. We will never be a dick about it and will never, ever, introduce something in what we do that will affect your right to the absolute privacy.'; + + $term = ' +Scope of service +Monica supports the following browsers: + +Internet Explorer (11+) +Firefox (50+) +Chrome (latest) +Safari (latest) +I do not guarantee that the site will work with other browsers, but it’s very likely that it will just work. + +Rights +You don’t have to provide your real name when you register to an account. You do however need a valid email address if you want to upgrade your account to the paid version, or receive reminders by email. + +You have the right to close your account at any time. + +You have the right to export your data at any time, in the SQL format. + +Your data will not be intentionally shown to other users or shared with third parties. + +Your personal data will not be shared with anyone without your consent. + +Your data is backed up every hour. + +If the site ceases operation, you will receive an opportunity to export all your data before the site dies. + +Any new features that affect privacy will be strictly opt-in. + +Responsibilities +You will not use the site to store illegal information or data under the Canadian law (or any law). + +You have to be at least 18+ to create an account and use the site. + +You must not abuse the site by knowingly posting malicious code that could harm you or the other users. + +You must only use the site to do things that are widely accepted as morally good. + +You may not make automated requests to the site. + +You may not abuse the invitation system. + +You are responsible for keeping your account secure. + +I reserve the right to close accounts that abuse the system (thousands of contacts with hundred of thousands of reminders for instance) or use it in an unreasonable manner. + +Other important legal stuff +Though I want to provide a great service, there are certain things about the service I cannot promise. For example, the services and software are provided “as-is”, at your own risk, without express or implied warranty or condition of any kind. I also disclaim any warranties of merchantability, fitness for a particular purpose or non-infringement. Monica will have no responsibility for any harm to your computer system, loss or corruption of data, or other harm that results from your access to or use of the Services or Software. + +These Terms can change at any time, but I’ll never be a dick about it. Running this site is a dream come true to me, and I hope I’ll be able to run it as long as I can. + '; + + $id = DB::table('terms')->insertGetId([ + 'privacy_content' => $privacy, + 'privacy_version' => '2', + 'term_content' => $term, + 'term_version' => '2', + 'created_at' => '2018-04-12', + ]); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('terms'); + Schema::dropIfExists('term_user'); + } +} diff --git a/database/migrations/2018_05_13_110706_add_ex_wife_husband_relationship.php b/database/migrations/2018_05_13_110706_add_ex_wife_husband_relationship.php new file mode 100644 index 0000000..551b9cc --- /dev/null +++ b/database/migrations/2018_05_13_110706_add_ex_wife_husband_relationship.php @@ -0,0 +1,39 @@ +where([ + 'name' => 'love', + ]) + ->value('id'); + + DB::table('default_relationship_types')->insert([ + 'name' => 'ex_husband', + 'name_reverse_relationship' => 'ex_husband', + 'relationship_type_group_id' => $id, + ]); + + // Add the default relationship type to the account relationship types + Account::chunk(200, function ($accounts) { + foreach ($accounts as $account) { + /* @var Account $account */ + $account->populateRelationshipTypesTable(true); + } + }); + + DB::table('default_relationship_types') + ->update(['migrated' => 1]); + } +} diff --git a/database/migrations/2018_05_16_143631_add_nickname_to_contacts.php b/database/migrations/2018_05_16_143631_add_nickname_to_contacts.php new file mode 100644 index 0000000..eff6272 --- /dev/null +++ b/database/migrations/2018_05_16_143631_add_nickname_to_contacts.php @@ -0,0 +1,26 @@ +dropColumn([ + 'surname', + ]); + }); + + Schema::table('contacts', function (Blueprint $table) { + $table->string('nickname')->nullable()->after('last_name'); + }); + } +} diff --git a/database/migrations/2018_05_16_214222_add_timestamps_to_currencies.php b/database/migrations/2018_05_16_214222_add_timestamps_to_currencies.php new file mode 100644 index 0000000..d1d4067 --- /dev/null +++ b/database/migrations/2018_05_16_214222_add_timestamps_to_currencies.php @@ -0,0 +1,20 @@ +timestamps(); + }); + } +} diff --git a/database/migrations/2018_05_20_121028_accept_terms.php b/database/migrations/2018_05_20_121028_accept_terms.php new file mode 100644 index 0000000..a1e2381 --- /dev/null +++ b/database/migrations/2018_05_20_121028_accept_terms.php @@ -0,0 +1,28 @@ +account) { + app(AcceptPolicy::class)->execute([ + 'account_id' => $user->account_id, + 'user_id' => $user->id, + 'ip_address' => null, + ]); + } + } + }); + } +} diff --git a/database/migrations/2018_05_20_225034_change_name_order_user-_preferencies.php b/database/migrations/2018_05_20_225034_change_name_order_user-_preferencies.php new file mode 100644 index 0000000..b5b3de9 --- /dev/null +++ b/database/migrations/2018_05_20_225034_change_name_order_user-_preferencies.php @@ -0,0 +1,23 @@ +where('name_order', 'firstname_first') + ->update(['name_order' => 'firstname_lastname_nickname']); + + DB::table('users') + ->where('name_order', 'lastname_first') + ->update(['name_order' => 'lastname_firstname_nickname']); + } +} diff --git a/database/migrations/2018_05_24_160546_fix-inconsistant-reminder-time.php b/database/migrations/2018_05_24_160546_fix-inconsistant-reminder-time.php new file mode 100644 index 0000000..06a90ce --- /dev/null +++ b/database/migrations/2018_05_24_160546_fix-inconsistant-reminder-time.php @@ -0,0 +1,24 @@ +default_time_reminder_is_sent) == 4) { + $account->default_time_reminder_is_sent = date('H:i', strtotime($account->default_time_reminder_is_sent)); + $account->save(); + } + } + }); + } +} diff --git a/database/migrations/2018_06_10_191450_add_love_metadata_relationshisp.php b/database/migrations/2018_06_10_191450_add_love_metadata_relationshisp.php new file mode 100644 index 0000000..5ca6e9d --- /dev/null +++ b/database/migrations/2018_06_10_191450_add_love_metadata_relationshisp.php @@ -0,0 +1,31 @@ +increments('id'); + $table->unsignedInteger('account_id'); + $table->unsignedInteger('relationship_id'); + $table->boolean('is_active'); + $table->mediumText('notes')->nullable(); + $table->datetime('meet_date')->nullable(); + $table->datetime('official_date')->nullable(); + $table->datetime('breakup_date')->nullable(); + $table->mediumText('breakup_reason')->nullable(); + $table->timestamps(); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + $table->foreign('relationship_id')->references('id')->on('relationships')->onDelete('cascade'); + }); + } +} diff --git a/database/migrations/2018_06_10_221746_migrate_entries_objects.php b/database/migrations/2018_06_10_221746_migrate_entries_objects.php new file mode 100644 index 0000000..ce33a73 --- /dev/null +++ b/database/migrations/2018_06_10_221746_migrate_entries_objects.php @@ -0,0 +1,27 @@ +where('journalable_type', 'App\Activity') + ->update(['journalable_type' => 'App\Models\Contact\Activity']); + + DB::table('journal_entries') + ->where('journalable_type', 'App\Day') + ->update(['journalable_type' => 'App\Models\Journal\Day']); + + DB::table('journal_entries') + ->where('journalable_type', 'App\Entry') + ->update(['journalable_type' => 'App\Models\Journal\Entry']); + } +} diff --git a/database/migrations/2018_06_11_184017_change_default_user_table.php b/database/migrations/2018_06_11_184017_change_default_user_table.php new file mode 100644 index 0000000..247b050 --- /dev/null +++ b/database/migrations/2018_06_11_184017_change_default_user_table.php @@ -0,0 +1,19 @@ +string('name_order')->default('firstname_lastname_nickname')->change(); + }); + } +} diff --git a/database/migrations/2018_06_13_000100_create_u2f_key_table.php b/database/migrations/2018_06_13_000100_create_u2f_key_table.php new file mode 100644 index 0000000..2c6a4e7 --- /dev/null +++ b/database/migrations/2018_06_13_000100_create_u2f_key_table.php @@ -0,0 +1,47 @@ +increments('id'); + $table->integer('user_id')->unsigned(); + $table->string('keyHandle'); + $table->string('publicKey')->unique(); + $table->text('certificate'); + $table->integer('counter'); + $table->timestamps(); + }); + + Schema::table('u2f_key', function (Blueprint $table) { + $table->foreign('user_id')->references('id')->on('users'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::drop('u2f_key'); + } +} diff --git a/database/migrations/2018_06_14_212502_change_default_name_order_user_table.php b/database/migrations/2018_06_14_212502_change_default_name_order_user_table.php new file mode 100644 index 0000000..3e2e642 --- /dev/null +++ b/database/migrations/2018_06_14_212502_change_default_name_order_user_table.php @@ -0,0 +1,19 @@ +string('name_order')->default('firstname_lastname_nickname')->change(); + }); + } +} diff --git a/database/migrations/2018_07_03_204220_create_default_activity_type_groups_table.php b/database/migrations/2018_07_03_204220_create_default_activity_type_groups_table.php new file mode 100644 index 0000000..1a9f839 --- /dev/null +++ b/database/migrations/2018_07_03_204220_create_default_activity_type_groups_table.php @@ -0,0 +1,270 @@ +increments('id'); + $table->string('translation_key'); + $table->timestamps(); + }); + + Schema::create('default_activity_types', function ($table) { + $table->increments('id'); + $table->integer('default_activity_type_category_id'); + $table->string('translation_key'); + $table->string('location_type'); + $table->timestamps(); + }); + + // SIMPLE ACTIVITIES + DB::table('default_activity_type_categories')->insert([ + 'translation_key' => 'simple_activities', + 'created_at' => now(), + 'updated_at' => now(), + ]); + + DB::table('default_activity_types')->insert([ + 'translation_key' => 'just_hung_out', + 'location_type' => 'outside', + 'default_activity_type_category_id' => 1, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + DB::table('default_activity_types')->insert([ + 'translation_key' => 'watched_movie_at_home', + 'location_type' => 'my_place', + 'default_activity_type_category_id' => 1, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + DB::table('default_activity_types')->insert([ + 'translation_key' => 'talked_at_home', + 'location_type' => 'my_place', + 'default_activity_type_category_id' => 1, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + // SPORT + DB::table('default_activity_type_categories')->insert([ + 'translation_key' => 'sport', + 'created_at' => now(), + 'updated_at' => now(), + ]); + + DB::table('default_activity_types')->insert([ + 'translation_key' => 'did_sport_activities_together', + 'location_type' => 'outside', + 'default_activity_type_category_id' => 2, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + // FOOD + DB::table('default_activity_type_categories')->insert([ + 'translation_key' => 'food', + 'created_at' => now(), + 'updated_at' => now(), + ]); + + DB::table('default_activity_types')->insert([ + 'translation_key' => 'ate_at_his_place', + 'location_type' => 'his_place', + 'default_activity_type_category_id' => 3, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + DB::table('default_activity_types')->insert([ + 'translation_key' => 'went_bar', + 'location_type' => 'outside', + 'default_activity_type_category_id' => 3, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + DB::table('default_activity_types')->insert([ + 'translation_key' => 'ate_at_home', + 'location_type' => 'my_place', + 'default_activity_type_category_id' => 3, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + DB::table('default_activity_types')->insert([ + 'translation_key' => 'picknicked', + 'location_type' => 'outside', + 'default_activity_type_category_id' => 3, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + DB::table('default_activity_types')->insert([ + 'translation_key' => 'ate_restaurant', + 'location_type' => 'outside', + 'default_activity_type_category_id' => 3, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + // CULTURAL + DB::table('default_activity_type_categories')->insert([ + 'translation_key' => 'cultural_activities', + 'created_at' => now(), + 'updated_at' => now(), + ]); + + DB::table('default_activity_types')->insert([ + 'translation_key' => 'went_theater', + 'location_type' => 'outside', + 'default_activity_type_category_id' => 4, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + DB::table('default_activity_types')->insert([ + 'translation_key' => 'went_concert', + 'location_type' => 'outside', + 'default_activity_type_category_id' => 4, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + DB::table('default_activity_types')->insert([ + 'translation_key' => 'went_play', + 'location_type' => 'outside', + 'default_activity_type_category_id' => 4, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + DB::table('default_activity_types')->insert([ + 'translation_key' => 'went_museum', + 'location_type' => 'outside', + 'default_activity_type_category_id' => 4, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + // in order to migrate all the activity types id, it'll be easier to + // create a temp column to associate activities with activity types + // through a label instead of an id, as the id will be different for each + // account. + Schema::table('activities', function (Blueprint $table) { + $table->string('activity_type_label'); + }); + + DB::table('activities') + ->where('activity_type_id', 0) + ->update(['activity_type_id' => null]); + + DB::table('activities')->whereNotNull('activity_type_id')->orderBy('id')->chunk(100, function ($activities) { + foreach ($activities as $activity) { + $activityType = DB::table('activity_types') + ->where('id', $activity->activity_type_id) + ->first(); + + DB::table('activities') + ->where('id', $activity->id) + ->update(['activity_type_label' => $activityType->key]); + } + }); + + // Creating temp tables as the new ones will have different columns + // than the originals + Schema::drop('activity_type_groups'); + Schema::drop('activity_types'); + + Schema::create('activity_type_categories', function (Blueprint $table) { + $table->increments('id'); + $table->unsignedInteger('account_id'); + $table->string('name')->nullable(); + $table->string('translation_key')->nullable(); + $table->timestamps(); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + }); + + Schema::create('activity_types', function ($table) { + $table->increments('id'); + $table->unsignedInteger('account_id'); + $table->unsignedInteger('activity_type_category_id'); + $table->string('name')->nullable(); + $table->string('translation_key')->nullable(); + $table->string('location_type')->nullable(); + $table->timestamps(); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + $table->foreign('activity_type_category_id')->references('id')->on('activity_type_categories')->onDelete('cascade'); + }); + + $defaultActivityTypeCategories = DB::table('default_activity_type_categories')->get(); + + DB::table('accounts')->orderBy('id')->chunk(100, function ($accounts) use ($defaultActivityTypeCategories) { + foreach ($accounts as $account) { + foreach ($defaultActivityTypeCategories as $defaultActivityTypeCategory) { + $activityTypeCategoryId = DB::table('activity_type_categories')->insertGetId([ + 'account_id' => $account->id, + 'translation_key' => $defaultActivityTypeCategory->translation_key, + ]); + + $defaultActivityTypes = DB::table('default_activity_types') + ->where('default_activity_type_category_id', $defaultActivityTypeCategory->id) + ->get(); + + foreach ($defaultActivityTypes as $defaultActivityType) { + DB::table('activity_types')->insert([ + 'account_id' => $account->id, + 'activity_type_category_id' => $activityTypeCategoryId, + 'translation_key' => $defaultActivityType->translation_key, + ]); + } + } + } + }); + + // final step + DB::table('activities')->orderBy('id')->where('activity_type_label', '!=', '')->chunk(100, function ($activities) { + foreach ($activities as $activity) { + $activityLabel = $activity->activity_type_label; + + $activityType = DB::table('activity_types')->where('account_id', $activity->account_id) + ->where('translation_key', $activity->activity_type_label) + ->first(); + + DB::table('activities') + ->where('id', $activity->id) + ->update(['activity_type_id' => $activityType->id]); + } + }); + + Schema::table('activities', function (Blueprint $table) { + $table->dropColumn('activity_type_label'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('default_activity_type_categories'); + Schema::dropIfExists('default_activity_types'); + Schema::dropIfExists('activity_type_categories'); + } +} diff --git a/database/migrations/2018_07_08_104306_update-timestamps-timezone.php b/database/migrations/2018_07_08_104306_update-timestamps-timezone.php new file mode 100644 index 0000000..8ece8b4 --- /dev/null +++ b/database/migrations/2018_07_08_104306_update-timestamps-timezone.php @@ -0,0 +1,123 @@ +update('accounts', $timezone); + $this->update('activities', $timezone); + $this->update('activity_statistics', $timezone); + $this->update('activity_types', $timezone); + $this->update('activity_type_categories', $timezone); + $this->update('addresses', $timezone); + $this->update('api_usage', $timezone); + $this->update('calls', $timezone); + $this->update('changelogs', $timezone); + $this->update('changelog_user', $timezone, 'changelog_id'); + DB::table('contacts')->chunkById(200, function ($models) use ($timezone) { + foreach ($models as $model) { + $created = is_null($model->created_at) ? null : Carbon::createFromTimeString($model->created_at, $timezone)->setTimezone('UTC'); + $updated = is_null($model->updated_at) ? null : Carbon::createFromTimeString($model->updated_at, $timezone)->setTimezone('UTC'); + $last_consulted_at = is_null($model->last_consulted_at) ? null : Carbon::createFromTimeString($model->last_consulted_at, $timezone)->setTimezone('UTC'); + + DB::table('contacts')->where('id', $model->id) + ->update([ + 'created_at' => $created, + 'updated_at' => $updated, + 'last_consulted_at' => $last_consulted_at, + ]); + } + }); + $this->update('contact_fields', $timezone); + $this->update('contact_field_types', $timezone); + $this->update('contact_tag', $timezone, 'contact_id'); + $this->update('currencies', $timezone); + $this->update('days', $timezone); + $this->update('debts', $timezone); + $this->update('default_contact_field_types', $timezone); + $this->update('default_contact_modules', $timezone); + $this->update('default_activity_types', $timezone); + $this->update('default_activity_type_categories', $timezone); + $this->update('default_relationship_types', $timezone); + $this->update('default_relationship_type_groups', $timezone); + $this->update('entries', $timezone); + $this->update('events', $timezone); + $this->update('genders', $timezone); + $this->update('gifts', $timezone); + $this->update('import_jobs', $timezone); + $this->update('import_job_reports', $timezone); + $this->update('instances', $timezone); + $this->update('invitations', $timezone); + DB::table('jobs')->chunkById(200, function ($models) use ($timezone) { + foreach ($models as $model) { + $created = is_null($model->created_at) ? null : Carbon::createFromTimeString($model->created_at, $timezone)->setTimezone('UTC'); + + DB::table('jobs')->where('id', $model->id) + ->update([ + 'created_at' => $created, + ]); + } + }); + $this->update('journal_entries', $timezone); + $this->update('metadata_love_relationships', $timezone); + $this->update('notes', $timezone); + $this->update('notifications', $timezone); + $this->update('oauth_access_tokens', $timezone); + $this->update('oauth_clients', $timezone); + $this->update('oauth_personal_access_clients', $timezone); + $this->update('pets', $timezone); + $this->update('pet_categories', $timezone); + $this->update('relationships', $timezone); + $this->update('relationship_types', $timezone); + $this->update('relationship_type_groups', $timezone); + $this->update('reminders', $timezone); + $this->update('reminder_rules', $timezone); + $this->update('special_dates', $timezone); + $this->update('statistics', $timezone); + $this->update('subscriptions', $timezone); + $this->update('tags', $timezone); + $this->update('tasks', $timezone); + $this->update('terms', $timezone); + $this->update('term_user', $timezone, 'account_id'); + $this->update('u2f_key', $timezone); + $this->update('users', $timezone); + } + + /** + * Update the timestamps table. + * + * @param string $table + * @param string $timezone + * @param string $id + */ + private static function update($table, $timezone, $id = 'id') + { + DB::table($table)->orderBy($id)->chunk(200, function ($models) use ($table, $timezone, $id) { + foreach ($models as $model) { + $created = is_null($model->created_at) ? null : Carbon::createFromTimeString($model->created_at, $timezone)->setTimezone('UTC'); + $updated = is_null($model->updated_at) ? null : Carbon::createFromTimeString($model->updated_at, $timezone)->setTimezone('UTC'); + + DB::table($table)->where($id, $model->$id) + ->update([ + 'created_at' => $created, + 'updated_at' => $updated, + ]); + } + }); + } +} diff --git a/database/migrations/2018_07_26_104306_create-conversations.php b/database/migrations/2018_07_26_104306_create-conversations.php new file mode 100644 index 0000000..dfc973f --- /dev/null +++ b/database/migrations/2018_07_26_104306_create-conversations.php @@ -0,0 +1,53 @@ +increments('id'); + $table->unsignedInteger('account_id'); + $table->unsignedInteger('contact_id'); + $table->unsignedInteger('contact_field_type_id'); + $table->datetime('happened_at'); + $table->timestamps(); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + $table->foreign('contact_id')->references('id')->on('contacts')->onDelete('cascade'); + $table->foreign('contact_field_type_id')->references('id')->on('contact_field_types')->onDelete('cascade'); + }); + + Schema::create('messages', function (Blueprint $table) { + $table->increments('id'); + $table->unsignedInteger('account_id'); + $table->unsignedInteger('contact_id'); + $table->unsignedInteger('conversation_id'); + $table->longText('content'); + $table->datetime('written_at'); + $table->boolean('written_by_me'); + $table->timestamps(); + $table->foreign('conversation_id')->references('id')->on('conversations')->onDelete('cascade'); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + $table->foreign('contact_id')->references('id')->on('contacts')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('conversations'); + Schema::dropIfExists('messages'); + } +} diff --git a/database/migrations/2018_08_06_145046_add_starred_to_contacts.php b/database/migrations/2018_08_06_145046_add_starred_to_contacts.php new file mode 100644 index 0000000..7c26341 --- /dev/null +++ b/database/migrations/2018_08_06_145046_add_starred_to_contacts.php @@ -0,0 +1,20 @@ +boolean('is_starred')->after('gender_id')->default(false); + }); + } +} diff --git a/database/migrations/2018_08_09_18000_fix-empty-reminder-time.php b/database/migrations/2018_08_09_18000_fix-empty-reminder-time.php new file mode 100644 index 0000000..16e8475 --- /dev/null +++ b/database/migrations/2018_08_09_18000_fix-empty-reminder-time.php @@ -0,0 +1,18 @@ +update(['default_time_reminder_is_sent' => '12:00']); + } +} diff --git a/database/migrations/2018_08_18_180426_add_legacy_free_plan.php b/database/migrations/2018_08_18_180426_add_legacy_free_plan.php new file mode 100644 index 0000000..6738d6b --- /dev/null +++ b/database/migrations/2018_08_18_180426_add_legacy_free_plan.php @@ -0,0 +1,23 @@ +boolean('legacy_free_plan_unlimited_contacts')->default(false)->after('trial_ends_at'); + }); + + DB::table('accounts')->update(['legacy_free_plan_unlimited_contacts' => true]); + } +} diff --git a/database/migrations/2018_08_29_124804_add_conversations_to_statistics.php b/database/migrations/2018_08_29_124804_add_conversations_to_statistics.php new file mode 100644 index 0000000..154f852 --- /dev/null +++ b/database/migrations/2018_08_29_124804_add_conversations_to_statistics.php @@ -0,0 +1,20 @@ +integer('number_of_conversations')->after('number_of_import_jobs')->nullable(); + $table->integer('number_of_messages')->after('number_of_conversations')->nullable(); + }); + } +} diff --git a/database/migrations/2018_08_29_222051_add_conversations_to_modules.php b/database/migrations/2018_08_29_222051_add_conversations_to_modules.php new file mode 100644 index 0000000..b33676a --- /dev/null +++ b/database/migrations/2018_08_29_222051_add_conversations_to_modules.php @@ -0,0 +1,35 @@ +update(['migrated' => 1]); + + // now add a new module to track conversations + DB::table('default_contact_modules')->insert(['key' => 'conversations', 'translation_key' => 'people.conversation_list_title']); + + Account::chunk(200, function ($accounts) { + foreach ($accounts as $account) { + app(PopulateModulesTable::class)->execute([ + 'account_id' => $account->id, + 'migrate_existing_data' => false, + ]); + } + }); + + DB::table('default_contact_modules')->update(['migrated' => 1]); + } +} diff --git a/database/migrations/2018_08_31_020908_create_life_events_table.php b/database/migrations/2018_08_31_020908_create_life_events_table.php new file mode 100644 index 0000000..b7e8e56 --- /dev/null +++ b/database/migrations/2018_08_31_020908_create_life_events_table.php @@ -0,0 +1,409 @@ +increments('id'); + $table->string('translation_key'); + $table->boolean('migrated')->default(0); + $table->timestamps(); + }); + + Schema::create('default_life_event_types', function (Blueprint $table) { + $table->increments('id'); + $table->unsignedInteger('default_life_event_category_id'); + $table->string('translation_key'); + $table->text('specific_information_structure')->nullable(); + $table->boolean('migrated')->default(0); + $table->timestamps(); + $table->foreign('default_life_event_category_id')->references('id')->on('default_life_event_categories')->onDelete('cascade'); + }); + + // Core monica data means that there are some special actions that we do + // with the data. For those core data, we will warn the user if he + // deletes them that we won't be able to do special actions with it. + // Example: let's say user indicates that the contact expects a baby. + // The system can see that this is linked to a specific action and we + // could write a special action for it to be reminded in X months to + // see if the baby is born, for instance. + Schema::create('life_event_categories', function (Blueprint $table) { + $table->increments('id'); + $table->unsignedInteger('account_id'); + $table->string('name'); + $table->string('default_life_event_category_key')->nullable(); + $table->boolean('core_monica_data')->default(0); + $table->timestamps(); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + }); + + Schema::create('life_event_types', function (Blueprint $table) { + $table->increments('id'); + $table->unsignedInteger('account_id'); + $table->unsignedInteger('life_event_category_id'); + $table->string('name'); + $table->string('default_life_event_type_key')->nullable(); + $table->boolean('core_monica_data')->default(0); + $table->text('specific_information_structure')->nullable(); + $table->timestamps(); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + $table->foreign('life_event_category_id')->references('id')->on('life_event_categories')->onDelete('cascade'); + }); + + Schema::create('life_events', function (Blueprint $table) { + $table->increments('id'); + $table->unsignedInteger('account_id'); + $table->unsignedInteger('contact_id'); + $table->unsignedInteger('life_event_type_id'); + $table->string('name')->nullable(); + $table->mediumText('note')->nullable(); + $table->dateTime('happened_at'); + $table->boolean('happened_at_month_unknown')->default(false); + $table->boolean('happened_at_day_unknown')->default(false); + $table->text('specific_information')->nullable(); + $table->timestamps(); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + $table->foreign('contact_id')->references('id')->on('contacts')->onDelete('cascade'); + $table->foreign('life_event_type_id')->references('id')->on('life_event_types')->onDelete('cascade'); + }); + + // POPULATE DEFAULT TABLES + // WORK AND EDUCATION + $defaultCategoryId = DB::table('default_life_event_categories')->insertGetId([ + 'translation_key' => 'work_education', + 'created_at' => now(), + 'updated_at' => now(), + ]); + + DB::table('default_life_event_types')->insert([ + 'translation_key' => 'new_job', + 'default_life_event_category_id' => $defaultCategoryId, + 'specific_information_structure' => '{"employer": {"type": "string", "value": ""}, "job_title": {"type": "string", "value": ""}}', + 'created_at' => now(), + 'updated_at' => now(), + ]); + DB::table('default_life_event_types')->insert([ + 'translation_key' => 'retirement', + 'default_life_event_category_id' => $defaultCategoryId, + 'specific_information_structure' => '{"profession": {"type": "string", "value": ""}}', + 'created_at' => now(), + 'updated_at' => now(), + ]); + DB::table('default_life_event_types')->insert([ + 'translation_key' => 'new_school', + 'default_life_event_category_id' => $defaultCategoryId, + 'specific_information_structure' => '{"degree": {"type": "string", "value": ""}, "end_date": {"type": "date", "value": ""}, "end_date_reminder_id": {"type": "integer", "value": ""}, "school_name": {"type": "string", "value": ""}, "studying": {"type": "string", "value": ""}}', + 'created_at' => now(), + 'updated_at' => now(), + ]); + DB::table('default_life_event_types')->insert([ + 'translation_key' => 'study_abroad', + 'default_life_event_category_id' => $defaultCategoryId, + 'specific_information_structure' => '{"degree": {"type": "string", "value": ""}, "end_date": {"type": "date", "value": ""}, "end_date_reminder_id": {"type": "integer", "value": ""}, "school_name": {"type": "string", "value": ""}, "studying": {"type": "string", "value": ""}}', + 'created_at' => now(), + 'updated_at' => now(), + ]); + DB::table('default_life_event_types')->insert([ + 'translation_key' => 'volunteer_work', + 'default_life_event_category_id' => $defaultCategoryId, + 'specific_information_structure' => '{"organization": {"type": "string", "value": ""}}', + 'created_at' => now(), + 'updated_at' => now(), + ]); + DB::table('default_life_event_types')->insert([ + 'translation_key' => 'published_book_or_paper', + 'default_life_event_category_id' => $defaultCategoryId, + 'specific_information_structure' => '{"full_citation": {"type": "string", "value": ""}, "url": {"type": "string", "value": ""}, "citation": {"type": "string", "value": ""}}', + 'created_at' => now(), + 'updated_at' => now(), + ]); + DB::table('default_life_event_types')->insert([ + 'translation_key' => 'military_service', + 'default_life_event_category_id' => $defaultCategoryId, + 'specific_information_structure' => '{"end_date": {"type": "date", "value": ""}, "end_date_reminder_id": {"type": "integer", "value": ""}, "branch": {"type": "string", "value": ""}, "division": {"type": "string", "value": ""}, "country": {"type": "string", "value": ""}}', + 'created_at' => now(), + 'updated_at' => now(), + ]); + + // HOME LIVING + $defaultCategoryId = DB::table('default_life_event_categories')->insertGetId([ + 'translation_key' => 'family_relationships', + 'created_at' => now(), + 'updated_at' => now(), + ]); + DB::table('default_life_event_types')->insert([ + 'translation_key' => 'new_relationship', + 'default_life_event_category_id' => $defaultCategoryId, + 'created_at' => now(), + 'updated_at' => now(), + ]); + DB::table('default_life_event_types')->insert([ + 'translation_key' => 'engagement', + 'default_life_event_category_id' => $defaultCategoryId, + 'specific_information_structure' => '{"with_contact_id": {"type": "integer", "value": ""}}', + 'created_at' => now(), + 'updated_at' => now(), + ]); + DB::table('default_life_event_types')->insert([ + 'translation_key' => 'marriage', + 'default_life_event_category_id' => $defaultCategoryId, + 'specific_information_structure' => '{"with_contact_id": {"type": "integer", "value": ""}}', + 'created_at' => now(), + 'updated_at' => now(), + ]); + DB::table('default_life_event_types')->insert([ + 'translation_key' => 'anniversary', + 'default_life_event_category_id' => $defaultCategoryId, + 'created_at' => now(), + 'updated_at' => now(), + ]); + DB::table('default_life_event_types')->insert([ + 'translation_key' => 'expecting_a_baby', + 'default_life_event_category_id' => $defaultCategoryId, + 'specific_information_structure' => '{"contact_id": {"type": "integer", "value": ""}, "expected_date": {"type": "date", "value": ""}, "expected_date_reminder_id": {"type": "integer", "value": ""}, "expected_gender": {"type": "string", "value": ""}}', + 'created_at' => now(), + 'updated_at' => now(), + ]); + DB::table('default_life_event_types')->insert([ + 'translation_key' => 'new_child', + 'default_life_event_category_id' => $defaultCategoryId, + 'created_at' => now(), + 'updated_at' => now(), + ]); + DB::table('default_life_event_types')->insert([ + 'translation_key' => 'new_family_member', + 'default_life_event_category_id' => $defaultCategoryId, + 'created_at' => now(), + 'updated_at' => now(), + ]); + DB::table('default_life_event_types')->insert([ + 'translation_key' => 'new_pet', + 'default_life_event_category_id' => $defaultCategoryId, + 'created_at' => now(), + 'updated_at' => now(), + ]); + DB::table('default_life_event_types')->insert([ + 'translation_key' => 'end_of_relationship', + 'default_life_event_category_id' => $defaultCategoryId, + 'specific_information_structure' => '{"breakup_reason": {"type": "string", "value": ""}, "who_broke_up_contact_id": {"type": "integer", "value": ""}}', + 'created_at' => now(), + 'updated_at' => now(), + ]); + DB::table('default_life_event_types')->insert([ + 'translation_key' => 'loss_of_a_loved_one', + 'default_life_event_category_id' => $defaultCategoryId, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + // HOME & LIVING + $defaultCategoryId = DB::table('default_life_event_categories')->insertGetId([ + 'translation_key' => 'home_living', + 'created_at' => now(), + 'updated_at' => now(), + ]); + + DB::table('default_life_event_types')->insert([ + 'translation_key' => 'moved', + 'default_life_event_category_id' => $defaultCategoryId, + 'specific_information_structure' => '{"where_to": {"type": "string", "value": ""}}', + 'created_at' => now(), + 'updated_at' => now(), + ]); + DB::table('default_life_event_types')->insert([ + 'translation_key' => 'bought_a_home', + 'default_life_event_category_id' => $defaultCategoryId, + 'specific_information_structure' => '{"address": {"type": "string", "value": ""}, "estimated_value": {"type": "number", "value": ""}}', + 'created_at' => now(), + 'updated_at' => now(), + ]); + DB::table('default_life_event_types')->insert([ + 'translation_key' => 'home_improvement', + 'default_life_event_category_id' => $defaultCategoryId, + 'created_at' => now(), + 'updated_at' => now(), + ]); + DB::table('default_life_event_types')->insert([ + 'translation_key' => 'holidays', + 'default_life_event_category_id' => $defaultCategoryId, + 'specific_information_structure' => '{"where": {"type": "string", "value": ""}, "duration_in_days": {"type": "integer", "value": ""}}', + 'created_at' => now(), + 'updated_at' => now(), + ]); + DB::table('default_life_event_types')->insert([ + 'translation_key' => 'new_vehicule', + 'default_life_event_category_id' => $defaultCategoryId, + 'specific_information_structure' => '{"type": {"type": "string", "value": ""}, "model": {"type": "string", "value": ""}, "model_year": {"type": "string", "value": ""}}', + 'created_at' => now(), + 'updated_at' => now(), + ]); + DB::table('default_life_event_types')->insert([ + 'translation_key' => 'new_roommate', + 'default_life_event_category_id' => $defaultCategoryId, + 'specific_information_structure' => '{"contact_id": {"type": "string", "value": ""}}', + 'created_at' => now(), + 'updated_at' => now(), + ]); + + // TRAVEL AND EXPERIENCES + $defaultCategoryId = DB::table('default_life_event_categories')->insertGetId([ + 'translation_key' => 'health_wellness', + 'created_at' => now(), + 'updated_at' => now(), + ]); + + DB::table('default_life_event_types')->insert([ + 'translation_key' => 'overcame_an_illness', + 'default_life_event_category_id' => $defaultCategoryId, + 'created_at' => now(), + 'updated_at' => now(), + ]); + DB::table('default_life_event_types')->insert([ + 'translation_key' => 'quit_a_habit', + 'default_life_event_category_id' => $defaultCategoryId, + 'created_at' => now(), + 'updated_at' => now(), + ]); + DB::table('default_life_event_types')->insert([ + 'translation_key' => 'new_eating_habits', + 'default_life_event_category_id' => $defaultCategoryId, + 'created_at' => now(), + 'updated_at' => now(), + ]); + DB::table('default_life_event_types')->insert([ + 'translation_key' => 'weight_loss', + 'default_life_event_category_id' => $defaultCategoryId, + 'specific_information_structure' => '{"amount": {"type": "string", "value": ""}, "unit": {"type": "string", "value": ""}}', + 'created_at' => now(), + 'updated_at' => now(), + ]); + DB::table('default_life_event_types')->insert([ + 'translation_key' => 'wear_glass_or_contact', + 'default_life_event_category_id' => $defaultCategoryId, + 'created_at' => now(), + 'updated_at' => now(), + ]); + DB::table('default_life_event_types')->insert([ + 'translation_key' => 'broken_bone', + 'default_life_event_category_id' => $defaultCategoryId, + 'created_at' => now(), + 'updated_at' => now(), + ]); + DB::table('default_life_event_types')->insert([ + 'translation_key' => 'removed_braces', + 'default_life_event_category_id' => $defaultCategoryId, + 'created_at' => now(), + 'updated_at' => now(), + ]); + DB::table('default_life_event_types')->insert([ + 'translation_key' => 'surgery', + 'default_life_event_category_id' => $defaultCategoryId, + 'specific_information_structure' => '{"nature": {"type": "string", "value": ""}, "number_days_in_hospital": {"type": "integer", "value": ""}, "number_days_in_hospital": {"type": "integer", "value": ""}, "expected_date_out_of_hospital_reminder_id": {"type": "integer", "value": ""}}', + 'created_at' => now(), + 'updated_at' => now(), + ]); + DB::table('default_life_event_types')->insert([ + 'translation_key' => 'dentist', + 'default_life_event_category_id' => $defaultCategoryId, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + // HEALTH AND WELLNESS + $defaultCategoryId = DB::table('default_life_event_categories')->insertGetId([ + 'translation_key' => 'travel_experiences', + 'created_at' => now(), + 'updated_at' => now(), + ]); + + DB::table('default_life_event_types')->insert([ + 'translation_key' => 'new_sport', + 'default_life_event_category_id' => $defaultCategoryId, + 'created_at' => now(), + 'updated_at' => now(), + ]); + DB::table('default_life_event_types')->insert([ + 'translation_key' => 'new_hobby', + 'default_life_event_category_id' => $defaultCategoryId, + 'created_at' => now(), + 'updated_at' => now(), + ]); + DB::table('default_life_event_types')->insert([ + 'translation_key' => 'new_instrument', + 'default_life_event_category_id' => $defaultCategoryId, + 'created_at' => now(), + 'updated_at' => now(), + ]); + DB::table('default_life_event_types')->insert([ + 'translation_key' => 'new_language', + 'default_life_event_category_id' => $defaultCategoryId, + 'created_at' => now(), + 'updated_at' => now(), + ]); + DB::table('default_life_event_types')->insert([ + 'translation_key' => 'tattoo_or_piercing', + 'default_life_event_category_id' => $defaultCategoryId, + 'created_at' => now(), + 'updated_at' => now(), + ]); + DB::table('default_life_event_types')->insert([ + 'translation_key' => 'new_license', + 'default_life_event_category_id' => $defaultCategoryId, + 'created_at' => now(), + 'updated_at' => now(), + ]); + DB::table('default_life_event_types')->insert([ + 'translation_key' => 'travel', + 'default_life_event_category_id' => $defaultCategoryId, + 'specific_information_structure' => '{"visited_place": {"type": "string", "value": ""}, "duration_in_days": {"type": "integer", "value": ""}}', + 'created_at' => now(), + 'updated_at' => now(), + ]); + DB::table('default_life_event_types')->insert([ + 'translation_key' => 'achievement_or_award', + 'default_life_event_category_id' => $defaultCategoryId, + 'created_at' => now(), + 'updated_at' => now(), + ]); + DB::table('default_life_event_types')->insert([ + 'translation_key' => 'changed_beliefs', + 'default_life_event_category_id' => $defaultCategoryId, + 'created_at' => now(), + 'updated_at' => now(), + ]); + DB::table('default_life_event_types')->insert([ + 'translation_key' => 'first_word', + 'default_life_event_category_id' => $defaultCategoryId, + 'created_at' => now(), + 'updated_at' => now(), + ]); + DB::table('default_life_event_types')->insert([ + 'translation_key' => 'first_kiss', + 'default_life_event_category_id' => $defaultCategoryId, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + Account::chunk(200, function ($accounts) { + foreach ($accounts as $account) { + app(PopulateLifeEventsTable::class)->execute([ + 'account_id' => $account->id, + 'migrate_existing_data' => true, + ]); + } + }); + } +} diff --git a/database/migrations/2018_09_02_150531_contact_archiving.php b/database/migrations/2018_09_02_150531_contact_archiving.php new file mode 100644 index 0000000..5de5153 --- /dev/null +++ b/database/migrations/2018_09_02_150531_contact_archiving.php @@ -0,0 +1,32 @@ +boolean('is_active')->default(1)->after('is_partial'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('contacts', function ($table) { + $table->dropColumn('is_active'); + }); + } +} diff --git a/database/migrations/2018_09_05_025008_add_default_profile_view.php b/database/migrations/2018_09_05_025008_add_default_profile_view.php new file mode 100644 index 0000000..ffc1756 --- /dev/null +++ b/database/migrations/2018_09_05_025008_add_default_profile_view.php @@ -0,0 +1,21 @@ +string('profile_active_tab')->default('notes')->after('gifts_active_tab'); + $table->boolean('profile_new_life_event_badge_seen')->default(false)->after('profile_active_tab'); + }); + } +} diff --git a/database/migrations/2018_09_05_213507_mark_modules_migrated.php b/database/migrations/2018_09_05_213507_mark_modules_migrated.php new file mode 100644 index 0000000..f4c1193 --- /dev/null +++ b/database/migrations/2018_09_05_213507_mark_modules_migrated.php @@ -0,0 +1,39 @@ +modules; + $uniqueModules = collect([]); + foreach ($modules as $module) { + $deleted = false; + foreach ($uniqueModules as $uniqueModule) { + if ($uniqueModule['translation_key'] == $module->translation_key) { + $module->delete(); + $deleted = true; + } + } + + if (! $deleted) { + $uniqueModules->push($module); + } + } + } + }); + } +} diff --git a/database/migrations/2018_09_13_135926_add_description_field_to_contacts.php b/database/migrations/2018_09_13_135926_add_description_field_to_contacts.php new file mode 100644 index 0000000..2a3f59b --- /dev/null +++ b/database/migrations/2018_09_13_135926_add_description_field_to_contacts.php @@ -0,0 +1,20 @@ +string('description')->after('gender_id')->nullable(); + }); + } +} diff --git a/database/migrations/2018_09_18_142844_remove_events.php b/database/migrations/2018_09_18_142844_remove_events.php new file mode 100644 index 0000000..04ab885 --- /dev/null +++ b/database/migrations/2018_09_18_142844_remove_events.php @@ -0,0 +1,17 @@ +increments('id'); + $table->unsignedInteger('account_id'); + $table->unsignedInteger('contact_id'); + $table->string('original_filename'); + $table->string('new_filename'); + $table->integer('filesize')->nullable(); + $table->string('type')->nullable(); + $table->string('mime_type')->nullable(); + $table->integer('number_of_downloads')->default(0); + $table->timestamps(); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + $table->foreign('contact_id')->references('id')->on('contacts')->onDelete('cascade'); + }); + } +} diff --git a/database/migrations/2018_09_29_114125_add_reminder_to_life_events.php b/database/migrations/2018_09_29_114125_add_reminder_to_life_events.php new file mode 100644 index 0000000..b5cd4dd --- /dev/null +++ b/database/migrations/2018_09_29_114125_add_reminder_to_life_events.php @@ -0,0 +1,20 @@ +unsignedInteger('reminder_id')->nullable()->after('life_event_type_id'); + }); + } +} diff --git a/database/migrations/2018_10_01_211757_add_number_of_views.php b/database/migrations/2018_10_01_211757_add_number_of_views.php new file mode 100644 index 0000000..92f67e6 --- /dev/null +++ b/database/migrations/2018_10_01_211757_add_number_of_views.php @@ -0,0 +1,20 @@ +integer('number_of_views')->after('last_consulted_at')->default(0); + }); + } +} diff --git a/database/migrations/2018_10_04_181116_life_event_vehicle.php b/database/migrations/2018_10_04_181116_life_event_vehicle.php new file mode 100644 index 0000000..77e663c --- /dev/null +++ b/database/migrations/2018_10_04_181116_life_event_vehicle.php @@ -0,0 +1,28 @@ +where('translation_key', 'new_vehicule') + ->update([ + 'translation_key' => 'new_vehicle', + ]); + + DB::table('life_event_types') + ->where('default_life_event_type_key', 'new_vehicule') + ->update([ + 'default_life_event_type_key' => 'new_vehicle', + 'name' => trans('settings.personalization_life_event_type_new_vehicle', [], 'en'), + ]); + } +} diff --git a/database/migrations/2018_10_07_120133_fix_json_column.php b/database/migrations/2018_10_07_120133_fix_json_column.php new file mode 100644 index 0000000..3103d7e --- /dev/null +++ b/database/migrations/2018_10_07_120133_fix_json_column.php @@ -0,0 +1,34 @@ +getDriverName() != 'mysql') { + return; + } + + $databasename = $connection->getDatabaseName(); + + $columns = DB::select( + 'select table_name, column_name from information_schema.columns where table_schema = ? and data_type = ? '. + ' and table_name in (?, ?, ?)', + [$databasename, 'json', 'default_life_event_types', 'life_event_types', 'life_events'] + ); + + foreach ($columns as $column) { + DB::statement('ALTER TABLE `'.$databasename.'`.'.DBHelper::getTable($column->table_name).' MODIFY `'.$column->column_name.'` text;'); + } + } +} diff --git a/database/migrations/2018_10_16_000703_add_documents_to_module_table.php b/database/migrations/2018_10_16_000703_add_documents_to_module_table.php new file mode 100644 index 0000000..2ad7d3a --- /dev/null +++ b/database/migrations/2018_10_16_000703_add_documents_to_module_table.php @@ -0,0 +1,30 @@ +insert(['key' => 'documents', 'translation_key' => 'people.document_list_title']); + + Account::chunk(200, function ($accounts) { + foreach ($accounts as $account) { + app(PopulateModulesTable::class)->execute([ + 'account_id' => $account->id, + 'migrate_existing_data' => false, + ]); + } + }); + + DB::table('default_contact_modules')->update(['migrated' => 1]); + } +} diff --git a/database/migrations/2018_10_19_081816_life_event_tattoo.php b/database/migrations/2018_10_19_081816_life_event_tattoo.php new file mode 100644 index 0000000..a75569f --- /dev/null +++ b/database/migrations/2018_10_19_081816_life_event_tattoo.php @@ -0,0 +1,28 @@ +where('translation_key', 'tatoo_or_piercing') + ->update([ + 'translation_key' => 'tattoo_or_piercing', + ]); + + DB::table('life_event_types') + ->where('default_life_event_type_key', 'tatoo_or_piercing') + ->update([ + 'default_life_event_type_key' => 'tattoo_or_piercing', + 'name' => trans('settings.personalization_life_event_type_tattoo_or_piercinge', [], 'en'), + ]); + } +} diff --git a/database/migrations/2018_10_27_230346_fix_non_english_tab_slugs.php b/database/migrations/2018_10_27_230346_fix_non_english_tab_slugs.php new file mode 100644 index 0000000..d7aea2b --- /dev/null +++ b/database/migrations/2018_10_27_230346_fix_non_english_tab_slugs.php @@ -0,0 +1,25 @@ +name_slug)) { + $tag->forceFill([ + 'name_slug' => htmlentities($tag->name), + ])->save(); + } + } + }); + } +} diff --git a/database/migrations/2018_10_28_165814_email_verified.php b/database/migrations/2018_10_28_165814_email_verified.php new file mode 100644 index 0000000..ab6ccfc --- /dev/null +++ b/database/migrations/2018_10_28_165814_email_verified.php @@ -0,0 +1,58 @@ +timestamp('email_verified_at')->after('email')->nullable(); + }); + + if (Schema::hasColumn('users', 'confirmed')) { + User::chunk(200, function ($users) { + foreach ($users as $user) { + if ($user->confirmed) { + $user->forceFill([ + 'email_verified_at' => $user->created_at, + ])->save(); + } + } + }); + + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('confirmed'); + }); + + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('confirmation_code'); + }); + } + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('users', function (Blueprint $table) { + $table->boolean('confirmed')->default(false); + $table->string('confirmation_code')->nullable(); + }); + + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('email_verified_at'); + }); + } +} diff --git a/database/migrations/2018_11_11_145035_remove_changelogs_table.php b/database/migrations/2018_11_11_145035_remove_changelogs_table.php new file mode 100644 index 0000000..4d24a86 --- /dev/null +++ b/database/migrations/2018_11_11_145035_remove_changelogs_table.php @@ -0,0 +1,18 @@ +unsignedInteger('contact_id')->nullable()->change(); + }); + } +} diff --git a/database/migrations/2018_11_18_021908_create_images_table.php b/database/migrations/2018_11_18_021908_create_images_table.php new file mode 100644 index 0000000..1c70d77 --- /dev/null +++ b/database/migrations/2018_11_18_021908_create_images_table.php @@ -0,0 +1,37 @@ +increments('id'); + $table->unsignedInteger('account_id'); + $table->string('original_filename'); + $table->string('new_filename'); + $table->integer('filesize')->nullable(); + $table->string('mime_type')->nullable(); + $table->timestamps(); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('photos'); + } +} diff --git a/database/migrations/2018_11_21_212932_add_contacts_uuid.php b/database/migrations/2018_11_21_212932_add_contacts_uuid.php new file mode 100644 index 0000000..efdcc70 --- /dev/null +++ b/database/migrations/2018_11_21_212932_add_contacts_uuid.php @@ -0,0 +1,32 @@ +uuid('uuid')->after('description')->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('contacts', function (Blueprint $table) { + $table->dropColumn('uuid'); + }); + } +} diff --git a/database/migrations/2018_11_25_020818_add_contact_photo_table.php b/database/migrations/2018_11_25_020818_add_contact_photo_table.php new file mode 100644 index 0000000..57deaff --- /dev/null +++ b/database/migrations/2018_11_25_020818_add_contact_photo_table.php @@ -0,0 +1,24 @@ +unsignedInteger('contact_id'); + $table->unsignedInteger('photo_id'); + $table->timestamps(); + $table->foreign('photo_id')->references('id')->on('photos')->onDelete('cascade'); + $table->foreign('contact_id')->references('id')->on('contacts')->onDelete('cascade'); + }); + } +} diff --git a/database/migrations/2018_11_30_154729_recovery_codes.php b/database/migrations/2018_11_30_154729_recovery_codes.php new file mode 100644 index 0000000..3fc42c8 --- /dev/null +++ b/database/migrations/2018_11_30_154729_recovery_codes.php @@ -0,0 +1,37 @@ +increments('id'); + $table->unsignedInteger('account_id'); + $table->unsignedInteger('user_id'); + $table->string('recovery'); + $table->boolean('used')->default(false); + $table->timestamps(); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::drop('recovery_codes'); + } +} diff --git a/database/migrations/2018_12_08_233140_add_who_called_to_calls.php b/database/migrations/2018_12_08_233140_add_who_called_to_calls.php new file mode 100644 index 0000000..286db4b --- /dev/null +++ b/database/migrations/2018_12_08_233140_add_who_called_to_calls.php @@ -0,0 +1,20 @@ +boolean('contact_called')->default(false)->after('content'); + }); + } +} diff --git a/database/migrations/2018_12_09_023232_add_emotions_table.php b/database/migrations/2018_12_09_023232_add_emotions_table.php new file mode 100644 index 0000000..962f5e4 --- /dev/null +++ b/database/migrations/2018_12_09_023232_add_emotions_table.php @@ -0,0 +1,258 @@ +increments('id'); + $table->string('name'); + $table->timestamps(); + }); + + Schema::create('emotions_secondary', function (Blueprint $table) { + $table->increments('id'); + $table->unsignedInteger('emotion_primary_id'); + $table->string('name'); + $table->timestamps(); + $table->foreign('emotion_primary_id')->references('id')->on('emotions_primary')->onDelete('cascade'); + }); + + Schema::create('emotions', function (Blueprint $table) { + $table->increments('id'); + $table->unsignedInteger('emotion_primary_id'); + $table->unsignedInteger('emotion_secondary_id'); + $table->string('name'); + $table->timestamps(); + $table->foreign('emotion_primary_id')->references('id')->on('emotions_primary')->onDelete('cascade'); + $table->foreign('emotion_secondary_id')->references('id')->on('emotions_secondary')->onDelete('cascade'); + }); + + $emotionPrimaryId = DB::table('emotions_primary')->insertGetId(['name' => 'love']); + + $emotionSecondaryId = DB::table('emotions_secondary')->insertGetId(['emotion_primary_id' => $emotionPrimaryId, 'name' => 'affection']); + + DB::table('emotions')->insert(['name' => 'adoration', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'affection', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'love', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'fondness', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'liking', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'attraction', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'caring', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'tenderness', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'compassion', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'sentimentality', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + + $emotionSecondaryId = DB::table('emotions_secondary')->insertGetId(['emotion_primary_id' => $emotionPrimaryId, 'name' => 'lust']); + + DB::table('emotions')->insert(['name' => 'arousal', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'desire', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'lust', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'passion', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'infatuation', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + + $emotionSecondaryId = DB::table('emotions_secondary')->insertGetId(['emotion_primary_id' => $emotionPrimaryId, 'name' => 'longing']); + + DB::table('emotions')->insert(['name' => 'longing', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + + $emotionPrimaryId = DB::table('emotions_primary')->insertGetId(['name' => 'joy']); + + $emotionSecondaryId = DB::table('emotions_secondary')->insertGetId(['emotion_primary_id' => $emotionPrimaryId, 'name' => 'cheerfulness']); + + DB::table('emotions')->insert(['name' => 'amusement', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'bliss', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'cheerfulness', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'gaiety', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'glee', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'jolliness', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'joviality', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'joy', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'delight', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'enjoyment', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'gladness', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'happiness', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'jubilation', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'elation', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'satisfaction', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'ecstasy', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'euphoria', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + + $emotionSecondaryId = DB::table('emotions_secondary')->insertGetId(['emotion_primary_id' => $emotionPrimaryId, 'name' => 'zest']); + + DB::table('emotions')->insert(['name' => 'enthusiasm', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'zeal', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'zest', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'excitement', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'thrill', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'exhilaration', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + + $emotionSecondaryId = DB::table('emotions_secondary')->insertGetId(['emotion_primary_id' => $emotionPrimaryId, 'name' => 'contentment']); + + DB::table('emotions')->insert(['name' => 'contentment', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'pleasure', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + + $emotionSecondaryId = DB::table('emotions_secondary')->insertGetId(['emotion_primary_id' => $emotionPrimaryId, 'name' => 'pride']); + + DB::table('emotions')->insert(['name' => 'pride', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'pleasure', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + + $emotionSecondaryId = DB::table('emotions_secondary')->insertGetId(['emotion_primary_id' => $emotionPrimaryId, 'name' => 'optimism']); + + DB::table('emotions')->insert(['name' => 'eagerness', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'hope', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + + $emotionSecondaryId = DB::table('emotions_secondary')->insertGetId(['emotion_primary_id' => $emotionPrimaryId, 'name' => 'enthrallment']); + + DB::table('emotions')->insert(['name' => 'enthrallment', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'rapture', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + + $emotionSecondaryId = DB::table('emotions_secondary')->insertGetId(['emotion_primary_id' => $emotionPrimaryId, 'name' => 'relief']); + + DB::table('emotions')->insert(['name' => 'relief', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + + $emotionPrimaryId = DB::table('emotions_primary')->insertGetId(['name' => 'surprise']); + + $emotionSecondaryId = DB::table('emotions_secondary')->insertGetId(['emotion_primary_id' => $emotionPrimaryId, 'name' => 'surprise']); + + DB::table('emotions')->insert(['name' => 'amazement', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'surprise', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'astonishment', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + + $emotionPrimaryId = DB::table('emotions_primary')->insertGetId(['name' => 'anger']); + + $emotionSecondaryId = DB::table('emotions_secondary')->insertGetId(['emotion_primary_id' => $emotionPrimaryId, 'name' => 'irritation']); + + DB::table('emotions')->insert(['name' => 'aggravation', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'irritation', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'agitation', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'annoyance', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'grouchiness', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'grumpiness', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + + $emotionSecondaryId = DB::table('emotions_secondary')->insertGetId(['emotion_primary_id' => $emotionPrimaryId, 'name' => 'exasperation']); + + DB::table('emotions')->insert(['name' => 'exasperation', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'frustration', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + + $emotionSecondaryId = DB::table('emotions_secondary')->insertGetId(['emotion_primary_id' => $emotionPrimaryId, 'name' => 'rage']); + + DB::table('emotions')->insert(['name' => 'anger', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'rage', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'outrage', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'fury', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'wrath', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'hostility', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'ferocity', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'bitterness', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'hate', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'loathing', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'scorn', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'spite', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'vengefulness', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'dislike', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'resentment', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + + $emotionSecondaryId = DB::table('emotions_secondary')->insertGetId(['emotion_primary_id' => $emotionPrimaryId, 'name' => 'disgust']); + + DB::table('emotions')->insert(['name' => 'disgust', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'revulsion', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'contempt', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + + $emotionSecondaryId = DB::table('emotions_secondary')->insertGetId(['emotion_primary_id' => $emotionPrimaryId, 'name' => 'envy']); + + DB::table('emotions')->insert(['name' => 'envy', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'jealousy', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + + $emotionPrimaryId = DB::table('emotions_primary')->insertGetId(['name' => 'sadness']); + + $emotionSecondaryId = DB::table('emotions_secondary')->insertGetId(['emotion_primary_id' => $emotionPrimaryId, 'name' => 'suffering']); + + DB::table('emotions')->insert(['name' => 'agony', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'suffering', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'hurt', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'anguish', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + + $emotionSecondaryId = DB::table('emotions_secondary')->insertGetId(['emotion_primary_id' => $emotionPrimaryId, 'name' => 'sadness']); + + DB::table('emotions')->insert(['name' => 'depression', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'despair', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'hopelessness', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'gloom', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'glumness', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'sadness', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'unhappiness', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'grief', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'sorrow', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'woe', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'misery', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'melancholy', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + + $emotionSecondaryId = DB::table('emotions_secondary')->insertGetId(['emotion_primary_id' => $emotionPrimaryId, 'name' => 'disappointment']); + + DB::table('emotions')->insert(['name' => 'dismay', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'disappointment', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'displeasure', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + + $emotionSecondaryId = DB::table('emotions_secondary')->insertGetId(['emotion_primary_id' => $emotionPrimaryId, 'name' => 'shame']); + + DB::table('emotions')->insert(['name' => 'guilt', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'shame', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'regret', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'remorse', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + + $emotionSecondaryId = DB::table('emotions_secondary')->insertGetId(['emotion_primary_id' => $emotionPrimaryId, 'name' => 'neglect']); + + DB::table('emotions')->insert(['name' => 'alienation', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'isolation', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'neglect', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'loneliness', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'rejection', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'homesickness', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'defeat', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'dejection', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'insecurity', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'embarrassment', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'humiliation', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'insult', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + + $emotionSecondaryId = DB::table('emotions_secondary')->insertGetId(['emotion_primary_id' => $emotionPrimaryId, 'name' => 'sympathy']); + + DB::table('emotions')->insert(['name' => 'pity', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'sympathy', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + + $emotionPrimaryId = DB::table('emotions_primary')->insertGetId(['name' => 'fear']); + + $emotionSecondaryId = DB::table('emotions_secondary')->insertGetId(['emotion_primary_id' => $emotionPrimaryId, 'name' => 'horror']); + + DB::table('emotions')->insert(['name' => 'alarm', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'shock', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'fear', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'fright', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'horror', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'terror', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'panic', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'hysteria', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'mortification', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + + $emotionSecondaryId = DB::table('emotions_secondary')->insertGetId(['emotion_primary_id' => $emotionPrimaryId, 'name' => 'nervousness']); + + DB::table('emotions')->insert(['name' => 'anxiety', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'nervousness', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'tenseness', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'uneasiness', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'apprehension', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'worry', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'distress', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + DB::table('emotions')->insert(['name' => 'dread', 'emotion_primary_id' => $emotionPrimaryId, 'emotion_secondary_id' => $emotionSecondaryId]); + } +} diff --git a/database/migrations/2018_12_09_145956_create_emotion_call_table.php b/database/migrations/2018_12_09_145956_create_emotion_call_table.php new file mode 100644 index 0000000..3a1e532 --- /dev/null +++ b/database/migrations/2018_12_09_145956_create_emotion_call_table.php @@ -0,0 +1,28 @@ +unsignedInteger('account_id'); + $table->unsignedInteger('call_id'); + $table->unsignedInteger('emotion_id'); + $table->unsignedInteger('contact_id'); + $table->timestamps(); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + $table->foreign('call_id')->references('id')->on('calls')->onDelete('cascade'); + $table->foreign('emotion_id')->references('id')->on('emotions')->onDelete('cascade'); + $table->foreign('contact_id')->references('id')->on('contacts')->onDelete('cascade'); + }); + } +} diff --git a/database/migrations/2018_12_16_195440_add_gps_coordinates_to_addressess.php b/database/migrations/2018_12_16_195440_add_gps_coordinates_to_addressess.php new file mode 100644 index 0000000..a4fb94f --- /dev/null +++ b/database/migrations/2018_12_16_195440_add_gps_coordinates_to_addressess.php @@ -0,0 +1,21 @@ +double('latitude')->nullable()->after('country'); + $table->double('longitude')->nullable()->after('country'); + }); + } +} diff --git a/database/migrations/2018_12_19_002819_create_places_table.php b/database/migrations/2018_12_19_002819_create_places_table.php new file mode 100644 index 0000000..bd2e646 --- /dev/null +++ b/database/migrations/2018_12_19_002819_create_places_table.php @@ -0,0 +1,45 @@ +increments('id'); + $table->unsignedInteger('account_id'); + $table->string('street')->nullable(); + $table->string('city')->nullable(); + $table->string('province')->nullable(); + $table->string('postal_code')->nullable(); + $table->char('country', 3)->nullable(); + $table->double('latitude')->nullable(); + $table->double('longitude')->nullable(); + $table->timestamps(); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + }); + + Schema::table('addresses', function (Blueprint $table) { + $table->unsignedInteger('place_id')->nullable()->after('account_id'); + $table->foreign('place_id')->references('id')->on('places')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('places'); + } +} diff --git a/database/migrations/2018_12_19_003444_move_addresses_data.php b/database/migrations/2018_12_19_003444_move_addresses_data.php new file mode 100644 index 0000000..940b4c4 --- /dev/null +++ b/database/migrations/2018_12_19_003444_move_addresses_data.php @@ -0,0 +1,46 @@ +account_id = $address->account_id; + $place->street = $address->street; + $place->city = $address->city; + $place->province = $address->province; + $place->postal_code = $address->postal_code; + $place->country = $address->country; + $place->latitude = $address->latitude; + $place->longitude = $address->longitude; + $place->save(); + + $address->place_id = $place->id; + $address->save(); + } + }); + + Schema::table('addresses', function (Blueprint $table) { + $table->dropColumn('street'); + $table->dropColumn('city'); + $table->dropColumn('province'); + $table->dropColumn('postal_code'); + $table->dropColumn('country'); + $table->dropColumn('latitude'); + $table->dropColumn('longitude'); + }); + } +} diff --git a/database/migrations/2018_12_21_235418_add_weather_table.php b/database/migrations/2018_12_21_235418_add_weather_table.php new file mode 100644 index 0000000..0b24b7a --- /dev/null +++ b/database/migrations/2018_12_21_235418_add_weather_table.php @@ -0,0 +1,26 @@ +increments('id'); + $table->unsignedInteger('account_id'); + $table->unsignedInteger('place_id'); + $table->string('weather_json', 2000); + $table->timestamps(); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + $table->foreign('place_id')->references('id')->on('places')->onDelete('cascade'); + }); + } +} diff --git a/database/migrations/2018_12_22_021123_add_weather_preferences_to_users.php b/database/migrations/2018_12_22_021123_add_weather_preferences_to_users.php new file mode 100644 index 0000000..64ed70c --- /dev/null +++ b/database/migrations/2018_12_22_021123_add_weather_preferences_to_users.php @@ -0,0 +1,20 @@ +string('temperature_scale')->after('profile_new_life_event_badge_seen')->default('fahrenheit')->nullable(); + }); + } +} diff --git a/database/migrations/2018_12_22_200413_add_reminder_initial_date_to_reminders.php b/database/migrations/2018_12_22_200413_add_reminder_initial_date_to_reminders.php new file mode 100644 index 0000000..eee65cd --- /dev/null +++ b/database/migrations/2018_12_22_200413_add_reminder_initial_date_to_reminders.php @@ -0,0 +1,73 @@ +date('initial_date')->after('contact_id'); + $table->boolean('delible')->default(true)->after('next_expected_date'); + }); + + // we need to migrate old data. Since we don't know what was the initial + // date for the reminder, we need to make a guess by taking the last + // triggered column information. + Reminder::chunk(200, function ($reminders) { + foreach ($reminders as $reminder) { + if (! is_null($reminder->special_date_id)) { + $reminder->initial_date = SpecialDate::find($reminder->special_date_id)->date; + // if the reminder had a special date, that meant it was a + // birthday. Reminder about birthdates can't be deleted, so + // we need to flag them as such. + $reminder->delible = false; + } else { + $reminder->initial_date = $reminder->next_expected_date; + } + $reminder->save(); + } + }); + + Schema::create('reminder_outbox', function (Blueprint $table) { + $table->increments('id'); + $table->unsignedInteger('account_id'); + $table->unsignedInteger('reminder_id'); + $table->unsignedInteger('user_id'); + $table->date('planned_date'); + $table->string('nature')->default('reminder'); + $table->integer('notification_number_days_before')->nullable(); + $table->timestamps(); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + $table->foreign('reminder_id')->references('id')->on('reminders')->onDelete('cascade'); + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + }); + + Schema::create('reminder_sent', function (Blueprint $table) { + $table->increments('id'); + $table->unsignedInteger('account_id'); + $table->unsignedInteger('reminder_id')->nullable(); + $table->unsignedInteger('user_id'); + $table->date('planned_date'); + $table->datetime('sent_date'); + $table->string('nature')->default('reminder'); + $table->string('frequency_type')->nullable(); + $table->integer('frequency_number')->nullable(); + $table->longText('html_content')->nullable(); + $table->longText('text_content')->nullable(); + $table->timestamps(); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + $table->foreign('reminder_id')->references('id')->on('reminders')->onDelete('set null'); + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + }); + } +} diff --git a/database/migrations/2018_12_24_164256_add_companies_table.php b/database/migrations/2018_12_24_164256_add_companies_table.php new file mode 100644 index 0000000..61cfd44 --- /dev/null +++ b/database/migrations/2018_12_24_164256_add_companies_table.php @@ -0,0 +1,26 @@ +increments('id'); + $table->unsignedInteger('account_id'); + $table->string('name'); + $table->string('website')->nullable(); + $table->integer('number_of_employees')->nullable(); + $table->timestamps(); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + }); + } +} diff --git a/database/migrations/2018_12_24_220019_add_occupations_table.php b/database/migrations/2018_12_24_220019_add_occupations_table.php new file mode 100644 index 0000000..923b441 --- /dev/null +++ b/database/migrations/2018_12_24_220019_add_occupations_table.php @@ -0,0 +1,34 @@ +increments('id'); + $table->unsignedInteger('account_id'); + $table->unsignedInteger('contact_id'); + $table->unsignedInteger('company_id'); + $table->string('title'); + $table->string('description', 1000)->nullable(); + $table->integer('salary')->nullable(); + $table->string('salary_unit')->nullable(); + $table->boolean('currently_works_here')->default(false)->nullable(); + $table->date('start_date')->nullable(); + $table->date('end_date')->nullable(); + $table->timestamps(); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + $table->foreign('contact_id')->references('id')->on('contacts')->onDelete('cascade'); + $table->foreign('company_id')->references('id')->on('companies')->onDelete('cascade'); + }); + } +} diff --git a/database/migrations/2018_12_25_001736_add_linkedin_to_default_contact_field_type.php b/database/migrations/2018_12_25_001736_add_linkedin_to_default_contact_field_type.php new file mode 100644 index 0000000..69d87c7 --- /dev/null +++ b/database/migrations/2018_12_25_001736_add_linkedin_to_default_contact_field_type.php @@ -0,0 +1,34 @@ +insertGetId([ + 'name' => 'LinkedIn', + 'fontawesome_icon' => 'fa fa-linkedin-square', + ]); + + Account::chunk(200, function ($accounts) { + foreach ($accounts as $account) { + app(PopulateContactFieldTypesTable::class)->execute([ + 'account_id' => $account->id, + 'migrate_existing_data' => false, + ]); + } + }); + + DB::table('default_contact_field_types') + ->update(['migrated' => 1]); + } +} diff --git a/database/migrations/2018_12_25_012011_move_linkedin_data_to_contact_field_type.php b/database/migrations/2018_12_25_012011_move_linkedin_data_to_contact_field_type.php new file mode 100644 index 0000000..a7a6220 --- /dev/null +++ b/database/migrations/2018_12_25_012011_move_linkedin_data_to_contact_field_type.php @@ -0,0 +1,38 @@ +whereNotNull('linkedin_profile_url') + ->chunk(50, function ($contacts) { + foreach ($contacts as $contact) { + $contactFieldType = ContactFieldType::where('account_id', $contact->account_id) + ->where('name', 'LinkedIn') + ->first(); + + $contact->contactFields()->create([ + 'account_id' => $contact->account_id, + 'contact_field_type_id' => $contactFieldType->id, + 'data' => $contact->linkedin_profile_url, + ]); + } + }); + + Schema::table('contacts', function (Blueprint $table) { + $table->dropColumn('linkedin_profile_url'); + }); + } +} diff --git a/database/migrations/2018_12_29_091017_default_temperature_scale.php b/database/migrations/2018_12_29_091017_default_temperature_scale.php new file mode 100644 index 0000000..67a50d6 --- /dev/null +++ b/database/migrations/2018_12_29_091017_default_temperature_scale.php @@ -0,0 +1,48 @@ +string('temperature_scale')->default('celsius')->change(); + }); + + $country = null; + $currentLocale = null; + User::orderBy('locale')->chunkById(200, function ($users) use ($country, $currentLocale) { + foreach ($users as $user) { + if ($user->locale != $currentLocale || $country == null) { + $country = CountriesHelper::getCountryFromLocale($user->locale); + $currentLocale = $user->locale; + } + + if ($country !== null) { + switch ($country->getIsoAlpha2()) { + case 'US': + case 'BZ': + case 'KY': + $user->temperature_scale = 'fahrenheit'; + break; + default: + $user->temperature_scale = 'celsius'; + break; + } + } + + $user->save(); + } + }); + } +} diff --git a/database/migrations/2018_12_29_135516_sync_token.php b/database/migrations/2018_12_29_135516_sync_token.php new file mode 100644 index 0000000..f433e88 --- /dev/null +++ b/database/migrations/2018_12_29_135516_sync_token.php @@ -0,0 +1,26 @@ +increments('id'); + $table->unsignedInteger('account_id'); + $table->unsignedInteger('user_id'); + $table->timestamp('timestamp'); + $table->timestamps(); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + }); + } +} diff --git a/database/migrations/2019_01_05_152329_add_reminder_ids_to_contacts.php b/database/migrations/2019_01_05_152329_add_reminder_ids_to_contacts.php new file mode 100644 index 0000000..ad95cc7 --- /dev/null +++ b/database/migrations/2019_01_05_152329_add_reminder_ids_to_contacts.php @@ -0,0 +1,25 @@ +unsignedInteger('birthday_reminder_id')->nullable()->after('birthday_special_date_id'); + $table->foreign('birthday_reminder_id')->references('id')->on('reminders')->onDelete('set null'); + $table->unsignedInteger('first_met_reminder_id')->nullable()->after('first_met_special_date_id'); + $table->foreign('first_met_reminder_id')->references('id')->on('reminders')->onDelete('set null'); + $table->unsignedInteger('deceased_reminder_id')->nullable()->after('deceased_special_date_id'); + $table->foreign('deceased_reminder_id')->references('id')->on('reminders')->onDelete('set null'); + }); + } +} diff --git a/database/migrations/2019_01_05_152405_migrate_previous_remiders.php b/database/migrations/2019_01_05_152405_migrate_previous_remiders.php new file mode 100644 index 0000000..70fe6b6 --- /dev/null +++ b/database/migrations/2019_01_05_152405_migrate_previous_remiders.php @@ -0,0 +1,45 @@ +chunk(50, function ($contacts) { + foreach ($contacts as $contact) { + try { + $specialDate = SpecialDate::findOrFail($contact->birthday_special_date_id); + + try { + $reminder = Reminder::findOrFail($specialDate->reminder_id); + $contact->birthday_reminder_id = $reminder->id; + $contact->save(); + } catch (ModelNotFoundException $e) { + $contact->birthday_special_date_id = null; + $contact->birthday_reminder_id = null; + $contact->save(); + } + } catch (ModelNotFoundException $e) { + $contact->birthday_special_date_id = null; + $contact->birthday_reminder_id = null; + $contact->save(); + } + } + }); + } +} diff --git a/database/migrations/2019_01_05_152456_drop_special_date_id_from_reminders.php b/database/migrations/2019_01_05_152456_drop_special_date_id_from_reminders.php new file mode 100644 index 0000000..c556000 --- /dev/null +++ b/database/migrations/2019_01_05_152456_drop_special_date_id_from_reminders.php @@ -0,0 +1,26 @@ +dropColumn('special_date_id'); + $table->dropColumn('last_triggered'); + $table->dropColumn('next_expected_date'); + }); + + Schema::table('special_dates', function (Blueprint $table) { + $table->dropColumn('reminder_id'); + }); + } +} diff --git a/database/migrations/2019_01_05_152526_schedule_new_reminders.php b/database/migrations/2019_01_05_152526_schedule_new_reminders.php new file mode 100644 index 0000000..d376ee1 --- /dev/null +++ b/database/migrations/2019_01_05_152526_schedule_new_reminders.php @@ -0,0 +1,28 @@ +frequency_number)) { + $reminder->frequency_number = 1; + $reminder->save(); + } + + foreach ($reminder->account->users as $user) { + $reminder->schedule($user); + } + } + }); + } +} diff --git a/database/migrations/2019_01_05_202557_add_foreign_keys_to_reminder.php b/database/migrations/2019_01_05_202557_add_foreign_keys_to_reminder.php new file mode 100644 index 0000000..39a269b --- /dev/null +++ b/database/migrations/2019_01_05_202557_add_foreign_keys_to_reminder.php @@ -0,0 +1,40 @@ +contact_id); + } catch (ModelNotFoundException $e) { + $reminder->delete(); + continue; + } + } + }); + + Schema::table('reminders', function (Blueprint $table) { + $table->unsignedInteger('account_id')->change(); + $table->unsignedInteger('contact_id')->change(); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + $table->foreign('contact_id')->references('id')->on('contacts')->onDelete('cascade'); + }); + } +} diff --git a/database/migrations/2019_01_05_202748_add_foreign_key_to_reminder_rule.php b/database/migrations/2019_01_05_202748_add_foreign_key_to_reminder_rule.php new file mode 100644 index 0000000..79a608f --- /dev/null +++ b/database/migrations/2019_01_05_202748_add_foreign_key_to_reminder_rule.php @@ -0,0 +1,40 @@ +account_id); + } catch (ModelNotFoundException $e) { + $reminderRule->delete(); + continue; + } + } + }); + + Schema::disableForeignKeyConstraints(); + Schema::table('reminder_rules', function (Blueprint $table) { + $table->unsignedInteger('account_id')->change(); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + }); + Schema::enableForeignKeyConstraints(); + } +} diff --git a/database/migrations/2019_01_05_202938_add_foreign_key_to_contacts.php b/database/migrations/2019_01_05_202938_add_foreign_key_to_contacts.php new file mode 100644 index 0000000..0e23cda --- /dev/null +++ b/database/migrations/2019_01_05_202938_add_foreign_key_to_contacts.php @@ -0,0 +1,73 @@ +whereNotNull('birthday_special_date_id') + ->chunk(50, function ($contacts) { + foreach ($contacts as $contact) { + try { + SpecialDate::findOrFail($contact->birthday_special_date_id); + } catch (ModelNotFoundException $e) { + $contact->birthday_special_date_id = null; + $contact->save(); + continue; + } + } + }); + + Contact::select('first_met_special_date_id') + ->whereNotNull('first_met_special_date_id') + ->chunk(50, function ($contacts) { + foreach ($contacts as $contact) { + try { + SpecialDate::findOrFail($contact->first_met_special_date_id); + } catch (ModelNotFoundException $e) { + $contact->first_met_special_date_id = null; + $contact->save(); + continue; + } + } + }); + + Contact::select('deceased_special_date_id') + ->whereNotNull('deceased_special_date_id') + ->chunk(50, function ($contacts) { + foreach ($contacts as $contact) { + try { + SpecialDate::findOrFail($contact->deceased_special_date_id); + } catch (ModelNotFoundException $e) { + $contact->deceased_special_date_id = null; + $contact->save(); + continue; + } + } + }); + + Schema::table('contacts', function (Blueprint $table) { + $table->unsignedInteger('birthday_special_date_id')->change(); + $table->unsignedInteger('first_met_special_date_id')->change(); + $table->unsignedInteger('deceased_special_date_id')->change(); + $table->foreign('birthday_special_date_id')->references('id')->on('special_dates')->onDelete('set null'); + $table->foreign('first_met_special_date_id')->references('id')->on('special_dates')->onDelete('set null'); + $table->foreign('deceased_special_date_id')->references('id')->on('special_dates')->onDelete('set null'); + }); + } +} diff --git a/database/migrations/2019_01_05_203201_add_foreign_key_for_reminder_in_life-events_table.php b/database/migrations/2019_01_05_203201_add_foreign_key_for_reminder_in_life-events_table.php new file mode 100644 index 0000000..b541b0e --- /dev/null +++ b/database/migrations/2019_01_05_203201_add_foreign_key_for_reminder_in_life-events_table.php @@ -0,0 +1,40 @@ +whereNotNull('reminder_id') + ->chunk(50, function ($lifeEvents) { + foreach ($lifeEvents as $lifeEvent) { + try { + Reminder::findOrFail($lifeEvent->reminder_id); + } catch (ModelNotFoundException $e) { + $lifeEvent->reminder_id = null; + $lifeEvent->save(); + continue; + } + } + }); + + Schema::disableForeignKeyConstraints(); + Schema::table('life_events', function (Blueprint $table) { + $table->unsignedInteger('reminder_id')->change(); + $table->foreign('reminder_id')->references('id')->on('reminders')->onDelete('set null'); + }); + Schema::enableForeignKeyConstraints(); + } +} diff --git a/database/migrations/2019_01_06_135133_update_u2f_key_table.php b/database/migrations/2019_01_06_135133_update_u2f_key_table.php new file mode 100644 index 0000000..d68384d --- /dev/null +++ b/database/migrations/2019_01_06_135133_update_u2f_key_table.php @@ -0,0 +1,23 @@ +dropForeign('u2f_key_user_id_foreign'); + }); + Schema::table('u2f_key', function (Blueprint $table) { + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + }); + } +} diff --git a/database/migrations/2019_01_06_150143_add_inactive_flag_to_reminders.php b/database/migrations/2019_01_06_150143_add_inactive_flag_to_reminders.php new file mode 100644 index 0000000..4d0aabc --- /dev/null +++ b/database/migrations/2019_01_06_150143_add_inactive_flag_to_reminders.php @@ -0,0 +1,20 @@ +boolean('inactive')->default(false)->after('delible'); + }); + } +} diff --git a/database/migrations/2019_01_06_190036_u2f_key_name.php b/database/migrations/2019_01_06_190036_u2f_key_name.php new file mode 100644 index 0000000..e5487b8 --- /dev/null +++ b/database/migrations/2019_01_06_190036_u2f_key_name.php @@ -0,0 +1,32 @@ +string('name')->after('id')->default('key'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('u2f_key', function (Blueprint $table) { + $table->dropColumn('name'); + }); + } +} diff --git a/database/migrations/2019_01_11_142944_add_foreign_keys_to_activities.php b/database/migrations/2019_01_11_142944_add_foreign_keys_to_activities.php new file mode 100644 index 0000000..5f4db6a --- /dev/null +++ b/database/migrations/2019_01_11_142944_add_foreign_keys_to_activities.php @@ -0,0 +1,58 @@ +account_id); + } catch (ModelNotFoundException $e) { + $activity->delete(); + continue; + } + } + }); + + Schema::table('activities', function (Blueprint $table) { + $table->unsignedInteger('account_id')->change(); + $table->unsignedInteger('activity_type_id')->change(); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + $table->foreign('activity_type_id')->references('id')->on('activity_types')->onDelete('set null'); + }); + + $activityContacts = DB::table('activity_contact')->get(); + foreach ($activityContacts as $activityContact) { + try { + Account::findOrFail($activityContact->account_id); + } catch (ModelNotFoundException $e) { + DB::table('activity_contact')->where('account_id', $activityContact->account_id) + ->where('activity_id', $activityContact->activity_id) + ->where('contact_id', $activityContact->contact_id) + ->delete(); + continue; + } + } + + Schema::table('activity_contact', function (Blueprint $table) { + $table->unsignedInteger('account_id')->change(); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + }); + } +} diff --git a/database/migrations/2019_01_11_183717_change_activities_date_type.php b/database/migrations/2019_01_11_183717_change_activities_date_type.php new file mode 100644 index 0000000..f778cc4 --- /dev/null +++ b/database/migrations/2019_01_11_183717_change_activities_date_type.php @@ -0,0 +1,24 @@ +date('date_it_happened')->change(); + }); + + Schema::table('activities', function (Blueprint $table) { + $table->renameColumn('date_it_happened', 'happened_at'); + }); + } +} diff --git a/database/migrations/2019_01_17_093812_add_admin_user.php b/database/migrations/2019_01_17_093812_add_admin_user.php new file mode 100644 index 0000000..ec323b1 --- /dev/null +++ b/database/migrations/2019_01_17_093812_add_admin_user.php @@ -0,0 +1,32 @@ +boolean('admin')->after('email')->default(false); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('admin'); + }); + } +} diff --git a/database/migrations/2019_01_18_142032_add_dav_uuid.php b/database/migrations/2019_01_18_142032_add_dav_uuid.php new file mode 100644 index 0000000..17f3cda --- /dev/null +++ b/database/migrations/2019_01_18_142032_add_dav_uuid.php @@ -0,0 +1,56 @@ +index(['account_id', 'uuid']); + }); + Schema::table('special_dates', function (Blueprint $table) { + $table->uuid('uuid')->after('contact_id')->nullable(); + $table->index(['account_id', 'uuid']); + }); + Schema::table('tasks', function (Blueprint $table) { + $table->uuid('uuid')->after('contact_id')->nullable(); + $table->index(['account_id', 'uuid']); + }); + Schema::table('synctoken', function (Blueprint $table) { + $table->string('name')->after('user_id')->default('contacts'); + $table->index(['account_id', 'user_id', 'name']); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('contacts', function (Blueprint $table) { + $table->dropIndex(['account_id', 'uuid']); + }); + Schema::table('special_dates', function (Blueprint $table) { + $table->dropIndex(['account_id', 'uuid']); + $table->dropColumn('uuid'); + }); + Schema::table('tasks', function (Blueprint $table) { + $table->dropIndex(['account_id', 'uuid']); + $table->dropColumn('uuid'); + }); + Schema::table('synctoken', function (Blueprint $table) { + $table->dropIndex(['account_id', 'user_id', 'name']); + $table->dropColumn('name'); + }); + } +} diff --git a/database/migrations/2019_01_22_034555_create_emotion_activity_table.php b/database/migrations/2019_01_22_034555_create_emotion_activity_table.php new file mode 100644 index 0000000..9158faa --- /dev/null +++ b/database/migrations/2019_01_22_034555_create_emotion_activity_table.php @@ -0,0 +1,26 @@ +unsignedInteger('account_id'); + $table->unsignedInteger('activity_id'); + $table->unsignedInteger('emotion_id'); + $table->timestamps(); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + $table->foreign('activity_id')->references('id')->on('activities')->onDelete('cascade'); + $table->foreign('emotion_id')->references('id')->on('emotions')->onDelete('cascade'); + }); + } +} diff --git a/database/migrations/2019_01_24_221539_change_activity_model_location.php b/database/migrations/2019_01_24_221539_change_activity_model_location.php new file mode 100644 index 0000000..cbb78bd --- /dev/null +++ b/database/migrations/2019_01_24_221539_change_activity_model_location.php @@ -0,0 +1,19 @@ +where('journalable_type', 'App\Models\Contact\Activity') + ->update(['journalable_type' => 'App\Models\Account\Activity']); + } +} diff --git a/database/migrations/2019_01_31_223600_add_swiss_chf_to_currencies_table.php b/database/migrations/2019_01_31_223600_add_swiss_chf_to_currencies_table.php new file mode 100644 index 0000000..d357594 --- /dev/null +++ b/database/migrations/2019_01_31_223600_add_swiss_chf_to_currencies_table.php @@ -0,0 +1,16 @@ +insert(['iso' => 'CHF', 'name' => 'Swiss CHF', 'symbol' => 'CHF']); + } +} diff --git a/database/migrations/2019_02_08_234959_remove_users_without_account.php b/database/migrations/2019_02_08_234959_remove_users_without_account.php new file mode 100644 index 0000000..2f358e1 --- /dev/null +++ b/database/migrations/2019_02_08_234959_remove_users_without_account.php @@ -0,0 +1,20 @@ +leftJoin('accounts', 'accounts.id', '=', 'users.account_id') + ->whereNull('accounts.id') + ->delete(); + } +} diff --git a/database/migrations/2019_02_09_200203_add_gender_type.php b/database/migrations/2019_02_09_200203_add_gender_type.php new file mode 100644 index 0000000..3418216 --- /dev/null +++ b/database/migrations/2019_02_09_200203_add_gender_type.php @@ -0,0 +1,65 @@ +char('type', 1)->after('name')->nullable(); + }); + + $appLocale = config('app.locale'); + $womanEn = trans('app.gender_female', [], $appLocale); + $manEn = trans('app.gender_male', [], $appLocale); + $otherEn = trans('app.gender_none', [], $appLocale); + + Account::with(['users', 'genders'])->chunk(200, function ($accounts) use ($appLocale, $womanEn, $manEn, $otherEn) { + foreach ($accounts as $account) { + $locale = $account->getFirstLocale() ?: $appLocale; + + $woman = trans('app.gender_female', [], $locale); + $man = trans('app.gender_male', [], $locale); + $other = trans('app.gender_none', [], $locale); + + foreach ($account->genders->all() as $gender) { + if ($gender->name == $woman || $gender->name == $womanEn) { + $gender->type = Gender::FEMALE; + } elseif ($gender->name == $man || $gender->name == $manEn) { + $gender->type = Gender::MALE; + } elseif ($gender->name == $other || $gender->name == $otherEn) { + $gender->type = Gender::OTHER; + } else { + $gender->type = Gender::UNKNOWN; + } + + // prevent timestamp update + $gender->timestamps = false; + $gender->save(); + } + } + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('genders', function (Blueprint $table) { + $table->dropColumn('type'); + }); + } +} diff --git a/database/migrations/2019_02_17_112452_add_default_gender.php b/database/migrations/2019_02_17_112452_add_default_gender.php new file mode 100644 index 0000000..8ed6e05 --- /dev/null +++ b/database/migrations/2019_02_17_112452_add_default_gender.php @@ -0,0 +1,51 @@ +unsignedInteger('default_gender_id')->after('default_time_reminder_is_sent')->nullable(); + $table->foreign('default_gender_id')->references('id')->on('genders')->onDelete('set null'); + }); + + DB::table('genders') + ->groupBy('account_id') + ->select(DB::raw('min(id) as id, account_id')) + ->orderBy('id') + ->chunk(200, function ($genders) { + foreach ($genders as $gender) { + /** @var Account */ + $account = Account::find($gender->account_id); + if ($account) { + $account->default_gender_id = $gender->id; + $account->save(); + } + } + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('accounts', function (Blueprint $table) { + $table->dropForeign(['default_gender_id']); + $table->dropColumn('default_gender_id'); + }); + } +} diff --git a/database/migrations/2019_02_20_205744_allow_gender_null.php b/database/migrations/2019_02_20_205744_allow_gender_null.php new file mode 100644 index 0000000..ee98d72 --- /dev/null +++ b/database/migrations/2019_02_20_205744_allow_gender_null.php @@ -0,0 +1,20 @@ +integer('gender_id')->nullable()->change(); + }); + } +} diff --git a/database/migrations/2019_02_24_223855_remove_relation_type_name.php b/database/migrations/2019_02_24_223855_remove_relation_type_name.php new file mode 100644 index 0000000..7f94d9c --- /dev/null +++ b/database/migrations/2019_02_24_223855_remove_relation_type_name.php @@ -0,0 +1,20 @@ +dropColumn('relationship_type_name'); + }); + } +} diff --git a/database/migrations/2019_03_27_103012_set_default_profile_links.php b/database/migrations/2019_03_27_103012_set_default_profile_links.php new file mode 100644 index 0000000..141dc0f --- /dev/null +++ b/database/migrations/2019_03_27_103012_set_default_profile_links.php @@ -0,0 +1,43 @@ +where('name', '=', 'Facebook') + ->where('protocol', '=', null) + ->update([ + 'protocol' => 'https://facebook.com/', + ]); + + DB::table('default_contact_field_types') + ->where('name', '=', 'Twitter') + ->where('protocol', '=', '') + ->update([ + 'protocol' => 'https://twitter.com/', + ]); + + DB::table('default_contact_field_types') + ->where('name', '=', 'WhatsApp') + ->where('protocol', '=', null) + ->update([ + 'protocol' => 'https://wa.me/', + ]); + + DB::table('default_contact_field_types') + ->where('name', '=', 'LinkedIn') + ->where('protocol', '=', '') + ->update([ + 'protocol' => 'https://linkedin.com/in/', + ]); + } +} diff --git a/database/migrations/2019_03_29_163611_add_webauthn.php b/database/migrations/2019_03_29_163611_add_webauthn.php new file mode 100644 index 0000000..9129354 --- /dev/null +++ b/database/migrations/2019_03_29_163611_add_webauthn.php @@ -0,0 +1,45 @@ +increments('id'); + $table->unsignedInteger('user_id'); + + $table->string('name')->default('key'); + $table->string('credentialId', 255); + $table->string('type', 255); + $table->text('transports'); + $table->string('attestationType', 255); + $table->text('trustPath'); + $table->text('aaguid'); + $table->text('credentialPublicKey'); + $table->integer('counter'); + $table->timestamps(); + + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + $table->index('credentialId'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('webauthn'); + } +} diff --git a/database/migrations/2019_05_05_194746_add_cron_schedule.php b/database/migrations/2019_05_05_194746_add_cron_schedule.php new file mode 100644 index 0000000..d0de38c --- /dev/null +++ b/database/migrations/2019_05_05_194746_add_cron_schedule.php @@ -0,0 +1,33 @@ +increments('id'); + $table->string('command')->unique(); + $table->timestamp('last_run'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('crons'); + } +} diff --git a/database/migrations/2019_05_15_205533_rename_preferences.php b/database/migrations/2019_05_15_205533_rename_preferences.php new file mode 100644 index 0000000..88b13d2 --- /dev/null +++ b/database/migrations/2019_05_15_205533_rename_preferences.php @@ -0,0 +1,27 @@ +renameColumn('food_preferencies', 'food_preferences'); + }); + + DB::table('default_contact_modules') + ->where('translation_key', 'people.food_preferencies_title') + ->update(['translation_key' => 'people.food_preferences_title']); + DB::table('modules') + ->where('translation_key', 'people.food_preferencies_title') + ->update(['translation_key' => 'people.food_preferences_title']); + } +} diff --git a/database/migrations/2019_05_26_000000_add_relationship_table_indexes.php b/database/migrations/2019_05_26_000000_add_relationship_table_indexes.php new file mode 100644 index 0000000..5807736 --- /dev/null +++ b/database/migrations/2019_05_26_000000_add_relationship_table_indexes.php @@ -0,0 +1,24 @@ +index(['account_id', 'name']); + }); + Schema::table('default_relationship_types', function (Blueprint $table) { + $table->index(['migrated']); + }); + } +} diff --git a/database/migrations/2019_05_27_000000_populate_relationship_type_tables_with_stepparent_values.php b/database/migrations/2019_05_27_000000_populate_relationship_type_tables_with_stepparent_values.php new file mode 100644 index 0000000..46d9c09 --- /dev/null +++ b/database/migrations/2019_05_27_000000_populate_relationship_type_tables_with_stepparent_values.php @@ -0,0 +1,65 @@ +where(['name' => 'family']) + ->value('id'); + + DB::table('default_relationship_types')->insert([ + 'name' => 'stepparent', + 'name_reverse_relationship' => 'stepchild', + 'relationship_type_group_id' => $defaultRelationshipTypeGroupId, + ]); + DB::table('default_relationship_types')->insert([ + 'name' => 'stepchild', + 'name_reverse_relationship' => 'stepparent', + 'relationship_type_group_id' => $defaultRelationshipTypeGroupId, + ]); + + $defaultRelationshipTypes = DB::table('default_relationship_types') + ->where('migrated', 0) + ->get(); + + // Add the default relationship type to the account relationship types + Account::chunk(500, function ($accounts) use ($defaultRelationshipTypes) { + foreach ($accounts as $account) { + /* @var Account $account */ + + $relationshipTypeGroupId = DB::table('relationship_type_groups') + ->where([ + 'account_id' => $account->id, + 'name' => 'family', + ]) + ->value('id'); + + if ($relationshipTypeGroupId) { + foreach ($defaultRelationshipTypes as $defaultRelationshipType) { + RelationshipType::create([ + 'account_id' => $account->id, + 'name' => $defaultRelationshipType->name, + 'name_reverse_relationship' => $defaultRelationshipType->name_reverse_relationship, + 'relationship_type_group_id' => $relationshipTypeGroupId, + 'delible' => $defaultRelationshipType->delible, + ]); + } + } + } + }); + + DB::table('default_relationship_types') + ->update(['migrated' => 1]); + } +} diff --git a/database/migrations/2019_08_12_213308_change_avatars_structure.php b/database/migrations/2019_08_12_213308_change_avatars_structure.php new file mode 100644 index 0000000..f4e5307 --- /dev/null +++ b/database/migrations/2019_08_12_213308_change_avatars_structure.php @@ -0,0 +1,42 @@ +string('avatar_source')->after('food_preferences')->default('default'); + $table->string('avatar_gravatar_url', 250)->after('avatar_source')->nullable(); + $table->uuid('avatar_adorable_uuid')->after('avatar_gravatar_url')->nullable(); + $table->string('avatar_adorable_url', 250)->after('avatar_adorable_uuid')->nullable(); + $table->string('avatar_default_url', 250)->after('avatar_adorable_url')->nullable(); + $table->integer('avatar_photo_id')->after('avatar_default_url')->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('contacts', function (Blueprint $table) { + $table->dropColumn('avatar_source'); + $table->dropColumn('avatar_gravatar_url'); + $table->dropColumn('avatar_adorable_uuid'); + $table->dropColumn('avatar_adorable_url'); + $table->dropColumn('avatar_default_url'); + $table->dropColumn('avatar_photo_id'); + }); + } +} diff --git a/database/migrations/2019_08_12_222938_create_avatars_for_existing_contacts.php b/database/migrations/2019_08_12_222938_create_avatars_for_existing_contacts.php new file mode 100644 index 0000000..3ce90a3 --- /dev/null +++ b/database/migrations/2019_08_12_222938_create_avatars_for_existing_contacts.php @@ -0,0 +1,21 @@ +unsignedInteger('me_contact_id')->after('email')->nullable(); + $table->foreign('me_contact_id')->references('id')->on('contacts')->onDelete('set null'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('users', function (Blueprint $table) { + $table->dropForeign(['me_contact_id']); + $table->dropColumn('me_contact_id'); + }); + } +} diff --git a/database/migrations/2019_08_14_091427_update_stripe_columns.php b/database/migrations/2019_08_14_091427_update_stripe_columns.php new file mode 100644 index 0000000..82ee0bd --- /dev/null +++ b/database/migrations/2019_08_14_091427_update_stripe_columns.php @@ -0,0 +1,44 @@ +string('card_last_four', 4)->change(); + $table->index(['stripe_id']); + }); + + Schema::table('subscriptions', function (Blueprint $table) { + $table->string('stripe_status')->after('stripe_id'); + $table->index(['account_id', 'stripe_status']); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('subscriptions', function (Blueprint $table) { + $table->dropColumn('stripe_status'); + $table->dropIndex(['account_id', 'stripe_status']); + }); + Schema::table('accounts', function (Blueprint $table) { + $table->dropIndex(['stripe_id']); + }); + } +} diff --git a/database/migrations/2019_09_04_075311_fix_tattoo_or_piercing_translation.php b/database/migrations/2019_09_04_075311_fix_tattoo_or_piercing_translation.php new file mode 100644 index 0000000..d5f60da --- /dev/null +++ b/database/migrations/2019_09_04_075311_fix_tattoo_or_piercing_translation.php @@ -0,0 +1,33 @@ +getFirstLocale() ?: $appLocale; + + DB::table('life_event_types') + ->where([ + 'account_id' => $account->id, + 'default_life_event_type_key' => 'tattoo_or_piercing', + ]) + ->update([ + 'name' => trans('settings.personalization_life_event_type_tattoo_or_piercing', [], $locale), + ]); + } + }); + } +} diff --git a/database/migrations/2019_12_17_024553_add_foreign_keys.php b/database/migrations/2019_12_17_024553_add_foreign_keys.php new file mode 100644 index 0000000..77565be --- /dev/null +++ b/database/migrations/2019_12_17_024553_add_foreign_keys.php @@ -0,0 +1,649 @@ + */ + private $existingAccounts; + /** @var array */ + private $existingUsers; + /** @var array */ + private $existingContacts; + + /** + * Add foreign keys to all the tables that don't have ones. + * Before adding foreign keys, in all the tables, we check whether the + * foreign keys actually have data from the table they point to. This will + * ensure data integrity from now on. + * In order to do this, we need to parse a lot of contacts, accounts and + * users, as most data point to them somehow. For those 3 models, when we + * launch the script, we put all those ids in arrays. Then, for each + * foreign key, we check in the arrays if the key exists, instead of querying + * the database again and again. This is much faster. + * + * @return void + */ + public function up() + { + Schema::disableForeignKeyConstraints(); + + $this->initialize(); + $this->cleanActivityStatisticTable(); + $this->cleanCallsTable(); + $this->cleanContactTagTable(); + $this->cleanDayTable(); + $this->cleanDebtTable(); + $this->cleanEntriesTable(); + $this->cleanGenderTable(); + $this->cleanGiftTable(); + $this->cleanImportJobReportTable(); + $this->cleanImportJobTable(); + $this->cleanInvitationTable(); + $this->cleanJournalEntryTable(); + $this->cleanModuleTable(); + $this->cleanNoteTable(); + $this->cleanNotificationTable(); + $this->cleanPetTable(); + $this->cleanRelationshipTypeGroupTable(); + $this->cleanRelationshipTypeTable(); + $this->cleanRelationshipTable(); + $this->cleanSpecialDateTable(); + $this->cleanTagTable(); + $this->cleanTaskTable(); + $this->cleanTermUserTable(); + $this->cleanUserTable(); + $this->cleanContactTable(); + + Schema::enableForeignKeyConstraints(); + } + + private function initialize() + { + $rows = DB::table('contacts')->select('id')->get(); + $this->existingContacts = []; + foreach ($rows as $row) { + $this->existingContacts[$row->id] = 1; + } + + $rows = DB::table('users')->select('id')->get(); + $this->existingUsers = []; + foreach ($rows as $row) { + $this->existingUsers[$row->id] = 1; + } + + $rows = DB::table('accounts')->select('id')->get(); + $this->existingAccounts = []; + foreach ($rows as $row) { + $this->existingAccounts[$row->id] = 1; + } + } + + private function contactExistOrFail(int $id) + { + if (isset($this->existingContacts[$id])) { + return true; + } else { + throw new ModelNotFoundException(); + } + } + + private function userExistOrFail(int $id) + { + if (isset($this->existingUsers[$id])) { + return true; + } else { + throw new ModelNotFoundException(); + } + } + + private function accountExistOrFail(int $id) + { + if (isset($this->existingAccounts[$id])) { + return true; + } else { + throw new ModelNotFoundException(); + } + } + + private function cleanActivityStatisticTable() + { + foreach (ActivityStatistic::cursor() as $activityStat) { + try { + $this->accountExistOrFail($activityStat->account_id); + $this->contactExistOrFail($activityStat->contact_id); + } catch (ModelNotFoundException $e) { + $activityStat->delete(); + continue; + } + } + + Schema::table('activity_statistics', function (Blueprint $table) { + $table->unsignedInteger('account_id')->change(); + $table->unsignedInteger('contact_id')->change(); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + $table->foreign('contact_id')->references('id')->on('contacts')->onDelete('cascade'); + }); + } + + private function cleanCallsTable() + { + foreach (Call::cursor() as $call) { + try { + $this->accountExistOrFail($call->account_id); + $this->contactExistOrFail($call->contact_id); + } catch (ModelNotFoundException $e) { + $call->delete(); + continue; + } + } + + Schema::table('calls', function (Blueprint $table) { + $table->unsignedInteger('account_id')->change(); + $table->unsignedInteger('contact_id')->change(); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + $table->foreign('contact_id')->references('id')->on('contacts')->onDelete('cascade'); + }); + } + + private function cleanContactTagTable() + { + DB::table('contact_tag') + ->orderBy('contact_id') + ->chunk(200, function ($contactTags) { + foreach ($contactTags as $contactTag) { + try { + $this->accountExistOrFail($contactTag->account_id); + $this->accountExistOrFail($contactTag->contact_id); + Tag::findOrFail($contactTag->tag_id); + } catch (ModelNotFoundException $e) { + DB::table('contact_tag') + ->where('account_id', $contactTag->account_id) + ->where('contact_id', $contactTag->contact_id) + ->where('tag_id', $contactTag->tag_id) + ->delete(); + } + } + }); + + Schema::table('contact_tag', function (Blueprint $table) { + $table->unsignedInteger('account_id')->change(); + $table->unsignedInteger('contact_id')->change(); + $table->unsignedInteger('tag_id')->change(); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + $table->foreign('contact_id')->references('id')->on('contacts')->onDelete('cascade'); + $table->foreign('tag_id')->references('id')->on('tags')->onDelete('cascade'); + }); + } + + private function cleanDayTable() + { + foreach (Day::cursor() as $day) { + try { + $this->accountExistOrFail($day->account_id); + } catch (ModelNotFoundException $e) { + $day->delete(); + continue; + } + } + + Schema::table('days', function (Blueprint $table) { + $table->unsignedInteger('account_id')->change(); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + }); + } + + private function cleanDebtTable() + { + foreach (Debt::cursor() as $debt) { + try { + $this->accountExistOrFail($debt->account_id); + $this->contactExistOrFail($debt->contact_id); + } catch (ModelNotFoundException $e) { + $debt->delete(); + continue; + } + } + + Schema::table('debts', function (Blueprint $table) { + $table->unsignedInteger('account_id')->change(); + $table->unsignedInteger('contact_id')->change(); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + $table->foreign('contact_id')->references('id')->on('contacts')->onDelete('cascade'); + }); + } + + private function cleanEntriesTable() + { + foreach (Entry::cursor() as $entry) { + try { + $this->accountExistOrFail($entry->account_id); + } catch (ModelNotFoundException $e) { + $entry->delete(); + continue; + } + } + + Schema::table('entries', function (Blueprint $table) { + $table->unsignedInteger('account_id')->change(); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + }); + } + + private function cleanGenderTable() + { + foreach (Gender::cursor() as $gender) { + try { + $this->accountExistOrFail($gender->account_id); + } catch (ModelNotFoundException $e) { + $gender->delete(); + continue; + } + } + + Schema::table('genders', function (Blueprint $table) { + $table->unsignedInteger('account_id')->change(); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + }); + } + + private function cleanGiftTable() + { + foreach (Gift::cursor() as $gift) { + try { + $this->accountExistOrFail($gift->account_id); + $this->contactExistOrFail($gift->contact_id); + } catch (ModelNotFoundException $e) { + $gift->delete(); + continue; + } + } + + Schema::table('gifts', function (Blueprint $table) { + $table->unsignedInteger('account_id')->change(); + $table->unsignedInteger('contact_id')->change(); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + $table->foreign('contact_id')->references('id')->on('contacts')->onDelete('cascade'); + }); + } + + private function cleanImportJobReportTable() + { + foreach (ImportJobReport::cursor() as $importJobReport) { + try { + $this->accountExistOrFail($importJobReport->account_id); + $this->userExistOrFail($importJobReport->user_id); + ImportJob::findOrFail($importJobReport->import_job_id); + } catch (ModelNotFoundException $e) { + $importJobReport->delete(); + continue; + } + } + + Schema::table('import_job_reports', function (Blueprint $table) { + $table->unsignedInteger('account_id')->change(); + $table->unsignedInteger('user_id')->change(); + $table->unsignedInteger('import_job_id')->change(); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + $table->foreign('import_job_id')->references('id')->on('import_jobs')->onDelete('cascade'); + }); + } + + private function cleanImportJobTable() + { + foreach (ImportJob::cursor() as $importJob) { + try { + $this->accountExistOrFail($importJob->account_id); + $this->userExistOrFail($importJob->user_id); + } catch (ModelNotFoundException $e) { + $importJob->delete(); + continue; + } + } + + Schema::table('import_jobs', function (Blueprint $table) { + $table->unsignedInteger('account_id')->change(); + $table->unsignedInteger('user_id')->change(); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + }); + } + + private function cleanInvitationTable() + { + foreach (Invitation::cursor() as $invitation) { + try { + $this->accountExistOrFail($invitation->account_id); + $this->userExistOrFail($invitation->invited_by_user_id); + } catch (ModelNotFoundException $e) { + $invitation->delete(); + continue; + } + } + + Schema::table('invitations', function (Blueprint $table) { + $table->unsignedInteger('account_id')->change(); + $table->unsignedInteger('invited_by_user_id')->change(); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + $table->foreign('invited_by_user_id')->references('id')->on('users')->onDelete('cascade'); + }); + } + + private function cleanJournalEntryTable() + { + foreach (JournalEntry::cursor() as $journalEntry) { + try { + $this->accountExistOrFail($journalEntry->account_id); + } catch (ModelNotFoundException $e) { + $journalEntry->delete(); + continue; + } + } + + Schema::table('journal_entries', function (Blueprint $table) { + $table->unsignedInteger('account_id')->change(); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + }); + } + + private function cleanModuleTable() + { + foreach (Module::cursor() as $module) { + try { + $this->accountExistOrFail($module->account_id); + } catch (ModelNotFoundException $e) { + $module->delete(); + continue; + } + } + + Schema::table('modules', function (Blueprint $table) { + $table->unsignedInteger('account_id')->change(); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + }); + } + + private function cleanNoteTable() + { + foreach (Note::cursor() as $note) { + try { + $this->accountExistOrFail($note->account_id); + $this->contactExistOrFail($note->contact_id); + } catch (ModelNotFoundException $e) { + $note->delete(); + continue; + } + } + + Schema::table('notes', function (Blueprint $table) { + $table->unsignedInteger('account_id')->change(); + $table->unsignedInteger('contact_id')->change(); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + $table->foreign('contact_id')->references('id')->on('contacts')->onDelete('cascade'); + }); + } + + private function cleanNotificationTable() + { + // this table is not used anymore, we can safely remove it from the + // database + Schema::drop('notifications'); + } + + private function cleanPetTable() + { + foreach (Pet::cursor() as $pet) { + try { + $this->accountExistOrFail($pet->account_id); + $this->contactExistOrFail($pet->contact_id); + PetCategory::findOrFail($pet->pet_category_id); + } catch (ModelNotFoundException $e) { + $pet->delete(); + continue; + } + } + + Schema::table('pets', function (Blueprint $table) { + $table->unsignedInteger('account_id')->change(); + $table->unsignedInteger('contact_id')->change(); + $table->unsignedInteger('pet_category_id')->change(); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + $table->foreign('contact_id')->references('id')->on('contacts')->onDelete('cascade'); + $table->foreign('pet_category_id')->references('id')->on('pet_categories')->onDelete('cascade'); + }); + } + + private function cleanRelationshipTypeGroupTable() + { + foreach (RelationshipTypeGroup::cursor() as $type) { + try { + $this->accountExistOrFail($type->account_id); + } catch (ModelNotFoundException $e) { + $type->delete(); + continue; + } + } + + Schema::table('relationship_type_groups', function (Blueprint $table) { + $table->unsignedInteger('account_id')->change(); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + }); + } + + private function cleanRelationshipTypeTable() + { + foreach (RelationshipType::cursor() as $type) { + try { + $this->accountExistOrFail($type->account_id); + RelationshipTypeGroup::findOrFail($type->relationship_type_group_id); + } catch (ModelNotFoundException $e) { + $type->delete(); + continue; + } + } + + Schema::table('relationship_types', function (Blueprint $table) { + $table->unsignedInteger('account_id')->change(); + $table->unsignedInteger('relationship_type_group_id')->change(); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + $table->foreign('relationship_type_group_id')->references('id')->on('relationship_type_groups')->onDelete('cascade'); + }); + } + + private function cleanRelationshipTable() + { + foreach (Relationship::cursor() as $relationship) { + try { + $this->accountExistOrFail($relationship->account_id); + RelationshipType::findOrFail($relationship->relationship_type_id); + $this->contactExistOrFail($relationship->contact_is); + $this->contactExistOrFail($relationship->of_contact); + } catch (ModelNotFoundException $e) { + $relationship->delete(); + continue; + } + } + + Schema::table('relationships', function (Blueprint $table) { + $table->unsignedInteger('account_id')->change(); + $table->unsignedInteger('relationship_type_id')->change(); + $table->unsignedInteger('contact_is')->change(); + $table->unsignedInteger('of_contact')->change(); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + $table->foreign('relationship_type_id')->references('id')->on('relationship_types')->onDelete('cascade'); + $table->foreign('contact_is')->references('id')->on('contacts')->onDelete('cascade'); + $table->foreign('of_contact')->references('id')->on('contacts')->onDelete('cascade'); + }); + } + + private function cleanSpecialDateTable() + { + foreach (SpecialDate::cursor() as $date) { + try { + $this->accountExistOrFail($date->account_id); + $this->contactExistOrFail($date->contact_id); + } catch (ModelNotFoundException $e) { + $date->delete(); + continue; + } + } + + Schema::table('special_dates', function (Blueprint $table) { + $table->unsignedInteger('account_id')->change(); + $table->unsignedInteger('contact_id')->change(); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + $table->foreign('contact_id')->references('id')->on('contacts')->onDelete('cascade'); + }); + } + + private function cleanTagTable() + { + foreach (Tag::cursor() as $tag) { + try { + $this->accountExistOrFail($tag->account_id); + } catch (ModelNotFoundException $e) { + $tag->delete(); + continue; + } + } + + Schema::table('tags', function (Blueprint $table) { + $table->unsignedInteger('account_id')->change(); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + }); + } + + private function cleanTaskTable() + { + foreach (Task::cursor() as $task) { + try { + $this->accountExistOrFail($task->account_id); + + if (! is_null($task->contact_id)) { + $this->contactExistOrFail($task->contact_id); + } + } catch (ModelNotFoundException $e) { + $task->delete(); + continue; + } + } + + Schema::table('tasks', function (Blueprint $table) { + $table->unsignedInteger('account_id')->change(); + $table->unsignedInteger('contact_id')->change(); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + $table->foreign('contact_id')->references('id')->on('contacts')->onDelete('cascade'); + }); + } + + private function cleanTermUserTable() + { + DB::table('term_user') + ->orderBy('user_id') + ->chunk(200, function ($termUsers) { + foreach ($termUsers as $termUser) { + try { + $this->accountExistOrFail($termUser->account_id); + $this->userExistOrFail($termUser->user_id); + Term::findOrFail($termUser->term_id); + } catch (ModelNotFoundException $e) { + DB::table('term_user') + ->where('account_id', $termUser->account_id) + ->where('user_id', $termUser->user_id) + ->where('term_id', $termUser->term_id) + ->delete(); + } + } + }); + + Schema::table('term_user', function (Blueprint $table) { + $table->unsignedInteger('account_id')->change(); + $table->unsignedInteger('user_id')->change(); + $table->unsignedInteger('term_id')->change(); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + $table->foreign('term_id')->references('id')->on('terms')->onDelete('cascade'); + }); + } + + private function cleanUserTable() + { + foreach (User::cursor() as $user) { + try { + $this->accountExistOrFail($user->account_id); + } catch (ModelNotFoundException $e) { + $user->delete(); + continue; + } + } + + Schema::table('users', function (Blueprint $table) { + $table->unsignedInteger('account_id')->change(); + $table->unsignedInteger('currency_id')->nullable()->change(); + $table->unsignedInteger('invited_by_user_id')->nullable()->change(); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + $table->foreign('currency_id')->references('id')->on('currencies')->onDelete('set null'); + $table->foreign('invited_by_user_id')->references('id')->on('users')->onDelete('set null'); + }); + + foreach (User::cursor() as $user) { + try { + if (! is_null($user->invited_by_user_id)) { + $this->userExistOrFail($user->invited_by_user_id); + } + } catch (ModelNotFoundException $e) { + $user->invited_by_user_id = null; + $user->save(); + continue; + } + } + } + + private function cleanContactTable() + { + foreach (Contact::cursor() as $contact) { + try { + $this->accountExistOrFail($contact->account_id); + } catch (ModelNotFoundException $e) { + $contact->forceDelete(); + continue; + } + } + + Schema::table('contacts', function (Blueprint $table) { + $table->unsignedInteger('account_id')->change(); + $table->unsignedInteger('avatar_photo_id')->change(); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + $table->foreign('avatar_photo_id')->references('id')->on('photos')->onDelete('set null'); + }); + } +} diff --git a/database/migrations/2019_12_21_100315_change_gift_status.php b/database/migrations/2019_12_21_100315_change_gift_status.php new file mode 100644 index 0000000..2898c5e --- /dev/null +++ b/database/migrations/2019_12_21_100315_change_gift_status.php @@ -0,0 +1,72 @@ +dropColumn(['status', 'date']); + }); + } else { + Schema::table('gifts', function (Blueprint $table) { + $table->dropColumn('status'); + }); + } + } elseif (Schema::hasColumn('gifts', 'date')) { + Schema::table('gifts', function (Blueprint $table) { + $table->dropColumn('date'); + }); + } + + Gift::chunk(500, function ($gifts) { + foreach ($gifts as $gift) { + try { + Contact::findOrFail($gift->is_for); + } catch (ModelNotFoundException $e) { + $gift->recipient = null; + $gift->save(); + } + } + }); + + Schema::table('gifts', function (Blueprint $table) { + $table->unsignedInteger('is_for')->nullable()->change(); + $table->string('status', 8)->after('has_been_received')->default('idea'); + $table->datetime('date')->after('status')->nullable(); + + $table->foreign('is_for')->references('id')->on('contacts')->onDelete('set null'); + }); + + Gift::chunk(500, function ($gifts) { + foreach ($gifts as $gift) { + $gift->status = $gift->has_been_offered === 1 ? 'offered' : + ($gift->has_been_received === 1 ? 'received' : 'idea'); + $gift->save(); + } + }); + + Schema::table('gifts', function (Blueprint $table) { + $table->dropColumn([ + 'is_an_idea', + 'has_been_offered', + 'has_been_received', + 'offered_at', + 'received_at', + ]); + }); + } +} diff --git a/database/migrations/2019_12_21_194559_add_photo_gift.php b/database/migrations/2019_12_21_194559_add_photo_gift.php new file mode 100644 index 0000000..ff1be0b --- /dev/null +++ b/database/migrations/2019_12_21_194559_add_photo_gift.php @@ -0,0 +1,37 @@ +unsignedInteger('photo_id'); + $table->unsignedInteger('gift_id'); + $table->timestamps(); + + $table->foreign('photo_id')->references('id')->on('photos')->onDelete('cascade'); + $table->foreign('gift_id')->references('id')->on('gifts')->onDelete('cascade'); + + $table->primary(['photo_id', 'gift_id']); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('gift_photo'); + } +} diff --git a/database/migrations/2019_12_27_23533_rename_picnicked.php b/database/migrations/2019_12_27_23533_rename_picnicked.php new file mode 100644 index 0000000..92bf1b6 --- /dev/null +++ b/database/migrations/2019_12_27_23533_rename_picnicked.php @@ -0,0 +1,22 @@ +where('translation_key', 'picknicked') + ->update(['translation_key' => 'picnicked']); + DB::table('activity_types') + ->where('translation_key', 'picknicked') + ->update(['translation_key' => 'picnicked']); + } +} diff --git a/database/migrations/2020_02_03_015403_create_audit_log_table.php b/database/migrations/2020_02_03_015403_create_audit_log_table.php new file mode 100644 index 0000000..a0704cc --- /dev/null +++ b/database/migrations/2020_02_03_015403_create_audit_log_table.php @@ -0,0 +1,42 @@ +bigIncrements('id'); + $table->unsignedInteger('account_id'); + $table->unsignedInteger('author_id')->nullable(); + $table->unsignedInteger('about_contact_id')->nullable(); + $table->string('author_name'); + $table->string('action'); + $table->text('objects'); + $table->datetime('audited_at'); + $table->boolean('should_appear_on_dashboard')->default(false); + $table->timestamps(); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + $table->foreign('author_id')->references('id')->on('users')->onDelete('set null'); + $table->foreign('about_contact_id')->references('id')->on('contacts')->onDelete('set null'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('audit_logs'); + } +} diff --git a/database/migrations/2020_02_18_211620_add_contact_field_label.php b/database/migrations/2020_02_18_211620_add_contact_field_label.php new file mode 100644 index 0000000..d47a087 --- /dev/null +++ b/database/migrations/2020_02_18_211620_add_contact_field_label.php @@ -0,0 +1,59 @@ +bigIncrements('id'); + $table->unsignedInteger('account_id'); + $table->string('label_i18n', 20)->nullable(); + $table->string('label', 500)->nullable(); + $table->timestamps(); + $table->index(['label_i18n', 'account_id']); + $table->index(['label', 'account_id']); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + }); + + Schema::create('contact_field_contact_field_label', function (Blueprint $table) { + $table->unsignedBigInteger('contact_field_label_id'); + $table->unsignedInteger('contact_field_id'); + $table->unsignedInteger('account_id'); + $table->index(['contact_field_label_id', 'contact_field_id', 'account_id'], 'contact_field_contact_field_label_index'); + $table->foreign('contact_field_label_id')->references('id')->on('contact_field_labels')->onDelete('cascade'); + $table->foreign('contact_field_id')->references('id')->on('contact_fields')->onDelete('cascade'); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + }); + + Schema::create('address_contact_field_label', function (Blueprint $table) { + $table->unsignedBigInteger('contact_field_label_id'); + $table->unsignedInteger('address_id'); + $table->unsignedInteger('account_id'); + $table->index(['contact_field_label_id', 'address_id', 'account_id'], 'address_contact_field_label_index'); + $table->foreign('contact_field_label_id')->references('id')->on('contact_field_labels')->onDelete('cascade'); + $table->foreign('address_id')->references('id')->on('addresses')->onDelete('cascade'); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('address_contact_field_label'); + Schema::dropIfExists('contact_field_contact_field_label'); + Schema::dropIfExists('contact_field_labels'); + } +} diff --git a/database/migrations/2020_03_22_132429_rename_birthday_reminder_title_deceased.php b/database/migrations/2020_03_22_132429_rename_birthday_reminder_title_deceased.php new file mode 100644 index 0000000..fd28d57 --- /dev/null +++ b/database/migrations/2020_03_22_132429_rename_birthday_reminder_title_deceased.php @@ -0,0 +1,30 @@ +filter(function ($contact) { + return $contact->is_dead && ! empty($contact->birthday_reminder_id); + }); + + foreach ($contacts as $contact) { + $locale = $contact->account->getFirstLocale(); + Reminder::where('id', $contact->birthday_reminder_id) + ->update([ + 'title' => trans('people.people_add_birthday_reminder_deceased', ['name' => $contact->first_name], $locale), + ]); + } + } +} diff --git a/database/migrations/2020_03_25_055551_add_address_book.php b/database/migrations/2020_03_25_055551_add_address_book.php new file mode 100644 index 0000000..6a77168 --- /dev/null +++ b/database/migrations/2020_03_25_055551_add_address_book.php @@ -0,0 +1,40 @@ +bigIncrements('id'); + $table->unsignedInteger('account_id'); + $table->unsignedInteger('user_id'); + + $table->string('description', 500)->nullable(); + $table->string('name', 100); + $table->timestamps(); + + $table->index('name'); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('addressbooks'); + } +} diff --git a/database/migrations/2020_03_25_065551_add_addressbook_subscription.php b/database/migrations/2020_03_25_065551_add_addressbook_subscription.php new file mode 100644 index 0000000..eee0bfb --- /dev/null +++ b/database/migrations/2020_03_25_065551_add_addressbook_subscription.php @@ -0,0 +1,50 @@ +bigIncrements('id'); + $table->unsignedInteger('account_id'); + $table->unsignedInteger('user_id'); + $table->unsignedBigInteger('address_book_id'); + + $table->string('name', 256); + $table->string('uri', 2096); + $table->string('username', 1024); + $table->string('password', 2048); + $table->boolean('readonly'); + $table->boolean('active')->default(true); + $table->string('capabilities', 2048); + $table->string('syncToken', 512)->nullable(); + $table->string('localSyncToken', 1024)->nullable(); + $table->smallInteger('frequency')->default(180); // 3 hours + $table->timestamp('last_synchronized_at', 0)->nullable(); + $table->timestamps(); + + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + $table->foreign('address_book_id')->references('id')->on('addressbooks')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('addressbook_subscriptions'); + } +} diff --git a/database/migrations/2020_03_25_082324_add_contact_address_book_id.php b/database/migrations/2020_03_25_082324_add_contact_address_book_id.php new file mode 100644 index 0000000..30a8783 --- /dev/null +++ b/database/migrations/2020_03_25_082324_add_contact_address_book_id.php @@ -0,0 +1,33 @@ +unsignedBigInteger('address_book_id')->after('account_id')->nullable(); + $table->foreign('address_book_id')->references('id')->on('addressbooks')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('contacts', function (Blueprint $table) { + $table->dropColumn('address_book_id'); + }); + } +} diff --git a/database/migrations/2020_03_25_201407_add_contact_vcard_data.php b/database/migrations/2020_03_25_201407_add_contact_vcard_data.php new file mode 100644 index 0000000..8d5f395 --- /dev/null +++ b/database/migrations/2020_03_25_201407_add_contact_vcard_data.php @@ -0,0 +1,32 @@ +mediumText('vcard')->after('gravatar_url')->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('contacts', function (Blueprint $table) { + $table->dropColumn('vcard'); + }); + } +} diff --git a/database/migrations/2020_04_24_185810_remove_duplicate_currency.php b/database/migrations/2020_04_24_185810_remove_duplicate_currency.php new file mode 100644 index 0000000..9db3fd0 --- /dev/null +++ b/database/migrations/2020_04_24_185810_remove_duplicate_currency.php @@ -0,0 +1,37 @@ +select(DB::raw('max(id) as id'), 'iso') + ->groupBy('iso') + ->havingRaw('count(*) > ?', [1]) + ->get(); + + foreach ($doubleCurrency as $currency) { + $newCurrency = Currency::where('iso', $currency->iso)->first(); + + User::where('currency_id', $currency->id)->chunk(500, function ($users) use ($newCurrency) { + foreach ($users as $user) { + $user->update([ + 'currency_id' => $newCurrency->id, + ]); + } + }); + + Currency::find($currency->id)->delete(); + } + } +} diff --git a/database/migrations/2020_04_24_205810_currencies_table_seed.php b/database/migrations/2020_04_24_205810_currencies_table_seed.php new file mode 100644 index 0000000..18cd6c5 --- /dev/null +++ b/database/migrations/2020_04_24_205810_currencies_table_seed.php @@ -0,0 +1,39 @@ +map(function ($currency) { + return $currency->iso; + }) + ->toArray(); + + $insert = $currencies->reject(function ($currency) use ($currentCurrencies) { + return in_array($currency['iso']['code'], $currentCurrencies); + })->map(function ($currency) { + return [ + 'iso' => $currency['iso']['code'], + 'name' => $currency['name'], + 'symbol' => $currency['units']['major']['symbol'], + ]; + })->values()->toArray(); + + DB::table('currencies')->insert($insert); + } +} diff --git a/database/migrations/2020_04_24_212138_update_amount_format.php b/database/migrations/2020_04_24_212138_update_amount_format.php new file mode 100644 index 0000000..9c0e8a7 --- /dev/null +++ b/database/migrations/2020_04_24_212138_update_amount_format.php @@ -0,0 +1,110 @@ +fixDebts(); + $this->fixGifts(); + } + + private function fixDebts() + { + Schema::table('debts', function (Blueprint $table) { + $table->decimal('amount', 13, 2)->change(); + $table->unsignedInteger('currency_id')->after('amount')->nullable(); + $table->foreign('currency_id')->references('id')->on('currencies')->onDelete('set null'); + }); + + DB::table('debts') + ->orderBy('id') + ->chunk(500, function ($debts) { + foreach ($debts as $debt) { + try { + $account = Account::findOrFail($debt->account_id); + $user = $account->users()->firstOrFail(); + } catch (ModelNotFoundException $e) { + continue; + } + + DB::update('update debts set amount = ?, currency_id = ? where id = ?', [ + floatval($debt->amount) * self::unitAdjustment($user->currency), + $user->currency_id, + $debt->id, + ]); + } + }); + + Schema::table('debts', function (Blueprint $table) { + $table->integer('amount')->change(); + }); + } + + private function fixGifts() + { + Schema::table('gifts', function (Blueprint $table) { + $table->unsignedInteger('currency_id')->after('value')->nullable(); + $table->foreign('currency_id')->references('id')->on('currencies')->onDelete('set null'); + }); + + DB::table('gifts') + ->where('value', '!=', 'null') + ->orderBy('id') + ->chunk(500, function ($gifts) { + foreach ($gifts as $gift) { + try { + $account = Account::findOrFail($gift->account_id); + $user = $account->users()->firstOrFail(); + } catch (ModelNotFoundException $e) { + continue; + } + + DB::update('update gifts set value = ?, currency_id = ? where id = ?', [ + floatval($gift->value) * self::unitAdjustment($user->currency), + $user->currency_id, + $gift->id, + ]); + } + }); + + Schema::table('gifts', function (Blueprint $table) { + $table->integer('value')->change()->nullable(); + $table->renameColumn('value', 'amount'); + }); + } + + /** + * Get unit adjustement value for the currency. + * + * @param \App\Models\Settings\Currency|int|null $currency + * @return int + */ + private static function unitAdjustment($currency): int + { + $currency = MoneyHelper::getCurrency($currency); + + if (! $currency) { + return 100; + } + + $moneyCurrency = new MoneyCurrency($currency->iso); + $currencies = new ISOCurrencies(); + + return (int) pow(10, $currencies->subunitFor($moneyCurrency)); + } +} diff --git a/database/migrations/2020_05_08_072433_google2fa_column_size.php b/database/migrations/2020_05_08_072433_google2fa_column_size.php new file mode 100644 index 0000000..e561b29 --- /dev/null +++ b/database/migrations/2020_05_08_072433_google2fa_column_size.php @@ -0,0 +1,19 @@ +string('google2fa_secret', 256)->change(); + }); + } +} diff --git a/database/migrations/2020_05_31_091556_custom_life_event_types.php b/database/migrations/2020_05_31_091556_custom_life_event_types.php new file mode 100644 index 0000000..e3ffc59 --- /dev/null +++ b/database/migrations/2020_05_31_091556_custom_life_event_types.php @@ -0,0 +1,24 @@ +string('name')->nullable()->change(); + }); + + DB::table('life_event_types') + ->update(['name' => null]); + } +} diff --git a/database/migrations/2020_08_05_184814_upgrade_passport.php b/database/migrations/2020_08_05_184814_upgrade_passport.php new file mode 100644 index 0000000..c4971bd --- /dev/null +++ b/database/migrations/2020_08_05_184814_upgrade_passport.php @@ -0,0 +1,26 @@ +string('secret', 100)->nullable()->change(); + }); + + if (! Schema::hasColumn('oauth_clients', 'provider')) { + Schema::table('oauth_clients', function (Blueprint $table) { + $table->string('provider')->after('secret')->nullable(); + }); + } + } +} diff --git a/database/migrations/2020_11_01_000001_create_subscription_items_table.php b/database/migrations/2020_11_01_000001_create_subscription_items_table.php new file mode 100644 index 0000000..467a9c8 --- /dev/null +++ b/database/migrations/2020_11_01_000001_create_subscription_items_table.php @@ -0,0 +1,41 @@ +integer('quantity')->nullable()->change(); + }); + + Schema::create('subscription_items', function (Blueprint $table) { + $table->bigIncrements('id'); + $table->unsignedBigInteger('subscription_id'); + $table->string('stripe_id')->index(); + $table->string('stripe_plan'); + $table->integer('quantity'); + $table->timestamps(); + + $table->unique(['subscription_id', 'stripe_plan']); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('subscription_items'); + } +} diff --git a/database/migrations/2020_12_19_205923_add_uuids.php b/database/migrations/2020_12_19_205923_add_uuids.php new file mode 100644 index 0000000..07e7791 --- /dev/null +++ b/database/migrations/2020_12_19_205923_add_uuids.php @@ -0,0 +1,79 @@ +tables as $name) { + if (! Schema::hasColumn($name, 'uuid')) { + Schema::table($name, function (Blueprint $table) use ($name) { + $table->uuid('uuid')->after('id')->nullable(); + $table->index($name === 'accounts' ? ['uuid'] : ['account_id', 'uuid']); + }); + } + } + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + foreach ($this->tables as $name) { + if (Schema::hasColumn($name, 'uuid')) { + Schema::table($name, function (Blueprint $table) use ($name) { + try { + $table->dropIndex($name === 'accounts' ? ['uuid'] : ['account_id', 'uuid']); + $table->dropColumn('uuid'); + } catch (\Exception $e) { + // + } + }); + } + } + } +} diff --git a/database/migrations/2021_04_23_190837_remove_reminder_sent.php b/database/migrations/2021_04_23_190837_remove_reminder_sent.php new file mode 100644 index 0000000..85fbf22 --- /dev/null +++ b/database/migrations/2021_04_23_190837_remove_reminder_sent.php @@ -0,0 +1,17 @@ +string('id')->primary(); + $table->string('name'); + $table->integer('total_jobs'); + $table->integer('pending_jobs'); + $table->integer('failed_jobs'); + $table->text('failed_job_ids'); + $table->mediumText('options')->nullable(); + $table->integer('cancelled_at')->nullable(); + $table->integer('created_at'); + $table->integer('finished_at')->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('job_batches'); + } +} diff --git a/database/migrations/2021_10_11_060512_add_distant_etag.php b/database/migrations/2021_10_11_060512_add_distant_etag.php new file mode 100644 index 0000000..0e86ffa --- /dev/null +++ b/database/migrations/2021_10_11_060512_add_distant_etag.php @@ -0,0 +1,32 @@ +string('distant_etag', 256)->after('vcard')->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('contacts', function (Blueprint $table) { + $table->dropColumn('distant_etag'); + }); + } +} diff --git a/database/migrations/2021_10_14_212144_v_card_company.php b/database/migrations/2021_10_14_212144_v_card_company.php new file mode 100644 index 0000000..7044dc1 --- /dev/null +++ b/database/migrations/2021_10_14_212144_v_card_company.php @@ -0,0 +1,19 @@ +where('company', ';') + ->update(['company' => null]); + } +} diff --git a/database/migrations/2022_01_01_202745_add_export_jobs.php b/database/migrations/2022_01_01_202745_add_export_jobs.php new file mode 100644 index 0000000..2375c12 --- /dev/null +++ b/database/migrations/2022_01_01_202745_add_export_jobs.php @@ -0,0 +1,42 @@ +id(); + $table->uuid('uuid')->nullable()->index(); + $table->unsignedInteger('account_id'); + $table->unsignedInteger('user_id'); + $table->string('type', 4); + $table->string('status', 6)->nullable(); + $table->string('location', 6)->nullable(); + $table->string('filename', 256)->nullable(); + $table->datetime('started_at')->nullable(); + $table->datetime('ended_at')->nullable(); + $table->timestamps(); + $table->foreign('account_id')->references('id')->on('accounts')->onDelete('cascade'); + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('export_jobs'); + } +} diff --git a/database/migrations/2022_01_02_222042_contact_soft_delete.php b/database/migrations/2022_01_02_222042_contact_soft_delete.php new file mode 100644 index 0000000..1abeb73 --- /dev/null +++ b/database/migrations/2022_01_02_222042_contact_soft_delete.php @@ -0,0 +1,34 @@ +softDeletes(); + }); + } + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('contacts', function (Blueprint $table) { + $table->dropSoftDeletes(); + }); + } +} diff --git a/database/migrations/2022_04_25_165338_cashier_stripe_rename_plan.php b/database/migrations/2022_04_25_165338_cashier_stripe_rename_plan.php new file mode 100644 index 0000000..35b34cc --- /dev/null +++ b/database/migrations/2022_04_25_165338_cashier_stripe_rename_plan.php @@ -0,0 +1,57 @@ +renameColumn('stripe_plan', 'stripe_price'); + }); + Schema::table('subscription_items', function (Blueprint $table) { + $table->renameColumn('stripe_plan', 'stripe_price'); + }); + + Schema::table('accounts', function (Blueprint $table) { + $table->renameColumn('card_brand', 'pm_type'); + $table->renameColumn('card_last_four', 'pm_last_four'); + }); + + Schema::table('subscription_items', function (Blueprint $table) { + $table->string('stripe_product')->nullable()->after('stripe_id'); + $table->integer('quantity')->nullable()->change(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('subscription_items', function (Blueprint $table) { + $table->dropColumn('stripe_product'); + }); + + Schema::table('accounts', function (Blueprint $table) { + $table->renameColumn('pm_type', 'card_brand'); + $table->renameColumn('pm_last_four', 'card_last_four'); + }); + + Schema::table('subscription_items', function (Blueprint $table) { + $table->renameColumn('stripe_price', 'stripe_plan'); + }); + Schema::table('subscriptions', function (Blueprint $table) { + $table->renameColumn('stripe_price', 'stripe_plan'); + }); + } +}; diff --git a/database/migrations/2024_05_03_100000_update_webauthn_keys.php b/database/migrations/2024_05_03_100000_update_webauthn_keys.php new file mode 100644 index 0000000..b5bd698 --- /dev/null +++ b/database/migrations/2024_05_03_100000_update_webauthn_keys.php @@ -0,0 +1,36 @@ +chunk(200, function ($keys) { + foreach ($keys as $key) { + $key->update(['credentialId' => $key->credentialId]); + } + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + WebauthnKey::select(['id', 'credentialId'])->chunk(200, function ($keys) { + foreach ($keys as $key) { + $key->setRawAttributes(['credentialId' => base64_encode($key->credentialId)]); + $key->save(); + } + }); + } +}; diff --git a/database/seeds/.gitkeep b/database/seeds/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/database/seeds/.gitkeep @@ -0,0 +1 @@ + diff --git a/database/seeds/DatabaseSeeder.php b/database/seeds/DatabaseSeeder.php new file mode 100644 index 0000000..cdea482 --- /dev/null +++ b/database/seeds/DatabaseSeeder.php @@ -0,0 +1,26 @@ +call(FakeUserTableSeeder::class); + break; + case 'testing': + $this->call(FakeUserTableSeeder::class); + break; + case 'production': + break; + } + } +} diff --git a/database/seeds/FakeUserTableSeeder.php b/database/seeds/FakeUserTableSeeder.php new file mode 100644 index 0000000..838ef38 --- /dev/null +++ b/database/seeds/FakeUserTableSeeder.php @@ -0,0 +1,18 @@ +setAvatarColor(); + $contact->save(); + } + } +} diff --git a/database/seeds/json/2017_08_02_124102_add_world_currencies.json b/database/seeds/json/2017_08_02_124102_add_world_currencies.json new file mode 100644 index 0000000..c8033d0 --- /dev/null +++ b/database/seeds/json/2017_08_02_124102_add_world_currencies.json @@ -0,0 +1,6291 @@ +{ + "AED": { + "name": "Emirati Dirham", + "iso": { + "code": "AED", + "number": "784" + }, + "units": { + "major": { + "name": "dirham", + "symbol": ".د.ب" + }, + "minor": { + "name": "fils", + "symbol": "فلس", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "5.د.ب", + "10.د.ب", + "20.د.ب", + "50.د.ب", + "100.د.ب", + "200.د.ب", + "500.د.ب" + ], + "rare": [ + "1000.د.ب" + ] + }, + "coins": { + "frequent": [ + "50فلس", + "1.د.ب" + ], + "rare": [ + "25فلس" + ] + } + }, + "AFN": { + "name": "Afghan Afghani", + "iso": { + "code": "AFN", + "number": "004" + }, + "units": { + "major": { + "name": "afghani", + "symbol": "؋" + }, + "minor": { + "name": "Pul", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "1؋", + "2؋", + "5؋", + "10؋", + "20؋", + "50؋", + "100؋", + "500؋", + "1000؋" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "1؋", + "2؋", + "5؋" + ], + "rare": [] + } + }, + "ALL": { + "name": "Albanian lek", + "iso": { + "code": "ALL", + "number": "008" + }, + "units": { + "major": { + "name": "lek", + "symbol": "lek" + }, + "minor": { + "name": "Qindarkë", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "Lek200", + "Lek500", + "Lek1000", + "Lek2000", + "Lek5000" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "Lek1", + "Lek5", + "Lek10", + "Lek20", + "Lek50", + "Lek100" + ], + "rare": [] + } + }, + "AMD": { + "name": "Armenian dram", + "iso": { + "code": "AMD", + "number": "051" + }, + "units": { + "major": { + "name": "dram", + "symbol": "" + }, + "minor": { + "name": "luma", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "500", + "1000", + "5000", + "10000", + "20000", + "50000", + "100000" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "10", + "20", + "50", + "100", + "200", + "500" + ], + "rare": [] + } + }, + "ANG": { + "name": "Dutch Guilder", + "iso": { + "code": "ANG", + "number": "532" + }, + "units": { + "major": { + "name": "guilder", + "symbol": "ƒ" + }, + "minor": { + "name": "cent", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "ƒ10", + "ƒ25", + "ƒ50", + "ƒ100" + ], + "rare": [ + "ƒ5", + "ƒ250" + ] + }, + "coins": { + "frequent": [ + "1", + "5", + "10", + "25", + "50", + "ƒ1", + "ƒ2½", + "ƒ5" + ], + "rare": [] + } + }, + "AOA": { + "name": "Angolan Kwanza", + "iso": { + "code": "AOA", + "number": "982" + }, + "units": { + "major": { + "name": "Kwanza", + "symbol": "Kz" + }, + "minor": { + "name": "cêntimos", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "Kz10", + "Kz50", + "Kz100", + "Kz200", + "Kz500", + "Kz1000", + "Kz2000" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "Kz1", + "Kz2", + "Kz5", + "10", + "50" + ], + "rare": [] + } + }, + "ARS": { + "name": "Argentine peso", + "iso": { + "code": "ARS", + "number": "032" + }, + "units": { + "major": { + "name": "peso", + "symbol": "$" + }, + "minor": { + "name": "centavo", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "$2", + "$5", + "$10", + "$20", + "$50", + "$100" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "5¢", + "10¢", + "25¢", + "50¢", + "$1", + "$2" + ], + "rare": [] + } + }, + "AUD": { + "name": "Australian Dollar", + "iso": { + "code": "AUD", + "number": "036" + }, + "units": { + "major": { + "name": "dollar", + "symbol": "$" + }, + "minor": { + "name": "cent", + "symbol": "c", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "5$", + "10$", + "20$", + "50$", + "100$" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "1$", + "2$", + "5c", + "10c", + "20c", + "50c" + ], + "rare": [] + } + }, + "AWG": { + "name": "Arubin florin", + "iso": { + "code": "AWG", + "number": "533" + }, + "units": { + "major": { + "name": "florin", + "symbol": "ƒ" + }, + "minor": { + "name": "cent", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "ƒl0", + "ƒ25", + "ƒ50", + "ƒ100", + "ƒ500" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "5", + "10", + "25", + "50", + "ƒ1", + "ƒ2½", + "ƒ5" + ], + "rare": [] + } + }, + "AZN": { + "name": "Azerbaijani manat", + "iso": { + "code": "AZN", + "number": "944" + }, + "units": { + "major": { + "name": "manat", + "symbol": "ман" + }, + "minor": { + "name": "Qepik", + "symbol": "qr", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "ман1", + "ман5", + "ман10", + "ман20", + "ман50", + "ман100" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "qr1", + "qr3", + "qr5", + "qr10", + "qr20", + "qr50" + ], + "rare": [] + } + }, + "BAM": { + "name": "Bosnian Convertible Marka", + "iso": { + "code": "BAM", + "number": "977" + }, + "units": { + "major": { + "name": "covertible marks", + "symbol": "KM" + }, + "minor": { + "name": "fening", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "KM10", + "KM20", + "KM50", + "KM100", + "KM200" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "5", + "10", + "20", + "50", + "KM1", + "KM2", + "KM5" + ], + "rare": [] + } + }, + "BBD": { + "name": "Barbadian dollar", + "iso": { + "code": "BBD", + "number": "052" + }, + "units": { + "major": { + "name": "dollar", + "symbol": "$" + }, + "minor": { + "name": "cent", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "$2", + "$5", + "$10", + "$20", + "$50", + "$100" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "1", + "5", + "10", + "25" + ], + "rare": [] + } + }, + "BDT": { + "name": "Bangladeshi Taka", + "iso": { + "code": "BDT", + "number": "050" + }, + "units": { + "major": { + "name": "Taka", + "symbol": "Tk" + }, + "minor": { + "name": "Poisha", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "Tk2", + "Tk5", + "Tk10", + "Tk20", + "Tk50", + "Tk100", + "Tk500", + "Tk1000" + ], + "rare": [ + "Tk1" + ] + }, + "coins": { + "frequent": [ + "Tk1", + "Tk2", + "Tk5" + ], + "rare": [ + "1", + "5", + "10", + "25", + "50" + ] + } + }, + "BGN": { + "name": "Bulgarian lev", + "iso": { + "code": "BGN", + "number": "975" + }, + "units": { + "major": { + "name": "lev", + "symbol": "лв" + }, + "minor": { + "name": "stotinki", + "symbol": "стотинки", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "лв2", + "лв5", + "лв10", + "лв20", + "лв50", + "лв100" + ], + "rare": [ + "лв1" + ] + }, + "coins": { + "frequent": [ + "стотинки1", + "стотинки2", + "стотинки5", + "стотинки10", + "стотинки20", + "стотинки50" + ], + "rare": [] + } + }, + "BHD": { + "name": "Bahraini Dinar", + "iso": { + "code": "BHD", + "number": "048" + }, + "units": { + "major": { + "name": "dinar", + "symbol": ".د.ب or BD" + }, + "minor": { + "name": "fils", + "symbol": "", + "majorValue": 0.001 + } + }, + "banknotes": { + "frequent": [ + "1.د.ب", + "5.د.ب", + "10.د.ب", + "50.د.ب", + "100.د.ب", + "500.د.ب" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "5", + "10", + "25", + "50", + "100", + "500" + ], + "rare": [] + } + }, + "BIF": { + "name": "Burundian Franc", + "iso": { + "code": "BIF", + "number": "108" + }, + "units": { + "major": { + "name": "franc", + "symbol": "" + }, + "minor": { + "name": "centime", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "10", + "20", + "50", + "100", + "500", + "1000", + "2000", + "5000", + "10000" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "1", + "5" + ], + "rare": [] + } + }, + "BMD": { + "name": "Bermudian dollar", + "iso": { + "code": "BMD", + "number": "060" + }, + "units": { + "major": { + "name": "dollar", + "symbol": "$" + }, + "minor": { + "name": "cent", + "symbol": "¢", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "$2", + "$5", + "$10", + "$20", + "$50", + "$100" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "1¢", + "5¢", + "10¢", + "25¢", + "$1" + ], + "rare": [ + "50¢", + "$5" + ] + } + }, + "BND": { + "name": "Bruneian Dollar", + "iso": { + "code": "BND", + "number": "096" + }, + "units": { + "major": { + "name": "dollar", + "symbol": "$" + }, + "minor": { + "name": "cent", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "$1", + "$5", + "$10" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "1", + "5", + "10", + "20", + "50" + ], + "rare": [] + } + }, + "BOB": { + "name": "Bolivian Boliviano", + "iso": { + "code": "BOB", + "number": "068" + }, + "units": { + "major": { + "name": "boliviano", + "symbol": "$b" + }, + "minor": { + "name": "centavo", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "$b10", + "$b20", + "$b50", + "$b100", + "$b200" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "10", + "20", + "50", + "$b1", + "$b2", + "$b5" + ], + "rare": [] + } + }, + "BRL": { + "name": "Brazilian real", + "iso": { + "code": "BRL", + "number": "986" + }, + "units": { + "major": { + "name": "real", + "symbol": "R$" + }, + "minor": { + "name": "centavo", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "R$2", + "R$5", + "R$10", + "R$20", + "R$50", + "R$100" + ], + "rare": [ + "R$1" + ] + }, + "coins": { + "frequent": [ + "5", + "10", + "25", + "50", + "R$1" + ], + "rare": [ + "1" + ] + } + }, + "BSD": { + "name": "Bahamian dollar", + "iso": { + "code": "BSD", + "number": "044" + }, + "units": { + "major": { + "name": "dollar", + "symbol": "B$" + }, + "minor": { + "name": "cent", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "$1", + "$2", + "$5", + "$10", + "$20", + "$50", + "$100" + ], + "rare": [ + "$1/2", + "$3" + ] + }, + "coins": { + "frequent": [ + "1", + "5", + "10", + "25" + ], + "rare": [ + "15", + "50", + "$1", + "$2", + "$5" + ] + } + }, + "BTN": { + "name": "Bhutanese Ngultrum", + "iso": { + "code": "BTN", + "number": "064" + }, + "units": { + "major": { + "name": "Ngultrum", + "symbol": "Nu." + }, + "minor": { + "name": "Chhertum", + "symbol": "Ch.", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "1", + "5", + "10", + "20", + "50", + "100", + "500", + "1000" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "1", + "5", + "10", + "20", + "25", + "50" + ], + "rare": [] + } + }, + "BWP": { + "name": "Botswana Pula", + "iso": { + "code": "BWP", + "number": "072" + }, + "units": { + "major": { + "name": "Pula", + "symbol": "P" + }, + "minor": { + "name": "Thebe", + "symbol": "t", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "P10", + "P20", + "P50", + "P100", + "P200" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "P1", + "P2", + "P5", + "5t", + "10t", + "25t", + "50t" + ], + "rare": [] + } + }, + "BYR": { + "name": "Belarusian ruble", + "iso": { + "code": "BYR", + "number": "974" + }, + "units": { + "major": { + "name": "ruble", + "symbol": "р" + }, + "minor": { + "name": "kapeyka", + "symbol": "к", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "5", + "10", + "20", + "50", + "100", + "200", + "500" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "k1", + "k2", + "k5", + "k10", + "k20", + "k50", + "1", + "2" + ], + "rare": [] + } + }, + "BZD": { + "name": "Belize dollar", + "iso": { + "code": "BZD", + "number": "084" + }, + "units": { + "major": { + "name": "dollar", + "symbol": "BZ$" + }, + "minor": { + "name": "cent", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "BZ$2", + "BZ$5", + "BZ$10", + "BZ$20", + "BZ$50", + "BZ$100" + ], + "rare": [ + "" + ] + }, + "coins": { + "frequent": [ + "1 cents", + "5 cents", + "10 cents", + "25 cents", + "50 cents", + "BZ$1", + "BZ$2" + ], + "rare": [] + } + }, + "CAD": { + "name": "Canadian Dollar", + "iso": { + "code": "CAD", + "number": "124" + }, + "units": { + "major": { + "name": "dollar", + "symbol": "$" + }, + "minor": { + "name": "cent", + "symbol": "¢", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "5$", + "10$", + "20$", + "50$", + "100$" + ], + "rare": [ + "1$", + "2$", + "500$", + "1000$" + ] + }, + "coins": { + "frequent": [ + "1$", + "2$", + "5¢", + "10¢", + "25¢" + ], + "rare": [ + "1¢", + "50¢" + ] + } + }, + "CHF": { + "name": "Swiss Franc", + "iso": { + "code": "CHF", + "number": "756" + }, + "units": { + "major": { + "name": "franc", + "symbol": "CHF" + }, + "minor": { + "name": "rappen", + "symbol": "Rp.", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "CHF10", + "CHF20", + "CHF50", + "CHF100", + "CHF200", + "CHF1000" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "5Rp.", + "10Rp.", + "20Rp.", + "50Rp.", + "CHF1", + "CHF2", + "CHF5" + ], + "rare": [] + } + }, + "CLP": { + "name": "Chilean Peso", + "iso": { + "code": "CLP", + "number": "152" + }, + "units": { + "major": { + "name": "peso", + "symbol": "$" + }, + "minor": { + "name": "centavo", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "$1000", + "$2000", + "$5000", + "$10000", + "$20000" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "$1", + "$5", + "$10", + "$50", + "$100", + "$500" + ], + "rare": [] + } + }, + "CNY": { + "name": "Yuan or chinese renminbi", + "iso": { + "code": "CNY", + "number": "156" + }, + "units": { + "major": { + "name": "yuan", + "symbol": "¥" + }, + "minor": { + "name": "jiǎo", + "symbol": "角", + "majorValue": 0.1 + } + }, + "banknotes": { + "frequent": [ + "¥1", + "¥5", + "¥10", + "¥20", + "¥50", + "¥100" + ], + "rare": [ + "角1", + "角2", + "角5", + "¥2" + ] + }, + "coins": { + "frequent": [ + "角1", + "角5", + "¥1" + ], + "rare": [] + } + }, + "COP": { + "name": "Colombian peso", + "iso": { + "code": "COP", + "number": "170" + }, + "units": { + "major": { + "name": "peso", + "symbol": "$" + }, + "minor": { + "name": "centavo", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "$1000", + "$2000", + "$5000", + "$10000", + "$20000", + "$50000" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "$5", + "$10", + "$20", + "$50", + "$100", + "$200", + "$500", + "$1000" + ], + "rare": [] + } + }, + "CRC": { + "name": "Costa Rican colón", + "iso": { + "code": "CRC", + "number": "188" + }, + "units": { + "major": { + "name": "colón", + "symbol": "₡" + }, + "minor": { + "name": "céntimo", + "symbol": "₡", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "₡1000", + "₡2000", + "₡5000", + "₡10000", + "₡20000", + "₡50000" + ], + "rare": [ + "" + ] + }, + "coins": { + "frequent": [ + "₡1", + "₡5", + "₡10", + "₡20", + "₡50", + "₡100", + "₡500" + ], + "rare": [] + } + }, + "CUC": { + "name": "Cuban convertible peso", + "iso": { + "code": "CUC", + "number": "931" + }, + "units": { + "major": { + "name": "peso", + "symbol": "$" + }, + "minor": { + "name": "centavo", + "symbol": "¢", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "$1", + "$3", + "$5", + "$10", + "$20", + "$50", + "$100" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "1¢", + "5¢", + "10¢", + "25¢", + "50¢", + "$1" + ], + "rare": [ + "$5" + ] + } + }, + "CUP": { + "name": "Cuban peso", + "iso": { + "code": "CUP", + "number": "192" + }, + "units": { + "major": { + "name": "peso", + "symbol": "₱" + }, + "minor": { + "name": "centavo", + "symbol": "¢", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "$1", + "$3", + "$5", + "$10", + "$20", + "$50", + "$100" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "¢1", + "¢2", + "¢5", + "¢20", + "$1", + "$3" + ], + "rare": [] + } + }, + "CVE": { + "name": "Cape Verdean Escudo", + "iso": { + "code": "CVE", + "number": "132" + }, + "units": { + "major": { + "name": "escudo", + "symbol": "$" + }, + "minor": { + "name": "centavo", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "$200", + "$500", + "$1000", + "$2000", + "$2500", + "$5000" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "$1", + "$5", + "$10", + "$20", + "$50", + "$100" + ], + "rare": [] + } + }, + "CZK": { + "name": "Czech koruna", + "iso": { + "code": "CZK", + "number": "200" + }, + "units": { + "major": { + "name": "koruna", + "symbol": "Kč" + }, + "minor": { + "name": "haléř", + "symbol": "h", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "100Kč", + "200Kč", + "500Kč", + "1000Kč", + "2000Kč", + "5000Kč" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "1Kč", + "2Kč", + "5Kč", + "10Kč", + "50Kč" + ], + "rare": [] + } + }, + "DJF": { + "name": "Djiboutian Franc", + "iso": { + "code": "DJF", + "number": "262" + }, + "units": { + "major": { + "name": "franc", + "symbol": "fdj" + }, + "minor": { + "name": "centime", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "1000fdj", + "2000fdj", + "5000fdj", + "10000fdj" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "1fdj", + "2fdj", + "5fdj", + "10fdj", + "20fdj", + "50fdj", + "100fdj", + "250fdj", + "500fdj" + ], + "rare": [] + } + }, + "DKK": { + "name": "Danish krone", + "iso": { + "code": "DKK", + "number": "208" + }, + "units": { + "major": { + "name": "kroner", + "symbol": "kr" + }, + "minor": { + "name": "kroner", + "symbol": "øre", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "kr50", + "kr100", + "kr200", + "kr500", + "kr1000" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "50øre", + "kr1", + "kr2", + "kr5", + "kr10", + "kr20" + ], + "rare": [] + } + }, + "DOP": { + "name": "Dominican peso", + "iso": { + "code": "DOP", + "number": "214" + }, + "units": { + "major": { + "name": "peso", + "symbol": "$" + }, + "minor": { + "name": "centavo", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "$20", + "$50", + "$100", + "$200", + "$500", + "$1000", + "$2000" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "$1", + "$5", + "$10", + "$25" + ], + "rare": [] + } + }, + "DZD": { + "name": "Algerian Dinar", + "iso": { + "code": "DZD", + "number": "012" + }, + "units": { + "major": { + "name": "dinar", + "symbol": "جد" + }, + "minor": { + "name": "Santeem", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "200", + "500", + "1000" + ], + "rare": [ + "100", + "2000" + ] + }, + "coins": { + "frequent": [ + "5", + "10", + "20", + "50" + ], + "rare": [ + "1", + "2", + "100" + ] + } + }, + "EGP": { + "name": "Egyptian Pound", + "iso": { + "code": "EGP", + "number": "818" + }, + "units": { + "major": { + "name": "pound", + "symbol": "£ " + }, + "minor": { + "name": "piastre", + "symbol": "Pt", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "Pt5", + "Pt10", + "Pt25", + "Pt50", + "£1", + "£5", + "£10", + "£20", + "£50", + "£100", + "£200" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "Pt1", + "Pt5", + "Pt10", + "Pt20", + "Pt25", + "Pt50", + "£1" + ], + "rare": [] + } + }, + "ERN": { + "name": "Eritrean nakfa", + "iso": { + "code": "ERN", + "number": "232" + }, + "units": { + "major": { + "name": "nafka", + "symbol": "ናቕፋ" + }, + "minor": { + "name": "cent", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "1ናቕፋ", + "5ናቕፋ", + "10ናቕፋ", + "20ናቕፋ", + "50ናቕፋ", + "100ናቕፋ" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "1", + "5", + "10", + "25", + "50", + "1ናቕፋ" + ], + "rare": [] + } + }, + "ETB": { + "name": "Ethiopian Birr", + "iso": { + "code": "ETB", + "number": "230" + }, + "units": { + "major": { + "name": "Birr", + "symbol": "Br" + }, + "minor": { + "name": "santim", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "Br1", + "Br5", + "Br10", + "Br50", + "Br100" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "1", + "5", + "10", + "25", + "50", + "Br1" + ], + "rare": [] + } + }, + "EUR": { + "name": "Euro", + "iso": { + "code": "EUR", + "number": "978" + }, + "units": { + "major": { + "name": "euro", + "symbol": "€" + }, + "minor": { + "name": "cent", + "symbol": "c", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "€5", + "€10", + "€20", + "€50", + "€100" + ], + "rare": [ + "€200", + "€500" + ] + }, + "coins": { + "frequent": [ + "€1", + "€2", + "5c", + "10c", + "20c", + "50c" + ], + "rare": [ + "1c", + "2c" + ] + } + }, + "FJD": { + "name": "Fijian dollar", + "iso": { + "code": "FJD", + "number": "242" + }, + "units": { + "major": { + "name": "dollar", + "symbol": "$" + }, + "minor": { + "name": "cent", + "symbol": "¢", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "$2", + "$5", + "$10", + "$20", + "$50", + "$100" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "5¢", + "10¢", + "20¢", + "50¢", + "$1", + "$2" + ], + "rare": [] + } + }, + "FKP": { + "name": "Falkland Island Pound", + "iso": { + "code": "FKP", + "number": "238" + }, + "units": { + "major": { + "name": "pound", + "symbol": "£" + }, + "minor": { + "name": "penny", + "symbol": "p", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "£5", + "£10", + "£20", + "£50" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "1p", + "2p", + "5p", + "10p", + "20p", + "50p", + "£1", + "£2" + ], + "rare": [] + } + }, + "GBP": { + "name": "British Pound", + "iso": { + "code": "GBP", + "number": "826" + }, + "units": { + "major": { + "name": "pound", + "symbol": "£" + }, + "minor": { + "name": "penny", + "symbol": "p", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "£5", + "£10", + "£20", + "£50" + ], + "rare": [ + "£100" + ] + }, + "coins": { + "frequent": [ + "£1", + "£2", + "1p", + "2p", + "5p", + "10p", + "20p", + "50p" + ], + "rare": [] + } + }, + "GEL": { + "name": "Georgian lari", + "iso": { + "code": "GEL", + "number": "981" + }, + "units": { + "major": { + "name": "lari", + "symbol": "ლ" + }, + "minor": { + "name": "tetri", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "ლ5", + "ლ10", + "ლ20", + "ლ50" + ], + "rare": [ + "ლ1", + "ლ2", + "ლ100", + "ლ200" + ] + }, + "coins": { + "frequent": [ + "1", + "2", + "5", + "10", + "20", + "50", + "ლ1", + "ლ2" + ], + "rare": [] + } + }, + "GHS": { + "name": "Ghanaian Cedi", + "iso": { + "code": "GHS", + "number": "936" + }, + "units": { + "major": { + "name": "Cedi", + "symbol": "GH¢" + }, + "minor": { + "name": "Pesewa", + "symbol": "Gp", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "GH¢5", + "GH¢10", + "GH¢20", + "GH¢50" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "GH¢1", + "1Gp", + "5Gp", + "10Gp", + "20Gp", + "50Gp" + ], + "rare": [] + } + }, + "GIP": { + "name": "Gibraltar pound", + "iso": { + "code": "GIP", + "number": "292" + }, + "units": { + "major": { + "name": "pound", + "symbol": "£" + }, + "minor": { + "name": "penny", + "symbol": "p", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "£5", + "£10", + "£20", + "£50", + "£100" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "1p", + "2p", + "5p", + "10p", + "20p", + "50p", + "£1", + "£2", + "£5" + ], + "rare": [] + } + }, + "GMD": { + "name": "Gambian dalasi", + "iso": { + "code": "GMD", + "number": "270" + }, + "units": { + "major": { + "name": "dalasi", + "symbol": "" + }, + "minor": { + "name": "butut", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "5 dalasis", + "10 dalasis", + "25 dalasis", + "50 dalasis", + "100 dalasis" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "1 bututs", + "5 bututs", + "10 bututs", + "25 bututs", + "50 bututs", + "1 dalasi" + ], + "rare": [] + } + }, + "GNF": { + "name": "Guinean Franc", + "iso": { + "code": "GNF", + "number": "324" + }, + "units": { + "major": { + "name": "franc", + "symbol": "" + }, + "minor": { + "name": "centime", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "100", + "500", + "1000", + "5000", + "10000" + ], + "rare": [ + "25", + "50" + ] + }, + "coins": { + "frequent": [ + "1", + "5", + "10", + "25", + "50" + ], + "rare": [] + } + }, + "GTQ": { + "name": "Guatemalan Quetzal", + "iso": { + "code": "GTQ", + "number": "320" + }, + "units": { + "major": { + "name": "quetzales", + "symbol": "Q" + }, + "minor": { + "name": "centavo", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "50", + "Q1", + "Q5", + "Q10", + "Q20", + "Q50", + "Q100", + "Q200" + ], + "rare": [ + "" + ] + }, + "coins": { + "frequent": [ + "1", + "5", + "10", + "25", + "50", + "Q1" + ], + "rare": [] + } + }, + "GYD": { + "name": "Guyanese dollar", + "iso": { + "code": "GYD", + "number": "328" + }, + "units": { + "major": { + "name": "dollar", + "symbol": "$" + }, + "minor": { + "name": "cent", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "$20", + "$100", + "$500", + "$1000", + "$5000" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "$1", + "$5", + "$10" + ], + "rare": [] + } + }, + "HKD": { + "name": "Hong Kong dollar", + "iso": { + "code": "HKD", + "number": "344" + }, + "units": { + "major": { + "name": "dollar", + "symbol": "HK$" + }, + "minor": { + "name": "cent", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "HK$10", + "HK$20", + "HK$50", + "HK$100", + "HK$500", + "HK$1000" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "10¢", + "20¢", + "50¢", + "HK$1", + "HK$2", + "HK$5", + "HK$10" + ], + "rare": [] + } + }, + "HNL": { + "name": "Honduran lempira", + "iso": { + "code": "HNL", + "number": "340" + }, + "units": { + "major": { + "name": "lempira", + "symbol": "L" + }, + "minor": { + "name": "centavo", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "L1", + "L2", + "L5", + "L10", + "L20", + "L50", + "L100", + "L500" + ], + "rare": [ + "" + ] + }, + "coins": { + "frequent": [ + "5¢", + "10¢", + "20¢", + "50¢" + ], + "rare": [] + } + }, + "HRK": { + "name": "Croatian kuna", + "iso": { + "code": "HRK", + "number": "191" + }, + "units": { + "major": { + "name": "kuna", + "symbol": "kn" + }, + "minor": { + "name": "lipa", + "symbol": "lp", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "kn5", + "kn10", + "kn20", + "kn50", + "kn100", + "kn200" + ], + "rare": [ + "kn500", + "kn1000" + ] + }, + "coins": { + "frequent": [ + "lp5", + "lp10", + "lp20", + "lp50", + "kn1", + "kn2", + "kn4" + ], + "rare": [ + "lp1", + "lp2", + "kn25" + ] + } + }, + "HTG": { + "name": "Haitian gourde", + "iso": { + "code": "HTG", + "number": "332" + }, + "units": { + "major": { + "name": "gourde", + "symbol": "G" + }, + "minor": { + "name": "centime", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "G10", + "G25", + "G50", + "G100", + "G250", + "G500" + ], + "rare": [ + "G1", + "G2", + "G5", + "G1000" + ] + }, + "coins": { + "frequent": [ + "5", + "10", + "20", + "50", + "G1", + "G5" + ], + "rare": [] + } + }, + "HUF": { + "name": "Hungarian forint", + "iso": { + "code": "HUF", + "number": "348" + }, + "units": { + "major": { + "name": "forint", + "symbol": "Ft" + }, + "minor": { + "name": "fillér", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "Ft500", + "Ft1000", + "Ft2000", + "Ft5000", + "Ft10000", + "Ft20000" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "Ft5", + "Ft10", + "Ft20", + "Ft50", + "Ft100", + "Ft200" + ], + "rare": [] + } + }, + "IDR": { + "name": "Indonesian Rupiah", + "iso": { + "code": "IDR", + "number": "360" + }, + "units": { + "major": { + "name": "Rupiah", + "symbol": "Rp" + }, + "minor": { + "name": "Sen", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "Rp1000", + "Rp2000", + "Rp5000", + "Rp10000", + "Rp20000", + "Rp50000", + "Rp100000" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "Rp100", + "Rp200", + "Rp500" + ], + "rare": [ + "Rp50", + "Rp1000" + ] + } + }, + "ILS": { + "name": "Israeli Shekel", + "iso": { + "code": "ILS", + "number": "376" + }, + "units": { + "major": { + "name": "Shekel", + "symbol": "₪" + }, + "minor": { + "name": "Agorat", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "20₪", + "50₪", + "100₪", + "200₪" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "10", + "0.5₪", + "2₪", + "5₪", + "10₪" + ], + "rare": [] + } + }, + "INR": { + "name": "Indian Rupee", + "iso": { + "code": "INR", + "number": "356" + }, + "units": { + "major": { + "name": "Rupee", + "symbol": "₹" + }, + "minor": { + "name": "paisa", + "symbol": "p", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "5", + "10", + "20", + "50", + "100", + "500", + "1000" + ], + "rare": [ + "1", + "2" + ] + }, + "coins": { + "frequent": [ + "1", + "2", + "5", + "100", + "1000" + ], + "rare": [ + "p50", + "10" + ] + } + }, + "IQD": { + "name": "Iraqi Dinar", + "iso": { + "code": "IQD", + "number": "368" + }, + "units": { + "major": { + "name": "dinar", + "symbol": "ع.د" + }, + "minor": { + "name": "fils", + "symbol": "", + "majorValue": 0.001 + } + }, + "banknotes": { + "frequent": [ + "50ع.د", + "100ع.د", + "250ع.د", + "500ع.د", + "1000ع.د", + "5000ع.د", + "10000ع.د", + "25000ع.د" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "25ع.د", + "50ع.د", + "100ع.د" + ], + "rare": [] + } + }, + "IRR": { + "name": "Iranian Rial", + "iso": { + "code": "IRR", + "number": "364" + }, + "units": { + "major": { + "name": "rial", + "symbol": "" + }, + "minor": { + "name": "dinar", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "100", + "200", + "500", + "1000", + "2000", + "5000", + "10000", + "20000", + "50000", + "100000" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "50", + "100", + "250", + "500", + "1000" + ], + "rare": [] + } + }, + "ISK": { + "name": "Icelandic Krona", + "iso": { + "code": "ISK", + "number": "352" + }, + "units": { + "major": { + "name": "krona", + "symbol": "kr" + }, + "minor": { + "name": "eyrir", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "kr500", + "kr1000", + "kr2000", + "kr5000" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "kr1", + "kr5", + "kr10", + "kr100" + ], + "rare": [] + } + }, + "JMD": { + "name": "Jamaican dollar", + "iso": { + "code": "JMD", + "number": "388" + }, + "units": { + "major": { + "name": "dollar", + "symbol": "J$" + }, + "minor": { + "name": "cent", + "symbol": "c", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "$50", + "$100", + "$500", + "$1000" + ], + "rare": [ + "$1", + "$2", + "$5", + "$10", + "$20", + "$5000" + ] + }, + "coins": { + "frequent": [ + "25c", + "$1", + "$5", + "$10", + "$20" + ], + "rare": [ + "1c", + "5c", + "10c", + "50c" + ] + } + }, + "JOD": { + "name": "Jordanian Dinar", + "iso": { + "code": "JOD", + "number": "400" + }, + "units": { + "major": { + "name": "Dinar", + "symbol": "" + }, + "minor": { + "name": "qirsh ou piastre", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "1 dinar", + "5 dinar", + "10 dinar", + "20 dinar", + "50 dinar" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "0.5 qirsh", + "1 qirsh", + "2.5 piastres", + "5 piastres", + "10 piastres", + "0.25 dinar", + "0.5 dinar", + "1 dinar" + ], + "rare": [] + } + }, + "JPY": { + "name": "Japanese yen", + "iso": { + "code": "JPY", + "number": "392" + }, + "units": { + "major": { + "name": "yen", + "symbol": "¥" + }, + "minor": { + "name": "sen", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "¥1000", + "¥2000", + "¥5000", + "¥10000" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "¥1", + "¥5", + "¥10", + "¥50", + "¥100", + "¥500" + ], + "rare": [] + } + }, + "KES": { + "name": "Kenyan Shilling", + "iso": { + "code": "KES", + "number": "404" + }, + "units": { + "major": { + "name": "Shilling", + "symbol": "KSh" + }, + "minor": { + "name": "cent", + "symbol": "c", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "KSh50", + "KSh100", + "KSh200", + "KSh500", + "KSh1000" + ], + "rare": [ + "KSh10", + "KSh20" + ] + }, + "coins": { + "frequent": [ + "KSh1", + "KSh5", + "KSh10", + "KSh20" + ], + "rare": [ + "c50", + "c40" + ] + } + }, + "KGS": { + "name": "Kyrgyzstani som", + "iso": { + "code": "KGS", + "number": "417" + }, + "units": { + "major": { + "name": "som", + "symbol": "лв" + }, + "minor": { + "name": "tyiyn", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "лв20", + "лв50", + "лв100", + "лв200", + "лв500", + "лв1000", + "лв5000" + ], + "rare": [ + "1tyiyn", + "10tyiyn", + "50tyiyn", + "лв1", + "лв5", + "лв10" + ] + }, + "coins": { + "frequent": [ + "лв1", + "лв3", + "лв5", + "лв10" + ], + "rare": [ + "1tyiyn", + "10tyiyn", + "50tyiyn" + ] + } + }, + "KHR": { + "name": "Cambodian Riel", + "iso": { + "code": "KHR", + "number": "116" + }, + "units": { + "major": { + "name": "riel", + "symbol": "៛" + }, + "minor": { + "name": "kak", + "symbol": "", + "majorValue": 0.1 + } + }, + "banknotes": { + "frequent": [ + "៛50", + "៛100", + "៛200", + "៛500", + "៛1000", + "៛2000", + "៛5000", + "៛10000", + "៛20000", + "៛50000", + "៛100000" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "៛50", + "៛100", + "៛200", + "៛500" + ], + "rare": [] + } + }, + "KMF": { + "name": "Comoran Franc", + "iso": { + "code": "KMF", + "number": "174" + }, + "units": { + "major": { + "name": "franc", + "symbol": "" + }, + "minor": { + "name": "centime", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "1", + "2", + "5", + "10" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "25", + "50", + "100" + ], + "rare": [] + } + }, + "KPW": { + "name": "North Korean won", + "iso": { + "code": "KPW", + "number": "408" + }, + "units": { + "major": { + "name": "won", + "symbol": "₩" + }, + "minor": { + "name": "chon", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "₩5", + "₩10", + "₩50", + "₩100", + "₩200", + "₩500", + "₩1000", + "₩2000", + "₩5000" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "1", + "5", + "10", + "50", + "₩1" + ], + "rare": [] + } + }, + "KRW": { + "name": "South Korean won", + "iso": { + "code": "KRW", + "number": "410" + }, + "units": { + "major": { + "name": "won", + "symbol": "₩" + }, + "minor": { + "name": "jeon", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "₩1000", + "₩5000", + "₩10000", + "₩50000" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "₩1", + "₩5", + "₩10", + "₩50", + "₩100", + "₩500" + ], + "rare": [] + } + }, + "KWD": { + "name": "Kuwaiti Dinar", + "iso": { + "code": "KWD", + "number": "414" + }, + "units": { + "major": { + "name": "dinar", + "symbol": "ك" + }, + "minor": { + "name": "fils", + "symbol": "", + "majorValue": 0.001 + } + }, + "banknotes": { + "frequent": [ + "250", + "500", + "1ك", + "5ك", + "10ك", + "20ك" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "5", + "10", + "20", + "50", + "100" + ], + "rare": [] + } + }, + "KYD": { + "name": "Caymanian Dollar", + "iso": { + "code": "KYD", + "number": "136" + }, + "units": { + "major": { + "name": "dollar", + "symbol": "$" + }, + "minor": { + "name": "cent", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "$1", + "$5", + "$10", + "$25", + "$50", + "$100" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "1¢", + "5¢", + "10¢", + "25¢" + ], + "rare": [] + } + }, + "KZT": { + "name": "Kazakhstani tenge", + "iso": { + "code": "KZT", + "number": "398" + }, + "units": { + "major": { + "name": "tenge", + "symbol": "₸" + }, + "minor": { + "name": "tïın", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "200", + "500", + "1000", + "2000", + "5000", + "10000" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "1", + "2", + "5", + "10", + "20", + "50", + "100" + ], + "rare": [] + } + }, + "LAK": { + "name": "Lao or Laotian Kip", + "iso": { + "code": "LAK", + "number": "418" + }, + "units": { + "major": { + "name": "Kip", + "symbol": "₭" + }, + "minor": { + "name": "Att", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "₭500", + "₭1000", + "₭2000", + "₭5000", + "₭10000", + "₭20000", + "₭50000", + "₭100000" + ], + "rare": [ + "₭1", + "₭5", + "₭10", + "₭20", + "₭50", + "₭100" + ] + }, + "coins": { + "frequent": [], + "rare": [ + "10", + "20", + "50" + ] + } + }, + "LBP": { + "name": "Lebanese Pound", + "iso": { + "code": "LBP", + "number": "422" + }, + "units": { + "major": { + "name": "pound", + "symbol": "ل.ل" + }, + "minor": { + "name": "piastre", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "1000ل.ل", + "5000ل.ل", + "10000ل.ل", + "20000ل.ل", + "50000ل.ل", + "100000ل.ل" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "250ل.ل", + "500ل.ل" + ], + "rare": [ + "500ل.ل", + "100ل.ل" + ] + } + }, + "LKR": { + "name": "Sri Lankan Rupee", + "iso": { + "code": "LKR", + "number": "144" + }, + "units": { + "major": { + "name": "Rupee", + "symbol": "Rs" + }, + "minor": { + "name": "cent", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "RS10", + "RS20", + "Rs50", + "Rs100", + "Rs500", + "Rs1000", + "Rs2000", + "Rs5000" + ], + "rare": [ + "Rs200" + ] + }, + "coins": { + "frequent": [ + "Rs1", + "Rs2", + "Rs5", + "Rs10" + ], + "rare": [ + "25", + "50" + ] + } + }, + "LRD": { + "name": "Liberian Dollar", + "iso": { + "code": "LRD", + "number": "430" + }, + "units": { + "major": { + "name": "dollar", + "symbol": "$" + }, + "minor": { + "name": "cent", + "symbol": "¢", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "$5", + "$10", + "$20", + "$50", + "$100" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "$1", + "¢5", + "¢10", + "¢25", + "¢50" + ], + "rare": [] + } + }, + "LSL": { + "name": "Lesotho loti", + "iso": { + "code": "LSL", + "number": "426" + }, + "units": { + "major": { + "name": "loti (maloti)", + "symbol": "L or M" + }, + "minor": { + "name": "sente (lisente)", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "M10", + "M20", + "M50", + "M100", + "M200" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "1", + "2", + "5", + "10", + "20", + "50", + "L1", + "M2", + "M5" + ], + "rare": [] + } + }, + "LTL": { + "name": "Lithuanian litas", + "iso": { + "code": "LTL", + "number": "440" + }, + "units": { + "major": { + "name": "litas", + "symbol": "Lt" + }, + "minor": { + "name": "centas", + "symbol": "ct", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "LT10", + "Lt20", + "Lt50", + "Lt100", + "Lt200", + "Lt500" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "ct1", + "ct2", + "ct5", + "ct10", + "ct20", + "ct50", + "Lt1", + "Lt2", + "Lt5" + ], + "rare": [] + } + }, + "LYD": { + "name": "Libyan Dinar", + "iso": { + "code": "LYD", + "number": "434" + }, + "units": { + "major": { + "name": "dinar", + "symbol": " د.ل" + }, + "minor": { + "name": "dirham", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "1 dinar", + "5 dinars", + "10 dinars", + "20 dinars", + "50 dinars" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "0.25 dinar", + "0.5 dinar", + "50 dirhams", + "100 dirhams" + ], + "rare": [] + } + }, + "MAD": { + "name": "Moroccan Dirham", + "iso": { + "code": "MAD", + "number": "504" + }, + "units": { + "major": { + "name": "dirham", + "symbol": "م.د." + }, + "minor": { + "name": "santim", + "symbol": "santimat", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "20 dirhams", + "50 dirhams", + "100 dirhams", + "200 dirhams" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "0.5 dirhams", + "1 dirhams", + "2 dirhams", + "5 dirhams", + "10 dirhams", + "1 santimat", + "5 santimat", + "10 santimat", + "20 santimat", + "50 santimat" + ], + "rare": [ + "5 santimat" + ] + } + }, + "MDL": { + "name": "Moldovan Leu", + "iso": { + "code": "MDL", + "number": "498" + }, + "units": { + "major": { + "name": "Leu", + "symbol": "L" + }, + "minor": { + "name": "Ban", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "1", + "5", + "10", + "20", + "50", + "100", + "200", + "500", + "1000" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "1", + "5", + "10", + "25", + "50" + ], + "rare": [] + } + }, + "MGA": { + "name": "Malagasy Ariary", + "iso": { + "code": "MGA", + "number": "969" + }, + "units": { + "major": { + "name": "Ariary", + "symbol": "Ar" + }, + "minor": { + "name": "Iraimbilanja", + "symbol": "", + "majorValue": 0.2 + } + }, + "banknotes": { + "frequent": [ + "Ar100", + "Ar200", + "Ar500", + "Ar1000", + "Ar2000", + "Ar5000", + "Ar10000" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "Ar1", + "Ar2", + "Ar4", + "Ar5", + "Ar10", + "Ar20", + "Ar50", + "1", + "25" + ], + "rare": [] + } + }, + "MKD": { + "name": "Macedonian Denar", + "iso": { + "code": "MKD", + "number": "807" + }, + "units": { + "major": { + "name": "denar", + "symbol": "ден" + }, + "minor": { + "name": "deni", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "ден10", + "ден50", + "ден100", + "ден500", + "ден1000", + "ден5000" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "50", + "ден1", + "ден2", + "ден5", + "ден10", + "ден50" + ], + "rare": [] + } + }, + "MMK": { + "name": "Burmese Kyat", + "iso": { + "code": "MMK", + "number": "104" + }, + "units": { + "major": { + "name": "Kyat", + "symbol": "K" + }, + "minor": { + "name": "Pya", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "50", + "K1", + "K5", + "K10", + "K20", + "K50", + "K100", + "K200", + "K500", + "K1000", + "K5000", + "K10000" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "1", + "5", + "10", + "25", + "50", + "K1", + "K5", + "K10", + "K50", + "K100" + ], + "rare": [] + } + }, + "MNT": { + "name": "Mongolian Tughrik", + "iso": { + "code": "MNT", + "number": "496" + }, + "units": { + "major": { + "name": "Tughrik", + "symbol": "₮" + }, + "minor": { + "name": "Möngö", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "₮10", + "₮20", + "₮100", + "₮500", + "₮1000", + "₮5000", + "₮10000", + "₮20000" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "₮20", + "₮50", + "₮100", + "₮200", + "₮500" + ], + "rare": [] + } + }, + "MOP": { + "name": "Macau Pataca", + "iso": { + "code": "MOP", + "number": "446" + }, + "units": { + "major": { + "name": "pataca", + "symbol": "MOP$" + }, + "minor": { + "name": "ho", + "symbol": "毫", + "majorValue": 0.1 + } + }, + "banknotes": { + "frequent": [ + "MOP$10", + "MOP$20", + "MOP$100", + "MOP$500", + "MOP$1000" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "毫10", + "毫20", + "毫50", + "MOP$1", + "MOP$2", + "MOP$5", + "MOP$10" + ], + "rare": [] + } + }, + "MRO": { + "name": "Mauritanian Ouguiya", + "iso": { + "code": "MRO", + "number": "478" + }, + "units": { + "major": { + "name": "Ouguiya", + "symbol": "UM" + }, + "minor": { + "name": "khoums", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "100", + "200", + "500", + "1000", + "2000", + "5000" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "5", + "10", + "20", + "50" + ], + "rare": [ + "1 khoums", + "1 ouguiya" + ] + } + }, + "MUR": { + "name": "Mauritian rupee", + "iso": { + "code": "MUR", + "number": "480" + }, + "units": { + "major": { + "name": "rupee", + "symbol": "Rs" + }, + "minor": { + "name": "cent", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "Rs25", + "Rs50", + "Rs100", + "Rs200", + "Rs500", + "Rs1000", + "RS2000" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "Rs1", + "Rs5", + "Rs10", + "Rs20" + ], + "rare": [] + } + }, + "MVR": { + "name": "Maldivian Rufiyaa", + "iso": { + "code": "MVR", + "number": "462" + }, + "units": { + "major": { + "name": "Rufiyaa", + "symbol": "rf" + }, + "minor": { + "name": "laari", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "5", + "10", + "20", + "50", + "100", + "500" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "1", + "2", + "5", + "10", + "25", + "50" + ], + "rare": [] + } + }, + "MWK": { + "name": "Malawian Kwacha", + "iso": { + "code": "MWK", + "number": "454" + }, + "units": { + "major": { + "name": "Kwacha", + "symbol": "MK" + }, + "minor": { + "name": "Tambala", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "MK5", + "MK10", + "MK20", + "MK50", + "MK100", + "MK200", + "MK500" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "MK1", + "MK5", + "MK10", + "MK15", + "MK20", + "MK40", + "MK50", + "MK75", + "MK100", + "1", + "2", + "5", + "50" + ], + "rare": [] + } + }, + "MXN": { + "name": "Mexico Peso", + "iso": { + "code": "MXN", + "number": "484" + }, + "units": { + "major": { + "name": "dollar", + "symbol": "$" + }, + "minor": { + "name": "centavo", + "symbol": "¢", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "$20", + "$50", + "$100", + "$200", + "$500" + ], + "rare": [ + "$1000" + ] + }, + "coins": { + "frequent": [ + "50¢", + "$1", + "$2", + "$5", + "$10" + ], + "rare": [ + "5¢", + "10¢", + "20¢", + "$20", + "$50", + "$100" + ] + } + }, + "MYR": { + "name": "Malaysian Ringgit", + "iso": { + "code": "MYR", + "number": "458" + }, + "units": { + "major": { + "name": "Ringgit", + "symbol": "RM" + }, + "minor": { + "name": "Sen", + "symbol": "Sen", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "RM1", + "RM5", + "RM10", + "RM50", + "RM100" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "sen5", + "sen10", + "sen20", + "sen50" + ], + "rare": [] + } + }, + "MZN": { + "name": "Mozambican Metical", + "iso": { + "code": "MZN", + "number": "943" + }, + "units": { + "major": { + "name": "metical", + "symbol": "MT" + }, + "minor": { + "name": "centavo", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "MT20", + "MT50", + "MT100", + "MT200", + "MT500", + "MT1000" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "MT1", + "MT2", + "MT5", + "MT10", + "5", + "10", + "50" + ], + "rare": [] + } + }, + "NAD": { + "name": "Namibian Dollar", + "iso": { + "code": "NAD", + "number": "516" + }, + "units": { + "major": { + "name": "dollar", + "symbol": "$" + }, + "minor": { + "name": "cent", + "symbol": "c", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "$10", + "$20", + "$50", + "$100", + "$200" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "$1", + "$5", + "c5", + "c10", + "c50" + ], + "rare": [] + } + }, + "NGN": { + "name": "Nigerian Naira", + "iso": { + "code": "NGN", + "number": "566" + }, + "units": { + "major": { + "name": "Naira", + "symbol": "₦" + }, + "minor": { + "name": "Kobo", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "₦5", + "₦10", + "₦20", + "₦50", + "₦100", + "₦200", + "₦500", + "₦1000" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "₦1", + "₦2", + "50" + ], + "rare": [] + } + }, + "NIO": { + "name": "Nicaraguan córdoba", + "iso": { + "code": "NIO", + "number": "558" + }, + "units": { + "major": { + "name": "córdoba", + "symbol": "C$" + }, + "minor": { + "name": "centavo", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "C$10", + "C$20", + "C$50", + "C$100", + "C$200", + "C$500" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "5", + "10", + "25", + "50", + "C$1", + "C$5", + "C$10" + ], + "rare": [] + } + }, + "NOK": { + "name": "Norwegian krone", + "iso": { + "code": "NOK", + "number": "578" + }, + "units": { + "major": { + "name": "Krone", + "symbol": "kr" + }, + "minor": { + "name": "øre", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "kr50", + "kr100", + "kr200", + "kr500", + "kr1000" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "kr1", + "kr5", + "kr10", + "kr20" + ], + "rare": [] + } + }, + "NPR": { + "name": "Nepalese Rupee", + "iso": { + "code": "NPR", + "number": "524" + }, + "units": { + "major": { + "name": "Rupee", + "symbol": "Rs" + }, + "minor": { + "name": "Paisa", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "Rs5", + "RS10", + "Rs25", + "Rs50", + "Rs100", + "Rs500", + "Rs1000" + ], + "rare": [ + "Rs1", + "Rs2" + ] + }, + "coins": { + "frequent": [ + "1", + "5", + "10", + "25", + "50", + "Rs1", + "Rs2", + "Rs5", + "Rs10" + ], + "rare": [] + } + }, + "NZD": { + "name": "New Zealand Dollar", + "iso": { + "code": "NZD", + "number": "554" + }, + "units": { + "major": { + "name": "dollar", + "symbol": "$" + }, + "minor": { + "name": "cent", + "symbol": "c", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "$5", + "$10", + "$20", + "$50", + "$100" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "10c", + "20c", + "50c", + "$1", + "$2" + ], + "rare": [] + } + }, + "OMR": { + "name": "Omani Rial", + "iso": { + "code": "OMR", + "number": "512" + }, + "units": { + "major": { + "name": "rial", + "symbol": "ع.ر." + }, + "minor": { + "name": "baisa", + "symbol": "bz", + "majorValue": 0.001 + } + }, + "banknotes": { + "frequent": [ + "bz100", + "bz200", + "bz500", + "1ع.ر.", + "5ع.ر.", + "10ع.ر.", + "20ع.ر.", + "50ع.ر." + ], + "rare": [] + }, + "coins": { + "frequent": [ + "bz5", + "bz10", + "bz25", + "bz50" + ], + "rare": [] + } + }, + "PAB": { + "name": "Balboa panamérn", + "iso": { + "code": "PAB", + "number": "590" + }, + "units": { + "major": { + "name": "balboa", + "symbol": "B/" + }, + "minor": { + "name": "Centésimo", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "$1", + "$5", + "$10", + "$20", + "$50", + "$100" + ], + "rare": [ + "2$" + ] + }, + "coins": { + "frequent": [ + "1", + "5", + "1⁄10", + "1⁄4", + "1⁄2", + "1B/", + "2B/" + ], + "rare": [] + } + }, + "PEN": { + "name": "Peruvian nuevo sol", + "iso": { + "code": "PEN", + "number": "604" + }, + "units": { + "major": { + "name": "nuevo sol", + "symbol": "S/" + }, + "minor": { + "name": "céntimo", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "S/10", + "S/20", + "S/50", + "S/100" + ], + "rare": [ + "S/200" + ] + }, + "coins": { + "frequent": [ + "10", + "20", + "50", + "S/1", + "S/2", + "S/5" + ], + "rare": [ + "1", + "5" + ] + } + }, + "PGK": { + "name": "Papua New Guinean Kina", + "iso": { + "code": "PGK", + "number": "598" + }, + "units": { + "major": { + "name": "Kina", + "symbol": "K" + }, + "minor": { + "name": "Toea", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "K2", + "K5", + "K10", + "K20", + "K50", + "K100" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "5", + "10", + "20", + "50", + "K1" + ], + "rare": [] + } + }, + "PHP": { + "name": "Philippine Peso", + "iso": { + "code": "PHP", + "number": "608" + }, + "units": { + "major": { + "name": "Peso", + "symbol": "₱" + }, + "minor": { + "name": "Sentimo", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "₱20", + "₱50", + "₱100", + "₱200", + "₱500", + "₱1000" + ], + "rare": [ + "₱5", + "₱10" + ] + }, + "coins": { + "frequent": [ + "25", + "₱1", + "₱5", + "₱10" + ], + "rare": [ + "1", + "2", + "5", + "10" + ] + } + }, + "PKR": { + "name": "Pakistani Rupee", + "iso": { + "code": "PKR", + "number": "586" + }, + "units": { + "major": { + "name": "Rupee", + "symbol": "Rs" + }, + "minor": { + "name": "Paisa", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "RS10", + "Rs50", + "Rs100", + "Rs500", + "Rs1000" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "Rs1", + "Rs2", + "Rs5" + ], + "rare": [] + } + }, + "PLN": { + "name": "Polish złoty", + "iso": { + "code": "PLN", + "number": "985" + }, + "units": { + "major": { + "name": "złoty", + "symbol": "zł" + }, + "minor": { + "name": "Grosz", + "symbol": "gr", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "zł10", + "zł20", + "zł50", + "zł100", + "zł200" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "1gr", + "2gr", + "5gr", + "10gr", + "20gr", + "50gr", + "zł1", + "zł2", + "zł5" + ], + "rare": [] + } + }, + "PYG": { + "name": "Paraguayan guarani", + "iso": { + "code": "PYG", + "number": "600" + }, + "units": { + "major": { + "name": "guarani", + "symbol": "₲" + }, + "minor": { + "name": "céntimo", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "₲1000", + "₲2000", + "₲5000", + "₲10000", + "₲20000", + "₲50000", + "₲100000" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "₲50", + "₲100", + "₲500", + "₲1000" + ], + "rare": [] + } + }, + "QAR": { + "name": "Qatari Riyal", + "iso": { + "code": "QAR", + "number": "634" + }, + "units": { + "major": { + "name": "riyal", + "symbol": "ق.ر " + }, + "minor": { + "name": "dirham", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "1ق.ر", + "5ق.ر", + "10ق.ر", + "50ق.ر", + "100ق.ر", + "500ق.ر" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "1", + "5", + "10", + "25", + "50" + ], + "rare": [] + } + }, + "RON": { + "name": "Romanian leu", + "iso": { + "code": "RON", + "number": "946" + }, + "units": { + "major": { + "name": "leu", + "symbol": "lei" + }, + "minor": { + "name": "bani", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "lei1", + "lei5", + "lei10", + "lei50", + "lei100" + ], + "rare": [ + "lei200", + "lei500" + ] + }, + "coins": { + "frequent": [ + "10", + "50" + ], + "rare": [ + "1", + "5" + ] + } + }, + "RSD": { + "name": "Serbian Dinar", + "iso": { + "code": "RSD", + "number": "941" + }, + "units": { + "major": { + "name": "dinar", + "symbol": "РСД" + }, + "minor": { + "name": "para", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "РСД10", + "РСД20", + "РСД50", + "РСД100", + "РСД200", + "РСД500", + "РСД1000" + ], + "rare": [ + "РСД2000", + "РСД5000" + ] + }, + "coins": { + "frequent": [ + "РСД1", + "РСД2", + "РСД5", + "РСД10", + "РСД20" + ], + "rare": [] + } + }, + "RUB": { + "name": "Russian Rouble", + "iso": { + "code": "RUB", + "number": "643" + }, + "units": { + "major": { + "name": "rouble", + "symbol": "₽" + }, + "minor": { + "name": "kopeyka", + "symbol": "к", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "50", + "100", + "500", + "1000", + "5000" + ], + "rare": [ + "10", + "5" + ] + }, + "coins": { + "frequent": [ + "1", + "2", + "5", + "10" + ], + "rare": [ + "k1", + "k5", + "k10", + "k50" + ] + } + }, + "RWF": { + "name": "Rwandan franc", + "iso": { + "code": "RWF", + "number": "646" + }, + "units": { + "major": { + "name": "franc", + "symbol": "FRw, RF, R₣" + }, + "minor": { + "name": "centime", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "500R₣", + "1000R₣", + "2000R₣", + "5000R₣" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "1R₣", + "2R₣", + "5R₣", + "10R₣", + "20R₣", + "50R₣", + "100R₣" + ], + "rare": [] + } + }, + "SAR": { + "name": "Saudi Arabian Riyal", + "iso": { + "code": "SAR", + "number": "682" + }, + "units": { + "major": { + "name": "riyal", + "symbol": "ر.س" + }, + "minor": { + "name": "Halala", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "1ر.س", + "5ر.س", + "10ر.س", + "20ر.س", + "50ر.س", + "100ر.س", + "500ر.س" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "5", + "10", + "25", + "50", + "100" + ], + "rare": [] + } + }, + "SBD": { + "name": "Solomon Islander Dollar", + "iso": { + "code": "SBD", + "number": "090" + }, + "units": { + "major": { + "name": "dollar", + "symbol": "SI$" + }, + "minor": { + "name": "cent", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "SI$5", + "SI$10", + "SI$20", + "SI$50", + "SI$100" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "10", + "20", + "50", + "SI$1", + "SI$2" + ], + "rare": [] + } + }, + "SCR": { + "name": "Seychellois Rupee", + "iso": { + "code": "SCR", + "number": "690" + }, + "units": { + "major": { + "name": "rupee", + "symbol": "Rs" + }, + "minor": { + "name": "cent", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "Rs50", + "Rs100", + "Rs500" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "Rs1", + "Rs5", + "1", + "5", + "10", + "25" + ], + "rare": [] + } + }, + "SDG": { + "name": "Sudanese Pound", + "iso": { + "code": "SDG", + "number": "736" + }, + "units": { + "major": { + "name": "pound", + "symbol": "" + }, + "minor": { + "name": "piastres", + "symbol": ".‏س.ج", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "1", + "2", + "5", + "10", + "20", + "50" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "1.‏س.ج", + "5.‏س.ج", + "10.‏س.ج", + "20.‏س.ج", + "50.‏س.ج", + "1" + ], + "rare": [] + } + }, + "SEK": { + "name": "Swedish krona", + "iso": { + "code": "SEK", + "number": "752" + }, + "units": { + "major": { + "name": "krona", + "symbol": "kr" + }, + "minor": { + "name": "ören", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "kr20", + "kr50", + "kr100", + "kr500" + ], + "rare": [ + "kr1000" + ] + }, + "coins": { + "frequent": [ + "kr1", + "kr5", + "kr10" + ], + "rare": [] + } + }, + "SGD": { + "name": "Singapore Dollar", + "iso": { + "code": "SGD", + "number": "702" + }, + "units": { + "major": { + "name": "dollar", + "symbol": "$" + }, + "minor": { + "name": "cent", + "symbol": "S¢", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "$2", + "$5", + "$10", + "$50" + ], + "rare": [ + "$1", + "$20", + "$25", + "$100", + "$500", + "$1000", + "$10000" + ] + }, + "coins": { + "frequent": [ + "S¢5", + "S¢10", + "S¢20", + "S¢50", + "$1" + ], + "rare": [ + "S¢1" + ] + } + }, + "SLL": { + "name": "Sierra Leonean Leone", + "iso": { + "code": "SLL", + "number": "694" + }, + "units": { + "major": { + "name": "Leone", + "symbol": "Le" + }, + "minor": { + "name": "cent", + "symbol": "c", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "Le1000", + "Le2000", + "Le5000", + "Le10000" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "Le10", + "Le50", + "Le100", + "Le500" + ], + "rare": [] + } + }, + "SOS": { + "name": "Somali Shilling", + "iso": { + "code": "SOS", + "number": "706" + }, + "units": { + "major": { + "name": "shilling", + "symbol": "S" + }, + "minor": { + "name": "senti", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "S5", + "S10", + "S20", + "S50", + "S100" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "S1", + "S5", + "S10", + "S20", + "S50", + "S100", + "1", + "5", + "10", + "50" + ], + "rare": [] + } + }, + "SRD": { + "name": "Surinamese dollar", + "iso": { + "code": "SRD", + "number": "968" + }, + "units": { + "major": { + "name": "dollar", + "symbol": "$" + }, + "minor": { + "name": "cent", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "$1", + "$2½", + "$5", + "$10", + "$20", + "$50", + "$100" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "1", + "5", + "10", + "25", + "100", + "250" + ], + "rare": [] + } + }, + "SSP": { + "name": "South Sudanese pound", + "iso": { + "code": "SSP", + "number": "728" + }, + "units": { + "major": { + "name": "pound", + "symbol": "£" + }, + "minor": { + "name": "piaster", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "5", + "10", + "25", + "£1", + "£5", + "£10", + "£25", + "£50", + "£100" + ], + "rare": [] + }, + "coins": { + "frequent": [], + "rare": [] + } + }, + "SYP": { + "name": "Syrian Pound", + "iso": { + "code": "SYP", + "number": "760" + }, + "units": { + "major": { + "name": "pound", + "symbol": "£" + }, + "minor": { + "name": "piastre", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "£5", + "£10", + "£25", + "£50", + "£100", + "£200", + "£500", + "£1000" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "£1", + "£2", + "£5", + "£10", + "£25" + ], + "rare": [] + } + }, + "SZL": { + "name": "Swazi Lilangeni", + "iso": { + "code": "SZL", + "number": "748" + }, + "units": { + "major": { + "name": "Lilangeni or emalangeni", + "symbol": "L or E" + }, + "minor": { + "name": "cent", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "E10", + "E20", + "E50", + "E100", + "E200" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "L1", + "E2", + "E5", + "1", + "2", + "5", + "10", + "20", + "50" + ], + "rare": [] + } + }, + "THB": { + "name": "Thai Baht", + "iso": { + "code": "THB", + "number": "764" + }, + "units": { + "major": { + "name": "baht", + "symbol": "฿" + }, + "minor": { + "name": "satang", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "฿20", + "฿50", + "฿100", + "฿500", + "฿1000" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "25", + "50", + "฿", + "฿2", + "฿5", + "฿10" + ], + "rare": [ + "1", + "5", + "10" + ] + } + }, + "TJS": { + "name": "Tajikistani somoni", + "iso": { + "code": "TJS", + "number": "762" + }, + "units": { + "major": { + "name": "somoni", + "symbol": "" + }, + "minor": { + "name": "diram", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "1diram", + "5diram", + "20diram", + "50diram", + "1somoni", + "3somoni", + "5somoni", + "10somoni", + "20somoni", + "50somoni", + "100somoni", + "200somoni", + "500somoni" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "5diram", + "10diram", + "20diram", + "25diram", + "50diram", + "1somoni", + "3somoni", + "5somoni" + ], + "rare": [] + } + }, + "TMT": { + "name": "Turkmenistan manat", + "iso": { + "code": "TMT", + "number": "795" + }, + "units": { + "major": { + "name": "manat", + "symbol": "T" + }, + "minor": { + "name": "tenge", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "T1", + "T5", + "T10", + "T20", + "T50", + "T100", + "T500" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "1", + "2", + "5", + "10", + "20", + "50", + "T1", + "T2" + ], + "rare": [] + } + }, + "TND": { + "name": "Tunisian Dinar", + "iso": { + "code": "TND", + "number": "788" + }, + "units": { + "major": { + "name": "dinar", + "symbol": "" + }, + "minor": { + "name": "milim or millime", + "symbol": "ت.د", + "majorValue": 0.001 + } + }, + "banknotes": { + "frequent": [ + "5", + "10", + "20", + "50" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "10ت.د", + "20ت.د", + "50ت.د", + "100ت.د", + "200ت.د", + "0.5", + "1", + "2", + "5" + ], + "rare": [] + } + }, + "TOP": { + "name": "Tongan Pa'anga", + "iso": { + "code": "TOP", + "number": "776" + }, + "units": { + "major": { + "name": "hau", + "symbol": "T$" + }, + "minor": { + "name": "seniti", + "symbol": "¢", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "T$1", + "T$2", + "T$5", + "T$10", + "T$20", + "T$50", + "T$100" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "5¢", + "10¢", + "20¢", + "50¢" + ], + "rare": [ + "1¢", + "2¢" + ] + } + }, + "TRY": { + "name": "Turkish Lira", + "iso": { + "code": "TRY", + "number": "949" + }, + "units": { + "major": { + "name": "lira", + "symbol": "" + }, + "minor": { + "name": "kuruş", + "symbol": "Kr", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "5", + "10", + "20", + "50" + ], + "rare": [ + "100", + "200" + ] + }, + "coins": { + "frequent": [ + "5Kr", + "10Kr", + "25Kr", + "50Kr", + "1" + ], + "rare": [ + "1Kr" + ] + } + }, + "TTD": { + "name": "Trinidadian dollar", + "iso": { + "code": "TTD", + "number": "780" + }, + "units": { + "major": { + "name": "dollar", + "symbol": "TT$" + }, + "minor": { + "name": "cent", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "TT$1", + "TT$5", + "TT$10", + "TT$20", + "TT$100" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "1", + "10", + "25", + "50" + ], + "rare": [] + } + }, + "TWD": { + "name": "Taiwan New Dollar", + "iso": { + "code": "TWD", + "number": "901" + }, + "units": { + "major": { + "name": "dollar", + "symbol": "NT$" + }, + "minor": { + "name": "jiao", + "symbol": "角", + "majorValue": 0.1 + } + }, + "banknotes": { + "frequent": [ + "NT$100", + "NT$500", + "NT$1000" + ], + "rare": [ + "NT$200", + "NT$2000" + ] + }, + "coins": { + "frequent": [ + "NT$1", + "NT$5", + "NT$10", + "NT$50" + ], + "rare": [ + "NT$20" + ] + } + }, + "TZS": { + "name": "Tanzanian Shilling", + "iso": { + "code": "TZS", + "number": "834" + }, + "units": { + "major": { + "name": "Shilling", + "symbol": "Sh" + }, + "minor": { + "name": "cent", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "500", + "1000", + "2000", + "5000", + "10000" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "50", + "100", + "200" + ], + "rare": [] + } + }, + "UAH": { + "name": "Ukrainian Hryvnia", + "iso": { + "code": "UAH", + "number": "980" + }, + "units": { + "major": { + "name": "Hryvnia", + "symbol": "₴" + }, + "minor": { + "name": "Kopiyka", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "₴1", + "₴2", + "₴5", + "₴10", + "₴20", + "₴50", + "₴100", + "₴200", + "₴500" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "1", + "2", + "5", + "10", + "25", + "50", + "₴1" + ], + "rare": [] + } + }, + "UGX": { + "name": "Ugandan Shilling", + "iso": { + "code": "UGX", + "number": "800" + }, + "units": { + "major": { + "name": "Shilling", + "symbol": "USh" + }, + "minor": { + "name": "cent", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "USh2", + "USh5", + "USh10", + "USh20", + "USh50", + "USh100" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "USh1", + "USh2", + "5", + "10", + "25", + "50" + ], + "rare": [] + } + }, + "USD": { + "name": "US Dollar", + "iso": { + "code": "USD", + "number": "840" + }, + "units": { + "major": { + "name": "dollar", + "symbol": "$" + }, + "minor": { + "name": "cent", + "symbol": "¢", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "$1", + "$5", + "$10", + "$20", + "$50", + "$100" + ], + "rare": [ + "2$" + ] + }, + "coins": { + "frequent": [ + "1¢", + "5¢", + "10¢", + "25¢" + ], + "rare": [ + "$1", + "50¢" + ] + } + }, + "UYU": { + "name": "Uruguayan peso", + "iso": { + "code": "UYU", + "number": "858" + }, + "units": { + "major": { + "name": "peso", + "symbol": "$U" + }, + "minor": { + "name": "centésimo", + "symbol": "¢", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "$U20", + "$U50", + "$U100", + "$U200", + "$U500", + "$U1000", + "$U2000" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "$U1", + "$U2", + "$U5", + "$U10", + "$U50" + ], + "rare": [] + } + }, + "UZS": { + "name": "Uzbekistani som", + "iso": { + "code": "UZS", + "number": "860" + }, + "units": { + "major": { + "name": "som", + "symbol": "лв" + }, + "minor": { + "name": "Tiyin", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "лв1", + "лв3", + "лв5", + "лв10", + "лв25", + "лв50", + "лв100", + "лв200", + "лв500" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "лв1", + "лв5", + "лв10", + "лв25", + "лв50", + "лв100" + ], + "rare": [] + } + }, + "VEF": { + "name": "Venezuelan bolivar", + "iso": { + "code": "VEF", + "number": "937" + }, + "units": { + "major": { + "name": "bolívares fuertes", + "symbol": "Bs" + }, + "minor": { + "name": "céntimo", + "symbol": "¢", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "Bs.2", + "Bs.5", + "Bs.10", + "Bs.20", + "Bs.50", + "Bs.100" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "Bs.1", + "10¢", + "25¢", + "50¢" + ], + "rare": [] + } + }, + "VND": { + "name": "Vietnamese Dong", + "iso": { + "code": "VND", + "number": "704" + }, + "units": { + "major": { + "name": "dong", + "symbol": "₫" + }, + "minor": { + "name": "Hào", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "₫100", + "₫200", + "₫500", + "₫1000", + "₫2000", + "₫5000", + "₫10000", + "₫20000", + "₫50000", + "₫100000", + "₫200000", + "₫500000" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "₫200", + "₫500", + "₫1000", + "₫2000", + "₫5000" + ], + "rare": [] + } + }, + "VUV": { + "name": "Ni-Vanuatu Vatu", + "iso": { + "code": "VUV", + "number": "548" + }, + "units": { + "major": { + "name": "vatu", + "symbol": "VT" + }, + "minor": { + "name": "", + "symbol": "", + "majorValue": "" + } + }, + "banknotes": { + "frequent": [ + "100VT", + "200VT", + "500VT", + "1000VT", + "2000VT", + "5000VT", + "10000VT" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "1VT", + "2VT", + "5VT", + "10VT", + "20VT", + "50VT", + "100VT" + ], + "rare": [] + } + }, + "WST": { + "name": "Samoan Tālā", + "iso": { + "code": "WST", + "number": "882" + }, + "units": { + "major": { + "name": "tālā", + "symbol": "$" + }, + "minor": { + "name": "sene", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "$2", + "$5", + "$10", + "$20", + "$50", + "$100" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "10", + "20", + "50", + "$1", + "$2" + ], + "rare": [] + } + }, + "XCD": { + "name": "East Caribbean dollar", + "iso": { + "code": "XCD", + "number": "951" + }, + "units": { + "major": { + "name": "dollar", + "symbol": "EC$" + }, + "minor": { + "name": "cent", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "EC$5", + "EC$10", + "EC$20", + "EC$50", + "EC$100" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "1", + "2", + "5", + "10", + "25", + "EC$1", + "EC$2" + ], + "rare": [] + } + }, + "XOF": { + "name": "CFA Franc", + "iso": { + "code": "XOF", + "number": "952" + }, + "units": { + "major": { + "name": "franc", + "symbol": "" + }, + "minor": { + "name": "centime", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "500", + "1000", + "2000", + "5000", + "10000" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "1", + "2", + "5", + "10", + "25", + "100", + "500" + ], + "rare": [] + } + }, + "XPF": { + "name": "CFP Franc", + "iso": { + "code": "XPF", + "number": "953" + }, + "units": { + "major": { + "name": "franc", + "symbol": "" + }, + "minor": { + "name": "centime", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "500", + "1000", + "5000", + "10000" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "1", + "2", + "5", + "10", + "20", + "50", + "100" + ], + "rare": [ + "$1", + "50¢" + ] + } + }, + "YER": { + "name": "Yemeni Rial", + "iso": { + "code": "YER", + "number": "886" + }, + "units": { + "major": { + "name": "rial", + "symbol": "" + }, + "minor": { + "name": "fils", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "50", + "100", + "200", + "250", + "500", + "1000" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "1", + "5", + "10", + "20" + ], + "rare": [] + } + }, + "ZAR": { + "name": "South African Rand", + "iso": { + "code": "ZAR", + "number": "710" + }, + "units": { + "major": { + "name": "Rand", + "symbol": "R" + }, + "minor": { + "name": "cent", + "symbol": "c", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "R10", + "R20", + "R50", + "R100", + "R200" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "R1", + "R2", + "R5", + "5c", + "10c", + "20c" + ], + "rare": [] + } + }, + "ZMW": { + "name": "Zambian Kwacha", + "iso": { + "code": "ZMW", + "number": "967" + }, + "units": { + "major": { + "name": "Kwacha", + "symbol": "ZMK" + }, + "minor": { + "name": "ngwee", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "2 kwacha", + "5 kwacha", + "10 kwacha", + "20 kwacha", + "50 kwacha", + "100 kwacha" + ], + "rare": [] + }, + "coins": { + "frequent": [ + "5 ngwee", + "10 ngwee", + "50 ngwee", + "1 kwacha" + ], + "rare": [] + } + }, + "ZWD": { + "name": "Zimbabwean Dollar", + "iso": { + "code": "ZWD", + "number": "932" + }, + "units": { + "major": { + "name": "dolalr", + "symbol": "Z$" + }, + "minor": { + "name": "cent", + "symbol": "", + "majorValue": 0.01 + } + }, + "banknotes": { + "frequent": [ + "Z$1", + "Z$5", + "Z$10", + "Z$20", + "Z$50", + "Z$100", + "Z$500" + ], + "rare": [] + }, + "coins": { + "frequent": [], + "rare": [] + } + } +} diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..dc486c7 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,58 @@ +### +### ~ Monica dev docker-compose +### +### This file is used for dev purpose. +### The standard monica image definition will be found here: https://github.com/monicahq/docker +### + +version: '3' + +services: + app: + build: + context: . + dockerfile: scripts/docker/Dockerfile + image: monica:dev + depends_on: + - mysql + ports: + - 8080:80 + env_file: .env.dev + volumes: + - /var/www/html/storage + - ./app:/var/www/html/app + - ./database:/var/www/html/database + - ./resources:/var/www/html/resources + - ./routes:/var/www/html/routes + + mysql: + image: mysql:8 + command: --default-authentication-plugin=mysql_native_password + environment: + - MYSQL_ROOT_PASSWORD=sekret_root_password + - MYSQL_DATABASE=monica + - MYSQL_USER=homestead + - MYSQL_PASSWORD=secret + volumes: + - /var/lib/mysql + + phpmyadmin: + image: phpmyadmin + depends_on: + - mysql + environment: + PMA_HOST: mysql + PMA_USER: root + PMA_PASSWORD: sekret_root_password + restart: always + ports: + - 3000:80 + volumes: + - /sessions + + mail: + container_name: fake_mail + image: mailhog/mailhog + ports: + - 8025:8025 + - 1025:1025 diff --git a/docker-compose.yml b/docker-compose.yml index bf44386..a722a49 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,13 +1,48 @@ services: - mischcrm: - container_name: mischcrm + monica: image: git.mischlabs.de/mrdiderot/crm:latest + container_name: mischcrm restart: unless-stopped + env_file: + - .env ports: - - '38090:8085' + - "${MONICA_PORT:-38090}:80" + depends_on: + db: + condition: service_healthy volumes: - - './data:/app/data' + - monica_storage:/var/www/html/storage + + cron: + image: git.mischlabs.de/mrdiderot/crm:latest + container_name: mischcrm_cron + restart: unless-stopped + command: cron.sh + env_file: + - .env + depends_on: + db: + condition: service_healthy + volumes: + - monica_storage:/var/www/html/storage + + db: + image: mariadb:11 + container_name: mischcrm_db + restart: unless-stopped environment: - - PORT=8085 - - DATABASE_PATH=/app/data/crm.db - - NODE_ENV=production + MYSQL_RANDOM_ROOT_PASSWORD: "true" + MYSQL_DATABASE: ${DB_DATABASE:-monica} + MYSQL_USER: ${DB_USERNAME:-monica} + MYSQL_PASSWORD: ${DB_PASSWORD:-change-me} + volumes: + - monica_db:/var/lib/mysql + healthcheck: + test: ["CMD-SHELL", "mariadb-admin ping -h localhost -u$${MYSQL_USER} -p$${MYSQL_PASSWORD} --silent"] + interval: 10s + timeout: 5s + retries: 10 + +volumes: + monica_storage: + monica_db: diff --git a/docs/administrators/deployment.md b/docs/administrators/deployment.md new file mode 100644 index 0000000..067cf59 --- /dev/null +++ b/docs/administrators/deployment.md @@ -0,0 +1,24 @@ +This document is aimed at the people deploying Monica on https://monicahq.com, and does not apply to the people installing Monica for their own use. + +## Before deploying a new version + +* Make sure `config/monica.php` contains the number of the new release you are about to create. +* Update `CHANGELOG.md` accordingly. +* Create a new release on https://version.monicahq.com with the release notes. +* Create a new release on https://github.com/monicahq/monica/releases, which will also automatically create a new tag. +* Write the release note with the tool hosted on https://monicahq.com/login +* Push the code to production. + +## Deployment of the Docker images + +This guide has been posted by [kstrauser](https://github.com/monicahq/monica/issues/676#issuecomment-352047750) - thanks for his help. Here are the steps necessary to deploy Monica on Docker hub: +* You need an account on [Docker hub](https://hub.docker.com). +* You need to have Docker installed on your machine. +* Run the following commands (example for the version 3.4.5): + +``` +git reset --hard v3.4.5 +make docker_build docker_tag docker_push +``` + +The image should be up on the Docker Hub. diff --git a/docs/administrators/press.md b/docs/administrators/press.md new file mode 100644 index 0000000..5f7a1d2 --- /dev/null +++ b/docs/administrators/press.md @@ -0,0 +1,15 @@ +# Press articles + +Here are all the mentions of Monica we've found on the web somewhere. Feel free to edit this list and submit new ones if you find any. + +* https://blog.desdelinux.net/gestione-relaciones-personales-monica/ +* https://tideways.io/profiler/blog/profiling-laravel-applications-using-the-open-source-monica-crm-as-example +* https://laravel-news.com/monica +* https://sivers.org/dbt +* https://medevel.com/monica-is-your-own-persona-crm-assistant-solution/ +* https://www.cloudron.io/store/com.monicahq.cloudronapp.html +* https://bestlaravel.com/p/monica +* https://betalist.com/startups/monicahq +* https://www.bypeople.com/free-personal-crm/ +* https://steemhunt.com/author/@mindblast/monica-open-source-personal-crm +* https://osinum.fr/monica/ diff --git a/docs/administrators/tips.md b/docs/administrators/tips.md new file mode 100644 index 0000000..b1f2ab2 --- /dev/null +++ b/docs/administrators/tips.md @@ -0,0 +1,34 @@ +This document lists some tips that developers can use to add new content in Monica and contains the solution to uncommon problems. + +## Add a new changelog entry + +Changelog entries are used to describe what's new in the product. Each time an entry is created, we also need to create an association between users of the instance and the entry that has been created. This association is stored in `changelog_user`. + +To add a new changelog entry, you need to: +* create a new entry in the `changelog.json` file in the public folder. +* make sure your new entry is at the TOP of the file, not the bottom. + +### When is it relevant to create a changelog entry + +We should not create a changelog entry every single time we make a change to the platform. The rule is to warn users only when we introduce something that they will benefit from. A bug fix is not something they will benefit from and is not worth mentioning. Simple visual changes, unless they drastically change the UI, should not be mentioned. Anything made on the Docker image or a packaging issue should not be mentioned. + +## Failed Jobs queue + +PostgreSQL users who previously failed Monica's update may receive errors similar to these: + +> SQLSTATE[Number]: Duplicate table: 7 ERROR: relation "**failed_jobs**" already exists (SQL: create table "failed_jobs" ("id" bigserial primary key not null, "connection" text not null, "queue" text not null, "payload" text not null, "exception" text not null, "failed_at" timestamp(0) without time zone default CURRENT_TIMESTAMP not null)) + +> SQLSTATE[Number]: Duplicate table: 7 ERROR: relation "**failed_jobs**" already exists + +The problem is solved by running the command: + +``` +php artisan queue:flush +``` +...with the appropriate privileges. + +Immediately after (through a tool like `psql`) access the Monica database and run this command: +``` +DROP TABLE "failed_jobs"; +``` +This will ensure that the failed job table has actually been deleted. diff --git a/docs/contribute/docker.md b/docs/contribute/docker.md new file mode 100644 index 0000000..5afcc77 --- /dev/null +++ b/docs/contribute/docker.md @@ -0,0 +1,55 @@ +# Build Docker image for Monica + +If you want to build your own docker image for Monica, follow these steps: + +## Use docker-compose to build and run your own image + +Use this process if you want to modify Monica source code and build +your image to run. + +Edit `.env` to set `DB_HOST=mysql` (as `mysql` is the creative name of the MySQL container). + +Then run: + +```sh +docker-compose -f docker-compose.dev.yml build +docker-compose -f docker-compose.dev.yml up +``` + +## Use Docker directly to run with your own database + +Use this process if you're a developer and want complete control over +your Monica container. + +If you aren't using docker-compose, edit `.env` again to set the `DB_*` variables to match your database. Then run: + +```sh +scripts/docker/build.sh +``` + +You can add the tag name as a parameter: +```sh +scripts/docker/build.sh monica-dev +``` + +Run monica with: +```sh +docker run --env-file .env -p 80:80 monica-dev +``` + +Or run a command in the container: +```sh +docker run --env-file .env -it monica-dev bash +``` + +There's a bunch of [docker-compose examples here.](https://github.com/monicahq/docker/tree/master/.examples) + +Note that uploaded files, like avatars, will disappear when you +restart the container. Map a volume to +`/var/www/monica/storage/app/public` if you want that data to persist +between runs. See `docker-compose.yml` for examples. + +## Other documents to read + +[Connecting to MySQL inside of a Docker container](/docs/installation/docker-mysql.md) +[Use mobile app with standalone server](/docs/installation/mobile.md) diff --git a/docs/contribute/readme.md b/docs/contribute/readme.md new file mode 100644 index 0000000..9f732a6 --- /dev/null +++ b/docs/contribute/readme.md @@ -0,0 +1,365 @@ +# Contribute as a developer + +- [Considerations](#considerations) +- [Design rules](#design-rules) +- [Install Monica locally](#install-monica-locally) + - [Homestead (macOS, Linux, Windows)](#homestead-macos-linux-windows) + - [Valet (macOS)](#valet-macos) + - [asdf (macOS)](#asdf-macos) + - [Instructions](#instructions) +- [Testing environment](#testing-environment) + - [Setup](#setup) + - [Run the test suite](#run-the-test-suite) + - [Run browser tests](#run-browser-tests) + - [Mocking HTTP calls](#mocking-http-calls) +- [Coding guidelines](#coding-guidelines) + - [Feature branch](#feature-branch) + - [Conventional commits](#conventional-commits) +- [Backend](#backend) + - [Things to consider when adding new code](#things-to-consider-when-adding-new-code) + - [Add a new table to the database schema](#add-a-new-table-to-the-database-schema) + - [Manipulating data during a migration](#manipulating-data-during-a-migration) + - [Email testing](#email-testing) + - [Email reminders](#email-reminders) + - [Statistics](#statistics) +- [Database](#database) + - [Connecting to mySQL](#connecting-to-mysql) +- [Front-end](#front-end) + - [Considerations](#considerations-1) + - [Mix](#mix) + - [Watching and compiling assets](#watching-and-compiling-assets) + - [CSS](#css) + - [JS and Vue](#js-and-vue) + - [Localization (i18n)](#localization-i18n) + - [Application](#application) + - [Laravel](#laravel) + - [VueJS](#vuejs) + +Are you interested in giving a hand? We can't be more excited about it. Thanks in advance! + +Notes: +* _we are doing everything we can to review pull requests submitted by the community as soon as possible. It can take days (or weeks) to finalize a review, going through rounds of changes, etc... This is why we kindly ask you to be patient during this process._ +* _no changes are too small. If you want to contribute, even fixing a typo will help._ + +Here are some guidelines that could help you to get started quickly. + + +## Considerations + +* Monica is written with a great framework, [Laravel](https://github.com/laravel/laravel). We care deeply about keeping Monica very simple on purpose. The simpler the code is, the simpler it will be to maintain it and debug it when needed. That means we don't want to make it a one page application, or add any kind of complexities whatsoever. +* That means we won't accept pull requests that add too much complexity, or written in a way we don't understand. Again, the number 1 priority should be to simplify the maintenance on the long run. +* It's better to move forward fast by shipping good features, than waiting for months and ship a perfect feature. +* Our product philosophy is simple. Things do not have to be perfect. They just need to be shipped. As long as it works and aligns with the vision, you should ship as soon as possible. Even if it's ugly, or very small, that does not matter. + + +## Design rules + +* **Keep it simple**. No new options, please. Options are evil. It creates a bloated software. Not everything should be configurable. +* **Use what already exists in the current stack**. When adding a feature, do not introduce a new software in the existing stack. For instance, at the moment, the current version does not require Redis to be used. If we do create a feature that (for some reasons) depends on Redis, we will need all existing instances to install Redis on top of all the other things people have to setup to install Monica (there are thousands of them). We can't afford to do that. +* **Always think about the API**. When introducing new classes and concepts in the app, your changes should always be implemented as well. Everything that we do should be accessible through the API. + + +## Install Monica locally + + +### Homestead (macOS, Linux, Windows) + +The best way to contribute to Monica is to use [Homestead](https://laravel.com/docs/homestead) as a development environment, which is an official, pre-packaged Vagrant box that provides you a wonderful development environment without requiring you to install PHP, a web server, and any other server software on your local machine. The big advantage is that it runs on any Windows, Mac, or Linux system. + +This is what is used to develop Monica and what will provide a common base for everyone who wants to contribute to the project. Once Homestead is installed, you can pull the repository and start setting up Monica. + +Note: the official Monica installation uses mySQL as the database system. While Laravel technically supports PostgreSQL and SQLite, we can't guarantee that it will work fine with Monica as we've never tested it. Feel free to read [Laravel's documentation](https://laravel.com/docs/5.5/database#configuration) on that topic if you feel adventurous. + + +### Valet (macOS) + +We've installed the development version with [Valet](https://laravel.com/docs/valet), which is a Laravel development environment for Mac minimalists. It works really well and is extremely fast, much faster than Homestead. + + + +### asdf (macOS) + +You can use [asdf](https://asdf-vm.com/#/) to install a version of php for monica: + +```bash +asdf install +asdf reshim + +pecl install redis +echo "extension=redis.so" > $(asdf where php)/conf.d/php.ini +``` + +You'll need to run the installation instructions below and setup a local mysql installation. After completing the application installation process you can run `php artisan serve` to get a local development server. +### Instructions + +**Prerequisites**: +* Git +* [Node](https://nodejs.org/en/) +* PHP 8.1+ +* [Composer](https://getcomposer.org/) +* GNU Make + +**Steps to install Monica** + +Once the above softwares are installed (or if you've finished the installation of Homestead/Valet): + +1. Create a database called `monica` in your mySQL instance. + 1. `mysql -e "CREATE DATABASE monica";` inside mySQL. + 2. If you use Homestead (which uses Vagrant under the hood), `vagrant ssh` will let you login as root inside your VM. +2. Setup the application environment: + 1. `composer install --no-interaction` to install all packages. + - Due to an issue with VirtualBox, you may encounter an error at this step due to a plug-in called `package-versions`. If this happens, delete the /vendor folder that was created and run `composer install --no-interaction --no-plugins --no-scripts` instead. + - See this [GitHub Issue](https://github.com/laravel/homestead/issues/1240) for more information. + - Run `cp .env.example .env` to create your own version of all the environment variables needed for the project to work. + 2. `yarn install` to install all the front-end dependencies and tools needed to compile assets. + - If you experience an error related to `EPROTO: protocol error, symlink`, see [here](https://github.com/yarnpkg/yarn/issues/4908). + 3. `yarn run dev` to compile js and css assets. + 4. `php artisan key:generate` to generate an application key. This will set `APP_KEY` with the right value automatically. + 5. `php artisan setup:test` to setup the database. + - By default this command will also populate the database with fake data. + - Use the `--skipSeed` option to skip the process of adding fake data in your dev environment. + 6. `php artisan passport:install` to create the access tokens required for the API (Optional). +3. Update `.env` to your specific needs. + +If you haven't skipped the seeding of fake data, two accounts are created by default: + +* First account is `admin@admin.com` with the password `admin0`. This account contains a lot of fake data that will let you play with the product. +* Second account is `blank@blank.com` with the password `blank0`. This account does not contain any data and shall be used to check all the blank states. + +To update a current installation with the latest dependencies, just run `make update` to run + 1. `composer install --no-interaction` + 1. `yarn upgrade` + 1. `yarn run dev` + 1. `php artisan migrate` + + +## Testing environment + +We try to cover most features and new methods with unit and functional tests. Any pull request submitted on GitHub will have to go through Travis and pass before being merged. Moreover, we **strongly** encourage adding unit tests for every new method added to the codebase to ensure code stability, and we will probably refuse a pull request if there is no tests for it. + + +### Setup + +To setup the test environment: + +* Create a database `mysql -e "CREATE DATABASE monica_test;"` +* `php artisan migrate --database testing` + - If this fails due to the `oauth_auth_codes_table` already existing, edit `config/passport.php` to update the storage driver so `'connection' => 'testing'`, then run `php artisan migrate --database testing` again. + - For more information, see this [GitHub Issue](https://github.com/laravel/passport/issues/1370). +* `php artisan db:seed --database testing` + + +### Run the test suite + +The test suite uses Phpunit. It's mainly used to perform unit tests or quick, small functional tests. + +To run the test suite: + +* `phpunit` or `./vendor/bin/phpunit` in the root of the folder containing Monica's code from GitHub. + + +### Run browser tests + +Browsers tests simulate user interactions in a live browser. + +* To run browser tests, first you need to install chrome +```sh +curl -sS -o - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add +echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" | sudo tee /etc/apt/sources.list.d/google-chrome.list +sudo apt -y update +sudo apt -y -f install google-chrome-stable fonts-liberation libappindicator1 +``` +* Then you can run the test suite: +`php artisan dusk` + + +### Mocking HTTP calls + +You should never make real HTTP calls in your unit tests - like querying an external API that is not linked to Monica. + +You can mock http calls by mocking calls made by Guzzle. + +You can find an example of how mocking is done in the `GetWeatherInformationTest.php` file. + +You need to provide a sample response of the external call that you are mocking in the Fixtures folder. + + +## Coding guidelines + + +### Feature branch + +We follow [GitHub Flow](https://guides.github.com/introduction/flow/) to manage the development of features. + + +### Conventional commits + +We follow the [conventional commit message](https://conventionalcommits.org/) syntax for our commits. For instance, `feat: allow provided config object to extend other configs` or `feat(lang): added polish language`. + +Every feature branch that is squashed onto main branch must follow these rules. + +The benefits are: +* a standard way of writing commit messages for every contributor, +* a way to quickly see and understand what the commit does and what it affects, +* automatic changelog creation based on those keywords. + +The keywords that support (heavily inspired by [config-conventional](https://github.com/conventional-changelog/commitlint/tree/master/%40commitlint/config-conventional)): +* `ci`, +* `chore`, +* `docs`, +* `feat`, +* `fix`, +* `perf`, +* `refactor`, +* `revert`, +* `style`, +* `test`. + +Moreover, every commit message needs to be written in lowercase. +* ✅ feat(lang): added polish language +* ❌ feat(lang): Added polish language + +All the commits in a pull request are squashed when merged into main branch. That means *only the commit message of the squashed branch needs to follow this commit message convention*. That also means that you don't need to follow this convention for commits within a branch, which will usually contains a lot of commits with a `wip` title. + + +## Backend + +The project follows strict [object calisthenics](http://www.slideshare.net/guilhermeblanco/object-calisthenics-applied-to-php), as much as possible and more and more over time. We will soon implement those rules in the Linters and will block a pull request for the code that does not follow those guidelines. +Here are the rules (adapted for PHP): +* Only one indentation level per method, +* Do not use the "else" keyword, +* Do not chain different objects, unless if the execution includes getters and setters, +* Keep your entities small: 100 lines per class and no more than 15 classes per package, +* Any class that contains a collection (or array) cannot use any other properties, +* Document your code. + + +### Things to consider when adding new code + + +#### Add a new table to the database schema + +If you add a new table, make sure there is a column called `account_id` in this new table. That way, we will make sure that the script responsible for resetting or deleting a user account will go take this new table into consideration while running. + + +#### Manipulating data during a migration + +Sometimes you need to manipulate and move data around when you decide to change the database structure. In that case, as much as possible, do not use Eloquent to change the data. Use either [raw SQL queries](https://laravel.com/docs/5.5/database#running-queries) or the [Query builder](https://laravel.com/docs/5.5/queries) to do it. This is due to the fact that objects might change overtime or even be deleted, which would break the migrations entirely. + + +### Email testing + +Emails are an important part of Monica. Emails are still the most significant mean of communication and people like receiving them when they are relevant. That being said, you will need to test emails to make sure they contain what they should contain. + +For development purposes, you have two choices to test emails: + +1. You can use [Mailtrap](https://mailtrap.io/). This is an amazing service that provides a free plan that is plenty enough to test all the emails that are sent. +1. You can use [mailhog](https://github.com/mailhog/MailHog) to test locally. On macOS, you can install via Homebrew (`brew install mailhog`). Then, run `mailhog` and point the browser to `http://127.0.0.1:8025` ([more complete instructions](https://github.com/maijs/homebrew-mailhog)). +1. If you use [Homestead](https://laravel.com/docs/homestead), [mailhog](https://github.com/mailhog/MailHog) is actually built-in. To use it, you first need to start mailhog (`sudo service mailhog restart`) inside your Vagrant. Then, head up to [http://localhost:8025](http://localhost:8025) in your local browser to load Mailhog's UI. + +Note: if you want to use mailhog, you need the following settings in your `.env` file: + +``` +MAIL_MAILER=smtp +MAIL_HOST=0.0.0.0 +MAIL_PORT=1025 +MAIL_USERNAME= +MAIL_PASSWORD= +MAIL_ENCRYPTION= +``` + + +### Email reminders + +Monica sends two types of emails: reminders, and notifications. Notifications are sent 7 and 30 days before an event happens, while reminders are sent the day the event happens. +Reminders are generated and sent using an Artisan command `send:reminders`. Notifications are sent using `send:notifications`. Those commands are scheduled to be triggered every hour in `app/console/Kernel.php`. + + +### Statistics + +Monica calculates every night (ie once per day) a set of metrics to help you understand how the instance is being used by users. That will also allow to measure growth over time. + +Statistics are generated by the Artisan command `monica:calculatestatistics` every night at midnight and this cron is defined in `app/console/Kernel.php`. + + +## Database + +As said above, Monica uses mySQL by default. While Laravel supports multiple DBMS, we can't assure you it will work with any other DBMS than mySQL. + + +### Connecting to mySQL + +If you want to connect directly to Monica's MySQL instance read [_Connecting to MySQL inside of a Docker container_](./docs/database/connecting.md). + + +## Front-end + + +### Considerations + +* If your contribution involves a change in the UI (even if it's very small), please ping @djaiss in an issue *before* you start working on it, explaining what you want to achieve, why and how. We want to maintain a high level of visual quality in the software and we will dismiss all pull requests that change the front end that have not been discussed before-hand. +* That being said, we'll probably receive pull requests that change the front end before any previous discussion on the topic. In this case, we do not guarantee that we'll accept the pull request, but in order to increase the chances that it will: + * Make sure to follow the current visual style and layout. + * Make sure you do not introduce new colors in the UI. + * Make sure the user experience is consistent with the rest of the application (ie buttons behave the same, modals are like other modals,...). + * Make sure you don't introduce new CSS classes, unless they are absolutely necessary. Use the classes provided by [Tachyons](https://tachyons.io) which is the functional CSS framework we currently use. + * Do not use Jquery. When needed, use VueJS. + +The above comments can seem harsh and we apologize in advance. However you have to understand that we deeply care about providing the best user experience to our users. Features that are purely backend do not have the same impact as the ones that the user interacts with. Features that modify the front end will have a tremendous impact on how users perceive the software. Therefore we want to make sure that anything that touches the frontend is perfect and aligned with our vision. + + +### Mix + +We use [mix](https://laravel.com/docs/5.5/mix) to manage the front-end and its dependencies, and also to compile and/watch the assets. **Please note that we should do our best to prevent introducing new dependencies if we can prevent it**. + +Mix should be available in your development environment if you have installed Monica locally and ran `yarn install` in the first place. + +If you need to add a new dependency, update `package.json` to add it and make sure you commit `package-lock.json` once `package.json` is updated. + + +### Watching and compiling assets + +CSS is written in SASS and therefore needs to be compiled before being used by the application. To compile those front-end assets, use `yarn run dev`. + +To monitor changes and compile assets on the fly, use `yarn run watch`. + + +### CSS + +At the current time, we are using a mix of Bootstrap 4 and [Tachyons](https://tachyons.io). We aim to use [Atomic CSS](https://adamwathan.me/css-utility-classes-and-separation-of-concerns/) instead of having bloated, super hard to maintain CSS files. We'll get rid of Bootstrap entirely over time. + +This means that we should add new CSS classes only if it's absolutely necessary. + + +### JS and Vue + +We are using [Vue.js](https://vuejs.org/) in some parts of the application, and we'll use it more and more over time. Vue is very simple to learn and use, and with [Vue Components](https://vuejs.org/v2/guide/components.html), we can easily create isolated, reusable components in the app. If you want to add a new feature, you don't need to use Vue.js - you can use plain HTML views served by the backend. But with Vue.js, it'll be a nicer experience. + + +### Localization (i18n) + + +#### Application + +Localization of the application is handled by the [default i18n helper provided by Laravel](https://laravel.com/docs/5.5/localization). When adding or modifying strings, you only have to handle the `en` language, which is stored in `resources/lang/en/`. The other locales are going to be handled by Crowdin, our translation platform. + +We also have [a dedicated page](/docs/contribute/translate.md) for our translators, just in case you need it. + + +##### Laravel + +We use the default Laravel helper: `trans('app.save')`. + + +##### VueJS + +For everything that is in VueJS though, things are a bit different. We have to use a special library to allow translated strings to be available in the javascript views. The helper in Vue is slightly different. + +You can use these replacements instead of the regular (php) definition: +* `trans('file.string')` is written `$t('file.string')`. +* `trans('file.string', ['param' => $value])` is written `$t('file.string', {param: value})`. +* `trans_choice('file.string', $count)` is written `$tc('file.string', count)` or `$tc('file.string', count, {param: value})`. + +Important note: every time a string changes in a translation file, you need to regenerate all the strings so they can be made available in JS. To do this, +* use `php artisan lang:generate` +* then compile all the JS assets `yarn run prod`, and commit the whole. diff --git a/docs/contribute/translate.md b/docs/contribute/translate.md new file mode 100644 index 0000000..1885d1e --- /dev/null +++ b/docs/contribute/translate.md @@ -0,0 +1,70 @@ +# External translators + +- [Crowdin](#crowdin) +- [Support a new language](#support-a-new-language) +- [Rules](#rules) + - [With Laravel](#with-laravel) + - [With Vue.js](#with-vuejs) +- [Rules for translation](#rules-for-translation) + - [Punctuation](#punctuation) + +First of all, thanks a lot for considering helping the project by translating it. We truly appreciate it. + +## Crowdin + +All translations are done with [crowdin](https://crowdin.com/project/monicahq) - we'd like to thank them for their gracious help with this project by providing us a free account. + +## Support a new language + +You can [open an issue](https://github.com/monicahq/monica/issues/new) to request a new language. + +⚠️ Do not edit languages file directly. + +To enable a new language in Monica: +* we have to configure it in Crowdin first. This is something we must do ourselves (we: members of the project). To do it, we need to go to Settings > Translations > Target Languages and add the new locale here. +* add the name of the language in [the main English settings file](https://github.com/monicahq/monica/blob/main/resources/lang/en/settings.php). +* update the [lang-detector.php file](https://github.com/monicahq/monica/blob/main/config/lang-detector.php) by adding the new locale abbreviation (please add it in alphabetic order). +* add the locale in the [webpack.mix.js file](https://github.com/monicahq/monica/blob/main/webpack.mix.js) (please add it in alphabetic order). +* (optional) when adding a country-specific language (like 'en-GB'), you may need to update the [crowdin.yml config file](https://github.com/monicahq/monica/blob/main/crowdin.yml). + +Then, submit your PR for review. A good example of adding a new locale can [be found here](https://github.com/monicahq/monica/pull/3356). + +## Rules + +Translation appears in two types of files in the codebase: in Laravel (php) and VueJS. + +### With Laravel + +- **simple string** +- **string with parameters**: see [laravel doc](https://laravel.com/docs/5.6/localization#replacing-parameters-in-translation-strings). + To translate: integrate the text replacement in your translation, like ":param". + Example: `:name’s birthday` => `anniversaire de :name` +- **plural forms**: see [laravel doc](https://laravel.com/docs/5.6/localization#pluralization) for documentation. It supports basic and occidental plural variations, each one being defined in [here](https://github.com/laravel/framework/blob/5.6/src/Illuminate/Translation/MessageSelector.php#L110). + Example: `1 message|:count messages` => `:count message|:count messages`, or: `{1}:count message|[2,*]:count messages` +- **format strings**: we use [Carbon](http://carbon.nesbot.com/docs/#api-commonformats) to handle dates. [format.php](https://github.com/monicahq/monica/blob/main/resources/lang/en/format.php) file contains format we use to export dates as strings in the right localized format. See [php doc](http://www.php.net/manual/en/function.date.php) to know which format you can use. + +### With Vue.js + +We use the [vue-i18n](https://www.npmjs.com/package/vue-i18n) package. + +- **simple string** +- **string with parameters**: see [vue-i18n doc](http://kazupon.github.io/vue-i18n/en/formatting.html#html-formatting). + - To translate: integrate the text replacement in your translation, like `{param}`. + - Example: `{name}’s birthday` => `anniversaire de {name}` + - Other example: `{{ $t('people.stay_in_touch_frequency', { count: frequency }) }}` +- **plural forms**: See [vue-i18n doc](http://kazupon.github.io/vue-i18n/en/pluralization.html). + Pluralization is customized in the [pluralization.js](https://github.com/monicahq/monica/blob/main/resources/js/pluralization.js) file. This should fit your language pluralization form. Messages must be separated by a pipe, but you cannot define the number of occurrences it applies to like with Laravel translation (no brackets or braces). + Example: `1 message|{count} messages` => `{count} message|{count} messages` in French, or: `{count}条消息` in Chinese (only 1 form) + +## Rules for translation + +Please respect typographic rules in your language. + +### Punctuation + +See https://en.wikipedia.org/wiki/Punctuation + +- [Apostrophe](https://en.wikipedia.org/wiki/Apostrophe): use real apostrophe character `’` instead of simple quote `'` +- [Quotes](https://en.wikipedia.org/wiki/Quotation_mark): use real quotation marks like `“ ”` or `« »` instead of double quote `"` +- [Dash](https://en.wikipedia.org/wiki/Dash): use en dash `—` instead of hyphen `-` when it’s necessary +- [Interpuct](https://en.wikipedia.org/wiki/Interpunct) for separate some lists: `·` diff --git a/docs/images/carddav_davx5_1.jpg b/docs/images/carddav_davx5_1.jpg new file mode 100644 index 0000000..ad83f10 Binary files /dev/null and b/docs/images/carddav_davx5_1.jpg differ diff --git a/docs/images/carddav_davx5_1.png b/docs/images/carddav_davx5_1.png new file mode 100644 index 0000000..4667f91 Binary files /dev/null and b/docs/images/carddav_davx5_1.png differ diff --git a/docs/images/carddav_token1.png b/docs/images/carddav_token1.png new file mode 100644 index 0000000..3a8f264 Binary files /dev/null and b/docs/images/carddav_token1.png differ diff --git a/docs/images/carddav_token2.png b/docs/images/carddav_token2.png new file mode 100644 index 0000000..194dfce Binary files /dev/null and b/docs/images/carddav_token2.png differ diff --git a/docs/images/carddav_url.png b/docs/images/carddav_url.png new file mode 100644 index 0000000..9a22852 Binary files /dev/null and b/docs/images/carddav_url.png differ diff --git a/docs/images/heroku_dashboard-resources.png b/docs/images/heroku_dashboard-resources.png new file mode 100644 index 0000000..6b09291 Binary files /dev/null and b/docs/images/heroku_dashboard-resources.png differ diff --git a/docs/images/heroku_dashboard.png b/docs/images/heroku_dashboard.png new file mode 100644 index 0000000..bc3cb8f Binary files /dev/null and b/docs/images/heroku_dashboard.png differ diff --git a/docs/images/heroku_manage_app.png b/docs/images/heroku_manage_app.png new file mode 100644 index 0000000..1838527 Binary files /dev/null and b/docs/images/heroku_manage_app.png differ diff --git a/docs/images/logo.png b/docs/images/logo.png new file mode 100644 index 0000000..24da584 Binary files /dev/null and b/docs/images/logo.png differ diff --git a/docs/images/main-app.png b/docs/images/main-app.png new file mode 100644 index 0000000..a3efc61 Binary files /dev/null and b/docs/images/main-app.png differ diff --git a/docs/images/screenshot.png b/docs/images/screenshot.png new file mode 100644 index 0000000..3f4cdad Binary files /dev/null and b/docs/images/screenshot.png differ diff --git a/docs/images/windows10_contacts_1.png b/docs/images/windows10_contacts_1.png new file mode 100644 index 0000000..2987e4c Binary files /dev/null and b/docs/images/windows10_contacts_1.png differ diff --git a/docs/images/windows10_contacts_2.png b/docs/images/windows10_contacts_2.png new file mode 100644 index 0000000..bcc6091 Binary files /dev/null and b/docs/images/windows10_contacts_2.png differ diff --git a/docs/images/windows10_contacts_3.png b/docs/images/windows10_contacts_3.png new file mode 100644 index 0000000..af577b9 Binary files /dev/null and b/docs/images/windows10_contacts_3.png differ diff --git a/docs/images/windows10_wheel.png b/docs/images/windows10_wheel.png new file mode 100644 index 0000000..1b13dbb Binary files /dev/null and b/docs/images/windows10_wheel.png differ diff --git a/docs/installation/faq.md b/docs/installation/faq.md new file mode 100644 index 0000000..b44d7af --- /dev/null +++ b/docs/installation/faq.md @@ -0,0 +1,6 @@ +# Common problems FAQ + +This document describes common errors/problems with a self hosted Monica installation. + +## Q: What are the default user credentials? +A: Monica should open a browser after setup to create your first user. If this does not happen or during setup you see the output `Seeding: FakeUserTableSeeder` you possibly did forget to set `APP_ENV` within the `.env` file to value `production` diff --git a/docs/installation/mail.md b/docs/installation/mail.md new file mode 100644 index 0000000..14689ba --- /dev/null +++ b/docs/installation/mail.md @@ -0,0 +1,92 @@ +# Configuring a Mail Server + +- [Use SMTP with Monica](#use-smtp-with-monica) +- [Use Amazon SES with Monica](#use-amazon-ses-with-monica) + - [1. Obtain SES Credentials](#1-obtain-ses-credentials) + - [2. Verify the Address You'll be Sending From](#2-verify-the-address-youll-be-sending-from) + - [3. Allow SES to Send Emails Out](#3-allow-ses-to-send-emails-out) + - [4. Configure Monica to Use SES SMTP Server](#4-configure-monica-to-use-ses-smtp-server) + +The Monica registration flow will send a validation email to the user who sent it. Whilst this is not required by default (see `APP_SIGNUP_DOUBLE_OPTIN` in your `.env` file), setting up a mail server is encouraged so that you can receive reminders. + +For this, you will require an SMTP server. If you don't have one of these, your options include (but are not limited to): + +* [Mailtrap](https://mailtrap.io/) +* [Postmark](https://postmarkapp.com/) +* [Mailgun](https://signup.mailgun.com/new/signup) (the [free plan](https://www.mailgun.com/pricing) should be sufficient) +* [Amazon Simple Email Service](https://aws.amazon.com/ses/) +* [Sendgrid](https://sendgrid.com) + +## Use SMTP with Monica + +The generic way to send emails with Monica is to provide a SMTP server, each one of the services mentioned above can provide you SMTP settings. While Amazon SES is a little bit custom, see bellow, here the configuration for a standard SMTP configuration. + +You need to add few environment variables in your configuration (working in generic installation and Docker): +``` +MAIL_MAILER: smtp +MAIL_HOST: smtp.service.com # ex: smtp.sendgrid.net +MAIL_PORT: 587 # is using tls, as you should +MAIL_USERNAME: my_service_username # ex: apikey +MAIL_PASSWORD: my_service_password # ex: SG.Psuoc6NZTrGHAF9fdsgsdgsbvjQ.JuxNWVYmJ8LE0 +MAIL_ENCRYPTION: tls +MAIL_FROM_ADDRESS: no-reply@xxx.com # ex: email you want the email to be FROM +MAIL_FROM_NAME: Monica # ex: name of the sender +``` + +Restart Monica to take in effect the new settings, quickest option to confirm is to add someone to your account (add one of your own email) and you will receive the invitation! + + +## Use Amazon SES with Monica + +Simple Email Service is a service provided through Amazon Web Services. This guide will assume that you have an [AWS Account](https://aws.amazon.com/) and have basic familiarity with the Management Console. + +For more detailed information on SES, see the [Amazon SES Docs](https://docs.aws.amazon.com/ses/latest/DeveloperGuide/choose-email-sending-method.html). + +### 1. Obtain SES Credentials + +The SES SMTP server will require Monica to authenticate with it. These are set through the `MAIL_USERNAME` and `MAIL_PASSWORD` fields in your `.env` file. + +Go to the SES Console - and take note of which Region you're working in. You'll need this to configure the correct SMTP server later. + +In the SES Console, go to "SMTP Settings", and select "Create My SMTP Credentials". This will take you into IAM (Identity and Access Management), which is where these credentials will be stored. The name is unimportant - just hit "Create". + + + +### 2. Verify the Address You'll be Sending From + +When using SES, you must verify that you own the email address that your email from Monica will appear to be from. This is the `MAIL_FROM_ADDRESS` in your `.env` file (Note that the `MAIL_FROM_NAME` can be whatever you like - and will be the "friendly name" that appears in your email client). + +In the SES console, go to "Email Addresses" and "Verify a New Email Address". Follow that flow through until the "Verification Status" for your email shows up as "Verified". + + + +### 3. Allow SES to Send Emails Out + +SES does not, by default, allow you to send email to arbitrary addresses. If you're only planning on having a single user on your Monica Instance, with a single email address for notifications, you can simply Verify the Address you're planning on sending to, just like in Step 2 above. + +If you're planning on having multiple users with unknown email addresses, you'll have to [move out of the SES Sandbox Environment](https://docs.aws.amazon.com/ses/latest/DeveloperGuide/request-production-access.html). + + + +### 4. Configure Monica to Use SES SMTP Server + +You now simply need to configure your `.env` file to use the SES SMTP server. Make sure you use the correct server for the Region where you've configured your email addresses, or this will not work! + +``` +# Mail credentials used to send emails from the application. +MAIL_MAILER=smtp +MAIL_HOST=email-smtp.us-east-1.amazonaws.com +MAIL_PORT=25 +MAIL_USERNAME= +MAIL_PASSWORD= +MAIL_ENCRYPTION=tls +# Outgoing emails will be sent with these identity +MAIL_FROM_ADDRESS= +MAIL_FROM_NAME="Monica" +# New registration notification sent to this email +APP_EMAIL_NEW_USERS_NOTIFICATION= +``` + + + +Now you're all done! If you've changed your `.env` file since you last started Monica, use `php artisan setup:production -v` so that Monica reads your new configuration. diff --git a/docs/installation/providers/cloudron.md b/docs/installation/providers/cloudron.md new file mode 100644 index 0000000..f7d2768 --- /dev/null +++ b/docs/installation/providers/cloudron.md @@ -0,0 +1,16 @@ +# Installing Monica on Cloudron + +Monica is available as a 1-click install on [Cloudron](https://cloudron.io). For those unaware, +Cloudron makes it easy to run web apps on your server and keep them up-to-date. + +[![Install](https://cloudron.io/img/button.svg)](https://cloudron.io/button.html?app=com.monicahq.cloudronapp) + +When you visit the app for the first time, you will be prompted to register a new account. Put in your email address +and password and you will be logged into the app. This is the only account that has access unless you invite other people. + +There is a demo available at https://my.demo.cloudron.io (username: cloudron password: cloudron) + +# Package source + +The Cloudron package is developed [here](https://git.cloudron.io/cloudron/monica-app). + diff --git a/docs/installation/providers/cpanel.md b/docs/installation/providers/cpanel.md new file mode 100644 index 0000000..891c22e --- /dev/null +++ b/docs/installation/providers/cpanel.md @@ -0,0 +1,143 @@ +# Installing Monica (cPanel Shared Hosting) + +- [Prerequisites](#prerequisites) +- [Installation steps](#installation-steps) + - [1. Download the repository](#1-download-the-repository) + - [2. Setup the database](#2-setup-the-database) + - [3. Configure Monica](#3-configure-monica) + - [4. Configure cron job](#4-configure-cron-job) + - [5. Configure cPanel webserver](#5-configure-cpanel-webserver) + - [Final step](#final-step) + +## Prerequisites + +Monica can be configured in shared hosting environments with a little differences that we can remedy easily. In this scenario, Monica depends on the following: + +- A shared cPanel Server +- PHP 8.1+ +- [Composer](https://getcomposer.org/) +- [MySQL](https://www.mysql.com/) +- SSH Access for an accont on the cPanel server + +**Git:** Git should come pre-installed with your server. If it doesn't - use the installation instructions in the link. + +**PHP:** Install php8.1 minimum. Generally cPanel will have a PHP 7 version installed, verify under the 'PHP Version' section from the cPanel section. Make sure these extensions are enabled: + +- bcmath +- curl +- dom +- gd +- gmp +- iconv +- intl +- json +- mbstring +- mysqli +- opcache +- pdo_mysql +- redis +- sodium +- tokenizer +- xml +- zip + +In most cases, this will be under the section called 'PHP Version' in cPanel where you can enable and disable modules. + +**Composer:** After you're done installing PHP, you'll need the Composer dependency manager. Generally on most capable shared hosts, this is already installed. If it is not, please reference the below: + +```sh +php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" +php composer-setup.php --install-dir=/usr/local/bin/ --filename=composer +php -r "unlink('composer-setup.php');" +``` + +**MySQL:** Almost every cPanel environment includes this by default, and this will be our desired DB + + +## Installation steps + +Once the softwares above are installed: + +### 1. Download the repository + +You may install Monica by simply downloading the repository. You can download it by using the download button at the main repo page for Monica. Some people may want to use Git, and when you have properly logged into the cPanel server, issue the following commands: + +```sh +cd /public_html/[subdomain you wish to install monica on] +git clone https://github.com/monicahq/monica.git +``` + +You should check out a tagged version of Monica since `main` branch may not always be stable. Find the latest official version on the [release page](https://github.com/monicahq/monica/releases). + +```sh +cd /var/www/monica +# Get latest tags from GitHub +git fetch +# Clone the desired version +git checkout tags/v3.7.0 +``` + +### 2. Setup the database + +Use the cPanel database wizard to create a new database. +
    +
  1. Search for 'Database Wizard' in the cPanel GUI. Click on that item.
  2. +
  3. Create a database name and click next.
  4. +
  5. Create a user name and password for the user to access the database. Click Next
  6. +
  7. Assign All Permissions to the user account.
  8. +
  9. Save the password to be referenced later
  10. + + +### 3. Configure Monica + +Open the cPanel file manager and navigate to the directory in which you want to install Monica. Then run these steps: + +1. Duplicate `.env.example` to a file called `.env` to create your own version of all the environment variables needed for the project to work. +2. Update `.env` to your specific needs + - set `DB_USERNAME` and `DB_PASSWORD` with the settings used above. + - DO NOT set a database prefix, as you will overrun the limit of table and constraint names. + - configure a [mailserver](/docs/installation/mail.md) for registration & reminders to work correctly. Generally you can configure a SMTP account within cPanel and be fine. + - set the `APP_ENV` variable to `production`, `local` is only used for the development version. Beware: setting `APP_ENV` to `production` will force HTTPS. Skip this if you're running Monica locally. +3. Log into the cPanel server via SSH and navigate to the directory in which you want to install Monica. +4. Run `composer install --no-interaction --no-dev` to install all packages. +5. Run `yarn install` to install frontend packages, then `yarn run production` to build the assets (js, css). +6. Run `php artisan key:generate` to generate an application key. This will set `APP_KEY` with the right value automatically. +7. Run `php artisan setup:production -v` to run the migrations, seed the database and symlink folders. + +The `setup:production` command will run migrations scripts for database, and flush all cache for config, route, and view, as an optimization process. +As the configuration of the application is cached, any update on the `.env` file will not be detected after that. You may have to run `php artisan config:cache` manually after every update of `.env` file. + +### 4. Configure cron job + +Monica requires some background processes to continuously run. The list of things Monica does in the background is described [here](https://github.com/monicahq/monica/blob/main/app/Console/Kernel.php#L63). +Basically those crons are needed to send reminder emails and check if a new version is available. +To do this, setup a cron that runs every minute that triggers the following command `php artisan schedule:run`. + +1. Navigate to 'Cron Jobs' in the cPanel GUI: + + +2. On that screen add the following: + +Under common settings, select 'Once Per Minute' + +Paste the following in the 'Command' section +``` +php /var/www/monica/artisan schedule:run >> /dev/null 2>&1 +``` + +### 5. Configure cPanel webserver + +1. Navigate to the 'Subdomain' section in the cPanel GUI: + + +2. Update the path of the domain you wish to assign to Monica to the following: + +```sh +/public_html/[subdomain you installed the monica folders on]/public +``` + +### Final step + +The final step is to have fun with your newly created instance, which should be up and running to `http://[domain you installed Monica on]`. + +From there you will be able to create an account and use the platform as normal. diff --git a/docs/installation/providers/debian.md b/docs/installation/providers/debian.md new file mode 100644 index 0000000..a5257b2 --- /dev/null +++ b/docs/installation/providers/debian.md @@ -0,0 +1,254 @@ +# Installing Monica on Debian + +Logo + +Monica can run on Debian Buster. + +- [Prerequisites](#prerequisites) +- [Installation steps](#installation-steps) + - [1. Clone the repository](#1-clone-the-repository) + - [2. Setup the database](#2-setup-the-database) + - [3. Configure Monica](#3-configure-monica) + - [4. Configure cron job](#4-configure-cron-job) + - [5. Configure Apache webserver](#5-configure-apache-webserver) + - [Final step](#final-step) + +## Prerequisites + +Monica depends on the following: + +- A Web server, like [Apache httpd webserver](https://httpd.apache.org/) +- [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) +- PHP 8.1+ +- [Composer](https://getcomposer.org/) +- [Node.js](https://nodejs.org) +- [Yarn](https://yarnpkg.com) +- MySQL / MariaDB + +An editor like vim or nano should be useful too. + +**Apache:** Install Apache with: + +```sh +sudo apt update +sudo apt install -y apache2 +``` + +**Git:** Install Git with: + +```sh +sudo apt install -y git +``` + +**PHP:** + +If you are using Debian 10 or lower, PHP 8.1 is not available from the Debian project directly. Instead use the [deb.sury.org](https://deb.sury.org/) package repository from Ondřej Surý, maintainer of the mainline Debian packages. + +```sh +sudo apt install -y curl software-properties-common +curl -sSL https://packages.sury.org/php/apt.gpg | sudo tee /etc/apt/trusted.gpg.d/php-sury.gpg +echo "deb https://packages.sury.org/php/ $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/php-sury.list +sudo apt update +``` + +Install PHP 8.1 with these extensions: + +- bcmath +- curl +- dom +- gd +- gmp +- iconv +- intl +- json +- mbstring +- mysqli +- opcache +- pdo_mysql +- redis +- sodium +- tokenizer +- xml +- zip + +Run: +```sh +sudo apt install -y php8.1 php8.1-bcmath php8.1-curl php8.1-gd php8.1-gmp \ + php8.1-intl php8.1-mbstring php8.1-mysql php8.1-redis php8.1-tokenizer php8.1-xml php8.1-zip +``` + +**Composer:** After you're done installing PHP, you'll need the Composer dependency manager. + +```sh +curl -sSL https://getcomposer.org/installer | sudo php -- --install-dir=/usr/local/bin/ --filename=composer +``` + +**Node.js:** Install node.js with package manager. + +```sh +curl -sSL https://deb.nodesource.com/setup_16.x | sudo bash - +sudo apt install -y nodejs +``` + +**Yarn:** Install yarn with npm. + +```sh +sudo npm install --global yarn +``` + +**MariaDB:** Install MariaDB. Note that this only installs the package, but does not setup Mysql. This is done later in the instructions: + +```sh +sudo apt install -y mariadb-server +``` + +## Installation steps + +Once the softwares above are installed: + +### 1. Clone the repository + +You may install Monica by simply cloning the repository. Consider cloning the repository into any folder, example here in `/var/www/monica` directory: + +```sh +cd /var/www/ +sudo git clone https://github.com/monicahq/monica.git +``` + +You should check out a tagged version of Monica since `main` branch may not always be stable. +Find the latest official version on the [release page](https://github.com/monicahq/monica/releases) + +```sh +cd /var/www/monica +# Get latest tags from GitHub +sudo git fetch +# Clone the desired version +sudo git checkout tags/v2.18.0 +``` + +### 2. Setup the database + +First make the database a bit more secure. + +```sh +sudo mysql_secure_installation +``` + +Next log in with the root account to configure the database. + +```sh +sudo mysql -uroot -p +``` + +Create a database called 'monica'. + +```sql +CREATE DATABASE monica CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +``` + +Create a user called 'monica' and its password 'strongpassword'. + +```sql +CREATE USER 'monica'@'localhost' IDENTIFIED BY 'strongpassword'; +``` + +We have to authorize the new user on the `monica` db so that he is allowed to change the database. + +```sql +GRANT ALL ON monica.* TO 'monica'@'localhost'; +``` + +And finally we apply the changes and exit the database. + +```sql +FLUSH PRIVILEGES; +exit +``` + +### 3. Configure Monica + +`cd /var/www/monica` then run these steps with `sudo`: + +1. `cp .env.example .env` to create your own version of all the environment variables needed for the project to work. +2. Update `.env` to your specific needs + - set `DB_USERNAME` and `DB_PASSWORD` with the settings used behind. + - configure a [mailserver](/docs/installation/mail.md) for registration & reminders to work correctly. + - set the `APP_ENV` variable to `production`, `local` is only used for the development version. Beware: setting `APP_ENV` to `production` will force HTTPS. Skip this if you're running Monica locally. +3. Run `composer install --no-interaction --no-dev` to install all packages. +4. Run `yarn install` to install frontend packages, then `yarn run production` to build the assets (js, css). +5. Run `php artisan key:generate` to generate an application key. This will set `APP_KEY` with the right value automatically. +6. Run `php artisan setup:production -v` to run the migrations, seed the database and symlink folders. + - You can use `email` and `password` parameter to setup a first account directly: `php artisan setup:production --email=your@email.com --password=yourpassword -v` +7. _Optional_: Setup the queues with Redis, Beanstalk or Amazon SQS: see optional instruction of [generic installation](generic.md#setup-queues) +8. _Optional_: Setup the access tokens to use the API follow optional instruction of [generic installation](generic.md#setup-access-tokens) + +### 4. Configure cron job + +Monica requires some background processes to continuously run. The list of things Monica does in the background is described [here](https://github.com/monicahq/monica/blob/main/app/Console/Kernel.php#L63). +Basically those crons are needed to send reminder emails and check if a new version is available. +To do this, setup a cron that runs every minute that triggers the following command `php artisan schedule:run`. + +Run the crontab command: + +```sh +sudo crontab -u www-data -e +``` + +Then, in the `crontab` editor window you just opened, paste the following at the end of the document: + +```sh +* * * * * php /var/www/monica/artisan schedule:run >> /dev/null 2>&1 +``` + +### 5. Configure Apache webserver + +1. Give proper permissions to the project directory by running: + +```sh +sudo chown -R www-data:www-data /var/www/monica +sudo chmod -R 775 /var/www/monica/storage +``` + +2. Enable the rewrite module of the Apache webserver: + +```sh +sudo a2enmod rewrite +``` + +3. Configure a new monica site in apache by doing: + +```sh +sudo nano /etc/apache2/sites-available/monica.conf +``` + +Then, in the `nano` text editor window you just opened, copy the following - swapping the `**YOUR IP ADDRESS/DOMAIN**` with your server's IP address/associated domain: + +```html + + ServerName **YOUR IP ADDRESS/DOMAIN** + + ServerAdmin webmaster@localhost + DocumentRoot /var/www/monica/public + + + Options Indexes FollowSymLinks + AllowOverride All + Require all granted + + + ErrorLog ${APACHE_LOG_DIR}/error.log + CustomLog ${APACHE_LOG_DIR}/access.log combined + +``` + +4. Apply the new `.conf` file and reload Apache. You can do that by running: + +```sh +sudo a2dissite 000-default.conf +sudo a2ensite monica.conf +sudo systemctl reload apache2 +``` + +### Final step + +The final step is to have fun with your newly created instance, which should be up and running to `http://localhost`. diff --git a/docs/installation/providers/docker.md b/docs/installation/providers/docker.md new file mode 100644 index 0000000..7f68c56 --- /dev/null +++ b/docs/installation/providers/docker.md @@ -0,0 +1,44 @@ +# Installing Monica on Docker + +Logo + +Monica can run with Docker images. + +- [Prerequisites](#prerequisites) +- [Use Monica docker image](#use-monica-docker-image) +- [Running the image with docker-compose](#running-the-image-with-docker-compose) + +## Prerequisites + +You can use [Docker](https://www.docker.com) and [docker-compose](https://docs.docker.com/compose/) to pull or build +and run a Monica image, complete with a self-contained MySQL database. +This has the nice properties that you don't have to install lots of software directly onto your system, and you can be up and running +quickly with a known working environment. + +For any help about how to install Docker, see their [documentation](https://docs.docker.com/get-docker/). + +## Use Monica docker image + +The [standard `monica` image](https://hub.docker.com/_/monica/) can be run with the latest release of Monica. + +Run the container with the command below (don't change the username/password): + +```sh +mysqlCid="$(docker run -d \ + -e MYSQL_RANDOM_ROOT_PASSWORD=true \ + -e MYSQL_DATABASE=monica \ + -e MYSQL_USER=homestead \ + -e MYSQL_PASSWORD=secret \ + "mysql:5.7")" +docker run -d \ + --link "$mysqlCid":mysql \ + -e DB_HOST=mysql \ + -p 8080:80 \ + monica +``` + +Wait for the migration db to complete, then go to [http://localhost:8080](http://localhost:8080). + +## Running the image with docker-compose + +See some examples of docker-compose possibilities in the [example section](https://github.com/monicahq/docker/tree/master/.examples). diff --git a/docs/installation/providers/generic.md b/docs/installation/providers/generic.md new file mode 100644 index 0000000..4499e39 --- /dev/null +++ b/docs/installation/providers/generic.md @@ -0,0 +1,319 @@ +# Installing Monica (Generic) + +- [Prerequisites](#prerequisites) + - [Types of databases](#types-of-databases) +- [Installation steps](#installation-steps) + - [1. Clone the repository](#1-clone-the-repository) + - [2. Setup the database](#2-setup-the-database) + - [3. Configure Monica](#3-configure-monica) + - [4. Configure cron job](#4-configure-cron-job) + - [5. Configure Apache webserver](#5-configure-apache-webserver) + - [6. Optional: Setup the queues with Redis, Beanstalk or Amazon SQS](#6-optional-setup-the-queues-with-redis-beanstalk-or-amazon-sqs) + - [7. Optional: Setup the access tokens to use the API](#7-optional-setup-the-access-tokens-to-use-the-api) + - [Generate the encryption keys](#generate-the-encryption-keys) + - [Optional: Save the encryption keys as variable](#optional-save-the-encryption-keys-as-variable) + - [Optional: Generate a Password grant client](#optional-generate-a-password-grant-client) + - [Final step](#final-step) + +## Prerequisites + +If you don't want to use Docker, the best way to setup the project is to use the same configuration that [Homestead](https://laravel.com/docs/homestead) uses. Basically, Monica depends on the following: + +- [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) +- PHP 8.1+ +- [Composer](https://getcomposer.org/) +- [Node.js](https://nodejs.org) +- [Yarn](https://yarnpkg.com) +- [MySQL](https://www.mysql.com/) +- Optional: Redis or Beanstalk + +**Git:** Git should come pre-installed with your server. If it doesn't - use the installation instructions in the link. + +**PHP:** Install php8.1 minimum, with these extensions: + +- bcmath +- curl +- dom +- gd +- gmp +- iconv +- intl +- json +- mbstring +- mysqli +- opcache +- pdo_mysql +- redis +- sodium +- tokenizer +- xml +- zip + +**Composer:** After you're done installing PHP, you'll need the Composer dependency manager. It is not enough to just install Composer, you also need to make sure it is installed globally for Monica's installation to run smoothly: + +```sh +php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" +php composer-setup.php --install-dir=/usr/local/bin/ --filename=composer +php -r "unlink('composer-setup.php');" +``` + +**Node.js:** Install node.js 16+ minimum + + +**Yarn:** Install yarn using npm + +```sh +npm install --global yarn +``` + +**Mysql:** Install Mysql 5.7+ + +### Types of databases + +The official Monica installation uses mySQL as the database system and **this is the only official system we support**. While Laravel technically supports PostgreSQL and SQLite, we can't guarantee that it will work fine with Monica as we've never tested it. Feel free to read [Laravel's documentation](https://laravel.com/docs/database#configuration) on that topic if you feel adventurous. + +## Installation steps + +Once the softwares above are installed: + +### 1. Clone the repository + +You may install Monica by simply cloning the repository. In order for this to work with Apache, which is often pre-packaged with many common linux instances ([DigitalOcean](https://www.digitalocean.com/) droplets are one example), you need to clone the repository in a specific folder: + +```sh +cd /var/www +git clone https://github.com/monicahq/monica.git +``` + +You should check out a tagged version of Monica since `main` branch may not always be stable. Find the latest official version on the [release page](https://github.com/monicahq/monica/releases). + +```sh +cd /var/www/monica +# Get latest tags from GitHub +git fetch +# Clone the desired version +git checkout tags/v2.18.0 +``` + +### 2. Setup the database + +Log in with the root account to configure the database. + +```sh +mysql -u root -p +``` + +Create a database called 'monica'. + +```sql +CREATE DATABASE monica CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +``` + +Create a user called 'monica' and its password 'strongpassword'. + +```sql +CREATE USER 'monica'@'localhost' IDENTIFIED BY 'strongpassword'; +``` + +We have to authorize the new user on the `monica` db so that he is allowed to change the database. + +```sql +GRANT ALL ON monica.* TO 'monica'@'localhost'; +``` + +And finally we apply the changes and exit the database. + +```sql +FLUSH PRIVILEGES; +exit +``` + +### 3. Configure Monica + +`cd /var/www/monica` then run these steps: + +1. `cp .env.example .env` to create your own version of all the environment variables needed for the project to work. +2. Update `.env` to your specific needs + - set `DB_USERNAME` and `DB_PASSWORD` with the settings used behind. + - configure a [mailserver](/docs/installation/mail.md) for registration & reminders to work correctly. + - set the `APP_ENV` variable to `production`, `local` is only used for the development version. Beware: setting `APP_ENV` to `production` will force HTTPS. Skip this if you're running Monica locally. +3. Run `composer install --no-interaction --no-dev` to install all packages. +4. Run `yarn install` to install frontend packages, then `yarn run production` to build the assets (js, css). +5. Run `php artisan key:generate` to generate an application key. This will set `APP_KEY` with the right value automatically. +6. Run `php artisan setup:production -v` to run the migrations, seed the database and symlink folders. + +The `setup:production` command will run migrations scripts for database, and flush all cache for config, route, and view, as an optimization process. +As the configuration of the application is cached, any update on the `.env` file will not be detected after that. You may have to run `php artisan config:cache` manually after every update of `.env` file. + +### 4. Configure cron job + +Monica requires some background processes to continuously run. The list of things Monica does in the background is described [here](https://github.com/monicahq/monica/blob/main/app/Console/Kernel.php#L63). +Basically those crons are needed to send reminder emails and check if a new version is available. +To do this, setup a cron that runs every minute that triggers the following command `php artisan schedule:run`. + +1. Open crontab edit for the apache user: + +```sh +crontab -u www-data -e +``` + +2. Then, in the text editor window you just opened, copy the following: + +``` +* * * * * /usr/bin/php /var/www/monica/artisan schedule:run >> /dev/null 2>&1 +``` + +### 5. Configure Apache webserver + +1. Give proper permissions to the project directory by running: + +```sh +chgrp -R www-data /var/www/monica +chmod -R 775 /var/www/monica/storage +``` + +2. Enable the rewrite module of the Apache webserver: + +```sh +a2enmod rewrite +``` + +2. Configure a new monica site in apache by doing: + +```sh +nano /etc/apache2/sites-available/monica.conf +``` + +3. Then, in the `nano` text editor window you just opened, copy the following - swapping the `YOUR IP ADDRESS/DOMAIN` with your server's IP address/associated domain: + +```html + + ServerName YOUR IP ADDRESS/DOMAIN + + ServerAdmin webmaster@localhost + DocumentRoot /var/www/monica/public + + + Options Indexes FollowSymLinks + AllowOverride All + Require all granted + + + ErrorLog ${APACHE_LOG_DIR}/error.log + CustomLog ${APACHE_LOG_DIR}/access.log combined + +``` + +4. Apply the new `.conf` file and restart Apache. You can do that by running: + +```sh +a2dissite 000-default.conf +a2ensite monica.conf +service apache2 restart +``` + + + +### 6. Optional: Setup the queues with Redis, Beanstalk or Amazon SQS + +Monica can work with a queue mechanism to handle different events, so we don't block the main thread while processing stuff that can be run asynchronously, like sending emails. By default, Monica does not use a queue mechanism but can be setup to do so. + +We recommend that you do not use a queue mechanism as it complexifies the overall system and can make debugging harder when things go wrong. + +This is why we suggest to use `QUEUE_CONNECTION=sync` in your .env file. This will bypass the queues entirely and will process requests as they come. In practice, unless you have thousands of users, you don't need to use an asynchronous queue. + +That being said, if you still want to make your life more complicated, here is what you can do. + +There are several choices for the queue mechanism: + +- Database (this will use the database used by the application to act as a queue) +- Redis +- Beanstalk +- Amazon SQS + +The simplest queue is the database driver. To set it up, simply change in your `.env` file the following `QUEUE_CONNECTION=sync` by `QUEUE_CONNECTION=database`. + +To configure the other queues, refer to the [official Laravel documentation](https://laravel.com/docs/master/queues#driver-prerequisites) on the topic. + +After configuring the queue, you'll have to run the queue worker, as described in the [Laravel documentation](https://laravel.com/docs/master/queues#running-the-queue-worker). + +```sh +php artisan queue:work --sleep=3 --tries=3 +``` + +Some process monitor such as [Supervisor](https://laravel.com/docs/master/queues#supervisor-configuration) could be useful to monitor the queue worker. + + + +### 7. Optional: Setup the access tokens to use the API + +In order to use the Monica API for your instance, you will have to instantiate encryption keys first. + +#### Generate the encryption keys + +Run this command: + +```sh +php artisan passport:keys +php artisan passport:client --personal --no-interaction +``` + +This command will generate encryption keys in the `storage` directory. +Be sure to backup the `oauth-private.key` and `oauth-public.key` files to maintain future access. + +#### Optional: Save the encryption keys as variable + +Instead of keeping the encryption keys as files, you can add them as environment variable. This is very useful for any environment where you cannot deploy these file in each server (heroku, fortrabbit, etc.). + +- Output the private key: + +```sh +sed -E ':a;N;$!ba;s/\r{0,1}\n/\\n/g' storage/oauth-private.key +``` + +Copy the output to an environment variable called `PASSPORT_PRIVATE_KEY` in your `.env` file. + +``` +PASSPORT_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\nMIIJKAIBAAKCAgEAsC..." +``` + +- Do the same thing with the contents of the public key: + +```sh +sed -E ':a;N;$!ba;s/\r{0,1}\n/\\n/g' storage/oauth-public.key +``` + +Copy the output to an environment variable called `PASSPORT_PUBLIC_KEY` in your `.env` file. + +``` +PASSPORT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhki..." +``` + +#### Optional: Generate a Password grant client + +A [password grant client](https://laravel.com/docs/master/passport#creating-a-password-grant-client) can be generated in order to use the OAuth access (used in the mobile application for instance). + +- Run this command to generate a password grant client: + +```sh +php artisan passport:client --password --no-interaction +``` + +- This will display a client ID and secret: + +``` +Password grant client created successfully. +Client ID: 5 +Client secret: zsfOHGnEbadlBP8kLsjOV8hMpHAxb0oAhenfmSqq +``` + +- Copy the two values into two new environment variables of your `.env` file: + + - The value of `Client ID` in a `PASSPORT_PASSWORD_GRANT_CLIENT_ID` variable + - The value of `Client secret` in a `PASSPORT_PASSWORD_GRANT_CLIENT_SECRET` variable + +- OAuth login can be access on `http://localhost/oauth/login`. + +### Final step + +The final step is to have fun with your newly created instance, which should be up and running to `http://localhost`. diff --git a/docs/installation/providers/heroku.md b/docs/installation/providers/heroku.md new file mode 100644 index 0000000..e82fc21 --- /dev/null +++ b/docs/installation/providers/heroku.md @@ -0,0 +1,137 @@ +# Installing Monica on Heroku + +Monica can be deployed on Heroku using the button below: + +[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/monicahq/monica/tree/main) + +- [Installation](#installation) + - [Configuration](#configuration) + - [Optional: Setup the access tokens to use the API](#optional-setup-the-access-tokens-to-use-the-api) + - [Optional: Generate a Password grant client for OAuth access](#optional-generate-a-password-grant-client-for-oauth-access) +- [Limitations](#limitations) +- [Updating Heroku instance](#updating-heroku-instance) +- [Update from 2.x to 3.x](#update-from-2x-to-3x) + +## Installation + +Before deployment, Heroku will ask you to define a few variables. +- Please ensure to enter a custom `APP_KEY` when asked (you can for instance copy and paste the output of `echo -n 'base64:'; openssl rand -base64 32`). +- In addition, you can edit the email address Monica will send emails to (`MAIL_FROM_ADDRESS`), the name of the sender (`MAIL_FROM_NAME`), where emails should link to (`APP_URL`) and some other important variables on that screen. + +After deployment, click on ![Manage App](../../images/heroku_manage_app.png) to open the dashboard of your new application: +![Heroku Dashboard](../../images/heroku_dashboard.png) + +Click on **Heroku Scheduler** to open scheduler dashboard. Create a new job, and define a new job to run this command every 10 minutes or every hour at 00 minutes: +```sh +php artisan schedule:run +``` + +You are now able to open the application and register a new user. + +### Configuration + +Your Monica instance will use a [JawsDB MySQL Kitefin Shared plan](https://elements.heroku.com/addons/jawsdb) (free) by default. Additional environment variables, such as details of the mail server, can be added after setup through the Heroku interface. +Monica doesn't require a lot of power - it will run perfectly fine on the free plan provided by Heroku. + +After deployment, the configuration of your app should look like this: + +![picture of configuration](https://raw.githubusercontent.com/monicahq/monica/main/docs/images/heroku_dashboard-resources.png) + +Note that when you deploy with the "Deploy to Heroku" purple button, only 1 dyno ("web") is activated while the "queue" one is not. That is OK - the "queue" dyno is only helpful if you set `QUEUE_CONNECTION=database` (default is 'sync'). + + +### Optional: Setup the access tokens to use the API + +In order to generate personal access tokens from the UI, you need to: + +* Install the [Heroku CLI](https://devcenter.heroku.com/categories/command-line) and log in. +* From your command line, run: +```sh +heroku run bash -a +``` +* Run: +```sh +php artisan passport:keys +php artisan passport:client --personal --no-interaction +``` + +This command will generate encryption keys in the `storage` directory. +The two keys `oauth-private.key` and `oauth-public.key` cannot be backup and recreate in heroku directly. + +* Still in the Heroku CLI, run this command to output the private key: +```sh +sed -E ':a;N;$!ba;s/\r{0,1}\n/\\n/g' ~/storage/oauth-private.key +``` + Copy the output to a new Heroku environment variable called `PASSPORT_PRIVATE_KEY` + +* Do the same thing with the contents of the public key: +```sh +sed -E ':a;N;$!ba;s/\r{0,1}\n/\\n/g' ~/storage/oauth-public.key +``` + Copy its contents to a new Heroku environment variable called `PASSPORT_PUBLIC_KEY` + + +Once Heroku is re-deploy, you should be able to use the 'Create new token' function in https://XXX.herokuapp.com/settings/api + +Once you have the token, you can use the API with a command line: +```sh +curl -H "Authorization: Bearer $API_TOKEN" https://XXX.herokuapp.com/api +``` + +If everything is well, this call will return: +```json +{"success":{"message":"Welcome to Monica"}} +``` + + +#### Optional: Generate a Password grant client for OAuth access + +* Still in the Heroku CLI, run this command to generate a password grant client: +```sh +php artisan passport:client --password --no-interaction +``` +* This will display a client ID and secret: +``` +Password grant client created successfully. +Client ID: 5 +Client secret: zsfOHGnEbadlBP8kLsjOV8hMpHAxb0oAhenfmSqq +``` + +* Copy the two values into two new environment variable of your `.env` file: + - The value of client ID in a `PASSPORT_PASSWORD_GRANT_CLIENT_ID` variable + - The value of client secret in a `PASSPORT_PASSWORD_GRANT_CLIENT_SECRET` variable + +## Limitations + +* No storage by default. It means you will not be able to upload photos, document, avatars for your contacts. + Follow [this documentation](/docs/installation/storage.md) to set an external storage. + +* No email by default - email configuration isn't required to use Monica on Heroku, but it's useful for reminders. You can configure your own [mailserver](/docs/installation/mail.md), though the easiest way to go about this is to use Mailgun's [free email add-on on Heroku](https://elements.heroku.com/addons/mailgun): + * [Sign up for Mailgun](https://signup.mailgun.com/new/signup) (the [free plan](https://www.mailgun.com/pricing) is sufficient) + * Add a custom domain in mailgun. + * Add the "To" and "From" e-mail addresses you're going to use as verified e-mail addresses on mailgun, and then actually verifying them. + * Upgrade mailgun by entering a credit card (there is no charge, but they do require you enter it so you'll be upgraded to some other tier that enables you to actually send messages). + * Verify the custom domain via DNS (there are instructions on their site) + * In Heroku, go to your app, then to the Settings tab. In it, you will have a button that reads "Reveal Config Vars". Click it, and change the following vars: + * `MAIL_MAILER`: `mailgun` + * `MAILGUN_DOMAIN`: your Mailgun domain + * `MAILGUN_SECRET`: your Mailgun API key — find it [here](https://app.mailgun.com/app/account/security) + * `MAIL_FROM_ADDRESS`: email address to use for 'from' email (could just use your own) + * `MAIL_FROM_NAME`: name of the 'from' user (could just use "Monica") + + +## Updating Heroku instance + +You can update your Monica instance to the latest version by cloning the repository and pushing it to Heroku git. + +Clone the Monica repository to your local environment by `git clone https://github.com/monicahq/monica`, and add heroku git repository by `heroku git:remote -a (heroku app name)`. Then, push to heroku by `git push heroku main:master`. Heroku will build and update the repository, automatically. + +See more information about updating Monica (including Heroku-specific things) [here](https://github.com/monicahq/monica/blob/main/docs/installation/update.md). + + +## Update from 2.x to 3.x + +If you already deployed a 2.x Monica instance, when you will upgrade to 3.x, you will have to manually add `node.js` as a buildpack: +- Go to `Settings` +- Under `Buildpacks`, add a new buildpack, and select `nodejs` + - `heroku/nodejs` will be selected automatically diff --git a/docs/installation/providers/ubuntu.md b/docs/installation/providers/ubuntu.md new file mode 100644 index 0000000..b4b706b --- /dev/null +++ b/docs/installation/providers/ubuntu.md @@ -0,0 +1,257 @@ +# Installing Monica on Ubuntu + +Ubuntu + +Monica can run on [Ubuntu 22.04 (Jammy Jellyfish)](http://releases.ubuntu.com/22.04/). + +- [Prerequisites](#prerequisites) + - [Types of databases](#types-of-databases) +- [Installation steps](#installation-steps) + - [1. Clone the repository](#1-clone-the-repository) + - [2. Setup the database](#2-setup-the-database) + - [3. Configure Monica](#3-configure-monica) + - [4. Configure cron job](#4-configure-cron-job) + - [5. Configure Apache webserver](#5-configure-apache-webserver) + - [Final step](#final-step) + +## Prerequisites + +Monica depends on the following: + +- [Apache httpd webserver](https://httpd.apache.org/) +- [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) +- PHP 8.1+ +- [Composer](https://getcomposer.org/) +- [Node.js](https://nodejs.org) +- [Yarn](https://yarnpkg.com) +- [MySQL](https://support.rackspace.com/how-to/installing-mysql-server-on-ubuntu/) + +**Apache:** If it doesn't come pre-installed with your server, follow the [instructions here](https://www.digitalocean.com/community/tutorials/how-to-install-linux-apache-mysql-php-lamp-stack-on-ubuntu-16-04#step-1-install-apache-and-allow-in-firewall) to setup Apache and config the firewall. + +**Git:** Git should come pre-installed with your server. If it's not, install it with: + +```sh +sudo apt update +sudo apt install -y git +``` + +**Unzip:** Unzip is required but was not installed by default. Install it with: + +```sh +sudo apt update +sudo apt install -y unzip +``` + +**Apache:** Apache should come pre-installed with your server. If it's not, install it with: + +```sh +sudo apt update +sudo apt install -y apache2 +``` + +**PHP 8.1+:** + +First add this PPA repository: + +```sh +sudo apt install -y software-properties-common +sudo add-apt-repository ppa:ondrej/php +``` + +Then install php 8.1 with these extensions: + +```sh +sudo apt update +sudo apt install -y php8.1-{bcmath,cli,curl,common,fpm,gd,gmp,intl,mbstring,mysql,opcache,redis,xml,zip} +``` + +**Composer:** After you're done installing PHP, you'll need the [Composer](https://getcomposer.org/download/) dependency manager. + +```sh +cd /tmp +curl -s https://getcomposer.org/installer -o composer-setup.php +sudo php composer-setup.php --install-dir=/usr/local/bin/ --filename=composer +rm -f composer-setup.php +``` + +(or you can follow instruction on [getcomposer.org](https://getcomposer.org/download/) page) + +**Node.js:** Install node.js with package manager. + +```sh +curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash - +sudo apt install -y nodejs +``` + +**Yarn:** Install yarn with npm. + +```sh +sudo npm install --global yarn +``` + +**Mysql:** Install Mysql 5.7. Note that this only installs the package, but does not setup Mysql. This is done later in the instructions: + +```sh +sudo apt update +sudo apt install -y mysql-server +``` + +### Types of databases + +The official Monica installation uses Mysql as the database system and **this is the only official system we support**. While Laravel technically supports PostgreSQL and SQLite, we can't guarantee that it will work fine with Monica as we've never tested it. Feel free to read [Laravel's documentation](https://laravel.com/docs/database#configuration) on that topic if you feel adventurous. + +## Installation steps + +Once the softwares above are installed: + +### 1. Clone the repository + +You may install Monica by simply cloning the repository. In order for this to work with Apache, you need to clone the repository in a specific folder: + +```sh +cd /var/www +git clone https://github.com/monicahq/monica.git +``` + +You should check out a tagged version of Monica since `main` branch may not always be stable. Find the latest official version on the [release page](https://github.com/monicahq/monica/releases): + +```sh +cd /var/www/monica +# Get latest tags from GitHub +git fetch +# Clone the desired version +git checkout tags/v4.0.0 +``` + +### 2. Setup the database + +Log in with the root account to configure the database. + +```sh +mysql -u root -p +``` + +Create a database called 'monica'. + +```sql +CREATE DATABASE monica CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; +``` + +Create a user called 'monica' and its password 'strongpassword'. + +```sql +CREATE USER 'monica'@'localhost' IDENTIFIED BY 'strongpassword'; +``` + +We have to authorize the new user on the `monica` db so that he is allowed to change the database. + +```sql +GRANT ALL ON monica.* TO 'monica'@'localhost'; +``` + +And finally we apply the changes and exit the database. + +```sql +FLUSH PRIVILEGES; +exit +``` + +### 3. Configure Monica + +`cd /var/www/monica` then run these steps: + +1. `cp .env.example .env` to create your own version of all the environment variables needed for the project to work. +2. Update `.env` to your specific needs + - Update database information. + ```diff + - DB_USERNAME=homestead + - DB_PASSWORD=secret + + DB_USERNAME=monica + # Use the password you created. + + DB_PASSWORD=strongpassword + ``` + - configure a [mailserver](/docs/installation/mail.md) for registration & reminders to work correctly. + - set the `APP_ENV` variable to `production`, `local` is only used for the development version. Beware: setting `APP_ENV` to `production` will force HTTPS. Skip this if you're running Monica locally. +4. Run `composer install --no-interaction --no-dev` to install all packages. +5. Run `yarn install` to install frontend packages, then `yarn run production` to build the assets (js, css). +6. Run `php artisan key:generate` to generate an application key. This will set `APP_KEY` with the right value automatically. +7. Run `php artisan setup:production -v` to run the migrations, seed the database and symlink folders. + - You can use `email` and `password` parameter to setup a first account directly: `php artisan setup:production --email=your@email.com --password=yourpassword -v` +8. _Optional_: Setup the queues with Redis, Beanstalk or Amazon SQS: see optional instruction of [generic installation](generic.md#setup-queues) +9. _Optional_: Setup the access tokens to use the API follow optional instruction of [generic installation](generic.md#setup-access-tokens) + +### 4. Configure cron job + +Monica requires some background processes to continuously run. The list of things Monica does in the background is described [here](https://github.com/monicahq/monica/blob/main/app/Console/Kernel.php#L33). +Basically those crons are needed to send reminder emails and check if a new version is available. +To do this, setup a cron that runs every minute that triggers the following command `php artisan schedule:run`. + +Run the crontab command: + +```sh +crontab -u www-data -e +``` + +Then, in the `crontab` editor window you just opened, paste the following at the end of the document: + +```sh +* * * * * php /var/www/monica/artisan schedule:run >> /dev/null 2>&1 +``` + +### 5. Configure Apache webserver + +1. Give proper permissions to the project directory by running: + +```sh +sudo chown -R www-data:www-data /var/www/monica +sudo chmod -R 775 /var/www/monica/storage +``` + +2. Enable the rewrite module of the Apache webserver: + +```sh +sudo a2enmod rewrite +``` + +3. Configure a new monica site in apache by doing: + +```sh +sudo nano /etc/apache2/sites-available/monica.conf +``` + +Then, in the `nano` text editor window you just opened, copy the following - swapping the `monica.example.com` with your server's IP address/associated domain: + +```html + + ServerName monica.example.com + + ServerAdmin webmaster@localhost + DocumentRoot /var/www/monica/public + + + Options Indexes FollowSymLinks + AllowOverride All + Require all granted + + + ErrorLog ${APACHE_LOG_DIR}/error.log + CustomLog ${APACHE_LOG_DIR}/access.log combined + +``` + +4. Apply the new `.conf` file and restart Apache. You can do that by running: + +```sh +sudo a2dissite 000-default.conf +sudo a2ensite monica.conf + +# Enable php8.1 fpm, and restart apache +sudo a2enmod proxy_fcgi setenvif +sudo a2enconf php8.1-fpm +sudo service php8.1-fpm restart +sudo service apache2 restart +``` + +### Final step + +The final step is to have fun with your newly created instance, which should be up and running to `http://localhost`. diff --git a/docs/installation/providers/vagrant.md b/docs/installation/providers/vagrant.md new file mode 100644 index 0000000..73fc2f1 --- /dev/null +++ b/docs/installation/providers/vagrant.md @@ -0,0 +1,77 @@ +# Installing Monica on Vagrant + + + +Monicahq vagrant box is available on [Vagrant Cloud](https://app.vagrantup.com/monicahq/boxes/monicahq). + +The only provider for this box is virtualbox. + +- [Run the monicahq vagrant box](#run-the-monicahq-vagrant-box) +- [Default Monica configuration in the VM](#default-monica-configuration-in-the-vm) + - [Database users](#database-users) + - [Apache configuration](#apache-configuration) +- [Build your own image](#build-your-own-image) + +## Run the monicahq vagrant box + +1. Download and install [Vagrant](https://www.vagrantup.com/) for your operating system +2. Create a folder to put the vagrant configuration files +```sh +mkdir ~/monica +cd ~/monica +``` +3. Download the `Vagrantfile` script +```sh +curl -sS https://raw.githubusercontent.com/monicahq/monica/main/scripts/vagrant/Vagrantfile -o Vagrantfile +``` +4. Edit Vagrantfile to set the appropriate host port number (default: 8080) +``` +config.vm.network "forwarded_port", guest: 80, host: 8080 +``` +5. Launch the virtual machine with +```sh +vagrant up +``` + +The virtual machine will be created and pulled up with Vagrantfile script. + +Once the process is complete you can either access the virtual machine by typing `vagrant ssh` in your terminal, or access the Monica web interface by opening [http://localhost:8080](http://localhost:8080) in your browser on your host machine. + +## Default Monica configuration in the VM + +### Database users + +* Root database user + - Username: `root` + - Password: `changeme` +* Monica database user + - Username: `monica` + - Password: `changeme` + +### Apache configuration + +* The project is installed in `/var/www/html/monica` +* The root folder for the web server is `/var/www/html/monica/public` + +## Build your own image + +1. Download the `Vagrantfile` script +```sh +curl -sS https://raw.githubusercontent.com/monicahq/monica/main/scripts/vagrant/build/Vagrantfile -o Vagrantfile +curl -sS https://raw.githubusercontent.com/monicahq/monica/main/scripts/vagrant/build/install-monica.sh -o install-monica.sh +``` +2. Run the box by calling: +```sh +vagrant up monicahq-latest +``` +for the latest commit, or with a GIT_TAG to run a specific version: +```sh +GIT_TAG=$(GIT_TAG) vagrant up monicahq-stable +``` +3. Package you own box +You can package it to use it more quickly later: +```sh +vagrant up monicahq-latest +vagrant package monicahq-latest --output ./my-monicahq.box +vagrant box add my-monicahq ./my-monicahq.box +``` diff --git a/docs/installation/readme.md b/docs/installation/readme.md new file mode 100644 index 0000000..ee79084 --- /dev/null +++ b/docs/installation/readme.md @@ -0,0 +1,48 @@ +# Installing Monica (Generic) + +Monica can be installed on a variety of platforms. The choice of the platform is yours. + +- [Requirements](#requirements) +- [Installation instructions for specific platforms](#installation-instructions-for-specific-platforms) + - [Generic Linux instructions](#generic-linux-instructions) + - [Platforms](#platforms) + - [Other documentation](#other-documentation) + + +## Requirements + +If you don't want to use [Docker](/docs/installation/providers/docker.md), the best way to setup the project is to use the same configuration that [Homestead](https://laravel.com/docs/homestead) uses. Basically, Monica depends on the following: + +* [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) +* PHP 8.1+ +* [Composer](https://getcomposer.org/) +* [MySQL](https://www.mysql.com/) +* Optional: Redis or Beanstalk + + +## Installation instructions for specific platforms + +The preferred OS distribution is Ubuntu 18.04, simply because all the development is made on it and we know it works. However, any OS that lets you install the above packages should work. + + +### Generic Linux instructions +* [Generic Instructions](/docs/installation/providers/generic.md) +* [Ubuntu](/docs/installation/providers/ubuntu.md) +* [Debian](/docs/installation/providers/debian.md) + + +### Platforms + +* [Docker](/docs/installation/providers/docker.md) +* [Heroku](/docs/installation/providers/heroku.md) +* [Vagrant](/docs/installation/providers/vagrant.md) +* [YunoHost](https://github.com/YunoHost-Apps/monica_ynh) +* [Cloudron](/docs/installation/providers/cloudron.md) +* [cPanel-based Shared Hosting](/docs/installation/providers/cpanel.md) + +### Other documentation + +* [Mail settings](/docs/installation/mail.md): allowing your instance to send mails. Useful for reminders. +* [Storage](/docs/installation/storage.md): define an external storage for your instance. +* [Ssl](/docs/installation/ssl.md): how to set ssl for your production-level instance. +* [FAQ](/docs/installation/faq.md): a list of common problems and solutions. diff --git a/docs/installation/ssl.md b/docs/installation/ssl.md new file mode 100644 index 0000000..9894bdf --- /dev/null +++ b/docs/installation/ssl.md @@ -0,0 +1,148 @@ +# Using monica with HTTPS + +- [Local Installation](#local-installation) +- [With a proxy](#with-a-proxy) + - [Example: Docker Compose](#example-docker-compose) + +When Monica is run with `APP_ENV=production`, it is required that Monica is running +with HTTPS. In order to satisfy this requirement, some additional configuration +needs to be performed. + +## Local Installation + +If you have Monica installed locally, and have HTTPS set up on your Apache server, +the only configuration required for Monica to support HTTPS is to set your `APP_URL` +to start with `https://`. This configuration parameter is used to generate external +links to your application for emails and such. + +## With a proxy + +Monica uses the [fideloper/proxy](https://packagist.org/packages/fideloper/proxy) +package to configure support *trusted proxies*. When enabled, Monica will trust +incoming headers like X-Forwarded-For, X-Forwarded-Host and X-Forwarded-Proto in +order to dynamically determine the setup of your application. + +You can configure this in your `.env` file: + +``` bash +# Set trusted proxy IP addresses. +# To trust all proxies that connect directly to your server, use a "*". +# To trust one or more specific proxies that connect directly to your server, use a comma separated list of IP addresses. +APP_TRUSTED_PROXIES= + +# Enable automatic cloudflare trusted proxy discover +APP_TRUSTED_CLOUDFLARE=false +``` + +Make sure that whatever proxy you are using is in your `APP_TRUSTED_PROXIES` list. +If you use Cloudflare, you can also simply set `APP_TRUSTED_CLOUDFLARE` to true to +automatically add cloudflare's IP addresses to the list. + +If you fail to have `APP_TRUSTED_PROXIES` set correctly, Monica will generate internal links that +have the wrong protocol or host on them. This might seem to work if you have redirects set up, +but can fail with insecure form submission errors. + +Remember to also update your `APP_URL` to correctly point to the HTTPS version of your application. + +### Example: Docker Compose + +If you are already using a dockerized version of Monica, you can use a Dockerized nginx +configuration to perform TLS termination. + +For example, you could use an `nginx.conf` similar to: + +``` nginx.conf +error_log stderr; +events { worker_connections 1024; } + +http { + server { + listen [::]:443; + listen 443; + server_name monica.example.com; + ssl on; + ssl_certificate /https-cert.pem; + ssl_certificate_key /https-key.pem; + ssl_protocols TLSv1.2; + + location / { + proxy_pass http://localhost:3001; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + } + server { + if ($host = monica.example.com) { + return 301 https://$host$request_uri; + } + listen 80 ; + listen [::]:80; + server_name monica.example.com; + return 404; + } +} +``` + +Or an apache.conf file similar to: +```virtual-site.conf + + ServerAdmin you@domain.com + ServerName monica.yourdomain.com + + RewriteEngine on + RewriteCond %{SERVER_NAME} =monica.yourdomain.com + # redirect all requests to port 80 to port 443 using 308 code + RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,QSA,NE,R=308] + +``` + +```virtual-site-ssl.conf + + + ServerAdmin you@domain.com + ServerName monica.yourdomain.com + + ProxyPreserveHost On + ProxyRequests Off + ProxyPass / http://localhost:3001/ + ProxyPassReverse / http://localhost:3001/ + RequestHeader add X-Forwarded-Proto https + + SSLCertificateFile /etc/letsencrypt/live/monica.yourdomain.com/fullchain.pem + SSLCertificateKeyFile /etc/letsencrypt/live/monica.yourdomain.com/privkey.pem + SSLCACertificateFile /etc/letsencrypt/live/monica.yourdomain.com/chain.pem + Include /etc/letsencrypt/options-ssl-apache.conf + + +``` + +And a `docker-compose.yml` like: + +``` yaml +version: '3.4' +services: + monica: + image: monica + expose: + - 3001:80 + volumes: + - '/var/monica-storage:/var/www/html/storage' + env_file: /etc/monica/monica.env + restart: unless-stopped + + nginx: + image: nginx:alpine + volumes: + - '/etc/monica/nginx.conf:/etc/nginx/nginx.conf:ro' + - '/etc/monica/https-cert.pem:/https-cert.pem:ro' + - '/etc/monica/https-key.pem:/https-key.pem:ro' + ports: + - 443:443 + depends_on: + - monica + restart: unless-stopped +``` + +You would also need to set `APP_TRUSTED_PROXIES=*` in your monica environment. diff --git a/docs/installation/storage.md b/docs/installation/storage.md new file mode 100644 index 0000000..4a63769 --- /dev/null +++ b/docs/installation/storage.md @@ -0,0 +1,105 @@ +# External storage + +- [Configure an external storage](#configure-an-external-storage) +- [Add an external storage](#add-an-external-storage) + - [1. Create AWS S3 storage](#1-create-aws-s3-storage) + - [2. Create a user](#2-create-a-user) + - [3. Set environment variables](#3-set-environment-variables) + - [(Optional) Use another S3 provider](#optional-use-another-s3-provider) + - [Move avatars to S3 storage](#move-avatars-to-s3-storage) + + +Some times you want to add an external storage for your avatars, photos, or documents. + +This is useful in particular if you install Monica on a stateless volatile instance, like Heroku, Platform.sh, etc. + +We currently only support AWS S3 driver as external storage. + + +## Configure an external storage + +You need to define at least these environment variables: + +``` +FILESYSTEM_DISK=s3 +AWS_BUCKET= +AWS_DEFAULT_REGION= +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +``` + +See below for more details about each environment variable. + + +## Add an external storage + +### 1. Create AWS S3 storage + +1. Go to the S3 [console](https://s3.console.aws.amazon.com/s3/home) +2. Add a new bucket +3. Save the name and location of the bucket in `AWS_BUCKET` and `AWS_DEFAULT_REGION` variables + +``` +AWS_BUCKET=my-bucket +AWS_DEFAULT_REGION=eu-west-3 +``` + +You can also use [AWS CLI](https://docs.aws.amazon.com/cli/index.html) to create the bucket: +```sh +aws s3 mb s3://my-bucket +``` + +### 2. Create a user + +1. Create a new user via the [console](https://console.aws.amazon.com/iam/home#/users). +2. Add the strategy for S3 access, for instance `AmazonS3FullAccess` is a good choice: + - add the user to a group with the right strategy + - or attach the strategy directly. + +3. Save credentials of the user in `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` variables + +``` +AWS_ACCESS_KEY_ID=AKXA3E2NYF7NPDJVQSOU +AWS_SECRET_ACCESS_KEY=aASalDme6wB8kGC7Xla6K3pI+FiFylpCVnGCmdnD +``` + +You can also use [AWS CLI](https://docs.aws.amazon.com/cli/index.html) to set credentials: +```sh +aws iam create-user --user-name user-monica-test +aws iam attach-user-policy --user-name user-monica-test --policy-arn arn:aws:iam::aws:policy/AmazonS3FullAccess +aws iam create-access-key --user-name user-monica-test +``` +Output: +``` +{ + "AccessKey": { + "UserName": "user-monica-test", + "AccessKeyId": "AKIAXE2N0F6NIMZXLCGB", + "Status": "Active", + "SecretAccessKey": "Lh5ValIoe9xlfrhkpqiZOub1TFFo4qn1sAdFvlOM", + "CreateDate": "2020-04-12T11:35:06+00:00" + } +} +``` +Then save `AccessKeyId` and `SecretAccessKey` in `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` variables. + +### 3. Set environment variables + +Set the `FILESYSTEM_DISK` variable to use S3 storage: +``` +FILESYSTEM_DISK=s3 +``` + + +### (Optional) Use another S3 provider + +*AWS_ENDPOINT* variable can be used to define a S3-compatible provider other than Amazon, like [Digitalocean](https://www.digitalocean.com/products/spaces/), [Scaleway](https://www.scaleway.com) or [Minio](https://min.io/). + example: `AWS_ENDPOINT=nyc3.digitaloceanspaces.com` + + +### Move avatars to S3 storage + +If you previously used local storage and want to move all avatars to a new S3 storage, use `monica:moveavatars` command once to move all files: +```sh +php artisan monica:moveavatars +``` diff --git a/docs/installation/update.md b/docs/installation/update.md new file mode 100644 index 0000000..33f8584 --- /dev/null +++ b/docs/installation/update.md @@ -0,0 +1,237 @@ +# Update your server + +- [Generic instructions](#generic-instructions) +- [Updating Heroku instance](#updating-heroku-instance) +- [Importing vCards (CLI only)](#importing-vcards-cli-only) +- [Importing SQL from the exporter feature](#importing-sql-from-the-exporter-feature) + - [Importing SQL into Heroku](#importing-sql-into-heroku) + - [WARNING: This will delete your current database. Only use on fresh installations, or if you know what you're doing.](#warning-this-will-delete-your-current-database-only-use-on-fresh-installations-or-if-you-know-what-youre-doing) + +## Generic instructions + +Monica uses the concept of releases and tries to follow +[Semantic Versioning](http://semver.org/) as much as possible. If you run the project locally, +or if you have installed Monica on your own server, you need to follow the steps below to update it, **every single time**, or you will run into problems. + +1. Always make a backup of your data before upgrading. +2. Check that your backup is valid. +3. Read the [release notes](https://github.com/monicahq/monica/blob/main/CHANGELOG.md) to check for breaking changes. +4. Update sources: + 1. Consider check out a tagged version of Monica since `main` branch may not always be stable. + Find the latest official version on the [release page](https://github.com/monicahq/monica/releases) + ```sh + # Get latest tags from GitHub + git fetch + # Clone the desired version + git checkout tags/v2.18.0 + ``` + 2. Or check out `main` + ```sh + git pull origin main + ``` +5. Update the dependencies of the project: + ```sh + composer install --no-interaction --no-dev + ``` +6. Run `yarn install` to install frontend packages, then `yarn run production` to build the assets (js, css). +7. Then, run the following command to make the proper update: + ```sh + php artisan monica:update --force + ``` + +The `monica:update` command runs migration scripts for the database, and flushes all caches for config, route, and view as an optimization process. It’s easier than running every required command individually. + + +Note: if you have just change some setting in your `.env` file, as the configuration of the application is cached, any update on the `.env` file will not be detected after that. You may have to run `php artisan config:cache` manually after every update of `.env` file. + + +## Updating Heroku instance + +You can update your Monica instance to the latest version by cloning the repository and pushing it to Heroku git. + +1. Clone the Monica repository to your local environment by `git clone https://github.com/monicahq/monica.git`. +1. Add your app's heroku git repository by `heroku git:remote -a (heroku app name)` (this of course requires the [Heroku CLI](https://devcenter.heroku.com/articles/heroku-cli). +1. Push to heroku by `git push heroku main:master`. Heroku will build and update the repository, automatically. + +## Importing vCards (CLI only) + +**Note**: this is only possible if you install Monica on your server or locally. + +You can import your contacts in vCard format in your account with one simple +CLI command: +`php artisan import:vcard {email user} {filename}.vcf` + +where `{email user}` is the email of the user in your Monica instance who will +be associated the new contacts to, and `{filename}` being the name of your .vcf file. +The .vcf file has to be in the root of your Monica installation (in the same directory +where the artisan file is). + +Example: `php artisan import:vcard john@doe.com contacts.vcf` + +The `.vcf` can contain as many contacts as you want. + +## Importing SQL from the exporter feature + +Monica allows you to export your data in SQL, under the Settings panel. When you +export your data in SQL, you'll get a file called `monica.sql`. + +To import it into your own instance, you need to make sure that the database of +your instance is completely empty (no tables, no data). + +Then, follow the steps: + +* `php artisan migrate` +* Then import `monica.sql` into your database. Tools like phpmyadmin or Sequel +Pro might help you with that. +* Finally, sign in with the same credentials as the ones used on +https://monicahq.com and you are good to go. + +There is one caveat with the SQL exporter: you can't get the photos you've uploaded for now. + +### Importing SQL into Heroku + +If you're running your own Monica Heroku instance as mentioned in the [Heroku Installation Documentation](https://github.com/monicahq/monica/blob/main/docs/installation/providers/heroku.md), you're not actually running your own SQL server, which means that the solutions above might not be of assistance. + +Heroku dynos use a [ClearDB MySQL add-on](https://devcenter.heroku.com/articles/cleardb) as their database. You can still use an SQL admin tool (like phpMyAdmin or Sequel Pro) to interact with the database, as well as use the `mysql-client` command line tool, you just need to know where to look for the credentials. + +If you open your app on the Heroku web interface, and click the "Settings" tab, you'll have an option to reveal your configuration vars. Do so, and look for the `CLEARDB_DATABASE_URL` variable. It's format should look like this: + +`mysql://:@/?reconnect=true` + +Which are the database's `HOST` URL, its name (i.e. `DATABASE`) and your `USERNAME` and `PASSWORD`. +The `HOST` should be the region where the database is located (i.e. `us-cdbr-iron-east-01.cleardb.net`), the `DATABASE` should be prepended with `heroku_` (i.e. `heroku_xxxx`) and the `USERNAME` and `PASSWORD` should be strings of alphanumeric characters. + +Now that you have the database's URL and access credentials, you can log into the database from your favorite database management tool. If you'd like to use a command-line tool, here are the step by step instructions for debian-based (e.g. Ubuntu) Linux: + +#### WARNING: This will delete your current database. Only use on fresh installations, or if you know what you're doing. + +1. **Update your Monica instance to the same version as the one you're importing into.** This will prevent nasty SQL mismatches later on. +2. Download your export file as explained above. Make sure you remember the username and password of the instance you **exported from**, as those will be your new sign-in information for the instance you're **importing into**. +3. Get `mysql-client` by `sudo apt-get install mysql-client`. Note you might need to first add the relevant repository using the instructions [here](https://downloads.mariadb.org/mariadb/repositories/#mirror=kku) (although don't follow them all the way, or you'll get a full running server on your own machine). If you're going to follow the scripted truncation listed on the steps below, you'll need access to the MySQL socket, which is only available if you also installed `mysql-server`. You can do so by `sudo apt-get install mysql-server`. +4. Connect to your database - `mysql --host= --user= --password= --reconnect `. You should see something like this in your terminal: + +``` +mysql: [Warning] Using a password on the command line interface can be insecure. +Reading table information for completion of table and column names +You can turn off this feature to get a quicker startup with -A +``` + +We are indeed using the password on the CLI, so disregard the warning. The `Reading table....` part should only take 10-20 seconds or so, wait it out. After that you should be prompted by your installation's MySQL database: + +``` +Welcome to the MySQL monitor. Commands end with ; or \g. +Your MySQL connection id is 195775195 +Server version: 5.5.62-log MySQL Community Server (GPL) + +Copyright (c) 2000, 2019, Oracle and/or its affiliates. All rights reserved. + +Oracle is a registered trademark of Oracle Corporation and/or its +affiliates. Other names may be trademarks of their respective +owners. + +Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. + +mysql> +``` +5. Take a look around, if you'd like. If you'll enter `SHOW DATABASES` you'll see: + +``` +Connection id: 195779265 +Current database: heroku_xxxxxxxxx + ++------------------------+ +| Database | ++------------------------+ +| information_schema | +| heroku_xxxxxxxxx | ++------------------------+ +2 rows in set (19.85 sec) + +``` +Where `heroku_xxxxxxxxx` is `DATABASE`, your database's name. Note that the `Current database` is your Monica database, `DATABASE`. + +We're now done looking around and you can disconnect from the database by entering `quit` and hitting the return key. + +**Note:** If at any point the server disconnects, you'll see something like this: +``` +mysql> SHOW DATABASES; +ERROR 2013 (HY000): Lost connection to MySQL server during query +mysql> SHOW DATABASES; +ERROR 2006 (HY000): MySQL server has gone away +No connection. Trying to reconnect... +``` + +This is perfectly fine, and the reason behind the `--reconnect` flag you saw earlier. + +6. **DANGER: This will delete all the things.** Make sure you're not connected to the database anymore (i.e. you entered `quit` and got back to your own machine). + +Empty out all tables by running the following few lines of code (slightly modified from [this SO question](https://stackoverflow.com/questions/1912813/truncate-all-tables-in-a-mysql-database-in-one-command)), where all the credentials are the same as mentioned earlier. You can also copy and paste it into a `.sh` file, `chmod 777 ` and then run it by `./`. + +``` +# USAGE: mysql_run_query +mysql_run_query() { +# Connect to the database silently (-N and -s) and execute the given command (-e) + mysql --host= --user= --password= --reconnect -Nse "$1" +} + + +# The command below lists all the tables in your database, and pipes it to this while loop +echo "Getting all of the database's table names..." +mysql_run_query "SHOW TABLES;" | +while read table; do + + # Empty out (i.e. "TRUNCATE" each table) + echo "Emptying out $table..." + mysql_run_query "SET FOREIGN_KEY_CHECKS = 0;TRUNCATE TABLE $table;SET FOREIGN_KEY_CHECKS = 1;" + +done + +echo "Done!" +``` + +This should take a bit of time to run, but you should be able to see the process as the truncated table go by. Wait for the `Done!` message. + +**Notes:** +* This script performs the table truncations independent of one another, and one by one - on different connections. This is done on purpose, to avoid any catastrophic finger-slips on the actual database's MySQL console. If something bad happens, this should allow you to kill the terminal in time, or at least `CTRL+C` out of there. If you know what you're doing, then you can just connect to the database and follow [this article](https://tableplus.com/blog/2018/08/mysql-how-to-truncate-all-tables.html) on how to truncate all the tables with one SQL query. +* If you get the following error: +``` +ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/var/run/mysqld/mysqld.sock' (2) +``` +This probably means you have not installed `mysql-server` as mentioned before. Please do so now, and repeat the process. +* The `SET_FOREIGN_KEYS` part above relieves you of facing these type of errors: +``` +ERROR 1701 (42000) at line 1: Cannot truncate a table referenced in a foreign key constraint +``` +Due to the database's schema. If you do end up seeing those types of errors, please open an issue. + +7. On your own machine (i.e. not on the remote database) import the fresh database into your installation (blatantly copied from this [SO answer](https://stackoverflow.com/questions/11803496/dump-sql-file-to-cleardb-in-heroku)): +``` +mysql ---host= --user= --password= --reconnect < monica.sql +``` + +If you get an error of the following format: +``` +ERROR 1452 (23000) at line 8: Cannot add or update a child row: a foreign key constraint fails +``` + +Than open up `monica.sql` and at the following at the start of the file, right before the first `INSERT INTO...` statement: + +``` +SET FOREIGN_KEY_CHECKS = 0; +``` + +And this, at the very end of the file (after the last `INSERT INTO...` statement: + +``` +SET FOREIGN_KEY_CHECKS = 1 +``` + +**Notes:** + +* If you get an error of the following format: +``` +ERROR 1064 (42000) at line 264: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ..... +``` +It means that the database schema you're trying to import into does not match the schema of that database you've exported from. This is usually due to a change in the schema between Monica version, and should only happen if you're migrating from an old, unupdated version of Monica to a new version on a new machine. Please file an issue if you see this error and we will attempt to assist you. + +You should now be able to access your Monica instance with the same credentials used for the old instance. diff --git a/docs/readme.md b/docs/readme.md new file mode 100644 index 0000000..38bd59b --- /dev/null +++ b/docs/readme.md @@ -0,0 +1,40 @@ +# Monica Documentation +Welcome to Monica - a great, simple yet complete, open-source personal relationship manager platform. + +This doc is the main source of documentation for developers working with (or contributing to) the Monica project, and advanced users who want to install Monica on their servers. If this is your first time hearing about Monica, we recommend starting with [Monica website](https://monicahq.com). + +## Table of content + +* [Use Monica](/docs/user/readme.md) +* Install Monica on your server + * [Install a new instance](/docs/installation/readme.md) + * [Maintain your server](/docs/installation/update.md) +* Contribute to Monica + * [Contribute as a developer](/docs/contribute/readme.md) + * [Help translate the application](/docs/contribute/translate.md) + +**Specific to Monica's core contributors** +* [Add content](/docs/administrators/tips.md) +* [Deployment instructions](/docs/administrators/deployment.md) + +## Thanks to companies and open source projects + +The Monica project is extremely grateful for the help provided by the following companies and open source projects. + +### Companies + +Those companies have graciously offered a paid plan for free. + +| **Supported by** | **Description** | +|----------------------------------|-----------------------------------------------------------------------------------------------| +| Sentry | [Sentry](https://sentry.io) is a cross-platform crash reporting and aggregation platform. We use it to analyze what's going on in production. | +| Crowdin | [Crowdin](https://crowdin.com/project/monicahq) is a translation platform. | +| Cypress | [Cypress](https://dashboard.cypress.io/projects/q8h6k9/runs) is an end-to-end test platform. | + +### Open source projects + +Monica is built upon the shoulders of incredible open source projects. We simply wouldn't exist without them. + +* [Laravel](http://laravel.com/) +* [Git](http://git-scm.com/) +* [Linux](http://linux.org/) diff --git a/docs/user/carddav.md b/docs/user/carddav.md new file mode 100644 index 0000000..101ec4b --- /dev/null +++ b/docs/user/carddav.md @@ -0,0 +1,146 @@ +# CardDAV and CalDAV + +**Using Monica as a CardDAV and CalDAV server** + +- [Authentication](#authentication) +- [CardDAV and CalDAV urls](#carddav-and-caldav-urls) +- [Clients](#clients) + - [Android](#android) + - [iPhone](#iphone) + - [Apple iOS](#apple-ios) + - [Thunderbird](#thunderbird) + - [Windows 10 Contacts application](#windows-10-contacts-application) + - [Outlook (Microsoft Office)](#outlook-microsoft-office) + + +CardDAV is a protocol based on WebDAV, allowing you to **synchronize your contacts** between multiple devices (mobile phone, mail software, etc.). +CalDAV is pretty much the same, with Calendars. In Monica it allows you to synchronize the birthdays anniversary of your contacts, and the task list (which uses the same CalDAV protocol). + +CardDAV and CalDAV for Monica are implemented with [sabre/dav](https://sabre.io/) library. + + +## Authentication + +To authenticate with the server, you'll need to create an API token. + +Go to the [Settings > API](https://app.monicahq.com/settings/api) page, and Create a new token. + +![Create a token](/docs/images/carddav_token1.png) +![Create a token](/docs/images/carddav_token2.png) + +Save this token to authenticate with CardDAV and CalDAV. + +The login is your email login. + +## CardDAV and CalDAV urls + +On the [Settings > DAV Resources](https://app.monicahq.com/settings/dav) page of your instance you will find some help about the URL to use. + +**In most of the cases, the base url should work.** So just copy/paste it to your client app to see the magic happen ! + +![Base url](/docs/images/carddav_url.png) + + +## Clients + +This is some example of clients configuration. + +This list is not exhaustive, as the synchronisation can work on every CardDAV compatible device. + + + +### Android + +Android devices do not support CardDAV natively, so you'll need to install a third-party application to use CardDAV. + +We recommend installing [DAVx5](https://www.davx5.com/) which is a great CardDAV client. You will find the application on the [Google Play store](https://play.google.com/store/apps/details?id=at.bitfire.davdroid) or even on [F-Droid store](https://f-droid.org/fr/packages/at.bitfire.davdroid/) for free. + +To add an account: +- Click on the `+` button +- Choose **Connection with URL and username.**, and enter the following details: + - **URL**: Enter the `/dav` base url, i.e. `https://app.monicahq.com/dav` + - **Username**: Your email login address + - **Password**: The token you've created on the API settings page. + + ![Davx5 config](/docs/images/carddav_davx5_1.png) + +- Chose the option **Groups are per-contact categories** +- Click on **Connect** +- Select the data you want to sync + +After that, you can use any Contacts application on your phone. Be sure to display your Monica account on the list of contacts, and to use it by default for new contacts. + + +### iPhone + + +### Apple iOS + + +### Thunderbird + +[Thunderbird](https://www.thunderbird.net) supports CardDAV natively as of version 91. + +For older versions, or enhanced functionality, we recommend installing [CardBook](https://addons.thunderbird.net/thunderbird/addon/cardbook/). +Download the add-on and install it through Thunderbird's add-on manager. + +To add an account: +- Create a new Address Book. +- Choose **Remote** +- Choose **CardDAV** and enter the following details: + - **URL**: Paste the `/dav` base url, i.e. `https://app.monicahq.com/dav` + - **Username**: Your email login address + - **Password**: The token you've created on the API settings page. +- Click **Validate** to check the credentials, then click on the **Next** button +- You can now see the address book, and the color to associate with. Select `4.0` as vCard format if you want. +- Click **Next** and **Finish** + + +### Windows 10 Contacts application + +Windows 10 Contacts application support CardDAV. It is used to synchronize iCloud kind account. + +- Open **Contacts** application +- Click on **Import contacts** or **Add an account** on the parameters +- Choose **iCloud** account kind and enter the following details: + - **User name**: your Monica email login address + - **Name**: enter your full name + - **Password**: write some scrap (do **not** enter Monica credentials for now) + + ![](/docs/images/windows10_contacts_1.png) + +- Click on **Connect**, then **OK** + + +After this step, the application will try to synchronize with iCloud servers. It will fail, but it's normal as we don't have an account on it. + +Fix the settings: +- Open the **Mail** application +- Open on the wheel ![wheel](/docs/images/windows10_wheel.png) to go to the settings. If the settings are not reachable, add a fake POP, IMAP account +- Click on your Monica account settings — it is named _iCloud_ at this point — and select **Change parameters** +- Enter the following details: + - **User name**: your Monica email login address + - **Password**: The token you've created on the API settings page + - **Account name**: enter the description for this account, like "Monica" + + ![](/docs/images/windows10_contacts_2.png) + +- Click on **Change mailbox sync settings, Options for syncing your content**, and change the following settings + - **Download new email**: select `manually` as we don't sync emails here + - **Sync options**: unselect **Email**, and select **Calendar** and **Contacts** +- Click on **Advanced mailbox settings, Contacts (CardDAV) and Calendar (CalDAV) server settings**, and change the following settings + - **Incoming email server**: enter `localhost` + - **Outgoing (SMTP) email server**: enter `localhost` + - **Contacts server (CardDAV)**: Paste the `/dav` base url, i.e. `https://app.monicahq.com/dav` + - **Calendar server (CalDAV)**: Paste the `/dav` base url, i.e. `https://app.monicahq.com/dav` + + ![](/docs/images/windows10_contacts_3.png) + +- Click on **Done** button, then **Save** + +Your contacts and calendar are now syncing. +- On **Contacts** application, be sure to display your Monica contacts account on the **Filter** +- On **Calendar** application, display your Contacts' Anniversary calendar + + +### Outlook (Microsoft Office) diff --git a/docs/user/readme.md b/docs/user/readme.md new file mode 100644 index 0000000..eff5e07 --- /dev/null +++ b/docs/user/readme.md @@ -0,0 +1,12 @@ +# Use Monica + +Monica is a personal relationship manager platform. +It helps you organize the social interactions with your loved ones. + +You can use a bunch of nice [features](https://www.monicahq.com/features), or use our [API](https://www.monicahq.com/api). +See the recent [changelog](https://www.monicahq.com/changelog) to track every changes with it. + +See documentations about: + +- [Setup your phone, or desktop app to sync contacts](carddav.md) +- [Enhance security of your account](security.md) diff --git a/docs/user/security.md b/docs/user/security.md new file mode 100644 index 0000000..ac5f649 --- /dev/null +++ b/docs/user/security.md @@ -0,0 +1,8 @@ +# Security + +If the option is set, you can add a [Multi Factor Authentication](https://en.wikipedia.org/wiki/Multi-factor_authentication) device to secure your connection. + +You can either: + +- add a 2FA security device, using an OTP application on your mobile phone +- or add a [Security Key](https://en.wikipedia.org/wiki/Universal_2nd_Factor) key. Monica provides [WebAuthn](https://en.wikipedia.org/wiki/WebAuthn) support. It's supported on most recent browsers. Be aware the instance must run in `https`. diff --git a/fortrabbit.yml b/fortrabbit.yml new file mode 100644 index 0000000..5ab6d03 --- /dev/null +++ b/fortrabbit.yml @@ -0,0 +1,15 @@ +# differentiate from the deployment files +version: 2 + +# optional Composer settings +composer: + # Resolves to the --no-dev parameter + no-dev: true + +# called after Composer runs +post: artisan monica:update --force -vvv + +# list of sustained folders in ~/htdocs. If not given, then it defaults to the "vendor" folder +sustained: + - storage + - vendor diff --git a/nginx_app.conf b/nginx_app.conf new file mode 100644 index 0000000..54cf502 --- /dev/null +++ b/nginx_app.conf @@ -0,0 +1,22 @@ +location / { + # try to serve file directly, fallback to rewrite + try_files $uri @rewriteapp; +} + +location @rewriteapp { + # Redirect .well-known urls (https://en.wikipedia.org/wiki/List_of_/.well-known/_services_offered_by_webservers) + rewrite .well-known/carddav /dav/ permanent; + rewrite .well-known/caldav /dav/ permanent; + rewrite .well-known/security.txt$ /security.txt permanent; + + # Old carddav url + rewrite carddav/(.*) /dav/$1 permanent; + + # rewrite all to app.php + rewrite ^(.*)$ /index.php/$1 last; +} + +location ~ ^/(app|app_dev|config)\.php(/|$) { + try_files @heroku-fcgi @heroku-fcgi; + internal; +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index fb50318..0000000 --- a/package-lock.json +++ /dev/null @@ -1,2301 +0,0 @@ -{ - "name": "mischcrm", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "mischcrm", - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "express": "^4.19.2", - "sqlite3": "^5.1.7" - } - }, - "node_modules/@gar/promisify": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", - "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", - "license": "MIT", - "optional": true - }, - "node_modules/@npmcli/fs": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", - "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", - "license": "ISC", - "optional": true, - "dependencies": { - "@gar/promisify": "^1.0.1", - "semver": "^7.3.5" - } - }, - "node_modules/@npmcli/move-file": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", - "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", - "deprecated": "This functionality has been moved to @npmcli/fs", - "license": "MIT", - "optional": true, - "dependencies": { - "mkdirp": "^1.0.4", - "rimraf": "^3.0.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@tootallnate/once": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", - "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "license": "ISC", - "optional": true - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/agent-base/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "optional": true, - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/agent-base/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT", - "optional": true - }, - "node_modules/agentkeepalive": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", - "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "humanize-ms": "^1.2.1" - }, - "engines": { - "node": ">= 8.0.0" - } - }, - "node_modules/aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "license": "MIT", - "optional": true, - "dependencies": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/aproba": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", - "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", - "license": "ISC", - "optional": true - }, - "node_modules/are-we-there-yet": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", - "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "optional": true, - "dependencies": { - "delegates": "^1.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "license": "MIT" - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT", - "optional": true - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "license": "MIT", - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/body-parser": { - "version": "1.20.5", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", - "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "~1.2.0", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "on-finished": "~2.4.1", - "qs": "~6.15.1", - "raw-body": "~2.5.3", - "type-is": "~1.6.18", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", - "license": "MIT", - "optional": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/cacache": { - "version": "15.3.0", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", - "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", - "license": "ISC", - "optional": true, - "dependencies": { - "@npmcli/fs": "^1.0.0", - "@npmcli/move-file": "^1.0.1", - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "glob": "^7.1.4", - "infer-owner": "^1.0.4", - "lru-cache": "^6.0.0", - "minipass": "^3.1.1", - "minipass-collect": "^1.0.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.2", - "mkdirp": "^1.0.3", - "p-map": "^4.0.0", - "promise-inflight": "^1.0.1", - "rimraf": "^3.0.2", - "ssri": "^8.0.1", - "tar": "^6.0.2", - "unique-filename": "^1.1.1" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/color-support": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", - "license": "ISC", - "optional": true, - "bin": { - "color-support": "bin.js" - } - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "license": "MIT", - "optional": true - }, - "node_modules/console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", - "license": "ISC", - "optional": true - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", - "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", - "license": "MIT" - }, - "node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", - "license": "MIT", - "optional": true - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT", - "optional": true - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "license": "MIT", - "optional": true, - "dependencies": { - "iconv-lite": "^0.6.2" - } - }, - "node_modules/encoding/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "license": "MIT", - "optional": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/err-code": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", - "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", - "license": "MIT", - "optional": true - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", - "license": "(MIT OR WTFPL)", - "engines": { - "node": ">=6" - } - }, - "node_modules/express": { - "version": "4.22.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", - "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", - "license": "MIT", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "~1.20.5", - "content-disposition": "~0.5.4", - "content-type": "~1.0.4", - "cookie": "~0.7.1", - "cookie-signature": "~1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.3.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "~0.1.12", - "proxy-addr": "~2.0.7", - "qs": "~6.15.1", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "~0.19.0", - "serve-static": "~1.16.2", - "setprototypeof": "1.2.0", - "statuses": "~2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "license": "MIT" - }, - "node_modules/finalhandler": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", - "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "statuses": "~2.0.2", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "license": "MIT" - }, - "node_modules/fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "license": "ISC", - "optional": true - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gauge": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", - "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "optional": true, - "dependencies": { - "aproba": "^1.0.3 || ^2.0.0", - "color-support": "^1.1.3", - "console-control-strings": "^1.1.0", - "has-unicode": "^2.0.1", - "signal-exit": "^3.0.7", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wide-align": "^1.1.5" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "license": "MIT" - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "license": "ISC", - "optional": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "license": "ISC", - "optional": true - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", - "license": "ISC", - "optional": true - }, - "node_modules/hasown": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", - "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/http-cache-semantics": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", - "license": "BSD-2-Clause", - "optional": true - }, - "node_modules/http-errors": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", - "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", - "license": "MIT", - "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/http-proxy-agent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", - "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", - "license": "MIT", - "optional": true, - "dependencies": { - "@tootallnate/once": "1", - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/http-proxy-agent/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "optional": true, - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/http-proxy-agent/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT", - "optional": true - }, - "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "license": "MIT", - "optional": true, - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/https-proxy-agent/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "optional": true, - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/https-proxy-agent/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT", - "optional": true - }, - "node_modules/humanize-ms": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", - "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "ms": "^2.0.0" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/infer-owner": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", - "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", - "license": "ISC", - "optional": true - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "license": "ISC", - "optional": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "license": "ISC" - }, - "node_modules/ip-address": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", - "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 12" - } - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-lambda": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", - "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", - "license": "MIT", - "optional": true - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC", - "optional": true - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "license": "ISC", - "optional": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/make-fetch-happen": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", - "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", - "license": "ISC", - "optional": true, - "dependencies": { - "agentkeepalive": "^4.1.3", - "cacache": "^15.2.0", - "http-cache-semantics": "^4.1.0", - "http-proxy-agent": "^4.0.1", - "https-proxy-agent": "^5.0.0", - "is-lambda": "^1.0.1", - "lru-cache": "^6.0.0", - "minipass": "^3.1.3", - "minipass-collect": "^1.0.2", - "minipass-fetch": "^1.3.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.2", - "promise-retry": "^2.0.1", - "socks-proxy-agent": "^6.0.0", - "ssri": "^8.0.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "license": "ISC", - "optional": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", - "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-collect": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", - "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", - "license": "ISC", - "optional": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minipass-fetch": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", - "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", - "license": "MIT", - "optional": true, - "dependencies": { - "minipass": "^3.1.0", - "minipass-sized": "^1.0.3", - "minizlib": "^2.0.0" - }, - "engines": { - "node": ">=8" - }, - "optionalDependencies": { - "encoding": "^0.1.12" - } - }, - "node_modules/minipass-flush": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.7.tgz", - "integrity": "sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==", - "license": "BlueOak-1.0.0", - "optional": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minipass-pipeline": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", - "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", - "license": "ISC", - "optional": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-sized": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", - "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", - "license": "ISC", - "optional": true, - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "license": "MIT", - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "license": "MIT", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "license": "MIT" - }, - "node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/napi-build-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", - "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", - "license": "MIT" - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/node-abi": { - "version": "3.92.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.92.0.tgz", - "integrity": "sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==", - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/node-addon-api": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "license": "MIT" - }, - "node_modules/node-gyp": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", - "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", - "license": "MIT", - "optional": true, - "dependencies": { - "env-paths": "^2.2.0", - "glob": "^7.1.4", - "graceful-fs": "^4.2.6", - "make-fetch-happen": "^9.1.0", - "nopt": "^5.0.0", - "npmlog": "^6.0.0", - "rimraf": "^3.0.2", - "semver": "^7.3.5", - "tar": "^6.1.2", - "which": "^2.0.2" - }, - "bin": { - "node-gyp": "bin/node-gyp.js" - }, - "engines": { - "node": ">= 10.12.0" - } - }, - "node_modules/nopt": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", - "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", - "license": "ISC", - "optional": true, - "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/npmlog": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", - "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", - "deprecated": "This package is no longer supported.", - "license": "ISC", - "optional": true, - "dependencies": { - "are-we-there-yet": "^3.0.0", - "console-control-strings": "^1.1.0", - "gauge": "^4.0.3", - "set-blocking": "^2.0.0" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "aggregate-error": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-to-regexp": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", - "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", - "license": "MIT" - }, - "node_modules/prebuild-install": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", - "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", - "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", - "license": "MIT", - "dependencies": { - "detect-libc": "^2.0.0", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp-classic": "^0.5.3", - "napi-build-utils": "^2.0.0", - "node-abi": "^3.3.0", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^4.0.0", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0" - }, - "bin": { - "prebuild-install": "bin.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/promise-inflight": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", - "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", - "license": "ISC", - "optional": true - }, - "node_modules/promise-retry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", - "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", - "license": "MIT", - "optional": true, - "dependencies": { - "err-code": "^2.0.2", - "retry": "^0.12.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/pump": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/qs": { - "version": "6.15.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", - "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", - "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", - "license": "MIT", - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "license": "ISC", - "optional": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/semver": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", - "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/send": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", - "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.1", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "~2.4.1", - "range-parser": "~1.2.1", - "statuses": "~2.0.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/serve-static": { - "version": "1.16.3", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", - "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", - "license": "MIT", - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "~0.19.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "license": "ISC", - "optional": true - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", - "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "license": "ISC", - "optional": true - }, - "node_modules/simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/simple-get": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", - "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "decompress-response": "^6.0.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", - "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", - "license": "MIT", - "optional": true, - "dependencies": { - "ip-address": "^10.1.1", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", - "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "agent-base": "^6.0.2", - "debug": "^4.3.3", - "socks": "^2.6.2" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/socks-proxy-agent/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "optional": true, - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/socks-proxy-agent/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT", - "optional": true - }, - "node_modules/sqlite3": { - "version": "5.1.7", - "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz", - "integrity": "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==", - "hasInstallScript": true, - "license": "BSD-3-Clause", - "dependencies": { - "bindings": "^1.5.0", - "node-addon-api": "^7.0.0", - "prebuild-install": "^7.1.1", - "tar": "^6.1.11" - }, - "optionalDependencies": { - "node-gyp": "8.x" - }, - "peerDependencies": { - "node-gyp": "8.x" - }, - "peerDependenciesMeta": { - "node-gyp": { - "optional": true - } - } - }, - "node_modules/ssri": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", - "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", - "license": "ISC", - "optional": true, - "dependencies": { - "minipass": "^3.1.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/statuses": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", - "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "optional": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "optional": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "deprecated": "Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "license": "ISC", - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/tar-fs": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", - "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", - "license": "MIT", - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.1.4" - } - }, - "node_modules/tar-fs/node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "license": "ISC" - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "license": "MIT", - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/tar/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "license": "ISC", - "engines": { - "node": ">=8" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/unique-filename": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", - "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", - "license": "ISC", - "optional": true, - "dependencies": { - "unique-slug": "^2.0.0" - } - }, - "node_modules/unique-slug": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", - "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", - "license": "ISC", - "optional": true, - "dependencies": { - "imurmurhash": "^0.1.4" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "license": "ISC", - "optional": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/wide-align": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", - "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", - "license": "ISC", - "optional": true, - "dependencies": { - "string-width": "^1.0.2 || 2 || 3 || 4" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" - } - } -} diff --git a/package.json b/package.json index 6e7996f..ef25223 100644 --- a/package.json +++ b/package.json @@ -1,23 +1,100 @@ { - "name": "mischcrm", - "version": "1.0.0", - "description": "Sleek, glassmorphic Personal CRM to track relationships, meetings, and friend details", - "main": "server.js", + "private": true, "scripts": { - "start": "node server.js", - "dev": "node --watch server.js" + "dev": "yarn development", + "development": "mix", + "predevelopment": "php artisan lang:generate -vvv", + "watch": "mix watch", + "prewatch": "php artisan lang:generate -vvv", + "watch-poll": "mix watch -- --watch-options-poll=1000", + "hot": "mix watch --hot", + "prod": "yarn production", + "production": "mix --production", + "preproduction": "php artisan lang:generate -vvv", + "heroku-postbuild": "yarn run production", + "e2e": "cypress run", + "e2e:chrome": "cypress run --browser chrome", + "e2e:record": "cypress run --record", + "e2e:record:parallel": "cypress run --record --parallel", + "e2e-gui": "cypress open", + "cy:verify": "cypress verify", + "cy:version": "cypress version", + "inst": "yarn install --frozen-lockfile", + "lint": "eslint --ext .js,.vue *.js .*.js resources/js/", + "lint:cypress": "eslint --ext .js tests/cypress/", + "lint:all": "yarn run lint & yarn run lint:cypress", + "lint:fix": "yarn run lint --fix & yarn run lint:cypress --fix", + "snyk-protect": "snyk-protect", + "prepublish": "yarn run snyk-protect", + "delete:reports": "rm results/cypress/* || true", + "pree2e": "yarn run delete:reports", + "migrate": "DB_CONNECTION=testing php artisan migrate:fresh && DB_CONNECTION=testing php artisan db:seed", + "pretest": "yarn run migrate", + "test": "vendor/bin/phpunit", + "posttest": "vendor/bin/phpstan analyse && vendor/bin/psalm", + "composer update": "COMPOSER_MEMORY_LIMIT=-1 composer update" + }, + "engines": { + "node": "20.x", + "yarn": "1.22.x" + }, + "devDependencies": { + "@snyk/protect": "^1.1034.0", + "cross-env": "^7.0", + "cypress": "^7.2.0", + "eslint": "^7.10", + "eslint-config-standard": "^16.0", + "eslint-plugin-cypress": ">=2.11.2", + "eslint-plugin-import": ">=2.22.1", + "eslint-plugin-node": ">=11.1.0", + "eslint-plugin-promise": ">=4.0.0", + "eslint-plugin-standard": ">=4.0.0", + "eslint-plugin-vue": "^7.0", + "faker": "^5.1", + "mocha": "^9.1.2", + "mocha-junit-reporter": "^2.0.2", + "mocha-multi-reporters": "^1.1", + "moment-locales-webpack-plugin": "^1.2", + "postcss": "^8.2.13", + "resolve-url-loader": "^4.0.0", + "sass-loader": "^11.0", + "vue-template-compiler": "^2.6" }, - "keywords": [ - "crm", - "personal-crm", - "relationship-manager", - "self-hosted", - "sqlite" - ], - "author": "Mischlabs", - "license": "MIT", "dependencies": { - "express": "^4.19.2", - "sqlite3": "^5.1.7" - } + "@hokify/vuejs-datepicker": "^2.0", + "animate.css": "^4.1", + "axios": "^0.21", + "bootstrap": "^4.6", + "font-awesome": "^4.7", + "hint.css": "^2.3", + "jquery": "^3.6", + "laravel-mix": "^6.0", + "laravel-mix-purgecss": "^6.0", + "list.js": "^2.3", + "lodash": "^4.17", + "marked": "^2.0", + "moment": "^2.26", + "moment-timezone": "^0.5", + "popper.js": "^1.16", + "pretty-checkbox-vue": "^1.1", + "rx-js": "^0.0.0", + "sass": "^1.32", + "sweet-modal-vue": "^2.0", + "tachyons": "^4.12", + "vue": "^2.6", + "vue-autosuggest": "^2.2", + "vue-checkbox-radio": "^0.6", + "vue-clipboard2": "^0.3", + "vue-directive-tooltip": "^1.6", + "vue-good-table": "^2.21", + "vue-i18n": "^8.24", + "vue-js-toggle-button": "^1.3", + "vue-loader": "^15.9", + "vue-notification": "^1.3", + "vue-rx": "^6.2", + "vue-select": "^3.11", + "vuejs-clipper": "4.0.0", + "vuelidate": "^0.7" + }, + "snyk": true } diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..2a5d059 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,50 @@ +includes: + - ./vendor/nunomaduro/larastan/extension.neon + - ./vendor/thecodingmachine/phpstan-safe-rule/phpstan-safe-rule.neon + +parameters: + paths: + - app + inferPrivatePropertyTypeFromConstructor: true + checkMissingIterableValueType: false + level: 5 + ignoreErrors: + - '#Access to an undefined property Sabre\\VObject\\Component\\[a-zA-Z0-9\\_]+::\$[a-zA-Z0-9_]+\.#' + - '#Unsafe call to private method .* through static::\.#' + - '#Unsafe access to private property .* through static::\.#' + + - message: '#Access to an undefined property Illuminate\\Support\\Fluent::\$[a-zA-Z0-9_]+\.#' + path: */Http/Location/Drivers/CloudflareDriver.php + - message: '#Access to an undefined property App\\Interfaces\\IsJournalableInterface::\$account_id\.#' + path: */app/Models/Journal/JournalEntry.php + - message: '#Property App\\Models\\Contact\\Contact::\$deceased_special_date_id \(int\) does not accept null\.#' + path: */Services/Contact/Contact/UpdateDeceasedInformation.php + - message: '#Parameter \#1 \$principalUri of method Sabre\\CardDAV\\Backend\\BackendInterface::getAddressBooksForUser\(\) expects string, array given\.#' + path: */Http/Controllers/DAV/Backend/CardDAV/AddressBookHome.php + - message: '#Property App\\Models\\Contact\\Contact::\$avatar_photo_id \(int\) does not accept null\.#' + path: */Services/Contact/Avatar/UpdateAvatar.php + - message: '#Call to an undefined method Illuminate\\Database\\Eloquent\\Builder::addressBook\(\)\.#' + path: */Helpers/SearchHelper.php + - message: '#Call to an undefined method Illuminate\\Database\\Eloquent\\Builder::real\(\)\.#' + path: */Http/Controllers/Api/ApiContactController.php + - message: '#Call to an undefined method Illuminate\\Database\\Eloquent\\Builder::sortedBy\(\)\.#' + path: */Traits/Searchable.php + - message: '#Access to an undefined property Faker\\Generator::\$state\.#' + path: */Console/Commands/SetupTest.php + - message: '#Access to an undefined property Stripe\\Subscription::\$plan\.#' + path: */Helpers/InstanceHelper.php + - message: '#Call to an undefined method Illuminate\\Database\\Eloquent\\Relations\\HasMany::recurring\(\)\.#' + path: */Traits/Subscription.php + - message: '#Function dns_get_record is unsafe to use\. It can return FALSE instead of throwing an exception\. Please add ''use function Safe\\dns_get_record;'' at the beginning of the file to use the variant provided by the ''thecodingmachine/safe'' library\.#' + path: */Services/DavClient/Utils/Dav/ServiceUrlQuery.php + - message: '#Access to an undefined property App\\Models\\Relationship\\Relationship::\$relationshipTypeLocalized\.#' + path: */Http/Controllers/ContactsController.php + - message: '#Call to an undefined method Traversable::filter\(\)\.#' + path: */Models/Contact/Contact.php + + excludePaths: + - */Http/Resources/**/*.php + - */ExportResources/**/*.php + - */ExportResources/*.php + - */Console/Commands/ImportAccounts.php + diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..f34d3b0 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,77 @@ + + + + + ./app + + + ./app/Http/routes.php + + + + + + ./tests/Api + + + + ./tests/Feature + + + + ./tests/Commands/OneTime + ./tests/Commands/Other + + + + ./tests/Commands/Scheduling + ./tests/Commands/Tests + + + + ./tests/Unit/Controllers + ./tests/Unit/Events + ./tests/Unit/Helpers + ./tests/Unit/Models + + + + ./tests/Unit/Services + ./tests/Unit/Traits + ./tests/Unit/Jobs + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 0000000..6ed0689 --- /dev/null +++ b/psalm.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + diff --git a/public/.htaccess b/public/.htaccess new file mode 100644 index 0000000..631d10e --- /dev/null +++ b/public/.htaccess @@ -0,0 +1,35 @@ + + + Options -MultiViews -Indexes + + + RewriteEngine On + + # Handle Authorization Header + RewriteCond %{HTTP:Authorization} . + RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] + + # Redirect .well-known urls (https://en.wikipedia.org/wiki/List_of_/.well-known/_services_offered_by_webservers) + RewriteCond %{REQUEST_URI} .well-known/carddav + RewriteRule ^ /dav/ [L,R=301] + + RewriteCond %{REQUEST_URI} .well-known/caldav + RewriteRule ^ /dav/ [L,R=301] + + RewriteCond %{REQUEST_URI} .well-known/security.txt + RewriteRule ^ /security.txt [L,R=301] + # old carddav url + RewriteCond %{REQUEST_URI} /carddav/(.+) + RewriteRule ^ /dav/%1 [L,R=301] + + # Redirect Trailing Slashes If Not A Folder... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !dav/* + RewriteCond %{REQUEST_URI} (.+)/$ + RewriteRule ^ %1 [L,R=301] + + # Handle Front Controller... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^ index.php [L] + diff --git a/public/app.js b/public/app.js deleted file mode 100644 index bd0aa61..0000000 --- a/public/app.js +++ /dev/null @@ -1,1132 +0,0 @@ -document.addEventListener('DOMContentLoaded', () => { - // --- APPLICATION STATE --- - let state = { - friends: [], - meetings: [], - selectedFriend: null, - activeTab: 'dashboard' - }; - - // --- SELECT DOM ELEMENTS --- - const DOM = { - // Navigation & Tabs - navItems: document.querySelectorAll('.nav-item'), - tabPanes: document.querySelectorAll('.tab-pane'), - pageTitle: document.getElementById('page-title'), - - // Dashboard Stats - statTotalFriends: document.getElementById('stat-total-friends'), - statMeetingsMonth: document.getElementById('stat-meetings-month'), - statAvgHonor: document.getElementById('stat-avg-honor'), - statUpcomingBirthdays: document.getElementById('stat-upcoming-birthdays'), - - // Dashboard Panels - urgentContactsList: document.getElementById('urgent-contacts-list'), - upcomingBirthdaysList: document.getElementById('upcoming-birthdays-list'), - quickLogForm: document.getElementById('quick-log-form'), - quickFriendSelect: document.getElementById('quick-friend-select'), - quickDate: document.getElementById('quick-date'), - quickActivity: document.getElementById('quick-activity'), - quickMood: document.getElementById('quick-mood'), - quickDetails: document.getElementById('quick-details'), - - // Friends Directory - friendsSearch: document.getElementById('friends-search'), - btnOpenAddModal: document.getElementById('btn-add-friend-modal'), - friendsGrid: document.getElementById('friends-grid'), - - // Add Friend Modal - addFriendModal: document.getElementById('add-friend-modal'), - addFriendForm: document.getElementById('add-friend-form'), - btnCloseAddModal: document.getElementById('btn-close-add-modal'), - btnCancelAddModal: document.getElementById('btn-cancel-add-modal'), - - // Friend Detail Modal - detailModal: document.getElementById('friend-detail-modal'), - btnCloseDetailModal: document.getElementById('btn-close-detail-modal'), - btnDeleteFriend: document.getElementById('btn-delete-friend'), - btnEditFriendTrigger: document.getElementById('btn-edit-friend-trigger'), - detailAvatar: document.getElementById('detail-avatar'), - detailName: document.getElementById('detail-name'), - detailRelationshipBadge: document.getElementById('detail-badge-relationship'), - detailHonorValue: document.getElementById('detail-honor-value'), - btnHonorPlus: document.getElementById('btn-honor-plus'), - btnHonorMinus: document.getElementById('btn-honor-minus'), - detailBirthday: document.getElementById('detail-birthday'), - detailContact: document.getElementById('detail-contact'), - detailAddress: document.getElementById('detail-address'), - detailFamily: document.getElementById('detail-family'), - detailLife: document.getElementById('detail-life'), - detailHobbies: document.getElementById('detail-hobbies'), - detailMilestones: document.getElementById('detail-milestones'), - detailFood: document.getElementById('detail-food'), - detailNotes: document.getElementById('detail-notes'), - detailTopicsList: document.getElementById('detail-topics-list'), - detailMeetingsTimeline: document.getElementById('detail-meetings-timeline'), - addTopicForm: document.getElementById('add-topic-form'), - newTopicInput: document.getElementById('new-topic-input'), - profileLogMeetingForm: document.getElementById('profile-log-meeting-form'), - profileMeetingDate: document.getElementById('profile-meeting-date'), - profileMeetingMood: document.getElementById('profile-meeting-mood'), - profileMeetingActivity: document.getElementById('profile-meeting-activity'), - profileMeetingDetails: document.getElementById('profile-meeting-details'), - - // Edit Friend Modal - editFriendModal: document.getElementById('edit-friend-modal'), - editFriendForm: document.getElementById('edit-friend-form'), - btnCloseEditModal: document.getElementById('btn-close-edit-modal'), - btnCancelEditModal: document.getElementById('btn-cancel-edit-modal'), - editId: document.getElementById('edit-id'), - editName: document.getElementById('edit-name'), - editBirthday: document.getElementById('edit-birthday'), - editContact: document.getElementById('edit-contact'), - editRelationship: document.getElementById('edit-relationship'), - editFamily: document.getElementById('edit-family'), - editAddress: document.getElementById('edit-address'), - editHonor: document.getElementById('edit-honor'), - editJob: document.getElementById('edit-job'), - editLife: document.getElementById('edit-life'), - editHobbies: document.getElementById('edit-hobbies'), - editMilestones: document.getElementById('edit-milestones'), - editFood: document.getElementById('edit-food'), - editNotes: document.getElementById('edit-notes'), - - // Obsidian Import Tab - obsidianImportForm: document.getElementById('obsidian-import-form'), - importFilename: document.getElementById('import-filename'), - importContent: document.getElementById('import-content'), - importResult: document.getElementById('import-result') - }; - - // --- INITIALIZATION --- - async function init() { - setupTabNavigation(); - setupEventListeners(); - setFormDefaultDates(); - - // Initial fetch of data - await refreshAllData(); - } - - // --- DATA FETCHING & SYNC --- - async function refreshAllData() { - showGlobalLoaders(); - await Promise.all([ - fetchFriends(), - fetchMeetings() - ]); - renderDashboard(); - renderFriendsDirectory(); - - // If a friend modal is open, refresh it as well - if (state.selectedFriend) { - await refreshFriendDetails(state.selectedFriend.id); - } - } - - async function fetchFriends() { - try { - const res = await fetch('/api/friends'); - if (!res.ok) throw new Error('Fehler beim Laden der Freunde'); - state.friends = await res.json(); - } catch (err) { - console.error(err); - alert('Konnte Freunde nicht laden: ' + err.message); - } - } - - async function fetchMeetings() { - try { - const res = await fetch('/api/meetings'); - if (!res.ok) throw new Error('Fehler beim Laden der Treffen'); - state.meetings = await res.json(); - } catch (err) { - console.error(err); - // We fall back to empty meetings list gracefully if route doesn't work yet - state.meetings = []; - } - } - - async function refreshFriendDetails(friendId) { - try { - const res = await fetch(`/api/friends/${friendId}`); - if (!res.ok) throw new Error('Konnte Details nicht laden'); - state.selectedFriend = await res.json(); - populateFriendDetails(state.selectedFriend); - } catch (err) { - console.error(err); - alert('Fehler beim Aktualisieren der Details: ' + err.message); - } - } - - // --- SPA ROUTING / TAB NAVIGATION --- - function setupTabNavigation() { - // URL Hash handling - window.addEventListener('hashchange', handleHashRoute); - - // Sidebar clicks - DOM.navItems.forEach(item => { - item.addEventListener('click', (e) => { - e.preventDefault(); - const tab = item.getAttribute('data-tab'); - window.location.hash = tab; - }); - }); - - // Handle initial load route - handleHashRoute(); - } - - function handleHashRoute() { - let tab = window.location.hash.replace('#', '') || 'dashboard'; - - // Validate tab - const validTabs = ['dashboard', 'friends', 'import']; - if (!validTabs.includes(tab)) tab = 'dashboard'; - - state.activeTab = tab; - - // Toggle nav items - DOM.navItems.forEach(item => { - if (item.getAttribute('data-tab') === tab) { - item.classList.add('active'); - } else { - item.classList.remove('active'); - } - }); - - // Toggle panes - DOM.tabPanes.forEach(pane => { - if (pane.id === `tab-${tab}`) { - pane.classList.add('active'); - } else { - pane.classList.remove('active'); - } - }); - - // Update Header Title - const titles = { - dashboard: 'Dashboard', - friends: 'Freunde-Verzeichnis', - import: 'Obsidian Markdown Import' - }; - DOM.pageTitle.textContent = titles[tab] || 'Dashboard'; - } - - // --- EVENT LISTENERS --- - function setupEventListeners() { - // --- Modals Toggle --- - DOM.btnOpenAddModal.addEventListener('click', () => openModal(DOM.addFriendModal)); - DOM.btnCloseAddModal.addEventListener('click', () => closeModal(DOM.addFriendModal)); - DOM.btnCancelAddModal.addEventListener('click', () => closeModal(DOM.addFriendModal)); - - DOM.btnCloseDetailModal.addEventListener('click', () => { - closeModal(DOM.detailModal); - state.selectedFriend = null; - }); - - DOM.btnCloseEditModal.addEventListener('click', () => closeModal(DOM.editFriendModal)); - DOM.btnCancelEditModal.addEventListener('click', () => closeModal(DOM.editFriendModal)); - - // Close modals on clicking outside container - window.addEventListener('click', (e) => { - if (e.target.classList.contains('modal-backdrop')) { - closeModal(e.target); - if (e.target.id === 'friend-detail-modal') { - state.selectedFriend = null; - } - } - }); - - // --- Search Filter --- - DOM.friendsSearch.addEventListener('input', (e) => { - renderFriendsDirectory(e.target.value); - }); - - // --- Add Friend Form Submit --- - DOM.addFriendForm.addEventListener('submit', async (e) => { - e.preventDefault(); - - const friendData = { - name: document.getElementById('add-name').value.trim(), - birthday: document.getElementById('add-birthday').value, - contact: document.getElementById('add-contact').value.trim(), - relationship_status: document.getElementById('add-relationship').value.trim(), - family: document.getElementById('add-family').value.trim(), - address: document.getElementById('add-address').value.trim(), - honor: parseInt(document.getElementById('add-honor').value, 10) || 0, - job: document.getElementById('add-job').value.trim(), - life_situation: document.getElementById('add-life').value.trim(), - hobbies: document.getElementById('add-hobbies').value.trim(), - milestones: document.getElementById('add-milestones').value.trim(), - food_preferences: document.getElementById('add-food').value.trim(), - random_notes: document.getElementById('add-notes').value.trim() - }; - - try { - const res = await fetch('/api/friends', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(friendData) - }); - - if (!res.ok) throw new Error('Fehler beim Anlegen'); - - DOM.addFriendForm.reset(); - setFormDefaultDates(); - closeModal(DOM.addFriendModal); - await refreshAllData(); - } catch (err) { - alert('Konnte Freund nicht speichern: ' + err.message); - } - }); - - // --- Edit Friend Form Submit --- - DOM.editFriendForm.addEventListener('submit', async (e) => { - e.preventDefault(); - - const friendId = DOM.editId.value; - const friendData = { - name: DOM.editName.value.trim(), - birthday: DOM.editBirthday.value, - contact: DOM.editContact.value.trim(), - relationship_status: DOM.editRelationship.value.trim(), - family: DOM.editFamily.value.trim(), - address: DOM.editAddress.value.trim(), - honor: parseInt(DOM.editHonor.value, 10) || 0, - job: DOM.editJob.value.trim(), - life_situation: DOM.editLife.value.trim(), - hobbies: DOM.editHobbies.value.trim(), - milestones: DOM.editMilestones.value.trim(), - food_preferences: DOM.editFood.value.trim(), - random_notes: DOM.editNotes.value.trim() - }; - - try { - const res = await fetch(`/api/friends/${friendId}`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(friendData) - }); - - if (!res.ok) throw new Error('Fehler beim Aktualisieren'); - - closeModal(DOM.editFriendModal); - await refreshAllData(); - if (state.selectedFriend) { - await refreshFriendDetails(friendId); - } - } catch (err) { - alert('Konnte Profildaten nicht aktualisieren: ' + err.message); - } - }); - - // --- Delete Friend Trigger --- - DOM.btnDeleteFriend.addEventListener('click', async () => { - if (!state.selectedFriend) return; - - const confirmDelete = confirm(`Bist du sicher, dass du ${state.selectedFriend.name} aus dem CRM löschen willst? Alle Treffen und Notizen gehen verloren.`); - if (!confirmDelete) return; - - try { - const res = await fetch(`/api/friends/${state.selectedFriend.id}`, { - method: 'DELETE' - }); - - if (!res.ok) throw new Error('Löschen fehlgeschlagen'); - - closeModal(DOM.detailModal); - state.selectedFriend = null; - await refreshAllData(); - } catch (err) { - alert('Fehler beim Löschen: ' + err.message); - } - }); - - // --- Edit Modal Prefill & Trigger --- - DOM.btnEditFriendTrigger.addEventListener('click', () => { - if (!state.selectedFriend) return; - const f = state.selectedFriend; - - DOM.editId.value = f.id; - DOM.editName.value = f.name || ''; - DOM.editBirthday.value = f.birthday || ''; - DOM.editContact.value = f.contact || ''; - DOM.editRelationship.value = f.relationship_status || ''; - DOM.editFamily.value = f.family || ''; - DOM.editAddress.value = f.address || ''; - DOM.editHonor.value = f.honor || 0; - DOM.editJob.value = f.job || ''; - DOM.editLife.value = f.life_situation || ''; - DOM.editHobbies.value = f.hobbies || ''; - DOM.editMilestones.value = f.milestones || ''; - DOM.editFood.value = f.food_preferences || ''; - DOM.editNotes.value = f.random_notes || ''; - - openModal(DOM.editFriendModal); - }); - - // --- Honor Score +/- Clickers --- - DOM.btnHonorPlus.addEventListener('click', () => adjustHonor(1)); - DOM.btnHonorMinus.addEventListener('click', () => adjustHonor(-1)); - - // --- Log Meeting Form (Dashboard) Submit --- - DOM.quickLogForm.addEventListener('submit', async (e) => { - e.preventDefault(); - - const meetingData = { - friend_id: parseInt(DOM.quickFriendSelect.value, 10), - date: DOM.quickDate.value, - activity: DOM.quickActivity.value.trim(), - mood: DOM.quickMood.value.trim(), - details: DOM.quickDetails.value.trim() - }; - - try { - const res = await fetch('/api/meetings', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(meetingData) - }); - - if (!res.ok) throw new Error('Eintrag fehlgeschlagen'); - - DOM.quickLogForm.reset(); - setFormDefaultDates(); - await refreshAllData(); - } catch (err) { - alert('Konnte Treffen nicht eintragen: ' + err.message); - } - }); - - // --- Log Meeting Form (Profile Modal) Submit --- - DOM.profileLogMeetingForm.addEventListener('submit', async (e) => { - e.preventDefault(); - if (!state.selectedFriend) return; - - const meetingData = { - friend_id: state.selectedFriend.id, - date: DOM.profileMeetingDate.value, - activity: DOM.profileMeetingActivity.value.trim(), - mood: DOM.profileMeetingMood.value.trim(), - details: DOM.profileMeetingDetails.value.trim() - }; - - try { - const res = await fetch('/api/meetings', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(meetingData) - }); - - if (!res.ok) throw new Error('Eintrag fehlgeschlagen'); - - DOM.profileLogMeetingForm.reset(); - setFormDefaultDates(); - await refreshFriendDetails(state.selectedFriend.id); - await refreshAllData(); - } catch (err) { - alert('Konnte Treffen nicht eintragen: ' + err.message); - } - }); - - // --- Add Topic Form Submit --- - DOM.addTopicForm.addEventListener('submit', async (e) => { - e.preventDefault(); - if (!state.selectedFriend) return; - - const topicData = { - friend_id: state.selectedFriend.id, - topic: DOM.newTopicInput.value.trim() - }; - - try { - const res = await fetch('/api/topics', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(topicData) - }); - - if (!res.ok) throw new Error('Hinzufügen fehlgeschlagen'); - - DOM.newTopicInput.value = ''; - await refreshFriendDetails(state.selectedFriend.id); - await refreshAllData(); - } catch (err) { - alert('Konnte Thema nicht hinzufügen: ' + err.message); - } - }); - - // --- Obsidian Import Submit --- - DOM.obsidianImportForm.addEventListener('submit', async (e) => { - e.preventDefault(); - - const importData = { - filename: DOM.importFilename.value.trim() || 'Imported_Obsidian_Profile.md', - content: DOM.importContent.value - }; - - try { - DOM.importResult.classList.remove('hidden'); - DOM.importResult.innerHTML = '

    Analysiere Markdown-Struktur...

    '; - - const res = await fetch('/api/import-obsidian', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(importData) - }); - - const data = await res.json(); - if (!res.ok) throw new Error(data.error || 'Import fehlgeschlagen'); - - DOM.importResult.innerHTML = ` -
    - -
    -

    ${data.name} erfolgreich importiert!

    -

    ${data.message}

    - -
    -
    - `; - - lucide.createIcons(); - DOM.obsidianImportForm.reset(); - - // Bind quick view button - document.getElementById('btn-view-imported').addEventListener('click', (e) => { - const friendId = e.currentTarget.getAttribute('data-id'); - openFriendDetail(friendId); - }); - - await refreshAllData(); - } catch (err) { - DOM.importResult.innerHTML = ` -
    - -
    -

    Fehler beim Importieren

    -

    ${err.message}

    -
    -
    - `; - lucide.createIcons(); - } - }); - } - - // --- DYNAMIC RENDERING: DASHBOARD --- - function renderDashboard() { - const friends = state.friends; - const meetings = state.meetings; - - // 1. STATS CALCULATIONS - // Stat: Total Friends - DOM.statTotalFriends.textContent = friends.length; - - // Stat: Meetings this month - const today = new Date(); - const currentYear = today.getFullYear(); - const currentMonth = today.getMonth(); // 0-indexed - - const meetingsThisMonth = meetings.filter(m => { - if (!m.date) return false; - const mDate = new Date(m.date); - return mDate.getFullYear() === currentYear && mDate.getMonth() === currentMonth; - }).length; - DOM.statMeetingsMonth.textContent = meetingsThisMonth; - - // Stat: Average Honor - const totalHonor = friends.reduce((sum, f) => sum + (f.honor || 0), 0); - const avgHonor = friends.length > 0 ? Math.round(totalHonor / friends.length) : 0; - DOM.statAvgHonor.textContent = avgHonor; - - // Stat: Upcoming Birthdays count - const upcomingBdays = friends.filter(f => { - if (!f.birthday) return false; - const days = getDaysUntilBirthday(f.birthday); - return days !== null && days >= 0 && days <= 30; - }); - DOM.statUpcomingBirthdays.textContent = upcomingBdays.length; - - // 2. RENDER URGENT CONTACTS ("Lange nicht gesehen") - DOM.urgentContactsList.innerHTML = ''; - if (friends.length === 0) { - DOM.urgentContactsList.innerHTML = '

    Noch keine Freunde eingetragen.

    '; - } else { - // Sort friends: those never met first, then those met longest ago - const sortedUrgent = [...friends].sort((a, b) => { - if (!a.last_meeting_date && !b.last_meeting_date) return a.name.localeCompare(b.name); - if (!a.last_meeting_date) return -1; // a first - if (!b.last_meeting_date) return 1; // b first - return new Date(a.last_meeting_date) - new Date(b.last_meeting_date); - }); - - // Show top 5 urgent contacts - sortedUrgent.slice(0, 5).forEach(f => { - const item = document.createElement('div'); - item.className = 'list-item-glass'; - - let subText = ''; - let warningClass = ''; - - if (!f.last_meeting_date) { - subText = 'Noch nie getroffen'; - warningClass = 'urgent'; - } else { - const days = getDaysSince(f.last_meeting_date); - subText = `Zuletzt vor ${days} Tagen getroffen (${formatGermanDate(f.last_meeting_date)})`; - if (days > 60) warningClass = 'urgent'; - else if (days > 30) warningClass = 'warning'; - } - - const initials = getInitials(f.name); - - item.innerHTML = ` -
    -
    ${initials}
    -
    - ${f.name} - ${subText} -
    -
    - - `; - DOM.urgentContactsList.appendChild(item); - }); - - // Bind click triggers - DOM.urgentContactsList.querySelectorAll('.btn-view-profile').forEach(btn => { - btn.addEventListener('click', (e) => { - const id = e.currentTarget.getAttribute('data-id'); - openFriendDetail(id); - }); - }); - } - - // 3. RENDER UPCOMING BIRTHDAYS PANEL - DOM.upcomingBirthdaysList.innerHTML = ''; - if (upcomingBdays.length === 0) { - DOM.upcomingBirthdaysList.innerHTML = '

    Keine Geburtstage in den nächsten 30 Tagen.

    '; - } else { - // Sort upcoming by closest day - upcomingBdays.sort((a, b) => getDaysUntilBirthday(a.birthday) - getDaysUntilBirthday(b.birthday)); - - upcomingBdays.forEach(f => { - const days = getDaysUntilBirthday(f.birthday); - const age = getAgeTurning(f.birthday); - const item = document.createElement('div'); - item.className = 'list-item-glass'; - - let dayString = ''; - if (days === 0) { - dayString = 'Heute! 🎉'; - } else if (days === 1) { - dayString = 'Morgen!'; - } else { - dayString = `in ${days} Tagen`; - } - - const initials = getInitials(f.name); - const cleanBday = formatGermanDate(f.birthday, false); // format without year or with dots - - item.innerHTML = ` -
    -
    -
    - ${f.name} - ${cleanBday} • wird ${age} (${dayString}) -
    -
    - - `; - DOM.upcomingBirthdaysList.appendChild(item); - }); - - // Bind click triggers - DOM.upcomingBirthdaysList.querySelectorAll('.btn-view-profile').forEach(btn => { - btn.addEventListener('click', (e) => { - const id = e.currentTarget.getAttribute('data-id'); - openFriendDetail(id); - }); - }); - } - - // 4. POPULATE QUICK LOG FRIEND SELECT - DOM.quickFriendSelect.innerHTML = ''; - friends.forEach(f => { - const opt = document.createElement('option'); - opt.value = f.id; - opt.textContent = f.name; - DOM.quickFriendSelect.appendChild(opt); - }); - - // Initialize Lucide Icons - lucide.createIcons(); - } - - // --- DYNAMIC RENDERING: FRIENDS DIRECTORY --- - function renderFriendsDirectory(searchQuery = '') { - const grid = DOM.friendsGrid; - grid.innerHTML = ''; - - const query = searchQuery.trim().toLowerCase(); - const filtered = state.friends.filter(f => { - if (!query) return true; - return (f.name || '').toLowerCase().includes(query) || - (f.address || '').toLowerCase().includes(query) || - (f.hobbies || '').toLowerCase().includes(query) || - (f.job || '').toLowerCase().includes(query); - }); - - if (filtered.length === 0) { - grid.innerHTML = ` -
    - -

    Keine Freunde gefunden

    -

    Passe deinen Suchbegriff an oder füge einen neuen Freund hinzu.

    -
    - `; - lucide.createIcons(); - return; - } - - filtered.forEach(f => { - const card = document.createElement('div'); - card.className = 'friend-card glass-panel'; - card.setAttribute('data-id', f.id); - - const initials = getInitials(f.name); - - // Residential town extraction (last line of address or simply cut address) - let town = 'Unbekannt'; - if (f.address) { - const addressLines = f.address.split('\n'); - town = addressLines[addressLines.length - 1].trim(); - } - - // Format last meeting subtext - let lastMeetSub = 'Noch nie getroffen'; - if (f.last_meeting_date) { - lastMeetSub = `Zuletzt: ${formatGermanDate(f.last_meeting_date)}`; - } - - card.innerHTML = ` -
    -
    ${initials}
    -
    -

    ${f.name}

    - ${town} -
    -
    - -
    -
    - Ehre: ${f.honor || 0} - ${f.birthday ? ` ${formatGermanDate(f.birthday, false)}` : ''} -
    -

    ${lastMeetSub}

    -
    - - - `; - - // Event Listeners for Card Items - card.querySelector('.btn-view-details').addEventListener('click', () => openFriendDetail(f.id)); - - // Card Honor +/- button handlers - card.querySelectorAll('.btn-honor-card').forEach(btn => { - btn.addEventListener('click', async (e) => { - e.stopPropagation(); - const action = btn.getAttribute('data-action'); - const change = action === 'plus' ? 1 : -1; - const valDisplay = card.querySelector('.honor-card-val'); - - try { - const res = await fetch(`/api/friends/${f.id}/honor`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ change }) - }); - const data = await res.json(); - - if (res.ok) { - // Update state locally - const friendIndex = state.friends.findIndex(x => x.id === f.id); - if (friendIndex !== -1) { - state.friends[friendIndex].honor = data.honor; - } - - // Direct UI bump animation - valDisplay.textContent = data.honor; - valDisplay.classList.add('bump-animate'); - setTimeout(() => valDisplay.classList.remove('bump-animate'), 300); - - // Refresh dashboard in background without full render flashes - renderDashboard(); - } - } catch (err) { - console.error(err); - } - }); - }); - - grid.appendChild(card); - }); - - lucide.createIcons(); - } - - // --- DYNAMIC RENDERING: FRIEND PROFILE DETAIL --- - async function openFriendDetail(friendId) { - showModal(DOM.detailModal); - await refreshFriendDetails(friendId); - } - - function populateFriendDetails(friend) { - DOM.detailAvatar.textContent = getInitials(friend.name); - DOM.detailName.textContent = friend.name; - - // Relationship badge - DOM.detailRelationshipBadge.textContent = friend.relationship_status || 'Kein Beziehungsstatus'; - if (friend.relationship_status && friend.relationship_status.toUpperCase().includes('ALONE')) { - DOM.detailRelationshipBadge.className = 'relationship-badge forever-alone'; - } else { - DOM.detailRelationshipBadge.className = 'relationship-badge'; - } - - DOM.detailHonorValue.textContent = friend.honor || 0; - - // Formatting General details - let bdayText = '-'; - if (friend.birthday) { - const days = getDaysUntilBirthday(friend.birthday); - const age = getAgeTurning(friend.birthday); - let daysRemainingStr = ''; - if (days === 0) daysRemainingStr = ' (Heute! 🎉)'; - else if (days === 1) daysRemainingStr = ' (Morgen!)'; - else daysRemainingStr = ` (in ${days} Tagen, wird ${age})`; - bdayText = `${formatGermanDate(friend.birthday)} ${daysRemainingStr}`; - } - DOM.detailBirthday.textContent = bdayText; - DOM.detailContact.textContent = friend.contact || '-'; - DOM.detailAddress.textContent = friend.address || '-'; - DOM.detailFamily.textContent = friend.family || '-'; - - // Job, Hobbies, Milestones - DOM.detailLife.innerHTML = formatMarkdownParagraphs(friend.life_situation || '-'); - - let hobbiesHtml = '-'; - if (friend.hobbies) { - hobbiesHtml = friend.hobbies.split(',').map(h => `${h.trim()}`).join(' '); - } else if (friend.job) { - hobbiesHtml = `${friend.job}`; - } - DOM.detailHobbies.innerHTML = hobbiesHtml; - DOM.detailMilestones.innerHTML = formatMarkdownParagraphs(friend.milestones || '-'); - - // Food & Random Notes - DOM.detailFood.textContent = friend.food_preferences || '-'; - DOM.detailNotes.innerHTML = formatMarkdownParagraphs(friend.random_notes || '-'); - - // RENDER TOPICS CHECKLIST - renderTopics(friend.topics || []); - - // RENDER MEETINGS TIMELINE - renderTimeline(friend.meetings || []); - - lucide.createIcons(); - } - - function renderTopics(topics) { - const list = DOM.detailTopicsList; - list.innerHTML = ''; - - if (topics.length === 0) { - list.innerHTML = '
  11. Keine offenen Gesprächsthemen.
  12. '; - return; - } - - topics.forEach(t => { - const li = document.createElement('li'); - li.className = `topic-item ${t.completed ? 'completed' : ''}`; - - li.innerHTML = ` - - - `; - - // Checkbox toggle logic - li.querySelector('.topic-checkbox').addEventListener('change', async (e) => { - const checked = e.target.checked; - try { - const res = await fetch(`/api/topics/${t.id}`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ completed: checked ? 1 : 0 }) - }); - if (res.ok) { - li.classList.toggle('completed', checked); - await refreshAllData(); - } - } catch (err) { - console.error(err); - } - }); - - // Delete topic logic - li.querySelector('.btn-delete-topic').addEventListener('click', async () => { - try { - const res = await fetch(`/api/topics/${t.id}`, { method: 'DELETE' }); - if (res.ok) { - li.remove(); - await refreshFriendDetails(state.selectedFriend.id); - await refreshAllData(); - } - } catch (err) { - console.error(err); - } - }); - - list.appendChild(li); - }); - - lucide.createIcons(); - } - - function renderTimeline(meetings) { - const timeline = DOM.detailMeetingsTimeline; - timeline.innerHTML = ''; - - if (meetings.length === 0) { - timeline.innerHTML = '

    Noch keine Treffen dokumentiert.

    '; - return; - } - - meetings.forEach(m => { - const item = document.createElement('div'); - item.className = 'timeline-item'; - - let moodBadge = ''; - if (m.mood) { - let moodClass = 'mood-default'; - const cleanedMood = m.mood.toLowerCase(); - if (cleanedMood.includes('legendär') || cleanedMood.includes('super') || cleanedMood.includes('genial')) { - moodClass = 'mood-excellent'; - } else if (cleanedMood.includes('entspannt') || cleanedMood.includes('gut') || cleanedMood.includes('zufrieden')) { - moodClass = 'mood-good'; - } - moodBadge = `${m.mood}`; - } - - item.innerHTML = ` -
    -
    -
    - ${formatGermanDate(m.date)} -
    - ${moodBadge} - -
    -
    -

    ${m.activity}

    - ${m.details ? `

    ${m.details.replace(/\n/g, '
    ')}

    ` : ''} -
    - `; - - // Delete meeting logic - item.querySelector('.btn-delete-meeting').addEventListener('click', async () => { - const confirmDel = confirm('Willst du diesen Treffen-Eintrag unwiderruflich löschen?'); - if (!confirmDel) return; - - try { - const res = await fetch(`/api/meetings/${m.id}`, { method: 'DELETE' }); - if (res.ok) { - item.remove(); - await refreshFriendDetails(state.selectedFriend.id); - await refreshAllData(); - } - } catch (err) { - console.error(err); - } - }); - - timeline.appendChild(item); - }); - - lucide.createIcons(); - } - - // --- EHRE COUNTER ADJUSTMENT --- - async function adjustHonor(change) { - if (!state.selectedFriend) return; - const f = state.selectedFriend; - const valDisplay = DOM.detailHonorValue; - - try { - const res = await fetch(`/api/friends/${f.id}/honor`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ change }) - }); - const data = await res.json(); - - if (res.ok) { - f.honor = data.honor; - valDisplay.textContent = data.honor; - - // Animate counter - valDisplay.classList.add('bump-animate'); - setTimeout(() => valDisplay.classList.remove('bump-animate'), 300); - - // Update main state and refresh background views - const fIndex = state.friends.findIndex(x => x.id === f.id); - if (fIndex !== -1) { - state.friends[fIndex].honor = data.honor; - } - - renderDashboard(); - renderFriendsDirectory(); - } - } catch (err) { - console.error(err); - } - } - - // --- GENERAL HELPER FUNCTIONS --- - - function openModal(modal) { - modal.classList.add('active'); - document.body.style.overflow = 'hidden'; // prevent bg scroll - } - - function closeModal(modal) { - modal.classList.remove('active'); - document.body.style.overflow = ''; - } - - function showModal(modal) { - openModal(modal); - } - - function setFormDefaultDates() { - const todayStr = new Date().toISOString().split('T')[0]; - DOM.quickDate.value = todayStr; - DOM.profileMeetingDate.value = todayStr; - } - - function showGlobalLoaders() { - DOM.urgentContactsList.innerHTML = '
    '; - DOM.upcomingBirthdaysList.innerHTML = '
    '; - DOM.friendsGrid.innerHTML = '
    '; - } - - function getInitials(name) { - if (!name) return '?'; - const parts = name.trim().split(/\s+/); - if (parts.length === 1) return parts[0].substring(0, 2).toUpperCase(); - return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase(); - } - - function getDaysSince(dateStr) { - const diff = new Date() - new Date(dateStr); - return Math.floor(diff / (1000 * 60 * 60 * 24)); - } - - function formatGermanDate(dateStr, includeYear = true) { - if (!dateStr) return ''; - const parts = dateStr.split('-'); - if (parts.length !== 3) return dateStr; - - const day = parts[2]; - const month = parts[1]; - const year = parts[0]; - - const months = ['Januar', 'Februar', 'März', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember']; - const monthIndex = parseInt(month, 10) - 1; - - if (includeYear) { - return `${day}. ${months[monthIndex]} ${year}`; - } else { - return `${day}. ${months[monthIndex].substring(0, 3)}`; - } - } - - function getDaysUntilBirthday(birthdayStr) { - if (!birthdayStr) return null; - const parts = birthdayStr.split('-'); - if (parts.length !== 3) return null; - const birthMonth = parseInt(parts[1], 10) - 1; - const birthDay = parseInt(parts[2], 10); - - const today = new Date(); - // Midnight check for perfect calculation - today.setHours(0,0,0,0); - - const currentYear = today.getFullYear(); - let nextBday = new Date(currentYear, birthMonth, birthDay); - nextBday.setHours(0,0,0,0); - - if (nextBday < today) { - nextBday.setFullYear(currentYear + 1); - } - - const oneDay = 24 * 60 * 60 * 1000; - const diffDays = Math.round((nextBday.getTime() - today.getTime()) / oneDay); - return diffDays; - } - - function getAgeTurning(birthdayStr) { - if (!birthdayStr) return null; - const parts = birthdayStr.split('-'); - if (parts.length !== 3) return null; - const birthYear = parseInt(parts[0], 10); - const birthMonth = parseInt(parts[1], 10) - 1; - const birthDay = parseInt(parts[2], 10); - - const today = new Date(); - const currentYear = today.getFullYear(); - - let nextBday = new Date(currentYear, birthMonth, birthDay); - if (nextBday < today) { - return currentYear + 1 - birthYear; - } - return currentYear - birthYear; - } - - function formatMarkdownParagraphs(text) { - if (!text) return ''; - return text - .split('\n\n') - .map(p => { - let cleaned = p.trim(); - if (!cleaned) return ''; - // Basic list items formatting if it starts with hyphen - if (cleaned.startsWith('-')) { - const listItems = cleaned.split('\n').map(li => `
  13. ${li.replace(/^-/, '').trim()}
  14. `).join(''); - return `
      ${listItems}
    `; - } - return `

    ${cleaned.replace(/\n/g, '
    ')}

    `; - }) - .join(''); - } - - // --- BOOTSTRAP APP --- - init(); -}); diff --git a/public/changelog.json b/public/changelog.json new file mode 100644 index 0000000..2dc8973 --- /dev/null +++ b/public/changelog.json @@ -0,0 +1,184 @@ +{ + "entries": [ + { + "date": "Jan 11, 2022", + "title": "Preview feature: export data as json", + "description": "We are experimenting with exporting data as json. [Try it now](/settings/export). This feature is in preview mode, please give us feedback on [this GitHub discussion](https://github.com/monicahq/monica/discussions/5824)." + }, + { + "date": "Aug 26, 2021", + "title": "New feature: change subscription frequency", + "description": "You can now change the frequency of your subscription: ![image](img/changelogs/2021-08-26-subscription.png) ![image](img/changelogs/2021-08-26-update.png)" + }, + { + "date": "May 11, 2020", + "title": "New feature: crop an avatar photo", + "description": "When uploading a new avatar for a contact, you are now invited to crop it to keep a square format.\n\n Choose a new file to upload, then select the zone to keep: ![image](img/changelogs/2020-05-11-crop-avatar-1.png) After validation, you can now save the new avatar: ![image](img/changelogs/2020-05-11-crop-avatar-2.png)" + }, + { + "date": "Mar 22, 2020", + "title": "New feature: define yourself as a contact", + "description": "In your Settings panel, you can now indicate who you are as a contact in the application." + }, + { + "date": "Jan 04, 2020", + "title": "New feature: associate a photo to a gift", + "description": "Adding a gift is now done inline, and you can associate a photo with it, to remember what you offered. ![image](img/changelogs/2020-01-04-gifts-photo.png)" + }, + { + "date": "Dec 22, 2019", + "title": "Enhancement: add emotions to activities", + "description": "Adding activities is now done inline, and you can add emotions and participants to an activity. ![image](img/changelogs/2019-03-22-new-activity.png)" + }, + { + "date": "Aug 17, 2019", + "title": "New feature: you can now change avatars", + "description": "You can now change the avatar of your contacts. Mouse over the profile picture and have fun. ![image](/img/changelogs/2019-08-17-avatars.png)" + }, + { + "date": "May 04, 2019", + "title": "New feature: WebAuthn two factor authentication", + "description": "WebAuthn is the new standard for strong authentication users, and is supported by every modern browsers. It is now available as a two factor authentication in Monica. Go to [security tab of settings](settings/security) to register a new key. ![image](img/changelogs/2019-05-04-webauthn.png)" + }, + { + "date": "May 02, 2019", + "title": "Enhancement: group relationships", + "description": "When creating or editing a relationship, relationships list is now grouped. ![image](img/changelogs/2019-05-02-group-relationships.png)" + }, + { + "date": "Apr 07, 2019", + "title": "Enhancement: edit relationships", + "description": "In a contact profile page, you can now edit a relationship, for a real or even a partial contact. ![image](img/changelogs/2019-04-07-edit-relationship.png)" + }, + { + "date": "Mar 28, 2019", + "title": "Enhancement: gender type/sex", + "description": "Gender is now optional on a contact profile. In the [personalization tab of settings](settings/personalization), you can define a default gender, and you can attribute a type (sex) for each gender to ensure compatibility on Import/Export. ![image](img/changelogs/2019-03-28-gender-type.png)" + }, + { + "date": "Jan 06, 2019", + "title": "Enhancement: comment when rating your day", + "description": "In the Journal, you can now add a comment after you rate your day. ![image](img/changelogs/2019-01-06-rate-day-comment.png)" + }, + { + "date": "Jan 02, 2019", + "title": "Enhancement: number of life events", + "description": "You can now see the number of life events when loading the contact profile page. This lets you know if there is something important that needs to be seen when looking at a contact." + }, + { + "date": "Dec 26, 2018", + "title": "Enhancement: LinkedIn address", + "description": "The LinkedIn address, which was under Work Information, has been moved to the Contact Information box." + }, + { + "date": "Dec 22, 2018", + "title": "New feature: weather and temperature", + "description": "You can now see the current weather on the profile page of a contact. This information is displayed if there is at least one address set on the contact. ![image](img/changelogs/2018_12_22_weather.png)" + }, + { + "date": "Dec 19, 2018", + "title": "Security: recovery codes", + "description": "In the [security tab of settings](settings/security), you can now generate recovery codes. Recovery codes are useful if you have activated Two Factor Authentication but can not login with it for some reasons. With a recovery code you can bypass Two Factor Authentication using this one time code. Be careful though, those codes should be stored in a secure place as they may allow someone to gain access to your account. ![image](img/changelogs/2018-12-02-recovery-codes.png)" + }, + { + "date": "Dec 16, 2018", + "title": "New feature: add latitude and longitude to addresses", + "description": "When you enter an address, you can now also add latitude and longitude. Note that these coordinates use the decimal degrees system (DD) and therefore should be formatted like `41.40338, 2.17403`." + }, + { + "date": "Dec 13, 2018", + "title": "New feature: indicate how you felt during a phone call", + "description": "If a call made you feel something specific, like hope, fear or fondness, you can now record it. You have now access to up to 136 different emotions to express how you felt. ![image](img/changelogs/2018-12-13-emotions.png)" + }, + { + "date": "Dec 12, 2018", + "title": "New feature: photos upload", + "description": "You can now add photos to a contact. Head over the profile of one of your contact and upload photos now. ![image](img/changelogs/2018-12-04-photo-upload.png)" + }, + { + "date": "Dec 09, 2018", + "title": "New feature: indicate who initiated a phone call", + "description": "You can now indicate who has initiated a phone call. ![image](img/changelogs/2018-12-08-who-called.png)" + }, + { + "date": "Dec 08, 2018", + "title": "New feature: edit a phone call", + "description": "You can finally edit a phone call. Moreover, logging phone call now happens inline and not in a popup anymore." + }, + { + "date": "Nov 17, 2018", + "title": "New feature: tasks not related to contacts", + "description": "In the dashboard, you can now create tasks that are not linked to any contacts. ![image](img/changelogs/2018-11-17-custom-tasks.png)" + }, + { + "date": "Nov 13, 2018", + "title": "Contacts for each tag in the Settings page", + "description": "In the [Tags tab](settings/tags) under the Settings page, you can now see which contacts are associated with each tag." + }, + { + "date": "Oct 27, 2018", + "title": "New feature: documents upload", + "description": "You can now upload and attach documents to a contact. The only limitation is the size of those documents - apart from this limitation, you can upload any type of documents. ![image](img/changelogs/2018-10-27-documents.png)" + }, + { + "date": "Oct 19, 2018", + "title": "New feature: archiving contact", + "description": "You can now archive a contact to no longer see him/her on the Dashboard, or the Contacts list. Archived contacts can still be found by search." + }, + { + "date": "Sept 28, 2018", + "title": "New feature: life events", + "description": "You can now log major life events that happen to a contact. Like if the contact has had a surgery, or where he travelled to. You have access to nearly 50 different possible life events to document what happens to the people you care about. ![image](img/changelogs/2018-09-28-life-events.png)" + }, + { + "date": "Sept 04, 2018", + "title": "New feature: conversations", + "description": "You can now log conversations that you have on social media or SMS or else. While you could already do that using notes, conversations is an easier way to record them using a nice user interface. ![image](img/changelogs/2018-09-04-conversations.png)" + }, + { + "date": "Aug 17, 2018", + "title": "Set as favorite", + "description": "You can now set a contact as favorite. Favorites will always appear at the top of the contact list, no matter the filter you are using. ![image](img/changelogs/2018-08-17-favorite.png)" + }, + { + "date": "Aug 17, 2018", + "title": "New activity report page", + "description": "When viewing a contact, you now have access to a new activity report page that will display useful statistics regarding all the activities you've done with a specific contact. ![image](img/changelogs/2018-08-17-activity-report.png)" + }, + { + "date": "Aug 08, 2018", + "title": "Customization of activity types", + "description": "What you do with your friends is different than what I do with my friends. Therefore, you can now add, edit or delete activity types in the Settings tab. This is a premium feature. ![image](img/changelogs/2018-08-08-activity-types.png)" + }, + { + "date": "May 23, 2018", + "title": "Debts on the dashboard", + "description": "We now display all the debts you owe (or the ones your contacts owe to you) on the dashboard. That way, it will be easier to keep an eye of who owes what. ![image](img/changelogs/2018-05-21-debts.png)" + }, + { + "date": "May 21, 2018", + "title": "Support for nicknames", + "description": "Many of you have asked for it - we now support nicknames for your contacts. By default, if you set a nickname, we will display it after a contact name (like John Doe (Rambo)). You can choose how the nickname is displayed in your Settings - there are actually 7 different ways now to display a name. ![image](img/changelogs/2018-05-21-nicknames.png)" + }, + { + "date": "May 04, 2018", + "title": "Disable automatic birthday reminders", + "description": "When you edit a contact, or manage a relationship, and add a birthday, we used to add a reminder for it automatically. This has annoyed you a lot, considering the number of emails you have sent about this. We've change this behaviour. You now have the option to decide if you want to be reminded for the birthday. Hope you like it! ![image](img/changelogs/2018-05-04-screenshot-macpro.png)" + }, + { + "date": "Apr 21, 2018", + "title": "Stay in touch with your contacts", + "description": "You can now indicate if you want to stay in touch with someone at a regular interval. If you do, you will receive an email at a given number of days that you decide, reminding you to contact the person. This feature is available only if you have a paid account. ![image](img/changelogs/2018-04-21-stayintouch.gif)" + }, + { + "date": "Apr 20, 2018", + "title": "New relationships", + "description": "You now have much more control on how you link contacts together. Before you could only have parent/child relationships and significant other relationships. Now you can have much more types of relationships, like uncle/nephew, lover, coworker, and so on. We hope you will like what we have done. ![image](img/changelogs/2018-04-14-relationships.png)" + }, + { + "date": "Apr 15, 2018", + "title": "Introducing a new product changes section", + "description": "There is a new header now in Monica. It shows a bell that slowly pulsate, with a red dot, if there is a new feature or an important change in the application. You will not have to find out by yourself what has changed in the product. ![image](img/changelogs/2018-04-14-new-product-section.png)" + } + ] +} diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..e69de29 diff --git a/public/images/vendor/vue-xeditable/src/editable/clear.png b/public/images/vendor/vue-xeditable/src/editable/clear.png new file mode 100644 index 0000000..3bf75a0 Binary files /dev/null and b/public/images/vendor/vue-xeditable/src/editable/clear.png differ diff --git a/public/images/vendor/vue-xeditable/src/editable/loading.gif b/public/images/vendor/vue-xeditable/src/editable/loading.gif new file mode 100644 index 0000000..dbb8259 Binary files /dev/null and b/public/images/vendor/vue-xeditable/src/editable/loading.gif differ diff --git a/public/img/changelogs/2018-04-14-new-product-section.png b/public/img/changelogs/2018-04-14-new-product-section.png new file mode 100644 index 0000000..0c77ac0 Binary files /dev/null and b/public/img/changelogs/2018-04-14-new-product-section.png differ diff --git a/public/img/changelogs/2018-04-14-relationships.png b/public/img/changelogs/2018-04-14-relationships.png new file mode 100644 index 0000000..0d89502 Binary files /dev/null and b/public/img/changelogs/2018-04-14-relationships.png differ diff --git a/public/img/changelogs/2018-04-21-stayintouch.gif b/public/img/changelogs/2018-04-21-stayintouch.gif new file mode 100644 index 0000000..6e5a424 Binary files /dev/null and b/public/img/changelogs/2018-04-21-stayintouch.gif differ diff --git a/public/img/changelogs/2018-05-04-screenshot-macpro.png b/public/img/changelogs/2018-05-04-screenshot-macpro.png new file mode 100644 index 0000000..3892c76 Binary files /dev/null and b/public/img/changelogs/2018-05-04-screenshot-macpro.png differ diff --git a/public/img/changelogs/2018-05-21-debts.png b/public/img/changelogs/2018-05-21-debts.png new file mode 100644 index 0000000..d86746f Binary files /dev/null and b/public/img/changelogs/2018-05-21-debts.png differ diff --git a/public/img/changelogs/2018-05-21-nicknames.png b/public/img/changelogs/2018-05-21-nicknames.png new file mode 100644 index 0000000..d68bc10 Binary files /dev/null and b/public/img/changelogs/2018-05-21-nicknames.png differ diff --git a/public/img/changelogs/2018-08-08-activity-types.png b/public/img/changelogs/2018-08-08-activity-types.png new file mode 100644 index 0000000..45a55ef Binary files /dev/null and b/public/img/changelogs/2018-08-08-activity-types.png differ diff --git a/public/img/changelogs/2018-08-17-activity-report.png b/public/img/changelogs/2018-08-17-activity-report.png new file mode 100644 index 0000000..4d226c6 Binary files /dev/null and b/public/img/changelogs/2018-08-17-activity-report.png differ diff --git a/public/img/changelogs/2018-08-17-favorite.png b/public/img/changelogs/2018-08-17-favorite.png new file mode 100644 index 0000000..b54795b Binary files /dev/null and b/public/img/changelogs/2018-08-17-favorite.png differ diff --git a/public/img/changelogs/2018-09-04-conversations.png b/public/img/changelogs/2018-09-04-conversations.png new file mode 100644 index 0000000..0262290 Binary files /dev/null and b/public/img/changelogs/2018-09-04-conversations.png differ diff --git a/public/img/changelogs/2018-09-28-life-events.png b/public/img/changelogs/2018-09-28-life-events.png new file mode 100644 index 0000000..36377a7 Binary files /dev/null and b/public/img/changelogs/2018-09-28-life-events.png differ diff --git a/public/img/changelogs/2018-10-27-documents.png b/public/img/changelogs/2018-10-27-documents.png new file mode 100644 index 0000000..6138426 Binary files /dev/null and b/public/img/changelogs/2018-10-27-documents.png differ diff --git a/public/img/changelogs/2018-11-17-custom-tasks.png b/public/img/changelogs/2018-11-17-custom-tasks.png new file mode 100644 index 0000000..afa24f8 Binary files /dev/null and b/public/img/changelogs/2018-11-17-custom-tasks.png differ diff --git a/public/img/changelogs/2018-12-02-recovery-codes.png b/public/img/changelogs/2018-12-02-recovery-codes.png new file mode 100644 index 0000000..1790806 Binary files /dev/null and b/public/img/changelogs/2018-12-02-recovery-codes.png differ diff --git a/public/img/changelogs/2018-12-04-photo-upload.png b/public/img/changelogs/2018-12-04-photo-upload.png new file mode 100644 index 0000000..2aa9c5a Binary files /dev/null and b/public/img/changelogs/2018-12-04-photo-upload.png differ diff --git a/public/img/changelogs/2018-12-08-who-called.png b/public/img/changelogs/2018-12-08-who-called.png new file mode 100644 index 0000000..481aa5d Binary files /dev/null and b/public/img/changelogs/2018-12-08-who-called.png differ diff --git a/public/img/changelogs/2018-12-13-emotions.png b/public/img/changelogs/2018-12-13-emotions.png new file mode 100644 index 0000000..2cd3c83 Binary files /dev/null and b/public/img/changelogs/2018-12-13-emotions.png differ diff --git a/public/img/changelogs/2018_12_22_weather.png b/public/img/changelogs/2018_12_22_weather.png new file mode 100644 index 0000000..f48f43d Binary files /dev/null and b/public/img/changelogs/2018_12_22_weather.png differ diff --git a/public/img/changelogs/2019-01-06-rate-day-comment.png b/public/img/changelogs/2019-01-06-rate-day-comment.png new file mode 100644 index 0000000..1040534 Binary files /dev/null and b/public/img/changelogs/2019-01-06-rate-day-comment.png differ diff --git a/public/img/changelogs/2019-03-22-new-activity.png b/public/img/changelogs/2019-03-22-new-activity.png new file mode 100644 index 0000000..bc15a80 Binary files /dev/null and b/public/img/changelogs/2019-03-22-new-activity.png differ diff --git a/public/img/changelogs/2019-03-28-gender-type.png b/public/img/changelogs/2019-03-28-gender-type.png new file mode 100644 index 0000000..0e3ae79 Binary files /dev/null and b/public/img/changelogs/2019-03-28-gender-type.png differ diff --git a/public/img/changelogs/2019-04-07-edit-relationship.png b/public/img/changelogs/2019-04-07-edit-relationship.png new file mode 100644 index 0000000..80d1a28 Binary files /dev/null and b/public/img/changelogs/2019-04-07-edit-relationship.png differ diff --git a/public/img/changelogs/2019-05-02-group-relationships.png b/public/img/changelogs/2019-05-02-group-relationships.png new file mode 100644 index 0000000..ad99823 Binary files /dev/null and b/public/img/changelogs/2019-05-02-group-relationships.png differ diff --git a/public/img/changelogs/2019-05-04-webauthn.png b/public/img/changelogs/2019-05-04-webauthn.png new file mode 100644 index 0000000..d573464 Binary files /dev/null and b/public/img/changelogs/2019-05-04-webauthn.png differ diff --git a/public/img/changelogs/2019-08-17-avatars.png b/public/img/changelogs/2019-08-17-avatars.png new file mode 100644 index 0000000..5900274 Binary files /dev/null and b/public/img/changelogs/2019-08-17-avatars.png differ diff --git a/public/img/changelogs/2020-01-04-gifts-photo.png b/public/img/changelogs/2020-01-04-gifts-photo.png new file mode 100644 index 0000000..c7e921b Binary files /dev/null and b/public/img/changelogs/2020-01-04-gifts-photo.png differ diff --git a/public/img/changelogs/2020-05-11-crop-avatar-1.png b/public/img/changelogs/2020-05-11-crop-avatar-1.png new file mode 100644 index 0000000..5db939a Binary files /dev/null and b/public/img/changelogs/2020-05-11-crop-avatar-1.png differ diff --git a/public/img/changelogs/2020-05-11-crop-avatar-2.png b/public/img/changelogs/2020-05-11-crop-avatar-2.png new file mode 100644 index 0000000..92de56a Binary files /dev/null and b/public/img/changelogs/2020-05-11-crop-avatar-2.png differ diff --git a/public/img/changelogs/2020-05.png b/public/img/changelogs/2020-05.png new file mode 100644 index 0000000..b99d048 Binary files /dev/null and b/public/img/changelogs/2020-05.png differ diff --git a/public/img/changelogs/2021-08-26-subscription.png b/public/img/changelogs/2021-08-26-subscription.png new file mode 100644 index 0000000..719ee47 Binary files /dev/null and b/public/img/changelogs/2021-08-26-subscription.png differ diff --git a/public/img/changelogs/2021-08-26-update.png b/public/img/changelogs/2021-08-26-update.png new file mode 100644 index 0000000..77cf5c1 Binary files /dev/null and b/public/img/changelogs/2021-08-26-update.png differ diff --git a/public/img/dashboard/blank.svg b/public/img/dashboard/blank.svg new file mode 100644 index 0000000..d228f6b --- /dev/null +++ b/public/img/dashboard/blank.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/dashboard/blank_your_tasks.svg b/public/img/dashboard/blank_your_tasks.svg new file mode 100644 index 0000000..3f04eae --- /dev/null +++ b/public/img/dashboard/blank_your_tasks.svg @@ -0,0 +1 @@ +undraw_to_do_list_a49bCreated with Sketch. \ No newline at end of file diff --git a/public/img/favicon.png b/public/img/favicon.png new file mode 100644 index 0000000..4ebb440 Binary files /dev/null and b/public/img/favicon.png differ diff --git a/public/img/icons/favicon-196.png b/public/img/icons/favicon-196.png new file mode 100644 index 0000000..c6b6539 Binary files /dev/null and b/public/img/icons/favicon-196.png differ diff --git a/public/img/icons/touch-icon-ipad-retina.png b/public/img/icons/touch-icon-ipad-retina.png new file mode 100644 index 0000000..e314032 Binary files /dev/null and b/public/img/icons/touch-icon-ipad-retina.png differ diff --git a/public/img/icons/touch-icon-ipad.png b/public/img/icons/touch-icon-ipad.png new file mode 100644 index 0000000..ac32cf3 Binary files /dev/null and b/public/img/icons/touch-icon-ipad.png differ diff --git a/public/img/icons/touch-icon-iphone-retina.png b/public/img/icons/touch-icon-iphone-retina.png new file mode 100644 index 0000000..731b9f5 Binary files /dev/null and b/public/img/icons/touch-icon-iphone-retina.png differ diff --git a/public/img/icons/touch-icon-iphone.png b/public/img/icons/touch-icon-iphone.png new file mode 100644 index 0000000..8646421 Binary files /dev/null and b/public/img/icons/touch-icon-iphone.png differ diff --git a/public/img/journal/blank.svg b/public/img/journal/blank.svg new file mode 100644 index 0000000..b2ae203 --- /dev/null +++ b/public/img/journal/blank.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/monica.svg b/public/img/monica.svg new file mode 100644 index 0000000..5c397c7 --- /dev/null +++ b/public/img/monica.svg @@ -0,0 +1 @@ +Artboard 3.1Created using Figma \ No newline at end of file diff --git a/public/img/monica_140.svg b/public/img/monica_140.svg new file mode 100644 index 0000000..b567896 --- /dev/null +++ b/public/img/monica_140.svg @@ -0,0 +1 @@ +Artboard 3.1Created using Figma \ No newline at end of file diff --git a/public/img/monica_192.svg b/public/img/monica_192.svg new file mode 100644 index 0000000..3dd240f --- /dev/null +++ b/public/img/monica_192.svg @@ -0,0 +1 @@ +Artboard 3.1Created using Figma \ No newline at end of file diff --git a/public/img/monica_512.svg b/public/img/monica_512.svg new file mode 100644 index 0000000..db6ee9b --- /dev/null +++ b/public/img/monica_512.svg @@ -0,0 +1 @@ +Artboard 3.1Created using Figma \ No newline at end of file diff --git a/public/img/monica_60.png b/public/img/monica_60.png new file mode 100644 index 0000000..2754a71 Binary files /dev/null and b/public/img/monica_60.png differ diff --git a/public/img/monica_reverse.svg b/public/img/monica_reverse.svg new file mode 100644 index 0000000..314be3f --- /dev/null +++ b/public/img/monica_reverse.svg @@ -0,0 +1 @@ +GroupCreated using Figma \ No newline at end of file diff --git a/public/img/people/activities.svg b/public/img/people/activities.svg new file mode 100644 index 0000000..eecb30a --- /dev/null +++ b/public/img/people/activities.svg @@ -0,0 +1 @@ +CycleCreated with Sketch. \ No newline at end of file diff --git a/public/img/people/activities/ate_his_place.svg b/public/img/people/activities/ate_his_place.svg new file mode 100644 index 0000000..0ce0a55 --- /dev/null +++ b/public/img/people/activities/ate_his_place.svg @@ -0,0 +1 @@ +CookCreated with Sketch. \ No newline at end of file diff --git a/public/img/people/activities/ate_home.svg b/public/img/people/activities/ate_home.svg new file mode 100644 index 0000000..5cef612 --- /dev/null +++ b/public/img/people/activities/ate_home.svg @@ -0,0 +1 @@ +BigkitchenCreated with Sketch. \ No newline at end of file diff --git a/public/img/people/activities/bar.svg b/public/img/people/activities/bar.svg new file mode 100644 index 0000000..02c8086 --- /dev/null +++ b/public/img/people/activities/bar.svg @@ -0,0 +1 @@ +CocktailCreated with Sketch. \ No newline at end of file diff --git a/public/img/people/activities/blank.svg b/public/img/people/activities/blank.svg new file mode 100644 index 0000000..deda4b9 --- /dev/null +++ b/public/img/people/activities/blank.svg @@ -0,0 +1 @@ +ArtboardCreated with Sketch. \ No newline at end of file diff --git a/public/img/people/activities/concert.svg b/public/img/people/activities/concert.svg new file mode 100644 index 0000000..b1af1b2 --- /dev/null +++ b/public/img/people/activities/concert.svg @@ -0,0 +1 @@ +SingingCreated with Sketch. \ No newline at end of file diff --git a/public/img/people/activities/hang_out.svg b/public/img/people/activities/hang_out.svg new file mode 100644 index 0000000..208bc93 --- /dev/null +++ b/public/img/people/activities/hang_out.svg @@ -0,0 +1 @@ +BusinessmanCreated with Sketch. \ No newline at end of file diff --git a/public/img/people/activities/movie_home.svg b/public/img/people/activities/movie_home.svg new file mode 100644 index 0000000..dc8ba3e --- /dev/null +++ b/public/img/people/activities/movie_home.svg @@ -0,0 +1 @@ +TvstandCreated with Sketch. \ No newline at end of file diff --git a/public/img/people/activities/museum.svg b/public/img/people/activities/museum.svg new file mode 100644 index 0000000..c31fb3d --- /dev/null +++ b/public/img/people/activities/museum.svg @@ -0,0 +1 @@ +PhotographyCreated with Sketch. \ No newline at end of file diff --git a/public/img/people/activities/picknicked.svg b/public/img/people/activities/picknicked.svg new file mode 100644 index 0000000..dd51b6a --- /dev/null +++ b/public/img/people/activities/picknicked.svg @@ -0,0 +1 @@ +BarbecueCreated with Sketch. \ No newline at end of file diff --git a/public/img/people/activities/play.svg b/public/img/people/activities/play.svg new file mode 100644 index 0000000..2155af2 --- /dev/null +++ b/public/img/people/activities/play.svg @@ -0,0 +1 @@ +MaskCreated with Sketch. \ No newline at end of file diff --git a/public/img/people/activities/restaurant.svg b/public/img/people/activities/restaurant.svg new file mode 100644 index 0000000..2d9b830 --- /dev/null +++ b/public/img/people/activities/restaurant.svg @@ -0,0 +1 @@ +SaladCreated with Sketch. \ No newline at end of file diff --git a/public/img/people/activities/sport.svg b/public/img/people/activities/sport.svg new file mode 100644 index 0000000..7bbc6a3 --- /dev/null +++ b/public/img/people/activities/sport.svg @@ -0,0 +1 @@ +TennisCreated with Sketch. \ No newline at end of file diff --git a/public/img/people/activities/talk_home.svg b/public/img/people/activities/talk_home.svg new file mode 100644 index 0000000..4b776b9 --- /dev/null +++ b/public/img/people/activities/talk_home.svg @@ -0,0 +1 @@ +CommentsCreated with Sketch. \ No newline at end of file diff --git a/public/img/people/activities/theater.svg b/public/img/people/activities/theater.svg new file mode 100644 index 0000000..38d2c02 --- /dev/null +++ b/public/img/people/activities/theater.svg @@ -0,0 +1 @@ +HomecinemaCreated with Sketch. \ No newline at end of file diff --git a/public/img/people/blank.svg b/public/img/people/blank.svg new file mode 100644 index 0000000..edea136 --- /dev/null +++ b/public/img/people/blank.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/people/debt/bill.svg b/public/img/people/debt/bill.svg new file mode 100644 index 0000000..806d133 --- /dev/null +++ b/public/img/people/debt/bill.svg @@ -0,0 +1 @@ +DollarpaperCreated with Sketch. \ No newline at end of file diff --git a/public/img/people/food_preferences.svg b/public/img/people/food_preferences.svg new file mode 100644 index 0000000..0ce0a55 --- /dev/null +++ b/public/img/people/food_preferences.svg @@ -0,0 +1 @@ +CookCreated with Sketch. \ No newline at end of file diff --git a/public/img/people/gifts.svg b/public/img/people/gifts.svg new file mode 100644 index 0000000..fea916f --- /dev/null +++ b/public/img/people/gifts.svg @@ -0,0 +1 @@ +GiftboxCreated with Sketch. \ No newline at end of file diff --git a/public/img/people/gifts/blank.svg b/public/img/people/gifts/blank.svg new file mode 100644 index 0000000..7d518fb --- /dev/null +++ b/public/img/people/gifts/blank.svg @@ -0,0 +1 @@ +ShoploveCreated with Sketch. \ No newline at end of file diff --git a/public/img/people/life-events/categories/family_relationships.svg b/public/img/people/life-events/categories/family_relationships.svg new file mode 100644 index 0000000..a97a170 --- /dev/null +++ b/public/img/people/life-events/categories/family_relationships.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/people/life-events/categories/health_wellness.svg b/public/img/people/life-events/categories/health_wellness.svg new file mode 100644 index 0000000..bd91a49 --- /dev/null +++ b/public/img/people/life-events/categories/health_wellness.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/people/life-events/categories/home_living.svg b/public/img/people/life-events/categories/home_living.svg new file mode 100644 index 0000000..53a7183 --- /dev/null +++ b/public/img/people/life-events/categories/home_living.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/people/life-events/categories/travel_experiences.svg b/public/img/people/life-events/categories/travel_experiences.svg new file mode 100644 index 0000000..34f827a --- /dev/null +++ b/public/img/people/life-events/categories/travel_experiences.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/people/life-events/categories/work_education.svg b/public/img/people/life-events/categories/work_education.svg new file mode 100644 index 0000000..ce82a2d --- /dev/null +++ b/public/img/people/life-events/categories/work_education.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/people/life-events/types/achievement_or_award.svg b/public/img/people/life-events/types/achievement_or_award.svg new file mode 100644 index 0000000..cef8fc1 --- /dev/null +++ b/public/img/people/life-events/types/achievement_or_award.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/people/life-events/types/anniversary.svg b/public/img/people/life-events/types/anniversary.svg new file mode 100644 index 0000000..e0a5716 --- /dev/null +++ b/public/img/people/life-events/types/anniversary.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/people/life-events/types/bought_a_home.svg b/public/img/people/life-events/types/bought_a_home.svg new file mode 100644 index 0000000..bb5acdf --- /dev/null +++ b/public/img/people/life-events/types/bought_a_home.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/people/life-events/types/broken_bone.svg b/public/img/people/life-events/types/broken_bone.svg new file mode 100644 index 0000000..4ebb7de --- /dev/null +++ b/public/img/people/life-events/types/broken_bone.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/people/life-events/types/changed_beliefs.svg b/public/img/people/life-events/types/changed_beliefs.svg new file mode 100644 index 0000000..08bf677 --- /dev/null +++ b/public/img/people/life-events/types/changed_beliefs.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/people/life-events/types/dentist.svg b/public/img/people/life-events/types/dentist.svg new file mode 100644 index 0000000..19cb653 --- /dev/null +++ b/public/img/people/life-events/types/dentist.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/people/life-events/types/end_of_relationship.svg b/public/img/people/life-events/types/end_of_relationship.svg new file mode 100644 index 0000000..1559071 --- /dev/null +++ b/public/img/people/life-events/types/end_of_relationship.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/people/life-events/types/engagement.svg b/public/img/people/life-events/types/engagement.svg new file mode 100644 index 0000000..72e8efd --- /dev/null +++ b/public/img/people/life-events/types/engagement.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/people/life-events/types/expecting_a_baby.svg b/public/img/people/life-events/types/expecting_a_baby.svg new file mode 100644 index 0000000..d94cd94 --- /dev/null +++ b/public/img/people/life-events/types/expecting_a_baby.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/people/life-events/types/first_kiss.svg b/public/img/people/life-events/types/first_kiss.svg new file mode 100644 index 0000000..02457aa --- /dev/null +++ b/public/img/people/life-events/types/first_kiss.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/people/life-events/types/first_met.svg b/public/img/people/life-events/types/first_met.svg new file mode 100644 index 0000000..3e66994 --- /dev/null +++ b/public/img/people/life-events/types/first_met.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/people/life-events/types/first_word.svg b/public/img/people/life-events/types/first_word.svg new file mode 100644 index 0000000..920912e --- /dev/null +++ b/public/img/people/life-events/types/first_word.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/people/life-events/types/holidays.svg b/public/img/people/life-events/types/holidays.svg new file mode 100644 index 0000000..d0d9c3c --- /dev/null +++ b/public/img/people/life-events/types/holidays.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/people/life-events/types/home_improvement.svg b/public/img/people/life-events/types/home_improvement.svg new file mode 100644 index 0000000..39946f9 --- /dev/null +++ b/public/img/people/life-events/types/home_improvement.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/people/life-events/types/loss_of_a_loved_one.svg b/public/img/people/life-events/types/loss_of_a_loved_one.svg new file mode 100644 index 0000000..f47a14d --- /dev/null +++ b/public/img/people/life-events/types/loss_of_a_loved_one.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/people/life-events/types/marriage.svg b/public/img/people/life-events/types/marriage.svg new file mode 100644 index 0000000..df833c5 --- /dev/null +++ b/public/img/people/life-events/types/marriage.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/people/life-events/types/military_service.svg b/public/img/people/life-events/types/military_service.svg new file mode 100644 index 0000000..4795639 --- /dev/null +++ b/public/img/people/life-events/types/military_service.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/people/life-events/types/moved.svg b/public/img/people/life-events/types/moved.svg new file mode 100644 index 0000000..f7573b6 --- /dev/null +++ b/public/img/people/life-events/types/moved.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/people/life-events/types/new_child.svg b/public/img/people/life-events/types/new_child.svg new file mode 100644 index 0000000..d2ac1c1 --- /dev/null +++ b/public/img/people/life-events/types/new_child.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/people/life-events/types/new_eating_habits.svg b/public/img/people/life-events/types/new_eating_habits.svg new file mode 100644 index 0000000..a16cfba --- /dev/null +++ b/public/img/people/life-events/types/new_eating_habits.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/people/life-events/types/new_family_member.svg b/public/img/people/life-events/types/new_family_member.svg new file mode 100644 index 0000000..1422a5c --- /dev/null +++ b/public/img/people/life-events/types/new_family_member.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/people/life-events/types/new_hobby.svg b/public/img/people/life-events/types/new_hobby.svg new file mode 100644 index 0000000..a601778 --- /dev/null +++ b/public/img/people/life-events/types/new_hobby.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/people/life-events/types/new_instrument.svg b/public/img/people/life-events/types/new_instrument.svg new file mode 100644 index 0000000..c9321dd --- /dev/null +++ b/public/img/people/life-events/types/new_instrument.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/people/life-events/types/new_job.svg b/public/img/people/life-events/types/new_job.svg new file mode 100644 index 0000000..f040171 --- /dev/null +++ b/public/img/people/life-events/types/new_job.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/people/life-events/types/new_language.svg b/public/img/people/life-events/types/new_language.svg new file mode 100644 index 0000000..28b2781 --- /dev/null +++ b/public/img/people/life-events/types/new_language.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/people/life-events/types/new_license.svg b/public/img/people/life-events/types/new_license.svg new file mode 100644 index 0000000..5dc5a76 --- /dev/null +++ b/public/img/people/life-events/types/new_license.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/people/life-events/types/new_pet.svg b/public/img/people/life-events/types/new_pet.svg new file mode 100644 index 0000000..12cdbb2 --- /dev/null +++ b/public/img/people/life-events/types/new_pet.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/people/life-events/types/new_relationship.svg b/public/img/people/life-events/types/new_relationship.svg new file mode 100644 index 0000000..3939bde --- /dev/null +++ b/public/img/people/life-events/types/new_relationship.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/people/life-events/types/new_roommate.svg b/public/img/people/life-events/types/new_roommate.svg new file mode 100644 index 0000000..2908328 --- /dev/null +++ b/public/img/people/life-events/types/new_roommate.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/people/life-events/types/new_school.svg b/public/img/people/life-events/types/new_school.svg new file mode 100644 index 0000000..2d6b8d2 --- /dev/null +++ b/public/img/people/life-events/types/new_school.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/people/life-events/types/new_sport.svg b/public/img/people/life-events/types/new_sport.svg new file mode 100644 index 0000000..e1c239a --- /dev/null +++ b/public/img/people/life-events/types/new_sport.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/people/life-events/types/new_vehicle.svg b/public/img/people/life-events/types/new_vehicle.svg new file mode 100644 index 0000000..c91c968 --- /dev/null +++ b/public/img/people/life-events/types/new_vehicle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/people/life-events/types/null.svg b/public/img/people/life-events/types/null.svg new file mode 100644 index 0000000..55c2669 --- /dev/null +++ b/public/img/people/life-events/types/null.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/people/life-events/types/organ_donor.svg b/public/img/people/life-events/types/organ_donor.svg new file mode 100644 index 0000000..4b589d2 --- /dev/null +++ b/public/img/people/life-events/types/organ_donor.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/people/life-events/types/overcame_an_illness.svg b/public/img/people/life-events/types/overcame_an_illness.svg new file mode 100644 index 0000000..de84ea1 --- /dev/null +++ b/public/img/people/life-events/types/overcame_an_illness.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/people/life-events/types/published_book_or_paper.svg b/public/img/people/life-events/types/published_book_or_paper.svg new file mode 100644 index 0000000..aeac256 --- /dev/null +++ b/public/img/people/life-events/types/published_book_or_paper.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/people/life-events/types/quit_a_habit.svg b/public/img/people/life-events/types/quit_a_habit.svg new file mode 100644 index 0000000..a9782ec --- /dev/null +++ b/public/img/people/life-events/types/quit_a_habit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/people/life-events/types/removed_braces.svg b/public/img/people/life-events/types/removed_braces.svg new file mode 100644 index 0000000..0dcd016 --- /dev/null +++ b/public/img/people/life-events/types/removed_braces.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/people/life-events/types/retirement.svg b/public/img/people/life-events/types/retirement.svg new file mode 100644 index 0000000..503ca17 --- /dev/null +++ b/public/img/people/life-events/types/retirement.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/people/life-events/types/study_abroad.svg b/public/img/people/life-events/types/study_abroad.svg new file mode 100644 index 0000000..c0c91fc --- /dev/null +++ b/public/img/people/life-events/types/study_abroad.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/people/life-events/types/surgery.svg b/public/img/people/life-events/types/surgery.svg new file mode 100644 index 0000000..10067bf --- /dev/null +++ b/public/img/people/life-events/types/surgery.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/people/life-events/types/tattoo_or_piercing.svg b/public/img/people/life-events/types/tattoo_or_piercing.svg new file mode 100644 index 0000000..0c4d848 --- /dev/null +++ b/public/img/people/life-events/types/tattoo_or_piercing.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/people/life-events/types/travel.svg b/public/img/people/life-events/types/travel.svg new file mode 100644 index 0000000..8eb1602 --- /dev/null +++ b/public/img/people/life-events/types/travel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/people/life-events/types/volunteer_work.svg b/public/img/people/life-events/types/volunteer_work.svg new file mode 100644 index 0000000..bcf3515 --- /dev/null +++ b/public/img/people/life-events/types/volunteer_work.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/people/life-events/types/wear_glass_or_contact.svg b/public/img/people/life-events/types/wear_glass_or_contact.svg new file mode 100644 index 0000000..8639e7b --- /dev/null +++ b/public/img/people/life-events/types/wear_glass_or_contact.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/people/life-events/types/weight_loss.svg b/public/img/people/life-events/types/weight_loss.svg new file mode 100644 index 0000000..6d40786 --- /dev/null +++ b/public/img/people/life-events/types/weight_loss.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/img/people/list/children.svg b/public/img/people/list/children.svg new file mode 100644 index 0000000..b13da50 --- /dev/null +++ b/public/img/people/list/children.svg @@ -0,0 +1 @@ +BabyheadCreated with Sketch. \ No newline at end of file diff --git a/public/img/people/list/reminders.svg b/public/img/people/list/reminders.svg new file mode 100644 index 0000000..6b31739 --- /dev/null +++ b/public/img/people/list/reminders.svg @@ -0,0 +1 @@ +AlarmclockCreated with Sketch. \ No newline at end of file diff --git a/public/img/people/no_record_found.svg b/public/img/people/no_record_found.svg new file mode 100644 index 0000000..a89b5e6 --- /dev/null +++ b/public/img/people/no_record_found.svg @@ -0,0 +1 @@ +GroupCreated with Sketch. \ No newline at end of file diff --git a/public/img/people/notes.svg b/public/img/people/notes.svg new file mode 100644 index 0000000..6d758ef --- /dev/null +++ b/public/img/people/notes.svg @@ -0,0 +1 @@ +FiletextCreated with Sketch. \ No newline at end of file diff --git a/public/img/people/photos/photos_empty.svg b/public/img/people/photos/photos_empty.svg new file mode 100644 index 0000000..047cf10 --- /dev/null +++ b/public/img/people/photos/photos_empty.svg @@ -0,0 +1 @@ +creativity \ No newline at end of file diff --git a/public/img/people/reminders.svg b/public/img/people/reminders.svg new file mode 100644 index 0000000..6b31739 --- /dev/null +++ b/public/img/people/reminders.svg @@ -0,0 +1 @@ +AlarmclockCreated with Sketch. \ No newline at end of file diff --git a/public/img/people/reminders/baby.svg b/public/img/people/reminders/baby.svg new file mode 100644 index 0000000..fd246b3 --- /dev/null +++ b/public/img/people/reminders/baby.svg @@ -0,0 +1 @@ +BabyCreated with Sketch. \ No newline at end of file diff --git a/public/img/people/reminders/birthday.svg b/public/img/people/reminders/birthday.svg new file mode 100644 index 0000000..48e6564 --- /dev/null +++ b/public/img/people/reminders/birthday.svg @@ -0,0 +1 @@ +CakeCreated with Sketch. \ No newline at end of file diff --git a/public/img/people/reminders/clock.svg b/public/img/people/reminders/clock.svg new file mode 100644 index 0000000..bf54174 --- /dev/null +++ b/public/img/people/reminders/clock.svg @@ -0,0 +1 @@ +WatchclockCreated with Sketch. \ No newline at end of file diff --git a/public/img/people/reminders/email.svg b/public/img/people/reminders/email.svg new file mode 100644 index 0000000..35ec555 --- /dev/null +++ b/public/img/people/reminders/email.svg @@ -0,0 +1 @@ +InboxfavCreated with Sketch. \ No newline at end of file diff --git a/public/img/people/reminders/hangout.svg b/public/img/people/reminders/hangout.svg new file mode 100644 index 0000000..18667f5 --- /dev/null +++ b/public/img/people/reminders/hangout.svg @@ -0,0 +1 @@ +ShakinghandsCreated with Sketch. \ No newline at end of file diff --git a/public/img/people/reminders/lunch.svg b/public/img/people/reminders/lunch.svg new file mode 100644 index 0000000..920b864 --- /dev/null +++ b/public/img/people/reminders/lunch.svg @@ -0,0 +1 @@ +DrinkmenuCreated with Sketch. \ No newline at end of file diff --git a/public/img/people/reminders/phone.svg b/public/img/people/reminders/phone.svg new file mode 100644 index 0000000..cf22712 --- /dev/null +++ b/public/img/people/reminders/phone.svg @@ -0,0 +1 @@ +RingphoneCreated with Sketch. \ No newline at end of file diff --git a/public/img/people/tasks.svg b/public/img/people/tasks.svg new file mode 100644 index 0000000..ef49ff4 --- /dev/null +++ b/public/img/people/tasks.svg @@ -0,0 +1 @@ +SquarelistCreated with Sketch. \ No newline at end of file diff --git a/public/img/people/tasks/blank.svg b/public/img/people/tasks/blank.svg new file mode 100644 index 0000000..9e787b6 --- /dev/null +++ b/public/img/people/tasks/blank.svg @@ -0,0 +1 @@ +ArtboardCreated with Sketch. \ No newline at end of file diff --git a/public/img/people/upgrade_account.png b/public/img/people/upgrade_account.png new file mode 100644 index 0000000..ddedc4c Binary files /dev/null and b/public/img/people/upgrade_account.png differ diff --git a/public/img/settings/imports/import.png b/public/img/settings/imports/import.png new file mode 100644 index 0000000..310ec78 Binary files /dev/null and b/public/img/settings/imports/import.png differ diff --git a/public/img/settings/imports/import.svg b/public/img/settings/imports/import.svg new file mode 100644 index 0000000..c5b928f --- /dev/null +++ b/public/img/settings/imports/import.svg @@ -0,0 +1 @@ +GroupCreated with Sketch.VCARD \ No newline at end of file diff --git a/public/img/settings/subscription/best_value.png b/public/img/settings/subscription/best_value.png new file mode 100644 index 0000000..969606f Binary files /dev/null and b/public/img/settings/subscription/best_value.png differ diff --git a/public/img/settings/tags/tags.png b/public/img/settings/tags/tags.png new file mode 100644 index 0000000..1da587b Binary files /dev/null and b/public/img/settings/tags/tags.png differ diff --git a/public/img/settings/users/blank.svg b/public/img/settings/users/blank.svg new file mode 100644 index 0000000..17c4163 --- /dev/null +++ b/public/img/settings/users/blank.svg @@ -0,0 +1 @@ +GroupCreated with Sketch. \ No newline at end of file diff --git a/public/index.html b/public/index.html deleted file mode 100644 index 3307bb9..0000000 --- a/public/index.html +++ /dev/null @@ -1,506 +0,0 @@ - - - - - - MischCRM - Personal Relationship Manager - - - - - - - - - -
    - - - - -
    - -
    -

    Dashboard

    - -
    - - -
    - -
    -
    -
    - -
    -
    - Gesamte Freunde - 0 -
    -
    -
    -
    - -
    -
    - Treffen diesen Monat - 0 -
    -
    -
    -
    - -
    -
    - Durchschnittliche Ehre - 0 -
    -
    -
    -
    - -
    -
    - Geburtstage (30 Tage) - 0 -
    -
    -
    - -
    - -
    - -
    -
    -

    Schnell-Log Treffen

    -
    -
    -
    -
    - - -
    -
    - - -
    -
    -
    - - -
    -
    - - -
    -
    - - -
    - -
    -
    -
    - - -
    - -
    -
    -

    Lange nicht gesehen

    -
    -
    - -
    -
    -
    - - -
    -
    -

    Anstehende Geburtstage

    -
    -
    - -
    -
    -
    -
    -
    - - -
    -
    -
    - - -
    - -
    - -
    - -
    -
    - - -
    -
    -
    -

    Obsidian Markdown Import

    -

    Kopiere den Inhalt deiner Obsidian Freundesnotiz (inklusive YAML Frontmatter) hier hinein, um die Daten vollautomatisch in dein CRM zu übertragen.

    -
    - -
    -
    - - -
    -
    - - -
    - -
    - - -
    -
    -
    -
    - - - - - - - - - - - - - diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..6e58bdc --- /dev/null +++ b/public/index.php @@ -0,0 +1,60 @@ + + */ + +define('LARAVEL_START', microtime(true)); + +/* +|-------------------------------------------------------------------------- +| Register The Auto Loader +|-------------------------------------------------------------------------- +| +| Composer provides a convenient, automatically generated class loader for +| our application. We just need to utilize it! We'll simply require it +| into the script here so that we don't have to worry about manual +| loading any of our classes later on. It feels nice to relax. +| +*/ + +require __DIR__.'/../vendor/autoload.php'; + +/* +|-------------------------------------------------------------------------- +| Turn On The Lights +|-------------------------------------------------------------------------- +| +| We need to illuminate PHP development, so let us turn on the lights. +| This bootstraps the framework and gets it ready for use, then it +| will load up this application so that we can run it and send +| the responses back to the browser and delight our users. +| +*/ + +$app = require_once __DIR__.'/../bootstrap/app.php'; + +/* +|-------------------------------------------------------------------------- +| Run The Application +|-------------------------------------------------------------------------- +| +| Once we have the application, we can handle the incoming request +| through the kernel, and send the associated response back to +| the client's browser allowing them to enjoy the creative +| and wonderful application we have prepared for them. +| +*/ + +$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class); + +$response = $kernel->handle( + $request = Illuminate\Http\Request::capture() +); + +$response->send(); + +$kernel->terminate($request, $response); diff --git a/public/manifest.webmanifest b/public/manifest.webmanifest new file mode 100644 index 0000000..6ded419 --- /dev/null +++ b/public/manifest.webmanifest @@ -0,0 +1,29 @@ +{ + "name": "Monica", + "short_name": "Monica", + "start_url": ".", + "display": "standalone", + "background_color": "#fff", + "description": "Intuitive personal relationships management.", + "icons": [ + { + "src": "/img/monica_140.svg", + "sizes": "140x140", + "type": "image/svg+xml" + }, + { + "src": "/img/monica_192.svg", + "sizes": "192x192", + "type": "image/svg+xml" + }, + { + "src": "/img/monica_512.svg", + "sizes": "512x512", + "type": "image/svg+xml" + } + ], + "orientation": "portrait-primary", + "related_applications": [{ + "platform": "web" + }] +} diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..1f53798 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / diff --git a/public/security.txt b/public/security.txt new file mode 100644 index 0000000..cdaf799 --- /dev/null +++ b/public/security.txt @@ -0,0 +1,4 @@ +# Our security address + +Contact: security@monicahq.com +Disclosure: Full diff --git a/public/style.css b/public/style.css deleted file mode 100644 index db4bc06..0000000 --- a/public/style.css +++ /dev/null @@ -1,1350 +0,0 @@ -/* ========================================== - MischCRM Premium Design System & Styles - ========================================== */ - -/* Design Tokens & Variables */ -:root { - --font-family-title: 'Outfit', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; - --font-family-body: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; - - /* Premium Color Palette (HSL Tailored) */ - --bg-primary: hsl(240, 15%, 8%); - --bg-secondary: hsl(250, 15%, 4%); - --accent-purple: hsl(270, 75%, 60%); - --accent-purple-glow: hsla(270, 75%, 60%, 0.15); - --accent-cyan: hsl(190, 85%, 50%); - --accent-cyan-glow: hsla(190, 85%, 50%, 0.15); - --accent-gold: hsl(45, 90%, 55%); - --accent-gold-glow: hsla(45, 90%, 55%, 0.15); - --accent-pink: hsl(330, 85%, 55%); - --accent-pink-glow: hsla(330, 85%, 55%, 0.15); - --accent-red: hsl(0, 75%, 55%); - - /* Glassmorphism settings */ - --glass-bg: rgba(18, 16, 28, 0.55); - --glass-bg-hover: rgba(26, 23, 40, 0.65); - --glass-border: rgba(255, 255, 255, 0.05); - --glass-border-hover: rgba(255, 255, 255, 0.09); - --glass-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37); - - /* Text colors */ - --text-main: hsl(0, 0%, 95%); - --text-muted: hsl(240, 10%, 70%); - --text-dimmed: hsl(240, 8%, 50%); - - /* Spacers & Border Radii */ - --radius-lg: 20px; - --radius-md: 12px; - --radius-sm: 8px; - --transition-smooth: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); - --transition-bounce: all 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275); -} - -/* Global Reset & Base */ -* { - margin: 0; - padding: 0; - box-sizing: border-box; - scrollbar-width: thin; - scrollbar-color: var(--accent-purple-glow) transparent; -} - -body { - font-family: var(--font-family-body); - background-color: var(--bg-secondary); - color: var(--text-main); - min-height: 100vh; - overflow-x: hidden; - position: relative; -} - -/* Animated Neon Background Gradients */ -body::before, body::after { - content: ""; - position: absolute; - width: 50vw; - height: 50vw; - border-radius: 50%; - filter: blur(140px); - z-index: -1; - opacity: 0.25; - pointer-events: none; - animation: pulse-glow 15s infinite alternate; -} - -body::before { - top: -10vw; - left: -10vw; - background: radial-gradient(circle, var(--accent-purple) 0%, transparent 70%); -} - -body::after { - bottom: -10vw; - right: -10vw; - background: radial-gradient(circle, var(--accent-cyan) 0%, transparent 70%); - animation-delay: -5s; -} - -@keyframes pulse-glow { - 0% { transform: scale(1) translate(0, 0); } - 100% { transform: scale(1.15) translate(5vw, 5vw); } -} - -/* App Container Layout */ -.app-container { - display: grid; - grid-template-columns: 280px 1fr; - min-height: 100vh; -} - -/* Glassmorphic Panel Base Utility */ -.glass-panel { - background: var(--glass-bg); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border: 1px solid var(--glass-border); - box-shadow: var(--glass-shadow); - border-radius: var(--radius-lg); - transition: var(--transition-smooth); -} - -.glass-panel:hover { - background: var(--glass-bg-hover); - border-color: var(--glass-border-hover); -} - -/* Sidebar Styling */ -.sidebar { - background: rgba(10, 8, 18, 0.8); - border-right: 1px solid var(--glass-border); - display: flex; - flex-direction: column; - padding: 30px 20px; - height: 100vh; - position: sticky; - top: 0; - z-index: 100; -} - -.logo-area { - display: flex; - align-items: center; - gap: 12px; - margin-bottom: 50px; - position: relative; - padding: 5px; -} - -.logo-glow { - position: absolute; - width: 45px; - height: 45px; - background: var(--accent-purple); - filter: blur(25px); - opacity: 0.6; - border-radius: 50%; - left: 0; - z-index: -1; -} - -.logo-icon { - width: 32px; - height: 32px; - color: var(--accent-purple); - filter: drop-shadow(0 0 8px var(--accent-purple)); -} - -.logo-text { - font-family: var(--font-family-title); - font-size: 24px; - font-weight: 800; - letter-spacing: -0.5px; -} - -.logo-text span { - color: var(--accent-cyan); - filter: drop-shadow(0 0 8px var(--accent-cyan-glow)); -} - -.nav-menu { - display: flex; - flex-direction: column; - gap: 8px; - flex: 1; -} - -.nav-item { - display: flex; - align-items: center; - gap: 16px; - padding: 14px 18px; - color: var(--text-muted); - text-decoration: none; - font-weight: 500; - border-radius: var(--radius-md); - transition: var(--transition-smooth); - border: 1px solid transparent; -} - -.nav-item i { - width: 20px; - height: 20px; - transition: var(--transition-bounce); -} - -.nav-item:hover { - color: var(--text-main); - background: rgba(255, 255, 255, 0.03); - transform: translateX(4px); -} - -.nav-item.active { - color: var(--text-main); - background: rgba(147, 51, 234, 0.12); - border: 1px solid rgba(147, 51, 234, 0.25); - box-shadow: 0 0 15px rgba(147, 51, 234, 0.1); -} - -.nav-item.active i { - color: var(--accent-purple); - transform: scale(1.15); - filter: drop-shadow(0 0 5px var(--accent-purple)); -} - -.sidebar-footer { - padding-top: 20px; - border-top: 1px solid var(--glass-border); - color: var(--text-dimmed); - font-size: 12px; - text-align: center; -} - -/* Main Content Area */ -.main-content { - padding: 40px; - display: flex; - flex-direction: column; - gap: 30px; - overflow-y: auto; - max-height: 100vh; -} - -.top-bar { - display: flex; - justify-content: space-between; - align-items: center; -} - -.top-bar h1 { - font-family: var(--font-family-title); - font-size: 34px; - font-weight: 800; - letter-spacing: -0.7px; - background: linear-gradient(135deg, var(--text-main) 30%, var(--text-muted) 100%); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; -} - -.user-profile { - display: flex; - align-items: center; - gap: 16px; -} - -.welcome-text { - font-size: 14px; - color: var(--text-muted); -} - -.welcome-text span { - font-weight: 600; - color: var(--text-main); -} - -.avatar-ring { - width: 42px; - height: 42px; - border-radius: 50%; - background: linear-gradient(135deg, var(--accent-purple) 0%, var(--accent-cyan) 100%); - padding: 2px; - box-shadow: 0 0 10px rgba(147, 51, 234, 0.25); -} - -.avatar-fallback { - width: 100%; - height: 100%; - background: var(--bg-primary); - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - font-weight: 700; - font-size: 14px; - color: var(--text-main); -} - -/* Tab Management */ -.tab-pane { - display: none; - opacity: 0; - transform: translateY(15px); - transition: opacity 0.4s ease, transform 0.4s ease; -} - -.tab-pane.active { - display: flex; - flex-direction: column; - gap: 30px; - opacity: 1; - transform: translateY(0); -} - -/* Stats Cards Grid */ -.stats-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); - gap: 20px; -} - -.stat-card { - display: flex; - align-items: center; - gap: 20px; - padding: 24px; -} - -.stat-icon-wrapper { - width: 52px; - height: 52px; - border-radius: var(--radius-md); - display: flex; - align-items: center; - justify-content: center; - box-shadow: inset 0 0 10px rgba(255, 255, 255, 0.05); -} - -.stat-icon-wrapper i { - width: 24px; - height: 24px; -} - -.stat-icon-wrapper.blue { background: var(--accent-cyan-glow); color: var(--accent-cyan); } -.stat-icon-wrapper.purple { background: var(--accent-purple-glow); color: var(--accent-purple); } -.stat-icon-wrapper.gold { background: var(--accent-gold-glow); color: var(--accent-gold); } -.stat-icon-wrapper.pink { background: var(--accent-pink-glow); color: var(--accent-pink); } - -.stat-data { - display: flex; - flex-direction: column; - gap: 4px; -} - -.stat-label { - font-size: 13px; - color: var(--text-muted); - font-weight: 500; -} - -.stat-value { - font-family: var(--font-family-title); - font-size: 28px; - font-weight: 800; -} - -/* Dashboard Grid Layout */ -.dashboard-columns { - display: grid; - grid-template-columns: 1.2fr 1fr; - gap: 24px; -} - -.dashboard-left, .dashboard-right { - display: flex; - flex-direction: column; - gap: 24px; -} - -/* General Sections in Dashboard */ -.card-section { - padding: 30px; - display: flex; - flex-direction: column; - gap: 20px; -} - -.section-header { - border-bottom: 1px solid var(--glass-border); - padding-bottom: 15px; -} - -.section-header h2 { - font-family: var(--font-family-title); - font-size: 20px; - font-weight: 700; - display: flex; - align-items: center; - gap: 12px; -} - -.header-icon { - width: 22px; - height: 22px; - color: var(--accent-purple); -} - -.section-desc { - font-size: 13px; - color: var(--text-muted); - margin-top: 6px; -} - -/* Premium Forms */ -.premium-form { - display: flex; - flex-direction: column; - gap: 16px; -} - -.form-row { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 16px; -} - -.form-group { - display: flex; - flex-direction: column; - gap: 8px; -} - -.form-group label { - font-size: 12px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.5px; - color: var(--text-muted); -} - -.premium-form input, -.premium-form select, -.premium-form textarea, -.inline-add-form input, -.inline-meeting-form input, -.inline-meeting-form textarea { - background: rgba(255, 255, 255, 0.02); - border: 1px solid var(--glass-border); - border-radius: var(--radius-sm); - padding: 12px 16px; - color: var(--text-main); - font-family: var(--font-family-body); - font-size: 14px; - transition: var(--transition-smooth); -} - -.premium-form input:focus, -.premium-form select:focus, -.premium-form textarea:focus, -.inline-add-form input:focus, -.inline-meeting-form input:focus, -.inline-meeting-form textarea:focus { - border-color: var(--accent-purple); - background: rgba(255, 255, 255, 0.04); - outline: none; - box-shadow: 0 0 12px var(--accent-purple-glow); -} - -.premium-form select option { - background: var(--bg-primary); - color: var(--text-main); -} - -/* Premium Buttons */ -.btn { - display: inline-flex; - align-items: center; - justify-content: center; - gap: 8px; - padding: 12px 24px; - font-family: var(--font-family-body); - font-size: 14px; - font-weight: 600; - border-radius: var(--radius-md); - border: none; - cursor: pointer; - transition: var(--transition-bounce); -} - -.btn-primary { - background: linear-gradient(135deg, var(--accent-purple) 0%, hsl(285, 75%, 55%) 100%); - color: var(--text-main); - box-shadow: 0 4px 15px var(--accent-purple-glow); -} - -.btn-primary:hover { - transform: translateY(-2px); - box-shadow: 0 6px 20px rgba(147, 51, 234, 0.35); - filter: brightness(1.1); -} - -.btn-secondary { - background: rgba(255, 255, 255, 0.05); - color: var(--text-main); - border: 1px solid var(--glass-border); -} - -.btn-secondary:hover { - background: rgba(255, 255, 255, 0.08); - border-color: var(--glass-border-hover); -} - -.btn-danger { - background: rgba(239, 68, 68, 0.1); - color: hsl(0, 85%, 65%); - border: 1px solid rgba(239, 68, 68, 0.2); -} - -.btn-danger:hover { - background: var(--accent-red); - color: white; - box-shadow: 0 4px 15px rgba(239, 68, 68, 0.35); -} - -.btn-full { - width: 100%; -} - -.btn-sm { - padding: 8px 14px; - font-size: 12px; -} - -/* Lists UI (Dashboard lists) */ -.list-container { - display: flex; - flex-direction: column; - gap: 12px; - max-height: 280px; - overflow-y: auto; - padding-right: 5px; -} - -.scrollable-list-container { - max-height: 380px; -} - -.list-item { - display: flex; - align-items: center; - justify-content: space-between; - padding: 14px 18px; - background: rgba(255, 255, 255, 0.01); - border: 1px solid var(--glass-border); - border-radius: var(--radius-md); - transition: var(--transition-smooth); - cursor: pointer; -} - -.list-item:hover { - background: rgba(255, 255, 255, 0.03); - border-color: var(--glass-border-hover); - transform: translateX(3px); -} - -.list-item-left { - display: flex; - align-items: center; - gap: 14px; -} - -.item-avatar-mini { - width: 38px; - height: 38px; - border-radius: 50%; - background: var(--accent-purple-glow); - color: var(--accent-purple); - display: flex; - align-items: center; - justify-content: center; - font-weight: 700; - font-size: 13px; - border: 1px solid rgba(147, 51, 234, 0.2); -} - -.list-item-left.urgent .item-avatar-mini { - background: rgba(239, 68, 68, 0.1); - color: hsl(0, 85%, 65%); - border-color: rgba(239, 68, 68, 0.2); -} - -.list-item-left.birthday .item-avatar-mini { - background: var(--accent-pink-glow); - color: var(--accent-pink); - border-color: rgba(236, 72, 153, 0.2); -} - -.item-main-info { - display: flex; - flex-direction: column; - gap: 2px; -} - -.item-title { - font-weight: 600; - font-size: 14px; -} - -.item-sub { - font-size: 12px; - color: var(--text-muted); -} - -.badge { - padding: 6px 12px; - border-radius: 30px; - font-size: 11px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.3px; -} - -.badge-urgent-red { background: rgba(239, 68, 68, 0.15); color: hsl(0, 85%, 65%); border: 1px solid rgba(239, 68, 68, 0.2); } -.badge-urgent-yellow { background: rgba(245, 158, 11, 0.15); color: hsl(35, 90%, 60%); border: 1px solid rgba(245, 158, 11, 0.2); } -.badge-purple { background: var(--accent-purple-glow); color: var(--accent-purple); border: 1px solid rgba(147, 51, 234, 0.2); } - -/* TAB 2: FRIENDS DIRECTORY ACCORDIONS & GRID */ -.directory-actions { - display: flex; - justify-content: space-between; - align-items: center; - gap: 20px; -} - -.search-bar-wrapper { - flex: 1; - max-width: 450px; - display: flex; - align-items: center; - gap: 12px; - padding: 10px 16px; - border-radius: var(--radius-md); -} - -.search-icon { - width: 18px; - height: 18px; - color: var(--text-dimmed); -} - -.search-bar-wrapper input { - background: transparent; - border: none; - color: var(--text-main); - font-size: 14px; - outline: none; - width: 100%; -} - -.friends-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); - gap: 24px; -} - -.friend-card { - padding: 24px; - display: flex; - flex-direction: column; - gap: 20px; - cursor: pointer; - position: relative; - overflow: hidden; -} - -.friend-card::after { - content: ""; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 4px; - background: linear-gradient(90deg, var(--accent-purple), var(--accent-cyan)); - opacity: 0; - transition: var(--transition-smooth); -} - -.friend-card:hover::after { - opacity: 1; -} - -.friend-card-header { - display: flex; - align-items: center; - gap: 16px; -} - -.card-avatar { - width: 52px; - height: 52px; - border-radius: 50%; - background: linear-gradient(135deg, var(--accent-purple) 0%, hsl(260, 60%, 45%) 100%); - display: flex; - align-items: center; - justify-content: center; - font-weight: 700; - font-size: 18px; - color: white; - box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2); -} - -.card-title-block { - display: flex; - flex-direction: column; - gap: 2px; -} - -.card-title-block h3 { - font-size: 16px; - font-weight: 700; - letter-spacing: -0.3px; -} - -.card-title-block span { - font-size: 11px; - color: var(--text-muted); - text-transform: uppercase; - font-weight: 600; - letter-spacing: 0.3px; -} - -.card-body-details { - display: flex; - flex-direction: column; - gap: 10px; - font-size: 13px; - border-top: 1px solid var(--glass-border); - border-bottom: 1px solid var(--glass-border); - padding: 14px 0; - color: var(--text-muted); -} - -.card-detail-item { - display: flex; - align-items: center; - gap: 10px; -} - -.card-detail-item i { - width: 16px; - height: 16px; - color: var(--text-dimmed); -} - -.friend-card-footer { - display: flex; - justify-content: space-between; - align-items: center; -} - -.card-honor-badge { - display: flex; - align-items: center; - gap: 6px; - font-size: 13px; - font-weight: 600; - color: var(--accent-gold); -} - -.card-honor-badge i { - width: 16px; - height: 16px; -} - -.btn-open-profile { - font-size: 12px; - color: var(--accent-cyan); - background: transparent; - border: none; - cursor: pointer; - display: flex; - align-items: center; - gap: 4px; - font-weight: 600; - transition: var(--transition-smooth); -} - -.friend-card:hover .btn-open-profile { - transform: translateX(4px); - color: white; -} - -/* Modals Core Styles */ -.modal-backdrop { - position: fixed; - top: 0; - left: 0; - width: 100vw; - height: 100vh; - background: rgba(5, 4, 10, 0.75); - backdrop-filter: blur(8px); - display: flex; - align-items: center; - justify-content: center; - z-index: 1000; - opacity: 0; - pointer-events: none; - transition: opacity 0.3s ease; -} - -.modal-backdrop.open { - opacity: 1; - pointer-events: auto; -} - -.modal-container { - width: 90%; - max-width: 600px; - max-height: 85vh; - display: flex; - flex-direction: column; - overflow: hidden; - transform: scale(0.9) translateY(20px); - transition: var(--transition-bounce); - padding: 0 !important; /* Managed locally */ -} - -.modal-backdrop.open .modal-container { - transform: scale(1) translateY(0); -} - -.detail-modal-size { - max-width: 950px; -} - -.modal-header { - padding: 24px 30px; - border-bottom: 1px solid var(--glass-border); - display: flex; - justify-content: space-between; - align-items: center; -} - -.modal-header h2 { - font-family: var(--font-family-title); - font-size: 22px; - font-weight: 700; - display: flex; - align-items: center; - gap: 12px; -} - -.modal-header h2 i { - color: var(--accent-purple); -} - -.modal-close { - background: transparent; - border: none; - color: var(--text-muted); - cursor: pointer; - transition: var(--transition-smooth); -} - -.modal-close:hover { - color: var(--accent-red); - transform: rotate(90deg); -} - -.modal-scroll-area { - padding: 30px; - overflow-y: auto; - flex: 1; - display: flex; - flex-direction: column; - gap: 20px; -} - -.modal-footer { - padding: 20px 30px; - border-top: 1px solid var(--glass-border); - display: flex; - justify-content: flex-end; - gap: 14px; -} - -/* Detail Modal Specific Layout */ -.modal-title-profile { - display: flex; - align-items: center; - gap: 20px; -} - -.profile-avatar { - width: 58px; - height: 58px; - border-radius: 50%; - background: linear-gradient(135deg, var(--accent-purple) 0%, var(--accent-cyan) 100%); - display: flex; - align-items: center; - justify-content: center; - font-weight: 800; - font-size: 22px; - color: white; - box-shadow: 0 4px 15px var(--accent-purple-glow); -} - -.profile-meta-title { - display: flex; - flex-direction: column; - gap: 4px; -} - -.profile-meta-title h2 { - margin: 0; - font-size: 24px; -} - -.relationship-badge { - font-size: 11px; - background: var(--accent-purple-glow); - color: var(--accent-purple); - padding: 3px 8px; - border-radius: 30px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.3px; - align-self: flex-start; - border: 1px solid rgba(147, 51, 234, 0.25); -} - -.detail-columns { - display: grid; - grid-template-columns: 1fr 1.3fr; - gap: 30px; -} - -.detail-col-info, .detail-col-interactive { - display: flex; - flex-direction: column; - gap: 24px; -} - -/* Ehre Meter / Honor Counter */ -.honor-badge-section { - padding: 20px; - display: flex; - flex-direction: column; - gap: 12px; - align-items: center; - border-color: rgba(229, 178, 59, 0.15); - background: radial-gradient(circle at center, rgba(229, 178, 59, 0.05) 0%, rgba(18, 16, 28, 0.55) 100%); -} - -.honor-header { - display: flex; - align-items: center; - gap: 8px; - font-size: 11px; - font-weight: 700; - color: var(--accent-gold); - letter-spacing: 1px; -} - -.honor-icon { - width: 14px; - height: 14px; - filter: drop-shadow(0 0 5px var(--accent-gold)); -} - -.honor-counter-row { - display: flex; - align-items: center; - gap: 24px; -} - -.btn-honor { - width: 38px; - height: 38px; - border-radius: 50%; - border: 1px solid rgba(229, 178, 59, 0.2); - background: rgba(255, 255, 255, 0.02); - color: var(--accent-gold); - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - transition: var(--transition-bounce); -} - -.btn-honor:hover { - background: var(--accent-gold); - color: var(--bg-primary); - transform: scale(1.1); - box-shadow: 0 0 15px rgba(229, 178, 59, 0.35); -} - -.btn-honor:active { - transform: scale(0.9); -} - -.honor-value-display { - display: flex; - flex-direction: column; - align-items: center; - min-width: 70px; -} - -#detail-honor-value { - font-family: var(--font-family-title); - font-size: 38px; - font-weight: 900; - color: var(--accent-gold); - line-height: 1; -} - -/* Ehre Counter Animation class when updated */ -.honor-bump { - animation: bounce-value 0.3s ease-out; -} - -@keyframes bounce-value { - 0% { transform: scale(1); } - 50% { transform: scale(1.3); color: white; } - 100% { transform: scale(1); } -} - -.honor-label-sub { - font-size: 11px; - text-transform: uppercase; - color: var(--text-dimmed); - font-weight: 600; - letter-spacing: 0.5px; - margin-top: 4px; -} - -.detail-card { - padding: 24px; -} - -.detail-card h3, .interactive-section h3 { - font-family: var(--font-family-title); - font-size: 16px; - font-weight: 700; - margin-bottom: 16px; - display: flex; - align-items: center; - gap: 10px; - border-bottom: 1px solid var(--glass-border); - padding-bottom: 10px; -} - -.card-icon-title { - width: 18px; - height: 18px; - color: var(--accent-cyan); -} - -.info-list { - display: flex; - flex-direction: column; - gap: 12px; -} - -.info-item { - display: flex; - justify-content: space-between; - font-size: 13px; - padding: 4px 0; -} - -.info-label { - color: var(--text-muted); - font-weight: 500; - display: flex; - align-items: center; - gap: 8px; -} - -.info-label i { - width: 14px; - height: 14px; - color: var(--text-dimmed); -} - -.info-val { - font-weight: 600; - text-align: right; - max-width: 60%; -} - -.pre-wrap { - white-space: pre-wrap; -} - -.info-item-block { - display: flex; - flex-direction: column; - gap: 6px; - font-size: 13px; -} - -.info-block-text { - background: rgba(255, 255, 255, 0.01); - border: 1px solid var(--glass-border); - padding: 10px 14px; - border-radius: var(--radius-sm); - color: var(--text-main); - line-height: 1.5; -} - -/* Interactive Components inside detail modal */ -.interactive-section { - padding: 24px; -} - -.inline-add-form, .inline-meeting-form { - display: flex; - gap: 10px; - margin-bottom: 15px; -} - -.inline-meeting-form { - flex-direction: column; -} - -.inline-add-form input { - flex: 1; -} - -/* Topics checklist */ -.topics-list { - display: flex; - flex-direction: column; - gap: 8px; - list-style: none; -} - -.topic-item { - display: flex; - align-items: center; - justify-content: space-between; - padding: 10px 14px; - background: rgba(255, 255, 255, 0.01); - border: 1px solid var(--glass-border); - border-radius: var(--radius-sm); - transition: var(--transition-smooth); -} - -.topic-item:hover { - background: rgba(255, 255, 255, 0.03); -} - -.topic-item-left { - display: flex; - align-items: center; - gap: 12px; - cursor: pointer; - flex: 1; -} - -.topic-checkbox { - width: 18px; - height: 18px; - border-radius: 4px; - border: 2px solid var(--text-dimmed); - display: flex; - align-items: center; - justify-content: center; - transition: var(--transition-bounce); -} - -.topic-checkbox i { - width: 12px; - height: 12px; - color: white; - display: none; -} - -.topic-item.completed .topic-checkbox { - border-color: var(--accent-cyan); - background: var(--accent-cyan); -} - -.topic-item.completed .topic-checkbox i { - display: block; -} - -.topic-text { - font-size: 13px; - font-weight: 500; - transition: var(--transition-smooth); -} - -.topic-item.completed .topic-text { - text-decoration: line-through; - color: var(--text-dimmed); -} - -.btn-topic-delete { - background: transparent; - border: none; - color: var(--text-dimmed); - cursor: pointer; - transition: var(--transition-smooth); -} - -.btn-topic-delete:hover { - color: var(--accent-red); -} - -/* Meetings timeline */ -.timeline { - display: flex; - flex-direction: column; - position: relative; - padding-left: 20px; -} - -.timeline::before { - content: ""; - position: absolute; - top: 5px; - left: 5px; - width: 2px; - height: 100%; - background: var(--accent-purple-glow); -} - -.timeline-item { - position: relative; - padding-bottom: 24px; -} - -.timeline-item:last-child { - padding-bottom: 0; -} - -.timeline-dot { - position: absolute; - left: -20px; - top: 4px; - width: 12px; - height: 12px; - border-radius: 50%; - background: var(--accent-purple); - box-shadow: 0 0 8px var(--accent-purple); -} - -.timeline-content { - background: rgba(255, 255, 255, 0.01); - border: 1px solid var(--glass-border); - padding: 14px 18px; - border-radius: var(--radius-md); -} - -.timeline-meta { - display: flex; - justify-content: space-between; - margin-bottom: 8px; - font-size: 11px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.timeline-date { - color: var(--accent-cyan); -} - -.timeline-mood { - color: var(--accent-gold); -} - -.timeline-activity { - font-weight: 700; - font-size: 14px; - margin-bottom: 6px; -} - -.timeline-details { - font-size: 13px; - color: var(--text-muted); - line-height: 1.5; -} - -.btn-delete-meeting { - background: transparent; - border: none; - color: var(--text-dimmed); - cursor: pointer; - font-size: 11px; - margin-top: 10px; - display: inline-flex; - align-items: center; - gap: 4px; - transition: var(--transition-smooth); -} - -.btn-delete-meeting:hover { - color: var(--accent-red); -} - -/* Obsidian Import tab specifically styling */ -.import-container { - max-width: 800px; - margin: 0 auto; -} - -.import-result-panel { - margin-top: 25px; - padding: 24px; - border-color: var(--accent-cyan); - background: radial-gradient(circle at top left, rgba(6, 182, 212, 0.05) 0%, rgba(18, 16, 28, 0.55) 100%); - display: flex; - flex-direction: column; - gap: 10px; -} - -.import-result-header { - font-family: var(--font-family-title); - font-size: 18px; - font-weight: 700; - color: var(--accent-cyan); - display: flex; - align-items: center; - gap: 10px; -} - -.hidden { - display: none !important; -} - -/* Utilities */ -.loading-spinner { - border: 3px solid rgba(255, 255, 255, 0.05); - border-radius: 50%; - border-top: 3px solid var(--accent-purple); - width: 24px; - height: 24px; - animation: spin 1s linear infinite; - margin: 20px auto; -} - -@keyframes spin { - 0% { transform: rotate(0deg); } - 100% { transform: rotate(360deg); } -} - -/* Responsive Overrides */ -@media (max-width: 1024px) { - .app-container { - grid-template-columns: 1fr; - } - - .sidebar { - display: none; /* In production: a top drawer menu would be implemented */ - } - - .detail-columns { - grid-template-columns: 1fr; - } -} diff --git a/public/web.config b/public/web.config new file mode 100644 index 0000000..624c176 --- /dev/null +++ b/public/web.config @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/js/app.js b/resources/js/app.js new file mode 100644 index 0000000..741e6cf --- /dev/null +++ b/resources/js/app.js @@ -0,0 +1,343 @@ + +/** + * First we will load all of this project's JavaScript dependencies which + * includes Vue and other libraries. It is a great starting point when + * building robust, powerful web applications using Vue and Laravel. + */ + +require('./bootstrap'); + +/** + * Next, we will create a fresh Vue application instance and attach it to + * the page. Then, you may begin adding components to this application + * or customize the JavaScript scaffolding to fit your unique needs. + */ + +import Vue from 'vue'; +window.Vue = Vue; + +// Notifications +import Notifications from 'vue-notification'; +Vue.use(Notifications); + +// Tooltip +import Tooltip from 'vue-directive-tooltip'; +Vue.use(Tooltip, { delay: 0 }); + +// Copy text from clipboard +import VueClipboard from 'vue-clipboard2'; +VueClipboard.config.autoSetContainer = true; +Vue.use(VueClipboard); + +// Dependency of vuejs-clipper +import VueRx from 'vue-rx'; +Vue.use(VueRx); + +// Custom components +Vue.component( + 'PassportClients', + require('./components/passport/Clients.vue').default +); + +Vue.component( + 'PassportAuthorizedClients', + require('./components/passport/AuthorizedClients.vue').default +); + +Vue.component( + 'PassportPersonalAccessTokens', + require('./components/passport/PersonalAccessTokens.vue').default +); + +// Vue select +Vue.component( + 'ContactSelect', + require('./components/people/ContactSelect.vue').default +); +Vue.component( + 'ContactSearch', + require('./components/people/ContactSearch.vue').default +); +Vue.component( + 'ContactMultiSearch', + require('./components/people/ContactMultiSearch.vue').default +); + +// Partials +Vue.component( + 'Avatar', + require('./components/partials/Avatar.vue').default +); +Vue.component( + 'Confirm', + require('./components/partials/Confirm.vue').default +); + +// Form elements +Vue.component( + 'FormInput', + require('./components/partials/form/Input.vue').default +); +Vue.component( + 'FormSelect', + require('./components/partials/form/Select.vue').default +); +Vue.component( + 'FormDate', + require('./components/partials/form/Date.vue').default +); +Vue.component( + 'FormCheckbox', + require('./components/partials/form/Checkbox.vue').default +); +Vue.component( + 'FormRadio', + require('./components/partials/form/Radio.vue').default +); +Vue.component( + 'FormTextarea', + require('./components/partials/form/Textarea.vue').default +); +Vue.component( + 'FormToggle', + require('./components/partials/form/Toggle.vue').default +); +Vue.component( + 'FormSpecialdate', + require('./components/partials/SpecialDate.vue').default +); +Vue.component( + 'FormSpecialdeceased', + require('./components/partials/SpecialDeceased.vue').default +); + +// Dashboard +Vue.component( + 'DashboardLog', + require('./components/dashboard/DashboardLog.vue').default +); + +// Contacts +Vue.component( + 'Tags', + require('./components/people/Tags.vue').default +); + +Vue.component( + 'ContactAvatar', + require('./components/people/SetAvatar.vue').default +); +Vue.component( + 'ContactFavorite', + require('./components/people/SetFavorite.vue').default +); + +Vue.component( + 'ContactArchive', + require('./components/people/Archive.vue').default +); + +Vue.component( + 'ContactAddress', + require('./components/people/Addresses.vue').default +); + +Vue.component( + 'ContactInformation', + require('./components/people/ContactInformation.vue').default +); + +Vue.component( + 'ContactList', + require('./components/people/ContactList.vue').default +); + +Vue.component( + 'ContactTask', + require('./components/people/Tasks.vue').default +); + +Vue.component( + 'ContactNote', + require('./components/people/Notes.vue').default +); + +Vue.component( + 'ContactGift', + require('./components/people/gifts/Gifts.vue').default +); + +Vue.component( + 'Pet', + require('./components/people/Pets.vue').default +); + +Vue.component( + 'MeContact', + require('./components/people/MeContact.vue').default +); +Vue.component( + 'StayInTouch', + require('./components/people/StayInTouch.vue').default +); + +Vue.component( + 'LastCalled', + require('./components/people/calls/LastCalled.vue').default +); + +Vue.component( + 'PhoneCallList', + require('./components/people/calls/PhoneCallList.vue').default +); + +Vue.component( + 'ConversationList', + require('./components/people/conversation/ConversationList.vue').default +); + +Vue.component( + 'Conversation', + require('./components/people/conversation/Conversation.vue').default +); + +Vue.component( + 'Message', + require('./components/people/conversation/Message.vue').default +); + +Vue.component( + 'ActivityList', + require('./components/people/activity/ActivityList.vue').default +); + +Vue.component( + 'DocumentList', + require('./components/people/document/DocumentList.vue').default +); + +Vue.component( + 'CreateLifeEvent', + require('./components/people/lifeevent/CreateLifeEvent.vue').default +); + +Vue.component( + 'CreateDefaultLifeEvent', + require('./components/people/lifeevent/content/CreateDefaultLifeEvent.vue').default +); + +Vue.component( + 'LifeEventList', + require('./components/people/lifeevent/LifeEventList.vue').default +); + +Vue.component( + 'PhotoList', + require('./components/people/photo/PhotoList.vue').default +); + +// Journal +Vue.component( + 'JournalList', + require('./components/journal/JournalList.vue').default +); + +Vue.component( + 'JournalRateDay', + require('./components/journal/RateDay.vue').default +); + +Vue.component( + 'JournalCalendar', + require('./components/journal/partials/JournalCalendar.vue').default +); + +Vue.component( + 'JournalContentRate', + require('./components/journal/partials/JournalContentRate.vue').default +); + +Vue.component( + 'JournalContentActivity', + require('./components/journal/partials/JournalContentActivity.vue').default +); + +Vue.component( + 'JournalContentEntry', + require('./components/journal/partials/JournalContentEntry.vue').default +); + +// Settings +Vue.component( + 'ContactFieldTypes', + require('./components/settings/ContactFieldTypes.vue').default +); +Vue.component( + 'Genders', + require('./components/settings/Genders.vue').default +); +Vue.component( + 'ReminderRules', + require('./components/settings/ReminderRules.vue').default +); +Vue.component( + 'ReminderTime', + require('./components/settings/ReminderTime.vue').default +); +Vue.component( + 'MfaActivate', + require('./components/settings/MfaActivate.vue').default +); +Vue.component( + 'WebauthnConnector', + require('./components/settings/WebauthnConnector.vue').default +); +Vue.component( + 'RecoveryCodes', + require('./components/settings/RecoveryCodes.vue').default +); +Vue.component( + 'Modules', + require('./components/settings/Modules.vue').default +); +Vue.component( + 'ActivityTypes', + require('./components/settings/ActivityTypes.vue').default +); +Vue.component( + 'LifeEventTypes', + require('./components/settings/LifeEventTypes.vue').default +); +Vue.component( + 'DavResources', + require('./components/settings/DAVResources.vue').default +); + +require('./testing'); + +var common = require('./common').default; + +common.loadLanguage(window.Laravel.locale, true).then((i18n) => { + // the Vue appplication + const app = new Vue({ + i18n, + data: { + htmldir: window.Laravel.htmldir, + timezone: window.Laravel.timezone, + locale: i18n.locale, + reminders_frequency: 'once', + accept_invite_user: false, + date_met_the_contact: 'known', + global_relationship_form_new_contact: true, + global_profile_default_view: window.Laravel.profileDefaultView, + }, + + // global methods + methods: require('./methods').default + }).$mount('#app'); + + return app; +}); + +$(document).ready(function() { +}); diff --git a/resources/js/bootstrap.js b/resources/js/bootstrap.js new file mode 100644 index 0000000..9bac46f --- /dev/null +++ b/resources/js/bootstrap.js @@ -0,0 +1,47 @@ +window._ = require('lodash'); + +/** + * We'll load jQuery and the Bootstrap jQuery plugin which provides support + * for JavaScript based Bootstrap features such as modals and tabs. This + * code may be modified to fit the specific needs of your application. + */ + +try { + window.Popper = require('popper.js').default; + window.$ = window.jQuery = require('jquery'); + + require('bootstrap/js/dist/util'); + require('bootstrap/js/dist/button'); + require('bootstrap/js/dist/collapse'); + require('bootstrap/js/dist/dropdown'); + require('bootstrap/js/dist/modal'); + require('bootstrap/js/dist/tab'); +} catch (e) {} + + +/** + * We'll load the axios HTTP library which allows us to easily issue requests + * to our Laravel back-end. This library automatically handles sending the + * CSRF token as a header based on the value of the "XSRF" token cookie. + */ + +window.axios = require('axios'); + +window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; + +/** + * Echo exposes an expressive API for subscribing to channels and listening + * for events that are broadcast by Laravel. Echo and event broadcasting + * allows your team to easily build robust real-time web applications. + */ + +// import Echo from 'laravel-echo' + +// window.Pusher = require('pusher-js'); + +// window.Echo = new Echo({ +// broadcaster: 'pusher', +// key: process.env.MIX_PUSHER_APP_KEY, +// cluster: process.env.MIX_PUSHER_APP_CLUSTER, +// encrypted: true +// }); diff --git a/resources/js/common.js b/resources/js/common.js new file mode 100644 index 0000000..d0c74e9 --- /dev/null +++ b/resources/js/common.js @@ -0,0 +1,63 @@ +'use strict'; + +// axios +import axios from 'axios'; + +// i18n +import VueI18n from 'vue-i18n'; +Vue.use(VueI18n); + +// Moments +import moment from 'moment'; +Vue.filter('formatDate', function(value) { + if (value) { + return moment(String(value)).format('LL'); + } +}); + +// Markdown +window.marked = require('marked'); + +// i18n +import messages from '../../public/js/langs/en.json'; +import pluralization from './pluralization.js'; + +export default { + i18n: new VueI18n({ + locale: 'en', // set locale + fallbackLocale: 'en', + messages: {'en': messages}, + pluralizationRules: pluralization, + }), + + loadedLanguages : ['en'], // our default language that is preloaded + + _setI18nLanguage (lang) { + this.i18n.locale = lang; + axios.defaults.headers.common['Accept-Language'] = lang; + document.querySelector('html').setAttribute('lang', lang); + }, + + _loadLanguageAsync (lang) { + if (this.i18n.locale !== lang) { + if (!this.loadedLanguages.includes(lang)) { + return axios.get(`js/langs/${lang}.json`).then(msgs => { + this.i18n.setLocaleMessage(lang, msgs.data); + this.loadedLanguages.push(lang); + return this.i18n; + }); + } + } + return Promise.resolve(this.i18n); + }, + + loadLanguage: function(lang, set) { + return this._loadLanguageAsync(lang).then(i18n => { + if (set) { + this._setI18nLanguage(lang); + } + moment.locale(lang === 'zh' ? 'zh-cn' : lang); + return i18n; + }); + } +}; \ No newline at end of file diff --git a/resources/js/components/dashboard/DashboardLog.vue b/resources/js/components/dashboard/DashboardLog.vue new file mode 100644 index 0000000..6ce17db --- /dev/null +++ b/resources/js/components/dashboard/DashboardLog.vue @@ -0,0 +1,457 @@ + + + diff --git a/resources/js/components/journal/JournalList.vue b/resources/js/components/journal/JournalList.vue new file mode 100644 index 0000000..c5e0868 --- /dev/null +++ b/resources/js/components/journal/JournalList.vue @@ -0,0 +1,217 @@ + + + + + diff --git a/resources/js/components/journal/RateDay.vue b/resources/js/components/journal/RateDay.vue new file mode 100644 index 0000000..8e769f4 --- /dev/null +++ b/resources/js/components/journal/RateDay.vue @@ -0,0 +1,250 @@ + + + + + diff --git a/resources/js/components/journal/partials/JournalCalendar.vue b/resources/js/components/journal/partials/JournalCalendar.vue new file mode 100644 index 0000000..d3d9fbd --- /dev/null +++ b/resources/js/components/journal/partials/JournalCalendar.vue @@ -0,0 +1,104 @@ + + + diff --git a/resources/js/components/journal/partials/JournalContentActivity.vue b/resources/js/components/journal/partials/JournalContentActivity.vue new file mode 100644 index 0000000..ee3a38b --- /dev/null +++ b/resources/js/components/journal/partials/JournalContentActivity.vue @@ -0,0 +1,150 @@ + + + + + diff --git a/resources/js/components/journal/partials/JournalContentEntry.vue b/resources/js/components/journal/partials/JournalContentEntry.vue new file mode 100644 index 0000000..9948d5f --- /dev/null +++ b/resources/js/components/journal/partials/JournalContentEntry.vue @@ -0,0 +1,104 @@ + + + diff --git a/resources/js/components/journal/partials/JournalContentRate.vue b/resources/js/components/journal/partials/JournalContentRate.vue new file mode 100644 index 0000000..0d5a6e7 --- /dev/null +++ b/resources/js/components/journal/partials/JournalContentRate.vue @@ -0,0 +1,185 @@ + + + diff --git a/resources/js/components/partials/Avatar.img.vue b/resources/js/components/partials/Avatar.img.vue new file mode 100644 index 0000000..4abbcfc --- /dev/null +++ b/resources/js/components/partials/Avatar.img.vue @@ -0,0 +1,58 @@ + + + + + diff --git a/resources/js/components/partials/Avatar.vue b/resources/js/components/partials/Avatar.vue new file mode 100644 index 0000000..036ae52 --- /dev/null +++ b/resources/js/components/partials/Avatar.vue @@ -0,0 +1,38 @@ + + + diff --git a/resources/js/components/partials/Confirm.vue b/resources/js/components/partials/Confirm.vue new file mode 100644 index 0000000..57073c1 --- /dev/null +++ b/resources/js/components/partials/Confirm.vue @@ -0,0 +1,74 @@ + + + diff --git a/resources/js/components/partials/Error.vue b/resources/js/components/partials/Error.vue new file mode 100644 index 0000000..799dc06 --- /dev/null +++ b/resources/js/components/partials/Error.vue @@ -0,0 +1,51 @@ + + + diff --git a/resources/js/components/partials/SpecialDate.vue b/resources/js/components/partials/SpecialDate.vue new file mode 100644 index 0000000..00bff32 --- /dev/null +++ b/resources/js/components/partials/SpecialDate.vue @@ -0,0 +1,255 @@ + + + + + diff --git a/resources/js/components/partials/SpecialDeceased.vue b/resources/js/components/partials/SpecialDeceased.vue new file mode 100644 index 0000000..e625755 --- /dev/null +++ b/resources/js/components/partials/SpecialDeceased.vue @@ -0,0 +1,133 @@ + + + + + diff --git a/resources/js/components/partials/form/Checkbox.vue b/resources/js/components/partials/form/Checkbox.vue new file mode 100644 index 0000000..79f45ef --- /dev/null +++ b/resources/js/components/partials/form/Checkbox.vue @@ -0,0 +1,23 @@ + diff --git a/resources/js/components/partials/form/Date.vue b/resources/js/components/partials/form/Date.vue new file mode 100644 index 0000000..59bb471 --- /dev/null +++ b/resources/js/components/partials/form/Date.vue @@ -0,0 +1,206 @@ + + + diff --git a/resources/js/components/partials/form/Input.vue b/resources/js/components/partials/form/Input.vue new file mode 100644 index 0000000..681ca61 --- /dev/null +++ b/resources/js/components/partials/form/Input.vue @@ -0,0 +1,181 @@ + + + + + diff --git a/resources/js/components/partials/form/PInput.vue b/resources/js/components/partials/form/PInput.vue new file mode 100644 index 0000000..29a5982 --- /dev/null +++ b/resources/js/components/partials/form/PInput.vue @@ -0,0 +1,137 @@ + + + diff --git a/resources/js/components/partials/form/Radio.vue b/resources/js/components/partials/form/Radio.vue new file mode 100644 index 0000000..3990464 --- /dev/null +++ b/resources/js/components/partials/form/Radio.vue @@ -0,0 +1,23 @@ + diff --git a/resources/js/components/partials/form/Select.vue b/resources/js/components/partials/form/Select.vue new file mode 100644 index 0000000..d0d64c4 --- /dev/null +++ b/resources/js/components/partials/form/Select.vue @@ -0,0 +1,164 @@ + + + + + diff --git a/resources/js/components/partials/form/Textarea.vue b/resources/js/components/partials/form/Textarea.vue new file mode 100644 index 0000000..1f12a1c --- /dev/null +++ b/resources/js/components/partials/form/Textarea.vue @@ -0,0 +1,96 @@ + + + + + diff --git a/resources/js/components/partials/form/Toggle.vue b/resources/js/components/partials/form/Toggle.vue new file mode 100644 index 0000000..7acac95 --- /dev/null +++ b/resources/js/components/partials/form/Toggle.vue @@ -0,0 +1,91 @@ + + + diff --git a/resources/js/components/passport/AuthorizedClients.vue b/resources/js/components/passport/AuthorizedClients.vue new file mode 100644 index 0000000..a0d5a54 --- /dev/null +++ b/resources/js/components/passport/AuthorizedClients.vue @@ -0,0 +1,96 @@ + + + diff --git a/resources/js/components/passport/Clients.vue b/resources/js/components/passport/Clients.vue new file mode 100644 index 0000000..817d72a --- /dev/null +++ b/resources/js/components/passport/Clients.vue @@ -0,0 +1,316 @@ + + + diff --git a/resources/js/components/passport/PersonalAccessTokens.vue b/resources/js/components/passport/PersonalAccessTokens.vue new file mode 100644 index 0000000..8c219ff --- /dev/null +++ b/resources/js/components/passport/PersonalAccessTokens.vue @@ -0,0 +1,338 @@ + + + + + diff --git a/resources/js/components/people/Addresses.vue b/resources/js/components/people/Addresses.vue new file mode 100644 index 0000000..8793688 --- /dev/null +++ b/resources/js/components/people/Addresses.vue @@ -0,0 +1,425 @@ + + + diff --git a/resources/js/components/people/Archive.vue b/resources/js/components/people/Archive.vue new file mode 100644 index 0000000..b52d71d --- /dev/null +++ b/resources/js/components/people/Archive.vue @@ -0,0 +1,66 @@ + + + + + diff --git a/resources/js/components/people/ContactInformation.vue b/resources/js/components/people/ContactInformation.vue new file mode 100644 index 0000000..6157e3d --- /dev/null +++ b/resources/js/components/people/ContactInformation.vue @@ -0,0 +1,260 @@ + + + diff --git a/resources/js/components/people/ContactList.vue b/resources/js/components/people/ContactList.vue new file mode 100644 index 0000000..bc9f33d --- /dev/null +++ b/resources/js/components/people/ContactList.vue @@ -0,0 +1,251 @@ + + + + + + + diff --git a/resources/js/components/people/ContactMultiSearch.vue b/resources/js/components/people/ContactMultiSearch.vue new file mode 100644 index 0000000..fbfa531 --- /dev/null +++ b/resources/js/components/people/ContactMultiSearch.vue @@ -0,0 +1,106 @@ + + + + + diff --git a/resources/js/components/people/ContactSearch.vue b/resources/js/components/people/ContactSearch.vue new file mode 100644 index 0000000..2a91d9e --- /dev/null +++ b/resources/js/components/people/ContactSearch.vue @@ -0,0 +1,101 @@ + + + + + diff --git a/resources/js/components/people/ContactSelect.vue b/resources/js/components/people/ContactSelect.vue new file mode 100644 index 0000000..b9f0022 --- /dev/null +++ b/resources/js/components/people/ContactSelect.vue @@ -0,0 +1,146 @@ + + + diff --git a/resources/js/components/people/Emotion.vue b/resources/js/components/people/Emotion.vue new file mode 100644 index 0000000..2807f42 --- /dev/null +++ b/resources/js/components/people/Emotion.vue @@ -0,0 +1,193 @@ + + + + + diff --git a/resources/js/components/people/MeContact.vue b/resources/js/components/people/MeContact.vue new file mode 100644 index 0000000..c7203fd --- /dev/null +++ b/resources/js/components/people/MeContact.vue @@ -0,0 +1,138 @@ + + + + + diff --git a/resources/js/components/people/Notes.vue b/resources/js/components/people/Notes.vue new file mode 100644 index 0000000..e157080 --- /dev/null +++ b/resources/js/components/people/Notes.vue @@ -0,0 +1,229 @@ + + + + + diff --git a/resources/js/components/people/Participant.vue b/resources/js/components/people/Participant.vue new file mode 100644 index 0000000..b18a996 --- /dev/null +++ b/resources/js/components/people/Participant.vue @@ -0,0 +1,128 @@ + + + + + diff --git a/resources/js/components/people/Pets.vue b/resources/js/components/people/Pets.vue new file mode 100644 index 0000000..1ef402d --- /dev/null +++ b/resources/js/components/people/Pets.vue @@ -0,0 +1,249 @@ + + + diff --git a/resources/js/components/people/SetAvatar.vue b/resources/js/components/people/SetAvatar.vue new file mode 100644 index 0000000..02c2339 --- /dev/null +++ b/resources/js/components/people/SetAvatar.vue @@ -0,0 +1,219 @@ + + + diff --git a/resources/js/components/people/SetFavorite.vue b/resources/js/components/people/SetFavorite.vue new file mode 100644 index 0000000..77ca632 --- /dev/null +++ b/resources/js/components/people/SetFavorite.vue @@ -0,0 +1,63 @@ + + + diff --git a/resources/js/components/people/StayInTouch.vue b/resources/js/components/people/StayInTouch.vue new file mode 100644 index 0000000..b3b2513 --- /dev/null +++ b/resources/js/components/people/StayInTouch.vue @@ -0,0 +1,304 @@ + + + + + diff --git a/resources/js/components/people/StayInTouchLabel.vue b/resources/js/components/people/StayInTouchLabel.vue new file mode 100644 index 0000000..f245959 --- /dev/null +++ b/resources/js/components/people/StayInTouchLabel.vue @@ -0,0 +1,20 @@ + diff --git a/resources/js/components/people/Tags.vue b/resources/js/components/people/Tags.vue new file mode 100644 index 0000000..67d38c2 --- /dev/null +++ b/resources/js/components/people/Tags.vue @@ -0,0 +1,231 @@ + + + + + diff --git a/resources/js/components/people/Tasks.vue b/resources/js/components/people/Tasks.vue new file mode 100644 index 0000000..95a0cb0 --- /dev/null +++ b/resources/js/components/people/Tasks.vue @@ -0,0 +1,286 @@ + + + diff --git a/resources/js/components/people/activity/ActivityList.vue b/resources/js/components/people/activity/ActivityList.vue new file mode 100644 index 0000000..b29a00d --- /dev/null +++ b/resources/js/components/people/activity/ActivityList.vue @@ -0,0 +1,237 @@ + + + + + diff --git a/resources/js/components/people/activity/ActivityTypeList.vue b/resources/js/components/people/activity/ActivityTypeList.vue new file mode 100644 index 0000000..85e8ab0 --- /dev/null +++ b/resources/js/components/people/activity/ActivityTypeList.vue @@ -0,0 +1,58 @@ + + + diff --git a/resources/js/components/people/activity/CreateActivity.vue b/resources/js/components/people/activity/CreateActivity.vue new file mode 100644 index 0000000..1443e37 --- /dev/null +++ b/resources/js/components/people/activity/CreateActivity.vue @@ -0,0 +1,297 @@ + + + diff --git a/resources/js/components/people/calls/LastCalled.vue b/resources/js/components/people/calls/LastCalled.vue new file mode 100644 index 0000000..eba570b --- /dev/null +++ b/resources/js/components/people/calls/LastCalled.vue @@ -0,0 +1,47 @@ + + + diff --git a/resources/js/components/people/calls/PhoneCallList.vue b/resources/js/components/people/calls/PhoneCallList.vue new file mode 100644 index 0000000..a2a0020 --- /dev/null +++ b/resources/js/components/people/calls/PhoneCallList.vue @@ -0,0 +1,417 @@ + + + diff --git a/resources/js/components/people/conversation/Conversation.vue b/resources/js/components/people/conversation/Conversation.vue new file mode 100644 index 0000000..25d1ea2 --- /dev/null +++ b/resources/js/components/people/conversation/Conversation.vue @@ -0,0 +1,114 @@ + + + + + diff --git a/resources/js/components/people/conversation/ConversationList.vue b/resources/js/components/people/conversation/ConversationList.vue new file mode 100644 index 0000000..d145321 --- /dev/null +++ b/resources/js/components/people/conversation/ConversationList.vue @@ -0,0 +1,93 @@ + + + diff --git a/resources/js/components/people/conversation/Message.vue b/resources/js/components/people/conversation/Message.vue new file mode 100644 index 0000000..46395b3 --- /dev/null +++ b/resources/js/components/people/conversation/Message.vue @@ -0,0 +1,139 @@ + + + + + diff --git a/resources/js/components/people/document/DocumentList.vue b/resources/js/components/people/document/DocumentList.vue new file mode 100644 index 0000000..4884e97 --- /dev/null +++ b/resources/js/components/people/document/DocumentList.vue @@ -0,0 +1,345 @@ + + + + + diff --git a/resources/js/components/people/gifts/CreateGift.vue b/resources/js/components/people/gifts/CreateGift.vue new file mode 100644 index 0000000..d2a62c7 --- /dev/null +++ b/resources/js/components/people/gifts/CreateGift.vue @@ -0,0 +1,460 @@ + + + + + diff --git a/resources/js/components/people/gifts/Gift.vue b/resources/js/components/people/gifts/Gift.vue new file mode 100644 index 0000000..122f41e --- /dev/null +++ b/resources/js/components/people/gifts/Gift.vue @@ -0,0 +1,120 @@ + + + + + diff --git a/resources/js/components/people/gifts/Gifts.vue b/resources/js/components/people/gifts/Gifts.vue new file mode 100644 index 0000000..161922b --- /dev/null +++ b/resources/js/components/people/gifts/Gifts.vue @@ -0,0 +1,257 @@ + + + diff --git a/resources/js/components/people/lifeevent/CreateLifeEvent.vue b/resources/js/components/people/lifeevent/CreateLifeEvent.vue new file mode 100644 index 0000000..d15100f --- /dev/null +++ b/resources/js/components/people/lifeevent/CreateLifeEvent.vue @@ -0,0 +1,294 @@ + + + diff --git a/resources/js/components/people/lifeevent/LifeEventList.vue b/resources/js/components/people/lifeevent/LifeEventList.vue new file mode 100644 index 0000000..04f3864 --- /dev/null +++ b/resources/js/components/people/lifeevent/LifeEventList.vue @@ -0,0 +1,649 @@ + + + + + diff --git a/resources/js/components/people/lifeevent/content/CreateDefaultLifeEvent.vue b/resources/js/components/people/lifeevent/content/CreateDefaultLifeEvent.vue new file mode 100644 index 0000000..f6b3e9a --- /dev/null +++ b/resources/js/components/people/lifeevent/content/CreateDefaultLifeEvent.vue @@ -0,0 +1,61 @@ + + + diff --git a/resources/js/components/people/partials/ContactAutosuggest.vue b/resources/js/components/people/partials/ContactAutosuggest.vue new file mode 100644 index 0000000..c95acef --- /dev/null +++ b/resources/js/components/people/partials/ContactAutosuggest.vue @@ -0,0 +1,226 @@ + + + + + diff --git a/resources/js/components/people/partials/ContactItem.vue b/resources/js/components/people/partials/ContactItem.vue new file mode 100644 index 0000000..821ff2d --- /dev/null +++ b/resources/js/components/people/partials/ContactItem.vue @@ -0,0 +1,106 @@ + + + + + diff --git a/resources/js/components/people/partials/ContactMultiItem.vue b/resources/js/components/people/partials/ContactMultiItem.vue new file mode 100644 index 0000000..044b9e4 --- /dev/null +++ b/resources/js/components/people/partials/ContactMultiItem.vue @@ -0,0 +1,97 @@ + + + + + diff --git a/resources/js/components/people/photo/PhotoList.vue b/resources/js/components/people/photo/PhotoList.vue new file mode 100644 index 0000000..2dee7fd --- /dev/null +++ b/resources/js/components/people/photo/PhotoList.vue @@ -0,0 +1,279 @@ + + + + + diff --git a/resources/js/components/people/photo/PhotoUpload.vue b/resources/js/components/people/photo/PhotoUpload.vue new file mode 100644 index 0000000..d1ebd90 --- /dev/null +++ b/resources/js/components/people/photo/PhotoUpload.vue @@ -0,0 +1,179 @@ + + + + + diff --git a/resources/js/components/settings/ActivityTypes.vue b/resources/js/components/settings/ActivityTypes.vue new file mode 100644 index 0000000..306f596 --- /dev/null +++ b/resources/js/components/settings/ActivityTypes.vue @@ -0,0 +1,478 @@ + + + diff --git a/resources/js/components/settings/ContactFieldTypes.vue b/resources/js/components/settings/ContactFieldTypes.vue new file mode 100644 index 0000000..f9c7f6f --- /dev/null +++ b/resources/js/components/settings/ContactFieldTypes.vue @@ -0,0 +1,392 @@ + + + diff --git a/resources/js/components/settings/DAVResources.vue b/resources/js/components/settings/DAVResources.vue new file mode 100644 index 0000000..4b49ba3 --- /dev/null +++ b/resources/js/components/settings/DAVResources.vue @@ -0,0 +1,128 @@ + + + + + diff --git a/resources/js/components/settings/Genders.vue b/resources/js/components/settings/Genders.vue new file mode 100644 index 0000000..5436cc6 --- /dev/null +++ b/resources/js/components/settings/Genders.vue @@ -0,0 +1,443 @@ + + + diff --git a/resources/js/components/settings/LifeEventTypes.vue b/resources/js/components/settings/LifeEventTypes.vue new file mode 100644 index 0000000..078bf62 --- /dev/null +++ b/resources/js/components/settings/LifeEventTypes.vue @@ -0,0 +1,300 @@ + + + diff --git a/resources/js/components/settings/MfaActivate.vue b/resources/js/components/settings/MfaActivate.vue new file mode 100644 index 0000000..80a7add --- /dev/null +++ b/resources/js/components/settings/MfaActivate.vue @@ -0,0 +1,188 @@ + + + diff --git a/resources/js/components/settings/Modules.vue b/resources/js/components/settings/Modules.vue new file mode 100644 index 0000000..b8335c2 --- /dev/null +++ b/resources/js/components/settings/Modules.vue @@ -0,0 +1,109 @@ + + + diff --git a/resources/js/components/settings/RecoveryCodes.vue b/resources/js/components/settings/RecoveryCodes.vue new file mode 100644 index 0000000..b3e6655 --- /dev/null +++ b/resources/js/components/settings/RecoveryCodes.vue @@ -0,0 +1,139 @@ + + + + + diff --git a/resources/js/components/settings/ReminderRules.vue b/resources/js/components/settings/ReminderRules.vue new file mode 100644 index 0000000..a77a1a1 --- /dev/null +++ b/resources/js/components/settings/ReminderRules.vue @@ -0,0 +1,92 @@ + + + diff --git a/resources/js/components/settings/ReminderTime.vue b/resources/js/components/settings/ReminderTime.vue new file mode 100644 index 0000000..09b34c7 --- /dev/null +++ b/resources/js/components/settings/ReminderTime.vue @@ -0,0 +1,108 @@ + + + diff --git a/resources/js/components/settings/Subscription.vue b/resources/js/components/settings/Subscription.vue new file mode 100644 index 0000000..e2a06f1 --- /dev/null +++ b/resources/js/components/settings/Subscription.vue @@ -0,0 +1,297 @@ + + + diff --git a/resources/js/components/settings/WebauthnConnector.vue b/resources/js/components/settings/WebauthnConnector.vue new file mode 100644 index 0000000..36421e1 --- /dev/null +++ b/resources/js/components/settings/WebauthnConnector.vue @@ -0,0 +1,402 @@ + + + + + diff --git a/resources/js/methods.js b/resources/js/methods.js new file mode 100644 index 0000000..52cdeeb --- /dev/null +++ b/resources/js/methods.js @@ -0,0 +1,23 @@ +export default { + /** + * Update the default tab view. + * + * @param {string} view + */ + updateDefaultProfileView(view) { + axios.post('settings/updateDefaultProfileView', { name: view }) + .then(response => { + this.global_profile_default_view = view; + }); + }, + + /** + * Fix avatar in case img is on error. + * + * @param {event} event + */ + fixAvatarDisplay(event) { + event.srcElement.classList = ['hidden']; + event.srcElement.nextElementSibling.classList.remove('hidden'); + } +}; diff --git a/resources/js/pluralization.js b/resources/js/pluralization.js new file mode 100644 index 0000000..2553856 --- /dev/null +++ b/resources/js/pluralization.js @@ -0,0 +1,53 @@ +/** + * Pluralization form for every langage not following engl-ish like form. + * + * 'plurals' functions represent possible plural form transformations. + * This return a list of lang/plural function to apply. + * + * @see https://github.com/laravel/framework/blob/master/src/Illuminate/Translation/MessageSelector.php + */ + +function pluralA (choice, choicesLength) { + return 0; +} +function pluralB (choice, choicesLength) { + let number = Math.abs(choice); + number = ((number == 0) || (number == 1)) ? 0 : 1; + return Math.min(number, choicesLength - 1); +} +function pluralC (choice, choicesLength) { + let number = Math.abs(choice); + number = (number == 1) ? 0 : (((number >= 2) && (number <= 4)) ? 1 : 2); + return Math.min(number, choicesLength - 1); +} +function pluralD (choice, choicesLength) { + let number = Math.abs(choice); + number = ((number % 10 == 1) && (number % 100 != 11)) ? 0 : (((number % 10 >= 2) && (number % 10 <= 4) && ((number % 100 < 10) || (number % 100 >= 20))) ? 1 : 2); + return Math.min(number, choicesLength - 1); +} +function pluralE (choice, choicesLength) { + let number = Math.abs(choice); + number = (number == 0) ? 0 : ((number == 1) ? 1 : ((number == 2) ? 2 : (((number % 100 >= 3) && (number % 100 <= 10)) ? 3 : (((number % 100 >= 11) && (number % 100 <= 99)) ? 4 : 5)))); + return Math.min(number, choicesLength - 1); +} +function pluralF (choice, choicesLength) { + let number = Math.abs(choice); + number = (number == 1) ? 0 : ((number == 2) ? 1 : (number < 10 && number % 10 == 0) ? 2 : 3); + return Math.min(number, choicesLength - 1); +} + +export default { + 'ar': pluralE, + 'cs': pluralC, + 'fr': pluralB, + 'he': pluralF, + 'hr': pluralD, + 'id': pluralA, + 'ja': pluralA, + 'ru': pluralD, + 'tr': pluralA, + 'uk': pluralD, + 'vi': pluralA, + 'zh': pluralA, + 'zh-TW': pluralA, +}; diff --git a/resources/js/stripe.js b/resources/js/stripe.js new file mode 100644 index 0000000..b21b9c5 --- /dev/null +++ b/resources/js/stripe.js @@ -0,0 +1,53 @@ + +/** + * First we will load all of this project's JavaScript dependencies which + * includes Vue and other libraries. It is a great starting point when + * building robust, powerful web applications using Vue and Laravel. + */ + +require('./bootstrap'); + +/** + * Next, we will create a fresh Vue application instance and attach it to + * the page. Then, you may begin adding components to this application + * or customize the JavaScript scaffolding to fit your unique needs. + */ + +import Vue from 'vue'; +window.Vue = Vue; + +// Notifications +import Notifications from 'vue-notification'; +Vue.use(Notifications); + +// Custom components +Vue.component( + 'StripeSubscription', + require('./components/settings/Subscription.vue').default +); + +// Form elements +Vue.component( + 'FormInput', + require('./components/partials/form/Input.vue').default +); + +Vue.component( + 'ContactSearch', + require('./components/people/ContactSearch.vue').default +); + +var common = require('./common').default; + +common.loadLanguage(window.Laravel.locale, true).then((i18n) => { + // the Vue appplication + const app = new Vue({ + i18n, + data: { + htmldir: window.Laravel.htmldir, + locale: i18n.locale, + }, + }).$mount('#app'); + + return app; +}); diff --git a/resources/js/testing.js b/resources/js/testing.js new file mode 100644 index 0000000..b4c04a3 --- /dev/null +++ b/resources/js/testing.js @@ -0,0 +1,21 @@ +/** + * Add cy-name and cy-items directives. + * These are only active on local or testing environment. + */ + +function testingDirective(el, binding, vnode) { + if (window.Laravel.env != 'production') { + var value = ''; + try { + value = function(expr) { + return eval(expr); + }.call(vnode.context, ' with(this) { ' + binding.expression + ' } '); + } catch (e) { + value = binding.value; + } + el.setAttribute(binding.name, value.toString()); + } +} + +Vue.directive('cy-items', testingDirective); +Vue.directive('cy-name', testingDirective); diff --git a/resources/lang/ar.json b/resources/lang/ar.json new file mode 100644 index 0000000..5992a06 --- /dev/null +++ b/resources/lang/ar.json @@ -0,0 +1,7 @@ +{ + "The :attribute must contain at least one uppercase and one lowercase letter.": ":attribute يجب أن يحتوي على الأقل حرف كبير واحد وحرف صغير واحد.", + "The :attribute must contain at least one letter.": ":attribute يجب أن يحتوي على الأقل حرف واحد.", + "The :attribute must contain at least one symbol.": ":attribute يجب أن يحتوي على الأقل رمز واحد.", + "The :attribute must contain at least one number.": ":attribute يجب أن يحتوي على الأقل رقم واحد.", + "The given :attribute has appeared in a data leak. Please choose a different :attribute.": ":attribute المستخدم ضمن قائمة البيانات المسربة. الرجاء اختيار :attribute مختلف." +} diff --git a/resources/lang/ar/app.php b/resources/lang/ar/app.php new file mode 100644 index 0000000..21402d6 --- /dev/null +++ b/resources/lang/ar/app.php @@ -0,0 +1,571 @@ + 'Yes', + 'no' => 'No', + 'update' => 'تحديث', + 'save' => 'حفظ', + 'add' => 'إضافة', + 'cancel' => 'إلغاء', + 'confirm' => 'Confirm', + 'delete_confirm' => 'Are you sure?', + 'delete' => 'حذف', + 'edit' => 'تعديل', + 'upload' => 'رفع', + 'download' => 'Download', + 'save_close' => 'Save and close', + 'close' => 'إغلاق', + 'copy' => 'Copy', + 'create' => 'إنشاء', + 'remove' => 'إزالة', + 'revoke' => 'إلغاء', + 'done' => 'تم', + 'back' => 'Back', + 'verify' => 'تحقق', + 'new' => 'جديد', + 'unknown' => 'لا أعرف', + 'load_more' => 'تحميل المزيد', + 'loading' => 'Loading…', + 'with' => 'مع', + 'today' => 'اليوم', + 'yesterday' => 'أمس', + 'another_day' => 'يوم آخر', + 'date' => 'التاريخ', + 'type' => 'النوع', + 'zoom' => 'Zoom', + 'upgrade' => 'Upgrade to unlock', + 'percent_uploaded' => '{percent}% uploaded', + 'retry' => 'Retry', + 'filter' => 'Filter the list', + 'go_back' => 'Go back', + 'file_selected' => 'One file selected…|{count} files selected…', + + 'application_title' => 'Monica (مونيكا) – مدير العلاقات الشخصية', + 'application_description' => 'Monica هو أداة لإدارة تفاعلاتك مع أحبائك، أصدقائك و عائلتك.', + 'application_og_title' => 'Have better relations with your loved ones. Free online CRM for friends and family.', + + 'markdown_description' => 'هل تريد تنسيق النص الخاص بك بطريقة لطيفة؟ نحن ندعم التحديد و إضافة السماكة، و الإمالة و القوائم و المزيد.', + 'markdown_link' => 'اقرأ الوثائق', + + 'header_settings_link' => 'الإعدادات', + 'header_logout_link' => 'تسجيل الخروج', + 'header_changelog_link' => 'تغيرات المنتج', + + 'main_nav_cta' => 'إضافة أشخاص', + 'main_nav_dashboard' => 'لوحة التحكم', + 'main_nav_family' => 'جهات الاتصال', + 'main_nav_journal' => 'يوميات', + 'main_nav_activities' => 'الأنشطة', + 'main_nav_tasks' => 'المهام', + + 'footer_remarks' => 'Comments?', + 'footer_send_email' => 'Send us an email', + 'footer_privacy' => 'سياسة الخصوصية', + 'footer_release' => 'ملاحظات الإصدار', + 'footer_newsletter' => 'النشرة الإخبارية', + 'footer_source_code' => 'ساهم', + 'footer_version' => 'الإصدار: :version', + 'footer_new_version' => 'A new version of Monica is available', + + 'footer_modal_version_whats_new' => 'ما الجديد؟', + 'footer_modal_version_release_away' => 'إن إصدارك أقدم من آخر إصدار متاح. يجب أن تقوم بالتحديث. | أنت متأخر بـ:number إصدارات من آخر إصدار متاح. يجب أن تقوم بالتحديث.', + + 'breadcrumb_dashboard' => 'لوحة التحكم', + 'breadcrumb_list_contacts' => 'قائمة الأشخاص', + 'breadcrumb_archived_contacts' => 'Archived contacts', + 'breadcrumb_journal' => 'يوميات', + 'breadcrumb_settings' => 'الإعدادات', + 'breadcrumb_settings_export' => 'استخراج', + 'breadcrumb_settings_users' => 'المستخدمون', + 'breadcrumb_settings_users_add' => 'إضافة مستخدم', + 'breadcrumb_settings_subscriptions' => 'اشتراك', + 'breadcrumb_settings_import' => 'استيراد', + 'breadcrumb_settings_import_report' => 'استيراد التقرير', + 'breadcrumb_settings_import_upload' => 'رفع', + 'breadcrumb_settings_tags' => 'العلامات', + 'breadcrumb_add_significant_other' => 'Add significant other', + 'breadcrumb_edit_significant_other' => 'Edit significant other', + 'breadcrumb_add_note' => 'أضف ملاحظة', + 'breadcrumb_edit_note' => 'حرر ملاحظة', + 'breadcrumb_api' => 'API (واجهة برمجة التطبيق)', + 'breadcrumb_dav' => 'DAV Resources', + 'breadcrumb_edit_introductions' => 'كيف تقابلتما', + 'breadcrumb_settings_personalization' => 'التخصيص', + 'breadcrumb_settings_security' => 'الأمن', + 'breadcrumb_settings_security_2fa' => 'المصادقة الثنائية', + 'breadcrumb_profile' => 'الملف الشخصي لـ :name', + + 'gender_male' => 'رجل', + 'gender_female' => 'إمرأة', + 'gender_none' => 'أُفَضل ألا أقول', + 'gender_no_gender' => 'No gender', + + 'error_title' => 'عفواً! حصل خطأ ما.', + 'error_unauthorized' => 'ليس لديك الصلاحية لتحرير هذا المصدر.', + 'error_user_account' => 'This user does not belong to the given account.', + 'error_save' => 'لقد حصل خطأ بينما كنا نحاول حفظ البيانات.', + 'error_try_again' => 'حدث خطأ ما. الرجاء المحاولة مرة أخرى.', + 'error_id' => 'معرف الخطأ: :id', + 'error_unavailable' => 'Service unavailable', + 'error_maintenance' => 'Maintenance in progress. We’ll be right back.', + 'error_help' => 'سنعود إليك حالاً.', + 'error_twitter' => 'تابع حساب تويتر الخاص بنا، ليتم تنبيهك عندما يعود الموقع مجدداً.', + 'error_no_term' => 'There is no policy for this instance yet.', + + 'default_save_success' => 'تم حفظ البيانات.', + + 'compliance_title' => 'نعتذر للإزعاج.', + 'compliance_desc' => 'لقد قمنا بتغيير شروط الإستخداموسياسة الخصوصية الخاصة بنا. بموجب القانون، نطلب منك مراجعتها و قبولها لكي يمكنك الإستمرار في استخدام حسابك.', + 'compliance_desc_end' => 'نحن لن نسيء إلى بياناتك أو حسابك و لن نفعل ذلك أبداً.', + 'compliance_terms' => 'القبول بالشروط و سياسة الخصوصية الجديدية', + + // Relationship types + // Yes, each relationship type has 8 strings associated with it. + // This is because we need to indicate the name of the relationship type, + // and also the name of the opposite side of this relationship (father/son), + // and then, the feminine version of the string. Finally, in some sentences + // in the UI, we need to include the name of the person we add the relationship + // to. + 'relationship_type_group_love' => 'علاقات حب', + 'relationship_type_group_family' => 'علاقات عائلية', + 'relationship_type_group_friend' => 'علاقات صداقة', + 'relationship_type_group_work' => 'علاقات عمل', + 'relationship_type_group_other' => 'نوع آخر من العلاقات', + + 'relationship_type_partner' => 'significant other', + 'relationship_type_partner_female' => 'significant other', + 'relationship_type_partner_male' => 'significant other', + 'relationship_type_partner_with_name' => ':name’s significant other', + 'relationship_type_partner_female_with_name' => ':name’s significant other', + 'relationship_type_partner_male_with_name' => ':name’s significant other', + + 'relationship_type_spouse' => 'زوج', + 'relationship_type_spouse_female' => 'wife', + 'relationship_type_spouse_male' => 'husband', + 'relationship_type_spouse_with_name' => 'زوج :name’s', + 'relationship_type_spouse_female_with_name' => ':name’s wife', + 'relationship_type_spouse_male_with_name' => ':name’s husband', + + 'relationship_type_date' => 'موعد', + 'relationship_type_date_female' => 'موعد', + 'relationship_type_date_male' => 'date', + 'relationship_type_date_with_name' => ':name’s date', + 'relationship_type_date_female_with_name' => ':name’s date', + 'relationship_type_date_male_with_name' => ':name’s date', + + 'relationship_type_lover' => 'عاشق', + 'relationship_type_lover_female' => 'عاشقة', + 'relationship_type_lover_male' => 'lover', + 'relationship_type_lover_with_name' => ':name’s lover', + 'relationship_type_lover_female_with_name' => ':name’s lover', + 'relationship_type_lover_male_with_name' => ':name’s lover', + + 'relationship_type_inlovewith' => 'مغرم بـ', + 'relationship_type_inlovewith_female' => 'مغرمة بـ', + 'relationship_type_inlovewith_male' => 'in love with', + 'relationship_type_inlovewith_with_name' => 'someone :name is in love with', + 'relationship_type_inlovewith_female_with_name' => 'someone :name is in love with', + 'relationship_type_inlovewith_male_with_name' => 'someone :name is in love with', + + 'relationship_type_lovedby' => 'محبوب من قِبل', + 'relationship_type_lovedby_female' => 'محبوبة من قِبل', + 'relationship_type_lovedby_male' => 'loved by', + 'relationship_type_lovedby_with_name' => ':name’s secret lover', + 'relationship_type_lovedby_female_with_name' => ':name’s secret lover', + 'relationship_type_lovedby_male_with_name' => ':name’s secret lover', + + 'relationship_type_ex' => 'ex-partner', + 'relationship_type_ex_female' => 'ex-girlfriend', + 'relationship_type_ex_male' => 'ex-boyfriend', + 'relationship_type_ex_with_name' => ':name’s ex-partner', + 'relationship_type_ex_female_with_name' => ':name’s ex-girlfriend', + 'relationship_type_ex_male_with_name' => ':name’s ex-boyfriend', + + 'relationship_type_parent' => 'parent', + 'relationship_type_parent_female' => 'أم', + 'relationship_type_parent_male' => 'father', + 'relationship_type_parent_with_name' => ':name’s parent', + 'relationship_type_parent_female_with_name' => 'والدة :name', + 'relationship_type_parent_male_with_name' => ':name’s father', + + 'relationship_type_child' => 'child', + 'relationship_type_child_female' => 'ابنة', + 'relationship_type_child_male' => 'son', + 'relationship_type_child_with_name' => ':name’s child', + 'relationship_type_child_female_with_name' => 'ابنة :name', + 'relationship_type_child_male_with_name' => ':name’s son', + + 'relationship_type_stepparent' => 'step-parent', + 'relationship_type_stepparent_female' => 'stepmother', + 'relationship_type_stepparent_male' => 'stepfather', + 'relationship_type_stepparent_with_name' => ':name’s step-parent', + 'relationship_type_stepparent_female_with_name' => ':name’s stepmother', + 'relationship_type_stepparent_male_with_name' => ':name’s stepfather', + + 'relationship_type_stepchild' => 'stepchild', + 'relationship_type_stepchild_female' => 'stepdaughter', + 'relationship_type_stepchild_male' => 'stepson', + 'relationship_type_stepchild_with_name' => ':name’s stepchild', + 'relationship_type_stepchild_female_with_name' => ':name’s stepdaughter', + 'relationship_type_stepchild_male_with_name' => ':name’s stepson', + + 'relationship_type_sibling' => 'sibling', + 'relationship_type_sibling_female' => 'أخت', + 'relationship_type_sibling_male' => 'brother', + 'relationship_type_sibling_with_name' => ':name’s sibling', + 'relationship_type_sibling_female_with_name' => 'شقيقة :name', + 'relationship_type_sibling_male_with_name' => ':name’s brother', + + 'relationship_type_grandparent' => 'grandparent', + 'relationship_type_grandparent_female' => 'grandmother', + 'relationship_type_grandparent_male' => 'grandfather', + 'relationship_type_grandparent_with_name' => ':name’s grandparent', + 'relationship_type_grandparent_female_with_name' => ':name’s grandmother', + 'relationship_type_grandparent_male_with_name' => ':name’s grandfather', + + 'relationship_type_grandchild' => 'grandchild', + 'relationship_type_grandchild_female' => 'granddaughter', + 'relationship_type_grandchild_male' => 'grandson', + 'relationship_type_grandchild_with_name' => ':name’s grandchild', + 'relationship_type_grandchild_female_with_name' => ':name’s granddauther', + 'relationship_type_grandchild_male_with_name' => ':name’s grandson', + + 'relationship_type_uncle' => 'عم/خال', + 'relationship_type_uncle_female' => 'عمة/خالة', + 'relationship_type_uncle_male' => 'uncle', + 'relationship_type_uncle_with_name' => 'عم/خال :name', + 'relationship_type_uncle_female_with_name' => 'عمة/خالة :name', + 'relationship_type_uncle_male_with_name' => ':name’s uncle', + + 'relationship_type_nephew' => 'ابن شقيق', + 'relationship_type_nephew_female' => 'ابنة شقيقة', + 'relationship_type_nephew_male' => 'nephew', + 'relationship_type_nephew_with_name' => 'ابن شقيق :name', + 'relationship_type_nephew_female_with_name' => 'ابنة شقيقة :name', + 'relationship_type_nephew_male_with_name' => ':name’s nephew', + + 'relationship_type_cousin' => 'ابن عم/خال', + 'relationship_type_cousin_female' => 'ابنة عمة/خالة', + 'relationship_type_cousin_male' => 'cousin', + 'relationship_type_cousin_with_name' => 'ابن عم/خال :name', + 'relationship_type_cousin_female_with_name' => 'ابنة عمة/خالة :name', + 'relationship_type_cousin_male_with_name' => ':name’s cousin', + + 'relationship_type_godfather' => 'godparent', + 'relationship_type_godfather_female' => 'godmother', + 'relationship_type_godfather_male' => 'godfather', + 'relationship_type_godfather_with_name' => ':name’s godparent', + 'relationship_type_godfather_female_with_name' => ':name’s godmother', + 'relationship_type_godfather_male_with_name' => ':name’s godfather', + + 'relationship_type_godson' => 'godchild', + 'relationship_type_godson_female' => 'goddaughter', + 'relationship_type_godson_male' => 'godson', + 'relationship_type_godson_with_name' => ':name’s godchild', + 'relationship_type_godson_female_with_name' => ':name’s goddaughter', + 'relationship_type_godson_male_with_name' => ':name’s godson', + + 'relationship_type_friend' => 'صديق', + 'relationship_type_friend_female' => 'صديقة', + 'relationship_type_friend_male' => 'friend', + 'relationship_type_friend_with_name' => 'صديق :name', + 'relationship_type_friend_female_with_name' => 'صديقة :name', + 'relationship_type_friend_male_with_name' => ':name’s friend', + + 'relationship_type_bestfriend' => 'الصديق المفضل', + 'relationship_type_bestfriend_female' => 'الصديقة المفضلة', + 'relationship_type_bestfriend_male' => 'best friend', + 'relationship_type_bestfriend_with_name' => 'صديق :name المفضل', + 'relationship_type_bestfriend_female_with_name' => 'صديقة :name المفضلة', + 'relationship_type_bestfriend_male_with_name' => ':name’s best friend', + + 'relationship_type_colleague' => 'زميل', + 'relationship_type_colleague_female' => 'زميلة', + 'relationship_type_colleague_male' => 'colleague', + 'relationship_type_colleague_with_name' => 'زميل :name', + 'relationship_type_colleague_female_with_name' => 'زميلة :name', + 'relationship_type_colleague_male_with_name' => ':name’s colleague', + + 'relationship_type_boss' => 'مدير', + 'relationship_type_boss_female' => 'مديرة', + 'relationship_type_boss_male' => 'boss', + 'relationship_type_boss_with_name' => 'مدير :name', + 'relationship_type_boss_female_with_name' => 'مديرة :name', + 'relationship_type_boss_male_with_name' => ':name’s boss', + + 'relationship_type_subordinate' => 'مرؤوس', + 'relationship_type_subordinate_female' => 'مرؤوسة', + 'relationship_type_subordinate_male' => 'subordinate', + 'relationship_type_subordinate_with_name' => 'مرؤوس :name', + 'relationship_type_subordinate_female_with_name' => 'مرؤوسة :name', + 'relationship_type_subordinate_male_with_name' => ':name’s subordinate', + + 'relationship_type_mentor' => 'مرشد', + 'relationship_type_mentor_female' => 'مرشدة', + 'relationship_type_mentor_male' => 'mentor', + 'relationship_type_mentor_with_name' => 'مرشد :name', + 'relationship_type_mentor_female_with_name' => 'مرشدة :name', + 'relationship_type_mentor_male_with_name' => ':name’s mentor', + + 'relationship_type_protege' => 'protégé', + 'relationship_type_protege_female' => 'protégé', + 'relationship_type_protege_male' => 'protégé', + 'relationship_type_protege_with_name' => ':name’s protégé', + 'relationship_type_protege_female_with_name' => ':name’s protégé', + 'relationship_type_protege_male_with_name' => ':name’s protégé', + + 'relationship_type_ex_husband' => 'ex-spouse', + 'relationship_type_ex_husband_female' => 'طليقة', + 'relationship_type_ex_husband_male' => 'ex-husband', + 'relationship_type_ex_husband_with_name' => ':name’s ex-spouse', + 'relationship_type_ex_husband_female_with_name' => 'زوجة :name السابقة', + 'relationship_type_ex_husband_male_with_name' => ':name’s ex-husband', + + // emotions + 'emotion_primary_love' => 'Love', + 'emotion_primary_joy' => 'Joy', + 'emotion_primary_surprise' => 'Surprise', + 'emotion_primary_anger' => 'Anger', + 'emotion_primary_sadness' => 'Sadness', + 'emotion_primary_fear' => 'Fear', + + 'emotion_secondary_affection' => 'Affection', + 'emotion_secondary_lust' => 'Lust', + 'emotion_secondary_longing' => 'Longing', + 'emotion_secondary_cheerfulness' => 'Cheerfulness', + 'emotion_secondary_zest' => 'Zest', + 'emotion_secondary_contentment' => 'Contentment', + 'emotion_secondary_pride' => 'Pride', + 'emotion_secondary_optimism' => 'Optimism', + 'emotion_secondary_enthrallment' => 'Enthrallment', + 'emotion_secondary_relief' => 'Relief', + 'emotion_secondary_surprise' => 'Surprise', + 'emotion_secondary_irritation' => 'Irritation', + 'emotion_secondary_exasperation' => 'Exasperation', + 'emotion_secondary_rage' => 'Rage', + 'emotion_secondary_disgust' => 'Disgust', + 'emotion_secondary_envy' => 'Envy', + 'emotion_secondary_suffering' => 'Suffering', + 'emotion_secondary_sadness' => 'Sadness', + 'emotion_secondary_disappointment' => 'Disappointment', + 'emotion_secondary_shame' => 'Shame', + 'emotion_secondary_neglect' => 'Neglect', + 'emotion_secondary_sympathy' => 'Sympathy', + 'emotion_secondary_horror' => 'Horror', + 'emotion_secondary_nervousness' => 'Nervousness', + + 'emotion_adoration' => 'Adoration', + 'emotion_affection' => 'Affection', + 'emotion_love' => 'Love', + 'emotion_fondness' => 'Fondness', + 'emotion_liking' => 'Liking', + 'emotion_attraction' => 'Attraction', + 'emotion_caring' => 'Caring', + 'emotion_tenderness' => 'Tenderness', + 'emotion_compassion' => 'Compassion', + 'emotion_sentimentality' => 'Sentimentality', + 'emotion_arousal' => 'Arousal', + 'emotion_desire' => 'Desire', + 'emotion_lust' => 'Lust', + 'emotion_passion' => 'Passion', + 'emotion_infatuation' => 'Infatuation', + 'emotion_longing' => 'Longing', + 'emotion_amusement' => 'Amusement', + 'emotion_bliss' => 'Bliss', + 'emotion_cheerfulness' => 'Cheerfulness', + 'emotion_gaiety' => 'Gaiety', + 'emotion_glee' => 'Glee', + 'emotion_jolliness' => 'Jolliness', + 'emotion_joviality' => 'Joviality', + 'emotion_joy' => 'Joy', + 'emotion_delight' => 'Delight', + 'emotion_enjoyment' => 'Enjoyment', + 'emotion_gladness' => 'Gladness', + 'emotion_happiness' => 'Happiness', + 'emotion_jubilation' => 'Jubilation', + 'emotion_elation' => 'Elation', + 'emotion_satisfaction' => 'Satisfaction', + 'emotion_ecstasy' => 'Ecstasy', + 'emotion_euphoria' => 'Euphoria', + 'emotion_enthusiasm' => 'Enthusiasm', + 'emotion_zeal' => 'Zeal', + 'emotion_zest' => 'Zest', + 'emotion_excitement' => 'Excitement', + 'emotion_thrill' => 'Thrill', + 'emotion_exhilaration' => 'Exhilaration', + 'emotion_contentment' => 'Contentment', + 'emotion_pleasure' => 'Pleasure', + 'emotion_pride' => 'Pride', + 'emotion_eagerness' => 'Eagerness', + 'emotion_hope' => 'Hope', + 'emotion_optimism' => 'Optimism', + 'emotion_enthrallment' => 'Enthrallment', + 'emotion_rapture' => 'Rapture', + 'emotion_relief' => 'Relief', + 'emotion_amazement' => 'Amazement', + 'emotion_surprise' => 'Surprise', + 'emotion_astonishment' => 'Astonishment', + 'emotion_aggravation' => 'Aggravation', + 'emotion_irritation' => 'Irritation', + 'emotion_agitation' => 'Agitation', + 'emotion_annoyance' => 'Annoyance', + 'emotion_grouchiness' => 'Grouchiness', + 'emotion_grumpiness' => 'Grumpiness', + 'emotion_exasperation' => 'Exasperation', + 'emotion_frustration' => 'Frustration', + 'emotion_anger' => 'Anger', + 'emotion_rage' => 'Rage', + 'emotion_outrage' => 'Outrage', + 'emotion_fury' => 'Fury', + 'emotion_wrath' => 'Wrath', + 'emotion_hostility' => 'Hostility', + 'emotion_ferocity' => 'Ferocity', + 'emotion_bitterness' => 'Bitterness', + 'emotion_hate' => 'Hate', + 'emotion_loathing' => 'Loathing', + 'emotion_scorn' => 'Scorn', + 'emotion_spite' => 'Spite', + 'emotion_vengefulness' => 'Vengefulness', + 'emotion_dislike' => 'Dislike', + 'emotion_resentment' => 'Resentment', + 'emotion_disgust' => 'Disgust', + 'emotion_revulsion' => 'Revulsion', + 'emotion_contempt' => 'Contempt', + 'emotion_envy' => 'Envy', + 'emotion_jealousy' => 'Jealousy', + 'emotion_agony' => 'Agony', + 'emotion_suffering' => 'Suffering', + 'emotion_hurt' => 'Hurt', + 'emotion_anguish' => 'Anguish', + 'emotion_depression' => 'Depression', + 'emotion_despair' => 'Despair', + 'emotion_hopelessness' => 'Hopelessness', + 'emotion_gloom' => 'Gloom', + 'emotion_glumness' => 'Glumness', + 'emotion_sadness' => 'Sadness', + 'emotion_unhappiness' => 'Unhappiness', + 'emotion_grief' => 'Grief', + 'emotion_sorrow' => 'Sorrow', + 'emotion_woe' => 'Woe', + 'emotion_misery' => 'Misery', + 'emotion_melancholy' => 'Melancholy', + 'emotion_dismay' => 'Dismay', + 'emotion_disappointment' => 'Disappointment', + 'emotion_displeasure' => 'Displeasure', + 'emotion_guilt' => 'Guilt', + 'emotion_shame' => 'Shame', + 'emotion_regret' => 'Regret', + 'emotion_remorse' => 'Remorse', + 'emotion_alienation' => 'Alienation', + 'emotion_isolation' => 'Isolation', + 'emotion_neglect' => 'Neglect', + 'emotion_loneliness' => 'Loneliness', + 'emotion_rejection' => 'Rejection', + 'emotion_homesickness' => 'Homesickness', + 'emotion_defeat' => 'Defeat', + 'emotion_dejection' => 'Dejection', + 'emotion_insecurity' => 'Insecurity', + 'emotion_embarrassment' => 'Embarrassment', + 'emotion_humiliation' => 'Humiliation', + 'emotion_insult' => 'Insult', + 'emotion_pity' => 'Pity', + 'emotion_sympathy' => 'Sympathy', + 'emotion_alarm' => 'Alarm', + 'emotion_shock' => 'Shock', + 'emotion_fear' => 'Fear', + 'emotion_fright' => 'Fright', + 'emotion_horror' => 'Horror', + 'emotion_terror' => 'Terror', + 'emotion_panic' => 'Panic', + 'emotion_hysteria' => 'Hysteria', + 'emotion_mortification' => 'Mortification', + 'emotion_anxiety' => 'Anxiety', + 'emotion_nervousness' => 'Nervousness', + 'emotion_tenseness' => 'Tenseness', + 'emotion_uneasiness' => 'Uneasiness', + 'emotion_apprehension' => 'Apprehension', + 'emotion_worry' => 'Worry', + 'emotion_distress' => 'Distress', + 'emotion_dread' => 'Dread', + + // weather + 'weather_sunny' => 'Sunny', + 'weather_clear' => 'Clear', + 'weather_clear-day' => 'Clear', + 'weather_clear-night' => 'Clear night', + 'weather_light-drizzle' => 'Light drizzle', + 'weather_patchy-light-drizzle' => 'Patchy light drizzle', + 'weather_patchy-light-rain' => 'Patchy light rain', + 'weather_light-rain' => 'Light rain', + 'weather_moderate-rain-at-times' => 'Moderate rain at times', + 'weather_moderate-rain' => 'Moderate rain', + 'weather_patchy-rain-possible' => 'Patchy rain possible', + 'weather_heavy-rain-at-times' => 'Heavy rain at times', + 'weather_heavy-rain' => 'Heavy rain', + 'weather_light-freezing-rain' => 'Light freezing rain', + 'weather_moderate-or-heavy-freezing-rain' => 'Moderate or heavy freezing rain', + 'weather_light-sleet' => 'Light sleet', + 'weather_moderate-or-heavy-rain-shower' => 'Moderate or heavy rain shower', + 'weather_light-rain-shower' => 'Light rain shower', + 'weather_torrential-rain-shower' => 'Torrential rain shower', + 'weather_rain' => 'Rain', + 'weather_snow' => 'Snow', + 'weather_blowing-snow' => 'Blowing snow', + 'weather_patchy-light-snow' => 'Patchy light snow', + 'weather_light-snow' => 'Light snow', + 'weather_patchy-moderate-snow' => 'Patchy moderate snow', + 'weather_moderate-snow' => 'Moderate snow', + 'weather_patchy-heavy-snow' => 'Patchy heavy snow', + 'weather_heavy-snow' => 'Heavy snow', + 'weather_light-snow-showers' => 'Light snow showers', + 'weather_moderate-or-heavy-snow-showers' => 'Moderate or heavy snow showers', + 'weather_patchy-snow-possible' => 'Patchy snow possible', + 'weather_patchy-sleet-possible' => 'Patchy sleet possible', + 'weather_moderate-or-heavy-sleet' => 'Moderate or heavy sleet', + 'weather_light-sleet-showers' => 'Light sleet showers', + 'weather_moderate-or-heavy-sleet-showers' => 'Moderate or heavy sleet showers', + 'weather_sleet' => 'Sleet', + 'weather_wind' => 'Wind', + 'weather_fog' => 'Fog', + 'weather_freezing-fog' => 'Freezing fog', + 'weather_mist' => 'Mist', + 'weather_blizzard' => 'Blizzard', + 'weather_overcast' => 'Overcast', + 'weather_cloudy' => 'Cloudy', + 'weather_partly-cloudy-day' => 'Partly cloudy', + 'weather_partly-cloudy-night' => 'Partly cloudy', + 'weather_freezing-drizzle' => 'Freezing drizzle', + 'weather_heavy-freezing-drizzle' => 'Heavy freezing drizzle', + 'weather_patchy-freezing-drizzle-possible' => 'Patchy freezing drizzle possible', + 'weather_ice-pellets' => 'Ice pellets', + 'weather_light-showers-of-ice-pellets' => 'Light showers of ice pellets', + 'weather_moderate-or-heavy-showers-of-ice-pellets' => 'Moderate or heavy showers of ice pellets', + 'weather_thundery-outbreaks-possible' => 'Thundery outbreaks possible', + 'weather_patchy-light-rain-with-thunder' => 'Patchy light rain with thunder', + 'weather_moderate-or-heavy-rain-with-thunder' => 'Moderate or heavy rain with thunder', + 'weather_patchy-light-snow-with-thunder' => 'Patchy light snow with thunder', + 'weather_moderate-or-heavy-snow-with-thunder' => 'Moderate or heavy snow with thunder', + 'weather_current_temperature_celsius' => ':temperature °C', + 'weather_current_temperature_fahrenheit' => ':temperature °F', + 'weather_current_title' => 'Current weather', + + // dav + 'dav_contacts' => 'Contacts', + 'dav_contacts_description' => ':name’s contacts', + 'dav_birthdays' => 'Birthdays', + 'dav_birthdays_description' => ':name’s contact’s birthdays', + 'dav_tasks' => 'Tasks', + 'dav_tasks_description' => ':name’s tasks', + + // contact list + 'contact_list_avatar' => 'Avatar', + 'contact_list_name' => 'Contact', + 'contact_list_description' => 'Description', + +]; diff --git a/resources/lang/ar/auth.php b/resources/lang/ar/auth.php new file mode 100644 index 0000000..aa54be9 --- /dev/null +++ b/resources/lang/ar/auth.php @@ -0,0 +1,89 @@ + 'بيانات الاعتماد هذه غير متطابقة مع البيانات المسجلة لدينا.', + 'throttle' => 'عدد كبير جدا من محاولات الدخول. يرجى المحاولة مرة أخرى بعد :seconds ثانية.', + 'not_authorized' => 'ليس لديك الصلاحية لتنفيذ هذا الأمر', + 'signup_disabled' => 'تسجيل الإشتراك معطل حالياً', + 'signup_error' => 'An error occured trying to register the user', + 'back_homepage' => 'العودة إلى الصفحة الرئيسية', + 'mfa_auth_otp' => 'المصادقة مع جهاز العامل الثنائي الخاص بك', + 'mfa_auth_webauthn' => 'Authenticate with a security key (WebAuthn)', + '2fa_title' => 'المصادقة الثنائية', + '2fa_wrong_validation' => 'فشلت المصادقة الثنائية.', + '2fa_one_time_password' => 'رمز المصادقة الثنائية', + '2fa_recuperation_code' => 'أدخل رمز استرداد العامل الثنائي', + '2fa_one_time_or_recuperation' => 'Enter a two factor authentication code or a recovery code', + '2fa_otp_help' => 'قم بفتح تطبيق المصادقة الثنائية في هاتفك و انسخ الرمز', + + 'login_to_account' => 'تسجيل الدخول إلى حسابك', + 'login_with_recovery' => 'Login with a recovery code', + 'login_again' => 'الرجاء تسجيل الدخول مجدداً لحسابك', + 'email' => 'البريد الإلكتروني', + 'password' => 'كلمة المرور', + 'recovery' => 'Recovery code', + 'login' => 'تسجيل الدخول', + 'button_remember' => 'تذكرني', + 'password_forget' => 'نسيت كلمة المرور؟', + 'password_reset' => 'إعادة تعيين كلمة مرورك', + 'use_recovery' => 'Or you can use a recovery code', + 'signup_no_account' => 'ليس لديك حساب؟', + 'signup' => 'تسجيل الإشتراك', + 'create_account' => 'انشئ الحساب الأول عبر تسجيل الإشتراك', + 'change_language_title' => 'تغيير اللغة:', + 'change_language' => 'تغيير اللغة إلى :lang', + + 'password_reset_title' => 'إعادة تعيين كلمة المرور', + 'password_reset_email' => 'عنوان البريد', + 'password_reset_send_link' => 'أرسل رابط إعادة تعيين كلمة المرور', + 'password_reset_password' => 'كلمة المرور', + 'password_reset_password_confirm' => 'تأكيد كلمة المرور', + 'password_reset_action' => 'إعادة تعيين كلمة المرور', + 'password_reset_email_content' => 'اضغط هنا لإعادة تعيين كلمة مرورك:', + + 'register_title_welcome' => 'مرحبا بك في تطبيق Monica المثبت حديثاً', + 'register_create_account' => 'يجب أن تنشئ حساباً لتستخدم Monica', + 'register_title_create' => 'انشئ حسابك لـMonica', + 'register_login' => 'قم بـ تسجيل الدخول إذا كان لديك حساب مسبقاً.', + 'register_email' => 'أدخل عنوان بريد صالح', + 'register_email_example' => 'example@example.com', + 'register_firstname' => 'الاسم الأول', + 'register_firstname_example' => 'مثال: أحمد', + 'register_lastname' => 'الاسم الأخير', + 'register_lastname_example' => 'مثال: مراد', + 'register_password' => 'كلمة المرور', + 'register_password_example' => 'أدخل كلمة مرور آمنة', + 'register_password_confirmation' => 'تأكيد كلمة المرور', + 'register_action' => 'تسجيل الإشتراك', + 'register_policy' => 'بتسجيل إشتراكك تُفيد بأنك قرأت و قبِلت سياسة الخصوصية و شروط الإستخدام الخاصة بنا.', + 'register_invitation_email' => 'لأسباب أمنية، الرجاء تحديد البريد الإلكتروني للشخص الذي قمت بدعوته للإنضمام لهذا الحساب. المعلومات موجودة في رسالة الدعوة.', + + 'confirmation_title' => 'Verify Your Email Address', + 'confirmation_fresh' => 'A fresh verification link has been sent to your email address.', + 'confirmation_check' => 'Before proceeding, please check your email for a verification link.', + 'confirmation_request_another' => 'If you did not receive the email click here to request another.', + + 'confirmation_again' => 'إذا أردت تغيير بريدك الإلكتروني يمكنك الضغط هنا.', + 'email_change_current_email' => 'البريد الإلكتروني الحالي:', + 'email_change_title' => 'قم بتغيير عنوان بريدك', + 'email_change_new' => 'بريد إلكتروني جديد', + 'email_changed' => 'لقد تم تغيير بريدك الإلكتروني. تحقق من بريدك لتأكيده.', +]; diff --git a/resources/lang/ar/changelog.php b/resources/lang/ar/changelog.php new file mode 100644 index 0000000..af9bb49 --- /dev/null +++ b/resources/lang/ar/changelog.php @@ -0,0 +1,12 @@ + 'تغيرات المنتج', + 'note' => 'ملاحظة: لسوء الحظ، هذه الصفحة تظهر فقط باللغة الإنكليزية.', +]; diff --git a/resources/lang/ar/dashboard.php b/resources/lang/ar/dashboard.php new file mode 100644 index 0000000..cfb2c3f --- /dev/null +++ b/resources/lang/ar/dashboard.php @@ -0,0 +1,42 @@ + 'مرحباً في حسابك!', + 'dashboard_blank_description' => 'Monica هو المكان المناسب لتنظيم جميع التفاعلات لديك مع الأشخاص الذين تهتم بأمرهم.', + 'dashboard_blank_cta' => 'أضف أول جهة اتصال لك', + 'dashboard_blank_illustration' => 'الرسم التوضيحي بواسطة Freepik', + + 'notes_title' => 'لم تقم بتأشير أي ملاحظات بعد.', + + 'tab_recent_calls' => 'المكالمات الأخيرة', + 'tab_favorite_notes' => 'الملاحظات المفضلة', + 'tab_calls_blank' => 'لم تقم بتسجيل أي مكالمة بعد.', + 'tab_debts' => 'الديون', + 'tab_debts_blank' => 'لم تقم بتسجيل أي دَين بعد.', + 'tab_tasks' => 'المهام', + 'tab_tasks_blank' => 'ليس لديك أي مهمة حتى الآن.', + + 'tasks_add_task_placeholder' => 'What is this task about?', + 'tasks_tab_your_contacts' => 'Tasks related to your contacts', + 'tasks_tab_your_tasks' => 'Your tasks', + 'tasks_add_note' => 'Press Enter to add the task.', + 'task_add_cta' => 'Add a task', + + 'debts_you_owe' => 'أنت مدين', + + 'statistics_contacts' => 'جهات الإتصال', + 'statistics_activities' => 'الأنشطة', + 'statistics_gifts' => 'الهدايا', + + 'reminders_next_months' => 'الأحداث التي ستقام في الأشهر الثلاث المقبلة', + 'reminders_none' => 'ا يوجد تذكير لهذا الشهر.', + + 'product_changes' => 'Product changes', + 'product_view_details' => 'View details', +]; diff --git a/resources/lang/ar/format.php b/resources/lang/ar/format.php new file mode 100644 index 0000000..a70a6ba --- /dev/null +++ b/resources/lang/ar/format.php @@ -0,0 +1,36 @@ + 'M d, Y H:i', + 'short_date_year' => 'M d, Y', + 'short_date' => 'M d', + 'short_month' => 'M', + 'short_month_year' => 'M Y', + 'short_day' => 'D', + 'full_date_year' => 'F d, Y', + 'full_month' => 'F', + 'full_month_year' => 'F Y', + 'full_hour' => 'h.i A', + + 'short_text' => '{text}…', +]; diff --git a/resources/lang/ar/journal.php b/resources/lang/ar/journal.php new file mode 100644 index 0000000..e52934e --- /dev/null +++ b/resources/lang/ar/journal.php @@ -0,0 +1,38 @@ + 'كيف كان يومك؟ يمكنك تقييمه مرة في اليوم.', + 'journal_come_back' => 'شكراً. تعال غداً لتقييم يومك مجدداً.', + 'journal_description' => 'ملاحظة: الملاحظة تُسجل كِلا التدوينات اليدوية، و التدوينات الفورية مثل الأنشطة التي فعلتَها مع أصدقائك. بينما يمكنك حذف تدوينات المذكرة يدوياً، لا يمكنك حذف الأنشطة إلا بحذفها مباشرة من صفحة أصدقائك.', + 'journal_add' => 'أضف تدويناً للمذكرة', + 'journal_edit' => 'Edit a journal entry', + 'journal_empty' => 'Empty journal', + 'journal_created_at' => 'Created at {date}', + 'journal_created_automatically' => 'تم إنشائه تلقائياً', + 'journal_entry_type_journal' => 'تدوين المذكرة', + 'journal_entry_type_activity' => 'نشاط', + 'journal_entry_rate' => 'لقد قمتَ بتقييم يومك.', + 'journal_add_comment' => 'Care to add a comment (optional)?', + 'journal_show_comment' => 'Show comment', + 'entry_delete_success' => 'لقد تم حذف تدوين هذه المذكرة بنجاح.', + 'journal_add_title' => 'العنوان (اختياري)', + 'journal_add_date' => 'التاريخ', + 'journal_add_post' => 'تدوين', + 'journal_add_cta' => 'حفظ', + 'journal_blank_cta' => 'قم بتدوين مذكرتك الأولى', + 'journal_blank_description' => 'المذكرة تدَعُك تدون الأحداث التي حصلت لك، لتتذكرها.', + 'delete_confirmation' => 'هل أنت متأكد من حذف تدوين هذه المذكرة؟', + 'apply_filter' => 'طبق الفلترة', + "start_date" => "تاريخ البدء" , + "end_date" => "تاريخ الانتهاء" , + "per_page" => "لكل صفحة" , + 'Sort_order' => 'فرز حسب تاريخ الإنشاء', + "ascending" => "تصاعدي" , + "descending" => "تنازلي" , +]; diff --git a/resources/lang/ar/logs.php b/resources/lang/ar/logs.php new file mode 100644 index 0000000..7b6654b --- /dev/null +++ b/resources/lang/ar/logs.php @@ -0,0 +1,29 @@ + 'Created the contact.', + 'settings_log_contact_created_with_name' => 'Added :name as a contact.', + + // contat description update + 'contact_log_contact_description_updated' => 'Updated the description.', + 'settings_log_contact_description_updated_with_name' => 'Updated the description of :name.', + + // contact description clear + 'contact_log_contact_description_cleared' => 'Cleared the description.', + 'settings_log_contact_description_cleared_with_name' => 'Cleared the description of :name.', + + // contact work information update + 'contact_log_contact_work_updated' => 'Updated work information.', + 'settings_log_contact_work_updated_with_name' => 'Updated work information of :name.', + + // company created + 'settings_log_company_created' => 'Created a company called :name.', +]; diff --git a/resources/lang/ar/mail.php b/resources/lang/ar/mail.php new file mode 100644 index 0000000..fdf044d --- /dev/null +++ b/resources/lang/ar/mail.php @@ -0,0 +1,53 @@ + 'تذكير إلى :contact', + 'greetings' => 'مرحباً :username', + 'want_reminded_of' => 'لقد أردتَ أن يتم تذكيرك بـ:reason', + 'for' => 'لـ: :name', + 'comment' => 'تعليق: :comment', + 'footer_contact_info' => 'أضف، عرض، أكمل، و قم بتغيير معلومات عن جهة الإتصال هذه:', + 'footer_contact_info2' => 'عرض ملف :name الشخصي', + 'footer_contact_info2_link' => 'See :name’s profile: :url', + + 'notification_subject_line' => 'لديك حدث قادم', + 'notification_description' => 'في :count أيام ( في :date)، سوف يحصل الحدث التالي:', + + 'stay_in_touch_subject_line' => 'ابقى على إتصال مع :name', + 'stay_in_touch_subject_description' => 'لقد طلبتَ أن يتم تذكيرك بالبقاء على اتصال مع :name كل :frequency يوم. | لقد طلبتَ أن يتم تذكيرك بالبقاء على اتصال مع :name كل :frequency أيام.', + + 'notifications_whoops' => 'المعذرة!', + 'notifications_hello' => 'مرحباً!', + 'notifications_regards' => 'مع خالص التحيات', + 'notifications_footer' => 'إذا كنتَ تواجه مشكلة في الضغط على زر ":actionText"، انسخ والصق العنوان في متصفح الويب الخاص بك: [:actionURL](:actionURL)', + 'notifications_rights' => 'جميع الحقوق محفوظة', + + 'confirmation_email_title' => 'Monica - تأكيد البريد الإلكتروني', + 'confirmation_email_intro'=> 'لتأكيد بريدك اضغط على الزر أدناه', + 'confirmation_email_button' => 'قم بتأكيد عنوان بريدك', + 'confirmation_email_bottom' => 'If you did not create an account, no further action is required.', + + 'password_reset_title' => 'Monica – Reset Password Notification', + 'password_reset_intro' => 'You are receiving this email because we received a password reset request for your account.', + 'password_reset_button' => 'Reset Password', + 'password_reset_expiration' => 'This password reset link will expire in :count minutes.', + 'password_reset_bottom' => 'If you did not request a password reset, no further action is required.', + + 'invitation_title' => 'Monica – You are invited by :name', + 'invitation_intro' => 'You’ve been invited by :name (:email) to use Monica, a nice Personal Relationship Management tool.', + 'invitation_link' => 'To accept the invitation, click on the link below:', + 'invitation_button' => 'Accept invitation', + 'invitation_expiration' => 'This link will expire in :count days.', + + 'export_title' => 'Your export is ready', + 'export_description' => 'You requested a data export on :date. It is now ready to download.', + 'export_download' => 'Download export', + +]; diff --git a/resources/lang/ar/pagination.php b/resources/lang/ar/pagination.php new file mode 100644 index 0000000..d863329 --- /dev/null +++ b/resources/lang/ar/pagination.php @@ -0,0 +1,25 @@ + '❮ السابق', + 'next' => 'التالي ❯', + +]; diff --git a/resources/lang/ar/passwords.php b/resources/lang/ar/passwords.php new file mode 100644 index 0000000..c383115 --- /dev/null +++ b/resources/lang/ar/passwords.php @@ -0,0 +1,30 @@ + 'تمت إعادة تعيين كلمة المرور', + 'sent' => 'تم إرسال تفاصيل استعادة كلمة المرور الخاصة بك إلى بريدك الإلكتروني', + 'token' => '.رمز استعادة كلمة المرور الذي أدخلته غير صحيح', + 'user' => 'لم يتم العثور على أيّ حسابٍ بهذا العنوان الإلكتروني', + 'changed' => 'Password changed successfully.', + 'invalid' => 'كلمة السر الحالية التي أدخلتها غير صحيحة.', + 'throttled' => 'الرجاء الانتظار قبل إعادة المحاولة.', + +]; diff --git a/resources/lang/ar/people.php b/resources/lang/ar/people.php new file mode 100644 index 0000000..407ebcf --- /dev/null +++ b/resources/lang/ar/people.php @@ -0,0 +1,539 @@ + 'Contact not found', + 'people_list_number_kids' => ':count child|:count children', + 'people_list_last_updated' => 'آخر استشارة:', + 'people_list_number_reminders' => ':count reminder|:count reminders', + 'people_list_blank_title' => 'ليس لديك أي شخص في الحساب الخاص بك بعد', + 'people_list_blank_cta' => 'أضف شخص', + 'people_list_sort' => 'ترتيب', + 'people_list_stats' => ':count contact|:count contacts', + 'people_list_firstnameAZ' => 'فرز حسب الاسم الأول A → Z', + 'people_list_firstnameZA' => 'فرز حسب الاسم الأول Z → A', + 'people_list_lastnameAZ' => 'فرز حسب الاسم الأخير A → Z', + 'people_list_lastnameZA' => 'فرز حسب اسم العائلة Z → A', + 'people_list_lastactivitydateNewtoOld' => 'Sort by last activity date, newest to oldest', + 'people_list_lastactivitydateOldtoNew' => 'Sort by last activity date, oldest to newest', + 'people_list_filter_tag' => 'عرض كل جهات الاتصال ذات الوسم', + 'people_list_clear_filter' => 'مسح التصفية', + 'people_list_contacts_per_tags' => ':count contact|:count contacts', + 'people_list_show_dead' => 'Show deceased people (:count)', + 'people_list_hide_dead' => 'Hide deceased people (:count)', + 'people_search' => 'Search your contacts…', + 'people_search_no_results' => 'No results found', + 'people_search_next' => 'Next', + 'people_search_prev' => 'Previous', + 'people_search_rows_per_page' => 'Rows per page', + 'people_search_of' => 'of', + 'people_search_page' => 'Page', + 'people_search_all' => 'All', + 'people_add_new' => 'Add new person', + 'people_list_account_usage' => 'استخدام حسابك: :current/:limit جهة اتصال', + 'people_list_account_upgrade_title' => 'قم بترقية حسابك لتفعيل كامل إمكانياته.', + 'people_list_account_upgrade_cta' => 'قم بالترقية الان', + 'people_list_untagged' => 'عرض جهات الاتصال الغير موسومة', + 'people_list_filter_untag' => 'عرض كل جهات الاتصال الغير موسومة', + 'archived_contact_readonly' => 'Archived contact can’t be edited, please unarchive it first.', + + // people add + 'people_add_title' => 'إضافة شخص جديد', + 'people_add_missing' => 'No person found – add a new one now', + 'people_add_firstname' => 'الاسم الأول', + 'people_add_middlename' => 'Middle name (optional)', + 'people_add_lastname' => 'Last name (optional)', + 'people_add_email' => 'Email (optional)', + 'people_add_nickname' => 'Nickname (optional)', + 'people_add_cta' => 'إضافة', + 'people_save_and_add_another_cta' => 'تقديم وأضف شخص آخر', + 'people_add_success' => ':name تم إضافته بنجاح', + 'people_add_gender' => 'الجنس', + 'people_delete_success' => 'تم حذف جهة الاتصال', + 'people_delete_message' => 'Delete contact', + 'people_delete_confirmation' => 'Are you sure you want to delete :name’s contact? Deletion is immediate and permanent.', + 'people_add_birthday_reminder' => 'تمنى :name عيد ميلاد سعيد', + 'people_add_birthday_reminder_deceased' => 'On this date, :name would have celebrated their birthday', + 'people_add_import' => 'هل تريد استيراد جهات الاتصال الخاصة بك؟', + 'people_edit_email_error' => 'يوجد جهة اتصال في حسابك بنفس عنوان البريد الالكتروني. الرجاء اختيار عنوان آخر.', + 'people_export' => 'تصدير ك vCard', + 'people_add_reminder_for_birthday' => 'Create an annual birthday reminder', + + // show + 'section_contact_information' => 'معلومات جهة الاتصال', + 'section_personal_activities' => 'الأنشطة', + 'section_personal_reminders' => 'رسائل تذكير', + 'section_personal_tasks' => 'المهام', + 'section_personal_gifts' => 'الهدايا', + 'section_personal_notes' => 'الملاحظات', + + // archived contacts + 'list_link_to_active_contacts' => 'You are viewing archived contacts. See the list of active contacts instead.', + 'list_link_to_archived_contacts' => 'List of archived contacts', + + // Header + 'me' => 'This is you', + 'edit_contact_information' => 'تعديل معلومات الإتصال', + 'contact_archive' => 'Archive contact', + 'contact_unarchive' => 'Unarchive contact', + 'contact_archive_help' => 'Archived contacts are not be shown on the contact list, but still appear in search results.', + 'call_button' => 'تسجيل مكالمة', + 'set_favorite' => 'جهات الاتصال المفضلة يتم وضعها في قمة قائمة جهات الاتصال', + + // Stay in touch + 'stay_in_touch' => 'ابقى على اتصال', + 'stay_in_touch_frequency' => 'ابقى على اتصال يوميا|ابقى على اتصال كل {count} يومًا', + 'stay_in_touch_next_date' => 'Next due: {date}', + 'stay_in_touch_invalid' => 'يجب أن يكون التواتر عدد أكبر من 0.', + 'stay_in_touch_premium' => 'تحتاج إلى ترقية الحساب الخاص بك لاستخدام هذه الميزة', + 'stay_in_touch_modal_title' => 'ابقى على اتصال', + 'stay_in_touch_modal_desc' => 'يمكن أن نذكرك عن طريق البريد الإلكتروني للبقاء على اتصال مع {firstname} على فترات منتظمة.', + 'stay_in_touch_modal_label' => 'Send me an email every… {count} day|Send me an email every… {count} days', + + // Calls + 'modal_call_title' => 'تسجيل مكالمة', + 'modal_call_comment' => 'ماذا تحدثتم عنه؟ (اختياري)', + 'modal_call_exact_date' => 'تمت المكالمة الهاتفية في', + 'modal_call_who_called' => 'Who called?', + 'modal_call_emotion' => 'Do you want to log how you felt during this call? (optional)', + 'calls_add_success' => 'تم حفظ المكالمة الهاتفية.', + 'call_delete_confirmation' => 'هل أنت متأكد من حذف هذه المكالمة؟', + 'call_delete_success' => 'تم حذف المكالمة بنجاح', + 'call_title' => 'المكالمات الهاتفية', + 'call_empty_comment' => 'لا تفاصيل', + 'call_blank_title' => 'Keep track of the phone calls you’ve done with {name}', + 'call_blank_desc' => 'You called {name}', + 'call_you_called' => 'You called', + 'call_he_called' => '{name} called', + 'call_emotions' => 'Emotions:', + + // Conversation + 'conversation_blank' => 'Record conversations you have with :name on social media, SMS…', + 'conversation_delete_link' => 'حذف المحادثة', + 'conversation_edit_title' => 'تحرير المحادثة', + 'conversation_edit_delete' => 'هل أنت متأكد من حذف هذه المحادثة؟ الحذف دائم.', + 'conversation_add_success' => 'The conversation has been successfully added.', + 'conversation_edit_success' => 'The conversation has been successfully updated.', + 'conversation_delete_success' => 'لقد تم حذف المحادثة بنجاح.', + 'conversation_add_title' => 'سَجل محادثة جديدة', + 'conversation_add_when' => 'متى تمت هذه المحادثة؟', + 'conversation_add_who_wrote' => 'Who sent this message?', + 'conversation_add_how' => 'كيف تواصلتما؟', + 'conversation_add_you' => 'أنت', + 'conversation_add_content' => 'قم بتدوين ما قيل', + 'conversation_add_what_was_said' => 'ماذا قلت؟', + 'conversation_add_another' => 'إضافة رسالة أخرى', + 'conversation_add_error' => 'You must add at least one message.', + 'conversation_list_table_messages' => 'الرسائل', + 'conversation_list_table_content' => 'محتوى جزئي (آخر رسالة)', + 'conversation_list_title' => 'المحادثات', + 'conversation_list_cta' => 'سجل المحادثة', + + // age - birthday + 'birthdate_not_set' => 'Birthday is not set', + 'age_approximate_in_years' => ':age سنوات تقريبًا', + 'age_exact_in_years' => ':age سنوات', + 'age_exact_birthdate' => 'ولد في :date', + + // Last called + 'last_called' => 'Last called: :date', + 'last_talked_to' => 'Last called: {date}', + 'last_called_empty' => 'Last called: unknown', + 'last_activity_date' => 'Last activity together: :date', + 'last_activity_date_empty' => 'Last activity together: unknown', + + // additional information + 'information_edit_success' => 'تم تحديث الملف الشخصي بنجاح', + 'information_edit_title' => 'تعديل معلومات :name الشخصية', + 'information_edit_max_size' => 'الحد الأقصى :size كيلوبايت.', + 'information_edit_max_size2' => 'Max {size} Kb.', + 'information_edit_firstname' => 'الاسم الأول', + 'information_edit_lastname' => 'Last name (optional)', + 'information_edit_description' => 'Description (optional)', + 'information_edit_description_help' => 'تستخدم في قائمة جهات الاتصال لإضافة بعض المعلومات، إذا لزم الأمر.', + 'information_edit_unknown' => 'أنا لا أعرف عمر هذا الشخص', + 'information_edit_probably' => 'This person is probably…', + 'information_edit_not_year' => 'I know the day and month of this person’s birthday, but not the year…', + 'information_edit_exact' => 'I know this person’s exact birthday…', + 'information_edit_birthdate_label' => 'Birthday', + 'information_no_work_defined' => 'لا توجد معلومات العمل', + 'information_work_at' => 'لدى :company', + 'work_add_cta' => 'تحديث معلومات العمل', + 'work_edit_success' => 'Work information updated', + 'work_edit_title' => 'تحيث معلومات العمل الخاصة بـ :name', + 'work_edit_job' => 'المسمى الوظيفي (اختياري)', + 'work_edit_company' => 'الشركة (اختياري)', + 'work_information' => 'معلومات العمل', + + // food preferences + 'food_preferences_add_success' => 'تم حفظ التفضيلات الغذائية', + 'food_preferences_edit_description' => 'قد يكون :firstname أو شخص في عائلة :family مصاب بالحساسية، أو لا يريد نوع معين من النبيذ. حدد ذلك هنا لكي تتذكر ذلك في المرة القادمة عندما تدعوهم للعشاء', + 'food_preferences_edit_description_no_last_name' => 'قد يكون :firstname مصاب بالحساسية، أو لا يريد نوع معين من النبيذ. حدد ذلك هنا لكي تتذكر ذلك في المرة القادمة عندما تدعوهم للعشاء', + 'food_preferences_edit_title' => 'حدد التفضيلات الغذائية', + 'food_preferences_edit_cta' => 'حفظ التفضيلات الغذائية', + 'food_preferences_title' => 'التفضيلات الغذائية', + 'food_preferences_cta' => 'أضف تفضيلات غذائية', + + // reminders + 'reminders_blank_title' => 'هل هناك شيء ما كنت ترغب أن يتم تذكيرك به بخصوص :name؟', + 'reminders_blank_add_activity' => 'إضافة تذكير', + 'reminders_add_title' => 'ما الذي ترغب بتذكره عن :name؟', + 'reminders_add_description' => 'Please remind me to…', + 'reminders_add_next_time' => 'متى تود أن تكون المرة القادمة التي يتم فيها تذكيرك بهذا؟', + 'reminders_add_once' => 'ذكرني بهذا مرة واحدة فقط', + 'reminders_add_recurrent' => 'ذكرني بهذا كل', + 'reminders_add_starting_from' => 'ابتداءً من التاريخ الموضح أعلاه', + 'reminders_add_cta' => 'إضافة تذكير', + 'reminders_edit_update_cta' => 'تحديث التذكير', + 'reminders_add_error_custom_text' => 'يجب إضافة نص لهذا التذكير', + 'reminders_create_success' => 'تم بنجاح إضافة تذكير', + 'reminders_delete_success' => 'تم حذف التذكير بنجاح', + 'reminders_update_success' => 'تم تحديث التذكير بنجاح', + 'reminders_add_optional_comment' => 'Optional comment', + + 'reminder_frequency_day' => 'يوميا|كل :number أيام', + 'reminder_frequency_week' => 'أسبوعيا|كل :number أسابيع', + 'reminder_frequency_month' => 'شهريا|كل :number أشهر', + 'reminder_frequency_year' => 'سنويا|كل :number سنوات', + 'reminder_frequency_one_time' => 'في :date', + 'reminders_delete_confirmation' => 'هل تريد فعلا حذف هذا التذكير؟', + 'reminders_delete_cta' => 'حذف', + 'reminders_next_expected_date' => 'على', + 'reminders_cta' => 'إضافة تذكير', + 'reminders_description' => 'We will send an email for each one of the reminders below. Reminders are sent every morning the day events will happen. Reminders automatically added for birthdays can not be deleted. If you want to change those dates, edit the birthday of the contacts.', + 'reminders_one_time' => 'مرة واحدة', + 'reminders_type_week' => 'أسبوع', + 'reminders_type_month' => 'شهر', + 'reminders_type_year' => 'سنة', + 'reminders_birthday' => 'عيد ميلاد :name', + 'reminders_free_plan_warning' => 'أنت على الخطة المجانية. لا يتم إرسال رسائل الكترونية في هذه الخطة. قم بترقية حسابك لاستلام رسائل الكترونية.', + + // relationships + 'relationship_form_add' => 'إضافة علاقة جديدة', + 'relationship_form_edit' => 'تعديل علاقة حالية', + 'relationship_form_is_with' => 'This person is…', + 'relationship_form_is_with_name' => ':name is…', + 'relationship_form_add_choice' => 'مع من هذه العلاقة؟', + 'relationship_form_create_contact' => 'إضافة شخص جديد', + 'relationship_form_associate_contact' => 'جهة اتصال موجودة', + 'relationship_form_associate_dropdown' => 'بحث واختيار جهة اتصال موجودة من القائمة المنسدلة أدناه', + 'relationship_form_associate_dropdown_placeholder' => 'بحث واختيار جهة اتصال موجودة', + 'relationship_form_also_create_contact' => 'إنشاء جهة اتصال لهذا الشخص.', + 'relationship_form_add_description' => 'This will let you treat this person like any other contact.', + 'relationship_form_add_no_existing_contact' => 'ليس لديك أي جهة اتصال قد تكون على قرابة بـ:name حاليا.', + 'relationship_delete_confirmation' => 'هل أنت متأكد من أنك تريد حذف هذه العلاقة؟ الحذف دائم.', + 'relationship_unlink_confirmation' => 'هل أنت متأكد من أنك تريد حذف هذه العلاقة؟ لن يتم حذف هذا الشخص – فقط العلاقة بين الشخصين.', + 'relationship_form_add_success' => 'تم تعيين العلاقة بنجاح.', + 'relationship_form_deletion_success' => 'تم حذف العلاقة.', + + // tasks + 'tasks_title' => 'Tasks', + 'tasks_blank_title' => 'ليس لديك مهام حتى الآن.', + 'tasks_form_title' => 'العنوان', + 'tasks_form_description' => 'الوصف (اختياري)', + 'tasks_add_task' => 'إضافة مهمة', + 'tasks_delete_success' => 'تم حذف المهمة بنجاح', + 'tasks_complete_success' => 'تم تغيير حالة المهمة بنجاح', + + // activities + 'activity_title' => 'الأنشطة', + 'activity_type_category_simple_activities' => 'أنشطة بسيطة', + 'activity_type_category_sport' => 'رياضة', + 'activity_type_category_food' => 'طعام', + 'activity_type_category_cultural_activities' => 'أنشطة ثقافية', + 'activity_type_just_hung_out' => 'قضينا الوقت معا', + 'activity_type_watched_movie_at_home' => 'شاهدنا فيلم في المنزل', + 'activity_type_talked_at_home' => 'تحدثنا في المنزل', + 'activity_type_did_sport_activities_together' => 'played a sport together', + 'activity_type_ate_at_his_place' => 'تناولنا الطعام بمنزلهم', + 'activity_type_went_bar' => 'ذهبنا إلى حانة', + 'activity_type_ate_at_home' => 'تناولنا الطعام في المنزل', + 'activity_type_picnicked' => 'picnicked', + 'activity_type_ate_restaurant' => 'تناولنا الطعام في المطعم', + 'activity_type_went_theater' => 'ذهبنا إلى دار العرض', + 'activity_type_went_concert' => 'ذهبنا إلى حفلة موسيقية', + 'activity_type_went_play' => 'ذهبنا إلى مسرحية', + 'activity_type_went_museum' => 'ذهبنا إلى المتحف', + 'activities_add_activity' => 'إضافة نشاط', + 'activities_add_more_details' => 'Add more details', + 'activities_add_emotions' => 'Add emotions', + 'activities_add_category' => 'Indicate a category', + 'activities_add_participants_cta' => 'Add participants', + 'activities_item_information' => ':Activity حدث في :date', + 'activities_add_title' => 'What did you do with {name}?', + 'activities_summary' => 'صف ما فعلته', + 'activities_add_pick_activity' => 'Would you like to categorize this activity? You don’t have to, but it will give you statistics later on (optional)', + 'activities_add_date_occured' => 'The activity happened on…', + 'activities_add_participants' => 'Who, apart from {name}, participated in this activity? (optional)', + 'activities_add_emotions_title' => 'Do you want to log how you felt during this activity? (optional)', + 'activities_blank_title' => 'Keep track of what you’ve done with {name} in the past, and what you’ve talked about', + 'activities_blank_add_activity' => 'إضافة نشاط', + 'activities_add_success' => 'تم إضافة النشاط بنجاح', + 'activities_add_error' => 'خطأ عند إضافة نشاط', + 'activities_update_success' => 'تم تحديث النشاط بنجاح', + 'activities_delete_success' => 'تم حذف النشاط بنجاح', + 'activities_who_was_involved' => 'من شارك؟', + 'activities_activity' => 'تصنيف النشاط', + 'activities_view_activities_report' => 'عرض تقرير الأنشطة', + 'activities_profile_title' => 'تقرير عن الأنشطة بينك وبين :name', + 'activities_profile_subtitle' => 'لقد سجلت :total_activities نشاطا مع :name إجمالا، و:activities_last_twelve_months خلال الاثناعشر شهرا الماضية حتى الآن.|لقد سجلت :total_activities نشاطا مع :name إجمالا، و:activities_last_twelve_months خلال الاثناعشر شهرا الماضية حتى الآن.', + 'activities_profile_year_summary_activity_types' => 'هنا تفاصيل هذا النوع من الأنشطة التي قضيتماها معا في عام :year', + 'activities_profile_year_summary' => 'هذا ما فعلتموه معا في عام :year', + 'activities_profile_number_occurences' => ':value نشاط|:value أنشطة', + 'activities_list_participants' => 'Participants ({total}):', + 'activities_list_emotions' => 'Emotions felt:', + 'activities_list_date' => 'Happened on', + 'activities_list_category' => 'Category:', + + // notes + 'notes_create_success' => 'تم إنشاء المذكرة بنجاح', + 'notes_update_success' => 'تم حفظ المذكرة بنجاح', + 'notes_delete_success' => 'تم حذف المذكرة بنجاح', + 'notes_add_cta' => 'إضافة ملاحظة', + 'notes_favorite' => 'إضافة/إزالة من المفضلة', + 'notes_delete_title' => 'حذف ملاحظة', + 'notes_delete_confirmation' => 'هل أنت متأكذ من أنك تريد حذف هذه المذكرة؟ الحذف دائم', + + // gifts + 'gifts_title' => 'الهدايا', + 'gifts_add_success' => 'تم بنجاح إضافة هدية', + 'gifts_delete_success' => 'تم بنجاح حذف هدية', + 'gifts_delete_confirmation' => 'هل أنت متأكد من أنك تريد حذف هذه الهدية؟', + 'gifts_add_gift' => 'إضافة هدية', + 'gifts_link' => 'الرابط', + 'gifts_for' => 'For: {name}', + 'gifts_delete_cta' => 'حذف', + 'gifts_add_title' => 'إدارة الهدايا لـ :name', + 'gifts_add_gift_idea' => 'فكرة هدية', + 'gifts_add_gift_already_offered' => 'هدية تم تقديمها', + 'gifts_add_gift_received' => 'هدية تم استلامها', + 'gifts_add_gift_title' => 'ما هذه الهدية؟', + 'gifts_add_gift_name' => 'Gift name', + 'gifts_add_link' => 'الرابط لصفحة الويب (اختياري)', + 'gifts_add_value' => 'القيمة (اختياري)', + 'gifts_add_comment' => 'تعليق (اختياري)', + 'gifts_add_recipient' => 'Recipient (optional)', + 'gifts_add_recipient_field' => 'Recipient', + 'gifts_add_photo' => 'Photo (optional)', + 'gifts_add_photo_title' => 'Add a photo for this gift', + 'gifts_add_someone' => 'This gift is for someone in {name}’s family in particular', + 'gifts_delete_title' => 'Delete a gift', + 'gifts_ideas' => 'أفكار للهدايا', + 'gifts_offered' => 'هدية تم تقديمها', + 'gifts_offered_as_an_idea' => 'وضع علامة كفكرة', + 'gifts_received' => 'استلام هدية', + 'gifts_view_comment' => 'عرض التعليق', + 'gifts_mark_offered' => 'ضع علامة بأنه تم عرضها', + 'gifts_update_success' => 'تم بنجاح تحديث هدية', + 'gifts_add_date' => 'Date (optional)', + + // debts + 'debt_delete_confirmation' => 'هل أنت متأكد من أنك تريد حذف هذا الدين؟', + 'debt_delete_success' => 'تم بنجاح حذف الدين', + 'debt_add_success' => 'تم بنجاح إضافة الدين', + 'debt_title' => 'الديون', + 'debt_add_cta' => 'إضافة دين', + 'debt_you_owe' => 'أنت مدين بـ :amount', + 'debt_they_owe' => ':name مدين لك بـ :amount', + 'debt_add_title' => 'إدارة الديون', + 'debt_add_you_owe' => 'أنت مدين لـ :name', + 'debt_add_they_owe' => ':name مدين لك', + 'debt_add_amount' => 'مجموع', + 'debt_add_reason' => 'للسبب التالي (اختياري)', + 'debt_add_add_cta' => 'إضافة دين', + 'debt_edit_update_cta' => 'تحديث الدين', + 'debt_edit_success' => 'تم بنجاح تحديث الدين', + 'debts_blank_title' => 'إدارة الديون التي تدين بها لـ:name أو يدين بها :name لك', + + // tags + 'tag_edit' => 'تعديل وسم', + 'tag_add' => 'Add tags', + 'tag_add_search' => 'Add or search tags', + 'tag_no_tags' => 'No tags yet', + + // Introductions + 'introductions_sidebar_title' => 'كيف إلتقيتم', + 'introductions_blank_cta' => 'حدد كيف قابلت :name', + 'introductions_title_edit' => 'كيف التقيت بـ :name؟', + 'introductions_additional_info' => 'اشرح كيف وأين إلتقيتم', + 'introductions_edit_met_through' => 'هل عرّفك أحد بهذا الشخص؟', + 'introductions_no_met_through' => 'لا أحد', + 'introductions_first_met_date' => 'تاريخ اللقاء', + 'introductions_no_first_met_date' => 'لا أعرف تاريخ اللقاء الأول', + 'introductions_first_met_date_known' => 'هذا هو تاريخ اللقاء الأول', + 'introductions_add_reminder' => 'أضف تذكير للاحتفال بذكرى هذه المناسبة', + 'introductions_update_success' => 'لقد حدثت معلومات كيفية لقائك هذا الشخص بنجاح', + 'introductions_met_through' => 'التقينا بواسطة :name', + 'introductions_met_date' => 'التقينا في :date', + 'introductions_reminder_title' => 'الذكرى السنوية لليوم الذي التقيتم فيه للمرة الأولى', + + // Deceased + 'deceased_reminder_title' => 'الذكرى السنوية لوفاة :name', + 'deceased_mark_person_deceased' => 'Mark this as deceased', + 'deceased_know_date' => 'I know the date that this person died', + 'deceased_add_reminder' => 'إضافة تذكير لهذا التاريخ', + 'deceased_label' => 'متوفى', + 'deceased_date_label' => 'Deceased date', + 'deceased_label_with_date' => 'توفي بتاريخ :date', + 'deceased_age' => 'العمر عند الوفاة', + + // Contact information + 'contact_info_title' => 'معلومات جهة الاتصال', + 'contact_info_form_content' => 'المحتوى', + 'contact_info_form_contact_type' => 'نوع الاتصال', + 'contact_info_form_personalize' => 'إضفاء طابع شخصي', + 'contact_info_address' => 'يعيش في', + + // Addresses + 'contact_address_title' => 'عناوين', + 'contact_address_form_name' => 'تسمية (اختياري)', + 'contact_address_form_street' => 'الشارع (اختياري)', + 'contact_address_form_city' => 'المدينة (اختياري)', + 'contact_address_form_province' => 'المنطقة (اختياري)', + 'contact_address_form_postal_code' => 'الرمز البريدي (اختياري)', + 'contact_address_form_country' => 'البلد (اختياري)', + 'contact_address_form_latitude' => 'Latitude (numbers only) (optional)', + 'contact_address_form_longitude' => 'Longitude (numbers only) (optional)', + + // Pets + 'pets_kind' => 'نوع الحيوان الأليف', + 'pets_name' => 'الاسم (اختيارى)', + 'pets_create_success' => 'The pet has been successfully added', + 'pets_update_success' => 'تم تحديث الحيوان الأليف', + 'pets_delete_success' => 'تم حذف الحيوان الأليف', + 'pets_title' => 'الحيوانات الأليفة', + 'pets_reptile' => 'زواحف', + 'pets_bird' => 'طيور', + 'pets_cat' => 'قطط', + 'pets_dog' => 'كلاب', + 'pets_fish' => 'أسماك', + 'pets_hamster' => 'هامستر', + 'pets_horse' => 'حصان', + 'pets_rabbit' => 'أرنب', + 'pets_rat' => 'جرذ', + 'pets_small_animal' => 'حيوانات صغيرة', + 'pets_other' => 'غير ذلك', + + // life events + 'life_event_list_tab_life_events' => 'أحداث الحياة', + 'life_event_list_tab_other' => 'Notes, reminders, …', + 'life_event_list_title' => 'أحداث الحياة', + 'life_event_blank' => 'قم بتسجيل ما يحدث في حياة {name} كمرجع لك في المستقبل.', + 'life_event_list_cta' => 'إضافة حدث الحياة', + 'life_event_create_category' => 'كل الفئات', + 'life_event_create_life_event' => 'إضافة حدث الحياة', + 'life_event_create_default_title' => 'العنوان (اختياري)', + 'life_event_create_default_story' => 'القصة (اختياري)', + 'life_event_create_date' => 'You do not need to indicate a month or a day – only the year is mandatory.', + 'life_event_create_default_description' => 'أضف معلومات حول ما تعرفه', + 'life_event_create_add_yearly_reminder' => 'أضف تذكيراً سنوياً لهذا الحدث', + 'life_event_create_success' => 'تمت إضافة الحدث', + 'life_event_delete_title' => 'حذف حدث الحياة', + 'life_event_delete_description' => 'هل أنت متأكد من أنك تريد حذف هذا الحدث؟ الحذف دائم.', + 'life_event_delete_success' => 'تمت حذف هذا الحدث', + 'life_event_date_it_happened' => 'التاريخ الذي حدث فيه', + 'life_event_category_work_education' => 'Work & education', + 'life_event_category_family_relationships' => 'Family & relationships', + 'life_event_category_home_living' => 'Home & living', + 'life_event_category_health_wellness' => 'Health & wellness', + 'life_event_category_travel_experiences' => 'Travel & experiences', + 'life_event_sentence_new_job' => 'بدأ في عمل جديد', + 'life_event_sentence_retirement' => 'تقاعد', + 'life_event_sentence_new_school' => 'بدأت المدرسة', + 'life_event_sentence_study_abroad' => 'درست خارجاً', + 'life_event_sentence_volunteer_work' => 'بدأت في التطوع', + 'life_event_sentence_published_book_or_paper' => 'نشرت مقال', + 'life_event_sentence_military_service' => 'بدأت الخدمة العسكرية', + 'life_event_sentence_new_relationship' => 'بدأت في علاقة', + 'life_event_sentence_engagement' => 'خطبت', + 'life_event_sentence_marriage' => 'تزوجت', + 'life_event_sentence_anniversary' => 'ذكرى', + 'life_event_sentence_expecting_a_baby' => 'انتظار مولود', + 'life_event_sentence_new_child' => 'رزق بمولود', + 'life_event_sentence_new_family_member' => 'إضافة فرد جديد في العائلة', + 'life_event_sentence_new_pet' => 'Got a pet', + 'life_event_sentence_end_of_relationship' => 'أنهيت علاقة', + 'life_event_sentence_loss_of_a_loved_one' => 'Lost a loved one', + 'life_event_sentence_moved' => 'إنتقل', + 'life_event_sentence_bought_a_home' => 'اشتريت منزل', + 'life_event_sentence_home_improvement' => 'Made a home improvement', + 'life_event_sentence_holidays' => 'ذهبت في عطلة', + 'life_event_sentence_new_vehicle' => 'Got a new vehicle', + 'life_event_sentence_new_roommate' => 'Got a roommate', + 'life_event_sentence_overcame_an_illness' => 'Overcame an illness', + 'life_event_sentence_quit_a_habit' => 'أقلعت عن عادة', + 'life_event_sentence_new_eating_habits' => 'Started new eating habits', + 'life_event_sentence_weight_loss' => 'فقدت الوزن', + 'life_event_sentence_wear_glass_or_contact' => 'بدأت في ارتداء النظارات أو العدسات', + 'life_event_sentence_broken_bone' => 'كسرت عظماً', + 'life_event_sentence_removed_braces' => 'أزلت تقويم الأسنان', + 'life_event_sentence_surgery' => 'Had surgery', + 'life_event_sentence_dentist' => 'ذهبت لطبيب الأسنان', + 'life_event_sentence_new_sport' => 'بدأت في رياضة', + 'life_event_sentence_new_hobby' => 'بدأت هواية', + 'life_event_sentence_new_instrument' => 'تعلم آلة جديدة', + 'life_event_sentence_new_language' => 'تعلمت لغة جديدة', + 'life_event_sentence_tattoo_or_piercing' => 'تلقى وشماً أو ثقب', + 'life_event_sentence_new_license' => 'حصلتُ على رخصة', + 'life_event_sentence_travel' => 'سافرت', + 'life_event_sentence_achievement_or_award' => 'تلقى إنجازاً أو جائزة', + 'life_event_sentence_changed_beliefs' => 'غيرت معتقداتي', + 'life_event_sentence_first_word' => 'تحدث للمرة الأولى', + 'life_event_sentence_first_kiss' => 'Kissed for the first time', + + // documents + 'document_list_title' => 'Documents', + 'document_list_cta' => 'Upload document', + 'document_list_blank_desc' => 'Here you can store documents related to this person.', + 'document_upload_zone_cta' => 'Upload a file', + 'document_upload_zone_progress' => 'Uploading the document…', + 'document_upload_zone_error' => 'There was an error uploading the document. Please try again below.', + + // Photos + 'photo_title' => 'Photos', + 'photo_list_title' => 'Related photos', + 'photo_list_cta' => 'Upload photo', + 'photo_list_blank_desc' => 'You can store images about this contact. Upload one now!', + 'photo_upload_zone_cta' => 'Upload a photo', + 'photo_current_profile_pic' => 'Current profile picture', + 'photo_make_profile_pic' => 'Make profile picture', + 'photo_delete' => 'Delete photo', + 'photo_next' => 'Next photo ❯', + 'photo_previous' => '❮ Previous photo', + + // Avatars + 'avatar_change_title' => 'Change your avatar', + 'avatar_question' => 'Which avatar would you like to use?', + 'avatar_default_avatar' => 'The default avatar', + 'avatar_adorable_avatar' => 'The Adorable avatar', + 'avatar_gravatar' => 'The Gravatar associated with the email address of this person. Gravatar is a global system that lets users associate email addresses with photos.', + 'avatar_current' => 'Keep the current avatar', + 'avatar_photo' => 'From a photo that you upload', + 'avatar_crop_new_avatar_photo' => 'Crop new avatar photo', + + // emotions + 'emotion_this_made_me_feel' => 'This made you feel…', + + // logs + 'auditlogs_link' => 'History', + 'auditlogs_title' => 'Everything that happened to :name', + 'auditlogs_breadcrumb' => 'History', + 'auditlogs_author' => 'By :name on :date', + + // contact field label + 'contact_field_label_home' => 'Home', + 'contact_field_label_work' => 'Work', + 'contact_field_label_cell' => 'Mobile', + 'contact_field_label_fax' => 'Fax', + 'contact_field_label_pager' => 'Pager', + 'contact_field_label_main' => 'Main', + 'contact_field_label_other' => 'Other', + 'contact_field_label_personal' => 'Personal', +]; diff --git a/resources/lang/ar/reminder.php b/resources/lang/ar/reminder.php new file mode 100644 index 0000000..798299e --- /dev/null +++ b/resources/lang/ar/reminder.php @@ -0,0 +1,16 @@ + 'تمنى يوم مولد سعيد لـ', + 'type_phone_call' => 'اتصل', + 'type_lunch' => 'تناول الغذاء مع', + 'type_hangout' => 'قضاء الوقت مع', + 'type_email' => 'أرسل رسالة', + 'type_birthday_kid' => 'Wish happy birthday to the child of', +]; diff --git a/resources/lang/ar/settings.php b/resources/lang/ar/settings.php new file mode 100644 index 0000000..15e7142 --- /dev/null +++ b/resources/lang/ar/settings.php @@ -0,0 +1,557 @@ + 'إعدادات الحساب', + 'sidebar_personalization' => 'التخصيص', + 'sidebar_settings_storage' => 'Storage', + 'sidebar_settings_export' => 'تصدير البيانات', + 'sidebar_settings_users' => 'المستخدمين', + 'sidebar_settings_subscriptions' => 'الإشتراك', + 'sidebar_settings_import' => 'استيراد البيانات', + 'sidebar_settings_tags' => 'Tag management', + 'sidebar_settings_api' => 'API (واجهة برمجة التطبيق)', + 'sidebar_settings_dav' => 'DAV Resources', + 'sidebar_settings_security' => 'الأمن', + 'sidebar_settings_auditlogs' => 'Audit logs', + + 'title_general' => 'General Information', + 'title_i18n' => 'International settings', + 'title_layout' => 'Layout', + + 'me_title' => 'Me as a contact', + 'me_help' => 'This is the contact that represents you in Monica', + 'me_select' => 'Select a contact', + 'me_no_contact' => 'No contact selected yet.', + 'me_select_click' => 'Click here to select a contact.', + 'me_remove_contact' => 'Remove the association', + 'me_choose' => 'Choose yourself', + 'me_choose_placeholder' => 'Choose yourself', + + 'export_title' => 'قم بتصدير بيانات حسابك', + 'export_be_patient' => 'Click the button to start the export. It might take several minutes to process the export – please be patient and do not spam the button.', + 'export_title_sql' => 'Export to SQL', + 'export_sql_explanation' => 'Exporting your data in SQL format allows you to take your data and import it to your own Monica instance. This is only valuable if you do have your own server.', + 'export_sql_cta' => 'Export to SQL', + 'export_sql_link_instructions' => 'Note: read the instructions to learn more about importing this file to your instance.', + 'export_title_json' => 'Export to Json', + 'export_submitted' => 'Your export has been submitted, it will be available in a few moment…', + 'export_json_explanation' => 'Exporting your data in Json format for backup.', + 'export_json_beta' => 'Json export is in preview mode. Tell us what you think about it:', + 'export_json_cta' => 'Export to Json', + 'export_header_type' => 'Type', + 'export_header_timestamp' => 'Creation date', + 'export_header_status' => 'Status', + 'export_header_actions' => 'Actions', + 'export_last_title' => 'Last exports', + 'export_empty_title' => 'No exports yet', + 'export_type_json' => 'Json export', + 'export_type_sql' => 'SQL export', + 'export_status_todo' => 'Submitted', + 'export_status_doing' => 'Doing', + 'export_status_done' => 'Done', + 'export_status_failed' => 'Failed', + 'export_not_done' => 'Download impossible, this export is not done yet.', + + 'firstname' => 'الاسم الأول', + 'lastname' => 'الاسم الأخير', + 'name_order' => 'ترتيب الاسم', + 'name_order_firstname_lastname' => ' – John Doe', + 'name_order_lastname_firstname' => ' – Doe John', + 'name_order_firstname_lastname_nickname' => ' () – John Doe (Rambo)', + 'name_order_firstname_nickname_lastname' => ' () – John (Rambo) Doe', + 'name_order_lastname_firstname_nickname' => ' () – Doe John (Rambo)', + 'name_order_lastname_nickname_firstname' => ' () – Doe (Rambo) John', + 'name_order_nickname_firstname_lastname' => ' ( ) – Rambo (John Doe)', + 'name_order_nickname_lastname_firstname' => ' ( ) – Rambo (Doe John)', + 'name_order_nickname' => ' – Rambo', + 'currency' => 'العملة', + 'name' => 'اسمك: :اسم', + 'email' => 'البريد الإلكتروني', + 'email_placeholder' => 'أدخل البريد الإلكتروني', + 'email_help' => 'This is the email used to login, and this is where Monica will send your reminders.', + 'timezone' => 'المنطقة الزمنية', + 'temperature_scale' => 'Temperature scale', + 'temperature_scale_fahrenheit' => 'Fahrenheit (°F)', + 'temperature_scale_celsius' => 'Celsius (°C)', + 'layout' => 'التصميم', + 'layout_small' => 'الحد الأقصى 1200 بكسل', + 'layout_big' => 'العرض الكامل للمستعرض', + 'save' => 'تحديث التفضيلات', + 'delete_title' => 'احذف حسابك', + 'delete_desc' => 'Do you wish to delete your account? Deletion is permanent and all of your data will be erased permanently. If you have a subscription, it will be cancelled immediately.', + 'delete_other_desc' => 'Your data in the main database will be deleted immediately. As described in our privacy policy, we carry out securely encrypted backups of the database every day. These backups are kept for 30 days after which they are completely deleted. We cannot delete specific data from the backups we hold any earlier than this. All of your data will be completely deleted no later than 31 days after your account’s deletion.', + 'reset_desc' => 'Do you wish to reset your account? This will remove all your contacts, and all of the data associated with them. Your account will not be deleted.', + 'reset_title' => 'أعد تعيين حسابك', + 'reset_cta' => 'إعادة تعيين الحساب', + 'reset_notice' => 'Are you sure to reset your account? This is permanent and cannot be undone.', + 'reset_success' => 'Your account has been reset successfully.', + 'delete_notice' => 'Are you sure you want to delete your account? This is permanent and cannot be undone. All of your data will be deleted and will not be recoverable.', + 'delete_cta' => 'حذف الحساب', + 'settings_success' => 'تم تحديث التفضيلات!', + 'locale' => 'اللغة المستخدمة في هذا التطبيق', + 'locale_help' => 'Do you want to help translating Monica or add a new language? Please follow this link for more information.', + 'locale_ar' => 'العربية', + 'locale_cs' => 'التشيكية', + 'locale_de' => 'الألمانية', + 'locale_el' => 'Greek', + 'locale_en' => 'الإنجليزية', + 'locale_en-GB' => 'English (United Kingdom)', + 'locale_es' => 'الإسبانية', + 'locale_fr' => 'الفرنسية', + 'locale_he' => 'العبرية', + 'locale_hr' => 'الكرواتية', + 'locale_id' => 'Indonesian', + 'locale_it' => 'الإيطالية', + 'locale_ja' => 'Japanese', + 'locale_nl' => 'الهولندية', + 'locale_pt' => 'البرتغالية', + 'locale_pt-BR' => 'Portuguese, Brazil', + 'locale_ru' => 'الروسية', + 'locale_sv' => 'Swedish', + 'locale_vi' => 'Vietnamese', + 'locale_zh' => 'الصينية المبسطة', + 'locale_zh-TW' => 'Chinese Traditional', + 'locale_tr' => 'التركية', + + 'security_title' => 'الأمن', + 'security_help' => 'قم بتغيير المسائل الأمنية للحساب الخاص بك.', + 'password_change' => 'Change your password', + 'password_current' => 'كلمة السر الحالية', + 'password_current_placeholder' => 'أدخل كلمة مرورك الحالية', + 'password_new1' => 'كلمة مرور جديدة', + 'password_new1_placeholder' => 'Enter your new password', + 'password_new2' => 'Confirm your new password', + 'password_new2_placeholder' => 'Retype your new password', + 'password_btn' => 'تغيير كلمة المرور', + '2fa_title' => 'المصادقة الثنائية', + '2fa_otp_title' => 'تطبيق المصادقة الثنائية', + '2fa_enable_title' => 'تمكين المصادقة الثنائية', + '2fa_enable_description' => 'Enable Two Factor Authentication to increase the security of your account.', + '2fa_enable_otp' => 'Open up your Two Factor Authentication mobile app and scan the following QR barcode:', + '2fa_enable_otp_help' => 'If your Two Factor Authentication mobile app does not support QR barcodes, enter in the following code:', + '2fa_enable_otp_validate' => 'Please validate the new device you’ve just set up:', + '2fa_enable_success' => 'تم تفعيل المصادقة الثنائية', + '2fa_enable_error' => 'حدث خطأ عند محاولة تنشيط المصادقة الثنائية', + '2fa_enable_error_already_set' => 'تم تفعيل المصادقة الثنائية مسبقاً', + '2fa_disable_title' => 'تعطيل المصادقة الثنائية', + '2fa_disable_description' => 'Disable Two Factor Authentication for your account. Be careful, your account will be much less secure!', + '2fa_disable_success' => 'تم تعطيل المصادقة الثنائية', + '2fa_disable_error' => 'حدث خطأ عند محاولة تعطيل المصادقة الثنائية', + + 'webauthn_title' => 'Security key — WebAuthn protocol', + 'webauthn_enable_description' => 'Add a new security key', + 'webauthn_key_name_help' => 'Give your key a name.', + 'webauthn_key_name' => 'Key name:', + 'webauthn_success' => 'Your key is detected and validated.', + 'webauthn_last_use' => 'Last use: {timestamp}', + 'webauthn_delete_confirmation' => 'Are you sure you want to delete this key?', + 'webauthn_delete_success' => 'Key deleted', + 'webauthn_insertKey' => 'Insert your security key.', + 'webauthn_buttonAdvise' => 'If your security key has a button, press it.', + 'webauthn_noButtonAdvise' => 'If it does not, remove it and insert it again.', + 'webauthn_not_supported' => 'Your browser doesn’t currently support WebAuthn.', + 'webauthn_not_secured' => 'WebAuthn only supports secure connections. Please load this page with https scheme.', + 'webauthn_error_already_used' => 'This key is already registered. It’s not necessary to register it again.', + 'webauthn_error_not_allowed' => 'The operation either timed out or was not allowed.', + + 'recovery_title' => 'Recovery codes', + 'recovery_show' => 'Get recovery codes', + 'recovery_copy_help' => 'Copy codes in your clipboard', + 'recovery_help_intro' => 'These are your recovery codes:', + 'recovery_help_information' => 'You can use each recovery code once.', + 'recovery_clipboard' => 'Codes copied to the clipboard.', + 'recovery_generate' => 'Generate new codes…', + 'recovery_generate_help' => 'Generating new codes will invalidate previously generated codes.', + 'recovery_already_used_help' => 'This code has already been used.', + + 'users_list_title' => 'المستخدمين الذين لديهم حق الوصول إلى حسابك', + 'users_list_add_user' => 'دعوة مستخدم جديد', + 'users_list_you' => 'هذا أنت', + 'users_list_invitations_title' => 'دعوات معلقة', + 'users_list_invitations_explanation' => 'فيما يلي الأشخاص الذين قمتَ بدعوتهم للإنضمام إلى Monica كمتعاون.', + 'users_list_invitations_invited_by' => 'دعوة من :name', + 'users_list_invitations_sent_date' => 'تم الإرسال في :date', + 'users_blank_title' => 'أنت الشخص الوحيد الذي لديه حق الوصول إلى هذا الحساب.', + 'users_blank_add_title' => 'هل ترغب في دعوة شخص آخر؟', + 'users_blank_description' => 'هذا الشخص سيكون له نفس حق الوصول الذي لديك، وسيكون قادراً على إضافة أو تحرير أو حذف معلومات جهة الاتصال.', + 'users_blank_cta' => 'دعوة شخص ما', + 'users_add_title' => 'Invite a new user to your account by email', + 'users_add_description' => 'This person will have the same access as you do, including inviting or deleting other users, including you. Make sure you trust this person before giving them access.', + 'users_add_email_field' => 'أدخل عنوان البريد الإلكتروني للشخص الذي تريد دعوته', + 'users_add_confirmation' => 'I confirm that I want to invite this user to my account. I understand that this person will have access to ALL of my data and see exactly what I see.', + 'users_add_cta' => 'دعوة المستخدم عن طريق البريد الإلكتروني', + 'users_accept_title' => 'قبول الدعوة و إنشاء حساب جديد', + 'users_error_please_confirm' => 'الرجاء التأكيد بأنك تريد دعوة هذا المستخدم قبل مواصلة الدعوة', + 'users_error_email_already_taken' => 'هذا البريد الإلكتروني موجود بالفعل. الرجاء إدخال بريد إلكتروني آخر', + 'users_error_already_invited' => 'لقد قمت بدعوة هذا المستخدم مسبقاً. الرجاء اختيار بريد آخر.', + 'users_error_email_not_similar' => 'هذا ليس عنوان بريد الشخص الذي قمتَ بدعوته.', + 'users_invitation_deleted_confirmation_message' => 'لقد تم حذف الدعوة بنجاح', + 'users_invitations_delete_confirmation' => 'هل أنت متأكد من حذف هذه الدعوة؟', + 'users_list_delete_confirmation' => 'هل أنت متأكد من حذف هذا المستخدم من حسابك؟', + 'users_invitation_need_subscription' => 'إضافة المزيد من المستخدمين يتطلب الإشتراك.', + + 'subscriptions_account_current_plan' => 'خطتك الحالية', + 'subscriptions_account_current_legacy' => 'Current plan, not selectable anymore:', + 'subscriptions_account_current_paid_plan' => 'أنت على الخطة :name. شكراً جزيلاً لكونك مشتركاً.', + + 'subscriptions_account_next_billing_title' => 'Next bill', + 'subscriptions_account_next_billing' => 'Your subscription will auto-renew on :date.', + 'subscriptions_account_bill_monthly' => 'We’ll bill you :price for another month.', + 'subscriptions_account_bill_annual' => 'We’ll bill you :price for another year.', + 'subscriptions_account_change' => 'Change plan', + + 'subscriptions_account_cancel_title' => 'Cancel subscription', + 'subscriptions_account_cancel_action' => 'Cancel subscription', + 'subscriptions_account_cancel' => 'You can cancel subscription anytime.', + 'subscriptions_account_free_plan' => 'أنت في الخطة المجانية.', + 'subscriptions_account_free_plan_upgrade' => 'تستطيع ترقية حسابك لخطة :name، و التي تكلف :price$ شهرياً. هذه هي المنافع:', + 'subscriptions_account_free_plan_benefits_users' => 'عدد لا نهائي من المستخدمين', + 'subscriptions_account_free_plan_benefits_reminders' => 'تذكير بواسطة البريد اإلكتروني', + 'subscriptions_account_free_plan_benefits_import_data_vcard' => 'قم بإستيراد جهات اتصالك مع vCard', + 'subscriptions_account_free_plan_benefits_support' => 'Support the project in the long run, so we can introduce more great features.', + 'subscriptions_account_upgrade' => 'قم بترقية حسابك', + 'subscriptions_account_upgrade_title' => 'قم بترقية Monica اليوم و احصل على علاقات أكثر أهمية.', + 'subscriptions_account_upgrade_choice' => 'اختر خطة أدناه و انضم إلى :customers الأشخاص الذين قاموا بترقية Monica الخاص بهم.', + 'subscriptions_account_update_title' => 'Update Monica subscription', + 'subscriptions_account_update_description' => 'You can change your subscription’s frequency here.', + 'subscriptions_account_update_information' => 'You will be billed immediately for the new amount. Your subscription will extend to the new period, depending on your choice.', + 'subscriptions_account_invoices' => 'الفواتير', + 'subscriptions_account_invoices_download' => 'تنزيل', + 'subscriptions_account_invoices_subscription' => 'Subscription from :startDate to :endDate', + 'subscriptions_account_payment' => 'أي خيار للدفع يناسبك أكثر؟', + 'subscriptions_account_confirm_payment' => 'Your payment is currently incomplete, please confirm your payment.', + 'subscriptions_downgrade_title' => 'قم بخفض مرتبة حسابك للخطة المجانية', + 'subscriptions_downgrade_limitations' => 'خطتك المجانية فيها قيود. لتمكين خفض المرتبة، يجب أن تجتاز القائمة أدناه:', + 'subscriptions_downgrade_rule_users' => 'يجب أن يكون لديك مستخدم 1 فقط في حسابك', + 'subscriptions_downgrade_rule_users_constraint' => 'حالياً لديك مستخدم واحد في حسابك. | لديك حالياً :count مستخدمين في حسابك.', + 'subscriptions_downgrade_rule_invitations' => 'You must not have any pending invitations', + 'subscriptions_downgrade_rule_invitations_constraint' => 'You currently have 1 pending invitation.|You currently have :count pending invitations.', + 'subscriptions_downgrade_rule_contacts' => 'You must not have more than :number active contacts', + 'subscriptions_downgrade_rule_contacts_constraint' => 'لديك حالياً جهة اتصال واحدة.| لديك حالياً :count جهات اتصال.', + 'subscriptions_downgrade_rule_contacts_archive' => 'We can also archive all your contacts for you – that would clear this rule and let you proceed with your account’s downgrade process.', + 'subscriptions_downgrade_cta' => 'خفض المرتبة', + 'subscriptions_downgrade_success' => 'لقد عدتَ للخطة المجانية!', + 'subscriptions_downgrade_thanks' => 'Thanks so much for trying the paid plan. We keep adding new features on Monica all the time – so you might want to come back in the future to see if you might be interested in taking a subscription again.', + 'subscriptions_back' => 'العودة للإعدادات', + 'subscriptions_upgrade_title' => 'قم بترقية حسابك', + 'subscriptions_upgrade_choose' => 'لقد اخترت خطة :plan.', + 'subscriptions_upgrade_infos' => 'لا يمكننا أن نكون أكثر سعادة. أدخل بيانات دفعك أدناه.', + 'subscriptions_upgrade_name' => 'الإسم على البطاقة', + 'subscriptions_upgrade_zip' => 'الرمز البريدي', + 'subscriptions_upgrade_credit' => 'بطاقة الإئتمان أو بطاقة الصراف', + 'subscriptions_upgrade_submit' => 'Pay {amount}', + 'subscriptions_upgrade_charge' => 'We’ll charge your card :price now. The next charge will be on :date. If you ever change your mind, you can cancel at any time, no questions asked.', + 'subscriptions_upgrade_charge_handled' => 'الدفع يتولاه Stripe. لا تلمس معلومات البطاقات خادمنا.', + 'subscriptions_upgrade_success' => 'شكراً لك! أنت مشترك الآن.', + 'subscriptions_upgrade_thanks' => 'مرحباً بك في مجتمع الأشخاص الذين يحاولون جعل العالم مكاناً أفضل.', + + 'subscriptions_payment_confirm_title' => 'Confirm your :amount payment', + 'subscriptions_payment_confirm_information' => 'Extra confirmation is needed to process your payment. Please confirm your payment by filling out your payment details below.', + 'subscriptions_payment_succeeded_title' => 'Payment Successful', + 'subscriptions_payment_succeeded' => 'This payment was already successfully confirmed.', + 'subscriptions_payment_cancelled_title' => 'Payment Cancelled', + 'subscriptions_payment_cancelled' => 'This payment was cancelled.', + 'subscriptions_payment_error_name' => 'Please provide your name.', + 'subscriptions_payment_success' => 'The payment was successful.', + + 'subscriptions_pdf_title' => 'اشتراكك :name الشهري', + 'subscriptions_plan_frequency_year' => ':amount / year', + 'subscriptions_plan_frequency_month' => ':amount / month', + 'subscriptions_plan_choose' => 'اختر هذه الخطة', + 'subscriptions_plan_year_title' => 'دفع سنوي', + 'subscriptions_plan_year_bonus' => 'راحة البال لسنة كاملة', + 'subscriptions_plan_month_title' => 'دفع شهري', + 'subscriptions_plan_month_bonus' => 'قم بالإلغاء في أي وقت', + 'subscriptions_plan_include1' => 'تشمل الترقية الخاصة بك:', + 'subscriptions_plan_include2' => 'عدد لا نهائي من جهات الإتصال • عدد لا نهائي من المستخدمين • تذكير عبر البريد الإلكتروني • استيراد بـvCard • تخصيص صفحة جهة الإتصال', + 'subscriptions_plan_include3' => '100% من الأرباح تذهب لتطوير هذا المشروع المفتوح المصدر الرائع.', + 'subscriptions_help_title' => 'تفاصيل إضافية قد تكون مهتماً بمعرفتها', + 'subscriptions_help_opensource_title' => 'ما هو المشروع المفتوح المصدر؟', + 'subscriptions_help_opensource_desc' => 'Monica is an open source project. This means it is built by a community who wants to build a great tool for the greater good. Being open source means the code is publicly available on GitHub, and everyone can inspect it, modify it or enhance it. All the money we raise is dedicated to building better features, paying for more powerful servers, and paying other costs. Thanks for your help. We couldn’t do it without you.', + 'subscriptions_help_limits_title' => 'Is there a limit to the number of contacts we can have on the free plan?', + 'subscriptions_help_limits_plan' => 'نعم. الخطط المجانية تدَعُك تُدِير:number جهات إتصال.', + 'subscriptions_help_discounts_title' => 'هل لديكم تخفيضات لأسباب غير ربحية أو للتعليم؟', + 'subscriptions_help_discounts_desc' => 'نعم لدينا! تطبيق Monica مجاني للطلاب، و مجاني لأسباب غير ربحية و للمؤسسات الخيرية. فقط تواصل مع الدعم بدليل على حالتك و سنقوم بتطبيق هذه الحالة الخاصة في حسابك.', + 'subscriptions_help_change_title' => 'ماذا إذا قمتُ بتغيير رأيي؟', + 'subscriptions_help_change_desc' => 'You can cancel anytime, no questions asked, and all by yourself – no need to contact support. However, you will not be refunded for the current period.', + + 'stripe_error_card' => 'تم رفض بطاقتك. الرسالة المنحدرة: :message', + 'stripe_error_api_connection' => 'فشل الإتصال مع Stripe. حاول مجدداً لاحقاً.', + 'stripe_error_rate_limit' => 'يوجد عدد كبير من الطلبات على Stripe الآن. حاول مجدداً لاحقاً.', + 'stripe_error_invalid_request' => 'Invalid parameters. Try again later.', + 'stripe_error_authentication' => 'خطأ مصادقة مع Stripe', + + 'import_title' => 'استيراد جهات الاتصال إلى حسابك', + 'import_cta' => 'تحميل جهات الإتصال', + 'import_stat' => 'قمت بإستيراد :number ملفات حتى الآن.', + 'import_result_stat' => 'Uploaded vCard with 1 contact (:total_imported imported, :total_skipped skipped)|Uploaded vCard with :total_contacts contacts (:total_imported imported, :total_skipped skipped)', + 'import_view_report' => 'عرض التقرير', + 'import_in_progress' => 'الاستيراد قيد التقدم. أعد تحميل الصفحة في دقيقة واحدة.', + 'import_upload_title' => 'قم بإستيراد جهات اتصالك من ملف vCard', + 'import_upload_rules_desc' => 'و لكن لدينا بعض الشروط:', + 'import_upload_rule_format' => 'نحن ندعم ملفات .vcard و .vcf.', + 'import_upload_rule_vcard' => 'We support the vCard 3.0 format, which is the default format for macOS’s Contacts.app and Google Contacts.', + 'import_upload_rule_instructions' => 'Export instructions for macOS Contacts.app and Google Contacts.', + 'import_upload_rule_multiple' => 'If your contacts have multiple email addresses or phone numbers, only the first entry will be saved.', + 'import_upload_rule_limit' => 'Files are limited to 10 MB.', + 'import_upload_rule_time' => 'It might take up to a minute to upload the contacts and process them. Please be patient.', + 'import_upload_rule_cant_revert' => 'Please make sure data is accurate before uploading, as you can’t undo the upload.', + 'import_upload_form_file' => 'ملف .vcf أو .vCard الخاص بك:', + 'import_upload_behaviour' => 'سلوك الاستيراد:', + 'import_upload_behaviour_add' => 'Add new contacts and skip existing', + 'import_upload_behaviour_replace' => 'استبدال جهات الاتصال الموجودة', + 'import_upload_behaviour_help' => 'Replacing will replace all data found in the vCard, but will keep existing contact fields.', + 'import_report_title' => 'تقرير الإستيراد', + 'import_report_date' => 'تاريخ الإستيراد', + 'import_report_type' => 'نوع الإستيراد', + 'import_report_number_contacts' => 'عدد جهات الإتصال في الملف', + 'import_report_number_contacts_imported' => 'عدد جهات الإتصال المستوردة', + 'import_report_number_contacts_skipped' => 'عدد جهات الإتصال المتخطى عنها', + 'import_report_status_imported' => 'تم استيراده', + 'import_report_status_skipped' => 'تم تخطيه', + 'import_vcard_parse_error' => 'Error when parsing the vCard entry', + 'import_vcard_contact_exist' => 'جهة الإتصال موجودة مسبقاً', + 'import_vcard_contact_no_firstname' => 'No first name (mandatory)', + 'import_vcard_file_not_found' => 'لم يتم إيجاد الملف', + 'import_vcard_unknown_entry' => 'جهة اتصال غير معروفة', + 'import_vcard_file_no_entries' => 'لا يوجد إدخالات في الملف', + 'import_blank_title' => 'لم تقم بإستيراد أي جهات إتصال بعد.', + 'import_blank_question' => 'هل تريد استيراد جهات الإتصال الآن؟', + 'import_blank_description' => 'يمكننا استيراد ملفات vCard التي يمكن أن تحصل عليها من جهات اتصال Google أو مدير جهات الاتصال الخاصة بك.', + 'import_blank_cta' => 'استيراد vCard', + 'import_need_subscription' => 'استيراد البيانات يتطلب اشتراكاً.', + + 'tags_list_title' => 'Tags', + 'tags_list_description' => 'You can organize your contacts by setting up tags. Tags work like folders, but you can add more than one tag to a contact. To add a new tag, add it on the contact itself.', + 'tags_list_contact_number' => '1 جهة اتصال|:count جهات اتصال', + 'tags_list_delete_success' => 'The tag has been successfully deleted', + 'tags_list_edit_success' => 'The tag has been successfully updated', + 'tags_list_delete_confirmation' => 'Are you sure you want to delete the tag? No contacts will be deleted, only the tag.', + 'tags_blank_title' => 'Tags are a great way of categorizing your contacts.', + 'tags_blank_description' => 'Tags work like folders, but you can add more than one tag to a contact. Go to a contact and tag a friend, right below the name. Once a contact is tagged, come back here to manage all the tags in your account.', + + 'api_title' => 'الوصول إلى API', + 'api_description' => 'The API can be used to manipulate Monica’s data from an external application, like a mobile application for instance.', + 'api_help' => 'To use the API, a token is mandatory. You can either create a personal access token (Bearer authentication), or authorize an OAuth client to create it for you. See API documentation.', + 'api_endpoint' => 'The API endpoint for this Monica instance is:', + + 'api_personal_access_tokens' => 'Personal access tokens', + 'api_pao_description' => 'Make sure you give this token to a source you trust – as they allow you to access all your data.', + 'api_token_title' => 'Personal Access Tokens', + 'api_token_create_new' => 'Create New Token', + 'api_token_not_created' => 'You have not created any personal access tokens.', + 'api_token_name' => 'Token name', + 'api_token_expire' => 'Expires at {date}', + 'api_token_delete' => 'حذف', + 'api_token_create' => 'Create Token', + 'api_token_scopes' => 'Scopes', + 'api_token_help' => 'Here is your new personal access token. This is the only time it will be shown so don’t lose it! You may now use this token to make API requests.', + + 'api_oauth_clients' => 'Your OAuth clients', + 'api_oauth_clients_desc' => 'هذا القسم يتيح لك تسجيل عملاء OAuth الخاصين بك.', + 'api_oauth_clients_desc2' => 'Use this client id to request a new token, and convert authorization codes to access tokens. See Laravel Passport documentation for more information.', + 'api_oauth_title' => 'OAuth Clients', + 'api_oauth_create_new' => 'إنشاء عميل جديد', + 'api_oauth_edit' => 'Edit Client', + 'api_oauth_not_created' => 'لم تقم بإنشاء أي عملاء OAuth.', + 'api_oauth_clientid' => 'معرف العميل', + 'api_oauth_name' => 'الاسم', + 'api_oauth_name_help' => 'شيء سيَتَعرف عليه مستخدمونك و يثقون به.', + 'api_oauth_secret' => 'سري', + 'api_oauth_create' => 'إنشاء عميل', + 'api_oauth_redirecturl' => 'رابط إعادة التوجيه', + 'api_oauth_redirecturl_help' => 'Your application’s authorization callback URL.', + + 'api_authorized_clients' => 'قائمة العملاء المصرح بهم', + 'api_authorized_clients_desc' => 'This section lists all the clients you’ve authorized to access your application data. You can revoke this authorization at anytime.', + 'api_authorized_clients_title' => 'التطبيقات المصرحة بها', + 'api_authorized_clients_none' => 'There are no authorized clients yet.', + 'api_authorized_clients_name' => 'الاسم', + 'api_authorized_clients_scopes' => 'Scopes', + + 'personalization_tab_title' => 'قم بتخصيص حسابك', + + 'personalization_title' => 'Here you will find different settings to configure your account. These features are intended for “power users” who want maximum control over Monica.', + 'personalization_contact_field_type_title' => 'أنواع حقول جهة الإتصال', + 'personalization_contact_field_type_add' => 'أضف نوع حقل جديد', + 'personalization_contact_field_type_description' => 'You can configure all the different types of contact fields that you can associate to all your contacts. For example, if a new social network appears in the future, you will be able to add this new way of communicating with your contacts right here.', + 'personalization_contact_field_type_table_name' => 'الاسم', + 'personalization_contact_field_type_table_protocol' => 'النظام', + 'personalization_contact_field_type_table_actions' => 'إجراءات', + 'personalization_contact_field_type_modal_title' => 'إضافة نوع حقل جهة اتصال جديد', + 'personalization_contact_field_type_modal_edit_title' => 'تحرير نوع حقل جهة اتصال موجود', + 'personalization_contact_field_type_modal_delete_title' => 'حذف نوع حقل جهة اتصال موجود', + 'personalization_contact_field_type_modal_delete_description' => 'Are you sure you want to delete this contact field type? Deleting this type of contact field will delete ALL the data with this type for all of your contacts.', + 'personalization_contact_field_type_modal_name' => 'الاسم', + 'personalization_contact_field_type_modal_protocol' => 'النظام (إختياري)', + 'personalization_contact_field_type_modal_protocol_help' => 'Each new contact field type can be clickable. If a protocol is set, we will use it to trigger the action that is set.', + 'personalization_contact_field_type_modal_icon' => 'الأيقونة (اختياري)', + 'personalization_contact_field_type_modal_icon_help' => 'You can associate an icon with this contact field type. You need to add a reference to a Font Awesome icon.', + 'personalization_contact_field_type_delete_success' => 'The contact field type has been successfully deleted.', + 'personalization_contact_field_type_add_success' => 'The contact field type has been successfully added.', + 'personalization_contact_field_type_edit_success' => 'The contact field type has been successfully updated.', + + 'personalization_genders_title' => 'أنواع الجنس', + 'personalization_genders_add' => 'إضافة نوع جنس جديد', + 'personalization_genders_desc' => 'You can define as many genders as you need to. You need at least one gender type in your account.', + 'personalization_genders_modal_add' => 'إضافة نوع جنس', + 'personalization_genders_modal_edit' => 'تحديث نوع الجنس', + 'personalization_genders_modal_name' => 'Name', + 'personalization_genders_modal_name_help' => 'The name used to display the gender on a contact page.', + 'personalization_genders_modal_sex' => 'Sex', + 'personalization_genders_modal_sex_help' => 'Used to define the relationships, and during the VCard import/export process.', + 'personalization_genders_modal_default' => 'Select the default gender for a new contact', + 'personalization_genders_modal_delete' => 'حذف نوع الجنس', + 'personalization_genders_modal_delete_desc' => 'Are you sure you want to delete the gender “{name}”?', + 'personalization_genders_modal_delete_question' => 'You currently have {count} contact with this gender. If you delete this gender, what gender should this contact have?|You currently have {count} contacts with this gender. If you delete this gender, what gender should these contacts have?', + 'personalization_genders_modal_delete_question_default' => 'This gender is the default one. If you delete this gender, which one will be the new default?', + 'personalization_genders_modal_error' => 'Please choose a gender from the list.', + 'personalization_genders_list_contact_number' => '{count} contact|{count} contacts', + 'personalization_genders_table_name' => 'Name', + 'personalization_genders_table_sex' => 'Sex', + 'personalization_genders_table_default' => 'Default', + 'personalization_genders_default' => 'Default gender', + 'personalization_genders_make_default' => 'Change default gender', + 'personalization_genders_select_default' => 'Select default gender', + 'personalization_genders_m' => 'Male', + 'personalization_genders_f' => 'Female', + 'personalization_genders_o' => 'Other', + 'personalization_genders_u' => 'Unknown', + 'personalization_genders_n' => 'None or not applicable', + + 'personalization_reminder_rule_save' => 'لقد تم حفظ التغيير', + 'personalization_reminder_rule_title' => 'شروط التذكير', + 'personalization_reminder_rule_line' => 'قبل {count} يوم | قبل {count} أيام', + 'personalization_reminder_rule_desc' => 'For every reminder that you set, Monica can send you an email a number of days before the event happens. You can adjust these notification settings here. These notifications only apply to monthly and yearly reminders.', + + 'personalization_module_save' => 'لقد تم حفظ التغيير', + 'personalization_module_title' => 'الميزات', + 'personalization_module_desc' => 'You may not need all of Monica’s features. Below you can toggle specific features that are used on a contact sheet. This change will affect ALL your contacts. Turning off a feature does not delete any data, it simply hides the feature.', + + 'personalisation_paid_upgrade' => 'This is a premium feature that requires a Paid subscription to be active. Upgrade your account by visiting Settings > Subscription.', + 'personalisation_paid_upgrade_vue' => 'This is a premium feature that requires a Paid subscription to be active. Upgrade your account by visiting Settings > Subscription.', + + 'reminder_time_to_send' => 'Time of the day reminders will be sent', + 'reminder_time_to_send_help' => 'Your next reminder is scheduled to be sent on {dateTime}.', + + 'personalization_activity_type_category_title' => 'تصنيفات نوع النشاط', + 'personalization_activity_type_category_add' => 'إضافة فئة نوع نشاط جديد', + 'personalization_activity_type_category_table_name' => 'الاسم', + 'personalization_activity_type_category_description' => 'An activity with one of your contacts can have a type and a category type. Your account comes with a set of predefined category types by default, but you can customize these here.', + 'personalization_activity_type_category_table_actions' => 'إجراءات', + 'personalization_activity_type_category_modal_add' => 'إضافة فئة نوع نشاط جديد', + 'personalization_activity_type_category_modal_edit' => 'تحرير فئة نوع نشاط', + 'personalization_activity_type_category_modal_question' => 'What should we name this new category?', + 'personalization_activity_type_add_button' => 'أضف نوع نشاط جديد', + 'personalization_activity_type_modal_add' => 'أضف نوع نشاط جديد', + 'personalization_activity_type_modal_question' => 'What should we name this new activity type?', + 'personalization_activity_type_modal_edit' => 'تحرير نوع نشاط', + 'personalization_activity_type_category_modal_delete' => 'حذف فئة نوع نشاط', + 'personalization_activity_type_category_modal_delete_desc' => 'Are you sure you want to delete this category? Deleting it will delete all associated activity types. Activities that belong to this category will not be affected by this deletion.', + 'personalization_activity_type_modal_delete' => 'حذف نوع نشاط', + 'personalization_activity_type_modal_delete_desc' => 'هل أنت متأكد من حذف نوع هذا النشاط؟ الأنشطة التي تعود لهذه الفئة لن تتأثر بهذا الحذف.', + 'personalization_activity_type_modal_delete_error' => 'لا نستطيع إيجاد نوع هذا النشاط.', + 'personalization_activity_type_category_modal_delete_error' => 'لا نستطيع إيجاد تصنيف هذا النوع من الأنشطة.', + + 'personalization_life_event_category_title' => 'Life event categories', + 'personalization_live_event_category_table_name' => 'Name', + 'personalization_life_event_category_description' => 'A life event can have a type and a category. Your account comes with a set of predefined categories and types by default, but you can customize life event types here.', + 'personalization_live_event_category_table_actions' => 'Actions', + 'personalization_life_event_type_add_button' => 'Add a new life event type', + 'personalization_life_event_type_modal_add' => 'Add a new life event type', + 'personalization_life_event_type_modal_question' => 'What should we name this new life event type?', + 'personalization_life_event_type_modal_edit' => 'Edit a life event type', + 'personalization_life_event_type_modal_delete' => 'Delete a life event type', + 'personalization_life_event_type_modal_delete_desc' => 'Are you sure you want to delete this life event type? Life events that belong to this type will be deleted by performing this action.', + 'personalization_life_event_type_modal_delete_error' => 'We can’t find this life event type.', + + 'personalization_life_event_category_work_education' => 'العمل و التعليم', + 'personalization_life_event_category_family_relationships' => 'العائلة و العلاقات', + 'personalization_life_event_category_home_living' => 'المنزل و العيش', + 'personalization_life_event_category_travel_experiences' => 'السفر و الخبرات', + 'personalization_life_event_category_health_wellness' => 'الصحة و العافية', + + 'personalization_life_event_type_new_job' => 'عمل جديد', + 'personalization_life_event_type_retirement' => 'التقاعد', + 'personalization_life_event_type_new_school' => 'مدرسة جديدة', + 'personalization_life_event_type_study_abroad' => 'الدراسة في الخارج', + 'personalization_life_event_type_volunteer_work' => 'عمل تطوعي', + 'personalization_life_event_type_published_book_or_paper' => 'نشر كتاب أو مقال', + 'personalization_life_event_type_military_service' => 'خدمة عسكرية', + 'personalization_life_event_type_first_met' => 'أول لقاء', + 'personalization_life_event_type_new_relationship' => 'علاقة جديدة', + 'personalization_life_event_type_engagement' => 'الخطوبة', + 'personalization_life_event_type_marriage' => 'الزواج', + 'personalization_life_event_type_anniversary' => 'ذكرى', + 'personalization_life_event_type_expecting_a_baby' => 'انتظار مولود', + 'personalization_life_event_type_new_child' => 'طفل جديد', + 'personalization_life_event_type_new_family_member' => 'فرد جديد في العائلة', + 'personalization_life_event_type_new_pet' => 'حيوان أليف جديد', + 'personalization_life_event_type_end_of_relationship' => 'نهاية علاقة', + 'personalization_life_event_type_loss_of_a_loved_one' => 'Loss of a loved one', + 'personalization_life_event_type_moved' => 'إنتقال', + 'personalization_life_event_type_bought_a_home' => 'شراء منزل', + 'personalization_life_event_type_home_improvement' => 'ترميم المنزل', + 'personalization_life_event_type_holidays' => 'العطل', + 'personalization_life_event_type_new_vehicle' => 'مركبة جديدة', + 'personalization_life_event_type_new_roommate' => 'رفيق سكن جديد', + 'personalization_life_event_type_overcame_an_illness' => 'التغلب على المرض', + 'personalization_life_event_type_quit_a_habit' => 'الإقلاع عن عادة', + 'personalization_life_event_type_new_eating_habits' => 'عادات أكل جديدة', + 'personalization_life_event_type_weight_loss' => 'فقدان الوزن', + 'personalization_life_event_type_wear_glass_or_contact' => 'Started wearing glasses or contacts', + 'personalization_life_event_type_broken_bone' => 'Broke a bone', + 'personalization_life_event_type_removed_braces' => 'Had braces removed', + 'personalization_life_event_type_surgery' => 'Had surgery', + 'personalization_life_event_type_dentist' => 'Had dental treatment', + 'personalization_life_event_type_new_sport' => 'Started playing a new sport', + 'personalization_life_event_type_new_hobby' => 'Took up a new hobby', + 'personalization_life_event_type_new_instrument' => 'Started learning a new instrument', + 'personalization_life_event_type_new_language' => 'Started learning a new language', + 'personalization_life_event_type_tattoo_or_piercing' => 'وشم أو ثقب', + 'personalization_life_event_type_new_license' => 'رخصة جديدة', + 'personalization_life_event_type_travel' => 'السفر', + 'personalization_life_event_type_achievement_or_award' => 'إنجاز أو جائزة', + 'personalization_life_event_type_changed_beliefs' => 'تغير المعتقدات', + 'personalization_life_event_type_first_word' => 'أول كلمة', + 'personalization_life_event_type_first_kiss' => 'First kiss', + + 'storage_title' => 'Storage', + 'storage_account_info' => 'Your account limit is :accountLimit MB. Your current usage is :currentAccountSize MB (about :percentUsage%).', + 'storage_upgrade_notice' => 'Upgrade your account to be able to upload documents and photos.', + 'storage_description' => 'Here you can see all the documents and photos uploaded about your contacts.', + + 'dav_title' => 'WebDAV', + 'dav_description' => 'Here you can find all settings to use WebDAV resources for CardDAV and CalDAV exports.', + 'dav_copy_help' => 'Copy into your clipboard', + 'dav_clipboard_copied' => 'Value copied into your clipboard', + 'dav_url_base' => 'Base url for all CardDAV and CalDAV resources:', + 'dav_connect_help' => 'You can connect your contacts and/or calendars with this base url on you phone or computer.', + 'dav_connect_help2' => 'Use your login (email) and create an API token as the password to authenticate.', + 'dav_url_carddav' => 'CardDAV url for Contacts resource:', + 'dav_url_caldav_birthdays' => 'CalDAV url for Birthdays resources:', + 'dav_url_caldav_tasks' => 'CalDAV url for Tasks resources:', + 'dav_title_carddav' => 'CardDAV', + 'dav_title_caldav' => 'CalDAV', + 'dav_carddav_export' => 'Export all contacts in one file', + 'dav_caldav_birthdays_export' => 'Export all birthdays in one file', + 'dav_caldav_tasks_export' => 'Export all tasks in one file', + + 'archive_title' => 'Archive all of the contacts in your account', + 'archive_desc' => 'This will archive all of the contacts in your account.', + 'archive_cta' => 'Archive all of your contacts', + + 'logs_title' => 'Everything that has happened to this account', + 'logs_actor' => 'Actor', + 'logs_timestamp' => 'Timestamp', + 'logs_description' => 'Description', + 'logs_subject' => 'Subject', + 'logs_size' => 'Size (Kb)', + 'logs_object' => 'Object', +]; diff --git a/resources/lang/ar/validation.php b/resources/lang/ar/validation.php new file mode 100644 index 0000000..b30668d --- /dev/null +++ b/resources/lang/ar/validation.php @@ -0,0 +1,166 @@ + 'يجب قبول :attribute', + 'active_url' => ':attribute لا يُمثّل رابطًا صحيحًا', + 'after' => 'يجب على :attribute أن يكون تاريخًا لاحقًا للتاريخ :date.', + 'after_or_equal' => 'يجب أن يكون تاريخ :attribute بعد أو مساوياً لـ:date.', + 'alpha' => 'يجب أن يحتوي :attribute فقط على أحرف.', + 'alpha_dash' => 'يجب أن لا يحتوي :attribute سوى على حروف، أرقام ومطّات.', + 'alpha_num' => 'يجب أن يحتوي :attribute فقط على أحرف و أرقام.', + 'array' => 'يجب أن يكون :attribute مرتباً.', + 'before' => 'يجب أن يكون تاريخ :attribute قبل :date.', + 'before_or_equal' => 'يجب أن يكون تاريخ :attribute قبل أو مساوياً لـ:date.', + 'between' => [ + 'numeric' => 'يجب أن يكون :attribute بين :min و :max.', + 'file' => 'يجب أن يكون :attribute بين :min و :max كيلوبايت.', + 'string' => 'يجب أن يكون :attribute بين :min و :max من الأحرف.', + 'array' => 'يجب أن يكون لـ :attribute بين :min و :max من العناصر.', + ], + 'boolean' => 'حقل :attribute يجب أن يكون صحيحاً أو خاطئاً.', + 'confirmed' => 'تأكيد :attribute غير متطابق.', + 'date' => 'إن تاريخ :attribute غير صالح.', + 'date_equals' => 'يجب أن يكون :attribute مطابقاً للتاريخ :date.', + 'date_format' => 'إن :attribute غير متطابق مع تنسيق :format.', + 'different' => 'إن :attribute و :other يجب أن يكونا مختلفين.', + 'digits' => 'يجب أن يحتوي :attribute على :digits رقمًا/أرقام', + 'digits_between' => 'يجب أن يحتوي :attribute بين :min و :max رقمًا/أرقام ', + 'dimensions' => 'الـ :attribute يحتوي على أبعاد صورة غير صالحة.', + 'distinct' => 'للحقل :attribute قيمة مُكرّرة.', + 'email' => 'يجب أن يكون :attribute عنوان بريد إلكتروني صحيح البُنية', + 'ends_with' => 'يجب أن ينتهي :attribute بأحد القيم التالية: :values', + 'exists' => 'القيمة المحددة :attribute غير موجودة', + 'file' => 'الـ :attribute يجب أن يكون ملفا.', + 'filled' => ':attribute إجباري', + 'gt' => [ + 'numeric' => 'يجب أن تكون قيمة :attribute أكبر من :value.', + 'file' => 'يجب أن يكون حجم الملف :attribute أكبر من :value كيلوبايت.', + 'string' => 'يجب أن يكون طول النّص :attribute أكثر من :value حروفٍ/حرفًا.', + 'array' => 'يجب أن يحتوي :attribute على أكثر من :value عناصر/عنصر.', + ], + 'gte' => [ + 'numeric' => 'يجب أن تكون قيمة :attribute مساوية أو أكبر من :value.', + 'file' => 'يجب أن يكون حجم الملف :attribute على الأقل :value كيلوبايت.', + 'string' => 'يجب أن يكون طول النص :attribute على الأقل :value حروفٍ/حرفًا.', + 'array' => 'يجب أن يحتوي :attribute على الأقل على :value عُنصرًا/عناصر.', + ], + 'image' => 'يجب أن يكون :attribute صورةً', + 'in' => ':attribute غير موجود', + 'in_array' => ':attribute غير موجود في :other.', + 'integer' => 'يجب أن يكون :attribute عددًا صحيحًا', + 'ip' => 'يجب أن يكون :attribute عنوان IP صحيحًا', + 'ipv4' => 'يجب أن يكون :attribute عنوان IPv4 صحيحًا.', + 'ipv6' => 'يجب أن يكون :attribute عنوان IPv6 صحيحًا.', + 'json' => 'يجب أن يكون :attribute نصآ من نوع JSON.', + 'lt' => [ + 'numeric' => 'يجب أن تكون قيمة :attribute أصغر من :value.', + 'file' => 'يجب أن يكون حجم الملف :attribute أصغر من :value كيلوبايت.', + 'string' => 'يجب أن يكون طول النّص :attribute أقل من :value حروفٍ/حرفًا.', + 'array' => 'يجب أن يحتوي :attribute على أقل من :value عناصر/عنصر.', + ], + 'lte' => [ + 'numeric' => 'يجب أن تكون قيمة :attribute مساوية أو أصغر من :value.', + 'file' => 'يجب أن لا يتجاوز حجم الملف :attribute :value كيلوبايت.', + 'string' => 'يجب أن لا يتجاوز طول النّص :attribute :value حروفٍ/حرفًا.', + 'array' => 'يجب أن لا يحتوي :attribute على أكثر من :value عناصر/عنصر.', + ], + 'max' => [ + 'numeric' => 'يجب أن تكون قيمة :attribute مساوية أو أصغر من :max.', + 'file' => 'يجب أن لا يتجاوز حجم الملف :attribute :max كيلوبايت', + 'string' => 'يجب أن لا يتجاوز طول النّص :attribute :max حروفٍ/حرفًا', + 'array' => 'يجب أن لا يحتوي :attribute على أكثر من :max عناصر/عنصر.', + ], + 'mimes' => 'يجب أن يكون ملفًا من نوع : :values.', + 'mimetypes' => 'يجب أن يكون ملفًا من نوع : :values.', + 'min' => [ + 'numeric' => 'يجب أن تكون قيمة :attribute مساوية أو أكبر من :min.', + 'file' => 'يجب أن يكون حجم الملف :attribute على الأقل :min كيلوبايت', + 'string' => 'يجب أن يكون طول النص :attribute على الأقل :min حروفٍ/حرفًا', + 'array' => 'يجب أن يحتوي :attribute على الأقل على :min عُنصرًا/عناصر', + ], + 'not_in' => ':attribute موجود', + 'not_regex' => 'صيغة :attribute غير صحيحة.', + 'numeric' => 'يجب أن يكون :attribute رقماً.', + 'password' => 'كلمة المرور غير صحيحة.', + 'present' => 'يجب تقديم :attribute', + 'regex' => 'صيغة :attribute .غير صحيحة', + 'required' => ':attribute مطلوب.', + 'required_if' => ':attribute مطلوب في حال ما إذا كان :other يساوي :value.', + 'required_unless' => ':attribute مطلوب في حال ما لم يكن :other يساوي :values.', + 'required_with' => ':attribute مطلوب إذا توفّر :values.', + 'required_with_all' => ':attribute مطلوب إذا توفّر :values.', + 'required_without' => ':attribute مطلوب إذا لم يتوفّر :values.', + 'required_without_all' => ':attribute مطلوب إذا لم يتوفّر :values.', + 'same' => 'يجب أن يتطابق :attribute مع :other', + 'size' => [ + 'numeric' => 'يجب أن تكون قيمة :attribute مساوية لـ :size', + 'file' => 'يجب أن يكون حجم الملف :attribute :size كيلوبايت', + 'string' => 'يجب أن يحتوي النص :attribute على :size حروفٍ/حرفًا بالضبط', + 'array' => 'يجب أن يحتوي :attribute على :size عنصرٍ/عناصر بالضبط', + ], + 'starts_with' => 'يجب أن يبدأ :attribute بأحد القيم التالية: :values', + 'string' => 'يجب أن يكون :attribute نصآ.', + 'timezone' => 'يجب أن يكون :attribute نطاقًا زمنيًا صحيحًا', + 'unique' => 'قيمة :attribute مُستخدمة من قبل', + 'uploaded' => 'فشل في تحميل الـ :attribute', + 'url' => 'صيغة الرابط :attribute غير صحيحة', + 'uuid' => ':attribute يجب أن يكون بصيغة UUID سليمة.', + + /* + |-------------------------------------------------------------------------- + | Custom Validation Language Lines + |-------------------------------------------------------------------------- + | + | Here you may specify custom validation messages for attributes using the + | convention "attribute.rule" to name the lines. This makes it quick to + | specify a specific custom language line for a given attribute rule. + | + */ + + 'custom' => [ + 'attribute-name' => [ + 'rule-name' => 'custom-message', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Custom Validation Attributes + |-------------------------------------------------------------------------- + | + | The following language lines are used to swap our attribute placeholder + | with something more reader friendly such as "E-Mail Address" instead + | of "email". This simply helps us make our message more expressive. + | + */ + + 'attributes' => [], + + 'vue' => [ + 'max' => [ + 'numeric' => '{field} may not be greater than {max}.', + 'string' => '{field} may not be greater than {max} characters.', + ], + 'required' => '{field} is required.', + 'url' => '{field} is not a valid URL.', + ], + +]; diff --git a/resources/lang/cs.json b/resources/lang/cs.json new file mode 100644 index 0000000..e36ce98 --- /dev/null +++ b/resources/lang/cs.json @@ -0,0 +1,7 @@ +{ + "The :attribute must contain at least one uppercase and one lowercase letter.": "Heslo musí obsahovat alespoň jedno velké písmeno a jedno malé písmeno.", + "The :attribute must contain at least one letter.": "Heslo musí obsahovat alespoň jedno písmeno.", + "The :attribute must contain at least one symbol.": "Heslo musí obsahovat alespoň jeden symbol.", + "The :attribute must contain at least one number.": "Heslo musí obsahovat alespoň jedno číslo.", + "The given :attribute has appeared in a data leak. Please choose a different :attribute.": "Uvedené :attribute se objevilo v úniku dat. Zvolte prosím jiné :attribute." +} diff --git a/resources/lang/cs/app.php b/resources/lang/cs/app.php new file mode 100644 index 0000000..568722b --- /dev/null +++ b/resources/lang/cs/app.php @@ -0,0 +1,571 @@ + 'Ano', + 'no' => 'Ne', + 'update' => 'Aktualizovat', + 'save' => 'Uložit', + 'add' => 'Přidat', + 'cancel' => 'Zrušit', + 'confirm' => 'Potvrdit', + 'delete_confirm' => 'Jste si jisti?', + 'delete' => 'Smazat', + 'edit' => 'Upravit', + 'upload' => 'Nahrát', + 'download' => 'Stáhnout', + 'save_close' => 'Uložit a zavřít', + 'close' => 'Zavřít', + 'copy' => 'Kopírovat', + 'create' => 'Vytvořit', + 'remove' => 'Odstranit', + 'revoke' => 'Odvolat', + 'done' => 'Hotovo', + 'back' => 'Zpět', + 'verify' => 'Ověřit', + 'new' => 'nový', + 'unknown' => 'Nevím', + 'load_more' => 'Načíst další', + 'loading' => 'Načítání…', + 'with' => 's', + 'today' => 'dnes', + 'yesterday' => 'včera', + 'another_day' => 'jiný den', + 'date' => 'Datum', + 'type' => 'Typ', + 'zoom' => 'Zvětšení', + 'upgrade' => 'Upgradujte pro odemknutí', + 'percent_uploaded' => '{percent}% nahráno', + 'retry' => 'Opakovat', + 'filter' => 'Filtrovat seznam', + 'go_back' => 'Jít zpět', + 'file_selected' => 'Jeden soubor vybrán…| vybráno {count} souborů…', + + 'application_title' => 'Monica – správce osobních vztahů', + 'application_description' => 'Monica is a tool to manage your interactions with your loved ones, friends and family.', + 'application_og_title' => 'Pro lepší vztahy s vašimi blízkými. Bezplatný online CRM pro přátele a rodinu.', + + 'markdown_description' => 'Chcete pohodlně formátovat text? Podporujeme formát markdown pro značení tučně, kurzivou, vytváření seznamu a další.', + 'markdown_link' => 'Číst dokumentaci', + + 'header_settings_link' => 'Nastavení', + 'header_logout_link' => 'Odhlásit', + 'header_changelog_link' => 'Změny produktu', + + 'main_nav_cta' => 'Přidat osobu', + 'main_nav_dashboard' => 'Nástěnka', + 'main_nav_family' => 'Kontakty', + 'main_nav_journal' => 'Deník', + 'main_nav_activities' => 'Aktivity', + 'main_nav_tasks' => 'Úkoly', + + 'footer_remarks' => 'Komentáře?', + 'footer_send_email' => 'Pošlete nám e-mail', + 'footer_privacy' => 'Podmínky používání', + 'footer_release' => 'Poznámky k vydání', + 'footer_newsletter' => 'Odběr novinek', + 'footer_source_code' => 'Monica na GitHubu', + 'footer_version' => 'Verze: :version', + 'footer_new_version' => 'Je dostupné nová verze aplikace Monica', + + 'footer_modal_version_whats_new' => 'Novinky', + 'footer_modal_version_release_away' => 'Jste jedno vydání pozadu za nejnovější dostupnou verzí. Měli byste aktualizovat svou instanci.|Jste :number vydání pozadu za nejnovější dostupnou verzí. Měli byste aktualizovat svou instanci.', + + 'breadcrumb_dashboard' => 'Nástěnka', + 'breadcrumb_list_contacts' => 'Seznam kontaktů', + 'breadcrumb_archived_contacts' => 'Archivované kontakty', + 'breadcrumb_journal' => 'Deník', + 'breadcrumb_settings' => 'Nastavení', + 'breadcrumb_settings_export' => 'Export', + 'breadcrumb_settings_users' => 'Uživatelé', + 'breadcrumb_settings_users_add' => 'Přidat uživatele', + 'breadcrumb_settings_subscriptions' => 'Odběry', + 'breadcrumb_settings_import' => 'Import', + 'breadcrumb_settings_import_report' => 'Importovat report', + 'breadcrumb_settings_import_upload' => 'Nahrát', + 'breadcrumb_settings_tags' => 'Tagy', + 'breadcrumb_add_significant_other' => 'Přidat drahou polovičku', + 'breadcrumb_edit_significant_other' => 'Upravit drahou polovičku', + 'breadcrumb_add_note' => 'Přidat poznámku', + 'breadcrumb_edit_note' => 'Upravit poznámku', + 'breadcrumb_api' => 'API', + 'breadcrumb_dav' => 'DAV zdroje', + 'breadcrumb_edit_introductions' => 'Jak jste se setkali', + 'breadcrumb_settings_personalization' => 'Přizpůsobení', + 'breadcrumb_settings_security' => 'Zabezpečení', + 'breadcrumb_settings_security_2fa' => 'Dvoufázové ověření', + 'breadcrumb_profile' => 'Profil :name', + + 'gender_male' => 'Muž', + 'gender_female' => 'Žena', + 'gender_none' => 'Nepovím', + 'gender_no_gender' => 'Žádné pohlaví', + + 'error_title' => 'Jejda! Něco se pokazilo.', + 'error_unauthorized' => 'Nemáte právo upravovat tento zdroj.', + 'error_user_account' => 'Tento uživatel nepatří k danému účtu.', + 'error_save' => 'Došlo k chybě při ukládání dat.', + 'error_try_again' => 'Něco se pokazilo. Zkuste to znovu.', + 'error_id' => 'ID chyby: :id', + 'error_unavailable' => 'Služba není dostupná', + 'error_maintenance' => 'Probíhá údržba. Budeme obratem zpátky.', + 'error_help' => 'Budeme hned zpátky.', + 'error_twitter' => 'Sledujte náš Twitter účet a buďte upozorněni, až služba znovu poběží.', + 'error_no_term' => 'Pro tuto instanci zatím neexistují žádná pravidla.', + + 'default_save_success' => 'Data byla uložena.', + + 'compliance_title' => 'Omlouváme se za vyrušení.', + 'compliance_desc' => 'Změnili jsme Smluvní podmínky a Zásady ochrany osobních údajů. Podle zákona vás musíme požádat, abyste je zkontrolovali a přijali, abyste mohli nadále používat svůj účet.', + 'compliance_desc_end' => 'We don’t do anything nasty with your data or account and will never do.', + 'compliance_terms' => 'Přijmout nové podmínky a zásady ochrany osobních údajů', + + // Relationship types + // Yes, each relationship type has 8 strings associated with it. + // This is because we need to indicate the name of the relationship type, + // and also the name of the opposite side of this relationship (father/son), + // and then, the feminine version of the string. Finally, in some sentences + // in the UI, we need to include the name of the person we add the relationship + // to. + 'relationship_type_group_love' => 'Milostné vztahy', + 'relationship_type_group_family' => 'Rodinné vztahy', + 'relationship_type_group_friend' => 'Vztahy s přáteli', + 'relationship_type_group_work' => 'Pracovní vztahy', + 'relationship_type_group_other' => 'Jiný druh vztahů', + + 'relationship_type_partner' => 'drahá polovička', + 'relationship_type_partner_female' => 'drahá polovička', + 'relationship_type_partner_male' => 'drahá polovička', + 'relationship_type_partner_with_name' => ':name’s significant other', + 'relationship_type_partner_female_with_name' => ':name’s significant other', + 'relationship_type_partner_male_with_name' => ':name’s significant other', + + 'relationship_type_spouse' => 'Manžel(ka)', + 'relationship_type_spouse_female' => 'manželka', + 'relationship_type_spouse_male' => 'manžel', + 'relationship_type_spouse_with_name' => ':name’s spouse', + 'relationship_type_spouse_female_with_name' => ':name’s wife', + 'relationship_type_spouse_male_with_name' => ':name’s husband', + + 'relationship_type_date' => 'date', + 'relationship_type_date_female' => 'date', + 'relationship_type_date_male' => 'date', + 'relationship_type_date_with_name' => ':name’s date', + 'relationship_type_date_female_with_name' => ':name’s date', + 'relationship_type_date_male_with_name' => ':name’s date', + + 'relationship_type_lover' => 'lover', + 'relationship_type_lover_female' => 'lover', + 'relationship_type_lover_male' => 'lover', + 'relationship_type_lover_with_name' => ':name’s lover', + 'relationship_type_lover_female_with_name' => ':name’s lover', + 'relationship_type_lover_male_with_name' => ':name’s lover', + + 'relationship_type_inlovewith' => 'in love with', + 'relationship_type_inlovewith_female' => 'in love with', + 'relationship_type_inlovewith_male' => 'in love with', + 'relationship_type_inlovewith_with_name' => 'someone :name is in love with', + 'relationship_type_inlovewith_female_with_name' => 'someone :name is in love with', + 'relationship_type_inlovewith_male_with_name' => 'someone :name is in love with', + + 'relationship_type_lovedby' => 'loved by', + 'relationship_type_lovedby_female' => 'loved by', + 'relationship_type_lovedby_male' => 'loved by', + 'relationship_type_lovedby_with_name' => ':name’s secret lover', + 'relationship_type_lovedby_female_with_name' => ':name’s secret lover', + 'relationship_type_lovedby_male_with_name' => ':name’s secret lover', + + 'relationship_type_ex' => 'bývalý partner', + 'relationship_type_ex_female' => 'bývalá přítelkyně', + 'relationship_type_ex_male' => 'bývalý přítel', + 'relationship_type_ex_with_name' => ':name’s ex-partner', + 'relationship_type_ex_female_with_name' => ':name’s ex-girlfriend', + 'relationship_type_ex_male_with_name' => ':name’s ex-boyfriend', + + 'relationship_type_parent' => 'rodič', + 'relationship_type_parent_female' => 'matka', + 'relationship_type_parent_male' => 'otec', + 'relationship_type_parent_with_name' => ':name’s parent', + 'relationship_type_parent_female_with_name' => ':name’s mother', + 'relationship_type_parent_male_with_name' => ':name’s father', + + 'relationship_type_child' => 'dítě', + 'relationship_type_child_female' => 'dcera', + 'relationship_type_child_male' => 'syn', + 'relationship_type_child_with_name' => ':name’s child', + 'relationship_type_child_female_with_name' => ':name’s daughter', + 'relationship_type_child_male_with_name' => ':name’s son', + + 'relationship_type_stepparent' => 'step-parent', + 'relationship_type_stepparent_female' => 'stepmother', + 'relationship_type_stepparent_male' => 'stepfather', + 'relationship_type_stepparent_with_name' => ':name’s step-parent', + 'relationship_type_stepparent_female_with_name' => ':name’s stepmother', + 'relationship_type_stepparent_male_with_name' => ':name’s stepfather', + + 'relationship_type_stepchild' => 'stepchild', + 'relationship_type_stepchild_female' => 'stepdaughter', + 'relationship_type_stepchild_male' => 'stepson', + 'relationship_type_stepchild_with_name' => ':name’s stepchild', + 'relationship_type_stepchild_female_with_name' => ':name’s stepdaughter', + 'relationship_type_stepchild_male_with_name' => ':name’s stepson', + + 'relationship_type_sibling' => 'sourozenec', + 'relationship_type_sibling_female' => 'sestra', + 'relationship_type_sibling_male' => 'bratr', + 'relationship_type_sibling_with_name' => ':name’s sibling', + 'relationship_type_sibling_female_with_name' => ':name’s sister', + 'relationship_type_sibling_male_with_name' => ':name’s brother', + + 'relationship_type_grandparent' => 'prarodič', + 'relationship_type_grandparent_female' => 'babička', + 'relationship_type_grandparent_male' => 'dědeček', + 'relationship_type_grandparent_with_name' => ':name’s grandparent', + 'relationship_type_grandparent_female_with_name' => ':name’s grandmother', + 'relationship_type_grandparent_male_with_name' => ':name’s grandfather', + + 'relationship_type_grandchild' => 'vnouče', + 'relationship_type_grandchild_female' => 'vnučka', + 'relationship_type_grandchild_male' => 'vnuk', + 'relationship_type_grandchild_with_name' => ':name’s grandchild', + 'relationship_type_grandchild_female_with_name' => ':name’s granddauther', + 'relationship_type_grandchild_male_with_name' => ':name’s grandson', + + 'relationship_type_uncle' => 'strýc', + 'relationship_type_uncle_female' => 'teta', + 'relationship_type_uncle_male' => 'strýc', + 'relationship_type_uncle_with_name' => ':name’s uncle', + 'relationship_type_uncle_female_with_name' => ':name’s aunt', + 'relationship_type_uncle_male_with_name' => ':name’s uncle', + + 'relationship_type_nephew' => 'synovec', + 'relationship_type_nephew_female' => 'neteř', + 'relationship_type_nephew_male' => 'synovec', + 'relationship_type_nephew_with_name' => ':name’s nephew', + 'relationship_type_nephew_female_with_name' => ':name’s niece', + 'relationship_type_nephew_male_with_name' => ':name’s nephew', + + 'relationship_type_cousin' => 'bratranec', + 'relationship_type_cousin_female' => 'sestřenice', + 'relationship_type_cousin_male' => 'bratranec', + 'relationship_type_cousin_with_name' => ':name’s cousin', + 'relationship_type_cousin_female_with_name' => ':name’s cousin', + 'relationship_type_cousin_male_with_name' => ':name’s cousin', + + 'relationship_type_godfather' => 'godparent', + 'relationship_type_godfather_female' => 'kmotra', + 'relationship_type_godfather_male' => 'kmotr', + 'relationship_type_godfather_with_name' => ':name’s godparent', + 'relationship_type_godfather_female_with_name' => ':name’s godmother', + 'relationship_type_godfather_male_with_name' => ':name’s godfather', + + 'relationship_type_godson' => 'godchild', + 'relationship_type_godson_female' => 'goddaughter', + 'relationship_type_godson_male' => 'godson', + 'relationship_type_godson_with_name' => ':name’s godchild', + 'relationship_type_godson_female_with_name' => ':name’s goddaughter', + 'relationship_type_godson_male_with_name' => ':name’s godson', + + 'relationship_type_friend' => 'kamarád', + 'relationship_type_friend_female' => 'kamarádka', + 'relationship_type_friend_male' => 'přítel', + 'relationship_type_friend_with_name' => ':name’s friend', + 'relationship_type_friend_female_with_name' => ':name’s friend', + 'relationship_type_friend_male_with_name' => ':name’s friend', + + 'relationship_type_bestfriend' => 'nejlepší kamarád', + 'relationship_type_bestfriend_female' => 'nejlepší kamarádka', + 'relationship_type_bestfriend_male' => 'nejlepší přítel', + 'relationship_type_bestfriend_with_name' => ':name’s best friend', + 'relationship_type_bestfriend_female_with_name' => ':name’s best friend', + 'relationship_type_bestfriend_male_with_name' => ':name’s best friend', + + 'relationship_type_colleague' => 'kolega', + 'relationship_type_colleague_female' => 'kolegyně', + 'relationship_type_colleague_male' => 'colleague', + 'relationship_type_colleague_with_name' => ':name’s colleague', + 'relationship_type_colleague_female_with_name' => ':name’s colleague', + 'relationship_type_colleague_male_with_name' => ':name’s colleague', + + 'relationship_type_boss' => 'nadřízený', + 'relationship_type_boss_female' => 'nadřízená', + 'relationship_type_boss_male' => 'boss', + 'relationship_type_boss_with_name' => ':name’s boss', + 'relationship_type_boss_female_with_name' => ':name’s boss', + 'relationship_type_boss_male_with_name' => ':name’s boss', + + 'relationship_type_subordinate' => 'podřízený', + 'relationship_type_subordinate_female' => 'podřízená', + 'relationship_type_subordinate_male' => 'subordinate', + 'relationship_type_subordinate_with_name' => ':name’s subordinate', + 'relationship_type_subordinate_female_with_name' => ':name’s subordinate', + 'relationship_type_subordinate_male_with_name' => ':name’s subordinate', + + 'relationship_type_mentor' => 'mentor', + 'relationship_type_mentor_female' => 'mentor', + 'relationship_type_mentor_male' => 'mentor', + 'relationship_type_mentor_with_name' => ':name’s mentor', + 'relationship_type_mentor_female_with_name' => ':name’s mentor', + 'relationship_type_mentor_male_with_name' => ':name’s mentor', + + 'relationship_type_protege' => 'protégé', + 'relationship_type_protege_female' => 'protégé', + 'relationship_type_protege_male' => 'protégé', + 'relationship_type_protege_with_name' => ':name’s protégé', + 'relationship_type_protege_female_with_name' => ':name’s protégé', + 'relationship_type_protege_male_with_name' => ':name’s protégé', + + 'relationship_type_ex_husband' => 'bývalý manžel/ka', + 'relationship_type_ex_husband_female' => 'ex wife', + 'relationship_type_ex_husband_male' => 'bývalý manžel', + 'relationship_type_ex_husband_with_name' => ':name’s ex-spouse', + 'relationship_type_ex_husband_female_with_name' => ':name’s ex wife', + 'relationship_type_ex_husband_male_with_name' => ':name’s ex-husband', + + // emotions + 'emotion_primary_love' => 'Láska', + 'emotion_primary_joy' => 'Radost', + 'emotion_primary_surprise' => 'Překvapení', + 'emotion_primary_anger' => 'Hněv', + 'emotion_primary_sadness' => 'Smutek', + 'emotion_primary_fear' => 'Strach', + + 'emotion_secondary_affection' => 'Náklonnost', + 'emotion_secondary_lust' => 'Lust', + 'emotion_secondary_longing' => 'Longing', + 'emotion_secondary_cheerfulness' => 'Cheerfulness', + 'emotion_secondary_zest' => 'Zest', + 'emotion_secondary_contentment' => 'Contentment', + 'emotion_secondary_pride' => 'Hrdost', + 'emotion_secondary_optimism' => 'Optimismus', + 'emotion_secondary_enthrallment' => 'Enthrallment', + 'emotion_secondary_relief' => 'Úleva', + 'emotion_secondary_surprise' => 'Překvapení', + 'emotion_secondary_irritation' => 'Podrážděnost', + 'emotion_secondary_exasperation' => 'Exasperation', + 'emotion_secondary_rage' => 'Vztek', + 'emotion_secondary_disgust' => 'Znechucení', + 'emotion_secondary_envy' => 'Envy', + 'emotion_secondary_suffering' => 'Suffering', + 'emotion_secondary_sadness' => 'Smutek', + 'emotion_secondary_disappointment' => 'Zklamání', + 'emotion_secondary_shame' => 'Shame', + 'emotion_secondary_neglect' => 'Neglect', + 'emotion_secondary_sympathy' => 'Sympatie', + 'emotion_secondary_horror' => 'Horror', + 'emotion_secondary_nervousness' => 'Nervozita', + + 'emotion_adoration' => 'Adoration', + 'emotion_affection' => 'Náklonnost', + 'emotion_love' => 'Láska', + 'emotion_fondness' => 'Fondness', + 'emotion_liking' => 'Liking', + 'emotion_attraction' => 'Attraction', + 'emotion_caring' => 'Caring', + 'emotion_tenderness' => 'Tenderness', + 'emotion_compassion' => 'Compassion', + 'emotion_sentimentality' => 'Sentimentality', + 'emotion_arousal' => 'Arousal', + 'emotion_desire' => 'Desire', + 'emotion_lust' => 'Lust', + 'emotion_passion' => 'Passion', + 'emotion_infatuation' => 'Infatuation', + 'emotion_longing' => 'Longing', + 'emotion_amusement' => 'Amusement', + 'emotion_bliss' => 'Bliss', + 'emotion_cheerfulness' => 'Cheerfulness', + 'emotion_gaiety' => 'Gaiety', + 'emotion_glee' => 'Glee', + 'emotion_jolliness' => 'Jolliness', + 'emotion_joviality' => 'Joviality', + 'emotion_joy' => 'Joy', + 'emotion_delight' => 'Delight', + 'emotion_enjoyment' => 'Enjoyment', + 'emotion_gladness' => 'Gladness', + 'emotion_happiness' => 'Happiness', + 'emotion_jubilation' => 'Jubilation', + 'emotion_elation' => 'Elation', + 'emotion_satisfaction' => 'Satisfaction', + 'emotion_ecstasy' => 'Ecstasy', + 'emotion_euphoria' => 'Euphoria', + 'emotion_enthusiasm' => 'Enthusiasm', + 'emotion_zeal' => 'Zeal', + 'emotion_zest' => 'Zest', + 'emotion_excitement' => 'Excitement', + 'emotion_thrill' => 'Thrill', + 'emotion_exhilaration' => 'Exhilaration', + 'emotion_contentment' => 'Contentment', + 'emotion_pleasure' => 'Pleasure', + 'emotion_pride' => 'Pride', + 'emotion_eagerness' => 'Eagerness', + 'emotion_hope' => 'Hope', + 'emotion_optimism' => 'Optimism', + 'emotion_enthrallment' => 'Enthrallment', + 'emotion_rapture' => 'Rapture', + 'emotion_relief' => 'Relief', + 'emotion_amazement' => 'Amazement', + 'emotion_surprise' => 'Surprise', + 'emotion_astonishment' => 'Astonishment', + 'emotion_aggravation' => 'Aggravation', + 'emotion_irritation' => 'Irritation', + 'emotion_agitation' => 'Agitation', + 'emotion_annoyance' => 'Annoyance', + 'emotion_grouchiness' => 'Grouchiness', + 'emotion_grumpiness' => 'Grumpiness', + 'emotion_exasperation' => 'Exasperation', + 'emotion_frustration' => 'Frustration', + 'emotion_anger' => 'Anger', + 'emotion_rage' => 'Rage', + 'emotion_outrage' => 'Outrage', + 'emotion_fury' => 'Fury', + 'emotion_wrath' => 'Wrath', + 'emotion_hostility' => 'Hostility', + 'emotion_ferocity' => 'Ferocity', + 'emotion_bitterness' => 'Bitterness', + 'emotion_hate' => 'Hate', + 'emotion_loathing' => 'Loathing', + 'emotion_scorn' => 'Scorn', + 'emotion_spite' => 'Spite', + 'emotion_vengefulness' => 'Vengefulness', + 'emotion_dislike' => 'Dislike', + 'emotion_resentment' => 'Resentment', + 'emotion_disgust' => 'Disgust', + 'emotion_revulsion' => 'Revulsion', + 'emotion_contempt' => 'Contempt', + 'emotion_envy' => 'Envy', + 'emotion_jealousy' => 'Jealousy', + 'emotion_agony' => 'Agony', + 'emotion_suffering' => 'Suffering', + 'emotion_hurt' => 'Hurt', + 'emotion_anguish' => 'Anguish', + 'emotion_depression' => 'Depression', + 'emotion_despair' => 'Despair', + 'emotion_hopelessness' => 'Hopelessness', + 'emotion_gloom' => 'Gloom', + 'emotion_glumness' => 'Glumness', + 'emotion_sadness' => 'Sadness', + 'emotion_unhappiness' => 'Unhappiness', + 'emotion_grief' => 'Grief', + 'emotion_sorrow' => 'Sorrow', + 'emotion_woe' => 'Woe', + 'emotion_misery' => 'Misery', + 'emotion_melancholy' => 'Melancholy', + 'emotion_dismay' => 'Dismay', + 'emotion_disappointment' => 'Disappointment', + 'emotion_displeasure' => 'Displeasure', + 'emotion_guilt' => 'Guilt', + 'emotion_shame' => 'Shame', + 'emotion_regret' => 'Regret', + 'emotion_remorse' => 'Remorse', + 'emotion_alienation' => 'Alienation', + 'emotion_isolation' => 'Isolation', + 'emotion_neglect' => 'Neglect', + 'emotion_loneliness' => 'Loneliness', + 'emotion_rejection' => 'Rejection', + 'emotion_homesickness' => 'Homesickness', + 'emotion_defeat' => 'Defeat', + 'emotion_dejection' => 'Dejection', + 'emotion_insecurity' => 'Insecurity', + 'emotion_embarrassment' => 'Embarrassment', + 'emotion_humiliation' => 'Humiliation', + 'emotion_insult' => 'Insult', + 'emotion_pity' => 'Pity', + 'emotion_sympathy' => 'Sympathy', + 'emotion_alarm' => 'Alarm', + 'emotion_shock' => 'Shock', + 'emotion_fear' => 'Fear', + 'emotion_fright' => 'Fright', + 'emotion_horror' => 'Horror', + 'emotion_terror' => 'Terror', + 'emotion_panic' => 'Panic', + 'emotion_hysteria' => 'Hysteria', + 'emotion_mortification' => 'Mortification', + 'emotion_anxiety' => 'Anxiety', + 'emotion_nervousness' => 'Nervousness', + 'emotion_tenseness' => 'Tenseness', + 'emotion_uneasiness' => 'Uneasiness', + 'emotion_apprehension' => 'Apprehension', + 'emotion_worry' => 'Worry', + 'emotion_distress' => 'Distress', + 'emotion_dread' => 'Dread', + + // weather + 'weather_sunny' => 'Sunny', + 'weather_clear' => 'Clear', + 'weather_clear-day' => 'Clear', + 'weather_clear-night' => 'Jasná noc', + 'weather_light-drizzle' => 'Slabé mrholení', + 'weather_patchy-light-drizzle' => 'Místy slabé mrholení', + 'weather_patchy-light-rain' => 'Místy slabý déšť', + 'weather_light-rain' => 'Slabý déšť', + 'weather_moderate-rain-at-times' => 'Občasný mírný déšť', + 'weather_moderate-rain' => 'Mírný déšť', + 'weather_patchy-rain-possible' => 'Místy možný déšť', + 'weather_heavy-rain-at-times' => 'Občasný vydatný déšť', + 'weather_heavy-rain' => 'Vydatný déšť', + 'weather_light-freezing-rain' => 'Slabý mrznoucí déšť', + 'weather_moderate-or-heavy-freezing-rain' => 'Střední nebo silný mrznoucí déšť', + 'weather_light-sleet' => 'Slabý déšť se sněhem', + 'weather_moderate-or-heavy-rain-shower' => 'Střední nebo silné déšťové přeháňky', + 'weather_light-rain-shower' => 'Slabé dešťové přeháňky', + 'weather_torrential-rain-shower' => 'Přívalové dešťové přeháňky', + 'weather_rain' => 'Déšť', + 'weather_snow' => 'Sněžení', + 'weather_blowing-snow' => 'Zvířený sníh', + 'weather_patchy-light-snow' => 'Místy slabé sněžení', + 'weather_light-snow' => 'Slabé sněžení', + 'weather_patchy-moderate-snow' => 'Místy střední sněžení', + 'weather_moderate-snow' => 'Mírné sněžení', + 'weather_patchy-heavy-snow' => 'Místy silné sněžení', + 'weather_heavy-snow' => 'Silné sněžení', + 'weather_light-snow-showers' => 'Slabé sněhové přeháňky', + 'weather_moderate-or-heavy-snow-showers' => 'Střední nebo silné sněhové přeháňky', + 'weather_patchy-snow-possible' => 'Místy možné sněžení', + 'weather_patchy-sleet-possible' => 'Místy možný déšť se sněhem', + 'weather_moderate-or-heavy-sleet' => 'Střední nebo silný déšť se sněhem', + 'weather_light-sleet-showers' => 'Slabé přeháňky deště se sněhem', + 'weather_moderate-or-heavy-sleet-showers' => 'Střední nebo silné přeháňky deště se sněhem', + 'weather_sleet' => 'Déšť se sněhem', + 'weather_wind' => 'Vítr', + 'weather_fog' => 'Mlha', + 'weather_freezing-fog' => 'Mrznoucí mlha', + 'weather_mist' => 'Opar', + 'weather_blizzard' => 'Sněhová vánice', + 'weather_overcast' => 'Zataženo', + 'weather_cloudy' => 'Oblačno', + 'weather_partly-cloudy-day' => 'Částečně oblačno', + 'weather_partly-cloudy-night' => 'Částečně oblačno', + 'weather_freezing-drizzle' => 'Mrznoucí mrholení', + 'weather_heavy-freezing-drizzle' => 'Heavy freezing drizzle', + 'weather_patchy-freezing-drizzle-possible' => 'Patchy freezing drizzle possible', + 'weather_ice-pellets' => 'Ice pellets', + 'weather_light-showers-of-ice-pellets' => 'Light showers of ice pellets', + 'weather_moderate-or-heavy-showers-of-ice-pellets' => 'Moderate or heavy showers of ice pellets', + 'weather_thundery-outbreaks-possible' => 'Thundery outbreaks possible', + 'weather_patchy-light-rain-with-thunder' => 'Patchy light rain with thunder', + 'weather_moderate-or-heavy-rain-with-thunder' => 'Moderate or heavy rain with thunder', + 'weather_patchy-light-snow-with-thunder' => 'Patchy light snow with thunder', + 'weather_moderate-or-heavy-snow-with-thunder' => 'Moderate or heavy snow with thunder', + 'weather_current_temperature_celsius' => ':temperature °C', + 'weather_current_temperature_fahrenheit' => ':temperature °F', + 'weather_current_title' => 'Current weather', + + // dav + 'dav_contacts' => 'Contacts', + 'dav_contacts_description' => ':name’s contacts', + 'dav_birthdays' => 'Birthdays', + 'dav_birthdays_description' => ':name’s contact’s birthdays', + 'dav_tasks' => 'Tasks', + 'dav_tasks_description' => ':name’s tasks', + + // contact list + 'contact_list_avatar' => 'Avatar', + 'contact_list_name' => 'Contact', + 'contact_list_description' => 'Description', + +]; diff --git a/resources/lang/cs/auth.php b/resources/lang/cs/auth.php new file mode 100644 index 0000000..4b57ed1 --- /dev/null +++ b/resources/lang/cs/auth.php @@ -0,0 +1,89 @@ + 'Tyto přihlašovací údaje neodpovídají žadnému záznamu.', + 'throttle' => 'Příliš mnoho pokusů o přihlášení. Zkuste to prosím znovu za :seconds vteřin.', + 'not_authorized' => 'Nejste oprávněni provést tuto akci', + 'signup_disabled' => 'Nové registrace jsou aktuálně zastaveny', + 'signup_error' => 'An error occured trying to register the user', + 'back_homepage' => 'Zpět na domovskou stránku', + 'mfa_auth_otp' => 'Authenticate with your two factor device', + 'mfa_auth_webauthn' => 'Authenticate with a security key (WebAuthn)', + '2fa_title' => 'Two Factor Authentication', + '2fa_wrong_validation' => 'The two factor authentication has failed.', + '2fa_one_time_password' => 'Two factor authentication code', + '2fa_recuperation_code' => 'Enter a two factor recovery code', + '2fa_one_time_or_recuperation' => 'Enter a two factor authentication code or a recovery code', + '2fa_otp_help' => 'Open up your two factor authentication mobile app and copy the code', + + 'login_to_account' => 'Login to your account', + 'login_with_recovery' => 'Login with a recovery code', + 'login_again' => 'Please login again to your account', + 'email' => 'Email', + 'password' => 'Password', + 'recovery' => 'Recovery code', + 'login' => 'Login', + 'button_remember' => 'Remember Me', + 'password_forget' => 'Forget your password?', + 'password_reset' => 'Reset your password', + 'use_recovery' => 'Or you can use a recovery code', + 'signup_no_account' => 'Don’t have an account?', + 'signup' => 'Registrace', + 'create_account' => 'Vytvořte první účet registrací', + 'change_language_title' => 'Změnit jazyk:', + 'change_language' => 'Změnit jazyk na :lang', + + 'password_reset_title' => 'Resetovat heslo', + 'password_reset_email' => 'E-mailová adresa', + 'password_reset_send_link' => 'Odeslat odkaz na obnovení hesla', + 'password_reset_password' => 'Heslo', + 'password_reset_password_confirm' => 'Potvrdit heslo', + 'password_reset_action' => 'Resetovat heslo', + 'password_reset_email_content' => 'Pro zresetování hesla klikněte na:', + + 'register_title_welcome' => 'Vítejte v nově nainstalované instanci Monica', + 'register_create_account' => 'Abyste mohli používat aplikaci Monica je třeba založit účet', + 'register_title_create' => 'Založte svůj účet Monica', + 'register_login' => 'Přihlaste se, pokud již máte účet.', + 'register_email' => 'Zadejte platnou e-mailovou adresu', + 'register_email_example' => 'ucet@domena', + 'register_firstname' => 'Jméno', + 'register_firstname_example' => 'např. Jan', + 'register_lastname' => 'Příjmení', + 'register_lastname_example' => 'např. Novák', + 'register_password' => 'Heslo', + 'register_password_example' => 'Zadejte bezpečné heslo', + 'register_password_confirmation' => 'Potvrzení hesla', + 'register_action' => 'Registrovat', + 'register_policy' => 'Signing up signifies you’ve read and agree to our Privacy Policy and Terms of use.', + 'register_invitation_email' => 'For security purposes, please indicate the email of the person who’ve invited you to join this account. This information is provided in the invitation email.', + + 'confirmation_title' => 'Verify Your Email Address', + 'confirmation_fresh' => 'A fresh verification link has been sent to your email address.', + 'confirmation_check' => 'Before proceeding, please check your email for a verification link.', + 'confirmation_request_another' => 'If you did not receive the email click here to request another.', + + 'confirmation_again' => 'If you want to change your email address you can click here.', + 'email_change_current_email' => 'Aktuální e-mailová adresa:', + 'email_change_title' => 'Změna e-mailové adresy', + 'email_change_new' => 'Nová e-mailová adresa', + 'email_changed' => 'Your email address has been changed. Check your mailbox to validate it.', +]; diff --git a/resources/lang/cs/changelog.php b/resources/lang/cs/changelog.php new file mode 100644 index 0000000..981b018 --- /dev/null +++ b/resources/lang/cs/changelog.php @@ -0,0 +1,12 @@ + 'Product changes', + 'note' => 'Note: unfortunately, this page is only in English.', +]; diff --git a/resources/lang/cs/dashboard.php b/resources/lang/cs/dashboard.php new file mode 100644 index 0000000..325d381 --- /dev/null +++ b/resources/lang/cs/dashboard.php @@ -0,0 +1,42 @@ + 'Welcome to your account!', + 'dashboard_blank_description' => 'Monica is the place to organize all the interactions you have with the people you care about.', + 'dashboard_blank_cta' => 'Add your first contact', + 'dashboard_blank_illustration' => 'Illustration by Freepik', + + 'notes_title' => 'You don’t have any starred notes yet.', + + 'tab_recent_calls' => 'Recent calls', + 'tab_favorite_notes' => 'Favorite notes', + 'tab_calls_blank' => 'You haven’t logged any calls yet.', + 'tab_debts' => 'Debts', + 'tab_debts_blank' => 'You haven’t logged any debts yet.', + 'tab_tasks' => 'Tasks', + 'tab_tasks_blank' => 'You haven’t any tasks yet.', + + 'tasks_add_task_placeholder' => 'What is this task about?', + 'tasks_tab_your_contacts' => 'Tasks related to your contacts', + 'tasks_tab_your_tasks' => 'Your tasks', + 'tasks_add_note' => 'Press Enter to add the task.', + 'task_add_cta' => 'Add a task', + + 'debts_you_owe' => 'You owe', + + 'statistics_contacts' => 'Kontaktů', + 'statistics_activities' => 'Aktivit', + 'statistics_gifts' => 'Dárků', + + 'reminders_next_months' => 'Events in the next 3 months', + 'reminders_none' => 'No reminders for this month.', + + 'product_changes' => 'Product changes', + 'product_view_details' => 'View details', +]; diff --git a/resources/lang/cs/format.php b/resources/lang/cs/format.php new file mode 100644 index 0000000..a70a6ba --- /dev/null +++ b/resources/lang/cs/format.php @@ -0,0 +1,36 @@ + 'M d, Y H:i', + 'short_date_year' => 'M d, Y', + 'short_date' => 'M d', + 'short_month' => 'M', + 'short_month_year' => 'M Y', + 'short_day' => 'D', + 'full_date_year' => 'F d, Y', + 'full_month' => 'F', + 'full_month_year' => 'F Y', + 'full_hour' => 'h.i A', + + 'short_text' => '{text}…', +]; diff --git a/resources/lang/cs/journal.php b/resources/lang/cs/journal.php new file mode 100644 index 0000000..84242f1 --- /dev/null +++ b/resources/lang/cs/journal.php @@ -0,0 +1,38 @@ + 'How was your day? You can rate it once a day.', + 'journal_come_back' => 'Thanks. Come back tomorrow to rate your day again.', + 'journal_description' => 'Note: the journal lists both manual journal entries, and automatic entries like Activities done with your contacts. While you can delete journal entries manually, you’ll have to delete the activity directly on the contact page.', + 'journal_add' => 'Přidat deníkový záznam', + 'journal_edit' => 'Edit a journal entry', + 'journal_empty' => 'Empty journal', + 'journal_created_at' => 'Created at {date}', + 'journal_created_automatically' => 'Created automatically', + 'journal_entry_type_journal' => 'Journal entry', + 'journal_entry_type_activity' => 'Activity', + 'journal_entry_rate' => 'You rated your day.', + 'journal_add_comment' => 'Care to add a comment (optional)?', + 'journal_show_comment' => 'Show comment', + 'entry_delete_success' => 'Záznam deníku byl úspěšně smazán.', + 'journal_add_title' => 'Title (optional)', + 'journal_add_date' => 'Date', + 'journal_add_post' => 'Zápis', + 'journal_add_cta' => 'Uložit', + 'journal_blank_cta' => 'Přidej svůj první deníkový záznam', + 'journal_blank_description' => 'Deník umožňuje zaznamenávání událostí které se staly a ulehčuje jejich zapamatování.', + 'delete_confirmation' => 'Opravdu chcete smazat tento deníkový záznam?', + 'apply_filter' => 'Apply filter', + 'start_date' => 'Start Date', + 'end_date' => 'End Date', + 'per_page' => 'Per Page', + 'sort_order' => 'Sort By Created At', + 'ascending' => 'Ascending', + 'descending' => 'Descending', +]; diff --git a/resources/lang/cs/logs.php b/resources/lang/cs/logs.php new file mode 100644 index 0000000..7b6654b --- /dev/null +++ b/resources/lang/cs/logs.php @@ -0,0 +1,29 @@ + 'Created the contact.', + 'settings_log_contact_created_with_name' => 'Added :name as a contact.', + + // contat description update + 'contact_log_contact_description_updated' => 'Updated the description.', + 'settings_log_contact_description_updated_with_name' => 'Updated the description of :name.', + + // contact description clear + 'contact_log_contact_description_cleared' => 'Cleared the description.', + 'settings_log_contact_description_cleared_with_name' => 'Cleared the description of :name.', + + // contact work information update + 'contact_log_contact_work_updated' => 'Updated work information.', + 'settings_log_contact_work_updated_with_name' => 'Updated work information of :name.', + + // company created + 'settings_log_company_created' => 'Created a company called :name.', +]; diff --git a/resources/lang/cs/mail.php b/resources/lang/cs/mail.php new file mode 100644 index 0000000..c6d3866 --- /dev/null +++ b/resources/lang/cs/mail.php @@ -0,0 +1,53 @@ + 'Připomínka pro :contact', + 'greetings' => 'Ahoj :username', + 'want_reminded_of' => 'You wanted to be reminded of :reason', + 'for' => 'For: :name', + 'comment' => 'Comment: :comment', + 'footer_contact_info' => 'Přidat, zobrazit, dodat a změnit informace k této osobě:', + 'footer_contact_info2' => 'See :name’s profile', + 'footer_contact_info2_link' => 'See :name’s profile: :url', + + 'notification_subject_line' => 'You have an upcoming event', + 'notification_description' => 'In :count days (on :date), the following event will happen:', + + 'stay_in_touch_subject_line' => 'Stay in touch with :name', + 'stay_in_touch_subject_description' => 'You asked to be reminded to stay in touch with :name every :frequency day.|You asked to be reminded to stay in touch with :name every :frequency days.', + + 'notifications_whoops' => 'Whoops!', + 'notifications_hello' => 'Hello!', + 'notifications_regards' => 'Regards', + 'notifications_footer' => 'If you’re having trouble clicking the ":actionText" button, copy and paste the URL below into your web browser: [:actionURL](:actionURL)', + 'notifications_rights' => 'Všechna práva vyhrazena', + + 'confirmation_email_title' => 'Monica – ověření e-mailu', + 'confirmation_email_intro'=> 'Pro ověření svého e-mailu klikněte na tlačítko níže', + 'confirmation_email_button' => 'Ověřit e-mailovou adresu', + 'confirmation_email_bottom' => 'Pokud jste si nevytvořili účet sami, není vyžadována žádná další akce.', + + 'password_reset_title' => 'Monica – oznámení o obnovení hesla', + 'password_reset_intro' => 'Tento e-mail jste obdrželi, protože jsme dostali žádost o obnovení hesla k Vašemu účtu.', + 'password_reset_button' => 'Resetovat heslo', + 'password_reset_expiration' => 'Tento odkaz pro obnovení hesla vyprší za :count minut.', + 'password_reset_bottom' => 'If you did not request a password reset, no further action is required.', + + 'invitation_title' => 'Monica – You are invited by :name', + 'invitation_intro' => 'You’ve been invited by :name (:email) to use Monica, a nice Personal Relationship Management tool.', + 'invitation_link' => 'To accept the invitation, click on the link below:', + 'invitation_button' => 'Accept invitation', + 'invitation_expiration' => 'This link will expire in :count days.', + + 'export_title' => 'Váš export je připraven', + 'export_description' => 'Žádali jste o export dat dne :date. Nyní je připraven ke stažení.', + 'export_download' => 'Stáhnout export', + +]; diff --git a/resources/lang/cs/pagination.php b/resources/lang/cs/pagination.php new file mode 100644 index 0000000..5b8a6f0 --- /dev/null +++ b/resources/lang/cs/pagination.php @@ -0,0 +1,25 @@ + '❮ Předchozí', + 'next' => 'Další ❯', + +]; diff --git a/resources/lang/cs/passwords.php b/resources/lang/cs/passwords.php new file mode 100644 index 0000000..8017b0b --- /dev/null +++ b/resources/lang/cs/passwords.php @@ -0,0 +1,30 @@ + 'Heslo bylo obnoveno!', + 'sent' => 'E-mail s instrukcemi k obnovení hesla byl odeslán!', + 'token' => 'Klíč pro obnovu hesla je nesprávný.', + 'user' => 'Nepodařilo se najít uživatele s touto e-mailovou adresou.', + 'changed' => 'Password changed successfully.', + 'invalid' => 'Current password you entered is not correct.', + 'throttled' => 'Please wait before retrying.', + +]; diff --git a/resources/lang/cs/people.php b/resources/lang/cs/people.php new file mode 100644 index 0000000..96ad7b3 --- /dev/null +++ b/resources/lang/cs/people.php @@ -0,0 +1,539 @@ + 'Kontakt nenalezen', + 'people_list_number_kids' => ':počet dětí', + 'people_list_last_updated' => 'Naposledy konzultováno:', + 'people_list_number_reminders' => ':počet upozornění', + 'people_list_blank_title' => 'Zatím jste do svého účtu nikoho nepřidali', + 'people_list_blank_cta' => 'Někoho přidat', + 'people_list_sort' => 'Řazení', + 'people_list_stats' => ':počet kontaktů', + 'people_list_firstnameAZ' => 'Řadit podle jména A → Z', + 'people_list_firstnameZA' => 'Řadit podle jména Z → A', + 'people_list_lastnameAZ' => 'Řadit podle příjmení A → Z', + 'people_list_lastnameZA' => 'Řadit podle příjmení Z → A', + 'people_list_lastactivitydateNewtoOld' => 'Seřadit podle posledního data aktivity od nejnovějšího k nejstaršímu', + 'people_list_lastactivitydateOldtoNew' => 'Seřadit podle posledního data aktivity od nejstaršího k nejnovějšímu', + 'people_list_filter_tag' => 'Zobrazeny všechny kontakty s tagem', + 'people_list_clear_filter' => 'Vyčistit filtr', + 'people_list_contacts_per_tags' => ':počet kontaktů', + 'people_list_show_dead' => 'Zobrazit zemřelé osoby (:count)', + 'people_list_hide_dead' => 'Skrýt zemřelé osoby (:count)', + 'people_search' => 'Prohledat kontakty…', + 'people_search_no_results' => 'Nenalezen žádný výsledek', + 'people_search_next' => 'Další', + 'people_search_prev' => 'Předchozí', + 'people_search_rows_per_page' => 'Řádků na stránku', + 'people_search_of' => 'z', + 'people_search_page' => 'Stránka', + 'people_search_all' => 'Vše ', + 'people_add_new' => 'Přidat novou osobu', + 'people_list_account_usage' => 'Využití vašeho účtu: :current/:limit kontaktů', + 'people_list_account_upgrade_title' => 'Upgradujte svůj účet a odemkněte jej na jeho plný potenciál.', + 'people_list_account_upgrade_cta' => 'Upgradovat nyní', + 'people_list_untagged' => 'Zobrazit neoznačené kontakty', + 'people_list_filter_untag' => 'Zobrazení všech neoznačených kontaktů', + 'archived_contact_readonly' => 'Archivovaný kontakt nelze upravit, nejprve jej prosím odarchivujte.', + + // people add + 'people_add_title' => 'Přidat novou osobu', + 'people_add_missing' => 'Nenalezena žádná osoba – přidejte novou osobu', + 'people_add_firstname' => 'Jméno', + 'people_add_middlename' => 'Prostřední jméno (volitelné)', + 'people_add_lastname' => 'Příjmení (volitelné)', + 'people_add_email' => 'E-mail (volitelné)', + 'people_add_nickname' => 'Přezdívka (volitelné)', + 'people_add_cta' => 'Přidat tuto osobu', + 'people_save_and_add_another_cta' => 'Odeslat a přidat někoho ďaľšího', + 'people_add_success' => 'Osoba :name byla úspěšně vytvořena', + 'people_add_gender' => 'Pohlaví', + 'people_delete_success' => 'Kontakt byl smazán', + 'people_delete_message' => 'Smazat kontakt', + 'people_delete_confirmation' => 'Opravdu chcete smazat tento kontakt? Smazání je trvalé.', + 'people_add_birthday_reminder' => 'Popřát k narozeninám :name', + 'people_add_birthday_reminder_deceased' => 'On this date, :name would have celebrated their birthday', + 'people_add_import' => 'Chcete importovat své kontakty?', + 'people_edit_email_error' => 'There is already a contact in your account with this email address. Please choose another one.', + 'people_export' => 'Exportovat jako vCard vizitku', + 'people_add_reminder_for_birthday' => 'Vytvořit každoroční připomenutí narozenin', + + // show + 'section_contact_information' => 'Kontaktní údaje', + 'section_personal_activities' => 'Aktivity', + 'section_personal_reminders' => 'Upozornění', + 'section_personal_tasks' => 'Úkoly', + 'section_personal_gifts' => 'Dárky', + 'section_personal_notes' => 'Notes', + + // archived contacts + 'list_link_to_active_contacts' => 'You are viewing archived contacts. See the list of active contacts instead.', + 'list_link_to_archived_contacts' => 'List of archived contacts', + + // Header + 'me' => 'This is you', + 'edit_contact_information' => 'Upravit informace kontaktu', + 'contact_archive' => 'Archive contact', + 'contact_unarchive' => 'Unarchive contact', + 'contact_archive_help' => 'Archivované kontakty se nezobrazují v seznamu kontaktů, ale stále se zobrazují ve výsledcích vyhledávání.', + 'call_button' => 'Zaznamenat telefonát', + 'set_favorite' => 'Favorite contacts are placed at the top of the contact list', + + // Stay in touch + 'stay_in_touch' => 'Stay in touch', + 'stay_in_touch_frequency' => 'Stay in touch every day|Stay in touch every {count} days', + 'stay_in_touch_next_date' => 'Next due: {date}', + 'stay_in_touch_invalid' => 'The frequency must be a number greater than 0.', + 'stay_in_touch_premium' => 'You need to upgrade your account to make use of this feature', + 'stay_in_touch_modal_title' => 'Stay in touch', + 'stay_in_touch_modal_desc' => 'We can remind you by email to keep in touch with {firstname} at a regular interval.', + 'stay_in_touch_modal_label' => 'Pošlete mi e-mail každých… {count} dní|Pošlete mi e-mail každých… {count} dní', + + // Calls + 'modal_call_title' => 'Zaznamenat telefonát', + 'modal_call_comment' => 'O čem byla řeč? (volitelné)', + 'modal_call_exact_date' => 'Telefonovali jsme', + 'modal_call_who_called' => 'Who called?', + 'modal_call_emotion' => 'Do you want to log how you felt during this call? (optional)', + 'calls_add_success' => 'Údaje o telefonátu byly uloženy.', + 'call_delete_confirmation' => 'Opravdu chcete údaje o telefonátu vymazat?', + 'call_delete_success' => 'Údaje o telefonátu byly úspěšně smazány', + 'call_title' => 'Telefonáty', + 'call_empty_comment' => 'Bez detailů', + 'call_blank_title' => 'Keep track of the phone calls you’ve done with {name}', + 'call_blank_desc' => 'You called {name}', + 'call_you_called' => 'You called', + 'call_he_called' => '{name} called', + 'call_emotions' => 'Emotions:', + + // Conversation + 'conversation_blank' => 'Zaznamenejte konverzace, které máte s :name na sociálních médiích, SMS…', + 'conversation_delete_link' => 'Delete the conversation', + 'conversation_edit_title' => 'Edit conversation', + 'conversation_edit_delete' => 'Are you sure you want to delete this conversation? Deletion is permanent.', + 'conversation_add_success' => 'The conversation has been successfully added.', + 'conversation_edit_success' => 'The conversation has been successfully updated.', + 'conversation_delete_success' => 'The conversation has been successfully deleted.', + 'conversation_add_title' => 'Record a new conversation', + 'conversation_add_when' => 'When did you have this conversation?', + 'conversation_add_who_wrote' => 'Kdo odeslal tuto zprávu?', + 'conversation_add_how' => 'How did you communicate?', + 'conversation_add_you' => 'You', + 'conversation_add_content' => 'Write down what was said', + 'conversation_add_what_was_said' => 'What did you say?', + 'conversation_add_another' => 'Add another message', + 'conversation_add_error' => 'You must add at least one message.', + 'conversation_list_table_messages' => 'Zprávy', + 'conversation_list_table_content' => 'Partial content (last message)', + 'conversation_list_title' => 'Konverzace', + 'conversation_list_cta' => 'Zaznamenat konverzaci', + + // age - birthday + 'birthdate_not_set' => 'Birthday is not set', + 'age_approximate_in_years' => 'věk okolo :age', + 'age_exact_in_years' => ':age let', + 'age_exact_birthdate' => 'narozeniny :date', + + // Last called + 'last_called' => 'Last called: :date', + 'last_talked_to' => 'Last called: {date}', + 'last_called_empty' => 'Last called: unknown', + 'last_activity_date' => 'Last activity together: :date', + 'last_activity_date_empty' => 'Last activity together: unknown', + + // additional information + 'information_edit_success' => 'Profil byl úspěšně aktualizován', + 'information_edit_title' => 'Upravit osobní informace o :name', + 'information_edit_max_size' => 'Max :size Kb.', + 'information_edit_max_size2' => 'Max {size} Kb.', + 'information_edit_firstname' => 'Jméno', + 'information_edit_lastname' => 'Last name (optional)', + 'information_edit_description' => 'Description (optional)', + 'information_edit_description_help' => 'Used on the contact list to add some context, if necessary.', + 'information_edit_unknown' => 'Neznám věk této osoby', + 'information_edit_probably' => 'Tato osoba je pravděpodobně…', + 'information_edit_not_year' => 'Znám den a měsíc narození, ale ne rok…', + 'information_edit_exact' => 'I know this person’s exact birthday…', + 'information_edit_birthdate_label' => 'Birthday', + 'information_no_work_defined' => 'Žádné informace o práci', + 'information_work_at' => 'v :company', + 'work_add_cta' => 'Aktualizovat informace o práci', + 'work_edit_success' => 'Údaje o zaměstnání byly aktualizovány', + 'work_edit_title' => 'Aktualizovat informace o práci pro :name', + 'work_edit_job' => 'Pracovní pozice (volitelné)', + 'work_edit_company' => 'Společnost (volitelné)', + 'work_information' => 'Work information', + + // food preferences + 'food_preferences_add_success' => 'Informace o oblíbených potravinách uloženy', + 'food_preferences_edit_description' => 'Možná má :firstname nebo někdo z rodiny :family alergii. Nebo nemusí nějaké specifické víno. Poznačte si to zde, abyste si vzpoměli před příštím pozváním na večeři', + 'food_preferences_edit_description_no_last_name' => 'Možná má :firstname alergii. Nebo nemusí nějaké specifické víno. Poznačte si to zde, abyste si vzpoměli před příštím pozváním na večeři', + 'food_preferences_edit_title' => 'Zapsat upřednostňované potraviny', + 'food_preferences_edit_cta' => 'Uložit informace o potravinách', + 'food_preferences_title' => 'Upřednostňované potraviny', + 'food_preferences_cta' => 'Přidat upřednostňované potraviny', + + // reminders + 'reminders_blank_title' => 'Je něco na co chcete být upozorňováni pro osobu :name?', + 'reminders_blank_add_activity' => 'Přidat upozornění', + 'reminders_add_title' => 'Na co chcete být upozorňováni pro osobu :name?', + 'reminders_add_description' => 'Please remind me to…', + 'reminders_add_next_time' => 'Kdy budete chtít být na tuto skutečnost příště upozorněni?', + 'reminders_add_once' => 'Upozornit pouze jedenkrát', + 'reminders_add_recurrent' => 'Upozornit', + 'reminders_add_starting_from' => 'po datu zadaném výše', + 'reminders_add_cta' => 'Přidat upozornění', + 'reminders_edit_update_cta' => 'Update reminder', + 'reminders_add_error_custom_text' => 'Musíte zadat text tohoto upozornění', + 'reminders_create_success' => 'Upozornění bylo úspěšně přidáno', + 'reminders_delete_success' => 'Upozornění bylo úspěšně smazáno', + 'reminders_update_success' => 'The reminder has been updated successfully', + 'reminders_add_optional_comment' => 'Optional comment', + + 'reminder_frequency_day' => 'every day|every :number days', + 'reminder_frequency_week' => 'každý týden|každé :number týdny', + 'reminder_frequency_month' => 'každý měsíc|každé :number měsíce', + 'reminder_frequency_year' => 'každý rok|každé :number roky', + 'reminder_frequency_one_time' => ':date', + 'reminders_delete_confirmation' => 'Opravdu chcete smazat toto upozornění?', + 'reminders_delete_cta' => 'Smazat', + 'reminders_next_expected_date' => 'v', + 'reminders_cta' => 'Přidat upozornění', + 'reminders_description' => 'We will send an email for each one of the reminders below. Reminders are sent every morning the day events will happen. Reminders automatically added for birthdays can not be deleted. If you want to change those dates, edit the birthday of the contacts.', + 'reminders_one_time' => 'Jedenkrát', + 'reminders_type_week' => 'týdně', + 'reminders_type_month' => 'měsíčně', + 'reminders_type_year' => 'ročně', + 'reminders_birthday' => 'Narozeniny má :name', + 'reminders_free_plan_warning' => 'You are on the Free plan. No emails are sent on this plan. To receive your reminders by email, upgrade your account.', + + // relationships + 'relationship_form_add' => 'Add a new relationship', + 'relationship_form_edit' => 'Edit an existing relationship', + 'relationship_form_is_with' => 'Tato osoba je…', + 'relationship_form_is_with_name' => 'Jméno je…', + 'relationship_form_add_choice' => 'Who is the relationship with?', + 'relationship_form_create_contact' => 'Add a new person', + 'relationship_form_associate_contact' => 'An existing contact', + 'relationship_form_associate_dropdown' => 'Search and select an existing contact from the dropdown below', + 'relationship_form_associate_dropdown_placeholder' => 'Search and select an existing contact', + 'relationship_form_also_create_contact' => 'Create a Contact entry for this person.', + 'relationship_form_add_description' => 'This will let you treat this person like any other contact.', + 'relationship_form_add_no_existing_contact' => 'You don’t have any contacts who can be related to :name at the moment.', + 'relationship_delete_confirmation' => 'Are you sure you want to delete this relationship? Deletion is permanent.', + 'relationship_unlink_confirmation' => 'Are you sure you want to delete this relationship? This person will not be deleted – only the relationship between the two.', + 'relationship_form_add_success' => 'The relationship has been successfully set.', + 'relationship_form_deletion_success' => 'The relationship has been deleted.', + + // tasks + 'tasks_title' => 'Tasks', + 'tasks_blank_title' => 'You don’t have any tasks yet.', + 'tasks_form_title' => 'Title', + 'tasks_form_description' => 'Description (optional)', + 'tasks_add_task' => 'Přidat úkol', + 'tasks_delete_success' => 'Úkol byl úspěšně smazán', + 'tasks_complete_success' => 'Úkol úspěšně změnil svůj stav', + + // activities + 'activity_title' => 'Aktivity', + 'activity_type_category_simple_activities' => 'Simple activities', + 'activity_type_category_sport' => 'Sport', + 'activity_type_category_food' => 'Food', + 'activity_type_category_cultural_activities' => 'Cultural activities', + 'activity_type_just_hung_out' => 'společný čas', + 'activity_type_watched_movie_at_home' => 'sledování filmu doma', + 'activity_type_talked_at_home' => 'promluvili jsme si doma', + 'activity_type_did_sport_activities_together' => 'played a sport together', + 'activity_type_ate_at_his_place' => 'ate at their place', + 'activity_type_went_bar' => 'návštěva baru', + 'activity_type_ate_at_home' => 'jídlo doma', + 'activity_type_picnicked' => 'picnicked', + 'activity_type_ate_restaurant' => 'jídlo v restauraci', + 'activity_type_went_theater' => 'návštěva divadla', + 'activity_type_went_concert' => 'návštěva koncertu', + 'activity_type_went_play' => 'návštěva zápasu', + 'activity_type_went_museum' => 'návštěva muzea', + 'activities_add_activity' => 'Přidat aktivitu', + 'activities_add_more_details' => 'Add more details', + 'activities_add_emotions' => 'Add emotions', + 'activities_add_category' => 'Indicate a category', + 'activities_add_participants_cta' => 'Add participants', + 'activities_item_information' => ':Activity. Stalo se :date', + 'activities_add_title' => 'What did you do with {name}?', + 'activities_summary' => 'Popište co jste dělali', + 'activities_add_pick_activity' => 'Would you like to categorize this activity? You don’t have to, but it will give you statistics later on (optional)', + 'activities_add_date_occured' => 'Aktivita se stala dne…', + 'activities_add_participants' => 'Who, apart from {name}, participated in this activity? (optional)', + 'activities_add_emotions_title' => 'Do you want to log how you felt during this activity? (optional)', + 'activities_blank_title' => 'Keep track of what you’ve done with {name} in the past, and what you’ve talked about', + 'activities_blank_add_activity' => 'Přidat aktivitu', + 'activities_add_success' => 'Aktivita byla úspěšně přidána', + 'activities_add_error' => 'Error when adding the activity', + 'activities_update_success' => 'Aktivita byla úspěšně aktualizována', + 'activities_delete_success' => 'Aktivita byla úspěšně smazána', + 'activities_who_was_involved' => 'Kdo byl zapojen?', + 'activities_activity' => 'Activity Category', + 'activities_view_activities_report' => 'View activities report', + 'activities_profile_title' => 'Activities report between :name and you', + 'activities_profile_subtitle' => 'You’ve logged :total_activities activity with :name in total and :activities_last_twelve_months in the last 12 months so far.|You’ve logged :total_activities activities with :name in total and :activities_last_twelve_months in the last 12 months so far.', + 'activities_profile_year_summary_activity_types' => 'Here is a breakdown of the type of activities you’ve done together in :year', + 'activities_profile_year_summary' => 'Here is what you two have done in :year', + 'activities_profile_number_occurences' => ':value activity|:value activities', + 'activities_list_participants' => 'Participants ({total}):', + 'activities_list_emotions' => 'Emotions felt:', + 'activities_list_date' => 'Happened on', + 'activities_list_category' => 'Category:', + + // notes + 'notes_create_success' => 'Poznámka byla úspěšně vytvořena', + 'notes_update_success' => 'Poznámka byla úspěšně uložena', + 'notes_delete_success' => 'Poznámka byla úspěšně smazána', + 'notes_add_cta' => 'Přidat poznámku', + 'notes_favorite' => 'Add/remove from favorites', + 'notes_delete_title' => 'Delete a note', + 'notes_delete_confirmation' => 'Opravdu chcete smazat tuto poznámku? Smazání je trvalé.', + + // gifts + 'gifts_title' => 'Gifts', + 'gifts_add_success' => 'Dárek byl úspěšně přidán', + 'gifts_delete_success' => 'Dárek byl úspěšně smazán', + 'gifts_delete_confirmation' => 'Opravdu chcete smazat tento dárek?', + 'gifts_add_gift' => 'Přidat dárek', + 'gifts_link' => 'Odkaz', + 'gifts_for' => 'For: {name}', + 'gifts_delete_cta' => 'Smazat', + 'gifts_add_title' => 'Správa dárků pro :name', + 'gifts_add_gift_idea' => 'Nápad na dárek', + 'gifts_add_gift_already_offered' => 'Dárek již darován', + 'gifts_add_gift_received' => 'Gift received', + 'gifts_add_gift_title' => 'Co je tento dárek zač?', + 'gifts_add_gift_name' => 'Gift name', + 'gifts_add_link' => 'Odkaz na webovou stránku (volitelné)', + 'gifts_add_value' => 'Hodnota (volitelné)', + 'gifts_add_comment' => 'Komentář (volitelné)', + 'gifts_add_recipient' => 'Recipient (optional)', + 'gifts_add_recipient_field' => 'Recipient', + 'gifts_add_photo' => 'Photo (optional)', + 'gifts_add_photo_title' => 'Add a photo for this gift', + 'gifts_add_someone' => 'This gift is for someone in {name}’s family in particular', + 'gifts_delete_title' => 'Delete a gift', + 'gifts_ideas' => 'Gift ideas', + 'gifts_offered' => 'Darováno', + 'gifts_offered_as_an_idea' => 'Mark as an idea', + 'gifts_received' => 'Gifts received', + 'gifts_view_comment' => 'View comment', + 'gifts_mark_offered' => 'Mark as given', + 'gifts_update_success' => 'The gift has been updated successfully', + 'gifts_add_date' => 'Date (optional)', + + // debts + 'debt_delete_confirmation' => 'Opravdu chcete smazat tento dluh?', + 'debt_delete_success' => 'Dluh byl úspěšně smazán', + 'debt_add_success' => 'Dluh byl úspěšně přidán', + 'debt_title' => 'Dluhů', + 'debt_add_cta' => 'Přidat dluh', + 'debt_you_owe' => 'Dlužím :amount', + 'debt_they_owe' => ':name mi dluží :amount', + 'debt_add_title' => 'Správa dluhů', + 'debt_add_you_owe' => 'Dlužím :name', + 'debt_add_they_owe' => ':name mi dluží', + 'debt_add_amount' => 'celkem', + 'debt_add_reason' => 'z následujícího důvodu (volitelné)', + 'debt_add_add_cta' => 'Přidat dluh', + 'debt_edit_update_cta' => 'Aktualizovat dluh', + 'debt_edit_success' => 'Dluh byl úspěšně aktualizován', + 'debts_blank_title' => 'Spravovat dluh pro :name nebo :name dlužící mně', + + // tags + 'tag_edit' => 'Upravit tag', + 'tag_add' => 'Add tags', + 'tag_add_search' => 'Add or search tags', + 'tag_no_tags' => 'No tags yet', + + // Introductions + 'introductions_sidebar_title' => 'How you met', + 'introductions_blank_cta' => 'Indicate how you met :name', + 'introductions_title_edit' => 'How did you meet :name?', + 'introductions_additional_info' => 'Explain how and where you met', + 'introductions_edit_met_through' => 'Has someone introduced you to this person?', + 'introductions_no_met_through' => 'No one', + 'introductions_first_met_date' => 'Date you met', + 'introductions_no_first_met_date' => 'I don’t know the date we met', + 'introductions_first_met_date_known' => 'This is the date we met', + 'introductions_add_reminder' => 'Add a reminder to celebrate this encounter on the anniversary this event happened', + 'introductions_update_success' => 'You’ve successfully updated the information about how you met this person', + 'introductions_met_through' => 'Met through :name', + 'introductions_met_date' => 'Met on :date', + 'introductions_reminder_title' => 'Anniversary of the day you first met', + + // Deceased + 'deceased_reminder_title' => 'Anniversary of the death of :name', + 'deceased_mark_person_deceased' => 'Mark this as deceased', + 'deceased_know_date' => 'I know the date that this person died', + 'deceased_add_reminder' => 'Add a reminder for this date', + 'deceased_label' => 'Deceased', + 'deceased_date_label' => 'Deceased date', + 'deceased_label_with_date' => 'Deceased on :date', + 'deceased_age' => 'Age at death', + + // Contact information + 'contact_info_title' => 'Contact information', + 'contact_info_form_content' => 'Content', + 'contact_info_form_contact_type' => 'Contact type', + 'contact_info_form_personalize' => 'Personalize', + 'contact_info_address' => 'Lives in', + + // Addresses + 'contact_address_title' => 'Addresses', + 'contact_address_form_name' => 'Label (optional)', + 'contact_address_form_street' => 'Street (optional)', + 'contact_address_form_city' => 'City (optional)', + 'contact_address_form_province' => 'Province (optional)', + 'contact_address_form_postal_code' => 'Postal code (optional)', + 'contact_address_form_country' => 'Country (optional)', + 'contact_address_form_latitude' => 'Latitude (numbers only) (optional)', + 'contact_address_form_longitude' => 'Longitude (numbers only) (optional)', + + // Pets + 'pets_kind' => 'Kind of pet', + 'pets_name' => 'Name (optional)', + 'pets_create_success' => 'The pet has been successfully added', + 'pets_update_success' => 'The pet has been updated', + 'pets_delete_success' => 'The pet has been deleted', + 'pets_title' => 'Pets', + 'pets_reptile' => 'Reptile', + 'pets_bird' => 'Bird', + 'pets_cat' => 'Cat', + 'pets_dog' => 'Dog', + 'pets_fish' => 'Fish', + 'pets_hamster' => 'Hamster', + 'pets_horse' => 'Horse', + 'pets_rabbit' => 'Rabbit', + 'pets_rat' => 'Rat', + 'pets_small_animal' => 'Small animal', + 'pets_other' => 'Other', + + // life events + 'life_event_list_tab_life_events' => 'Life events', + 'life_event_list_tab_other' => 'Poznámky, připomenutí, …', + 'life_event_list_title' => 'Life events', + 'life_event_blank' => 'Log what happens to the life of {name} for your future reference.', + 'life_event_list_cta' => 'Add life event', + 'life_event_create_category' => 'All categories', + 'life_event_create_life_event' => 'Add life event', + 'life_event_create_default_title' => 'Title (optional)', + 'life_event_create_default_story' => 'Story (optional)', + 'life_event_create_date' => 'You do not need to indicate a month or a day – only the year is mandatory.', + 'life_event_create_default_description' => 'Add information about what you know', + 'life_event_create_add_yearly_reminder' => 'Add a yearly reminder for this event', + 'life_event_create_success' => 'The life event has been added', + 'life_event_delete_title' => 'Delete a life event', + 'life_event_delete_description' => 'Are you sure you want to delete this life event? Deletion is permanent.', + 'life_event_delete_success' => 'The life event has been deleted', + 'life_event_date_it_happened' => 'Date it happened', + 'life_event_category_work_education' => 'Work & education', + 'life_event_category_family_relationships' => 'Family & relationships', + 'life_event_category_home_living' => 'Home & living', + 'life_event_category_health_wellness' => 'Health & wellness', + 'life_event_category_travel_experiences' => 'Travel & experiences', + 'life_event_sentence_new_job' => 'Started a new job', + 'life_event_sentence_retirement' => 'Retired', + 'life_event_sentence_new_school' => 'Started school', + 'life_event_sentence_study_abroad' => 'Studied abroad', + 'life_event_sentence_volunteer_work' => 'Started volunteering', + 'life_event_sentence_published_book_or_paper' => 'Published a paper', + 'life_event_sentence_military_service' => 'Started military service', + 'life_event_sentence_new_relationship' => 'Started a relationship', + 'life_event_sentence_engagement' => 'Got engaged', + 'life_event_sentence_marriage' => 'Got married', + 'life_event_sentence_anniversary' => 'Anniversary', + 'life_event_sentence_expecting_a_baby' => 'Expects a baby', + 'life_event_sentence_new_child' => 'Had a child', + 'life_event_sentence_new_family_member' => 'Added a family member', + 'life_event_sentence_new_pet' => 'Got a pet', + 'life_event_sentence_end_of_relationship' => 'Ended a relationship', + 'life_event_sentence_loss_of_a_loved_one' => 'Lost a loved one', + 'life_event_sentence_moved' => 'Moved', + 'life_event_sentence_bought_a_home' => 'Bought a home', + 'life_event_sentence_home_improvement' => 'Made a home improvement', + 'life_event_sentence_holidays' => 'Went on holidays', + 'life_event_sentence_new_vehicle' => 'Got a new vehicle', + 'life_event_sentence_new_roommate' => 'Got a roommate', + 'life_event_sentence_overcame_an_illness' => 'Overcame an illness', + 'life_event_sentence_quit_a_habit' => 'Quit a habit', + 'life_event_sentence_new_eating_habits' => 'Started new eating habits', + 'life_event_sentence_weight_loss' => 'Lost weight', + 'life_event_sentence_wear_glass_or_contact' => 'Started to wear glass or contact lenses', + 'life_event_sentence_broken_bone' => 'Broke a bone', + 'life_event_sentence_removed_braces' => 'Removed braces', + 'life_event_sentence_surgery' => 'Had surgery', + 'life_event_sentence_dentist' => 'Went to the dentist', + 'life_event_sentence_new_sport' => 'Started a sport', + 'life_event_sentence_new_hobby' => 'Started a hobby', + 'life_event_sentence_new_instrument' => 'Learned a new instrument', + 'life_event_sentence_new_language' => 'Learned a new language', + 'life_event_sentence_tattoo_or_piercing' => 'Got a tattoo or piercing', + 'life_event_sentence_new_license' => 'Got a license', + 'life_event_sentence_travel' => 'Traveled', + 'life_event_sentence_achievement_or_award' => 'Got an achievement or award', + 'life_event_sentence_changed_beliefs' => 'Changed beliefs', + 'life_event_sentence_first_word' => 'Spoke for the first time', + 'life_event_sentence_first_kiss' => 'Kissed for the first time', + + // documents + 'document_list_title' => 'Documents', + 'document_list_cta' => 'Upload document', + 'document_list_blank_desc' => 'Here you can store documents related to this person.', + 'document_upload_zone_cta' => 'Upload a file', + 'document_upload_zone_progress' => 'Nahrávání dokumentu…', + 'document_upload_zone_error' => 'There was an error uploading the document. Please try again below.', + + // Photos + 'photo_title' => 'Photos', + 'photo_list_title' => 'Related photos', + 'photo_list_cta' => 'Upload photo', + 'photo_list_blank_desc' => 'You can store images about this contact. Upload one now!', + 'photo_upload_zone_cta' => 'Upload a photo', + 'photo_current_profile_pic' => 'Current profile picture', + 'photo_make_profile_pic' => 'Make profile picture', + 'photo_delete' => 'Delete photo', + 'photo_next' => 'Next photo ❯', + 'photo_previous' => '❮ Previous photo', + + // Avatars + 'avatar_change_title' => 'Change your avatar', + 'avatar_question' => 'Which avatar would you like to use?', + 'avatar_default_avatar' => 'The default avatar', + 'avatar_adorable_avatar' => 'The Adorable avatar', + 'avatar_gravatar' => 'The Gravatar associated with the email address of this person. Gravatar is a global system that lets users associate email addresses with photos.', + 'avatar_current' => 'Keep the current avatar', + 'avatar_photo' => 'From a photo that you upload', + 'avatar_crop_new_avatar_photo' => 'Crop new avatar photo', + + // emotions + 'emotion_this_made_me_feel' => 'This made you feel…', + + // logs + 'auditlogs_link' => 'History', + 'auditlogs_title' => 'Everything that happened to :name', + 'auditlogs_breadcrumb' => 'History', + 'auditlogs_author' => 'By :name on :date', + + // contact field label + 'contact_field_label_home' => 'Home', + 'contact_field_label_work' => 'Work', + 'contact_field_label_cell' => 'Mobile', + 'contact_field_label_fax' => 'Fax', + 'contact_field_label_pager' => 'Pager', + 'contact_field_label_main' => 'Main', + 'contact_field_label_other' => 'Other', + 'contact_field_label_personal' => 'Personal', +]; diff --git a/resources/lang/cs/reminder.php b/resources/lang/cs/reminder.php new file mode 100644 index 0000000..fb8d60a --- /dev/null +++ b/resources/lang/cs/reminder.php @@ -0,0 +1,16 @@ + 'Popřát k narozeninám', + 'type_phone_call' => 'Zavolat', + 'type_lunch' => 'Oběd s', + 'type_hangout' => 'Setkání s', + 'type_email' => 'Email', + 'type_birthday_kid' => 'Wish happy birthday to the child of', +]; diff --git a/resources/lang/cs/settings.php b/resources/lang/cs/settings.php new file mode 100644 index 0000000..889a91b --- /dev/null +++ b/resources/lang/cs/settings.php @@ -0,0 +1,557 @@ + 'Nastavení účtu', + 'sidebar_personalization' => 'Personalization', + 'sidebar_settings_storage' => 'Storage', + 'sidebar_settings_export' => 'Exportovat data', + 'sidebar_settings_users' => 'Uživatelé', + 'sidebar_settings_subscriptions' => 'Odběry', + 'sidebar_settings_import' => 'Importovat data', + 'sidebar_settings_tags' => 'Tag management', + 'sidebar_settings_api' => 'API', + 'sidebar_settings_dav' => 'DAV Resources', + 'sidebar_settings_security' => 'Security', + 'sidebar_settings_auditlogs' => 'Audit logs', + + 'title_general' => 'General Information', + 'title_i18n' => 'International settings', + 'title_layout' => 'Layout', + + 'me_title' => 'Me as a contact', + 'me_help' => 'This is the contact that represents you in Monica', + 'me_select' => 'Select a contact', + 'me_no_contact' => 'No contact selected yet.', + 'me_select_click' => 'Click here to select a contact.', + 'me_remove_contact' => 'Remove the association', + 'me_choose' => 'Choose yourself', + 'me_choose_placeholder' => 'Choose yourself', + + 'export_title' => 'Exportovat data účtu', + 'export_be_patient' => 'Kliknout na tlačítko pro spuštění exportu. Zpracování exportu může zabrat až několik minut – buďte prosím trpěliví a neklikejte vícekrát.', + 'export_title_sql' => 'Exportovat do SQL', + 'export_sql_explanation' => 'Export dat v SQL formátu je umožňuje převést a importovat do vlastní instance Monica. Toto se hodí hlavně pokud provozujete vlastní server.', + 'export_sql_cta' => 'Exportovat do SQL', + 'export_sql_link_instructions' => 'Poznámka: přečtěte si instrukce abyste se dozvěděli více o importu tohoto souboru do vlastní instance.', + 'export_title_json' => 'Export do JSON', + 'export_submitted' => 'Váš export byl odeslán, bude k dispozici za chvíli…', + 'export_json_explanation' => 'Exportování vašich dat ve formátu Json pro zálohu.', + 'export_json_beta' => 'Export Json je v režimu náhledu. Řekněte nám, co si o něm myslíte:', + 'export_json_cta' => 'Export do JSON', + 'export_header_type' => 'Typ', + 'export_header_timestamp' => 'Datum vytvoření', + 'export_header_status' => 'Stav', + 'export_header_actions' => 'Akce', + 'export_last_title' => 'Poslední export', + 'export_empty_title' => 'Zatím žádné exporty', + 'export_type_json' => 'JSON export', + 'export_type_sql' => 'SQL export', + 'export_status_todo' => 'Odesláno', + 'export_status_doing' => 'Probíhá', + 'export_status_done' => 'Hotovo', + 'export_status_failed' => 'Selhalo', + 'export_not_done' => 'Stahovat nemožné, tento export ještě není proveden.', + + 'firstname' => 'First name', + 'lastname' => 'Last name', + 'name_order' => 'Řazení jmen', + 'name_order_firstname_lastname' => ' – John Doe', + 'name_order_lastname_firstname' => ' – Doe John', + 'name_order_firstname_lastname_nickname' => ' () – John Doe (Rambo)', + 'name_order_firstname_nickname_lastname' => ' () – John (Rambo) Doe', + 'name_order_lastname_firstname_nickname' => ' () – Doe John (Rambo)', + 'name_order_lastname_nickname_firstname' => ' () – Doe (Rambo) John', + 'name_order_nickname_firstname_lastname' => ' ( ) – Rambo (John Doe)', + 'name_order_nickname_lastname_firstname' => ' ( ) – Rambo (Doe John)', + 'name_order_nickname' => ' – Rambo', + 'currency' => 'Měna', + 'name' => 'Vlastní jméno: :name', + 'email' => 'Emailová adresa', + 'email_placeholder' => 'Vložit email', + 'email_help' => 'This is the email used to login, and this is where Monica will send your reminders.', + 'timezone' => 'Časová zóna', + 'temperature_scale' => 'Temperature scale', + 'temperature_scale_fahrenheit' => 'Fahrenheit (°F)', + 'temperature_scale_celsius' => 'Celsius (°C)', + 'layout' => 'Vzhled', + 'layout_small' => 'Maximálně 1200 pixelů široký', + 'layout_big' => 'Plná šířka prohlížeče', + 'save' => 'Aktualizovat předvolby', + 'delete_title' => 'Smazat účet', + 'delete_desc' => 'Do you wish to delete your account? Deletion is permanent and all of your data will be erased permanently. If you have a subscription, it will be cancelled immediately.', + 'delete_other_desc' => 'Your data in the main database will be deleted immediately. As described in our privacy policy, we carry out securely encrypted backups of the database every day. These backups are kept for 30 days after which they are completely deleted. We cannot delete specific data from the backups we hold any earlier than this. All of your data will be completely deleted no later than 31 days after your account’s deletion.', + 'reset_desc' => 'Do you wish to reset your account? This will remove all your contacts, and all of the data associated with them. Your account will not be deleted.', + 'reset_title' => 'Resetovat vlastní účet', + 'reset_cta' => 'Resetovat účet', + 'reset_notice' => 'Are you sure to reset your account? This is permanent and cannot be undone.', + 'reset_success' => 'Your account has been reset successfully.', + 'delete_notice' => 'Are you sure you want to delete your account? This is permanent and cannot be undone. All of your data will be deleted and will not be recoverable.', + 'delete_cta' => 'Smazat účet', + 'settings_success' => 'Předvolby aktualizovány!', + 'locale' => 'Jazyk použitý v aplikaci', + 'locale_help' => 'Do you want to help translating Monica or add a new language? Please follow this link for more information.', + 'locale_ar' => 'Arabic', + 'locale_cs' => 'Czech', + 'locale_de' => 'Němčina', + 'locale_el' => 'Greek', + 'locale_en' => 'Angličtina', + 'locale_en-GB' => 'English (United Kingdom)', + 'locale_es' => 'Spanish', + 'locale_fr' => 'Francouzština', + 'locale_he' => 'Hebrew', + 'locale_hr' => 'Croatian', + 'locale_id' => 'Indonesian', + 'locale_it' => 'Italian', + 'locale_ja' => 'Japanese', + 'locale_nl' => 'Dutch', + 'locale_pt' => 'Portuguese', + 'locale_pt-BR' => 'Portuguese, Brazil', + 'locale_ru' => 'Ruština', + 'locale_sv' => 'Swedish', + 'locale_vi' => 'Vietnamese', + 'locale_zh' => 'Chinese Simplified', + 'locale_zh-TW' => 'Chinese Traditional', + 'locale_tr' => 'Turkish', + + 'security_title' => 'Security', + 'security_help' => 'Change security matters for your account.', + 'password_change' => 'Change your password', + 'password_current' => 'Current password', + 'password_current_placeholder' => 'Enter your current password', + 'password_new1' => 'New password', + 'password_new1_placeholder' => 'Enter your new password', + 'password_new2' => 'Confirm your new password', + 'password_new2_placeholder' => 'Retype your new password', + 'password_btn' => 'Change password', + '2fa_title' => 'Two Factor Authentication', + '2fa_otp_title' => 'Two Factor Authentication mobile application', + '2fa_enable_title' => 'Enable Two Factor Authentication', + '2fa_enable_description' => 'Enable Two Factor Authentication to increase the security of your account.', + '2fa_enable_otp' => 'Open up your Two Factor Authentication mobile app and scan the following QR barcode:', + '2fa_enable_otp_help' => 'If your Two Factor Authentication mobile app does not support QR barcodes, enter in the following code:', + '2fa_enable_otp_validate' => 'Please validate the new device you’ve just set up:', + '2fa_enable_success' => 'Two Factor Authentication activated', + '2fa_enable_error' => 'Error when trying to activate Two Factor Authentication', + '2fa_enable_error_already_set' => 'Two Factor Authentication is already activated', + '2fa_disable_title' => 'Disable Two Factor Authentication', + '2fa_disable_description' => 'Disable Two Factor Authentication for your account. Be careful, your account will be much less secure!', + '2fa_disable_success' => 'Two Factor Authentication disabled', + '2fa_disable_error' => 'Error when trying to disable Two Factor Authentication', + + 'webauthn_title' => 'Security key — WebAuthn protocol', + 'webauthn_enable_description' => 'Add a new security key', + 'webauthn_key_name_help' => 'Give your key a name.', + 'webauthn_key_name' => 'Key name:', + 'webauthn_success' => 'Your key is detected and validated.', + 'webauthn_last_use' => 'Last use: {timestamp}', + 'webauthn_delete_confirmation' => 'Are you sure you want to delete this key?', + 'webauthn_delete_success' => 'Key deleted', + 'webauthn_insertKey' => 'Insert your security key.', + 'webauthn_buttonAdvise' => 'If your security key has a button, press it.', + 'webauthn_noButtonAdvise' => 'If it does not, remove it and insert it again.', + 'webauthn_not_supported' => 'Your browser doesn’t currently support WebAuthn.', + 'webauthn_not_secured' => 'WebAuthn only supports secure connections. Please load this page with https scheme.', + 'webauthn_error_already_used' => 'This key is already registered. It’s not necessary to register it again.', + 'webauthn_error_not_allowed' => 'The operation either timed out or was not allowed.', + + 'recovery_title' => 'Recovery codes', + 'recovery_show' => 'Get recovery codes', + 'recovery_copy_help' => 'Copy codes in your clipboard', + 'recovery_help_intro' => 'These are your recovery codes:', + 'recovery_help_information' => 'You can use each recovery code once.', + 'recovery_clipboard' => 'Codes copied to the clipboard.', + 'recovery_generate' => 'Generate new codes…', + 'recovery_generate_help' => 'Generating new codes will invalidate previously generated codes.', + 'recovery_already_used_help' => 'This code has already been used.', + + 'users_list_title' => 'Uživatelé s přístupem k tomuto účtu', + 'users_list_add_user' => 'Pozvat nového uživatele', + 'users_list_you' => 'That’s you', + 'users_list_invitations_title' => 'Čekající pozvánky', + 'users_list_invitations_explanation' => 'Below are the people you’ve invited to join Monica as a collaborator.', + 'users_list_invitations_invited_by' => 'pozván uživatelem :name', + 'users_list_invitations_sent_date' => 'pozvání odesláno :date', + 'users_blank_title' => 'Jste zatím samotným uživatelem s přístupem k tomuto účtu.', + 'users_blank_add_title' => 'Chcete pozvat někoho dalšího?', + 'users_blank_description' => 'Tato osoba bude mít stejný přístup a bude schopna přidávat, upravovat a mazat informace kontaktů.', + 'users_blank_cta' => 'Někoho pozvat', + 'users_add_title' => 'Invite a new user to your account by email', + 'users_add_description' => 'This person will have the same access as you do, including inviting or deleting other users, including you. Make sure you trust this person before giving them access.', + 'users_add_email_field' => 'Zadejte email osoby, kterou chcete pozvat', + 'users_add_confirmation' => 'I confirm that I want to invite this user to my account. I understand that this person will have access to ALL of my data and see exactly what I see.', + 'users_add_cta' => 'Pozvat uživatele emailem', + 'users_accept_title' => 'Accept invitation and create a new account', + 'users_error_please_confirm' => 'Potvrďte prosím, že chcete přizvat ?tuto osobu?, než bude pozvánka zpracována', + 'users_error_email_already_taken' => 'Email byl již použit. Vyberte prosím nějaký jiný', + 'users_error_already_invited' => 'Již jste tohoto uživatele pozvali. Vyberte prosím jinou emailovou adresu.', + 'users_error_email_not_similar' => 'This is not the email of the person who’ve invited you.', + 'users_invitation_deleted_confirmation_message' => 'Pozvánka byla úspěšně smazána', + 'users_invitations_delete_confirmation' => 'Opravdu chcete smazat tuto pozvánku?', + 'users_list_delete_confirmation' => 'Opravdu chcete smazat tohoto uživatele z tohoto účtu?', + 'users_invitation_need_subscription' => 'Adding more users requires a subscription.', + + 'subscriptions_account_current_plan' => 'Aktuální plán', + 'subscriptions_account_current_legacy' => 'Current plan, not selectable anymore:', + 'subscriptions_account_current_paid_plan' => 'You are on the :name plan. Thanks so much for being a subscriber.', + + 'subscriptions_account_next_billing_title' => 'Next bill', + 'subscriptions_account_next_billing' => 'Your subscription will auto-renew on :date.', + 'subscriptions_account_bill_monthly' => 'We’ll bill you :price for another month.', + 'subscriptions_account_bill_annual' => 'We’ll bill you :price for another year.', + 'subscriptions_account_change' => 'Change plan', + + 'subscriptions_account_cancel_title' => 'Cancel subscription', + 'subscriptions_account_cancel_action' => 'Cancel subscription', + 'subscriptions_account_cancel' => 'You can cancel subscription anytime.', + 'subscriptions_account_free_plan' => 'Využíváte bezplatnou verzi.', + 'subscriptions_account_free_plan_upgrade' => 'Svůj účet můžete povýšit na verzi :name, která měsíčně stojí $:price. Zde jsou výhody:', + 'subscriptions_account_free_plan_benefits_users' => 'Neomezený počet uživatelů', + 'subscriptions_account_free_plan_benefits_reminders' => 'Reminders by email', + 'subscriptions_account_free_plan_benefits_import_data_vcard' => 'Import your contacts with vCard', + 'subscriptions_account_free_plan_benefits_support' => 'Support the project in the long run, so we can introduce more great features.', + 'subscriptions_account_upgrade' => 'Navýšit svůj účet', + 'subscriptions_account_upgrade_title' => 'Upgrade Monica today and have more meaningful relationships.', + 'subscriptions_account_upgrade_choice' => 'Pick a plan below and join over :customers persons who upgraded their Monica.', + 'subscriptions_account_update_title' => 'Update Monica subscription', + 'subscriptions_account_update_description' => 'You can change your subscription’s frequency here.', + 'subscriptions_account_update_information' => 'You will be billed immediately for the new amount. Your subscription will extend to the new period, depending on your choice.', + 'subscriptions_account_invoices' => 'Faktury', + 'subscriptions_account_invoices_download' => 'Stáhnout', + 'subscriptions_account_invoices_subscription' => 'Subscription from :startDate to :endDate', + 'subscriptions_account_payment' => 'Which payment option fits you best?', + 'subscriptions_account_confirm_payment' => 'Your payment is currently incomplete, please confirm your payment.', + 'subscriptions_downgrade_title' => 'Přejít na bezplatnou verzi', + 'subscriptions_downgrade_limitations' => 'Bezplatná verze má omezení. K přechodu na bezplatnou verzi musíte projít seznam níže:', + 'subscriptions_downgrade_rule_users' => 'Smíte mít pouze jednoho uživatele účtu', + 'subscriptions_downgrade_rule_users_constraint' => 'You currently have 1 user in your account.|You currently have :count users in your account.', + 'subscriptions_downgrade_rule_invitations' => 'You must not have any pending invitations', + 'subscriptions_downgrade_rule_invitations_constraint' => 'You currently have 1 pending invitation.|You currently have :count pending invitations.', + 'subscriptions_downgrade_rule_contacts' => 'You must not have more than :number active contacts', + 'subscriptions_downgrade_rule_contacts_constraint' => 'You currently have 1 contact.|You currently have :count contacts.', + 'subscriptions_downgrade_rule_contacts_archive' => 'We can also archive all your contacts for you – that would clear this rule and let you proceed with your account’s downgrade process.', + 'subscriptions_downgrade_cta' => 'Přejít', + 'subscriptions_downgrade_success' => 'You are back to the Free plan!', + 'subscriptions_downgrade_thanks' => 'Thanks so much for trying the paid plan. We keep adding new features on Monica all the time – so you might want to come back in the future to see if you might be interested in taking a subscription again.', + 'subscriptions_back' => 'Back to settings', + 'subscriptions_upgrade_title' => 'Navýšit svůj účet', + 'subscriptions_upgrade_choose' => 'You picked the :plan plan.', + 'subscriptions_upgrade_infos' => 'We couldn’t be happier. Enter your payment info below.', + 'subscriptions_upgrade_name' => 'Name on card', + 'subscriptions_upgrade_zip' => 'ZIP or postal code', + 'subscriptions_upgrade_credit' => 'Kreditní nebo debetní karta', + 'subscriptions_upgrade_submit' => 'Pay {amount}', + 'subscriptions_upgrade_charge' => 'We’ll charge your card :price now. The next charge will be on :date. If you ever change your mind, you can cancel at any time, no questions asked.', + 'subscriptions_upgrade_charge_handled' => 'The payment is handled by Stripe. No card information touches our server.', + 'subscriptions_upgrade_success' => 'Thank you! You are now subscribed.', + 'subscriptions_upgrade_thanks' => 'Welcome to the community of people who try to make the world a better place.', + + 'subscriptions_payment_confirm_title' => 'Confirm your :amount payment', + 'subscriptions_payment_confirm_information' => 'Extra confirmation is needed to process your payment. Please confirm your payment by filling out your payment details below.', + 'subscriptions_payment_succeeded_title' => 'Payment Successful', + 'subscriptions_payment_succeeded' => 'This payment was already successfully confirmed.', + 'subscriptions_payment_cancelled_title' => 'Payment Cancelled', + 'subscriptions_payment_cancelled' => 'This payment was cancelled.', + 'subscriptions_payment_error_name' => 'Please provide your name.', + 'subscriptions_payment_success' => 'The payment was successful.', + + 'subscriptions_pdf_title' => 'Váš měsíční plán :name', + 'subscriptions_plan_frequency_year' => ':amount / year', + 'subscriptions_plan_frequency_month' => ':amount / month', + 'subscriptions_plan_choose' => 'Choose this plan', + 'subscriptions_plan_year_title' => 'Pay annually', + 'subscriptions_plan_year_bonus' => 'Peace of mind for a whole year', + 'subscriptions_plan_month_title' => 'Pay monthly', + 'subscriptions_plan_month_bonus' => 'Cancel any time', + 'subscriptions_plan_include1' => 'Included with your upgrade:', + 'subscriptions_plan_include2' => 'Unlimited number of contacts • Unlimited number of users • Reminders by email • Import with vCard • Personalization of the contact sheet', + 'subscriptions_plan_include3' => '100% of the profits go the development of this great open source project.', + 'subscriptions_help_title' => 'Additional details you may be curious about', + 'subscriptions_help_opensource_title' => 'What is an open source project?', + 'subscriptions_help_opensource_desc' => 'Monica is an open source project. This means it is built by a community who wants to build a great tool for the greater good. Being open source means the code is publicly available on GitHub, and everyone can inspect it, modify it or enhance it. All the money we raise is dedicated to building better features, paying for more powerful servers, and paying other costs. Thanks for your help. We couldn’t do it without you.', + 'subscriptions_help_limits_title' => 'Is there a limit to the number of contacts we can have on the free plan?', + 'subscriptions_help_limits_plan' => 'Yes. Free plans let you manage :number contacts.', + 'subscriptions_help_discounts_title' => 'Do you have discounts for non-profits and education?', + 'subscriptions_help_discounts_desc' => 'We do! Monica is free for students, and free for non-profits and charities. Just contact the support with a proof of your status and we’ll apply this special status in your account.', + 'subscriptions_help_change_title' => 'What if I change my mind?', + 'subscriptions_help_change_desc' => 'You can cancel anytime, no questions asked, and all by yourself – no need to contact support. However, you will not be refunded for the current period.', + + 'stripe_error_card' => 'Your card was declined. Decline message is: :message', + 'stripe_error_api_connection' => 'Network communication with Stripe failed. Try again later.', + 'stripe_error_rate_limit' => 'Too many requests with Stripe right now. Try again later.', + 'stripe_error_invalid_request' => 'Invalid parameters. Try again later.', + 'stripe_error_authentication' => 'Wrong authentication with Stripe', + + 'import_title' => 'Importovat kontakty do svého účtu', + 'import_cta' => 'Nahrát kontakty', + 'import_stat' => 'You’ve imported :number files so far.', + 'import_result_stat' => 'Uploaded vCard with 1 contact (:total_imported imported, :total_skipped skipped)|Uploaded vCard with :total_contacts contacts (:total_imported imported, :total_skipped skipped)', + 'import_view_report' => 'Zobrazit report', + 'import_in_progress' => 'Probíhá import. Obnovit stránku za jednu minutu.', + 'import_upload_title' => 'Importovat kontakty z vCard souboru', + 'import_upload_rules_desc' => 'Máme ale několik pravidel:', + 'import_upload_rule_format' => 'Podporujeme .vcard a .vcf soubory.', + 'import_upload_rule_vcard' => 'We support the vCard 3.0 format, which is the default format for macOS’s Contacts.app and Google Contacts.', + 'import_upload_rule_instructions' => 'Export instructions for macOS Contacts.app and Google Contacts.', + 'import_upload_rule_multiple' => 'If your contacts have multiple email addresses or phone numbers, only the first entry will be saved.', + 'import_upload_rule_limit' => 'Files are limited to 10 MB.', + 'import_upload_rule_time' => 'It might take up to a minute to upload the contacts and process them. Please be patient.', + 'import_upload_rule_cant_revert' => 'Please make sure data is accurate before uploading, as you can’t undo the upload.', + 'import_upload_form_file' => 'Váš .vcf nebo .vCard soubor:', + 'import_upload_behaviour' => 'Import behaviour:', + 'import_upload_behaviour_add' => 'Add new contacts and skip existing', + 'import_upload_behaviour_replace' => 'Replace existing contacts', + 'import_upload_behaviour_help' => 'Replacing will replace all data found in the vCard, but will keep existing contact fields.', + 'import_report_title' => 'Report importu', + 'import_report_date' => 'Datum importu', + 'import_report_type' => 'Typ importu', + 'import_report_number_contacts' => 'Počet kontaktů v souboru', + 'import_report_number_contacts_imported' => 'Počet importovaných kontaktů', + 'import_report_number_contacts_skipped' => 'Počet přeskočených kontaktů', + 'import_report_status_imported' => 'Importováno', + 'import_report_status_skipped' => 'Přeskočeno', + 'import_vcard_parse_error' => 'Error when parsing the vCard entry', + 'import_vcard_contact_exist' => 'Kontakt již existuje', + 'import_vcard_contact_no_firstname' => 'No first name (mandatory)', + 'import_vcard_file_not_found' => 'File not found', + 'import_vcard_unknown_entry' => 'Unknown contact name', + 'import_vcard_file_no_entries' => 'File contains no entries', + 'import_blank_title' => 'You haven’t imported any contacts yet.', + 'import_blank_question' => 'Chcete nyní importovat kontakty?', + 'import_blank_description' => 'Umíme importovat soubory vCard, které můžete získat z Google Contacts nebo svého správce kontaktů.', + 'import_blank_cta' => 'Importovat vCard', + 'import_need_subscription' => 'Importing data requires a subscription.', + + 'tags_list_title' => 'Tagy', + 'tags_list_description' => 'Své kontakty můžete organizovat pomocí tagů. Tagy fungují jako adresáře, kontaktům můžete ale přidat vícero tagů. Nový tag přidáte úpravou vlastního kontaktu.', + 'tags_list_contact_number' => '1 contact|:count contacts', + 'tags_list_delete_success' => 'Tag byl úspěšně smazán', + 'tags_list_edit_success' => 'The tag has been successfully updated', + 'tags_list_delete_confirmation' => 'Opravdu chcete smazat tento tag? Bude smazán pouze vybraný tag, žádné kontakty.', + 'tags_blank_title' => 'Tagy jsou šikovné řešení kategorizace kontaktů.', + 'tags_blank_description' => 'Tags work like folders, but you can add more than one tag to a contact. Go to a contact and tag a friend, right below the name. Once a contact is tagged, come back here to manage all the tags in your account.', + + 'api_title' => 'API access', + 'api_description' => 'The API can be used to manipulate Monica’s data from an external application, like a mobile application for instance.', + 'api_help' => 'To use the API, a token is mandatory. You can either create a personal access token (Bearer authentication), or authorize an OAuth client to create it for you. See API documentation.', + 'api_endpoint' => 'The API endpoint for this Monica instance is:', + + 'api_personal_access_tokens' => 'Personal access tokens', + 'api_pao_description' => 'Make sure you give this token to a source you trust – as they allow you to access all your data.', + 'api_token_title' => 'Personal Access Tokens', + 'api_token_create_new' => 'Create New Token', + 'api_token_not_created' => 'You have not created any personal access tokens.', + 'api_token_name' => 'Token name', + 'api_token_expire' => 'Expires at {date}', + 'api_token_delete' => 'Delete', + 'api_token_create' => 'Create Token', + 'api_token_scopes' => 'Scopes', + 'api_token_help' => 'Here is your new personal access token. This is the only time it will be shown so don’t lose it! You may now use this token to make API requests.', + + 'api_oauth_clients' => 'Your OAuth clients', + 'api_oauth_clients_desc' => 'This section lets you register your own OAuth clients.', + 'api_oauth_clients_desc2' => 'Use this client id to request a new token, and convert authorization codes to access tokens. See Laravel Passport documentation for more information.', + 'api_oauth_title' => 'OAuth Clients', + 'api_oauth_create_new' => 'Create New Client', + 'api_oauth_edit' => 'Edit Client', + 'api_oauth_not_created' => 'You have not created any OAuth clients.', + 'api_oauth_clientid' => 'Client ID', + 'api_oauth_name' => 'Name', + 'api_oauth_name_help' => 'Something your users will recognize and trust.', + 'api_oauth_secret' => 'Secret', + 'api_oauth_create' => 'Create Client', + 'api_oauth_redirecturl' => 'Redirect URL', + 'api_oauth_redirecturl_help' => 'Your application’s authorization callback URL.', + + 'api_authorized_clients' => 'List of authorized clients', + 'api_authorized_clients_desc' => 'This section lists all the clients you’ve authorized to access your application data. You can revoke this authorization at anytime.', + 'api_authorized_clients_title' => 'Authorized Applications', + 'api_authorized_clients_none' => 'There are no authorized clients yet.', + 'api_authorized_clients_name' => 'Name', + 'api_authorized_clients_scopes' => 'Scopes', + + 'personalization_tab_title' => 'Personalize your account', + + 'personalization_title' => 'Here you will find different settings to configure your account. These features are intended for “power users” who want maximum control over Monica.', + 'personalization_contact_field_type_title' => 'Contact field types', + 'personalization_contact_field_type_add' => 'Add new field type', + 'personalization_contact_field_type_description' => 'You can configure all the different types of contact fields that you can associate to all your contacts. For example, if a new social network appears in the future, you will be able to add this new way of communicating with your contacts right here.', + 'personalization_contact_field_type_table_name' => 'Name', + 'personalization_contact_field_type_table_protocol' => 'Protocol', + 'personalization_contact_field_type_table_actions' => 'Actions', + 'personalization_contact_field_type_modal_title' => 'Add a new contact field type', + 'personalization_contact_field_type_modal_edit_title' => 'Edit an existing contact field type', + 'personalization_contact_field_type_modal_delete_title' => 'Delete an existing contact field type', + 'personalization_contact_field_type_modal_delete_description' => 'Are you sure you want to delete this contact field type? Deleting this type of contact field will delete ALL the data with this type for all of your contacts.', + 'personalization_contact_field_type_modal_name' => 'Name', + 'personalization_contact_field_type_modal_protocol' => 'Protocol (optional)', + 'personalization_contact_field_type_modal_protocol_help' => 'Each new contact field type can be clickable. If a protocol is set, we will use it to trigger the action that is set.', + 'personalization_contact_field_type_modal_icon' => 'Icon (optional)', + 'personalization_contact_field_type_modal_icon_help' => 'You can associate an icon with this contact field type. You need to add a reference to a Font Awesome icon.', + 'personalization_contact_field_type_delete_success' => 'The contact field type has been successfully deleted.', + 'personalization_contact_field_type_add_success' => 'The contact field type has been successfully added.', + 'personalization_contact_field_type_edit_success' => 'The contact field type has been successfully updated.', + + 'personalization_genders_title' => 'Gender types', + 'personalization_genders_add' => 'Add new gender type', + 'personalization_genders_desc' => 'You can define as many genders as you need to. You need at least one gender type in your account.', + 'personalization_genders_modal_add' => 'Add gender type', + 'personalization_genders_modal_edit' => 'Update gender type', + 'personalization_genders_modal_name' => 'Name', + 'personalization_genders_modal_name_help' => 'The name used to display the gender on a contact page.', + 'personalization_genders_modal_sex' => 'Sex', + 'personalization_genders_modal_sex_help' => 'Used to define the relationships, and during the VCard import/export process.', + 'personalization_genders_modal_default' => 'Select the default gender for a new contact', + 'personalization_genders_modal_delete' => 'Delete gender type', + 'personalization_genders_modal_delete_desc' => 'Are you sure you want to delete the gender “{name}”?', + 'personalization_genders_modal_delete_question' => 'You currently have {count} contact with this gender. If you delete this gender, what gender should this contact have?|You currently have {count} contacts with this gender. If you delete this gender, what gender should these contacts have?', + 'personalization_genders_modal_delete_question_default' => 'This gender is the default one. If you delete this gender, which one will be the new default?', + 'personalization_genders_modal_error' => 'Please choose a gender from the list.', + 'personalization_genders_list_contact_number' => '{count} contact|{count} contacts', + 'personalization_genders_table_name' => 'Name', + 'personalization_genders_table_sex' => 'Sex', + 'personalization_genders_table_default' => 'Default', + 'personalization_genders_default' => 'Default gender', + 'personalization_genders_make_default' => 'Change default gender', + 'personalization_genders_select_default' => 'Select default gender', + 'personalization_genders_m' => 'Male', + 'personalization_genders_f' => 'Female', + 'personalization_genders_o' => 'Other', + 'personalization_genders_u' => 'Unknown', + 'personalization_genders_n' => 'None or not applicable', + + 'personalization_reminder_rule_save' => 'The change has been saved', + 'personalization_reminder_rule_title' => 'Reminder rules', + 'personalization_reminder_rule_line' => '{count} day before|{count} days before', + 'personalization_reminder_rule_desc' => 'For every reminder that you set, Monica can send you an email a number of days before the event happens. You can adjust these notification settings here. These notifications only apply to monthly and yearly reminders.', + + 'personalization_module_save' => 'The change has been saved', + 'personalization_module_title' => 'Features', + 'personalization_module_desc' => 'You may not need all of Monica’s features. Below you can toggle specific features that are used on a contact sheet. This change will affect ALL your contacts. Turning off a feature does not delete any data, it simply hides the feature.', + + 'personalisation_paid_upgrade' => 'This is a premium feature that requires a Paid subscription to be active. Upgrade your account by visiting Settings > Subscription.', + 'personalisation_paid_upgrade_vue' => 'This is a premium feature that requires a Paid subscription to be active. Upgrade your account by visiting Settings > Subscription.', + + 'reminder_time_to_send' => 'Time of the day reminders will be sent', + 'reminder_time_to_send_help' => 'Your next reminder is scheduled to be sent on {dateTime}.', + + 'personalization_activity_type_category_title' => 'Activity type categories', + 'personalization_activity_type_category_add' => 'Add a new activity type category', + 'personalization_activity_type_category_table_name' => 'Name', + 'personalization_activity_type_category_description' => 'An activity with one of your contacts can have a type and a category type. Your account comes with a set of predefined category types by default, but you can customize these here.', + 'personalization_activity_type_category_table_actions' => 'Actions', + 'personalization_activity_type_category_modal_add' => 'Add a new activity type category', + 'personalization_activity_type_category_modal_edit' => 'Edit an activity type category', + 'personalization_activity_type_category_modal_question' => 'What should we name this new category?', + 'personalization_activity_type_add_button' => 'Add a new activity type', + 'personalization_activity_type_modal_add' => 'Add a new activity type', + 'personalization_activity_type_modal_question' => 'What should we name this new activity type?', + 'personalization_activity_type_modal_edit' => 'Edit an activity type', + 'personalization_activity_type_category_modal_delete' => 'Delete an activity type category', + 'personalization_activity_type_category_modal_delete_desc' => 'Are you sure you want to delete this category? Deleting it will delete all associated activity types. Activities that belong to this category will not be affected by this deletion.', + 'personalization_activity_type_modal_delete' => 'Delete an activity type', + 'personalization_activity_type_modal_delete_desc' => 'Are you sure you want to delete this activity type? Activities that belong to this category will not be affected by this deletion.', + 'personalization_activity_type_modal_delete_error' => 'We can’t find this activity type.', + 'personalization_activity_type_category_modal_delete_error' => 'We can’t find this activity type category.', + + 'personalization_life_event_category_title' => 'Life event categories', + 'personalization_live_event_category_table_name' => 'Name', + 'personalization_life_event_category_description' => 'A life event can have a type and a category. Your account comes with a set of predefined categories and types by default, but you can customize life event types here.', + 'personalization_live_event_category_table_actions' => 'Actions', + 'personalization_life_event_type_add_button' => 'Add a new life event type', + 'personalization_life_event_type_modal_add' => 'Add a new life event type', + 'personalization_life_event_type_modal_question' => 'What should we name this new life event type?', + 'personalization_life_event_type_modal_edit' => 'Edit a life event type', + 'personalization_life_event_type_modal_delete' => 'Delete a life event type', + 'personalization_life_event_type_modal_delete_desc' => 'Are you sure you want to delete this life event type? Life events that belong to this type will be deleted by performing this action.', + 'personalization_life_event_type_modal_delete_error' => 'We can’t find this life event type.', + + 'personalization_life_event_category_work_education' => 'Work & education', + 'personalization_life_event_category_family_relationships' => 'Family & relationships', + 'personalization_life_event_category_home_living' => 'Home & living', + 'personalization_life_event_category_travel_experiences' => 'Travel & experiences', + 'personalization_life_event_category_health_wellness' => 'Health & wellness', + + 'personalization_life_event_type_new_job' => 'New job', + 'personalization_life_event_type_retirement' => 'Retirement', + 'personalization_life_event_type_new_school' => 'New school', + 'personalization_life_event_type_study_abroad' => 'Study abroad', + 'personalization_life_event_type_volunteer_work' => 'Volunteer work', + 'personalization_life_event_type_published_book_or_paper' => 'Published a book or paper', + 'personalization_life_event_type_military_service' => 'Military service', + 'personalization_life_event_type_first_met' => 'First met', + 'personalization_life_event_type_new_relationship' => 'New relationship', + 'personalization_life_event_type_engagement' => 'Engagement', + 'personalization_life_event_type_marriage' => 'Marriage', + 'personalization_life_event_type_anniversary' => 'Anniversary', + 'personalization_life_event_type_expecting_a_baby' => 'Expecting a baby', + 'personalization_life_event_type_new_child' => 'New child', + 'personalization_life_event_type_new_family_member' => 'New family member', + 'personalization_life_event_type_new_pet' => 'New pet', + 'personalization_life_event_type_end_of_relationship' => 'End of relationship', + 'personalization_life_event_type_loss_of_a_loved_one' => 'Loss of a loved one', + 'personalization_life_event_type_moved' => 'Moved', + 'personalization_life_event_type_bought_a_home' => 'Bought a home', + 'personalization_life_event_type_home_improvement' => 'Home improvement', + 'personalization_life_event_type_holidays' => 'Holidays', + 'personalization_life_event_type_new_vehicle' => 'New vehicle', + 'personalization_life_event_type_new_roommate' => 'New roommate', + 'personalization_life_event_type_overcame_an_illness' => 'Overcame an illness', + 'personalization_life_event_type_quit_a_habit' => 'Quit a habit', + 'personalization_life_event_type_new_eating_habits' => 'New eating habits', + 'personalization_life_event_type_weight_loss' => 'Weight loss', + 'personalization_life_event_type_wear_glass_or_contact' => 'Started wearing glasses or contacts', + 'personalization_life_event_type_broken_bone' => 'Broke a bone', + 'personalization_life_event_type_removed_braces' => 'Had braces removed', + 'personalization_life_event_type_surgery' => 'Had surgery', + 'personalization_life_event_type_dentist' => 'Had dental treatment', + 'personalization_life_event_type_new_sport' => 'Started playing a new sport', + 'personalization_life_event_type_new_hobby' => 'Took up a new hobby', + 'personalization_life_event_type_new_instrument' => 'Started learning a new instrument', + 'personalization_life_event_type_new_language' => 'Started learning a new language', + 'personalization_life_event_type_tattoo_or_piercing' => 'Tattoo or piercing', + 'personalization_life_event_type_new_license' => 'New license', + 'personalization_life_event_type_travel' => 'Travel', + 'personalization_life_event_type_achievement_or_award' => 'Achievement or award', + 'personalization_life_event_type_changed_beliefs' => 'Changed beliefs', + 'personalization_life_event_type_first_word' => 'First word', + 'personalization_life_event_type_first_kiss' => 'First kiss', + + 'storage_title' => 'Storage', + 'storage_account_info' => 'Your account limit is :accountLimit MB. Your current usage is :currentAccountSize MB (about :percentUsage%).', + 'storage_upgrade_notice' => 'Upgrade your account to be able to upload documents and photos.', + 'storage_description' => 'Here you can see all the documents and photos uploaded about your contacts.', + + 'dav_title' => 'WebDAV', + 'dav_description' => 'Here you can find all settings to use WebDAV resources for CardDAV and CalDAV exports.', + 'dav_copy_help' => 'Copy into your clipboard', + 'dav_clipboard_copied' => 'Value copied into your clipboard', + 'dav_url_base' => 'Base url for all CardDAV and CalDAV resources:', + 'dav_connect_help' => 'You can connect your contacts and/or calendars with this base url on you phone or computer.', + 'dav_connect_help2' => 'Use your login (email) and create an API token as the password to authenticate.', + 'dav_url_carddav' => 'CardDAV url for Contacts resource:', + 'dav_url_caldav_birthdays' => 'CalDAV url for Birthdays resources:', + 'dav_url_caldav_tasks' => 'CalDAV url for Tasks resources:', + 'dav_title_carddav' => 'CardDAV', + 'dav_title_caldav' => 'CalDAV', + 'dav_carddav_export' => 'Export all contacts in one file', + 'dav_caldav_birthdays_export' => 'Export all birthdays in one file', + 'dav_caldav_tasks_export' => 'Export all tasks in one file', + + 'archive_title' => 'Archive all of the contacts in your account', + 'archive_desc' => 'This will archive all of the contacts in your account.', + 'archive_cta' => 'Archive all of your contacts', + + 'logs_title' => 'Everything that has happened to this account', + 'logs_actor' => 'Actor', + 'logs_timestamp' => 'Timestamp', + 'logs_description' => 'Description', + 'logs_subject' => 'Subject', + 'logs_size' => 'Size (Kb)', + 'logs_object' => 'Object', +]; diff --git a/resources/lang/cs/validation.php b/resources/lang/cs/validation.php new file mode 100644 index 0000000..936d8e3 --- /dev/null +++ b/resources/lang/cs/validation.php @@ -0,0 +1,166 @@ + ':attribute musí být přijat.', + 'active_url' => ':attribute není platnou URL adresou.', + 'after' => ':attribute musí být datum po :date.', + 'after_or_equal' => ':attribute musí být datum :date nebo pozdější.', + 'alpha' => ':attribute může obsahovat pouze písmena.', + 'alpha_dash' => ':attribute může obsahovat pouze písmena, číslice, pomlčky a podtržítka. České znaky (á, é, í, ó, ú, ů, ž, š, č, ř, ď, ť, ň) nejsou podporovány.', + 'alpha_num' => ':attribute může obsahovat pouze písmena a číslice.', + 'array' => ':attribute musí být pole.', + 'before' => ':attribute musí být datum před :date.', + 'before_or_equal' => 'Datum :attribute musí být před nebo rovno :date.', + 'between' => [ + 'numeric' => ':attribute musí být hodnota mezi :min a :max.', + 'file' => ':attribute musí být větší než :min a menší než :max Kilobytů.', + 'string' => ':attribute musí být delší než :min a kratší než :max znaků.', + 'array' => ':attribute musí obsahovat nejméně :min a nesmí obsahovat více než :max prvků.', + ], + 'boolean' => ':attribute musí být true nebo false', + 'confirmed' => ':attribute nebylo odsouhlaseno.', + 'date' => ':attribute musí být platné datum.', + 'date_equals' => ':attribute musí být datum shodné s :date.', + 'date_format' => ':attribute není platný formát data podle :format.', + 'different' => ':attribute a :other se musí lišit.', + 'digits' => ':attribute musí být :digits pozic dlouhé.', + 'digits_between' => ':attribute musí být dlouhé nejméně :min a nejvíce :max pozic.', + 'dimensions' => ':attribute má neplatné rozměry.', + 'distinct' => ':attribute má duplicitní hodnotu.', + 'email' => ':attribute není platný formát.', + 'ends_with' => ':attribute musí končit jednou z následujících hodnot: :values', + 'exists' => 'Zvolená hodnota pro :attribute není platná.', + 'file' => ':attribute musí být soubor.', + 'filled' => ':attribute musí být vyplněno.', + 'gt' => [ + 'numeric' => ':attribute musí být větší než :value.', + 'file' => 'Velikost souboru :attribute musí být větší než :value kB.', + 'string' => 'Počet znaků :attribute musí být větší :value.', + 'array' => 'Pole :attribute musí mít více prvků než :value.', + ], + 'gte' => [ + 'numeric' => ':attribute musí být větší nebo rovno :value.', + 'file' => 'Velikost souboru :attribute musí být větší nebo rovno :value kB.', + 'string' => 'Počet znaků :attribute musí být větší nebo rovno :value.', + 'array' => 'Pole :attribute musí mít :value prvků nebo více.', + ], + 'image' => ':attribute musí být obrázek.', + 'in' => 'Zvolená hodnota pro :attribute je neplatná.', + 'in_array' => ':attribute není obsažen v :other.', + 'integer' => ':attribute musí být celé číslo.', + 'ip' => ':attribute musí být platnou IP adresou.', + 'ipv4' => ':attribute musí být platná IPv4 adresa.', + 'ipv6' => ':attribute musí být platná IPv6 adresa.', + 'json' => ':attribute musí být platný JSON řetězec.', + 'lt' => [ + 'numeric' => ':attribute musí být menší než :value.', + 'file' => 'Velikost souboru :attribute musí být menší než :value kB.', + 'string' => ':attribute musí obsahovat méně než :value znaků.', + 'array' => ':attribute by měl obsahovat méně než :value položek.', + ], + 'lte' => [ + 'numeric' => ':attribute musí být menší nebo rovno než :value.', + 'file' => 'Velikost souboru :attribute musí být menší než :value kB.', + 'string' => ':attribute nesmí být delší než :value znaků.', + 'array' => ':attribute by měl obsahovat maximálně :value položek.', + ], + 'max' => [ + 'numeric' => ':attribute musí být nižší než :max.', + 'file' => ':attribute musí být menší než :max Kilobytů.', + 'string' => ':attribute musí být kratší než :max znaků.', + 'array' => ':attribute nesmí obsahovat více než :max prvků.', + ], + 'mimes' => ':attribute musí být jeden z následujících datových typů :values.', + 'mimetypes' => ':attribute musí být jeden z následujících datových typů :values.', + 'min' => [ + 'numeric' => ':attribute musí být větší než :min.', + 'file' => ':attribute musí být větší než :min Kilobytů.', + 'string' => ':attribute musí být delší než :min znaků.', + 'array' => ':attribute musí obsahovat více než :min prvků.', + ], + 'not_in' => 'Zvolená hodnota pro :attribute je neplatná.', + 'not_regex' => ':attribute musí být regulární výraz.', + 'numeric' => ':attribute musí být číslo.', + 'password' => 'The password is incorrect.', + 'present' => ':attribute musí být vyplněno.', + 'regex' => ':attribute nemá správný formát.', + 'required' => ':attribute musí být vyplněno.', + 'required_if' => ':attribute musí být vyplněno pokud :other je :value.', + 'required_unless' => ':attribute musí být vyplněno dokud :other je v :values.', + 'required_with' => ':attribute musí být vyplněno pokud :values je vyplněno.', + 'required_with_all' => ':attribute musí být vyplněno pokud :values je zvoleno.', + 'required_without' => ':attribute musí být vyplněno pokud :values není vyplněno.', + 'required_without_all' => ':attribute musí být vyplněno pokud není žádné z :values zvoleno.', + 'same' => ':attribute a :other se musí shodovat.', + 'size' => [ + 'numeric' => ':attribute musí být přesně :size.', + 'file' => ':attribute musí mít přesně :size Kilobytů.', + 'string' => ':attribute musí být přesně :size znaků dlouhý.', + 'array' => ':attribute musí obsahovat právě :size prvků.', + ], + 'starts_with' => ':attribute musí začínat jednou z následujících hodnot: :values.', + 'string' => ':attribute musí být řetězec znaků.', + 'timezone' => ':attribute musí být platná časová zóna.', + 'unique' => ':attribute musí být unikátní.', + 'uploaded' => 'Nahrávání :attribute se nezdařilo.', + 'url' => 'Formát :attribute je neplatný.', + 'uuid' => ':attribute musí být validní UUID.', + + /* + |-------------------------------------------------------------------------- + | Custom Validation Language Lines + |-------------------------------------------------------------------------- + | + | Here you may specify custom validation messages for attributes using the + | convention "attribute.rule" to name the lines. This makes it quick to + | specify a specific custom language line for a given attribute rule. + | + */ + + 'custom' => [ + 'attribute-name' => [ + 'rule-name' => 'custom-message', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Custom Validation Attributes + |-------------------------------------------------------------------------- + | + | The following language lines are used to swap our attribute placeholder + | with something more reader friendly such as "E-Mail Address" instead + | of "email". This simply helps us make our message more expressive. + | + */ + + 'attributes' => [], + + 'vue' => [ + 'max' => [ + 'numeric' => '{field} nesmí být větší než {max}.', + 'string' => '{field} nesmí být delší než {max} znaků.', + ], + 'required' => '{field} je povinné.', + 'url' => '{field} není platná adresa URL.', + ], + +]; diff --git a/resources/lang/da.json b/resources/lang/da.json new file mode 100644 index 0000000..ddea72e --- /dev/null +++ b/resources/lang/da.json @@ -0,0 +1,7 @@ +{ + "The :attribute must contain at least one uppercase and one lowercase letter.": "The :attribute must contain at least one uppercase and one lowercase letter.", + "The :attribute must contain at least one letter.": "The :attribute must contain at least one letter.", + "The :attribute must contain at least one symbol.": "The :attribute must contain at least one symbol.", + "The :attribute must contain at least one number.": "The :attribute must contain at least one number.", + "The given :attribute has appeared in a data leak. Please choose a different :attribute.": "The given :attribute has appeared in a data leak. Please choose a different :attribute." +} diff --git a/resources/lang/da/app.php b/resources/lang/da/app.php new file mode 100644 index 0000000..5573820 --- /dev/null +++ b/resources/lang/da/app.php @@ -0,0 +1,571 @@ + 'Ja', + 'no' => 'Nej', + 'update' => 'Opdatér', + 'save' => 'Gem', + 'add' => 'Tilføj', + 'cancel' => 'Annullér', + 'confirm' => 'Confirm', + 'delete_confirm' => 'Are you sure?', + 'delete' => 'Slet', + 'edit' => 'Redigér', + 'upload' => 'Upload', + 'download' => 'Download', + 'save_close' => 'Gem og luk', + 'close' => 'Luk', + 'copy' => 'Kopiér', + 'create' => 'Opret', + 'remove' => 'Fjern', + 'revoke' => 'Tilbagekald', + 'done' => 'Udført', + 'back' => 'Tilbage', + 'verify' => 'Bekræft', + 'new' => 'ny', + 'unknown' => 'I don’t know', + 'load_more' => 'Indlæs flere', + 'loading' => 'Loading…', + 'with' => 'with', + 'today' => 'i dag', + 'yesterday' => 'i går', + 'another_day' => 'another day', + 'date' => 'Date', + 'type' => 'Type', + 'zoom' => 'Zoom', + 'upgrade' => 'Opgradér for at låse op', + 'percent_uploaded' => '{percent}% uploaded', + 'retry' => 'Prøv igen', + 'filter' => 'Filter the list', + 'go_back' => 'Gå tilbage', + 'file_selected' => 'One file selected…|{count} files selected…', + + 'application_title' => 'Monica – personal relationship manager', + 'application_description' => 'Monica is a tool to manage your interactions with your loved ones, friends and family.', + 'application_og_title' => 'Have better relations with your loved ones. Free online CRM for friends and family.', + + 'markdown_description' => 'Want to format your text in a nice way? We support Markdown to add bold, italic, lists and more.', + 'markdown_link' => 'Læs dokumentation', + + 'header_settings_link' => 'Indstillinger', + 'header_logout_link' => 'Log af', + 'header_changelog_link' => 'Produktændringer', + + 'main_nav_cta' => 'Tilføj personer', + 'main_nav_dashboard' => 'Oversigt', + 'main_nav_family' => 'Kontakter', + 'main_nav_journal' => 'Journal', + 'main_nav_activities' => 'Aktiviteter', + 'main_nav_tasks' => 'Tasks', + + 'footer_remarks' => 'Comments?', + 'footer_send_email' => 'Send us an email', + 'footer_privacy' => 'Privacy policy', + 'footer_release' => 'Release notes', + 'footer_newsletter' => 'Newsletter', + 'footer_source_code' => 'Contribute', + 'footer_version' => 'Version: :version', + 'footer_new_version' => 'A new version of Monica is available', + + 'footer_modal_version_whats_new' => 'What’s new', + 'footer_modal_version_release_away' => 'You are 1 release behind the latest version available. You should update your instance.|You are :number releases behind the latest version available. You should update your instance.', + + 'breadcrumb_dashboard' => 'Dashboard', + 'breadcrumb_list_contacts' => 'List of people', + 'breadcrumb_archived_contacts' => 'Archived contacts', + 'breadcrumb_journal' => 'Journal', + 'breadcrumb_settings' => 'Settings', + 'breadcrumb_settings_export' => 'Export', + 'breadcrumb_settings_users' => 'Users', + 'breadcrumb_settings_users_add' => 'Add a user', + 'breadcrumb_settings_subscriptions' => 'Subscription', + 'breadcrumb_settings_import' => 'Import', + 'breadcrumb_settings_import_report' => 'Import report', + 'breadcrumb_settings_import_upload' => 'Upload', + 'breadcrumb_settings_tags' => 'Tags', + 'breadcrumb_add_significant_other' => 'Add significant other', + 'breadcrumb_edit_significant_other' => 'Edit significant other', + 'breadcrumb_add_note' => 'Add a note', + 'breadcrumb_edit_note' => 'Edit a note', + 'breadcrumb_api' => 'API', + 'breadcrumb_dav' => 'DAV Resources', + 'breadcrumb_edit_introductions' => 'How did you meet', + 'breadcrumb_settings_personalization' => 'Tilpasning', + 'breadcrumb_settings_security' => 'Sikkerhed', + 'breadcrumb_settings_security_2fa' => 'Two Factor Authentication', + 'breadcrumb_profile' => 'Profile of :name', + + 'gender_male' => 'Mand', + 'gender_female' => 'Kvinde', + 'gender_none' => 'Rather not say', + 'gender_no_gender' => 'No gender', + + 'error_title' => 'Hovsa! Noget gik galt.', + 'error_unauthorized' => 'You don’t have the right to edit this resource.', + 'error_user_account' => 'This user does not belong to the given account.', + 'error_save' => 'We had an error trying to save the data.', + 'error_try_again' => 'Something went wrong. Please try again.', + 'error_id' => 'Error ID: :id', + 'error_unavailable' => 'Tjenesten er ikke tilgængelig', + 'error_maintenance' => 'Maintenance in progress. We’ll be right back.', + 'error_help' => 'Vi er snart tilbage.', + 'error_twitter' => 'Follow our Twitter account to be alerted when it’s up again.', + 'error_no_term' => 'There is no policy for this instance yet.', + + 'default_save_success' => 'The data has been saved.', + + 'compliance_title' => 'Sorry for the interruption.', + 'compliance_desc' => 'We have changed our Terms of Use and Privacy Policy. By law we have to ask you to review them and accept them so you can continue to use your account.', + 'compliance_desc_end' => 'We don’t do anything nasty with your data or account and will never do.', + 'compliance_terms' => 'Accept new terms and privacy policy', + + // Relationship types + // Yes, each relationship type has 8 strings associated with it. + // This is because we need to indicate the name of the relationship type, + // and also the name of the opposite side of this relationship (father/son), + // and then, the feminine version of the string. Finally, in some sentences + // in the UI, we need to include the name of the person we add the relationship + // to. + 'relationship_type_group_love' => 'Love relationships', + 'relationship_type_group_family' => 'Family relationships', + 'relationship_type_group_friend' => 'Friend relationships', + 'relationship_type_group_work' => 'Work relationships', + 'relationship_type_group_other' => 'Other kind of relationships', + + 'relationship_type_partner' => 'partner', + 'relationship_type_partner_female' => 'partner', + 'relationship_type_partner_male' => 'significant other', + 'relationship_type_partner_with_name' => ':name’s partner', + 'relationship_type_partner_female_with_name' => ':name’s partner', + 'relationship_type_partner_male_with_name' => ':name’s significant other', + + 'relationship_type_spouse' => 'ægtefælle', + 'relationship_type_spouse_female' => 'wife', + 'relationship_type_spouse_male' => 'husband', + 'relationship_type_spouse_with_name' => ':name’s ægtefælle', + 'relationship_type_spouse_female_with_name' => ':name’s wife', + 'relationship_type_spouse_male_with_name' => ':name’s husband', + + 'relationship_type_date' => 'date', + 'relationship_type_date_female' => 'date', + 'relationship_type_date_male' => 'date', + 'relationship_type_date_with_name' => ':name’s date', + 'relationship_type_date_female_with_name' => ':name’s date', + 'relationship_type_date_male_with_name' => ':name’s date', + + 'relationship_type_lover' => 'elsker', + 'relationship_type_lover_female' => 'elsker', + 'relationship_type_lover_male' => 'lover', + 'relationship_type_lover_with_name' => ':name’s elsker', + 'relationship_type_lover_female_with_name' => ':name’s elsker', + 'relationship_type_lover_male_with_name' => ':name’s lover', + + 'relationship_type_inlovewith' => 'in love with', + 'relationship_type_inlovewith_female' => 'in love with', + 'relationship_type_inlovewith_male' => 'in love with', + 'relationship_type_inlovewith_with_name' => 'someone :name is in love with', + 'relationship_type_inlovewith_female_with_name' => 'someone :name is in love with', + 'relationship_type_inlovewith_male_with_name' => 'someone :name is in love with', + + 'relationship_type_lovedby' => 'loved by', + 'relationship_type_lovedby_female' => 'loved by', + 'relationship_type_lovedby_male' => 'loved by', + 'relationship_type_lovedby_with_name' => ':name’s secret lover', + 'relationship_type_lovedby_female_with_name' => ':name’s secret lover', + 'relationship_type_lovedby_male_with_name' => ':name’s secret lover', + + 'relationship_type_ex' => 'ex-partner', + 'relationship_type_ex_female' => 'ekskæreste', + 'relationship_type_ex_male' => 'ex-boyfriend', + 'relationship_type_ex_with_name' => ':name’s ex-partner', + 'relationship_type_ex_female_with_name' => ':name’s ekskæreste', + 'relationship_type_ex_male_with_name' => ':name’s ex-boyfriend', + + 'relationship_type_parent' => 'parent', + 'relationship_type_parent_female' => 'mor', + 'relationship_type_parent_male' => 'father', + 'relationship_type_parent_with_name' => ':name’s parent', + 'relationship_type_parent_female_with_name' => ':name’s mor', + 'relationship_type_parent_male_with_name' => ':name’s father', + + 'relationship_type_child' => 'child', + 'relationship_type_child_female' => 'datter', + 'relationship_type_child_male' => 'son', + 'relationship_type_child_with_name' => ':name’s child', + 'relationship_type_child_female_with_name' => ':name’s daughter', + 'relationship_type_child_male_with_name' => ':name’s son', + + 'relationship_type_stepparent' => 'step-parent', + 'relationship_type_stepparent_female' => 'stedmor', + 'relationship_type_stepparent_male' => 'stepfather', + 'relationship_type_stepparent_with_name' => ':name’s step-parent', + 'relationship_type_stepparent_female_with_name' => ':name’s stepmother', + 'relationship_type_stepparent_male_with_name' => ':name’s stepfather', + + 'relationship_type_stepchild' => 'stepchild', + 'relationship_type_stepchild_female' => 'steddatter', + 'relationship_type_stepchild_male' => 'stepson', + 'relationship_type_stepchild_with_name' => ':name’s stepchild', + 'relationship_type_stepchild_female_with_name' => ':name’s stepdaughter', + 'relationship_type_stepchild_male_with_name' => ':name’s stepson', + + 'relationship_type_sibling' => 'sibling', + 'relationship_type_sibling_female' => 'søster', + 'relationship_type_sibling_male' => 'brother', + 'relationship_type_sibling_with_name' => ':name’s sibling', + 'relationship_type_sibling_female_with_name' => ':name’s sister', + 'relationship_type_sibling_male_with_name' => ':name’s brother', + + 'relationship_type_grandparent' => 'grandparent', + 'relationship_type_grandparent_female' => 'grandmother', + 'relationship_type_grandparent_male' => 'grandfather', + 'relationship_type_grandparent_with_name' => ':name’s grandparent', + 'relationship_type_grandparent_female_with_name' => ':name’s grandmother', + 'relationship_type_grandparent_male_with_name' => ':name’s grandfather', + + 'relationship_type_grandchild' => 'grandchild', + 'relationship_type_grandchild_female' => 'granddaughter', + 'relationship_type_grandchild_male' => 'grandson', + 'relationship_type_grandchild_with_name' => ':name’s grandchild', + 'relationship_type_grandchild_female_with_name' => ':name’s granddauther', + 'relationship_type_grandchild_male_with_name' => ':name’s grandson', + + 'relationship_type_uncle' => 'onkel', + 'relationship_type_uncle_female' => 'tante', + 'relationship_type_uncle_male' => 'uncle', + 'relationship_type_uncle_with_name' => ':name’s onkel', + 'relationship_type_uncle_female_with_name' => ':name’s tante', + 'relationship_type_uncle_male_with_name' => ':name’s uncle', + + 'relationship_type_nephew' => 'nevø', + 'relationship_type_nephew_female' => 'niece', + 'relationship_type_nephew_male' => 'nephew', + 'relationship_type_nephew_with_name' => ':name’s nevø', + 'relationship_type_nephew_female_with_name' => ':name’s niece', + 'relationship_type_nephew_male_with_name' => ':name’s nephew', + + 'relationship_type_cousin' => 'fætter', + 'relationship_type_cousin_female' => 'kusine', + 'relationship_type_cousin_male' => 'cousin', + 'relationship_type_cousin_with_name' => ':name’s fætter', + 'relationship_type_cousin_female_with_name' => ':name’s kusine', + 'relationship_type_cousin_male_with_name' => ':name’s cousin', + + 'relationship_type_godfather' => 'godparent', + 'relationship_type_godfather_female' => 'gudmor', + 'relationship_type_godfather_male' => 'godfather', + 'relationship_type_godfather_with_name' => ':name’s godparent', + 'relationship_type_godfather_female_with_name' => ':name’s gudmor', + 'relationship_type_godfather_male_with_name' => ':name’s godfather', + + 'relationship_type_godson' => 'godchild', + 'relationship_type_godson_female' => 'guddatter', + 'relationship_type_godson_male' => 'godson', + 'relationship_type_godson_with_name' => ':name’s godchild', + 'relationship_type_godson_female_with_name' => ':name’s guddatter', + 'relationship_type_godson_male_with_name' => ':name’s godson', + + 'relationship_type_friend' => 'ven', + 'relationship_type_friend_female' => 'veninde', + 'relationship_type_friend_male' => 'friend', + 'relationship_type_friend_with_name' => ':name’s ven', + 'relationship_type_friend_female_with_name' => ':name’s veninde', + 'relationship_type_friend_male_with_name' => ':name’s friend', + + 'relationship_type_bestfriend' => 'bedste ven', + 'relationship_type_bestfriend_female' => 'bedste veninde', + 'relationship_type_bestfriend_male' => 'best friend', + 'relationship_type_bestfriend_with_name' => ':name’s bedste ven', + 'relationship_type_bestfriend_female_with_name' => ':name’s bedste veninde', + 'relationship_type_bestfriend_male_with_name' => ':name’s best friend', + + 'relationship_type_colleague' => 'kollega', + 'relationship_type_colleague_female' => 'kollega', + 'relationship_type_colleague_male' => 'colleague', + 'relationship_type_colleague_with_name' => ':name’s kollega', + 'relationship_type_colleague_female_with_name' => ':name’s kollega', + 'relationship_type_colleague_male_with_name' => ':name’s colleague', + + 'relationship_type_boss' => 'boss', + 'relationship_type_boss_female' => 'boss', + 'relationship_type_boss_male' => 'boss', + 'relationship_type_boss_with_name' => ':name’s boss', + 'relationship_type_boss_female_with_name' => ':name’s boss', + 'relationship_type_boss_male_with_name' => ':name’s boss', + + 'relationship_type_subordinate' => 'subordinate', + 'relationship_type_subordinate_female' => 'subordinate', + 'relationship_type_subordinate_male' => 'subordinate', + 'relationship_type_subordinate_with_name' => ':name’s subordinate', + 'relationship_type_subordinate_female_with_name' => ':name’s subordinate', + 'relationship_type_subordinate_male_with_name' => ':name’s subordinate', + + 'relationship_type_mentor' => 'mentor', + 'relationship_type_mentor_female' => 'mentor', + 'relationship_type_mentor_male' => 'mentor', + 'relationship_type_mentor_with_name' => ':name’s mentor', + 'relationship_type_mentor_female_with_name' => ':name’s mentor', + 'relationship_type_mentor_male_with_name' => ':name’s mentor', + + 'relationship_type_protege' => 'protégé', + 'relationship_type_protege_female' => 'protégé', + 'relationship_type_protege_male' => 'protégé', + 'relationship_type_protege_with_name' => ':name’s protégé', + 'relationship_type_protege_female_with_name' => ':name’s protégé', + 'relationship_type_protege_male_with_name' => ':name’s protégé', + + 'relationship_type_ex_husband' => 'ex-spouse', + 'relationship_type_ex_husband_female' => 'ekskone', + 'relationship_type_ex_husband_male' => 'ex-husband', + 'relationship_type_ex_husband_with_name' => ':name’s ex-spouse', + 'relationship_type_ex_husband_female_with_name' => ':name’s ekskone', + 'relationship_type_ex_husband_male_with_name' => ':name’s ex-husband', + + // emotions + 'emotion_primary_love' => 'Kærlighed', + 'emotion_primary_joy' => 'Joy', + 'emotion_primary_surprise' => 'Overrasket', + 'emotion_primary_anger' => 'Vrede', + 'emotion_primary_sadness' => 'Sørgmodighed', + 'emotion_primary_fear' => 'Frygt', + + 'emotion_secondary_affection' => 'Hengivenhed', + 'emotion_secondary_lust' => 'Lust', + 'emotion_secondary_longing' => 'Longing', + 'emotion_secondary_cheerfulness' => 'Cheerfulness', + 'emotion_secondary_zest' => 'Zest', + 'emotion_secondary_contentment' => 'Contentment', + 'emotion_secondary_pride' => 'Stolthed', + 'emotion_secondary_optimism' => 'Optimisme', + 'emotion_secondary_enthrallment' => 'Enthrallment', + 'emotion_secondary_relief' => 'Relief', + 'emotion_secondary_surprise' => 'Overrasket', + 'emotion_secondary_irritation' => 'Irritation', + 'emotion_secondary_exasperation' => 'Exasperation', + 'emotion_secondary_rage' => 'Raseri', + 'emotion_secondary_disgust' => 'Væmmelse', + 'emotion_secondary_envy' => 'Envy', + 'emotion_secondary_suffering' => 'Suffering', + 'emotion_secondary_sadness' => 'Sadness', + 'emotion_secondary_disappointment' => 'Disappointment', + 'emotion_secondary_shame' => 'Shame', + 'emotion_secondary_neglect' => 'Neglect', + 'emotion_secondary_sympathy' => 'Sympathy', + 'emotion_secondary_horror' => 'Horror', + 'emotion_secondary_nervousness' => 'Nervøsitet', + + 'emotion_adoration' => 'Adoration', + 'emotion_affection' => 'Hengivenhed', + 'emotion_love' => 'Kærlighed', + 'emotion_fondness' => 'Fondness', + 'emotion_liking' => 'Liking', + 'emotion_attraction' => 'Attraction', + 'emotion_caring' => 'Caring', + 'emotion_tenderness' => 'Tenderness', + 'emotion_compassion' => 'Compassion', + 'emotion_sentimentality' => 'Sentimentality', + 'emotion_arousal' => 'Arousal', + 'emotion_desire' => 'Desire', + 'emotion_lust' => 'Lust', + 'emotion_passion' => 'Passion', + 'emotion_infatuation' => 'Infatuation', + 'emotion_longing' => 'Longing', + 'emotion_amusement' => 'Amusement', + 'emotion_bliss' => 'Bliss', + 'emotion_cheerfulness' => 'Cheerfulness', + 'emotion_gaiety' => 'Gaiety', + 'emotion_glee' => 'Glee', + 'emotion_jolliness' => 'Jolliness', + 'emotion_joviality' => 'Joviality', + 'emotion_joy' => 'Joy', + 'emotion_delight' => 'Delight', + 'emotion_enjoyment' => 'Enjoyment', + 'emotion_gladness' => 'Gladness', + 'emotion_happiness' => 'Glæde', + 'emotion_jubilation' => 'Jubilation', + 'emotion_elation' => 'Elation', + 'emotion_satisfaction' => 'Tilfredshed', + 'emotion_ecstasy' => 'Ecstasy', + 'emotion_euphoria' => 'Euphoria', + 'emotion_enthusiasm' => 'Enthusiasm', + 'emotion_zeal' => 'Zeal', + 'emotion_zest' => 'Zest', + 'emotion_excitement' => 'Excitement', + 'emotion_thrill' => 'Thrill', + 'emotion_exhilaration' => 'Exhilaration', + 'emotion_contentment' => 'Contentment', + 'emotion_pleasure' => 'Pleasure', + 'emotion_pride' => 'Stolthed', + 'emotion_eagerness' => 'Eagerness', + 'emotion_hope' => 'Hope', + 'emotion_optimism' => 'Optimisme', + 'emotion_enthrallment' => 'Enthrallment', + 'emotion_rapture' => 'Rapture', + 'emotion_relief' => 'Relief', + 'emotion_amazement' => 'Amazement', + 'emotion_surprise' => 'Surprise', + 'emotion_astonishment' => 'Astonishment', + 'emotion_aggravation' => 'Aggravation', + 'emotion_irritation' => 'Irritation', + 'emotion_agitation' => 'Agitation', + 'emotion_annoyance' => 'Annoyance', + 'emotion_grouchiness' => 'Grouchiness', + 'emotion_grumpiness' => 'Grumpiness', + 'emotion_exasperation' => 'Exasperation', + 'emotion_frustration' => 'Frustration', + 'emotion_anger' => 'Anger', + 'emotion_rage' => 'Rage', + 'emotion_outrage' => 'Outrage', + 'emotion_fury' => 'Fury', + 'emotion_wrath' => 'Wrath', + 'emotion_hostility' => 'Hostility', + 'emotion_ferocity' => 'Ferocity', + 'emotion_bitterness' => 'Bitterness', + 'emotion_hate' => 'Hate', + 'emotion_loathing' => 'Loathing', + 'emotion_scorn' => 'Scorn', + 'emotion_spite' => 'Spite', + 'emotion_vengefulness' => 'Vengefulness', + 'emotion_dislike' => 'Dislike', + 'emotion_resentment' => 'Resentment', + 'emotion_disgust' => 'Disgust', + 'emotion_revulsion' => 'Revulsion', + 'emotion_contempt' => 'Contempt', + 'emotion_envy' => 'Envy', + 'emotion_jealousy' => 'Jealousy', + 'emotion_agony' => 'Agony', + 'emotion_suffering' => 'Suffering', + 'emotion_hurt' => 'Hurt', + 'emotion_anguish' => 'Anguish', + 'emotion_depression' => 'Depression', + 'emotion_despair' => 'Despair', + 'emotion_hopelessness' => 'Hopelessness', + 'emotion_gloom' => 'Gloom', + 'emotion_glumness' => 'Glumness', + 'emotion_sadness' => 'Sadness', + 'emotion_unhappiness' => 'Unhappiness', + 'emotion_grief' => 'Grief', + 'emotion_sorrow' => 'Sorrow', + 'emotion_woe' => 'Woe', + 'emotion_misery' => 'Misery', + 'emotion_melancholy' => 'Melancholy', + 'emotion_dismay' => 'Dismay', + 'emotion_disappointment' => 'Disappointment', + 'emotion_displeasure' => 'Displeasure', + 'emotion_guilt' => 'Guilt', + 'emotion_shame' => 'Shame', + 'emotion_regret' => 'Regret', + 'emotion_remorse' => 'Remorse', + 'emotion_alienation' => 'Alienation', + 'emotion_isolation' => 'Isolation', + 'emotion_neglect' => 'Neglect', + 'emotion_loneliness' => 'Loneliness', + 'emotion_rejection' => 'Rejection', + 'emotion_homesickness' => 'Homesickness', + 'emotion_defeat' => 'Defeat', + 'emotion_dejection' => 'Dejection', + 'emotion_insecurity' => 'Insecurity', + 'emotion_embarrassment' => 'Embarrassment', + 'emotion_humiliation' => 'Humiliation', + 'emotion_insult' => 'Insult', + 'emotion_pity' => 'Pity', + 'emotion_sympathy' => 'Sympathy', + 'emotion_alarm' => 'Alarm', + 'emotion_shock' => 'Shock', + 'emotion_fear' => 'Frygt', + 'emotion_fright' => 'Fright', + 'emotion_horror' => 'Horror', + 'emotion_terror' => 'Terror', + 'emotion_panic' => 'Panik', + 'emotion_hysteria' => 'Hysteria', + 'emotion_mortification' => 'Mortification', + 'emotion_anxiety' => 'Ængstelighed', + 'emotion_nervousness' => 'Nervøsitet', + 'emotion_tenseness' => 'Tenseness', + 'emotion_uneasiness' => 'Uneasiness', + 'emotion_apprehension' => 'Apprehension', + 'emotion_worry' => 'Worry', + 'emotion_distress' => 'Distress', + 'emotion_dread' => 'Dread', + + // weather + 'weather_sunny' => 'Sunny', + 'weather_clear' => 'Clear', + 'weather_clear-day' => 'Clear', + 'weather_clear-night' => 'Clear night', + 'weather_light-drizzle' => 'Light drizzle', + 'weather_patchy-light-drizzle' => 'Patchy light drizzle', + 'weather_patchy-light-rain' => 'Patchy light rain', + 'weather_light-rain' => 'Light rain', + 'weather_moderate-rain-at-times' => 'Moderate rain at times', + 'weather_moderate-rain' => 'Moderate rain', + 'weather_patchy-rain-possible' => 'Patchy rain possible', + 'weather_heavy-rain-at-times' => 'Heavy rain at times', + 'weather_heavy-rain' => 'Heavy rain', + 'weather_light-freezing-rain' => 'Light freezing rain', + 'weather_moderate-or-heavy-freezing-rain' => 'Moderate or heavy freezing rain', + 'weather_light-sleet' => 'Light sleet', + 'weather_moderate-or-heavy-rain-shower' => 'Moderate or heavy rain shower', + 'weather_light-rain-shower' => 'Light rain shower', + 'weather_torrential-rain-shower' => 'Torrential rain shower', + 'weather_rain' => 'Regn', + 'weather_snow' => 'Sne', + 'weather_blowing-snow' => 'Blowing snow', + 'weather_patchy-light-snow' => 'Patchy light snow', + 'weather_light-snow' => 'Light snow', + 'weather_patchy-moderate-snow' => 'Patchy moderate snow', + 'weather_moderate-snow' => 'Moderate snow', + 'weather_patchy-heavy-snow' => 'Patchy heavy snow', + 'weather_heavy-snow' => 'Heavy snow', + 'weather_light-snow-showers' => 'Light snow showers', + 'weather_moderate-or-heavy-snow-showers' => 'Moderate or heavy snow showers', + 'weather_patchy-snow-possible' => 'Patchy snow possible', + 'weather_patchy-sleet-possible' => 'Patchy sleet possible', + 'weather_moderate-or-heavy-sleet' => 'Moderate or heavy sleet', + 'weather_light-sleet-showers' => 'Light sleet showers', + 'weather_moderate-or-heavy-sleet-showers' => 'Moderate or heavy sleet showers', + 'weather_sleet' => 'Slud', + 'weather_wind' => 'Vind', + 'weather_fog' => 'Tåge', + 'weather_freezing-fog' => 'Freezing fog', + 'weather_mist' => 'Mist', + 'weather_blizzard' => 'Blizzard', + 'weather_overcast' => 'Overcast', + 'weather_cloudy' => 'Overskyet', + 'weather_partly-cloudy-day' => 'Partly cloudy', + 'weather_partly-cloudy-night' => 'Partly cloudy', + 'weather_freezing-drizzle' => 'Freezing drizzle', + 'weather_heavy-freezing-drizzle' => 'Heavy freezing drizzle', + 'weather_patchy-freezing-drizzle-possible' => 'Patchy freezing drizzle possible', + 'weather_ice-pellets' => 'Ice pellets', + 'weather_light-showers-of-ice-pellets' => 'Light showers of ice pellets', + 'weather_moderate-or-heavy-showers-of-ice-pellets' => 'Moderate or heavy showers of ice pellets', + 'weather_thundery-outbreaks-possible' => 'Thundery outbreaks possible', + 'weather_patchy-light-rain-with-thunder' => 'Patchy light rain with thunder', + 'weather_moderate-or-heavy-rain-with-thunder' => 'Moderate or heavy rain with thunder', + 'weather_patchy-light-snow-with-thunder' => 'Patchy light snow with thunder', + 'weather_moderate-or-heavy-snow-with-thunder' => 'Moderate or heavy snow with thunder', + 'weather_current_temperature_celsius' => ':temperature °C', + 'weather_current_temperature_fahrenheit' => ':temperature °F', + 'weather_current_title' => 'Current weather', + + // dav + 'dav_contacts' => 'Kontakter', + 'dav_contacts_description' => ':name’s kontakter', + 'dav_birthdays' => 'Fødselsdage', + 'dav_birthdays_description' => ':name’s contact’s birthdays', + 'dav_tasks' => 'Opgaver', + 'dav_tasks_description' => ':name’s opgaver', + + // contact list + 'contact_list_avatar' => 'Avatar', + 'contact_list_name' => 'Contact', + 'contact_list_description' => 'Description', + +]; diff --git a/resources/lang/da/auth.php b/resources/lang/da/auth.php new file mode 100644 index 0000000..f448b32 --- /dev/null +++ b/resources/lang/da/auth.php @@ -0,0 +1,89 @@ + 'These credentials do not match our records.', + 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.', + 'not_authorized' => 'You are not authorized to execute this action', + 'signup_disabled' => 'Registration is currently disabled', + 'signup_error' => 'An error occured trying to register the user', + 'back_homepage' => 'Gå til startside', + 'mfa_auth_otp' => 'Authenticate with your two factor device', + 'mfa_auth_webauthn' => 'Authenticate with a security key (WebAuthn)', + '2fa_title' => 'Two Factor Authentication', + '2fa_wrong_validation' => 'The two factor authentication has failed.', + '2fa_one_time_password' => 'Two factor authentication code', + '2fa_recuperation_code' => 'Enter a two factor recovery code', + '2fa_one_time_or_recuperation' => 'Enter a two factor authentication code or a recovery code', + '2fa_otp_help' => 'Open up your two factor authentication mobile app and copy the code', + + 'login_to_account' => 'Login to your account', + 'login_with_recovery' => 'Login with a recovery code', + 'login_again' => 'Please login again to your account', + 'email' => 'E-mail', + 'password' => 'Adgangskode', + 'recovery' => 'Gendannelseskode', + 'login' => 'Log ind', + 'button_remember' => 'Husk mig', + 'password_forget' => 'Glemt adgangskode?', + 'password_reset' => 'Nulstil din adgangskode', + 'use_recovery' => 'Or you can use a recovery code', + 'signup_no_account' => 'Har du ikke en bruger?', + 'signup' => 'Tilmeld dig', + 'create_account' => 'Create the first account by signing up', + 'change_language_title' => 'Skift sprog:', + 'change_language' => 'Skift sprog til :lang', + + 'password_reset_title' => 'Nulstil adgangskode', + 'password_reset_email' => 'E-mail adresse', + 'password_reset_send_link' => 'Send Password Reset Link', + 'password_reset_password' => 'Adgangskode', + 'password_reset_password_confirm' => 'Bekræft adgangskode', + 'password_reset_action' => 'Nulstil adgangskode', + 'password_reset_email_content' => 'Klik her for at ændre din adgangskode:', + + 'register_title_welcome' => 'Welcome to your newly installed Monica instance', + 'register_create_account' => 'You need to create an account to use Monica', + 'register_title_create' => 'Create your Monica account', + 'register_login' => 'Log in if you already have an account.', + 'register_email' => 'Indtast en gyldig e-mail adresse', + 'register_email_example' => 'you@home', + 'register_firstname' => 'Fornavn', + 'register_firstname_example' => 'eg. John', + 'register_lastname' => 'Efternavn', + 'register_lastname_example' => 'eg. Doe', + 'register_password' => 'Kodeord', + 'register_password_example' => 'Enter a secure password', + 'register_password_confirmation' => 'Password confirmation', + 'register_action' => 'Registrér', + 'register_policy' => 'Signing up signifies you’ve read and agree to our Privacy Policy and Terms of use.', + 'register_invitation_email' => 'For security purposes, please indicate the email of the person who’ve invited you to join this account. This information is provided in the invitation email.', + + 'confirmation_title' => 'Bekræft din e-mail adresse', + 'confirmation_fresh' => 'A fresh verification link has been sent to your email address.', + 'confirmation_check' => 'Before proceeding, please check your email for a verification link.', + 'confirmation_request_another' => 'If you did not receive the email click here to request another.', + + 'confirmation_again' => 'If you want to change your email address you can click here.', + 'email_change_current_email' => 'Nuværende e-mail adresse:', + 'email_change_title' => 'Skift din e-mail adresse', + 'email_change_new' => 'Ny e-mail adresse', + 'email_changed' => 'Your email address has been changed. Check your mailbox to validate it.', +]; diff --git a/resources/lang/da/changelog.php b/resources/lang/da/changelog.php new file mode 100644 index 0000000..60f3835 --- /dev/null +++ b/resources/lang/da/changelog.php @@ -0,0 +1,12 @@ + 'Produktændringer', + 'note' => 'Note: Denne side er desværre kun på engelsk.', +]; diff --git a/resources/lang/da/dashboard.php b/resources/lang/da/dashboard.php new file mode 100644 index 0000000..81be878 --- /dev/null +++ b/resources/lang/da/dashboard.php @@ -0,0 +1,42 @@ + 'Velkommen til din konto!', + 'dashboard_blank_description' => 'Monica is the place to organize all the interactions you have with the people you care about.', + 'dashboard_blank_cta' => 'Tilføj din første kontakt', + 'dashboard_blank_illustration' => 'Illustration af Freepik', + + 'notes_title' => 'Du har ingen foretrukne noter endnu.', + + 'tab_recent_calls' => 'Seneste opkald', + 'tab_favorite_notes' => 'Foretrukne noter', + 'tab_calls_blank' => 'Du har ikke logget nogle opkald endnu.', + 'tab_debts' => 'Gæld', + 'tab_debts_blank' => 'Du har ikke registreret noget gæld endnu.', + 'tab_tasks' => 'Opgaver', + 'tab_tasks_blank' => 'Du har ingen opgaver endnu.', + + 'tasks_add_task_placeholder' => 'Hvad går opgaven ud på?', + 'tasks_tab_your_contacts' => 'Opgaver relateret til dine kontakter', + 'tasks_tab_your_tasks' => 'Dine opgaver', + 'tasks_add_note' => 'Tryk Enter for at tilføje opgaven.', + 'task_add_cta' => 'Tilføj en opgave', + + 'debts_you_owe' => 'Du skylder', + + 'statistics_contacts' => 'Kontakter', + 'statistics_activities' => 'Aktiviteter', + 'statistics_gifts' => 'Gaver', + + 'reminders_next_months' => 'Begivenheder de næste 3 måneder', + 'reminders_none' => 'Ingen påmindelser denne måned.', + + 'product_changes' => 'Produktændringer', + 'product_view_details' => 'Se detaljer', +]; diff --git a/resources/lang/da/format.php b/resources/lang/da/format.php new file mode 100644 index 0000000..944e82a --- /dev/null +++ b/resources/lang/da/format.php @@ -0,0 +1,36 @@ + 'd. M Y, H:i', + 'short_date_year' => 'd. M Y', + 'short_date' => 'd. M', + 'short_month' => 'M', + 'short_month_year' => 'M Y', + 'short_day' => 'D', + 'full_date_year' => 'd. F Y', + 'full_month' => 'F', + 'full_month_year' => 'F Y', + 'full_hour' => 'H:i', + + 'short_text' => '{text}…', +]; diff --git a/resources/lang/da/journal.php b/resources/lang/da/journal.php new file mode 100644 index 0000000..d0c90ed --- /dev/null +++ b/resources/lang/da/journal.php @@ -0,0 +1,38 @@ + 'How was your day? You can rate it once a day.', + 'journal_come_back' => 'Thanks. Come back tomorrow to rate your day again.', + 'journal_description' => 'Note: the journal lists both manual journal entries, and automatic entries like Activities done with your contacts. While you can delete journal entries manually, you’ll have to delete the activity directly on the contact page.', + 'journal_add' => 'Add a journal entry', + 'journal_edit' => 'Edit a journal entry', + 'journal_empty' => 'Empty journal', + 'journal_created_at' => 'Created at {date}', + 'journal_created_automatically' => 'Created automatically', + 'journal_entry_type_journal' => 'Journal entry', + 'journal_entry_type_activity' => 'Aktivitet', + 'journal_entry_rate' => 'You rated your day.', + 'journal_add_comment' => 'Care to add a comment (optional)?', + 'journal_show_comment' => 'Vis kommentar', + 'entry_delete_success' => 'The journal entry has been successfully deleted.', + 'journal_add_title' => 'Title (optional)', + 'journal_add_date' => 'Date', + 'journal_add_post' => 'Entry', + 'journal_add_cta' => 'Save', + 'journal_blank_cta' => 'Add your first journal entry', + 'journal_blank_description' => 'The journal lets you write events that happened to you, and remember them.', + 'delete_confirmation' => 'Are you sure you want to delete this journal entry?', + 'apply_filter' => 'Apply filter', + 'start_date' => 'Start Date', + 'end_date' => 'End Date', + 'per_page' => 'Per Page', + 'sort_order' => 'Sort By Created At', + 'ascending' => 'Ascending', + 'descending' => 'Descending', +]; diff --git a/resources/lang/da/logs.php b/resources/lang/da/logs.php new file mode 100644 index 0000000..7b6654b --- /dev/null +++ b/resources/lang/da/logs.php @@ -0,0 +1,29 @@ + 'Created the contact.', + 'settings_log_contact_created_with_name' => 'Added :name as a contact.', + + // contat description update + 'contact_log_contact_description_updated' => 'Updated the description.', + 'settings_log_contact_description_updated_with_name' => 'Updated the description of :name.', + + // contact description clear + 'contact_log_contact_description_cleared' => 'Cleared the description.', + 'settings_log_contact_description_cleared_with_name' => 'Cleared the description of :name.', + + // contact work information update + 'contact_log_contact_work_updated' => 'Updated work information.', + 'settings_log_contact_work_updated_with_name' => 'Updated work information of :name.', + + // company created + 'settings_log_company_created' => 'Created a company called :name.', +]; diff --git a/resources/lang/da/mail.php b/resources/lang/da/mail.php new file mode 100644 index 0000000..b1263c2 --- /dev/null +++ b/resources/lang/da/mail.php @@ -0,0 +1,53 @@ + 'Reminder for :contact', + 'greetings' => 'Hi :username', + 'want_reminded_of' => 'You wanted to be reminded of :reason', + 'for' => 'For: :name', + 'comment' => 'Comment: :comment', + 'footer_contact_info' => 'Add, view, complete, and change information about this contact:', + 'footer_contact_info2' => 'See :name’s profile', + 'footer_contact_info2_link' => 'See :name’s profile: :url', + + 'notification_subject_line' => 'You have an upcoming event', + 'notification_description' => 'In :count days (on :date), the following event will happen:', + + 'stay_in_touch_subject_line' => 'Stay in touch with :name', + 'stay_in_touch_subject_description' => 'You asked to be reminded to stay in touch with :name every :frequency day.|You asked to be reminded to stay in touch with :name every :frequency days.', + + 'notifications_whoops' => 'Whoops!', + 'notifications_hello' => 'Hello!', + 'notifications_regards' => 'Regards', + 'notifications_footer' => 'If you’re having trouble clicking the ":actionText" button, copy and paste the URL below into your web browser: [:actionURL](:actionURL)', + 'notifications_rights' => 'All rights reserved', + + 'confirmation_email_title' => 'Monica – Email verification', + 'confirmation_email_intro'=> 'To validate your email click on the button below', + 'confirmation_email_button' => 'Bekræft din e-mail adresse', + 'confirmation_email_bottom' => 'If you did not create an account, no further action is required.', + + 'password_reset_title' => 'Monica – Reset Password Notification', + 'password_reset_intro' => 'You are receiving this email because we received a password reset request for your account.', + 'password_reset_button' => 'Nulstil adgangskode', + 'password_reset_expiration' => 'This password reset link will expire in :count minutes.', + 'password_reset_bottom' => 'If you did not request a password reset, no further action is required.', + + 'invitation_title' => 'Monica – You are invited by :name', + 'invitation_intro' => 'You’ve been invited by :name (:email) to use Monica, a nice Personal Relationship Management tool.', + 'invitation_link' => 'To accept the invitation, click on the link below:', + 'invitation_button' => 'Acceptér invitation', + 'invitation_expiration' => 'This link will expire in :count days.', + + 'export_title' => 'Your export is ready', + 'export_description' => 'You requested a data export on :date. It is now ready to download.', + 'export_download' => 'Download export', + +]; diff --git a/resources/lang/da/pagination.php b/resources/lang/da/pagination.php new file mode 100644 index 0000000..9884712 --- /dev/null +++ b/resources/lang/da/pagination.php @@ -0,0 +1,25 @@ + '❮ Forrige', + 'next' => 'Næste ❯', + +]; diff --git a/resources/lang/da/passwords.php b/resources/lang/da/passwords.php new file mode 100644 index 0000000..790d2e9 --- /dev/null +++ b/resources/lang/da/passwords.php @@ -0,0 +1,30 @@ + 'Din adgangskode er blevet nulstillet!', + 'sent' => 'Hvis din e-mail adresse eksisterer i systemet, vil du modtage en e-mail til nulstilling af din adgangskode.', + 'token' => 'Nulstillingsnøglen til denne adgangskode er ugyldig.', + 'user' => 'Hvis din e-mail adresse eksisterer i systemet, vil du modtage en e-mail til nulstilling af din adgangskode.', + 'changed' => 'Adgangskoden er ændret.', + 'invalid' => 'Adgangskode er forkert.', + 'throttled' => 'Please wait before retrying.', + +]; diff --git a/resources/lang/da/people.php b/resources/lang/da/people.php new file mode 100644 index 0000000..c39cd35 --- /dev/null +++ b/resources/lang/da/people.php @@ -0,0 +1,539 @@ + 'Kontakt ikke fundet', + 'people_list_number_kids' => ':count child|:count children', + 'people_list_last_updated' => 'Sidste checket:', + 'people_list_number_reminders' => ':count reminder|:count reminders', + 'people_list_blank_title' => 'Du har endnu ikke nogen på din konto endnu', + 'people_list_blank_cta' => 'Tilføj en anden person', + 'people_list_sort' => 'Sortér', + 'people_list_stats' => ':count contact|:count contacts', + 'people_list_firstnameAZ' => 'Sortér på fornavn A → Z', + 'people_list_firstnameZA' => 'Sortér på fornavn Z → A', + 'people_list_lastnameAZ' => 'Sortér på efternavn A → Z', + 'people_list_lastnameZA' => 'Sortér på efternavn Z → A', + 'people_list_lastactivitydateNewtoOld' => 'Sort by last activity date, newest to oldest', + 'people_list_lastactivitydateOldtoNew' => 'Sort by last activity date, oldest to newest', + 'people_list_filter_tag' => 'Viser alle kontakter tagget med', + 'people_list_clear_filter' => 'Ryd filter', + 'people_list_contacts_per_tags' => ':count contact|:count contacts', + 'people_list_show_dead' => 'Vis afdøde (:count)', + 'people_list_hide_dead' => 'Skjul afdøde (:count)', + 'people_search' => 'Search your contacts…', + 'people_search_no_results' => 'Ingen resultater fundet', + 'people_search_next' => 'Næste', + 'people_search_prev' => 'Previous', + 'people_search_rows_per_page' => 'Rows per page', + 'people_search_of' => 'af', + 'people_search_page' => 'Side', + 'people_search_all' => 'Alle', + 'people_add_new' => 'Tilføj ny person', + 'people_list_account_usage' => 'Dit konto forbrug: :current/:limit contacts', + 'people_list_account_upgrade_title' => 'Opgrader din konto for at låse den op for dets fulde potentiale.', + 'people_list_account_upgrade_cta' => 'Opgradér nu', + 'people_list_untagged' => 'Vis ikke-taggede kontakter', + 'people_list_filter_untag' => 'Viser alle ikke-taggede kontakter', + 'archived_contact_readonly' => 'Archived contact can’t be edited, please unarchive it first.', + + // people add + 'people_add_title' => 'Tilføj en ny person', + 'people_add_missing' => 'No person found – add a new one now', + 'people_add_firstname' => 'Fornavn', + 'people_add_middlename' => 'Middle name (optional)', + 'people_add_lastname' => 'Last name (optional)', + 'people_add_email' => 'Email (optional)', + 'people_add_nickname' => 'Nickname (optional)', + 'people_add_cta' => 'Tilføj', + 'people_save_and_add_another_cta' => 'Indsend og tilføj en anden', + 'people_add_success' => ':name er blevet oprettet', + 'people_add_gender' => 'Køn', + 'people_delete_success' => 'Kontakten er blevet slettet', + 'people_delete_message' => 'Slet kontakt', + 'people_delete_confirmation' => 'Are you sure you want to delete :name’s contact? Deletion is immediate and permanent.', + 'people_add_birthday_reminder' => 'Ønsk :name tillykke', + 'people_add_birthday_reminder_deceased' => 'On this date, :name would have celebrated their birthday', + 'people_add_import' => 'Ønsker du at importere dine kontakter?', + 'people_edit_email_error' => 'Der findes allerede en kontakt på din konto med denne e-mailadresse. Vælg venligst en anden.', + 'people_export' => 'Eksportér som vCard', + 'people_add_reminder_for_birthday' => 'Create an annual birthday reminder', + + // show + 'section_contact_information' => 'Kontaktoplysninger', + 'section_personal_activities' => 'Aktiviteter', + 'section_personal_reminders' => 'Påmindelser', + 'section_personal_tasks' => 'Opgaver', + 'section_personal_gifts' => 'Gaver', + 'section_personal_notes' => 'Noter', + + // archived contacts + 'list_link_to_active_contacts' => 'Du ser arkiverede kontakter. Se i stedet listen over aktive kontakter.', + 'list_link_to_archived_contacts' => 'Liste af arkiverede kontakter', + + // Header + 'me' => 'Dette er dig', + 'edit_contact_information' => 'Redigér kontaktoplysninger', + 'contact_archive' => 'Arkivér kontakt', + 'contact_unarchive' => 'Gendan kontakt fra arkiv', + 'contact_archive_help' => 'Archived contacts are not be shown on the contact list, but still appear in search results.', + 'call_button' => 'Log et opkald', + 'set_favorite' => 'Favoritkontakter er placeret øverst på kontaktlisten', + + // Stay in touch + 'stay_in_touch' => 'Hold kontakten', + 'stay_in_touch_frequency' => 'Hold kontakten hver dag|Hold kontakten hver {count}. dag', + 'stay_in_touch_next_date' => 'Next due: {date}', + 'stay_in_touch_invalid' => 'Frekvensen skal være et tal større end 0.', + 'stay_in_touch_premium' => 'Du skal opgradere din konto for at gøre brug af denne funktion', + 'stay_in_touch_modal_title' => 'Hold kontakten', + 'stay_in_touch_modal_desc' => 'Vi kan påminde dig via e-mail for at holde kontakten med {firstname} med jævne mellemrum.', + 'stay_in_touch_modal_label' => 'Send me an email every… {count} day|Send me an email every… {count} days', + + // Calls + 'modal_call_title' => 'Log et opkald', + 'modal_call_comment' => 'Hvad snakkede I om? (valgfrit)', + 'modal_call_exact_date' => 'Telefonopkaldet skete den', + 'modal_call_who_called' => 'Hvem ringede?', + 'modal_call_emotion' => 'Vil du logge hvordan du følte under dette opkald? (valgfrit)', + 'calls_add_success' => 'Opkaldet er blevet gemt.', + 'call_delete_confirmation' => 'Er du sikker på, at du vil slette dette opkald?', + 'call_delete_success' => 'Opkaldet er blevet slettet', + 'call_title' => 'Opkald', + 'call_empty_comment' => 'Ingen detaljer', + 'call_blank_title' => 'Hold styr på de telefonopkald, du har foretaget med {name}', + 'call_blank_desc' => 'Du ringede til {name}', + 'call_you_called' => 'Du ringede', + 'call_he_called' => '{name} ringede', + 'call_emotions' => 'Følelser:', + + // Conversation + 'conversation_blank' => 'Record conversations you have with :name on social media, SMS…', + 'conversation_delete_link' => 'Slet samtalen', + 'conversation_edit_title' => 'Redigér samtalen', + 'conversation_edit_delete' => 'Er du sikker på, at du ønsker at slette denne samtale? Denne handling er permanent.', + 'conversation_add_success' => 'Samtalen er blevet tilføjet.', + 'conversation_edit_success' => 'Samtalen er blevet opdateret.', + 'conversation_delete_success' => 'Samtalen er blevet slettet.', + 'conversation_add_title' => 'Opret en ny samtale', + 'conversation_add_when' => 'Hvornår havde du samtalen?', + 'conversation_add_who_wrote' => 'Who sent this message?', + 'conversation_add_how' => 'Hvordan kommunikerede du?', + 'conversation_add_you' => 'Dig', + 'conversation_add_content' => 'Skriv ned hvad der blev sagt', + 'conversation_add_what_was_said' => 'Hvad sagde du?', + 'conversation_add_another' => 'Tilføj en anden besked', + 'conversation_add_error' => 'Du skal angive mindst én besked.', + 'conversation_list_table_messages' => 'Beskeder', + 'conversation_list_table_content' => 'Delvis indhold (sidste meddelelse)', + 'conversation_list_title' => 'Samtaler', + 'conversation_list_cta' => 'Log samtalen', + + // age - birthday + 'birthdate_not_set' => 'Birthday is not set', + 'age_approximate_in_years' => 'omkring :age år', + 'age_exact_in_years' => ':age år', + 'age_exact_birthdate' => 'født :date', + + // Last called + 'last_called' => 'Sidste opkald: :date', + 'last_talked_to' => 'Last called: {date}', + 'last_called_empty' => 'Sidste opkald: ukendt', + 'last_activity_date' => 'Sidste aktivitet sammen: :date', + 'last_activity_date_empty' => 'Sidste aktivitet sammen: ukendt', + + // additional information + 'information_edit_success' => 'Profilen er blevet opdateret', + 'information_edit_title' => 'Redigér :name’s personlige oplysninger', + 'information_edit_max_size' => 'Maks. :size Kb.', + 'information_edit_max_size2' => 'Maks. {size} Kb.', + 'information_edit_firstname' => 'Fornavn', + 'information_edit_lastname' => 'Last name (optional)', + 'information_edit_description' => 'Description (optional)', + 'information_edit_description_help' => 'Bruges på kontaktlisten til at tilføje noget sammenhæng, hvis det er nødvendigt.', + 'information_edit_unknown' => 'Jeg kender ikke denne person’s alder', + 'information_edit_probably' => 'This person is probably…', + 'information_edit_not_year' => 'I know the day and month of this person’s birthday, but not the year…', + 'information_edit_exact' => 'I know this person’s exact birthday…', + 'information_edit_birthdate_label' => 'Birthday', + 'information_no_work_defined' => 'Ingen arbejdsinformation defineret', + 'information_work_at' => 'hos :company', + 'work_add_cta' => 'Opdatér arbejdsoplysninger', + 'work_edit_success' => 'Work information updated', + 'work_edit_title' => 'Opdater :names\' jobinformation', + 'work_edit_job' => 'Jobtitel (valgfri)', + 'work_edit_company' => 'Virksomhed (valgfri)', + 'work_information' => 'Arbejdsinformation', + + // food preferences + 'food_preferences_add_success' => 'Mad preferencer er gemt', + 'food_preferences_edit_description' => 'Måske :firstname eller nogen i :family familie har en allergi. Eller kan ikke lide en bestemt flaske vin. Angiv dem her, så du kan huske det næste gang du inviterer dem til middag', + 'food_preferences_edit_description_no_last_name' => 'Måske :firstname har en allergi. Eller kan ikke lide en bestemt flaske vin. Angiv dem her, så du kan huske det næste gang du inviterer dem til middag', + 'food_preferences_edit_title' => 'Indikér mad preferencer', + 'food_preferences_edit_cta' => 'Gem mad preferencer', + 'food_preferences_title' => 'Mad preferencer', + 'food_preferences_cta' => 'Tilføj mad preferencer', + + // reminders + 'reminders_blank_title' => 'Er der noget, du ønsker at blive mindet om, om :name?', + 'reminders_blank_add_activity' => 'Tilføj en påmindelse', + 'reminders_add_title' => 'Hvad ønsker du at blive mindet om, om :name?', + 'reminders_add_description' => 'Please remind me to…', + 'reminders_add_next_time' => 'Hvornår er den næste gang, du ønsket at blive mindet om dette?', + 'reminders_add_once' => 'Påmind mig om dette en gang', + 'reminders_add_recurrent' => 'Påmind mig om dette hver', + 'reminders_add_starting_from' => 'fra den dato, der er angivet ovenfor', + 'reminders_add_cta' => 'Tilføj påmindelse', + 'reminders_edit_update_cta' => 'Opdatér påmindelse', + 'reminders_add_error_custom_text' => 'Du skal angive en tekst til denne påmindelse', + 'reminders_create_success' => 'Påmindelsen er blevet tilføjet', + 'reminders_delete_success' => 'Påmindelsen er blevet slettet', + 'reminders_update_success' => 'Påmindelsen er blevet opdateret', + 'reminders_add_optional_comment' => 'Valgfri kommentar', + + 'reminder_frequency_day' => 'hver dag|hver :number. dag', + 'reminder_frequency_week' => 'hver uge|hver :number. uge', + 'reminder_frequency_month' => 'hver måned|hver :number. måned', + 'reminder_frequency_year' => 'hvert år|hvert :number. år', + 'reminder_frequency_one_time' => 'den :date', + 'reminders_delete_confirmation' => 'Er du sikker på, at du vil slette denne påmindelse?', + 'reminders_delete_cta' => 'Slet', + 'reminders_next_expected_date' => 'den', + 'reminders_cta' => 'Tilføj en påmindelse', + 'reminders_description' => 'We will send an email for each one of the reminders below. Reminders are sent every morning the day events will happen. Reminders automatically added for birthdays can not be deleted. If you want to change those dates, edit the birthday of the contacts.', + 'reminders_one_time' => 'En gang', + 'reminders_type_week' => 'uge', + 'reminders_type_month' => 'måned', + 'reminders_type_year' => 'år', + 'reminders_birthday' => 'Fødselsdag for :name', + 'reminders_free_plan_warning' => 'Du er på den gratis plan. Ingen e-mails sendes på denne plan. For at modtage dine påmindelser via e-mail, skal du opgradere din konto.', + + // relationships + 'relationship_form_add' => 'Tilføj et nyt forhold', + 'relationship_form_edit' => 'Tilføj et eksisterende forhold', + 'relationship_form_is_with' => 'This person is…', + 'relationship_form_is_with_name' => ':name is…', + 'relationship_form_add_choice' => 'Hvem er forholdet med?', + 'relationship_form_create_contact' => 'Tilføj en ny person', + 'relationship_form_associate_contact' => 'En eksisterende kontakt', + 'relationship_form_associate_dropdown' => 'Søg efter en eksisterende kontakt og vælg nedenfor', + 'relationship_form_associate_dropdown_placeholder' => 'Søg efter en eksisterende kontakt', + 'relationship_form_also_create_contact' => 'Opret en kontakt for denne person.', + 'relationship_form_add_description' => 'Dette vil lade dig behandle denne person som enhver anden kontakt.', + 'relationship_form_add_no_existing_contact' => 'Du har ikke nogen kontakter, der kan være relateret til :name i øjeblikket.', + 'relationship_delete_confirmation' => 'Er du sikker på, at du ønsker at slette dette forhold? Denne handling er permanent.', + 'relationship_unlink_confirmation' => 'Er du sikker på, at du vil slette dette forhold? Personen bliver ikke slettet – kun forholdet mellem de to.', + 'relationship_form_add_success' => 'Forholdet er blevet slettet.', + 'relationship_form_deletion_success' => 'Forholdet er blevet slettet.', + + // tasks + 'tasks_title' => 'Opgaver', + 'tasks_blank_title' => 'Du har ingen opgaver endnu.', + 'tasks_form_title' => 'Titel', + 'tasks_form_description' => 'Beskrivelse (valgfrit)', + 'tasks_add_task' => 'Tilføj en opgave', + 'tasks_delete_success' => 'Opgaven er blevet slettet', + 'tasks_complete_success' => 'Opgaven har fået ændret status', + + // activities + 'activity_title' => 'Aktiviteter', + 'activity_type_category_simple_activities' => 'Simple aktiviteter', + 'activity_type_category_sport' => 'Sport', + 'activity_type_category_food' => 'Mad', + 'activity_type_category_cultural_activities' => 'Kulturelle aktiviteter', + 'activity_type_just_hung_out' => 'hang bare ud', + 'activity_type_watched_movie_at_home' => 'så en film hjemme', + 'activity_type_talked_at_home' => 'snakkede bare hjemme', + 'activity_type_did_sport_activities_together' => 'spillede en sport sammen', + 'activity_type_ate_at_his_place' => 'spiste hos dem', + 'activity_type_went_bar' => 'gik på bar', + 'activity_type_ate_at_home' => 'spiste hjemme', + 'activity_type_picnicked' => 'picnicked', + 'activity_type_ate_restaurant' => 'spiste på restaurent', + 'activity_type_went_theater' => 'gik i teateret', + 'activity_type_went_concert' => 'gik til koncert', + 'activity_type_went_play' => 'gik til en forestilling', + 'activity_type_went_museum' => 'gik på museum', + 'activities_add_activity' => 'Tilføj aktivitet', + 'activities_add_more_details' => 'Tilføj flere detaljer', + 'activities_add_emotions' => 'Tilføj følelser', + 'activities_add_category' => 'Angiv en kategori', + 'activities_add_participants_cta' => 'Tilføj deltagere', + 'activities_item_information' => ':Acitivity. Skete den :date', + 'activities_add_title' => 'Hvad lavede du med {name}?', + 'activities_summary' => 'Beskriv hvad I gjorde', + 'activities_add_pick_activity' => 'Would you like to categorize this activity? You don’t have to, but it will give you statistics later on (optional)', + 'activities_add_date_occured' => 'The activity happened on…', + 'activities_add_participants' => 'Hvem, udover {name}, deltog i denne aktivitet? (valgfrit)', + 'activities_add_emotions_title' => 'Vil du logge hvordan du følte dig under denne aktivitet? (valgfrit)', + 'activities_blank_title' => 'Hold styr på, hvad du har lavet med {name} i fortiden, og hvad I har talt om', + 'activities_blank_add_activity' => 'Tilføj en aktivitet', + 'activities_add_success' => 'Aktiviteten er blevet tilføjet', + 'activities_add_error' => 'Fejl ved tilføjelse af aktiviteten', + 'activities_update_success' => 'Aktiviteten er blevet opdateret', + 'activities_delete_success' => 'Aktiviteten er blevet slettet', + 'activities_who_was_involved' => 'Hvem var med?', + 'activities_activity' => 'Aktivitetskategori', + 'activities_view_activities_report' => 'Vis aktivitetsoversigt', + 'activities_profile_title' => 'Aktivitetsrapport mellem :name og dig', + 'activities_profile_subtitle' => 'Du har logget :total_activities aktivitet med :name i alt og :activities_last_twelve_months i de sidste 12 måneder indtil nu.|Du har logget :total_activities aktiviteter med :name i alt og :activities_last_twelve_months i de sidste 12 måneder indtil nu.', + 'activities_profile_year_summary_activity_types' => 'Her er en opdeling af den type aktiviteter, I har lavet sammen i :year', + 'activities_profile_year_summary' => 'Her er hvad I to har lavet i :year', + 'activities_profile_number_occurences' => ':value aktivitet|:value aktiviteter', + 'activities_list_participants' => 'Participants ({total}):', + 'activities_list_emotions' => 'Følelser følt:', + 'activities_list_date' => 'Skete den', + 'activities_list_category' => 'Kategori:', + + // notes + 'notes_create_success' => 'Noten er blevet oprettet', + 'notes_update_success' => 'Noten er blevet gemt', + 'notes_delete_success' => 'Noten er blevet slettet', + 'notes_add_cta' => 'Tilføj note', + 'notes_favorite' => 'Tilføj/Fjern fra favoritter', + 'notes_delete_title' => 'Slet note', + 'notes_delete_confirmation' => 'Er du sikker på, at du ønsker at slette denne note? Denne handling er permanent', + + // gifts + 'gifts_title' => 'Gaver', + 'gifts_add_success' => 'Gaven er blevet tilføjet', + 'gifts_delete_success' => 'Gaven er blevet slettet', + 'gifts_delete_confirmation' => 'Er du sikker på at du vil slette denne gave?', + 'gifts_add_gift' => 'Tilføj en gave', + 'gifts_link' => 'Link', + 'gifts_for' => 'Til: {name}', + 'gifts_delete_cta' => 'Slet', + 'gifts_add_title' => 'Gavestyring for :name', + 'gifts_add_gift_idea' => 'Gave idé', + 'gifts_add_gift_already_offered' => 'Gave givet', + 'gifts_add_gift_received' => 'Gave modtaget', + 'gifts_add_gift_title' => 'Hvad er gaven?', + 'gifts_add_gift_name' => 'Gavenavn', + 'gifts_add_link' => 'Link til web side (valgfri)', + 'gifts_add_value' => 'Værdi (valgfri)', + 'gifts_add_comment' => 'Kommentar (valgfri)', + 'gifts_add_recipient' => 'Modtager (valgfrit)', + 'gifts_add_recipient_field' => 'Modtager', + 'gifts_add_photo' => 'Billede (valgfrit)', + 'gifts_add_photo_title' => 'Tilføj et billede til denne gave', + 'gifts_add_someone' => 'Denne gave er til nogen bestemt i {name}s familie', + 'gifts_delete_title' => 'Slet en gave', + 'gifts_ideas' => 'Gave idéer', + 'gifts_offered' => 'Gaver givet', + 'gifts_offered_as_an_idea' => 'Markér som idé', + 'gifts_received' => 'Gave modtaget', + 'gifts_view_comment' => 'Se kommentar', + 'gifts_mark_offered' => 'Marker som givet', + 'gifts_update_success' => 'Gaven er blevet opdateret', + 'gifts_add_date' => 'Date (optional)', + + // debts + 'debt_delete_confirmation' => 'Er du sikker på, at du vil slette denne gæld?', + 'debt_delete_success' => 'Gælden er blevet slettet', + 'debt_add_success' => 'Gælden er blevet tilføjet', + 'debt_title' => 'Gæld', + 'debt_add_cta' => 'Tilføj gæld', + 'debt_you_owe' => 'Du skylder :amount', + 'debt_they_owe' => ':name skylder dig :amount', + 'debt_add_title' => 'Gældstyring', + 'debt_add_you_owe' => 'Du skylder :name', + 'debt_add_they_owe' => ':name skylder dig', + 'debt_add_amount' => 'summen af', + 'debt_add_reason' => 'af følgende årsag (valgfri)', + 'debt_add_add_cta' => 'Tilføj gæld', + 'debt_edit_update_cta' => 'Opdatér gæld', + 'debt_edit_success' => 'Gælden er blevet opdateret', + 'debts_blank_title' => 'Administrer gæld du skylder :name eller :name skylder dig', + + // tags + 'tag_edit' => 'Redigér tag', + 'tag_add' => 'Tilføj tags', + 'tag_add_search' => 'Tilføj eller søg tags', + 'tag_no_tags' => 'Ingen tags endnu', + + // Introductions + 'introductions_sidebar_title' => 'Hvordan I mødtes', + 'introductions_blank_cta' => 'Beskriv hvordan du mødte :name', + 'introductions_title_edit' => 'Hvordan mødte du :name?', + 'introductions_additional_info' => 'Beskriv hvordan og hvor I mødtes', + 'introductions_edit_met_through' => 'Blev du introduceret til denne person af en anden?', + 'introductions_no_met_through' => 'Ingen', + 'introductions_first_met_date' => 'Dag I mødtes', + 'introductions_no_first_met_date' => 'Jeg kender ikke den dato, vi mødte', + 'introductions_first_met_date_known' => 'Dette er den dato, vi mødtes', + 'introductions_add_reminder' => 'Tilføj en påmindelse for at fejre dette møde på årsdagen denne begivenhed fandt sted', + 'introductions_update_success' => 'Du har opdateret oplysningerne om, hvordan du har mødt denne person', + 'introductions_met_through' => 'Mødtes gennem :name', + 'introductions_met_date' => 'Mødtes den :date', + 'introductions_reminder_title' => 'Årsdag for første gang i mødtes', + + // Deceased + 'deceased_reminder_title' => 'Dødsdag for :name', + 'deceased_mark_person_deceased' => 'Mark this as deceased', + 'deceased_know_date' => 'I know the date that this person died', + 'deceased_add_reminder' => 'Tilføj en påmindelse for denne dag', + 'deceased_label' => 'Afdød', + 'deceased_date_label' => 'Afdød dato', + 'deceased_label_with_date' => 'Døde den :date', + 'deceased_age' => 'Alder ved døden', + + // Contact information + 'contact_info_title' => 'Kontaktoplysninger', + 'contact_info_form_content' => 'Indhold', + 'contact_info_form_contact_type' => 'Kontaktype', + 'contact_info_form_personalize' => 'Tilpas', + 'contact_info_address' => 'Bor i', + + // Addresses + 'contact_address_title' => 'Adresser', + 'contact_address_form_name' => 'Etiket (valgfrit)', + 'contact_address_form_street' => 'Gadenavn (valgfri)', + 'contact_address_form_city' => 'By (valgfri)', + 'contact_address_form_province' => 'Provins (valgfrit)', + 'contact_address_form_postal_code' => 'Postnummer (valgfri)', + 'contact_address_form_country' => 'Land (valgfri)', + 'contact_address_form_latitude' => 'Breddegrad (kun tal) (valgfrit)', + 'contact_address_form_longitude' => 'Længdegrad (kun tal) (valgfrit)', + + // Pets + 'pets_kind' => 'Kæledyr', + 'pets_name' => 'Navn (valgfrit)', + 'pets_create_success' => 'Kæledyret er blevet tilføjet', + 'pets_update_success' => 'Kæledyret er blevet opdateret', + 'pets_delete_success' => 'Kæledyret er blevet slettet', + 'pets_title' => 'Kæledyr', + 'pets_reptile' => 'Krybdyr', + 'pets_bird' => 'Fugl', + 'pets_cat' => 'Kat', + 'pets_dog' => 'Hund', + 'pets_fish' => 'Fisk', + 'pets_hamster' => 'Hamster', + 'pets_horse' => 'Hest', + 'pets_rabbit' => 'Kanin', + 'pets_rat' => 'Rotte', + 'pets_small_animal' => 'Lille dyr', + 'pets_other' => 'Andet', + + // life events + 'life_event_list_tab_life_events' => 'Livsbegivenheder', + 'life_event_list_tab_other' => 'Notes, reminders, …', + 'life_event_list_title' => 'Livsbegivenheder', + 'life_event_blank' => 'Log hvad der sker i livet for {name} til din fremtidige reference.', + 'life_event_list_cta' => 'Tilføj livsbegivenhed', + 'life_event_create_category' => 'Alle kategorier', + 'life_event_create_life_event' => 'Tilføj livsbegivenhed', + 'life_event_create_default_title' => 'Titel (valgfrit)', + 'life_event_create_default_story' => 'Historie (valgfrit)', + 'life_event_create_date' => 'You do not need to indicate a month or a day – only the year is mandatory.', + 'life_event_create_default_description' => 'Tilføj information om hvad du ved', + 'life_event_create_add_yearly_reminder' => 'Tilføj en årlig påmindelse for denne begivenhed', + 'life_event_create_success' => 'Livsbegivenheden er blevet tilføjet', + 'life_event_delete_title' => 'Slet en livsbegivenhed', + 'life_event_delete_description' => 'Er du sikker på, at du vil slette denne livsbegivenhed? Sletning er permanent.', + 'life_event_delete_success' => 'Livsbegivenheden er blevet slettet', + 'life_event_date_it_happened' => 'Datoen hvor det skete', + 'life_event_category_work_education' => 'Arbejde & uddannelse', + 'life_event_category_family_relationships' => 'Familie & relationer', + 'life_event_category_home_living' => 'Hjem & liv', + 'life_event_category_health_wellness' => 'Sundhed & velvære', + 'life_event_category_travel_experiences' => 'Rejser & oplevelser', + 'life_event_sentence_new_job' => 'Startede på nyt arbejde', + 'life_event_sentence_retirement' => 'Pensioneret', + 'life_event_sentence_new_school' => 'Startede i skole', + 'life_event_sentence_study_abroad' => 'Studerede i udlandet', + 'life_event_sentence_volunteer_work' => 'Begyndt frivilligt arbejde', + 'life_event_sentence_published_book_or_paper' => 'Udgav en artikel', + 'life_event_sentence_military_service' => 'Startede i militæret', + 'life_event_sentence_new_relationship' => 'Startede et forhold', + 'life_event_sentence_engagement' => 'Blev forlovet', + 'life_event_sentence_marriage' => 'Blev gift', + 'life_event_sentence_anniversary' => 'Årsdag', + 'life_event_sentence_expecting_a_baby' => 'Forventer et barn', + 'life_event_sentence_new_child' => 'Fik et barn', + 'life_event_sentence_new_family_member' => 'Fik et familie medlem', + 'life_event_sentence_new_pet' => 'Fik et kæledyr', + 'life_event_sentence_end_of_relationship' => 'Afsluttede et forhold', + 'life_event_sentence_loss_of_a_loved_one' => 'Mistede en elsket', + 'life_event_sentence_moved' => 'Flyttede', + 'life_event_sentence_bought_a_home' => 'Købte et hjem', + 'life_event_sentence_home_improvement' => 'Lavede en forbedring af hjemmet', + 'life_event_sentence_holidays' => 'Tog på ferie', + 'life_event_sentence_new_vehicle' => 'Fik et nyt køretøj', + 'life_event_sentence_new_roommate' => 'Fik en roommate', + 'life_event_sentence_overcame_an_illness' => 'Overvandt en sygdom', + 'life_event_sentence_quit_a_habit' => 'Stoppede en dårlig vane', + 'life_event_sentence_new_eating_habits' => 'Fik nye spisevaner', + 'life_event_sentence_weight_loss' => 'Tabte sig', + 'life_event_sentence_wear_glass_or_contact' => 'Begynde at gå med briller eller linser', + 'life_event_sentence_broken_bone' => 'Brækkede en knogle', + 'life_event_sentence_removed_braces' => 'Fjernede bøjler', + 'life_event_sentence_surgery' => 'Undergik en operation', + 'life_event_sentence_dentist' => 'Gik til tandlægen', + 'life_event_sentence_new_sport' => 'Startede til sport', + 'life_event_sentence_new_hobby' => 'Startede en hobby', + 'life_event_sentence_new_instrument' => 'Lærte at spille et instrument', + 'life_event_sentence_new_language' => 'Lærte et ny sprog', + 'life_event_sentence_tattoo_or_piercing' => 'Fik en tatovering eller piercing', + 'life_event_sentence_new_license' => 'Fik et kørekort', + 'life_event_sentence_travel' => 'Rejste', + 'life_event_sentence_achievement_or_award' => 'Got an achievement or award', + 'life_event_sentence_changed_beliefs' => 'Skiftede tro', + 'life_event_sentence_first_word' => 'Snakkede for første gang', + 'life_event_sentence_first_kiss' => 'Kyssede for første gang', + + // documents + 'document_list_title' => 'Dokumenter', + 'document_list_cta' => 'Overfør dokument', + 'document_list_blank_desc' => 'Her kan du opbevare dokumenter relateret til denne person.', + 'document_upload_zone_cta' => 'Overfør en fil', + 'document_upload_zone_progress' => 'Uploading the document…', + 'document_upload_zone_error' => 'Der opstod en fejl under overførslen af dokumentet. Prøv venligst igen.', + + // Photos + 'photo_title' => 'Billeder', + 'photo_list_title' => 'Relaterede billeder', + 'photo_list_cta' => 'Upload billede', + 'photo_list_blank_desc' => 'Du kan gemme billeder om denne kontakt. Upload et nu!', + 'photo_upload_zone_cta' => 'Tilføj et billede', + 'photo_current_profile_pic' => 'Nuværende profilbillede', + 'photo_make_profile_pic' => 'Brug som profilbillede', + 'photo_delete' => 'Slet billede', + 'photo_next' => 'Next photo ❯', + 'photo_previous' => '❮ Previous photo', + + // Avatars + 'avatar_change_title' => 'Skift profilbillede', + 'avatar_question' => 'Hvilket profilbillede ønsker du at bruge?', + 'avatar_default_avatar' => 'Standard profilbilledet', + 'avatar_adorable_avatar' => 'Det bedårende profilbillede', + 'avatar_gravatar' => 'Det Gravatar forbundet med e-mailadressen for denne person. Gravatar er et globalt system der gør det muligt for brugere at tilknytte e-mailadresser med billeder.', + 'avatar_current' => 'Behold det nuværende profilbillede', + 'avatar_photo' => 'Fra et billede, du uploader', + 'avatar_crop_new_avatar_photo' => 'Beskær nyt profilbillede', + + // emotions + 'emotion_this_made_me_feel' => 'Dette fik dig til at føle…', + + // logs + 'auditlogs_link' => 'Historik', + 'auditlogs_title' => 'Alt, hvad der skete :name', + 'auditlogs_breadcrumb' => 'Historik', + 'auditlogs_author' => 'Efter :name på :date', + + // contact field label + 'contact_field_label_home' => 'Hjem', + 'contact_field_label_work' => 'Arbejde', + 'contact_field_label_cell' => 'Mobil', + 'contact_field_label_fax' => 'Fax', + 'contact_field_label_pager' => 'Personsøger', + 'contact_field_label_main' => 'Primær', + 'contact_field_label_other' => 'Andet', + 'contact_field_label_personal' => 'Personlig', +]; diff --git a/resources/lang/da/reminder.php b/resources/lang/da/reminder.php new file mode 100644 index 0000000..0ac9ed6 --- /dev/null +++ b/resources/lang/da/reminder.php @@ -0,0 +1,16 @@ + 'Ønsk tillykke til', + 'type_phone_call' => 'Ring til', + 'type_lunch' => 'Frokost med', + 'type_hangout' => 'Hæng ud med', + 'type_email' => 'E-mail', + 'type_birthday_kid' => 'Wish happy birthday to the child of', +]; diff --git a/resources/lang/da/settings.php b/resources/lang/da/settings.php new file mode 100644 index 0000000..5159274 --- /dev/null +++ b/resources/lang/da/settings.php @@ -0,0 +1,557 @@ + 'Account settings', + 'sidebar_personalization' => 'Personalization', + 'sidebar_settings_storage' => 'Storage', + 'sidebar_settings_export' => 'Export data', + 'sidebar_settings_users' => 'Users', + 'sidebar_settings_subscriptions' => 'Subscription', + 'sidebar_settings_import' => 'Import data', + 'sidebar_settings_tags' => 'Tag management', + 'sidebar_settings_api' => 'API', + 'sidebar_settings_dav' => 'DAV Resources', + 'sidebar_settings_security' => 'Security', + 'sidebar_settings_auditlogs' => 'Audit logs', + + 'title_general' => 'General Information', + 'title_i18n' => 'International settings', + 'title_layout' => 'Layout', + + 'me_title' => 'Me as a contact', + 'me_help' => 'This is the contact that represents you in Monica', + 'me_select' => 'Select a contact', + 'me_no_contact' => 'No contact selected yet.', + 'me_select_click' => 'Click here to select a contact.', + 'me_remove_contact' => 'Remove the association', + 'me_choose' => 'Choose yourself', + 'me_choose_placeholder' => 'Choose yourself', + + 'export_title' => 'Export your account data', + 'export_be_patient' => 'Click the button to start the export. It might take several minutes to process the export – please be patient and do not spam the button.', + 'export_title_sql' => 'Export to SQL', + 'export_sql_explanation' => 'Exporting your data in SQL format allows you to take your data and import it to your own Monica instance. This is only valuable if you do have your own server.', + 'export_sql_cta' => 'Export to SQL', + 'export_sql_link_instructions' => 'Note: read the instructions to learn more about importing this file to your instance.', + 'export_title_json' => 'Export to Json', + 'export_submitted' => 'Your export has been submitted, it will be available in a few moment…', + 'export_json_explanation' => 'Exporting your data in Json format for backup.', + 'export_json_beta' => 'Json export is in preview mode. Tell us what you think about it:', + 'export_json_cta' => 'Export to Json', + 'export_header_type' => 'Type', + 'export_header_timestamp' => 'Creation date', + 'export_header_status' => 'Status', + 'export_header_actions' => 'Actions', + 'export_last_title' => 'Last exports', + 'export_empty_title' => 'No exports yet', + 'export_type_json' => 'Json export', + 'export_type_sql' => 'SQL export', + 'export_status_todo' => 'Submitted', + 'export_status_doing' => 'Doing', + 'export_status_done' => 'Done', + 'export_status_failed' => 'Failed', + 'export_not_done' => 'Download impossible, this export is not done yet.', + + 'firstname' => 'First name', + 'lastname' => 'Last name', + 'name_order' => 'Name order', + 'name_order_firstname_lastname' => ' – John Doe', + 'name_order_lastname_firstname' => ' – Doe John', + 'name_order_firstname_lastname_nickname' => ' () – John Doe (Rambo)', + 'name_order_firstname_nickname_lastname' => ' () – John (Rambo) Doe', + 'name_order_lastname_firstname_nickname' => ' () – Doe John (Rambo)', + 'name_order_lastname_nickname_firstname' => ' () – Doe (Rambo) John', + 'name_order_nickname_firstname_lastname' => ' ( ) – Rambo (John Doe)', + 'name_order_nickname_lastname_firstname' => ' ( ) – Rambo (Doe John)', + 'name_order_nickname' => ' – Rambo', + 'currency' => 'Currency', + 'name' => 'Your name: :name', + 'email' => 'E-mail adresse', + 'email_placeholder' => 'Indtast e-mail', + 'email_help' => 'This is the email used to login, and this is where Monica will send your reminders.', + 'timezone' => 'Timezone', + 'temperature_scale' => 'Temperature scale', + 'temperature_scale_fahrenheit' => 'Fahrenheit (°F)', + 'temperature_scale_celsius' => 'Celsius (°C)', + 'layout' => 'Layout', + 'layout_small' => 'Maximum 1200 pixels wide', + 'layout_big' => 'Full width of the browser', + 'save' => 'Update preferences', + 'delete_title' => 'Delete your account', + 'delete_desc' => 'Do you wish to delete your account? Deletion is permanent and all of your data will be erased permanently. If you have a subscription, it will be cancelled immediately.', + 'delete_other_desc' => 'Your data in the main database will be deleted immediately. As described in our privacy policy, we carry out securely encrypted backups of the database every day. These backups are kept for 30 days after which they are completely deleted. We cannot delete specific data from the backups we hold any earlier than this. All of your data will be completely deleted no later than 31 days after your account’s deletion.', + 'reset_desc' => 'Do you wish to reset your account? This will remove all your contacts, and all of the data associated with them. Your account will not be deleted.', + 'reset_title' => 'Reset your account', + 'reset_cta' => 'Reset account', + 'reset_notice' => 'Are you sure to reset your account? This is permanent and cannot be undone.', + 'reset_success' => 'Your account has been reset successfully.', + 'delete_notice' => 'Are you sure you want to delete your account? This is permanent and cannot be undone. All of your data will be deleted and will not be recoverable.', + 'delete_cta' => 'Delete account', + 'settings_success' => 'Preferences updated!', + 'locale' => 'Language used in the app', + 'locale_help' => 'Do you want to help translating Monica or add a new language? Please follow this link for more information.', + 'locale_ar' => 'Arabic', + 'locale_cs' => 'Czech', + 'locale_de' => 'German', + 'locale_el' => 'Greek', + 'locale_en' => 'English', + 'locale_en-GB' => 'English (United Kingdom)', + 'locale_es' => 'Spanish', + 'locale_fr' => 'French', + 'locale_he' => 'Hebrew', + 'locale_hr' => 'Croatian', + 'locale_id' => 'Indonesian', + 'locale_it' => 'Italian', + 'locale_ja' => 'Japanese', + 'locale_nl' => 'Dutch', + 'locale_pt' => 'Portuguese', + 'locale_pt-BR' => 'Portuguese, Brazil', + 'locale_ru' => 'Russian', + 'locale_sv' => 'Swedish', + 'locale_vi' => 'Vietnamese', + 'locale_zh' => 'Chinese Simplified', + 'locale_zh-TW' => 'Chinese Traditional', + 'locale_tr' => 'Turkish', + + 'security_title' => 'Security', + 'security_help' => 'Change security matters for your account.', + 'password_change' => 'Change your password', + 'password_current' => 'Current password', + 'password_current_placeholder' => 'Enter your current password', + 'password_new1' => 'New password', + 'password_new1_placeholder' => 'Enter your new password', + 'password_new2' => 'Confirm your new password', + 'password_new2_placeholder' => 'Retype your new password', + 'password_btn' => 'Change password', + '2fa_title' => 'Two Factor Authentication', + '2fa_otp_title' => 'Two Factor Authentication mobile application', + '2fa_enable_title' => 'Enable Two Factor Authentication', + '2fa_enable_description' => 'Enable Two Factor Authentication to increase the security of your account.', + '2fa_enable_otp' => 'Open up your Two Factor Authentication mobile app and scan the following QR barcode:', + '2fa_enable_otp_help' => 'If your Two Factor Authentication mobile app does not support QR barcodes, enter in the following code:', + '2fa_enable_otp_validate' => 'Please validate the new device you’ve just set up:', + '2fa_enable_success' => 'Two Factor Authentication activated', + '2fa_enable_error' => 'Error when trying to activate Two Factor Authentication', + '2fa_enable_error_already_set' => 'Two Factor Authentication is already activated', + '2fa_disable_title' => 'Disable Two Factor Authentication', + '2fa_disable_description' => 'Disable Two Factor Authentication for your account. Be careful, your account will be much less secure!', + '2fa_disable_success' => 'Two Factor Authentication disabled', + '2fa_disable_error' => 'Error when trying to disable Two Factor Authentication', + + 'webauthn_title' => 'Security key — WebAuthn protocol', + 'webauthn_enable_description' => 'Add a new security key', + 'webauthn_key_name_help' => 'Give your key a name.', + 'webauthn_key_name' => 'Key name:', + 'webauthn_success' => 'Your key is detected and validated.', + 'webauthn_last_use' => 'Last use: {timestamp}', + 'webauthn_delete_confirmation' => 'Are you sure you want to delete this key?', + 'webauthn_delete_success' => 'Key deleted', + 'webauthn_insertKey' => 'Insert your security key.', + 'webauthn_buttonAdvise' => 'If your security key has a button, press it.', + 'webauthn_noButtonAdvise' => 'If it does not, remove it and insert it again.', + 'webauthn_not_supported' => 'Your browser doesn’t currently support WebAuthn.', + 'webauthn_not_secured' => 'WebAuthn only supports secure connections. Please load this page with https scheme.', + 'webauthn_error_already_used' => 'This key is already registered. It’s not necessary to register it again.', + 'webauthn_error_not_allowed' => 'The operation either timed out or was not allowed.', + + 'recovery_title' => 'Recovery codes', + 'recovery_show' => 'Get recovery codes', + 'recovery_copy_help' => 'Copy codes in your clipboard', + 'recovery_help_intro' => 'These are your recovery codes:', + 'recovery_help_information' => 'You can use each recovery code once.', + 'recovery_clipboard' => 'Codes copied to the clipboard.', + 'recovery_generate' => 'Generate new codes…', + 'recovery_generate_help' => 'Generating new codes will invalidate previously generated codes.', + 'recovery_already_used_help' => 'This code has already been used.', + + 'users_list_title' => 'Users with access to your account', + 'users_list_add_user' => 'Invite a new user', + 'users_list_you' => 'That’s you', + 'users_list_invitations_title' => 'Pending invitations', + 'users_list_invitations_explanation' => 'Below are the people you’ve invited to join Monica as a collaborator.', + 'users_list_invitations_invited_by' => 'invited by :name', + 'users_list_invitations_sent_date' => 'sent on :date', + 'users_blank_title' => 'You are the only one who has access to this account.', + 'users_blank_add_title' => 'Would you like to invite someone else?', + 'users_blank_description' => 'This person will have the same access that you have, and will be able to add, edit or delete contact information.', + 'users_blank_cta' => 'Invite someone', + 'users_add_title' => 'Invite a new user to your account by email', + 'users_add_description' => 'This person will have the same access as you do, including inviting or deleting other users, including you. Make sure you trust this person before giving them access.', + 'users_add_email_field' => 'Enter the email of the person you want to invite', + 'users_add_confirmation' => 'I confirm that I want to invite this user to my account. I understand that this person will have access to ALL of my data and see exactly what I see.', + 'users_add_cta' => 'Invitér bruger via e-mail', + 'users_accept_title' => 'Accept invitation and create a new account', + 'users_error_please_confirm' => 'Please confirm that you want to invite this user before proceeding with the invitation', + 'users_error_email_already_taken' => 'This email is already taken. Please choose another one', + 'users_error_already_invited' => 'You already have invited this user. Please choose another email address.', + 'users_error_email_not_similar' => 'This is not the email of the person who’ve invited you.', + 'users_invitation_deleted_confirmation_message' => 'The invitation has been successfully deleted', + 'users_invitations_delete_confirmation' => 'Are you sure you want to delete this invitation?', + 'users_list_delete_confirmation' => 'Are you sure to delete this user from your account?', + 'users_invitation_need_subscription' => 'Adding more users requires a subscription.', + + 'subscriptions_account_current_plan' => 'Your current plan', + 'subscriptions_account_current_legacy' => 'Current plan, not selectable anymore:', + 'subscriptions_account_current_paid_plan' => 'You are on the :name plan. Thanks so much for being a subscriber.', + + 'subscriptions_account_next_billing_title' => 'Next bill', + 'subscriptions_account_next_billing' => 'Your subscription will auto-renew on :date.', + 'subscriptions_account_bill_monthly' => 'We’ll bill you :price for another month.', + 'subscriptions_account_bill_annual' => 'We’ll bill you :price for another year.', + 'subscriptions_account_change' => 'Change plan', + + 'subscriptions_account_cancel_title' => 'Cancel subscription', + 'subscriptions_account_cancel_action' => 'Cancel subscription', + 'subscriptions_account_cancel' => 'You can cancel subscription anytime.', + 'subscriptions_account_free_plan' => 'You are on the free plan.', + 'subscriptions_account_free_plan_upgrade' => 'You can upgrade your account to the :name plan, which costs $:price per month. Here are the advantages:', + 'subscriptions_account_free_plan_benefits_users' => 'Unlimited number of users', + 'subscriptions_account_free_plan_benefits_reminders' => 'Reminders by email', + 'subscriptions_account_free_plan_benefits_import_data_vcard' => 'Import your contacts with vCard', + 'subscriptions_account_free_plan_benefits_support' => 'Support the project in the long run, so we can introduce more great features.', + 'subscriptions_account_upgrade' => 'Upgrade your account', + 'subscriptions_account_upgrade_title' => 'Upgrade Monica today and have more meaningful relationships.', + 'subscriptions_account_upgrade_choice' => 'Pick a plan below and join over :customers persons who upgraded their Monica.', + 'subscriptions_account_update_title' => 'Update Monica subscription', + 'subscriptions_account_update_description' => 'You can change your subscription’s frequency here.', + 'subscriptions_account_update_information' => 'You will be billed immediately for the new amount. Your subscription will extend to the new period, depending on your choice.', + 'subscriptions_account_invoices' => 'Invoices', + 'subscriptions_account_invoices_download' => 'Download', + 'subscriptions_account_invoices_subscription' => 'Subscription from :startDate to :endDate', + 'subscriptions_account_payment' => 'Which payment option fits you best?', + 'subscriptions_account_confirm_payment' => 'Your payment is currently incomplete, please confirm your payment.', + 'subscriptions_downgrade_title' => 'Downgrade your account to the free plan', + 'subscriptions_downgrade_limitations' => 'The free plan has limitations. In order to be able to downgrade, you need to pass the checklist below:', + 'subscriptions_downgrade_rule_users' => 'You must have only 1 user in your account', + 'subscriptions_downgrade_rule_users_constraint' => 'You currently have 1 user in your account.|You currently have :count users in your account.', + 'subscriptions_downgrade_rule_invitations' => 'You must not have any pending invitations', + 'subscriptions_downgrade_rule_invitations_constraint' => 'You currently have 1 pending invitation.|You currently have :count pending invitations.', + 'subscriptions_downgrade_rule_contacts' => 'You must not have more than :number active contacts', + 'subscriptions_downgrade_rule_contacts_constraint' => 'You currently have 1 contact.|You currently have :count contacts.', + 'subscriptions_downgrade_rule_contacts_archive' => 'We can also archive all your contacts for you – that would clear this rule and let you proceed with your account’s downgrade process.', + 'subscriptions_downgrade_cta' => 'Downgrade', + 'subscriptions_downgrade_success' => 'You are back to the Free plan!', + 'subscriptions_downgrade_thanks' => 'Thanks so much for trying the paid plan. We keep adding new features on Monica all the time – so you might want to come back in the future to see if you might be interested in taking a subscription again.', + 'subscriptions_back' => 'Back to settings', + 'subscriptions_upgrade_title' => 'Upgrade your account', + 'subscriptions_upgrade_choose' => 'You picked the :plan plan.', + 'subscriptions_upgrade_infos' => 'We couldn’t be happier. Enter your payment info below.', + 'subscriptions_upgrade_name' => 'Name on card', + 'subscriptions_upgrade_zip' => 'ZIP or postal code', + 'subscriptions_upgrade_credit' => 'Credit or debit card', + 'subscriptions_upgrade_submit' => 'Pay {amount}', + 'subscriptions_upgrade_charge' => 'We’ll charge your card :price now. The next charge will be on :date. If you ever change your mind, you can cancel at any time, no questions asked.', + 'subscriptions_upgrade_charge_handled' => 'The payment is handled by Stripe. No card information touches our server.', + 'subscriptions_upgrade_success' => 'Thank you! You are now subscribed.', + 'subscriptions_upgrade_thanks' => 'Welcome to the community of people who try to make the world a better place.', + + 'subscriptions_payment_confirm_title' => 'Confirm your :amount payment', + 'subscriptions_payment_confirm_information' => 'Extra confirmation is needed to process your payment. Please confirm your payment by filling out your payment details below.', + 'subscriptions_payment_succeeded_title' => 'Payment Successful', + 'subscriptions_payment_succeeded' => 'This payment was already successfully confirmed.', + 'subscriptions_payment_cancelled_title' => 'Payment Cancelled', + 'subscriptions_payment_cancelled' => 'This payment was cancelled.', + 'subscriptions_payment_error_name' => 'Please provide your name.', + 'subscriptions_payment_success' => 'The payment was successful.', + + 'subscriptions_pdf_title' => 'Your :name monthly subscription', + 'subscriptions_plan_frequency_year' => ':amount / year', + 'subscriptions_plan_frequency_month' => ':amount / month', + 'subscriptions_plan_choose' => 'Choose this plan', + 'subscriptions_plan_year_title' => 'Pay annually', + 'subscriptions_plan_year_bonus' => 'Peace of mind for a whole year', + 'subscriptions_plan_month_title' => 'Pay monthly', + 'subscriptions_plan_month_bonus' => 'Cancel any time', + 'subscriptions_plan_include1' => 'Included with your upgrade:', + 'subscriptions_plan_include2' => 'Unlimited number of contacts • Unlimited number of users • Reminders by email • Import with vCard • Personalization of the contact sheet', + 'subscriptions_plan_include3' => '100% of the profits go the development of this great open source project.', + 'subscriptions_help_title' => 'Additional details you may be curious about', + 'subscriptions_help_opensource_title' => 'What is an open source project?', + 'subscriptions_help_opensource_desc' => 'Monica is an open source project. This means it is built by a community who wants to build a great tool for the greater good. Being open source means the code is publicly available on GitHub, and everyone can inspect it, modify it or enhance it. All the money we raise is dedicated to building better features, paying for more powerful servers, and paying other costs. Thanks for your help. We couldn’t do it without you.', + 'subscriptions_help_limits_title' => 'Is there a limit to the number of contacts we can have on the free plan?', + 'subscriptions_help_limits_plan' => 'Yes. Free plans let you manage :number contacts.', + 'subscriptions_help_discounts_title' => 'Do you have discounts for non-profits and education?', + 'subscriptions_help_discounts_desc' => 'We do! Monica is free for students, and free for non-profits and charities. Just contact the support with a proof of your status and we’ll apply this special status in your account.', + 'subscriptions_help_change_title' => 'What if I change my mind?', + 'subscriptions_help_change_desc' => 'You can cancel anytime, no questions asked, and all by yourself – no need to contact support. However, you will not be refunded for the current period.', + + 'stripe_error_card' => 'Your card was declined. Decline message is: :message', + 'stripe_error_api_connection' => 'Network communication with Stripe failed. Try again later.', + 'stripe_error_rate_limit' => 'Too many requests with Stripe right now. Try again later.', + 'stripe_error_invalid_request' => 'Invalid parameters. Try again later.', + 'stripe_error_authentication' => 'Wrong authentication with Stripe', + + 'import_title' => 'Import contacts in your account', + 'import_cta' => 'Upload contacts', + 'import_stat' => 'You’ve imported :number files so far.', + 'import_result_stat' => 'Uploaded vCard with 1 contact (:total_imported imported, :total_skipped skipped)|Uploaded vCard with :total_contacts contacts (:total_imported imported, :total_skipped skipped)', + 'import_view_report' => 'View report', + 'import_in_progress' => 'The import is in progress. Reload the page in one minute.', + 'import_upload_title' => 'Import your contacts from a vCard file', + 'import_upload_rules_desc' => 'We do however have some rules:', + 'import_upload_rule_format' => 'We support .vcard and .vcf files.', + 'import_upload_rule_vcard' => 'We support the vCard 3.0 format, which is the default format for macOS’s Contacts.app and Google Contacts.', + 'import_upload_rule_instructions' => 'Export instructions for macOS Contacts.app and Google Contacts.', + 'import_upload_rule_multiple' => 'If your contacts have multiple email addresses or phone numbers, only the first entry will be saved.', + 'import_upload_rule_limit' => 'Files are limited to 10 MB.', + 'import_upload_rule_time' => 'It might take up to a minute to upload the contacts and process them. Please be patient.', + 'import_upload_rule_cant_revert' => 'Please make sure data is accurate before uploading, as you can’t undo the upload.', + 'import_upload_form_file' => 'Your .vcf or .vCard file:', + 'import_upload_behaviour' => 'Import behaviour:', + 'import_upload_behaviour_add' => 'Add new contacts and skip existing', + 'import_upload_behaviour_replace' => 'Replace existing contacts', + 'import_upload_behaviour_help' => 'Replacing will replace all data found in the vCard, but will keep existing contact fields.', + 'import_report_title' => 'Importing report', + 'import_report_date' => 'Date of the import', + 'import_report_type' => 'Type of import', + 'import_report_number_contacts' => 'Number of contacts in the file', + 'import_report_number_contacts_imported' => 'Number of imported contacts', + 'import_report_number_contacts_skipped' => 'Number of skipped contacts', + 'import_report_status_imported' => 'Imported', + 'import_report_status_skipped' => 'Skipped', + 'import_vcard_parse_error' => 'Error when parsing the vCard entry', + 'import_vcard_contact_exist' => 'Contact already exists', + 'import_vcard_contact_no_firstname' => 'No first name (mandatory)', + 'import_vcard_file_not_found' => 'File not found', + 'import_vcard_unknown_entry' => 'Unknown contact name', + 'import_vcard_file_no_entries' => 'File contains no entries', + 'import_blank_title' => 'You haven’t imported any contacts yet.', + 'import_blank_question' => 'Would you like to import contacts now?', + 'import_blank_description' => 'We can import vCard files that you can get from Google Contacts or your Contact manager.', + 'import_blank_cta' => 'Import vCard', + 'import_need_subscription' => 'Importing data requires a subscription.', + + 'tags_list_title' => 'Tags', + 'tags_list_description' => 'You can organize your contacts by setting up tags. Tags work like folders, but you can add more than one tag to a contact. To add a new tag, add it on the contact itself.', + 'tags_list_contact_number' => '1 contact|:count contacts', + 'tags_list_delete_success' => 'The tag has been successfully deleted', + 'tags_list_edit_success' => 'The tag has been successfully updated', + 'tags_list_delete_confirmation' => 'Are you sure you want to delete the tag? No contacts will be deleted, only the tag.', + 'tags_blank_title' => 'Tags are a great way of categorizing your contacts.', + 'tags_blank_description' => 'Tags work like folders, but you can add more than one tag to a contact. Go to a contact and tag a friend, right below the name. Once a contact is tagged, come back here to manage all the tags in your account.', + + 'api_title' => 'API access', + 'api_description' => 'The API can be used to manipulate Monica’s data from an external application, like a mobile application for instance.', + 'api_help' => 'To use the API, a token is mandatory. You can either create a personal access token (Bearer authentication), or authorize an OAuth client to create it for you. See API documentation.', + 'api_endpoint' => 'The API endpoint for this Monica instance is:', + + 'api_personal_access_tokens' => 'Personal access tokens', + 'api_pao_description' => 'Make sure you give this token to a source you trust – as they allow you to access all your data.', + 'api_token_title' => 'Personal Access Tokens', + 'api_token_create_new' => 'Create New Token', + 'api_token_not_created' => 'You have not created any personal access tokens.', + 'api_token_name' => 'Token name', + 'api_token_expire' => 'Expires at {date}', + 'api_token_delete' => 'Delete', + 'api_token_create' => 'Create Token', + 'api_token_scopes' => 'Scopes', + 'api_token_help' => 'Here is your new personal access token. This is the only time it will be shown so don’t lose it! You may now use this token to make API requests.', + + 'api_oauth_clients' => 'Your OAuth clients', + 'api_oauth_clients_desc' => 'This section lets you register your own OAuth clients.', + 'api_oauth_clients_desc2' => 'Use this client id to request a new token, and convert authorization codes to access tokens. See Laravel Passport documentation for more information.', + 'api_oauth_title' => 'OAuth Clients', + 'api_oauth_create_new' => 'Create New Client', + 'api_oauth_edit' => 'Edit Client', + 'api_oauth_not_created' => 'You have not created any OAuth clients.', + 'api_oauth_clientid' => 'Client ID', + 'api_oauth_name' => 'Name', + 'api_oauth_name_help' => 'Something your users will recognize and trust.', + 'api_oauth_secret' => 'Secret', + 'api_oauth_create' => 'Create Client', + 'api_oauth_redirecturl' => 'Redirect URL', + 'api_oauth_redirecturl_help' => 'Your application’s authorization callback URL.', + + 'api_authorized_clients' => 'List of authorized clients', + 'api_authorized_clients_desc' => 'This section lists all the clients you’ve authorized to access your application data. You can revoke this authorization at anytime.', + 'api_authorized_clients_title' => 'Authorized Applications', + 'api_authorized_clients_none' => 'There are no authorized clients yet.', + 'api_authorized_clients_name' => 'Name', + 'api_authorized_clients_scopes' => 'Scopes', + + 'personalization_tab_title' => 'Personalize your account', + + 'personalization_title' => 'Here you will find different settings to configure your account. These features are intended for “power users” who want maximum control over Monica.', + 'personalization_contact_field_type_title' => 'Contact field types', + 'personalization_contact_field_type_add' => 'Add new field type', + 'personalization_contact_field_type_description' => 'You can configure all the different types of contact fields that you can associate to all your contacts. For example, if a new social network appears in the future, you will be able to add this new way of communicating with your contacts right here.', + 'personalization_contact_field_type_table_name' => 'Name', + 'personalization_contact_field_type_table_protocol' => 'Protocol', + 'personalization_contact_field_type_table_actions' => 'Actions', + 'personalization_contact_field_type_modal_title' => 'Add a new contact field type', + 'personalization_contact_field_type_modal_edit_title' => 'Edit an existing contact field type', + 'personalization_contact_field_type_modal_delete_title' => 'Delete an existing contact field type', + 'personalization_contact_field_type_modal_delete_description' => 'Are you sure you want to delete this contact field type? Deleting this type of contact field will delete ALL the data with this type for all of your contacts.', + 'personalization_contact_field_type_modal_name' => 'Name', + 'personalization_contact_field_type_modal_protocol' => 'Protocol (optional)', + 'personalization_contact_field_type_modal_protocol_help' => 'Each new contact field type can be clickable. If a protocol is set, we will use it to trigger the action that is set.', + 'personalization_contact_field_type_modal_icon' => 'Icon (optional)', + 'personalization_contact_field_type_modal_icon_help' => 'You can associate an icon with this contact field type. You need to add a reference to a Font Awesome icon.', + 'personalization_contact_field_type_delete_success' => 'The contact field type has been successfully deleted.', + 'personalization_contact_field_type_add_success' => 'The contact field type has been successfully added.', + 'personalization_contact_field_type_edit_success' => 'The contact field type has been successfully updated.', + + 'personalization_genders_title' => 'Gender types', + 'personalization_genders_add' => 'Add new gender type', + 'personalization_genders_desc' => 'You can define as many genders as you need to. You need at least one gender type in your account.', + 'personalization_genders_modal_add' => 'Add gender type', + 'personalization_genders_modal_edit' => 'Update gender type', + 'personalization_genders_modal_name' => 'Name', + 'personalization_genders_modal_name_help' => 'The name used to display the gender on a contact page.', + 'personalization_genders_modal_sex' => 'Sex', + 'personalization_genders_modal_sex_help' => 'Used to define the relationships, and during the VCard import/export process.', + 'personalization_genders_modal_default' => 'Select the default gender for a new contact', + 'personalization_genders_modal_delete' => 'Delete gender type', + 'personalization_genders_modal_delete_desc' => 'Are you sure you want to delete the gender “{name}”?', + 'personalization_genders_modal_delete_question' => 'You currently have {count} contact with this gender. If you delete this gender, what gender should this contact have?|You currently have {count} contacts with this gender. If you delete this gender, what gender should these contacts have?', + 'personalization_genders_modal_delete_question_default' => 'This gender is the default one. If you delete this gender, which one will be the new default?', + 'personalization_genders_modal_error' => 'Please choose a gender from the list.', + 'personalization_genders_list_contact_number' => '{count} contact|{count} contacts', + 'personalization_genders_table_name' => 'Name', + 'personalization_genders_table_sex' => 'Sex', + 'personalization_genders_table_default' => 'Default', + 'personalization_genders_default' => 'Default gender', + 'personalization_genders_make_default' => 'Change default gender', + 'personalization_genders_select_default' => 'Select default gender', + 'personalization_genders_m' => 'Male', + 'personalization_genders_f' => 'Female', + 'personalization_genders_o' => 'Other', + 'personalization_genders_u' => 'Unknown', + 'personalization_genders_n' => 'None or not applicable', + + 'personalization_reminder_rule_save' => 'The change has been saved', + 'personalization_reminder_rule_title' => 'Reminder rules', + 'personalization_reminder_rule_line' => '{count} day before|{count} days before', + 'personalization_reminder_rule_desc' => 'For every reminder that you set, Monica can send you an email a number of days before the event happens. You can adjust these notification settings here. These notifications only apply to monthly and yearly reminders.', + + 'personalization_module_save' => 'The change has been saved', + 'personalization_module_title' => 'Features', + 'personalization_module_desc' => 'You may not need all of Monica’s features. Below you can toggle specific features that are used on a contact sheet. This change will affect ALL your contacts. Turning off a feature does not delete any data, it simply hides the feature.', + + 'personalisation_paid_upgrade' => 'This is a premium feature that requires a Paid subscription to be active. Upgrade your account by visiting Settings > Subscription.', + 'personalisation_paid_upgrade_vue' => 'This is a premium feature that requires a Paid subscription to be active. Upgrade your account by visiting Settings > Subscription.', + + 'reminder_time_to_send' => 'Time of the day reminders will be sent', + 'reminder_time_to_send_help' => 'Your next reminder is scheduled to be sent on {dateTime}.', + + 'personalization_activity_type_category_title' => 'Activity type categories', + 'personalization_activity_type_category_add' => 'Add a new activity type category', + 'personalization_activity_type_category_table_name' => 'Name', + 'personalization_activity_type_category_description' => 'An activity with one of your contacts can have a type and a category type. Your account comes with a set of predefined category types by default, but you can customize these here.', + 'personalization_activity_type_category_table_actions' => 'Actions', + 'personalization_activity_type_category_modal_add' => 'Add a new activity type category', + 'personalization_activity_type_category_modal_edit' => 'Edit an activity type category', + 'personalization_activity_type_category_modal_question' => 'What should we name this new category?', + 'personalization_activity_type_add_button' => 'Add a new activity type', + 'personalization_activity_type_modal_add' => 'Add a new activity type', + 'personalization_activity_type_modal_question' => 'What should we name this new activity type?', + 'personalization_activity_type_modal_edit' => 'Edit an activity type', + 'personalization_activity_type_category_modal_delete' => 'Delete an activity type category', + 'personalization_activity_type_category_modal_delete_desc' => 'Are you sure you want to delete this category? Deleting it will delete all associated activity types. Activities that belong to this category will not be affected by this deletion.', + 'personalization_activity_type_modal_delete' => 'Delete an activity type', + 'personalization_activity_type_modal_delete_desc' => 'Are you sure you want to delete this activity type? Activities that belong to this category will not be affected by this deletion.', + 'personalization_activity_type_modal_delete_error' => 'We can’t find this activity type.', + 'personalization_activity_type_category_modal_delete_error' => 'We can’t find this activity type category.', + + 'personalization_life_event_category_title' => 'Life event categories', + 'personalization_live_event_category_table_name' => 'Name', + 'personalization_life_event_category_description' => 'A life event can have a type and a category. Your account comes with a set of predefined categories and types by default, but you can customize life event types here.', + 'personalization_live_event_category_table_actions' => 'Actions', + 'personalization_life_event_type_add_button' => 'Add a new life event type', + 'personalization_life_event_type_modal_add' => 'Add a new life event type', + 'personalization_life_event_type_modal_question' => 'What should we name this new life event type?', + 'personalization_life_event_type_modal_edit' => 'Edit a life event type', + 'personalization_life_event_type_modal_delete' => 'Delete a life event type', + 'personalization_life_event_type_modal_delete_desc' => 'Are you sure you want to delete this life event type? Life events that belong to this type will be deleted by performing this action.', + 'personalization_life_event_type_modal_delete_error' => 'We can’t find this life event type.', + + 'personalization_life_event_category_work_education' => 'Work & education', + 'personalization_life_event_category_family_relationships' => 'Family & relationships', + 'personalization_life_event_category_home_living' => 'Home & living', + 'personalization_life_event_category_travel_experiences' => 'Travel & experiences', + 'personalization_life_event_category_health_wellness' => 'Health & wellness', + + 'personalization_life_event_type_new_job' => 'New job', + 'personalization_life_event_type_retirement' => 'Retirement', + 'personalization_life_event_type_new_school' => 'New school', + 'personalization_life_event_type_study_abroad' => 'Study abroad', + 'personalization_life_event_type_volunteer_work' => 'Volunteer work', + 'personalization_life_event_type_published_book_or_paper' => 'Published a book or paper', + 'personalization_life_event_type_military_service' => 'Military service', + 'personalization_life_event_type_first_met' => 'First met', + 'personalization_life_event_type_new_relationship' => 'New relationship', + 'personalization_life_event_type_engagement' => 'Engagement', + 'personalization_life_event_type_marriage' => 'Marriage', + 'personalization_life_event_type_anniversary' => 'Anniversary', + 'personalization_life_event_type_expecting_a_baby' => 'Expecting a baby', + 'personalization_life_event_type_new_child' => 'New child', + 'personalization_life_event_type_new_family_member' => 'New family member', + 'personalization_life_event_type_new_pet' => 'New pet', + 'personalization_life_event_type_end_of_relationship' => 'End of relationship', + 'personalization_life_event_type_loss_of_a_loved_one' => 'Loss of a loved one', + 'personalization_life_event_type_moved' => 'Moved', + 'personalization_life_event_type_bought_a_home' => 'Bought a home', + 'personalization_life_event_type_home_improvement' => 'Home improvement', + 'personalization_life_event_type_holidays' => 'Holidays', + 'personalization_life_event_type_new_vehicle' => 'New vehicle', + 'personalization_life_event_type_new_roommate' => 'New roommate', + 'personalization_life_event_type_overcame_an_illness' => 'Overcame an illness', + 'personalization_life_event_type_quit_a_habit' => 'Quit a habit', + 'personalization_life_event_type_new_eating_habits' => 'New eating habits', + 'personalization_life_event_type_weight_loss' => 'Weight loss', + 'personalization_life_event_type_wear_glass_or_contact' => 'Started wearing glasses or contacts', + 'personalization_life_event_type_broken_bone' => 'Broke a bone', + 'personalization_life_event_type_removed_braces' => 'Had braces removed', + 'personalization_life_event_type_surgery' => 'Had surgery', + 'personalization_life_event_type_dentist' => 'Had dental treatment', + 'personalization_life_event_type_new_sport' => 'Started playing a new sport', + 'personalization_life_event_type_new_hobby' => 'Took up a new hobby', + 'personalization_life_event_type_new_instrument' => 'Started learning a new instrument', + 'personalization_life_event_type_new_language' => 'Started learning a new language', + 'personalization_life_event_type_tattoo_or_piercing' => 'Tattoo or piercing', + 'personalization_life_event_type_new_license' => 'New license', + 'personalization_life_event_type_travel' => 'Travel', + 'personalization_life_event_type_achievement_or_award' => 'Achievement or award', + 'personalization_life_event_type_changed_beliefs' => 'Changed beliefs', + 'personalization_life_event_type_first_word' => 'First word', + 'personalization_life_event_type_first_kiss' => 'First kiss', + + 'storage_title' => 'Storage', + 'storage_account_info' => 'Your account limit is :accountLimit MB. Your current usage is :currentAccountSize MB (about :percentUsage%).', + 'storage_upgrade_notice' => 'Upgrade your account to be able to upload documents and photos.', + 'storage_description' => 'Here you can see all the documents and photos uploaded about your contacts.', + + 'dav_title' => 'WebDAV', + 'dav_description' => 'Here you can find all settings to use WebDAV resources for CardDAV and CalDAV exports.', + 'dav_copy_help' => 'Copy into your clipboard', + 'dav_clipboard_copied' => 'Value copied into your clipboard', + 'dav_url_base' => 'Base url for all CardDAV and CalDAV resources:', + 'dav_connect_help' => 'You can connect your contacts and/or calendars with this base url on you phone or computer.', + 'dav_connect_help2' => 'Use your login (email) and create an API token as the password to authenticate.', + 'dav_url_carddav' => 'CardDAV url for Contacts resource:', + 'dav_url_caldav_birthdays' => 'CalDAV url for Birthdays resources:', + 'dav_url_caldav_tasks' => 'CalDAV url for Tasks resources:', + 'dav_title_carddav' => 'CardDAV', + 'dav_title_caldav' => 'CalDAV', + 'dav_carddav_export' => 'Export all contacts in one file', + 'dav_caldav_birthdays_export' => 'Export all birthdays in one file', + 'dav_caldav_tasks_export' => 'Export all tasks in one file', + + 'archive_title' => 'Archive all of the contacts in your account', + 'archive_desc' => 'This will archive all of the contacts in your account.', + 'archive_cta' => 'Archive all of your contacts', + + 'logs_title' => 'Everything that has happened to this account', + 'logs_actor' => 'Actor', + 'logs_timestamp' => 'Timestamp', + 'logs_description' => 'Description', + 'logs_subject' => 'Subject', + 'logs_size' => 'Size (Kb)', + 'logs_object' => 'Object', +]; diff --git a/resources/lang/da/validation.php b/resources/lang/da/validation.php new file mode 100644 index 0000000..0153365 --- /dev/null +++ b/resources/lang/da/validation.php @@ -0,0 +1,166 @@ + 'The :attribute must be accepted.', + 'active_url' => 'The :attribute is not a valid URL.', + 'after' => 'The :attribute must be a date after :date.', + 'after_or_equal' => 'The :attribute must be a date after or equal to :date.', + 'alpha' => 'The :attribute may only contain letters.', + 'alpha_dash' => 'The :attribute may only contain letters, numbers, dashes and underscores.', + 'alpha_num' => 'The :attribute may only contain letters and numbers.', + 'array' => 'The :attribute must be an array.', + 'before' => 'The :attribute must be a date before :date.', + 'before_or_equal' => 'The :attribute must be a date before or equal to :date.', + 'between' => [ + 'numeric' => 'The :attribute must be between :min and :max.', + 'file' => 'The :attribute must be between :min and :max kilobytes.', + 'string' => 'The :attribute must be between :min and :max characters.', + 'array' => 'The :attribute must have between :min and :max items.', + ], + 'boolean' => 'The :attribute field must be true or false.', + 'confirmed' => 'The :attribute confirmation does not match.', + 'date' => 'The :attribute is not a valid date.', + 'date_equals' => 'The :attribute must be a date equal to :date.', + 'date_format' => 'The :attribute does not match the format :format.', + 'different' => 'The :attribute and :other must be different.', + 'digits' => 'The :attribute must be :digits digits.', + 'digits_between' => 'The :attribute must be between :min and :max digits.', + 'dimensions' => 'The :attribute has invalid image dimensions.', + 'distinct' => 'The :attribute field has a duplicate value.', + 'email' => 'The :attribute must be a valid email address.', + 'ends_with' => 'The :attribute must end with one of the following: :values.', + 'exists' => 'The selected :attribute is invalid.', + 'file' => 'The :attribute must be a file.', + 'filled' => 'The :attribute field must have a value.', + 'gt' => [ + 'numeric' => 'The :attribute must be greater than :value.', + 'file' => 'The :attribute must be greater than :value kilobytes.', + 'string' => 'The :attribute must be greater than :value characters.', + 'array' => 'The :attribute must have more than :value items.', + ], + 'gte' => [ + 'numeric' => 'The :attribute must be greater than or equal :value.', + 'file' => 'The :attribute must be greater than or equal :value kilobytes.', + 'string' => 'The :attribute must be greater than or equal :value characters.', + 'array' => 'The :attribute must have :value items or more.', + ], + 'image' => 'The :attribute must be an image.', + 'in' => 'The selected :attribute is invalid.', + 'in_array' => 'The :attribute field does not exist in :other.', + 'integer' => 'The :attribute must be an integer.', + 'ip' => 'The :attribute must be a valid IP address.', + 'ipv4' => 'The :attribute must be a valid IPv4 address.', + 'ipv6' => 'The :attribute must be a valid IPv6 address.', + 'json' => 'The :attribute must be a valid JSON string.', + 'lt' => [ + 'numeric' => 'The :attribute must be less than :value.', + 'file' => 'The :attribute must be less than :value kilobytes.', + 'string' => 'The :attribute must be less than :value characters.', + 'array' => 'The :attribute must have less than :value items.', + ], + 'lte' => [ + 'numeric' => 'The :attribute must be less than or equal :value.', + 'file' => 'The :attribute must be less than or equal :value kilobytes.', + 'string' => 'The :attribute must be less than or equal :value characters.', + 'array' => 'The :attribute must not have more than :value items.', + ], + 'max' => [ + 'numeric' => 'The :attribute may not be greater than :max.', + 'file' => 'The :attribute may not be greater than :max kilobytes.', + 'string' => 'The :attribute may not be greater than :max characters.', + 'array' => 'The :attribute may not have more than :max items.', + ], + 'mimes' => 'The :attribute must be a file of type: :values.', + 'mimetypes' => 'The :attribute must be a file of type: :values.', + 'min' => [ + 'numeric' => 'The :attribute must be at least :min.', + 'file' => 'The :attribute must be at least :min kilobytes.', + 'string' => 'The :attribute must be at least :min characters.', + 'array' => 'The :attribute must have at least :min items.', + ], + 'not_in' => 'The selected :attribute is invalid.', + 'not_regex' => 'The :attribute format is invalid.', + 'numeric' => 'The :attribute must be a number.', + 'password' => 'The password is incorrect.', + 'present' => 'The :attribute field must be present.', + 'regex' => 'The :attribute format is invalid.', + 'required' => 'The :attribute field is required.', + 'required_if' => 'The :attribute field is required when :other is :value.', + 'required_unless' => 'The :attribute field is required unless :other is in :values.', + 'required_with' => 'The :attribute field is required when :values is present.', + 'required_with_all' => 'The :attribute field is required when :values are present.', + 'required_without' => 'The :attribute field is required when :values is not present.', + 'required_without_all' => 'The :attribute field is required when none of :values are present.', + 'same' => 'The :attribute and :other must match.', + 'size' => [ + 'numeric' => 'The :attribute must be :size.', + 'file' => 'The :attribute must be :size kilobytes.', + 'string' => 'The :attribute must be :size characters.', + 'array' => 'The :attribute must contain :size items.', + ], + 'starts_with' => 'The :attribute must start with one of the following: :values.', + 'string' => 'The :attribute must be a string.', + 'timezone' => 'The :attribute must be a valid zone.', + 'unique' => 'The :attribute has already been taken.', + 'uploaded' => 'The :attribute failed to upload.', + 'url' => 'The :attribute format is invalid.', + 'uuid' => 'The :attribute must be a valid UUID.', + + /* + |-------------------------------------------------------------------------- + | Custom Validation Language Lines + |-------------------------------------------------------------------------- + | + | Here you may specify custom validation messages for attributes using the + | convention "attribute.rule" to name the lines. This makes it quick to + | specify a specific custom language line for a given attribute rule. + | + */ + + 'custom' => [ + 'attribute-name' => [ + 'rule-name' => 'custom-message', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Custom Validation Attributes + |-------------------------------------------------------------------------- + | + | The following language lines are used to swap our attribute placeholder + | with something more reader friendly such as "E-Mail Address" instead + | of "email". This simply helps us make our message more expressive. + | + */ + + 'attributes' => [], + + 'vue' => [ + 'max' => [ + 'numeric' => '{field} may not be greater than {max}.', + 'string' => '{field} may not be greater than {max} characters.', + ], + 'required' => '{field} is required.', + 'url' => '{field} is not a valid URL.', + ], + +]; diff --git a/resources/lang/de.json b/resources/lang/de.json new file mode 100644 index 0000000..7afa9ba --- /dev/null +++ b/resources/lang/de.json @@ -0,0 +1,7 @@ +{ + "The :attribute must contain at least one uppercase and one lowercase letter.": "Das :attribute muss aus mindestens einem Groß- und einem Kleinbuchstaben bestehen.", + "The :attribute must contain at least one letter.": "Das :attribute muss aus mindestens einem Zeichen bestehen.", + "The :attribute must contain at least one symbol.": "Das :attribute muss aus mindestens einem Sonderzeichen bestehen.", + "The :attribute must contain at least one number.": "Das :attribute muss aus mindestens einer Zahl bestehen.", + "The given :attribute has appeared in a data leak. Please choose a different :attribute.": "Das :attribute ist bereits in einem Datenleck aufgetaucht. Bitte wähle ein anderes :attribute." +} diff --git a/resources/lang/de/app.php b/resources/lang/de/app.php new file mode 100644 index 0000000..e88c42c --- /dev/null +++ b/resources/lang/de/app.php @@ -0,0 +1,571 @@ + 'Ja', + 'no' => 'Nein', + 'update' => 'Aktualisieren', + 'save' => 'Speichern', + 'add' => 'Hinzufügen', + 'cancel' => 'Abbrechen', + 'confirm' => 'Bestätigen', + 'delete_confirm' => 'Bist du dir sicher?', + 'delete' => 'Löschen', + 'edit' => 'Bearbeiten', + 'upload' => 'Hochladen', + 'download' => 'Herunterladen', + 'save_close' => 'Speichern und schließen', + 'close' => 'Schließen', + 'copy' => 'kopieren', + 'create' => 'Erstellen', + 'remove' => 'Entfernen', + 'revoke' => 'Aufheben', + 'done' => 'Fertig', + 'back' => 'Zurück', + 'verify' => 'Überprüfe', + 'new' => 'Neu', + 'unknown' => 'Ich weiß es nicht', + 'load_more' => 'Lade mehr', + 'loading' => 'Lädt…', + 'with' => 'mit', + 'today' => 'heute', + 'yesterday' => 'gestern', + 'another_day' => 'anderen Tag', + 'date' => 'Datum', + 'type' => 'Typ', + 'zoom' => 'vergrößern', + 'upgrade' => 'Zum Freischalten aktualisieren', + 'percent_uploaded' => '{percent}% hochgeladen', + 'retry' => 'Wiederholen', + 'filter' => 'Liste filtern', + 'go_back' => 'Zurück', + 'file_selected' => 'Eine Datei ausgewählt…|{count} Dateien ausgewählt…', + + 'application_title' => 'Monica – persönlicher Beziehungsmanager', + 'application_description' => 'Monica ist ein Werkzeug, um Ihre Interaktionen mit Ihren Lieben, Freunden und Familie zu verwalten.', + 'application_og_title' => 'Bessere Beziehungen zu deinen Liebsten. Kostenloses Online CRM für Freunde und Familie.', + + 'markdown_description' => 'Du möchtest deinen Text schöner formatieren? Monica unterstützt Markdown.', + 'markdown_link' => 'Öffne die Dokumentation', + + 'header_settings_link' => 'Einstellungen', + 'header_logout_link' => 'Ausloggen', + 'header_changelog_link' => 'Produktänderungen', + + 'main_nav_cta' => 'Person hinzufügen', + 'main_nav_dashboard' => 'Dashboard', + 'main_nav_family' => 'Personen', + 'main_nav_journal' => 'Tagebuch', + 'main_nav_activities' => 'Aktivitäten', + 'main_nav_tasks' => 'Aufgaben', + + 'footer_remarks' => 'Anmerkungen?', + 'footer_send_email' => 'Schreib uns eine E-Mail', + 'footer_privacy' => 'Datenschutzrichtlinie', + 'footer_release' => 'Versionshinweise', + 'footer_newsletter' => 'Newsletter', + 'footer_source_code' => 'Monica bei GitHub', + 'footer_version' => 'Version: :version', + 'footer_new_version' => 'Eine neue Version von Monica ist verfügbar', + + 'footer_modal_version_whats_new' => 'Was gibt\'s Neues', + 'footer_modal_version_release_away' => 'Du bist ein Release hinter der neuesten verfügbaren Version. Du solltest deine Installation updaten.|Du bist :number Releases hinter der neuesten verfügbaren Version. Du solltest deine Installation updaten.', + + 'breadcrumb_dashboard' => 'Dashboard', + 'breadcrumb_list_contacts' => 'Kontaktliste', + 'breadcrumb_archived_contacts' => 'Gespeicherte Kontakte', + 'breadcrumb_journal' => 'Tagebuch', + 'breadcrumb_settings' => 'Einstellungen', + 'breadcrumb_settings_export' => 'Export', + 'breadcrumb_settings_users' => 'Benutzer', + 'breadcrumb_settings_users_add' => 'Benutzer hinzufügen', + 'breadcrumb_settings_subscriptions' => 'Abonnement', + 'breadcrumb_settings_import' => 'Import', + 'breadcrumb_settings_import_report' => 'Import-Bericht', + 'breadcrumb_settings_import_upload' => 'Hochladen', + 'breadcrumb_settings_tags' => 'Markierungen', + 'breadcrumb_add_significant_other' => 'Lebensgefährte hinzufügen', + 'breadcrumb_edit_significant_other' => 'Lebensgefährte bearbeiten', + 'breadcrumb_add_note' => 'Notiz hinzufügen', + 'breadcrumb_edit_note' => 'Notiz bearbeiten', + 'breadcrumb_api' => 'API', + 'breadcrumb_dav' => 'DAV-Ressourcen', + 'breadcrumb_edit_introductions' => 'Wie habt ihr euch getroffen', + 'breadcrumb_settings_personalization' => 'Personalisierung', + 'breadcrumb_settings_security' => 'Sicherheit', + 'breadcrumb_settings_security_2fa' => 'Zwei-Faktor-Authentifizierung', + 'breadcrumb_profile' => 'Profil von :name', + + 'gender_male' => 'Männlich', + 'gender_female' => 'Weiblich', + 'gender_none' => 'Möchte ich nicht angeben', + 'gender_no_gender' => 'Kein Geschlecht', + + 'error_title' => 'Whoops! Da lief etwas falsch.', + 'error_unauthorized' => 'Du darfst das leider nicht, da du nicht angemeldet bist.', + 'error_user_account' => 'Dieser Benutzer gehört nicht zum angegebenen Konto.', + 'error_save' => 'Beim Versuch die Daten zu speichern ist ein Fehler aufgetreten.', + 'error_try_again' => 'Etwas ist schiefgegangen. Bitte versuche es noch mal.', + 'error_id' => 'Fehler Nr: :id', + 'error_unavailable' => 'Dienst nicht verfügbar', + 'error_maintenance' => 'Wartungsarbeiten im Gange. Wir sind gleich wieder für dich da.', + 'error_help' => 'Wir sind gleich wieder da.', + 'error_twitter' => 'Folgen Sie uns auf Twitter um informiert zu werden, wenn es weitergeht.', + 'error_no_term' => 'Für diese Instanz gibt es noch keine Richtlinie.', + + 'default_save_success' => 'Die Daten wurden gespeichert.', + + 'compliance_title' => 'Entschuldige die Unterbrechung.', + 'compliance_desc' => 'Wir haben unsere AGBs und Datenschutzerklärung geändert. Wir sind gesetzlich dazu verpflichtet zu verlangen, dass du beides durchliest und akzeptierst, damit du deinen Account weiter nutzen kannst.', + 'compliance_desc_end' => 'Wir machen nichts Böses mit deinen Daten oder deinem Account und werden das auch nie tun.', + 'compliance_terms' => 'Neue AGB und Datenschutzerklärung akzeptieren', + + // Relationship types + // Yes, each relationship type has 8 strings associated with it. + // This is because we need to indicate the name of the relationship type, + // and also the name of the opposite side of this relationship (father/son), + // and then, the feminine version of the string. Finally, in some sentences + // in the UI, we need to include the name of the person we add the relationship + // to. + 'relationship_type_group_love' => 'Liebesbeziehungen', + 'relationship_type_group_family' => 'Familienverhältnisse', + 'relationship_type_group_friend' => 'Freundschaftsbeziehungen', + 'relationship_type_group_work' => 'Arbeitsverhältnisse', + 'relationship_type_group_other' => 'Andere Art von Beziehungen', + + 'relationship_type_partner' => 'Lebensgefährte', + 'relationship_type_partner_female' => 'Lebensgefährtin', + 'relationship_type_partner_male' => 'Lebensgefährte', + 'relationship_type_partner_with_name' => ':names Lebensgefährte', + 'relationship_type_partner_female_with_name' => ':names Lebensgefährtin', + 'relationship_type_partner_male_with_name' => ':name’s Lebensgefährte', + + 'relationship_type_spouse' => 'Ehegatte', + 'relationship_type_spouse_female' => 'Ehefrau', + 'relationship_type_spouse_male' => 'Ehemann', + 'relationship_type_spouse_with_name' => ':names Ehegatten', + 'relationship_type_spouse_female_with_name' => ':name’s Ehefrau', + 'relationship_type_spouse_male_with_name' => ':name’s Ehemann', + + 'relationship_type_date' => 'Verabredung', + 'relationship_type_date_female' => 'Verabredung', + 'relationship_type_date_male' => 'Verabredung', + 'relationship_type_date_with_name' => ':names Verabredung', + 'relationship_type_date_female_with_name' => ':names Verabredung', + 'relationship_type_date_male_with_name' => ':name’s Verabredung', + + 'relationship_type_lover' => 'Liebhaber', + 'relationship_type_lover_female' => 'Liebhaberin', + 'relationship_type_lover_male' => 'Liebhaber', + 'relationship_type_lover_with_name' => ':names Liebhaber', + 'relationship_type_lover_female_with_name' => ':names Liebhaberin', + 'relationship_type_lover_male_with_name' => ':name’s Liebhaber', + + 'relationship_type_inlovewith' => 'verliebt in', + 'relationship_type_inlovewith_female' => 'verliebt in', + 'relationship_type_inlovewith_male' => 'verliebt in', + 'relationship_type_inlovewith_with_name' => ':name ist verliebt in', + 'relationship_type_inlovewith_female_with_name' => ':name ist verliebt in', + 'relationship_type_inlovewith_male_with_name' => ':name ist verliebt in', + + 'relationship_type_lovedby' => 'geliebt von', + 'relationship_type_lovedby_female' => 'geliebt von', + 'relationship_type_lovedby_male' => 'geliebt von', + 'relationship_type_lovedby_with_name' => ':names heimlicher Verehrer', + 'relationship_type_lovedby_female_with_name' => ':names heimliche Verehrerin', + 'relationship_type_lovedby_male_with_name' => ':name’s heimlicher Verehrer', + + 'relationship_type_ex' => 'Ex-Partner', + 'relationship_type_ex_female' => 'Ex-Freundin', + 'relationship_type_ex_male' => 'Ex-Freund', + 'relationship_type_ex_with_name' => ':name’s Ex-Partner', + 'relationship_type_ex_female_with_name' => ':name\'s Ex-Freundin', + 'relationship_type_ex_male_with_name' => ':name’s Ex-Freund', + + 'relationship_type_parent' => 'Elternteil', + 'relationship_type_parent_female' => 'Mutter', + 'relationship_type_parent_male' => 'Vater', + 'relationship_type_parent_with_name' => ':name’s Elternteil', + 'relationship_type_parent_female_with_name' => ':name\'s Mutter', + 'relationship_type_parent_male_with_name' => ':name’s Vater', + + 'relationship_type_child' => 'Kind', + 'relationship_type_child_female' => 'Tochter', + 'relationship_type_child_male' => 'Sohn', + 'relationship_type_child_with_name' => ':name’s Kind', + 'relationship_type_child_female_with_name' => ':names Tochter', + 'relationship_type_child_male_with_name' => ':name’s Sohn', + + 'relationship_type_stepparent' => 'Stiefelternteil', + 'relationship_type_stepparent_female' => 'Stiefmutter', + 'relationship_type_stepparent_male' => 'Stiefvater', + 'relationship_type_stepparent_with_name' => ':name’s Stiefelternteil', + 'relationship_type_stepparent_female_with_name' => ':name\'s Stiefmutter', + 'relationship_type_stepparent_male_with_name' => ':name’s Stiefvater', + + 'relationship_type_stepchild' => 'Stiefkind', + 'relationship_type_stepchild_female' => 'Stieftochter', + 'relationship_type_stepchild_male' => 'Stiefsohn', + 'relationship_type_stepchild_with_name' => ':name’s Stiefkind', + 'relationship_type_stepchild_female_with_name' => ':names Stieftochter', + 'relationship_type_stepchild_male_with_name' => ':name’s Stiefsohn', + + 'relationship_type_sibling' => 'Geschwister', + 'relationship_type_sibling_female' => 'Schwester', + 'relationship_type_sibling_male' => 'Bruder', + 'relationship_type_sibling_with_name' => ':name’s Geschwister', + 'relationship_type_sibling_female_with_name' => ':names Schwester', + 'relationship_type_sibling_male_with_name' => ':name’s Bruder', + + 'relationship_type_grandparent' => 'Großelternteil', + 'relationship_type_grandparent_female' => 'Oma', + 'relationship_type_grandparent_male' => 'Opa', + 'relationship_type_grandparent_with_name' => ':name’s Großelternteil', + 'relationship_type_grandparent_female_with_name' => ':name’s Großmutter', + 'relationship_type_grandparent_male_with_name' => ':name’s Großvater', + + 'relationship_type_grandchild' => 'Enkelkind', + 'relationship_type_grandchild_female' => 'Enkelin', + 'relationship_type_grandchild_male' => 'Enkel', + 'relationship_type_grandchild_with_name' => ':name’s Enkelkind', + 'relationship_type_grandchild_female_with_name' => ':name’s Enkelin', + 'relationship_type_grandchild_male_with_name' => ':name’s Enkel', + + 'relationship_type_uncle' => 'Onkel', + 'relationship_type_uncle_female' => 'Tante', + 'relationship_type_uncle_male' => 'Onkel', + 'relationship_type_uncle_with_name' => ':name\'s Onkel', + 'relationship_type_uncle_female_with_name' => ':name\'s Tante', + 'relationship_type_uncle_male_with_name' => ':name’s Onkel', + + 'relationship_type_nephew' => 'Neffe', + 'relationship_type_nephew_female' => 'Nichte', + 'relationship_type_nephew_male' => 'Neffe', + 'relationship_type_nephew_with_name' => ':name\'s Neffe', + 'relationship_type_nephew_female_with_name' => ':name\'s Nichte', + 'relationship_type_nephew_male_with_name' => ':name’s Neffe', + + 'relationship_type_cousin' => 'Cousin', + 'relationship_type_cousin_female' => 'Cousine', + 'relationship_type_cousin_male' => 'Cousin', + 'relationship_type_cousin_with_name' => ':name\'s Cousin', + 'relationship_type_cousin_female_with_name' => ':name\'s Cousine', + 'relationship_type_cousin_male_with_name' => ':name’s Cousin', + + 'relationship_type_godfather' => 'Pate', + 'relationship_type_godfather_female' => 'Patin', + 'relationship_type_godfather_male' => 'Patenonkel', + 'relationship_type_godfather_with_name' => ':name’s Pate', + 'relationship_type_godfather_female_with_name' => ':names Patin', + 'relationship_type_godfather_male_with_name' => ':name’s Patenonkel', + + 'relationship_type_godson' => 'Patenkind', + 'relationship_type_godson_female' => 'Patenkind', + 'relationship_type_godson_male' => 'Patensohn', + 'relationship_type_godson_with_name' => ':name’s Patenkind', + 'relationship_type_godson_female_with_name' => ':name\'s Patenkind', + 'relationship_type_godson_male_with_name' => ':name’s Patensohn', + + 'relationship_type_friend' => 'Freund', + 'relationship_type_friend_female' => 'Freundin', + 'relationship_type_friend_male' => 'Freund', + 'relationship_type_friend_with_name' => ':name\'s Freund', + 'relationship_type_friend_female_with_name' => ':name\'s Freundin', + 'relationship_type_friend_male_with_name' => ':name’s Freund', + + 'relationship_type_bestfriend' => 'Bester Freund', + 'relationship_type_bestfriend_female' => 'Beste Freundin', + 'relationship_type_bestfriend_male' => 'bester Freund', + 'relationship_type_bestfriend_with_name' => ':name\'s bester Freund', + 'relationship_type_bestfriend_female_with_name' => ':name\'s beste Freundin', + 'relationship_type_bestfriend_male_with_name' => ':name’s bester Freund', + + 'relationship_type_colleague' => 'Kollege', + 'relationship_type_colleague_female' => 'Kollegin', + 'relationship_type_colleague_male' => 'Kollege', + 'relationship_type_colleague_with_name' => ':name\'s Kollege', + 'relationship_type_colleague_female_with_name' => ':name\'s Kollegin', + 'relationship_type_colleague_male_with_name' => ':name’s Kollege', + + 'relationship_type_boss' => 'Chef', + 'relationship_type_boss_female' => 'Chefin', + 'relationship_type_boss_male' => 'Chef', + 'relationship_type_boss_with_name' => ':name\'s Chef', + 'relationship_type_boss_female_with_name' => ':name\'s Chefin', + 'relationship_type_boss_male_with_name' => ':name’s Chef', + + 'relationship_type_subordinate' => ':name\'s Untergebener', + 'relationship_type_subordinate_female' => ':name\'s Untergebene', + 'relationship_type_subordinate_male' => 'Untergebener', + 'relationship_type_subordinate_with_name' => ':name\'s Mitarbeiter', + 'relationship_type_subordinate_female_with_name' => ':name\'s Mitarbeiterin', + 'relationship_type_subordinate_male_with_name' => ':name’s Untergebener', + + 'relationship_type_mentor' => 'Mentor', + 'relationship_type_mentor_female' => 'Mentorin', + 'relationship_type_mentor_male' => 'Mentor', + 'relationship_type_mentor_with_name' => ':name\'s Mentor', + 'relationship_type_mentor_female_with_name' => ':name\'s Mentorin', + 'relationship_type_mentor_male_with_name' => ':name’s Mentor', + + 'relationship_type_protege' => 'Schützling', + 'relationship_type_protege_female' => 'Schützling', + 'relationship_type_protege_male' => 'Schützling', + 'relationship_type_protege_with_name' => ':name’s Schützling', + 'relationship_type_protege_female_with_name' => ':name’s Schützling', + 'relationship_type_protege_male_with_name' => ':name’s Schützling', + + 'relationship_type_ex_husband' => 'Ex-Mann', + 'relationship_type_ex_husband_female' => 'Ex-Frau', + 'relationship_type_ex_husband_male' => 'Ex-Frau', + 'relationship_type_ex_husband_with_name' => ':name’s Ex-Mann', + 'relationship_type_ex_husband_female_with_name' => ':name’s Ex-Frau', + 'relationship_type_ex_husband_male_with_name' => ':name’s Ex-Frau', + + // emotions + 'emotion_primary_love' => 'Liebe', + 'emotion_primary_joy' => 'Freude', + 'emotion_primary_surprise' => 'Überraschung', + 'emotion_primary_anger' => 'Zorn', + 'emotion_primary_sadness' => 'Traurigkeit', + 'emotion_primary_fear' => 'Angst', + + 'emotion_secondary_affection' => 'Zuneigung', + 'emotion_secondary_lust' => 'Begierde', + 'emotion_secondary_longing' => 'Sehnsucht', + 'emotion_secondary_cheerfulness' => 'Fröhlich', + 'emotion_secondary_zest' => 'Elan', + 'emotion_secondary_contentment' => 'Zufriedenheit', + 'emotion_secondary_pride' => 'Stolz', + 'emotion_secondary_optimism' => 'Optimismus', + 'emotion_secondary_enthrallment' => 'Begeisterung', + 'emotion_secondary_relief' => 'Erleichterung', + 'emotion_secondary_surprise' => 'Überraschung', + 'emotion_secondary_irritation' => 'Irritation', + 'emotion_secondary_exasperation' => 'Verzweiflung', + 'emotion_secondary_rage' => 'Wut', + 'emotion_secondary_disgust' => 'Ekel', + 'emotion_secondary_envy' => 'Neid', + 'emotion_secondary_suffering' => 'Leiden', + 'emotion_secondary_sadness' => 'Traurigkeit', + 'emotion_secondary_disappointment' => 'Enttäuscht', + 'emotion_secondary_shame' => 'Scham', + 'emotion_secondary_neglect' => 'Vernachlässigung', + 'emotion_secondary_sympathy' => 'Sympathie', + 'emotion_secondary_horror' => 'Entsetzen', + 'emotion_secondary_nervousness' => 'Nervosität', + + 'emotion_adoration' => 'Verehrung', + 'emotion_affection' => 'Zuneigung', + 'emotion_love' => 'Liebe', + 'emotion_fondness' => 'Zuneigung', + 'emotion_liking' => 'Gefallen', + 'emotion_attraction' => 'Anziehung', + 'emotion_caring' => 'Fürsorglich', + 'emotion_tenderness' => 'Zärtlichkeit', + 'emotion_compassion' => 'Mitgefühl', + 'emotion_sentimentality' => 'Sentimentalität', + 'emotion_arousal' => 'Erregung', + 'emotion_desire' => 'Verlangen', + 'emotion_lust' => 'Begierde', + 'emotion_passion' => 'Leidenschaft', + 'emotion_infatuation' => 'Betörung', + 'emotion_longing' => 'Sehnsucht', + 'emotion_amusement' => 'Vergnügen', + 'emotion_bliss' => 'Glück', + 'emotion_cheerfulness' => 'Fröhlichkeit', + 'emotion_gaiety' => 'Heiterkeit', + 'emotion_glee' => 'Freude', + 'emotion_jolliness' => 'Fröhlichkeit', + 'emotion_joviality' => 'Herzlichkeit', + 'emotion_joy' => 'Freude', + 'emotion_delight' => 'Freude', + 'emotion_enjoyment' => 'Genuss', + 'emotion_gladness' => 'Freude', + 'emotion_happiness' => 'Glück', + 'emotion_jubilation' => 'Jubel', + 'emotion_elation' => 'Hochgefühl', + 'emotion_satisfaction' => 'Zufriedenheit', + 'emotion_ecstasy' => 'Ecstasy', + 'emotion_euphoria' => 'Euphorie', + 'emotion_enthusiasm' => 'Begeisterung', + 'emotion_zeal' => 'Eifer', + 'emotion_zest' => 'Elan', + 'emotion_excitement' => 'Aufregung', + 'emotion_thrill' => 'Nervenkitzel', + 'emotion_exhilaration' => 'Heiterkeit', + 'emotion_contentment' => 'Zufriedenheit', + 'emotion_pleasure' => 'Vergnügen', + 'emotion_pride' => 'Stolz', + 'emotion_eagerness' => 'Verlangen', + 'emotion_hope' => 'Hoffnung', + 'emotion_optimism' => 'Optimismus', + 'emotion_enthrallment' => 'Begeisterung', + 'emotion_rapture' => 'Entrückung', + 'emotion_relief' => 'Erleichterung', + 'emotion_amazement' => 'Verwunderung', + 'emotion_surprise' => 'Überraschung', + 'emotion_astonishment' => 'Erstaunen', + 'emotion_aggravation' => 'Verärgerung', + 'emotion_irritation' => 'Irritation', + 'emotion_agitation' => 'Erregt', + 'emotion_annoyance' => 'Verärgerung', + 'emotion_grouchiness' => 'Miesepetrigkeit', + 'emotion_grumpiness' => 'Mür­risch­keit', + 'emotion_exasperation' => 'Verzweiflung', + 'emotion_frustration' => 'Frustration', + 'emotion_anger' => 'Zorn', + 'emotion_rage' => 'Wut', + 'emotion_outrage' => 'Entrüstung', + 'emotion_fury' => 'Rage', + 'emotion_wrath' => 'Zorn', + 'emotion_hostility' => 'Feindseligkeit', + 'emotion_ferocity' => 'Wildheit', + 'emotion_bitterness' => 'Bitterkeit', + 'emotion_hate' => 'Hass', + 'emotion_loathing' => 'Abscheu', + 'emotion_scorn' => 'Verachtung', + 'emotion_spite' => 'Boshaftigkeit', + 'emotion_vengefulness' => 'Rachsucht', + 'emotion_dislike' => 'Abneigung', + 'emotion_resentment' => 'Missgunst', + 'emotion_disgust' => 'Ekel', + 'emotion_revulsion' => 'Abscheu', + 'emotion_contempt' => 'Verachtung', + 'emotion_envy' => 'Neid', + 'emotion_jealousy' => 'Eifersucht', + 'emotion_agony' => 'Pein', + 'emotion_suffering' => 'Leid', + 'emotion_hurt' => 'Schmerz', + 'emotion_anguish' => 'Qual', + 'emotion_depression' => 'Depression', + 'emotion_despair' => 'Verzweiflung', + 'emotion_hopelessness' => 'Hoffnungslosigkeit', + 'emotion_gloom' => 'Trübsinn', + 'emotion_glumness' => 'Verdrießlich', + 'emotion_sadness' => 'Traurigkeit', + 'emotion_unhappiness' => 'Unglücklichkeit', + 'emotion_grief' => 'Trauer', + 'emotion_sorrow' => 'Bedauern', + 'emotion_woe' => 'Kummer', + 'emotion_misery' => 'Elend', + 'emotion_melancholy' => 'Melancholie', + 'emotion_dismay' => 'Bestürzung', + 'emotion_disappointment' => 'Enttäuschung', + 'emotion_displeasure' => 'Missfallen', + 'emotion_guilt' => 'Schuld', + 'emotion_shame' => 'Scham', + 'emotion_regret' => 'Bedauern', + 'emotion_remorse' => 'Reue', + 'emotion_alienation' => 'Entfremdung', + 'emotion_isolation' => 'Ausgrenzung', + 'emotion_neglect' => 'Vernachlässigung', + 'emotion_loneliness' => 'Einsamkeit', + 'emotion_rejection' => 'Zurückweisung', + 'emotion_homesickness' => 'Heimweh', + 'emotion_defeat' => 'Versagen', + 'emotion_dejection' => 'Niedergeschlagenheit', + 'emotion_insecurity' => 'Unsicherheit', + 'emotion_embarrassment' => 'Peinlichkeit', + 'emotion_humiliation' => 'Demütigung', + 'emotion_insult' => 'Beleidigung', + 'emotion_pity' => 'Mitleid', + 'emotion_sympathy' => 'Sympathie', + 'emotion_alarm' => 'Sorge', + 'emotion_shock' => 'Schock', + 'emotion_fear' => 'Angst', + 'emotion_fright' => 'Furcht', + 'emotion_horror' => 'Entsetzen', + 'emotion_terror' => 'Grauen', + 'emotion_panic' => 'Panik', + 'emotion_hysteria' => 'Hysterie', + 'emotion_mortification' => 'Kränkung', + 'emotion_anxiety' => 'Beklommenheit', + 'emotion_nervousness' => 'Nervosität', + 'emotion_tenseness' => 'Anspannung', + 'emotion_uneasiness' => 'Unbehagen', + 'emotion_apprehension' => 'Befürchtung', + 'emotion_worry' => 'Besorgnis', + 'emotion_distress' => 'Belastung', + 'emotion_dread' => 'Grauen', + + // weather + 'weather_sunny' => 'Sonnig', + 'weather_clear' => 'Klar', + 'weather_clear-day' => 'Klar', + 'weather_clear-night' => 'Klare Nacht', + 'weather_light-drizzle' => 'Leichter Nieselregen', + 'weather_patchy-light-drizzle' => 'Vereinzelt leichter Nieselregen', + 'weather_patchy-light-rain' => 'Stellenweise leichter Regen', + 'weather_light-rain' => 'Leichter Regen', + 'weather_moderate-rain-at-times' => 'Gelegentlich mäßiger Regen', + 'weather_moderate-rain' => 'Mäßiger Regen', + 'weather_patchy-rain-possible' => 'Stellenweise Regen möglich', + 'weather_heavy-rain-at-times' => 'Gelegentlich starker Regen', + 'weather_heavy-rain' => 'Starker Regen', + 'weather_light-freezing-rain' => 'Leichter gefrierender Regen', + 'weather_moderate-or-heavy-freezing-rain' => 'Mäßiger bis starker gefrierender Regen', + 'weather_light-sleet' => 'Leichter Schneeregen', + 'weather_moderate-or-heavy-rain-shower' => 'Mäßige bis starke Regenschauer', + 'weather_light-rain-shower' => 'Leichte Regenschauer', + 'weather_torrential-rain-shower' => 'Starkregenschauer', + 'weather_rain' => 'Regen', + 'weather_snow' => 'Schnee', + 'weather_blowing-snow' => 'Schneegestöber', + 'weather_patchy-light-snow' => 'Vereinzelt leichter Schneefall', + 'weather_light-snow' => 'Leichter Schneefall', + 'weather_patchy-moderate-snow' => 'Stellenweise mäßiger Schneefall', + 'weather_moderate-snow' => 'Mäßiger Schneefall', + 'weather_patchy-heavy-snow' => 'Vereinzelt starker Schneefall', + 'weather_heavy-snow' => 'Starker Schneefall', + 'weather_light-snow-showers' => 'Leichter Schneeschauer', + 'weather_moderate-or-heavy-snow-showers' => 'Mäßige bis starke Schneeschauer', + 'weather_patchy-snow-possible' => 'Stellenweiser Schneefall möglich', + 'weather_patchy-sleet-possible' => 'Stellenweiser Schneeregen möglich', + 'weather_moderate-or-heavy-sleet' => 'Mäßiger bis starker Schneeregen', + 'weather_light-sleet-showers' => 'Leichte Schneeregenschauer', + 'weather_moderate-or-heavy-sleet-showers' => 'Mäßige bis starke Schneeregenschauer', + 'weather_sleet' => 'Schneeregen', + 'weather_wind' => 'Wind', + 'weather_fog' => 'Nebel', + 'weather_freezing-fog' => 'Gefrierender Nebel', + 'weather_mist' => 'Nebel', + 'weather_blizzard' => 'Schneesturm', + 'weather_overcast' => 'Bewölkt', + 'weather_cloudy' => 'Bewölkt', + 'weather_partly-cloudy-day' => 'Teilweise bewölkt', + 'weather_partly-cloudy-night' => 'Teilweise bewölkt', + 'weather_freezing-drizzle' => 'Gefrierender Nieselregen', + 'weather_heavy-freezing-drizzle' => 'Starker gefrierender Nieselregen', + 'weather_patchy-freezing-drizzle-possible' => 'Stellenweise gefrierender Nieselregen möglich', + 'weather_ice-pellets' => 'Graupel', + 'weather_light-showers-of-ice-pellets' => 'Leichte Graupelschauer', + 'weather_moderate-or-heavy-showers-of-ice-pellets' => 'Mäßige bis starke Graupelschauer', + 'weather_thundery-outbreaks-possible' => 'Gewitterausbrüche möglich', + 'weather_patchy-light-rain-with-thunder' => 'Stellenweise leichter Regen bei Gewitter', + 'weather_moderate-or-heavy-rain-with-thunder' => 'Mäßiger bis starker Regen bei Gewitter', + 'weather_patchy-light-snow-with-thunder' => 'Stellenweise leichter Schneefall bei Gewitter', + 'weather_moderate-or-heavy-snow-with-thunder' => 'Mäßiger bis starker Schneefall bei Gewitter', + 'weather_current_temperature_celsius' => ':temperature °C', + 'weather_current_temperature_fahrenheit' => ':temperature °F', + 'weather_current_title' => 'Aktuelles Wetter', + + // dav + 'dav_contacts' => 'Kontakte', + 'dav_contacts_description' => 'Kontakte von :name', + 'dav_birthdays' => 'Geburtstage', + 'dav_birthdays_description' => 'Geburtstage der Kontakte von :name', + 'dav_tasks' => 'Aufgaben', + 'dav_tasks_description' => ':names Aufgaben', + + // contact list + 'contact_list_avatar' => 'Profilbild', + 'contact_list_name' => 'Kontakt', + 'contact_list_description' => 'Beschreibung', + +]; diff --git a/resources/lang/de/auth.php b/resources/lang/de/auth.php new file mode 100644 index 0000000..d8d7ddf --- /dev/null +++ b/resources/lang/de/auth.php @@ -0,0 +1,89 @@ + 'Die Anmeldedaten stimmen nicht.', + 'throttle' => 'Zu viele Anmeldeversuche. Bitte in :seconds Sekunden erneut versuchen.', + 'not_authorized' => 'Du hast keine Berechtigung diese Aktion auszuführen', + 'signup_disabled' => 'Neue Registrierungen sind zur Zeit nicht möglich', + 'signup_error' => 'Es ist ein Fehler bei der Registrierung des Benutzers aufgetreten', + 'back_homepage' => 'Zurück zur Seite', + 'mfa_auth_otp' => 'Authentifizieren Sie sich mit Ihrem Zwei-Faktor-Gerät', + 'mfa_auth_webauthn' => 'Authentifizieren mit einem Sicherheitsschlüssel (WebAuthn)', + '2fa_title' => 'Zwei-Faktor-Authentifizierung', + '2fa_wrong_validation' => 'Die Zwei-Faktor-Authentifizierung ist fehlgeschlagen.', + '2fa_one_time_password' => 'Zwei-Faktor-Authentifizierungscode', + '2fa_recuperation_code' => 'Bitte gib deinen Zwei-Faktor-Wiederherstellungscode ein', + '2fa_one_time_or_recuperation' => 'Gib einen Zwei-Faktor-Authentifizierungscode oder einen Wiederherstellungscode ein', + '2fa_otp_help' => 'Öffne deine Zwei-Faktor-Authentifizierungs-App und scanne den folgenden QR-Code', + + 'login_to_account' => 'In Konto einloggen', + 'login_with_recovery' => 'Mit einem Wiederherstellungsschlüssel anmelden', + 'login_again' => 'Bitte loggen Sie sich wieder in Ihren Account ein', + 'email' => 'E-Mail', + 'password' => 'Passwort', + 'recovery' => 'Wiederherstellungsschlüssel', + 'login' => 'Einloggen', + 'button_remember' => 'Eingeloggt bleiben', + 'password_forget' => 'Passwort vergessen?', + 'password_reset' => 'Passwort zurücksetzen', + 'use_recovery' => 'Oder sie verwenden einen Wiederherstellungsschlüssel', + 'signup_no_account' => 'Haben Sie noch kein Konto?', + 'signup' => 'Registrieren', + 'create_account' => 'Erstellen Sie ihr erstes Konto, indem sie sich registrieren', + 'change_language_title' => 'Sprache ändern:', + 'change_language' => 'Sprache ändern zu :lang', + + 'password_reset_title' => 'Passwort zurücksetzen', + 'password_reset_email' => 'E-Mail-Adresse', + 'password_reset_send_link' => 'E-Mail zum Zurücksetzen des Passworts senden', + 'password_reset_password' => 'Passwort', + 'password_reset_password_confirm' => 'Passwort bestätigen', + 'password_reset_action' => 'Passwort zurücksetzen', + 'password_reset_email_content' => 'Hier klicken, um das Passwort zurückzusetzen:', + + 'register_title_welcome' => 'Herzlich Willkommen in Ihrer neu installierten Instanz von Monica', + 'register_create_account' => 'Sie benötigen ein Konto, um Monica zu verwenden', + 'register_title_create' => 'Monica Konto erstellen', + 'register_login' => 'Einloggen wenn Sie bereits ein Konto haben.', + 'register_email' => 'Gültige E-Mail Adresse eingeben', + 'register_email_example' => 'du@zuhause', + 'register_firstname' => 'Vorname', + 'register_firstname_example' => 'z.B. Max', + 'register_lastname' => 'Nachname', + 'register_lastname_example' => 'z.B. Mustermann', + 'register_password' => 'Passwort', + 'register_password_example' => 'Sicheres Kennwort eingeben', + 'register_password_confirmation' => 'Passwortbestätigung', + 'register_action' => 'Anmelden', + 'register_policy' => 'Deine Anmeldung bedeutet, dass du unsere Datenschutzrichtlinien and AGBs gelesen und akzeptiert hast.', + 'register_invitation_email' => 'Aus Sicherheitsgründen geben Sie bitte die E-Mail-Adresse der Person an, die Sie eingeladen hat, diesem Konto beizutreten. Diese Informationen finden Sie in der Einladungs-E-Mail.', + + 'confirmation_title' => 'E-Mail-Adresse bestätigen', + 'confirmation_fresh' => 'Ein Bestätigungslink wurde an Ihre E-Mail-Adresse geschickt.', + 'confirmation_check' => 'Bevor sie weitermachen, überprüfen sie bitte ihre E-mails nach einem Bestätigungslink.', + 'confirmation_request_another' => 'Falls Sie keine E-Mail erhalten haben, klicken Sie hier um eine neue E-Mail zu erhalten.', + + 'confirmation_again' => 'Wenn Sie Ihre E-Mail-Adresse ändern möchten, klicken Sie bitte hier.', + 'email_change_current_email' => 'Aktuelle E-Mail-Adresse:', + 'email_change_title' => 'E-Mail-Adresse ändern', + 'email_change_new' => 'Neue E-Mail-Adresse', + 'email_changed' => 'Ihre E-Mail-Adresse wurde geändert. Überprüfen Sie Ihre E-Mails um sie zu bestätigen.', +]; diff --git a/resources/lang/de/changelog.php b/resources/lang/de/changelog.php new file mode 100644 index 0000000..d3049fd --- /dev/null +++ b/resources/lang/de/changelog.php @@ -0,0 +1,12 @@ + 'Produktänderungen', + 'note' => 'Anmerkung: Diese Seite gibt es leider nur auf englisch.', +]; diff --git a/resources/lang/de/dashboard.php b/resources/lang/de/dashboard.php new file mode 100644 index 0000000..2b4cec7 --- /dev/null +++ b/resources/lang/de/dashboard.php @@ -0,0 +1,42 @@ + 'Herzlich Willkommen auf deinem Account!', + 'dashboard_blank_description' => 'Monica ist der Ort um all deine Interaktionen zu organisieren die dir wichtig sind.', + 'dashboard_blank_cta' => 'Füge deinen ersten Kontakt hinzu', + 'dashboard_blank_illustration' => 'Illustration von Freepik', + + 'notes_title' => 'Du hast noch keine Notizen.', + + 'tab_recent_calls' => 'Kürzliche Telefonate', + 'tab_favorite_notes' => 'Markierte Notizen', + 'tab_calls_blank' => 'Du hast noch keine Telefonate protokolliert.', + 'tab_debts' => 'Schulden', + 'tab_debts_blank' => 'Du hast noch keine Schulden protokolliert.', + 'tab_tasks' => 'Aufgaben', + 'tab_tasks_blank' => 'Sie haben noch keine Aufgaben.', + + 'tasks_add_task_placeholder' => 'Worum geht es bei dieser Aufgabe?', + 'tasks_tab_your_contacts' => 'Aufgaben im Zusammenhang mit Ihren Kontakten', + 'tasks_tab_your_tasks' => 'Ihre Aufgaben', + 'tasks_add_note' => 'Drücken Sie Eingabe um die Aufgabe hinzuzufügen.', + 'task_add_cta' => 'Aufgabe hinzufügen', + + 'debts_you_owe' => 'Du schuldest', + + 'statistics_contacts' => 'Kontakte', + 'statistics_activities' => 'Aktivitäten', + 'statistics_gifts' => 'Geschenke', + + 'reminders_next_months' => 'Ereignisse der nächsten 3 Monate', + 'reminders_none' => 'Keine Erinnerungen für diesen Monat.', + + 'product_changes' => 'Produktänderungen', + 'product_view_details' => 'Details anzeigen', +]; diff --git a/resources/lang/de/format.php b/resources/lang/de/format.php new file mode 100644 index 0000000..5e5e0ce --- /dev/null +++ b/resources/lang/de/format.php @@ -0,0 +1,36 @@ + 'd. M Y H:i', + 'short_date_year' => 'd. M Y', + 'short_date' => 'd. M', + 'short_month' => 'M', + 'short_month_year' => 'M Y', + 'short_day' => 'D', + 'full_date_year' => 'd. F Y', + 'full_month' => 'F', + 'full_month_year' => 'F Y', + 'full_hour' => 'H:i', + + 'short_text' => '{text}…', +]; diff --git a/resources/lang/de/journal.php b/resources/lang/de/journal.php new file mode 100644 index 0000000..7d7046d --- /dev/null +++ b/resources/lang/de/journal.php @@ -0,0 +1,38 @@ + 'Wie war dein Tag? Einmal am Tag kannst du ihn bewerten.', + 'journal_come_back' => 'Danke. Morgen kannst du wieder deinen Tag bewerten.', + 'journal_description' => 'Hinweis: Das Journal zeigt sowohl manuelle Einträge, als auch Aktivitäten mit deinen Kontakten an. Manuelle Einträge kannst du hier löschen, Aktivitäten kannst du auf der jeweiligen Profilseite der beteiligten Person editieren oder löschen.', + 'journal_add' => 'Tagebucheintrag hinzufügen', + 'journal_edit' => 'Tagebucheintrag bearbeiten', + 'journal_empty' => 'Leeres Tagebuch', + 'journal_created_at' => 'Erstellt am {date}', + 'journal_created_automatically' => 'Autmatisch hinzugefügt', + 'journal_entry_type_journal' => 'Tagebucheintrag', + 'journal_entry_type_activity' => 'Aktivität', + 'journal_entry_rate' => 'Du hast deinen Tag bewertet', + 'journal_add_comment' => 'Möchtest du einen Kommentar hinzufügen (optional)?', + 'journal_show_comment' => 'Kommentar anzeigen', + 'entry_delete_success' => 'Der Tagebucheintrag wurde erfolgreich gelöscht.', + 'journal_add_title' => 'Titel (optional)', + 'journal_add_date' => 'Datum', + 'journal_add_post' => 'Eintrag', + 'journal_add_cta' => 'Speichern', + 'journal_blank_cta' => 'Schreibe deinen ersten Eintrag', + 'journal_blank_description' => 'Im Tagebuch kannst du deine Erlebnisse festhalten und dich später an sie erinnern.', + 'delete_confirmation' => 'Willst du diesen Eintrag wirklich löschen?', + 'apply_filter' => 'Apply filter', + 'start_date' => 'Start Date', + 'end_date' => 'End Date', + 'per_page' => 'Per Page', + 'sort_order' => 'Sort By Created At', + 'ascending' => 'Ascending', + 'descending' => 'Descending', +]; diff --git a/resources/lang/de/logs.php b/resources/lang/de/logs.php new file mode 100644 index 0000000..3b64034 --- /dev/null +++ b/resources/lang/de/logs.php @@ -0,0 +1,29 @@ + 'Kontakt erstellt.', + 'settings_log_contact_created_with_name' => ':name als Kontakt hinzugefügt.', + + // contat description update + 'contact_log_contact_description_updated' => 'Beschreibung aktualisiert.', + 'settings_log_contact_description_updated_with_name' => ':name wurde aktualisiert.', + + // contact description clear + 'contact_log_contact_description_cleared' => 'Beschreibung gelöscht.', + 'settings_log_contact_description_cleared_with_name' => 'Beschreibung von :name gelöscht.', + + // contact work information update + 'contact_log_contact_work_updated' => 'Arbeitsinformationen aktualisiert.', + 'settings_log_contact_work_updated_with_name' => 'Arbeitsinformationen von :name aktualisiert.', + + // company created + 'settings_log_company_created' => 'Firma :name erstellt.', +]; diff --git a/resources/lang/de/mail.php b/resources/lang/de/mail.php new file mode 100644 index 0000000..7ae8c87 --- /dev/null +++ b/resources/lang/de/mail.php @@ -0,0 +1,54 @@ + 'Erinnerung für :contact', + 'greetings' => 'Hallo :username', + 'want_reminded_of' => 'Sie wollten erinnert werden :reason', + 'for' => 'Für: :name', + 'comment' => 'Kommentar: :comment', + 'footer_contact_info' => 'Ergänze, betrachte, vervollständige und ändere Informationen zu diesem Kontakt:', + 'footer_contact_info2' => 'Siehe :name’s profile', + 'footer_contact_info2_link' => 'Siehe :name Profil', + + 'notification_subject_line' => 'Du hast ein bevorstehendes Ereignis', + 'notification_description' => 'In :count Tagen (am :date), findet folgendes Ereignis statt:', + + 'stay_in_touch_subject_line' => 'Mit :name in Kontakt bleiben', + 'stay_in_touch_subject_description' => 'Sie wollten jeden :frequency Tag erinnert werden, um in Kontakt zu bleiben mit :name .| +Sie wollten alle :frequency Tage erinnert werden um in Kontakt zu bleiben mit :name .', + + 'notifications_whoops' => 'Hoppla!', + 'notifications_hello' => 'Hallo!', + 'notifications_regards' => 'Grüße', + 'notifications_footer' => 'Wenn Sie Probleme beim Klicken auf die Schaltfläche ":actionText" haben, kopieren Sie einfach die folgende URL in die Adresszeile Ihres Webbrowsers: [:actionURL](:actionURL)', + 'notifications_rights' => 'Alle Rechte vorbehalten', + + 'confirmation_email_title' => 'Monica – E-Mail-Verifikation', + 'confirmation_email_intro'=> 'Um Ihre E-Mail-Adresse zu validieren, klicken Sie bitte auf den untenstehenden Button', + 'confirmation_email_button' => 'E-Mail-Adresse bestätigen', + 'confirmation_email_bottom' => 'Wenn Sie kein Konto erstellt haben, ist keine weitere Aktion erforderlich.', + + 'password_reset_title' => 'Monica – Passwort-Benachrichtigung zurücksetzen', + 'password_reset_intro' => 'Sie erhalten diese E-Mail, weil wir eine Anfrage zum Zurücksetzen des Passworts für Ihr Konto erhalten haben.', + 'password_reset_button' => 'Passwort zurücksetzen', + 'password_reset_expiration' => 'Dieser Link zum Zurücksetzen des Passworts läuft in :count Minuten ab.', + 'password_reset_bottom' => 'Wenn Sie keine Passwortzurücksetzung angefordert haben, ist keine weitere Aktion erforderlich.', + + 'invitation_title' => 'Monica – Du wurdest von :name eingeladen', + 'invitation_intro' => 'Sie wurden von :name (:email) eingeladen, um Monica zu verwenden, ein Personal Relationship Management Tool.', + 'invitation_link' => 'Um die Einladung anzunehmen, klicken Sie auf den folgenden Link:', + 'invitation_button' => 'Einladung annehmen', + 'invitation_expiration' => 'Dieser Link läuft in :count Tagen ab.', + + 'export_title' => 'Ihr Export ist fertig', + 'export_description' => 'Sie haben einen Datenexport am :date angefordert. Dieser ist nun zum Download bereit.', + 'export_download' => 'Export herunterladen', + +]; diff --git a/resources/lang/de/pagination.php b/resources/lang/de/pagination.php new file mode 100644 index 0000000..d28bfb3 --- /dev/null +++ b/resources/lang/de/pagination.php @@ -0,0 +1,25 @@ + '❮ Zurück', + 'next' => 'Weiter ❯', + +]; diff --git a/resources/lang/de/passwords.php b/resources/lang/de/passwords.php new file mode 100644 index 0000000..c0af7ff --- /dev/null +++ b/resources/lang/de/passwords.php @@ -0,0 +1,30 @@ + 'Dein Passwort wurde zurückgesetzt!', + 'sent' => 'Wenn die E-Mail-Adresse, die du eingegeben hast mit der in unserem System übereinstimmt, hast du eine E-Mail mit Reset-Link bekommen.', + 'token' => 'Der Passwort-Reset-Token ist ungültig.', + 'user' => 'Wenn die E-Mail-Adresse, die du eingegeben hast mit der in unserem System übereinstimmt, hast du eine E-Mail mit Reset-Link bekommen.', + 'changed' => 'Das Kennwort wurde erfolgreich geändert.', + 'invalid' => 'Das eingegebene Passwort stimmt nicht.', + 'throttled' => 'Bitte warte, bevor du es erneut versuchst.', + +]; diff --git a/resources/lang/de/people.php b/resources/lang/de/people.php new file mode 100644 index 0000000..a622de4 --- /dev/null +++ b/resources/lang/de/people.php @@ -0,0 +1,539 @@ + 'Kontakt nicht gefunden', + 'people_list_number_kids' => ':count Kind|:count Kinder', + 'people_list_last_updated' => 'Zuletzt aufgerufen:', + 'people_list_number_reminders' => ':count Erinnerung|:count Erinnerungen', + 'people_list_blank_title' => 'Du hast noch niemanden in deinem Konto angelegt', + 'people_list_blank_cta' => 'Neuer Kontakt', + 'people_list_sort' => 'Sortieren', + 'people_list_stats' => ':count Kontakt|:count Kontakte', + 'people_list_firstnameAZ' => 'Nach Vorname sortieren A → Z', + 'people_list_firstnameZA' => 'Nach Vorname sortieren Z → A', + 'people_list_lastnameAZ' => 'Nach Nachname sortieren A → Z', + 'people_list_lastnameZA' => 'Nach Nachname sortieren Z → A', + 'people_list_lastactivitydateNewtoOld' => 'Neueste Aktivitäten zuerst anzeigen', + 'people_list_lastactivitydateOldtoNew' => 'Älteste Aktivitäten zuerst anzeigen', + 'people_list_filter_tag' => 'Es werden alle Kontakte mit den folgenden Tags angezeigt', + 'people_list_clear_filter' => 'Filter löschen', + 'people_list_contacts_per_tags' => ':count Kontakt|:count Kontakte', + 'people_list_show_dead' => 'Verstorbene Kontakte anzeigen (:count)', + 'people_list_hide_dead' => 'Verstorbene Kontakte ausblenden (:count)', + 'people_search' => 'Suche in deinen Kontakten…', + 'people_search_no_results' => 'Keine Ergebnisse gefunden', + 'people_search_next' => 'Nächste', + 'people_search_prev' => 'Zurück', + 'people_search_rows_per_page' => 'Einträge pro Seite', + 'people_search_of' => 'von', + 'people_search_page' => 'Seite', + 'people_search_all' => 'Alle', + 'people_add_new' => 'Neue Person hinzufügen', + 'people_list_account_usage' => 'Dein Account nutzt: :current/:limit Kontakte', + 'people_list_account_upgrade_title' => 'Führe ein Upgrade aus, um alle Funktionen freizuschalten.', + 'people_list_account_upgrade_cta' => 'Jetzt upgraden', + 'people_list_untagged' => 'Unmarkierte Kontakte anzeigen', + 'people_list_filter_untag' => 'Es werden alle Kontakte ohne Tags angezeigt', + 'archived_contact_readonly' => 'Archivierter Kontakt kann nicht bearbeitet werden, bitte zuerst entpacken.', + + // people add + 'people_add_title' => 'Person hinzufügen', + 'people_add_missing' => 'Keine Person gefunden – füge jetzt eine neue hinzu', + 'people_add_firstname' => 'Vorname', + 'people_add_middlename' => 'Zweiter Vorname (optional)', + 'people_add_lastname' => 'Nachname (optional)', + 'people_add_email' => 'E-Mail (optional)', + 'people_add_nickname' => 'Spitzname (optional)', + 'people_add_cta' => 'Person hinzufügen', + 'people_save_and_add_another_cta' => 'Hinzufügen und weitere Person anlegen', + 'people_add_success' => ':name wurde erfolgreich angelegt.', + 'people_add_gender' => 'Geschlecht', + 'people_delete_success' => 'Der Kontakt wurde gelöscht', + 'people_delete_message' => 'Kontakt löschen', + 'people_delete_confirmation' => 'Möchtest du :name’s Kontakt wirklich löschen? Es gibt kein Zurück.', + 'people_add_birthday_reminder' => 'Gratuliere :name zum Geburtstag', + 'people_add_birthday_reminder_deceased' => 'Heute hätte :name seinen Geburtstag gefeiert', + 'people_add_import' => 'Möchtest du Kontakte importieren?', + 'people_edit_email_error' => 'Es gibt bereits ein Kontakt in deinem Konto mit dieser e-Mail-Adresse. Bitte wähle eine anderen.', + 'people_export' => 'Als vCard exportieren', + 'people_add_reminder_for_birthday' => 'Erstelle eine jährliche Geburtstagserinnerung', + + // show + 'section_contact_information' => 'Kontaktinformationen', + 'section_personal_activities' => 'Aktivitäten', + 'section_personal_reminders' => 'Erinnerungen', + 'section_personal_tasks' => 'Aufgaben', + 'section_personal_gifts' => 'Geschenke', + 'section_personal_notes' => 'Notizen', + + // archived contacts + 'list_link_to_active_contacts' => 'Dies sind archivierte Kontakte. Hier gelangen Sie zur Liste aktiver Kontakte.', + 'list_link_to_archived_contacts' => 'Liste der archivierten Kontakte', + + // Header + 'me' => 'Das bist du', + 'edit_contact_information' => 'Kontaktinformationen bearbeiten', + 'contact_archive' => 'Kontakt archivieren', + 'contact_unarchive' => 'Nicht gespeicherter Kontakt', + 'contact_archive_help' => 'Archivierte Kontakte werden nicht in der Kontaktliste angezeigt, erscheinen aber weiterhin in den Suchergebnissen.', + 'call_button' => 'Telefonat vermerken', + 'set_favorite' => 'Favoriten werden in der Kontaktliste ganz oben angezeigt', + + // Stay in touch + 'stay_in_touch' => 'In Kontakt bleiben', + 'stay_in_touch_frequency' => 'Jeden Tag in Kontakt bleiben|Alle {count} Tage in Kontakt bleiben', + 'stay_in_touch_next_date' => 'Nächster Termin: {date}', + 'stay_in_touch_invalid' => 'Die Zahl muss größer als 0 sein.', + 'stay_in_touch_premium' => 'Du musst dein Konto upgraden, um diese Funktion nutzen zu können', + 'stay_in_touch_modal_title' => 'In Kontakt bleiben', + 'stay_in_touch_modal_desc' => 'Wir können dich per E-Mail daran erinnern, in regelmäßigen Abständen mit {firstname} in Kontakt zu bleiben.', + 'stay_in_touch_modal_label' => 'Sende mir eine E-Mail jeden… {count} Tag|Sende mir eine E-Mail alle… {count} Tage', + + // Calls + 'modal_call_title' => 'Telefonat vermerken', + 'modal_call_comment' => 'Worüber habt ihr geredet? (optional)', + 'modal_call_exact_date' => 'Das Telefonat war am', + 'modal_call_who_called' => 'Wer hat angerufen?', + 'modal_call_emotion' => 'Möchten sie speichern wie Sie sich während das Anrufs fühlten? (optional)', + 'calls_add_success' => 'Telefonat gespeichert.', + 'call_delete_confirmation' => 'Möchtest du das Telefonat wirklich löschen?', + 'call_delete_success' => 'Das Telefonat wurde erfolgreich gelöscht', + 'call_title' => 'Telefonate', + 'call_empty_comment' => 'Keine Details', + 'call_blank_title' => 'Behalte deine Telefonate mit {name} im Auge', + 'call_blank_desc' => 'Du hast {name} angerufen', + 'call_you_called' => 'Du hast angerufen', + 'call_he_called' => '{name} rief an', + 'call_emotions' => 'Emotionen:', + + // Conversation + 'conversation_blank' => 'Führe ein Logbuch über die Konversationen, mit :name auf Social Media, via SMS…', + 'conversation_delete_link' => 'Unterhaltung löschen', + 'conversation_edit_title' => 'Unterhaltung bearbeiten', + 'conversation_edit_delete' => 'Bist du sicher, dass du diese Unterhaltung löschen willst? Dies kann nicht rückgängig gemacht werden.', + 'conversation_add_success' => 'Die Unterhaltung wurde erfolgreich hinzugefügt.', + 'conversation_edit_success' => 'Die Unterhaltung wurde erfolgreich aktualisiert.', + 'conversation_delete_success' => 'Die Unterhaltung wurde gelöscht.', + 'conversation_add_title' => 'Eine neue Unterhaltung ins Logbuch protokollieren', + 'conversation_add_when' => 'Wann hattet ihr diese Unterhaltung?', + 'conversation_add_who_wrote' => 'Wer hat diese Nachricht gesendet?', + 'conversation_add_how' => 'Über welches Medium habt ihr kommuniziert?', + 'conversation_add_you' => 'Ich selber', + 'conversation_add_content' => 'Schreibe hier, was gesagt wurde', + 'conversation_add_what_was_said' => 'Was war der Gesprächsinhalt?', + 'conversation_add_another' => 'Eine weitere Nachricht hinzufügen', + 'conversation_add_error' => 'Sie müssen mindestens eine Nachricht hinzufügen.', + 'conversation_list_table_messages' => 'Nachrichten', + 'conversation_list_table_content' => 'Teilinhalt (letzte Nachricht)', + 'conversation_list_title' => 'Unterhaltungen', + 'conversation_list_cta' => 'Unterhaltung protokollieren', + + // age - birthday + 'birthdate_not_set' => 'Geburtstag noch nicht gesetzt', + 'age_approximate_in_years' => 'ungefähr :age Jahre alt', + 'age_exact_in_years' => ':age Jahre alt', + 'age_exact_birthdate' => 'geboren am :date', + + // Last called + 'last_called' => 'Letztes Telefonat: :date', + 'last_talked_to' => 'Letztes Telefonat: {date}', + 'last_called_empty' => 'Letztes Telefonat: unbekannt', + 'last_activity_date' => 'Letzte gemeinsame Aktivität: :date', + 'last_activity_date_empty' => 'Letzte gemeinsame Aktivität: unbekannt', + + // additional information + 'information_edit_success' => 'Das Profil wurde erfolgreich aktualisiert', + 'information_edit_title' => 'Ändere :name\'s persönliche Daten', + 'information_edit_max_size' => 'Maximal :size Kb.', + 'information_edit_max_size2' => 'Maximal :size KB', + 'information_edit_firstname' => 'Vorname', + 'information_edit_lastname' => 'Nachname (optional)', + 'information_edit_description' => 'Beschreibung (optional)', + 'information_edit_description_help' => 'Wird in der Kontaktliste verwendet, um gegebenenfalls Kontext hinzuzufügen.', + 'information_edit_unknown' => 'Ich kenne das Alter dieser Person nicht', + 'information_edit_probably' => 'Diese Person ist wahrscheinlich…', + 'information_edit_not_year' => 'Ich kenne den Tag und den Monat des Geburtstages dieser Person, aber nicht das Jahr…', + 'information_edit_exact' => 'Ich kenne den genauen Geburtstag dieser Person…', + 'information_edit_birthdate_label' => 'Geburtstag', + 'information_no_work_defined' => 'keine Arbeitsplatz-Informationen angegeben', + 'information_work_at' => 'bei :company', + 'work_add_cta' => 'Ändere Arbeitsplatz-Informationen', + 'work_edit_success' => 'Arbeitsplatz-Informationen aktualisiert', + 'work_edit_title' => 'Ändere :name\'s Beruf-Informationen', + 'work_edit_job' => 'Position (optional)', + 'work_edit_company' => 'Firma (optional)', + 'work_information' => 'Arbeitsinformationen', + + // food preferences + 'food_preferences_add_success' => 'Essensvorlieben gespeichert', + 'food_preferences_edit_description' => 'Vielleicht hat :firstname oder jemand in der :family Familie eine Allergie oder mag einen bestimmten Wein nicht. Vermerke so etwas hier, damit du dich bei der nächsten Einladung zum Abendessen daran erinnerst', + 'food_preferences_edit_description_no_last_name' => 'Vielleicht hat :firstname eine Allergie oder mag einen bestimmten Wein nicht. Vermerke so etwas hier, damit du dich bei der nächsten Einladung zum Abendessen daran erinnerst', + 'food_preferences_edit_title' => 'Gib Essensvorlieben an', + 'food_preferences_edit_cta' => 'Speichere Essensvorlieben', + 'food_preferences_title' => 'Essensvorlieben', + 'food_preferences_cta' => 'Essensvorlieben hinzufügen', + + // reminders + 'reminders_blank_title' => 'Gibt es etwas, an das du über :name erinnert werden willst?', + 'reminders_blank_add_activity' => 'Erinnerung hinzufügen', + 'reminders_add_title' => 'Woran würdest du gerne über :name erinnert werden?', + 'reminders_add_description' => 'Erinnere mich daran…', + 'reminders_add_next_time' => 'Wann möchtest du das nächste mal daran erinnert werden?', + 'reminders_add_once' => 'Erinnere mich daran nur einmal', + 'reminders_add_recurrent' => 'Erinnere mich daran jeden', + 'reminders_add_starting_from' => 'angefangen vom oben angegebenen Datum', + 'reminders_add_cta' => 'Erinnerung hinzufügen', + 'reminders_edit_update_cta' => 'Erinnerung ändern', + 'reminders_add_error_custom_text' => 'Du musst einen Text für die Erinnerung angeben', + 'reminders_create_success' => 'Die Erinnerung wurde erfolgreich hinzugefügt', + 'reminders_delete_success' => 'Die Erinnerung wurde erfolgreich gelöscht', + 'reminders_update_success' => 'Die Erinnerung wurde erfolgreich geändert', + 'reminders_add_optional_comment' => 'Optionaler Kommentar', + + 'reminder_frequency_day' => 'jeden Tag | alle :number Tage', + 'reminder_frequency_week' => 'jede Woche|alle :number Wochen', + 'reminder_frequency_month' => 'jeden Monat|alle :number Monate', + 'reminder_frequency_year' => 'jedes jahr|alle :number Jahre', + 'reminder_frequency_one_time' => 'am :date', + 'reminders_delete_confirmation' => 'Möchtest du diese Erinnerung wirklich löschen?', + 'reminders_delete_cta' => 'löschen', + 'reminders_next_expected_date' => 'am', + 'reminders_cta' => 'Erinnerung hinzufügen', + 'reminders_description' => 'Wir werden eine E-Mail für jede der unten stehenden Erinnerungen verschicken. Erinnerungen werden immer morgens verschickt. Erinnerungen, die automatisch für Geburtstage angelegt wurden, können nicht gelöscht werden. Wenn du dieses Datum ändern willst, dann ändere den Geburtstag des Kontakts.', + 'reminders_one_time' => 'Einmal', + 'reminders_type_week' => 'Woche', + 'reminders_type_month' => 'Monat', + 'reminders_type_year' => 'Jahr', + 'reminders_birthday' => 'Geburtstag von :name', + 'reminders_free_plan_warning' => 'Du befindest dich im kostenlosen Abonnement. Hier werden keine E-Mails versendet. Um die Erinnerungs-E-Mails zu erhalten upgrade deinen Account.', + + // relationships + 'relationship_form_add' => 'Eine neue Beziehung hinzufügen', + 'relationship_form_edit' => 'Eine bestehende Beziehung ändern', + 'relationship_form_is_with' => 'Diese Person ist…', + 'relationship_form_is_with_name' => ':name ist…', + 'relationship_form_add_choice' => 'Wer ist die Beziehung zu?', + 'relationship_form_create_contact' => 'Neue Person hinzufügen', + 'relationship_form_associate_contact' => 'Ein bestehender Kontakt', + 'relationship_form_associate_dropdown' => 'Wählen Sie einen vorhandenen Kontakt aus der Dropdown-Liste unten aus', + 'relationship_form_associate_dropdown_placeholder' => 'Suche und wähle einen bestehenden Kontakt', + 'relationship_form_also_create_contact' => 'Erstellen Sie einen Kontakt-Eintrag für diese Person.', + 'relationship_form_add_description' => 'Dies erlaubt dir diese Person wie jeden anderen Kontakt zu verwalten.', + 'relationship_form_add_no_existing_contact' => 'Sie haben zur Zeit keine Kontakte, die mit :name in Verbindung gebracht werden können.', + 'relationship_delete_confirmation' => 'Sind Sie sicher, dass Sie diese Beziehung löschen wollen? Das Löschen ist dauerhaft.', + 'relationship_unlink_confirmation' => 'Sind Sie sicher, dass Sie diese Beziehung löschen wollen? Diese Person wird nicht gelöscht - nur die Beziehung zwischen den beiden.', + 'relationship_form_add_success' => 'Die Beziehung wurde erfolgreich gesetzt.', + 'relationship_form_deletion_success' => 'Die Beziehung wurde gelöscht.', + + // tasks + 'tasks_title' => 'Aufgaben', + 'tasks_blank_title' => 'Du hast noch keine Aufgaben.', + 'tasks_form_title' => 'Titel', + 'tasks_form_description' => 'Beschreibung (optional)', + 'tasks_add_task' => 'Aufgabe hinzufügen', + 'tasks_delete_success' => 'Die Aufgabe wurde erfolgreich gelöscht', + 'tasks_complete_success' => 'Der Status der Aufgabe wurder erfolgreich geändert', + + // activities + 'activity_title' => 'Aktivitäten', + 'activity_type_category_simple_activities' => 'Einfacher Zeitvertreib', + 'activity_type_category_sport' => 'Sport', + 'activity_type_category_food' => 'Essen', + 'activity_type_category_cultural_activities' => 'Kulturelle Aktivitäten', + 'activity_type_just_hung_out' => 'einfach zusammen Zeit verbracht', + 'activity_type_watched_movie_at_home' => 'zu Hause einen Film gesehen', + 'activity_type_talked_at_home' => 'zu Hause geredet', + 'activity_type_did_sport_activities_together' => 'haben zusammen Sport gemacht', + 'activity_type_ate_at_his_place' => 'bei Ihnen gegessen', + 'activity_type_went_bar' => 'in eine Bar gegangen', + 'activity_type_ate_at_home' => 'zu Hause gegessen', + 'activity_type_picnicked' => 'gepicknickt', + 'activity_type_ate_restaurant' => 'im Restaurant gegessen', + 'activity_type_went_theater' => 'ins Theater gegangen', + 'activity_type_went_concert' => 'zu einem Konzert gegangen', + 'activity_type_went_play' => 'ein Theaterstück angesehen', + 'activity_type_went_museum' => 'ins Museum gegangen', + 'activities_add_activity' => 'Aktivität hinzufügen', + 'activities_add_more_details' => 'Weitere Details hinzufügen', + 'activities_add_emotions' => 'Emotionen hinzufügen', + 'activities_add_category' => 'Kategorie angeben', + 'activities_add_participants_cta' => 'Teilnehmer hinzufügen', + 'activities_item_information' => ':Activity. Fand am :date statt', + 'activities_add_title' => 'Was hast du mit {name} gemacht?', + 'activities_summary' => 'Beschreibe, was ihr gemacht habt', + 'activities_add_pick_activity' => 'Möchten Sie diese Aktivität kategorisieren? Das müssen Sie nicht, aber es wird Ihnen später Statistiken liefern. (optional)', + 'activities_add_date_occured' => 'Die Aktivität war am…', + 'activities_add_participants' => 'Wer hat außer {name} an dieser Aktivität teilgenommen? (optional)', + 'activities_add_emotions_title' => 'Möchtest du protokollieren, wie du dich während dieser Aktivität gefühlt hast? (optional)', + 'activities_blank_title' => 'Behalte im Auge, was du mit {name} unternommen hast und worüber ihr geredet habt', + 'activities_blank_add_activity' => 'Aktivität hinzufügen', + 'activities_add_success' => 'Aktivität erfolgreich hinzugefügt', + 'activities_add_error' => 'Fehler beim Hinzufügen der Aktivität', + 'activities_update_success' => 'Aktivität erfolgreich aktualisiert', + 'activities_delete_success' => 'Aktivität erfolgreich gelöscht', + 'activities_who_was_involved' => 'Wer war beteiligt?', + 'activities_activity' => 'Kategorie', + 'activities_view_activities_report' => 'Aktivitätsbericht anzeigen', + 'activities_profile_title' => 'Aktivitätsbericht zwischen :name und dir', + 'activities_profile_subtitle' => 'Sie haben gesamt :total_activities Aktivitäten mit :name und :activities_last_twelve_months Aktivitäten in den letzten 12 Monaten aufgezeichnet.|Sie haben gesamt :total_activities Aktivitäten mit :name und :activities_last_twelve_months Aktivitäten in den letzten 12 Monaten aufgezeichnet.', + 'activities_profile_year_summary_activity_types' => 'Hier ist eine Aufzeichnung von Aktivitäten, die Sie gemeinsam im letzten Jahr erlebt haben', + 'activities_profile_year_summary' => 'Das haben Sie zwei im :year gemeinsam gemacht', + 'activities_profile_number_occurences' => ':value Aktivität|:value Aktivitäten', + 'activities_list_participants' => 'Teilnehmer ({total}):', + 'activities_list_emotions' => 'Emotionen gefühlt:', + 'activities_list_date' => 'Geschehen am', + 'activities_list_category' => 'Kategorie:', + + // notes + 'notes_create_success' => 'Die Notiz wurde erfolgreich hinzugefügt', + 'notes_update_success' => 'Die Notiz wurde erfolgreich aktualisiert', + 'notes_delete_success' => 'Die Notiz wurde erfolgreich gelöscht', + 'notes_add_cta' => 'Notiz hinzufügen', + 'notes_favorite' => 'Markieren/Markierung entfernen', + 'notes_delete_title' => 'Notiz löschen', + 'notes_delete_confirmation' => 'Möchtest du diese Notiz wirklich löschen?', + + // gifts + 'gifts_title' => 'Geschenke', + 'gifts_add_success' => 'Geschenk erfolgreich hinzugefügt', + 'gifts_delete_success' => 'Geschenk erfolgreich gelöscht', + 'gifts_delete_confirmation' => 'Möchtest du das Geschenk wirklich löschen?', + 'gifts_add_gift' => 'Geschenk hinzufügen', + 'gifts_link' => 'Link', + 'gifts_for' => 'Für: {name}', + 'gifts_delete_cta' => 'Löschen', + 'gifts_add_title' => 'Geschenkverwaltung für :name', + 'gifts_add_gift_idea' => 'Geschenkidee', + 'gifts_add_gift_already_offered' => 'Bereits verschenkt', + 'gifts_add_gift_received' => 'Geschenk erhalten', + 'gifts_add_gift_title' => 'Was ist es für ein Geschenk?', + 'gifts_add_gift_name' => 'Geschenkname', + 'gifts_add_link' => 'Link zur Website (optional)', + 'gifts_add_value' => 'Wert (optional)', + 'gifts_add_comment' => 'Kommentar (optional)', + 'gifts_add_recipient' => 'Empfänger (optional)', + 'gifts_add_recipient_field' => 'Empfänger', + 'gifts_add_photo' => 'Foto (optional)', + 'gifts_add_photo_title' => 'Foto für dieses Geschenk hinzufügen', + 'gifts_add_someone' => 'Dieses Geschenk ist insbesondere für jemanden in {name}\'s Familie', + 'gifts_delete_title' => 'Ein Geschenk löschen', + 'gifts_ideas' => 'Geschenkideen', + 'gifts_offered' => 'Verschenkte Geschenke', + 'gifts_offered_as_an_idea' => 'Als Idee markieren', + 'gifts_received' => 'Erhaltene Geschenke', + 'gifts_view_comment' => 'Kommentar anzeigen', + 'gifts_mark_offered' => 'Als angeboten markieren', + 'gifts_update_success' => 'Das Geschenk wurde erfolgreich aktualisiert', + 'gifts_add_date' => 'Datum (optional)', + + // debts + 'debt_delete_confirmation' => 'Möchtest du die Schulden wirklich löschen?', + 'debt_delete_success' => 'Die Schulden wurden erfolgreich gelöscht', + 'debt_add_success' => 'Die Schulden wurden erfolgreich hinzugefügt', + 'debt_title' => 'Schulden', + 'debt_add_cta' => 'Schulden hinzufügen', + 'debt_you_owe' => 'Du schuldest :amount', + 'debt_they_owe' => ':name schuldet dir :amount', + 'debt_add_title' => 'Schuldenverwaltung', + 'debt_add_you_owe' => 'Du schuldest :name', + 'debt_add_they_owe' => ':name schuldet dir', + 'debt_add_amount' => 'eine Summe von', + 'debt_add_reason' => 'aus folgendem Grund (optional)', + 'debt_add_add_cta' => 'Schulden hinzufügen', + 'debt_edit_update_cta' => 'Schulden bearbeiten', + 'debt_edit_success' => 'Die Schulden wurden erfolgreich aktualisiert', + 'debts_blank_title' => 'Verwalte die Schulden zwischen dir und :name', + + // tags + 'tag_edit' => 'Tag bearbeiten', + 'tag_add' => 'Stichworte hinzufügen', + 'tag_add_search' => 'Stichwort hinzufügen oder suchen', + 'tag_no_tags' => 'Bisher keine Stichworte vorhanden', + + // Introductions + 'introductions_sidebar_title' => 'Wie ihr euch kennengelernt habt', + 'introductions_blank_cta' => 'Beschreibe, wie du :name kennengelernt hast', + 'introductions_title_edit' => 'Wie hast du :name kennengelernt?', + 'introductions_additional_info' => 'Beschreibe, wo und wann ihr euch kennengelernt habt', + 'introductions_edit_met_through' => 'Hat euch jemand vorgestellt?', + 'introductions_no_met_through' => 'Keiner', + 'introductions_first_met_date' => 'Datum des ersten Treffens', + 'introductions_no_first_met_date' => 'Ich weiß nicht mehr wann wir uns das erste mal getroffen haben', + 'introductions_first_met_date_known' => 'An diesem Datum haben wir uns das erste mal getroffen', + 'introductions_add_reminder' => 'Erstelle eine Erinnerung für den Jahrestag unseres ersten Zusammentreffens', + 'introductions_update_success' => 'Euer erstes Kennenlernen wurde erfolgreich geändert', + 'introductions_met_through' => 'Kennengelernt durch :name', + 'introductions_met_date' => 'Am :date', + 'introductions_reminder_title' => 'Jahrestag eures ersten Zusammentreffens', + + // Deceased + 'deceased_reminder_title' => 'Todestag von :name', + 'deceased_mark_person_deceased' => 'Als verstorben markieren', + 'deceased_know_date' => 'Ich weiß das Datum an dem diese Person verstarb', + 'deceased_add_reminder' => 'Erstelle eine Erinnerung für den Todestag', + 'deceased_label' => 'Verstorben', + 'deceased_date_label' => 'Todestag', + 'deceased_label_with_date' => 'Verstorben am :date', + 'deceased_age' => 'Todesalter', + + // Contact information + 'contact_info_title' => 'Kontaktinformationen', + 'contact_info_form_content' => 'Inhalt', + 'contact_info_form_contact_type' => 'Kontakt Art', + 'contact_info_form_personalize' => 'Anpassen', + 'contact_info_address' => 'Wohnt in', + + // Addresses + 'contact_address_title' => 'Adressen', + 'contact_address_form_name' => 'Titel (optional)', + 'contact_address_form_street' => 'Straße (optional)', + 'contact_address_form_city' => 'Stadt (optional)', + 'contact_address_form_province' => 'Bundesland (optional)', + 'contact_address_form_postal_code' => 'Postleitzahl (optional)', + 'contact_address_form_country' => 'Land (optional)', + 'contact_address_form_latitude' => 'Geographische Breite (nur Nummern) (optional)', + 'contact_address_form_longitude' => 'Geographische Länge (nur Nummern) (optional)', + + // Pets + 'pets_kind' => 'Tierart', + 'pets_name' => 'Name (optional)', + 'pets_create_success' => 'Das Haustier wurde erfolgreich hinzugefügt', + 'pets_update_success' => 'Das Haustier wurde erfolgreich geändert', + 'pets_delete_success' => 'Das Haustier wurde erfolgreich entfernt', + 'pets_title' => 'Haustiere', + 'pets_reptile' => 'Reptil', + 'pets_bird' => 'Vogel', + 'pets_cat' => 'Katze', + 'pets_dog' => 'Hund', + 'pets_fish' => 'Fisch', + 'pets_hamster' => 'Hamster', + 'pets_horse' => 'Pferd', + 'pets_rabbit' => 'Hase', + 'pets_rat' => 'Ratte', + 'pets_small_animal' => 'Kleintier', + 'pets_other' => 'Anderes', + + // life events + 'life_event_list_tab_life_events' => 'Lebensereignisse', + 'life_event_list_tab_other' => 'Notizen, Erinnerungen, …', + 'life_event_list_title' => 'Lebensereignisse', + 'life_event_blank' => 'Notiere dir zukünftige Lebensereignisse von {name}.', + 'life_event_list_cta' => 'Lebensereignis hinzufügen', + 'life_event_create_category' => 'Alle Kategorien', + 'life_event_create_life_event' => 'Lebensereignis hinzufügen', + 'life_event_create_default_title' => 'Titel (optional)', + 'life_event_create_default_story' => 'Geschichte (optional)', + 'life_event_create_date' => 'Du musst keinen Monat oder Tag angeben – nur das Jahr ist obligatorisch.', + 'life_event_create_default_description' => 'Fügen Sie Informationen hinzu', + 'life_event_create_add_yearly_reminder' => 'Eine jährliche Erinnerung für dieses Ereignis hinzufügen', + 'life_event_create_success' => 'Das Lebensereignis wurde hinzugefügt', + 'life_event_delete_title' => 'Lebensereignis löschen', + 'life_event_delete_description' => 'Möchten Sie das Lebensereignis löschen? Dies kann nicht rückgängig gemacht werden.', + 'life_event_delete_success' => 'Das Ereignis wurde gelöscht', + 'life_event_date_it_happened' => 'Tag an dem es passierte', + 'life_event_category_work_education' => 'Arbeit & Ausbildung', + 'life_event_category_family_relationships' => 'Familie & Beziehungen', + 'life_event_category_home_living' => 'Zuhause & Wohnen', + 'life_event_category_health_wellness' => 'Gesundheit & Wellness', + 'life_event_category_travel_experiences' => 'Reisen & Erfahrungen', + 'life_event_sentence_new_job' => 'Neuen Arbeitsplatz angetreten', + 'life_event_sentence_retirement' => 'Im Ruhestand', + 'life_event_sentence_new_school' => 'Schulbeginn', + 'life_event_sentence_study_abroad' => 'Studium im Ausland', + 'life_event_sentence_volunteer_work' => 'Anfang vom Praktikum', + 'life_event_sentence_published_book_or_paper' => 'Veröffentlicht ein Buch', + 'life_event_sentence_military_service' => 'Militärdienst angetreten', + 'life_event_sentence_new_relationship' => 'Beginn einer Beziehung', + 'life_event_sentence_engagement' => 'Hat sich verlobt', + 'life_event_sentence_marriage' => 'Verheiratet seit', + 'life_event_sentence_anniversary' => 'Jahrestag', + 'life_event_sentence_expecting_a_baby' => 'Erwartet ein baby', + 'life_event_sentence_new_child' => 'Hat ein Kind', + 'life_event_sentence_new_family_member' => 'Familienmitglied hinzugefügt', + 'life_event_sentence_new_pet' => 'Hat ein Haustier', + 'life_event_sentence_end_of_relationship' => 'Ende einer Beziehung', + 'life_event_sentence_loss_of_a_loved_one' => 'Einen geliebten Menschen verloren', + 'life_event_sentence_moved' => 'Umgezogen', + 'life_event_sentence_bought_a_home' => 'Ein Haus gekauft', + 'life_event_sentence_home_improvement' => 'Hat das Zuhause renoviert', + 'life_event_sentence_holidays' => 'Urlaub machen', + 'life_event_sentence_new_vehicle' => 'Neues Fahrzeug erhalten', + 'life_event_sentence_new_roommate' => 'Mitbewohner/in bekommen', + 'life_event_sentence_overcame_an_illness' => 'Krankheit überwunden', + 'life_event_sentence_quit_a_habit' => 'Gewohnheit beendet', + 'life_event_sentence_new_eating_habits' => 'Neue Essgewohnheit begonnen', + 'life_event_sentence_weight_loss' => 'Gewicht abgenommen', + 'life_event_sentence_wear_glass_or_contact' => 'Hat angefangen, eine Brille oder Kontaktlinsen zu tragen', + 'life_event_sentence_broken_bone' => 'Knochen gebrochen', + 'life_event_sentence_removed_braces' => 'Zahnspange entfernt', + 'life_event_sentence_surgery' => 'Hatte eine Operation', + 'life_event_sentence_dentist' => 'Ging zum Zahnarzt', + 'life_event_sentence_new_sport' => 'Hat eine Sportart begonnen', + 'life_event_sentence_new_hobby' => 'Ein Hobby begonnen', + 'life_event_sentence_new_instrument' => 'Ein neues Musik-Instrument gelernt', + 'life_event_sentence_new_language' => 'Eine neue Sprache gelernt', + 'life_event_sentence_tattoo_or_piercing' => 'Hat einen Tattoo oder Piercing bekommen', + 'life_event_sentence_new_license' => 'Lizenz erhalten', + 'life_event_sentence_travel' => 'Reise', + 'life_event_sentence_achievement_or_award' => 'Wurde ausgezeichnet', + 'life_event_sentence_changed_beliefs' => 'Änderte Überzeugung', + 'life_event_sentence_first_word' => 'Zum ersten mal Gesprochen', + 'life_event_sentence_first_kiss' => 'Der erste Kuss', + + // documents + 'document_list_title' => 'Dokumente', + 'document_list_cta' => 'Dokument hochladen', + 'document_list_blank_desc' => 'Hier können Sie Dokumente im Zusammenhang mit dieser Person speichern.', + 'document_upload_zone_cta' => 'Datei hochladen', + 'document_upload_zone_progress' => 'Das Dokument wird hochgeladen…', + 'document_upload_zone_error' => 'Es ist ein Fehler beim hochladen des Dokumentes aufgetreten, bitte versuchen sie es erneut.', + + // Photos + 'photo_title' => 'Fotos', + 'photo_list_title' => 'Zugehörige Fotos', + 'photo_list_cta' => 'Foto hochladen', + 'photo_list_blank_desc' => 'Sie können Bilder zu diesem Kontakt speichern. Jetzt hochladen!', + 'photo_upload_zone_cta' => 'Foto hochladen', + 'photo_current_profile_pic' => 'Aktuelles Profilbild', + 'photo_make_profile_pic' => 'Zu Profilbild machen', + 'photo_delete' => 'Bild löschen', + 'photo_next' => 'Nächstes Bild ❯', + 'photo_previous' => '❮ Vorheriges Bild', + + // Avatars + 'avatar_change_title' => 'Avatar ändern', + 'avatar_question' => 'Welchen Avatar möchtest du verwenden?', + 'avatar_default_avatar' => 'Den Standard-Avatar', + 'avatar_adorable_avatar' => 'Den niedlichen Avatar', + 'avatar_gravatar' => 'Den Gravatar, welcher der E-Mail-Adresse dieser Person zugeordnet ist. Gravatar ist ein globales System, mit dem Benutzer E-Mail-Adressen mit Fotos verknüpfen können.', + 'avatar_current' => 'Aktuellen Avatar beibehalten', + 'avatar_photo' => 'Ein Foto hochladen', + 'avatar_crop_new_avatar_photo' => 'Neuen Avatar zuschneiden', + + // emotions + 'emotion_this_made_me_feel' => 'Dadurch fühlen sie sich…', + + // logs + 'auditlogs_link' => 'Verlauf', + 'auditlogs_title' => 'Alles, was :name passiert ist', + 'auditlogs_breadcrumb' => 'Verlauf', + 'auditlogs_author' => 'Nach :name am :date', + + // contact field label + 'contact_field_label_home' => 'Zuhause', + 'contact_field_label_work' => 'Arbeit', + 'contact_field_label_cell' => 'Mobil', + 'contact_field_label_fax' => 'Fax', + 'contact_field_label_pager' => 'Pager', + 'contact_field_label_main' => 'Hauptinformationen', + 'contact_field_label_other' => 'Sonstiges', + 'contact_field_label_personal' => 'Persönlich', +]; diff --git a/resources/lang/de/reminder.php b/resources/lang/de/reminder.php new file mode 100644 index 0000000..9694f1b --- /dev/null +++ b/resources/lang/de/reminder.php @@ -0,0 +1,16 @@ + 'Gratuliere', + 'type_phone_call' => 'Anrufen', + 'type_lunch' => 'Essen gehen mit', + 'type_hangout' => 'Treffen mit', + 'type_email' => 'E-Mail', + 'type_birthday_kid' => 'Gratuliere dem Kind von', +]; diff --git a/resources/lang/de/settings.php b/resources/lang/de/settings.php new file mode 100644 index 0000000..3a90c5e --- /dev/null +++ b/resources/lang/de/settings.php @@ -0,0 +1,557 @@ + 'Kontoeinstellungen', + 'sidebar_personalization' => 'Personalisierung', + 'sidebar_settings_storage' => 'Speicher', + 'sidebar_settings_export' => 'Daten exportieren', + 'sidebar_settings_users' => 'Benutzer', + 'sidebar_settings_subscriptions' => 'Abonnement', + 'sidebar_settings_import' => 'Daten importieren', + 'sidebar_settings_tags' => 'Tag-Verwaltung', + 'sidebar_settings_api' => 'Schnittstelle (API)', + 'sidebar_settings_dav' => 'DAV-Ressourcen', + 'sidebar_settings_security' => 'Sicherheit', + 'sidebar_settings_auditlogs' => 'Prüfprotokolle', + + 'title_general' => 'Allgemeine Information', + 'title_i18n' => 'Internationale Einstellungen', + 'title_layout' => 'Layout', + + 'me_title' => 'Dein eigener Kontakt', + 'me_help' => 'Dies ist der Kontakt, der Sie in Monica vertritt', + 'me_select' => 'Wählen Sie einen Kontakt aus', + 'me_no_contact' => 'Es wurde kein Kontakt ausgewählt.', + 'me_select_click' => 'Klicken Sie hier, um einen Kontakt auszuwählen.', + 'me_remove_contact' => 'Entfernen Sie die Zuordnung', + 'me_choose' => 'Wähle dich selbst', + 'me_choose_placeholder' => 'Wähle dich selbst', + + 'export_title' => 'Exportiere die Daten deines Kontos', + 'export_be_patient' => 'Button klicken um den Export zu starten. Dies kann mehrere Minuten dauern – sei bitte geduldig und klicke nicht mehrfach auf den Button.', + 'export_title_sql' => 'Nach SQL exportieren', + 'export_sql_explanation' => 'Der SQL-Export ermöglicht es dir deine Daten in einer eigenen monica-Installation zu importieren. Dies ist nur sinnvoll, wenn du einen eigenen Server besitzt.', + 'export_sql_cta' => 'SQL exportieren', + 'export_sql_link_instructions' => 'Hinweis: lies die Anleitung um mehr über das Importieren in die eigene Installation zu erfahren.', + 'export_title_json' => 'Nach JSON exportieren', + 'export_submitted' => 'Ihr Export wurde übermittelt, er wird in wenigen Augenblicken verfügbar sein…', + 'export_json_explanation' => 'Exportieren Ihrer Daten im JSON-Format für Sicherungszwecke.', + 'export_json_beta' => 'JSON-Export ist eine Vorschaufunktion. Sag uns, was du davon hältst:', + 'export_json_cta' => 'Nach JSON exportieren', + 'export_header_type' => 'Typ', + 'export_header_timestamp' => 'Erstellungsdatum', + 'export_header_status' => 'Status', + 'export_header_actions' => 'Aktionen', + 'export_last_title' => 'Letzte Exporte', + 'export_empty_title' => 'Noch keine Exporte', + 'export_type_json' => 'JSON-Export', + 'export_type_sql' => 'SQL-Export', + 'export_status_todo' => 'Übermittelt', + 'export_status_doing' => 'In Arbeit', + 'export_status_done' => 'Fertig', + 'export_status_failed' => 'Fehlgeschlagen', + 'export_not_done' => 'Download nicht möglich, dieser Export ist noch nicht abgeschlossen.', + + 'firstname' => 'Vorname', + 'lastname' => 'Nachname', + 'name_order' => 'Namensortierrichtung', + 'name_order_firstname_lastname' => ' – Max Mustermann', + 'name_order_lastname_firstname' => ' – Mustermann Max', + 'name_order_firstname_lastname_nickname' => ' () – Max Mustermann (Rambo)', + 'name_order_firstname_nickname_lastname' => ' () – Max (Rambo) Mustermann', + 'name_order_lastname_firstname_nickname' => ' () – Mustermann Max (Rambo)', + 'name_order_lastname_nickname_firstname' => ' () – Mustermann (Rambo) Max', + 'name_order_nickname_firstname_lastname' => ' ( ) – Rambo (Max Mustermann)', + 'name_order_nickname_lastname_firstname' => ' ( ) – Rambo (Doe John)', + 'name_order_nickname' => ' – Rambo', + 'currency' => 'Währung', + 'name' => 'Dein Name: :name', + 'email' => 'E-Mail-Adresse', + 'email_placeholder' => 'E-Mail eingeben', + 'email_help' => 'Mit dieser Adresse kannst du dich einloggen und dorthin sendet Monica auch deine Erinnerungen.', + 'timezone' => 'Zeitzone', + 'temperature_scale' => 'Temperaturskala', + 'temperature_scale_fahrenheit' => 'Fahrenheit (°F)', + 'temperature_scale_celsius' => 'Celsius (°C)', + 'layout' => 'Ansicht', + 'layout_small' => 'Maximal 1200 Pixel breit', + 'layout_big' => 'Gesamte Breite des Browsers', + 'save' => 'Einstellungen speichern', + 'delete_title' => 'Konto löschen', + 'delete_desc' => 'Möchtest du dein Konto löschen? Dies kann nicht rückgangig gemacht werden und alle deine Daten werden dauerhaft gelöscht. Wenn du ein Abonnement hast, wird es sofort storniert.', + 'delete_other_desc' => 'Deine Daten in der Hauptdatenbank werden umgehend gelöscht. Wie in unseren Datenschutzrichtlinien beschrieben, führen wir täglich verschlüsselte Sicherungen durch, welche 30 Tage lang aufbewahrt und danach vollständig gelöscht werden. Wir können keine einzelnen Daten vor diesem Zeitraum aus den Backups löschen. Alle deine Daten werden innerhalb von 31 Tagen nach Löschung deines Kontos vollständig gelöscht.', + 'reset_desc' => 'Möchtest du dein Konto zurücksetzen? Dies entfernt alle deine Kontakte und die zugehörigen Daten. Dein Konto bleibt erhalten.', + 'reset_title' => 'Konto zurücksetzen', + 'reset_cta' => 'Konto zurücksetzen', + 'reset_notice' => 'Möchtest du dein Konto wirklich zurücksetzen? Dies ist dauerhaft und kann nicht rückgängig gemacht werden.', + 'reset_success' => 'Dein Konto wurde erfolgreich zurückgesetzt.', + 'delete_notice' => 'Bist du sicher, dass du dein Konto löschen möchtest? Dies ist dauerhaft und kann nicht rückgängig gemacht werden. Alle deine Daten werden gelöscht und können nicht wiederhergestellt werden.', + 'delete_cta' => 'Konto löschen', + 'settings_success' => 'Einstellungen aktualisiert!', + 'locale' => 'Sprache der Anwendung', + 'locale_help' => 'Möchtest du bei der Übersetzung von Monica helfen oder eine neue Sprache hinzufügen? Bitte folge diesem Link für weitere Informationen.', + 'locale_ar' => 'Arabisch', + 'locale_cs' => 'Tschechisch', + 'locale_de' => 'Deutsch', + 'locale_el' => 'Griechisch', + 'locale_en' => 'Englisch', + 'locale_en-GB' => 'Englisch (Vereinigtes Königreich)', + 'locale_es' => 'Spanisch', + 'locale_fr' => 'Französisch', + 'locale_he' => 'Hebräisch', + 'locale_hr' => 'Kroatisch', + 'locale_id' => 'Indonesisch', + 'locale_it' => 'Italienisch', + 'locale_ja' => 'Japanisch', + 'locale_nl' => 'Niederländisch', + 'locale_pt' => 'Portugiesisch', + 'locale_pt-BR' => 'Brasilianisches Portugiesisch', + 'locale_ru' => 'Russisch', + 'locale_sv' => 'Schwedisch', + 'locale_vi' => 'Vietnamesisch', + 'locale_zh' => 'Vereinfachtes Chinesisch', + 'locale_zh-TW' => 'Chinesisch (Traditionell)', + 'locale_tr' => 'Türkisch', + + 'security_title' => 'Sicherheit', + 'security_help' => 'Ändere die Sicherheitseinstellungen für dein Konto.', + 'password_change' => 'Passwort ändern', + 'password_current' => 'Aktuelles Passwort', + 'password_current_placeholder' => 'Aktuelles Passwort', + 'password_new1' => 'Neues Passwort', + 'password_new1_placeholder' => 'Gib dein neues Passwort ein', + 'password_new2' => 'Bestätige dein neues Passwort', + 'password_new2_placeholder' => 'Gib dein neues Passwort erneut ein', + 'password_btn' => 'Passwort ändern', + '2fa_title' => 'Zwei-Faktor-Authentifizierung', + '2fa_otp_title' => 'Zwei-Faktor-Authentifizierung Mobileapp', + '2fa_enable_title' => 'Zwei-Faktor-Authentifizierung aktivieren', + '2fa_enable_description' => 'Richte die Zwei-Faktor-Authentifizierung ein um die Sicherheit deines Kontos zu erhöhen.', + '2fa_enable_otp' => 'Öffne deine Zwei-Faktor-Authentifizierungs-App und scanne den folgenden QR-Code:', + '2fa_enable_otp_help' => 'Falls deine Zwei-Faktor-Authentifizierungs-App keine QR-Codes unterstützt, gib folgenden Code manuell ein:', + '2fa_enable_otp_validate' => 'Bitte bestätige dein neu eingerichtetes Gerät:', + '2fa_enable_success' => 'Zwei-Faktor-Authentifizierung ist nun aktiviert', + '2fa_enable_error' => 'Fehler beim Einrichten der Zwei-Faktor-Authentifizierung', + '2fa_enable_error_already_set' => 'Zwei-Faktor-Authentifizierung ist bereits aktiviert', + '2fa_disable_title' => 'Zwei-Faktor-Authentifizierung deaktivieren', + '2fa_disable_description' => 'Deaktiviere die Zwei-Faktor-Authentifizierung für dein Konto. Achtung, dies reduziert die Sicherheit deines Kontos erheblich!', + '2fa_disable_success' => 'Zwei-Faktor-Authentifizierung ist nun deaktiviert', + '2fa_disable_error' => 'Fehler beim Ausschalten der Zwei-Faktor-Authentifizierung', + + 'webauthn_title' => 'Sicherheitsschlüssel — WebAuthn Protokoll', + 'webauthn_enable_description' => 'Neuen Sicherheitsschlüssel hinzufügen', + 'webauthn_key_name_help' => 'Geben Sie Ihren Schlüssel einen Namen.', + 'webauthn_key_name' => 'Schlüssel name:', + 'webauthn_success' => 'Ihr Schlüssel wurde erkannt und validiert.', + 'webauthn_last_use' => 'Letzte Verwendung: {timestamp}', + 'webauthn_delete_confirmation' => 'Diesen Schlüssel wirklich löschen?', + 'webauthn_delete_success' => 'Schlüssel gelöscht', + 'webauthn_insertKey' => 'Fügen Sie Ihren Sicherheitsschlüssel ein.', + 'webauthn_buttonAdvise' => 'Sofern Ihr Sicherheitsschlüssel einen Knopf hat, drücken Sie ihn.', + 'webauthn_noButtonAdvise' => 'Wenn nicht, entfernen Sie ihn und fügen Sie ihn erneut ein.', + 'webauthn_not_supported' => 'Ihr Browser unterstützt derzeit nicht WebAuthn.', + 'webauthn_not_secured' => 'WebAuthn unterstützt nur sichere Verbindungen. Bitte laden Sie diese Seite mit https Schema.', + 'webauthn_error_already_used' => 'Dieser Schlüssel ist bereits registriert. Es ist nicht erforderlich, ihn erneut zu registrieren.', + 'webauthn_error_not_allowed' => 'Die Operation ist entweder abgelaufen oder nicht zulässig.', + + 'recovery_title' => 'Wiederherstellungsschlüssel', + 'recovery_show' => 'Wiederherstellungscodes generieren', + 'recovery_copy_help' => 'Codes in die Zwischenablage kopieren', + 'recovery_help_intro' => 'Dies sind Ihre Wiederherstellungscodes:', + 'recovery_help_information' => 'Sie können jeden Wiederherstellungscode nur einmal verwenden.', + 'recovery_clipboard' => 'Codes wurden in die Zwischenablage kopiert.', + 'recovery_generate' => 'Neue Codes generieren…', + 'recovery_generate_help' => 'Beachte das die Generierung neuer Codes zuvor generierte Codes ungültig macht.', + 'recovery_already_used_help' => 'Dieser Code wurde bereits verwendet.', + + 'users_list_title' => 'Benutzer, die Zugriff auf dein Konto haben', + 'users_list_add_user' => 'Einen Benutzer einladen', + 'users_list_you' => 'Das bist du', + 'users_list_invitations_title' => 'Ausstehende Einladungen', + 'users_list_invitations_explanation' => 'Unten stehen Personen, die du als Mithelfer eingeladen hast.', + 'users_list_invitations_invited_by' => 'eingeladen von :name', + 'users_list_invitations_sent_date' => 'versendet :date', + 'users_blank_title' => 'Du bist der Einzige mit Zugriff auf dieses Konto.', + 'users_blank_add_title' => 'Möchtest du jemand anderes einladen?', + 'users_blank_description' => 'Diese Person wird den gleichen Zugriff auf das System haben wie du und wird Kontakte hinzufügen, ändern und löschen können.', + 'users_blank_cta' => 'Jemanden einladen', + 'users_add_title' => 'Einen neuen Benutzer per E-Mail zu deinem Konto einladen', + 'users_add_description' => 'Diese Person wird den gleichen Zugriff wie du haben, einschließlich der Einladung oder des Löschens anderer Benutzer, inklusive dir selbst. Vergewissere dich, dass du dieser Person vertraust, bevor du ihr Zugang gewährst.', + 'users_add_email_field' => 'Gib die E-Mail-Adresse der Person an, die du einladen möchtest', + 'users_add_confirmation' => 'Ich bestätige, dass ich diesen Benutzer zu meinem Account einladen möchte. Ich verstehe, dass diese Person Zugriff auf ALLE meine Daten hat und sieht, was ich sehe.', + 'users_add_cta' => 'Benutzer per E-Mail einladen', + 'users_accept_title' => 'Einladung annehmen und neues Benutzerkonto erstellen', + 'users_error_please_confirm' => 'Bitte bestätige, dass du diesen Benutzer einladen willst', + 'users_error_email_already_taken' => 'Diese E-Mail-Adresse ist bereits vergeben. Bitte eine andere wählen.', + 'users_error_already_invited' => 'Diesen Benutzer hast du schon eingeladen. Bitte andere E-Mail-Adresse wählen.', + 'users_error_email_not_similar' => 'Dies ist nicht die E-Mail-Adresse der Person, die dich eingeladen hat.', + 'users_invitation_deleted_confirmation_message' => 'Die Einladung wurde erfolgreich gelöscht', + 'users_invitations_delete_confirmation' => 'Möchtest du die Einladung wirklich löschen?', + 'users_list_delete_confirmation' => 'Möchtest du den Benutzer wirklich aus deinem Konto entfernen?', + 'users_invitation_need_subscription' => 'Das Hinzufügen von weiteren Benutzern erfordert ein Abonnement.', + + 'subscriptions_account_current_plan' => 'Dein aktuelles Abonnement', + 'subscriptions_account_current_legacy' => 'Aktueller Plan, nicht mehr wählbar:', + 'subscriptions_account_current_paid_plan' => 'Sie befinden sich im :name Abo. Vielen Dank für die Anmeldung.', + + 'subscriptions_account_next_billing_title' => 'Nächste Rechnung', + 'subscriptions_account_next_billing' => 'Ihr Abonnement wird automatisch erneuert am :date.', + 'subscriptions_account_bill_monthly' => 'Wir berechnen Ihnen :price für einen weiteren Monat.', + 'subscriptions_account_bill_annual' => 'Wir berechnen Ihnen :price für ein weiteres Jahr.', + 'subscriptions_account_change' => 'Abo wechseln', + + 'subscriptions_account_cancel_title' => 'Abonnement kündigen', + 'subscriptions_account_cancel_action' => 'Abonnement kündigen', + 'subscriptions_account_cancel' => 'Sie können Ihr Abonnement jederzeit kündigen.', + 'subscriptions_account_free_plan' => 'Du hast das kostenlose Abonnement.', + 'subscriptions_account_free_plan_upgrade' => 'Du kannst dein Konto auf :name upgraden, was $:price pro Monat kostet. Es beinhaltet folgende Vorteile:', + 'subscriptions_account_free_plan_benefits_users' => 'Beliebige Anzahl von Benutzern', + 'subscriptions_account_free_plan_benefits_reminders' => 'Erinnerungen per email', + 'subscriptions_account_free_plan_benefits_import_data_vcard' => 'Importiere Kontakte über vCards', + 'subscriptions_account_free_plan_benefits_support' => 'Unterstütze das Projekt auf lange Sicht, so dass wir mehr großartige Features umsetzen können.', + 'subscriptions_account_upgrade' => 'Konto upgraden', + 'subscriptions_account_upgrade_title' => 'Upgrade Monica heute und mache deine persönlichen Beziehungen gehaltvoller.', + 'subscriptions_account_upgrade_choice' => 'Wähle eines der Abos und schließe dich :customers Personen an, die bereits die Premium-Version von Monica nutzen.', + 'subscriptions_account_update_title' => 'Abonnement aktualisieren', + 'subscriptions_account_update_description' => 'Hier können Sie das Intervall Ihres Abonnements ändern.', + 'subscriptions_account_update_information' => 'Der neue Betrag wird ihnen unmittelbar in Rechnung gestellt. Ihr Abonnement verlängert sich um den von Ihnen gewählten Zeitraum.', + 'subscriptions_account_invoices' => 'Rechnungen', + 'subscriptions_account_invoices_download' => 'Herunterladen', + 'subscriptions_account_invoices_subscription' => 'Abonnement von :startDate bis :endDate', + 'subscriptions_account_payment' => 'Wie möchtest du bezahlen?', + 'subscriptions_account_confirm_payment' => 'Ihre Zahlung ist derzeit unvollständig, bitte bestätigen Sie die Zahlung.', + 'subscriptions_downgrade_title' => 'Konto auf kostenlose Variante downgraden', + 'subscriptions_downgrade_limitations' => 'Die kostenlose Variante hat Einschränkungen. um downdgraden zu können müssen folgende Dinge zutreffen:', + 'subscriptions_downgrade_rule_users' => 'Du darfst nur einen Benutzer in deinem Konto haben', + 'subscriptions_downgrade_rule_users_constraint' => 'Du hast derzeit 1 Benutzer in deinem Konto. | Du hast derzeit :count Benutzer in deinem Konto.', + 'subscriptions_downgrade_rule_invitations' => 'Du darfst keine ausstehenden Einladungen haben', + 'subscriptions_downgrade_rule_invitations_constraint' => 'Du hast aktuell eine ausstehende Einladung.|Du hast aktuell :count ausstehende Einladungen.', + 'subscriptions_downgrade_rule_contacts' => 'Sie dürfen nicht mehr als :number aktive Kontakte haben', + 'subscriptions_downgrade_rule_contacts_constraint' => 'Sie haben derzeit 1 Kontakt .|Sie haben derzeit :count contacts.', + 'subscriptions_downgrade_rule_contacts_archive' => 'Wir können auch alle Ihre Kontakte für Sie archivieren: – das würde diese Regel deaktivieren und Sie mit dem Herabstufungsprozess Ihres Kontos fortfahren lassen.', + 'subscriptions_downgrade_cta' => 'Zurückstufen', + 'subscriptions_downgrade_success' => 'Du hast das kostenlose Abonnement!', + 'subscriptions_downgrade_thanks' => 'Vielen Dank, dass du das kostenpflichtige Abo ausprobiert hast. Wir fügen kontinuierlich weitere Funktionen hinzu. Vielleicht hast du in Zukunft ja wieder Interesse daran ein Abo abzuschließen.', + 'subscriptions_back' => 'Zurück zu Einstellungen', + 'subscriptions_upgrade_title' => 'Konto upgraden', + 'subscriptions_upgrade_choose' => 'Du hast das :plan Abonnement ausgewählt.', + 'subscriptions_upgrade_infos' => 'Wir freuen uns sehr. Bitte gebe deine Zahlungsinformationen unten ein.', + 'subscriptions_upgrade_name' => 'Name auf der Karte', + 'subscriptions_upgrade_zip' => 'Postleitzahl / ZIP-Code', + 'subscriptions_upgrade_credit' => 'Kreditkarte', + 'subscriptions_upgrade_submit' => 'Zahlen {amount}', + 'subscriptions_upgrade_charge' => 'Wir belasten deine Kreditkarte mit :price. Die nächste Gebühr wird am :date fällig sein. Wenn du es dir irgendwann anders überlegst, kannst du jederzeit ohne weitere Angaben kündigen.', + 'subscriptions_upgrade_charge_handled' => 'Die Zahlung erfolgt über Stripe. Keine Kreditkarteninformationen gelangen auf unsere Server.', + 'subscriptions_upgrade_success' => 'Danke! Du bist nun angemeldet.', + 'subscriptions_upgrade_thanks' => 'Willkommen in der Community von Leuten, die versuchen die Welt zu einem besseren Ort zu machen.', + + 'subscriptions_payment_confirm_title' => 'Zahlung bestätigen', + 'subscriptions_payment_confirm_information' => 'Zur Bearbeitung Ihrer Zahlung ist eine zusätzliche Bestätigung erforderlich. Bitte bestätigen Sie Ihre Zahlung, indem Sie Ihre Zahlungsinformationen unten ausfüllen.', + 'subscriptions_payment_succeeded_title' => 'Zahlung erfolgreich', + 'subscriptions_payment_succeeded' => 'Diese Zahlung wurde bereits erfolgreich bestätigt.', + 'subscriptions_payment_cancelled_title' => 'Zahlung storniert', + 'subscriptions_payment_cancelled' => 'Der Bezahlvorgang wurde abgebrochen.', + 'subscriptions_payment_error_name' => 'Bitte geben Sie Ihren Namen ein.', + 'subscriptions_payment_success' => 'Zahlung wurde erfolgreich ausgeführt.', + + 'subscriptions_pdf_title' => 'Dein :name monatliches Abonnement', + 'subscriptions_plan_frequency_year' => ':amount / Jahr', + 'subscriptions_plan_frequency_month' => ':amount / Monat', + 'subscriptions_plan_choose' => 'Bitte ein Paket auswählen', + 'subscriptions_plan_year_title' => 'Jährlich zahlen', + 'subscriptions_plan_year_bonus' => 'Ein ganzes Jahr lang keine Gedanken mehr machen', + 'subscriptions_plan_month_title' => 'Monatlich zahlen', + 'subscriptions_plan_month_bonus' => 'Jederzeit kündbar', + 'subscriptions_plan_include1' => 'Bei deinem Upgrade inklusive:', + 'subscriptions_plan_include2' => 'Unbegrenzte Anzahl an Nutzern • Erinnerungen per E-Mail • Importieren per vCard • Personalisierung der Kontaktseiten', + 'subscriptions_plan_include3' => '100% der Einnahmen fließen in die Entwicklung dieses großartigen Open-Source-Projektes.', + 'subscriptions_help_title' => 'Weitere Details, die dich interessieren könnten', + 'subscriptions_help_opensource_title' => 'Was ist ein Open-Source-Projekt?', + 'subscriptions_help_opensource_desc' => 'Monica ist ein Open-Source-Projekt. Das bedeutet, es wird von einer Community erstellt, die einfach ein tolles Programm der Allgemeinheit zur Verfügung stellen will. Open-Source bedeutet, dass der Quellcode auf GitHub öffentlich zugänglich ist und von jedermann eingesehen, verändert oder erweitert werden kann. Alle Einnahmen werden genutzt um das Programm zu verbessern, schnellere Server zu erwerben und andere Kosten zu bezahlen. Vielen Dank für deine Hilfe. Wir könnten das Ganze nicht ohne dich schaffen.', + 'subscriptions_help_limits_title' => 'Gibt es im kostenlosen Abo eine Begrenzung bei der Anzahl an Kontakten, die man haben kann?', + 'subscriptions_help_limits_plan' => 'Ja. Kostenlose Pläne ermöglichen es Ihnen, :number Kontakte zu verwalten.', + 'subscriptions_help_discounts_title' => 'Gibt es Ermäßigungen für gemeinnützige Organisationen und Bildungseinrichtungen?', + 'subscriptions_help_discounts_desc' => 'Ja! Monica ist kostenlos für Schüler, Studenten und gemeinnützige Organisationen. Kontaktiere einfach den Support mit einem entsprechenden Nachweis und wir werden den speziellen Status auf deinen Account anwenden.', + 'subscriptions_help_change_title' => 'Was passiert, wenn ich meine Meinung ändere?', + 'subscriptions_help_change_desc' => 'Du kannst jederzeit ohne weiteres selber kündigen, du musst dazu nicht den Support kontaktieren. Laufende Abos werden jedoch nicht zurückerstattet.', + + 'stripe_error_card' => 'Ihre Karte wurde abgelehnt. Grund: :message', + 'stripe_error_api_connection' => 'Netzwerkkommunikation mit Stripe fehlgeschlagen. Versuchen Sie es später erneut.', + 'stripe_error_rate_limit' => 'Zu viele Anfragen mit Stripe. Versuchen Sie es später erneut.', + 'stripe_error_invalid_request' => 'Ungültige Parameter. Versuchen Sie es später erneut.', + 'stripe_error_authentication' => 'Falsche Authentifizierung mit Stripe', + + 'import_title' => 'Importiere Kontakte in dein Konto', + 'import_cta' => 'Kontakte hochladen', + 'import_stat' => 'Du hast bisher :number Dateien importiert.', + 'import_result_stat' => 'vCard mit 1 Kontakt hochgeladen (:total_imported importiert, :total_skipped übersprungen)|vCard mit :total_contacts Kontakten hochgeladen (:total_imported importiert, :total_skipped übersprungen)', + 'import_view_report' => 'Bericht anzeigen', + 'import_in_progress' => 'Der Import ist im Gange. Lade die Seite in einer Minute neu.', + 'import_upload_title' => 'Kontakte aus vCard importieren', + 'import_upload_rules_desc' => 'Es gibt Einschränkungen:', + 'import_upload_rule_format' => 'Wir unterstützen .vcard und .vcf Dateien.', + 'import_upload_rule_vcard' => 'Wir unterstützen das vCard 3.0 Format, welches der Standard für MacOS und Google Kontakte ist.', + 'import_upload_rule_instructions' => 'Export-Anleitung für MacOS Kontakte und Google Kontakte.', + 'import_upload_rule_multiple' => 'Wenn deine Kontakte mehrere E-Mail-Adressen und Telefonnummern haben, werden jeweils nur die ersten Einträge importiert.', + 'import_upload_rule_limit' => 'Die Dateien dürfen nicht größer als 10 MB sein.', + 'import_upload_rule_time' => 'Es kann bis zu einer Minute dauern die Kontakte hochzuladen und zu verarbeiten. Wir bitten um Geduld.', + 'import_upload_rule_cant_revert' => 'Stell sicher, dass die Daten fehlerfrei sind, da der Upload nicht rückgängig gemacht werden kann.', + 'import_upload_form_file' => 'Deine .vcf oder .vCard Datei:', + 'import_upload_behaviour' => 'Import-Verhalten:', + 'import_upload_behaviour_add' => 'Neue Kontakte hinzufügen und bestehende überspringen', + 'import_upload_behaviour_replace' => 'Ersetze bestehende Kontakte', + 'import_upload_behaviour_help' => 'Ersetzen bedeutet, dass alle Daten mit den Informationen aus der vCard ersetzt werden, wobei existierende Kontaktfelder erhalten bleiben.', + 'import_report_title' => 'Importbericht', + 'import_report_date' => 'Importdatum', + 'import_report_type' => 'Importtyp', + 'import_report_number_contacts' => 'Anzahl der Kontakte in der Datei', + 'import_report_number_contacts_imported' => 'Anzahl der importieren Kontakte', + 'import_report_number_contacts_skipped' => 'Anzahl der übersprungenden Kontakte', + 'import_report_status_imported' => 'Importiert', + 'import_report_status_skipped' => 'Übersprungen', + 'import_vcard_parse_error' => 'Fehler beim Parsen des vCard-Eintrags', + 'import_vcard_contact_exist' => 'Kontakt existiert bereits', + 'import_vcard_contact_no_firstname' => 'Kein Vorname (Pflicht)', + 'import_vcard_file_not_found' => 'Datei nicht gefunden', + 'import_vcard_unknown_entry' => 'Unbekannter Kontakt', + 'import_vcard_file_no_entries' => 'Datei enthält keine Einträge', + 'import_blank_title' => 'Du has noch keine Kontakte importiert.', + 'import_blank_question' => 'Möchtest du jetzt Kontakte importieren?', + 'import_blank_description' => 'Wir können vCard-Dateien importieren, die du aus Google Contacts oder deinem Kontakt-Manager erhalten kannst.', + 'import_blank_cta' => 'Importiere vCard', + 'import_need_subscription' => 'Importieren von Daten erfordert ein Abonnement.', + + 'tags_list_title' => 'Markierungen', + 'tags_list_description' => 'Du kannst deine Kontakte mithilfe von Tags organisieren. Tags funktionieren wie Ordner, wobei ein Kontakt auch mehrere Tags erhalten kann. Um einen neuen Tag anzulegen, musst du ihn nur beim Kontakt hinzufügen.', + 'tags_list_contact_number' => '1 Kontakt|:count Kontakte', + 'tags_list_delete_success' => 'Der Tag wurde erfolgreich gelöscht', + 'tags_list_edit_success' => 'The tag has been successfully updated', + 'tags_list_delete_confirmation' => 'Möchtest du den Tag wirklich löschen? Kontakte werden nicht gelöscht, sondern nur der Tag.', + 'tags_blank_title' => 'Tags bieten eine tolle Möglichkeit Kontakte zu organisieren.', + 'tags_blank_description' => 'Tags funktionieren wie Ordner, wobei ein Kontakt auch mehrere Tags erhalten kann. Öffne einen Kontakt und tagge einen Freund direkt unter dem Namen. Sobald ein Kontakt getaggt ist, kannst du hier deine Tags verwalten.', + + 'api_title' => 'API Zugriff', + 'api_description' => 'Über die API ist es möglich, Monica über eine externe Applikation zu nutzen, wie z.B. eine App auf deinem Handy.', + 'api_help' => 'Um die API zu verwenden, ist ein Token obligatorisch. Sie können entweder ein persönliches Zugangs-Token (Bearer authentication) erstellen oder einen OAuth-Client autorisieren, es für Sie zu erstellen. Siehe API-Dokumentation.', + 'api_endpoint' => 'Der API-Endpunkt für diese Monica-Instanz ist:', + + 'api_personal_access_tokens' => 'Persönliche Zugangscodes', + 'api_pao_description' => 'Stelle sicher, dass du dieses Token nur an Quellen gibst, denen du vertraust, denn es erlaubt den Zugriff auf all deine Daten.', + 'api_token_title' => 'Persönliche Zugangs-Tokens', + 'api_token_create_new' => 'Neues Token erstellen', + 'api_token_not_created' => 'Du hast keine persönlichen Zugangs-Token erstellt.', + 'api_token_name' => 'Tokenname', + 'api_token_expire' => 'Läuft ab am {date}', + 'api_token_delete' => 'Löschen', + 'api_token_create' => 'Token erstellen', + 'api_token_scopes' => 'Geltungsbereiche', + 'api_token_help' => 'Hier ist dein neuer persönlicher Zugangs-Token. Dies ist das einzige Mal, dass er angezeigt wird, also verliere ihn nicht! Du kannst nun diesen Token verwenden, um API-Anfragen zu machen.', + + 'api_oauth_clients' => 'Deine Oauth Clients', + 'api_oauth_clients_desc' => 'Hier kannst du deine eigenen OAuth Clients registrieren.', + 'api_oauth_clients_desc2' => 'Benutzen Sie diese Client-Id, um ein neues Token anzufordern und um Berechtigungscodes für den Zugriff auf Token zu konvertieren. Siehe Laravel Passport-Dokumentation für weitere Informationen.', + 'api_oauth_title' => 'OAuth-Clients', + 'api_oauth_create_new' => 'Neuen Client erstellen', + 'api_oauth_edit' => 'Client bearbeiten', + 'api_oauth_not_created' => 'Du hast noch keine OAuth-Clients erstellt.', + 'api_oauth_clientid' => 'Kundennummer', + 'api_oauth_name' => 'Name', + 'api_oauth_name_help' => 'Etwas das deine Nutzer erkennen und dem sie vertrauen.', + 'api_oauth_secret' => 'Geheimbegriff', + 'api_oauth_create' => 'Client erstellen', + 'api_oauth_redirecturl' => 'Weiterleitungs-URL', + 'api_oauth_redirecturl_help' => 'Die Authorisierungs-Callback-URL deiner Anwendung.', + + 'api_authorized_clients' => 'Liste der authorisierten Clients', + 'api_authorized_clients_desc' => 'Diese Liste zeigt dir alle Clients, denen du Zugriff auf deine Anwendung gewährt hast. Du kannst die Authorisierungen jederzeit widerrufen.', + 'api_authorized_clients_title' => 'Zugelassene Anwendungen', + 'api_authorized_clients_none' => 'Es sind noch keine autorisierten Clients vorhanden.', + 'api_authorized_clients_name' => 'Name', + 'api_authorized_clients_scopes' => 'Geltungsbereiche', + + 'personalization_tab_title' => 'Personalisiere dein Konto', + + 'personalization_title' => 'Hier findest du verschiedene Einstellungsoptionen für deinen Account. Diese Funktionen sind eher für erfahrene Nutzer gedacht, die maximale Kontrolle über Monica möchten.', + 'personalization_contact_field_type_title' => 'Kontaktfelder', + 'personalization_contact_field_type_add' => 'Neues Feld hinzufügen', + 'personalization_contact_field_type_description' => 'Du kannst verschiedene Typen von Kontaktfeldern definieren, die du dann bei deinen Kontakten verwenden kannst. Wenn es beispielsweise in der Zukunft ein neues soziales Netzwerk gibt, kannst du diese neue Art der Kommunikation mit deinen Kontakten direkt hier hinzufügen.', + 'personalization_contact_field_type_table_name' => 'Name', + 'personalization_contact_field_type_table_protocol' => 'Protokoll', + 'personalization_contact_field_type_table_actions' => 'Aktionen', + 'personalization_contact_field_type_modal_title' => 'Neues Kontaktfeld hinzufügen', + 'personalization_contact_field_type_modal_edit_title' => 'Bestehendes Kontaktfeld bearbeiten', + 'personalization_contact_field_type_modal_delete_title' => 'Bestehendes Kontaktfeld löschen', + 'personalization_contact_field_type_modal_delete_description' => 'Bist du sicher, dass du dieses Kontaktfeld löschen möchtest? Wenn du dieses Kontaktfeld löschst, werden auch alle Einträge dieses Typs bei bestehenden Kontakten entfernt.', + 'personalization_contact_field_type_modal_name' => 'Name', + 'personalization_contact_field_type_modal_protocol' => 'Protokoll (optional)', + 'personalization_contact_field_type_modal_protocol_help' => 'Wenn ein Protokoll für ein Kontaktfeld gesetzt ist, wird bei Klick auf das Feld die verknüpfte Aktion ausgelöst.', + 'personalization_contact_field_type_modal_icon' => 'Symbol (Optional)', + 'personalization_contact_field_type_modal_icon_help' => 'Du kannst ein Icon für dieses Kontaktfeld hinterlegen. Es muss eine Referenz auf ein Font Awesome Icon sein.', + 'personalization_contact_field_type_delete_success' => 'Das Kontaktfeld wurde erfolgreich gelöscht.', + 'personalization_contact_field_type_add_success' => 'Das Kontakfeld wurde erfolgreich hinzugefügt.', + 'personalization_contact_field_type_edit_success' => 'Das Kontakfeld wurde erfolgreich editiert.', + + 'personalization_genders_title' => 'Geschlechter Typen', + 'personalization_genders_add' => 'Neue Geschlechtsidentität hinzufügen', + 'personalization_genders_desc' => 'Du kannst so viele Geschlechtsidentitäten anlegen wie du möchtest. Du brauchst mindestens eine Geschlechtsidentität in deinem Account.', + 'personalization_genders_modal_add' => 'Neue Geschlechtsidentität hinzufügen', + 'personalization_genders_modal_edit' => 'Geschlechtsidentität bearbeiten', + 'personalization_genders_modal_name' => 'Name', + 'personalization_genders_modal_name_help' => 'Der Name, mit dem das Geschlecht auf einer Kontaktseite angezeigt wird.', + 'personalization_genders_modal_sex' => 'Geschlecht', + 'personalization_genders_modal_sex_help' => 'Wird verwendet, um die Beziehungen zu definieren, und während des VCard Import/Export Prozesses.', + 'personalization_genders_modal_default' => 'Neuen Kontakten das Standardgeschlecht zuweisen', + 'personalization_genders_modal_delete' => 'Geschlechtsidentität löschen', + 'personalization_genders_modal_delete_desc' => 'Möchtest Du das Geschlecht "{name}" wirklich löschen?', + 'personalization_genders_modal_delete_question' => 'Du hast aktuell {count} Kontakt mit diesem Geschlecht. Wenn du dieses Geschlecht löschst, welches Geschlecht soll der Kontakt dann haben?|Du hast aktuell {count} Kontakte mit diesem Geschlecht. Wenn du dieses Geschlecht löschst, welches Geschlecht sollen die Kontakte dann haben?', + 'personalization_genders_modal_delete_question_default' => 'Dieses Geschlecht ist der aktuelle Standardwert. Wenn du dieses Geschlecht löschst, welches soll der neue Standardwert sein?', + 'personalization_genders_modal_error' => 'Bitte wähle ein Geschlecht aus der Liste.', + 'personalization_genders_list_contact_number' => '{count} Kontakt|{count} Kontakte', + 'personalization_genders_table_name' => 'Name', + 'personalization_genders_table_sex' => 'Geschlecht', + 'personalization_genders_table_default' => 'Standard', + 'personalization_genders_default' => 'Standard Geschlecht', + 'personalization_genders_make_default' => 'Ändern Sie das Standardgeschlecht', + 'personalization_genders_select_default' => 'Wählen Sie das Standardgeschlecht', + 'personalization_genders_m' => 'Männlich', + 'personalization_genders_f' => 'Weiblich', + 'personalization_genders_o' => 'Andere', + 'personalization_genders_u' => 'Unbekannt', + 'personalization_genders_n' => 'Keine oder nicht zutreffend', + + 'personalization_reminder_rule_save' => 'Die Änderung wurde gespeichert', + 'personalization_reminder_rule_title' => 'Erinnerungen', + 'personalization_reminder_rule_line' => '{count} Tag zuvor|{count} Tage zuvor', + 'personalization_reminder_rule_desc' => 'Für jede Erinnerung die du setzt, kann Monica dir ein paar Tage bevor das Ereignis stattfindet eine Email senden. Du kannst diese Benachrichtigungen hier anpassen. Diese Einstellungen gelten nur für monatliche und jährliche Erinnerungen.', + + 'personalization_module_save' => 'Die Änderung wurde gespeichert', + 'personalization_module_title' => 'Funktionen', + 'personalization_module_desc' => 'Möglicherweise brauchst du nicht alle Funktionen von Monica. Unten kannst du die Funktionen umschalten, die auf einer Kontaktseite zur Verfügung stehen. Diese Änderungen werden für ALLE Kontakte übernommen. Wenn du eine Funktion ausschaltest, gehen die Daten darin nicht verloren, sie werden nur verborgen.', + + 'personalisation_paid_upgrade' => 'Dies ist eine Premium-Funktion, die nur im kostenpflichtigen Abo aktiv ist. Upgrade deinen Account unter Einstellungen > Abonnement.', + 'personalisation_paid_upgrade_vue' => 'Dies ist eine Premium-Funktion, die nur im kostenpflichtigen Abo aktiv ist. Upgrade deinen Account unter Einstellungen > Abonnement.', + + 'reminder_time_to_send' => 'Uhrzeit zu der die Erinnerungen versandt werden', + 'reminder_time_to_send_help' => 'Der Versand deiner nächsten Erinnerung ist für {dateTime} geplant.', + + 'personalization_activity_type_category_title' => 'Aktivitätstyp Kategorien', + 'personalization_activity_type_category_add' => 'Neue Aktivitätstyp Kategorie hinzufügen', + 'personalization_activity_type_category_table_name' => 'Name', + 'personalization_activity_type_category_description' => 'Eine Aktivität mit einem deiner Kontakte kann einen Typ und eine Kategorie haben. Dein Konto kommt standardmäßig mit einer Reihe vordefinierter Kategorien, aber du kannst diese hier anpassen.', + 'personalization_activity_type_category_table_actions' => 'Aktionen', + 'personalization_activity_type_category_modal_add' => 'Neue Aktivitätstyp Kategorie hinzufügen', + 'personalization_activity_type_category_modal_edit' => 'Aktivitätstyps Kategorie bearbeiten', + 'personalization_activity_type_category_modal_question' => 'Wie sollen wir die neue Kategorie nennen?', + 'personalization_activity_type_add_button' => 'Neuen Aktivitätstyp hinzufügen', + 'personalization_activity_type_modal_add' => 'Neuen Aktivitätstyp hinzufügen', + 'personalization_activity_type_modal_question' => 'Wie sollen wir den neuen Aktivitätstyp nennen?', + 'personalization_activity_type_modal_edit' => 'Aktivitätstyp bearbeiten', + 'personalization_activity_type_category_modal_delete' => 'Eine Aktivitätstyp Kategorie löschen', + 'personalization_activity_type_category_modal_delete_desc' => 'Bist du sicher, dass du diese Kategorie löschen möchtest? Es werden alle zugehörigen Aktivitätstypen gelöscht. Tätigkeiten, die zu dieser Kategorie gehören, sind nicht von der Löschung betroffen.', + 'personalization_activity_type_modal_delete' => 'Aktivitätstyp löschen', + 'personalization_activity_type_modal_delete_desc' => 'Sind Sie sicher, dass Sie diesen Aktivitätstyp löschen möchten? Aktivitäten, die dieser Kategorie angehören, werden von dieser Löschung nicht betroffen sein.', + 'personalization_activity_type_modal_delete_error' => 'Wir können diesen Aktivitätstyp nicht finden.', + 'personalization_activity_type_category_modal_delete_error' => 'Wir können diese Aktivitätstyp Kategorie nicht finden.', + + 'personalization_life_event_category_title' => 'Lebensereigniskategorien', + 'personalization_live_event_category_table_name' => 'Name', + 'personalization_life_event_category_description' => 'Eine Lebensereignis kann einen Typ und eine Kategorie haben. Dein Konto verfügt über eine Reihe vordefinierter Kategorien und Typen, welche du hier anpassen kannst.', + 'personalization_live_event_category_table_actions' => 'Aktionen', + 'personalization_life_event_type_add_button' => 'Neuen Lebensereignis-Typ hinzufügen', + 'personalization_life_event_type_modal_add' => 'Neuen Lebensereignis-Typ hinzufügen', + 'personalization_life_event_type_modal_question' => 'Wie sollen wir den neuen Lebensereignistyp nennen?', + 'personalization_life_event_type_modal_edit' => 'Lebensereignis-Typ bearbeiten', + 'personalization_life_event_type_modal_delete' => 'Lebensereignis-Typ löschen', + 'personalization_life_event_type_modal_delete_desc' => 'Bist du sicher, dass du diesen Lebensereignis-Typ löschen möchtest? Lebensereignisse, die zu diesem Typ gehören, werden durch diese Aktion gelöscht.', + 'personalization_life_event_type_modal_delete_error' => 'Wir können diesen Lebensereignis-Typ nicht finden.', + + 'personalization_life_event_category_work_education' => 'Arbeit & Bildung', + 'personalization_life_event_category_family_relationships' => 'Familie & Beziehungen', + 'personalization_life_event_category_home_living' => 'Zuhause & Leben', + 'personalization_life_event_category_travel_experiences' => 'Reisen & Erfahrungen', + 'personalization_life_event_category_health_wellness' => 'Gesundheit & Fitness', + + 'personalization_life_event_type_new_job' => 'Neuer Job', + 'personalization_life_event_type_retirement' => 'Pensionierung', + 'personalization_life_event_type_new_school' => 'Neue Schule', + 'personalization_life_event_type_study_abroad' => 'Studium im Ausland', + 'personalization_life_event_type_volunteer_work' => 'Ehrenamtliche Arbeit', + 'personalization_life_event_type_published_book_or_paper' => 'Buch oder Bericht veröffentlicht', + 'personalization_life_event_type_military_service' => 'Militärdienst', + 'personalization_life_event_type_first_met' => 'Erste Begegnung', + 'personalization_life_event_type_new_relationship' => 'Neue Beziehung', + 'personalization_life_event_type_engagement' => 'Engagement', + 'personalization_life_event_type_marriage' => 'Heirat', + 'personalization_life_event_type_anniversary' => 'Jahrestag', + 'personalization_life_event_type_expecting_a_baby' => 'Erwartet ein Baby', + 'personalization_life_event_type_new_child' => 'Neues Kind', + 'personalization_life_event_type_new_family_member' => 'Neues Familienmitglied', + 'personalization_life_event_type_new_pet' => 'Neues Haustier', + 'personalization_life_event_type_end_of_relationship' => 'Ende einer Beziehung', + 'personalization_life_event_type_loss_of_a_loved_one' => 'Einen geliebten Menschen verloren', + 'personalization_life_event_type_moved' => 'Umgezogen', + 'personalization_life_event_type_bought_a_home' => 'Ein Haus gekauft', + 'personalization_life_event_type_home_improvement' => 'Immobilien-Renovierung', + 'personalization_life_event_type_holidays' => 'Feiertage', + 'personalization_life_event_type_new_vehicle' => 'Neues Fahrzeug', + 'personalization_life_event_type_new_roommate' => 'Neuer Mitbewohner', + 'personalization_life_event_type_overcame_an_illness' => 'Krankheit überwunden', + 'personalization_life_event_type_quit_a_habit' => 'Gewohnheit beendet', + 'personalization_life_event_type_new_eating_habits' => 'Neue Essgewohnheiten', + 'personalization_life_event_type_weight_loss' => 'Gewichts-Verlust', + 'personalization_life_event_type_wear_glass_or_contact' => 'Trägt nun eine Brille oder Kontaktlinsen', + 'personalization_life_event_type_broken_bone' => 'Hat sich einen Knochen gebrochen', + 'personalization_life_event_type_removed_braces' => 'Hat die Zahnspange entfernt bekommen', + 'personalization_life_event_type_surgery' => 'Hatte eine Operation', + 'personalization_life_event_type_dentist' => 'Hatte eine Zahnbehandlung', + 'personalization_life_event_type_new_sport' => 'Hat eine neue Sportart begonnen', + 'personalization_life_event_type_new_hobby' => 'Hat ein neues Hobby begonnen', + 'personalization_life_event_type_new_instrument' => 'Hat angefangen ein neues Instrument zu lernen', + 'personalization_life_event_type_new_language' => 'Hat angefangen eine neue Sprache zu lernen', + 'personalization_life_event_type_tattoo_or_piercing' => 'Tattoo oder Piercing', + 'personalization_life_event_type_new_license' => 'Neue Lizenz', + 'personalization_life_event_type_travel' => 'Reise', + 'personalization_life_event_type_achievement_or_award' => 'Errungenschaft oder Auszeichnung', + 'personalization_life_event_type_changed_beliefs' => 'Überzeugung geändert', + 'personalization_life_event_type_first_word' => 'Erstes Wort', + 'personalization_life_event_type_first_kiss' => 'Erster Kuss', + + 'storage_title' => 'Speicher', + 'storage_account_info' => 'Dein Kontolimit ist :accountLimit MB / Deine aktuelle Nutzung ist :currentAccountSize MB (ungefähr :percentUsage%).', + 'storage_upgrade_notice' => 'Upgraden Sie Ihr Konto, um Dokumente und Fotos hochladen zu können.', + 'storage_description' => 'Hier sehen Sie alle Dokumente und Fotos, die Sie über Ihre Kontakte hochgeladen haben.', + + 'dav_title' => 'WebDAV', + 'dav_description' => 'Hier finden Sie alle Einstellungen für den Einsatz von WebDAV Ressourcen für CardDAV und CalDAV Export.', + 'dav_copy_help' => 'In die Zwischenablage kopieren', + 'dav_clipboard_copied' => 'Wert in die Zwischenablage kopiert', + 'dav_url_base' => 'Basis-Url für alle CardDAV- und CalDAV-Ressourcen:', + 'dav_connect_help' => 'Sie können Ihre Kontakte und/oder Kalender mit dieser Basis-Url auf Ihrem Telefon oder Computer verbinden.', + 'dav_connect_help2' => 'Verwenden Sie Ihren Login (E-Mail) und erstellen Sie ein API-Token als Passwort, um sich zu authentifizieren.', + 'dav_url_carddav' => 'CardDAV-Url für Kontaktressourcen:', + 'dav_url_caldav_birthdays' => 'CalDAV Url für Geburtstage Ressourcen:', + 'dav_url_caldav_tasks' => 'CalDAV-Url für Aufgabenressourcen:', + 'dav_title_carddav' => 'CardDAV', + 'dav_title_caldav' => 'CalDAV', + 'dav_carddav_export' => 'Alle Kontakte in einer Datei exportieren', + 'dav_caldav_birthdays_export' => 'Alle Geburtstage in einer Datei exportieren', + 'dav_caldav_tasks_export' => 'Alle Aufgaben in einer Datei exportieren', + + 'archive_title' => 'Alle Kontakte in deinem Konto archivieren', + 'archive_desc' => 'Dies wird alle Kontakte in deinem Konto archivieren.', + 'archive_cta' => 'Alle Kontakte archivieren', + + 'logs_title' => 'Alles, was mit diesem Konto passiert ist', + 'logs_actor' => 'Benutzer', + 'logs_timestamp' => 'Zeitpunkt', + 'logs_description' => 'Beschreibung', + 'logs_subject' => 'Betroffener Kontakt', + 'logs_size' => 'Größe (KB)', + 'logs_object' => 'Objekt', +]; diff --git a/resources/lang/de/validation.php b/resources/lang/de/validation.php new file mode 100644 index 0000000..39fe308 --- /dev/null +++ b/resources/lang/de/validation.php @@ -0,0 +1,166 @@ + ':attribute muss akzeptiert werden.', + 'active_url' => ':attribute keine gültige URL.', + 'after' => ':attribute muss ein Datum nach :date sein.', + 'after_or_equal' => 'Das :attribute muss ein Datum nach oder gleich :date sein.', + 'alpha' => ':attribute darf nur Buchstaben enthalten.', + 'alpha_dash' => ':attribute darf nur aus Buchstaben, Zahlen, Binde- und Unterstrichen bestehen.', + 'alpha_num' => ':attribute darf nur Buchstaben und Nummern enthalten.', + 'array' => ':attribute muss ein Array sein.', + 'before' => ':attribute muss ein Datum vor :date sein.', + 'before_or_equal' => 'Das :attribute muss ein Datum vor oder gleich :date sein.', + 'between' => [ + 'numeric' => ':attribute muss zwischen :min und :max liegen.', + 'file' => ':attribute muss zwischen :min und :max Kilobyte liegen.', + 'string' => ':attribute muss zwischen :min und :max Zeichen liegen.', + 'array' => ':attribute muss zwischen :min und :max Elemente haben.', + ], + 'boolean' => 'Das :attribute Feld muss Wahr oder Falsch sein.', + 'confirmed' => 'Die :attribute Bestätigung stimmt nicht überein.', + 'date' => ':attribute ist kein gültiges Datum.', + 'date_equals' => ':attribute muss ein Datum gleich :date sein.', + 'date_format' => ':attribute stimmt nicht mit dem Format :format überein.', + 'different' => ':attribute und :other müssen sich unterscheiden.', + 'digits' => ':attribute müssen :digits Ziffern sein.', + 'digits_between' => ':attribute muss zwischen :min und :max Ziffern liegen.', + 'dimensions' => 'Das :attribute hat ungültige Bilddimensionen.', + 'distinct' => 'Das :attribute Feld hat einen doppelten Wert.', + 'email' => ':attribute muss eine gültige E-Mail-Adresse sein.', + 'ends_with' => ':attribute muss eine der folgenden Endungen aufweisen: :values.', + 'exists' => ':attribute ist ungültig.', + 'file' => 'Das :attribute muss eine Datei sein.', + 'filled' => 'Das :attribute Feld muss einen Wert haben.', + 'gt' => [ + 'numeric' => ':attribute muss größer als :value sein.', + 'file' => ':attribute muss größer als :value Kilobytes sein.', + 'string' => ':attribute muss länger als :value Zeichen sein.', + 'array' => ':attribute muss mehr als :value Elemente haben.', + ], + 'gte' => [ + 'numeric' => ':attribute muss größer oder gleich :value sein.', + 'file' => ':attribute muss größer oder gleich :value Kilobytes sein.', + 'string' => ':attribute muss mindestens :value Zeichen lang sein.', + 'array' => ':attribute muss mindestens :value Elemente haben.', + ], + 'image' => ':attribute muss ein Bild sein.', + 'in' => ':attribute ist ungültig.', + 'in_array' => 'Das :attribute Feld existiert nicht in :other.', + 'integer' => ':attribute muss eine Ganzzahl sein.', + 'ip' => ':attribute muss eine gültige IP-Adresse sein.', + 'ipv4' => ':attribute muss eine gültige IPv4 Adresse sein.', + 'ipv6' => ':attribute muss eine gültige IPv6 Adresse sein.', + 'json' => ':attribute muss eine gültige JSON-Zeichenfolge sein.', + 'lt' => [ + 'numeric' => ':attribute muss kleiner als :value sein.', + 'file' => ':attribute muss kleiner als :value Kilobytes sein.', + 'string' => ':attribute muss kürzer als :value Zeichen sein.', + 'array' => ':attribute muss weniger als :value Elemente haben.', + ], + 'lte' => [ + 'numeric' => ':attribute muss kleiner oder gleich :value sein.', + 'file' => ':attribute muss kleiner oder gleich :value Kilobytes sein.', + 'string' => ':attribute darf maximal :value Zeichen lang sein.', + 'array' => ':attribute darf maximal :value Elemente haben.', + ], + 'max' => [ + 'numeric' => ':attribute darf nicht größer als :max sein.', + 'file' => ':attribute darf nicht größer als :max Kilobytes sein.', + 'string' => ':attribute darf nicht größer als :max Zeichen sein.', + 'array' => ':attribute darf nicht mehr als :max Elemente haben.', + ], + 'mimes' => ':attribute muss vom typ: :values sein.', + 'mimetypes' => ':attribute muss den Dateityp :values haben.', + 'min' => [ + 'numeric' => ':attribute muss mindestens :min sein.', + 'file' => ':attribute muss mindestens :min Kilobytes sein.', + 'string' => ':attribute muss mindestens :min Zeichen haben.', + 'array' => ':attribute muss mindestens :min Elemente haben.', + ], + 'not_in' => ':attribute ist ungültig.', + 'not_regex' => 'Das Format von :attribute ist ungültig.', + 'numeric' => ':attribute muss eine Zahl sein.', + 'password' => 'Das Passwort ist falsch.', + 'present' => 'Das :attribute Feld muss vorhanden sein.', + 'regex' => 'Das :attribute Format ist ungültig.', + 'required' => 'Das :attribute Feld ist ein Pflichtfeld.', + 'required_if' => ':attribute ist Pflicht, wenn :other :value ist.', + 'required_unless' => ':attribute ist Pflicht, außer :other ist in :values.', + 'required_with' => ':attribute ist Pflicht, wenn :values vorhanden ist.', + 'required_with_all' => ':attribute muss ausgefüllt werden, wenn :values ausgefüllt wurde.', + 'required_without' => ':attribute ist Pflicht, wenn :values nicht vorhanden ist.', + 'required_without_all' => ':attribute ist Pflicht, wenn keiner der folgenden Werte vorhandne ist :values.', + 'same' => ':attribute und :other müssen übereinstimmen.', + 'size' => [ + 'numeric' => ':attribute muss :size sein.', + 'file' => ':attribute muss :size Kilobytes sein.', + 'string' => ':attribute muss :size Zeichen sein.', + 'array' => ':attribute muss :size Elemente enthalten.', + ], + 'starts_with' => ':attribute muss mit einem der folgenden Anfänge aufweisen: :values.', + 'string' => ':attribute muss eine Zeichenkette sein.', + 'timezone' => ':attribute muss eine gültige Zone sein.', + 'unique' => ':attribute muss einzigartig sein.', + 'uploaded' => ':attribute konnte nicht hochgeladen werden.', + 'url' => ':attribute hat ein ungültiges Format.', + 'uuid' => ':attribute muss ein UUID sein.', + + /* + |-------------------------------------------------------------------------- + | Custom Validation Language Lines + |-------------------------------------------------------------------------- + | + | Here you may specify custom validation messages for attributes using the + | convention "attribute.rule" to name the lines. This makes it quick to + | specify a specific custom language line for a given attribute rule. + | + */ + + 'custom' => [ + 'attribute-name' => [ + 'rule-name' => 'custom-message', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Custom Validation Attributes + |-------------------------------------------------------------------------- + | + | The following language lines are used to swap our attribute placeholder + | with something more reader friendly such as "E-Mail Address" instead + | of "email". This simply helps us make our message more expressive. + | + */ + + 'attributes' => [], + + 'vue' => [ + 'max' => [ + 'numeric' => '{field} darf nicht größer sein als {max}.', + 'string' => '{field} darf nicht mehr als {max} Zeichen enthalten.', + ], + 'required' => '{field} ist erforderlich.', + 'url' => '{field} ist keine gültige URL.', + ], + +]; diff --git a/resources/lang/el.json b/resources/lang/el.json new file mode 100644 index 0000000..ddea72e --- /dev/null +++ b/resources/lang/el.json @@ -0,0 +1,7 @@ +{ + "The :attribute must contain at least one uppercase and one lowercase letter.": "The :attribute must contain at least one uppercase and one lowercase letter.", + "The :attribute must contain at least one letter.": "The :attribute must contain at least one letter.", + "The :attribute must contain at least one symbol.": "The :attribute must contain at least one symbol.", + "The :attribute must contain at least one number.": "The :attribute must contain at least one number.", + "The given :attribute has appeared in a data leak. Please choose a different :attribute.": "The given :attribute has appeared in a data leak. Please choose a different :attribute." +} diff --git a/resources/lang/el/app.php b/resources/lang/el/app.php new file mode 100644 index 0000000..430e7c9 --- /dev/null +++ b/resources/lang/el/app.php @@ -0,0 +1,571 @@ + 'Ναι', + 'no' => 'Όχι', + 'update' => 'Ενημέρωση', + 'save' => 'Αποθήκευση', + 'add' => 'Προσθήκη', + 'cancel' => 'Ακύρωση', + 'confirm' => 'Επιβεβαίωση', + 'delete_confirm' => 'Είστε σίγουροι;', + 'delete' => 'Διαγραφή', + 'edit' => 'Επεξεργασία', + 'upload' => 'Μεταφόρτωση', + 'download' => 'Λήψη', + 'save_close' => 'Αποθήκευση και κλείσιμο', + 'close' => 'Κλείσιμο', + 'copy' => 'Αντιγραφή', + 'create' => 'Δημιουργία', + 'remove' => 'Κατάργηση', + 'revoke' => 'Ανάκληση', + 'done' => 'Ολοκληρώθηκε', + 'back' => 'Πίσω', + 'verify' => 'Επαλήθευση', + 'new' => 'νέο', + 'unknown' => 'Δεν γνωρίζω', + 'load_more' => 'Εμφάνιση περισσότερων', + 'loading' => 'Φόρτωση…', + 'with' => 'με', + 'today' => 'σήμερα', + 'yesterday' => 'χθες', + 'another_day' => 'άλλη μέρα', + 'date' => 'Ημερομηνία', + 'type' => 'Τύπος', + 'zoom' => 'Μεγέθυνση', + 'upgrade' => 'Αναβαθμίστε για να ξεκλειδώσετε', + 'percent_uploaded' => '{percent}% έχει μεταφορτωθεί', + 'retry' => 'Ξαναδοκιμάστε', + 'filter' => 'Φιλτράρισμα λίστας', + 'go_back' => 'Επιστροφή', + 'file_selected' => 'Επιλέχθηκε ένα αρχείο…|{count} επιλεγμένα αρχεία…', + + 'application_title' => 'Monica – διαχειριστής προσωπικών σχέσεων', + 'application_description' => 'Το Monica είναι ένα εργαλείο που διαχειρίζεται τις επαφές σας με τους αγαπημένους σας, τους φίλους και την οικογένεια.', + 'application_og_title' => 'Βελτιώστε τις σχέσεις με του αγαπημένους σας. Δωρεάν Online CRM για φίλους και οικογένεια.', + + 'markdown_description' => 'Θέλετε να μορφοποιήσετε το κείμενο σας όμορφα; Υποστηρίζουμε Markdown για να προσθέσετε εντονη, πλάγια γραφή, λίστες και περισσότερα.', + 'markdown_link' => 'Διαβάστε την τεκμηρίωση', + + 'header_settings_link' => 'Ρυθμίσεις', + 'header_logout_link' => 'Αποσύνδεση', + 'header_changelog_link' => 'Αλλαγές προϊόντος', + + 'main_nav_cta' => 'Προσθήκη ατόμων', + 'main_nav_dashboard' => 'Επισκόπηση', + 'main_nav_family' => 'Επαφές', + 'main_nav_journal' => 'Προσωπικό Ημερολόγιο', + 'main_nav_activities' => 'Δραστηριότητες', + 'main_nav_tasks' => 'Εργασίες', + + 'footer_remarks' => 'Σχόλια;', + 'footer_send_email' => 'Στείλτε μας ένα email', + 'footer_privacy' => 'Πολιτική απορρήτου', + 'footer_release' => 'Σημειώσεις έκδοσης', + 'footer_newsletter' => 'Newsletter', + 'footer_source_code' => 'Συμβάλλετε', + 'footer_version' => 'Έκδοση :version', + 'footer_new_version' => 'Μια νέα έκδοση της εφαρμογής Monica είναι διαθέσιμη', + + 'footer_modal_version_whats_new' => 'Τι νέο υπάρχει;', + 'footer_modal_version_release_away' => 'Είστε 1 έκδοση πίσω από την τελευταία διαθέσιμη έκδοση. Θα πρέπει να αναβαθμίσετε την εγκατάσταση σας.|Είστε :number εκδόσεις πίσω από την τελευταία διαθέσιμη έκδοση. Θα πρέπει να αναβαθμίσετε την εγκατάσταση σας.', + + 'breadcrumb_dashboard' => 'Επισκόπηση', + 'breadcrumb_list_contacts' => 'Λίστα επαφών', + 'breadcrumb_archived_contacts' => 'Αρχειοθετημένες επαφές', + 'breadcrumb_journal' => 'Προσωπικό Ημερολόγιο', + 'breadcrumb_settings' => 'Ρυθμίσεις', + 'breadcrumb_settings_export' => 'Εξαγωγή', + 'breadcrumb_settings_users' => 'Χρήστες', + 'breadcrumb_settings_users_add' => 'Προσθήκη χρήστη', + 'breadcrumb_settings_subscriptions' => 'Συνδρομή', + 'breadcrumb_settings_import' => 'Εισαγωγή', + 'breadcrumb_settings_import_report' => 'Αναφορά εισαγωγής', + 'breadcrumb_settings_import_upload' => 'Μεταφόρτωση', + 'breadcrumb_settings_tags' => 'Ετικέτες', + 'breadcrumb_add_significant_other' => 'Προσθήκη συντρόφου', + 'breadcrumb_edit_significant_other' => 'Επεξεργασία συντρόφου', + 'breadcrumb_add_note' => 'Προσθήκη σημείωσης', + 'breadcrumb_edit_note' => 'Επεξεργασία σημείωσης', + 'breadcrumb_api' => 'API', + 'breadcrumb_dav' => 'Πόροι DAV', + 'breadcrumb_edit_introductions' => 'Πώς γνωριστήκατε;', + 'breadcrumb_settings_personalization' => 'Εξατομίκευση', + 'breadcrumb_settings_security' => 'Ασφάλεια', + 'breadcrumb_settings_security_2fa' => 'Έλεγχος Ταυτότητας Δυο Παραγόντων', + 'breadcrumb_profile' => 'Προφίλ του :name', + + 'gender_male' => 'Άντρας', + 'gender_female' => 'Γυναίκα', + 'gender_none' => 'Χωρίς δήλωση', + 'gender_no_gender' => 'Χωρίς φύλο', + + 'error_title' => 'Ωχ... κάτι πήγε στραβά.', + 'error_unauthorized' => 'Δεν έχετε δικαίωμα να επεξεργαστείτε αυτή τη σελίδα.', + 'error_user_account' => 'Αυτός ο χρήστης δεν ανήκει στο λογαριασμό που δόθηκε.', + 'error_save' => 'Υπήρξε σφάλμα κατά την αποθήκευση των δεδομένων.', + 'error_try_again' => 'Κάτι πήγε στραβά. Παρακαλώ προσπαθήστε ξανά.', + 'error_id' => 'ID Σφάλματος: :id', + 'error_unavailable' => 'Η υπηρεσία δεν είναι διαθέσιμη', + 'error_maintenance' => 'Συντήρηση σε εξέλιξη. Θα επιστρέψουμε σύντομα.', + 'error_help' => 'Θα επιστρέψουμε σύντομα.', + 'error_twitter' => 'Ακολουθήστε το λογαριασμό μας στο Twitter για να ενημερωθείτε όταν επανέλθουμε.', + 'error_no_term' => 'Δεν υπάρχει πολιτική για αυτή την εγκατάσταση ακόμη.', + + 'default_save_success' => 'Τα δεδομένα αποθηκεύτηκαν.', + + 'compliance_title' => 'Με συγχωρείτε για την διακοπή.', + 'compliance_desc' => 'Έχουμε αλλάξει τους Όρους Χρήσης και την Πολιτική Απορρήτου. Σύμφωνα με το νόμο πρέπει να σας ζητήσουμε να ελέγξετε και να αποδεχτείτε για να συνεχίσετε να χρησιμοποιείτε τον λογαριασμό σας.', + 'compliance_desc_end' => 'Δεν κάνουμε τίποτα κακό με τα δεδομένα σας ή το λογαριασμό σας και δεν θα κάνουμε ποτέ.', + 'compliance_terms' => 'Αποδεχτείτε τους νέους όρους και πολιτική απορρήτου', + + // Relationship types + // Yes, each relationship type has 8 strings associated with it. + // This is because we need to indicate the name of the relationship type, + // and also the name of the opposite side of this relationship (father/son), + // and then, the feminine version of the string. Finally, in some sentences + // in the UI, we need to include the name of the person we add the relationship + // to. + 'relationship_type_group_love' => 'Ερωτικές σχέσεις', + 'relationship_type_group_family' => 'Οικογενειακές σχέσεις', + 'relationship_type_group_friend' => 'Φιλικές σχέσεις', + 'relationship_type_group_work' => 'Εργασιακές σχέσεις', + 'relationship_type_group_other' => 'Σχέσεις άλλων τύπων', + + 'relationship_type_partner' => 'σύντροφος', + 'relationship_type_partner_female' => 'σύντροφος', + 'relationship_type_partner_male' => 'significant other', + 'relationship_type_partner_with_name' => 'σύντροφος :name', + 'relationship_type_partner_female_with_name' => 'σύντροφος :name', + 'relationship_type_partner_male_with_name' => ':name’s significant other', + + 'relationship_type_spouse' => 'σύζυγος', + 'relationship_type_spouse_female' => 'wife', + 'relationship_type_spouse_male' => 'husband', + 'relationship_type_spouse_with_name' => 'σύζυγος :name', + 'relationship_type_spouse_female_with_name' => ':name’s wife', + 'relationship_type_spouse_male_with_name' => ':name’s husband', + + 'relationship_type_date' => 'ημερομηνία', + 'relationship_type_date_female' => 'ημερομηνία', + 'relationship_type_date_male' => 'date', + 'relationship_type_date_with_name' => 'ημερομηνία :name', + 'relationship_type_date_female_with_name' => 'ημερομηνία :name', + 'relationship_type_date_male_with_name' => ':name’s date', + + 'relationship_type_lover' => 'εραστής', + 'relationship_type_lover_female' => 'εραστής', + 'relationship_type_lover_male' => 'lover', + 'relationship_type_lover_with_name' => 'εραστής :name', + 'relationship_type_lover_female_with_name' => 'εραστής :name', + 'relationship_type_lover_male_with_name' => ':name’s lover', + + 'relationship_type_inlovewith' => 'ερωτευμένος με', + 'relationship_type_inlovewith_female' => 'ερωτευμένος με', + 'relationship_type_inlovewith_male' => 'in love with', + 'relationship_type_inlovewith_with_name' => 'κάποιος με τον οποίο ο :name είναι ερωτευμένος', + 'relationship_type_inlovewith_female_with_name' => 'κάποιος με τον οποίο ο :name είναι ερωτευμένος', + 'relationship_type_inlovewith_male_with_name' => 'someone :name is in love with', + + 'relationship_type_lovedby' => 'αγαπημένος του', + 'relationship_type_lovedby_female' => 'αγαπημένος του', + 'relationship_type_lovedby_male' => 'loved by', + 'relationship_type_lovedby_with_name' => 'κρυφός εραστής :name', + 'relationship_type_lovedby_female_with_name' => 'κρυφός εραστής της :name', + 'relationship_type_lovedby_male_with_name' => ':name’s secret lover', + + 'relationship_type_ex' => 'ex-partner', + 'relationship_type_ex_female' => 'πρώην φίλη', + 'relationship_type_ex_male' => 'ex-boyfriend', + 'relationship_type_ex_with_name' => ':name’s ex-partner', + 'relationship_type_ex_female_with_name' => 'πρώην φίλη :name', + 'relationship_type_ex_male_with_name' => ':name’s ex-boyfriend', + + 'relationship_type_parent' => 'parent', + 'relationship_type_parent_female' => 'μητέρα', + 'relationship_type_parent_male' => 'father', + 'relationship_type_parent_with_name' => ':name’s parent', + 'relationship_type_parent_female_with_name' => 'μητέρα :name', + 'relationship_type_parent_male_with_name' => ':name’s father', + + 'relationship_type_child' => 'child', + 'relationship_type_child_female' => 'κόρη', + 'relationship_type_child_male' => 'son', + 'relationship_type_child_with_name' => ':name’s child', + 'relationship_type_child_female_with_name' => 'κόρη :name', + 'relationship_type_child_male_with_name' => ':name’s son', + + 'relationship_type_stepparent' => 'step-parent', + 'relationship_type_stepparent_female' => 'μητριά', + 'relationship_type_stepparent_male' => 'stepfather', + 'relationship_type_stepparent_with_name' => ':name’s step-parent', + 'relationship_type_stepparent_female_with_name' => 'πατριός :name', + 'relationship_type_stepparent_male_with_name' => ':name’s stepfather', + + 'relationship_type_stepchild' => 'stepchild', + 'relationship_type_stepchild_female' => 'κόρη εξ\' αγχιστείας', + 'relationship_type_stepchild_male' => 'stepson', + 'relationship_type_stepchild_with_name' => ':name’s stepchild', + 'relationship_type_stepchild_female_with_name' => 'κόρη εξ\' αγχιστείας του :name', + 'relationship_type_stepchild_male_with_name' => ':name’s stepson', + + 'relationship_type_sibling' => 'sibling', + 'relationship_type_sibling_female' => 'αδερφή', + 'relationship_type_sibling_male' => 'brother', + 'relationship_type_sibling_with_name' => ':name’s sibling', + 'relationship_type_sibling_female_with_name' => 'αδερφή του :name', + 'relationship_type_sibling_male_with_name' => ':name’s brother', + + 'relationship_type_grandparent' => 'grandparent', + 'relationship_type_grandparent_female' => 'grandmother', + 'relationship_type_grandparent_male' => 'grandfather', + 'relationship_type_grandparent_with_name' => ':name’s grandparent', + 'relationship_type_grandparent_female_with_name' => ':name’s grandmother', + 'relationship_type_grandparent_male_with_name' => ':name’s grandfather', + + 'relationship_type_grandchild' => 'grandchild', + 'relationship_type_grandchild_female' => 'granddaughter', + 'relationship_type_grandchild_male' => 'grandson', + 'relationship_type_grandchild_with_name' => ':name’s grandchild', + 'relationship_type_grandchild_female_with_name' => ':name’s granddauther', + 'relationship_type_grandchild_male_with_name' => ':name’s grandson', + + 'relationship_type_uncle' => 'θείος', + 'relationship_type_uncle_female' => 'θεία', + 'relationship_type_uncle_male' => 'uncle', + 'relationship_type_uncle_with_name' => 'θείος του :name', + 'relationship_type_uncle_female_with_name' => 'θεία του :name', + 'relationship_type_uncle_male_with_name' => ':name’s uncle', + + 'relationship_type_nephew' => 'ανιψιός', + 'relationship_type_nephew_female' => 'ανιψιά', + 'relationship_type_nephew_male' => 'nephew', + 'relationship_type_nephew_with_name' => 'ανιψιός του :name', + 'relationship_type_nephew_female_with_name' => 'ανιψιά του :name', + 'relationship_type_nephew_male_with_name' => ':name’s nephew', + + 'relationship_type_cousin' => 'ξάδερφος', + 'relationship_type_cousin_female' => 'ξαδέρφη', + 'relationship_type_cousin_male' => 'cousin', + 'relationship_type_cousin_with_name' => 'ξάδερφος του :name', + 'relationship_type_cousin_female_with_name' => 'ξαδέρφη του :name', + 'relationship_type_cousin_male_with_name' => ':name’s cousin', + + 'relationship_type_godfather' => 'godparent', + 'relationship_type_godfather_female' => 'νονά', + 'relationship_type_godfather_male' => 'godfather', + 'relationship_type_godfather_with_name' => ':name’s godparent', + 'relationship_type_godfather_female_with_name' => 'νονά του :name', + 'relationship_type_godfather_male_with_name' => ':name’s godfather', + + 'relationship_type_godson' => 'godchild', + 'relationship_type_godson_female' => 'βαφτισιμιά', + 'relationship_type_godson_male' => 'godson', + 'relationship_type_godson_with_name' => ':name’s godchild', + 'relationship_type_godson_female_with_name' => 'βαφτισιμιά του :name', + 'relationship_type_godson_male_with_name' => ':name’s godson', + + 'relationship_type_friend' => 'φίλος', + 'relationship_type_friend_female' => 'φίλη', + 'relationship_type_friend_male' => 'friend', + 'relationship_type_friend_with_name' => 'φίλος του :name', + 'relationship_type_friend_female_with_name' => 'φίλη του :name', + 'relationship_type_friend_male_with_name' => ':name’s friend', + + 'relationship_type_bestfriend' => 'καλύτερος φίλος', + 'relationship_type_bestfriend_female' => 'καλύτερη φίλη', + 'relationship_type_bestfriend_male' => 'best friend', + 'relationship_type_bestfriend_with_name' => 'καλύτερος φίλος του :name', + 'relationship_type_bestfriend_female_with_name' => 'καλύτερη φίλη του :name', + 'relationship_type_bestfriend_male_with_name' => ':name’s best friend', + + 'relationship_type_colleague' => 'συνάδελφος', + 'relationship_type_colleague_female' => 'συνάδελφος', + 'relationship_type_colleague_male' => 'colleague', + 'relationship_type_colleague_with_name' => 'συνάδελφος του :name', + 'relationship_type_colleague_female_with_name' => 'συνάδελφος του :name', + 'relationship_type_colleague_male_with_name' => ':name’s colleague', + + 'relationship_type_boss' => 'αφεντικό', + 'relationship_type_boss_female' => 'αφεντικό', + 'relationship_type_boss_male' => 'boss', + 'relationship_type_boss_with_name' => 'αφεντικό του :name', + 'relationship_type_boss_female_with_name' => 'αφεντικό του :name', + 'relationship_type_boss_male_with_name' => ':name’s boss', + + 'relationship_type_subordinate' => 'υφιστάμενος', + 'relationship_type_subordinate_female' => 'υφιστάμενη', + 'relationship_type_subordinate_male' => 'subordinate', + 'relationship_type_subordinate_with_name' => 'υφιστάμενος του :name', + 'relationship_type_subordinate_female_with_name' => 'υφιστάμενη του :name', + 'relationship_type_subordinate_male_with_name' => ':name’s subordinate', + + 'relationship_type_mentor' => 'σύμβουλος', + 'relationship_type_mentor_female' => 'μέντορας', + 'relationship_type_mentor_male' => 'mentor', + 'relationship_type_mentor_with_name' => 'σύμβουλος του :name', + 'relationship_type_mentor_female_with_name' => 'σύμβουλος της :name', + 'relationship_type_mentor_male_with_name' => ':name’s mentor', + + 'relationship_type_protege' => 'protégé', + 'relationship_type_protege_female' => 'protégé', + 'relationship_type_protege_male' => 'protégé', + 'relationship_type_protege_with_name' => ':name’s protégé', + 'relationship_type_protege_female_with_name' => ':name’s protégé', + 'relationship_type_protege_male_with_name' => ':name’s protégé', + + 'relationship_type_ex_husband' => 'ex-spouse', + 'relationship_type_ex_husband_female' => 'πρώην σύζυγος', + 'relationship_type_ex_husband_male' => 'ex-husband', + 'relationship_type_ex_husband_with_name' => ':name’s ex-spouse', + 'relationship_type_ex_husband_female_with_name' => 'πρώην σύζυγος της :name', + 'relationship_type_ex_husband_male_with_name' => ':name’s ex-husband', + + // emotions + 'emotion_primary_love' => 'Αγάπη', + 'emotion_primary_joy' => 'Χαρά', + 'emotion_primary_surprise' => 'Έκπληξη', + 'emotion_primary_anger' => 'Θυμός', + 'emotion_primary_sadness' => 'Θλίψη', + 'emotion_primary_fear' => 'Φόβος', + + 'emotion_secondary_affection' => 'Στοργή', + 'emotion_secondary_lust' => 'Πόθος', + 'emotion_secondary_longing' => 'Λαχτάρα', + 'emotion_secondary_cheerfulness' => 'Χαρά', + 'emotion_secondary_zest' => 'Ζέση', + 'emotion_secondary_contentment' => 'Ικανοποίηση', + 'emotion_secondary_pride' => 'Περηφάνια', + 'emotion_secondary_optimism' => 'Αισιοδοξία', + 'emotion_secondary_enthrallment' => 'Ενθάρρυνση', + 'emotion_secondary_relief' => 'Ανακούφιση', + 'emotion_secondary_surprise' => 'Έκπληξη', + 'emotion_secondary_irritation' => 'Ενόχληση', + 'emotion_secondary_exasperation' => 'Εξόργιση', + 'emotion_secondary_rage' => 'Θυμός', + 'emotion_secondary_disgust' => 'Αηδία', + 'emotion_secondary_envy' => 'Ζήλια', + 'emotion_secondary_suffering' => 'Ταλαιπωρία', + 'emotion_secondary_sadness' => 'Θλίψη', + 'emotion_secondary_disappointment' => 'Απογοήτευση', + 'emotion_secondary_shame' => 'Ντροπή', + 'emotion_secondary_neglect' => 'Παραμέληση', + 'emotion_secondary_sympathy' => 'Συμπάθεια', + 'emotion_secondary_horror' => 'Τρόμος', + 'emotion_secondary_nervousness' => 'Νευρικότητα', + + 'emotion_adoration' => 'Λατρεία', + 'emotion_affection' => 'Στοργή', + 'emotion_love' => 'Αγάπη', + 'emotion_fondness' => 'Συμπάθεια', + 'emotion_liking' => 'Αρέσκεια', + 'emotion_attraction' => 'Έλξη', + 'emotion_caring' => 'Νοιάζομαι', + 'emotion_tenderness' => 'Τρυφερότητα', + 'emotion_compassion' => 'Συμπόνια', + 'emotion_sentimentality' => 'Συναισθηματικότητα', + 'emotion_arousal' => 'Διέγερση', + 'emotion_desire' => 'Επιθυμία', + 'emotion_lust' => 'Πόθος', + 'emotion_passion' => 'Πάθος', + 'emotion_infatuation' => 'Ξελόγιασμα', + 'emotion_longing' => 'Λαχτάρα', + 'emotion_amusement' => 'Ψυχαγωγία', + 'emotion_bliss' => 'Ευδαιμονία', + 'emotion_cheerfulness' => 'Χαρά', + 'emotion_gaiety' => 'Ευθυμία', + 'emotion_glee' => 'Χαρά', + 'emotion_jolliness' => 'Ευχαρίστηση', + 'emotion_joviality' => 'Κέφι', + 'emotion_joy' => 'Χαρά', + 'emotion_delight' => 'Απόλαυση', + 'emotion_enjoyment' => 'Απόλαυση', + 'emotion_gladness' => 'Χαρά', + 'emotion_happiness' => 'Ευτυχία', + 'emotion_jubilation' => 'Αγαλλίαση', + 'emotion_elation' => 'Έξαρση', + 'emotion_satisfaction' => 'Ικανοποίηση', + 'emotion_ecstasy' => 'Έκσταση', + 'emotion_euphoria' => 'Εφορία', + 'emotion_enthusiasm' => 'Ενθουσιασμός', + 'emotion_zeal' => 'Ζήλος', + 'emotion_zest' => 'Ζέση', + 'emotion_excitement' => 'Ενθουσιασμός', + 'emotion_thrill' => 'Συγκίνηση', + 'emotion_exhilaration' => 'Χαρά', + 'emotion_contentment' => 'Ικανοποίηση', + 'emotion_pleasure' => 'Απόλαυση', + 'emotion_pride' => 'Περηφάνια', + 'emotion_eagerness' => 'Προθυμία', + 'emotion_hope' => 'Ελπίδα', + 'emotion_optimism' => 'Αισιοδοξία', + 'emotion_enthrallment' => 'Ενθάρρυνση', + 'emotion_rapture' => 'Αγαλλίαση', + 'emotion_relief' => 'Ανακούφιση', + 'emotion_amazement' => 'Κατάπληξη', + 'emotion_surprise' => 'Έκπληξη', + 'emotion_astonishment' => 'Έκπληκτος', + 'emotion_aggravation' => 'Επιδείνωση', + 'emotion_irritation' => 'Ερεθισμός', + 'emotion_agitation' => 'Ταραχή', + 'emotion_annoyance' => 'Ενόχληση', + 'emotion_grouchiness' => 'Δυστροπία', + 'emotion_grumpiness' => 'Γκρίνια', + 'emotion_exasperation' => 'Εξόργιση', + 'emotion_frustration' => 'Εκνευρισμός', + 'emotion_anger' => 'Θυμός', + 'emotion_rage' => 'Οργή', + 'emotion_outrage' => 'Προσβολή', + 'emotion_fury' => 'Θυμός', + 'emotion_wrath' => 'Οργή', + 'emotion_hostility' => 'Εχθρότητα', + 'emotion_ferocity' => 'Αγριότητα', + 'emotion_bitterness' => 'Πικρία', + 'emotion_hate' => 'Μίσος', + 'emotion_loathing' => 'Σιχαμάρα', + 'emotion_scorn' => 'Περιφρόνηση', + 'emotion_spite' => 'Πείσμα', + 'emotion_vengefulness' => 'Εκδικητικότητα', + 'emotion_dislike' => 'Αντιπάθεια', + 'emotion_resentment' => 'Μνησικακία', + 'emotion_disgust' => 'Αηδία', + 'emotion_revulsion' => 'Μεταστροφή', + 'emotion_contempt' => 'Περιφρόνηση', + 'emotion_envy' => 'Φθόνος', + 'emotion_jealousy' => 'Ζήλια', + 'emotion_agony' => 'Αγωνία', + 'emotion_suffering' => 'Ταλαιπωρία', + 'emotion_hurt' => 'Πόνος', + 'emotion_anguish' => 'Οδύνη', + 'emotion_depression' => 'Κατάθλιψη', + 'emotion_despair' => 'Απόγνωση', + 'emotion_hopelessness' => 'Απελπισία', + 'emotion_gloom' => 'Κατηφής', + 'emotion_glumness' => 'Κατήφεια', + 'emotion_sadness' => 'Θλίψη', + 'emotion_unhappiness' => 'Δυστυχία', + 'emotion_grief' => 'Πένθος', + 'emotion_sorrow' => 'Πόνος', + 'emotion_woe' => 'Συμφορά', + 'emotion_misery' => 'Μιζέρια', + 'emotion_melancholy' => 'Μελαγχολία', + 'emotion_dismay' => 'Φόβος', + 'emotion_disappointment' => 'Απογοήτευση', + 'emotion_displeasure' => 'Δυσαρέσκεια', + 'emotion_guilt' => 'Ενοχή', + 'emotion_shame' => 'Ντροπή', + 'emotion_regret' => 'Μετάνοια', + 'emotion_remorse' => 'Τύψεις', + 'emotion_alienation' => 'Αποξένωση', + 'emotion_isolation' => 'Απομόνωση', + 'emotion_neglect' => 'Παραμέληση', + 'emotion_loneliness' => 'Μοναξιά', + 'emotion_rejection' => 'Απόρριψη', + 'emotion_homesickness' => 'Νοσταλγία', + 'emotion_defeat' => 'Ήττα', + 'emotion_dejection' => 'Κατήφεια', + 'emotion_insecurity' => 'Ανασφάλεια', + 'emotion_embarrassment' => 'Ντροπή', + 'emotion_humiliation' => 'Ταπείνωση', + 'emotion_insult' => 'Προσβολή', + 'emotion_pity' => 'Οίκτος', + 'emotion_sympathy' => 'Συμπάθεια', + 'emotion_alarm' => 'Ανήσυχος', + 'emotion_shock' => 'Έκπληξη', + 'emotion_fear' => 'Φόβος', + 'emotion_fright' => 'Τρομάρα', + 'emotion_horror' => 'Τρόμος', + 'emotion_terror' => 'Τρόμος', + 'emotion_panic' => 'Πανικός', + 'emotion_hysteria' => 'Υστερία', + 'emotion_mortification' => 'Ταπείνωση', + 'emotion_anxiety' => 'Ανησυχία', + 'emotion_nervousness' => 'Νευρικότητα', + 'emotion_tenseness' => 'Υπερένταση', + 'emotion_uneasiness' => 'Ανησυχία', + 'emotion_apprehension' => 'Σύλληψη', + 'emotion_worry' => 'Ανησυχία', + 'emotion_distress' => 'Δυσφορία', + 'emotion_dread' => 'Τρόμος', + + // weather + 'weather_sunny' => 'Λιακάδα', + 'weather_clear' => 'Καθαρός', + 'weather_clear-day' => 'Καθαρός', + 'weather_clear-night' => 'Καθαρή νύχτα', + 'weather_light-drizzle' => 'Ψιλόβροχο', + 'weather_patchy-light-drizzle' => 'Σποραδικό ψιλόβροχο', + 'weather_patchy-light-rain' => 'Σποραδική ελαφριά βροχή', + 'weather_light-rain' => 'Ελαφρά βροχόπτωση', + 'weather_moderate-rain-at-times' => 'Μέτρια βροχή κατά καιρούς', + 'weather_moderate-rain' => 'Μέτρια βροχή', + 'weather_patchy-rain-possible' => 'Πιθανή σποραδική βροχή', + 'weather_heavy-rain-at-times' => 'Βαριά βροχή κατά καιρούς', + 'weather_heavy-rain' => 'Έντονη βροχή', + 'weather_light-freezing-rain' => 'Ελαφριά παγωμένη βροχή', + 'weather_moderate-or-heavy-freezing-rain' => 'Μέτρια ή δυνατή παγωμένη βροχή', + 'weather_light-sleet' => 'Ελαφρύ χιονόνερο', + 'weather_moderate-or-heavy-rain-shower' => 'Μέτρια ή δυνατή βροχή', + 'weather_light-rain-shower' => 'Ελαφριά βροχή', + 'weather_torrential-rain-shower' => 'Καταρρακτώδης βροχή', + 'weather_rain' => 'Βροχή', + 'weather_snow' => 'Χιόνι', + 'weather_blowing-snow' => 'Χιονόπτωση με αέρα', + 'weather_patchy-light-snow' => 'Αραιό χιόνι', + 'weather_light-snow' => 'Ελαφρά χιονόπτωση', + 'weather_patchy-moderate-snow' => 'Patchy moderate snow', + 'weather_moderate-snow' => 'Μέτρια χιονόπτωση', + 'weather_patchy-heavy-snow' => 'Patchy heavy snow', + 'weather_heavy-snow' => 'Ισχυρή χιονόπτωση', + 'weather_light-snow-showers' => 'Light snow showers', + 'weather_moderate-or-heavy-snow-showers' => 'Moderate or heavy snow showers', + 'weather_patchy-snow-possible' => 'Patchy snow possible', + 'weather_patchy-sleet-possible' => 'Patchy sleet possible', + 'weather_moderate-or-heavy-sleet' => 'Moderate or heavy sleet', + 'weather_light-sleet-showers' => 'Light sleet showers', + 'weather_moderate-or-heavy-sleet-showers' => 'Moderate or heavy sleet showers', + 'weather_sleet' => 'Χιονόνερο', + 'weather_wind' => 'Άνεμος', + 'weather_fog' => 'Ομίχλη', + 'weather_freezing-fog' => 'Παγωμένη ομίχλη', + 'weather_mist' => 'Ομίχλη', + 'weather_blizzard' => 'Χιονοθύελλα', + 'weather_overcast' => 'Νεφελώδης', + 'weather_cloudy' => 'Συννεφιά', + 'weather_partly-cloudy-day' => 'Αραιές νεφώσεις', + 'weather_partly-cloudy-night' => 'Αραιές νεφώσεις', + 'weather_freezing-drizzle' => 'Παγωμένο ψιλόβροχο', + 'weather_heavy-freezing-drizzle' => 'Heavy freezing drizzle', + 'weather_patchy-freezing-drizzle-possible' => 'Patchy freezing drizzle possible', + 'weather_ice-pellets' => 'Ice pellets', + 'weather_light-showers-of-ice-pellets' => 'Light showers of ice pellets', + 'weather_moderate-or-heavy-showers-of-ice-pellets' => 'Moderate or heavy showers of ice pellets', + 'weather_thundery-outbreaks-possible' => 'Thundery outbreaks possible', + 'weather_patchy-light-rain-with-thunder' => 'Patchy light rain with thunder', + 'weather_moderate-or-heavy-rain-with-thunder' => 'Moderate or heavy rain with thunder', + 'weather_patchy-light-snow-with-thunder' => 'Patchy light snow with thunder', + 'weather_moderate-or-heavy-snow-with-thunder' => 'Moderate or heavy snow with thunder', + 'weather_current_temperature_celsius' => ':temperature °C', + 'weather_current_temperature_fahrenheit' => ':temperature °F', + 'weather_current_title' => 'Τρέχων καιρός', + + // dav + 'dav_contacts' => 'Επαφές', + 'dav_contacts_description' => 'Επαφές του :name', + 'dav_birthdays' => 'Γενέθλια', + 'dav_birthdays_description' => 'Γενέθλια επαφών του :name', + 'dav_tasks' => 'Εργασίες', + 'dav_tasks_description' => 'εργασίες του :name', + + // contact list + 'contact_list_avatar' => 'Avatar', + 'contact_list_name' => 'Επαφή', + 'contact_list_description' => 'Περιγραφή', + +]; diff --git a/resources/lang/el/auth.php b/resources/lang/el/auth.php new file mode 100644 index 0000000..0135027 --- /dev/null +++ b/resources/lang/el/auth.php @@ -0,0 +1,89 @@ + 'Τα διαπιστευτήρια δεν ταιριάζουν με τα αρχεία μας.', + 'throttle' => 'Παρα πολλές προσπάθειες σύνδεσης. Παρακαλώ δοκιμάστε ξανά σε :seconds δευτερόλεπτα.', + 'not_authorized' => 'Δεν είστε εξουσιοδοτημένοι να εκτελέσετε αυτήν την ενέργεια', + 'signup_disabled' => 'Η εγγραφές είναι απενεργοποιημένες αυτήν τη στιγμή', + 'signup_error' => 'Παρουσιάστηκε σφάλμα κατά την εγγραφή του χρήστη', + 'back_homepage' => 'Επιστροφή στην αρχική σελίδα', + 'mfa_auth_otp' => 'Πραγματοποιήστε έλεγχο ταυτότητας με τη συσκευή δύο παραγόντων', + 'mfa_auth_webauthn' => 'Έλεγχος ταυτότητας με κλειδί ασφαλείας (WebAuthn)', + '2fa_title' => 'Έλεγχος Ταυτότητας Δυο Παραγόντων', + '2fa_wrong_validation' => 'Ο έλεγχος ταυτότητας δύο παραγόντων απέτυχε.', + '2fa_one_time_password' => 'Κωδικός ελέγχου ταυτότητας δύο παραγόντων', + '2fa_recuperation_code' => 'Πληκτρολογήστε έναν κωδικό ελέγχου ταυτότητας δύο παραγόντων', + '2fa_one_time_or_recuperation' => 'Εισάγετε έναν κωδικό πιστοποίησης 2 παραγόντων (2FA) ή έναν κωδικό ανάκτησης', + '2fa_otp_help' => 'Ανοίξτε την εφαρμογή ελέγχου ταυτότητας δύο παραγόντων στο κινητό σας και αντιγράψτε τον κωδικό', + + 'login_to_account' => 'Συνδεθείτε στο λογαριασμό σας', + 'login_with_recovery' => 'Συνδεθείτε με έναν κωδικό ανάκτησης', + 'login_again' => 'Παρακαλούμε συνδεθείτε στο λογαριασμό σας ξανά', + 'email' => 'Email', + 'password' => 'Κωδικός', + 'recovery' => 'Κωδικός ανάκτησης', + 'login' => 'Σύνδεση', + 'button_remember' => 'Να με θυμάσαι', + 'password_forget' => 'Ξεχάσατε τον κωδικό πρόσβασης;', + 'password_reset' => 'Επαναφορά κωδικού πρόσβασης', + 'use_recovery' => 'Ή μπορείτε να χρησιμοποιήσετε έναν κωδικό επαναφοράς', + 'signup_no_account' => 'Δεν έχετε λογαριασμό;', + 'signup' => 'Εγγραφή', + 'create_account' => 'Δημιουργήστε τον πρώτο λογαριασμό με εγγραφή', + 'change_language_title' => 'Αλλαγή γλώσσας:', + 'change_language' => 'Αλλαγή γλώσσας σε :lang', + + 'password_reset_title' => 'Επαναφορά κωδικού πρόσβασης', + 'password_reset_email' => 'Διεύθυνση E-mail', + 'password_reset_send_link' => 'Αποστολή συνδέσμου επαναφοράς κωδικού πρόσβασης', + 'password_reset_password' => 'Κωδικός πρόσβασης', + 'password_reset_password_confirm' => 'Επιβεβαίωση Κωδικού πρόσβασης', + 'password_reset_action' => 'Επαναφορά Κωδικού πρόσβασης', + 'password_reset_email_content' => 'Κάντε κλικ εδώ για να επαναφέρετε τον κωδικό πρόσβασής σας:', + + 'register_title_welcome' => 'Καλώς ήλθατε στην νέα εγκατάσταση του Monica', + 'register_create_account' => 'Πρέπει να δημιουργήσετε έναν λογαριασμό για να χρησιμοποιήσετε το Monica', + 'register_title_create' => 'Δημιουργήστε το λογαριασμό Monica', + 'register_login' => 'Συνδεθείτε αν έχετε ήδη λογαριασμό.', + 'register_email' => 'Εισάγετε μια έγκυρη διεύθυνση email', + 'register_email_example' => 'εσείς@σπίτι', + 'register_firstname' => 'Όνομα', + 'register_firstname_example' => 'π.χ. Κώστας', + 'register_lastname' => 'Επώνυμο', + 'register_lastname_example' => 'π.χ. Παπαδόπουλος', + 'register_password' => 'Κωδικός πρόσβασης', + 'register_password_example' => 'Εισάγετε έναν ασφαλή κωδικό', + 'register_password_confirmation' => 'Επιβεβαίωση Κωδικού πρόσβασης', + 'register_action' => 'Εγγραφή', + 'register_policy' => 'Η εγγραφή σας σημαίνει ότι διαβάσατε και συμφωνείτε με την Πολιτική απορρήτου και τους Όρους χρήσης.', + 'register_invitation_email' => 'Για λόγους ασφαλείας, αναφέρετε τη διεύθυνση ηλεκτρονικού ταχυδρομείου του ατόμου που σας προσκάλεσε να εγγραφείτε σε αυτόν τον λογαριασμό. Αυτές οι πληροφορίες παρέχονται στο email πρόσκλησης.', + + 'confirmation_title' => 'Επιβεβαίωση διεύθυνσης email', + 'confirmation_fresh' => 'Ένα επιβεβαιωτικό email στάλθηκε στην διεύθυνση ηλεκτρονικού ταχυδρομείου σας.', + 'confirmation_check' => 'Πριν προχωρήσετε, παρακαλώ ελέγξτε το email σας για τον σύνδεσμο επαλήθευσης.', + 'confirmation_request_another' => 'Αν δεν έχετε παραλάβει το email πατήστε εδώ για να αποστείλουμε νέο.', + + 'confirmation_again' => 'Αν θέλετε να αλλάξετε την διεύθυνση email σας πατήστε εδώ.', + 'email_change_current_email' => 'Διεύθυνση email αυτή τη στιγμή:', + 'email_change_title' => 'Αλλάξτε την διεύθυνση email σας', + 'email_change_new' => 'Νέα διεύθυνση email', + 'email_changed' => 'Η διεύθυνση email σας έχει αλλάξει. Ελέγξτε το γραμματοκιβώτιό σας για να το επικυρώσετε.', +]; diff --git a/resources/lang/el/changelog.php b/resources/lang/el/changelog.php new file mode 100644 index 0000000..ed533d8 --- /dev/null +++ b/resources/lang/el/changelog.php @@ -0,0 +1,12 @@ + 'Αλλαγές προϊόντος', + 'note' => 'Σημείωση: Δυστυχώς αυτή η σελίδα είναι διαθέσιμη μόνο στα Αγγλικά.', +]; diff --git a/resources/lang/el/dashboard.php b/resources/lang/el/dashboard.php new file mode 100644 index 0000000..92507e6 --- /dev/null +++ b/resources/lang/el/dashboard.php @@ -0,0 +1,42 @@ + 'Καλώς ήρθατε στο λογαριασμό σας!', + 'dashboard_blank_description' => 'Η εφαρμογή Monica είναι το μέρος για να οργανώσετε όλες τις αλληλεπιδράσεις που έχετε με τους ανθρώπους που σας ενδιαφέρουν.', + 'dashboard_blank_cta' => 'Προσθέστε την πρώτη σας επαφή', + 'dashboard_blank_illustration' => 'Εικονογράφηση από τον Freepik', + + 'notes_title' => 'Δεν έχετε σημειώσεις με αστέρι.', + + 'tab_recent_calls' => 'Πρόσφατες κλήσεις', + 'tab_favorite_notes' => 'Αγαπημένες σημειώσεις', + 'tab_calls_blank' => 'Δεν έχετε καταγράψει καμία κλήση ακόμα.', + 'tab_debts' => 'Χρέη', + 'tab_debts_blank' => 'Δεν έχετε καταγράψει ακόμη χρέη.', + 'tab_tasks' => 'Εργασίες', + 'tab_tasks_blank' => 'Δεν έχετε ακόμα καμία εργασία.', + + 'tasks_add_task_placeholder' => 'Τι είναι αυτή η εργασία;', + 'tasks_tab_your_contacts' => 'Εργασίες που σχετίζονται με τις επαφές σας', + 'tasks_tab_your_tasks' => 'Οι εργασίες σας', + 'tasks_add_note' => 'Πατήστε Enter για να προσθέσετε την εργασία.', + 'task_add_cta' => 'Προσθήκη Εργασίας', + + 'debts_you_owe' => 'Χρωστάτε', + + 'statistics_contacts' => 'Επαφές', + 'statistics_activities' => 'Δραστηριότητες', + 'statistics_gifts' => 'Δώρα', + + 'reminders_next_months' => 'Γεγονότα τους επόμενους 3 μήνες', + 'reminders_none' => 'Δεν υπάρχουν υπενθυμίσεις για αυτό το μήνα.', + + 'product_changes' => 'Αλλαγές προϊόντος', + 'product_view_details' => 'Προβολή λεπτομερειών', +]; diff --git a/resources/lang/el/format.php b/resources/lang/el/format.php new file mode 100644 index 0000000..a70a6ba --- /dev/null +++ b/resources/lang/el/format.php @@ -0,0 +1,36 @@ + 'M d, Y H:i', + 'short_date_year' => 'M d, Y', + 'short_date' => 'M d', + 'short_month' => 'M', + 'short_month_year' => 'M Y', + 'short_day' => 'D', + 'full_date_year' => 'F d, Y', + 'full_month' => 'F', + 'full_month_year' => 'F Y', + 'full_hour' => 'h.i A', + + 'short_text' => '{text}…', +]; diff --git a/resources/lang/el/journal.php b/resources/lang/el/journal.php new file mode 100644 index 0000000..50b4d51 --- /dev/null +++ b/resources/lang/el/journal.php @@ -0,0 +1,38 @@ + 'Πώς ήταν η μέρα σας? Μπορείτε να αξιολογήσετε μια φορά την ημέρα.', + 'journal_come_back' => 'Ευχαριστούμε. Ελάτε αύριο για να αξιολογήσετε τη μέρα σας και πάλι.', + 'journal_description' => 'Σημείωση: το ημερολόγιο παραθέτει τόσο τις χειροκίνητες εγγραφές ημερολογίου όσο και τις αυτόματες εγγραφές όπως Δραστηριότητες που έγιναν με τις επαφές σας. Ενώ μπορείτε να διαγράψετε χειροκίνητα τις εγγραφές ημερολογίου, θα πρέπει να διαγράψετε τη Δραστηριότητα απευθείας στη σελίδα της επαφής.', + 'journal_add' => 'Προσθήκη καταχώρησης ημερολογίου', + 'journal_edit' => 'Επεξεργασία μιας καταχώρησης ημερολογίου', + 'journal_empty' => 'Κενό ημερολόγιο', + 'journal_created_at' => 'Δημιουργήθηκε στις {date}', + 'journal_created_automatically' => 'Δημιουργήθηκε αυτόματα', + 'journal_entry_type_journal' => 'Καταχώρηση ημερολογίου', + 'journal_entry_type_activity' => 'Δραστηριότητα', + 'journal_entry_rate' => 'Αξιολογήσατε την ημέρα σας.', + 'journal_add_comment' => 'Θέλετε να προσθέσετε ένα σχόλιο (προαιρετικό);', + 'journal_show_comment' => 'Προβολή σχολίων', + 'entry_delete_success' => 'Η καταχώρηση ημερολογίου έχει διαγραφεί επιτυχώς.', + 'journal_add_title' => 'Τίτλος (προαιρετικό)', + 'journal_add_date' => 'Ημερομηνία', + 'journal_add_post' => 'Καταχώριση', + 'journal_add_cta' => 'Aποθήκευση', + 'journal_blank_cta' => 'Προσθέστε την πρώτη καταχώρηση ημερολογίου', + 'journal_blank_description' => 'Το ημερολόγιο σας επιτρέπει να γράφετε γεγονότα που σας συνέβησαν και να τα θυμάστε.', + 'delete_confirmation' => 'Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτή την καταχώρηση ημερολογίου;', + 'apply_filter' => 'Apply filter', + 'start_date' => 'Start Date', + 'end_date' => 'End Date', + 'per_page' => 'Per Page', + 'sort_order' => 'Sort By Created At', + 'ascending' => 'Ascending', + 'descending' => 'Descending', +]; diff --git a/resources/lang/el/logs.php b/resources/lang/el/logs.php new file mode 100644 index 0000000..3805e1c --- /dev/null +++ b/resources/lang/el/logs.php @@ -0,0 +1,29 @@ + 'Δημιουργήθηκε νέα επαφή.', + 'settings_log_contact_created_with_name' => 'Προστέθηκε :name ως επαφή.', + + // contat description update + 'contact_log_contact_description_updated' => 'Ενημερώθηκε η περιγραφή.', + 'settings_log_contact_description_updated_with_name' => 'Ενημερώθηκε η περιγραφή του :name.', + + // contact description clear + 'contact_log_contact_description_cleared' => 'Διαγράφηκε η περιγραφή.', + 'settings_log_contact_description_cleared_with_name' => 'Διαγράφηκε η περιγραφή του :name.', + + // contact work information update + 'contact_log_contact_work_updated' => 'Ενημέρωση πληροφοριών εργασίας.', + 'settings_log_contact_work_updated_with_name' => 'Ενημέρωση πληροφοριών εργασίας του :name.', + + // company created + 'settings_log_company_created' => 'Δημιουργήθηκε εταιρεία με όνομα :name.', +]; diff --git a/resources/lang/el/mail.php b/resources/lang/el/mail.php new file mode 100644 index 0000000..1ac8000 --- /dev/null +++ b/resources/lang/el/mail.php @@ -0,0 +1,53 @@ + 'Υπενθύμιση για :contact', + 'greetings' => 'Γεια σου :username', + 'want_reminded_of' => 'Είχατε ζητήσει υπενθύμιση για :reason', + 'for' => 'Προς: :name', + 'comment' => 'Σχόλιο: :comment', + 'footer_contact_info' => 'Προσθέστε, δείτε, ολοκληρώστε και επεξεργαστείτε τις πληροφορίες για αυτή την επαφή:', + 'footer_contact_info2' => 'Δείτε το προφίλ του :name', + 'footer_contact_info2_link' => 'Δείτε τη διεύθυνση του προφίλ του :name', + + 'notification_subject_line' => 'Έχετε μια επερχόμενη εκδήλωση', + 'notification_description' => 'Σε :count ημέρες (:date) έχετε την επόμενη εκδήλωση:', + + 'stay_in_touch_subject_line' => 'Μείνετε σε επαφή με :name', + 'stay_in_touch_subject_description' => 'Ζητήσατε να σας υπενθυμίζουμε να μένετε σε επαφή με τον :name κάθε :frequency μέρα.|Ζητήσατε να σας υπενθυμίζουμε να μένετε σε επαφή με τον :name κάθε :frequency μέρες.', + + 'notifications_whoops' => 'Ουπς!', + 'notifications_hello' => 'Γειά!', + 'notifications_regards' => 'Με εκτίμηση,', + 'notifications_footer' => 'Εάν αντιμετωπίζετε προβλήματα κάνοντας κλικ στο κουμπί ":actionText", αντιγράψτε και επικολλήστε την παρακάτω διεύθυνση URL στο πρόγραμμα περιήγησης: [:actionURL] (:actionURL)', + 'notifications_rights' => 'Με επιφύλαξη παντός δικαιώματος', + + 'confirmation_email_title' => 'Monica – Email επιβεβαίωσης', + 'confirmation_email_intro'=> 'Για να επιβεβαιώσετε το email σας, πατήστε στο παρακάτω κουμπί', + 'confirmation_email_button' => 'Επιβεβαίωση διεύθυνσης email', + 'confirmation_email_bottom' => 'Αν δεν δημιουργήστε έναν λογαριασμό, δεν χρειάζεται κάποια περαιτέρω ενέργεια.', + + 'password_reset_title' => 'Monica – Ειδοποίηση Επαναφοράς Κωδικού Πρόσβασης', + 'password_reset_intro' => 'Λαμβάνετε αυτό το email γιατί λάβαμε μία αίτηση για επαναφορά κωδικού πρόσβασης για αυτό το λογαριασμό.', + 'password_reset_button' => 'Επαναφορά κωδικού πρόσβασης', + 'password_reset_expiration' => 'Αυτός ο σύνδεσμος επαναφοράς κωδικού πρόσβασης θα λήξει σε :count λεπτά.', + 'password_reset_bottom' => 'Αν δεν ζητήσατε επαναφορά κωδικού πρόσβασης, δεν χρειάζεται κάποια περαιτέρω ενέργεια.', + + 'invitation_title' => 'Monica – Η επαφή :name σας έχει προσκαλέσει', + 'invitation_intro' => 'Η επαφή :name (:email) σας προσκάλεσε να χρησιμοποιήσετε το Monica, ένα ωραίο εργαλείο Διαχείρισης Προσωπικών Σχέσεων.', + 'invitation_link' => 'Για να αποδεχτείτε την πρόσκληση, πατήστε στον σύνδεσμο παρακάτω:', + 'invitation_button' => 'Αποδοχή πρόσκλησης', + 'invitation_expiration' => 'Ο σύνδεσμος θα λήξει σε :count μέρες.', + + 'export_title' => 'Το αρχείο εξαγωγής δεδομένων είναι έτοιμο', + 'export_description' => 'Ζητήσατε εξαγωγή δεδομένων στις :date. Τώρα είναι έτοιμο για λήψη.', + 'export_download' => 'Λήψη εξαγωγής δεδομένων', + +]; diff --git a/resources/lang/el/pagination.php b/resources/lang/el/pagination.php new file mode 100644 index 0000000..f272fc0 --- /dev/null +++ b/resources/lang/el/pagination.php @@ -0,0 +1,25 @@ + '❮ Προηγούμενη', + 'next' => 'Επόμενη ❯', + +]; diff --git a/resources/lang/el/passwords.php b/resources/lang/el/passwords.php new file mode 100644 index 0000000..cb28304 --- /dev/null +++ b/resources/lang/el/passwords.php @@ -0,0 +1,30 @@ + 'Έχει γίνει επαναφορά του κωδικού πρόσβασης σας!', + 'sent' => 'Αν το email που πληκτρολογήσατε υπάρχει στις εγγραφές μας, θα σας έχουμε στείλει έναν σύνδεσμο για επαναφορά κωδικού πρόσβασης.', + 'token' => 'Αυτό το διακριτικό επαναφοράς κωδικού πρόσβασης δεν είναι έγκυρο.', + 'user' => 'Αν το email που πληκτρολογήσατε υπάρχει στις εγγραφές μας, θα σας έχουμε στείλει έναν σύνδεσμο για επαναφορά κωδικού πρόσβασης.', + 'changed' => 'Ο κωδικός πρόσβασης άλλαξε επιτυχώς.', + 'invalid' => 'Ο τρέχων κωδικός πρόσβασης που εισάγατε δεν είναι σωστός.', + 'throttled' => 'Παρακαλώ περιμένετε πριν ξαναπροσπαθήσετε.', + +]; diff --git a/resources/lang/el/people.php b/resources/lang/el/people.php new file mode 100644 index 0000000..19c7ebd --- /dev/null +++ b/resources/lang/el/people.php @@ -0,0 +1,539 @@ + 'Δεν βρέθηκε η επαφή', + 'people_list_number_kids' => ':count παιδί|:count παιδιά', + 'people_list_last_updated' => 'Συμβουλευτήκατε τελευταία:', + 'people_list_number_reminders' => ':count υπενθύμιση|:count υπενθυμίσεις', + 'people_list_blank_title' => 'Δεν υπάρχει κάποια επαφή στον λογαριασμό σας ακόμη', + 'people_list_blank_cta' => 'Προσθέστε κάποιον', + 'people_list_sort' => 'Ταξινόμηση', + 'people_list_stats' => ':count επαφή|:count επαφές', + 'people_list_firstnameAZ' => 'Ταξινόμηση με Όνομα Α → Ω', + 'people_list_firstnameZA' => 'Ταξινόμηση με Όνομα Ω → Α', + 'people_list_lastnameAZ' => 'Ταξινόμηση με Επώνυμο Α → Ω', + 'people_list_lastnameZA' => 'Ταξινόμηση με Επώνυμο Ω → Α', + 'people_list_lastactivitydateNewtoOld' => 'Ταξινόμηση κατά την τελευταία ημερομηνία δραστηριότητας, νεότερη έως παλαιότερη', + 'people_list_lastactivitydateOldtoNew' => 'Ταξινόμηση κατά την τελευταία ημερομηνία δραστηριότητας, παλαιότερη έως νεότερη', + 'people_list_filter_tag' => 'Εμφάνιση όλων των επαφών με ετικέτα', + 'people_list_clear_filter' => 'Κατάργηση φίλτρου', + 'people_list_contacts_per_tags' => ':count επαφή|:count επαφές', + 'people_list_show_dead' => 'Εμφάνιση θανόντων (:count)', + 'people_list_hide_dead' => 'Απόκρυψη θανόντων (:count)', + 'people_search' => 'Αναζήτηση στις επαφές σας…', + 'people_search_no_results' => 'Δε βρέθηκαν αποτελέσματα', + 'people_search_next' => 'Επόμενο', + 'people_search_prev' => 'Προηγούμενο', + 'people_search_rows_per_page' => 'Γραμμές ανά σελίδα', + 'people_search_of' => 'από', + 'people_search_page' => 'Σελίδα', + 'people_search_all' => 'Όλα', + 'people_add_new' => 'Προσθήκη νέου ατόμου', + 'people_list_account_usage' => 'Χρήση του λογαριασμού σας: :current/:limit επαφές', + 'people_list_account_upgrade_title' => 'Αναβαθμίστε τον λογαριασμό σας για να ξεκλειδώσετε όλες τις δυνατότητες του.', + 'people_list_account_upgrade_cta' => 'Αναβάθμιση τώρα', + 'people_list_untagged' => 'Εμφάνιση επαφών χωρίς ετικέτα', + 'people_list_filter_untag' => 'Εμφάνιση όλων των επαφών χωρίς ετικέτες', + 'archived_contact_readonly' => 'Δεν είναι δυνατή η επεξεργασία της αρχειοθετημένης επαφής, παρακαλώ επαναφέρετε την επαφή πρώτα.', + + // people add + 'people_add_title' => 'Προσθήκη νέου ατόμου', + 'people_add_missing' => 'Δεν βρέθηκε άτομο – προσθέστε ένα νέο τώρα', + 'people_add_firstname' => 'Όνομα', + 'people_add_middlename' => 'Μεσαίο όνομα (προαιρετικό)', + 'people_add_lastname' => 'Επίθετο (προαιρετικό)', + 'people_add_email' => 'Email (προαιρετικό)', + 'people_add_nickname' => 'Ψευδώνυμο (προαιρετικό)', + 'people_add_cta' => 'Προσθήκη', + 'people_save_and_add_another_cta' => 'Αποθήκευση και προσθήκη νέου', + 'people_add_success' => 'Η επαφή :name δημιουργήθηκε με επιτυχία', + 'people_add_gender' => 'Φύλο', + 'people_delete_success' => 'Η επαφή έχει διαγραφεί', + 'people_delete_message' => 'Διαγραφή επαφής', + 'people_delete_confirmation' => 'Είστε σίγουροι ότι θέλετε να διαγράψετε την επαφή :name; Η διαγραφή είναι άμεση και μόνιμη.', + 'people_add_birthday_reminder' => 'Ευχηθείτε χρόνια πολλά στον :name', + 'people_add_birthday_reminder_deceased' => 'Την ημερομηνία αυτή, η επαφή :name, θα είχε γενέθλια', + 'people_add_import' => 'Θέλετε να εισαγάγετε τις επαφές σας;', + 'people_edit_email_error' => 'Μία επαφή με αυτή τη διεύθυνση email υπάρχει ήδη στον λογαριασμό σας. Παρακαλώ διαλέξτε μία άλλη.', + 'people_export' => 'Εξαγωγή ως vCard', + 'people_add_reminder_for_birthday' => 'Δημιουργία ετήσιας υπενθύμισης γενεθλίων', + + // show + 'section_contact_information' => 'Πληροφορίες επικοινωνίας', + 'section_personal_activities' => 'Δραστηριότητες', + 'section_personal_reminders' => 'Υπενθυμίσεις', + 'section_personal_tasks' => 'Εργασίες', + 'section_personal_gifts' => 'Δώρα', + 'section_personal_notes' => 'Σημειώσεις', + + // archived contacts + 'list_link_to_active_contacts' => 'Βλέπετε τις αρχειοθετημένες επαφές. Κάντε κλικ εδώ για να δείτε τη λίστα με τις ενεργές επαφές.', + 'list_link_to_archived_contacts' => 'Λίστα αρχειοθετημένων επαφών', + + // Header + 'me' => 'Αυτός είσαι εσύ', + 'edit_contact_information' => 'Επεξεργασία πληροφοριών επικοινωνίας', + 'contact_archive' => 'Αρχειοθέτηση επαφής', + 'contact_unarchive' => 'Ανάκληση αρχειοθέτησης επαφής', + 'contact_archive_help' => 'Οι αρχειοθετημένες επαφές δεν εμφανίζονται στη λίστα επαφών, αλλά εξακολουθούν να εμφανίζονται στα αποτελέσματα αναζήτησης.', + 'call_button' => 'Καταχώρηση κλήσης', + 'set_favorite' => 'Οι αγαπημένες επαφές εμφανίζονται στο πάνω μέρος της λίστας επαφών', + + // Stay in touch + 'stay_in_touch' => 'Μείνετε σε επαφή', + 'stay_in_touch_frequency' => 'Μείνετε σε επαφή κάθε μέρα|Μείνετε σε επαφή κάθε {count} μέρες', + 'stay_in_touch_next_date' => 'Επόμενη φορά: {date}', + 'stay_in_touch_invalid' => 'Η συχνότητα πρέπει να είναι ένας αριθμός μεγαλύτερος του 0.', + 'stay_in_touch_premium' => 'Πρέπει να κάνετε αναβάθμιση του λογαριασμού για να χρησιμοποιήσετε αυτή τη δυνατότητα', + 'stay_in_touch_modal_title' => 'Μείνετε σε επαφή', + 'stay_in_touch_modal_desc' => 'Μπορούμε να σας υπενθυμίζουμε με email να μείνετε σε επαφή με την επαφή {firstname} σε σταθερό ρυθμό.', + 'stay_in_touch_modal_label' => 'Στείλε μου email κάθε… {count} μέρα|Στείλε μου email κάθε… {count} μέρες', + + // Calls + 'modal_call_title' => 'Καταχώρηση κλήσης', + 'modal_call_comment' => 'Για τι μιλήσατε; (προαιρετικό)', + 'modal_call_exact_date' => 'Το τηλεφώνημα έγινε στις', + 'modal_call_who_called' => 'Ποιός πήρε τηλέφωνο;', + 'modal_call_emotion' => 'Θέλετε να καταχωρήσετε το πως νιώσατε κατά τη διάρκεια του τηλεφωνήματος; (προαιρετικό)', + 'calls_add_success' => 'Η εγγραφή αποθηκεύτηκε.', + 'call_delete_confirmation' => 'Είστε σίγουροι ότι θέλετε να διαγράψετε την εγγραφή;', + 'call_delete_success' => 'Η εγγραφή έχει διαγραφεί με επιτυχία', + 'call_title' => 'Τηλεφωνικές κλήσεις', + 'call_empty_comment' => 'Δεν υπάρχουν λεπτομέρειες', + 'call_blank_title' => 'Παρακολουθήστε τις τηλεφωνικές κλήσεις που έχετε πραγματοποιήσει με την επαφή {name}', + 'call_blank_desc' => 'Κλήση σε {name}', + 'call_you_called' => 'Καλέσατε εσείς', + 'call_he_called' => 'κάλεσε η επαφή {name}', + 'call_emotions' => 'Συναισθήματα:', + + // Conversation + 'conversation_blank' => 'Καταγράψτε τις συνομιλίες σας με :name στα μέσα κοινωνικής δικτύωσης, SMS…', + 'conversation_delete_link' => 'Διαγραφή της συνομιλίας', + 'conversation_edit_title' => 'Επεξεργασία της συνομιλίας', + 'conversation_edit_delete' => 'Είστε σίγουροι ότι θέλετε να διαγράψετε αυτή την επαφή; Δεν γίνεται αναίρεση.', + 'conversation_add_success' => 'Η συνομιλία προστέθηκε με επιτυχία.', + 'conversation_edit_success' => 'Η συνομιλία ενημερώθηκε με επιτυχία.', + 'conversation_delete_success' => 'Η συνομιλία έχει διαγραφεί επιτυχώς.', + 'conversation_add_title' => 'Εγγραφή μιας νέας συνομιλίας', + 'conversation_add_when' => 'Πότε είχατε αυτή τη συνομιλία;', + 'conversation_add_who_wrote' => 'Ποιος έστειλε αυτό το μήνυμα;', + 'conversation_add_how' => 'Πώς επικοινωνήσατε;', + 'conversation_add_you' => 'Εσείς', + 'conversation_add_content' => 'Γράψτε τι ειπώθηκε', + 'conversation_add_what_was_said' => 'Τι είπατε;', + 'conversation_add_another' => 'Προσθέστε άλλο ένα μήνυμα', + 'conversation_add_error' => 'Πρέπει να προσθέσετε τουλάχιστον ένα μήνυμα.', + 'conversation_list_table_messages' => 'Μυνήματα', + 'conversation_list_table_content' => 'Μερικό περιεχόμενο (τελευταίο μήνυμα)', + 'conversation_list_title' => 'Συνομιλίες', + 'conversation_list_cta' => 'Καταγράψτε μία συνομιλία', + + // age - birthday + 'birthdate_not_set' => 'Δεν έχει οριστεί ημέρα γενεθλίων', + 'age_approximate_in_years' => 'περίπου :age ετών', + 'age_exact_in_years' => ':age ετών', + 'age_exact_birthdate' => 'γεννήθηκε :date', + + // Last called + 'last_called' => 'Τελευταία κλήση: :date', + 'last_talked_to' => 'Τελευταία κλήση: {date}', + 'last_called_empty' => 'Τελευταία κλήση: άγνωστο', + 'last_activity_date' => 'Τελευταία δραστηριότητα μαζί: :date', + 'last_activity_date_empty' => 'Τελευταία δραστηριότητα μαζί: άγνωστο', + + // additional information + 'information_edit_success' => 'Προφίλ έχει ενημερωθεί με επιτυχία', + 'information_edit_title' => 'Επεξεργαστείτε τις προσωπικές πληροφορίες της επαφής :name', + 'information_edit_max_size' => 'Μέγιστο :size Kb.', + 'information_edit_max_size2' => 'Μέγιστο {size} Kb.', + 'information_edit_firstname' => 'Όνομα', + 'information_edit_lastname' => 'Επίθετο (προαιρετικό)', + 'information_edit_description' => 'Περιγραφή (προαιρετική)', + 'information_edit_description_help' => 'Χρησιμοποιείται στη λίστα επαφών για να προσθέσετε κάποια διευκρίνιση, εάν είναι απαραίτητη.', + 'information_edit_unknown' => 'Δεν γνωρίζω την ηλικία αυτού του ατόμου', + 'information_edit_probably' => 'Αυτό το άτομο είναι περίπου…', + 'information_edit_not_year' => 'Ξέρω την ημέρα και το μήνα των γενεθλίων αυτού του ατόμου, αλλά όχι το έτος…', + 'information_edit_exact' => 'Γνωρίζω τα ακριβή γενέθλια αυτού του ατόμου…', + 'information_edit_birthdate_label' => 'Ημερομηνία γέννησης', + 'information_no_work_defined' => 'Δεν έχουν προσδιοριστεί πληροφορίες εργασίας', + 'information_work_at' => 'στο :company', + 'work_add_cta' => 'Ενημέρωση πληροφοριών εργασίας', + 'work_edit_success' => 'Οι πληροφορίες εργασίας ενημερώθηκαν', + 'work_edit_title' => 'Ενημέρωση πληροφοριών εργασίας της επαφής :name', + 'work_edit_job' => 'Επαγγελματικός τίτλος (προαιρετικό)', + 'work_edit_company' => 'Εταιρεία (προαιρετικό)', + 'work_information' => 'Πληροφορίες εργασίας', + + // food preferences + 'food_preferences_add_success' => 'Οι διατροφικές προτιμήσεις έχουν αποθηκευτεί', + 'food_preferences_edit_description' => 'Ίσως η επαφή με το όνομα :firstname ή κάποιος άλλος στην οικογένεια :family να έχει αλλεργία. Ή δεν του αρέσει ένα συγκεκριμένο μπουκάλι κρασί. Σημειώστε το εδώ για να το θυμάστε την επόμενη φορά που θα τους καλέσετε για δείπνο', + 'food_preferences_edit_description_no_last_name' => 'Ίσως η επαφή με το όνομα :firstname να έχει αλλεργία. Ή δεν του αρέσει ένα συγκεκριμένο μπουκάλι κρασί. Σημειώστε το εδώ για να το θυμάστε την επόμενη φορά που θα τον καλέσετε για δείπνο', + 'food_preferences_edit_title' => 'Προσδιορίστε τις διατροφικές προτιμήσεις', + 'food_preferences_edit_cta' => 'Αποθήκευση διατροφικών προτιμήσεων', + 'food_preferences_title' => 'Διατροφικές προτιμήσεις', + 'food_preferences_cta' => 'Προσθήκη διατροφικών προτιμήσεων', + + // reminders + 'reminders_blank_title' => 'Υπάρχει κάτι που θέλετε να σας υπενθυμίσουμε για το άτομο :name;', + 'reminders_blank_add_activity' => 'Προσθήκη υπενθύμισης', + 'reminders_add_title' => 'Τι θα θέλατε να σας υπενθυμίσουμε για το άτομο :name;', + 'reminders_add_description' => 'Παρακαλώ υπενθυμίστε μου να…', + 'reminders_add_next_time' => 'Πότε είναι η επόμενη φορά που θα θέλατε να σας το υπενθυμίσουμε;', + 'reminders_add_once' => 'Υπενθύμιση γι\' αυτό μόνο μία φορά', + 'reminders_add_recurrent' => 'Υπενθύμιση για αυτό κάθε', + 'reminders_add_starting_from' => 'ξεκινώντας από την παραπάνω ημερομηνία', + 'reminders_add_cta' => 'Προσθήκη υπενθύμισης', + 'reminders_edit_update_cta' => 'Ενημερώση υπενθύμισης', + 'reminders_add_error_custom_text' => 'Πρέπει να υποδείξετε ένα κείμενο για αυτήν την υπενθύμιση', + 'reminders_create_success' => 'Η υπενθύμιση προστέθηκε με επιτυχία', + 'reminders_delete_success' => 'Η υπενθύμιση έχει διαγραφεί με επιτυχία', + 'reminders_update_success' => 'Η υπενθύμιση έχει ενημερωθεί με επιτυχία', + 'reminders_add_optional_comment' => 'Προαιρετικό σχόλιο', + + 'reminder_frequency_day' => 'κάθε μέρα|κάθε :number μέρες', + 'reminder_frequency_week' => 'κάθε εβδομάδα|κάθε :number εβδομάδες', + 'reminder_frequency_month' => 'κάθε μήνα|κάθε :number μήνες', + 'reminder_frequency_year' => 'κάθε έτος|κάθε :number έτη', + 'reminder_frequency_one_time' => 'στις :date', + 'reminders_delete_confirmation' => 'Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτήν την υπενθύμιση;', + 'reminders_delete_cta' => 'Διαγραφή', + 'reminders_next_expected_date' => 'στις', + 'reminders_cta' => 'Προσθήκη υπενθύμισης', + 'reminders_description' => 'Θα στείλουμε ένα μήνυμα ηλεκτρονικού ταχυδρομείου για κάθε μία από τις υπενθυμίσεις παρακάτω. Οι υπενθυμίσεις αποστέλλονται κάθε πρωί την ημέρα που θα συμβούν γεγονότα. Οι υπενθυμίσεις που προστίθενται αυτόματα για τα γενέθλια δεν μπορούν να διαγραφούν. Αν θέλετε να αλλάξετε αυτές τις ημερομηνίες, επεξεργαστείτε τα γενέθλια των επαφών.', + 'reminders_one_time' => 'Μία φορά', + 'reminders_type_week' => 'εβδομάδα', + 'reminders_type_month' => 'μήνας', + 'reminders_type_year' => 'έτος', + 'reminders_birthday' => 'Γενέθλια του ατόμου :name', + 'reminders_free_plan_warning' => 'Είστε στο δωρεάν πρόγραμμα. Δεν αποστέλλονται μηνύματα ηλεκτρονικού ταχυδρομείου σε αυτό το σχέδιο. Για να λάβετε τις υπενθυμίσεις σας μέσω ηλεκτρονικού ταχυδρομείου, αναβαθμίστε το λογαριασμό σας.', + + // relationships + 'relationship_form_add' => 'Προσθήκη σχέσης', + 'relationship_form_edit' => 'Επεξεργασία υπάρχουσας σχέσης', + 'relationship_form_is_with' => 'Αυτό το άτομο είναι…', + 'relationship_form_is_with_name' => 'Η επαφή :name είναι…', + 'relationship_form_add_choice' => 'Με ποιον είναι η σχέση;', + 'relationship_form_create_contact' => 'Προσθήκη νέου ατόμου', + 'relationship_form_associate_contact' => 'Υπάρχουσα επαφή', + 'relationship_form_associate_dropdown' => 'Αναζήτηση και επιλογή μιας υπάρχουσας επαφής από το πτυσσόμενο μενού παρακάτω', + 'relationship_form_associate_dropdown_placeholder' => 'Αναζήτηση και επιλογή μιας υπάρχουσας επαφής', + 'relationship_form_also_create_contact' => 'Δημιουργήστε μια καταχώρηση επαφής για αυτό το άτομο.', + 'relationship_form_add_description' => 'Αυτό θα σας επιτρέψει να χειριστείτε αυτό το άτομο όπως κάθε άλλη επαφή.', + 'relationship_form_add_no_existing_contact' => 'Δεν έχετε επαφές που σχετίζονται με την επαφή :name αυτή τη στιγμή.', + 'relationship_delete_confirmation' => 'Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτή τη σχέση? Η διαγραφή είναι μόνιμη.', + 'relationship_unlink_confirmation' => 'Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτή τη σχέση? Αυτό το άτομο δεν θα διαγραφεί – μόνο η σχέση μεταξύ των δύο.', + 'relationship_form_add_success' => 'Η σχέση έχει οριστεί με επιτυχία.', + 'relationship_form_deletion_success' => 'Η σχέση έχει διαγραφεί.', + + // tasks + 'tasks_title' => 'Εργασίες', + 'tasks_blank_title' => 'Δεν έχετε καμία εργασία.', + 'tasks_form_title' => 'Τίτλος', + 'tasks_form_description' => 'Περιγραφή (προαιρετικό)', + 'tasks_add_task' => 'Προσθέστε μια εργασία', + 'tasks_delete_success' => 'Η εργασία έχει διαγραφεί με επιτυχία', + 'tasks_complete_success' => 'Η κατάσταση της εργασίες άλλαξε επιτυχώς', + + // activities + 'activity_title' => 'Δραστηριότητες', + 'activity_type_category_simple_activities' => 'Απλές δραστηριότητες', + 'activity_type_category_sport' => 'Αθλητισμός', + 'activity_type_category_food' => 'Φαγητό', + 'activity_type_category_cultural_activities' => 'Πολιτιστικές δραστηριότητες', + 'activity_type_just_hung_out' => 'απλά κάναμε παρέα', + 'activity_type_watched_movie_at_home' => 'παρακολουθήσαμε μια ταινία στο σπίτι', + 'activity_type_talked_at_home' => 'απλά μιλήσαμε στο σπίτι', + 'activity_type_did_sport_activities_together' => 'κάναμε ένα άθλημα μαζί', + 'activity_type_ate_at_his_place' => 'έφαγα στο σπίτι τους', + 'activity_type_went_bar' => 'πήγαμε σε ένα μπαρ', + 'activity_type_ate_at_home' => 'φάγαμε στο σπίτι', + 'activity_type_picnicked' => 'κάναμε πικ νικ', + 'activity_type_ate_restaurant' => 'φάγαμε σε ένα εστιατόριο', + 'activity_type_went_theater' => 'πήγαμε στο θέατρο', + 'activity_type_went_concert' => 'πήγαμε σε μια συναυλία', + 'activity_type_went_play' => 'πήγαμε σε μία παράσταση', + 'activity_type_went_museum' => 'πήγαμε στο μουσείο', + 'activities_add_activity' => 'Προσθήκη δραστηριότητας', + 'activities_add_more_details' => 'Προσθέστε περισσότερες λεπτομέρειες', + 'activities_add_emotions' => 'Προσθήκη συναισθημάτων', + 'activities_add_category' => 'Δηλώστε μια κατηγορία', + 'activities_add_participants_cta' => 'Προσθήκη συμμετεχόντων', + 'activities_item_information' => ':Activity. Συνέβη στις :date', + 'activities_add_title' => 'Τι κάνατε με {name};', + 'activities_summary' => 'Περιγράψτε τι κάνατε', + 'activities_add_pick_activity' => 'Θα θέλατε να κατηγοριοποιήσετε αυτήν τη δραστηριότητα; Δεν χρειάζεται, αλλά θα σας δώσει στατιστικά αργότερα (προαιρετικό)', + 'activities_add_date_occured' => 'Η δραστηριότητα συνέβη την…', + 'activities_add_participants' => 'Ποιοι, εκτός από την επαφή {name}, συμμετείχαν σε αυτή τη δραστηριότητα; (προαιρετικό)', + 'activities_add_emotions_title' => 'Θέλετε να καταγράψετε πώς αισθανθήκατε κατά τη διάρκεια αυτής της δραστηριότητας; (προαιρετικό)', + 'activities_blank_title' => 'Παρακολουθήστε τι έχετε κάνει με την επαφή {name} στο παρελθόν και για τι έχετε μιλήσει', + 'activities_blank_add_activity' => 'Προσθήκη δραστηριότητας', + 'activities_add_success' => 'Η δραστηριότητα προστέθηκε με επιτυχία', + 'activities_add_error' => 'Σφάλμα κατά την προσθήκη δραστηριότητας', + 'activities_update_success' => 'Η δραστηριότητα ενημερώθηκε με επιτυχία', + 'activities_delete_success' => 'Η δραστηριότητα διαγράφηκε με επιτυχία', + 'activities_who_was_involved' => 'Ποιός συμμετείχε;', + 'activities_activity' => 'Κατηγορία Δραστηριότητας', + 'activities_view_activities_report' => 'Προβολή αναφοράς δραστηριοτήτων', + 'activities_profile_title' => 'Αναφορά δραστηριοτήτων μεταξύ :name και εσάς', + 'activities_profile_subtitle' => 'Έχετε καταγράψει συνολικά :total_activities δραστηριότητα με την επαφή :name ενώ ήταν :activities_last_twelve_months τους τελευταίους 12 μήνες . Έχετε καταγράψει συνολικά :total_activities δραστηριότητες με την επαφή :name ενώ ήταν :activities_last_twelve_months τους τελευταίους 12 μήνες.', + 'activities_profile_year_summary_activity_types' => 'Ακολουθεί μια ανάλυση του είδους των δραστηριοτήτων που έχετε κάνει μαζί το :year', + 'activities_profile_year_summary' => 'Δείτε εδώ τι έχετε κάνει εσείς οι δύο το :year', + 'activities_profile_number_occurences' => ':value δραστηριότητα|:value δραστηριότητες', + 'activities_list_participants' => 'Συμμετέχοντες ({total}):', + 'activities_list_emotions' => 'Συναισθήματα:', + 'activities_list_date' => 'Συνέβη στις', + 'activities_list_category' => 'Κατηγορία:', + + // notes + 'notes_create_success' => 'Η σημείωση έχει δημιουργηθεί με επιτυχία', + 'notes_update_success' => 'Η σημείωση αποθηκεύτηκε με επιτυχία', + 'notes_delete_success' => 'Η σημείωση διαγράφηκε με επιτυχία', + 'notes_add_cta' => 'Προσθήκη σημείωσης', + 'notes_favorite' => 'Προσθήκη/διαγραφή από τα αγαπημένα', + 'notes_delete_title' => 'Διαγραφή σημείωσης', + 'notes_delete_confirmation' => 'Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτή τη σημείωση; Η διαγραφή είναι μόνιμη', + + // gifts + 'gifts_title' => 'Δώρα', + 'gifts_add_success' => 'Το δώρο έχει προστεθεί με επιτυχία', + 'gifts_delete_success' => 'Το δώρο έχει διαγραφεί με επιτυχία', + 'gifts_delete_confirmation' => 'Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτό το δώρο;', + 'gifts_add_gift' => 'Προσθέστε ένα δώρο', + 'gifts_link' => 'Σύνδεσμος', + 'gifts_for' => 'Για: {name}', + 'gifts_delete_cta' => 'Διαγραφή', + 'gifts_add_title' => 'Διαχείριση δώρων για :name', + 'gifts_add_gift_idea' => 'Ιδέα για δώρο', + 'gifts_add_gift_already_offered' => 'Δώρο που το δώσατε', + 'gifts_add_gift_received' => 'Δώρο που το λάβατε', + 'gifts_add_gift_title' => 'Τι είναι αυτό το δώρο;', + 'gifts_add_gift_name' => 'Όνομα δώρου', + 'gifts_add_link' => 'Σύνδεσμος στην ιστοσελίδα (προαιρετικό)', + 'gifts_add_value' => 'Τιμή (προαιρετικό)', + 'gifts_add_comment' => 'Σχόλιο (προαιρετικό)', + 'gifts_add_recipient' => 'Παραλήπτης (προαιρετικό)', + 'gifts_add_recipient_field' => 'Παραλήπτης', + 'gifts_add_photo' => 'Φωτογραφία (προαιρετικό)', + 'gifts_add_photo_title' => 'Προσθέστε μια φωτογραφία για αυτό το δώρο', + 'gifts_add_someone' => 'Αυτό το δώρο είναι ειδικά για κάποιον στην οικογένεια της επαφής {name}', + 'gifts_delete_title' => 'Διαγραφή ενός δώρου', + 'gifts_ideas' => 'Ιδέες για δώρο', + 'gifts_offered' => 'Δώρα που έχετε δώσει', + 'gifts_offered_as_an_idea' => 'Σήμανση ως μία ιδέα', + 'gifts_received' => 'Δώρα που έχετε λάβει', + 'gifts_view_comment' => 'Προβολή σχολίου', + 'gifts_mark_offered' => 'Σήμανση ως δοσμένο', + 'gifts_update_success' => 'Το δώρο έχει ενημερωθεί με επιτυχία', + 'gifts_add_date' => 'Ημερομηνία (προαιρετικό)', + + // debts + 'debt_delete_confirmation' => 'Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτό το χρέος;', + 'debt_delete_success' => 'Το χρέος έχει διαγραφεί με επιτυχία', + 'debt_add_success' => 'Το χρέος έχει προστεθεί με επιτυχία', + 'debt_title' => 'Χρέη', + 'debt_add_cta' => 'Προσθέστε χρέος', + 'debt_you_owe' => 'Χρωστάτε :amount', + 'debt_they_owe' => 'Ε επαφή :name σας χρωστάει :amount', + 'debt_add_title' => 'Διαχείριση χρέους', + 'debt_add_you_owe' => 'Χρωστάς στην επαφή :name', + 'debt_add_they_owe' => 'Η επαφή :name σου χρωστάει', + 'debt_add_amount' => 'το σύνολο του', + 'debt_add_reason' => 'για τον παρακάτω λόγο (προαιρετικό)', + 'debt_add_add_cta' => 'Προσθέστε χρέος', + 'debt_edit_update_cta' => 'Ενημέρωση χρέους', + 'debt_edit_success' => 'Το χρέος ενημερώθηκε με επιτυχία', + 'debts_blank_title' => 'Διαχείριση χρέους που χρωστάτε ή σας χρωστάει η επαφή :name', + + // tags + 'tag_edit' => 'Επεξεργασία ετικέτας', + 'tag_add' => 'Προσθήκη ετικέτας', + 'tag_add_search' => 'Προσθήκη ή αναζήτηση ετικετών', + 'tag_no_tags' => 'Δεν υπάρχουν ετικέτες ακόμη', + + // Introductions + 'introductions_sidebar_title' => 'Πώς γνωριστήκατε;', + 'introductions_blank_cta' => 'Αναφέρετε πώς γνωριστήκατε με την επαφή :name', + 'introductions_title_edit' => 'Πως γνωριστήκατε με την επαφή :name;', + 'introductions_additional_info' => 'Εξηγήστε πώς και πού γνωριστήκατε', + 'introductions_edit_met_through' => 'Σας έχει συστήσει κάποιος σε αυτό το άτομο;', + 'introductions_no_met_through' => 'Κανένας', + 'introductions_first_met_date' => 'Ημερομηνία που γνωριστήκατε', + 'introductions_no_first_met_date' => 'Δεν γνωρίζω την ημερομηνία που γνωριστήκαμε', + 'introductions_first_met_date_known' => 'Αυτή είναι η ημερομηνία που γνωριστήκαμε', + 'introductions_add_reminder' => 'Προσθέστε μια υπενθύμιση για να γιορτάσετε αυτή την γνωριμία στην επέτειο που συνέβη αυτό το γεγονός', + 'introductions_update_success' => 'Έχετε ενημερώσει με επιτυχία τις πληροφορίες σχετικά με το πώς γνωρίσατε αυτό το άτομο', + 'introductions_met_through' => 'Γνωριστήκαμε μέσω :name', + 'introductions_met_date' => 'Γνωριστήκαμε στις :date', + 'introductions_reminder_title' => 'Επέτειος της ημέρας που γνωριστήκαμε για πρώτη φορά', + + // Deceased + 'deceased_reminder_title' => 'Επέτειος του θανάτου :name', + 'deceased_mark_person_deceased' => 'Σημειώστε αυτό το άτομο ως αποβιώσαν', + 'deceased_know_date' => 'Γνωρίζω την ημερομηνία που πέθανε αυτό το άτομο', + 'deceased_add_reminder' => 'Προσθήκη υπενθύμισης για αυτή την ημερομηνία', + 'deceased_label' => 'Απεβίωσε', + 'deceased_date_label' => 'Ημερομηνία αποβίωσης', + 'deceased_label_with_date' => 'Απεβίωσε στις :date', + 'deceased_age' => 'Ηλικία κατά το θάνατο', + + // Contact information + 'contact_info_title' => 'Στοιχεία επικοινωνίας', + 'contact_info_form_content' => 'Περιεχόμενο', + 'contact_info_form_contact_type' => 'Τύπος επαφής', + 'contact_info_form_personalize' => 'Εξατομίκευση', + 'contact_info_address' => 'Ζει σε', + + // Addresses + 'contact_address_title' => 'Διευθύνσεις', + 'contact_address_form_name' => 'Ετικέτα (προαιρετικό)', + 'contact_address_form_street' => 'Οδός (προαιρετικό)', + 'contact_address_form_city' => 'Πόλη (προαιρετικό)', + 'contact_address_form_province' => 'Επαρχία (προαιρετικό)', + 'contact_address_form_postal_code' => 'Ταχυδρομικός κώδικας (προαιρετικός)', + 'contact_address_form_country' => 'Χώρα (προαιρετικό)', + 'contact_address_form_latitude' => 'Γεωγραφικό πλάτος (μόνο αριθμοί) (προαιρετικό)', + 'contact_address_form_longitude' => 'Γεωγραφικό μήκος (μόνο αριθμοί) (προαιρετικό)', + + // Pets + 'pets_kind' => 'Είδος κατοικίδιου ζώου', + 'pets_name' => 'Όνομα (προαιρετικό)', + 'pets_create_success' => 'Το κατοικίδιο ζώο προστέθηκε με επιτυχία', + 'pets_update_success' => 'Το κατοικίδιο ζώο έχει ενημερωθεί', + 'pets_delete_success' => 'Το κατοικίδιο ζώο έχει διαγραφεί', + 'pets_title' => 'Κατοικίδια', + 'pets_reptile' => 'Ερπετό', + 'pets_bird' => 'Πτηνό', + 'pets_cat' => 'Γάτα', + 'pets_dog' => 'Σκύλος', + 'pets_fish' => 'Ψάρι', + 'pets_hamster' => 'Χάμστερ', + 'pets_horse' => 'Άλογο', + 'pets_rabbit' => 'Κουνέλι', + 'pets_rat' => 'Αρουραίος', + 'pets_small_animal' => 'Μικρό ζώο', + 'pets_other' => 'Άλλο', + + // life events + 'life_event_list_tab_life_events' => 'Εκδηλώσεις ζωής', + 'life_event_list_tab_other' => 'Σημειώσεις, υπενθυμίσεις, …', + 'life_event_list_title' => 'Εκδηλώσεις ζωής', + 'life_event_blank' => 'Καταγράψτε τι συμβαίνει στη ζωή του ατόμου {name} για τη μελλοντική ανασκόπηση.', + 'life_event_list_cta' => 'Προσθήκη συμβάντος ζωής', + 'life_event_create_category' => 'Όλες οι κατηγορίες', + 'life_event_create_life_event' => 'Προσθήκη συμβάντος ζωής', + 'life_event_create_default_title' => 'Τίτλος (προαιρετικό)', + 'life_event_create_default_story' => 'Ιστορία (προαιρετικό)', + 'life_event_create_date' => 'Δεν χρειάζεται να υποδείξετε ένα μήνα ή μια ημέρα - μόνο το έτος είναι υποχρεωτικό.', + 'life_event_create_default_description' => 'Προσθέστε πληροφορίες σχετικά με αυτά που γνωρίζετε', + 'life_event_create_add_yearly_reminder' => 'Προσθήκη ετήσιας υπενθύμισης για αυτό το γεγονός', + 'life_event_create_success' => 'Το συμβάν ζωής έχει προστεθεί', + 'life_event_delete_title' => 'Διαγραφή συμβάντος ζωής', + 'life_event_delete_description' => 'Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτό το γεγονός ζωής; Η διαγραφή είναι μόνιμη.', + 'life_event_delete_success' => 'Το συμβάν ζωής έχει διαγραφεί', + 'life_event_date_it_happened' => 'Ημερομηνία που συνέβη', + 'life_event_category_work_education' => 'Εργασία και εκπαίδευση', + 'life_event_category_family_relationships' => 'Οικογένεια και σχέσεις', + 'life_event_category_home_living' => 'Οικία και διαβίωση', + 'life_event_category_health_wellness' => 'Υγεία και ευεξία', + 'life_event_category_travel_experiences' => 'Ταξίδια και εμπειρίες', + 'life_event_sentence_new_job' => 'Ξεκίνησε μια νέα εργασία', + 'life_event_sentence_retirement' => 'Συνταξιούχος', + 'life_event_sentence_new_school' => 'Ξεκίνησε σχολείο', + 'life_event_sentence_study_abroad' => 'Σπούδασε στο εξωτερικό', + 'life_event_sentence_volunteer_work' => 'Έγινε εθελοντής', + 'life_event_sentence_published_book_or_paper' => 'Δημοσίευσε μια εργασία', + 'life_event_sentence_military_service' => 'Ξεκίνησε στρατιωτική θητεία', + 'life_event_sentence_new_relationship' => 'Ξεκίνησε μια σχέση', + 'life_event_sentence_engagement' => 'Αρραβωνιάστηκε', + 'life_event_sentence_marriage' => 'Παντρεύτηκε', + 'life_event_sentence_anniversary' => 'Επέτειος', + 'life_event_sentence_expecting_a_baby' => 'Περιμένει μωρό', + 'life_event_sentence_new_child' => 'Έκανε ένα παιδί', + 'life_event_sentence_new_family_member' => 'Προστέθηκε νέο μέλος στην οικογένεια', + 'life_event_sentence_new_pet' => 'Πήρε κατοικίδιο', + 'life_event_sentence_end_of_relationship' => 'Τελείωσε μια σχέση', + 'life_event_sentence_loss_of_a_loved_one' => 'Έχασε κάποιον αγαπημένο', + 'life_event_sentence_moved' => 'Μετακόμισε', + 'life_event_sentence_bought_a_home' => 'Αγόρασε ένα σπίτι', + 'life_event_sentence_home_improvement' => 'Έκανε μια εργασία ή προσθήκη στο σπίτι', + 'life_event_sentence_holidays' => 'Πήγε διακοπές', + 'life_event_sentence_new_vehicle' => 'Πήρε ένα νέο όχημα', + 'life_event_sentence_new_roommate' => 'Απέκτησε συγκάτοικο', + 'life_event_sentence_overcame_an_illness' => 'Ξεπέρασε μια ασθένεια', + 'life_event_sentence_quit_a_habit' => 'Έκοψε μια συνήθεια', + 'life_event_sentence_new_eating_habits' => 'Ξεκίνησε νέες διατροφικές συνήθειες', + 'life_event_sentence_weight_loss' => 'Έχασε βάρος', + 'life_event_sentence_wear_glass_or_contact' => 'Ξεκίνησε να φοράει γυαλιά ή φακούς επαφής', + 'life_event_sentence_broken_bone' => 'Έσπασε κόκαλο', + 'life_event_sentence_removed_braces' => 'Έβγαλε τα σιδεράκια', + 'life_event_sentence_surgery' => 'Είχε χειρουργική επέμβαση', + 'life_event_sentence_dentist' => 'Πήγε στον οδοντίατρο', + 'life_event_sentence_new_sport' => 'Ξεκίνησε ένα άθλημα', + 'life_event_sentence_new_hobby' => 'Ξεκίνησε ένα χόμπι', + 'life_event_sentence_new_instrument' => 'Έμαθε ένα νέο όργανο', + 'life_event_sentence_new_language' => 'Έμαθε μια νέα γλώσσα', + 'life_event_sentence_tattoo_or_piercing' => 'Έκανε τατουάζ ή τρύπημα', + 'life_event_sentence_new_license' => 'Πήρε μια άδεια', + 'life_event_sentence_travel' => 'Ταξίδεψε', + 'life_event_sentence_achievement_or_award' => 'Πήρε ένα επίτευγμα ή ένα βραβείο', + 'life_event_sentence_changed_beliefs' => 'Άλλαξε πεποιθήσεις', + 'life_event_sentence_first_word' => 'Μίλησε για πρώτη φορά', + 'life_event_sentence_first_kiss' => 'Φιλήθηκε για πρώτη φορά', + + // documents + 'document_list_title' => 'Έγγραφα', + 'document_list_cta' => 'Μεταφόρτωση εγγράφου', + 'document_list_blank_desc' => 'Εδώ μπορείτε να αποθηκεύσετε έγγραφα που σχετίζονται με αυτό το άτομο.', + 'document_upload_zone_cta' => 'Μεταφορτώστε ένα αρχείο', + 'document_upload_zone_progress' => 'Μεταφόρτωση του εγγράφου…', + 'document_upload_zone_error' => 'Παρουσιάστηκε σφάλμα κατά τη μεταφόρτωση του εγγράφου. Παρακαλώ δοκιμάστε ξανά παρακάτω.', + + // Photos + 'photo_title' => 'Φωτογραφίες', + 'photo_list_title' => 'Σχετικές φωτογραφίες', + 'photo_list_cta' => 'Μεταφόρτωση φωτογραφίας', + 'photo_list_blank_desc' => 'Μπορείτε να αποθηκεύσετε εικόνες σχετικά με αυτήν την επαφή. Ανεβάστε μία τώρα!', + 'photo_upload_zone_cta' => 'Μεταφορτώστε μια φωτογραφία', + 'photo_current_profile_pic' => 'Τρέχουσα εικόνα προφίλ', + 'photo_make_profile_pic' => 'Δημιουργία εικόνας προφίλ', + 'photo_delete' => 'Διαγραφή φωτογραφίας', + 'photo_next' => 'Επόμενη φωτογραφία ❯', + 'photo_previous' => '❮ Προηγούμενη φωτογραφία', + + // Avatars + 'avatar_change_title' => 'Αλλαγή προσωπείου avatar', + 'avatar_question' => 'Ποια avatar θα θέλατε να χρησιμοποιήσετε;', + 'avatar_default_avatar' => 'Το προεπιλεγμένο avatar', + 'avatar_adorable_avatar' => 'Το αξιολάτρευτο avatar', + 'avatar_gravatar' => 'Το Gravatar που συνδέεται με τη διεύθυνση ηλεκτρονικού ταχυδρομείου αυτού του προσώπου. Gravatar είναι ένα παγκόσμιο σύστημα που επιτρέπει στους χρήστες να συσχετίσουν τις διευθύνσεις ηλεκτρονικού ταχυδρομείου με φωτογραφίες.', + 'avatar_current' => 'Διατήρηση του τρέχοντος avatar', + 'avatar_photo' => 'Από μια φωτογραφία που μεταφορτώσατε', + 'avatar_crop_new_avatar_photo' => 'Περικοπή νέας εικόνας avatar', + + // emotions + 'emotion_this_made_me_feel' => 'Αυτό σας έκανε να αισθανθείτε…', + + // logs + 'auditlogs_link' => 'Ιστορικό', + 'auditlogs_title' => 'Όλα όσα συνέβησαν στο άτομο :name', + 'auditlogs_breadcrumb' => 'Ιστορικό', + 'auditlogs_author' => 'Με :name στις :date', + + // contact field label + 'contact_field_label_home' => 'Οικία', + 'contact_field_label_work' => 'Εργασία', + 'contact_field_label_cell' => 'Κινητό', + 'contact_field_label_fax' => 'Φαξ', + 'contact_field_label_pager' => 'Βομβητής', + 'contact_field_label_main' => 'Κυρίο', + 'contact_field_label_other' => 'Άλλο', + 'contact_field_label_personal' => 'Προσωπικό', +]; diff --git a/resources/lang/el/reminder.php b/resources/lang/el/reminder.php new file mode 100644 index 0000000..2c37476 --- /dev/null +++ b/resources/lang/el/reminder.php @@ -0,0 +1,16 @@ + 'Ευχόμαστε ευτυχισμένα γενέθλια στο άτομο', + 'type_phone_call' => 'Κλήση', + 'type_lunch' => 'Γεύμα με', + 'type_hangout' => 'Βρέθηκα με', + 'type_email' => 'Email', + 'type_birthday_kid' => 'Ευχηθείτε χαρούμενα γενέθλια στο παιδί του', +]; diff --git a/resources/lang/el/settings.php b/resources/lang/el/settings.php new file mode 100644 index 0000000..0e7afcb --- /dev/null +++ b/resources/lang/el/settings.php @@ -0,0 +1,557 @@ + 'Ρυθμίσεις λογαριασμού', + 'sidebar_personalization' => 'Εξατομίκευση', + 'sidebar_settings_storage' => 'Αποθηκευτικός χώρος', + 'sidebar_settings_export' => 'Εξαγωγή δεδομένων', + 'sidebar_settings_users' => 'Χρήστες', + 'sidebar_settings_subscriptions' => 'Συνδρομή', + 'sidebar_settings_import' => 'Εισαγωγή δεδομένων', + 'sidebar_settings_tags' => 'Διαχείριση ετικετών', + 'sidebar_settings_api' => 'API', + 'sidebar_settings_dav' => 'Πόροι DAV', + 'sidebar_settings_security' => 'Ασφάλεια', + 'sidebar_settings_auditlogs' => 'Αρχείο ελέγχου', + + 'title_general' => 'Γενικές πληροφορίες', + 'title_i18n' => 'Ρυθμίσεις περιοχής', + 'title_layout' => 'Διάταξη', + + 'me_title' => 'Εγώ ως επαφή', + 'me_help' => 'Αυτή είναι η επαφή που σας εκπροσωπεί στην Monica', + 'me_select' => 'Επιλέξτε μια επαφή', + 'me_no_contact' => 'Δεν έχουν επιλεγεί επαφές.', + 'me_select_click' => 'Κάντε κλικ εδώ για να επιλέξετε μια επαφή.', + 'me_remove_contact' => 'Αφαίρεση σύνδεσης', + 'me_choose' => 'Επιλέξτε εσάς', + 'me_choose_placeholder' => 'Επιλέξτε εσάς', + + 'export_title' => 'Εξαγωγή των δεδομένων λογαριασμού σας', + 'export_be_patient' => 'Κάντε κλικ στο κουμπί για να ξεκινήσει η εξαγωγή. Ενδέχεται να χρειαστούν αρκετά λεπτά για την επεξεργασία της εξαγωγής – να είστε υπομονετικοί και μη πατάτε το κουμπί αν δεν ολοκληρωθεί η διαδικασία.', + 'export_title_sql' => 'Εξαγωγή σε SQL', + 'export_sql_explanation' => 'Η εξαγωγή των δεδομένων σε μορφή SQL σάς επιτρέπει να πάρετε τα δεδομένα και να τα εισαγάγετε σε μια δική σας εγκατάσταση του Monica. Αυτό είναι αξιοποιήσιμο μόνο εάν έχετε δικό σας διακομιστή (server).', + 'export_sql_cta' => 'Εξαγωγή σε SQL', + 'export_sql_link_instructions' => 'Σημείωση: διαβάστε τις οδηγίες για να μάθετε περισσότερα σχετικά με την εισαγωγή αυτού του αρχείου στην δική σας εγκατάσταση.', + 'export_title_json' => 'Εξαγωγή σε Json', + 'export_submitted' => 'Η εξαγωγή σας έχει υποβληθεί και θα είναι διαθέσιμη σε λίγο…', + 'export_json_explanation' => 'Εξαγωγή των δεδομένων σας σε μορφή Json για δημιουργία αντιγράφων ασφαλείας.', + 'export_json_beta' => 'Η εξαγωγή Json είναι σε λειτουργία προεπισκόπησης. Πείτε μας τι σκέφτεστε:', + 'export_json_cta' => 'Εξαγωγή σε Json', + 'export_header_type' => 'Τύπος', + 'export_header_timestamp' => 'Ημερομηνία δημιουργίας', + 'export_header_status' => 'Κατάσταση', + 'export_header_actions' => 'Ενέργειες', + 'export_last_title' => 'Τελευταίες εξαγωγές', + 'export_empty_title' => 'Δεν υπάρχουν ακόμη εξαγωγές', + 'export_type_json' => 'Εξαγωγή Json', + 'export_type_sql' => 'Εξαγωγή SQL', + 'export_status_todo' => 'Υποβλήθηκε', + 'export_status_doing' => 'Σε επεξεργασία', + 'export_status_done' => 'Ολοκληρώθηκε', + 'export_status_failed' => 'Απέτυχε', + 'export_not_done' => 'Η λήψη δεν είναι δυνατή, αυτή η εξαγωγή δεν έχει ολοκληρωθεί ακόμα.', + + 'firstname' => 'Όνομα', + 'lastname' => 'Επώνυμο', + 'name_order' => 'Εμφάνιση ονόματος', + 'name_order_firstname_lastname' => ' – Τάδε Δείνα', + 'name_order_lastname_firstname' => ' – Δείνα Τάδε', + 'name_order_firstname_lastname_nickname' => '<Όνομα> <Επώνυμο> (<Ψευδώνυμο>) – Γιάννης Τάδε (Ράμπο)', + 'name_order_firstname_nickname_lastname' => '<Όνομα> (<Ψευδώνυμο>) <Επώνυμο> – Γιάννης (Ράμπο) Τάδε', + 'name_order_lastname_firstname_nickname' => '<Επώνυμο> <Όνομα> (<Ψευδώνυμο>) – Τάδε Γιάννης (Ράμπο)', + 'name_order_lastname_nickname_firstname' => '<Επώνυμο> (<Ψευδώνυμο>) <Όνομα> – Τάδε (Ράμπο) Γιάννης', + 'name_order_nickname_firstname_lastname' => '<Ψευδώνυμο> (<Όνομα> <Επώνυμο>) – Ράμπο (Γιάννης Τάδε)', + 'name_order_nickname_lastname_firstname' => '<Ψευδώνυμο> (<Επώνυμο> <Όνομα>) – Ράμπο (Τάδε Γιάννης)', + 'name_order_nickname' => '<Ψευδώνυμο> – Ράμπο', + 'currency' => 'Nόμισμα', + 'name' => 'Το όνομά σας: :name', + 'email' => 'Διεύθυνση email', + 'email_placeholder' => 'Εισάγετε email', + 'email_help' => 'Αυτό είναι το email που χρησιμοποιείται για την σύνδεση, και στο οποίο θα λαμβάνετε τις ειδοποιήσεις σας.', + 'timezone' => 'Ζώνη ώρας', + 'temperature_scale' => 'Κλίμακα θερμοκρασίας', + 'temperature_scale_fahrenheit' => 'Φαρενάιτ (°F)', + 'temperature_scale_celsius' => 'Κελσίου (°C)', + 'layout' => 'Διάταξη', + 'layout_small' => 'Μέγιστο πλάτος 1200 εικονοστοιχείων', + 'layout_big' => 'Πλήρες πλάτος του προγράμματος περιήγησης', + 'save' => 'Ενημέρωση προτιμήσεων', + 'delete_title' => 'Διαγραφή του λογαριασμού σας', + 'delete_desc' => 'Θέλετε να διαγράψετε τον λογαριασμό σας; Η διαγραφή είναι μόνιμη και όλα τα δεδομένα σας θα διαγραφούν οριστικά. Εάν έχετε συνδρομή, θα ακυρωθεί αμέσως.', + 'delete_other_desc' => 'Τα δεδομένα σας στην κύρια βάση δεδομένων θα διαγραφούν αμέσως. Όπως περιγράφεται στην πολιτική απορρήτου μας, πραγματοποιούμε κρυπτογραφημένα αντίγραφα ασφαλείας της βάσης δεδομένων καθημερινά. Αυτά τα αντίγραφα ασφαλείας διατηρούνται για 30 ημέρες μετά τις οποίες διαγράφονται πλήρως. Δεν μπορούμε να διαγράψουμε συγκεκριμένα δεδομένα από τα αντίγραφα ασφαλείας που διατηρούμε πριν από αυτό. Όλα τα δεδομένα σας θα διαγραφούν πλήρως το αργότερο 31 ημέρες μετά τη διαγραφή του λογαριασμού σας.', + 'reset_desc' => 'Θέλετε να κάνετε επαναφορά του λογαριασμού σας; Αυτό θα καταργήσει όλες τις επαφές σας και όλα τα δεδομένα που σχετίζονται με αυτές. Ο λογαριασμός σας δεν θα διαγραφεί.', + 'reset_title' => 'Επαναφορά του λογαριασμού σας', + 'reset_cta' => 'Επαναφορά λογαριασμού', + 'reset_notice' => 'Είστε βέβαιοι ότι θέλετε να κάνετε επαναφορά στο λογαριασμό σας; Αυτό είναι μια μόνιμη κατάσταση και δεν μπορεί να αναιρεθεί.', + 'reset_success' => 'Η επαναφορά του λογαριασμού σας έγινε με επιτυχία.', + 'delete_notice' => 'Είστε βέβαιοι ότι θέλετε να διαγράψετε τον λογαριασμό σας; Αυτό είναι μόνιμο και δεν μπορεί να αναιρεθεί. Όλα τα δεδομένα σας θα διαγραφούν και δεν θα ανακτηθούν.', + 'delete_cta' => 'Διαγραφή λογαριασμού', + 'settings_success' => 'Οι προτιμήσεις ενημερώθηκαν!', + 'locale' => 'Γλώσσα που χρησιμοποιείται στην εφαρμογή', + 'locale_help' => 'Θέλετε να βοηθήσετε στη μετάφραση της Μόνικα ή να προσθέσετε μια νέα γλώσσα; Παρακαλώ ακολουθήστε αυτόν τον σύνδεσμο για περισσότερες πληροφορίες.', + 'locale_ar' => 'Αραβικά', + 'locale_cs' => 'Τσέχικα', + 'locale_de' => 'Γερμανικά', + 'locale_el' => 'Ελληνικά', + 'locale_en' => 'Αγγλικά', + 'locale_en-GB' => 'Αγγλικά (Ηνωμένου Βασιλείου)', + 'locale_es' => 'Ισπανικά', + 'locale_fr' => 'Γαλλικά', + 'locale_he' => 'Εβραϊκά', + 'locale_hr' => 'Κροάτια', + 'locale_id' => 'Ινδονησιακά', + 'locale_it' => 'Ιταλικά', + 'locale_ja' => 'Ιαπωνικά', + 'locale_nl' => 'Ολλανδικά', + 'locale_pt' => 'Πορτογαλικά', + 'locale_pt-BR' => 'Βραζιλιάνικα', + 'locale_ru' => 'Ρωσικά', + 'locale_sv' => 'Σουηδικά', + 'locale_vi' => 'Βιετναμέζικα', + 'locale_zh' => 'Κινεζικά (απλοποιημένα)', + 'locale_zh-TW' => 'Κινεζικά (παραδοσιακά)', + 'locale_tr' => 'Τουρκικά', + + 'security_title' => 'Ασφάλεια', + 'security_help' => 'Αλλάξτε θέματα ασφαλείας για το λογαριασμό σας.', + 'password_change' => 'Αλλάξτε τον κωδικό πρόσβασής σας', + 'password_current' => 'Τρέχον κωδικός πρόσβασης', + 'password_current_placeholder' => 'Εισαγάγετε τον τρέχοντα κωδικό πρόσβασης', + 'password_new1' => 'Νέος κωδικός πρόσβασης', + 'password_new1_placeholder' => 'Εισάγετε το νέο σας κωδικό', + 'password_new2' => 'Επιβεβαιώστε τον νέο κωδικό σας', + 'password_new2_placeholder' => 'Πληκτρολογήστε ξανά τον νέο σας κωδικό', + 'password_btn' => 'Αλλαγή κωδικού πρόσβασης', + '2fa_title' => 'Έλεγχος ταυτότητας δυο παραγόντων', + '2fa_otp_title' => 'Two Factor Authentication mobile application', + '2fa_enable_title' => 'Enable Two Factor Authentication', + '2fa_enable_description' => 'Enable Two Factor Authentication to increase the security of your account.', + '2fa_enable_otp' => 'Open up your Two Factor Authentication mobile app and scan the following QR barcode:', + '2fa_enable_otp_help' => 'If your Two Factor Authentication mobile app does not support QR barcodes, enter in the following code:', + '2fa_enable_otp_validate' => 'Please validate the new device you’ve just set up:', + '2fa_enable_success' => 'Two Factor Authentication activated', + '2fa_enable_error' => 'Error when trying to activate Two Factor Authentication', + '2fa_enable_error_already_set' => 'Two Factor Authentication is already activated', + '2fa_disable_title' => 'Disable Two Factor Authentication', + '2fa_disable_description' => 'Disable Two Factor Authentication for your account. Be careful, your account will be much less secure!', + '2fa_disable_success' => 'Two Factor Authentication disabled', + '2fa_disable_error' => 'Error when trying to disable Two Factor Authentication', + + 'webauthn_title' => 'Security key — WebAuthn protocol', + 'webauthn_enable_description' => 'Add a new security key', + 'webauthn_key_name_help' => 'Give your key a name.', + 'webauthn_key_name' => 'Key name:', + 'webauthn_success' => 'Your key is detected and validated.', + 'webauthn_last_use' => 'Last use: {timestamp}', + 'webauthn_delete_confirmation' => 'Are you sure you want to delete this key?', + 'webauthn_delete_success' => 'Key deleted', + 'webauthn_insertKey' => 'Insert your security key.', + 'webauthn_buttonAdvise' => 'If your security key has a button, press it.', + 'webauthn_noButtonAdvise' => 'If it does not, remove it and insert it again.', + 'webauthn_not_supported' => 'Your browser doesn’t currently support WebAuthn.', + 'webauthn_not_secured' => 'WebAuthn only supports secure connections. Please load this page with https scheme.', + 'webauthn_error_already_used' => 'This key is already registered. It’s not necessary to register it again.', + 'webauthn_error_not_allowed' => 'The operation either timed out or was not allowed.', + + 'recovery_title' => 'Κωδικοί ανάκτησης', + 'recovery_show' => 'Λήψη κωδικών ανάκτησης', + 'recovery_copy_help' => 'Αντιγράψτε τους κωδικούς στο πρόχειρο σας', + 'recovery_help_intro' => 'Αυτοί είναι οι κωδικοί ανάκτησης σας:', + 'recovery_help_information' => 'Μπορείτε να χρησιμοποιήσετε κάθε κωδικό ανάκτησης μόνο μία φορά.', + 'recovery_clipboard' => 'Οι κωδικοί αντιγράφηκαν στο πρόχειρο.', + 'recovery_generate' => 'Δημιουργία νέων κωδικών…', + 'recovery_generate_help' => 'Η δημιουργία νέων κωδικών θα ακυρώσει τους προηγούμενους κωδικούς.', + 'recovery_already_used_help' => 'Αυτός ο κωδικός έχει ήδη χρησιμοποιηθεί.', + + 'users_list_title' => 'Users with access to your account', + 'users_list_add_user' => 'Invite a new user', + 'users_list_you' => 'That’s you', + 'users_list_invitations_title' => 'Pending invitations', + 'users_list_invitations_explanation' => 'Below are the people you’ve invited to join Monica as a collaborator.', + 'users_list_invitations_invited_by' => 'invited by :name', + 'users_list_invitations_sent_date' => 'sent on :date', + 'users_blank_title' => 'You are the only one who has access to this account.', + 'users_blank_add_title' => 'Would you like to invite someone else?', + 'users_blank_description' => 'This person will have the same access that you have, and will be able to add, edit or delete contact information.', + 'users_blank_cta' => 'Invite someone', + 'users_add_title' => 'Invite a new user to your account by email', + 'users_add_description' => 'This person will have the same access as you do, including inviting or deleting other users, including you. Make sure you trust this person before giving them access.', + 'users_add_email_field' => 'Enter the email of the person you want to invite', + 'users_add_confirmation' => 'I confirm that I want to invite this user to my account. I understand that this person will have access to ALL of my data and see exactly what I see.', + 'users_add_cta' => 'Invite user by email', + 'users_accept_title' => 'Accept invitation and create a new account', + 'users_error_please_confirm' => 'Please confirm that you want to invite this user before proceeding with the invitation', + 'users_error_email_already_taken' => 'This email is already taken. Please choose another one', + 'users_error_already_invited' => 'You already have invited this user. Please choose another email address.', + 'users_error_email_not_similar' => 'This is not the email of the person who’ve invited you.', + 'users_invitation_deleted_confirmation_message' => 'The invitation has been successfully deleted', + 'users_invitations_delete_confirmation' => 'Are you sure you want to delete this invitation?', + 'users_list_delete_confirmation' => 'Are you sure to delete this user from your account?', + 'users_invitation_need_subscription' => 'Adding more users requires a subscription.', + + 'subscriptions_account_current_plan' => 'Your current plan', + 'subscriptions_account_current_legacy' => 'Current plan, not selectable anymore:', + 'subscriptions_account_current_paid_plan' => 'You are on the :name plan. Thanks so much for being a subscriber.', + + 'subscriptions_account_next_billing_title' => 'Next bill', + 'subscriptions_account_next_billing' => 'Your subscription will auto-renew on :date.', + 'subscriptions_account_bill_monthly' => 'We’ll bill you :price for another month.', + 'subscriptions_account_bill_annual' => 'We’ll bill you :price for another year.', + 'subscriptions_account_change' => 'Change plan', + + 'subscriptions_account_cancel_title' => 'Cancel subscription', + 'subscriptions_account_cancel_action' => 'Cancel subscription', + 'subscriptions_account_cancel' => 'You can cancel your subscription at any time.', + 'subscriptions_account_free_plan' => 'You are on the free plan.', + 'subscriptions_account_free_plan_upgrade' => 'You can upgrade your account to the :name plan, which costs $:price per month. Here are the advantages:', + 'subscriptions_account_free_plan_benefits_users' => 'Unlimited number of users', + 'subscriptions_account_free_plan_benefits_reminders' => 'Reminders by email', + 'subscriptions_account_free_plan_benefits_import_data_vcard' => 'Import your contacts with vCard', + 'subscriptions_account_free_plan_benefits_support' => 'Support the project in the long run, so we can introduce more great features.', + 'subscriptions_account_upgrade' => 'Upgrade your account', + 'subscriptions_account_upgrade_title' => 'Upgrade Monica today and have more meaningful relationships.', + 'subscriptions_account_upgrade_choice' => 'Pick a plan below and join over :customers persons who upgraded their Monica.', + 'subscriptions_account_update_title' => 'Update Monica subscription', + 'subscriptions_account_update_description' => 'You can change your subscription’s frequency here.', + 'subscriptions_account_update_information' => 'You will be billed immediately for the new amount. Your subscription will extend to the new period, depending on your choice.', + 'subscriptions_account_invoices' => 'Invoices', + 'subscriptions_account_invoices_download' => 'Download', + 'subscriptions_account_invoices_subscription' => 'Subscription from :startDate to :endDate', + 'subscriptions_account_payment' => 'Which payment option fits you best?', + 'subscriptions_account_confirm_payment' => 'Your payment is currently incomplete, please confirm your payment.', + 'subscriptions_downgrade_title' => 'Downgrade your account to the free plan', + 'subscriptions_downgrade_limitations' => 'The free plan has limitations. In order to be able to downgrade, you need to pass the checklist below:', + 'subscriptions_downgrade_rule_users' => 'You must have only 1 user in your account', + 'subscriptions_downgrade_rule_users_constraint' => 'You currently have 1 user in your account.|You currently have :count users in your account.', + 'subscriptions_downgrade_rule_invitations' => 'You must not have any pending invitations', + 'subscriptions_downgrade_rule_invitations_constraint' => 'You currently have 1 pending invitation.|You currently have :count pending invitations.', + 'subscriptions_downgrade_rule_contacts' => 'You must not have more than :number active contacts', + 'subscriptions_downgrade_rule_contacts_constraint' => 'You currently have 1 contact.|You currently have :count contacts.', + 'subscriptions_downgrade_rule_contacts_archive' => 'We can also archive all your contacts for you – that would clear this rule and let you proceed with your account’s downgrade process.', + 'subscriptions_downgrade_cta' => 'Downgrade', + 'subscriptions_downgrade_success' => 'You are back to the Free plan!', + 'subscriptions_downgrade_thanks' => 'Thanks so much for trying the paid plan. We keep adding new features on Monica all the time – so you might want to come back in the future to see if you might be interested in taking a subscription again.', + 'subscriptions_back' => 'Back to settings', + 'subscriptions_upgrade_title' => 'Upgrade your account', + 'subscriptions_upgrade_choose' => 'You picked the :plan plan.', + 'subscriptions_upgrade_infos' => 'We couldn’t be happier. Enter your payment info below.', + 'subscriptions_upgrade_name' => 'Name on card', + 'subscriptions_upgrade_zip' => 'ZIP or postal code', + 'subscriptions_upgrade_credit' => 'Credit or debit card', + 'subscriptions_upgrade_submit' => 'Pay {amount}', + 'subscriptions_upgrade_charge' => 'We’ll charge your card :price now. The next charge will be on :date. If you ever change your mind, you can cancel at any time, no questions asked.', + 'subscriptions_upgrade_charge_handled' => 'The payment is handled by Stripe. No card information touches our server.', + 'subscriptions_upgrade_success' => 'Thank you! You are now subscribed.', + 'subscriptions_upgrade_thanks' => 'Welcome to the community of people who try to make the world a better place.', + + 'subscriptions_payment_confirm_title' => 'Confirm your :amount payment', + 'subscriptions_payment_confirm_information' => 'Extra confirmation is needed to process your payment. Please confirm your payment by filling out your payment details below.', + 'subscriptions_payment_succeeded_title' => 'Payment Successful', + 'subscriptions_payment_succeeded' => 'This payment was already successfully confirmed.', + 'subscriptions_payment_cancelled_title' => 'Payment Cancelled', + 'subscriptions_payment_cancelled' => 'This payment was cancelled.', + 'subscriptions_payment_error_name' => 'Please provide your name.', + 'subscriptions_payment_success' => 'The payment was successful.', + + 'subscriptions_pdf_title' => 'Your :name monthly subscription', + 'subscriptions_plan_frequency_year' => ':amount / year', + 'subscriptions_plan_frequency_month' => ':amount / month', + 'subscriptions_plan_choose' => 'Choose this plan', + 'subscriptions_plan_year_title' => 'Pay annually', + 'subscriptions_plan_year_bonus' => 'Peace of mind for a whole year', + 'subscriptions_plan_month_title' => 'Pay monthly', + 'subscriptions_plan_month_bonus' => 'Cancel any time', + 'subscriptions_plan_include1' => 'Included with your upgrade:', + 'subscriptions_plan_include2' => 'Unlimited number of contacts • Unlimited number of users • Reminders by email • Import with vCard • Personalization of the contact sheet', + 'subscriptions_plan_include3' => '100% of the profits go the development of this great open source project.', + 'subscriptions_help_title' => 'Additional details you may be curious about', + 'subscriptions_help_opensource_title' => 'What is an open source project?', + 'subscriptions_help_opensource_desc' => 'Monica is an open source project. This means it is built by a community who wants to build a great tool for the greater good. Being open source means the code is publicly available on GitHub, and everyone can inspect it, modify it or enhance it. All the money we raise is dedicated to building better features, paying for more powerful servers, and paying other costs. Thanks for your help. We couldn’t do it without you.', + 'subscriptions_help_limits_title' => 'Is there a limit to the number of contacts we can have on the free plan?', + 'subscriptions_help_limits_plan' => 'Yes. Free plans let you manage :number contacts.', + 'subscriptions_help_discounts_title' => 'Do you have discounts for non-profits and education?', + 'subscriptions_help_discounts_desc' => 'We do! Monica is free for students, and free for non-profits and charities. Just contact the support with a proof of your status and we’ll apply this special status in your account.', + 'subscriptions_help_change_title' => 'What if I change my mind?', + 'subscriptions_help_change_desc' => 'You can cancel anytime, no questions asked, and all by yourself – no need to contact support. However, you will not be refunded for the current period.', + + 'stripe_error_card' => 'Your card was declined. Decline message is: :message', + 'stripe_error_api_connection' => 'Network communication with Stripe failed. Try again later.', + 'stripe_error_rate_limit' => 'Too many requests with Stripe right now. Try again later.', + 'stripe_error_invalid_request' => 'Invalid parameters. Try again later.', + 'stripe_error_authentication' => 'Wrong authentication with Stripe', + + 'import_title' => 'Εισαγωγή επαφών στο λογαριασμό σας', + 'import_cta' => 'Μεταφόρτωση επαφών', + 'import_stat' => 'Έχετε εισάγει :number αρχεία μέχρι στιγμής.', + 'import_result_stat' => 'Μεταφορτώθηκε vCard με 1 επαφή (:total_imported εισήχθηκε, :total_skipped παραλείφθηκε)|Μεταφορτώθηκε vCard με :total_contacts επαφές (:total_imported εισήχθησαν, :total_skipped παραλείφθηκαν)', + 'import_view_report' => 'Προβολή αναφοράς', + 'import_in_progress' => 'Η εισαγωγή βρίσκεται σε εξέλιξη. Κάνετε μια ανανέωση της σελίδας σε ένα λεπτό.', + 'import_upload_title' => 'Εισαγωγή των επαφών σας από ένα αρχείο vCard', + 'import_upload_rules_desc' => 'Ωστόσο, έχουμε ορισμένους κανόνες:', + 'import_upload_rule_format' => 'Υποστηρίζουμε αρχεία .vcard και .vcf.', + 'import_upload_rule_vcard' => 'Υποστηρίζουμε τη μορφή vCard 3.0, η οποία είναι η προεπιλεγμένη μορφή για το Contacts.app του macOS και τις Επαφές Google.', + 'import_upload_rule_instructions' => 'Οδηγίες εξαγωγής για macOS Contacts.app και Google Contacts.', + 'import_upload_rule_multiple' => 'Εάν οι επαφές σας έχουν πολλές διευθύνσεις email ή αριθμούς τηλεφώνου, μόνο η πρώτη καταχώριση θα αποθηκευτεί.', + 'import_upload_rule_limit' => 'Τα αρχεία έχουν περιορισμό στα 10 MB.', + 'import_upload_rule_time' => 'Μπορεί να χρειαστεί έως και ένα λεπτό για τη μεταφόρτωση των επαφών και την επεξεργασία τους. Παρακαλώ να είστε υπομονετικοί.', + 'import_upload_rule_cant_revert' => 'Βεβαιωθείτε ότι τα δεδομένα είναι ακριβή πριν από τη μεταφόρτωση, καθώς δεν μπορείτε να αναιρέσετε τη μεταφόρτωση.', + 'import_upload_form_file' => 'Το αρχείο σας .vcf ή .vCard:', + 'import_upload_behaviour' => 'Συμπεριφορά κατά την εισαγωγή:', + 'import_upload_behaviour_add' => 'Προσθήκη νέων επαφών και παράλειψη για τις υπάρχουσες', + 'import_upload_behaviour_replace' => 'Αντικατάσταση για τις υπάρχουσες επαφές', + 'import_upload_behaviour_help' => 'Η αντικατάσταση θα αντικαταστήσει όλα τα δεδομένα που βρίσκονται στην vCard, αλλά θα διατηρήσει τα υπάρχοντα πεδία επαφών.', + 'import_report_title' => 'Αναφορά εισαγωγής', + 'import_report_date' => 'Ημερομηνία εισαγωγής', + 'import_report_type' => 'Είδος εισαγωγής', + 'import_report_number_contacts' => 'Αριθμός επαφών στο αρχείο', + 'import_report_number_contacts_imported' => 'Αριθμός επαφών που έχουν εισαχθεί', + 'import_report_number_contacts_skipped' => 'Αριθμός επαφών που παραλείφθηκαν', + 'import_report_status_imported' => 'Εισαγωγή', + 'import_report_status_skipped' => 'Παράλειψη', + 'import_vcard_parse_error' => 'Σφάλμα κατά την ανάλυση της καταχώρισης vCard', + 'import_vcard_contact_exist' => 'Η επαφή υπάρχει ήδη', + 'import_vcard_contact_no_firstname' => 'Χωρίς Όνομα (υποχρεωτικό πεδίο)', + 'import_vcard_file_not_found' => 'Το αρχείο δεν βρέθηκε', + 'import_vcard_unknown_entry' => 'Άγνωστο όνομα επαφής', + 'import_vcard_file_no_entries' => 'Το αρχείο δεν περιέχει καταχωρήσεις', + 'import_blank_title' => 'Δεν έχετε εισαγάγει ακόμη καμία επαφή.', + 'import_blank_question' => 'Θέλετε να εισάγετε επαφές τώρα;', + 'import_blank_description' => 'Μπορούμε να εισαγάγουμε αρχεία vCard που μπορείτε να λάβετε από το Google Contacts ή τον διαχειριστή επαφών σας.', + 'import_blank_cta' => 'Εισαγωγή vCard', + 'import_need_subscription' => 'Η εισαγωγή δεδομένων απαιτεί συνδρομή.', + + 'tags_list_title' => 'Ετικέτες', + 'tags_list_description' => 'Μπορείτε να οργανώσετε τις επαφές σας χρησιμοποιώντας ετικέτες. Οι ετικέτες λειτουργούν σαν φάκελοι οργάνωσης, αλλά μπορείτε να προσθέσετε περισσότερες από μία ετικέτες σε κάθε επαφή. Για να δημιουργήσετε μια νέα ετικέτα, απλά προσθέστε την στην ίδια την επαφή.', + 'tags_list_contact_number' => '1 επαφή|:count επαφές', + 'tags_list_delete_success' => 'Η ετικέτα έχει διαγραφεί με επιτυχία', + 'tags_list_edit_success' => 'The tag has been successfully updated', + 'tags_list_delete_confirmation' => 'Είστε βέβαιοι ότι θέλετε να διαγράψετε την ετικέτα? Καμία επαφή δεν θα διαγραφεί, μόνο αυτή η ετικέτα από τις επαφές που είχε χρησιμοποιηθεί.', + 'tags_blank_title' => 'Οι ετικέτες είναι ένας πολύ καλός τρόπος για να κατηγοριοποιήσετε τις επαφές σας.', + 'tags_blank_description' => 'Οι ετικέτες λειτουργούν σαν φάκελοι οργάνωσης, ενώ μπορείτε να προσθέσετε περισσότερες από μία ετικέτες σε μια επαφή. Μεταβείτε σε μια επαφή και προσθέστε μια ετικέτα, ακριβώς κάτω από το όνομα. Μόλις προστεθεί η ετικέτα, μπορείτε να επιστρέψτε εδώ για να διαχειριστείτε όλες τις ετικέτες στον λογαριασμό σας.', + + 'api_title' => 'API access', + 'api_description' => 'The API can be used to manipulate Monica’s data from an external application, like a mobile application for instance.', + 'api_help' => 'To use the API, a token is mandatory. You can either create a personal access token (Bearer authentication), or authorize an OAuth client to create it for you. See API documentation.', + 'api_endpoint' => 'The API endpoint for this Monica instance is:', + + 'api_personal_access_tokens' => 'Personal access tokens', + 'api_pao_description' => 'Make sure you give this token to a source you trust – as they allow you to access all your data.', + 'api_token_title' => 'Personal Access Tokens', + 'api_token_create_new' => 'Create New Token', + 'api_token_not_created' => 'You have not created any personal access tokens.', + 'api_token_name' => 'Token name', + 'api_token_expire' => 'Expires at {date}', + 'api_token_delete' => 'Delete', + 'api_token_create' => 'Create Token', + 'api_token_scopes' => 'Scopes', + 'api_token_help' => 'Here is your new personal access token. This is the only time it will be shown so don’t lose it! You may now use this token to make API requests.', + + 'api_oauth_clients' => 'Your OAuth clients', + 'api_oauth_clients_desc' => 'This section lets you register your own OAuth clients.', + 'api_oauth_clients_desc2' => 'Use this client id to request a new token, and convert authorization codes to access tokens. See Laravel Passport documentation for more information.', + 'api_oauth_title' => 'OAuth Clients', + 'api_oauth_create_new' => 'Create New Client', + 'api_oauth_edit' => 'Edit Client', + 'api_oauth_not_created' => 'You have not created any OAuth clients.', + 'api_oauth_clientid' => 'Client ID', + 'api_oauth_name' => 'Name', + 'api_oauth_name_help' => 'Something your users will recognize and trust.', + 'api_oauth_secret' => 'Secret', + 'api_oauth_create' => 'Create Client', + 'api_oauth_redirecturl' => 'Redirect URL', + 'api_oauth_redirecturl_help' => 'Your application’s authorization callback URL.', + + 'api_authorized_clients' => 'List of authorized clients', + 'api_authorized_clients_desc' => 'This section lists all the clients you’ve authorized to access your application data. You can revoke this authorization at anytime.', + 'api_authorized_clients_title' => 'Authorized Applications', + 'api_authorized_clients_none' => 'There are no authorized clients yet.', + 'api_authorized_clients_name' => 'Name', + 'api_authorized_clients_scopes' => 'Scopes', + + 'personalization_tab_title' => 'Προσαρμόστε τον λογαριασμό σας', + + 'personalization_title' => 'Εδώ θα βρείτε διάφορες ρυθμίσεις για τη διαμόρφωση του λογαριασμού σας. Αυτές οι δυνατότητες προορίζονται για «προχωρημένους» χρήστες που θέλουν μέγιστο έλεγχο της Monica.', + 'personalization_contact_field_type_title' => 'Τύποι πεδίων επαφών', + 'personalization_contact_field_type_add' => 'Προσθήκη νέου τύπου πεδίων', + 'personalization_contact_field_type_description' => 'Μπορείτε να διαμορφώσετε όλους τους διαφορετικούς τύπους πεδίων επαφών που μπορείτε να συσχετίσετε με όλες τις επαφές σας. Για παράδειγμα, εάν εμφανιστεί ένα νέο κοινωνικό δίκτυο στο μέλλον, θα μπορείτε να προσθέσετε αυτόν τον νέο τρόπο επικοινωνίας με τις επαφές σας εδώ.', + 'personalization_contact_field_type_table_name' => 'Όνομα', + 'personalization_contact_field_type_table_protocol' => 'Πρωτόκολλο', + 'personalization_contact_field_type_table_actions' => 'Ενέργειες', + 'personalization_contact_field_type_modal_title' => 'Προσθήκη νέου τύπου πεδίου επαφής', + 'personalization_contact_field_type_modal_edit_title' => 'Επεξεργασία ενός υπάρχοντος τύπου πεδίου επαφής', + 'personalization_contact_field_type_modal_delete_title' => 'Διαγραφή ενός υπάρχοντος τύπου πεδίου επαφής', + 'personalization_contact_field_type_modal_delete_description' => 'Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτόν τον τύπο πεδίου επαφών; Η διαγραφή αυτού του τύπου πεδίου επαφών θα διαγράψει ΟΛΑ τα δεδομένα με αυτόν τον τύπο για όλες τις επαφές σας.', + 'personalization_contact_field_type_modal_name' => 'Όνομα', + 'personalization_contact_field_type_modal_protocol' => 'Πρωτόκολλο (προαιρετικό)', + 'personalization_contact_field_type_modal_protocol_help' => 'Μπορείτε να κάνετε κλικ σε κάθε νέο τύπο πεδίου επαφών. Εάν έχει οριστεί ένα πρωτόκολλο, θα γίνεται χρήση με την ενέργεια που έχει οριστεί.', + 'personalization_contact_field_type_modal_icon' => 'Εικονίδιο (προαιρετικό)', + 'personalization_contact_field_type_modal_icon_help' => 'Μπορείτε να συσχετίσετε ένα εικονίδιο με αυτόν τον τύπο πεδίου επαφών. Πρέπει να κάνετε χρήση αναφοράς σε ένα εικονίδιο Font Awesome.', + 'personalization_contact_field_type_delete_success' => 'Ο τύπος πεδίου επαφών έχει διαγραφεί επιτυχώς.', + 'personalization_contact_field_type_add_success' => 'Ο τύπος πεδίου επαφών έχει προστεθεί με επιτυχία.', + 'personalization_contact_field_type_edit_success' => 'Ο τύπος πεδίου επαφών έχει ενημερωθεί με επιτυχία.', + + 'personalization_genders_title' => 'Gender types', + 'personalization_genders_add' => 'Add new gender type', + 'personalization_genders_desc' => 'You can define as many genders as you need to. You need at least one gender type in your account.', + 'personalization_genders_modal_add' => 'Add gender type', + 'personalization_genders_modal_edit' => 'Update gender type', + 'personalization_genders_modal_name' => 'Name', + 'personalization_genders_modal_name_help' => 'The name used to display the gender on a contact page.', + 'personalization_genders_modal_sex' => 'Sex', + 'personalization_genders_modal_sex_help' => 'Χρησιμοποιείται για τον καθορισμό των σχέσεων και κατά τη διαδικασία εισαγωγής/εξαγωγής των VCard.', + 'personalization_genders_modal_default' => 'Select the default gender for a new contact', + 'personalization_genders_modal_delete' => 'Delete gender type', + 'personalization_genders_modal_delete_desc' => 'Are you sure you want to delete the gender “{name}”?', + 'personalization_genders_modal_delete_question' => 'You currently have {count} contact with this gender. If you delete this gender, what gender should this contact have?|You currently have {count} contacts with this gender. If you delete this gender, what gender should these contacts have?', + 'personalization_genders_modal_delete_question_default' => 'This gender is the default one. If you delete this gender, which one will be the new default?', + 'personalization_genders_modal_error' => 'Please choose a gender from the list.', + 'personalization_genders_list_contact_number' => '{count} contact|{count} contacts', + 'personalization_genders_table_name' => 'Name', + 'personalization_genders_table_sex' => 'Sex', + 'personalization_genders_table_default' => 'Default', + 'personalization_genders_default' => 'Default gender', + 'personalization_genders_make_default' => 'Change default gender', + 'personalization_genders_select_default' => 'Select default gender', + 'personalization_genders_m' => 'Male', + 'personalization_genders_f' => 'Female', + 'personalization_genders_o' => 'Other', + 'personalization_genders_u' => 'Unknown', + 'personalization_genders_n' => 'None or not applicable', + + 'personalization_reminder_rule_save' => 'The change has been saved', + 'personalization_reminder_rule_title' => 'Κανόνες υπενθύμισης', + 'personalization_reminder_rule_line' => '{count} ημέρα νωρίτερα|{count} ημέρες νωρίτερα', + 'personalization_reminder_rule_desc' => 'Για κάθε υπενθύμιση που ορίζετε, η Monica μπορεί να σας στείλει ένα email αρκετές ημέρες πριν από το γεγονός. Εδώ μπορείτε να προσαρμόσετε αυτές τις ρυθμίσεις ειδοποιήσεων. Αυτές οι ειδοποιήσεις ισχύουν μόνο για μηνιαίες και ετήσιες υπενθυμίσεις.', + + 'personalization_module_save' => 'Η αλλαγή έχει αποθηκευτεί', + 'personalization_module_title' => 'Δυνατότητες', + 'personalization_module_desc' => 'Μπορεί να μην χρειάζεστε όλες τις δυνατότητες της Monica. Παρακάτω μπορείτε να αλλάξετε συγκεκριμένες λειτουργίες που δε χρειάζεστε στις καρτέλες επαφών. Αυτή η αλλαγή θα επηρεάσει την εμφάνιση σε ΟΛΕΣ τις επαφές σας. Η απενεργοποίηση μιας λειτουργίας δεν διαγράφει δεδομένα, απλώς αποκρύπτει τη δυνατότητα από τις καρτέλες επαφών.', + + 'personalisation_paid_upgrade' => 'This is a premium feature that requires a Paid subscription to be active. Upgrade your account by visiting Settings > Subscription.', + 'personalisation_paid_upgrade_vue' => 'This is a premium feature that requires a Paid subscription to be active. Upgrade your account by visiting Settings > Subscription.', + + 'reminder_time_to_send' => 'Time of the day reminders will be sent', + 'reminder_time_to_send_help' => 'Your next reminder is scheduled to be sent on {dateTime}.', + + 'personalization_activity_type_category_title' => 'Κατηγορίες τύπων δραστηριότητας', + 'personalization_activity_type_category_add' => 'Προσθήκη νέας κατηγορίας δραστηριότητας', + 'personalization_activity_type_category_table_name' => 'Όνομα', + 'personalization_activity_type_category_description' => 'Μια δραστηριότητα με μια από τις επαφές σας μπορεί να έχει έναν τύπο και έναν τύπο κατηγορίας. Ο λογαριασμός σας διαθέτει ένα σύνολο προκαθορισμένων τύπων κατηγοριών από προεπιλογή, αλλά εδώ μπορείτε να τους προσαρμόσετε όπως επιθυμείτε.', + 'personalization_activity_type_category_table_actions' => 'Ενέργειες', + 'personalization_activity_type_category_modal_add' => 'Προσθήκη νέας κατηγορίας δραστηριότητας', + 'personalization_activity_type_category_modal_edit' => 'Επεξεργασία κατηγορίας τύπου δραστηριότητας', + 'personalization_activity_type_category_modal_question' => 'Πώς να ονομάσουμε αυτή τη νέα κατηγορία;', + 'personalization_activity_type_add_button' => 'Προσθήκη νέου τύπου δραστηριότητας', + 'personalization_activity_type_modal_add' => 'Προσθήκη νέου τύπου δραστηριότητας', + 'personalization_activity_type_modal_question' => 'Πώς πρέπει να ονομάσουμε αυτόν τον νέο τύπο δραστηριότητας;', + 'personalization_activity_type_modal_edit' => 'Επεξεργασία τύπου δραστηριότητας', + 'personalization_activity_type_category_modal_delete' => 'Διαγραφή κατηγορίας τύπου δραστηριότητας', + 'personalization_activity_type_category_modal_delete_desc' => 'Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτήν την κατηγορία; Η διαγραφή του θα διαγράψει όλους τους σχετικούς τύπους δραστηριότητας. Οι δραστηριότητες που ανήκουν σε αυτήν την κατηγορία δε θα επηρεαστούν από αυτήν τη διαγραφή.', + 'personalization_activity_type_modal_delete' => 'Διαγραφή τύπου δραστηριότητας', + 'personalization_activity_type_modal_delete_desc' => 'Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτόν τον τύπο δραστηριότητας; Οι δραστηριότητες που ανήκουν σε αυτήν την κατηγορία δε θα επηρεαστούν από αυτήν τη διαγραφή.', + 'personalization_activity_type_modal_delete_error' => 'Δε μπορεί να βρεθεί αυτός ο τύπος δραστηριότητας.', + 'personalization_activity_type_category_modal_delete_error' => 'Δε μπορεί να βρεθεί αυτή η κατηγορία τύπου δραστηριότητας.', + + 'personalization_life_event_category_title' => 'Life event categories', + 'personalization_live_event_category_table_name' => 'Name', + 'personalization_life_event_category_description' => 'A life event can have a type and a category. Your account comes with a set of predefined categories and types by default, but you can customize life event types here.', + 'personalization_live_event_category_table_actions' => 'Actions', + 'personalization_life_event_type_add_button' => 'Add a new life event type', + 'personalization_life_event_type_modal_add' => 'Add a new life event type', + 'personalization_life_event_type_modal_question' => 'What should we name this new life event type?', + 'personalization_life_event_type_modal_edit' => 'Edit a life event type', + 'personalization_life_event_type_modal_delete' => 'Delete a life event type', + 'personalization_life_event_type_modal_delete_desc' => 'Are you sure you want to delete this life event type? Life events that belong to this type will be deleted by performing this action.', + 'personalization_life_event_type_modal_delete_error' => 'We can’t find this life event type.', + + 'personalization_life_event_category_work_education' => 'Work & education', + 'personalization_life_event_category_family_relationships' => 'Family & relationships', + 'personalization_life_event_category_home_living' => 'Home & living', + 'personalization_life_event_category_travel_experiences' => 'Travel & experiences', + 'personalization_life_event_category_health_wellness' => 'Health & wellness', + + 'personalization_life_event_type_new_job' => 'New job', + 'personalization_life_event_type_retirement' => 'Retirement', + 'personalization_life_event_type_new_school' => 'New school', + 'personalization_life_event_type_study_abroad' => 'Study abroad', + 'personalization_life_event_type_volunteer_work' => 'Volunteer work', + 'personalization_life_event_type_published_book_or_paper' => 'Published a book or paper', + 'personalization_life_event_type_military_service' => 'Military service', + 'personalization_life_event_type_first_met' => 'First met', + 'personalization_life_event_type_new_relationship' => 'New relationship', + 'personalization_life_event_type_engagement' => 'Engagement', + 'personalization_life_event_type_marriage' => 'Marriage', + 'personalization_life_event_type_anniversary' => 'Anniversary', + 'personalization_life_event_type_expecting_a_baby' => 'Expecting a baby', + 'personalization_life_event_type_new_child' => 'New child', + 'personalization_life_event_type_new_family_member' => 'New family member', + 'personalization_life_event_type_new_pet' => 'New pet', + 'personalization_life_event_type_end_of_relationship' => 'End of relationship', + 'personalization_life_event_type_loss_of_a_loved_one' => 'Loss of a loved one', + 'personalization_life_event_type_moved' => 'Moved', + 'personalization_life_event_type_bought_a_home' => 'Bought a home', + 'personalization_life_event_type_home_improvement' => 'Home improvement', + 'personalization_life_event_type_holidays' => 'Holidays', + 'personalization_life_event_type_new_vehicle' => 'New vehicle', + 'personalization_life_event_type_new_roommate' => 'New roommate', + 'personalization_life_event_type_overcame_an_illness' => 'Overcame an illness', + 'personalization_life_event_type_quit_a_habit' => 'Quit a habit', + 'personalization_life_event_type_new_eating_habits' => 'New eating habits', + 'personalization_life_event_type_weight_loss' => 'Weight loss', + 'personalization_life_event_type_wear_glass_or_contact' => 'Started wearing glasses or contacts', + 'personalization_life_event_type_broken_bone' => 'Broke a bone', + 'personalization_life_event_type_removed_braces' => 'Had braces removed', + 'personalization_life_event_type_surgery' => 'Had surgery', + 'personalization_life_event_type_dentist' => 'Had dental treatment', + 'personalization_life_event_type_new_sport' => 'Started playing a new sport', + 'personalization_life_event_type_new_hobby' => 'Took up a new hobby', + 'personalization_life_event_type_new_instrument' => 'Started learning a new instrument', + 'personalization_life_event_type_new_language' => 'Started learning a new language', + 'personalization_life_event_type_tattoo_or_piercing' => 'Tattoo or piercing', + 'personalization_life_event_type_new_license' => 'New license', + 'personalization_life_event_type_travel' => 'Travel', + 'personalization_life_event_type_achievement_or_award' => 'Achievement or award', + 'personalization_life_event_type_changed_beliefs' => 'Changed beliefs', + 'personalization_life_event_type_first_word' => 'First word', + 'personalization_life_event_type_first_kiss' => 'First kiss', + + 'storage_title' => 'Storage', + 'storage_account_info' => 'Your account limit is :accountLimit MB. Your current usage is :currentAccountSize MB (about :percentUsage%).', + 'storage_upgrade_notice' => 'Upgrade your account to be able to upload documents and photos.', + 'storage_description' => 'Here you can see all the documents and photos uploaded about your contacts.', + + 'dav_title' => 'WebDAV', + 'dav_description' => 'Εδώ μπορείτε να βρείτε όλες τις ρυθμίσεις για τη χρήση πόρων WebDAV για εξαγωγές CardDAV και CalDAV.', + 'dav_copy_help' => 'Αντιγραφή στο πρόχειρο σας', + 'dav_clipboard_copied' => 'Η τιμή αντιγράφηκε στο πρόχειρο σας', + 'dav_url_base' => 'Βασικό URL για όλους τους πόρους CardDAV και CalDAV:', + 'dav_connect_help' => 'Μπορείτε να συνδέσετε τις επαφές ή / και τα ημερολόγιά σας με αυτό το URL στο τηλέφωνο ή τον υπολογιστή σας.', + 'dav_connect_help2' => 'Use your login (email) and create an API token as the password to authenticate.', + 'dav_url_carddav' => 'CardDAV url for Contacts resource:', + 'dav_url_caldav_birthdays' => 'CalDAV url for Birthdays resources:', + 'dav_url_caldav_tasks' => 'CalDAV url for Tasks resources:', + 'dav_title_carddav' => 'CardDAV', + 'dav_title_caldav' => 'CalDAV', + 'dav_carddav_export' => 'Εξαγωγή όλων των επαφών σε ένα αρχείο', + 'dav_caldav_birthdays_export' => 'Εξαγωγή όλων των γενεθλίων σε ένα αρχείο', + 'dav_caldav_tasks_export' => 'Εξαγωγή όλων των εργασιών σε ένα αρχείο', + + 'archive_title' => 'Αρχειοθέτηση όλων των επαφών στον λογαριασμό σας', + 'archive_desc' => 'Αυτό θα αρχειοθετήσει όλες τις επαφές στον λογαριασμό σας.', + 'archive_cta' => 'Αρχειοθέτηση όλων των επαφών σας', + + 'logs_title' => 'Όλα όσα έχουν συμβεί σε αυτόν τον λογαριασμό', + 'logs_actor' => 'Χρήστης', + 'logs_timestamp' => 'Χρονοσήμανση', + 'logs_description' => 'Περιγραφή', + 'logs_subject' => 'Θέμα', + 'logs_size' => 'Μέγεθος (Kb)', + 'logs_object' => 'Αντικείμενο', +]; diff --git a/resources/lang/el/validation.php b/resources/lang/el/validation.php new file mode 100644 index 0000000..a04b564 --- /dev/null +++ b/resources/lang/el/validation.php @@ -0,0 +1,166 @@ + 'Το :attribute πρέπει να γίνει αποδεκτό.', + 'active_url' => 'Το :attribute δεν είναι έγκυρο URL.', + 'after' => 'To :attribute πρέπει να είναι μια ημερομηνία μετά τις :date.', + 'after_or_equal' => 'Tο :attribute πρέπει να είναι μια ημερομηνία μετά ή ίδια με :date.', + 'alpha' => 'Το :attribute μπορεί να περιέχει μόνο γράμματα.', + 'alpha_dash' => 'Το :attribute μπορεί να περιέχει μόνο γράμματα, αριθμούς, παύλες και κάτω παύλες.', + 'alpha_num' => 'Το :attribute μπορεί να περιέχει μόνο γράμματα και αριθμούς.', + 'array' => 'Το :attribute πρέπει να είναι πίνακας.', + 'before' => 'Η ιδιότητα: πρέπει να είναι μια ημερομηνία πριν από :date.', + 'before_or_equal' => 'Το :attribute πρέπει να είναι ημερομηνία πριν ή ίδια με :date.', + 'between' => [ + 'numeric' => 'Το :attribute πρέπει να είναι μεταξύ :min και :max.', + 'file' => 'Το :attribute πρέπει να είναι μεταξύ :min και :max kilobytes.', + 'string' => 'Το χαρακτηριστικό: πρέπει να είναι μεταξύ :min και :max χαρακτήρες.', + 'array' => 'Το :attribute πρέπει να έχει μεταξύ :min και :max αντικείμενα.', + ], + 'boolean' => 'Το πεδίο :attribute πρέπει να είναι αληθές ή ψευδές.', + 'confirmed' => 'Η επιβεβαίωση του :attribute δεν ταιριάζει.', + 'date' => 'Το :attribute δεν είναι έγκυρη ημερομηνία.', + 'date_equals' => 'To :attribute πρέπει να είναι μια ημερομηνία ίση με :date.', + 'date_format' => 'Tο :attribute δεν ταιριάζει με την μορφή :format.', + 'different' => 'Το :attribute και :other πρέπει να είναι διαφορετικά.', + 'digits' => 'Το :attribute πρέπει να είναι :digits ψηφία.', + 'digits_between' => 'To :attribute πρέπει να είναι μεταξύ :min και :max ψηφία.', + 'dimensions' => 'Το :attribute δεν έχει έγκυρες διαστάσεις εικόνας.', + 'distinct' => 'Το πεδίο :attribute έχει διπλότυπη τιμή.', + 'email' => 'Το πεδίο :attribute πρέπει να είναι μία έγκυρη διεύθυνση E-mail.', + 'ends_with' => 'Το :attribute πρέπει να τελειώνει με μια απο τις ακόλουθες τιμές: :values.', + 'exists' => 'Το επιλεγμένο :attribute δεν είναι έγκυρο.', + 'file' => 'Tο :attribute πρέπει να είναι αρχείο.', + 'filled' => 'To πεδίο :attribute πρέπει να έχει τιμή.', + 'gt' => [ + 'numeric' => 'Το :attribute πρέπει να είναι μεγαλύτερο από :value.', + 'file' => 'To :attribute πρέπει να είναι μεγαλύτερο από :value kilobytes.', + 'string' => 'Tο :attribute πρέπει να έχει περισσότερους από :value χαρακτήρες.', + 'array' => 'Το :attribute πρέπει να περιέχει περισσότερα από :value αντικείμενα.', + ], + 'gte' => [ + 'numeric' => 'Το :attribute πρέπει να είναι μεγαλύτερο ή ίσο από :value.', + 'file' => 'Το :attribute πρέπει να είναι μεγαλύτερο ή ίσο με :value kilobytes.', + 'string' => 'To :attribute πρέπει να είναι μεγαλύτερο ή ίσο από :value χαρακτήρες.', + 'array' => 'Το :attribute πρέπει να έχει :value αντικείμενα ή παραπάνω.', + ], + 'image' => 'Tο :attribute πρέπει να είναι εικόνα.', + 'in' => 'Το επιλεγμένο :attribute δεν είναι έγκυρο.', + 'in_array' => 'Το πεδίο :attribute δεν υπάρχει σε :other.', + 'integer' => 'To :attribute πρέπει να είναι ακέραιος αριθμός.', + 'ip' => 'Το :attribute πρέπει να είναι έγκυρη διεύθυνση IP.', + 'ipv4' => 'Το :attribute πρέπει να είναι μια έγκυρη διεύθυνση IPv4.', + 'ipv6' => 'Το :attribute πρέπει να είναι μια έγκυρη IPv6 διεύθυνση.', + 'json' => 'Το :attribute πρέπει να είναι μια έγκυρη συμβολοσειρά JSON.', + 'lt' => [ + 'numeric' => 'Το :attribute πρέπει να είναι μικρότερο του :value.', + 'file' => 'To :attribute πρέπει να είναι μικρότερο από :value kilobytes.', + 'string' => 'To :attribute πρέπει να είναι μικρότερο από :value kilobytes.', + 'array' => 'Tο :attribute πρέπει να έχει λιγότερα από :value αντικείμενα.', + ], + 'lte' => [ + 'numeric' => 'Το :attribute πρέπει να είναι μικρότερο ή ίσο του :value.', + 'file' => 'Το :attribute πρέπει να είναι μικρότερο ή ίσο του :value kilobytes.', + 'string' => 'Το :attribute πρέπει να είναι μικρότερο ή ίσο του :value kilobytes.', + 'array' => 'Tο :attribute δεν πρέπει να έχει περισσότερα από :value αντικείμενα.', + ], + 'max' => [ + 'numeric' => 'Tο :attribute δεν μπορεί να είναι μεγαλύτερο από :max.', + 'file' => 'To :attribute δεν μπορεί να είναι μεγαλύτερο από :max kilobytes.', + 'string' => 'Το :attribute δεν μπορεί να είναι μεγαλύτερο από :max χαρακτήρες.', + 'array' => 'Το :attribute δεν μπορεί να περιέχει περισσότερα από :max αντικείμενα.', + ], + 'mimes' => 'Το :attribute πρέπει να είναι ένα αρχείου τύπου: :values.', + 'mimetypes' => 'Το :attribute πρέπει να είναι ένα αρχείου τύπου: :values.', + 'min' => [ + 'numeric' => 'To :attribute πρέπει να είναι τουλάχιστον :min.', + 'file' => 'To :attribute πρέπει να είναι τουλάχιστον :min kilobytes.', + 'string' => 'Το :attribute πρέπει να είναι τουλάχιστον :min χαρακτήρες.', + 'array' => 'To :attribute πρέπει να έχει τουλάχιστον :min αντικείμενα.', + ], + 'not_in' => 'Το επιλεγμένο :attribute δεν είναι έγκυρο.', + 'not_regex' => 'Η μορφή του :attribute δεν είναι έγκυρη.', + 'numeric' => 'To :attribute πρέπει να είναι αριθμός.', + 'password' => 'Ο κωδικός πρόσβασης είναι εσφαλμένος.', + 'present' => 'Tο πεδίο :attribute δεν πρέπει να παραλείπεται.', + 'regex' => 'Η μορφή του :attribute δεν είναι έγκυρη.', + 'required' => 'Το πεδίο :attribute είναι υποχρεωτικό.', + 'required_if' => 'Το πεδίο :attribute είναι απαραίτητο όταν η τιμή του :other είναι :value.', + 'required_unless' => 'Το πεδίο :attribute είναι απαραίτητο εκτός αν το :other περιέχεται στα :values.', + 'required_with' => 'Tο πεδίο :attribute είναι απαραίτητο όταν :values είναι παρούσα.', + 'required_with_all' => 'Tο πεδίο :attribute είναι απαραίτητο όταν :values είναι παρούσες.', + 'required_without' => 'Tο πεδίο :attribute είναι απαραίτητο όταν :values δεν είναι παρούσα.', + 'required_without_all' => 'Το πεδίο :attribute είναι υποχρεωτικό όταν κανένα από τα :values δεν εμφανίζονται.', + 'same' => 'Το :attribute και :other πρέπει να ταιριάζουν.', + 'size' => [ + 'numeric' => 'Το :attribute πρέπει να είναι :size.', + 'file' => 'Το :attribute πρέπει να είναι :size kilobytes.', + 'string' => 'Το :attribute πρέπει να είναι :size χαρακτήρες.', + 'array' => 'Το :attribute πρέπει να περιέχει :size αντικείμενα.', + ], + 'starts_with' => 'Το :attribute πρέπει να αρχίζει με μια από τις ακόλουθες τιμές: :values.', + 'string' => 'Το :attribute πρέπει να είναι κείμενο.', + 'timezone' => 'Το :attribute πρέπει να είναι μία έγκυρη ζώνη.', + 'unique' => 'Το :attribute δεν είναι διαθέσιμο.', + 'uploaded' => 'Το :attribute απέτυχε να μεταφορτωθεί.', + 'url' => 'Η μορφή του :attribute δεν είναι έγκυρη.', + 'uuid' => 'Tο :attribute πρέπει να είναι ένα έγκυρο UUID.', + + /* + |-------------------------------------------------------------------------- + | Custom Validation Language Lines + |-------------------------------------------------------------------------- + | + | Here you may specify custom validation messages for attributes using the + | convention "attribute.rule" to name the lines. This makes it quick to + | specify a specific custom language line for a given attribute rule. + | + */ + + 'custom' => [ + 'attribute-name' => [ + 'rule-name' => 'custom-message', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Custom Validation Attributes + |-------------------------------------------------------------------------- + | + | The following language lines are used to swap our attribute placeholder + | with something more reader friendly such as "E-Mail Address" instead + | of "email". This simply helps us make our message more expressive. + | + */ + + 'attributes' => [], + + 'vue' => [ + 'max' => [ + 'numeric' => '{field} δεν μπορεί να είναι μεγαλύτερο από {max}.', + 'string' => '{field} δεν μπορεί να είναι μεγαλύτερο από {max} χαρακτήρες.', + ], + 'required' => '{field} απαιτείται.', + 'url' => '{field} δεν είναι έγκυρη διεύθυνση URL.', + ], + +]; diff --git a/resources/lang/en-GB.json b/resources/lang/en-GB.json new file mode 100644 index 0000000..ddea72e --- /dev/null +++ b/resources/lang/en-GB.json @@ -0,0 +1,7 @@ +{ + "The :attribute must contain at least one uppercase and one lowercase letter.": "The :attribute must contain at least one uppercase and one lowercase letter.", + "The :attribute must contain at least one letter.": "The :attribute must contain at least one letter.", + "The :attribute must contain at least one symbol.": "The :attribute must contain at least one symbol.", + "The :attribute must contain at least one number.": "The :attribute must contain at least one number.", + "The given :attribute has appeared in a data leak. Please choose a different :attribute.": "The given :attribute has appeared in a data leak. Please choose a different :attribute." +} diff --git a/resources/lang/en-GB/app.php b/resources/lang/en-GB/app.php new file mode 100644 index 0000000..62471ce --- /dev/null +++ b/resources/lang/en-GB/app.php @@ -0,0 +1,571 @@ + 'Yes', + 'no' => 'No', + 'update' => 'Update', + 'save' => 'Save', + 'add' => 'Add', + 'cancel' => 'Cancel', + 'confirm' => 'Confirm', + 'delete_confirm' => 'Are you sure?', + 'delete' => 'Delete', + 'edit' => 'Edit', + 'upload' => 'Upload', + 'download' => 'Download', + 'save_close' => 'Save and close', + 'close' => 'Close', + 'copy' => 'Copy', + 'create' => 'Create', + 'remove' => 'Remove', + 'revoke' => 'Revoke', + 'done' => 'Done', + 'back' => 'Back', + 'verify' => 'Verify', + 'new' => 'new', + 'unknown' => 'I don’t know', + 'load_more' => 'Load more', + 'loading' => 'Loading…', + 'with' => 'with', + 'today' => 'today', + 'yesterday' => 'yesterday', + 'another_day' => 'another day', + 'date' => 'Date', + 'type' => 'Type', + 'zoom' => 'Zoom', + 'upgrade' => 'Upgrade to unlock', + 'percent_uploaded' => '{percent}% uploaded', + 'retry' => 'Retry', + 'filter' => 'Filter the list', + 'go_back' => 'Go back', + 'file_selected' => 'One file selected…|{count} files selected…', + + 'application_title' => 'Monica – personal relationship manager', + 'application_description' => 'Monica is a tool to manage your interactions with your loved ones, friends and family.', + 'application_og_title' => 'Have better relations with your loved ones. Free online CRM for friends and family.', + + 'markdown_description' => 'Want to format your text in a nice way? We support Markdown to add bold, italic, lists and more.', + 'markdown_link' => 'Read documentation', + + 'header_settings_link' => 'Settings', + 'header_logout_link' => 'Logout', + 'header_changelog_link' => 'Product changes', + + 'main_nav_cta' => 'Add people', + 'main_nav_dashboard' => 'Dashboard', + 'main_nav_family' => 'Contacts', + 'main_nav_journal' => 'Journal', + 'main_nav_activities' => 'Activities', + 'main_nav_tasks' => 'Tasks', + + 'footer_remarks' => 'Comments?', + 'footer_send_email' => 'Send us an email', + 'footer_privacy' => 'Privacy policy', + 'footer_release' => 'Release notes', + 'footer_newsletter' => 'Newsletter', + 'footer_source_code' => 'Contribute', + 'footer_version' => 'Version: :version', + 'footer_new_version' => 'A new version of Monica is available', + + 'footer_modal_version_whats_new' => 'What’s new', + 'footer_modal_version_release_away' => 'You are 1 release behind the latest version available. You should update your instance.|You are :number releases behind the latest version available. You should update your instance.', + + 'breadcrumb_dashboard' => 'Dashboard', + 'breadcrumb_list_contacts' => 'List of people', + 'breadcrumb_archived_contacts' => 'Archived contacts', + 'breadcrumb_journal' => 'Journal', + 'breadcrumb_settings' => 'Settings', + 'breadcrumb_settings_export' => 'Export', + 'breadcrumb_settings_users' => 'Users', + 'breadcrumb_settings_users_add' => 'Add a user', + 'breadcrumb_settings_subscriptions' => 'Subscription', + 'breadcrumb_settings_import' => 'Import', + 'breadcrumb_settings_import_report' => 'Import report', + 'breadcrumb_settings_import_upload' => 'Upload', + 'breadcrumb_settings_tags' => 'Tags', + 'breadcrumb_add_significant_other' => 'Add significant other', + 'breadcrumb_edit_significant_other' => 'Edit significant other', + 'breadcrumb_add_note' => 'Add a note', + 'breadcrumb_edit_note' => 'Edit a note', + 'breadcrumb_api' => 'API', + 'breadcrumb_dav' => 'DAV Resources', + 'breadcrumb_edit_introductions' => 'How did you meet', + 'breadcrumb_settings_personalization' => 'Personalization', + 'breadcrumb_settings_security' => 'Security', + 'breadcrumb_settings_security_2fa' => 'Two Factor Authentication', + 'breadcrumb_profile' => 'Profile of :name', + + 'gender_male' => 'Man', + 'gender_female' => 'Woman', + 'gender_none' => 'Rather not say', + 'gender_no_gender' => 'No gender', + + 'error_title' => 'Whoops! Something went wrong.', + 'error_unauthorized' => 'You don’t have the right to edit this resource.', + 'error_user_account' => 'This user does not belong to the given account.', + 'error_save' => 'We had an error trying to save the data.', + 'error_try_again' => 'Something went wrong. Please try again.', + 'error_id' => 'Error ID: :id', + 'error_unavailable' => 'Service unavailable', + 'error_maintenance' => 'Maintenance in progress. We’ll be right back.', + 'error_help' => 'We’ll be right back.', + 'error_twitter' => 'Follow our Twitter account to be alerted when it’s up again.', + 'error_no_term' => 'There is no policy for this instance yet.', + + 'default_save_success' => 'The data has been saved.', + + 'compliance_title' => 'Sorry for the interruption.', + 'compliance_desc' => 'We have changed our Terms of Use and Privacy Policy. By law we have to ask you to review them and accept them so you can continue to use your account.', + 'compliance_desc_end' => 'We don’t do anything nasty with your data or account and will never do.', + 'compliance_terms' => 'Accept new terms and privacy policy', + + // Relationship types + // Yes, each relationship type has 8 strings associated with it. + // This is because we need to indicate the name of the relationship type, + // and also the name of the opposite side of this relationship (father/son), + // and then, the feminine version of the string. Finally, in some sentences + // in the UI, we need to include the name of the person we add the relationship + // to. + 'relationship_type_group_love' => 'Love relationships', + 'relationship_type_group_family' => 'Family relationships', + 'relationship_type_group_friend' => 'Friend relationships', + 'relationship_type_group_work' => 'Work relationships', + 'relationship_type_group_other' => 'Other kind of relationships', + + 'relationship_type_partner' => 'significant other', + 'relationship_type_partner_female' => 'significant other', + 'relationship_type_partner_male' => 'partner', + 'relationship_type_partner_with_name' => ':name’s significant other', + 'relationship_type_partner_female_with_name' => ':name’s significant other', + 'relationship_type_partner_male_with_name' => ':name’s partner', + + 'relationship_type_spouse' => 'spouse', + 'relationship_type_spouse_female' => 'wife', + 'relationship_type_spouse_male' => 'husband', + 'relationship_type_spouse_with_name' => ':name’s spouse', + 'relationship_type_spouse_female_with_name' => ':name’s wife', + 'relationship_type_spouse_male_with_name' => ':name’s husband', + + 'relationship_type_date' => 'date', + 'relationship_type_date_female' => 'girlfriend', + 'relationship_type_date_male' => 'boyfriend', + 'relationship_type_date_with_name' => ':name’s date', + 'relationship_type_date_female_with_name' => ':name’s girlfriend', + 'relationship_type_date_male_with_name' => ':name’s boyfriend', + + 'relationship_type_lover' => 'lover', + 'relationship_type_lover_female' => 'lover', + 'relationship_type_lover_male' => 'lover', + 'relationship_type_lover_with_name' => ':name’s lover', + 'relationship_type_lover_female_with_name' => ':name’s lover', + 'relationship_type_lover_male_with_name' => ':name’s lover', + + 'relationship_type_inlovewith' => 'in love with', + 'relationship_type_inlovewith_female' => 'in love with', + 'relationship_type_inlovewith_male' => 'in love with', + 'relationship_type_inlovewith_with_name' => 'someone :name is in love with', + 'relationship_type_inlovewith_female_with_name' => 'someone :name is in love with', + 'relationship_type_inlovewith_male_with_name' => 'someone :name is in love with', + + 'relationship_type_lovedby' => 'loved by', + 'relationship_type_lovedby_female' => 'loved by', + 'relationship_type_lovedby_male' => 'loved by', + 'relationship_type_lovedby_with_name' => ':name’s secret lover', + 'relationship_type_lovedby_female_with_name' => ':name’s secret lover', + 'relationship_type_lovedby_male_with_name' => ':name’s secret lover', + + 'relationship_type_ex' => 'ex-partner', + 'relationship_type_ex_female' => 'ex-girlfriend', + 'relationship_type_ex_male' => 'ex-boyfriend', + 'relationship_type_ex_with_name' => ':name’s ex-partner', + 'relationship_type_ex_female_with_name' => ':name’s ex-girlfriend', + 'relationship_type_ex_male_with_name' => ':name’s ex-boyfriend', + + 'relationship_type_parent' => 'parent', + 'relationship_type_parent_female' => 'mother', + 'relationship_type_parent_male' => 'father', + 'relationship_type_parent_with_name' => ':name’s parent', + 'relationship_type_parent_female_with_name' => ':name’s mother', + 'relationship_type_parent_male_with_name' => ':name’s father', + + 'relationship_type_child' => 'child', + 'relationship_type_child_female' => 'daughter', + 'relationship_type_child_male' => 'son', + 'relationship_type_child_with_name' => ':name’s child', + 'relationship_type_child_female_with_name' => ':name’s daughter', + 'relationship_type_child_male_with_name' => ':name’s son', + + 'relationship_type_stepparent' => 'step-parent', + 'relationship_type_stepparent_female' => 'stepmother', + 'relationship_type_stepparent_male' => 'stepfather', + 'relationship_type_stepparent_with_name' => ':name’s step-parent', + 'relationship_type_stepparent_female_with_name' => ':name’s stepmother', + 'relationship_type_stepparent_male_with_name' => ':name’s stepfather', + + 'relationship_type_stepchild' => 'stepchild', + 'relationship_type_stepchild_female' => 'stepdaughter', + 'relationship_type_stepchild_male' => 'stepson', + 'relationship_type_stepchild_with_name' => ':name’s stepchild', + 'relationship_type_stepchild_female_with_name' => ':name’s stepdaughter', + 'relationship_type_stepchild_male_with_name' => ':name’s stepson', + + 'relationship_type_sibling' => 'sibling', + 'relationship_type_sibling_female' => 'sister', + 'relationship_type_sibling_male' => 'brother', + 'relationship_type_sibling_with_name' => ':name’s sibling', + 'relationship_type_sibling_female_with_name' => ':name’s sister', + 'relationship_type_sibling_male_with_name' => ':name’s brother', + + 'relationship_type_grandparent' => 'grandparent', + 'relationship_type_grandparent_female' => 'grandmother', + 'relationship_type_grandparent_male' => 'grandfather', + 'relationship_type_grandparent_with_name' => ':name’s grandparent', + 'relationship_type_grandparent_female_with_name' => ':name’s grandmother', + 'relationship_type_grandparent_male_with_name' => ':name’s grandfather', + + 'relationship_type_grandchild' => 'grandchild', + 'relationship_type_grandchild_female' => 'granddaughter', + 'relationship_type_grandchild_male' => 'grandson', + 'relationship_type_grandchild_with_name' => ':name’s grandchild', + 'relationship_type_grandchild_female_with_name' => ':name’s granddaughter', + 'relationship_type_grandchild_male_with_name' => ':name’s grandson', + + 'relationship_type_uncle' => 'uncle', + 'relationship_type_uncle_female' => 'aunt', + 'relationship_type_uncle_male' => 'uncle', + 'relationship_type_uncle_with_name' => ':name’s uncle', + 'relationship_type_uncle_female_with_name' => ':name’s aunt', + 'relationship_type_uncle_male_with_name' => ':name’s uncle', + + 'relationship_type_nephew' => 'nephew', + 'relationship_type_nephew_female' => 'niece', + 'relationship_type_nephew_male' => 'nephew', + 'relationship_type_nephew_with_name' => ':name’s nephew', + 'relationship_type_nephew_female_with_name' => ':name’s niece', + 'relationship_type_nephew_male_with_name' => ':name’s nephew', + + 'relationship_type_cousin' => 'cousin', + 'relationship_type_cousin_female' => 'cousin', + 'relationship_type_cousin_male' => 'cousin', + 'relationship_type_cousin_with_name' => ':name’s cousin', + 'relationship_type_cousin_female_with_name' => ':name’s cousin', + 'relationship_type_cousin_male_with_name' => ':name’s cousin', + + 'relationship_type_godfather' => 'godparent', + 'relationship_type_godfather_female' => 'godmother', + 'relationship_type_godfather_male' => 'godfather', + 'relationship_type_godfather_with_name' => ':name’s godparent', + 'relationship_type_godfather_female_with_name' => ':name’s godmother', + 'relationship_type_godfather_male_with_name' => ':name’s godfather', + + 'relationship_type_godson' => 'godchild', + 'relationship_type_godson_female' => 'goddaughter', + 'relationship_type_godson_male' => 'godson', + 'relationship_type_godson_with_name' => ':name’s godchild', + 'relationship_type_godson_female_with_name' => ':name’s goddaughter', + 'relationship_type_godson_male_with_name' => ':name’s godson', + + 'relationship_type_friend' => 'friend', + 'relationship_type_friend_female' => 'friend', + 'relationship_type_friend_male' => 'friend', + 'relationship_type_friend_with_name' => ':name’s friend', + 'relationship_type_friend_female_with_name' => ':name’s friend', + 'relationship_type_friend_male_with_name' => ':name’s friend', + + 'relationship_type_bestfriend' => 'best friend', + 'relationship_type_bestfriend_female' => 'best friend', + 'relationship_type_bestfriend_male' => 'best friend', + 'relationship_type_bestfriend_with_name' => ':name’s best friend', + 'relationship_type_bestfriend_female_with_name' => ':name’s best friend', + 'relationship_type_bestfriend_male_with_name' => ':name’s best friend', + + 'relationship_type_colleague' => 'colleague', + 'relationship_type_colleague_female' => 'colleague', + 'relationship_type_colleague_male' => 'colleague', + 'relationship_type_colleague_with_name' => ':name’s colleague', + 'relationship_type_colleague_female_with_name' => ':name’s colleague', + 'relationship_type_colleague_male_with_name' => ':name’s colleague', + + 'relationship_type_boss' => 'boss', + 'relationship_type_boss_female' => 'boss', + 'relationship_type_boss_male' => 'boss', + 'relationship_type_boss_with_name' => ':name’s boss', + 'relationship_type_boss_female_with_name' => ':name’s boss', + 'relationship_type_boss_male_with_name' => ':name’s boss', + + 'relationship_type_subordinate' => 'subordinate', + 'relationship_type_subordinate_female' => 'subordinate', + 'relationship_type_subordinate_male' => 'subordinate', + 'relationship_type_subordinate_with_name' => ':name’s subordinate', + 'relationship_type_subordinate_female_with_name' => ':name’s subordinate', + 'relationship_type_subordinate_male_with_name' => ':name’s subordinate', + + 'relationship_type_mentor' => 'mentor', + 'relationship_type_mentor_female' => 'mentor', + 'relationship_type_mentor_male' => 'mentor', + 'relationship_type_mentor_with_name' => ':name’s mentor', + 'relationship_type_mentor_female_with_name' => ':name’s mentor', + 'relationship_type_mentor_male_with_name' => ':name’s mentor', + + 'relationship_type_protege' => 'protégé', + 'relationship_type_protege_female' => 'protégé', + 'relationship_type_protege_male' => 'protégé', + 'relationship_type_protege_with_name' => ':name’s protégé', + 'relationship_type_protege_female_with_name' => ':name’s protégé', + 'relationship_type_protege_male_with_name' => ':name’s protégé', + + 'relationship_type_ex_husband' => 'ex-spouse', + 'relationship_type_ex_husband_female' => 'ex-wife', + 'relationship_type_ex_husband_male' => 'ex-husband', + 'relationship_type_ex_husband_with_name' => ':name’s ex-spouse', + 'relationship_type_ex_husband_female_with_name' => ':name’s ex-wife', + 'relationship_type_ex_husband_male_with_name' => ':name’s ex-husband', + + // emotions + 'emotion_primary_love' => 'Love', + 'emotion_primary_joy' => 'Joy', + 'emotion_primary_surprise' => 'Surprise', + 'emotion_primary_anger' => 'Anger', + 'emotion_primary_sadness' => 'Sadness', + 'emotion_primary_fear' => 'Fear', + + 'emotion_secondary_affection' => 'Affection', + 'emotion_secondary_lust' => 'Lust', + 'emotion_secondary_longing' => 'Longing', + 'emotion_secondary_cheerfulness' => 'Cheerfulness', + 'emotion_secondary_zest' => 'Zest', + 'emotion_secondary_contentment' => 'Contentment', + 'emotion_secondary_pride' => 'Pride', + 'emotion_secondary_optimism' => 'Optimism', + 'emotion_secondary_enthrallment' => 'Enthrallment', + 'emotion_secondary_relief' => 'Relief', + 'emotion_secondary_surprise' => 'Surprise', + 'emotion_secondary_irritation' => 'Irritation', + 'emotion_secondary_exasperation' => 'Exasperation', + 'emotion_secondary_rage' => 'Rage', + 'emotion_secondary_disgust' => 'Disgust', + 'emotion_secondary_envy' => 'Envy', + 'emotion_secondary_suffering' => 'Suffering', + 'emotion_secondary_sadness' => 'Sadness', + 'emotion_secondary_disappointment' => 'Disappointment', + 'emotion_secondary_shame' => 'Shame', + 'emotion_secondary_neglect' => 'Neglect', + 'emotion_secondary_sympathy' => 'Sympathy', + 'emotion_secondary_horror' => 'Horror', + 'emotion_secondary_nervousness' => 'Nervousness', + + 'emotion_adoration' => 'Adoration', + 'emotion_affection' => 'Affection', + 'emotion_love' => 'Love', + 'emotion_fondness' => 'Fondness', + 'emotion_liking' => 'Liking', + 'emotion_attraction' => 'Attraction', + 'emotion_caring' => 'Caring', + 'emotion_tenderness' => 'Tenderness', + 'emotion_compassion' => 'Compassion', + 'emotion_sentimentality' => 'Sentimentality', + 'emotion_arousal' => 'Arousal', + 'emotion_desire' => 'Desire', + 'emotion_lust' => 'Lust', + 'emotion_passion' => 'Passion', + 'emotion_infatuation' => 'Infatuation', + 'emotion_longing' => 'Longing', + 'emotion_amusement' => 'Amusement', + 'emotion_bliss' => 'Bliss', + 'emotion_cheerfulness' => 'Cheerfulness', + 'emotion_gaiety' => 'Gaiety', + 'emotion_glee' => 'Glee', + 'emotion_jolliness' => 'Jolliness', + 'emotion_joviality' => 'Joviality', + 'emotion_joy' => 'Joy', + 'emotion_delight' => 'Delight', + 'emotion_enjoyment' => 'Enjoyment', + 'emotion_gladness' => 'Gladness', + 'emotion_happiness' => 'Happiness', + 'emotion_jubilation' => 'Jubilation', + 'emotion_elation' => 'Elation', + 'emotion_satisfaction' => 'Satisfaction', + 'emotion_ecstasy' => 'Ecstasy', + 'emotion_euphoria' => 'Euphoria', + 'emotion_enthusiasm' => 'Enthusiasm', + 'emotion_zeal' => 'Zeal', + 'emotion_zest' => 'Zest', + 'emotion_excitement' => 'Excitement', + 'emotion_thrill' => 'Thrill', + 'emotion_exhilaration' => 'Exhilaration', + 'emotion_contentment' => 'Contentment', + 'emotion_pleasure' => 'Pleasure', + 'emotion_pride' => 'Pride', + 'emotion_eagerness' => 'Eagerness', + 'emotion_hope' => 'Hope', + 'emotion_optimism' => 'Optimism', + 'emotion_enthrallment' => 'Enthrallment', + 'emotion_rapture' => 'Rapture', + 'emotion_relief' => 'Relief', + 'emotion_amazement' => 'Amazement', + 'emotion_surprise' => 'Surprise', + 'emotion_astonishment' => 'Astonishment', + 'emotion_aggravation' => 'Aggravation', + 'emotion_irritation' => 'Irritation', + 'emotion_agitation' => 'Agitation', + 'emotion_annoyance' => 'Annoyance', + 'emotion_grouchiness' => 'Grouchiness', + 'emotion_grumpiness' => 'Grumpiness', + 'emotion_exasperation' => 'Exasperation', + 'emotion_frustration' => 'Frustration', + 'emotion_anger' => 'Anger', + 'emotion_rage' => 'Rage', + 'emotion_outrage' => 'Outrage', + 'emotion_fury' => 'Fury', + 'emotion_wrath' => 'Wrath', + 'emotion_hostility' => 'Hostility', + 'emotion_ferocity' => 'Ferocity', + 'emotion_bitterness' => 'Bitterness', + 'emotion_hate' => 'Hate', + 'emotion_loathing' => 'Loathing', + 'emotion_scorn' => 'Scorn', + 'emotion_spite' => 'Spite', + 'emotion_vengefulness' => 'Vengefulness', + 'emotion_dislike' => 'Dislike', + 'emotion_resentment' => 'Resentment', + 'emotion_disgust' => 'Disgust', + 'emotion_revulsion' => 'Revulsion', + 'emotion_contempt' => 'Contempt', + 'emotion_envy' => 'Envy', + 'emotion_jealousy' => 'Jealousy', + 'emotion_agony' => 'Agony', + 'emotion_suffering' => 'Suffering', + 'emotion_hurt' => 'Hurt', + 'emotion_anguish' => 'Anguish', + 'emotion_depression' => 'Depression', + 'emotion_despair' => 'Despair', + 'emotion_hopelessness' => 'Hopelessness', + 'emotion_gloom' => 'Gloom', + 'emotion_glumness' => 'Glumness', + 'emotion_sadness' => 'Sadness', + 'emotion_unhappiness' => 'Unhappiness', + 'emotion_grief' => 'Grief', + 'emotion_sorrow' => 'Sorrow', + 'emotion_woe' => 'Woe', + 'emotion_misery' => 'Misery', + 'emotion_melancholy' => 'Melancholy', + 'emotion_dismay' => 'Dismay', + 'emotion_disappointment' => 'Disappointment', + 'emotion_displeasure' => 'Displeasure', + 'emotion_guilt' => 'Guilt', + 'emotion_shame' => 'Shame', + 'emotion_regret' => 'Regret', + 'emotion_remorse' => 'Remorse', + 'emotion_alienation' => 'Alienation', + 'emotion_isolation' => 'Isolation', + 'emotion_neglect' => 'Neglect', + 'emotion_loneliness' => 'Loneliness', + 'emotion_rejection' => 'Rejection', + 'emotion_homesickness' => 'Homesickness', + 'emotion_defeat' => 'Defeat', + 'emotion_dejection' => 'Dejection', + 'emotion_insecurity' => 'Insecurity', + 'emotion_embarrassment' => 'Embarrassment', + 'emotion_humiliation' => 'Humiliation', + 'emotion_insult' => 'Insult', + 'emotion_pity' => 'Pity', + 'emotion_sympathy' => 'Sympathy', + 'emotion_alarm' => 'Alarm', + 'emotion_shock' => 'Shock', + 'emotion_fear' => 'Fear', + 'emotion_fright' => 'Fright', + 'emotion_horror' => 'Horror', + 'emotion_terror' => 'Terror', + 'emotion_panic' => 'Panic', + 'emotion_hysteria' => 'Hysteria', + 'emotion_mortification' => 'Mortification', + 'emotion_anxiety' => 'Anxiety', + 'emotion_nervousness' => 'Nervousness', + 'emotion_tenseness' => 'Tenseness', + 'emotion_uneasiness' => 'Uneasiness', + 'emotion_apprehension' => 'Apprehension', + 'emotion_worry' => 'Worry', + 'emotion_distress' => 'Distress', + 'emotion_dread' => 'Dread', + + // weather + 'weather_sunny' => 'Sunny', + 'weather_clear' => 'Clear', + 'weather_clear-day' => 'Clear', + 'weather_clear-night' => 'Clear night', + 'weather_light-drizzle' => 'Light drizzle', + 'weather_patchy-light-drizzle' => 'Patchy light drizzle', + 'weather_patchy-light-rain' => 'Patchy light rain', + 'weather_light-rain' => 'Light rain', + 'weather_moderate-rain-at-times' => 'Moderate rain at times', + 'weather_moderate-rain' => 'Moderate rain', + 'weather_patchy-rain-possible' => 'Patchy rain possible', + 'weather_heavy-rain-at-times' => 'Heavy rain at times', + 'weather_heavy-rain' => 'Heavy rain', + 'weather_light-freezing-rain' => 'Light freezing rain', + 'weather_moderate-or-heavy-freezing-rain' => 'Moderate or heavy freezing rain', + 'weather_light-sleet' => 'Light sleet', + 'weather_moderate-or-heavy-rain-shower' => 'Moderate or heavy rain shower', + 'weather_light-rain-shower' => 'Light rain shower', + 'weather_torrential-rain-shower' => 'Torrential rain shower', + 'weather_rain' => 'Rain', + 'weather_snow' => 'Snow', + 'weather_blowing-snow' => 'Blowing snow', + 'weather_patchy-light-snow' => 'Patchy light snow', + 'weather_light-snow' => 'Light snow', + 'weather_patchy-moderate-snow' => 'Patchy moderate snow', + 'weather_moderate-snow' => 'Moderate snow', + 'weather_patchy-heavy-snow' => 'Patchy heavy snow', + 'weather_heavy-snow' => 'Heavy snow', + 'weather_light-snow-showers' => 'Light snow showers', + 'weather_moderate-or-heavy-snow-showers' => 'Moderate or heavy snow showers', + 'weather_patchy-snow-possible' => 'Patchy snow possible', + 'weather_patchy-sleet-possible' => 'Patchy sleet possible', + 'weather_moderate-or-heavy-sleet' => 'Moderate or heavy sleet', + 'weather_light-sleet-showers' => 'Light sleet showers', + 'weather_moderate-or-heavy-sleet-showers' => 'Moderate or heavy sleet showers', + 'weather_sleet' => 'Sleet', + 'weather_wind' => 'Wind', + 'weather_fog' => 'Fog', + 'weather_freezing-fog' => 'Freezing fog', + 'weather_mist' => 'Mist', + 'weather_blizzard' => 'Blizzard', + 'weather_overcast' => 'Overcast', + 'weather_cloudy' => 'Cloudy', + 'weather_partly-cloudy-day' => 'Partly cloudy', + 'weather_partly-cloudy-night' => 'Partly cloudy', + 'weather_freezing-drizzle' => 'Freezing drizzle', + 'weather_heavy-freezing-drizzle' => 'Heavy freezing drizzle', + 'weather_patchy-freezing-drizzle-possible' => 'Patchy freezing drizzle possible', + 'weather_ice-pellets' => 'Ice pellets', + 'weather_light-showers-of-ice-pellets' => 'Light showers of ice pellets', + 'weather_moderate-or-heavy-showers-of-ice-pellets' => 'Moderate or heavy showers of ice pellets', + 'weather_thundery-outbreaks-possible' => 'Thundery outbreaks possible', + 'weather_patchy-light-rain-with-thunder' => 'Patchy light rain with thunder', + 'weather_moderate-or-heavy-rain-with-thunder' => 'Moderate or heavy rain with thunder', + 'weather_patchy-light-snow-with-thunder' => 'Patchy light snow with thunder', + 'weather_moderate-or-heavy-snow-with-thunder' => 'Moderate or heavy snow with thunder', + 'weather_current_temperature_celsius' => ':temperature °C', + 'weather_current_temperature_fahrenheit' => ':temperature °F', + 'weather_current_title' => 'Current weather', + + // dav + 'dav_contacts' => 'Contacts', + 'dav_contacts_description' => ':name’s contacts', + 'dav_birthdays' => 'Birthdays', + 'dav_birthdays_description' => ':name’s contact’s birthdays', + 'dav_tasks' => 'Tasks', + 'dav_tasks_description' => ':name’s tasks', + + // contact list + 'contact_list_avatar' => 'Avatar', + 'contact_list_name' => 'Contact', + 'contact_list_description' => 'Description', + +]; diff --git a/resources/lang/en-GB/auth.php b/resources/lang/en-GB/auth.php new file mode 100644 index 0000000..993420d --- /dev/null +++ b/resources/lang/en-GB/auth.php @@ -0,0 +1,89 @@ + 'These credentials do not match our records.', + 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.', + 'not_authorized' => 'You are not authorized to execute this action', + 'signup_disabled' => 'Registration is currently disabled', + 'signup_error' => 'An error occurred trying to register the user', + 'back_homepage' => 'Back to homepage', + 'mfa_auth_otp' => 'Authenticate with your two factor device', + 'mfa_auth_webauthn' => 'Authenticate with a security key (WebAuthn)', + '2fa_title' => 'Two Factor Authentication', + '2fa_wrong_validation' => 'The two factor authentication has failed.', + '2fa_one_time_password' => 'Two factor authentication code', + '2fa_recuperation_code' => 'Enter a two factor recovery code', + '2fa_one_time_or_recuperation' => 'Enter a two factor authentication code or a recovery code', + '2fa_otp_help' => 'Open up your two factor authentication mobile app and copy the code', + + 'login_to_account' => 'Login to your account', + 'login_with_recovery' => 'Login with a recovery code', + 'login_again' => 'Please login again to your account', + 'email' => 'Email', + 'password' => 'Password', + 'recovery' => 'Recovery code', + 'login' => 'Login', + 'button_remember' => 'Remember Me', + 'password_forget' => 'Forget your password?', + 'password_reset' => 'Reset your password', + 'use_recovery' => 'Or you can use a recovery code', + 'signup_no_account' => 'Don’t have an account?', + 'signup' => 'Sign up', + 'create_account' => 'Create the first account by signing up', + 'change_language_title' => 'Change language:', + 'change_language' => 'Change language to :lang', + + 'password_reset_title' => 'Reset Password', + 'password_reset_email' => 'E-Mail Address', + 'password_reset_send_link' => 'Send Password Reset Link', + 'password_reset_password' => 'Password', + 'password_reset_password_confirm' => 'Confirm Password', + 'password_reset_action' => 'Reset Password', + 'password_reset_email_content' => 'Click here to reset your password:', + + 'register_title_welcome' => 'Welcome to your newly installed Monica instance', + 'register_create_account' => 'You need to create an account to use Monica', + 'register_title_create' => 'Create your Monica account', + 'register_login' => 'Log in if you already have an account.', + 'register_email' => 'Enter a valid email address', + 'register_email_example' => 'you@home', + 'register_firstname' => 'First name', + 'register_firstname_example' => 'eg. John', + 'register_lastname' => 'Last name', + 'register_lastname_example' => 'eg. Doe', + 'register_password' => 'Password', + 'register_password_example' => 'Enter a secure password', + 'register_password_confirmation' => 'Password confirmation', + 'register_action' => 'Register', + 'register_policy' => 'Signing up signifies you’ve read and agree to our Privacy Policy and Terms of use.', + 'register_invitation_email' => 'For security purposes, please indicate the email of the person who’ve invited you to join this account. This information is provided in the invitation email.', + + 'confirmation_title' => 'Verify Your Email Address', + 'confirmation_fresh' => 'A fresh verification link has been sent to your email address.', + 'confirmation_check' => 'Before proceeding, please check your email for a verification link.', + 'confirmation_request_another' => 'If you did not receive the email click here to request another.', + + 'confirmation_again' => 'If you want to change your email address you can click here.', + 'email_change_current_email' => 'Current email address:', + 'email_change_title' => 'Change your email address', + 'email_change_new' => 'New email address', + 'email_changed' => 'Your email address has been changed. Check your mailbox to validate it.', +]; diff --git a/resources/lang/en-GB/changelog.php b/resources/lang/en-GB/changelog.php new file mode 100644 index 0000000..981b018 --- /dev/null +++ b/resources/lang/en-GB/changelog.php @@ -0,0 +1,12 @@ + 'Product changes', + 'note' => 'Note: unfortunately, this page is only in English.', +]; diff --git a/resources/lang/en-GB/dashboard.php b/resources/lang/en-GB/dashboard.php new file mode 100644 index 0000000..ef61647 --- /dev/null +++ b/resources/lang/en-GB/dashboard.php @@ -0,0 +1,42 @@ + 'Welcome to your account!', + 'dashboard_blank_description' => 'Monica is the place to organise all the interactions you have with the people you care about.', + 'dashboard_blank_cta' => 'Add your first contact', + 'dashboard_blank_illustration' => 'Illustration by Freepik', + + 'notes_title' => 'You don’t have any starred notes yet.', + + 'tab_recent_calls' => 'Recent calls', + 'tab_favorite_notes' => 'Favorite notes', + 'tab_calls_blank' => 'You haven’t logged any calls yet.', + 'tab_debts' => 'Debts', + 'tab_debts_blank' => 'You haven’t logged any debts yet.', + 'tab_tasks' => 'Tasks', + 'tab_tasks_blank' => 'You haven’t any tasks yet.', + + 'tasks_add_task_placeholder' => 'What is this task about?', + 'tasks_tab_your_contacts' => 'Tasks related to your contacts', + 'tasks_tab_your_tasks' => 'Your tasks', + 'tasks_add_note' => 'Press Enter to add the task.', + 'task_add_cta' => 'Add a task', + + 'debts_you_owe' => 'You owe', + + 'statistics_contacts' => 'Contacts', + 'statistics_activities' => 'Activities', + 'statistics_gifts' => 'Gifts', + + 'reminders_next_months' => 'Events in the next 3 months', + 'reminders_none' => 'No reminders for this month.', + + 'product_changes' => 'Product changes', + 'product_view_details' => 'View details', +]; diff --git a/resources/lang/en-GB/format.php b/resources/lang/en-GB/format.php new file mode 100644 index 0000000..e6a0ad7 --- /dev/null +++ b/resources/lang/en-GB/format.php @@ -0,0 +1,36 @@ + 'd M Y H:i', + 'short_date_year' => 'd M Y', + 'short_date' => 'd M', + 'short_month' => 'M', + 'short_month_year' => 'M Y', + 'short_day' => 'D', + 'full_date_year' => 'd F Y', + 'full_month' => 'F', + 'full_month_year' => 'F Y', + 'full_hour' => 'H:i', + + 'short_text' => '{text}…', +]; diff --git a/resources/lang/en-GB/journal.php b/resources/lang/en-GB/journal.php new file mode 100644 index 0000000..9b1f0be --- /dev/null +++ b/resources/lang/en-GB/journal.php @@ -0,0 +1,38 @@ + 'How was your day? You can rate it once a day.', + 'journal_come_back' => 'Thanks. Come back tomorrow to rate your day again.', + 'journal_description' => 'Note: the journal lists both manual journal entries, and automatic entries like Activities done with your contacts. While you can delete journal entries manually, you’ll have to delete the activity directly on the contact page.', + 'journal_add' => 'Add a journal entry', + 'journal_edit' => 'Edit a journal entry', + 'journal_empty' => 'Empty journal', + 'journal_created_at' => 'Created at {date}', + 'journal_created_automatically' => 'Created automatically', + 'journal_entry_type_journal' => 'Journal entry', + 'journal_entry_type_activity' => 'Activity', + 'journal_entry_rate' => 'You rated your day.', + 'journal_add_comment' => 'Care to add a comment (optional)?', + 'journal_show_comment' => 'Show comment', + 'entry_delete_success' => 'The journal entry has been successfully deleted.', + 'journal_add_title' => 'Title (optional)', + 'journal_add_date' => 'Date', + 'journal_add_post' => 'Entry', + 'journal_add_cta' => 'Save', + 'journal_blank_cta' => 'Add your first journal entry', + 'journal_blank_description' => 'The journal lets you write events that happened to you, and remember them.', + 'delete_confirmation' => 'Are you sure you want to delete this journal entry?', + 'apply_filter' => 'Apply filter', + 'start_date' => 'Start Date', + 'end_date' => 'End Date', + 'per_page' => 'Per Page', + 'sort_order' => 'Sort By Created At', + 'ascending' => 'Ascending', + 'descending' => 'Descending', +]; diff --git a/resources/lang/en-GB/logs.php b/resources/lang/en-GB/logs.php new file mode 100644 index 0000000..7b6654b --- /dev/null +++ b/resources/lang/en-GB/logs.php @@ -0,0 +1,29 @@ + 'Created the contact.', + 'settings_log_contact_created_with_name' => 'Added :name as a contact.', + + // contat description update + 'contact_log_contact_description_updated' => 'Updated the description.', + 'settings_log_contact_description_updated_with_name' => 'Updated the description of :name.', + + // contact description clear + 'contact_log_contact_description_cleared' => 'Cleared the description.', + 'settings_log_contact_description_cleared_with_name' => 'Cleared the description of :name.', + + // contact work information update + 'contact_log_contact_work_updated' => 'Updated work information.', + 'settings_log_contact_work_updated_with_name' => 'Updated work information of :name.', + + // company created + 'settings_log_company_created' => 'Created a company called :name.', +]; diff --git a/resources/lang/en-GB/mail.php b/resources/lang/en-GB/mail.php new file mode 100644 index 0000000..749f3d1 --- /dev/null +++ b/resources/lang/en-GB/mail.php @@ -0,0 +1,53 @@ + 'Reminder for :contact', + 'greetings' => 'Hi :username', + 'want_reminded_of' => 'You wanted to be reminded of :reason', + 'for' => 'For: :name', + 'comment' => 'Comment: :comment', + 'footer_contact_info' => 'Add, view, complete, and change information about this contact:', + 'footer_contact_info2' => 'See :name’s profile', + 'footer_contact_info2_link' => 'See :name’s profile: :url', + + 'notification_subject_line' => 'You have an upcoming event', + 'notification_description' => 'In :count days (on :date), the following event will happen:', + + 'stay_in_touch_subject_line' => 'Stay in touch with :name', + 'stay_in_touch_subject_description' => 'You asked to be reminded to stay in touch with :name every :frequency day.|You asked to be reminded to stay in touch with :name every :frequency days.', + + 'notifications_whoops' => 'Whoops!', + 'notifications_hello' => 'Hello!', + 'notifications_regards' => 'Regards', + 'notifications_footer' => 'If you’re having trouble clicking the ":actionText" button, copy and paste the URL below into your web browser: [:actionURL](:actionURL)', + 'notifications_rights' => 'All rights reserved', + + 'confirmation_email_title' => 'Monica – Email verification', + 'confirmation_email_intro'=> 'To validate your email click on the button below', + 'confirmation_email_button' => 'Verify email address', + 'confirmation_email_bottom' => 'If you did not create an account, no further action is required.', + + 'password_reset_title' => 'Monica – Reset Password Notification', + 'password_reset_intro' => 'You are receiving this email because we received a password reset request for your account.', + 'password_reset_button' => 'Reset Password', + 'password_reset_expiration' => 'This password reset link will expire in :count minutes.', + 'password_reset_bottom' => 'If you did not request a password reset, no further action is required.', + + 'invitation_title' => 'Monica – You are invited by :name', + 'invitation_intro' => 'You’ve been invited by :name (:email) to use Monica, a nice Personal Relationship Management tool.', + 'invitation_link' => 'To accept the invitation, click on the link below:', + 'invitation_button' => 'Accept invitation', + 'invitation_expiration' => 'This link will expire in :count days.', + + 'export_title' => 'Your export is ready', + 'export_description' => 'You requested a data export on :date. It is now ready to download.', + 'export_download' => 'Download export', + +]; diff --git a/resources/lang/en-GB/pagination.php b/resources/lang/en-GB/pagination.php new file mode 100644 index 0000000..d663041 --- /dev/null +++ b/resources/lang/en-GB/pagination.php @@ -0,0 +1,25 @@ + '❮ Previous', + 'next' => 'Next ❯', + +]; diff --git a/resources/lang/en-GB/passwords.php b/resources/lang/en-GB/passwords.php new file mode 100644 index 0000000..1487bb9 --- /dev/null +++ b/resources/lang/en-GB/passwords.php @@ -0,0 +1,30 @@ + 'Your password has been reset!', + 'sent' => 'If the email you entered exists in our records, you’ve been sent a password reset link.', + 'token' => 'This password reset token is invalid.', + 'user' => 'If the email you entered exists in our records, you’ve been sent a password reset link.', + 'changed' => 'Password changed successfully.', + 'invalid' => 'Current password you entered is not correct.', + 'throttled' => 'Please wait before retrying.', + +]; diff --git a/resources/lang/en-GB/people.php b/resources/lang/en-GB/people.php new file mode 100644 index 0000000..b3c51ca --- /dev/null +++ b/resources/lang/en-GB/people.php @@ -0,0 +1,539 @@ + 'Contact not found', + 'people_list_number_kids' => ':count child|:count children', + 'people_list_last_updated' => 'Last consulted:', + 'people_list_number_reminders' => ':count reminder|:count reminders', + 'people_list_blank_title' => 'You don’t have anyone in your account yet', + 'people_list_blank_cta' => 'Add someone', + 'people_list_sort' => 'Sort', + 'people_list_stats' => ':count contact|:count contacts', + 'people_list_firstnameAZ' => 'Sort by first name A → Z', + 'people_list_firstnameZA' => 'Sort by first name Z → A', + 'people_list_lastnameAZ' => 'Sort by last name A → Z', + 'people_list_lastnameZA' => 'Sort by last name Z → A', + 'people_list_lastactivitydateNewtoOld' => 'Sort by last activity date, newest to oldest', + 'people_list_lastactivitydateOldtoNew' => 'Sort by last activity date, oldest to newest', + 'people_list_filter_tag' => 'Showing all the contacts tagged with', + 'people_list_clear_filter' => 'Clear filter', + 'people_list_contacts_per_tags' => ':count contact|:count contacts', + 'people_list_show_dead' => 'Show deceased people (:count)', + 'people_list_hide_dead' => 'Hide deceased people (:count)', + 'people_search' => 'Search your contacts…', + 'people_search_no_results' => 'No results found', + 'people_search_next' => 'Next', + 'people_search_prev' => 'Previous', + 'people_search_rows_per_page' => 'Rows per page', + 'people_search_of' => 'of', + 'people_search_page' => 'Page', + 'people_search_all' => 'All', + 'people_add_new' => 'Add new person', + 'people_list_account_usage' => 'Your account usage: :current/:limit contacts', + 'people_list_account_upgrade_title' => 'Upgrade your account to unlock it to its full potential.', + 'people_list_account_upgrade_cta' => 'Upgrade now', + 'people_list_untagged' => 'View untagged contacts', + 'people_list_filter_untag' => 'Showing all untagged contacts', + 'archived_contact_readonly' => 'Archived contact can’t be edited, please unarchive it first.', + + // people add + 'people_add_title' => 'Add a new person', + 'people_add_missing' => 'No person found – add a new one now', + 'people_add_firstname' => 'First name', + 'people_add_middlename' => 'Middle name (optional)', + 'people_add_lastname' => 'Last name (optional)', + 'people_add_email' => 'Email (optional)', + 'people_add_nickname' => 'Nickname (optional)', + 'people_add_cta' => 'Add', + 'people_save_and_add_another_cta' => 'Submit and add someone else', + 'people_add_success' => ':name has been successfully created', + 'people_add_gender' => 'Gender', + 'people_delete_success' => 'The contact has been deleted', + 'people_delete_message' => 'Delete contact', + 'people_delete_confirmation' => 'Are you sure you want to delete :name’s contact? Deletion is immediate and permanent.', + 'people_add_birthday_reminder' => 'Wish happy birthday to :name', + 'people_add_birthday_reminder_deceased' => 'On this date, :name would have celebrated their birthday', + 'people_add_import' => 'Do you want to import your contacts?', + 'people_edit_email_error' => 'There is already a contact in your account with this email address. Please choose another one.', + 'people_export' => 'Export as vCard', + 'people_add_reminder_for_birthday' => 'Create an annual birthday reminder', + + // show + 'section_contact_information' => 'Contact information', + 'section_personal_activities' => 'Activities', + 'section_personal_reminders' => 'Reminders', + 'section_personal_tasks' => 'Tasks', + 'section_personal_gifts' => 'Gifts', + 'section_personal_notes' => 'Notes', + + // archived contacts + 'list_link_to_active_contacts' => 'You are viewing archived contacts. See the list of active contacts instead.', + 'list_link_to_archived_contacts' => 'List of archived contacts', + + // Header + 'me' => 'This is you', + 'edit_contact_information' => 'Edit contact information', + 'contact_archive' => 'Archive contact', + 'contact_unarchive' => 'Unarchive contact', + 'contact_archive_help' => 'Archived contacts are not be shown on the contact list, but still appear in search results.', + 'call_button' => 'Log a call', + 'set_favorite' => 'Favourite contacts are placed at the top of the contact list', + + // Stay in touch + 'stay_in_touch' => 'Stay in touch', + 'stay_in_touch_frequency' => 'Stay in touch every day|Stay in touch every {count} days', + 'stay_in_touch_next_date' => 'Next due: {date}', + 'stay_in_touch_invalid' => 'The frequency must be a number greater than 0.', + 'stay_in_touch_premium' => 'You need to upgrade your account to make use of this feature', + 'stay_in_touch_modal_title' => 'Stay in touch', + 'stay_in_touch_modal_desc' => 'We can remind you by email to keep in touch with {firstname} at a regular interval.', + 'stay_in_touch_modal_label' => 'Send me an email every… {count} day|Send me an email every… {count} days', + + // Calls + 'modal_call_title' => 'Log a call', + 'modal_call_comment' => 'What did you talk about? (optional)', + 'modal_call_exact_date' => 'The phone call happened on', + 'modal_call_who_called' => 'Who called?', + 'modal_call_emotion' => 'Do you want to log how you felt during this call? (optional)', + 'calls_add_success' => 'The phone call has been saved.', + 'call_delete_confirmation' => 'Are you sure you want to delete this call?', + 'call_delete_success' => 'The call has been deleted successfully', + 'call_title' => 'Phone calls', + 'call_empty_comment' => 'No details', + 'call_blank_title' => 'Keep track of the phone calls you’ve done with {name}', + 'call_blank_desc' => 'You called {name}', + 'call_you_called' => 'You called', + 'call_he_called' => '{name} called', + 'call_emotions' => 'Emotions:', + + // Conversation + 'conversation_blank' => 'Record conversations you have with :name on social media, SMS…', + 'conversation_delete_link' => 'Delete the conversation', + 'conversation_edit_title' => 'Edit conversation', + 'conversation_edit_delete' => 'Are you sure you want to delete this conversation? Deletion is permanent.', + 'conversation_add_success' => 'The conversation has been successfully added.', + 'conversation_edit_success' => 'The conversation has been successfully updated.', + 'conversation_delete_success' => 'The conversation has been successfully deleted.', + 'conversation_add_title' => 'Record a new conversation', + 'conversation_add_when' => 'When did you have this conversation?', + 'conversation_add_who_wrote' => 'Who sent this message?', + 'conversation_add_how' => 'How did you communicate?', + 'conversation_add_you' => 'You', + 'conversation_add_content' => 'Write down what was said', + 'conversation_add_what_was_said' => 'What did you say?', + 'conversation_add_another' => 'Add another message', + 'conversation_add_error' => 'You must add at least one message.', + 'conversation_list_table_messages' => 'Messages', + 'conversation_list_table_content' => 'Partial content (last message)', + 'conversation_list_title' => 'Conversations', + 'conversation_list_cta' => 'Log conversation', + + // age - birthday + 'birthdate_not_set' => 'Birthday is not set', + 'age_approximate_in_years' => 'around :age years old', + 'age_exact_in_years' => ':age years old', + 'age_exact_birthdate' => 'born :date', + + // Last called + 'last_called' => 'Last called: :date', + 'last_talked_to' => 'Last called: {date}', + 'last_called_empty' => 'Last called: unknown', + 'last_activity_date' => 'Last activity together: :date', + 'last_activity_date_empty' => 'Last activity together: unknown', + + // additional information + 'information_edit_success' => 'The profile has been updated successfully', + 'information_edit_title' => 'Edit :name’s personal information', + 'information_edit_max_size' => 'Max :size Kb.', + 'information_edit_max_size2' => 'Max {size} Kb.', + 'information_edit_firstname' => 'First name', + 'information_edit_lastname' => 'Last name (optional)', + 'information_edit_description' => 'Description (optional)', + 'information_edit_description_help' => 'Used on the contact list to add some context, if necessary.', + 'information_edit_unknown' => 'I do not know this person’s age', + 'information_edit_probably' => 'This person is probably…', + 'information_edit_not_year' => 'I know the day and month of this person’s birthday, but not the year…', + 'information_edit_exact' => 'I know this person’s exact birthday…', + 'information_edit_birthdate_label' => 'Birthday', + 'information_no_work_defined' => 'No work information defined', + 'information_work_at' => 'at :company', + 'work_add_cta' => 'Update work information', + 'work_edit_success' => 'Work information updated', + 'work_edit_title' => 'Update :name’s job information', + 'work_edit_job' => 'Job title (optional)', + 'work_edit_company' => 'Company (optional)', + 'work_information' => 'Work information', + + // food preferences + 'food_preferences_add_success' => 'Food preferences have been saved', + 'food_preferences_edit_description' => 'Perhaps :firstname or someone in the :family’s family has an allergy. Or doesn’t like a specific bottle of wine. Indicate them here so you will remember it next time you invite them for dinner', + 'food_preferences_edit_description_no_last_name' => 'Perhaps :firstname has an allergy. Or doesn’t like a specific bottle of wine. Indicate them here so you will remember it next time you invite them for dinner', + 'food_preferences_edit_title' => 'Indicate food preferences', + 'food_preferences_edit_cta' => 'Save food preferences', + 'food_preferences_title' => 'Food preferences', + 'food_preferences_cta' => 'Add food preferences', + + // reminders + 'reminders_blank_title' => 'Is there something you want to be reminded of about :name?', + 'reminders_blank_add_activity' => 'Add a reminder', + 'reminders_add_title' => 'What would you like to be reminded of about :name?', + 'reminders_add_description' => 'Please remind me to…', + 'reminders_add_next_time' => 'When is the next time you would like to be reminded about this?', + 'reminders_add_once' => 'Remind me about this just once', + 'reminders_add_recurrent' => 'Remind me about this every', + 'reminders_add_starting_from' => 'starting from the date specified above', + 'reminders_add_cta' => 'Add reminder', + 'reminders_edit_update_cta' => 'Update reminder', + 'reminders_add_error_custom_text' => 'You need to indicate a text for this reminder', + 'reminders_create_success' => 'The reminder has been added successfully', + 'reminders_delete_success' => 'The reminder has been deleted successfully', + 'reminders_update_success' => 'The reminder has been updated successfully', + 'reminders_add_optional_comment' => 'Optional comment', + + 'reminder_frequency_day' => 'every day|every :number days', + 'reminder_frequency_week' => 'every week|every :number weeks', + 'reminder_frequency_month' => 'every month|every :number months', + 'reminder_frequency_year' => 'every year|every :number year', + 'reminder_frequency_one_time' => 'on :date', + 'reminders_delete_confirmation' => 'Are you sure you want to delete this reminder?', + 'reminders_delete_cta' => 'Delete', + 'reminders_next_expected_date' => 'on', + 'reminders_cta' => 'Add a reminder', + 'reminders_description' => 'We will send an email for each one of the reminders below. Reminders are sent every morning the day events will happen. Reminders automatically added for birthdays can not be deleted. If you want to change those dates, edit the birthday of the contacts.', + 'reminders_one_time' => 'One time', + 'reminders_type_week' => 'week', + 'reminders_type_month' => 'month', + 'reminders_type_year' => 'year', + 'reminders_birthday' => 'Birthday of :name', + 'reminders_free_plan_warning' => 'You are on the Free plan. No emails are sent on this plan. To receive your reminders by email, upgrade your account.', + + // relationships + 'relationship_form_add' => 'Add a new relationship', + 'relationship_form_edit' => 'Edit an existing relationship', + 'relationship_form_is_with' => 'This person is…', + 'relationship_form_is_with_name' => ':name is…', + 'relationship_form_add_choice' => 'Who is the relationship with?', + 'relationship_form_create_contact' => 'Add a new person', + 'relationship_form_associate_contact' => 'An existing contact', + 'relationship_form_associate_dropdown' => 'Search and select an existing contact from the dropdown below', + 'relationship_form_associate_dropdown_placeholder' => 'Search and select an existing contact', + 'relationship_form_also_create_contact' => 'Create a Contact entry for this person.', + 'relationship_form_add_description' => 'This will let you treat this person like any other contact.', + 'relationship_form_add_no_existing_contact' => 'You don’t have any contacts who can be related to :name at the moment.', + 'relationship_delete_confirmation' => 'Are you sure you want to delete this relationship? Deletion is permanent.', + 'relationship_unlink_confirmation' => 'Are you sure you want to delete this relationship? This person will not be deleted – only the relationship between the two.', + 'relationship_form_add_success' => 'The relationship has been successfully set.', + 'relationship_form_deletion_success' => 'The relationship has been deleted.', + + // tasks + 'tasks_title' => 'Tasks', + 'tasks_blank_title' => 'You don’t have any tasks yet.', + 'tasks_form_title' => 'Title', + 'tasks_form_description' => 'Description (optional)', + 'tasks_add_task' => 'Add a task', + 'tasks_delete_success' => 'The task has been deleted successfully', + 'tasks_complete_success' => 'The task has changed status successfully', + + // activities + 'activity_title' => 'Activities', + 'activity_type_category_simple_activities' => 'Simple activities', + 'activity_type_category_sport' => 'Sport', + 'activity_type_category_food' => 'Food', + 'activity_type_category_cultural_activities' => 'Cultural activities', + 'activity_type_just_hung_out' => 'just hung out', + 'activity_type_watched_movie_at_home' => 'watched a movie at home', + 'activity_type_talked_at_home' => 'just talked at home', + 'activity_type_did_sport_activities_together' => 'played a sport together', + 'activity_type_ate_at_his_place' => 'ate at their place', + 'activity_type_went_bar' => 'went to a bar', + 'activity_type_ate_at_home' => 'ate at home', + 'activity_type_picnicked' => 'picnicked', + 'activity_type_ate_restaurant' => 'ate at a restaurant', + 'activity_type_went_theater' => 'went to the theater', + 'activity_type_went_concert' => 'went to a concert', + 'activity_type_went_play' => 'went to a play', + 'activity_type_went_museum' => 'went to the museum', + 'activities_add_activity' => 'Add activity', + 'activities_add_more_details' => 'Add more details', + 'activities_add_emotions' => 'Add emotions', + 'activities_add_category' => 'Indicate a category', + 'activities_add_participants_cta' => 'Add participants', + 'activities_item_information' => ':Activity. Happened on :date', + 'activities_add_title' => 'What did you do with {name}?', + 'activities_summary' => 'Describe what you did', + 'activities_add_pick_activity' => 'Would you like to categorise this activity? You don’t have to, but it will give you statistics later on (optional)', + 'activities_add_date_occured' => 'The activity happened on…', + 'activities_add_participants' => 'Who, apart from {name}, participated in this activity? (optional)', + 'activities_add_emotions_title' => 'Do you want to log how you felt during this activity? (optional)', + 'activities_blank_title' => 'Keep track of what you’ve done with {name} in the past, and what you’ve talked about', + 'activities_blank_add_activity' => 'Add an activity', + 'activities_add_success' => 'The activity has been added successfully', + 'activities_add_error' => 'Error when adding the activity', + 'activities_update_success' => 'The activity has been updated successfully', + 'activities_delete_success' => 'The activity has been deleted successfully', + 'activities_who_was_involved' => 'Who was involved?', + 'activities_activity' => 'Activity Category', + 'activities_view_activities_report' => 'View activities report', + 'activities_profile_title' => 'Activities report between :name and you', + 'activities_profile_subtitle' => 'You’ve logged :total_activities activity with :name in total and :activities_last_twelve_months in the last 12 months so far.|You’ve logged :total_activities activities with :name in total and :activities_last_twelve_months in the last 12 months so far.', + 'activities_profile_year_summary_activity_types' => 'Here is a breakdown of the type of activities you’ve done together in :year', + 'activities_profile_year_summary' => 'Here is what you two have done in :year', + 'activities_profile_number_occurences' => ':value activity|:value activities', + 'activities_list_participants' => 'Participants ({total}):', + 'activities_list_emotions' => 'Emotions felt:', + 'activities_list_date' => 'Happened on', + 'activities_list_category' => 'Category:', + + // notes + 'notes_create_success' => 'The note has been created successfully', + 'notes_update_success' => 'The note has been saved successfully', + 'notes_delete_success' => 'The note has been deleted successfully', + 'notes_add_cta' => 'Add note', + 'notes_favorite' => 'Add/remove from favourites', + 'notes_delete_title' => 'Delete a note', + 'notes_delete_confirmation' => 'Are you sure you want to delete this note? Deletion is permanent', + + // gifts + 'gifts_title' => 'Gifts', + 'gifts_add_success' => 'The gift has been added successfully', + 'gifts_delete_success' => 'The gift has been deleted successfully', + 'gifts_delete_confirmation' => 'Are you sure you want to delete this gift?', + 'gifts_add_gift' => 'Add a gift', + 'gifts_link' => 'Link', + 'gifts_for' => 'For: {name}', + 'gifts_delete_cta' => 'Delete', + 'gifts_add_title' => 'Gift management for :name', + 'gifts_add_gift_idea' => 'Gift idea', + 'gifts_add_gift_already_offered' => 'Gift given', + 'gifts_add_gift_received' => 'Gift received', + 'gifts_add_gift_title' => 'What is this gift?', + 'gifts_add_gift_name' => 'Gift name', + 'gifts_add_link' => 'Link to the web page (optional)', + 'gifts_add_value' => 'Value (optional)', + 'gifts_add_comment' => 'Comment (optional)', + 'gifts_add_recipient' => 'Recipient (optional)', + 'gifts_add_recipient_field' => 'Recipient', + 'gifts_add_photo' => 'Photo (optional)', + 'gifts_add_photo_title' => 'Add a photo for this gift', + 'gifts_add_someone' => 'This gift is for someone in {name}’s family in particular', + 'gifts_delete_title' => 'Delete a gift', + 'gifts_ideas' => 'Gift ideas', + 'gifts_offered' => 'Gifts given', + 'gifts_offered_as_an_idea' => 'Mark as an idea', + 'gifts_received' => 'Gifts received', + 'gifts_view_comment' => 'View comment', + 'gifts_mark_offered' => 'Mark as given', + 'gifts_update_success' => 'The gift has been updated successfully', + 'gifts_add_date' => 'Date (optional)', + + // debts + 'debt_delete_confirmation' => 'Are you sure you want to delete this debt?', + 'debt_delete_success' => 'The debt has been deleted successfully', + 'debt_add_success' => 'The debt has been added successfully', + 'debt_title' => 'Debts', + 'debt_add_cta' => 'Add debt', + 'debt_you_owe' => 'You owe :amount', + 'debt_they_owe' => ':name owes you :amount', + 'debt_add_title' => 'Debt management', + 'debt_add_you_owe' => 'You owe :name', + 'debt_add_they_owe' => ':name owes you', + 'debt_add_amount' => 'the sum of', + 'debt_add_reason' => 'for the following reason (optional)', + 'debt_add_add_cta' => 'Add debt', + 'debt_edit_update_cta' => 'Update debt', + 'debt_edit_success' => 'The debt has been updated successfully', + 'debts_blank_title' => 'Manage debts you owe to :name or :name owes you', + + // tags + 'tag_edit' => 'Edit tag', + 'tag_add' => 'Add tags', + 'tag_add_search' => 'Add or search tags', + 'tag_no_tags' => 'No tags yet', + + // Introductions + 'introductions_sidebar_title' => 'How you met', + 'introductions_blank_cta' => 'Indicate how you met :name', + 'introductions_title_edit' => 'How did you meet :name?', + 'introductions_additional_info' => 'Explain how and where you met', + 'introductions_edit_met_through' => 'Has someone introduced you to this person?', + 'introductions_no_met_through' => 'No one', + 'introductions_first_met_date' => 'Date you met', + 'introductions_no_first_met_date' => 'I don’t know the date we met', + 'introductions_first_met_date_known' => 'This is the date we met', + 'introductions_add_reminder' => 'Add a reminder to celebrate this encounter on the anniversary this event happened', + 'introductions_update_success' => 'You’ve successfully updated the information about how you met this person', + 'introductions_met_through' => 'Met through :name', + 'introductions_met_date' => 'Met on :date', + 'introductions_reminder_title' => 'Anniversary of the day you first met', + + // Deceased + 'deceased_reminder_title' => 'Anniversary of the death of :name', + 'deceased_mark_person_deceased' => 'Mark this as deceased', + 'deceased_know_date' => 'I know the date that this person died', + 'deceased_add_reminder' => 'Add a reminder for this date', + 'deceased_label' => 'Deceased', + 'deceased_date_label' => 'Deceased date', + 'deceased_label_with_date' => 'Deceased on :date', + 'deceased_age' => 'Age at death', + + // Contact information + 'contact_info_title' => 'Contact information', + 'contact_info_form_content' => 'Content', + 'contact_info_form_contact_type' => 'Contact type', + 'contact_info_form_personalize' => 'Personalize', + 'contact_info_address' => 'Lives in', + + // Addresses + 'contact_address_title' => 'Addresses', + 'contact_address_form_name' => 'Label (optional)', + 'contact_address_form_street' => 'Street (optional)', + 'contact_address_form_city' => 'City (optional)', + 'contact_address_form_province' => 'Province (optional)', + 'contact_address_form_postal_code' => 'Postal code (optional)', + 'contact_address_form_country' => 'Country (optional)', + 'contact_address_form_latitude' => 'Latitude (numbers only) (optional)', + 'contact_address_form_longitude' => 'Longitude (numbers only) (optional)', + + // Pets + 'pets_kind' => 'Kind of pet', + 'pets_name' => 'Name (optional)', + 'pets_create_success' => 'The pet has been successfully added', + 'pets_update_success' => 'The pet has been updated', + 'pets_delete_success' => 'The pet has been deleted', + 'pets_title' => 'Pets', + 'pets_reptile' => 'Reptile', + 'pets_bird' => 'Bird', + 'pets_cat' => 'Cat', + 'pets_dog' => 'Dog', + 'pets_fish' => 'Fish', + 'pets_hamster' => 'Hamster', + 'pets_horse' => 'Horse', + 'pets_rabbit' => 'Rabbit', + 'pets_rat' => 'Rat', + 'pets_small_animal' => 'Small animal', + 'pets_other' => 'Other', + + // life events + 'life_event_list_tab_life_events' => 'Life events', + 'life_event_list_tab_other' => 'Notes, reminders, …', + 'life_event_list_title' => 'Life events', + 'life_event_blank' => 'Log what happens to the life of {name} for your future reference.', + 'life_event_list_cta' => 'Add life event', + 'life_event_create_category' => 'All categories', + 'life_event_create_life_event' => 'Add life event', + 'life_event_create_default_title' => 'Title (optional)', + 'life_event_create_default_story' => 'Story (optional)', + 'life_event_create_date' => 'You do not need to indicate a month or a day – only the year is mandatory.', + 'life_event_create_default_description' => 'Add information about what you know', + 'life_event_create_add_yearly_reminder' => 'Add a yearly reminder for this event', + 'life_event_create_success' => 'The life event has been added', + 'life_event_delete_title' => 'Delete a life event', + 'life_event_delete_description' => 'Are you sure you want to delete this life event? Deletion is permanent.', + 'life_event_delete_success' => 'The life event has been deleted', + 'life_event_date_it_happened' => 'Date it happened', + 'life_event_category_work_education' => 'Work & education', + 'life_event_category_family_relationships' => 'Family & relationships', + 'life_event_category_home_living' => 'Home & living', + 'life_event_category_health_wellness' => 'Health & wellness', + 'life_event_category_travel_experiences' => 'Travel & experiences', + 'life_event_sentence_new_job' => 'Started a new job', + 'life_event_sentence_retirement' => 'Retired', + 'life_event_sentence_new_school' => 'Started school', + 'life_event_sentence_study_abroad' => 'Studied abroad', + 'life_event_sentence_volunteer_work' => 'Started volunteering', + 'life_event_sentence_published_book_or_paper' => 'Published a paper', + 'life_event_sentence_military_service' => 'Started military service', + 'life_event_sentence_new_relationship' => 'Started a relationship', + 'life_event_sentence_engagement' => 'Got engaged', + 'life_event_sentence_marriage' => 'Got married', + 'life_event_sentence_anniversary' => 'Anniversary', + 'life_event_sentence_expecting_a_baby' => 'Expects a baby', + 'life_event_sentence_new_child' => 'Had a child', + 'life_event_sentence_new_family_member' => 'Added a family member', + 'life_event_sentence_new_pet' => 'Got a pet', + 'life_event_sentence_end_of_relationship' => 'Ended a relationship', + 'life_event_sentence_loss_of_a_loved_one' => 'Lost a loved one', + 'life_event_sentence_moved' => 'Moved', + 'life_event_sentence_bought_a_home' => 'Bought a home', + 'life_event_sentence_home_improvement' => 'Made a home improvement', + 'life_event_sentence_holidays' => 'Went on holidays', + 'life_event_sentence_new_vehicle' => 'Got a new vehicle', + 'life_event_sentence_new_roommate' => 'Got a roommate', + 'life_event_sentence_overcame_an_illness' => 'Overcame an illness', + 'life_event_sentence_quit_a_habit' => 'Quit a habit', + 'life_event_sentence_new_eating_habits' => 'Started new eating habits', + 'life_event_sentence_weight_loss' => 'Lost weight', + 'life_event_sentence_wear_glass_or_contact' => 'Started to wear glass or contact lenses', + 'life_event_sentence_broken_bone' => 'Broke a bone', + 'life_event_sentence_removed_braces' => 'Removed braces', + 'life_event_sentence_surgery' => 'Had surgery', + 'life_event_sentence_dentist' => 'Went to the dentist', + 'life_event_sentence_new_sport' => 'Started a sport', + 'life_event_sentence_new_hobby' => 'Started a hobby', + 'life_event_sentence_new_instrument' => 'Learned a new instrument', + 'life_event_sentence_new_language' => 'Learned a new language', + 'life_event_sentence_tattoo_or_piercing' => 'Got a tattoo or piercing', + 'life_event_sentence_new_license' => 'Got a license', + 'life_event_sentence_travel' => 'Traveled', + 'life_event_sentence_achievement_or_award' => 'Got an achievement or award', + 'life_event_sentence_changed_beliefs' => 'Changed beliefs', + 'life_event_sentence_first_word' => 'Spoke for the first time', + 'life_event_sentence_first_kiss' => 'Kissed for the first time', + + // documents + 'document_list_title' => 'Documents', + 'document_list_cta' => 'Upload document', + 'document_list_blank_desc' => 'Here you can store documents related to this person.', + 'document_upload_zone_cta' => 'Upload a file', + 'document_upload_zone_progress' => 'Uploading the document…', + 'document_upload_zone_error' => 'There was an error uploading the document. Please try again below.', + + // Photos + 'photo_title' => 'Photos', + 'photo_list_title' => 'Related photos', + 'photo_list_cta' => 'Upload photo', + 'photo_list_blank_desc' => 'You can store images about this contact. Upload one now!', + 'photo_upload_zone_cta' => 'Upload a photo', + 'photo_current_profile_pic' => 'Current profile picture', + 'photo_make_profile_pic' => 'Make profile picture', + 'photo_delete' => 'Delete photo', + 'photo_next' => 'Next photo ❯', + 'photo_previous' => '❮ Previous photo', + + // Avatars + 'avatar_change_title' => 'Change your avatar', + 'avatar_question' => 'Which avatar would you like to use?', + 'avatar_default_avatar' => 'The default avatar', + 'avatar_adorable_avatar' => 'The Adorable avatar', + 'avatar_gravatar' => 'The Gravatar associated with the email address of this person. Gravatar is a global system that lets users associate email addresses with photos.', + 'avatar_current' => 'Keep the current avatar', + 'avatar_photo' => 'From a photo that you upload', + 'avatar_crop_new_avatar_photo' => 'Crop new avatar photo', + + // emotions + 'emotion_this_made_me_feel' => 'This made you feel…', + + // logs + 'auditlogs_link' => 'History', + 'auditlogs_title' => 'Everything that happened to :name', + 'auditlogs_breadcrumb' => 'History', + 'auditlogs_author' => 'By :name on :date', + + // contact field label + 'contact_field_label_home' => 'Home', + 'contact_field_label_work' => 'Work', + 'contact_field_label_cell' => 'Mobile', + 'contact_field_label_fax' => 'Fax', + 'contact_field_label_pager' => 'Pager', + 'contact_field_label_main' => 'Main', + 'contact_field_label_other' => 'Other', + 'contact_field_label_personal' => 'Personal', +]; diff --git a/resources/lang/en-GB/reminder.php b/resources/lang/en-GB/reminder.php new file mode 100644 index 0000000..bcab17c --- /dev/null +++ b/resources/lang/en-GB/reminder.php @@ -0,0 +1,16 @@ + 'Wish happy birthday to', + 'type_phone_call' => 'Call', + 'type_lunch' => 'Lunch with', + 'type_hangout' => 'Hangout with', + 'type_email' => 'Email', + 'type_birthday_kid' => 'Wish happy birthday to the child of', +]; diff --git a/resources/lang/en-GB/settings.php b/resources/lang/en-GB/settings.php new file mode 100644 index 0000000..a6e647b --- /dev/null +++ b/resources/lang/en-GB/settings.php @@ -0,0 +1,557 @@ + 'Account settings', + 'sidebar_personalization' => 'Personalisation', + 'sidebar_settings_storage' => 'Storage', + 'sidebar_settings_export' => 'Export data', + 'sidebar_settings_users' => 'Users', + 'sidebar_settings_subscriptions' => 'Subscription', + 'sidebar_settings_import' => 'Import data', + 'sidebar_settings_tags' => 'Tag management', + 'sidebar_settings_api' => 'API', + 'sidebar_settings_dav' => 'DAV Resources', + 'sidebar_settings_security' => 'Security', + 'sidebar_settings_auditlogs' => 'Audit log', + + 'title_general' => 'General Information', + 'title_i18n' => 'International settings', + 'title_layout' => 'Layout', + + 'me_title' => 'Me as a contact', + 'me_help' => 'This is the contact that represents you in Monica', + 'me_select' => 'Select a contact', + 'me_no_contact' => 'No contact selected yet.', + 'me_select_click' => 'Click here to select a contact.', + 'me_remove_contact' => 'Remove the association', + 'me_choose' => 'Choose yourself', + 'me_choose_placeholder' => 'Choose yourself', + + 'export_title' => 'Export your account data', + 'export_be_patient' => 'Click the button to start the export. It might take several minutes to process the export – please be patient and do not spam the button.', + 'export_title_sql' => 'Export to SQL', + 'export_sql_explanation' => 'Exporting your data in SQL format allows you to take your data and import it to your own Monica instance. This is only valuable if you do have your own server.', + 'export_sql_cta' => 'Export to SQL', + 'export_sql_link_instructions' => 'Note: read the instructions to learn more about importing this file to your instance.', + 'export_title_json' => 'Export to Json', + 'export_submitted' => 'Your export has been submitted, it will be available in a few moment…', + 'export_json_explanation' => 'Exporting your data in Json format for backup.', + 'export_json_beta' => 'Json export is in preview mode. Tell us what you think about it:', + 'export_json_cta' => 'Export to Json', + 'export_header_type' => 'Type', + 'export_header_timestamp' => 'Creation date', + 'export_header_status' => 'Status', + 'export_header_actions' => 'Actions', + 'export_last_title' => 'Last exports', + 'export_empty_title' => 'No exports yet', + 'export_type_json' => 'Json export', + 'export_type_sql' => 'SQL export', + 'export_status_todo' => 'Submitted', + 'export_status_doing' => 'Doing', + 'export_status_done' => 'Done', + 'export_status_failed' => 'Failed', + 'export_not_done' => 'Download impossible, this export is not done yet.', + + 'firstname' => 'First name', + 'lastname' => 'Last name', + 'name_order' => 'Name order', + 'name_order_firstname_lastname' => ' – John Doe', + 'name_order_lastname_firstname' => ' – Doe John', + 'name_order_firstname_lastname_nickname' => ' () – John Doe (Rambo)', + 'name_order_firstname_nickname_lastname' => ' () – John (Rambo) Doe', + 'name_order_lastname_firstname_nickname' => ' () – Doe John (Rambo)', + 'name_order_lastname_nickname_firstname' => ' () – Doe (Rambo) John', + 'name_order_nickname_firstname_lastname' => ' ( ) – Rambo (John Doe)', + 'name_order_nickname_lastname_firstname' => ' ( ) – Rambo (Doe John)', + 'name_order_nickname' => ' – Rambo', + 'currency' => 'Currency', + 'name' => 'Your name: :name', + 'email' => 'Email address', + 'email_placeholder' => 'Enter email', + 'email_help' => 'This is the email used to login, and this is where Monica will send your reminders.', + 'timezone' => 'Timezone', + 'temperature_scale' => 'Temperature scale', + 'temperature_scale_fahrenheit' => 'Fahrenheit (°F)', + 'temperature_scale_celsius' => 'Celsius (°C)', + 'layout' => 'Layout', + 'layout_small' => 'Maximum 1200 pixels wide', + 'layout_big' => 'Full width of the browser', + 'save' => 'Update preferences', + 'delete_title' => 'Delete your account', + 'delete_desc' => 'Do you wish to delete your account? Deletion is permanent and all of your data will be erased permanently. If you have a subscription, it will be cancelled immediately.', + 'delete_other_desc' => 'Your data in the main database will be deleted immediately. As described in our privacy policy, we carry out securely encrypted backups of the database every day. These backups are kept for 30 days after which they are completely deleted. We cannot delete specific data from the backups we hold any earlier than this. All of your data will be completely deleted no later than 31 days after your account’s deletion.', + 'reset_desc' => 'Do you wish to reset your account? This will remove all your contacts, and all of the data associated with them. Your account will not be deleted.', + 'reset_title' => 'Reset your account', + 'reset_cta' => 'Reset account', + 'reset_notice' => 'Are you sure you want to reset your account? This removes all data permanently and it cannot be undone.', + 'reset_success' => 'Your account has been successfully reset.', + 'delete_notice' => 'Are you sure you want to delete your account? This is permanent and cannot be undone. All of your data will be deleted and will not be recoverable.', + 'delete_cta' => 'Delete account', + 'settings_success' => 'Preferences updated!', + 'locale' => 'Language used in the app', + 'locale_help' => 'Do you want to help translating Monica or add a new language? Please follow this link for more information.', + 'locale_ar' => 'Arabic', + 'locale_cs' => 'Czech', + 'locale_de' => 'German', + 'locale_el' => 'Greek', + 'locale_en' => 'English', + 'locale_en-GB' => 'English (United Kingdom)', + 'locale_es' => 'Spanish', + 'locale_fr' => 'French', + 'locale_he' => 'Hebrew', + 'locale_hr' => 'Croatian', + 'locale_id' => 'Indonesian', + 'locale_it' => 'Italian', + 'locale_ja' => 'Japanese', + 'locale_nl' => 'Dutch', + 'locale_pt' => 'Portuguese', + 'locale_pt-BR' => 'Brazilian Portuguese', + 'locale_ru' => 'Russian', + 'locale_sv' => 'Swedish', + 'locale_vi' => 'Vietnamese', + 'locale_zh' => 'Chinese Simplified', + 'locale_zh-TW' => 'Chinese Traditional', + 'locale_tr' => 'Turkish', + + 'security_title' => 'Security', + 'security_help' => 'Change security matters for your account.', + 'password_change' => 'Change your password', + 'password_current' => 'Current password', + 'password_current_placeholder' => 'Enter your current password', + 'password_new1' => 'New password', + 'password_new1_placeholder' => 'Enter your new password', + 'password_new2' => 'Confirm your new password', + 'password_new2_placeholder' => 'Retype your new password', + 'password_btn' => 'Change password', + '2fa_title' => 'Two Factor Authentication', + '2fa_otp_title' => 'Two Factor Authentication mobile application', + '2fa_enable_title' => 'Enable Two Factor Authentication', + '2fa_enable_description' => 'Enable Two Factor Authentication to increase the security of your account.', + '2fa_enable_otp' => 'Open up your Two Factor Authentication mobile app and scan the following QR barcode:', + '2fa_enable_otp_help' => 'If your Two Factor Authentication mobile app does not support QR barcodes, enter in the following code:', + '2fa_enable_otp_validate' => 'Please validate the new device you’ve just set up:', + '2fa_enable_success' => 'Two Factor Authentication activated', + '2fa_enable_error' => 'Error when trying to activate Two Factor Authentication', + '2fa_enable_error_already_set' => 'Two Factor Authentication is already activated', + '2fa_disable_title' => 'Disable Two Factor Authentication', + '2fa_disable_description' => 'Disable Two Factor Authentication for your account. Be careful, your account will be much less secure!', + '2fa_disable_success' => 'Two Factor Authentication disabled', + '2fa_disable_error' => 'Error when trying to disable Two Factor Authentication', + + 'webauthn_title' => 'Security key — WebAuthn protocol', + 'webauthn_enable_description' => 'Add a new security key', + 'webauthn_key_name_help' => 'Give your key a name.', + 'webauthn_key_name' => 'Key name:', + 'webauthn_success' => 'Your key is detected and validated.', + 'webauthn_last_use' => 'Last use: {timestamp}', + 'webauthn_delete_confirmation' => 'Are you sure you want to delete this key?', + 'webauthn_delete_success' => 'Key deleted', + 'webauthn_insertKey' => 'Insert your security key.', + 'webauthn_buttonAdvise' => 'If your security key has a button, press it.', + 'webauthn_noButtonAdvise' => 'If it does not, remove it and insert it again.', + 'webauthn_not_supported' => 'Your browser doesn’t currently support WebAuthn.', + 'webauthn_not_secured' => 'WebAuthn only supports secure connections. Please load this page with https scheme.', + 'webauthn_error_already_used' => 'This key is already registered. It’s not necessary to register it again.', + 'webauthn_error_not_allowed' => 'The operation either timed out or was not allowed.', + + 'recovery_title' => 'Recovery codes', + 'recovery_show' => 'Get recovery codes', + 'recovery_copy_help' => 'Copy codes in your clipboard', + 'recovery_help_intro' => 'These are your recovery codes:', + 'recovery_help_information' => 'You can use each recovery code once.', + 'recovery_clipboard' => 'Codes copied to the clipboard.', + 'recovery_generate' => 'Generate new codes…', + 'recovery_generate_help' => 'Generating new codes will invalidate previously generated codes.', + 'recovery_already_used_help' => 'This code has already been used.', + + 'users_list_title' => 'Users with access to your account', + 'users_list_add_user' => 'Invite a new user', + 'users_list_you' => 'That’s you', + 'users_list_invitations_title' => 'Pending invitations', + 'users_list_invitations_explanation' => 'Below are the people you’ve invited to join Monica as a collaborator.', + 'users_list_invitations_invited_by' => 'invited by :name', + 'users_list_invitations_sent_date' => 'sent on :date', + 'users_blank_title' => 'You are the only one who has access to this account.', + 'users_blank_add_title' => 'Would you like to invite someone else?', + 'users_blank_description' => 'This person will have the same access that you have, and will be able to add, edit or delete contact information.', + 'users_blank_cta' => 'Invite someone', + 'users_add_title' => 'Invite a new user to your account by email', + 'users_add_description' => 'This person will have the same access as you do, including inviting or deleting other users, including you. Make sure you trust this person before giving them access.', + 'users_add_email_field' => 'Enter the email of the person you want to invite', + 'users_add_confirmation' => 'I confirm that I want to invite this user to my account. I understand that this person will have access to ALL of my data and see exactly what I see.', + 'users_add_cta' => 'Invite user by email', + 'users_accept_title' => 'Accept invitation and create a new account', + 'users_error_please_confirm' => 'Please confirm that you want to invite this user before proceeding with the invitation', + 'users_error_email_already_taken' => 'This email is already taken. Please choose another one', + 'users_error_already_invited' => 'You already have invited this user. Please choose another email address.', + 'users_error_email_not_similar' => 'This is not the email of the person who’ve invited you.', + 'users_invitation_deleted_confirmation_message' => 'The invitation has been successfully deleted', + 'users_invitations_delete_confirmation' => 'Are you sure you want to delete this invitation?', + 'users_list_delete_confirmation' => 'Are you sure to delete this user from your account?', + 'users_invitation_need_subscription' => 'Adding more users requires a subscription.', + + 'subscriptions_account_current_plan' => 'Your current plan', + 'subscriptions_account_current_legacy' => 'Current plan, not selectable anymore:', + 'subscriptions_account_current_paid_plan' => 'You are on the :name plan. Thanks so much for being a subscriber.', + + 'subscriptions_account_next_billing_title' => 'Next bill', + 'subscriptions_account_next_billing' => 'Your subscription will auto-renew on :date.', + 'subscriptions_account_bill_monthly' => 'We’ll bill you :price for another month.', + 'subscriptions_account_bill_annual' => 'We’ll bill you :price for another year.', + 'subscriptions_account_change' => 'Change plan', + + 'subscriptions_account_cancel_title' => 'Cancel subscription', + 'subscriptions_account_cancel_action' => 'Cancel subscription', + 'subscriptions_account_cancel' => 'You can cancel your subscription at any time.', + 'subscriptions_account_free_plan' => 'You are on the free plan.', + 'subscriptions_account_free_plan_upgrade' => 'You can upgrade your account to the :name plan, which costs $:price per month. Here are the advantages:', + 'subscriptions_account_free_plan_benefits_users' => 'Unlimited number of users', + 'subscriptions_account_free_plan_benefits_reminders' => 'Reminders by email', + 'subscriptions_account_free_plan_benefits_import_data_vcard' => 'Import your contacts with vCard', + 'subscriptions_account_free_plan_benefits_support' => 'Support the project in the long run, so we can introduce more great features.', + 'subscriptions_account_upgrade' => 'Upgrade your account', + 'subscriptions_account_upgrade_title' => 'Upgrade Monica today and have more meaningful relationships.', + 'subscriptions_account_upgrade_choice' => 'Pick a plan below and join over :customers persons who upgraded their Monica.', + 'subscriptions_account_update_title' => 'Update Monica subscription', + 'subscriptions_account_update_description' => 'You can change your subscription’s frequency here.', + 'subscriptions_account_update_information' => 'You will be billed immediately for the new amount. Your subscription will extend to the new period, depending on your choice.', + 'subscriptions_account_invoices' => 'Invoices', + 'subscriptions_account_invoices_download' => 'Download', + 'subscriptions_account_invoices_subscription' => 'Subscription from :startDate to :endDate', + 'subscriptions_account_payment' => 'Which payment option fits you best?', + 'subscriptions_account_confirm_payment' => 'Your payment is currently incomplete, please confirm your payment.', + 'subscriptions_downgrade_title' => 'Downgrade your account to the free plan', + 'subscriptions_downgrade_limitations' => 'The free plan has limitations. In order to be able to downgrade, you need to pass the checklist below:', + 'subscriptions_downgrade_rule_users' => 'You must have only 1 user in your account', + 'subscriptions_downgrade_rule_users_constraint' => 'You currently have 1 user in your account.|You currently have :count users in your account.', + 'subscriptions_downgrade_rule_invitations' => 'You must not have any pending invitations', + 'subscriptions_downgrade_rule_invitations_constraint' => 'You currently have 1 pending invitation.|You currently have :count pending invitations.', + 'subscriptions_downgrade_rule_contacts' => 'You must not have more than :number active contacts', + 'subscriptions_downgrade_rule_contacts_constraint' => 'You currently have 1 contact.|You currently have :count contacts.', + 'subscriptions_downgrade_rule_contacts_archive' => 'We can also archive all your contacts for you – that would clear this rule and let you proceed with your account’s downgrade process.', + 'subscriptions_downgrade_cta' => 'Downgrade', + 'subscriptions_downgrade_success' => 'You are back to the Free plan!', + 'subscriptions_downgrade_thanks' => 'Thanks so much for trying the paid plan. We keep adding new features on Monica all the time – so you might want to come back in the future to see if you might be interested in taking a subscription again.', + 'subscriptions_back' => 'Back to settings', + 'subscriptions_upgrade_title' => 'Upgrade your account', + 'subscriptions_upgrade_choose' => 'You picked the :plan plan.', + 'subscriptions_upgrade_infos' => 'We couldn’t be happier. Enter your payment info below.', + 'subscriptions_upgrade_name' => 'Name on card', + 'subscriptions_upgrade_zip' => 'ZIP or postal code', + 'subscriptions_upgrade_credit' => 'Credit or debit card', + 'subscriptions_upgrade_submit' => 'Pay {amount}', + 'subscriptions_upgrade_charge' => 'We’ll charge your card :price now. The next charge will be on :date. If you ever change your mind, you can cancel at any time, no questions asked.', + 'subscriptions_upgrade_charge_handled' => 'The payment is handled by Stripe. No card information touches our server.', + 'subscriptions_upgrade_success' => 'Thank you! You are now subscribed.', + 'subscriptions_upgrade_thanks' => 'Welcome to the community of people who try to make the world a better place.', + + 'subscriptions_payment_confirm_title' => 'Confirm your :amount payment', + 'subscriptions_payment_confirm_information' => 'Extra confirmation is needed to process your payment. Please confirm your payment by filling out your payment details below.', + 'subscriptions_payment_succeeded_title' => 'Payment Successful', + 'subscriptions_payment_succeeded' => 'This payment was already successfully confirmed.', + 'subscriptions_payment_cancelled_title' => 'Payment Cancelled', + 'subscriptions_payment_cancelled' => 'This payment was cancelled.', + 'subscriptions_payment_error_name' => 'Please provide your name.', + 'subscriptions_payment_success' => 'The payment was successful.', + + 'subscriptions_pdf_title' => 'Your :name monthly subscription', + 'subscriptions_plan_frequency_year' => ':amount / year', + 'subscriptions_plan_frequency_month' => ':amount / month', + 'subscriptions_plan_choose' => 'Choose this plan', + 'subscriptions_plan_year_title' => 'Pay annually', + 'subscriptions_plan_year_bonus' => 'Peace of mind for a whole year', + 'subscriptions_plan_month_title' => 'Pay monthly', + 'subscriptions_plan_month_bonus' => 'Cancel any time', + 'subscriptions_plan_include1' => 'Included with your upgrade:', + 'subscriptions_plan_include2' => 'Unlimited number of contacts • Unlimited number of users • Reminders by email • Import with vCard • Personalization of the contact sheet', + 'subscriptions_plan_include3' => '100% of the profits go the development of this great open source project.', + 'subscriptions_help_title' => 'Additional details you may be curious about', + 'subscriptions_help_opensource_title' => 'What is an open source project?', + 'subscriptions_help_opensource_desc' => 'Monica is an open source project. This means it is built by a community who wants to build a great tool for the greater good. Being open source means the code is publicly available on GitHub, and everyone can inspect it, modify it or enhance it. All the money we raise is dedicated to building better features, paying for more powerful servers, and paying other costs. Thanks for your help. We couldn’t do it without you.', + 'subscriptions_help_limits_title' => 'Is there a limit to the number of contacts we can have on the free plan?', + 'subscriptions_help_limits_plan' => 'Yes. Free plans let you manage :number contacts.', + 'subscriptions_help_discounts_title' => 'Do you have discounts for non-profits and education?', + 'subscriptions_help_discounts_desc' => 'We do! Monica is free for students, and free for non-profits and charities. Just contact the support with a proof of your status and we’ll apply this special status in your account.', + 'subscriptions_help_change_title' => 'What if I change my mind?', + 'subscriptions_help_change_desc' => 'You can cancel anytime, no questions asked, and all by yourself – no need to contact support. However, you will not be refunded for the current period.', + + 'stripe_error_card' => 'Your card was declined. Decline message is: :message', + 'stripe_error_api_connection' => 'Network communication with Stripe failed. Try again later.', + 'stripe_error_rate_limit' => 'Too many requests with Stripe right now. Try again later.', + 'stripe_error_invalid_request' => 'Invalid parameters. Try again later.', + 'stripe_error_authentication' => 'Wrong authentication with Stripe', + + 'import_title' => 'Import contacts in your account', + 'import_cta' => 'Upload contacts', + 'import_stat' => 'You’ve imported :number files so far.', + 'import_result_stat' => 'Uploaded vCard with 1 contact (:total_imported imported, :total_skipped skipped)|Uploaded vCard with :total_contacts contacts (:total_imported imported, :total_skipped skipped)', + 'import_view_report' => 'View report', + 'import_in_progress' => 'The import is in progress. Reload the page in one minute.', + 'import_upload_title' => 'Import your contacts from a vCard file', + 'import_upload_rules_desc' => 'We do however have some rules:', + 'import_upload_rule_format' => 'We support .vcard and .vcf files.', + 'import_upload_rule_vcard' => 'We support the vCard 3.0 format, which is the default format for macOS’s Contacts.app and Google Contacts.', + 'import_upload_rule_instructions' => 'Export instructions for macOS Contacts.app and Google Contacts.', + 'import_upload_rule_multiple' => 'If your contacts have multiple email addresses or phone numbers, only the first entry will be saved.', + 'import_upload_rule_limit' => 'Files are limited to 10 MB.', + 'import_upload_rule_time' => 'It might take up to a minute to upload the contacts and process them. Please be patient.', + 'import_upload_rule_cant_revert' => 'Please make sure data is accurate before uploading, as you can’t undo the upload.', + 'import_upload_form_file' => 'Your .vcf or .vCard file:', + 'import_upload_behaviour' => 'Import behaviour:', + 'import_upload_behaviour_add' => 'Add new contacts and skip existing', + 'import_upload_behaviour_replace' => 'Replace existing contacts', + 'import_upload_behaviour_help' => 'Replacing will replace all data found in the vCard, but will keep existing contact fields.', + 'import_report_title' => 'Importing report', + 'import_report_date' => 'Date of the import', + 'import_report_type' => 'Type of import', + 'import_report_number_contacts' => 'Number of contacts in the file', + 'import_report_number_contacts_imported' => 'Number of imported contacts', + 'import_report_number_contacts_skipped' => 'Number of skipped contacts', + 'import_report_status_imported' => 'Imported', + 'import_report_status_skipped' => 'Skipped', + 'import_vcard_parse_error' => 'Error when parsing the vCard entry', + 'import_vcard_contact_exist' => 'Contact already exists', + 'import_vcard_contact_no_firstname' => 'No first name (mandatory)', + 'import_vcard_file_not_found' => 'File not found', + 'import_vcard_unknown_entry' => 'Unknown contact name', + 'import_vcard_file_no_entries' => 'File contains no entries', + 'import_blank_title' => 'You haven’t imported any contacts yet.', + 'import_blank_question' => 'Would you like to import contacts now?', + 'import_blank_description' => 'We can import vCard files that you can get from Google Contacts or your Contact manager.', + 'import_blank_cta' => 'Import vCard', + 'import_need_subscription' => 'Importing data requires a subscription.', + + 'tags_list_title' => 'Tags', + 'tags_list_description' => 'You can organize your contacts by setting up tags. Tags work like folders, but you can add more than one tag to a contact. To add a new tag, add it on the contact itself.', + 'tags_list_contact_number' => '1 contact|:count contacts', + 'tags_list_delete_success' => 'The tag has been successfully deleted', + 'tags_list_edit_success' => 'The tag has been successfully updated', + 'tags_list_delete_confirmation' => 'Are you sure you want to delete the tag? No contacts will be deleted, only the tag.', + 'tags_blank_title' => 'Tags are a great way of categorizing your contacts.', + 'tags_blank_description' => 'Tags work like folders, but you can add more than one tag to a contact. Go to a contact and tag a friend, right below the name. Once a contact is tagged, come back here to manage all the tags in your account.', + + 'api_title' => 'API access', + 'api_description' => 'The API can be used to manipulate Monica’s data from an external application, like a mobile application for instance.', + 'api_help' => 'To use the API, a token is mandatory. You can either create a personal access token (Bearer authentication), or authorize an OAuth client to create it for you. See API documentation.', + 'api_endpoint' => 'The API endpoint for this Monica instance is:', + + 'api_personal_access_tokens' => 'Personal access tokens', + 'api_pao_description' => 'Make sure you give this token to a source you trust – as they allow you to access all your data.', + 'api_token_title' => 'Personal Access Tokens', + 'api_token_create_new' => 'Create New Token', + 'api_token_not_created' => 'You have not created any personal access tokens.', + 'api_token_name' => 'Token name', + 'api_token_expire' => 'Expires at {date}', + 'api_token_delete' => 'Delete', + 'api_token_create' => 'Create Token', + 'api_token_scopes' => 'Scopes', + 'api_token_help' => 'Here is your new personal access token. This is the only time it will be shown so don’t lose it! You may now use this token to make API requests.', + + 'api_oauth_clients' => 'Your OAuth clients', + 'api_oauth_clients_desc' => 'This section lets you register your own OAuth clients.', + 'api_oauth_clients_desc2' => 'Use this client id to request a new token, and convert authorisation codes to access tokens. See Laravel Passport documentation for more information.', + 'api_oauth_title' => 'OAuth Clients', + 'api_oauth_create_new' => 'Create New Client', + 'api_oauth_edit' => 'Edit Client', + 'api_oauth_not_created' => 'You have not created any OAuth clients.', + 'api_oauth_clientid' => 'Client ID', + 'api_oauth_name' => 'Name', + 'api_oauth_name_help' => 'Something your users will recognize and trust.', + 'api_oauth_secret' => 'Secret', + 'api_oauth_create' => 'Create Client', + 'api_oauth_redirecturl' => 'Redirect URL', + 'api_oauth_redirecturl_help' => 'Your application’s authorization callback URL.', + + 'api_authorized_clients' => 'List of authorised clients', + 'api_authorized_clients_desc' => 'This section lists all the clients you’ve authorised to access your application data. You can revoke this authorisation at any time.', + 'api_authorized_clients_title' => 'Authorised Applications', + 'api_authorized_clients_none' => 'There are no authorised clients yet.', + 'api_authorized_clients_name' => 'Name', + 'api_authorized_clients_scopes' => 'Scopes', + + 'personalization_tab_title' => 'Personalise your account', + + 'personalization_title' => 'Here you will find different settings to configure your account. These features are intended for “power users” who want maximum control over Monica.', + 'personalization_contact_field_type_title' => 'Contact field types', + 'personalization_contact_field_type_add' => 'Add new field type', + 'personalization_contact_field_type_description' => 'You can configure all the different types of contact fields that you can associate to all your contacts. For example, if a new social network appears in the future, you will be able to add this new way of communicating with your contacts right here.', + 'personalization_contact_field_type_table_name' => 'Name', + 'personalization_contact_field_type_table_protocol' => 'Protocol', + 'personalization_contact_field_type_table_actions' => 'Actions', + 'personalization_contact_field_type_modal_title' => 'Add a new contact field type', + 'personalization_contact_field_type_modal_edit_title' => 'Edit an existing contact field type', + 'personalization_contact_field_type_modal_delete_title' => 'Delete an existing contact field type', + 'personalization_contact_field_type_modal_delete_description' => 'Are you sure you want to delete this contact field type? Deleting this type of contact field will delete ALL the data with this type for all of your contacts.', + 'personalization_contact_field_type_modal_name' => 'Name', + 'personalization_contact_field_type_modal_protocol' => 'Protocol (optional)', + 'personalization_contact_field_type_modal_protocol_help' => 'Each new contact field type can be clickable. If a protocol is set, we will use it to trigger the action that is set.', + 'personalization_contact_field_type_modal_icon' => 'Icon (optional)', + 'personalization_contact_field_type_modal_icon_help' => 'You can associate an icon with this contact field type. You need to add a reference to a Font Awesome icon.', + 'personalization_contact_field_type_delete_success' => 'The contact field type has been successfully deleted.', + 'personalization_contact_field_type_add_success' => 'The contact field type has been successfully added.', + 'personalization_contact_field_type_edit_success' => 'The contact field type has been successfully updated.', + + 'personalization_genders_title' => 'Gender types', + 'personalization_genders_add' => 'Add new gender type', + 'personalization_genders_desc' => 'You can define as many genders as you need to. You need at least one gender type in your account.', + 'personalization_genders_modal_add' => 'Add gender type', + 'personalization_genders_modal_edit' => 'Update gender type', + 'personalization_genders_modal_name' => 'Name', + 'personalization_genders_modal_name_help' => 'The name used to display the gender on a contact page.', + 'personalization_genders_modal_sex' => 'Sex', + 'personalization_genders_modal_sex_help' => 'Used to define the relationships, and during the VCard import/export process.', + 'personalization_genders_modal_default' => 'Select the default gender for a new contact', + 'personalization_genders_modal_delete' => 'Delete gender type', + 'personalization_genders_modal_delete_desc' => 'Are you sure you want to delete the gender “{name}”?', + 'personalization_genders_modal_delete_question' => 'You currently have {count} contact with this gender. If you delete this gender, what gender should this contact have?|You currently have {count} contacts with this gender. If you delete this gender, what gender should these contacts have?', + 'personalization_genders_modal_delete_question_default' => 'This gender is the default one. If you delete this gender, which one will be the new default?', + 'personalization_genders_modal_error' => 'Please choose a gender from the list.', + 'personalization_genders_list_contact_number' => '{count} contact|{count} contacts', + 'personalization_genders_table_name' => 'Name', + 'personalization_genders_table_sex' => 'Sex', + 'personalization_genders_table_default' => 'Default', + 'personalization_genders_default' => 'Default gender', + 'personalization_genders_make_default' => 'Change default gender', + 'personalization_genders_select_default' => 'Select default gender', + 'personalization_genders_m' => 'Male', + 'personalization_genders_f' => 'Female', + 'personalization_genders_o' => 'Other', + 'personalization_genders_u' => 'Unknown', + 'personalization_genders_n' => 'None or not applicable', + + 'personalization_reminder_rule_save' => 'The change has been saved', + 'personalization_reminder_rule_title' => 'Reminder rules', + 'personalization_reminder_rule_line' => '{count} day before|{count} days before', + 'personalization_reminder_rule_desc' => 'For every reminder that you set, Monica can send you an email a number of days before the event happens. You can adjust these notification settings here. These notifications only apply to monthly and yearly reminders.', + + 'personalization_module_save' => 'The change has been saved', + 'personalization_module_title' => 'Features', + 'personalization_module_desc' => 'You may not need all of Monica’s features. Below you can toggle specific features that are used on a contact sheet. This change will affect ALL your contacts. Turning off a feature does not delete any data, it simply hides the feature.', + + 'personalisation_paid_upgrade' => 'This is a premium feature that requires a Paid subscription to be active. Upgrade your account by visiting Settings > Subscription.', + 'personalisation_paid_upgrade_vue' => 'This is a premium feature that requires a Paid subscription to be active. Upgrade your account by visiting Settings > Subscription.', + + 'reminder_time_to_send' => 'Time of the day reminders will be sent', + 'reminder_time_to_send_help' => 'Your next reminder is scheduled to be sent on {dateTime}.', + + 'personalization_activity_type_category_title' => 'Activity type categories', + 'personalization_activity_type_category_add' => 'Add a new activity type category', + 'personalization_activity_type_category_table_name' => 'Name', + 'personalization_activity_type_category_description' => 'An activity with one of your contacts can have a type and a category type. Your account comes with a set of predefined category types by default, but you can customise these here.', + 'personalization_activity_type_category_table_actions' => 'Actions', + 'personalization_activity_type_category_modal_add' => 'Add a new activity type category', + 'personalization_activity_type_category_modal_edit' => 'Edit an activity type category', + 'personalization_activity_type_category_modal_question' => 'What should we name this new category?', + 'personalization_activity_type_add_button' => 'Add a new activity type', + 'personalization_activity_type_modal_add' => 'Add a new activity type', + 'personalization_activity_type_modal_question' => 'What should we name this new activity type?', + 'personalization_activity_type_modal_edit' => 'Edit an activity type', + 'personalization_activity_type_category_modal_delete' => 'Delete an activity type category', + 'personalization_activity_type_category_modal_delete_desc' => 'Are you sure you want to delete this category? Deleting it will delete all associated activity types. Activities that belong to this category will not be affected by this deletion.', + 'personalization_activity_type_modal_delete' => 'Delete an activity type', + 'personalization_activity_type_modal_delete_desc' => 'Are you sure you want to delete this activity type? Activities that belong to this category will not be affected by this deletion.', + 'personalization_activity_type_modal_delete_error' => 'We can’t find this activity type.', + 'personalization_activity_type_category_modal_delete_error' => 'We can’t find this activity type category.', + + 'personalization_life_event_category_title' => 'Life event categories', + 'personalization_live_event_category_table_name' => 'Name', + 'personalization_life_event_category_description' => 'A life event can have a type and a category. Your account comes with a set of predefined categories and types by default, but you can customise life event types here.', + 'personalization_live_event_category_table_actions' => 'Actions', + 'personalization_life_event_type_add_button' => 'Add a new life event type', + 'personalization_life_event_type_modal_add' => 'Add a new life event type', + 'personalization_life_event_type_modal_question' => 'What should we name this new life event type?', + 'personalization_life_event_type_modal_edit' => 'Edit a life event type', + 'personalization_life_event_type_modal_delete' => 'Delete a life event type', + 'personalization_life_event_type_modal_delete_desc' => 'Are you sure you want to delete this life event type? Life events that belong to this type will be deleted by performing this action.', + 'personalization_life_event_type_modal_delete_error' => 'We can’t find this life event type.', + + 'personalization_life_event_category_work_education' => 'Work & education', + 'personalization_life_event_category_family_relationships' => 'Family & relationships', + 'personalization_life_event_category_home_living' => 'Home & living', + 'personalization_life_event_category_travel_experiences' => 'Travel & experiences', + 'personalization_life_event_category_health_wellness' => 'Health & wellness', + + 'personalization_life_event_type_new_job' => 'New job', + 'personalization_life_event_type_retirement' => 'Retirement', + 'personalization_life_event_type_new_school' => 'New school', + 'personalization_life_event_type_study_abroad' => 'Study abroad', + 'personalization_life_event_type_volunteer_work' => 'Volunteer work', + 'personalization_life_event_type_published_book_or_paper' => 'Published a book or paper', + 'personalization_life_event_type_military_service' => 'Military service', + 'personalization_life_event_type_first_met' => 'First met', + 'personalization_life_event_type_new_relationship' => 'New relationship', + 'personalization_life_event_type_engagement' => 'Engagement', + 'personalization_life_event_type_marriage' => 'Marriage', + 'personalization_life_event_type_anniversary' => 'Anniversary', + 'personalization_life_event_type_expecting_a_baby' => 'Expecting a baby', + 'personalization_life_event_type_new_child' => 'New child', + 'personalization_life_event_type_new_family_member' => 'New family member', + 'personalization_life_event_type_new_pet' => 'New pet', + 'personalization_life_event_type_end_of_relationship' => 'End of relationship', + 'personalization_life_event_type_loss_of_a_loved_one' => 'Loss of a loved one', + 'personalization_life_event_type_moved' => 'Moved', + 'personalization_life_event_type_bought_a_home' => 'Bought a home', + 'personalization_life_event_type_home_improvement' => 'Home improvement', + 'personalization_life_event_type_holidays' => 'Holidays', + 'personalization_life_event_type_new_vehicle' => 'New vehicle', + 'personalization_life_event_type_new_roommate' => 'New roommate', + 'personalization_life_event_type_overcame_an_illness' => 'Overcame an illness', + 'personalization_life_event_type_quit_a_habit' => 'Quit a habit', + 'personalization_life_event_type_new_eating_habits' => 'New eating habits', + 'personalization_life_event_type_weight_loss' => 'Weight loss', + 'personalization_life_event_type_wear_glass_or_contact' => 'Started wearing glasses or contacts', + 'personalization_life_event_type_broken_bone' => 'Broke a bone', + 'personalization_life_event_type_removed_braces' => 'Had braces removed', + 'personalization_life_event_type_surgery' => 'Had surgery', + 'personalization_life_event_type_dentist' => 'Had dental treatment', + 'personalization_life_event_type_new_sport' => 'Started playing a new sport', + 'personalization_life_event_type_new_hobby' => 'Took up a new hobby', + 'personalization_life_event_type_new_instrument' => 'Started learning a new instrument', + 'personalization_life_event_type_new_language' => 'Started learning a new language', + 'personalization_life_event_type_tattoo_or_piercing' => 'Tattoo or piercing', + 'personalization_life_event_type_new_license' => 'New license', + 'personalization_life_event_type_travel' => 'Travel', + 'personalization_life_event_type_achievement_or_award' => 'Achievement or award', + 'personalization_life_event_type_changed_beliefs' => 'Changed beliefs', + 'personalization_life_event_type_first_word' => 'First word', + 'personalization_life_event_type_first_kiss' => 'First kiss', + + 'storage_title' => 'Storage', + 'storage_account_info' => 'Your account limit is :accountLimit MB. Your current usage is :currentAccountSize MB (about :percentUsage%).', + 'storage_upgrade_notice' => 'Upgrade your account to be able to upload documents and photos.', + 'storage_description' => 'Here you can see all the documents and photos uploaded about your contacts.', + + 'dav_title' => 'WebDAV', + 'dav_description' => 'Here you can find all settings to use WebDAV resources for CardDAV and CalDAV exports.', + 'dav_copy_help' => 'Copy into your clipboard', + 'dav_clipboard_copied' => 'Value copied into your clipboard', + 'dav_url_base' => 'Base url for all CardDAV and CalDAV resources:', + 'dav_connect_help' => 'You can connect your contacts and/or calendars with this base url on you phone or computer.', + 'dav_connect_help2' => 'Use your login (email) and create an API token as the password to authenticate.', + 'dav_url_carddav' => 'CardDAV url for Contacts resource:', + 'dav_url_caldav_birthdays' => 'CalDAV url for Birthdays resources:', + 'dav_url_caldav_tasks' => 'CalDAV url for Tasks resources:', + 'dav_title_carddav' => 'CardDAV', + 'dav_title_caldav' => 'CalDAV', + 'dav_carddav_export' => 'Export all contacts in one file', + 'dav_caldav_birthdays_export' => 'Export all birthdays in one file', + 'dav_caldav_tasks_export' => 'Export all tasks in one file', + + 'archive_title' => 'Archive all of the contacts in your account', + 'archive_desc' => 'This will archive all of the contacts in your account.', + 'archive_cta' => 'Archive all of your contacts', + + 'logs_title' => 'Everything that has happened to this account', + 'logs_actor' => 'Actor', + 'logs_timestamp' => 'Timestamp', + 'logs_description' => 'Description', + 'logs_subject' => 'Subject', + 'logs_size' => 'Size (KB)', + 'logs_object' => 'Object', +]; diff --git a/resources/lang/en-GB/validation.php b/resources/lang/en-GB/validation.php new file mode 100644 index 0000000..0153365 --- /dev/null +++ b/resources/lang/en-GB/validation.php @@ -0,0 +1,166 @@ + 'The :attribute must be accepted.', + 'active_url' => 'The :attribute is not a valid URL.', + 'after' => 'The :attribute must be a date after :date.', + 'after_or_equal' => 'The :attribute must be a date after or equal to :date.', + 'alpha' => 'The :attribute may only contain letters.', + 'alpha_dash' => 'The :attribute may only contain letters, numbers, dashes and underscores.', + 'alpha_num' => 'The :attribute may only contain letters and numbers.', + 'array' => 'The :attribute must be an array.', + 'before' => 'The :attribute must be a date before :date.', + 'before_or_equal' => 'The :attribute must be a date before or equal to :date.', + 'between' => [ + 'numeric' => 'The :attribute must be between :min and :max.', + 'file' => 'The :attribute must be between :min and :max kilobytes.', + 'string' => 'The :attribute must be between :min and :max characters.', + 'array' => 'The :attribute must have between :min and :max items.', + ], + 'boolean' => 'The :attribute field must be true or false.', + 'confirmed' => 'The :attribute confirmation does not match.', + 'date' => 'The :attribute is not a valid date.', + 'date_equals' => 'The :attribute must be a date equal to :date.', + 'date_format' => 'The :attribute does not match the format :format.', + 'different' => 'The :attribute and :other must be different.', + 'digits' => 'The :attribute must be :digits digits.', + 'digits_between' => 'The :attribute must be between :min and :max digits.', + 'dimensions' => 'The :attribute has invalid image dimensions.', + 'distinct' => 'The :attribute field has a duplicate value.', + 'email' => 'The :attribute must be a valid email address.', + 'ends_with' => 'The :attribute must end with one of the following: :values.', + 'exists' => 'The selected :attribute is invalid.', + 'file' => 'The :attribute must be a file.', + 'filled' => 'The :attribute field must have a value.', + 'gt' => [ + 'numeric' => 'The :attribute must be greater than :value.', + 'file' => 'The :attribute must be greater than :value kilobytes.', + 'string' => 'The :attribute must be greater than :value characters.', + 'array' => 'The :attribute must have more than :value items.', + ], + 'gte' => [ + 'numeric' => 'The :attribute must be greater than or equal :value.', + 'file' => 'The :attribute must be greater than or equal :value kilobytes.', + 'string' => 'The :attribute must be greater than or equal :value characters.', + 'array' => 'The :attribute must have :value items or more.', + ], + 'image' => 'The :attribute must be an image.', + 'in' => 'The selected :attribute is invalid.', + 'in_array' => 'The :attribute field does not exist in :other.', + 'integer' => 'The :attribute must be an integer.', + 'ip' => 'The :attribute must be a valid IP address.', + 'ipv4' => 'The :attribute must be a valid IPv4 address.', + 'ipv6' => 'The :attribute must be a valid IPv6 address.', + 'json' => 'The :attribute must be a valid JSON string.', + 'lt' => [ + 'numeric' => 'The :attribute must be less than :value.', + 'file' => 'The :attribute must be less than :value kilobytes.', + 'string' => 'The :attribute must be less than :value characters.', + 'array' => 'The :attribute must have less than :value items.', + ], + 'lte' => [ + 'numeric' => 'The :attribute must be less than or equal :value.', + 'file' => 'The :attribute must be less than or equal :value kilobytes.', + 'string' => 'The :attribute must be less than or equal :value characters.', + 'array' => 'The :attribute must not have more than :value items.', + ], + 'max' => [ + 'numeric' => 'The :attribute may not be greater than :max.', + 'file' => 'The :attribute may not be greater than :max kilobytes.', + 'string' => 'The :attribute may not be greater than :max characters.', + 'array' => 'The :attribute may not have more than :max items.', + ], + 'mimes' => 'The :attribute must be a file of type: :values.', + 'mimetypes' => 'The :attribute must be a file of type: :values.', + 'min' => [ + 'numeric' => 'The :attribute must be at least :min.', + 'file' => 'The :attribute must be at least :min kilobytes.', + 'string' => 'The :attribute must be at least :min characters.', + 'array' => 'The :attribute must have at least :min items.', + ], + 'not_in' => 'The selected :attribute is invalid.', + 'not_regex' => 'The :attribute format is invalid.', + 'numeric' => 'The :attribute must be a number.', + 'password' => 'The password is incorrect.', + 'present' => 'The :attribute field must be present.', + 'regex' => 'The :attribute format is invalid.', + 'required' => 'The :attribute field is required.', + 'required_if' => 'The :attribute field is required when :other is :value.', + 'required_unless' => 'The :attribute field is required unless :other is in :values.', + 'required_with' => 'The :attribute field is required when :values is present.', + 'required_with_all' => 'The :attribute field is required when :values are present.', + 'required_without' => 'The :attribute field is required when :values is not present.', + 'required_without_all' => 'The :attribute field is required when none of :values are present.', + 'same' => 'The :attribute and :other must match.', + 'size' => [ + 'numeric' => 'The :attribute must be :size.', + 'file' => 'The :attribute must be :size kilobytes.', + 'string' => 'The :attribute must be :size characters.', + 'array' => 'The :attribute must contain :size items.', + ], + 'starts_with' => 'The :attribute must start with one of the following: :values.', + 'string' => 'The :attribute must be a string.', + 'timezone' => 'The :attribute must be a valid zone.', + 'unique' => 'The :attribute has already been taken.', + 'uploaded' => 'The :attribute failed to upload.', + 'url' => 'The :attribute format is invalid.', + 'uuid' => 'The :attribute must be a valid UUID.', + + /* + |-------------------------------------------------------------------------- + | Custom Validation Language Lines + |-------------------------------------------------------------------------- + | + | Here you may specify custom validation messages for attributes using the + | convention "attribute.rule" to name the lines. This makes it quick to + | specify a specific custom language line for a given attribute rule. + | + */ + + 'custom' => [ + 'attribute-name' => [ + 'rule-name' => 'custom-message', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Custom Validation Attributes + |-------------------------------------------------------------------------- + | + | The following language lines are used to swap our attribute placeholder + | with something more reader friendly such as "E-Mail Address" instead + | of "email". This simply helps us make our message more expressive. + | + */ + + 'attributes' => [], + + 'vue' => [ + 'max' => [ + 'numeric' => '{field} may not be greater than {max}.', + 'string' => '{field} may not be greater than {max} characters.', + ], + 'required' => '{field} is required.', + 'url' => '{field} is not a valid URL.', + ], + +]; diff --git a/resources/lang/en.json b/resources/lang/en.json new file mode 100644 index 0000000..ddea72e --- /dev/null +++ b/resources/lang/en.json @@ -0,0 +1,7 @@ +{ + "The :attribute must contain at least one uppercase and one lowercase letter.": "The :attribute must contain at least one uppercase and one lowercase letter.", + "The :attribute must contain at least one letter.": "The :attribute must contain at least one letter.", + "The :attribute must contain at least one symbol.": "The :attribute must contain at least one symbol.", + "The :attribute must contain at least one number.": "The :attribute must contain at least one number.", + "The given :attribute has appeared in a data leak. Please choose a different :attribute.": "The given :attribute has appeared in a data leak. Please choose a different :attribute." +} diff --git a/resources/lang/en/app.php b/resources/lang/en/app.php new file mode 100644 index 0000000..da7bc31 --- /dev/null +++ b/resources/lang/en/app.php @@ -0,0 +1,571 @@ + 'Yes', + 'no' => 'No', + 'update' => 'Update', + 'save' => 'Save', + 'add' => 'Add', + 'cancel' => 'Cancel', + 'confirm' => 'Confirm', + 'delete_confirm' => 'Are you sure?', + 'delete' => 'Delete', + 'edit' => 'Edit', + 'upload' => 'Upload', + 'download' => 'Download', + 'save_close' => 'Save and close', + 'close' => 'Close', + 'copy' => 'Copy', + 'create' => 'Create', + 'remove' => 'Remove', + 'revoke' => 'Revoke', + 'done' => 'Done', + 'back' => 'Back', + 'verify' => 'Verify', + 'new' => 'new', + 'unknown' => 'I don’t know', + 'load_more' => 'Load more', + 'loading' => 'Loading…', + 'with' => 'with', + 'today' => 'today', + 'yesterday' => 'yesterday', + 'another_day' => 'another day', + 'date' => 'Date', + 'type' => 'Type', + 'zoom' => 'Zoom', + 'upgrade' => 'Upgrade to unlock', + 'percent_uploaded' => '{percent}% uploaded', + 'retry' => 'Retry', + 'filter' => 'Filter the list', + 'go_back' => 'Go back', + 'file_selected' => 'One file selected…|{count} files selected…', + + 'application_title' => 'Monica – personal relationship manager', + 'application_description' => 'Monica is a tool to manage your interactions with your loved ones, friends, and family.', + 'application_og_title' => 'Have better relations with your loved ones. Free online CRM for friends and family.', + + 'markdown_description' => 'Want to format your text nicely? We support Markdown to add bold, italic, lists, and more.', + 'markdown_link' => 'Read documentation', + + 'header_settings_link' => 'Settings', + 'header_logout_link' => 'Logout', + 'header_changelog_link' => 'Product changes', + + 'main_nav_cta' => 'Add people', + 'main_nav_dashboard' => 'Dashboard', + 'main_nav_family' => 'Contacts', + 'main_nav_journal' => 'Journal', + 'main_nav_activities' => 'Activities', + 'main_nav_tasks' => 'Tasks', + + 'footer_remarks' => 'Comments?', + 'footer_send_email' => 'Send us an email', + 'footer_privacy' => 'Privacy policy', + 'footer_release' => 'Release notes', + 'footer_newsletter' => 'Newsletter', + 'footer_source_code' => 'Contribute', + 'footer_version' => 'Version: :version', + 'footer_new_version' => 'A new version of Monica is available', + + 'footer_modal_version_whats_new' => 'What’s new', + 'footer_modal_version_release_away' => 'You are 1 release behind the latest version available. You should update your instance.|You are :number releases behind the latest version available. You should update your instance.', + + 'breadcrumb_dashboard' => 'Dashboard', + 'breadcrumb_list_contacts' => 'List of people', + 'breadcrumb_archived_contacts' => 'Archived contacts', + 'breadcrumb_journal' => 'Journal', + 'breadcrumb_settings' => 'Settings', + 'breadcrumb_settings_export' => 'Export', + 'breadcrumb_settings_users' => 'Users', + 'breadcrumb_settings_users_add' => 'Add a user', + 'breadcrumb_settings_subscriptions' => 'Subscription', + 'breadcrumb_settings_import' => 'Import', + 'breadcrumb_settings_import_report' => 'Import report', + 'breadcrumb_settings_import_upload' => 'Upload', + 'breadcrumb_settings_tags' => 'Tags', + 'breadcrumb_add_significant_other' => 'Add significant other', + 'breadcrumb_edit_significant_other' => 'Edit significant other', + 'breadcrumb_add_note' => 'Add a note', + 'breadcrumb_edit_note' => 'Edit a note', + 'breadcrumb_api' => 'API', + 'breadcrumb_dav' => 'DAV Resources', + 'breadcrumb_edit_introductions' => 'How did you meet', + 'breadcrumb_settings_personalization' => 'Personalization', + 'breadcrumb_settings_security' => 'Security', + 'breadcrumb_settings_security_2fa' => 'Two Factor Authentication', + 'breadcrumb_profile' => 'Profile of :name', + + 'gender_male' => 'Man', + 'gender_female' => 'Woman', + 'gender_none' => 'Rather not say', + 'gender_no_gender' => 'No gender', + + 'error_title' => 'Whoops! Something went wrong.', + 'error_unauthorized' => 'You don’t have the right to edit this resource.', + 'error_user_account' => 'This user does not belong to the given account.', + 'error_save' => 'We had an error trying to save the data.', + 'error_try_again' => 'Something went wrong. Please try again.', + 'error_id' => 'Error ID: :id', + 'error_unavailable' => 'Service unavailable', + 'error_maintenance' => 'Maintenance in progress. We’ll be right back.', + 'error_help' => 'We’ll be right back.', + 'error_twitter' => 'Follow our Twitter account to be alerted when it’s up again.', + 'error_no_term' => 'There is no policy for this instance yet.', + + 'default_save_success' => 'The data has been saved.', + + 'compliance_title' => 'Sorry for the interruption.', + 'compliance_desc' => 'We have changed our Terms of Use and Privacy Policy. By law we have to ask you to review them and accept them so you can continue to use your account.', + 'compliance_desc_end' => 'We don’t do anything nasty with your data or your account and we never will.', + 'compliance_terms' => 'Accept new terms and privacy policy', + + // Relationship types + // Yes, each relationship type has 8 strings associated with it. + // This is because we need to indicate the name of the relationship type, + // and also the name of the opposite side of this relationship (father/son), + // and then, the feminine version of the string. Finally, in some sentences + // in the UI, we need to include the name of the person we add the relationship + // to. + 'relationship_type_group_love' => 'Love relationships', + 'relationship_type_group_family' => 'Family relationships', + 'relationship_type_group_friend' => 'Friend relationships', + 'relationship_type_group_work' => 'Work relationships', + 'relationship_type_group_other' => 'Other kind of relationships', + + 'relationship_type_partner' => 'significant other', + 'relationship_type_partner_female' => 'significant other', + 'relationship_type_partner_male' => 'significant other', + 'relationship_type_partner_with_name' => ':name’s significant other', + 'relationship_type_partner_female_with_name' => ':name’s significant other', + 'relationship_type_partner_male_with_name' => ':name’s significant other', + + 'relationship_type_spouse' => 'spouse', + 'relationship_type_spouse_female' => 'wife', + 'relationship_type_spouse_male' => 'husband', + 'relationship_type_spouse_with_name' => ':name’s spouse', + 'relationship_type_spouse_female_with_name' => ':name’s wife', + 'relationship_type_spouse_male_with_name' => ':name’s husband', + + 'relationship_type_date' => 'date', + 'relationship_type_date_female' => 'date', + 'relationship_type_date_male' => 'date', + 'relationship_type_date_with_name' => ':name’s date', + 'relationship_type_date_female_with_name' => ':name’s date', + 'relationship_type_date_male_with_name' => ':name’s date', + + 'relationship_type_lover' => 'lover', + 'relationship_type_lover_female' => 'lover', + 'relationship_type_lover_male' => 'lover', + 'relationship_type_lover_with_name' => ':name’s lover', + 'relationship_type_lover_female_with_name' => ':name’s lover', + 'relationship_type_lover_male_with_name' => ':name’s lover', + + 'relationship_type_inlovewith' => 'in love with', + 'relationship_type_inlovewith_female' => 'in love with', + 'relationship_type_inlovewith_male' => 'in love with', + 'relationship_type_inlovewith_with_name' => 'someone :name is in love with', + 'relationship_type_inlovewith_female_with_name' => 'someone :name is in love with', + 'relationship_type_inlovewith_male_with_name' => 'someone :name is in love with', + + 'relationship_type_lovedby' => 'loved by', + 'relationship_type_lovedby_female' => 'loved by', + 'relationship_type_lovedby_male' => 'loved by', + 'relationship_type_lovedby_with_name' => ':name’s secret lover', + 'relationship_type_lovedby_female_with_name' => ':name’s secret lover', + 'relationship_type_lovedby_male_with_name' => ':name’s secret lover', + + 'relationship_type_ex' => 'ex-partner', + 'relationship_type_ex_female' => 'ex-girlfriend', + 'relationship_type_ex_male' => 'ex-boyfriend', + 'relationship_type_ex_with_name' => ':name’s ex-partner', + 'relationship_type_ex_female_with_name' => ':name’s ex-girlfriend', + 'relationship_type_ex_male_with_name' => ':name’s ex-boyfriend', + + 'relationship_type_parent' => 'parent', + 'relationship_type_parent_female' => 'mother', + 'relationship_type_parent_male' => 'father', + 'relationship_type_parent_with_name' => ':name’s parent', + 'relationship_type_parent_female_with_name' => ':name’s mother', + 'relationship_type_parent_male_with_name' => ':name’s father', + + 'relationship_type_child' => 'child', + 'relationship_type_child_female' => 'daughter', + 'relationship_type_child_male' => 'son', + 'relationship_type_child_with_name' => ':name’s child', + 'relationship_type_child_female_with_name' => ':name’s daughter', + 'relationship_type_child_male_with_name' => ':name’s son', + + 'relationship_type_stepparent' => 'step-parent', + 'relationship_type_stepparent_female' => 'stepmother', + 'relationship_type_stepparent_male' => 'stepfather', + 'relationship_type_stepparent_with_name' => ':name’s step-parent', + 'relationship_type_stepparent_female_with_name' => ':name’s stepmother', + 'relationship_type_stepparent_male_with_name' => ':name’s stepfather', + + 'relationship_type_stepchild' => 'stepchild', + 'relationship_type_stepchild_female' => 'stepdaughter', + 'relationship_type_stepchild_male' => 'stepson', + 'relationship_type_stepchild_with_name' => ':name’s stepchild', + 'relationship_type_stepchild_female_with_name' => ':name’s stepdaughter', + 'relationship_type_stepchild_male_with_name' => ':name’s stepson', + + 'relationship_type_sibling' => 'sibling', + 'relationship_type_sibling_female' => 'sister', + 'relationship_type_sibling_male' => 'brother', + 'relationship_type_sibling_with_name' => ':name’s sibling', + 'relationship_type_sibling_female_with_name' => ':name’s sister', + 'relationship_type_sibling_male_with_name' => ':name’s brother', + + 'relationship_type_grandparent' => 'grandparent', + 'relationship_type_grandparent_female' => 'grandmother', + 'relationship_type_grandparent_male' => 'grandfather', + 'relationship_type_grandparent_with_name' => ':name’s grandparent', + 'relationship_type_grandparent_female_with_name' => ':name’s grandmother', + 'relationship_type_grandparent_male_with_name' => ':name’s grandfather', + + 'relationship_type_grandchild' => 'grandchild', + 'relationship_type_grandchild_female' => 'granddaughter', + 'relationship_type_grandchild_male' => 'grandson', + 'relationship_type_grandchild_with_name' => ':name’s grandchild', + 'relationship_type_grandchild_female_with_name' => ':name’s granddauther', + 'relationship_type_grandchild_male_with_name' => ':name’s grandson', + + 'relationship_type_uncle' => 'uncle', + 'relationship_type_uncle_female' => 'aunt', + 'relationship_type_uncle_male' => 'uncle', + 'relationship_type_uncle_with_name' => ':name’s uncle', + 'relationship_type_uncle_female_with_name' => ':name’s aunt', + 'relationship_type_uncle_male_with_name' => ':name’s uncle', + + 'relationship_type_nephew' => 'nephew', + 'relationship_type_nephew_female' => 'niece', + 'relationship_type_nephew_male' => 'nephew', + 'relationship_type_nephew_with_name' => ':name’s nephew', + 'relationship_type_nephew_female_with_name' => ':name’s niece', + 'relationship_type_nephew_male_with_name' => ':name’s nephew', + + 'relationship_type_cousin' => 'cousin', + 'relationship_type_cousin_female' => 'cousin', + 'relationship_type_cousin_male' => 'cousin', + 'relationship_type_cousin_with_name' => ':name’s cousin', + 'relationship_type_cousin_female_with_name' => ':name’s cousin', + 'relationship_type_cousin_male_with_name' => ':name’s cousin', + + 'relationship_type_godfather' => 'godparent', + 'relationship_type_godfather_female' => 'godmother', + 'relationship_type_godfather_male' => 'godfather', + 'relationship_type_godfather_with_name' => ':name’s godparent', + 'relationship_type_godfather_female_with_name' => ':name’s godmother', + 'relationship_type_godfather_male_with_name' => ':name’s godfather', + + 'relationship_type_godson' => 'godchild', + 'relationship_type_godson_female' => 'goddaughter', + 'relationship_type_godson_male' => 'godson', + 'relationship_type_godson_with_name' => ':name’s godchild', + 'relationship_type_godson_female_with_name' => ':name’s goddaughter', + 'relationship_type_godson_male_with_name' => ':name’s godson', + + 'relationship_type_friend' => 'friend', + 'relationship_type_friend_female' => 'friend', + 'relationship_type_friend_male' => 'friend', + 'relationship_type_friend_with_name' => ':name’s friend', + 'relationship_type_friend_female_with_name' => ':name’s friend', + 'relationship_type_friend_male_with_name' => ':name’s friend', + + 'relationship_type_bestfriend' => 'best friend', + 'relationship_type_bestfriend_female' => 'best friend', + 'relationship_type_bestfriend_male' => 'best friend', + 'relationship_type_bestfriend_with_name' => ':name’s best friend', + 'relationship_type_bestfriend_female_with_name' => ':name’s best friend', + 'relationship_type_bestfriend_male_with_name' => ':name’s best friend', + + 'relationship_type_colleague' => 'colleague', + 'relationship_type_colleague_female' => 'colleague', + 'relationship_type_colleague_male' => 'colleague', + 'relationship_type_colleague_with_name' => ':name’s colleague', + 'relationship_type_colleague_female_with_name' => ':name’s colleague', + 'relationship_type_colleague_male_with_name' => ':name’s colleague', + + 'relationship_type_boss' => 'boss', + 'relationship_type_boss_female' => 'boss', + 'relationship_type_boss_male' => 'boss', + 'relationship_type_boss_with_name' => ':name’s boss', + 'relationship_type_boss_female_with_name' => ':name’s boss', + 'relationship_type_boss_male_with_name' => ':name’s boss', + + 'relationship_type_subordinate' => 'subordinate', + 'relationship_type_subordinate_female' => 'subordinate', + 'relationship_type_subordinate_male' => 'subordinate', + 'relationship_type_subordinate_with_name' => ':name’s subordinate', + 'relationship_type_subordinate_female_with_name' => ':name’s subordinate', + 'relationship_type_subordinate_male_with_name' => ':name’s subordinate', + + 'relationship_type_mentor' => 'mentor', + 'relationship_type_mentor_female' => 'mentor', + 'relationship_type_mentor_male' => 'mentor', + 'relationship_type_mentor_with_name' => ':name’s mentor', + 'relationship_type_mentor_female_with_name' => ':name’s mentor', + 'relationship_type_mentor_male_with_name' => ':name’s mentor', + + 'relationship_type_protege' => 'protégé', + 'relationship_type_protege_female' => 'protégé', + 'relationship_type_protege_male' => 'protégé', + 'relationship_type_protege_with_name' => ':name’s protégé', + 'relationship_type_protege_female_with_name' => ':name’s protégé', + 'relationship_type_protege_male_with_name' => ':name’s protégé', + + 'relationship_type_ex_husband' => 'ex-spouse', + 'relationship_type_ex_husband_female' => 'ex-wife', + 'relationship_type_ex_husband_male' => 'ex-husband', + 'relationship_type_ex_husband_with_name' => ':name’s ex-spouse', + 'relationship_type_ex_husband_female_with_name' => ':name’s ex-wife', + 'relationship_type_ex_husband_male_with_name' => ':name’s ex-husband', + + // emotions + 'emotion_primary_love' => 'Love', + 'emotion_primary_joy' => 'Joy', + 'emotion_primary_surprise' => 'Surprise', + 'emotion_primary_anger' => 'Anger', + 'emotion_primary_sadness' => 'Sadness', + 'emotion_primary_fear' => 'Fear', + + 'emotion_secondary_affection' => 'Affection', + 'emotion_secondary_lust' => 'Lust', + 'emotion_secondary_longing' => 'Longing', + 'emotion_secondary_cheerfulness' => 'Cheerfulness', + 'emotion_secondary_zest' => 'Zest', + 'emotion_secondary_contentment' => 'Contentment', + 'emotion_secondary_pride' => 'Pride', + 'emotion_secondary_optimism' => 'Optimism', + 'emotion_secondary_enthrallment' => 'Enthrallment', + 'emotion_secondary_relief' => 'Relief', + 'emotion_secondary_surprise' => 'Surprise', + 'emotion_secondary_irritation' => 'Irritation', + 'emotion_secondary_exasperation' => 'Exasperation', + 'emotion_secondary_rage' => 'Rage', + 'emotion_secondary_disgust' => 'Disgust', + 'emotion_secondary_envy' => 'Envy', + 'emotion_secondary_suffering' => 'Suffering', + 'emotion_secondary_sadness' => 'Sadness', + 'emotion_secondary_disappointment' => 'Disappointment', + 'emotion_secondary_shame' => 'Shame', + 'emotion_secondary_neglect' => 'Neglect', + 'emotion_secondary_sympathy' => 'Sympathy', + 'emotion_secondary_horror' => 'Horror', + 'emotion_secondary_nervousness' => 'Nervousness', + + 'emotion_adoration' => 'Adoration', + 'emotion_affection' => 'Affection', + 'emotion_love' => 'Love', + 'emotion_fondness' => 'Fondness', + 'emotion_liking' => 'Liking', + 'emotion_attraction' => 'Attraction', + 'emotion_caring' => 'Caring', + 'emotion_tenderness' => 'Tenderness', + 'emotion_compassion' => 'Compassion', + 'emotion_sentimentality' => 'Sentimentality', + 'emotion_arousal' => 'Arousal', + 'emotion_desire' => 'Desire', + 'emotion_lust' => 'Lust', + 'emotion_passion' => 'Passion', + 'emotion_infatuation' => 'Infatuation', + 'emotion_longing' => 'Longing', + 'emotion_amusement' => 'Amusement', + 'emotion_bliss' => 'Bliss', + 'emotion_cheerfulness' => 'Cheerfulness', + 'emotion_gaiety' => 'Gaiety', + 'emotion_glee' => 'Glee', + 'emotion_jolliness' => 'Jolliness', + 'emotion_joviality' => 'Joviality', + 'emotion_joy' => 'Joy', + 'emotion_delight' => 'Delight', + 'emotion_enjoyment' => 'Enjoyment', + 'emotion_gladness' => 'Gladness', + 'emotion_happiness' => 'Happiness', + 'emotion_jubilation' => 'Jubilation', + 'emotion_elation' => 'Elation', + 'emotion_satisfaction' => 'Satisfaction', + 'emotion_ecstasy' => 'Ecstasy', + 'emotion_euphoria' => 'Euphoria', + 'emotion_enthusiasm' => 'Enthusiasm', + 'emotion_zeal' => 'Zeal', + 'emotion_zest' => 'Zest', + 'emotion_excitement' => 'Excitement', + 'emotion_thrill' => 'Thrill', + 'emotion_exhilaration' => 'Exhilaration', + 'emotion_contentment' => 'Contentment', + 'emotion_pleasure' => 'Pleasure', + 'emotion_pride' => 'Pride', + 'emotion_eagerness' => 'Eagerness', + 'emotion_hope' => 'Hope', + 'emotion_optimism' => 'Optimism', + 'emotion_enthrallment' => 'Enthrallment', + 'emotion_rapture' => 'Rapture', + 'emotion_relief' => 'Relief', + 'emotion_amazement' => 'Amazement', + 'emotion_surprise' => 'Surprise', + 'emotion_astonishment' => 'Astonishment', + 'emotion_aggravation' => 'Aggravation', + 'emotion_irritation' => 'Irritation', + 'emotion_agitation' => 'Agitation', + 'emotion_annoyance' => 'Annoyance', + 'emotion_grouchiness' => 'Grouchiness', + 'emotion_grumpiness' => 'Grumpiness', + 'emotion_exasperation' => 'Exasperation', + 'emotion_frustration' => 'Frustration', + 'emotion_anger' => 'Anger', + 'emotion_rage' => 'Rage', + 'emotion_outrage' => 'Outrage', + 'emotion_fury' => 'Fury', + 'emotion_wrath' => 'Wrath', + 'emotion_hostility' => 'Hostility', + 'emotion_ferocity' => 'Ferocity', + 'emotion_bitterness' => 'Bitterness', + 'emotion_hate' => 'Hate', + 'emotion_loathing' => 'Loathing', + 'emotion_scorn' => 'Scorn', + 'emotion_spite' => 'Spite', + 'emotion_vengefulness' => 'Vengefulness', + 'emotion_dislike' => 'Dislike', + 'emotion_resentment' => 'Resentment', + 'emotion_disgust' => 'Disgust', + 'emotion_revulsion' => 'Revulsion', + 'emotion_contempt' => 'Contempt', + 'emotion_envy' => 'Envy', + 'emotion_jealousy' => 'Jealousy', + 'emotion_agony' => 'Agony', + 'emotion_suffering' => 'Suffering', + 'emotion_hurt' => 'Hurt', + 'emotion_anguish' => 'Anguish', + 'emotion_depression' => 'Depression', + 'emotion_despair' => 'Despair', + 'emotion_hopelessness' => 'Hopelessness', + 'emotion_gloom' => 'Gloom', + 'emotion_glumness' => 'Glumness', + 'emotion_sadness' => 'Sadness', + 'emotion_unhappiness' => 'Unhappiness', + 'emotion_grief' => 'Grief', + 'emotion_sorrow' => 'Sorrow', + 'emotion_woe' => 'Woe', + 'emotion_misery' => 'Misery', + 'emotion_melancholy' => 'Melancholy', + 'emotion_dismay' => 'Dismay', + 'emotion_disappointment' => 'Disappointment', + 'emotion_displeasure' => 'Displeasure', + 'emotion_guilt' => 'Guilt', + 'emotion_shame' => 'Shame', + 'emotion_regret' => 'Regret', + 'emotion_remorse' => 'Remorse', + 'emotion_alienation' => 'Alienation', + 'emotion_isolation' => 'Isolation', + 'emotion_neglect' => 'Neglect', + 'emotion_loneliness' => 'Loneliness', + 'emotion_rejection' => 'Rejection', + 'emotion_homesickness' => 'Homesickness', + 'emotion_defeat' => 'Defeat', + 'emotion_dejection' => 'Dejection', + 'emotion_insecurity' => 'Insecurity', + 'emotion_embarrassment' => 'Embarrassment', + 'emotion_humiliation' => 'Humiliation', + 'emotion_insult' => 'Insult', + 'emotion_pity' => 'Pity', + 'emotion_sympathy' => 'Sympathy', + 'emotion_alarm' => 'Alarm', + 'emotion_shock' => 'Shock', + 'emotion_fear' => 'Fear', + 'emotion_fright' => 'Fright', + 'emotion_horror' => 'Horror', + 'emotion_terror' => 'Terror', + 'emotion_panic' => 'Panic', + 'emotion_hysteria' => 'Hysteria', + 'emotion_mortification' => 'Mortification', + 'emotion_anxiety' => 'Anxiety', + 'emotion_nervousness' => 'Nervousness', + 'emotion_tenseness' => 'Tenseness', + 'emotion_uneasiness' => 'Uneasiness', + 'emotion_apprehension' => 'Apprehension', + 'emotion_worry' => 'Worry', + 'emotion_distress' => 'Distress', + 'emotion_dread' => 'Dread', + + // weather + 'weather_sunny' => 'Sunny', + 'weather_clear' => 'Clear', + 'weather_clear-day' => 'Clear', + 'weather_clear-night' => 'Clear night', + 'weather_light-drizzle' => 'Light drizzle', + 'weather_patchy-light-drizzle' => 'Patchy light drizzle', + 'weather_patchy-light-rain' => 'Patchy light rain', + 'weather_light-rain' => 'Light rain', + 'weather_moderate-rain-at-times' => 'Moderate rain at times', + 'weather_moderate-rain' => 'Moderate rain', + 'weather_patchy-rain-possible' => 'Patchy rain possible', + 'weather_heavy-rain-at-times' => 'Heavy rain at times', + 'weather_heavy-rain' => 'Heavy rain', + 'weather_light-freezing-rain' => 'Light freezing rain', + 'weather_moderate-or-heavy-freezing-rain' => 'Moderate or heavy freezing rain', + 'weather_light-sleet' => 'Light sleet', + 'weather_moderate-or-heavy-rain-shower' => 'Moderate or heavy rain shower', + 'weather_light-rain-shower' => 'Light rain shower', + 'weather_torrential-rain-shower' => 'Torrential rain shower', + 'weather_rain' => 'Rain', + 'weather_snow' => 'Snow', + 'weather_blowing-snow' => 'Blowing snow', + 'weather_patchy-light-snow' => 'Patchy light snow', + 'weather_light-snow' => 'Light snow', + 'weather_patchy-moderate-snow' => 'Patchy moderate snow', + 'weather_moderate-snow' => 'Moderate snow', + 'weather_patchy-heavy-snow' => 'Patchy heavy snow', + 'weather_heavy-snow' => 'Heavy snow', + 'weather_light-snow-showers' => 'Light snow showers', + 'weather_moderate-or-heavy-snow-showers' => 'Moderate or heavy snow showers', + 'weather_patchy-snow-possible' => 'Patchy snow possible', + 'weather_patchy-sleet-possible' => 'Patchy sleet possible', + 'weather_moderate-or-heavy-sleet' => 'Moderate or heavy sleet', + 'weather_light-sleet-showers' => 'Light sleet showers', + 'weather_moderate-or-heavy-sleet-showers' => 'Moderate or heavy sleet showers', + 'weather_sleet' => 'Sleet', + 'weather_wind' => 'Wind', + 'weather_fog' => 'Fog', + 'weather_freezing-fog' => 'Freezing fog', + 'weather_mist' => 'Mist', + 'weather_blizzard' => 'Blizzard', + 'weather_overcast' => 'Overcast', + 'weather_cloudy' => 'Cloudy', + 'weather_partly-cloudy-day' => 'Partly cloudy', + 'weather_partly-cloudy-night' => 'Partly cloudy', + 'weather_freezing-drizzle' => 'Freezing drizzle', + 'weather_heavy-freezing-drizzle' => 'Heavy freezing drizzle', + 'weather_patchy-freezing-drizzle-possible' => 'Patchy freezing drizzle possible', + 'weather_ice-pellets' => 'Ice pellets', + 'weather_light-showers-of-ice-pellets' => 'Light showers of ice pellets', + 'weather_moderate-or-heavy-showers-of-ice-pellets' => 'Moderate or heavy showers of ice pellets', + 'weather_thundery-outbreaks-possible' => 'Thundery outbreaks possible', + 'weather_patchy-light-rain-with-thunder' => 'Patchy light rain with thunder', + 'weather_moderate-or-heavy-rain-with-thunder' => 'Moderate or heavy rain with thunder', + 'weather_patchy-light-snow-with-thunder' => 'Patchy light snow with thunder', + 'weather_moderate-or-heavy-snow-with-thunder' => 'Moderate or heavy snow with thunder', + 'weather_current_temperature_celsius' => ':temperature °C', + 'weather_current_temperature_fahrenheit' => ':temperature °F', + 'weather_current_title' => 'Current weather', + + // dav + 'dav_contacts' => 'Contacts', + 'dav_contacts_description' => ':name’s contacts', + 'dav_birthdays' => 'Birthdays', + 'dav_birthdays_description' => ':name’s contact’s birthdays', + 'dav_tasks' => 'Tasks', + 'dav_tasks_description' => ':name’s tasks', + + // contact list + 'contact_list_avatar' => 'Avatar', + 'contact_list_name' => 'Contact', + 'contact_list_description' => 'Description', + +]; diff --git a/resources/lang/en/auth.php b/resources/lang/en/auth.php new file mode 100644 index 0000000..73e0db0 --- /dev/null +++ b/resources/lang/en/auth.php @@ -0,0 +1,89 @@ + 'These credentials do not match our records.', + 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.', + 'not_authorized' => 'You are not authorized to execute this action', + 'signup_disabled' => 'Registration is currently disabled', + 'signup_error' => 'An error occured trying to register the user', + 'back_homepage' => 'Back to homepage', + 'mfa_auth_otp' => 'Authenticate with your two factor device', + 'mfa_auth_webauthn' => 'Authenticate with a security key (WebAuthn)', + '2fa_title' => 'Two Factor Authentication', + '2fa_wrong_validation' => 'The two factor authentication has failed.', + '2fa_one_time_password' => 'Two factor authentication code', + '2fa_recuperation_code' => 'Enter a two factor recovery code', + '2fa_one_time_or_recuperation' => 'Enter a two factor authentication code or a recovery code', + '2fa_otp_help' => 'Open up your two factor authentication mobile app and copy the code', + + 'login_to_account' => 'Login to your account', + 'login_with_recovery' => 'Login with a recovery code', + 'login_again' => 'Please login again to your account', + 'email' => 'Email', + 'password' => 'Password', + 'recovery' => 'Recovery code', + 'login' => 'Login', + 'button_remember' => 'Remember Me', + 'password_forget' => 'Forget your password?', + 'password_reset' => 'Reset your password', + 'use_recovery' => 'Or you can use a recovery code', + 'signup_no_account' => 'Don’t have an account?', + 'signup' => 'Sign up', + 'create_account' => 'Create the first account by signing up', + 'change_language_title' => 'Change language:', + 'change_language' => 'Change language to :lang', + + 'password_reset_title' => 'Reset Password', + 'password_reset_email' => 'E-Mail Address', + 'password_reset_send_link' => 'Send Password Reset Link', + 'password_reset_password' => 'Password', + 'password_reset_password_confirm' => 'Confirm Password', + 'password_reset_action' => 'Reset Password', + 'password_reset_email_content' => 'Click here to reset your password:', + + 'register_title_welcome' => 'Welcome to your newly installed Monica instance', + 'register_create_account' => 'You need to create an account to use Monica', + 'register_title_create' => 'Create your Monica account', + 'register_login' => 'Log in if you already have an account.', + 'register_email' => 'Enter a valid email address', + 'register_email_example' => 'you@home', + 'register_firstname' => 'First name', + 'register_firstname_example' => 'eg. John', + 'register_lastname' => 'Last name', + 'register_lastname_example' => 'eg. Doe', + 'register_password' => 'Password', + 'register_password_example' => 'Enter a secure password', + 'register_password_confirmation' => 'Password confirmation', + 'register_action' => 'Register', + 'register_policy' => 'Signing up signifies you’ve read and agree to our Privacy Policy and Terms of use.', + 'register_invitation_email' => 'For security purposes, please indicate the email of the person who’ve invited you to join this account. This information is provided in the invitation email.', + + 'confirmation_title' => 'Verify Your Email Address', + 'confirmation_fresh' => 'A fresh verification link has been sent to your email address.', + 'confirmation_check' => 'Before proceeding, please check your email for a verification link.', + 'confirmation_request_another' => 'If you did not receive the email click here to request another.', + + 'confirmation_again' => 'If you want to change your email address you can click here.', + 'email_change_current_email' => 'Current email address:', + 'email_change_title' => 'Change your email address', + 'email_change_new' => 'New email address', + 'email_changed' => 'Your email address has been changed. Check your mailbox to validate it.', +]; diff --git a/resources/lang/en/changelog.php b/resources/lang/en/changelog.php new file mode 100644 index 0000000..981b018 --- /dev/null +++ b/resources/lang/en/changelog.php @@ -0,0 +1,12 @@ + 'Product changes', + 'note' => 'Note: unfortunately, this page is only in English.', +]; diff --git a/resources/lang/en/dashboard.php b/resources/lang/en/dashboard.php new file mode 100644 index 0000000..5190352 --- /dev/null +++ b/resources/lang/en/dashboard.php @@ -0,0 +1,42 @@ + 'Welcome to your account!', + 'dashboard_blank_description' => 'Monica is the place to organize all the interactions you have with the people you care about.', + 'dashboard_blank_cta' => 'Add your first contact', + 'dashboard_blank_illustration' => 'Illustration by Freepik', + + 'notes_title' => 'You don’t have any starred notes yet.', + + 'tab_recent_calls' => 'Recent calls', + 'tab_favorite_notes' => 'Favorite notes', + 'tab_calls_blank' => 'You haven’t logged any calls yet.', + 'tab_debts' => 'Debts', + 'tab_debts_blank' => 'You haven’t logged any debts yet.', + 'tab_tasks' => 'Tasks', + 'tab_tasks_blank' => 'You haven’t any tasks yet.', + + 'tasks_add_task_placeholder' => 'What is this task about?', + 'tasks_tab_your_contacts' => 'Tasks related to your contacts', + 'tasks_tab_your_tasks' => 'Your tasks', + 'tasks_add_note' => 'Press Enter to add the task.', + 'task_add_cta' => 'Add a task', + + 'debts_you_owe' => 'You owe', + + 'statistics_contacts' => 'Contacts', + 'statistics_activities' => 'Activities', + 'statistics_gifts' => 'Gifts', + + 'reminders_next_months' => 'Events in the next 3 months', + 'reminders_none' => 'No reminders for this month.', + + 'product_changes' => 'Product changes', + 'product_view_details' => 'View details', +]; diff --git a/resources/lang/en/format.php b/resources/lang/en/format.php new file mode 100644 index 0000000..a70a6ba --- /dev/null +++ b/resources/lang/en/format.php @@ -0,0 +1,36 @@ + 'M d, Y H:i', + 'short_date_year' => 'M d, Y', + 'short_date' => 'M d', + 'short_month' => 'M', + 'short_month_year' => 'M Y', + 'short_day' => 'D', + 'full_date_year' => 'F d, Y', + 'full_month' => 'F', + 'full_month_year' => 'F Y', + 'full_hour' => 'h.i A', + + 'short_text' => '{text}…', +]; diff --git a/resources/lang/en/journal.php b/resources/lang/en/journal.php new file mode 100644 index 0000000..9b1f0be --- /dev/null +++ b/resources/lang/en/journal.php @@ -0,0 +1,38 @@ + 'How was your day? You can rate it once a day.', + 'journal_come_back' => 'Thanks. Come back tomorrow to rate your day again.', + 'journal_description' => 'Note: the journal lists both manual journal entries, and automatic entries like Activities done with your contacts. While you can delete journal entries manually, you’ll have to delete the activity directly on the contact page.', + 'journal_add' => 'Add a journal entry', + 'journal_edit' => 'Edit a journal entry', + 'journal_empty' => 'Empty journal', + 'journal_created_at' => 'Created at {date}', + 'journal_created_automatically' => 'Created automatically', + 'journal_entry_type_journal' => 'Journal entry', + 'journal_entry_type_activity' => 'Activity', + 'journal_entry_rate' => 'You rated your day.', + 'journal_add_comment' => 'Care to add a comment (optional)?', + 'journal_show_comment' => 'Show comment', + 'entry_delete_success' => 'The journal entry has been successfully deleted.', + 'journal_add_title' => 'Title (optional)', + 'journal_add_date' => 'Date', + 'journal_add_post' => 'Entry', + 'journal_add_cta' => 'Save', + 'journal_blank_cta' => 'Add your first journal entry', + 'journal_blank_description' => 'The journal lets you write events that happened to you, and remember them.', + 'delete_confirmation' => 'Are you sure you want to delete this journal entry?', + 'apply_filter' => 'Apply filter', + 'start_date' => 'Start Date', + 'end_date' => 'End Date', + 'per_page' => 'Per Page', + 'sort_order' => 'Sort By Created At', + 'ascending' => 'Ascending', + 'descending' => 'Descending', +]; diff --git a/resources/lang/en/logs.php b/resources/lang/en/logs.php new file mode 100644 index 0000000..7b6654b --- /dev/null +++ b/resources/lang/en/logs.php @@ -0,0 +1,29 @@ + 'Created the contact.', + 'settings_log_contact_created_with_name' => 'Added :name as a contact.', + + // contat description update + 'contact_log_contact_description_updated' => 'Updated the description.', + 'settings_log_contact_description_updated_with_name' => 'Updated the description of :name.', + + // contact description clear + 'contact_log_contact_description_cleared' => 'Cleared the description.', + 'settings_log_contact_description_cleared_with_name' => 'Cleared the description of :name.', + + // contact work information update + 'contact_log_contact_work_updated' => 'Updated work information.', + 'settings_log_contact_work_updated_with_name' => 'Updated work information of :name.', + + // company created + 'settings_log_company_created' => 'Created a company called :name.', +]; diff --git a/resources/lang/en/mail.php b/resources/lang/en/mail.php new file mode 100644 index 0000000..749f3d1 --- /dev/null +++ b/resources/lang/en/mail.php @@ -0,0 +1,53 @@ + 'Reminder for :contact', + 'greetings' => 'Hi :username', + 'want_reminded_of' => 'You wanted to be reminded of :reason', + 'for' => 'For: :name', + 'comment' => 'Comment: :comment', + 'footer_contact_info' => 'Add, view, complete, and change information about this contact:', + 'footer_contact_info2' => 'See :name’s profile', + 'footer_contact_info2_link' => 'See :name’s profile: :url', + + 'notification_subject_line' => 'You have an upcoming event', + 'notification_description' => 'In :count days (on :date), the following event will happen:', + + 'stay_in_touch_subject_line' => 'Stay in touch with :name', + 'stay_in_touch_subject_description' => 'You asked to be reminded to stay in touch with :name every :frequency day.|You asked to be reminded to stay in touch with :name every :frequency days.', + + 'notifications_whoops' => 'Whoops!', + 'notifications_hello' => 'Hello!', + 'notifications_regards' => 'Regards', + 'notifications_footer' => 'If you’re having trouble clicking the ":actionText" button, copy and paste the URL below into your web browser: [:actionURL](:actionURL)', + 'notifications_rights' => 'All rights reserved', + + 'confirmation_email_title' => 'Monica – Email verification', + 'confirmation_email_intro'=> 'To validate your email click on the button below', + 'confirmation_email_button' => 'Verify email address', + 'confirmation_email_bottom' => 'If you did not create an account, no further action is required.', + + 'password_reset_title' => 'Monica – Reset Password Notification', + 'password_reset_intro' => 'You are receiving this email because we received a password reset request for your account.', + 'password_reset_button' => 'Reset Password', + 'password_reset_expiration' => 'This password reset link will expire in :count minutes.', + 'password_reset_bottom' => 'If you did not request a password reset, no further action is required.', + + 'invitation_title' => 'Monica – You are invited by :name', + 'invitation_intro' => 'You’ve been invited by :name (:email) to use Monica, a nice Personal Relationship Management tool.', + 'invitation_link' => 'To accept the invitation, click on the link below:', + 'invitation_button' => 'Accept invitation', + 'invitation_expiration' => 'This link will expire in :count days.', + + 'export_title' => 'Your export is ready', + 'export_description' => 'You requested a data export on :date. It is now ready to download.', + 'export_download' => 'Download export', + +]; diff --git a/resources/lang/en/pagination.php b/resources/lang/en/pagination.php new file mode 100644 index 0000000..d663041 --- /dev/null +++ b/resources/lang/en/pagination.php @@ -0,0 +1,25 @@ + '❮ Previous', + 'next' => 'Next ❯', + +]; diff --git a/resources/lang/en/passwords.php b/resources/lang/en/passwords.php new file mode 100644 index 0000000..1487bb9 --- /dev/null +++ b/resources/lang/en/passwords.php @@ -0,0 +1,30 @@ + 'Your password has been reset!', + 'sent' => 'If the email you entered exists in our records, you’ve been sent a password reset link.', + 'token' => 'This password reset token is invalid.', + 'user' => 'If the email you entered exists in our records, you’ve been sent a password reset link.', + 'changed' => 'Password changed successfully.', + 'invalid' => 'Current password you entered is not correct.', + 'throttled' => 'Please wait before retrying.', + +]; diff --git a/resources/lang/en/people.php b/resources/lang/en/people.php new file mode 100644 index 0000000..e049195 --- /dev/null +++ b/resources/lang/en/people.php @@ -0,0 +1,539 @@ + 'Contact not found', + 'people_list_number_kids' => ':count child|:count children', + 'people_list_last_updated' => 'Last consulted:', + 'people_list_number_reminders' => ':count reminder|:count reminders', + 'people_list_blank_title' => 'You don’t have anyone in your account yet', + 'people_list_blank_cta' => 'Add someone', + 'people_list_sort' => 'Sort', + 'people_list_stats' => ':count contact|:count contacts', + 'people_list_firstnameAZ' => 'Sort by first name A → Z', + 'people_list_firstnameZA' => 'Sort by first name Z → A', + 'people_list_lastnameAZ' => 'Sort by last name A → Z', + 'people_list_lastnameZA' => 'Sort by last name Z → A', + 'people_list_lastactivitydateNewtoOld' => 'Sort by last activity date, newest to oldest', + 'people_list_lastactivitydateOldtoNew' => 'Sort by last activity date, oldest to newest', + 'people_list_filter_tag' => 'Showing all the contacts tagged with', + 'people_list_clear_filter' => 'Clear filter', + 'people_list_contacts_per_tags' => ':count contact|:count contacts', + 'people_list_show_dead' => 'Show deceased people (:count)', + 'people_list_hide_dead' => 'Hide deceased people (:count)', + 'people_search' => 'Search your contacts…', + 'people_search_no_results' => 'No results found', + 'people_search_next' => 'Next', + 'people_search_prev' => 'Previous', + 'people_search_rows_per_page' => 'Rows per page', + 'people_search_of' => 'of', + 'people_search_page' => 'Page', + 'people_search_all' => 'All', + 'people_add_new' => 'Add new person', + 'people_list_account_usage' => 'Your account usage: :current/:limit contacts', + 'people_list_account_upgrade_title' => 'Upgrade your account to unlock it to its full potential.', + 'people_list_account_upgrade_cta' => 'Upgrade now', + 'people_list_untagged' => 'View untagged contacts', + 'people_list_filter_untag' => 'Showing all untagged contacts', + 'archived_contact_readonly' => 'Archived contact can’t be edited, please unarchive it first.', + + // people add + 'people_add_title' => 'Add a new person', + 'people_add_missing' => 'No person found – add a new one now', + 'people_add_firstname' => 'First name', + 'people_add_middlename' => 'Middle name (optional)', + 'people_add_lastname' => 'Last name (optional)', + 'people_add_email' => 'Email (optional)', + 'people_add_nickname' => 'Nickname (optional)', + 'people_add_cta' => 'Add', + 'people_save_and_add_another_cta' => 'Submit and add someone else', + 'people_add_success' => ':name has been successfully created', + 'people_add_gender' => 'Gender', + 'people_delete_success' => 'The contact has been deleted', + 'people_delete_message' => 'Delete contact', + 'people_delete_confirmation' => 'Are you sure you want to delete :name’s contact? Deletion is immediate and permanent.', + 'people_add_birthday_reminder' => 'Wish happy birthday to :name', + 'people_add_birthday_reminder_deceased' => 'On this date, :name would have celebrated their birthday', + 'people_add_import' => 'Do you want to import your contacts?', + 'people_edit_email_error' => 'There is already a contact in your account with this email address. Please choose another one.', + 'people_export' => 'Export as vCard', + 'people_add_reminder_for_birthday' => 'Create an annual birthday reminder', + + // show + 'section_contact_information' => 'Contact information', + 'section_personal_activities' => 'Activities', + 'section_personal_reminders' => 'Reminders', + 'section_personal_tasks' => 'Tasks', + 'section_personal_gifts' => 'Gifts', + 'section_personal_notes' => 'Notes', + + // archived contacts + 'list_link_to_active_contacts' => 'You are viewing archived contacts. See the list of active contacts instead.', + 'list_link_to_archived_contacts' => 'List of archived contacts', + + // Header + 'me' => 'This is you', + 'edit_contact_information' => 'Edit contact information', + 'contact_archive' => 'Archive contact', + 'contact_unarchive' => 'Unarchive contact', + 'contact_archive_help' => 'Archived contacts are not be shown on the contact list, but still appear in search results.', + 'call_button' => 'Log a call', + 'set_favorite' => 'Favorite contacts are placed at the top of the contact list', + + // Stay in touch + 'stay_in_touch' => 'Stay in touch', + 'stay_in_touch_frequency' => 'Stay in touch every day|Stay in touch every {count} days', + 'stay_in_touch_next_date' => 'Next due: {date}', + 'stay_in_touch_invalid' => 'The frequency must be a number greater than 0.', + 'stay_in_touch_premium' => 'You need to upgrade your account to make use of this feature', + 'stay_in_touch_modal_title' => 'Stay in touch', + 'stay_in_touch_modal_desc' => 'We can remind you by email to keep in touch with {firstname} at a regular interval.', + 'stay_in_touch_modal_label' => 'Send me an email every… {count} day|Send me an email every… {count} days', + + // Calls + 'modal_call_title' => 'Log a call', + 'modal_call_comment' => 'What did you talk about? (optional)', + 'modal_call_exact_date' => 'The phone call happened on', + 'modal_call_who_called' => 'Who called?', + 'modal_call_emotion' => 'Do you want to log how you felt during this call? (optional)', + 'calls_add_success' => 'The phone call has been saved.', + 'call_delete_confirmation' => 'Are you sure you want to delete this call?', + 'call_delete_success' => 'The call has been deleted successfully', + 'call_title' => 'Phone calls', + 'call_empty_comment' => 'No details', + 'call_blank_title' => 'Keep track of the phone calls you’ve done with {name}', + 'call_blank_desc' => 'You called {name}', + 'call_you_called' => 'You called', + 'call_he_called' => '{name} called', + 'call_emotions' => 'Emotions:', + + // Conversation + 'conversation_blank' => 'Record conversations you have with :name on social media, SMS…', + 'conversation_delete_link' => 'Delete the conversation', + 'conversation_edit_title' => 'Edit conversation', + 'conversation_edit_delete' => 'Are you sure you want to delete this conversation? Deletion is permanent.', + 'conversation_add_success' => 'The conversation has been successfully added.', + 'conversation_edit_success' => 'The conversation has been successfully updated.', + 'conversation_delete_success' => 'The conversation has been successfully deleted.', + 'conversation_add_title' => 'Record a new conversation', + 'conversation_add_when' => 'When did you have this conversation?', + 'conversation_add_who_wrote' => 'Who sent this message?', + 'conversation_add_how' => 'How did you communicate?', + 'conversation_add_you' => 'You', + 'conversation_add_content' => 'Write down what was said', + 'conversation_add_what_was_said' => 'What did you say?', + 'conversation_add_another' => 'Add another message', + 'conversation_add_error' => 'You must add at least one message.', + 'conversation_list_table_messages' => 'Messages', + 'conversation_list_table_content' => 'Partial content (last message)', + 'conversation_list_title' => 'Conversations', + 'conversation_list_cta' => 'Log conversation', + + // age - birthday + 'birthdate_not_set' => 'Birthday is not set', + 'age_approximate_in_years' => 'around :age years old', + 'age_exact_in_years' => ':age years old', + 'age_exact_birthdate' => 'born :date', + + // Last called + 'last_called' => 'Last called: :date', + 'last_talked_to' => 'Last called: {date}', + 'last_called_empty' => 'Last called: unknown', + 'last_activity_date' => 'Last activity together: :date', + 'last_activity_date_empty' => 'Last activity together: unknown', + + // additional information + 'information_edit_success' => 'The profile has been updated successfully', + 'information_edit_title' => 'Edit :name’s personal information', + 'information_edit_max_size' => 'Max :size Kb.', + 'information_edit_max_size2' => 'Max {size} Kb.', + 'information_edit_firstname' => 'First name', + 'information_edit_lastname' => 'Last name (optional)', + 'information_edit_description' => 'Description (optional)', + 'information_edit_description_help' => 'Used on the contact list to add some context, if necessary.', + 'information_edit_unknown' => 'I do not know this person’s age', + 'information_edit_probably' => 'This person is probably…', + 'information_edit_not_year' => 'I know the day and month of this person’s birthday, but not the year…', + 'information_edit_exact' => 'I know this person’s exact birthday…', + 'information_edit_birthdate_label' => 'Birthday', + 'information_no_work_defined' => 'No work information defined', + 'information_work_at' => 'at :company', + 'work_add_cta' => 'Update work information', + 'work_edit_success' => 'Work information updated', + 'work_edit_title' => 'Update :name’s job information', + 'work_edit_job' => 'Job title (optional)', + 'work_edit_company' => 'Company (optional)', + 'work_information' => 'Work information', + + // food preferences + 'food_preferences_add_success' => 'Food preferences have been saved', + 'food_preferences_edit_description' => 'Perhaps :firstname or someone in the :family’s family has an allergy. Or doesn’t like a specific bottle of wine. Indicate them here so you will remember it next time you invite them for dinner', + 'food_preferences_edit_description_no_last_name' => 'Perhaps :firstname has an allergy. Or doesn’t like a specific bottle of wine. Indicate them here so you will remember it next time you invite them for dinner', + 'food_preferences_edit_title' => 'Indicate food preferences', + 'food_preferences_edit_cta' => 'Save food preferences', + 'food_preferences_title' => 'Food preferences', + 'food_preferences_cta' => 'Add food preferences', + + // reminders + 'reminders_blank_title' => 'Is there something you want to be reminded of about :name?', + 'reminders_blank_add_activity' => 'Add a reminder', + 'reminders_add_title' => 'What would you like to be reminded of about :name?', + 'reminders_add_description' => 'Please remind me to…', + 'reminders_add_next_time' => 'When is the next time you would like to be reminded about this?', + 'reminders_add_once' => 'Remind me about this just once', + 'reminders_add_recurrent' => 'Remind me about this every', + 'reminders_add_starting_from' => 'starting from the date specified above', + 'reminders_add_cta' => 'Add reminder', + 'reminders_edit_update_cta' => 'Update reminder', + 'reminders_add_error_custom_text' => 'You need to indicate a text for this reminder', + 'reminders_create_success' => 'The reminder has been added successfully', + 'reminders_delete_success' => 'The reminder has been deleted successfully', + 'reminders_update_success' => 'The reminder has been updated successfully', + 'reminders_add_optional_comment' => 'Optional comment', + + 'reminder_frequency_day' => 'every day|every :number days', + 'reminder_frequency_week' => 'every week|every :number weeks', + 'reminder_frequency_month' => 'every month|every :number months', + 'reminder_frequency_year' => 'every year|every :number year', + 'reminder_frequency_one_time' => 'on :date', + 'reminders_delete_confirmation' => 'Are you sure you want to delete this reminder?', + 'reminders_delete_cta' => 'Delete', + 'reminders_next_expected_date' => 'on', + 'reminders_cta' => 'Add a reminder', + 'reminders_description' => 'We will send an email for each one of the reminders below. Reminders are sent every morning the day events will happen. Reminders automatically added for birthdays can not be deleted. If you want to change those dates, edit the birthday of the contacts.', + 'reminders_one_time' => 'One time', + 'reminders_type_week' => 'week', + 'reminders_type_month' => 'month', + 'reminders_type_year' => 'year', + 'reminders_birthday' => 'Birthday of :name', + 'reminders_free_plan_warning' => 'You are on the Free plan. No emails are sent on this plan. To receive your reminders by email, upgrade your account.', + + // relationships + 'relationship_form_add' => 'Add a new relationship', + 'relationship_form_edit' => 'Edit an existing relationship', + 'relationship_form_is_with' => 'This person is…', + 'relationship_form_is_with_name' => ':name is…', + 'relationship_form_add_choice' => 'Who is the relationship with?', + 'relationship_form_create_contact' => 'Add a new person', + 'relationship_form_associate_contact' => 'An existing contact', + 'relationship_form_associate_dropdown' => 'Search and select an existing contact from the dropdown below', + 'relationship_form_associate_dropdown_placeholder' => 'Search and select an existing contact', + 'relationship_form_also_create_contact' => 'Create a Contact entry for this person.', + 'relationship_form_add_description' => 'This will let you treat this person like any other contact.', + 'relationship_form_add_no_existing_contact' => 'You don’t have any contacts who can be related to :name at the moment.', + 'relationship_delete_confirmation' => 'Are you sure you want to delete this relationship? Deletion is permanent.', + 'relationship_unlink_confirmation' => 'Are you sure you want to delete this relationship? This person will not be deleted – only the relationship between the two.', + 'relationship_form_add_success' => 'The relationship has been successfully set.', + 'relationship_form_deletion_success' => 'The relationship has been deleted.', + + // tasks + 'tasks_title' => 'Tasks', + 'tasks_blank_title' => 'You don’t have any tasks yet.', + 'tasks_form_title' => 'Title', + 'tasks_form_description' => 'Description (optional)', + 'tasks_add_task' => 'Add a task', + 'tasks_delete_success' => 'The task has been deleted successfully', + 'tasks_complete_success' => 'The task has changed status successfully', + + // activities + 'activity_title' => 'Activities', + 'activity_type_category_simple_activities' => 'Simple activities', + 'activity_type_category_sport' => 'Sport', + 'activity_type_category_food' => 'Food', + 'activity_type_category_cultural_activities' => 'Cultural activities', + 'activity_type_just_hung_out' => 'just hung out', + 'activity_type_watched_movie_at_home' => 'watched a movie at home', + 'activity_type_talked_at_home' => 'just talked at home', + 'activity_type_did_sport_activities_together' => 'played a sport together', + 'activity_type_ate_at_his_place' => 'ate at their place', + 'activity_type_went_bar' => 'went to a bar', + 'activity_type_ate_at_home' => 'ate at home', + 'activity_type_picnicked' => 'picnicked', + 'activity_type_ate_restaurant' => 'ate at a restaurant', + 'activity_type_went_theater' => 'went to the theater', + 'activity_type_went_concert' => 'went to a concert', + 'activity_type_went_play' => 'went to a play', + 'activity_type_went_museum' => 'went to the museum', + 'activities_add_activity' => 'Add activity', + 'activities_add_more_details' => 'Add more details', + 'activities_add_emotions' => 'Add emotions', + 'activities_add_category' => 'Indicate a category', + 'activities_add_participants_cta' => 'Add participants', + 'activities_item_information' => ':Activity. Happened on :date', + 'activities_add_title' => 'What did you do with {name}?', + 'activities_summary' => 'Describe what you did', + 'activities_add_pick_activity' => 'Would you like to categorize this activity? You don’t have to, but it will give you statistics later on (optional)', + 'activities_add_date_occured' => 'The activity happened on…', + 'activities_add_participants' => 'Who, apart from {name}, participated in this activity? (optional)', + 'activities_add_emotions_title' => 'Do you want to log how you felt during this activity? (optional)', + 'activities_blank_title' => 'Keep track of what you’ve done with {name} in the past, and what you’ve talked about', + 'activities_blank_add_activity' => 'Add an activity', + 'activities_add_success' => 'The activity has been added successfully', + 'activities_add_error' => 'Error when adding the activity', + 'activities_update_success' => 'The activity has been updated successfully', + 'activities_delete_success' => 'The activity has been deleted successfully', + 'activities_who_was_involved' => 'Who was involved?', + 'activities_activity' => 'Activity Category', + 'activities_view_activities_report' => 'View activities report', + 'activities_profile_title' => 'Activities report between :name and you', + 'activities_profile_subtitle' => 'You’ve logged :total_activities activity with :name in total and :activities_last_twelve_months in the last 12 months so far.|You’ve logged :total_activities activities with :name in total and :activities_last_twelve_months in the last 12 months so far.', + 'activities_profile_year_summary_activity_types' => 'Here is a breakdown of the type of activities you’ve done together in :year', + 'activities_profile_year_summary' => 'Here is what you two have done in :year', + 'activities_profile_number_occurences' => ':value activity|:value activities', + 'activities_list_participants' => 'Participants ({total}):', + 'activities_list_emotions' => 'Emotions felt:', + 'activities_list_date' => 'Happened on', + 'activities_list_category' => 'Category:', + + // notes + 'notes_create_success' => 'The note has been created successfully', + 'notes_update_success' => 'The note has been saved successfully', + 'notes_delete_success' => 'The note has been deleted successfully', + 'notes_add_cta' => 'Add note', + 'notes_favorite' => 'Add/remove from favorites', + 'notes_delete_title' => 'Delete a note', + 'notes_delete_confirmation' => 'Are you sure you want to delete this note? Deletion is permanent', + + // gifts + 'gifts_title' => 'Gifts', + 'gifts_add_success' => 'The gift has been added successfully', + 'gifts_delete_success' => 'The gift has been deleted successfully', + 'gifts_delete_confirmation' => 'Are you sure you want to delete this gift?', + 'gifts_add_gift' => 'Add a gift', + 'gifts_link' => 'Link', + 'gifts_for' => 'For: {name}', + 'gifts_delete_cta' => 'Delete', + 'gifts_add_title' => 'Gift management for :name', + 'gifts_add_gift_idea' => 'Gift idea', + 'gifts_add_gift_already_offered' => 'Gift given', + 'gifts_add_gift_received' => 'Gift received', + 'gifts_add_gift_title' => 'What is this gift?', + 'gifts_add_gift_name' => 'Gift name', + 'gifts_add_link' => 'Link to the web page (optional)', + 'gifts_add_value' => 'Value (optional)', + 'gifts_add_comment' => 'Comment (optional)', + 'gifts_add_recipient' => 'Recipient (optional)', + 'gifts_add_recipient_field' => 'Recipient', + 'gifts_add_photo' => 'Photo (optional)', + 'gifts_add_photo_title' => 'Add a photo for this gift', + 'gifts_add_someone' => 'This gift is for someone in {name}’s family in particular', + 'gifts_delete_title' => 'Delete a gift', + 'gifts_ideas' => 'Gift ideas', + 'gifts_offered' => 'Gifts given', + 'gifts_offered_as_an_idea' => 'Mark as an idea', + 'gifts_received' => 'Gifts received', + 'gifts_view_comment' => 'View comment', + 'gifts_mark_offered' => 'Mark as given', + 'gifts_update_success' => 'The gift has been updated successfully', + 'gifts_add_date' => 'Date (optional)', + + // debts + 'debt_delete_confirmation' => 'Are you sure you want to delete this debt?', + 'debt_delete_success' => 'The debt has been deleted successfully', + 'debt_add_success' => 'The debt has been added successfully', + 'debt_title' => 'Debts', + 'debt_add_cta' => 'Add debt', + 'debt_you_owe' => 'You owe :amount', + 'debt_they_owe' => ':name owes you :amount', + 'debt_add_title' => 'Debt management', + 'debt_add_you_owe' => 'You owe :name', + 'debt_add_they_owe' => ':name owes you', + 'debt_add_amount' => 'the sum of', + 'debt_add_reason' => 'for the following reason (optional)', + 'debt_add_add_cta' => 'Add debt', + 'debt_edit_update_cta' => 'Update debt', + 'debt_edit_success' => 'The debt has been updated successfully', + 'debts_blank_title' => 'Manage debts you owe to :name or :name owes you', + + // tags + 'tag_edit' => 'Edit tag', + 'tag_add' => 'Add tags', + 'tag_add_search' => 'Add or search tags', + 'tag_no_tags' => 'No tags yet', + + // Introductions + 'introductions_sidebar_title' => 'How you met', + 'introductions_blank_cta' => 'Indicate how you met :name', + 'introductions_title_edit' => 'How did you meet :name?', + 'introductions_additional_info' => 'Explain how and where you met', + 'introductions_edit_met_through' => 'Has someone introduced you to this person?', + 'introductions_no_met_through' => 'No one', + 'introductions_first_met_date' => 'Date you met', + 'introductions_no_first_met_date' => 'I don’t know the date we met', + 'introductions_first_met_date_known' => 'This is the date we met', + 'introductions_add_reminder' => 'Add a reminder to celebrate this encounter on the anniversary this event happened', + 'introductions_update_success' => 'You’ve successfully updated the information about how you met this person', + 'introductions_met_through' => 'Met through :name', + 'introductions_met_date' => 'Met on :date', + 'introductions_reminder_title' => 'Anniversary of the day you first met', + + // Deceased + 'deceased_reminder_title' => 'Anniversary of the death of :name', + 'deceased_mark_person_deceased' => 'Mark this as deceased', + 'deceased_know_date' => 'I know the date that this person died', + 'deceased_add_reminder' => 'Add a reminder for this date', + 'deceased_label' => 'Deceased', + 'deceased_date_label' => 'Deceased date', + 'deceased_label_with_date' => 'Deceased on :date', + 'deceased_age' => 'Age at death', + + // Contact information + 'contact_info_title' => 'Contact information', + 'contact_info_form_content' => 'Content', + 'contact_info_form_contact_type' => 'Contact type', + 'contact_info_form_personalize' => 'Personalize', + 'contact_info_address' => 'Lives in', + + // Addresses + 'contact_address_title' => 'Addresses', + 'contact_address_form_name' => 'Label (optional)', + 'contact_address_form_street' => 'Street (optional)', + 'contact_address_form_city' => 'City (optional)', + 'contact_address_form_province' => 'Province (optional)', + 'contact_address_form_postal_code' => 'Postal code (optional)', + 'contact_address_form_country' => 'Country (optional)', + 'contact_address_form_latitude' => 'Latitude (numbers only) (optional)', + 'contact_address_form_longitude' => 'Longitude (numbers only) (optional)', + + // Pets + 'pets_kind' => 'Kind of pet', + 'pets_name' => 'Name (optional)', + 'pets_create_success' => 'The pet has been successfully added', + 'pets_update_success' => 'The pet has been updated', + 'pets_delete_success' => 'The pet has been deleted', + 'pets_title' => 'Pets', + 'pets_reptile' => 'Reptile', + 'pets_bird' => 'Bird', + 'pets_cat' => 'Cat', + 'pets_dog' => 'Dog', + 'pets_fish' => 'Fish', + 'pets_hamster' => 'Hamster', + 'pets_horse' => 'Horse', + 'pets_rabbit' => 'Rabbit', + 'pets_rat' => 'Rat', + 'pets_small_animal' => 'Small animal', + 'pets_other' => 'Other', + + // life events + 'life_event_list_tab_life_events' => 'Life events', + 'life_event_list_tab_other' => 'Notes, reminders, …', + 'life_event_list_title' => 'Life events', + 'life_event_blank' => 'Log what happens to the life of {name} for your future reference.', + 'life_event_list_cta' => 'Add life event', + 'life_event_create_category' => 'All categories', + 'life_event_create_life_event' => 'Add life event', + 'life_event_create_default_title' => 'Title (optional)', + 'life_event_create_default_story' => 'Story (optional)', + 'life_event_create_date' => 'You do not need to indicate a month or a day – only the year is mandatory.', + 'life_event_create_default_description' => 'Add information about what you know', + 'life_event_create_add_yearly_reminder' => 'Add a yearly reminder for this event', + 'life_event_create_success' => 'The life event has been added', + 'life_event_delete_title' => 'Delete a life event', + 'life_event_delete_description' => 'Are you sure you want to delete this life event? Deletion is permanent.', + 'life_event_delete_success' => 'The life event has been deleted', + 'life_event_date_it_happened' => 'Date it happened', + 'life_event_category_work_education' => 'Work & education', + 'life_event_category_family_relationships' => 'Family & relationships', + 'life_event_category_home_living' => 'Home & living', + 'life_event_category_health_wellness' => 'Health & wellness', + 'life_event_category_travel_experiences' => 'Travel & experiences', + 'life_event_sentence_new_job' => 'Started a new job', + 'life_event_sentence_retirement' => 'Retired', + 'life_event_sentence_new_school' => 'Started school', + 'life_event_sentence_study_abroad' => 'Studied abroad', + 'life_event_sentence_volunteer_work' => 'Started volunteering', + 'life_event_sentence_published_book_or_paper' => 'Published a paper', + 'life_event_sentence_military_service' => 'Started military service', + 'life_event_sentence_new_relationship' => 'Started a relationship', + 'life_event_sentence_engagement' => 'Got engaged', + 'life_event_sentence_marriage' => 'Got married', + 'life_event_sentence_anniversary' => 'Anniversary', + 'life_event_sentence_expecting_a_baby' => 'Expects a baby', + 'life_event_sentence_new_child' => 'Had a child', + 'life_event_sentence_new_family_member' => 'Added a family member', + 'life_event_sentence_new_pet' => 'Got a pet', + 'life_event_sentence_end_of_relationship' => 'Ended a relationship', + 'life_event_sentence_loss_of_a_loved_one' => 'Lost a loved one', + 'life_event_sentence_moved' => 'Moved', + 'life_event_sentence_bought_a_home' => 'Bought a home', + 'life_event_sentence_home_improvement' => 'Made a home improvement', + 'life_event_sentence_holidays' => 'Went on holidays', + 'life_event_sentence_new_vehicle' => 'Got a new vehicle', + 'life_event_sentence_new_roommate' => 'Got a roommate', + 'life_event_sentence_overcame_an_illness' => 'Overcame an illness', + 'life_event_sentence_quit_a_habit' => 'Quit a habit', + 'life_event_sentence_new_eating_habits' => 'Started new eating habits', + 'life_event_sentence_weight_loss' => 'Lost weight', + 'life_event_sentence_wear_glass_or_contact' => 'Started to wear glass or contact lenses', + 'life_event_sentence_broken_bone' => 'Broke a bone', + 'life_event_sentence_removed_braces' => 'Removed braces', + 'life_event_sentence_surgery' => 'Had surgery', + 'life_event_sentence_dentist' => 'Went to the dentist', + 'life_event_sentence_new_sport' => 'Started a sport', + 'life_event_sentence_new_hobby' => 'Started a hobby', + 'life_event_sentence_new_instrument' => 'Learned a new instrument', + 'life_event_sentence_new_language' => 'Learned a new language', + 'life_event_sentence_tattoo_or_piercing' => 'Got a tattoo or piercing', + 'life_event_sentence_new_license' => 'Got a license', + 'life_event_sentence_travel' => 'Traveled', + 'life_event_sentence_achievement_or_award' => 'Got an achievement or award', + 'life_event_sentence_changed_beliefs' => 'Changed beliefs', + 'life_event_sentence_first_word' => 'Spoke for the first time', + 'life_event_sentence_first_kiss' => 'Kissed for the first time', + + // documents + 'document_list_title' => 'Documents', + 'document_list_cta' => 'Upload document', + 'document_list_blank_desc' => 'Here you can store documents related to this person.', + 'document_upload_zone_cta' => 'Upload a file', + 'document_upload_zone_progress' => 'Uploading the document…', + 'document_upload_zone_error' => 'There was an error uploading the document. Please try again below.', + + // Photos + 'photo_title' => 'Photos', + 'photo_list_title' => 'Related photos', + 'photo_list_cta' => 'Upload photo', + 'photo_list_blank_desc' => 'You can store images about this contact. Upload one now!', + 'photo_upload_zone_cta' => 'Upload a photo', + 'photo_current_profile_pic' => 'Current profile picture', + 'photo_make_profile_pic' => 'Make profile picture', + 'photo_delete' => 'Delete photo', + 'photo_next' => 'Next photo ❯', + 'photo_previous' => '❮ Previous photo', + + // Avatars + 'avatar_change_title' => 'Change your avatar', + 'avatar_question' => 'Which avatar would you like to use?', + 'avatar_default_avatar' => 'The default avatar', + 'avatar_adorable_avatar' => 'The Adorable avatar', + 'avatar_gravatar' => 'The Gravatar associated with the email address of this person. Gravatar is a global system that lets users associate email addresses with photos.', + 'avatar_current' => 'Keep the current avatar', + 'avatar_photo' => 'From a photo that you upload', + 'avatar_crop_new_avatar_photo' => 'Crop new avatar photo', + + // emotions + 'emotion_this_made_me_feel' => 'This made you feel…', + + // logs + 'auditlogs_link' => 'History', + 'auditlogs_title' => 'Everything that happened to :name', + 'auditlogs_breadcrumb' => 'History', + 'auditlogs_author' => 'By :name on :date', + + // contact field label + 'contact_field_label_home' => 'Home', + 'contact_field_label_work' => 'Work', + 'contact_field_label_cell' => 'Mobile', + 'contact_field_label_fax' => 'Fax', + 'contact_field_label_pager' => 'Pager', + 'contact_field_label_main' => 'Main', + 'contact_field_label_other' => 'Other', + 'contact_field_label_personal' => 'Personal', +]; diff --git a/resources/lang/en/reminder.php b/resources/lang/en/reminder.php new file mode 100644 index 0000000..bcab17c --- /dev/null +++ b/resources/lang/en/reminder.php @@ -0,0 +1,16 @@ + 'Wish happy birthday to', + 'type_phone_call' => 'Call', + 'type_lunch' => 'Lunch with', + 'type_hangout' => 'Hangout with', + 'type_email' => 'Email', + 'type_birthday_kid' => 'Wish happy birthday to the child of', +]; diff --git a/resources/lang/en/settings.php b/resources/lang/en/settings.php new file mode 100644 index 0000000..7a8f1ba --- /dev/null +++ b/resources/lang/en/settings.php @@ -0,0 +1,558 @@ + 'Account settings', + 'sidebar_personalization' => 'Personalization', + 'sidebar_settings_storage' => 'Storage', + 'sidebar_settings_export' => 'Export data', + 'sidebar_settings_users' => 'Users', + 'sidebar_settings_subscriptions' => 'Subscription', + 'sidebar_settings_import' => 'Import data', + 'sidebar_settings_tags' => 'Tag management', + 'sidebar_settings_api' => 'API', + 'sidebar_settings_dav' => 'DAV Resources', + 'sidebar_settings_security' => 'Security', + 'sidebar_settings_auditlogs' => 'Audit logs', + + 'title_general' => 'General Information', + 'title_i18n' => 'International settings', + 'title_layout' => 'Layout', + + 'me_title' => 'Me as a contact', + 'me_help' => 'This is the contact that represents you in Monica', + 'me_select' => 'Select a contact', + 'me_no_contact' => 'No contact selected yet.', + 'me_select_click' => 'Click here to select a contact.', + 'me_remove_contact' => 'Remove the association', + 'me_choose' => 'Choose yourself', + 'me_choose_placeholder' => 'Choose yourself', + + 'export_title' => 'Export your account data', + 'export_be_patient' => 'Click the button to start the export. It might take several minutes to process the export – please be patient and do not spam the button.', + 'export_title_sql' => 'Export to SQL', + 'export_sql_explanation' => 'Exporting your data in SQL format allows you to take your data and import it to your own Monica instance. This is only valuable if you do have your own server.', + 'export_sql_cta' => 'Export to SQL', + 'export_sql_link_instructions' => 'Note: read the instructions to learn more about importing this file to your instance.', + 'export_title_json' => 'Export to Json', + 'export_submitted' => 'Your export has been submitted, it will be available in a few moment…', + 'export_json_explanation' => 'Exporting your data in Json format for backup.', + 'export_json_beta' => 'Json export is in preview mode. Tell us what you think about it:', + 'export_json_cta' => 'Export to Json', + 'export_header_type' => 'Type', + 'export_header_timestamp' => 'Creation date', + 'export_header_status' => 'Status', + 'export_header_actions' => 'Actions', + 'export_last_title' => 'Last exports', + 'export_empty_title' => 'No exports yet', + 'export_type_json' => 'Json export', + 'export_type_sql' => 'SQL export', + 'export_status_todo' => 'Submitted', + 'export_status_doing' => 'Doing', + 'export_status_done' => 'Done', + 'export_status_failed' => 'Failed', + 'export_not_done' => 'Download impossible, this export is not done yet.', + + 'firstname' => 'First name', + 'lastname' => 'Last name', + 'name_order' => 'Name order', + 'name_order_firstname_lastname' => ' – John Doe', + 'name_order_lastname_firstname' => ' – Doe John', + 'name_order_firstname_lastname_nickname' => ' () – John Doe (Rambo)', + 'name_order_firstname_nickname_lastname' => ' () – John (Rambo) Doe', + 'name_order_lastname_firstname_nickname' => ' () – Doe John (Rambo)', + 'name_order_lastname_nickname_firstname' => ' () – Doe (Rambo) John', + 'name_order_nickname_firstname_lastname' => ' ( ) – Rambo (John Doe)', + 'name_order_nickname_lastname_firstname' => ' ( ) – Rambo (Doe John)', + 'name_order_nickname_bracketed_firstname_lastname' => ' () - Rambo (John) Doe', + 'name_order_nickname' => ' – Rambo', + 'currency' => 'Currency', + 'name' => 'Your name: :name', + 'email' => 'Email address', + 'email_placeholder' => 'Enter email', + 'email_help' => 'This is the email used to login, and this is where Monica will send your reminders.', + 'timezone' => 'Timezone', + 'temperature_scale' => 'Temperature scale', + 'temperature_scale_fahrenheit' => 'Fahrenheit (°F)', + 'temperature_scale_celsius' => 'Celsius (°C)', + 'layout' => 'Layout', + 'layout_small' => 'Maximum 1200 pixels wide', + 'layout_big' => 'Full width of the browser', + 'save' => 'Update preferences', + 'delete_title' => 'Delete your account', + 'delete_desc' => 'Do you wish to delete your account? Deletion is permanent and all of your data will be erased permanently. If you have a subscription, it will be cancelled immediately.', + 'delete_other_desc' => 'Your data in the main database will be deleted immediately. As described in our privacy policy, we carry out securely encrypted backups of the database every day. These backups are kept for 30 days after which they are completely deleted. We cannot delete specific data from the backups we hold any earlier than this. All of your data will be completely deleted no later than 31 days after your account’s deletion.', + 'reset_desc' => 'Do you wish to reset your account? This will remove all your contacts, and all of the data associated with them. Your account will not be deleted.', + 'reset_title' => 'Reset your account', + 'reset_cta' => 'Reset account', + 'reset_notice' => 'Are you sure to reset your account? This is permanent and cannot be undone.', + 'reset_success' => 'Your account has been reset successfully.', + 'delete_notice' => 'Are you sure you want to delete your account? This is permanent and cannot be undone. All of your data will be deleted and will not be recoverable.', + 'delete_cta' => 'Delete account', + 'settings_success' => 'Preferences updated!', + 'locale' => 'Language used in the app', + 'locale_help' => 'Do you want to help translating Monica or add a new language? Please follow this link for more information.', + 'locale_ar' => 'Arabic', + 'locale_cs' => 'Czech', + 'locale_de' => 'German', + 'locale_el' => 'Greek', + 'locale_en' => 'English', + 'locale_en-GB' => 'English (United Kingdom)', + 'locale_es' => 'Spanish', + 'locale_fr' => 'French', + 'locale_he' => 'Hebrew', + 'locale_hr' => 'Croatian', + 'locale_id' => 'Indonesian', + 'locale_it' => 'Italian', + 'locale_ja' => 'Japanese', + 'locale_nl' => 'Dutch', + 'locale_pt' => 'Portuguese', + 'locale_pt-BR' => 'Brazilian Portuguese', + 'locale_ru' => 'Russian', + 'locale_sv' => 'Swedish', + 'locale_vi' => 'Vietnamese', + 'locale_zh' => 'Chinese Simplified', + 'locale_zh-TW' => 'Chinese Traditional', + 'locale_tr' => 'Turkish', + + 'security_title' => 'Security', + 'security_help' => 'Change security matters for your account.', + 'password_change' => 'Change your password', + 'password_current' => 'Current password', + 'password_current_placeholder' => 'Enter your current password', + 'password_new1' => 'New password', + 'password_new1_placeholder' => 'Enter your new password', + 'password_new2' => 'Confirm your new password', + 'password_new2_placeholder' => 'Retype your new password', + 'password_btn' => 'Change password', + '2fa_title' => 'Two Factor Authentication', + '2fa_otp_title' => 'Two Factor Authentication mobile application', + '2fa_enable_title' => 'Enable Two Factor Authentication', + '2fa_enable_description' => 'Enable Two Factor Authentication to increase the security of your account.', + '2fa_enable_otp' => 'Open up your Two Factor Authentication mobile app and scan the following QR barcode:', + '2fa_enable_otp_help' => 'If your Two Factor Authentication mobile app does not support QR barcodes, enter in the following code:', + '2fa_enable_otp_validate' => 'Please validate the new device you’ve just set up:', + '2fa_enable_success' => 'Two Factor Authentication activated', + '2fa_enable_error' => 'Error when trying to activate Two Factor Authentication', + '2fa_enable_error_already_set' => 'Two Factor Authentication is already activated', + '2fa_disable_title' => 'Disable Two Factor Authentication', + '2fa_disable_description' => 'Disable Two Factor Authentication for your account. Be careful, your account will be much less secure!', + '2fa_disable_success' => 'Two Factor Authentication disabled', + '2fa_disable_error' => 'Error when trying to disable Two Factor Authentication', + + 'webauthn_title' => 'Security key — WebAuthn protocol', + 'webauthn_enable_description' => 'Add a new security key', + 'webauthn_key_name_help' => 'Give your key a name.', + 'webauthn_key_name' => 'Key name:', + 'webauthn_success' => 'Your key is detected and validated.', + 'webauthn_last_use' => 'Last use: {timestamp}', + 'webauthn_delete_confirmation' => 'Are you sure you want to delete this key?', + 'webauthn_delete_success' => 'Key deleted', + 'webauthn_insertKey' => 'Insert your security key.', + 'webauthn_buttonAdvise' => 'If your security key has a button, press it.', + 'webauthn_noButtonAdvise' => 'If it does not, remove it and insert it again.', + 'webauthn_not_supported' => 'Your browser doesn’t currently support WebAuthn.', + 'webauthn_not_secured' => 'WebAuthn only supports secure connections. Please load this page with https scheme.', + 'webauthn_error_already_used' => 'This key is already registered. It’s not necessary to register it again.', + 'webauthn_error_not_allowed' => 'The operation either timed out or was not allowed.', + + 'recovery_title' => 'Recovery codes', + 'recovery_show' => 'Get recovery codes', + 'recovery_copy_help' => 'Copy codes in your clipboard', + 'recovery_help_intro' => 'These are your recovery codes:', + 'recovery_help_information' => 'You can use each recovery code once.', + 'recovery_clipboard' => 'Codes copied to the clipboard.', + 'recovery_generate' => 'Generate new codes…', + 'recovery_generate_help' => 'Generating new codes will invalidate previously generated codes.', + 'recovery_already_used_help' => 'This code has already been used.', + + 'users_list_title' => 'Users with access to your account', + 'users_list_add_user' => 'Invite a new user', + 'users_list_you' => 'That’s you', + 'users_list_invitations_title' => 'Pending invitations', + 'users_list_invitations_explanation' => 'Below are the people you’ve invited to join Monica as a collaborator.', + 'users_list_invitations_invited_by' => 'invited by :name', + 'users_list_invitations_sent_date' => 'sent on :date', + 'users_blank_title' => 'You are the only one who has access to this account.', + 'users_blank_add_title' => 'Would you like to invite someone else?', + 'users_blank_description' => 'This person will have the same access that you have, and will be able to add, edit or delete contact information.', + 'users_blank_cta' => 'Invite someone', + 'users_add_title' => 'Invite a new user to your account by email', + 'users_add_description' => 'This person will have the same access as you do, including inviting or deleting other users, including you. Make sure you trust this person before giving them access.', + 'users_add_email_field' => 'Enter the email of the person you want to invite', + 'users_add_confirmation' => 'I confirm that I want to invite this user to my account. I understand that this person will have access to ALL of my data and see exactly what I see.', + 'users_add_cta' => 'Invite user by email', + 'users_accept_title' => 'Accept invitation and create a new account', + 'users_error_please_confirm' => 'Please confirm that you want to invite this user before proceeding with the invitation', + 'users_error_email_already_taken' => 'This email is already taken. Please choose another one', + 'users_error_already_invited' => 'You already have invited this user. Please choose another email address.', + 'users_error_email_not_similar' => 'This is not the email of the person who’ve invited you.', + 'users_invitation_deleted_confirmation_message' => 'The invitation has been successfully deleted', + 'users_invitations_delete_confirmation' => 'Are you sure you want to delete this invitation?', + 'users_list_delete_confirmation' => 'Are you sure to delete this user from your account?', + 'users_invitation_need_subscription' => 'Adding more users requires a subscription.', + + 'subscriptions_account_current_plan' => 'Your current plan', + 'subscriptions_account_current_legacy' => 'Current plan, not selectable anymore:', + 'subscriptions_account_current_paid_plan' => 'You are on the :name plan. Thanks so much for being a subscriber.', + + 'subscriptions_account_next_billing_title' => 'Next bill', + 'subscriptions_account_next_billing' => 'Your subscription will auto-renew on :date.', + 'subscriptions_account_bill_monthly' => 'We’ll bill you :price for another month.', + 'subscriptions_account_bill_annual' => 'We’ll bill you :price for another year.', + 'subscriptions_account_change' => 'Change plan', + + 'subscriptions_account_cancel_title' => 'Cancel subscription', + 'subscriptions_account_cancel_action' => 'Cancel subscription', + 'subscriptions_account_cancel' => 'You can cancel your subscription at any time.', + 'subscriptions_account_free_plan' => 'You are on the free plan.', + 'subscriptions_account_free_plan_upgrade' => 'You can upgrade your account to the :name plan, which costs $:price per month. Here are the advantages:', + 'subscriptions_account_free_plan_benefits_users' => 'Unlimited number of users', + 'subscriptions_account_free_plan_benefits_reminders' => 'Reminders by email', + 'subscriptions_account_free_plan_benefits_import_data_vcard' => 'Import your contacts with vCard', + 'subscriptions_account_free_plan_benefits_support' => 'Support the project in the long run, so we can introduce more great features.', + 'subscriptions_account_upgrade' => 'Upgrade your account', + 'subscriptions_account_upgrade_title' => 'Upgrade Monica today and have more meaningful relationships.', + 'subscriptions_account_upgrade_choice' => 'Pick a plan below and join over :customers persons who upgraded their Monica.', + 'subscriptions_account_update_title' => 'Update Monica subscription', + 'subscriptions_account_update_description' => 'You can change your subscription’s frequency here.', + 'subscriptions_account_update_information' => 'You will be billed immediately for the new amount. Your subscription will extend to the new period, depending on your choice.', + 'subscriptions_account_invoices' => 'Invoices', + 'subscriptions_account_invoices_download' => 'Download', + 'subscriptions_account_invoices_subscription' => 'Subscription from :startDate to :endDate', + 'subscriptions_account_payment' => 'Which payment option fits you best?', + 'subscriptions_account_confirm_payment' => 'Your payment is currently incomplete, please confirm your payment.', + 'subscriptions_downgrade_title' => 'Downgrade your account to the free plan', + 'subscriptions_downgrade_limitations' => 'The free plan has limitations. In order to be able to downgrade, you need to pass the checklist below:', + 'subscriptions_downgrade_rule_users' => 'You must have only 1 user in your account', + 'subscriptions_downgrade_rule_users_constraint' => 'You currently have 1 user in your account.|You currently have :count users in your account.', + 'subscriptions_downgrade_rule_invitations' => 'You must not have any pending invitations', + 'subscriptions_downgrade_rule_invitations_constraint' => 'You currently have 1 pending invitation.|You currently have :count pending invitations.', + 'subscriptions_downgrade_rule_contacts' => 'You must not have more than :number active contacts', + 'subscriptions_downgrade_rule_contacts_constraint' => 'You currently have 1 contact.|You currently have :count contacts.', + 'subscriptions_downgrade_rule_contacts_archive' => 'We can also archive all your contacts for you – that would clear this rule and let you proceed with your account’s downgrade process.', + 'subscriptions_downgrade_cta' => 'Downgrade', + 'subscriptions_downgrade_success' => 'You are back to the Free plan!', + 'subscriptions_downgrade_thanks' => 'Thanks so much for trying the paid plan. We keep adding new features on Monica all the time – so you might want to come back in the future to see if you might be interested in taking a subscription again.', + 'subscriptions_back' => 'Back to settings', + 'subscriptions_upgrade_title' => 'Upgrade your account', + 'subscriptions_upgrade_choose' => 'You picked the :plan plan.', + 'subscriptions_upgrade_infos' => 'We couldn’t be happier. Enter your payment info below.', + 'subscriptions_upgrade_name' => 'Name on card', + 'subscriptions_upgrade_zip' => 'ZIP or postal code', + 'subscriptions_upgrade_credit' => 'Credit or debit card', + 'subscriptions_upgrade_submit' => 'Pay {amount}', + 'subscriptions_upgrade_charge' => 'We’ll charge your card :price now. The next charge will be on :date. If you ever change your mind, you can cancel at any time, no questions asked.', + 'subscriptions_upgrade_charge_handled' => 'The payment is handled by Stripe. No card information touches our server.', + 'subscriptions_upgrade_success' => 'Thank you! You are now subscribed.', + 'subscriptions_upgrade_thanks' => 'Welcome to the community of people who try to make the world a better place.', + + 'subscriptions_payment_confirm_title' => 'Confirm your :amount payment', + 'subscriptions_payment_confirm_information' => 'Extra confirmation is needed to process your payment. Please confirm your payment by filling out your payment details below.', + 'subscriptions_payment_succeeded_title' => 'Payment Successful', + 'subscriptions_payment_succeeded' => 'This payment was already successfully confirmed.', + 'subscriptions_payment_cancelled_title' => 'Payment Cancelled', + 'subscriptions_payment_cancelled' => 'This payment was cancelled.', + 'subscriptions_payment_error_name' => 'Please provide your name.', + 'subscriptions_payment_success' => 'The payment was successful.', + + 'subscriptions_pdf_title' => 'Your :name monthly subscription', + 'subscriptions_plan_frequency_year' => ':amount / year', + 'subscriptions_plan_frequency_month' => ':amount / month', + 'subscriptions_plan_choose' => 'Choose this plan', + 'subscriptions_plan_year_title' => 'Pay annually', + 'subscriptions_plan_year_bonus' => 'Peace of mind for a whole year', + 'subscriptions_plan_month_title' => 'Pay monthly', + 'subscriptions_plan_month_bonus' => 'Cancel any time', + 'subscriptions_plan_include1' => 'Included with your upgrade:', + 'subscriptions_plan_include2' => 'Unlimited number of contacts • Unlimited number of users • Reminders by email • Import with vCard • Personalization of the contact sheet', + 'subscriptions_plan_include3' => '100% of the profits go the development of this great open source project.', + 'subscriptions_help_title' => 'Additional details you may be curious about', + 'subscriptions_help_opensource_title' => 'What is an open source project?', + 'subscriptions_help_opensource_desc' => 'Monica is an open source project. This means it is built by a community who wants to build a great tool for the greater good. Being open source means the code is publicly available on GitHub, and everyone can inspect it, modify it or enhance it. All the money we raise is dedicated to building better features, paying for more powerful servers, and paying other costs. Thanks for your help. We couldn’t do it without you.', + 'subscriptions_help_limits_title' => 'Is there a limit to the number of contacts we can have on the free plan?', + 'subscriptions_help_limits_plan' => 'Yes. Free plans let you manage :number contacts.', + 'subscriptions_help_discounts_title' => 'Do you have discounts for non-profits and education?', + 'subscriptions_help_discounts_desc' => 'We do! Monica is free for students, and free for non-profits and charities. Just contact the support with a proof of your status and we’ll apply this special status in your account.', + 'subscriptions_help_change_title' => 'What if I change my mind?', + 'subscriptions_help_change_desc' => 'You can cancel anytime, no questions asked, and all by yourself – no need to contact support. However, you will not be refunded for the current period.', + + 'stripe_error_card' => 'Your card was declined. Decline message is: :message', + 'stripe_error_api_connection' => 'Network communication with Stripe failed. Try again later.', + 'stripe_error_rate_limit' => 'Too many requests with Stripe right now. Try again later.', + 'stripe_error_invalid_request' => 'Invalid parameters. Try again later.', + 'stripe_error_authentication' => 'Wrong authentication with Stripe', + + 'import_title' => 'Import contacts in your account', + 'import_cta' => 'Upload contacts', + 'import_stat' => 'You’ve imported :number files so far.', + 'import_result_stat' => 'Uploaded vCard with 1 contact (:total_imported imported, :total_skipped skipped)|Uploaded vCard with :total_contacts contacts (:total_imported imported, :total_skipped skipped)', + 'import_view_report' => 'View report', + 'import_in_progress' => 'The import is in progress. Reload the page in one minute.', + 'import_upload_title' => 'Import your contacts from a vCard file', + 'import_upload_rules_desc' => 'We do however have some rules:', + 'import_upload_rule_format' => 'We support .vcard and .vcf files.', + 'import_upload_rule_vcard' => 'We support the vCard 3.0 format, which is the default format for macOS’s Contacts.app and Google Contacts.', + 'import_upload_rule_instructions' => 'Export instructions for macOS Contacts.app and Google Contacts.', + 'import_upload_rule_multiple' => 'If your contacts have multiple email addresses or phone numbers, only the first entry will be saved.', + 'import_upload_rule_limit' => 'Files are limited to 10 MB.', + 'import_upload_rule_time' => 'It might take up to a minute to upload the contacts and process them. Please be patient.', + 'import_upload_rule_cant_revert' => 'Please make sure data is accurate before uploading, as you can’t undo the upload.', + 'import_upload_form_file' => 'Your .vcf or .vCard file:', + 'import_upload_behaviour' => 'Import behaviour:', + 'import_upload_behaviour_add' => 'Add new contacts and skip existing', + 'import_upload_behaviour_replace' => 'Replace existing contacts', + 'import_upload_behaviour_help' => 'Replacing will replace all data found in the vCard, but will keep existing contact fields.', + 'import_report_title' => 'Importing report', + 'import_report_date' => 'Date of the import', + 'import_report_type' => 'Type of import', + 'import_report_number_contacts' => 'Number of contacts in the file', + 'import_report_number_contacts_imported' => 'Number of imported contacts', + 'import_report_number_contacts_skipped' => 'Number of skipped contacts', + 'import_report_status_imported' => 'Imported', + 'import_report_status_skipped' => 'Skipped', + 'import_vcard_parse_error' => 'Error when parsing the vCard entry', + 'import_vcard_contact_exist' => 'Contact already exists', + 'import_vcard_contact_no_firstname' => 'No first name (mandatory)', + 'import_vcard_file_not_found' => 'File not found', + 'import_vcard_unknown_entry' => 'Unknown contact name', + 'import_vcard_file_no_entries' => 'File contains no entries', + 'import_blank_title' => 'You haven’t imported any contacts yet.', + 'import_blank_question' => 'Would you like to import contacts now?', + 'import_blank_description' => 'We can import vCard files that you can get from Google Contacts or your Contact manager.', + 'import_blank_cta' => 'Import vCard', + 'import_need_subscription' => 'Importing data requires a subscription.', + + 'tags_list_title' => 'Tags', + 'tags_list_description' => 'You can organize your contacts by setting up tags. Tags work like folders, but you can add more than one tag to a contact. To add a new tag, add it on the contact itself.', + 'tags_list_contact_number' => '1 contact|:count contacts', + 'tags_list_delete_success' => 'The tag has been successfully deleted', + 'tags_list_edit_success' => 'The tag has been successfully updated', + 'tags_list_delete_confirmation' => 'Are you sure you want to delete the tag? No contacts will be deleted, only the tag.', + 'tags_blank_title' => 'Tags are a great way of categorizing your contacts.', + 'tags_blank_description' => 'Tags work like folders, but you can add more than one tag to a contact. Go to a contact and tag a friend, right below the name. Once a contact is tagged, come back here to manage all the tags in your account.', + + 'api_title' => 'API access', + 'api_description' => 'The API can be used to manipulate Monica’s data from an external application, like a mobile application for instance.', + 'api_help' => 'To use the API, a token is mandatory. You can either create a personal access token (Bearer authentication), or authorize an OAuth client to create it for you. See API documentation.', + 'api_endpoint' => 'The API endpoint for this Monica instance is:', + + 'api_personal_access_tokens' => 'Personal access tokens', + 'api_pao_description' => 'Make sure you give this token to a source you trust – as they allow you to access all your data.', + 'api_token_title' => 'Personal Access Tokens', + 'api_token_create_new' => 'Create New Token', + 'api_token_not_created' => 'You have not created any personal access tokens.', + 'api_token_name' => 'Token name', + 'api_token_expire' => 'Expires at {date}', + 'api_token_delete' => 'Delete', + 'api_token_create' => 'Create Token', + 'api_token_scopes' => 'Scopes', + 'api_token_help' => 'Here is your new personal access token. This is the only time it will be shown so don’t lose it! You may now use this token to make API requests.', + + 'api_oauth_clients' => 'Your OAuth clients', + 'api_oauth_clients_desc' => 'This section lets you register your own OAuth clients.', + 'api_oauth_clients_desc2' => 'Use this client id to request a new token, and convert authorization codes to access tokens. See Laravel Passport documentation for more information.', + 'api_oauth_title' => 'OAuth Clients', + 'api_oauth_create_new' => 'Create New Client', + 'api_oauth_edit' => 'Edit Client', + 'api_oauth_not_created' => 'You have not created any OAuth clients.', + 'api_oauth_clientid' => 'Client ID', + 'api_oauth_name' => 'Name', + 'api_oauth_name_help' => 'Something your users will recognize and trust.', + 'api_oauth_secret' => 'Secret', + 'api_oauth_create' => 'Create Client', + 'api_oauth_redirecturl' => 'Redirect URL', + 'api_oauth_redirecturl_help' => 'Your application’s authorization callback URL.', + + 'api_authorized_clients' => 'List of authorized clients', + 'api_authorized_clients_desc' => 'This section lists all the clients you’ve authorized to access your application data. You can revoke this authorization at anytime.', + 'api_authorized_clients_title' => 'Authorized Applications', + 'api_authorized_clients_none' => 'There are no authorized clients yet.', + 'api_authorized_clients_name' => 'Name', + 'api_authorized_clients_scopes' => 'Scopes', + + 'personalization_tab_title' => 'Personalize your account', + + 'personalization_title' => 'Here you will find different settings to configure your account. These features are intended for “power users” who want maximum control over Monica.', + 'personalization_contact_field_type_title' => 'Contact field types', + 'personalization_contact_field_type_add' => 'Add new field type', + 'personalization_contact_field_type_description' => 'You can configure all the different types of contact fields that you can associate to all your contacts. For example, if a new social network appears in the future, you will be able to add this new way of communicating with your contacts right here.', + 'personalization_contact_field_type_table_name' => 'Name', + 'personalization_contact_field_type_table_protocol' => 'Protocol', + 'personalization_contact_field_type_table_actions' => 'Actions', + 'personalization_contact_field_type_modal_title' => 'Add a new contact field type', + 'personalization_contact_field_type_modal_edit_title' => 'Edit an existing contact field type', + 'personalization_contact_field_type_modal_delete_title' => 'Delete an existing contact field type', + 'personalization_contact_field_type_modal_delete_description' => 'Are you sure you want to delete this contact field type? Deleting this type of contact field will delete ALL the data with this type for all of your contacts.', + 'personalization_contact_field_type_modal_name' => 'Name', + 'personalization_contact_field_type_modal_protocol' => 'Protocol (optional)', + 'personalization_contact_field_type_modal_protocol_help' => 'Each new contact field type can be clickable. If a protocol is set, we will use it to trigger the action that is set.', + 'personalization_contact_field_type_modal_icon' => 'Icon (optional)', + 'personalization_contact_field_type_modal_icon_help' => 'You can associate an icon with this contact field type. You need to add a reference to a Font Awesome icon.', + 'personalization_contact_field_type_delete_success' => 'The contact field type has been successfully deleted.', + 'personalization_contact_field_type_add_success' => 'The contact field type has been successfully added.', + 'personalization_contact_field_type_edit_success' => 'The contact field type has been successfully updated.', + + 'personalization_genders_title' => 'Gender types', + 'personalization_genders_add' => 'Add new gender type', + 'personalization_genders_desc' => 'You can define as many genders as you need to. You need at least one gender type in your account.', + 'personalization_genders_modal_add' => 'Add gender type', + 'personalization_genders_modal_edit' => 'Update gender type', + 'personalization_genders_modal_name' => 'Name', + 'personalization_genders_modal_name_help' => 'The name used to display the gender on a contact page.', + 'personalization_genders_modal_sex' => 'Sex', + 'personalization_genders_modal_sex_help' => 'Used to define the relationships, and during the VCard import/export process.', + 'personalization_genders_modal_default' => 'Select the default gender for a new contact', + 'personalization_genders_modal_delete' => 'Delete gender type', + 'personalization_genders_modal_delete_desc' => 'Are you sure you want to delete the gender “{name}”?', + 'personalization_genders_modal_delete_question' => 'You currently have {count} contact with this gender. If you delete this gender, what gender should this contact have?|You currently have {count} contacts with this gender. If you delete this gender, what gender should these contacts have?', + 'personalization_genders_modal_delete_question_default' => 'This gender is the default one. If you delete this gender, which one will be the new default?', + 'personalization_genders_modal_error' => 'Please choose a gender from the list.', + 'personalization_genders_list_contact_number' => '{count} contact|{count} contacts', + 'personalization_genders_table_name' => 'Name', + 'personalization_genders_table_sex' => 'Sex', + 'personalization_genders_table_default' => 'Default', + 'personalization_genders_default' => 'Default gender', + 'personalization_genders_make_default' => 'Change default gender', + 'personalization_genders_select_default' => 'Select default gender', + 'personalization_genders_m' => 'Male', + 'personalization_genders_f' => 'Female', + 'personalization_genders_o' => 'Other', + 'personalization_genders_u' => 'Unknown', + 'personalization_genders_n' => 'None or not applicable', + + 'personalization_reminder_rule_save' => 'The change has been saved', + 'personalization_reminder_rule_title' => 'Reminder rules', + 'personalization_reminder_rule_line' => '{count} day before|{count} days before', + 'personalization_reminder_rule_desc' => 'For every reminder that you set, Monica can send you an email a number of days before the event happens. You can adjust these notification settings here. These notifications only apply to monthly and yearly reminders.', + + 'personalization_module_save' => 'The change has been saved', + 'personalization_module_title' => 'Features', + 'personalization_module_desc' => 'You may not need all of Monica’s features. Below you can toggle specific features that are used on a contact sheet. This change will affect ALL your contacts. Turning off a feature does not delete any data, it simply hides the feature.', + + 'personalisation_paid_upgrade' => 'This is a premium feature that requires a Paid subscription to be active. Upgrade your account by visiting Settings > Subscription.', + 'personalisation_paid_upgrade_vue' => 'This is a premium feature that requires a Paid subscription to be active. Upgrade your account by visiting Settings > Subscription.', + + 'reminder_time_to_send' => 'Time of the day reminders will be sent', + 'reminder_time_to_send_help' => 'Your next reminder is scheduled to be sent on {dateTime}.', + + 'personalization_activity_type_category_title' => 'Activity type categories', + 'personalization_activity_type_category_add' => 'Add a new activity type category', + 'personalization_activity_type_category_table_name' => 'Name', + 'personalization_activity_type_category_description' => 'An activity with one of your contacts can have a type and a category type. Your account comes with a set of predefined category types by default, but you can customize these here.', + 'personalization_activity_type_category_table_actions' => 'Actions', + 'personalization_activity_type_category_modal_add' => 'Add a new activity type category', + 'personalization_activity_type_category_modal_edit' => 'Edit an activity type category', + 'personalization_activity_type_category_modal_question' => 'What should we name this new category?', + 'personalization_activity_type_add_button' => 'Add a new activity type', + 'personalization_activity_type_modal_add' => 'Add a new activity type', + 'personalization_activity_type_modal_question' => 'What should we name this new activity type?', + 'personalization_activity_type_modal_edit' => 'Edit an activity type', + 'personalization_activity_type_category_modal_delete' => 'Delete an activity type category', + 'personalization_activity_type_category_modal_delete_desc' => 'Are you sure you want to delete this category? Deleting it will delete all associated activity types. Activities that belong to this category will not be affected by this deletion.', + 'personalization_activity_type_modal_delete' => 'Delete an activity type', + 'personalization_activity_type_modal_delete_desc' => 'Are you sure you want to delete this activity type? Activities that belong to this category will not be affected by this deletion.', + 'personalization_activity_type_modal_delete_error' => 'We can’t find this activity type.', + 'personalization_activity_type_category_modal_delete_error' => 'We can’t find this activity type category.', + + 'personalization_life_event_category_title' => 'Life event categories', + 'personalization_live_event_category_table_name' => 'Name', + 'personalization_life_event_category_description' => 'A life event can have a type and a category. Your account comes with a set of predefined categories and types by default, but you can customize life event types here.', + 'personalization_live_event_category_table_actions' => 'Actions', + 'personalization_life_event_type_add_button' => 'Add a new life event type', + 'personalization_life_event_type_modal_add' => 'Add a new life event type', + 'personalization_life_event_type_modal_question' => 'What should we name this new life event type?', + 'personalization_life_event_type_modal_edit' => 'Edit a life event type', + 'personalization_life_event_type_modal_delete' => 'Delete a life event type', + 'personalization_life_event_type_modal_delete_desc' => 'Are you sure you want to delete this life event type? Life events that belong to this type will be deleted by performing this action.', + 'personalization_life_event_type_modal_delete_error' => 'We can’t find this life event type.', + + 'personalization_life_event_category_work_education' => 'Work & education', + 'personalization_life_event_category_family_relationships' => 'Family & relationships', + 'personalization_life_event_category_home_living' => 'Home & living', + 'personalization_life_event_category_travel_experiences' => 'Travel & experiences', + 'personalization_life_event_category_health_wellness' => 'Health & wellness', + + 'personalization_life_event_type_new_job' => 'New job', + 'personalization_life_event_type_retirement' => 'Retirement', + 'personalization_life_event_type_new_school' => 'New school', + 'personalization_life_event_type_study_abroad' => 'Study abroad', + 'personalization_life_event_type_volunteer_work' => 'Volunteer work', + 'personalization_life_event_type_published_book_or_paper' => 'Published a book or paper', + 'personalization_life_event_type_military_service' => 'Military service', + 'personalization_life_event_type_first_met' => 'First met', + 'personalization_life_event_type_new_relationship' => 'New relationship', + 'personalization_life_event_type_engagement' => 'Engagement', + 'personalization_life_event_type_marriage' => 'Marriage', + 'personalization_life_event_type_anniversary' => 'Anniversary', + 'personalization_life_event_type_expecting_a_baby' => 'Expecting a baby', + 'personalization_life_event_type_new_child' => 'New child', + 'personalization_life_event_type_new_family_member' => 'New family member', + 'personalization_life_event_type_new_pet' => 'New pet', + 'personalization_life_event_type_end_of_relationship' => 'End of relationship', + 'personalization_life_event_type_loss_of_a_loved_one' => 'Loss of a loved one', + 'personalization_life_event_type_moved' => 'Moved', + 'personalization_life_event_type_bought_a_home' => 'Bought a home', + 'personalization_life_event_type_home_improvement' => 'Home improvement', + 'personalization_life_event_type_holidays' => 'Holidays', + 'personalization_life_event_type_new_vehicle' => 'New vehicle', + 'personalization_life_event_type_new_roommate' => 'New roommate', + 'personalization_life_event_type_overcame_an_illness' => 'Overcame an illness', + 'personalization_life_event_type_quit_a_habit' => 'Quit a habit', + 'personalization_life_event_type_new_eating_habits' => 'New eating habits', + 'personalization_life_event_type_weight_loss' => 'Weight loss', + 'personalization_life_event_type_wear_glass_or_contact' => 'Started wearing glasses or contacts', + 'personalization_life_event_type_broken_bone' => 'Broke a bone', + 'personalization_life_event_type_removed_braces' => 'Had braces removed', + 'personalization_life_event_type_surgery' => 'Had surgery', + 'personalization_life_event_type_dentist' => 'Had dental treatment', + 'personalization_life_event_type_new_sport' => 'Started playing a new sport', + 'personalization_life_event_type_new_hobby' => 'Took up a new hobby', + 'personalization_life_event_type_new_instrument' => 'Started learning a new instrument', + 'personalization_life_event_type_new_language' => 'Started learning a new language', + 'personalization_life_event_type_tattoo_or_piercing' => 'Tattoo or piercing', + 'personalization_life_event_type_new_license' => 'New license', + 'personalization_life_event_type_travel' => 'Travel', + 'personalization_life_event_type_achievement_or_award' => 'Achievement or award', + 'personalization_life_event_type_changed_beliefs' => 'Changed beliefs', + 'personalization_life_event_type_first_word' => 'First word', + 'personalization_life_event_type_first_kiss' => 'First kiss', + + 'storage_title' => 'Storage', + 'storage_account_info' => 'Your account limit is :accountLimit MB. Your current usage is :currentAccountSize MB (about :percentUsage%).', + 'storage_upgrade_notice' => 'Upgrade your account to be able to upload documents and photos.', + 'storage_description' => 'Here you can see all the documents and photos uploaded about your contacts.', + + 'dav_title' => 'WebDAV', + 'dav_description' => 'Here you can find all settings to use WebDAV resources for CardDAV and CalDAV exports.', + 'dav_copy_help' => 'Copy into your clipboard', + 'dav_clipboard_copied' => 'Value copied into your clipboard', + 'dav_url_base' => 'Base url for all CardDAV and CalDAV resources:', + 'dav_connect_help' => 'You can connect your contacts and/or calendars with this base url on you phone or computer.', + 'dav_connect_help2' => 'Use your login (email) and create an API token as the password to authenticate.', + 'dav_url_carddav' => 'CardDAV url for Contacts resource:', + 'dav_url_caldav_birthdays' => 'CalDAV url for Birthdays resources:', + 'dav_url_caldav_tasks' => 'CalDAV url for Tasks resources:', + 'dav_title_carddav' => 'CardDAV', + 'dav_title_caldav' => 'CalDAV', + 'dav_carddav_export' => 'Export all contacts in one file', + 'dav_caldav_birthdays_export' => 'Export all birthdays in one file', + 'dav_caldav_tasks_export' => 'Export all tasks in one file', + + 'archive_title' => 'Archive all of the contacts in your account', + 'archive_desc' => 'This will archive all of the contacts in your account.', + 'archive_cta' => 'Archive all of your contacts', + + 'logs_title' => 'Everything that has happened to this account', + 'logs_actor' => 'Actor', + 'logs_timestamp' => 'Timestamp', + 'logs_description' => 'Description', + 'logs_subject' => 'Subject', + 'logs_size' => 'Size (Kb)', + 'logs_object' => 'Object', +]; diff --git a/resources/lang/en/validation.php b/resources/lang/en/validation.php new file mode 100644 index 0000000..0153365 --- /dev/null +++ b/resources/lang/en/validation.php @@ -0,0 +1,166 @@ + 'The :attribute must be accepted.', + 'active_url' => 'The :attribute is not a valid URL.', + 'after' => 'The :attribute must be a date after :date.', + 'after_or_equal' => 'The :attribute must be a date after or equal to :date.', + 'alpha' => 'The :attribute may only contain letters.', + 'alpha_dash' => 'The :attribute may only contain letters, numbers, dashes and underscores.', + 'alpha_num' => 'The :attribute may only contain letters and numbers.', + 'array' => 'The :attribute must be an array.', + 'before' => 'The :attribute must be a date before :date.', + 'before_or_equal' => 'The :attribute must be a date before or equal to :date.', + 'between' => [ + 'numeric' => 'The :attribute must be between :min and :max.', + 'file' => 'The :attribute must be between :min and :max kilobytes.', + 'string' => 'The :attribute must be between :min and :max characters.', + 'array' => 'The :attribute must have between :min and :max items.', + ], + 'boolean' => 'The :attribute field must be true or false.', + 'confirmed' => 'The :attribute confirmation does not match.', + 'date' => 'The :attribute is not a valid date.', + 'date_equals' => 'The :attribute must be a date equal to :date.', + 'date_format' => 'The :attribute does not match the format :format.', + 'different' => 'The :attribute and :other must be different.', + 'digits' => 'The :attribute must be :digits digits.', + 'digits_between' => 'The :attribute must be between :min and :max digits.', + 'dimensions' => 'The :attribute has invalid image dimensions.', + 'distinct' => 'The :attribute field has a duplicate value.', + 'email' => 'The :attribute must be a valid email address.', + 'ends_with' => 'The :attribute must end with one of the following: :values.', + 'exists' => 'The selected :attribute is invalid.', + 'file' => 'The :attribute must be a file.', + 'filled' => 'The :attribute field must have a value.', + 'gt' => [ + 'numeric' => 'The :attribute must be greater than :value.', + 'file' => 'The :attribute must be greater than :value kilobytes.', + 'string' => 'The :attribute must be greater than :value characters.', + 'array' => 'The :attribute must have more than :value items.', + ], + 'gte' => [ + 'numeric' => 'The :attribute must be greater than or equal :value.', + 'file' => 'The :attribute must be greater than or equal :value kilobytes.', + 'string' => 'The :attribute must be greater than or equal :value characters.', + 'array' => 'The :attribute must have :value items or more.', + ], + 'image' => 'The :attribute must be an image.', + 'in' => 'The selected :attribute is invalid.', + 'in_array' => 'The :attribute field does not exist in :other.', + 'integer' => 'The :attribute must be an integer.', + 'ip' => 'The :attribute must be a valid IP address.', + 'ipv4' => 'The :attribute must be a valid IPv4 address.', + 'ipv6' => 'The :attribute must be a valid IPv6 address.', + 'json' => 'The :attribute must be a valid JSON string.', + 'lt' => [ + 'numeric' => 'The :attribute must be less than :value.', + 'file' => 'The :attribute must be less than :value kilobytes.', + 'string' => 'The :attribute must be less than :value characters.', + 'array' => 'The :attribute must have less than :value items.', + ], + 'lte' => [ + 'numeric' => 'The :attribute must be less than or equal :value.', + 'file' => 'The :attribute must be less than or equal :value kilobytes.', + 'string' => 'The :attribute must be less than or equal :value characters.', + 'array' => 'The :attribute must not have more than :value items.', + ], + 'max' => [ + 'numeric' => 'The :attribute may not be greater than :max.', + 'file' => 'The :attribute may not be greater than :max kilobytes.', + 'string' => 'The :attribute may not be greater than :max characters.', + 'array' => 'The :attribute may not have more than :max items.', + ], + 'mimes' => 'The :attribute must be a file of type: :values.', + 'mimetypes' => 'The :attribute must be a file of type: :values.', + 'min' => [ + 'numeric' => 'The :attribute must be at least :min.', + 'file' => 'The :attribute must be at least :min kilobytes.', + 'string' => 'The :attribute must be at least :min characters.', + 'array' => 'The :attribute must have at least :min items.', + ], + 'not_in' => 'The selected :attribute is invalid.', + 'not_regex' => 'The :attribute format is invalid.', + 'numeric' => 'The :attribute must be a number.', + 'password' => 'The password is incorrect.', + 'present' => 'The :attribute field must be present.', + 'regex' => 'The :attribute format is invalid.', + 'required' => 'The :attribute field is required.', + 'required_if' => 'The :attribute field is required when :other is :value.', + 'required_unless' => 'The :attribute field is required unless :other is in :values.', + 'required_with' => 'The :attribute field is required when :values is present.', + 'required_with_all' => 'The :attribute field is required when :values are present.', + 'required_without' => 'The :attribute field is required when :values is not present.', + 'required_without_all' => 'The :attribute field is required when none of :values are present.', + 'same' => 'The :attribute and :other must match.', + 'size' => [ + 'numeric' => 'The :attribute must be :size.', + 'file' => 'The :attribute must be :size kilobytes.', + 'string' => 'The :attribute must be :size characters.', + 'array' => 'The :attribute must contain :size items.', + ], + 'starts_with' => 'The :attribute must start with one of the following: :values.', + 'string' => 'The :attribute must be a string.', + 'timezone' => 'The :attribute must be a valid zone.', + 'unique' => 'The :attribute has already been taken.', + 'uploaded' => 'The :attribute failed to upload.', + 'url' => 'The :attribute format is invalid.', + 'uuid' => 'The :attribute must be a valid UUID.', + + /* + |-------------------------------------------------------------------------- + | Custom Validation Language Lines + |-------------------------------------------------------------------------- + | + | Here you may specify custom validation messages for attributes using the + | convention "attribute.rule" to name the lines. This makes it quick to + | specify a specific custom language line for a given attribute rule. + | + */ + + 'custom' => [ + 'attribute-name' => [ + 'rule-name' => 'custom-message', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Custom Validation Attributes + |-------------------------------------------------------------------------- + | + | The following language lines are used to swap our attribute placeholder + | with something more reader friendly such as "E-Mail Address" instead + | of "email". This simply helps us make our message more expressive. + | + */ + + 'attributes' => [], + + 'vue' => [ + 'max' => [ + 'numeric' => '{field} may not be greater than {max}.', + 'string' => '{field} may not be greater than {max} characters.', + ], + 'required' => '{field} is required.', + 'url' => '{field} is not a valid URL.', + ], + +]; diff --git a/resources/lang/es.json b/resources/lang/es.json new file mode 100644 index 0000000..9134bec --- /dev/null +++ b/resources/lang/es.json @@ -0,0 +1,7 @@ +{ + "The :attribute must contain at least one uppercase and one lowercase letter.": "La :attribute debe contener al menos una letra mayúscula y una minúscula.", + "The :attribute must contain at least one letter.": "La :attribute debe contener al menos una letra.", + "The :attribute must contain at least one symbol.": "La :attribute debe contener al menos un símbolo.", + "The :attribute must contain at least one number.": "La :attribute debe contener al menos un número.", + "The given :attribute has appeared in a data leak. Please choose a different :attribute.": "La :attribute proporcionada se ha visto comprometida en una filtración de datos (data leak). Elija una :attribute diferente." +} diff --git a/resources/lang/es/app.php b/resources/lang/es/app.php new file mode 100644 index 0000000..b93b00a --- /dev/null +++ b/resources/lang/es/app.php @@ -0,0 +1,571 @@ + 'Si', + 'no' => 'No', + 'update' => 'Actualizar', + 'save' => 'Guardar', + 'add' => 'Añadir', + 'cancel' => 'Cancelar', + 'confirm' => 'Confirmar', + 'delete_confirm' => '¿Seguro?', + 'delete' => 'Eliminar', + 'edit' => 'Editar', + 'upload' => 'Subir', + 'download' => 'Descargar', + 'save_close' => 'Guardar y cerrar', + 'close' => 'Cerrar', + 'copy' => 'Copiar', + 'create' => 'Crear', + 'remove' => 'Eliminar', + 'revoke' => 'Revocar', + 'done' => 'Hecho', + 'back' => 'Volver', + 'verify' => 'Verificar', + 'new' => 'nuevo', + 'unknown' => 'No lo sé', + 'load_more' => 'Cargar más', + 'loading' => 'Cargando…', + 'with' => 'con', + 'today' => 'hoy', + 'yesterday' => 'ayer', + 'another_day' => 'otro día', + 'date' => 'Fecha', + 'type' => 'Tipo', + 'zoom' => 'Zoom', + 'upgrade' => 'Actualiza para desbloquear', + 'percent_uploaded' => '{percent}% cargado', + 'retry' => 'Reintentar', + 'filter' => 'Filtrar la lista', + 'go_back' => 'Volver atrás', + 'file_selected' => '1 archivo seleccionado…|{count} archivos seleccionados…', + + 'application_title' => 'Monica – gestor de relaciones personales', + 'application_description' => 'Monica es una herramienta para gestionar tus interacciones con tus seres queridos, amigos y familiares.', + 'application_og_title' => 'Mejora la relación con tus seres queridos. CRM gratis en línea para amigos y familia.', + + 'markdown_description' => '¿Quieres dar formato al texto de una manera agradable? Soportamos el uso de Markdown para añadir negrita, cursiva, listas y más.', + 'markdown_link' => 'Leer documentación', + + 'header_settings_link' => 'Configuración', + 'header_logout_link' => 'Salir', + 'header_changelog_link' => 'Cambios del producto', + + 'main_nav_cta' => 'Añadir personas', + 'main_nav_dashboard' => 'Panel de control', + 'main_nav_family' => 'Contactos', + 'main_nav_journal' => 'Diario', + 'main_nav_activities' => 'Actividades', + 'main_nav_tasks' => 'Tareas', + + 'footer_remarks' => 'Comentarios', + 'footer_send_email' => 'Envíanos un correo electrónico', + 'footer_privacy' => 'Políticas de privacidad', + 'footer_release' => 'Notas de la versión', + 'footer_newsletter' => 'Boletín', + 'footer_source_code' => 'Contribuir', + 'footer_version' => 'Versión :version', + 'footer_new_version' => 'Una nueva versión de Monica está disponible', + + 'footer_modal_version_whats_new' => 'Qué hay de nuevo', + 'footer_modal_version_release_away' => 'Estás una versión por detrás de la última disponible. Deberías actualizar tu instancia. | Estás :number versiones por detrás de la última versión disponible. Deberías actualizar tu instancia.', + + 'breadcrumb_dashboard' => 'Panel de control', + 'breadcrumb_list_contacts' => 'Lista de personas', + 'breadcrumb_archived_contacts' => 'Contactos archivados', + 'breadcrumb_journal' => 'Diario', + 'breadcrumb_settings' => 'Ajustes', + 'breadcrumb_settings_export' => 'Exportar', + 'breadcrumb_settings_users' => 'Usuarios', + 'breadcrumb_settings_users_add' => 'Añadir un usuario', + 'breadcrumb_settings_subscriptions' => 'Suscripción', + 'breadcrumb_settings_import' => 'Importar', + 'breadcrumb_settings_import_report' => 'Informe de importación', + 'breadcrumb_settings_import_upload' => 'Subir', + 'breadcrumb_settings_tags' => 'Etiquetas', + 'breadcrumb_add_significant_other' => 'Añadir relación', + 'breadcrumb_edit_significant_other' => 'Editar relación', + 'breadcrumb_add_note' => 'Añadir una nota', + 'breadcrumb_edit_note' => 'Editar una nota', + 'breadcrumb_api' => 'API', + 'breadcrumb_dav' => 'Recursos DAV', + 'breadcrumb_edit_introductions' => 'Cómo os conocisteis', + 'breadcrumb_settings_personalization' => 'Personalización', + 'breadcrumb_settings_security' => 'Seguridad', + 'breadcrumb_settings_security_2fa' => 'Autenticación en dos pasos', + 'breadcrumb_profile' => 'Perfil de :name', + + 'gender_male' => 'Hombre', + 'gender_female' => 'Mujer', + 'gender_none' => 'Prefiero no decirlo', + 'gender_no_gender' => 'Sin género', + + 'error_title' => '¡Ups! algo ha fallado.', + 'error_unauthorized' => 'No tienes permisos para editar este recurso.', + 'error_user_account' => 'Este usuario no pertenece a la cuenta dada.', + 'error_save' => 'Tuvimos un error tratando de guardar los datos.', + 'error_try_again' => 'Se ha producido un error. Por favor, inténtelo de nuevo.', + 'error_id' => 'Error ID: :id', + 'error_unavailable' => 'Servicio no disponible', + 'error_maintenance' => 'Mantenimiento en curso. Enseguida regresamos.', + 'error_help' => 'Enseguida regresamos.', + 'error_twitter' => 'Siguenos en nuestra cuenta de Twitter para saber cuando estamos de vuelta.', + 'error_no_term' => 'Todavía no hay ninguna política para esta instancia.', + + 'default_save_success' => 'Los datos han sido guardados.', + + 'compliance_title' => 'Lamentamos la interrupción.', + 'compliance_desc' => 'Hemos cambiado nuestros Terminos de Uso y Política de Privacidad. Por ley tenemos que pedirte que los revises y los aceptes para que puedas seguir usando tu cuenta.', + 'compliance_desc_end' => 'No hacemos nada dudoso con tus datos o con tu cuenta, y nunca lo haremos.', + 'compliance_terms' => 'Aceptar nuevos términos y política de privacidad', + + // Relationship types + // Yes, each relationship type has 8 strings associated with it. + // This is because we need to indicate the name of the relationship type, + // and also the name of the opposite side of this relationship (father/son), + // and then, the feminine version of the string. Finally, in some sentences + // in the UI, we need to include the name of the person we add the relationship + // to. + 'relationship_type_group_love' => 'Relaciones amorosas', + 'relationship_type_group_family' => 'Relaciones familiares', + 'relationship_type_group_friend' => 'Relaciones de amistad', + 'relationship_type_group_work' => 'Relaciones laborales', + 'relationship_type_group_other' => 'Otro tipo de relaciones', + + 'relationship_type_partner' => 'pareja', + 'relationship_type_partner_female' => 'pareja', + 'relationship_type_partner_male' => 'significant other', + 'relationship_type_partner_with_name' => 'la pareja de :name', + 'relationship_type_partner_female_with_name' => 'la pareja de :name', + 'relationship_type_partner_male_with_name' => ':name’s significant other', + + 'relationship_type_spouse' => 'esposa', + 'relationship_type_spouse_female' => 'esposa', + 'relationship_type_spouse_male' => 'esposo/marido', + 'relationship_type_spouse_with_name' => 'la esposa de :name', + 'relationship_type_spouse_female_with_name' => 'Esposa de:name', + 'relationship_type_spouse_male_with_name' => 'Marido de:name', + + 'relationship_type_date' => 'cita', + 'relationship_type_date_female' => 'cita', + 'relationship_type_date_male' => 'date', + 'relationship_type_date_with_name' => 'la cita de :name', + 'relationship_type_date_female_with_name' => 'la cita de :name', + 'relationship_type_date_male_with_name' => ':name’s date', + + 'relationship_type_lover' => 'amante', + 'relationship_type_lover_female' => 'amante', + 'relationship_type_lover_male' => 'lover', + 'relationship_type_lover_with_name' => 'el amante de :name', + 'relationship_type_lover_female_with_name' => 'la amante de :name', + 'relationship_type_lover_male_with_name' => ':name’s lover', + + 'relationship_type_inlovewith' => 'enamorado de', + 'relationship_type_inlovewith_female' => 'enamorada de', + 'relationship_type_inlovewith_male' => 'enamorado de', + 'relationship_type_inlovewith_with_name' => 'alguien :name está enamorado de', + 'relationship_type_inlovewith_female_with_name' => 'alguien :name está enamorado de', + 'relationship_type_inlovewith_male_with_name' => 'someone :name is in love with', + + 'relationship_type_lovedby' => 'querido por', + 'relationship_type_lovedby_female' => 'querido por', + 'relationship_type_lovedby_male' => 'loved by', + 'relationship_type_lovedby_with_name' => 'amante secreto de :name', + 'relationship_type_lovedby_female_with_name' => 'amante secreto de :name', + 'relationship_type_lovedby_male_with_name' => ':name’s secret lover', + + 'relationship_type_ex' => 'ex-pareja', + 'relationship_type_ex_female' => 'ex-novia', + 'relationship_type_ex_male' => 'ex-novio', + 'relationship_type_ex_with_name' => 'Ex-pareja de:name', + 'relationship_type_ex_female_with_name' => 'ex-novia de :name', + 'relationship_type_ex_male_with_name' => 'ex-novio de:name', + + 'relationship_type_parent' => 'padre/madre', + 'relationship_type_parent_female' => 'madre', + 'relationship_type_parent_male' => 'padre', + 'relationship_type_parent_with_name' => 'padre/madre de:name', + 'relationship_type_parent_female_with_name' => 'madre de :name', + 'relationship_type_parent_male_with_name' => 'padre de:name', + + 'relationship_type_child' => 'hijo/hija', + 'relationship_type_child_female' => 'hija', + 'relationship_type_child_male' => 'hijo', + 'relationship_type_child_with_name' => 'hijo/hija de:name', + 'relationship_type_child_female_with_name' => 'hija de :name', + 'relationship_type_child_male_with_name' => 'hijo de:name', + + 'relationship_type_stepparent' => 'madrastra/padrastro', + 'relationship_type_stepparent_female' => 'madrastra', + 'relationship_type_stepparent_male' => 'padrastro', + 'relationship_type_stepparent_with_name' => 'madrastra/padrastro de:name', + 'relationship_type_stepparent_female_with_name' => 'madre de :name', + 'relationship_type_stepparent_male_with_name' => 'padrastro de:name', + + 'relationship_type_stepchild' => 'hijastro/hijastra', + 'relationship_type_stepchild_female' => 'hijastra', + 'relationship_type_stepchild_male' => 'hijastro', + 'relationship_type_stepchild_with_name' => 'hijastro/hijastra de:name', + 'relationship_type_stepchild_female_with_name' => 'hijastra de :name', + 'relationship_type_stepchild_male_with_name' => 'hijastro de :name', + + 'relationship_type_sibling' => 'hermanos', + 'relationship_type_sibling_female' => 'hermana', + 'relationship_type_sibling_male' => 'hermano', + 'relationship_type_sibling_with_name' => ':name hermano/a', + 'relationship_type_sibling_female_with_name' => 'hermana de :name', + 'relationship_type_sibling_male_with_name' => 'hermano de:name', + + 'relationship_type_grandparent' => 'abuelos', + 'relationship_type_grandparent_female' => 'abuela', + 'relationship_type_grandparent_male' => 'abuelo', + 'relationship_type_grandparent_with_name' => 'abuelo/a de:name', + 'relationship_type_grandparent_female_with_name' => 'Abuela de:name', + 'relationship_type_grandparent_male_with_name' => 'Abuelo de:name', + + 'relationship_type_grandchild' => 'nieto(a)', + 'relationship_type_grandchild_female' => 'nieta', + 'relationship_type_grandchild_male' => 'nieto', + 'relationship_type_grandchild_with_name' => 'nieto/a de:name', + 'relationship_type_grandchild_female_with_name' => 'nieta de :name', + 'relationship_type_grandchild_male_with_name' => 'nieto de :name', + + 'relationship_type_uncle' => 'tío', + 'relationship_type_uncle_female' => 'tía', + 'relationship_type_uncle_male' => 'uncle', + 'relationship_type_uncle_with_name' => 'tío de :name', + 'relationship_type_uncle_female_with_name' => 'tía de :name', + 'relationship_type_uncle_male_with_name' => ':name’s uncle', + + 'relationship_type_nephew' => 'sobrino', + 'relationship_type_nephew_female' => 'sobrina', + 'relationship_type_nephew_male' => 'sobrino', + 'relationship_type_nephew_with_name' => 'sobrino de :name', + 'relationship_type_nephew_female_with_name' => 'sobrina de :name', + 'relationship_type_nephew_male_with_name' => ':name’s nephew', + + 'relationship_type_cousin' => 'primo/a', + 'relationship_type_cousin_female' => 'prima', + 'relationship_type_cousin_male' => 'primo/a', + 'relationship_type_cousin_with_name' => 'primo de :name', + 'relationship_type_cousin_female_with_name' => 'prima de :name', + 'relationship_type_cousin_male_with_name' => ':name’s cousin', + + 'relationship_type_godfather' => 'abuelo/a', + 'relationship_type_godfather_female' => 'madrina', + 'relationship_type_godfather_male' => 'padrino', + 'relationship_type_godfather_with_name' => 'padrino/madrina de:name', + 'relationship_type_godfather_female_with_name' => 'madrina de :name', + 'relationship_type_godfather_male_with_name' => 'padrino de :name', + + 'relationship_type_godson' => 'ahijado', + 'relationship_type_godson_female' => 'ahijada', + 'relationship_type_godson_male' => 'ahijado', + 'relationship_type_godson_with_name' => 'ahijado de :name', + 'relationship_type_godson_female_with_name' => 'madrina de :name', + 'relationship_type_godson_male_with_name' => 'ahijado de :name', + + 'relationship_type_friend' => 'amigo', + 'relationship_type_friend_female' => 'amigo', + 'relationship_type_friend_male' => 'amigo', + 'relationship_type_friend_with_name' => 'amigo de :name', + 'relationship_type_friend_female_with_name' => 'amiga de :name', + 'relationship_type_friend_male_with_name' => ':name’s friend', + + 'relationship_type_bestfriend' => 'mejor amigo', + 'relationship_type_bestfriend_female' => 'mejor amiga', + 'relationship_type_bestfriend_male' => 'mejor amigo', + 'relationship_type_bestfriend_with_name' => 'mejor amigo de :name', + 'relationship_type_bestfriend_female_with_name' => 'mejor amiga de :name', + 'relationship_type_bestfriend_male_with_name' => ':name’s best friend', + + 'relationship_type_colleague' => 'colega', + 'relationship_type_colleague_female' => 'colega', + 'relationship_type_colleague_male' => 'colleague', + 'relationship_type_colleague_with_name' => 'colega de :name', + 'relationship_type_colleague_female_with_name' => 'colega de :name', + 'relationship_type_colleague_male_with_name' => ':name’s colleague', + + 'relationship_type_boss' => 'jefe', + 'relationship_type_boss_female' => 'jefa', + 'relationship_type_boss_male' => 'jefe', + 'relationship_type_boss_with_name' => 'jefe de :name', + 'relationship_type_boss_female_with_name' => 'jefe de :name', + 'relationship_type_boss_male_with_name' => ':name’s boss', + + 'relationship_type_subordinate' => 'subordinado', + 'relationship_type_subordinate_female' => 'subordinada', + 'relationship_type_subordinate_male' => 'subordinado', + 'relationship_type_subordinate_with_name' => 'subordinado de :name', + 'relationship_type_subordinate_female_with_name' => 'subordinada de :name', + 'relationship_type_subordinate_male_with_name' => ':name’s subordinate', + + 'relationship_type_mentor' => 'mentor', + 'relationship_type_mentor_female' => 'mentora', + 'relationship_type_mentor_male' => 'mentor', + 'relationship_type_mentor_with_name' => 'mentor de :name', + 'relationship_type_mentor_female_with_name' => 'mentora de :name', + 'relationship_type_mentor_male_with_name' => ':name’s mentor', + + 'relationship_type_protege' => 'protegido', + 'relationship_type_protege_female' => 'protegida', + 'relationship_type_protege_male' => 'protégé', + 'relationship_type_protege_with_name' => 'protegido de :name', + 'relationship_type_protege_female_with_name' => 'protegida de :name', + 'relationship_type_protege_male_with_name' => ':name’s protégé', + + 'relationship_type_ex_husband' => 'ex esposa', + 'relationship_type_ex_husband_female' => 'ex esposa', + 'relationship_type_ex_husband_male' => 'ex marido', + 'relationship_type_ex_husband_with_name' => 'ex mujer de :name', + 'relationship_type_ex_husband_female_with_name' => 'ex mujer de :name', + 'relationship_type_ex_husband_male_with_name' => 'ex marido de :name', + + // emotions + 'emotion_primary_love' => 'Amor', + 'emotion_primary_joy' => 'Alegría', + 'emotion_primary_surprise' => 'Sorpresa', + 'emotion_primary_anger' => 'Enfado', + 'emotion_primary_sadness' => 'Tristeza', + 'emotion_primary_fear' => 'Miedo', + + 'emotion_secondary_affection' => 'Afecto', + 'emotion_secondary_lust' => 'Lujuria', + 'emotion_secondary_longing' => 'Nostalgia', + 'emotion_secondary_cheerfulness' => 'Jovialidad', + 'emotion_secondary_zest' => 'Ánimo', + 'emotion_secondary_contentment' => 'Contentamiento', + 'emotion_secondary_pride' => 'Orgullo', + 'emotion_secondary_optimism' => 'Optimismo', + 'emotion_secondary_enthrallment' => 'Fascinación', + 'emotion_secondary_relief' => 'Alivio', + 'emotion_secondary_surprise' => 'Sorpresa', + 'emotion_secondary_irritation' => 'Irritación', + 'emotion_secondary_exasperation' => 'Exasperación', + 'emotion_secondary_rage' => 'Ira', + 'emotion_secondary_disgust' => 'Asco', + 'emotion_secondary_envy' => 'Envidia', + 'emotion_secondary_suffering' => 'Sufriendo', + 'emotion_secondary_sadness' => 'Tristeza', + 'emotion_secondary_disappointment' => 'Decepción', + 'emotion_secondary_shame' => 'Vergüenza', + 'emotion_secondary_neglect' => 'Desatendido', + 'emotion_secondary_sympathy' => 'Simpatía', + 'emotion_secondary_horror' => 'Horror', + 'emotion_secondary_nervousness' => 'Nerviosismo', + + 'emotion_adoration' => 'Adoración', + 'emotion_affection' => 'Afecto', + 'emotion_love' => 'Amor', + 'emotion_fondness' => 'Cariño', + 'emotion_liking' => 'Gusto', + 'emotion_attraction' => 'Atracción', + 'emotion_caring' => 'Cariñoso', + 'emotion_tenderness' => 'Ternura', + 'emotion_compassion' => 'Compasión', + 'emotion_sentimentality' => 'Sentimentalidad', + 'emotion_arousal' => 'Excitación', + 'emotion_desire' => 'Deseo', + 'emotion_lust' => 'Lujuria', + 'emotion_passion' => 'Pasión', + 'emotion_infatuation' => 'Infatuación', + 'emotion_longing' => 'Nostalgia', + 'emotion_amusement' => 'Diversión', + 'emotion_bliss' => 'Dicha', + 'emotion_cheerfulness' => 'Jovialidad', + 'emotion_gaiety' => 'Regocijo', + 'emotion_glee' => 'Júbilo', + 'emotion_jolliness' => 'Felicidad', + 'emotion_joviality' => 'Jovialidad', + 'emotion_joy' => 'Alegría', + 'emotion_delight' => 'Deleite', + 'emotion_enjoyment' => 'Disfrute', + 'emotion_gladness' => 'Gozo', + 'emotion_happiness' => 'Felicidad', + 'emotion_jubilation' => 'Júbilo', + 'emotion_elation' => 'Elación', + 'emotion_satisfaction' => 'Satisfacción', + 'emotion_ecstasy' => 'Éxtasis', + 'emotion_euphoria' => 'Euforia', + 'emotion_enthusiasm' => 'Entusiasmo', + 'emotion_zeal' => 'Celo', + 'emotion_zest' => 'Ánimo', + 'emotion_excitement' => 'Excitación', + 'emotion_thrill' => 'Estremecimiento', + 'emotion_exhilaration' => 'Regocijo', + 'emotion_contentment' => 'Satisfacción', + 'emotion_pleasure' => 'Placer', + 'emotion_pride' => 'Orgullo', + 'emotion_eagerness' => 'Afán', + 'emotion_hope' => 'Esperanza', + 'emotion_optimism' => 'Optimismo', + 'emotion_enthrallment' => 'Incitación', + 'emotion_rapture' => 'Éxtasis', + 'emotion_relief' => 'Alivio', + 'emotion_amazement' => 'Asombro', + 'emotion_surprise' => 'Sorpresa', + 'emotion_astonishment' => 'Asombro', + 'emotion_aggravation' => 'Agravación', + 'emotion_irritation' => 'Irritación', + 'emotion_agitation' => 'Agitación', + 'emotion_annoyance' => 'Molestia', + 'emotion_grouchiness' => 'Mal humor', + 'emotion_grumpiness' => 'Mal humor', + 'emotion_exasperation' => 'Exasperación', + 'emotion_frustration' => 'Frustración', + 'emotion_anger' => 'Enfado', + 'emotion_rage' => 'Ira', + 'emotion_outrage' => 'Ira', + 'emotion_fury' => 'Furia', + 'emotion_wrath' => 'Cólera', + 'emotion_hostility' => 'Hostilidad', + 'emotion_ferocity' => 'Ferocidad', + 'emotion_bitterness' => 'Amargura', + 'emotion_hate' => 'Odio', + 'emotion_loathing' => 'Aversión', + 'emotion_scorn' => 'Desprecio', + 'emotion_spite' => 'Rencor', + 'emotion_vengefulness' => 'Venganza', + 'emotion_dislike' => 'Desagrado', + 'emotion_resentment' => 'Resentimiento', + 'emotion_disgust' => 'Asco', + 'emotion_revulsion' => 'Repugnancia', + 'emotion_contempt' => 'Desprecio', + 'emotion_envy' => 'Envidia', + 'emotion_jealousy' => 'Celoso', + 'emotion_agony' => 'Agonía', + 'emotion_suffering' => 'Sufrimiento', + 'emotion_hurt' => 'Dolor', + 'emotion_anguish' => 'Angustia', + 'emotion_depression' => 'Depresión', + 'emotion_despair' => 'Desesperanza', + 'emotion_hopelessness' => 'Desesperanza', + 'emotion_gloom' => 'Tristeza', + 'emotion_glumness' => 'Melancolía', + 'emotion_sadness' => 'Tristeza', + 'emotion_unhappiness' => 'Infelicidad', + 'emotion_grief' => 'Duelo', + 'emotion_sorrow' => 'Pesar', + 'emotion_woe' => 'Aflicción', + 'emotion_misery' => 'Miseria', + 'emotion_melancholy' => 'Melancolía', + 'emotion_dismay' => 'Consternación', + 'emotion_disappointment' => 'Decepción', + 'emotion_displeasure' => 'Disgusto', + 'emotion_guilt' => 'Culpa', + 'emotion_shame' => 'Vergüenza', + 'emotion_regret' => 'Arrepentimiento', + 'emotion_remorse' => 'Remordimiento', + 'emotion_alienation' => 'Alienación', + 'emotion_isolation' => 'Aislamiento', + 'emotion_neglect' => 'Desatendido', + 'emotion_loneliness' => 'Soledad', + 'emotion_rejection' => 'Rechazo', + 'emotion_homesickness' => 'Nostalgia', + 'emotion_defeat' => 'Derrota', + 'emotion_dejection' => 'Abatimiento', + 'emotion_insecurity' => 'Inseguridad', + 'emotion_embarrassment' => 'Avergonzado', + 'emotion_humiliation' => 'Humillación', + 'emotion_insult' => 'Insultado', + 'emotion_pity' => 'Lástima', + 'emotion_sympathy' => 'Simpatía', + 'emotion_alarm' => 'Alarmado', + 'emotion_shock' => 'Conmoción', + 'emotion_fear' => 'Miedo', + 'emotion_fright' => 'Susto', + 'emotion_horror' => 'Horror', + 'emotion_terror' => 'Terror', + 'emotion_panic' => 'Pánico', + 'emotion_hysteria' => 'Histeria', + 'emotion_mortification' => 'Mortificación', + 'emotion_anxiety' => 'Ansiedad', + 'emotion_nervousness' => 'Nerviosismo', + 'emotion_tenseness' => 'Tensión', + 'emotion_uneasiness' => 'Inquietud', + 'emotion_apprehension' => 'Aprensión', + 'emotion_worry' => 'Preocupación', + 'emotion_distress' => 'Angustia', + 'emotion_dread' => 'Pavor', + + // weather + 'weather_sunny' => 'Soleado', + 'weather_clear' => 'Despejado', + 'weather_clear-day' => 'Despejado', + 'weather_clear-night' => 'Noche despejada', + 'weather_light-drizzle' => 'Llovizna débil', + 'weather_patchy-light-drizzle' => 'Llovizna débil localizada', + 'weather_patchy-light-rain' => 'Lluvia débil localizada', + 'weather_light-rain' => 'Lluvia débil', + 'weather_moderate-rain-at-times' => 'Lluvia moderada a veces', + 'weather_moderate-rain' => 'Lluvia moderada', + 'weather_patchy-rain-possible' => 'Posible lluvia irregular', + 'weather_heavy-rain-at-times' => 'Lluvia fuerte ocasional', + 'weather_heavy-rain' => 'Lluvia fuerte', + 'weather_light-freezing-rain' => 'Lluvia leve helada', + 'weather_moderate-or-heavy-freezing-rain' => 'Lluvia gélida moderada o fuerte', + 'weather_light-sleet' => 'Aguanieve leve', + 'weather_moderate-or-heavy-rain-shower' => 'Chubascos de lluvia moderados o fuertes', + 'weather_light-rain-shower' => 'Llovizna leve', + 'weather_torrential-rain-shower' => 'Lluvias torrenciales', + 'weather_rain' => 'Lluvia', + 'weather_snow' => 'Nieve', + 'weather_blowing-snow' => 'Ventisca', + 'weather_patchy-light-snow' => 'Nevada leve irregular', + 'weather_light-snow' => 'Leve nevada', + 'weather_patchy-moderate-snow' => 'Nevada irregular moderada', + 'weather_moderate-snow' => 'Nevada moderada', + 'weather_patchy-heavy-snow' => 'Nevada irregular fuerte', + 'weather_heavy-snow' => 'Fuertes nevadas', + 'weather_light-snow-showers' => 'Chubascos débiles de nieve', + 'weather_moderate-or-heavy-snow-showers' => 'Nevadas moderadas o pesadas', + 'weather_patchy-snow-possible' => 'Posible nevada irregular', + 'weather_patchy-sleet-possible' => 'Posible aguanieve irregular', + 'weather_moderate-or-heavy-sleet' => 'Aguanieve moderada o fuerte', + 'weather_light-sleet-showers' => 'Aguanieve leve', + 'weather_moderate-or-heavy-sleet-showers' => 'Chubascos de aguanieve moderados o fuertes', + 'weather_sleet' => 'Aguanieve', + 'weather_wind' => 'Viento', + 'weather_fog' => 'Niebla', + 'weather_freezing-fog' => 'Niebla gélida', + 'weather_mist' => 'Neblina', + 'weather_blizzard' => 'Ventisca', + 'weather_overcast' => 'Nublado', + 'weather_cloudy' => 'Nublado', + 'weather_partly-cloudy-day' => 'Parcialmente nublado', + 'weather_partly-cloudy-night' => 'Parcialmente nublado', + 'weather_freezing-drizzle' => 'Llovizna gélida', + 'weather_heavy-freezing-drizzle' => 'Llovizna gélida fuerte', + 'weather_patchy-freezing-drizzle-possible' => 'Posible llovizna gélida irregular', + 'weather_ice-pellets' => 'Granizo', + 'weather_light-showers-of-ice-pellets' => 'Granizada débil', + 'weather_moderate-or-heavy-showers-of-ice-pellets' => 'Granizada moderada o fuerte', + 'weather_thundery-outbreaks-possible' => 'Posibles brotes de trueno', + 'weather_patchy-light-rain-with-thunder' => 'Lluvia débil irregular acompañada de truenos', + 'weather_moderate-or-heavy-rain-with-thunder' => 'Lluvia moderada o fuerte acompañada de truenos', + 'weather_patchy-light-snow-with-thunder' => 'Nevadas suaves acompañadas de truenos', + 'weather_moderate-or-heavy-snow-with-thunder' => 'Nevadas moderadas o fuertes acompañadas de truenos', + 'weather_current_temperature_celsius' => ':temperatura ºC', + 'weather_current_temperature_fahrenheit' => ':temperatura ºF', + 'weather_current_title' => 'Clima actual', + + // dav + 'dav_contacts' => 'Contactos', + 'dav_contacts_description' => 'Contactos de :name', + 'dav_birthdays' => 'Cumpleaños', + 'dav_birthdays_description' => 'Cumpleaños del contacto :name', + 'dav_tasks' => 'Tareas', + 'dav_tasks_description' => 'Tareas de :name', + + // contact list + 'contact_list_avatar' => 'Avatar', + 'contact_list_name' => 'Contacto', + 'contact_list_description' => 'Descripción', + +]; diff --git a/resources/lang/es/auth.php b/resources/lang/es/auth.php new file mode 100644 index 0000000..9aa3962 --- /dev/null +++ b/resources/lang/es/auth.php @@ -0,0 +1,89 @@ + 'Estas credenciales no coinciden con nuestros registros.', + 'throttle' => 'Demasiados intentos de acceso. Por favor intente nuevamente en :seconds segundos.', + 'not_authorized' => 'Usted no esta autorizado para ejecutar esta acción', + 'signup_disabled' => 'La registración se encuentra actualmente deshabilitada', + 'signup_error' => 'Se ha producido un error al intentar registrar el usuario', + 'back_homepage' => 'Volver al inicio', + 'mfa_auth_otp' => 'Autentícate con tú dispositivo de dos pasos', + 'mfa_auth_webauthn' => 'Autenticar con una clave de seguridad (WebAuthn)', + '2fa_title' => 'Autenticación en dos pasos', + '2fa_wrong_validation' => 'La autenticación en dos pasos ha fallado.', + '2fa_one_time_password' => 'Código de autenticación en dos pasos', + '2fa_recuperation_code' => 'Introduce un código de recuperación de autenticación en dos pasos', + '2fa_one_time_or_recuperation' => 'Introduzca un código de autenticación de doble factor o un código de recuperación', + '2fa_otp_help' => 'Abre tú aplicación móvil de autenticación en dos pasos y copia el código', + + 'login_to_account' => 'Inicia sesión en tu cuenta', + 'login_with_recovery' => 'Inicia sesión con un código de recuperación', + 'login_again' => 'Por favor inicia sesión de nuevo en tu cuenta', + 'email' => 'Email', + 'password' => 'Contraseña', + 'recovery' => 'Código de recuperación', + 'login' => 'Identificarse', + 'button_remember' => 'Recordarme', + 'password_forget' => '¿Olvidaste tu contraseña?', + 'password_reset' => 'Restablece tu contraseña', + 'use_recovery' => 'O puedes usar un código de recuperación', + 'signup_no_account' => '¿No tienes una cuenta?', + 'signup' => 'Regístrate', + 'create_account' => 'Crea la primera cuenta registrándote', + 'change_language_title' => 'Cambiar idioma:', + 'change_language' => 'Cambiar el idioma a :lang', + + 'password_reset_title' => 'Restablecer contraseña', + 'password_reset_email' => 'Correo Eletrónico', + 'password_reset_send_link' => 'Enviar enlace para restablecer la contraseña', + 'password_reset_password' => 'Contraseña', + 'password_reset_password_confirm' => 'Confirma Contraseña', + 'password_reset_action' => 'Restablecer contraseña', + 'password_reset_email_content' => 'Haz clic aquí para restablecer tu contraseña:', + + 'register_title_welcome' => 'Bienvenido a tu nueva instancia de Monica', + 'register_create_account' => 'Debes crear una cuenta para usar Monica', + 'register_title_create' => 'Crea tu cuenta de Monica', + 'register_login' => 'Inicia sesión si ya tienes una cuenta.', + 'register_email' => 'Introduzca una dirección de correo electrónico válida', + 'register_email_example' => 'you@home', + 'register_firstname' => 'Nombre', + 'register_firstname_example' => 'ej. John', + 'register_lastname' => 'Apellidos', + 'register_lastname_example' => 'ej. Doe', + 'register_password' => 'Contraseña', + 'register_password_example' => 'Escribe una contraseña segura', + 'register_password_confirmation' => 'Confirmar contraseña', + 'register_action' => 'Registrarse', + 'register_policy' => 'Registrarte implica que has leido y aceptas nuestra Política de Privacidad y Términos de uso.', + 'register_invitation_email' => 'Por motivos de seguridad, porfavor indica el email de la persona que te ha invitado a unirte a esta cuenta. Esta información aparece in el email de invitación.', + + 'confirmation_title' => 'Verifica tu dirección de correo electrónico', + 'confirmation_fresh' => 'Se ha enviado un correo electrónico con el enlace de verificación a tu dirección de correo electrónico.', + 'confirmation_check' => 'Antes de proceder, por favor comprueba el link de verificación en tu correo electrónico.', + 'confirmation_request_another' => 'Si no has recibido el correo electrónico haz clic aquí para solicitar otro.', + + 'confirmation_again' => 'Si deseas cambiar tu dirección de correo electrónico, puedes hacer clic aquí.', + 'email_change_current_email' => 'Dirección de correo electrónico actual:', + 'email_change_title' => 'Cambiar tu dirección de correo electrónico', + 'email_change_new' => 'Nueva dirección de correo electrónico', + 'email_changed' => 'Tu dirección de correo electrónico ha sido cambiada. Revisa tu bandeja de correo para validarla.', +]; diff --git a/resources/lang/es/changelog.php b/resources/lang/es/changelog.php new file mode 100644 index 0000000..ffd2e6d --- /dev/null +++ b/resources/lang/es/changelog.php @@ -0,0 +1,12 @@ + 'Cambios del producto', + 'note' => 'Nota: desafortunadamente, esta página está solo en inglés.', +]; diff --git a/resources/lang/es/dashboard.php b/resources/lang/es/dashboard.php new file mode 100644 index 0000000..5a368b4 --- /dev/null +++ b/resources/lang/es/dashboard.php @@ -0,0 +1,42 @@ + '¡Bienvenido/a a tu cuenta!', + 'dashboard_blank_description' => 'Monica es el lugar donde organizar todas las interacciones con los que te importan.', + 'dashboard_blank_cta' => 'Añade tu primer contacto', + 'dashboard_blank_illustration' => 'Ilustración por Freepik', + + 'notes_title' => 'Todavía no tienes notas destacadas.', + + 'tab_recent_calls' => 'Llamadas recientes', + 'tab_favorite_notes' => 'Notas favoritas', + 'tab_calls_blank' => 'Todavía no has registrado una llamada.', + 'tab_debts' => 'Deudas', + 'tab_debts_blank' => 'Todavía no has registrado una deuda.', + 'tab_tasks' => 'Tareas', + 'tab_tasks_blank' => 'Todavía no tienes ninguna tarea.', + + 'tasks_add_task_placeholder' => '¿En qué consiste esta tarea?', + 'tasks_tab_your_contacts' => 'Tareas relacionadas con tus contactos', + 'tasks_tab_your_tasks' => 'Tus tareas', + 'tasks_add_note' => 'Presiona Enter para añadir la tarea.', + 'task_add_cta' => 'Añadir una tarea', + + 'debts_you_owe' => 'Le debes a', + + 'statistics_contacts' => 'Contactos', + 'statistics_activities' => 'Actividades', + 'statistics_gifts' => 'Regalos', + + 'reminders_next_months' => 'Eventos en los próximos 3 meses', + 'reminders_none' => 'Ningún recordatorio este mes.', + + 'product_changes' => 'Cambios del producto', + 'product_view_details' => 'Ver detalles', +]; diff --git a/resources/lang/es/format.php b/resources/lang/es/format.php new file mode 100644 index 0000000..d321e30 --- /dev/null +++ b/resources/lang/es/format.php @@ -0,0 +1,36 @@ + 'M d, Y H:i', + 'short_date_year' => 'j M Y', + 'short_date' => 'j M', + 'short_month' => 'M', + 'short_month_year' => 'M Y', + 'short_day' => 'D', + 'full_date_year' => 'F d, Y', + 'full_month' => 'F', + 'full_month_year' => 'F Y', + 'full_hour' => 'h.i A', + + 'short_text' => '{text}…', +]; diff --git a/resources/lang/es/journal.php b/resources/lang/es/journal.php new file mode 100644 index 0000000..d0e63b0 --- /dev/null +++ b/resources/lang/es/journal.php @@ -0,0 +1,38 @@ + '¿Cómo fue tu día? Puedes valorarlo una vez al día.', + 'journal_come_back' => 'Gracias. Vuelve mañana para puntuar tu día de nuevo.', + 'journal_description' => 'Nota: el diario lista las entradas del diario y las automáticas como Actividades con tus contactos. Aunque puedes borrar las entradas del diario manualmente, la actividad tendrás que borrarla directamente en la página del contacto.', + 'journal_add' => 'Agregar una entrada de diario', + 'journal_edit' => 'Editar página de diario', + 'journal_empty' => 'Borrar diario', + 'journal_created_at' => 'Creada: {date}', + 'journal_created_automatically' => 'Creada autmáticamente', + 'journal_entry_type_journal' => 'Entrada de diario', + 'journal_entry_type_activity' => 'Actividad', + 'journal_entry_rate' => 'Valoraste tú día.', + 'journal_add_comment' => '¿Quieres añadir un comentario (opcional)?', + 'journal_show_comment' => 'Mostrar comentario', + 'entry_delete_success' => 'La entrada de diario ha sido eliminada correctamente.', + 'journal_add_title' => 'Título (opcional)', + 'journal_add_date' => 'Fecha', + 'journal_add_post' => 'Entrada', + 'journal_add_cta' => 'Guardar', + 'journal_blank_cta' => 'Añade tu primera entrada de diario', + 'journal_blank_description' => 'El diario te permite escribir eventos que te han pasado y recordarlos.', + 'delete_confirmation' => '¿Seguro que deseas eliminar esta entrada de tu diario?', + 'apply_filter' => 'Apply filter', + 'start_date' => 'Start Date', + 'end_date' => 'End Date', + 'per_page' => 'Per Page', + 'sort_order' => 'Sort By Created At', + 'ascending' => 'Ascending', + 'descending' => 'Descending', +]; diff --git a/resources/lang/es/logs.php b/resources/lang/es/logs.php new file mode 100644 index 0000000..19fc218 --- /dev/null +++ b/resources/lang/es/logs.php @@ -0,0 +1,29 @@ + 'Contacto creado.', + 'settings_log_contact_created_with_name' => 'Contacto añadido como :name.', + + // contat description update + 'contact_log_contact_description_updated' => 'Descripción actualizada.', + 'settings_log_contact_description_updated_with_name' => 'Actualizada la descripción de :name.', + + // contact description clear + 'contact_log_contact_description_cleared' => 'Descripción eliminada.', + 'settings_log_contact_description_cleared_with_name' => 'Descripción de :name eliminada.', + + // contact work information update + 'contact_log_contact_work_updated' => 'Actualizada información laboral.', + 'settings_log_contact_work_updated_with_name' => 'Actualizada información laboral de :name.', + + // company created + 'settings_log_company_created' => 'Creada una empresa llamada :name.', +]; diff --git a/resources/lang/es/mail.php b/resources/lang/es/mail.php new file mode 100644 index 0000000..2496a64 --- /dev/null +++ b/resources/lang/es/mail.php @@ -0,0 +1,53 @@ + 'Recordatorio para :contact', + 'greetings' => 'Hola, :username', + 'want_reminded_of' => 'Querías ser recordado de :reason', + 'for' => 'Para: :name', + 'comment' => 'Comentario: :comment', + 'footer_contact_info' => 'Añadir, ver, completar y cambiar información sobre este contacto:', + 'footer_contact_info2' => 'Ver el perfil de :name', + 'footer_contact_info2_link' => 'Ver el perfil de :name: :url', + + 'notification_subject_line' => 'Tiene un evento próximo', + 'notification_description' => 'En :count días (el :date), el siguiente evento sucederá:', + + 'stay_in_touch_subject_line' => 'Mantenga el contacto con :name', + 'stay_in_touch_subject_description' => 'Has pedido un recordatorio para permanecer en contacto con :name cada :frequency día. Has pedido un recordatorio para permanecer en contacto con :name cada :frequency días.', + + 'notifications_whoops' => '¡Ups!', + 'notifications_hello' => '¡Hola!', + 'notifications_regards' => 'Saludos', + 'notifications_footer' => 'Si tienes problemas para hacer clic en el botón ":actionText", copie y pegue la siguiente URL en su navegador web: [:actionURL](:actionURL)', + 'notifications_rights' => 'Todos los derechos reservados', + + 'confirmation_email_title' => 'Monica – Email de verificación', + 'confirmation_email_intro'=> 'Para validar tu email haz click en el botón de abajo', + 'confirmation_email_button' => 'Verificar la dirección de correo electrónico', + 'confirmation_email_bottom' => 'Si no has sido tu quién se ha registrado, no se requiere ninguna acción adicional.', + + 'password_reset_title' => 'Monica – Notificación de Restablecimiento de Contraseña', + 'password_reset_intro' => 'Ha recibido este mensaje porque se solicitó un restablecimiento de contraseña para su cuenta.', + 'password_reset_button' => 'Restablecer contraseña', + 'password_reset_expiration' => 'Este enlace de restablecimiento de contraseña expirará en :count minutos.', + 'password_reset_bottom' => 'Si no solicitó un restablecimiento de contraseña, no es necesaria ninguna acción adicional.', + + 'invitation_title' => 'Monica – Has sido invitado por :name', + 'invitation_intro' => 'Has sido invitado por :name (:email) a usar Monica, una buena herramienta de administración de relaciones personales.', + 'invitation_link' => 'Para aceptar la invitación haz clic en el siguiente enlace:', + 'invitation_button' => 'Aceptar invitación', + 'invitation_expiration' => 'Este enlace caducará en :count días.', + + 'export_title' => 'Su exportación está lista', + 'export_description' => 'Has solicitado una exportación de datos el :date. Ahora está listo para descargar.', + 'export_download' => 'Descargar archivo exportado', + +]; diff --git a/resources/lang/es/pagination.php b/resources/lang/es/pagination.php new file mode 100644 index 0000000..41e3172 --- /dev/null +++ b/resources/lang/es/pagination.php @@ -0,0 +1,25 @@ + '❮ Anterior', + 'next' => 'Siguiente ❯', + +]; diff --git a/resources/lang/es/passwords.php b/resources/lang/es/passwords.php new file mode 100644 index 0000000..3a269fd --- /dev/null +++ b/resources/lang/es/passwords.php @@ -0,0 +1,30 @@ + '¡Tu contraseña ha sido reestablecida!', + 'sent' => 'Si el correo eletrónico que ingresaste existe en nuestros registros te hemos enviado un correo para restablecer tu contraseña.', + 'token' => 'Este token para reestablecer la contraseña no es válido.', + 'user' => 'Si el correo eletrónico que ingresaste existe en nuestros registros te hemos enviado un correo para restablecer tu contraseña.', + 'changed' => 'Contraseña cambiada con éxito.', + 'invalid' => 'La contraseña que ingresaste no es correcta.', + 'throttled' => 'Por favor, espera antes de intentarlo de nuevo.', + +]; diff --git a/resources/lang/es/people.php b/resources/lang/es/people.php new file mode 100644 index 0000000..5d642ec --- /dev/null +++ b/resources/lang/es/people.php @@ -0,0 +1,539 @@ + 'Contacto no encontrado', + 'people_list_number_kids' => ':count hijo/a|:count hijos/as', + 'people_list_last_updated' => 'Última consulta:', + 'people_list_number_reminders' => ':count recordatorio|:count recordatorios', + 'people_list_blank_title' => 'Todavía no tienes a nadie en tu cuenta', + 'people_list_blank_cta' => 'Añade a alguien', + 'people_list_sort' => 'Ordenar', + 'people_list_stats' => ':count contacto|:count contactos', + 'people_list_firstnameAZ' => 'Ordenar por nombre A → Z', + 'people_list_firstnameZA' => 'Ordenar por nombre Z → A', + 'people_list_lastnameAZ' => 'Ordenar por apellido A → Z', + 'people_list_lastnameZA' => 'Ordenar por apellido Z → A', + 'people_list_lastactivitydateNewtoOld' => 'Ordenar por fecha de última actividad, de más reciente a más antiguo', + 'people_list_lastactivitydateOldtoNew' => 'Ordenar por fecha de última actividad, de más antiguo a más reciente', + 'people_list_filter_tag' => 'Mostrar todos los contactos etiquetados con', + 'people_list_clear_filter' => 'Quitar filtro', + 'people_list_contacts_per_tags' => ':count contacto|:count contactos', + 'people_list_show_dead' => 'Mostrar fallecidos (:count)', + 'people_list_hide_dead' => 'Ocultar fallecidos (:count)', + 'people_search' => 'Buscar en tus contactos…', + 'people_search_no_results' => 'No se encontraron resultados', + 'people_search_next' => 'Siguiente', + 'people_search_prev' => 'Anterior', + 'people_search_rows_per_page' => 'Filas por página', + 'people_search_of' => 'de', + 'people_search_page' => 'Página', + 'people_search_all' => 'Todos', + 'people_add_new' => 'Añadir a una nueva persona', + 'people_list_account_usage' => 'El uso de tu cuenta: :current/:limit contactos', + 'people_list_account_upgrade_title' => 'Actualiza tu cuenta para desbloquear todo su potencial.', + 'people_list_account_upgrade_cta' => 'Actualizar ahora', + 'people_list_untagged' => 'Ver contactos sin etiqueta', + 'people_list_filter_untag' => 'Mostrando todos los contactos sin etiqueta', + 'archived_contact_readonly' => 'El contacto archivado no puede ser editado, por favor desarchivelo primero.', + + // people add + 'people_add_title' => 'Añade a una nueva persona', + 'people_add_missing' => 'Persona no encontrada - añade una nueva ahora', + 'people_add_firstname' => 'Nombre', + 'people_add_middlename' => 'Segundo Nombre (Opcional)', + 'people_add_lastname' => 'Apellidos (opcional)', + 'people_add_email' => 'Correo electrónico (opcional)', + 'people_add_nickname' => 'Apodo (opcional)', + 'people_add_cta' => 'Añadir', + 'people_save_and_add_another_cta' => 'Añadir y agregar a alguien más', + 'people_add_success' => ':name ha sido creado exitosamente', + 'people_add_gender' => 'Género', + 'people_delete_success' => 'El contacto ha sido eliminado', + 'people_delete_message' => 'Borrar contacto', + 'people_delete_confirmation' => '¿Estás seguro que quieres eliminar el siguiente contacto: :name? Esta acción es permanente.', + 'people_add_birthday_reminder' => 'Desearle feliz cumpleaños a :name', + 'people_add_birthday_reminder_deceased' => 'En esta fecha, :name habría celebrado su cumpleaños', + 'people_add_import' => '¿Quieres importar tus contactos?', + 'people_edit_email_error' => 'Ya existe un contacto en tu cuenta con esta dirección de correo electrónico. Por favor, elije otro.', + 'people_export' => 'Exportar como vCard', + 'people_add_reminder_for_birthday' => 'Crear un recordatorio anual de cumpleaños', + + // show + 'section_contact_information' => 'Información de contacto', + 'section_personal_activities' => 'Actividades', + 'section_personal_reminders' => 'Recordatorios', + 'section_personal_tasks' => 'Tareas', + 'section_personal_gifts' => 'Regalos', + 'section_personal_notes' => 'Notas', + + // archived contacts + 'list_link_to_active_contacts' => 'Usted está viendo los contactos archivados. Cambiar a ver la lista de contactos activos.', + 'list_link_to_archived_contacts' => 'Lista de contactos archivados', + + // Header + 'me' => 'Este eres tú', + 'edit_contact_information' => 'Editar información de contacto', + 'contact_archive' => 'Archivar contacto', + 'contact_unarchive' => 'Des-archivar contacto', + 'contact_archive_help' => 'Los contactos archivados no se muestran en la lista de contactos, pero todavía aparecen en los resultados de búsqueda.', + 'call_button' => 'Registrar una llamada', + 'set_favorite' => 'Los contactos favoritos son colocados en la parte superior de la lista de contactos', + + // Stay in touch + 'stay_in_touch' => 'Mantenerse en contacto', + 'stay_in_touch_frequency' => 'Mantenerse en contacto cada día|Mantenerse en contacto cada {count} días', + 'stay_in_touch_next_date' => 'Siguiente vencido: {date}', + 'stay_in_touch_invalid' => 'La frecuencia debe ser un número mayor que 0.', + 'stay_in_touch_premium' => 'Necesitas actualizar tu cuenta para hacer uso de esta característica', + 'stay_in_touch_modal_title' => 'Mantenerse en contacto', + 'stay_in_touch_modal_desc' => 'Podemos recordarte que te mantengas en contacto con {firstname} regularmente por correo electrónico.', + 'stay_in_touch_modal_label' => 'Envíame un correo electrónico cada… {count} día|Envíame un correo electrónico cada… {count} días', + + // Calls + 'modal_call_title' => 'Registrar una llamada', + 'modal_call_comment' => '¿De qué hablaron? (Opcional)', + 'modal_call_exact_date' => 'La llamada telefónica ocurrió el', + 'modal_call_who_called' => 'Quién llamó?', + 'modal_call_emotion' => '¿Deseas registrar cómo te sentiste durante esta llamada? (opcional)', + 'calls_add_success' => 'La llamada telefónica ha sido guardada.', + 'call_delete_confirmation' => '¿Estás seguro que deseas eliminar esta llamada?', + 'call_delete_success' => 'La llamada telefónica ha sido eliminada exitosamente', + 'call_title' => 'Llamadas telefónicas', + 'call_empty_comment' => 'No hay detalles', + 'call_blank_title' => 'Mantén un seguimiento de las llamadas realizadas con {name}', + 'call_blank_desc' => 'Has llamado a {name}', + 'call_you_called' => 'Has llamado', + 'call_he_called' => '{name} llamó', + 'call_emotions' => 'Emociones:', + + // Conversation + 'conversation_blank' => 'Registra conversaciones que has tenido con :name por redes sociales, SMS…', + 'conversation_delete_link' => 'Borrar conversación', + 'conversation_edit_title' => 'Editar conversación', + 'conversation_edit_delete' => '¿Estás seguro que quieres eliminar esta conversación? Esta acción es permanente.', + 'conversation_add_success' => 'La conversación ha sido añadida con éxito.', + 'conversation_edit_success' => 'La conversación ha sido actualizada con éxito.', + 'conversation_delete_success' => 'La conversación ha sido eliminada exitosamente.', + 'conversation_add_title' => 'Registrar una nueva conversación', + 'conversation_add_when' => '¿Cuándo tuviste esta conversación?', + 'conversation_add_who_wrote' => '¿Quién envió este mensaje?', + 'conversation_add_how' => '¿Qué medio utilizaste para comunicarte?', + 'conversation_add_you' => 'Tu', + 'conversation_add_content' => 'Escribe lo que se dijo', + 'conversation_add_what_was_said' => '¿Qué fue lo que dijiste?', + 'conversation_add_another' => 'Añade otro mensaje', + 'conversation_add_error' => 'Debes añadir al menos un mensaje.', + 'conversation_list_table_messages' => 'Mensajes', + 'conversation_list_table_content' => 'Contenido parcial (último mensaje)', + 'conversation_list_title' => 'Conversaciones', + 'conversation_list_cta' => 'Registrar conversación', + + // age - birthday + 'birthdate_not_set' => 'No se ha definido cumpleaños', + 'age_approximate_in_years' => 'aproximadamente :age años de edad', + 'age_exact_in_years' => ':age años de edad', + 'age_exact_birthdate' => 'nació el :date', + + // Last called + 'last_called' => 'Última llamada: :date', + 'last_talked_to' => 'Última llamada: {date}', + 'last_called_empty' => 'Última llamada: desconocido', + 'last_activity_date' => 'Última actividad juntos: :date', + 'last_activity_date_empty' => 'Última actividad juntos: desconocido', + + // additional information + 'information_edit_success' => 'El perfil ha sido actualizado exitosamente', + 'information_edit_title' => 'Editar la información personal de :name', + 'information_edit_max_size' => 'Máximo :size Kb.', + 'information_edit_max_size2' => 'Máximo {size} kB.', + 'information_edit_firstname' => 'Nombre', + 'information_edit_lastname' => 'Apellidos (opcional)', + 'information_edit_description' => 'Descripción (opcional)', + 'information_edit_description_help' => 'Usado en la lista de contactos para agregar contexto, si fuera necesario.', + 'information_edit_unknown' => 'No sé la edad de esta persona', + 'information_edit_probably' => 'Esta persona es probablemente…', + 'information_edit_not_year' => 'Sé el día y mes de la fecha de nacimiento de esta persona, pero no el año…', + 'information_edit_exact' => 'Se la fecha exacta del cumpleaños de esta persona…', + 'information_edit_birthdate_label' => 'Cumpleaños', + 'information_no_work_defined' => 'Información de trabajo no definida', + 'information_work_at' => 'en :company', + 'work_add_cta' => 'Actualizar la información de trabajo', + 'work_edit_success' => 'Información de trabajo actualizada', + 'work_edit_title' => 'Actualizar la información de trabajo de :name', + 'work_edit_job' => 'Título (opcional)', + 'work_edit_company' => 'Empresa (opcional)', + 'work_information' => 'Información de trabajo', + + // food preferences + 'food_preferences_add_success' => 'Las preferencias de comida han sido guardadas', + 'food_preferences_edit_description' => 'Tal vez :firstname o alguien en la familia :family tiene una alergia. O no le gusta una botella específica de vino. Indica eso aquí, así lo recordaras la próxima vez que los invites a cenar', + 'food_preferences_edit_description_no_last_name' => 'Tal vez :firstname tiene una alergia. O no le gusta una botella específica de vino. Indica eso aquí, así lo recordaras la próxima vez que lo invites a cenar', + 'food_preferences_edit_title' => 'Indica preferencias en comida', + 'food_preferences_edit_cta' => 'Guardar preferencias en comida', + 'food_preferences_title' => 'Preferencias de comida', + 'food_preferences_cta' => 'Añadir preferencias en comida', + + // reminders + 'reminders_blank_title' => '¿Hay algo en lo que quisieras ser recordado sobre :name?', + 'reminders_blank_add_activity' => 'Agregar un recordatorio', + 'reminders_add_title' => '¿Qué te gustaría que se te recordara sobre :name?', + 'reminders_add_description' => 'Por favor recuérdame…', + 'reminders_add_next_time' => '¿Cuándo es la próxima vez que te gustaría que te recordaramos sobre esto?', + 'reminders_add_once' => 'Recuerdame sobre esto solo una vez', + 'reminders_add_recurrent' => 'Recuerdame sobre esto cada', + 'reminders_add_starting_from' => 'a partir de la fecha indicada arriba', + 'reminders_add_cta' => 'Añadir recordatorio', + 'reminders_edit_update_cta' => 'Actualizar recordatorio', + 'reminders_add_error_custom_text' => 'Necesitas indicar un texto para este recordatorio', + 'reminders_create_success' => 'El recordatorio ha sido añadido exitosamente', + 'reminders_delete_success' => 'El recordatorio ha sido eliminado exitosamente', + 'reminders_update_success' => 'El recordatorio ha sido actualizado exitosamente', + 'reminders_add_optional_comment' => 'Comentario opcional', + + 'reminder_frequency_day' => 'cada día|cada :number días', + 'reminder_frequency_week' => 'cada semana|cada :number semanas', + 'reminder_frequency_month' => 'cada mes|cada :number meses', + 'reminder_frequency_year' => 'cada año|cada :number años', + 'reminder_frequency_one_time' => 'el :date', + 'reminders_delete_confirmation' => '¿Estás seguro de que deseas eliminar este recordatorio?', + 'reminders_delete_cta' => 'Eliminar', + 'reminders_next_expected_date' => 'en', + 'reminders_cta' => 'Agregar un recordatorio', + 'reminders_description' => 'Te enviaremos un correo electrónico por cada uno de los siguientes recordatorios. Los recordatorios son enviados la mañana del día que el evento ocurre. Recordatorios automáticamente añadidos para cumpleaños no pueden ser eliminados. Si quieres cambiar esa fecha, edita la fecha de cumpleaños de los contactos.', + 'reminders_one_time' => 'Una vez', + 'reminders_type_week' => 'semana', + 'reminders_type_month' => 'mes', + 'reminders_type_year' => 'año', + 'reminders_birthday' => 'Cumpleaños de :name', + 'reminders_free_plan_warning' => 'Estas en el plan gratuito. Correos electrónicos no son enviados para este plan. Para recibir recordatorio por correo electrónico actualiza tu cuenta.', + + // relationships + 'relationship_form_add' => 'Añadir una nueva relación', + 'relationship_form_edit' => 'Editar una relación existente', + 'relationship_form_is_with' => 'Esta persona es…', + 'relationship_form_is_with_name' => ':name es…', + 'relationship_form_add_choice' => '¿Con quién es esta relación?', + 'relationship_form_create_contact' => 'Añade a una nueva persona', + 'relationship_form_associate_contact' => 'Un contacto existente', + 'relationship_form_associate_dropdown' => 'Busca y selecciona un contacto existente del menú desplegable a continuación', + 'relationship_form_associate_dropdown_placeholder' => 'Busca y selecciona un contacto existente', + 'relationship_form_also_create_contact' => 'Crear una entrada de contacto para esta persona.', + 'relationship_form_add_description' => 'Esto te permitirá tratar a esta persona como cualquier otro de tus contactos.', + 'relationship_form_add_no_existing_contact' => 'No tienes ningún contacto que se pueda relacionar con :name al momento.', + 'relationship_delete_confirmation' => '¿Estás seguro de que deseas eliminar esta relación? Esto es permanente.', + 'relationship_unlink_confirmation' => '¿Estás seguro de que deseas eliminar esta relación? Esta persona no será eliminada - solo la relación entre estas dos personas.', + 'relationship_form_add_success' => 'La relación ha sido creada exitosamente.', + 'relationship_form_deletion_success' => 'La relación ha sido eliminada.', + + // tasks + 'tasks_title' => 'Tareas', + 'tasks_blank_title' => 'Aún no tienes tareas.', + 'tasks_form_title' => 'Título', + 'tasks_form_description' => 'Descripción (opcional)', + 'tasks_add_task' => 'Añadir una tarea', + 'tasks_delete_success' => 'La tarea ha sido eliminada exitosamente', + 'tasks_complete_success' => 'La tarea ha cambiado de estado exitosamente', + + // activities + 'activity_title' => 'Actividades', + 'activity_type_category_simple_activities' => 'Actividades simples', + 'activity_type_category_sport' => 'Deportes', + 'activity_type_category_food' => 'Comida', + 'activity_type_category_cultural_activities' => 'Actividades culturales', + 'activity_type_just_hung_out' => 'solo pasamos el rato', + 'activity_type_watched_movie_at_home' => 'vimos una película en casa', + 'activity_type_talked_at_home' => 'solo hablamos en casa', + 'activity_type_did_sport_activities_together' => 'hicimos deporte juntos', + 'activity_type_ate_at_his_place' => 'comimos en su casa', + 'activity_type_went_bar' => 'fuimos al bar', + 'activity_type_ate_at_home' => 'comimos en casa', + 'activity_type_picnicked' => 'fuimos de picnic', + 'activity_type_ate_restaurant' => 'comimos en un restaurante', + 'activity_type_went_theater' => 'fuimos al teatro', + 'activity_type_went_concert' => 'fuimos a un concierto', + 'activity_type_went_play' => 'fuimos a jugar', + 'activity_type_went_museum' => 'fuimos al museo', + 'activities_add_activity' => 'Añadir actividad', + 'activities_add_more_details' => 'Añadir más detalles', + 'activities_add_emotions' => 'Añadir emociones', + 'activities_add_category' => 'Indicar una categoría', + 'activities_add_participants_cta' => 'Añadir participantes', + 'activities_item_information' => ':Activity. Sucedió el :date', + 'activities_add_title' => '¿Qué hiciste con {name}?', + 'activities_summary' => 'Describe lo que hiciste', + 'activities_add_pick_activity' => '¿Te gustaría categorizar esta actividad? No tienes que hacerlo, pero esto te dará estadísticas en el futuro (Opcional)', + 'activities_add_date_occured' => 'La actividad ocurrió el…', + 'activities_add_participants' => '¿Quién, aparte de {name}, participó en esta actividad? (opcional)', + 'activities_add_emotions_title' => '¿Deseas registrar cómo te sentiste durante esta llamada? (opcional)', + 'activities_blank_title' => 'Dale seguimiento a lo que has hecho con {name} en el pasado, y de que han hablado', + 'activities_blank_add_activity' => 'Añadir una actividad', + 'activities_add_success' => 'La actividad ha sido añadida exitosamente', + 'activities_add_error' => 'Ocurrió un error al añadir la actividad', + 'activities_update_success' => 'La actividad ha sido actualizada exitosamente', + 'activities_delete_success' => 'La actividad ha sido eliminada exitosamente', + 'activities_who_was_involved' => '¿Quién estuvo envuelto?', + 'activities_activity' => 'Categoría de la Actividad', + 'activities_view_activities_report' => 'Ver reporte de actividades', + 'activities_profile_title' => 'Reporte de actividades entre :name y tu', + 'activities_profile_subtitle' => 'Has registrado :total_activities actividad con :name en total, y :activities_last_twelve_months en los últimos 12 meses.|Has registrado :total_activities actividades con :name en total, y :activities_last_twelve_months en los últimos 12 meses.', + 'activities_profile_year_summary_activity_types' => 'Aquí tienes un desglose del tipo de actvidades que han tenido juntos en :year', + 'activities_profile_year_summary' => 'Aquí tienes lo que han hecho en :year', + 'activities_profile_number_occurences' => ':value actividad|:value actividades', + 'activities_list_participants' => 'Participantes ({total}):', + 'activities_list_emotions' => 'Emociones sentidas:', + 'activities_list_date' => 'Ocurrió el', + 'activities_list_category' => 'Categoría:', + + // notes + 'notes_create_success' => 'La nota ha sido creada exitosamente', + 'notes_update_success' => 'La nota ha sido guardada exitosamente', + 'notes_delete_success' => 'La nota ha sido eliminada exitosamente', + 'notes_add_cta' => 'Añadir nota', + 'notes_favorite' => 'Añadir/remover de favoritos', + 'notes_delete_title' => 'Eliminar una nota', + 'notes_delete_confirmation' => '¿Estás seguro que deseas eliminar esta nota? Esta acción es permanente', + + // gifts + 'gifts_title' => 'Regalos', + 'gifts_add_success' => 'El regalo ha sido añadido exitosamente', + 'gifts_delete_success' => 'El regalo ha sido eliminado exitosamente', + 'gifts_delete_confirmation' => '¿Estás seguro que deseas eliminar este regalo?', + 'gifts_add_gift' => 'Añadir un regalo', + 'gifts_link' => 'Enlace', + 'gifts_for' => 'Para: {name}', + 'gifts_delete_cta' => 'Eliminar', + 'gifts_add_title' => 'Gestión de regalos para :name', + 'gifts_add_gift_idea' => 'Idea de regalo', + 'gifts_add_gift_already_offered' => 'Regalo dado', + 'gifts_add_gift_received' => 'Regalos recibidos', + 'gifts_add_gift_title' => '¿Qué es este regalo?', + 'gifts_add_gift_name' => 'Nombre del regalo', + 'gifts_add_link' => 'Enlace al sitio web (opcional)', + 'gifts_add_value' => 'Valor (opcional)', + 'gifts_add_comment' => 'Comentario (opcional)', + 'gifts_add_recipient' => 'Destinatario (opcional)', + 'gifts_add_recipient_field' => 'Destinatario', + 'gifts_add_photo' => 'Foto (opcional)', + 'gifts_add_photo_title' => 'Añadir una foto para este regalo', + 'gifts_add_someone' => 'Este regalo es para alguien en particular de la familia de {name}', + 'gifts_delete_title' => 'Borrar un regalo', + 'gifts_ideas' => 'Idea de regalo', + 'gifts_offered' => 'Regalos dados', + 'gifts_offered_as_an_idea' => 'Marcar como una idea', + 'gifts_received' => 'Regalos recibidos', + 'gifts_view_comment' => 'Ver comentario', + 'gifts_mark_offered' => 'Marcar como dado', + 'gifts_update_success' => 'El regalo ha sido actualizado exitosamente', + 'gifts_add_date' => 'Fecha (opcional)', + + // debts + 'debt_delete_confirmation' => '¿Está seguro de que desea eliminar esta deuda?', + 'debt_delete_success' => 'La deuda ha sido eliminada exitosamente', + 'debt_add_success' => 'La deuda ha sido añadida exitosamente', + 'debt_title' => 'Deudas', + 'debt_add_cta' => 'Añadir deuda', + 'debt_you_owe' => 'Tú debes :amount', + 'debt_they_owe' => ':name te debe :amount', + 'debt_add_title' => 'Gestión de deudas', + 'debt_add_you_owe' => 'Tú le debes a :name', + 'debt_add_they_owe' => ':name te debe', + 'debt_add_amount' => 'la cantidad de', + 'debt_add_reason' => 'por la siguiente razón (opcional)', + 'debt_add_add_cta' => 'Añadir deuda', + 'debt_edit_update_cta' => 'Actualizar deuda', + 'debt_edit_success' => 'La deuda ha sido actualizada exitosamente', + 'debts_blank_title' => 'Gestiona deudas que tienes con :name ó deudas que :name te debe', + + // tags + 'tag_edit' => 'Editar etiqueta', + 'tag_add' => 'Agregar etiquetas', + 'tag_add_search' => 'Añadir o buscar etiquetas', + 'tag_no_tags' => 'No hay etiquetas aún', + + // Introductions + 'introductions_sidebar_title' => 'Cómo nos conocimos', + 'introductions_blank_cta' => 'Indica como conociste a :name', + 'introductions_title_edit' => '¿Cómo conociste a :name?', + 'introductions_additional_info' => 'Explica cómo y dónde se conocieron', + 'introductions_edit_met_through' => '¿Alguien te ha presentado esta persona?', + 'introductions_no_met_through' => 'Nadie', + 'introductions_first_met_date' => 'Fecha que se conocieron', + 'introductions_no_first_met_date' => 'No sé la fecha que nos conocimos', + 'introductions_first_met_date_known' => 'Esta es la fecha que nos conocimos', + 'introductions_add_reminder' => 'Añadir un recordatorio para celebrar este encuentro en el aniversario de cuando este evento sucedió', + 'introductions_update_success' => 'Has actualizado con éxito la información sobre cómo conociste a esta persona', + 'introductions_met_through' => 'Nos conocimos a través de :name', + 'introductions_met_date' => 'Nos conocimos en :date', + 'introductions_reminder_title' => 'Aniversario del día que se conocieron', + + // Deceased + 'deceased_reminder_title' => 'Aniversario de la muerte de :name', + 'deceased_mark_person_deceased' => 'Marcar esta persona como fallecida', + 'deceased_know_date' => 'Conozco la fecha en que esta persona murió', + 'deceased_add_reminder' => 'Añadir un recordatorio para esta fecha', + 'deceased_label' => 'Fallecido', + 'deceased_date_label' => 'Fecha de defunción', + 'deceased_label_with_date' => 'Fallecido el', + 'deceased_age' => 'Edad al momento del fallecimiento', + + // Contact information + 'contact_info_title' => 'Información de contacto', + 'contact_info_form_content' => 'Contenido', + 'contact_info_form_contact_type' => 'Tipo de contacto', + 'contact_info_form_personalize' => 'Personalizar', + 'contact_info_address' => 'Vive en', + + // Addresses + 'contact_address_title' => 'Direcciones', + 'contact_address_form_name' => 'Etiqueta (opcional)', + 'contact_address_form_street' => 'Calle (opcional)', + 'contact_address_form_city' => 'Ciudad (opcional)', + 'contact_address_form_province' => 'Provincia (opcional)', + 'contact_address_form_postal_code' => 'Código postal (opcional)', + 'contact_address_form_country' => 'País (opcional)', + 'contact_address_form_latitude' => 'Latitud (sólo números) (opcional)', + 'contact_address_form_longitude' => 'Longitud (sólo números) (opcional)', + + // Pets + 'pets_kind' => 'Tipo de mascota', + 'pets_name' => 'Nombre (opcional)', + 'pets_create_success' => 'La mascota ha sido añadida', + 'pets_update_success' => 'La mascota ha sido actualizada', + 'pets_delete_success' => 'La mascota ha sido eliminada', + 'pets_title' => 'Mascotas', + 'pets_reptile' => 'Reptil', + 'pets_bird' => 'Ave', + 'pets_cat' => 'Gato', + 'pets_dog' => 'Perro', + 'pets_fish' => 'Pez', + 'pets_hamster' => 'Hámster', + 'pets_horse' => 'Caballo', + 'pets_rabbit' => 'Conejo', + 'pets_rat' => 'Rata', + 'pets_small_animal' => 'Animal pequeño', + 'pets_other' => 'Otro', + + // life events + 'life_event_list_tab_life_events' => 'Eventos cotidianos', + 'life_event_list_tab_other' => 'Notas, recordatorios…', + 'life_event_list_title' => 'Eventos cotidianos', + 'life_event_blank' => 'Apunta los hechos de la vida de {name} Para tu futura referencia.', + 'life_event_list_cta' => 'Añade un evento notable', + 'life_event_create_category' => 'Todas las categorías', + 'life_event_create_life_event' => 'Añade un evento notable', + 'life_event_create_default_title' => 'Título (opcional)', + 'life_event_create_default_story' => 'Historia (opcional)', + 'life_event_create_date' => 'No necesitas indicar un mes o un día - sólo el año es obligatorio.', + 'life_event_create_default_description' => 'Añadir información sobre lo que sabes', + 'life_event_create_add_yearly_reminder' => 'Añadir un recordatorio anual para este evento', + 'life_event_create_success' => 'El evento notable ha sido añadido', + 'life_event_delete_title' => 'Borrar un evento notable', + 'life_event_delete_description' => '¿Estás seguro que quieres eliminar este evento notable? Esta acción es permanente.', + 'life_event_delete_success' => 'El evento notable ha sido eliminado', + 'life_event_date_it_happened' => 'Fecha cuando ocurrió', + 'life_event_category_work_education' => 'Trabajo y educación', + 'life_event_category_family_relationships' => 'Familia y amistades', + 'life_event_category_home_living' => 'Hogar y estilo de vida', + 'life_event_category_health_wellness' => 'Salud y bienestar', + 'life_event_category_travel_experiences' => 'Viajes y experiencias', + 'life_event_sentence_new_job' => 'Comenzó un nuevo trabajo', + 'life_event_sentence_retirement' => 'Jubilado', + 'life_event_sentence_new_school' => 'Comenzó la escuela', + 'life_event_sentence_study_abroad' => 'Estudió en el extranjero', + 'life_event_sentence_volunteer_work' => 'Comenzó a ser voluntario', + 'life_event_sentence_published_book_or_paper' => 'Publicó un artículo', + 'life_event_sentence_military_service' => 'Comenzó el servicio militar', + 'life_event_sentence_new_relationship' => 'Comenzó una relación', + 'life_event_sentence_engagement' => 'Se comprometió', + 'life_event_sentence_marriage' => 'Se casó', + 'life_event_sentence_anniversary' => 'Aniversario', + 'life_event_sentence_expecting_a_baby' => 'Espera un bebé', + 'life_event_sentence_new_child' => 'Tuvo un hijo', + 'life_event_sentence_new_family_member' => 'Añadido un miembro familiar', + 'life_event_sentence_new_pet' => 'Consiguió una mascota', + 'life_event_sentence_end_of_relationship' => 'Finalizó una relación', + 'life_event_sentence_loss_of_a_loved_one' => 'Perdió un ser querido', + 'life_event_sentence_moved' => 'Se trasladó', + 'life_event_sentence_bought_a_home' => 'Compró una casa', + 'life_event_sentence_home_improvement' => 'Hizo unas mejoras en el hogar', + 'life_event_sentence_holidays' => 'Se fue de vacaciones', + 'life_event_sentence_new_vehicle' => 'Consiguió un vehículo nuevo', + 'life_event_sentence_new_roommate' => 'Encontró alguien para compartir piso', + 'life_event_sentence_overcame_an_illness' => 'Superó una enfermedad', + 'life_event_sentence_quit_a_habit' => 'Dejó un vicio', + 'life_event_sentence_new_eating_habits' => 'Empezó nuevos hábitos alimenticios', + 'life_event_sentence_weight_loss' => 'Perdió peso', + 'life_event_sentence_wear_glass_or_contact' => 'Empezó a llevar gafas o lentillas', + 'life_event_sentence_broken_bone' => 'Se rompió un hueso', + 'life_event_sentence_removed_braces' => 'Se quitó los brackets', + 'life_event_sentence_surgery' => 'Tuvo una operación', + 'life_event_sentence_dentist' => 'Fue al dentista', + 'life_event_sentence_new_sport' => 'Inició la práctica de un deporte', + 'life_event_sentence_new_hobby' => 'Comenzó un hobby', + 'life_event_sentence_new_instrument' => 'Aprendió un nuevo instrumento', + 'life_event_sentence_new_language' => 'Aprendió un idioma adicional', + 'life_event_sentence_tattoo_or_piercing' => 'Se hizo un tatuaje o piercing', + 'life_event_sentence_new_license' => 'Consiguió un permiso', + 'life_event_sentence_travel' => 'Viajado', + 'life_event_sentence_achievement_or_award' => 'Consiguió un logro o premio', + 'life_event_sentence_changed_beliefs' => 'Creencias cambiadas', + 'life_event_sentence_first_word' => 'Habló por primera vez', + 'life_event_sentence_first_kiss' => 'Su primer beso', + + // documents + 'document_list_title' => 'Documentos', + 'document_list_cta' => 'Subir documento', + 'document_list_blank_desc' => 'Aquí usted puede almacenar documentos relacionados con esta persona.', + 'document_upload_zone_cta' => 'Subir un fichero', + 'document_upload_zone_progress' => 'Subiendo el documento…', + 'document_upload_zone_error' => 'Se produjo un error al subir el archivo. Por favor inténtelo de nuevo.', + + // Photos + 'photo_title' => 'Fotos', + 'photo_list_title' => 'Fotos relacionadas', + 'photo_list_cta' => 'Subir foto', + 'photo_list_blank_desc' => 'Puede almacenar imágenes acerca de este contacto. ¡Suba una ahora!', + 'photo_upload_zone_cta' => 'Carga una foto', + 'photo_current_profile_pic' => 'Foto de perfil actual', + 'photo_make_profile_pic' => 'Usar como foto de perfil', + 'photo_delete' => 'Eliminar foto', + 'photo_next' => 'Siguiente foto ❯', + 'photo_previous' => '❮ Foto anterior', + + // Avatars + 'avatar_change_title' => 'Cambiar el avatar', + 'avatar_question' => '¿Qué avatar deseas usar?', + 'avatar_default_avatar' => 'El avatar por defecto', + 'avatar_adorable_avatar' => 'El avatar Adorable', + 'avatar_gravatar' => 'El "Gravatar" asociado con la dirección de correo electrónico de esta persona. Gravatar es un sistema global que permite a los usuarios asociar direcciones de correo electrónico con fotos.', + 'avatar_current' => 'Mantener el avatar actual', + 'avatar_photo' => 'De una foto que subes', + 'avatar_crop_new_avatar_photo' => 'Recortar nueva foto de avatar', + + // emotions + 'emotion_this_made_me_feel' => 'Esto te hizo sentir…', + + // logs + 'auditlogs_link' => 'Historial', + 'auditlogs_title' => 'Todo lo que le pasó a :name', + 'auditlogs_breadcrumb' => 'Historial', + 'auditlogs_author' => 'Por :name el :date', + + // contact field label + 'contact_field_label_home' => 'Casa', + 'contact_field_label_work' => 'Trabajo', + 'contact_field_label_cell' => 'Móvil/Celular', + 'contact_field_label_fax' => 'Fax', + 'contact_field_label_pager' => 'Busca', + 'contact_field_label_main' => 'Principal', + 'contact_field_label_other' => 'Otro', + 'contact_field_label_personal' => 'Personal', +]; diff --git a/resources/lang/es/reminder.php b/resources/lang/es/reminder.php new file mode 100644 index 0000000..d028d11 --- /dev/null +++ b/resources/lang/es/reminder.php @@ -0,0 +1,16 @@ + 'Desear feliz cumpleaños a', + 'type_phone_call' => 'Llamar a', + 'type_lunch' => 'Comer con', + 'type_hangout' => 'Salir con', + 'type_email' => 'Email', + 'type_birthday_kid' => 'Desea feliz cumpleaños al hijo de', +]; diff --git a/resources/lang/es/settings.php b/resources/lang/es/settings.php new file mode 100644 index 0000000..24de4d0 --- /dev/null +++ b/resources/lang/es/settings.php @@ -0,0 +1,557 @@ + 'Configuración de cuenta', + 'sidebar_personalization' => 'Personalización', + 'sidebar_settings_storage' => 'Almacenamiento', + 'sidebar_settings_export' => 'Exportar datos', + 'sidebar_settings_users' => 'Usuarios', + 'sidebar_settings_subscriptions' => 'Subscripción', + 'sidebar_settings_import' => 'Importar datos', + 'sidebar_settings_tags' => 'Gestión de etiquetas', + 'sidebar_settings_api' => 'API', + 'sidebar_settings_dav' => 'Recursos DAV', + 'sidebar_settings_security' => 'Seguridad', + 'sidebar_settings_auditlogs' => 'Registro de auditoría', + + 'title_general' => 'Información general', + 'title_i18n' => 'Configuración internacional', + 'title_layout' => 'Diseño', + + 'me_title' => 'Yo como contacto', + 'me_help' => 'Este es el contacto que te representa a ti en Monica', + 'me_select' => 'Selecciona un contacto', + 'me_no_contact' => 'Ningún contacto seleccionado.', + 'me_select_click' => 'Haz clic aquí para seleccionar un contacto.', + 'me_remove_contact' => 'Eliminar la asociación', + 'me_choose' => 'Selecciónate a ti mismo', + 'me_choose_placeholder' => 'Selecciónate a ti mismo', + + 'export_title' => 'Exportar los datos de tu cuenta', + 'export_be_patient' => 'Haga clic en el botón para iniciar la exportación. Puede tardar varios minutos en procesar la exportación - por favor sea paciente y no haga spam en el botón.', + 'export_title_sql' => 'Exportar a SQL', + 'export_sql_explanation' => 'Exportar tus datos en formato SQL te permite coger tu información e importarla en tu propia instancia de Monica. Esto es útil sólo si tienes tu propio servidor.', + 'export_sql_cta' => 'Exportar a SQL', + 'export_sql_link_instructions' => 'Nota: lee las instrucciones para aprender más sobre como importar este archivo a tu propia instancia.', + 'export_title_json' => 'Exportar a Json', + 'export_submitted' => 'Su exportación ha sido enviada, estará disponible en un momento…', + 'export_json_explanation' => 'Exportando sus datos en formato Json para la copia de seguridad.', + 'export_json_beta' => 'La exportación de Json está en modo de vista previa. Dinos lo que piensas al respecto:', + 'export_json_cta' => 'Exportar a Json', + 'export_header_type' => 'Tipo', + 'export_header_timestamp' => 'Fecha de creación', + 'export_header_status' => 'Estado', + 'export_header_actions' => 'Acciones', + 'export_last_title' => 'Últimas exportaciones', + 'export_empty_title' => 'Aún no hay exportaciones', + 'export_type_json' => 'Exportación Json', + 'export_type_sql' => 'Exportación SQL', + 'export_status_todo' => 'Enviados', + 'export_status_doing' => 'En proceso', + 'export_status_done' => 'Hecho', + 'export_status_failed' => 'Fallido', + 'export_not_done' => 'Descarga imposible, esta exportación no ha terminado todavía.', + + 'firstname' => 'Nombre', + 'lastname' => 'Apellidos', + 'name_order' => 'Orden de los nombres', + 'name_order_firstname_lastname' => ' – Juan Pérez', + 'name_order_lastname_firstname' => ' – Pérez Juan', + 'name_order_firstname_lastname_nickname' => ' () – Juan Pérez (el Nota)', + 'name_order_firstname_nickname_lastname' => ' () – Juan (el Nota) Pérez', + 'name_order_lastname_firstname_nickname' => ' () – Pérez Juan (el Nota)', + 'name_order_lastname_nickname_firstname' => ' () – Pérez (el Nota) Juan', + 'name_order_nickname_firstname_lastname' => ' ( ) – El Nota (Juan Pérez)', + 'name_order_nickname_lastname_firstname' => ' ( ) – El Nota (Pérez Juan)', + 'name_order_nickname' => ' — el Nota', + 'currency' => 'Moneda', + 'name' => 'Tu nombre: :name', + 'email' => 'Correo electrónico', + 'email_placeholder' => 'Ingrese un email', + 'email_help' => 'Este es el correo electrónico usado para identificarte, y en el que recibirás tus recordatorios.', + 'timezone' => 'Zona horaria', + 'temperature_scale' => 'Escala de temperatura', + 'temperature_scale_fahrenheit' => 'Fahrenheit (°F)', + 'temperature_scale_celsius' => 'Celsius (˚C)', + 'layout' => 'Disposición', + 'layout_small' => 'Máximo 1200 pixels de ancho', + 'layout_big' => 'Usar ancho del navegador', + 'save' => 'Actualiza preferencias', + 'delete_title' => 'Eliminar tu cuenta', + 'delete_desc' => '¿Deseas eliminar tu cuenta? La eliminación es permanente, todos tus datos se perderán de manera permanente. Si tienes una suscripción, se cancelará inmediatamente.', + 'delete_other_desc' => 'Tus datos en la base de datos principal se borrarán inmediatamente. Tal y como explica nuestra política de privacidad, hacemos copias de seguridad diarias, cifradas de forma segura, que retenemos durante 30 días, tras los cuales se borran completamente. No podemos borrar información específica de las copias de seguridad antes de esto. Todos tus datos se borrarán completamente a los 31 días del borrado de tu cuenta.', + 'reset_desc' => '¿Deseas reiniciar tu cuenta? Esto borrará todos tus contactos, y todos los datos asociados con ellos. Tu cuenta no se eliminará.', + 'reset_title' => 'Resetear tu cuenta', + 'reset_cta' => 'Resetear tu cuenta', + 'reset_notice' => '¿Estás seguro de que quieres resetear tu cuenta? Esta acción es permanente e irreversible.', + 'reset_success' => 'Tu cuenta ha sido reseteada con éxito.', + 'delete_notice' => '¿Estás seguro de que deseas eliminar tu cuenta? Esto es permanente y no se puede deshacer. Todos tus datos serán eliminados y no se podrán recuperar.', + 'delete_cta' => 'Eliminar cuenta', + 'settings_success' => 'Preferencias actualizadas!', + 'locale' => 'Idiomas utilizados en la aplicación', + 'locale_help' => '¿Quieres ayudar a traducir Monica o añadir un nuevo idioma? Sigue este enlace para más información.', + 'locale_ar' => 'Árabe', + 'locale_cs' => 'Checo', + 'locale_de' => 'Alemán', + 'locale_el' => 'Griego', + 'locale_en' => 'Ingles', + 'locale_en-GB' => 'Inglés (Reino Unido)', + 'locale_es' => 'Español', + 'locale_fr' => 'Frances', + 'locale_he' => 'Hebreo', + 'locale_hr' => 'Croata', + 'locale_id' => 'Indonesio', + 'locale_it' => 'Italiano', + 'locale_ja' => 'Japonés', + 'locale_nl' => 'Alemán', + 'locale_pt' => 'Portugues', + 'locale_pt-BR' => 'Portuguese, Brazil', + 'locale_ru' => 'Ruso', + 'locale_sv' => 'Sueco', + 'locale_vi' => 'Vietnamita', + 'locale_zh' => 'Chino simplificado', + 'locale_zh-TW' => 'Chino tradicional', + 'locale_tr' => 'Turco', + + 'security_title' => 'Seguridad', + 'security_help' => 'Cambiar configuración de seguridad para tu cuenta.', + 'password_change' => 'Cambia tu contraseña', + 'password_current' => 'Contraseña actual', + 'password_current_placeholder' => 'Introduce tu contraseña actual', + 'password_new1' => 'Nueva contraseña', + 'password_new1_placeholder' => 'Introduzca su nueva contraseña', + 'password_new2' => 'Confirma tu nueva contraseña', + 'password_new2_placeholder' => 'Vuelve a escribir tu nueva contraseña', + 'password_btn' => 'Cambiar Contraseña', + '2fa_title' => 'Autenticación en dos pasos', + '2fa_otp_title' => 'Aplicación móvil de autenticación en dos pasos', + '2fa_enable_title' => 'Activar autenticación de dos pasos', + '2fa_enable_description' => 'Activar autenticación en dos pasos para aumentar la seguridad de tu cuenta.', + '2fa_enable_otp' => 'Abre tu aplicación móvil de autenticación en dos pasos y escanea el siguente código QR:', + '2fa_enable_otp_help' => 'Si tu aplicación móvil de Autenticación en dos pasos no soporta códigos QR, introduce el siguiente código:', + '2fa_enable_otp_validate' => 'Por favor, valida el dispositivo que acabas de configurar:', + '2fa_enable_success' => 'Autenticación en dos pasos activada', + '2fa_enable_error' => 'Se ha producido al activar la Autenticación en dos pasos', + '2fa_enable_error_already_set' => 'Autenticación en dos pasos ya está activada', + '2fa_disable_title' => 'Desactivar Autenticación en dos pasos', + '2fa_disable_description' => 'Desactivar la autenticación de dos factores para tu cuenta. ¡Ten cuidado, tu cuenta será mucho menos segura!', + '2fa_disable_success' => 'Autenticación en dos pasos desactivada', + '2fa_disable_error' => 'Se ha producido un error al desactivar la Autenticación en dos pasos', + + 'webauthn_title' => 'Clave de seguridad — Protocolo WebAuthn', + 'webauthn_enable_description' => 'Agregar nueva clave de seguridad', + 'webauthn_key_name_help' => 'Dale un nombre a tu clave.', + 'webauthn_key_name' => 'Nombre de la clave:', + 'webauthn_success' => 'Su clave ha sido detectada y validada.', + 'webauthn_last_use' => 'Último uso: {timestamp}', + 'webauthn_delete_confirmation' => '¿Estás seguro de que quieres borrar esta clave?', + 'webauthn_delete_success' => 'Clave eliminada', + 'webauthn_insertKey' => 'Inserte su clave de seguridad.', + 'webauthn_buttonAdvise' => 'Si tu clave de seguridad tiene un botón, presiónalo.', + 'webauthn_noButtonAdvise' => 'Si no lo hace, quítalo e insertalo de nuevo.', + 'webauthn_not_supported' => 'Tu navegador no soporta actualmente WebAuthn.', + 'webauthn_not_secured' => 'WebAuthn sólo soporta conexiones seguras. Por favor, cargue esta página con https.', + 'webauthn_error_already_used' => 'Esta clave ya está registrada. No es necesario registrarla de nuevo.', + 'webauthn_error_not_allowed' => 'La operación ha agotado el tiempo de espera o no ha sido permitida.', + + 'recovery_title' => 'Códigos de recuperación', + 'recovery_show' => 'Obtener códigos de recuperación', + 'recovery_copy_help' => 'Copiar códigos al portapaples', + 'recovery_help_intro' => 'Estos son tus códigos de recuperación:', + 'recovery_help_information' => 'Puedes usar cada código de recuperación una vez.', + 'recovery_clipboard' => 'Códigos copiados al portapapeles.', + 'recovery_generate' => 'Generar nuevos códigos…', + 'recovery_generate_help' => 'Generar nuevos códigos invalidará los códigos previamente generados.', + 'recovery_already_used_help' => 'Este código ya ha sido utilizado.', + + 'users_list_title' => 'Usuarios con acceso a tu cuenta', + 'users_list_add_user' => 'Invitar a un nuevo usuario', + 'users_list_you' => 'Ese/a eres tú', + 'users_list_invitations_title' => 'Invitaciones pendientes', + 'users_list_invitations_explanation' => 'Debajo están las personas que has invitado a unirse a Monica como colaboradores.', + 'users_list_invitations_invited_by' => 'invitado por :name', + 'users_list_invitations_sent_date' => 'enviada el :date', + 'users_blank_title' => 'Eres la única persona que tiene acceso a esta cuenta.', + 'users_blank_add_title' => '¿Te gustaría invitar a otra persona?', + 'users_blank_description' => 'Esta persona tendrá el mismo acceso que tú y podrá añadir, editar o eliminar la información de contactos.', + 'users_blank_cta' => 'Invitar a alguien', + 'users_add_title' => 'Invita a un nuevo usuario a tu cuenta por correo electrónico', + 'users_add_description' => 'Esta persona tendrá el mismo acceso que tú, incluyendo invitar o eliminar a otros usuarios, incluido tú. Asegúrate de confiar en esta persona antes de darle acceso.', + 'users_add_email_field' => 'Introduce el correo electrónico de la persona a la que quieres invitar', + 'users_add_confirmation' => 'Confirmo que quiero invitar a este usuario a mi cuenta. Entiendo que esta persona tendrá acceso a TODOS mis datos y verá exactamente lo mismo que yo veo.', + 'users_add_cta' => 'Invitar usuario por email', + 'users_accept_title' => 'Aceptar invitación y crear una nueva cuenta', + 'users_error_please_confirm' => 'Por favor, confirma que deseas invitar a este usuario antes de continuar con la invitación', + 'users_error_email_already_taken' => 'La dirección de correo ya ha sido utilizada. Utilice otra distinta', + 'users_error_already_invited' => 'Ya has invitado a este usuario. Por favor, elige otra dirección de correo electrónico.', + 'users_error_email_not_similar' => 'Este no es el correo electrónico de la persona que te ha invitado.', + 'users_invitation_deleted_confirmation_message' => 'La invitación se ha eliminado correctamente', + 'users_invitations_delete_confirmation' => '¿Estás seguro de que quieres borrar esta invitación?', + 'users_list_delete_confirmation' => '¿Estás seguro que deseas borar este usuario de tu cuenta?', + 'users_invitation_need_subscription' => 'Añadir más usuarios requiere una suscripción.', + + 'subscriptions_account_current_plan' => 'Tu plan actual', + 'subscriptions_account_current_legacy' => 'Plan actual, ya no seleccionable:', + 'subscriptions_account_current_paid_plan' => 'Tú plan actual es :name. Muchas gracias por tu suscripción.', + + 'subscriptions_account_next_billing_title' => 'Próxima factura', + 'subscriptions_account_next_billing' => 'Tu suscripción se renovará automáticamente el :date.', + 'subscriptions_account_bill_monthly' => 'Te facturaremos :price por otro mes.', + 'subscriptions_account_bill_annual' => 'Te facturaremos :price por otro año.', + 'subscriptions_account_change' => 'Cambiar Plan', + + 'subscriptions_account_cancel_title' => 'Cancelar suscripción', + 'subscriptions_account_cancel_action' => 'Cancelar suscripción', + 'subscriptions_account_cancel' => 'You can cancel subscription anytime.', + 'subscriptions_account_free_plan' => 'Tienes el plan gratuito.', + 'subscriptions_account_free_plan_upgrade' => 'Puedes mejorar tu cuenta al plan :name, que cuesta $:price al mes. Estas son las ventajas:', + 'subscriptions_account_free_plan_benefits_users' => 'Número ilimitado de usuarios', + 'subscriptions_account_free_plan_benefits_reminders' => 'Recordatorios por correo electrónico', + 'subscriptions_account_free_plan_benefits_import_data_vcard' => 'Importar tus contactos con vCard', + 'subscriptions_account_free_plan_benefits_support' => 'Respaldar el proyecto a largo plazo, para que podamos seguir añadiendo estupendas mejoras.', + 'subscriptions_account_upgrade' => 'Mejorar tu cuenta', + 'subscriptions_account_upgrade_title' => 'Mejora Mónica hoy y ten relaciones más significativas.', + 'subscriptions_account_upgrade_choice' => 'Escoja un plan a continuación y únase a :clientes que mejoraron su Monica.', + 'subscriptions_account_update_title' => 'Actualizar suscripción de Monica', + 'subscriptions_account_update_description' => 'Puedes cambiar la frecuencia de tu suscripción aquí.', + 'subscriptions_account_update_information' => 'Se te facturará inmediatamente por la nueva cantidad. Tu suscripción se ampliará al nuevo período, dependiendo de tu elección.', + 'subscriptions_account_invoices' => 'Recibos', + 'subscriptions_account_invoices_download' => 'Descargar', + 'subscriptions_account_invoices_subscription' => 'Suscripción de :startDate a :endDate', + 'subscriptions_account_payment' => '¿Qué opción de pago se ajusta mejor a ti?', + 'subscriptions_account_confirm_payment' => 'Tu pago está actualmente incompleto, por favor confirma tu pago.', + 'subscriptions_downgrade_title' => 'Reduzca su cuenta al plan gratuito', + 'subscriptions_downgrade_limitations' => 'El plan gratuito tiene limitaciones. Para poder degradar, tienes que pasar la siguiente lista de verificación:', + 'subscriptions_downgrade_rule_users' => 'Debes tener sólo 1 usuario en tu cuenta', + 'subscriptions_downgrade_rule_users_constraint' => 'Actualmente tienes 1 usuario en tu cuenta.|Actualmente tienes :count usuarios en tu cuenta.', + 'subscriptions_downgrade_rule_invitations' => 'No debes tener ninguna invitación pendiente', + 'subscriptions_downgrade_rule_invitations_constraint' => 'Actualmente tienes 1 invitación pendiente. |Actualmente tienes :count invitaciones pendientes.', + 'subscriptions_downgrade_rule_contacts' => 'No debe tener más de :number contactos activos', + 'subscriptions_downgrade_rule_contacts_constraint' => 'Actualmente tienes 1 contacto.|Actualmente tienes :count contactos.', + 'subscriptions_downgrade_rule_contacts_archive' => 'También podemos archivar todos tus contactos para ti – que borrarían esta regla y te permitirán continuar con el proceso de rebaja de tu cuenta.', + 'subscriptions_downgrade_cta' => 'Pasar a una suscripción inferior', + 'subscriptions_downgrade_success' => '¡Estás de vuelta al plan Gratis!', + 'subscriptions_downgrade_thanks' => 'Muchas gracias por probar el plan de pago. Seguimos añadiendo nuevas mejoras en Monica todo el tiempo, así que tal vez quieras volver en el futuro para ver si estás interesado en suscribirte de nuevo.', + 'subscriptions_back' => 'Volver a ajustes', + 'subscriptions_upgrade_title' => 'Mejorar tu cuenta', + 'subscriptions_upgrade_choose' => 'Has elegido el plan :plan.', + 'subscriptions_upgrade_infos' => 'No podemos estar más contentos. Introduce tu información de pago a continuación.', + 'subscriptions_upgrade_name' => 'Nombre en la tarjeta', + 'subscriptions_upgrade_zip' => 'ZIP / código postal', + 'subscriptions_upgrade_credit' => 'Tarjeta de crédito o débito', + 'subscriptions_upgrade_submit' => 'Pagar {amount}', + 'subscriptions_upgrade_charge' => 'Le cobraremos a tu tarjeta :price ahora. El siguiente cargo será el :date. Si alguna vez cambias de opinión, puedes cancelar en cualquier momento, sin preguntas formuladas.', + 'subscriptions_upgrade_charge_handled' => 'El pago es gestionado por Stripe. Ninguna información de la tarjeta toca nuestro servidor.', + 'subscriptions_upgrade_success' => '¡Gracias! Ahora estás suscrito.', + 'subscriptions_upgrade_thanks' => 'Bienvenidos a la comunidad de personas que intentan hacer del mundo un lugar mejor.', + + 'subscriptions_payment_confirm_title' => 'Confirme su pago :amount', + 'subscriptions_payment_confirm_information' => 'Se necesita confirmación adicional para procesar tu pago. Por favor, confirma tu pago completando los detalles de tu pago a continuación.', + 'subscriptions_payment_succeeded_title' => 'Pago exitoso', + 'subscriptions_payment_succeeded' => 'Este pago ya fue confirmado con éxito.', + 'subscriptions_payment_cancelled_title' => 'Pago Cancelado', + 'subscriptions_payment_cancelled' => 'El pago ha sido cancelado.', + 'subscriptions_payment_error_name' => 'Por favor ingrese su nombre.', + 'subscriptions_payment_success' => 'Pago realizado con exito.', + + 'subscriptions_pdf_title' => 'Tu suscripción mensual de :name', + 'subscriptions_plan_frequency_year' => ':amount / año', + 'subscriptions_plan_frequency_month' => ':amount / mes', + 'subscriptions_plan_choose' => 'Elegir este plan', + 'subscriptions_plan_year_title' => 'Pagar anualmente', + 'subscriptions_plan_year_bonus' => 'Paz mental durante todo un año', + 'subscriptions_plan_month_title' => 'Pagar mensualmente', + 'subscriptions_plan_month_bonus' => 'Cancelar en cualquier momento', + 'subscriptions_plan_include1' => 'Incluye con tu actualización:', + 'subscriptions_plan_include2' => 'Número ilimitado de contactos • Número ilimitado de usuarios • Recordatorios por correo electrónico • Importación con vCard • Personalización de la hoja de contacto', + 'subscriptions_plan_include3' => 'El 100% de los beneficios se destinan al desarrollo de este gran proyecto de código abierto.', + 'subscriptions_help_title' => 'Detalles adicionales sobre los que puedes tener curiosidad', + 'subscriptions_help_opensource_title' => '¿Qué es un proyecto de código abierto?', + 'subscriptions_help_opensource_desc' => 'Monica es un proyecto de código abierto. Esto significa que está construido por una comunidad que quiere construir una gran herramienta para el bien mayor. Ser de código abierto significa que el código está disponible públicamente en GitHub, y todos pueden inspeccionarlo, modificarlo o mejorarlo. Todo el dinero que recaudamos se dedica a construir mejores características, a pagar por servidores más poderosos y a pagar otros costes. Gracias por tu ayuda. No podríamos hacerlo sin ti.', + 'subscriptions_help_limits_title' => '¿Existe un límite en el número de contactos que podemos tener en el plan gratuito?', + 'subscriptions_help_limits_plan' => 'Sí. Los planes gratuitos te permiten administrar :number contacts.', + 'subscriptions_help_discounts_title' => '¿Tiene descuentos para educación y sin fines de lucro?', + 'subscriptions_help_discounts_desc' => '¡Los tenemos! Mónica es gratuita para los estudiantes, y gratuita para organizaciones benéficas y sin fines de lucro. Póngase en contacto con el equipo de soporte con una prueba de su estado y aplicaremos este estado especial en su cuenta.', + 'subscriptions_help_change_title' => '¿Qué pasa si cambio de opinión?', + 'subscriptions_help_change_desc' => 'Puedes cancelar en cualquier momento, sin preguntas y por ti mismo – sin necesidad de ponerte en contacto con soporte técnico. Sin embargo, no se le reembolsará durante el período actual.', + + 'stripe_error_card' => 'Tu tarjeta fue rechazada. El mensaje es: :message', + 'stripe_error_api_connection' => 'La comunicación de red con Stripe falló. Inténtalo de nuevo más tarde.', + 'stripe_error_rate_limit' => 'Demasiadas solicitudes con Stripe ahora mismo. Inténtalo de nuevo más tarde.', + 'stripe_error_invalid_request' => 'Parámetros inválidos. Inténtalo de nuevo más tarde.', + 'stripe_error_authentication' => 'Autenticación incorrecta con Stripe', + + 'import_title' => 'Importar contactos en tu cuenta', + 'import_cta' => 'Subir contactos', + 'import_stat' => 'Has importado :number archivos hasta ahora.', + 'import_result_stat' => 'Tarjeta vCard cargada con 1 contacto (:total_imported, :total_skipped saltado)|Tarjeta vs cargada con :total_contacts contactos (:total_imported, :total_skipped saltado)', + 'import_view_report' => 'Ver informe', + 'import_in_progress' => 'La importación está en progreso. Recarga la página en un minuto.', + 'import_upload_title' => 'Importa tus contactos con vCard', + 'import_upload_rules_desc' => 'Sin embargo, tenemos algunas normas:', + 'import_upload_rule_format' => 'Soportamos archivos .vcard y .vcf.', + 'import_upload_rule_vcard' => 'Soportamos el formato vCard 3.0, que es el formato predeterminado para Contacts.app (macOS) y Google Contacts.', + 'import_upload_rule_instructions' => 'Instrucciones de exportación para macOS Contacts.app y Google Contacts.', + 'import_upload_rule_multiple' => 'Si sus contactos tienen varias direcciones de correo electrónico o números de teléfono, sólo se guardará la primera entrada.', + 'import_upload_rule_limit' => 'Los archivos están limitados a 10 MB.', + 'import_upload_rule_time' => 'Puede tardar hasta un minuto en subir los contactos y procesarlos. Por favor, sea paciente.', + 'import_upload_rule_cant_revert' => 'Por favor, asegúrese de que los datos son precisos antes de cargarlos, ya que no puede deshacer.', + 'import_upload_form_file' => 'Tu archivo .vcf o .vCard:', + 'import_upload_behaviour' => 'Comportamiento de importación:', + 'import_upload_behaviour_add' => 'Añadir nuevos contactos y omitir existentes', + 'import_upload_behaviour_replace' => 'Reemplazar contactos existentes', + 'import_upload_behaviour_help' => 'La sustitución reemplazará todos los datos encontrados en la vCard, pero mantendrá los campos de contacto existentes.', + 'import_report_title' => 'Importando reporte', + 'import_report_date' => 'Fecha de importación', + 'import_report_type' => 'Tipo de importación', + 'import_report_number_contacts' => 'Número de contactos en el archivo', + 'import_report_number_contacts_imported' => 'Número de contactos importados', + 'import_report_number_contacts_skipped' => 'Número de contactos omitidos', + 'import_report_status_imported' => 'Importados', + 'import_report_status_skipped' => 'Omitidos', + 'import_vcard_parse_error' => 'Error al analizar la entrada vCard', + 'import_vcard_contact_exist' => 'El contacto ya existe', + 'import_vcard_contact_no_firstname' => 'Sin nombre (obligatorio)', + 'import_vcard_file_not_found' => 'Archivo no encontrado', + 'import_vcard_unknown_entry' => 'Nombre de contacto desconocido', + 'import_vcard_file_no_entries' => 'El archivo no contiene entradas', + 'import_blank_title' => 'Aún no has importado ningún contacto.', + 'import_blank_question' => '¿Quieres importar contactos ahora?', + 'import_blank_description' => 'Podemos importar archivos vCard que puedes obtener de Google Contacts o de tu administrador de contactos.', + 'import_blank_cta' => 'Importar vCard', + 'import_need_subscription' => 'La importación de datos requiere una suscripción.', + + 'tags_list_title' => 'Etiquetas', + 'tags_list_description' => 'Puede organizar sus contactos configurando etiquetas. Las etiquetas funcionan como carpetas, pero puede añadir más de una etiqueta a un contacto. Para añadir una nueva etiqueta, añádela en el contacto mismo.', + 'tags_list_contact_number' => '1 contacto|:count contactos', + 'tags_list_delete_success' => 'La etiqueta se ha eliminado correctamente', + 'tags_list_edit_success' => 'The tag has been successfully updated', + 'tags_list_delete_confirmation' => '¿Está seguro que desea eliminar la etiqueta? Ningún contacto será eliminado, sólo la etiqueta.', + 'tags_blank_title' => 'Las etiquetas son una excelente forma de clasificar tus contactos.', + 'tags_blank_description' => 'Las etiquetas funcionan como carpetas, pero puede añadir más de una etiqueta a un contacto. Ir a un contacto y etiquetar a un amigo, justo debajo del nombre. Una vez que un contacto está etiquetado, vuelve aquí para gestionar todas las etiquetas de tu cuenta.', + + 'api_title' => 'Acceso API', + 'api_description' => 'La API se puede utilizar para manipular los datos de Monica desde una aplicación externa, como una aplicación móvil por ejemplo.', + 'api_help' => 'Para utilizar la API, un token es obligatorio. Puede crear un token de acceso personal (autenticación de portador), o autorizar a un cliente de OAuth a crearlo para usted. Vea documentación API.', + 'api_endpoint' => 'El punto final de la API para esta instancia de Monica es:', + + 'api_personal_access_tokens' => 'Tokens de acceso personal', + 'api_pao_description' => 'Asegúrese de dar este token a una fuente en la que confíe – ya que le permite acceder a todos sus datos.', + 'api_token_title' => 'Tokens de acceso personal', + 'api_token_create_new' => 'Crear nuevo token', + 'api_token_not_created' => 'No ha creado ningún token de acceso personal.', + 'api_token_name' => 'Nombre del token', + 'api_token_expire' => 'Expira el {date}', + 'api_token_delete' => 'Eliminar', + 'api_token_create' => 'Crear token', + 'api_token_scopes' => 'Alcances', + 'api_token_help' => 'Aquí está su nuevo token de acceso personal. Esta es la única vez que se mostrará así que no lo pierdas! Ahora puedes usar este token para hacer solicitudes de API.', + + 'api_oauth_clients' => 'Sus clientes OAuth', + 'api_oauth_clients_desc' => 'Esta sección le permite registrar sus propios clientes de OAuth.', + 'api_oauth_clients_desc2' => 'Usa este id de cliente para solicitar un nuevo token y convertir códigos de autorización para los tokens de acceso. Consulta la documentación de Laravel Passport para más información.', + 'api_oauth_title' => 'Clientes de OAuth', + 'api_oauth_create_new' => 'Crear un Nuevo Cliente', + 'api_oauth_edit' => 'Editar cliente', + 'api_oauth_not_created' => 'No ha creado ningún cliente OAuth.', + 'api_oauth_clientid' => 'ID de cliente', + 'api_oauth_name' => 'Nombre', + 'api_oauth_name_help' => 'Algo que sus usuarios reconocerán y en lo que confiarán.', + 'api_oauth_secret' => 'Secreto', + 'api_oauth_create' => 'Crear un nuevo cliente', + 'api_oauth_redirecturl' => 'URL de redirección', + 'api_oauth_redirecturl_help' => 'La URL de devolución de autorización de tu aplicación.', + + 'api_authorized_clients' => 'Lista de clientes autorizados', + 'api_authorized_clients_desc' => 'Esta sección lista a todos los clientes que has autorizado para acceder a los datos de tu aplicación. Puedes revocar esta autorización en cualquier momento.', + 'api_authorized_clients_title' => 'Aplicaciones Autorizadas', + 'api_authorized_clients_none' => 'Todavía no hay clientes autorizados.', + 'api_authorized_clients_name' => 'Nombre', + 'api_authorized_clients_scopes' => 'Alcances', + + 'personalization_tab_title' => 'Personaliza tu cuenta', + + 'personalization_title' => 'Aquí encontrará diferentes ajustes para configurar su cuenta. Estas características están destinadas a “usuarios potenciales” que desean el máximo control sobre Mónica.', + 'personalization_contact_field_type_title' => 'Tipos de campos de contacto', + 'personalization_contact_field_type_add' => 'Añadir nuevo tipo de campo', + 'personalization_contact_field_type_description' => 'Puede configurar todos los diferentes tipos de campos de contacto que puede asociar a todos sus contactos. Por ejemplo, si aparece una nueva red social en el futuro, podrás añadir esta nueva forma de comunicarte con tus contactos aquí mismo.', + 'personalization_contact_field_type_table_name' => 'Nombre', + 'personalization_contact_field_type_table_protocol' => 'Protocolo', + 'personalization_contact_field_type_table_actions' => 'Acciones', + 'personalization_contact_field_type_modal_title' => 'Añadir un nuevo tipo de campo de contacto', + 'personalization_contact_field_type_modal_edit_title' => 'Editar un tipo de campo de contacto existente', + 'personalization_contact_field_type_modal_delete_title' => 'Eliminar un tipo de campo de contacto existente', + 'personalization_contact_field_type_modal_delete_description' => '¿Está seguro que desea eliminar este tipo de campo de contacto? Eliminar este tipo de campo de contacto eliminará TODOS los datos con este tipo para todos sus contactos.', + 'personalization_contact_field_type_modal_name' => 'Nombre', + 'personalization_contact_field_type_modal_protocol' => 'Protocolo (opcional)', + 'personalization_contact_field_type_modal_protocol_help' => 'Cada nuevo tipo de campo de contacto puede ser clicable. Si se establece un protocolo, lo usaremos para desencadenar la acción que se establece.', + 'personalization_contact_field_type_modal_icon' => 'Icono (Opcional)', + 'personalization_contact_field_type_modal_icon_help' => 'Puede asociar un icono con este tipo de campo de contacto. Necesita añadir una referencia a un icono de fuente impresionante.', + 'personalization_contact_field_type_delete_success' => 'El tipo de campo de contacto se ha eliminado correctamente.', + 'personalization_contact_field_type_add_success' => 'El tipo de campo de contacto se ha añadido correctamente.', + 'personalization_contact_field_type_edit_success' => 'El tipo de campo de contacto se ha actualizado correctamente.', + + 'personalization_genders_title' => 'Tipos de género', + 'personalization_genders_add' => 'Añadir nuevo tipo de género', + 'personalization_genders_desc' => 'Puedes definir tantos géneros como necesites. Necesitas al menos un tipo de género en tu cuenta.', + 'personalization_genders_modal_add' => 'Añadir tipo de género', + 'personalization_genders_modal_edit' => 'Actualizar tipo de género', + 'personalization_genders_modal_name' => 'Nombre', + 'personalization_genders_modal_name_help' => 'El nombre utilizado para mostrar el género en una página de contacto.', + 'personalization_genders_modal_sex' => 'Sexo', + 'personalization_genders_modal_sex_help' => 'Utilizado para definir las relaciones, y durante el proceso de importación/exportación de VCard.', + 'personalization_genders_modal_default' => 'Seleccione el género por defecto para un nuevo contacto', + 'personalization_genders_modal_delete' => 'Eliminar tipo de género', + 'personalization_genders_modal_delete_desc' => '¿Está seguro de que desea eliminar el género “{name}”?', + 'personalization_genders_modal_delete_question' => 'Actualmente tienes {count} contacto con este género. Si eliminas este género, ¿qué género debe tener este contacto?|Actualmente tienes {count} contactos con este género. Si eliminas este género, ¿qué género deberían tener estos contactos?', + 'personalization_genders_modal_delete_question_default' => 'Este género es el predeterminado. Si eliminas este género, ¿cuál será el nuevo valor predeterminado?', + 'personalization_genders_modal_error' => 'Por favor, elija un género de la lista.', + 'personalization_genders_list_contact_number' => '{count} contacto|{count} contactos', + 'personalization_genders_table_name' => 'Nombre', + 'personalization_genders_table_sex' => 'Sexo', + 'personalization_genders_table_default' => 'Por defecto', + 'personalization_genders_default' => 'Género por defecto', + 'personalization_genders_make_default' => 'Cambiar género por defecto', + 'personalization_genders_select_default' => 'Seleccionar género por defecto', + 'personalization_genders_m' => 'Masculino', + 'personalization_genders_f' => 'Femenino', + 'personalization_genders_o' => 'Otro', + 'personalization_genders_u' => 'Desconocido', + 'personalization_genders_n' => 'Ninguno o no aplicable', + + 'personalization_reminder_rule_save' => 'El cambio se ha guardado', + 'personalization_reminder_rule_title' => 'Reglas de recordatorio', + 'personalization_reminder_rule_line' => '{count} días antes|{count} días antes', + 'personalization_reminder_rule_desc' => 'Por cada recordatorio que haya establecido, Monica puede enviarle un correo electrónico varios días antes de que el evento suceda. Puedes ajustar estos ajustes de notificación aquí. Estas notificaciones solo se aplican a recordatorios mensuales y anuales.', + + 'personalization_module_save' => 'El cambio se ha guardado', + 'personalization_module_title' => 'Características', + 'personalization_module_desc' => 'Puede que no necesite todas las características de Monica. Debajo puede cambiar características específicas que se utilizan en una hoja de contacto. Este cambio afectará a TODOS tus contactos. Desactivar una función no elimina ningún dato, simplemente oculta la función.', + + 'personalisation_paid_upgrade' => 'Esta es una característica premium que requiere que esté activa una suscripción de pago. Actualice su cuenta visitando Ajustes > Suscripción.', + 'personalisation_paid_upgrade_vue' => 'Esta es una característica premium que requiere que esté activa una suscripción de pago. Actualice su cuenta visitando Ajustes > Suscripción.', + + 'reminder_time_to_send' => 'Hora del día a la que se enviarán los recordatorios', + 'reminder_time_to_send_help' => 'Su siguiente recordatorio está programado para ser enviado el {dateTime}.', + + 'personalization_activity_type_category_title' => 'Categorías de tipo de actividad', + 'personalization_activity_type_category_add' => 'Añadir una nueva categoría de tipo de actividad', + 'personalization_activity_type_category_table_name' => 'Nombre', + 'personalization_activity_type_category_description' => 'Una actividad con uno de tus contactos puede tener un tipo y un tipo de categoría. Su cuenta viene con un conjunto de tipos de categorías predefinidas por defecto, pero puede personalizar estos aquí.', + 'personalization_activity_type_category_table_actions' => 'Acciones', + 'personalization_activity_type_category_modal_add' => 'Añadir una nueva categoría de tipo de actividad', + 'personalization_activity_type_category_modal_edit' => 'Editar una categoría de tipo de actividad', + 'personalization_activity_type_category_modal_question' => '¿Qué debemos llamar a esta nueva categoría?', + 'personalization_activity_type_add_button' => 'Añadir un nuevo tipo de actividad', + 'personalization_activity_type_modal_add' => 'Añadir un nuevo tipo de actividad', + 'personalization_activity_type_modal_question' => '¿Qué debemos llamar a este nuevo tipo de actividad?', + 'personalization_activity_type_modal_edit' => 'Editar un tipo de actividad', + 'personalization_activity_type_category_modal_delete' => 'Eliminar una categoría de tipo de actividad', + 'personalization_activity_type_category_modal_delete_desc' => '¿Está seguro que desea eliminar esta categoría? Eliminarla eliminará todos los tipos de actividad asociados. Las actividades que pertenecen a esta categoría no se verán afectadas por esta eliminación.', + 'personalization_activity_type_modal_delete' => 'Eliminar un tipo de actividad', + 'personalization_activity_type_modal_delete_desc' => '¿Está seguro que desea eliminar este tipo de actividad? Las actividades que pertenecen a esta categoría no se verán afectadas por esta eliminación.', + 'personalization_activity_type_modal_delete_error' => 'No podemos encontrar este tipo de actividad.', + 'personalization_activity_type_category_modal_delete_error' => 'No podemos encontrar esta categoría de tipo de actividad.', + + 'personalization_life_event_category_title' => 'Categorías de eventos vitales', + 'personalization_live_event_category_table_name' => 'Nombre', + 'personalization_life_event_category_description' => 'Un evento de vida puede tener un tipo y una categoría. Su cuenta viene con un conjunto de categorías y tipos predefinidos por defecto, pero puede personalizar los tipos de eventos de la vida aquí.', + 'personalization_live_event_category_table_actions' => 'Acciones', + 'personalization_life_event_type_add_button' => 'Añadir un nuevo tipo de evento de vida', + 'personalization_life_event_type_modal_add' => 'Añadir un nuevo tipo de evento de vida', + 'personalization_life_event_type_modal_question' => '¿Qué debemos llamar a este nuevo tipo de evento de vida?', + 'personalization_life_event_type_modal_edit' => 'Editar un tipo de evento de vida', + 'personalization_life_event_type_modal_delete' => 'Eliminar un tipo de evento de vida', + 'personalization_life_event_type_modal_delete_desc' => '¿Está seguro de que desea eliminar este tipo de evento vital? Los eventos de la vida que pertenecen a este tipo se eliminarán al realizar esta acción.', + 'personalization_life_event_type_modal_delete_error' => 'No podemos encontrar este tipo de evento de vida.', + + 'personalization_life_event_category_work_education' => 'Trabajo y educación', + 'personalization_life_event_category_family_relationships' => 'Familia y relaciones', + 'personalization_life_event_category_home_living' => 'Hogar y estilo de vida', + 'personalization_life_event_category_travel_experiences' => 'Viajes y experiencias', + 'personalization_life_event_category_health_wellness' => 'Salud y bienestar', + + 'personalization_life_event_type_new_job' => 'Nuevo trabajo', + 'personalization_life_event_type_retirement' => 'Jubilación', + 'personalization_life_event_type_new_school' => 'Nueva escuela', + 'personalization_life_event_type_study_abroad' => 'Estudiar en el extranjero', + 'personalization_life_event_type_volunteer_work' => 'Trabajo voluntario', + 'personalization_life_event_type_published_book_or_paper' => 'Publicó un libro o papel', + 'personalization_life_event_type_military_service' => 'Servicio militar', + 'personalization_life_event_type_first_met' => 'Primer encuentro', + 'personalization_life_event_type_new_relationship' => 'Nueva relación', + 'personalization_life_event_type_engagement' => 'Compromiso', + 'personalization_life_event_type_marriage' => 'Matrimonio', + 'personalization_life_event_type_anniversary' => 'Aniversario', + 'personalization_life_event_type_expecting_a_baby' => 'Esperando un bebé', + 'personalization_life_event_type_new_child' => 'Nuevo hijo', + 'personalization_life_event_type_new_family_member' => 'Nuevo miembro de la familia', + 'personalization_life_event_type_new_pet' => 'Nueva mascota', + 'personalization_life_event_type_end_of_relationship' => 'Fin de la relación', + 'personalization_life_event_type_loss_of_a_loved_one' => 'Pérdida de un ser querido', + 'personalization_life_event_type_moved' => 'Mudanza', + 'personalization_life_event_type_bought_a_home' => 'Compró una casa', + 'personalization_life_event_type_home_improvement' => 'Mejoras en el hogar', + 'personalization_life_event_type_holidays' => 'Vacaciones', + 'personalization_life_event_type_new_vehicle' => 'Nuevo vehículo', + 'personalization_life_event_type_new_roommate' => 'Nuevo compañero de habitación', + 'personalization_life_event_type_overcame_an_illness' => 'Superó una enfermedad', + 'personalization_life_event_type_quit_a_habit' => 'Dejó un vicio', + 'personalization_life_event_type_new_eating_habits' => 'Nuevos hábitos alimenticios', + 'personalization_life_event_type_weight_loss' => 'Pérdida de peso', + 'personalization_life_event_type_wear_glass_or_contact' => 'Empezó a usar gafas o lentillas', + 'personalization_life_event_type_broken_bone' => 'Se rompió un hueso', + 'personalization_life_event_type_removed_braces' => 'Se quitó los brackets', + 'personalization_life_event_type_surgery' => 'Tuvo una operación', + 'personalization_life_event_type_dentist' => 'Tratamiento dental', + 'personalization_life_event_type_new_sport' => 'Comenzó a practicar un nuevo deporte', + 'personalization_life_event_type_new_hobby' => 'Comenzó un nuevo hobby', + 'personalization_life_event_type_new_instrument' => 'Comenzó a aprender un nuevo instrumento', + 'personalization_life_event_type_new_language' => 'Comenzó a aprender un nuevo idioma', + 'personalization_life_event_type_tattoo_or_piercing' => 'Tatuaje o piercing', + 'personalization_life_event_type_new_license' => 'Nueva licencia', + 'personalization_life_event_type_travel' => 'Viaje', + 'personalization_life_event_type_achievement_or_award' => 'Logro o premio', + 'personalization_life_event_type_changed_beliefs' => 'Creencias cambiadas', + 'personalization_life_event_type_first_word' => 'Primera palabra', + 'personalization_life_event_type_first_kiss' => 'Primer beso', + + 'storage_title' => 'Almacenamiento', + 'storage_account_info' => 'El límite de su cuenta es :accountLimit MB. Su uso actual es :currentAccountSize MB (alrededor de :percentUsage%).', + 'storage_upgrade_notice' => 'Actualiza tu cuenta para poder subir documentos y fotos.', + 'storage_description' => 'Aquí puedes ver todos los documentos y fotos subidos sobre tus contactos.', + + 'dav_title' => 'WebDAV', + 'dav_description' => 'Aquí puede encontrar todos los ajustes para utilizar recursos WebDAV para las exportaciones de CardDAV y CalDAV.', + 'dav_copy_help' => 'Copiar al portapapeles', + 'dav_clipboard_copied' => 'Valor copiado al portapapeles', + 'dav_url_base' => 'Url base para todos los recursos CardDAV y CalDAV:', + 'dav_connect_help' => 'Puede conectar sus contactos y/o calendarios con esta url base en su teléfono u ordenador.', + 'dav_connect_help2' => 'Usa tu login (email) y crea un token API como la contraseña para autenticar.', + 'dav_url_carddav' => 'url de CardDAV para el recurso Contactos:', + 'dav_url_caldav_birthdays' => 'Url de CalDAV para recursos de cumpleaños:', + 'dav_url_caldav_tasks' => 'Url de CalDAV para los recursos de tareas:', + 'dav_title_carddav' => 'CardDAV', + 'dav_title_caldav' => 'CalDAV', + 'dav_carddav_export' => 'Exportar todos los contactos en un archivo', + 'dav_caldav_birthdays_export' => 'Exportar todos los cumpleaños en un archivo', + 'dav_caldav_tasks_export' => 'Exportar todas las tareas en un archivo', + + 'archive_title' => 'Archivar todos los contactos de tu cuenta', + 'archive_desc' => 'Esto archivará todos los contactos de su cuenta.', + 'archive_cta' => 'Archivar todos tus contactos', + + 'logs_title' => 'Todo lo que ha pasado a esta cuenta', + 'logs_actor' => 'Actor', + 'logs_timestamp' => 'Fecha y Hora', + 'logs_description' => 'Descripción', + 'logs_subject' => 'Asunto', + 'logs_size' => 'Tamaño (kB)', + 'logs_object' => 'Objeto', +]; diff --git a/resources/lang/es/validation.php b/resources/lang/es/validation.php new file mode 100644 index 0000000..24d5c14 --- /dev/null +++ b/resources/lang/es/validation.php @@ -0,0 +1,166 @@ + ':attribute debe ser aceptado.', + 'active_url' => ':attribute no es una URL válida.', + 'after' => ':attribute debe ser una fecha posterior a :date.', + 'after_or_equal' => ':attribute debe ser una fecha posterior o igual a :date.', + 'alpha' => ':attribute sólo debe contener letras.', + 'alpha_dash' => 'El campo :attribute solo puede contener letras, números, guiones y guiones bajos.', + 'alpha_num' => ':attribute sólo debe contener letras y números.', + 'array' => ':attribute debe ser un conjunto.', + 'before' => ':attribute debe ser una fecha anterior a :date.', + 'before_or_equal' => ':attribute debe ser una fecha anterior o igual a :date.', + 'between' => [ + 'numeric' => ':attribute tiene que estar entre :min - :max.', + 'file' => ':attribute debe pesar entre :min - :max kilobytes.', + 'string' => ':attribute tiene que tener entre :min - :max caracteres.', + 'array' => ':attribute tiene que tener entre :min - :max ítems.', + ], + 'boolean' => 'El campo :attribute debe tener un valor verdadero o falso.', + 'confirmed' => 'La confirmación de :attribute no coincide.', + 'date' => ':attribute no es una fecha válida.', + 'date_equals' => 'El campo :attribute debe ser una fecha igual a :date.', + 'date_format' => ':attribute no corresponde al formato :format.', + 'different' => ':attribute y :other deben ser diferentes.', + 'digits' => ':attribute debe tener :digits dígitos.', + 'digits_between' => ':attribute debe tener entre :min y :max dígitos.', + 'dimensions' => 'Las dimensiones de la imagen :attribute no son válidas.', + 'distinct' => 'El campo :attribute contiene un valor duplicado.', + 'email' => ':attribute no es un correo válido.', + 'ends_with' => 'El campo :attribute debe terminar con uno de los siguientes valores :values.', + 'exists' => ':attribute es inválido.', + 'file' => 'El campo :attribute debe ser un archivo.', + 'filled' => 'El campo :attribute es obligatorio.', + 'gt' => [ + 'numeric' => 'El campo :attribute debe ser mayor que :value.', + 'file' => 'El campo :attribute debe tener más de :value kilobytes.', + 'string' => 'El campo :attribute debe tener más de :value caracteres.', + 'array' => 'El campo :attribute no puede tener más de :value elementos.', + ], + 'gte' => [ + 'numeric' => 'El campo :attribute debe ser como mínimo :value.', + 'file' => 'El campo :attribute debe tener como mínimo :value kilobytes.', + 'string' => 'El campo :attribute debe tener como mínimo :value caracteres.', + 'array' => 'El campo :attribute debe tener :value elementos o más.', + ], + 'image' => ':attribute debe ser una imagen.', + 'in' => ':attribute es inválido.', + 'in_array' => 'El campo :attribute no existe en :other.', + 'integer' => ':attribute debe ser un número entero.', + 'ip' => ':attribute debe ser una dirección IP válida.', + 'ipv4' => ':attribute debe ser un dirección IPv4 válida.', + 'ipv6' => ':attribute debe ser un dirección IPv6 válida.', + 'json' => 'El campo :attribute debe tener una cadena JSON válida.', + 'lt' => [ + 'numeric' => 'El campo :attribute debe ser menor que :value.', + 'file' => 'El campo :attribute debe ser menor que :value kilobytes.', + 'string' => 'El campo :attribute debe tener menos de :value caracteres.', + 'array' => 'El campo :attribute debe tener menos de :value elementos.', + ], + 'lte' => [ + 'numeric' => 'El campo :attribute debe ser menor o igual que :value.', + 'file' => 'El campo :attribute debe ser como máximo :value kilobytes.', + 'string' => 'El campo :attribute debe tener como máximo :value caracteres.', + 'array' => 'El campo :attribute no puede tener más de :value ítems.', + ], + 'max' => [ + 'numeric' => ':attribute no debe ser mayor a :max.', + 'file' => ':attribute no debe ser mayor que :max kilobytes.', + 'string' => ':attribute no debe ser mayor que :max caracteres.', + 'array' => ':attribute no debe tener más de :max elementos.', + ], + 'mimes' => ':attribute debe ser un archivo con formato: :values.', + 'mimetypes' => ':attribute debe ser un archivo con formato: :values.', + 'min' => [ + 'numeric' => 'El tamaño de :attribute debe ser de al menos :min.', + 'file' => 'El tamaño de :attribute debe ser de al menos :min kilobytes.', + 'string' => ':attribute debe contener al menos :min caracteres.', + 'array' => ':attribute debe tener al menos :min elementos.', + ], + 'not_in' => ':attribute es inválido.', + 'not_regex' => 'El formato del campo :attribute no es válido.', + 'numeric' => ':attribute debe ser numérico.', + 'password' => 'La contraseña es incorrecta.', + 'present' => 'El campo :attribute debe estar presente.', + 'regex' => 'El formato de :attribute es inválido.', + 'required' => 'El campo :attribute es obligatorio.', + 'required_if' => 'El campo :attribute es obligatorio cuando :other es :value.', + 'required_unless' => 'El campo :attribute es obligatorio a menos que :other esté en :values.', + 'required_with' => 'El campo :attribute es obligatorio cuando :values está presente.', + 'required_with_all' => 'El campo :attribute es obligatorio cuando :values están presentes.', + 'required_without' => 'El campo :attribute es obligatorio cuando :values no está presente.', + 'required_without_all' => 'El campo :attribute es obligatorio cuando ninguno de :values estén presentes.', + 'same' => ':attribute y :other deben coincidir.', + 'size' => [ + 'numeric' => 'El tamaño de :attribute debe ser :size.', + 'file' => 'El tamaño de :attribute debe ser :size kilobytes.', + 'string' => ':attribute debe contener :size caracteres.', + 'array' => ':attribute debe contener :size elementos.', + ], + 'starts_with' => 'El campo :attribute debe comenzar con uno de los siguientes valores: :values.', + 'string' => 'El campo :attribute debe ser una cadena de caracteres.', + 'timezone' => 'El :attribute debe ser una zona válida.', + 'unique' => 'El campo :attribute ya ha sido registrado.', + 'uploaded' => 'Subir :attribute ha fallado.', + 'url' => 'El formato :attribute es inválido.', + 'uuid' => 'El campo :attribute debe ser un UUID válido.', + + /* + |-------------------------------------------------------------------------- + | Custom Validation Language Lines + |-------------------------------------------------------------------------- + | + | Here you may specify custom validation messages for attributes using the + | convention "attribute.rule" to name the lines. This makes it quick to + | specify a specific custom language line for a given attribute rule. + | + */ + + 'custom' => [ + 'attribute-name' => [ + 'rule-name' => 'custom-message', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Custom Validation Attributes + |-------------------------------------------------------------------------- + | + | The following language lines are used to swap our attribute placeholder + | with something more reader friendly such as "E-Mail Address" instead + | of "email". This simply helps us make our message more expressive. + | + */ + + 'attributes' => [], + + 'vue' => [ + 'max' => [ + 'numeric' => '{field} no puede ser mayor que {max}.', + 'string' => '{field} no debe ser mayor que {max} caracteres.', + ], + 'required' => '{field} es obligatorio.', + 'url' => '{field} no es una dirección URL válida.', + ], + +]; diff --git a/resources/lang/fa.json b/resources/lang/fa.json new file mode 100644 index 0000000..ddea72e --- /dev/null +++ b/resources/lang/fa.json @@ -0,0 +1,7 @@ +{ + "The :attribute must contain at least one uppercase and one lowercase letter.": "The :attribute must contain at least one uppercase and one lowercase letter.", + "The :attribute must contain at least one letter.": "The :attribute must contain at least one letter.", + "The :attribute must contain at least one symbol.": "The :attribute must contain at least one symbol.", + "The :attribute must contain at least one number.": "The :attribute must contain at least one number.", + "The given :attribute has appeared in a data leak. Please choose a different :attribute.": "The given :attribute has appeared in a data leak. Please choose a different :attribute." +} diff --git a/resources/lang/fa/app.php b/resources/lang/fa/app.php new file mode 100644 index 0000000..0aacdfc --- /dev/null +++ b/resources/lang/fa/app.php @@ -0,0 +1,571 @@ + 'بله', + 'no' => 'خیر', + 'update' => 'به‌روزرسانی', + 'save' => 'ذخیره', + 'add' => 'افزودن', + 'cancel' => 'لغو', + 'confirm' => 'تایید', + 'delete_confirm' => 'آیا مطمئن هستید؟', + 'delete' => 'حذف', + 'edit' => 'ویرایش', + 'upload' => 'بارگذاری', + 'download' => 'دانلود', + 'save_close' => 'ذخیره و بستن', + 'close' => 'بستن', + 'copy' => 'کپی', + 'create' => 'ایجاد', + 'remove' => 'پاک کردن', + 'revoke' => 'ابطال', + 'done' => 'انجام شد', + 'back' => 'بازگشت', + 'verify' => 'تأیید', + 'new' => 'جدید', + 'unknown' => 'من نمی دانم', + 'load_more' => 'بارگذاری بیشتر', + 'loading' => 'در حال بارگذاری…', + 'with' => 'با', + 'today' => 'امروز', + 'yesterday' => 'دیروز', + 'another_day' => 'یک روز دیگر', + 'date' => 'تاریخ', + 'type' => 'نوع', + 'zoom' => 'بزرگنمايی', + 'upgrade' => 'برای باز کردن ارتقاء دهید', + 'percent_uploaded' => '{percent}% آپلود شد', + 'retry' => 'تلاش دوباره', + 'filter' => 'لیست را فیلتر کن', + 'go_back' => 'بازگشت', + 'file_selected' => 'یک فایل انتخاب شده …|{count} فایل انتخاب شده …', + + 'application_title' => 'مونیکا - مدیریت روابط شخصی', + 'application_description' => 'مونیکا ابزاری برای مدیریت تعاملات شما با عزیزان، دوستان و خانواده خود است. ', + 'application_og_title' => 'با عزیزان خود روابط بهتری داشته باشید. CRM آنلاین رایگان برای دوستان و خانواده. ', + + 'markdown_description' => 'آیا می خواهید متن خود را به خوبی قالب بندی کنید؟ ما از Markdown برای Bold، italic، لیست‌ها و موارد دیگر پشتیبانی می‌کنیم. ', + 'markdown_link' => 'مستندات را بخوانید', + + 'header_settings_link' => 'تنظیمات', + 'header_logout_link' => 'خروج از سیستم', + 'header_changelog_link' => 'تغییرات محصول', + + 'main_nav_cta' => 'افزودن افراد', + 'main_nav_dashboard' => 'پیشخوان', + 'main_nav_family' => 'مخاطبین', + 'main_nav_journal' => 'مجله', + 'main_nav_activities' => 'فعالیت ها', + 'main_nav_tasks' => 'وظایف', + + 'footer_remarks' => 'نظرات؟', + 'footer_send_email' => 'یک ایمیل به ما بفرستید', + 'footer_privacy' => 'سیاست حفظ حریم خصوصی', + 'footer_release' => 'یادداشت‌های انتشار', + 'footer_newsletter' => 'خبرنامه', + 'footer_source_code' => 'مشارکت', + 'footer_version' => 'نسخه: :version', + 'footer_new_version' => 'یک نسخه جدید از مونیکا در دسترس می‌باشد.', + + 'footer_modal_version_whats_new' => 'تغییرات جدید', + 'footer_modal_version_release_away' => 'You are 1 release behind the latest version available. You should update your instance.|You are :number releases behind the latest version available. You should update your instance.', + + 'breadcrumb_dashboard' => 'پیشخوان', + 'breadcrumb_list_contacts' => 'لیست افراد', + 'breadcrumb_archived_contacts' => 'مخاطبین ارشیو شده', + 'breadcrumb_journal' => 'مجله', + 'breadcrumb_settings' => 'تنظیمات', + 'breadcrumb_settings_export' => 'خروجی', + 'breadcrumb_settings_users' => 'کاربران', + 'breadcrumb_settings_users_add' => 'افزودن کاربر', + 'breadcrumb_settings_subscriptions' => 'اشتراک', + 'breadcrumb_settings_import' => 'وارد کردن', + 'breadcrumb_settings_import_report' => 'Import report', + 'breadcrumb_settings_import_upload' => 'بارگذاری', + 'breadcrumb_settings_tags' => 'برچسب ها', + 'breadcrumb_add_significant_other' => 'Add significant other', + 'breadcrumb_edit_significant_other' => 'Edit significant other', + 'breadcrumb_add_note' => 'افزودن يادداشت', + 'breadcrumb_edit_note' => 'ویرایش یادداشت', + 'breadcrumb_api' => 'وب سرویس', + 'breadcrumb_dav' => 'منابع DAV', + 'breadcrumb_edit_introductions' => 'How did you meet', + 'breadcrumb_settings_personalization' => 'شخصی سازی', + 'breadcrumb_settings_security' => 'امنیت', + 'breadcrumb_settings_security_2fa' => 'احراز هویت دو عاملی', + 'breadcrumb_profile' => 'Profile of :name', + + 'gender_male' => 'Man', + 'gender_female' => 'Woman', + 'gender_none' => 'Rather not say', + 'gender_no_gender' => 'No gender', + + 'error_title' => 'Whoops! Something went wrong.', + 'error_unauthorized' => 'You don’t have the right to edit this resource.', + 'error_user_account' => 'This user does not belong to the given account.', + 'error_save' => 'We had an error trying to save the data.', + 'error_try_again' => 'Something went wrong. Please try again.', + 'error_id' => 'Error ID: :id', + 'error_unavailable' => 'Service unavailable', + 'error_maintenance' => 'Maintenance in progress. We’ll be right back.', + 'error_help' => 'We’ll be right back.', + 'error_twitter' => 'Follow our Twitter account to be alerted when it’s up again.', + 'error_no_term' => 'There is no policy for this instance yet.', + + 'default_save_success' => 'The data has been saved.', + + 'compliance_title' => 'Sorry for the interruption.', + 'compliance_desc' => 'We have changed our Terms of Use and Privacy Policy. By law we have to ask you to review them and accept them so you can continue to use your account.', + 'compliance_desc_end' => 'We don’t do anything nasty with your data or your account and we never will.', + 'compliance_terms' => 'Accept new terms and privacy policy', + + // Relationship types + // Yes, each relationship type has 8 strings associated with it. + // This is because we need to indicate the name of the relationship type, + // and also the name of the opposite side of this relationship (father/son), + // and then, the feminine version of the string. Finally, in some sentences + // in the UI, we need to include the name of the person we add the relationship + // to. + 'relationship_type_group_love' => 'Love relationships', + 'relationship_type_group_family' => 'Family relationships', + 'relationship_type_group_friend' => 'Friend relationships', + 'relationship_type_group_work' => 'Work relationships', + 'relationship_type_group_other' => 'Other kind of relationships', + + 'relationship_type_partner' => 'significant other', + 'relationship_type_partner_female' => 'significant other', + 'relationship_type_partner_male' => 'significant other', + 'relationship_type_partner_with_name' => ':name’s significant other', + 'relationship_type_partner_female_with_name' => ':name’s significant other', + 'relationship_type_partner_male_with_name' => ':name’s significant other', + + 'relationship_type_spouse' => 'spouse', + 'relationship_type_spouse_female' => 'wife', + 'relationship_type_spouse_male' => 'husband', + 'relationship_type_spouse_with_name' => ':name’s spouse', + 'relationship_type_spouse_female_with_name' => ':name’s wife', + 'relationship_type_spouse_male_with_name' => ':name’s husband', + + 'relationship_type_date' => 'date', + 'relationship_type_date_female' => 'date', + 'relationship_type_date_male' => 'date', + 'relationship_type_date_with_name' => ':name’s date', + 'relationship_type_date_female_with_name' => ':name’s date', + 'relationship_type_date_male_with_name' => ':name’s date', + + 'relationship_type_lover' => 'lover', + 'relationship_type_lover_female' => 'lover', + 'relationship_type_lover_male' => 'lover', + 'relationship_type_lover_with_name' => ':name’s lover', + 'relationship_type_lover_female_with_name' => ':name’s lover', + 'relationship_type_lover_male_with_name' => ':name’s lover', + + 'relationship_type_inlovewith' => 'in love with', + 'relationship_type_inlovewith_female' => 'in love with', + 'relationship_type_inlovewith_male' => 'in love with', + 'relationship_type_inlovewith_with_name' => 'someone :name is in love with', + 'relationship_type_inlovewith_female_with_name' => 'someone :name is in love with', + 'relationship_type_inlovewith_male_with_name' => 'someone :name is in love with', + + 'relationship_type_lovedby' => 'loved by', + 'relationship_type_lovedby_female' => 'loved by', + 'relationship_type_lovedby_male' => 'loved by', + 'relationship_type_lovedby_with_name' => ':name’s secret lover', + 'relationship_type_lovedby_female_with_name' => ':name’s secret lover', + 'relationship_type_lovedby_male_with_name' => ':name’s secret lover', + + 'relationship_type_ex' => 'ex-partner', + 'relationship_type_ex_female' => 'ex-girlfriend', + 'relationship_type_ex_male' => 'ex-boyfriend', + 'relationship_type_ex_with_name' => ':name’s ex-partner', + 'relationship_type_ex_female_with_name' => ':name’s ex-girlfriend', + 'relationship_type_ex_male_with_name' => ':name’s ex-boyfriend', + + 'relationship_type_parent' => 'parent', + 'relationship_type_parent_female' => 'mother', + 'relationship_type_parent_male' => 'father', + 'relationship_type_parent_with_name' => ':name’s parent', + 'relationship_type_parent_female_with_name' => ':name’s mother', + 'relationship_type_parent_male_with_name' => ':name’s father', + + 'relationship_type_child' => 'child', + 'relationship_type_child_female' => 'daughter', + 'relationship_type_child_male' => 'son', + 'relationship_type_child_with_name' => ':name’s child', + 'relationship_type_child_female_with_name' => ':name’s daughter', + 'relationship_type_child_male_with_name' => ':name’s son', + + 'relationship_type_stepparent' => 'step-parent', + 'relationship_type_stepparent_female' => 'stepmother', + 'relationship_type_stepparent_male' => 'stepfather', + 'relationship_type_stepparent_with_name' => ':name’s step-parent', + 'relationship_type_stepparent_female_with_name' => ':name’s stepmother', + 'relationship_type_stepparent_male_with_name' => ':name’s stepfather', + + 'relationship_type_stepchild' => 'stepchild', + 'relationship_type_stepchild_female' => 'stepdaughter', + 'relationship_type_stepchild_male' => 'stepson', + 'relationship_type_stepchild_with_name' => ':name’s stepchild', + 'relationship_type_stepchild_female_with_name' => ':name’s stepdaughter', + 'relationship_type_stepchild_male_with_name' => ':name’s stepson', + + 'relationship_type_sibling' => 'sibling', + 'relationship_type_sibling_female' => 'sister', + 'relationship_type_sibling_male' => 'brother', + 'relationship_type_sibling_with_name' => ':name’s sibling', + 'relationship_type_sibling_female_with_name' => ':name’s sister', + 'relationship_type_sibling_male_with_name' => ':name’s brother', + + 'relationship_type_grandparent' => 'grandparent', + 'relationship_type_grandparent_female' => 'grandmother', + 'relationship_type_grandparent_male' => 'grandfather', + 'relationship_type_grandparent_with_name' => ':name’s grandparent', + 'relationship_type_grandparent_female_with_name' => ':name’s grandmother', + 'relationship_type_grandparent_male_with_name' => ':name’s grandfather', + + 'relationship_type_grandchild' => 'grandchild', + 'relationship_type_grandchild_female' => 'granddaughter', + 'relationship_type_grandchild_male' => 'grandson', + 'relationship_type_grandchild_with_name' => ':name’s grandchild', + 'relationship_type_grandchild_female_with_name' => ':name’s granddauther', + 'relationship_type_grandchild_male_with_name' => ':name’s grandson', + + 'relationship_type_uncle' => 'uncle', + 'relationship_type_uncle_female' => 'aunt', + 'relationship_type_uncle_male' => 'uncle', + 'relationship_type_uncle_with_name' => ':name’s uncle', + 'relationship_type_uncle_female_with_name' => ':name’s aunt', + 'relationship_type_uncle_male_with_name' => ':name’s uncle', + + 'relationship_type_nephew' => 'nephew', + 'relationship_type_nephew_female' => 'niece', + 'relationship_type_nephew_male' => 'nephew', + 'relationship_type_nephew_with_name' => ':name’s nephew', + 'relationship_type_nephew_female_with_name' => ':name’s niece', + 'relationship_type_nephew_male_with_name' => ':name’s nephew', + + 'relationship_type_cousin' => 'cousin', + 'relationship_type_cousin_female' => 'cousin', + 'relationship_type_cousin_male' => 'cousin', + 'relationship_type_cousin_with_name' => ':name’s cousin', + 'relationship_type_cousin_female_with_name' => ':name’s cousin', + 'relationship_type_cousin_male_with_name' => ':name’s cousin', + + 'relationship_type_godfather' => 'godparent', + 'relationship_type_godfather_female' => 'godmother', + 'relationship_type_godfather_male' => 'godfather', + 'relationship_type_godfather_with_name' => ':name’s godparent', + 'relationship_type_godfather_female_with_name' => ':name’s godmother', + 'relationship_type_godfather_male_with_name' => ':name’s godfather', + + 'relationship_type_godson' => 'godchild', + 'relationship_type_godson_female' => 'goddaughter', + 'relationship_type_godson_male' => 'godson', + 'relationship_type_godson_with_name' => ':name’s godchild', + 'relationship_type_godson_female_with_name' => ':name’s goddaughter', + 'relationship_type_godson_male_with_name' => ':name’s godson', + + 'relationship_type_friend' => 'friend', + 'relationship_type_friend_female' => 'friend', + 'relationship_type_friend_male' => 'friend', + 'relationship_type_friend_with_name' => ':name’s friend', + 'relationship_type_friend_female_with_name' => ':name’s friend', + 'relationship_type_friend_male_with_name' => ':name’s friend', + + 'relationship_type_bestfriend' => 'best friend', + 'relationship_type_bestfriend_female' => 'best friend', + 'relationship_type_bestfriend_male' => 'best friend', + 'relationship_type_bestfriend_with_name' => ':name’s best friend', + 'relationship_type_bestfriend_female_with_name' => ':name’s best friend', + 'relationship_type_bestfriend_male_with_name' => ':name’s best friend', + + 'relationship_type_colleague' => 'colleague', + 'relationship_type_colleague_female' => 'colleague', + 'relationship_type_colleague_male' => 'colleague', + 'relationship_type_colleague_with_name' => ':name’s colleague', + 'relationship_type_colleague_female_with_name' => ':name’s colleague', + 'relationship_type_colleague_male_with_name' => ':name’s colleague', + + 'relationship_type_boss' => 'boss', + 'relationship_type_boss_female' => 'boss', + 'relationship_type_boss_male' => 'boss', + 'relationship_type_boss_with_name' => ':name’s boss', + 'relationship_type_boss_female_with_name' => ':name’s boss', + 'relationship_type_boss_male_with_name' => ':name’s boss', + + 'relationship_type_subordinate' => 'subordinate', + 'relationship_type_subordinate_female' => 'subordinate', + 'relationship_type_subordinate_male' => 'subordinate', + 'relationship_type_subordinate_with_name' => ':name’s subordinate', + 'relationship_type_subordinate_female_with_name' => ':name’s subordinate', + 'relationship_type_subordinate_male_with_name' => ':name’s subordinate', + + 'relationship_type_mentor' => 'mentor', + 'relationship_type_mentor_female' => 'mentor', + 'relationship_type_mentor_male' => 'mentor', + 'relationship_type_mentor_with_name' => ':name’s mentor', + 'relationship_type_mentor_female_with_name' => ':name’s mentor', + 'relationship_type_mentor_male_with_name' => ':name’s mentor', + + 'relationship_type_protege' => 'protégé', + 'relationship_type_protege_female' => 'protégé', + 'relationship_type_protege_male' => 'protégé', + 'relationship_type_protege_with_name' => ':name’s protégé', + 'relationship_type_protege_female_with_name' => ':name’s protégé', + 'relationship_type_protege_male_with_name' => ':name’s protégé', + + 'relationship_type_ex_husband' => 'ex-spouse', + 'relationship_type_ex_husband_female' => 'ex-wife', + 'relationship_type_ex_husband_male' => 'ex-husband', + 'relationship_type_ex_husband_with_name' => ':name’s ex-spouse', + 'relationship_type_ex_husband_female_with_name' => ':name’s ex-wife', + 'relationship_type_ex_husband_male_with_name' => ':name’s ex-husband', + + // emotions + 'emotion_primary_love' => 'Love', + 'emotion_primary_joy' => 'Joy', + 'emotion_primary_surprise' => 'Surprise', + 'emotion_primary_anger' => 'Anger', + 'emotion_primary_sadness' => 'Sadness', + 'emotion_primary_fear' => 'Fear', + + 'emotion_secondary_affection' => 'Affection', + 'emotion_secondary_lust' => 'Lust', + 'emotion_secondary_longing' => 'Longing', + 'emotion_secondary_cheerfulness' => 'Cheerfulness', + 'emotion_secondary_zest' => 'Zest', + 'emotion_secondary_contentment' => 'Contentment', + 'emotion_secondary_pride' => 'Pride', + 'emotion_secondary_optimism' => 'Optimism', + 'emotion_secondary_enthrallment' => 'Enthrallment', + 'emotion_secondary_relief' => 'Relief', + 'emotion_secondary_surprise' => 'Surprise', + 'emotion_secondary_irritation' => 'Irritation', + 'emotion_secondary_exasperation' => 'Exasperation', + 'emotion_secondary_rage' => 'Rage', + 'emotion_secondary_disgust' => 'Disgust', + 'emotion_secondary_envy' => 'Envy', + 'emotion_secondary_suffering' => 'Suffering', + 'emotion_secondary_sadness' => 'Sadness', + 'emotion_secondary_disappointment' => 'Disappointment', + 'emotion_secondary_shame' => 'Shame', + 'emotion_secondary_neglect' => 'Neglect', + 'emotion_secondary_sympathy' => 'Sympathy', + 'emotion_secondary_horror' => 'Horror', + 'emotion_secondary_nervousness' => 'Nervousness', + + 'emotion_adoration' => 'Adoration', + 'emotion_affection' => 'Affection', + 'emotion_love' => 'Love', + 'emotion_fondness' => 'Fondness', + 'emotion_liking' => 'Liking', + 'emotion_attraction' => 'Attraction', + 'emotion_caring' => 'Caring', + 'emotion_tenderness' => 'Tenderness', + 'emotion_compassion' => 'Compassion', + 'emotion_sentimentality' => 'Sentimentality', + 'emotion_arousal' => 'Arousal', + 'emotion_desire' => 'Desire', + 'emotion_lust' => 'Lust', + 'emotion_passion' => 'Passion', + 'emotion_infatuation' => 'Infatuation', + 'emotion_longing' => 'Longing', + 'emotion_amusement' => 'Amusement', + 'emotion_bliss' => 'Bliss', + 'emotion_cheerfulness' => 'Cheerfulness', + 'emotion_gaiety' => 'Gaiety', + 'emotion_glee' => 'Glee', + 'emotion_jolliness' => 'Jolliness', + 'emotion_joviality' => 'Joviality', + 'emotion_joy' => 'Joy', + 'emotion_delight' => 'Delight', + 'emotion_enjoyment' => 'Enjoyment', + 'emotion_gladness' => 'Gladness', + 'emotion_happiness' => 'Happiness', + 'emotion_jubilation' => 'Jubilation', + 'emotion_elation' => 'Elation', + 'emotion_satisfaction' => 'Satisfaction', + 'emotion_ecstasy' => 'Ecstasy', + 'emotion_euphoria' => 'Euphoria', + 'emotion_enthusiasm' => 'Enthusiasm', + 'emotion_zeal' => 'Zeal', + 'emotion_zest' => 'Zest', + 'emotion_excitement' => 'Excitement', + 'emotion_thrill' => 'Thrill', + 'emotion_exhilaration' => 'Exhilaration', + 'emotion_contentment' => 'Contentment', + 'emotion_pleasure' => 'Pleasure', + 'emotion_pride' => 'Pride', + 'emotion_eagerness' => 'Eagerness', + 'emotion_hope' => 'Hope', + 'emotion_optimism' => 'Optimism', + 'emotion_enthrallment' => 'Enthrallment', + 'emotion_rapture' => 'Rapture', + 'emotion_relief' => 'Relief', + 'emotion_amazement' => 'Amazement', + 'emotion_surprise' => 'Surprise', + 'emotion_astonishment' => 'Astonishment', + 'emotion_aggravation' => 'Aggravation', + 'emotion_irritation' => 'Irritation', + 'emotion_agitation' => 'Agitation', + 'emotion_annoyance' => 'Annoyance', + 'emotion_grouchiness' => 'Grouchiness', + 'emotion_grumpiness' => 'Grumpiness', + 'emotion_exasperation' => 'Exasperation', + 'emotion_frustration' => 'Frustration', + 'emotion_anger' => 'Anger', + 'emotion_rage' => 'Rage', + 'emotion_outrage' => 'Outrage', + 'emotion_fury' => 'Fury', + 'emotion_wrath' => 'Wrath', + 'emotion_hostility' => 'Hostility', + 'emotion_ferocity' => 'Ferocity', + 'emotion_bitterness' => 'Bitterness', + 'emotion_hate' => 'Hate', + 'emotion_loathing' => 'Loathing', + 'emotion_scorn' => 'Scorn', + 'emotion_spite' => 'Spite', + 'emotion_vengefulness' => 'Vengefulness', + 'emotion_dislike' => 'Dislike', + 'emotion_resentment' => 'Resentment', + 'emotion_disgust' => 'Disgust', + 'emotion_revulsion' => 'Revulsion', + 'emotion_contempt' => 'Contempt', + 'emotion_envy' => 'Envy', + 'emotion_jealousy' => 'Jealousy', + 'emotion_agony' => 'Agony', + 'emotion_suffering' => 'Suffering', + 'emotion_hurt' => 'Hurt', + 'emotion_anguish' => 'Anguish', + 'emotion_depression' => 'Depression', + 'emotion_despair' => 'Despair', + 'emotion_hopelessness' => 'Hopelessness', + 'emotion_gloom' => 'Gloom', + 'emotion_glumness' => 'Glumness', + 'emotion_sadness' => 'Sadness', + 'emotion_unhappiness' => 'Unhappiness', + 'emotion_grief' => 'Grief', + 'emotion_sorrow' => 'Sorrow', + 'emotion_woe' => 'Woe', + 'emotion_misery' => 'Misery', + 'emotion_melancholy' => 'Melancholy', + 'emotion_dismay' => 'Dismay', + 'emotion_disappointment' => 'Disappointment', + 'emotion_displeasure' => 'Displeasure', + 'emotion_guilt' => 'Guilt', + 'emotion_shame' => 'Shame', + 'emotion_regret' => 'Regret', + 'emotion_remorse' => 'Remorse', + 'emotion_alienation' => 'Alienation', + 'emotion_isolation' => 'Isolation', + 'emotion_neglect' => 'Neglect', + 'emotion_loneliness' => 'Loneliness', + 'emotion_rejection' => 'Rejection', + 'emotion_homesickness' => 'Homesickness', + 'emotion_defeat' => 'Defeat', + 'emotion_dejection' => 'Dejection', + 'emotion_insecurity' => 'Insecurity', + 'emotion_embarrassment' => 'Embarrassment', + 'emotion_humiliation' => 'Humiliation', + 'emotion_insult' => 'Insult', + 'emotion_pity' => 'Pity', + 'emotion_sympathy' => 'Sympathy', + 'emotion_alarm' => 'Alarm', + 'emotion_shock' => 'Shock', + 'emotion_fear' => 'Fear', + 'emotion_fright' => 'Fright', + 'emotion_horror' => 'Horror', + 'emotion_terror' => 'Terror', + 'emotion_panic' => 'Panic', + 'emotion_hysteria' => 'Hysteria', + 'emotion_mortification' => 'Mortification', + 'emotion_anxiety' => 'Anxiety', + 'emotion_nervousness' => 'Nervousness', + 'emotion_tenseness' => 'Tenseness', + 'emotion_uneasiness' => 'Uneasiness', + 'emotion_apprehension' => 'Apprehension', + 'emotion_worry' => 'Worry', + 'emotion_distress' => 'Distress', + 'emotion_dread' => 'Dread', + + // weather + 'weather_sunny' => 'آفتابی', + 'weather_clear' => 'پاک سازی', + 'weather_clear-day' => 'پاک', + 'weather_clear-night' => 'Clear night', + 'weather_light-drizzle' => 'نم نم باران خفیف', + 'weather_patchy-light-drizzle' => 'Patchy light drizzle', + 'weather_patchy-light-rain' => 'Patchy light rain', + 'weather_light-rain' => 'Light rain', + 'weather_moderate-rain-at-times' => 'Moderate rain at times', + 'weather_moderate-rain' => 'Moderate rain', + 'weather_patchy-rain-possible' => 'Patchy rain possible', + 'weather_heavy-rain-at-times' => 'Heavy rain at times', + 'weather_heavy-rain' => 'Heavy rain', + 'weather_light-freezing-rain' => 'Light freezing rain', + 'weather_moderate-or-heavy-freezing-rain' => 'Moderate or heavy freezing rain', + 'weather_light-sleet' => 'Light sleet', + 'weather_moderate-or-heavy-rain-shower' => 'Moderate or heavy rain shower', + 'weather_light-rain-shower' => 'Light rain shower', + 'weather_torrential-rain-shower' => 'Torrential rain shower', + 'weather_rain' => 'Rain', + 'weather_snow' => 'Snow', + 'weather_blowing-snow' => 'Blowing snow', + 'weather_patchy-light-snow' => 'Patchy light snow', + 'weather_light-snow' => 'Light snow', + 'weather_patchy-moderate-snow' => 'Patchy moderate snow', + 'weather_moderate-snow' => 'Moderate snow', + 'weather_patchy-heavy-snow' => 'Patchy heavy snow', + 'weather_heavy-snow' => 'Heavy snow', + 'weather_light-snow-showers' => 'Light snow showers', + 'weather_moderate-or-heavy-snow-showers' => 'Moderate or heavy snow showers', + 'weather_patchy-snow-possible' => 'Patchy snow possible', + 'weather_patchy-sleet-possible' => 'Patchy sleet possible', + 'weather_moderate-or-heavy-sleet' => 'Moderate or heavy sleet', + 'weather_light-sleet-showers' => 'Light sleet showers', + 'weather_moderate-or-heavy-sleet-showers' => 'Moderate or heavy sleet showers', + 'weather_sleet' => 'Sleet', + 'weather_wind' => 'Wind', + 'weather_fog' => 'Fog', + 'weather_freezing-fog' => 'Freezing fog', + 'weather_mist' => 'Mist', + 'weather_blizzard' => 'Blizzard', + 'weather_overcast' => 'Overcast', + 'weather_cloudy' => 'Cloudy', + 'weather_partly-cloudy-day' => 'Partly cloudy', + 'weather_partly-cloudy-night' => 'Partly cloudy', + 'weather_freezing-drizzle' => 'Freezing drizzle', + 'weather_heavy-freezing-drizzle' => 'Heavy freezing drizzle', + 'weather_patchy-freezing-drizzle-possible' => 'Patchy freezing drizzle possible', + 'weather_ice-pellets' => 'Ice pellets', + 'weather_light-showers-of-ice-pellets' => 'Light showers of ice pellets', + 'weather_moderate-or-heavy-showers-of-ice-pellets' => 'Moderate or heavy showers of ice pellets', + 'weather_thundery-outbreaks-possible' => 'Thundery outbreaks possible', + 'weather_patchy-light-rain-with-thunder' => 'Patchy light rain with thunder', + 'weather_moderate-or-heavy-rain-with-thunder' => 'Moderate or heavy rain with thunder', + 'weather_patchy-light-snow-with-thunder' => 'Patchy light snow with thunder', + 'weather_moderate-or-heavy-snow-with-thunder' => 'Moderate or heavy snow with thunder', + 'weather_current_temperature_celsius' => ':temperature °C', + 'weather_current_temperature_fahrenheit' => ':temperature °F', + 'weather_current_title' => 'Current weather', + + // dav + 'dav_contacts' => 'Contacts', + 'dav_contacts_description' => ':name’s contacts', + 'dav_birthdays' => 'Birthdays', + 'dav_birthdays_description' => ':name’s contact’s birthdays', + 'dav_tasks' => 'Tasks', + 'dav_tasks_description' => ':name’s tasks', + + // contact list + 'contact_list_avatar' => 'Avatar', + 'contact_list_name' => 'Contact', + 'contact_list_description' => 'Description', + +]; diff --git a/resources/lang/fa/auth.php b/resources/lang/fa/auth.php new file mode 100644 index 0000000..63eda33 --- /dev/null +++ b/resources/lang/fa/auth.php @@ -0,0 +1,89 @@ + 'مشخصات وارد شده با اطلاعات ما سازگار نیست.', + 'throttle' => 'تعداد دفعات تلاش برای ورود به سیستم بسیار زیاد است. لطفا در :seconds ثانیه دیگر تلاش نمایید.', + 'not_authorized' => 'شما مجاز به انجام این عمل نیستید.', + 'signup_disabled' => 'ثبت نام در حال حاضر غیر فعال است', + 'signup_error' => 'هنگام ثبت نام کاربر خطایی روی داد', + 'back_homepage' => 'بازگشت به صفحه اصلی', + 'mfa_auth_otp' => 'با دستگاه دو عاملی خود احراز هویت کنید', + 'mfa_auth_webauthn' => 'احراز هویت با کلید امنیتی (WebAuthn)', + '2fa_title' => 'احراز هویت دو عاملی', + '2fa_wrong_validation' => 'مجوز دو مرحله‌ای ناموفق بود.', + '2fa_one_time_password' => 'کد تایید هویت دو مرحله‌ای', + '2fa_recuperation_code' => 'کد بازیابی دوگانه را وارد کنید', + '2fa_one_time_or_recuperation' => 'کد احراز هویت دو عاملی یا کد بازیابی را وارد کنید', + '2fa_otp_help' => 'برنامه تلفن همراه احراز هویت دو عاملی خود را باز کنید و کد را کپی کنید', + + 'login_to_account' => 'ورود به حساب کابری', + 'login_with_recovery' => 'ورود با کد بازیابی', + 'login_again' => 'لطفاً مجدداً وارد حساب کاربری خود شوید', + 'email' => 'ایمیل', + 'password' => 'رمز عبور', + 'recovery' => 'کد بازیابی', + 'login' => 'ورود', + 'button_remember' => 'مرا به خاطر بسپار', + 'password_forget' => 'رمز عبور را فراموش کردید؟', + 'password_reset' => 'بازنشانی کلمه عبور', + 'use_recovery' => 'یا شما می‌توانید از کد بازیابی استفاده کنید', + 'signup_no_account' => 'حساب کاربری ندارید؟', + 'signup' => 'ثبت نام', + 'create_account' => 'برای ایجاد یک حساب کاربری جدید اینجا کلیک کنید', + 'change_language_title' => 'تغییر زبان:', + 'change_language' => 'تغییر زبان به :lang', + + 'password_reset_title' => 'بازنشانی گذرواژه', + 'password_reset_email' => 'آدرس ایمیل', + 'password_reset_send_link' => 'ارسال لینک بازنشانی کلمه عبور', + 'password_reset_password' => 'رمز عبور', + 'password_reset_password_confirm' => 'تایید کلمه عبور', + 'password_reset_action' => 'بازنشانی گذرواژه', + 'password_reset_email_content' => 'برای تغییر رمز عبور اینجا کلیک کنید:', + + 'register_title_welcome' => 'به نمونه تازه نصب شده مونیکا خود خوش آمدید', + 'register_create_account' => 'شما برای استفاده از مونیکا لازم است یک حساب کاربری ایجاد کنید', + 'register_title_create' => 'حساب کاربری مونیکا خود را بسازید', + 'register_login' => 'اگر حساب کاربری دارید وارد شوید ​', + 'register_email' => 'لطفا یک آدرس ایمیل معتبر را وارد کنید', + 'register_email_example' => 'you@home', + 'register_firstname' => 'نام', + 'register_firstname_example' => 'مثال: صالح', + 'register_lastname' => 'نام خانوادگی', + 'register_lastname_example' => 'مثال: شریفی', + 'register_password' => 'رمز عبور', + 'register_password_example' => 'یک رمز عبور مطمئن وارد کنید', + 'register_password_confirmation' => 'تایید رمز عبور', + 'register_action' => 'ثبت نام', + 'register_policy' => 'ثبت نام شما به معنی مطالعه و قبول سیاست حفظ حریم خصوصی و قوانین و مقررات می باشد.', + 'register_invitation_email' => 'برای اهداف امنیتی، لطفاً ایمیل شخصی را که شما را برای پیوستن به این حساب دعوت کرده است، مشخص کنید. این اطلاعات در ایمیل دعوت وجود دارد.', + + 'confirmation_title' => 'آدرس ایمیل خود را تایید کنید', + 'confirmation_fresh' => 'یک لینک تأیید جدید به آدرس ایمیلتان ارسال شد.', + 'confirmation_check' => 'قبل از ادامه، لطفاً ایمیل خود را برای لینک تأیید بررسی کنید.', + 'confirmation_request_another' => 'اگر ایمیل را دریافت نکرده اید برای درخواست مجدد اینجا کلیک کنید.', + + 'confirmation_again' => 'اگر میخواهید ایمیل خود را تغییر دهید می‌توانید اینجا کلیک کنید.', + 'email_change_current_email' => 'ادرس ایمیل فعلی:', + 'email_change_title' => 'آدرس ایمیل خود را تغییر دهید', + 'email_change_new' => 'آدرس ایمیل جدید', + 'email_changed' => 'ادرس ایمیل شما تغییر کرد . صندوق پستی خود را برای تایید ایمیل چک کنید.', +]; diff --git a/resources/lang/fa/changelog.php b/resources/lang/fa/changelog.php new file mode 100644 index 0000000..981b018 --- /dev/null +++ b/resources/lang/fa/changelog.php @@ -0,0 +1,12 @@ + 'Product changes', + 'note' => 'Note: unfortunately, this page is only in English.', +]; diff --git a/resources/lang/fa/dashboard.php b/resources/lang/fa/dashboard.php new file mode 100644 index 0000000..5190352 --- /dev/null +++ b/resources/lang/fa/dashboard.php @@ -0,0 +1,42 @@ + 'Welcome to your account!', + 'dashboard_blank_description' => 'Monica is the place to organize all the interactions you have with the people you care about.', + 'dashboard_blank_cta' => 'Add your first contact', + 'dashboard_blank_illustration' => 'Illustration by Freepik', + + 'notes_title' => 'You don’t have any starred notes yet.', + + 'tab_recent_calls' => 'Recent calls', + 'tab_favorite_notes' => 'Favorite notes', + 'tab_calls_blank' => 'You haven’t logged any calls yet.', + 'tab_debts' => 'Debts', + 'tab_debts_blank' => 'You haven’t logged any debts yet.', + 'tab_tasks' => 'Tasks', + 'tab_tasks_blank' => 'You haven’t any tasks yet.', + + 'tasks_add_task_placeholder' => 'What is this task about?', + 'tasks_tab_your_contacts' => 'Tasks related to your contacts', + 'tasks_tab_your_tasks' => 'Your tasks', + 'tasks_add_note' => 'Press Enter to add the task.', + 'task_add_cta' => 'Add a task', + + 'debts_you_owe' => 'You owe', + + 'statistics_contacts' => 'Contacts', + 'statistics_activities' => 'Activities', + 'statistics_gifts' => 'Gifts', + + 'reminders_next_months' => 'Events in the next 3 months', + 'reminders_none' => 'No reminders for this month.', + + 'product_changes' => 'Product changes', + 'product_view_details' => 'View details', +]; diff --git a/resources/lang/fa/format.php b/resources/lang/fa/format.php new file mode 100644 index 0000000..a70a6ba --- /dev/null +++ b/resources/lang/fa/format.php @@ -0,0 +1,36 @@ + 'M d, Y H:i', + 'short_date_year' => 'M d, Y', + 'short_date' => 'M d', + 'short_month' => 'M', + 'short_month_year' => 'M Y', + 'short_day' => 'D', + 'full_date_year' => 'F d, Y', + 'full_month' => 'F', + 'full_month_year' => 'F Y', + 'full_hour' => 'h.i A', + + 'short_text' => '{text}…', +]; diff --git a/resources/lang/fa/journal.php b/resources/lang/fa/journal.php new file mode 100644 index 0000000..9b1f0be --- /dev/null +++ b/resources/lang/fa/journal.php @@ -0,0 +1,38 @@ + 'How was your day? You can rate it once a day.', + 'journal_come_back' => 'Thanks. Come back tomorrow to rate your day again.', + 'journal_description' => 'Note: the journal lists both manual journal entries, and automatic entries like Activities done with your contacts. While you can delete journal entries manually, you’ll have to delete the activity directly on the contact page.', + 'journal_add' => 'Add a journal entry', + 'journal_edit' => 'Edit a journal entry', + 'journal_empty' => 'Empty journal', + 'journal_created_at' => 'Created at {date}', + 'journal_created_automatically' => 'Created automatically', + 'journal_entry_type_journal' => 'Journal entry', + 'journal_entry_type_activity' => 'Activity', + 'journal_entry_rate' => 'You rated your day.', + 'journal_add_comment' => 'Care to add a comment (optional)?', + 'journal_show_comment' => 'Show comment', + 'entry_delete_success' => 'The journal entry has been successfully deleted.', + 'journal_add_title' => 'Title (optional)', + 'journal_add_date' => 'Date', + 'journal_add_post' => 'Entry', + 'journal_add_cta' => 'Save', + 'journal_blank_cta' => 'Add your first journal entry', + 'journal_blank_description' => 'The journal lets you write events that happened to you, and remember them.', + 'delete_confirmation' => 'Are you sure you want to delete this journal entry?', + 'apply_filter' => 'Apply filter', + 'start_date' => 'Start Date', + 'end_date' => 'End Date', + 'per_page' => 'Per Page', + 'sort_order' => 'Sort By Created At', + 'ascending' => 'Ascending', + 'descending' => 'Descending', +]; diff --git a/resources/lang/fa/logs.php b/resources/lang/fa/logs.php new file mode 100644 index 0000000..7cbe702 --- /dev/null +++ b/resources/lang/fa/logs.php @@ -0,0 +1,29 @@ + 'مخاطب جدید ایجاد شد.', + 'settings_log_contact_created_with_name' => 'افزودن :name بعنوان مخاطب.', + + // contat description update + 'contact_log_contact_description_updated' => 'بروزرسانی توضیحات.', + 'settings_log_contact_description_updated_with_name' => 'بروزرسانی توضیحات :name', + + // contact description clear + 'contact_log_contact_description_cleared' => 'پاک کردن توضیحات.', + 'settings_log_contact_description_cleared_with_name' => 'پاک کردن توضیحات :name.', + + // contact work information update + 'contact_log_contact_work_updated' => 'بروزرسانی اطلاعات شغلی', + 'settings_log_contact_work_updated_with_name' => 'بروزرسانی اطلاعات شغلی :name', + + // company created + 'settings_log_company_created' => 'ایجاد یک شرکت به نام :name.', +]; diff --git a/resources/lang/fa/mail.php b/resources/lang/fa/mail.php new file mode 100644 index 0000000..749f3d1 --- /dev/null +++ b/resources/lang/fa/mail.php @@ -0,0 +1,53 @@ + 'Reminder for :contact', + 'greetings' => 'Hi :username', + 'want_reminded_of' => 'You wanted to be reminded of :reason', + 'for' => 'For: :name', + 'comment' => 'Comment: :comment', + 'footer_contact_info' => 'Add, view, complete, and change information about this contact:', + 'footer_contact_info2' => 'See :name’s profile', + 'footer_contact_info2_link' => 'See :name’s profile: :url', + + 'notification_subject_line' => 'You have an upcoming event', + 'notification_description' => 'In :count days (on :date), the following event will happen:', + + 'stay_in_touch_subject_line' => 'Stay in touch with :name', + 'stay_in_touch_subject_description' => 'You asked to be reminded to stay in touch with :name every :frequency day.|You asked to be reminded to stay in touch with :name every :frequency days.', + + 'notifications_whoops' => 'Whoops!', + 'notifications_hello' => 'Hello!', + 'notifications_regards' => 'Regards', + 'notifications_footer' => 'If you’re having trouble clicking the ":actionText" button, copy and paste the URL below into your web browser: [:actionURL](:actionURL)', + 'notifications_rights' => 'All rights reserved', + + 'confirmation_email_title' => 'Monica – Email verification', + 'confirmation_email_intro'=> 'To validate your email click on the button below', + 'confirmation_email_button' => 'Verify email address', + 'confirmation_email_bottom' => 'If you did not create an account, no further action is required.', + + 'password_reset_title' => 'Monica – Reset Password Notification', + 'password_reset_intro' => 'You are receiving this email because we received a password reset request for your account.', + 'password_reset_button' => 'Reset Password', + 'password_reset_expiration' => 'This password reset link will expire in :count minutes.', + 'password_reset_bottom' => 'If you did not request a password reset, no further action is required.', + + 'invitation_title' => 'Monica – You are invited by :name', + 'invitation_intro' => 'You’ve been invited by :name (:email) to use Monica, a nice Personal Relationship Management tool.', + 'invitation_link' => 'To accept the invitation, click on the link below:', + 'invitation_button' => 'Accept invitation', + 'invitation_expiration' => 'This link will expire in :count days.', + + 'export_title' => 'Your export is ready', + 'export_description' => 'You requested a data export on :date. It is now ready to download.', + 'export_download' => 'Download export', + +]; diff --git a/resources/lang/fa/pagination.php b/resources/lang/fa/pagination.php new file mode 100644 index 0000000..a81aae2 --- /dev/null +++ b/resources/lang/fa/pagination.php @@ -0,0 +1,25 @@ + '❮ قبلی', + 'next' => 'بعدی ❯', + +]; diff --git a/resources/lang/fa/passwords.php b/resources/lang/fa/passwords.php new file mode 100644 index 0000000..ab9535d --- /dev/null +++ b/resources/lang/fa/passwords.php @@ -0,0 +1,30 @@ + 'رمز عبور شما با موفقیت تغییر کرد!', + 'sent' => 'اگر ایمیل وارد شده موجود باشد ،یک لینک برای بازنشانی رمز به ان ارسال کردیم.', + 'token' => 'توکن بازنشانی رمز عبور معتبر نمی باشد.', + 'user' => 'اگر ایمیل وارد شده موجود باشد ،یک لینک برای بازنشانی رمز به ان ارسال کردیم.', + 'changed' => 'رمز عبور با موفقیت تغییر کرد.', + 'invalid' => 'رمز عبور فعلی وارد شده صحیح نمی باشد.', + 'throttled' => 'لطفا قبل از تلاش مجدد صبر کنید.', + +]; diff --git a/resources/lang/fa/people.php b/resources/lang/fa/people.php new file mode 100644 index 0000000..083c6d8 --- /dev/null +++ b/resources/lang/fa/people.php @@ -0,0 +1,539 @@ + 'مخاطب یافت نشد', + 'people_list_number_kids' => ':count بچه|:count بچه', + 'people_list_last_updated' => 'آخرین مشاوره:', + 'people_list_number_reminders' => ':count یادآوری|:count یادآوری', + 'people_list_blank_title' => 'شما هنوز کسی را در اکانتتان ندارید', + 'people_list_blank_cta' => 'یک نفر رو اضافه کنید', + 'people_list_sort' => 'مرتب سازی', + 'people_list_stats' => ':count مخاطب|:count مخاطب', + 'people_list_firstnameAZ' => 'مرتب سازی نام از "الف" تا "ی"', + 'people_list_firstnameZA' => 'مرتب سازی نام از "ی" تا "الف" ', + 'people_list_lastnameAZ' => 'مرتب سازی نام خانوادگی از "الف" تا "ی"', + 'people_list_lastnameZA' => 'مرتب سازی نام خانوادگی از "ی" تا "الف" ', + 'people_list_lastactivitydateNewtoOld' => 'مرتب سازی آخرین فعالیت ها بر اساس تاریخ نزدیک به دور', + 'people_list_lastactivitydateOldtoNew' => 'مرتب سازی آخرین فعالیت ها بر اساس تاریخ دور به نزدیک', + 'people_list_filter_tag' => 'نمایش همه ی مخاطبینی که برچسب گذاری شده اند با ', + 'people_list_clear_filter' => 'پاک کردن فيلتر', + 'people_list_contacts_per_tags' => ':count مخاطب|:count مخاطب', + 'people_list_show_dead' => 'نمایش افراد فوت شده (:count)', + 'people_list_hide_dead' => 'مخفی کردن افراد فوت شده (:count)', + 'people_search' => 'مخاطبین خود را بیابید...', + 'people_search_no_results' => 'نتایجی پیدا نشد', + 'people_search_next' => 'بعدی', + 'people_search_prev' => 'قبلی', + 'people_search_rows_per_page' => 'تعداد در هر صفحه', + 'people_search_of' => 'از', + 'people_search_page' => 'صفحه', + 'people_search_all' => 'همه', + 'people_add_new' => 'افزودن فرد جدید', + 'people_list_account_usage' => 'استفاده از حساب: :current/:limit مخاطب', + 'people_list_account_upgrade_title' => 'اکانت خود را برای استفاده از تمام پتانسیل ها ارتقا دهید.', + 'people_list_account_upgrade_cta' => 'هم اکنون ارتقا داده شود', + 'people_list_untagged' => 'مشاهده کاربران تگ نشده', + 'people_list_filter_untag' => 'نمایش تمامی کاربران تگ نشده', + 'archived_contact_readonly' => 'مخاطبین آرشیو شده قابل ویرایش نیستند،لطفا ابتدا از آرشیو دربیارید.', + + // people add + 'people_add_title' => 'افزودن فرد جدید', + 'people_add_missing' => 'فردی پیدا نشد - اکنون فرد جدیدی اضافه کنید', + 'people_add_firstname' => 'نام', + 'people_add_middlename' => 'نام میانی (اختیاری)', + 'people_add_lastname' => 'نام خانوادگی (اختیاری)', + 'people_add_email' => 'رایانامه (اختیاری)', + 'people_add_nickname' => 'نام مستعار (اختیاری)', + 'people_add_cta' => 'افزودن', + 'people_save_and_add_another_cta' => 'ذخیره و افزودن فرد دیگر', + 'people_add_success' => ':name با موفقیت ساخته شد', + 'people_add_gender' => 'جنسیت', + 'people_delete_success' => 'مخاطب حذف شد', + 'people_delete_message' => 'حذف مخاطب', + 'people_delete_confirmation' => 'از حذف مخاطب ( :name ) اطمینان دارید؟ حذف به صورت کامل و دائمی است.', + 'people_add_birthday_reminder' => 'تولدت مبارک :name', + 'people_add_birthday_reminder_deceased' => 'در این تاریخ :name ، حتما تولدش را جشن گرفته ', + 'people_add_import' => 'تمایل دارید مخاطبی خود را درون ریزی کنید؟', + 'people_edit_email_error' => 'There is already a contact in your account with this email address. Please choose another one.', + 'people_export' => 'Export as vCard', + 'people_add_reminder_for_birthday' => 'Create an annual birthday reminder', + + // show + 'section_contact_information' => 'Contact information', + 'section_personal_activities' => 'Activities', + 'section_personal_reminders' => 'Reminders', + 'section_personal_tasks' => 'Tasks', + 'section_personal_gifts' => 'Gifts', + 'section_personal_notes' => 'Notes', + + // archived contacts + 'list_link_to_active_contacts' => 'You are viewing archived contacts. See the list of active contacts instead.', + 'list_link_to_archived_contacts' => 'List of archived contacts', + + // Header + 'me' => 'This is you', + 'edit_contact_information' => 'Edit contact information', + 'contact_archive' => 'Archive contact', + 'contact_unarchive' => 'Unarchive contact', + 'contact_archive_help' => 'Archived contacts are not be shown on the contact list, but still appear in search results.', + 'call_button' => 'Log a call', + 'set_favorite' => 'Favorite contacts are placed at the top of the contact list', + + // Stay in touch + 'stay_in_touch' => 'Stay in touch', + 'stay_in_touch_frequency' => 'Stay in touch every day|Stay in touch every {count} days', + 'stay_in_touch_next_date' => 'Next due: {date}', + 'stay_in_touch_invalid' => 'The frequency must be a number greater than 0.', + 'stay_in_touch_premium' => 'You need to upgrade your account to make use of this feature', + 'stay_in_touch_modal_title' => 'Stay in touch', + 'stay_in_touch_modal_desc' => 'We can remind you by email to keep in touch with {firstname} at a regular interval.', + 'stay_in_touch_modal_label' => 'Send me an email every… {count} day|Send me an email every… {count} days', + + // Calls + 'modal_call_title' => 'Log a call', + 'modal_call_comment' => 'What did you talk about? (optional)', + 'modal_call_exact_date' => 'The phone call happened on', + 'modal_call_who_called' => 'Who called?', + 'modal_call_emotion' => 'Do you want to log how you felt during this call? (optional)', + 'calls_add_success' => 'The phone call has been saved.', + 'call_delete_confirmation' => 'Are you sure you want to delete this call?', + 'call_delete_success' => 'The call has been deleted successfully', + 'call_title' => 'Phone calls', + 'call_empty_comment' => 'No details', + 'call_blank_title' => 'Keep track of the phone calls you’ve done with {name}', + 'call_blank_desc' => 'You called {name}', + 'call_you_called' => 'You called', + 'call_he_called' => '{name} called', + 'call_emotions' => 'Emotions:', + + // Conversation + 'conversation_blank' => 'Record conversations you have with :name on social media, SMS…', + 'conversation_delete_link' => 'Delete the conversation', + 'conversation_edit_title' => 'Edit conversation', + 'conversation_edit_delete' => 'Are you sure you want to delete this conversation? Deletion is permanent.', + 'conversation_add_success' => 'The conversation has been successfully added.', + 'conversation_edit_success' => 'The conversation has been successfully updated.', + 'conversation_delete_success' => 'The conversation has been successfully deleted.', + 'conversation_add_title' => 'Record a new conversation', + 'conversation_add_when' => 'When did you have this conversation?', + 'conversation_add_who_wrote' => 'Who sent this message?', + 'conversation_add_how' => 'How did you communicate?', + 'conversation_add_you' => 'You', + 'conversation_add_content' => 'Write down what was said', + 'conversation_add_what_was_said' => 'What did you say?', + 'conversation_add_another' => 'Add another message', + 'conversation_add_error' => 'You must add at least one message.', + 'conversation_list_table_messages' => 'Messages', + 'conversation_list_table_content' => 'Partial content (last message)', + 'conversation_list_title' => 'Conversations', + 'conversation_list_cta' => 'Log conversation', + + // age - birthday + 'birthdate_not_set' => 'Birthday is not set', + 'age_approximate_in_years' => 'around :age years old', + 'age_exact_in_years' => ':age years old', + 'age_exact_birthdate' => 'born :date', + + // Last called + 'last_called' => 'Last called: :date', + 'last_talked_to' => 'Last called: {date}', + 'last_called_empty' => 'Last called: unknown', + 'last_activity_date' => 'Last activity together: :date', + 'last_activity_date_empty' => 'Last activity together: unknown', + + // additional information + 'information_edit_success' => 'The profile has been updated successfully', + 'information_edit_title' => 'Edit :name’s personal information', + 'information_edit_max_size' => 'Max :size Kb.', + 'information_edit_max_size2' => 'Max {size} Kb.', + 'information_edit_firstname' => 'First name', + 'information_edit_lastname' => 'Last name (optional)', + 'information_edit_description' => 'Description (optional)', + 'information_edit_description_help' => 'Used on the contact list to add some context, if necessary.', + 'information_edit_unknown' => 'I do not know this person’s age', + 'information_edit_probably' => 'This person is probably…', + 'information_edit_not_year' => 'I know the day and month of this person’s birthday, but not the year…', + 'information_edit_exact' => 'I know this person’s exact birthday…', + 'information_edit_birthdate_label' => 'Birthday', + 'information_no_work_defined' => 'No work information defined', + 'information_work_at' => 'at :company', + 'work_add_cta' => 'Update work information', + 'work_edit_success' => 'Work information updated', + 'work_edit_title' => 'Update :name’s job information', + 'work_edit_job' => 'Job title (optional)', + 'work_edit_company' => 'Company (optional)', + 'work_information' => 'Work information', + + // food preferences + 'food_preferences_add_success' => 'Food preferences have been saved', + 'food_preferences_edit_description' => 'Perhaps :firstname or someone in the :family’s family has an allergy. Or doesn’t like a specific bottle of wine. Indicate them here so you will remember it next time you invite them for dinner', + 'food_preferences_edit_description_no_last_name' => 'Perhaps :firstname has an allergy. Or doesn’t like a specific bottle of wine. Indicate them here so you will remember it next time you invite them for dinner', + 'food_preferences_edit_title' => 'Indicate food preferences', + 'food_preferences_edit_cta' => 'Save food preferences', + 'food_preferences_title' => 'Food preferences', + 'food_preferences_cta' => 'Add food preferences', + + // reminders + 'reminders_blank_title' => 'Is there something you want to be reminded of about :name?', + 'reminders_blank_add_activity' => 'Add a reminder', + 'reminders_add_title' => 'What would you like to be reminded of about :name?', + 'reminders_add_description' => 'Please remind me to…', + 'reminders_add_next_time' => 'When is the next time you would like to be reminded about this?', + 'reminders_add_once' => 'Remind me about this just once', + 'reminders_add_recurrent' => 'Remind me about this every', + 'reminders_add_starting_from' => 'starting from the date specified above', + 'reminders_add_cta' => 'Add reminder', + 'reminders_edit_update_cta' => 'Update reminder', + 'reminders_add_error_custom_text' => 'You need to indicate a text for this reminder', + 'reminders_create_success' => 'The reminder has been added successfully', + 'reminders_delete_success' => 'The reminder has been deleted successfully', + 'reminders_update_success' => 'The reminder has been updated successfully', + 'reminders_add_optional_comment' => 'Optional comment', + + 'reminder_frequency_day' => 'every day|every :number days', + 'reminder_frequency_week' => 'every week|every :number weeks', + 'reminder_frequency_month' => 'every month|every :number months', + 'reminder_frequency_year' => 'every year|every :number year', + 'reminder_frequency_one_time' => 'on :date', + 'reminders_delete_confirmation' => 'Are you sure you want to delete this reminder?', + 'reminders_delete_cta' => 'Delete', + 'reminders_next_expected_date' => 'on', + 'reminders_cta' => 'Add a reminder', + 'reminders_description' => 'We will send an email for each one of the reminders below. Reminders are sent every morning the day events will happen. Reminders automatically added for birthdays can not be deleted. If you want to change those dates, edit the birthday of the contacts.', + 'reminders_one_time' => 'One time', + 'reminders_type_week' => 'week', + 'reminders_type_month' => 'month', + 'reminders_type_year' => 'year', + 'reminders_birthday' => 'Birthday of :name', + 'reminders_free_plan_warning' => 'You are on the Free plan. No emails are sent on this plan. To receive your reminders by email, upgrade your account.', + + // relationships + 'relationship_form_add' => 'Add a new relationship', + 'relationship_form_edit' => 'Edit an existing relationship', + 'relationship_form_is_with' => 'This person is…', + 'relationship_form_is_with_name' => ':name is…', + 'relationship_form_add_choice' => 'Who is the relationship with?', + 'relationship_form_create_contact' => 'Add a new person', + 'relationship_form_associate_contact' => 'An existing contact', + 'relationship_form_associate_dropdown' => 'Search and select an existing contact from the dropdown below', + 'relationship_form_associate_dropdown_placeholder' => 'Search and select an existing contact', + 'relationship_form_also_create_contact' => 'Create a Contact entry for this person.', + 'relationship_form_add_description' => 'This will let you treat this person like any other contact.', + 'relationship_form_add_no_existing_contact' => 'You don’t have any contacts who can be related to :name at the moment.', + 'relationship_delete_confirmation' => 'Are you sure you want to delete this relationship? Deletion is permanent.', + 'relationship_unlink_confirmation' => 'Are you sure you want to delete this relationship? This person will not be deleted – only the relationship between the two.', + 'relationship_form_add_success' => 'The relationship has been successfully set.', + 'relationship_form_deletion_success' => 'The relationship has been deleted.', + + // tasks + 'tasks_title' => 'Tasks', + 'tasks_blank_title' => 'You don’t have any tasks yet.', + 'tasks_form_title' => 'Title', + 'tasks_form_description' => 'Description (optional)', + 'tasks_add_task' => 'Add a task', + 'tasks_delete_success' => 'The task has been deleted successfully', + 'tasks_complete_success' => 'The task has changed status successfully', + + // activities + 'activity_title' => 'Activities', + 'activity_type_category_simple_activities' => 'Simple activities', + 'activity_type_category_sport' => 'Sport', + 'activity_type_category_food' => 'Food', + 'activity_type_category_cultural_activities' => 'Cultural activities', + 'activity_type_just_hung_out' => 'just hung out', + 'activity_type_watched_movie_at_home' => 'watched a movie at home', + 'activity_type_talked_at_home' => 'just talked at home', + 'activity_type_did_sport_activities_together' => 'played a sport together', + 'activity_type_ate_at_his_place' => 'ate at their place', + 'activity_type_went_bar' => 'went to a bar', + 'activity_type_ate_at_home' => 'ate at home', + 'activity_type_picnicked' => 'picnicked', + 'activity_type_ate_restaurant' => 'ate at a restaurant', + 'activity_type_went_theater' => 'went to the theater', + 'activity_type_went_concert' => 'went to a concert', + 'activity_type_went_play' => 'went to a play', + 'activity_type_went_museum' => 'went to the museum', + 'activities_add_activity' => 'Add activity', + 'activities_add_more_details' => 'Add more details', + 'activities_add_emotions' => 'Add emotions', + 'activities_add_category' => 'Indicate a category', + 'activities_add_participants_cta' => 'Add participants', + 'activities_item_information' => ':Activity. Happened on :date', + 'activities_add_title' => 'What did you do with {name}?', + 'activities_summary' => 'Describe what you did', + 'activities_add_pick_activity' => 'Would you like to categorize this activity? You don’t have to, but it will give you statistics later on (optional)', + 'activities_add_date_occured' => 'The activity happened on…', + 'activities_add_participants' => 'Who, apart from {name}, participated in this activity? (optional)', + 'activities_add_emotions_title' => 'Do you want to log how you felt during this activity? (optional)', + 'activities_blank_title' => 'Keep track of what you’ve done with {name} in the past, and what you’ve talked about', + 'activities_blank_add_activity' => 'Add an activity', + 'activities_add_success' => 'The activity has been added successfully', + 'activities_add_error' => 'Error when adding the activity', + 'activities_update_success' => 'The activity has been updated successfully', + 'activities_delete_success' => 'The activity has been deleted successfully', + 'activities_who_was_involved' => 'Who was involved?', + 'activities_activity' => 'Activity Category', + 'activities_view_activities_report' => 'View activities report', + 'activities_profile_title' => 'Activities report between :name and you', + 'activities_profile_subtitle' => 'You’ve logged :total_activities activity with :name in total and :activities_last_twelve_months in the last 12 months so far.|You’ve logged :total_activities activities with :name in total and :activities_last_twelve_months in the last 12 months so far.', + 'activities_profile_year_summary_activity_types' => 'Here is a breakdown of the type of activities you’ve done together in :year', + 'activities_profile_year_summary' => 'Here is what you two have done in :year', + 'activities_profile_number_occurences' => ':value activity|:value activities', + 'activities_list_participants' => 'Participants ({total}):', + 'activities_list_emotions' => 'Emotions felt:', + 'activities_list_date' => 'Happened on', + 'activities_list_category' => 'Category:', + + // notes + 'notes_create_success' => 'The note has been created successfully', + 'notes_update_success' => 'The note has been saved successfully', + 'notes_delete_success' => 'The note has been deleted successfully', + 'notes_add_cta' => 'Add note', + 'notes_favorite' => 'Add/remove from favorites', + 'notes_delete_title' => 'Delete a note', + 'notes_delete_confirmation' => 'Are you sure you want to delete this note? Deletion is permanent', + + // gifts + 'gifts_title' => 'Gifts', + 'gifts_add_success' => 'The gift has been added successfully', + 'gifts_delete_success' => 'The gift has been deleted successfully', + 'gifts_delete_confirmation' => 'Are you sure you want to delete this gift?', + 'gifts_add_gift' => 'Add a gift', + 'gifts_link' => 'Link', + 'gifts_for' => 'For: {name}', + 'gifts_delete_cta' => 'Delete', + 'gifts_add_title' => 'Gift management for :name', + 'gifts_add_gift_idea' => 'Gift idea', + 'gifts_add_gift_already_offered' => 'Gift given', + 'gifts_add_gift_received' => 'Gift received', + 'gifts_add_gift_title' => 'What is this gift?', + 'gifts_add_gift_name' => 'Gift name', + 'gifts_add_link' => 'Link to the web page (optional)', + 'gifts_add_value' => 'Value (optional)', + 'gifts_add_comment' => 'Comment (optional)', + 'gifts_add_recipient' => 'Recipient (optional)', + 'gifts_add_recipient_field' => 'Recipient', + 'gifts_add_photo' => 'Photo (optional)', + 'gifts_add_photo_title' => 'Add a photo for this gift', + 'gifts_add_someone' => 'This gift is for someone in {name}’s family in particular', + 'gifts_delete_title' => 'Delete a gift', + 'gifts_ideas' => 'Gift ideas', + 'gifts_offered' => 'Gifts given', + 'gifts_offered_as_an_idea' => 'Mark as an idea', + 'gifts_received' => 'Gifts received', + 'gifts_view_comment' => 'View comment', + 'gifts_mark_offered' => 'Mark as given', + 'gifts_update_success' => 'The gift has been updated successfully', + 'gifts_add_date' => 'Date (optional)', + + // debts + 'debt_delete_confirmation' => 'Are you sure you want to delete this debt?', + 'debt_delete_success' => 'The debt has been deleted successfully', + 'debt_add_success' => 'The debt has been added successfully', + 'debt_title' => 'Debts', + 'debt_add_cta' => 'Add debt', + 'debt_you_owe' => 'You owe :amount', + 'debt_they_owe' => ':name owes you :amount', + 'debt_add_title' => 'Debt management', + 'debt_add_you_owe' => 'You owe :name', + 'debt_add_they_owe' => ':name owes you', + 'debt_add_amount' => 'the sum of', + 'debt_add_reason' => 'for the following reason (optional)', + 'debt_add_add_cta' => 'Add debt', + 'debt_edit_update_cta' => 'Update debt', + 'debt_edit_success' => 'The debt has been updated successfully', + 'debts_blank_title' => 'Manage debts you owe to :name or :name owes you', + + // tags + 'tag_edit' => 'Edit tag', + 'tag_add' => 'Add tags', + 'tag_add_search' => 'Add or search tags', + 'tag_no_tags' => 'No tags yet', + + // Introductions + 'introductions_sidebar_title' => 'How you met', + 'introductions_blank_cta' => 'Indicate how you met :name', + 'introductions_title_edit' => 'How did you meet :name?', + 'introductions_additional_info' => 'Explain how and where you met', + 'introductions_edit_met_through' => 'Has someone introduced you to this person?', + 'introductions_no_met_through' => 'No one', + 'introductions_first_met_date' => 'Date you met', + 'introductions_no_first_met_date' => 'I don’t know the date we met', + 'introductions_first_met_date_known' => 'This is the date we met', + 'introductions_add_reminder' => 'Add a reminder to celebrate this encounter on the anniversary this event happened', + 'introductions_update_success' => 'You’ve successfully updated the information about how you met this person', + 'introductions_met_through' => 'Met through :name', + 'introductions_met_date' => 'Met on :date', + 'introductions_reminder_title' => 'Anniversary of the day you first met', + + // Deceased + 'deceased_reminder_title' => 'Anniversary of the death of :name', + 'deceased_mark_person_deceased' => 'Mark this as deceased', + 'deceased_know_date' => 'I know the date that this person died', + 'deceased_add_reminder' => 'Add a reminder for this date', + 'deceased_label' => 'Deceased', + 'deceased_date_label' => 'Deceased date', + 'deceased_label_with_date' => 'Deceased on :date', + 'deceased_age' => 'Age at death', + + // Contact information + 'contact_info_title' => 'Contact information', + 'contact_info_form_content' => 'Content', + 'contact_info_form_contact_type' => 'Contact type', + 'contact_info_form_personalize' => 'Personalize', + 'contact_info_address' => 'Lives in', + + // Addresses + 'contact_address_title' => 'Addresses', + 'contact_address_form_name' => 'Label (optional)', + 'contact_address_form_street' => 'Street (optional)', + 'contact_address_form_city' => 'City (optional)', + 'contact_address_form_province' => 'Province (optional)', + 'contact_address_form_postal_code' => 'Postal code (optional)', + 'contact_address_form_country' => 'Country (optional)', + 'contact_address_form_latitude' => 'Latitude (numbers only) (optional)', + 'contact_address_form_longitude' => 'Longitude (numbers only) (optional)', + + // Pets + 'pets_kind' => 'Kind of pet', + 'pets_name' => 'Name (optional)', + 'pets_create_success' => 'The pet has been successfully added', + 'pets_update_success' => 'The pet has been updated', + 'pets_delete_success' => 'The pet has been deleted', + 'pets_title' => 'Pets', + 'pets_reptile' => 'Reptile', + 'pets_bird' => 'Bird', + 'pets_cat' => 'Cat', + 'pets_dog' => 'Dog', + 'pets_fish' => 'Fish', + 'pets_hamster' => 'Hamster', + 'pets_horse' => 'Horse', + 'pets_rabbit' => 'Rabbit', + 'pets_rat' => 'Rat', + 'pets_small_animal' => 'Small animal', + 'pets_other' => 'Other', + + // life events + 'life_event_list_tab_life_events' => 'Life events', + 'life_event_list_tab_other' => 'Notes, reminders, …', + 'life_event_list_title' => 'Life events', + 'life_event_blank' => 'Log what happens to the life of {name} for your future reference.', + 'life_event_list_cta' => 'Add life event', + 'life_event_create_category' => 'All categories', + 'life_event_create_life_event' => 'Add life event', + 'life_event_create_default_title' => 'Title (optional)', + 'life_event_create_default_story' => 'Story (optional)', + 'life_event_create_date' => 'You do not need to indicate a month or a day – only the year is mandatory.', + 'life_event_create_default_description' => 'Add information about what you know', + 'life_event_create_add_yearly_reminder' => 'Add a yearly reminder for this event', + 'life_event_create_success' => 'The life event has been added', + 'life_event_delete_title' => 'Delete a life event', + 'life_event_delete_description' => 'Are you sure you want to delete this life event? Deletion is permanent.', + 'life_event_delete_success' => 'The life event has been deleted', + 'life_event_date_it_happened' => 'Date it happened', + 'life_event_category_work_education' => 'Work & education', + 'life_event_category_family_relationships' => 'Family & relationships', + 'life_event_category_home_living' => 'Home & living', + 'life_event_category_health_wellness' => 'Health & wellness', + 'life_event_category_travel_experiences' => 'Travel & experiences', + 'life_event_sentence_new_job' => 'Started a new job', + 'life_event_sentence_retirement' => 'Retired', + 'life_event_sentence_new_school' => 'Started school', + 'life_event_sentence_study_abroad' => 'Studied abroad', + 'life_event_sentence_volunteer_work' => 'Started volunteering', + 'life_event_sentence_published_book_or_paper' => 'Published a paper', + 'life_event_sentence_military_service' => 'Started military service', + 'life_event_sentence_new_relationship' => 'Started a relationship', + 'life_event_sentence_engagement' => 'Got engaged', + 'life_event_sentence_marriage' => 'Got married', + 'life_event_sentence_anniversary' => 'Anniversary', + 'life_event_sentence_expecting_a_baby' => 'Expects a baby', + 'life_event_sentence_new_child' => 'Had a child', + 'life_event_sentence_new_family_member' => 'Added a family member', + 'life_event_sentence_new_pet' => 'Got a pet', + 'life_event_sentence_end_of_relationship' => 'Ended a relationship', + 'life_event_sentence_loss_of_a_loved_one' => 'Lost a loved one', + 'life_event_sentence_moved' => 'Moved', + 'life_event_sentence_bought_a_home' => 'Bought a home', + 'life_event_sentence_home_improvement' => 'Made a home improvement', + 'life_event_sentence_holidays' => 'Went on holidays', + 'life_event_sentence_new_vehicle' => 'Got a new vehicle', + 'life_event_sentence_new_roommate' => 'Got a roommate', + 'life_event_sentence_overcame_an_illness' => 'Overcame an illness', + 'life_event_sentence_quit_a_habit' => 'Quit a habit', + 'life_event_sentence_new_eating_habits' => 'Started new eating habits', + 'life_event_sentence_weight_loss' => 'Lost weight', + 'life_event_sentence_wear_glass_or_contact' => 'Started to wear glass or contact lenses', + 'life_event_sentence_broken_bone' => 'Broke a bone', + 'life_event_sentence_removed_braces' => 'Removed braces', + 'life_event_sentence_surgery' => 'Had surgery', + 'life_event_sentence_dentist' => 'Went to the dentist', + 'life_event_sentence_new_sport' => 'Started a sport', + 'life_event_sentence_new_hobby' => 'Started a hobby', + 'life_event_sentence_new_instrument' => 'Learned a new instrument', + 'life_event_sentence_new_language' => 'Learned a new language', + 'life_event_sentence_tattoo_or_piercing' => 'Got a tattoo or piercing', + 'life_event_sentence_new_license' => 'Got a license', + 'life_event_sentence_travel' => 'Traveled', + 'life_event_sentence_achievement_or_award' => 'Got an achievement or award', + 'life_event_sentence_changed_beliefs' => 'Changed beliefs', + 'life_event_sentence_first_word' => 'Spoke for the first time', + 'life_event_sentence_first_kiss' => 'Kissed for the first time', + + // documents + 'document_list_title' => 'Documents', + 'document_list_cta' => 'Upload document', + 'document_list_blank_desc' => 'Here you can store documents related to this person.', + 'document_upload_zone_cta' => 'Upload a file', + 'document_upload_zone_progress' => 'Uploading the document…', + 'document_upload_zone_error' => 'There was an error uploading the document. Please try again below.', + + // Photos + 'photo_title' => 'Photos', + 'photo_list_title' => 'Related photos', + 'photo_list_cta' => 'Upload photo', + 'photo_list_blank_desc' => 'You can store images about this contact. Upload one now!', + 'photo_upload_zone_cta' => 'Upload a photo', + 'photo_current_profile_pic' => 'Current profile picture', + 'photo_make_profile_pic' => 'Make profile picture', + 'photo_delete' => 'Delete photo', + 'photo_next' => 'Next photo ❯', + 'photo_previous' => '❮ Previous photo', + + // Avatars + 'avatar_change_title' => 'Change your avatar', + 'avatar_question' => 'Which avatar would you like to use?', + 'avatar_default_avatar' => 'The default avatar', + 'avatar_adorable_avatar' => 'The Adorable avatar', + 'avatar_gravatar' => 'The Gravatar associated with the email address of this person. Gravatar is a global system that lets users associate email addresses with photos.', + 'avatar_current' => 'Keep the current avatar', + 'avatar_photo' => 'From a photo that you upload', + 'avatar_crop_new_avatar_photo' => 'Crop new avatar photo', + + // emotions + 'emotion_this_made_me_feel' => 'This made you feel…', + + // logs + 'auditlogs_link' => 'History', + 'auditlogs_title' => 'Everything that happened to :name', + 'auditlogs_breadcrumb' => 'History', + 'auditlogs_author' => 'By :name on :date', + + // contact field label + 'contact_field_label_home' => 'Home', + 'contact_field_label_work' => 'Work', + 'contact_field_label_cell' => 'Mobile', + 'contact_field_label_fax' => 'Fax', + 'contact_field_label_pager' => 'Pager', + 'contact_field_label_main' => 'Main', + 'contact_field_label_other' => 'Other', + 'contact_field_label_personal' => 'Personal', +]; diff --git a/resources/lang/fa/reminder.php b/resources/lang/fa/reminder.php new file mode 100644 index 0000000..bcab17c --- /dev/null +++ b/resources/lang/fa/reminder.php @@ -0,0 +1,16 @@ + 'Wish happy birthday to', + 'type_phone_call' => 'Call', + 'type_lunch' => 'Lunch with', + 'type_hangout' => 'Hangout with', + 'type_email' => 'Email', + 'type_birthday_kid' => 'Wish happy birthday to the child of', +]; diff --git a/resources/lang/fa/settings.php b/resources/lang/fa/settings.php new file mode 100644 index 0000000..76141a2 --- /dev/null +++ b/resources/lang/fa/settings.php @@ -0,0 +1,558 @@ + 'تنظیمات حساب', + 'sidebar_personalization' => 'شخصی سازی', + 'sidebar_settings_storage' => 'فضای ذخیره سازی', + 'sidebar_settings_export' => 'استخراج داده', + 'sidebar_settings_users' => 'کاربران', + 'sidebar_settings_subscriptions' => 'اشتراک', + 'sidebar_settings_import' => 'وارد کردن دیتا', + 'sidebar_settings_tags' => 'مدیریت برچسب ها', + 'sidebar_settings_api' => 'وب سرویس', + 'sidebar_settings_dav' => 'منابع DAV', + 'sidebar_settings_security' => 'امنیت', + 'sidebar_settings_auditlogs' => 'گزارش های حسابرسی', + + 'title_general' => 'اطلاعات عمومی', + 'title_i18n' => 'تنظیمات بین المللی', + 'title_layout' => 'طرح بندی', + + 'me_title' => 'من بعنوان مخاطب', + 'me_help' => 'این مخاطب شما را در مونیکا نشان می دهد +', + 'me_select' => 'مخاطب را انتخاب کنید', + 'me_no_contact' => 'هیچ مخاطبی انتخاب نشده است.', + 'me_select_click' => 'برای انتخاب یک مخاطب اینجا کلیک کنید', + 'me_remove_contact' => 'انجمن را حذف کنید', + 'me_choose' => 'خودتان را انتخاب کنید', + 'me_choose_placeholder' => 'خودتان را انتخاب کنید', + + 'export_title' => 'داده های حساب خود را برون ریزی کنید', + 'export_be_patient' => 'Click the button to start the export. It might take several minutes to process the export – please be patient and do not spam the button.', + 'export_title_sql' => 'Export to SQL', + 'export_sql_explanation' => 'Exporting your data in SQL format allows you to take your data and import it to your own Monica instance. This is only valuable if you do have your own server.', + 'export_sql_cta' => 'Export to SQL', + 'export_sql_link_instructions' => 'Note: read the instructions to learn more about importing this file to your instance.', + 'export_title_json' => 'Export to Json', + 'export_submitted' => 'Your export has been submitted, it will be available in a few moment…', + 'export_json_explanation' => 'Exporting your data in Json format for backup.', + 'export_json_beta' => 'Json export is in preview mode. Tell us what you think about it:', + 'export_json_cta' => 'Export to Json', + 'export_header_type' => 'Type', + 'export_header_timestamp' => 'Creation date', + 'export_header_status' => 'Status', + 'export_header_actions' => 'Actions', + 'export_last_title' => 'Last exports', + 'export_empty_title' => 'No exports yet', + 'export_type_json' => 'Json export', + 'export_type_sql' => 'SQL export', + 'export_status_todo' => 'Submitted', + 'export_status_doing' => 'Doing', + 'export_status_done' => 'Done', + 'export_status_failed' => 'Failed', + 'export_not_done' => 'Download impossible, this export is not done yet.', + + 'firstname' => 'نام', + 'lastname' => 'نام خانوادگی', + 'name_order' => 'ترتیب نام', + 'name_order_firstname_lastname' => ' – صالح شریفی', + 'name_order_lastname_firstname' => ' – شریفی صالح', + 'name_order_firstname_lastname_nickname' => 'صالح شریفی (فرمانده)', + 'name_order_firstname_nickname_lastname' => 'صالح (فرمانده) شریفی', + 'name_order_lastname_firstname_nickname' => ' () – شریفی صالح (فرمانده)', + 'name_order_lastname_nickname_firstname' => ' () – شریفی (فرمانده) صالح', + 'name_order_nickname_firstname_lastname' => ' ( ) – فرمانده (صالح شریفی)', + 'name_order_nickname_lastname_firstname' => ' ( ) – فرمانده (شریفی صالح)', + 'name_order_nickname' => ' – فرمانده', + 'currency' => 'واحدپول', + 'name' => 'نام شما: :name', + 'email' => 'آدرس ایمیل', + 'email_placeholder' => 'ایمیل را وارد کنید', + 'email_help' => 'از این ایمیل برای ورود و همچنین ارسال یاد آوری ها استفاده می شود.', + 'timezone' => 'منقطه زمانی', + 'temperature_scale' => 'مقیاس دما', + 'temperature_scale_fahrenheit' => 'Fahrenheit (°F)', + 'temperature_scale_celsius' => 'Celsius (°C)', + 'layout' => 'Layout', + 'layout_small' => 'Maximum 1200 pixels wide', + 'layout_big' => 'Full width of the browser', + 'save' => 'Update preferences', + 'delete_title' => 'Delete your account', + 'delete_desc' => 'Do you wish to delete your account? Deletion is permanent and all of your data will be erased permanently. If you have a subscription, it will be cancelled immediately.', + 'delete_other_desc' => 'Your data in the main database will be deleted immediately. As described in our privacy policy, we carry out securely encrypted backups of the database every day. These backups are kept for 30 days after which they are completely deleted. We cannot delete specific data from the backups we hold any earlier than this. All of your data will be completely deleted no later than 31 days after your account’s deletion.', + 'reset_desc' => 'Do you wish to reset your account? This will remove all your contacts, and all of the data associated with them. Your account will not be deleted.', + 'reset_title' => 'Reset your account', + 'reset_cta' => 'Reset account', + 'reset_notice' => 'Are you sure to reset your account? This is permanent and cannot be undone.', + 'reset_success' => 'Your account has been reset successfully.', + 'delete_notice' => 'Are you sure you want to delete your account? This is permanent and cannot be undone. All of your data will be deleted and will not be recoverable.', + 'delete_cta' => 'Delete account', + 'settings_success' => 'Preferences updated!', + 'locale' => 'Language used in the app', + 'locale_help' => 'Do you want to help translating Monica or add a new language? Please follow this link for more information.', + 'locale_ar' => 'Arabic', + 'locale_cs' => 'Czech', + 'locale_de' => 'German', + 'locale_el' => 'Greek', + 'locale_en' => 'English', + 'locale_en-GB' => 'English (United Kingdom)', + 'locale_es' => 'Spanish', + 'locale_fr' => 'French', + 'locale_he' => 'Hebrew', + 'locale_hr' => 'Croatian', + 'locale_id' => 'Indonesian', + 'locale_it' => 'Italian', + 'locale_ja' => 'Japanese', + 'locale_nl' => 'Dutch', + 'locale_pt' => 'Portuguese', + 'locale_pt-BR' => 'Brazilian Portuguese', + 'locale_ru' => 'Russian', + 'locale_sv' => 'Swedish', + 'locale_vi' => 'Vietnamese', + 'locale_zh' => 'Chinese Simplified', + 'locale_zh-TW' => 'Chinese Traditional', + 'locale_tr' => 'Turkish', + + 'security_title' => 'Security', + 'security_help' => 'Change security matters for your account.', + 'password_change' => 'Change your password', + 'password_current' => 'Current password', + 'password_current_placeholder' => 'Enter your current password', + 'password_new1' => 'New password', + 'password_new1_placeholder' => 'Enter your new password', + 'password_new2' => 'Confirm your new password', + 'password_new2_placeholder' => 'Retype your new password', + 'password_btn' => 'Change password', + '2fa_title' => 'Two Factor Authentication', + '2fa_otp_title' => 'اپلیکیشن موبایل احرازهویت دومرحله ای 2FA', + '2fa_enable_title' => 'فعال‌سازی احرازهویت دو مرحله‌ای', + '2fa_enable_description' => 'برای افزایش امنیت حساب خود، احراز هویت دو عاملی را فعال کنید.', + '2fa_enable_otp' => 'برنامه تلفن همراه احراز هویت دو عاملی خود را باز کنید و بارکد QR زیر را اسکن کنید:', + '2fa_enable_otp_help' => 'اگر برنامه موبایل احراز هویت دو عاملی شما از بارکد QR پشتیبانی نمی کند، کد زیر را وارد کنید:', + '2fa_enable_otp_validate' => 'لطفاً دستگاه جدیدی را که به تازگی راه‌اندازی کرده‌اید تأیید کنید:', + '2fa_enable_success' => 'Two Factor Authentication activated', + '2fa_enable_error' => 'Error when trying to activate Two Factor Authentication', + '2fa_enable_error_already_set' => 'Two Factor Authentication is already activated', + '2fa_disable_title' => 'Disable Two Factor Authentication', + '2fa_disable_description' => 'Disable Two Factor Authentication for your account. Be careful, your account will be much less secure!', + '2fa_disable_success' => 'Two Factor Authentication disabled', + '2fa_disable_error' => 'Error when trying to disable Two Factor Authentication', + + 'webauthn_title' => 'Security key — WebAuthn protocol', + 'webauthn_enable_description' => 'Add a new security key', + 'webauthn_key_name_help' => 'Give your key a name.', + 'webauthn_key_name' => 'Key name:', + 'webauthn_success' => 'Your key is detected and validated.', + 'webauthn_last_use' => 'Last use: {timestamp}', + 'webauthn_delete_confirmation' => 'Are you sure you want to delete this key?', + 'webauthn_delete_success' => 'Key deleted', + 'webauthn_insertKey' => 'Insert your security key.', + 'webauthn_buttonAdvise' => 'If your security key has a button, press it.', + 'webauthn_noButtonAdvise' => 'If it does not, remove it and insert it again.', + 'webauthn_not_supported' => 'Your browser doesn’t currently support WebAuthn.', + 'webauthn_not_secured' => 'WebAuthn only supports secure connections. Please load this page with https scheme.', + 'webauthn_error_already_used' => 'This key is already registered. It’s not necessary to register it again.', + 'webauthn_error_not_allowed' => 'The operation either timed out or was not allowed.', + + 'recovery_title' => 'Recovery codes', + 'recovery_show' => 'Get recovery codes', + 'recovery_copy_help' => 'Copy codes in your clipboard', + 'recovery_help_intro' => 'These are your recovery codes:', + 'recovery_help_information' => 'You can use each recovery code once.', + 'recovery_clipboard' => 'Codes copied to the clipboard.', + 'recovery_generate' => 'Generate new codes…', + 'recovery_generate_help' => 'Generating new codes will invalidate previously generated codes.', + 'recovery_already_used_help' => 'This code has already been used.', + + 'users_list_title' => 'Users with access to your account', + 'users_list_add_user' => 'Invite a new user', + 'users_list_you' => 'That’s you', + 'users_list_invitations_title' => 'Pending invitations', + 'users_list_invitations_explanation' => 'Below are the people you’ve invited to join Monica as a collaborator.', + 'users_list_invitations_invited_by' => 'invited by :name', + 'users_list_invitations_sent_date' => 'sent on :date', + 'users_blank_title' => 'You are the only one who has access to this account.', + 'users_blank_add_title' => 'Would you like to invite someone else?', + 'users_blank_description' => 'This person will have the same access that you have, and will be able to add, edit or delete contact information.', + 'users_blank_cta' => 'Invite someone', + 'users_add_title' => 'Invite a new user to your account by email', + 'users_add_description' => 'This person will have the same access as you do, including inviting or deleting other users, including you. Make sure you trust this person before giving them access.', + 'users_add_email_field' => 'Enter the email of the person you want to invite', + 'users_add_confirmation' => 'I confirm that I want to invite this user to my account. I understand that this person will have access to ALL of my data and see exactly what I see.', + 'users_add_cta' => 'Invite user by email', + 'users_accept_title' => 'Accept invitation and create a new account', + 'users_error_please_confirm' => 'Please confirm that you want to invite this user before proceeding with the invitation', + 'users_error_email_already_taken' => 'This email is already taken. Please choose another one', + 'users_error_already_invited' => 'You already have invited this user. Please choose another email address.', + 'users_error_email_not_similar' => 'This is not the email of the person who’ve invited you.', + 'users_invitation_deleted_confirmation_message' => 'The invitation has been successfully deleted', + 'users_invitations_delete_confirmation' => 'Are you sure you want to delete this invitation?', + 'users_list_delete_confirmation' => 'Are you sure to delete this user from your account?', + 'users_invitation_need_subscription' => 'Adding more users requires a subscription.', + + 'subscriptions_account_current_plan' => 'Your current plan', + 'subscriptions_account_current_legacy' => 'Current plan, not selectable anymore:', + 'subscriptions_account_current_paid_plan' => 'You are on the :name plan. Thanks so much for being a subscriber.', + + 'subscriptions_account_next_billing_title' => 'Next bill', + 'subscriptions_account_next_billing' => 'Your subscription will auto-renew on :date.', + 'subscriptions_account_bill_monthly' => 'We’ll bill you :price for another month.', + 'subscriptions_account_bill_annual' => 'We’ll bill you :price for another year.', + 'subscriptions_account_change' => 'Change plan', + + 'subscriptions_account_cancel_title' => 'Cancel subscription', + 'subscriptions_account_cancel_action' => 'Cancel subscription', + 'subscriptions_account_cancel' => 'You can cancel your subscription at any time.', + 'subscriptions_account_free_plan' => 'You are on the free plan.', + 'subscriptions_account_free_plan_upgrade' => 'You can upgrade your account to the :name plan, which costs $:price per month. Here are the advantages:', + 'subscriptions_account_free_plan_benefits_users' => 'Unlimited number of users', + 'subscriptions_account_free_plan_benefits_reminders' => 'Reminders by email', + 'subscriptions_account_free_plan_benefits_import_data_vcard' => 'Import your contacts with vCard', + 'subscriptions_account_free_plan_benefits_support' => 'Support the project in the long run, so we can introduce more great features.', + 'subscriptions_account_upgrade' => 'Upgrade your account', + 'subscriptions_account_upgrade_title' => 'Upgrade Monica today and have more meaningful relationships.', + 'subscriptions_account_upgrade_choice' => 'Pick a plan below and join over :customers persons who upgraded their Monica.', + 'subscriptions_account_update_title' => 'Update Monica subscription', + 'subscriptions_account_update_description' => 'You can change your subscription’s frequency here.', + 'subscriptions_account_update_information' => 'You will be billed immediately for the new amount. Your subscription will extend to the new period, depending on your choice.', + 'subscriptions_account_invoices' => 'Invoices', + 'subscriptions_account_invoices_download' => 'Download', + 'subscriptions_account_invoices_subscription' => 'Subscription from :startDate to :endDate', + 'subscriptions_account_payment' => 'Which payment option fits you best?', + 'subscriptions_account_confirm_payment' => 'Your payment is currently incomplete, please confirm your payment.', + 'subscriptions_downgrade_title' => 'Downgrade your account to the free plan', + 'subscriptions_downgrade_limitations' => 'The free plan has limitations. In order to be able to downgrade, you need to pass the checklist below:', + 'subscriptions_downgrade_rule_users' => 'You must have only 1 user in your account', + 'subscriptions_downgrade_rule_users_constraint' => 'You currently have 1 user in your account.|You currently have :count users in your account.', + 'subscriptions_downgrade_rule_invitations' => 'You must not have any pending invitations', + 'subscriptions_downgrade_rule_invitations_constraint' => 'You currently have 1 pending invitation.|You currently have :count pending invitations.', + 'subscriptions_downgrade_rule_contacts' => 'You must not have more than :number active contacts', + 'subscriptions_downgrade_rule_contacts_constraint' => 'You currently have 1 contact.|You currently have :count contacts.', + 'subscriptions_downgrade_rule_contacts_archive' => 'We can also archive all your contacts for you – that would clear this rule and let you proceed with your account’s downgrade process.', + 'subscriptions_downgrade_cta' => 'Downgrade', + 'subscriptions_downgrade_success' => 'You are back to the Free plan!', + 'subscriptions_downgrade_thanks' => 'Thanks so much for trying the paid plan. We keep adding new features on Monica all the time – so you might want to come back in the future to see if you might be interested in taking a subscription again.', + 'subscriptions_back' => 'Back to settings', + 'subscriptions_upgrade_title' => 'Upgrade your account', + 'subscriptions_upgrade_choose' => 'You picked the :plan plan.', + 'subscriptions_upgrade_infos' => 'We couldn’t be happier. Enter your payment info below.', + 'subscriptions_upgrade_name' => 'Name on card', + 'subscriptions_upgrade_zip' => 'ZIP or postal code', + 'subscriptions_upgrade_credit' => 'Credit or debit card', + 'subscriptions_upgrade_submit' => 'Pay {amount}', + 'subscriptions_upgrade_charge' => 'We’ll charge your card :price now. The next charge will be on :date. If you ever change your mind, you can cancel at any time, no questions asked.', + 'subscriptions_upgrade_charge_handled' => 'The payment is handled by Stripe. No card information touches our server.', + 'subscriptions_upgrade_success' => 'Thank you! You are now subscribed.', + 'subscriptions_upgrade_thanks' => 'Welcome to the community of people who try to make the world a better place.', + + 'subscriptions_payment_confirm_title' => 'Confirm your :amount payment', + 'subscriptions_payment_confirm_information' => 'Extra confirmation is needed to process your payment. Please confirm your payment by filling out your payment details below.', + 'subscriptions_payment_succeeded_title' => 'Payment Successful', + 'subscriptions_payment_succeeded' => 'This payment was already successfully confirmed.', + 'subscriptions_payment_cancelled_title' => 'Payment Cancelled', + 'subscriptions_payment_cancelled' => 'This payment was cancelled.', + 'subscriptions_payment_error_name' => 'Please provide your name.', + 'subscriptions_payment_success' => 'The payment was successful.', + + 'subscriptions_pdf_title' => 'Your :name monthly subscription', + 'subscriptions_plan_frequency_year' => ':amount / year', + 'subscriptions_plan_frequency_month' => ':amount / month', + 'subscriptions_plan_choose' => 'Choose this plan', + 'subscriptions_plan_year_title' => 'Pay annually', + 'subscriptions_plan_year_bonus' => 'Peace of mind for a whole year', + 'subscriptions_plan_month_title' => 'Pay monthly', + 'subscriptions_plan_month_bonus' => 'Cancel any time', + 'subscriptions_plan_include1' => 'Included with your upgrade:', + 'subscriptions_plan_include2' => 'Unlimited number of contacts • Unlimited number of users • Reminders by email • Import with vCard • Personalization of the contact sheet', + 'subscriptions_plan_include3' => '100% of the profits go the development of this great open source project.', + 'subscriptions_help_title' => 'Additional details you may be curious about', + 'subscriptions_help_opensource_title' => 'What is an open source project?', + 'subscriptions_help_opensource_desc' => 'Monica is an open source project. This means it is built by a community who wants to build a great tool for the greater good. Being open source means the code is publicly available on GitHub, and everyone can inspect it, modify it or enhance it. All the money we raise is dedicated to building better features, paying for more powerful servers, and paying other costs. Thanks for your help. We couldn’t do it without you.', + 'subscriptions_help_limits_title' => 'Is there a limit to the number of contacts we can have on the free plan?', + 'subscriptions_help_limits_plan' => 'Yes. Free plans let you manage :number contacts.', + 'subscriptions_help_discounts_title' => 'Do you have discounts for non-profits and education?', + 'subscriptions_help_discounts_desc' => 'We do! Monica is free for students, and free for non-profits and charities. Just contact the support with a proof of your status and we’ll apply this special status in your account.', + 'subscriptions_help_change_title' => 'What if I change my mind?', + 'subscriptions_help_change_desc' => 'You can cancel anytime, no questions asked, and all by yourself – no need to contact support. However, you will not be refunded for the current period.', + + 'stripe_error_card' => 'Your card was declined. Decline message is: :message', + 'stripe_error_api_connection' => 'Network communication with Stripe failed. Try again later.', + 'stripe_error_rate_limit' => 'Too many requests with Stripe right now. Try again later.', + 'stripe_error_invalid_request' => 'Invalid parameters. Try again later.', + 'stripe_error_authentication' => 'Wrong authentication with Stripe', + + 'import_title' => 'Import contacts in your account', + 'import_cta' => 'Upload contacts', + 'import_stat' => 'You’ve imported :number files so far.', + 'import_result_stat' => 'Uploaded vCard with 1 contact (:total_imported imported, :total_skipped skipped)|Uploaded vCard with :total_contacts contacts (:total_imported imported, :total_skipped skipped)', + 'import_view_report' => 'View report', + 'import_in_progress' => 'The import is in progress. Reload the page in one minute.', + 'import_upload_title' => 'Import your contacts from a vCard file', + 'import_upload_rules_desc' => 'We do however have some rules:', + 'import_upload_rule_format' => 'We support .vcard and .vcf files.', + 'import_upload_rule_vcard' => 'We support the vCard 3.0 format, which is the default format for macOS’s Contacts.app and Google Contacts.', + 'import_upload_rule_instructions' => 'Export instructions for macOS Contacts.app and Google Contacts.', + 'import_upload_rule_multiple' => 'If your contacts have multiple email addresses or phone numbers, only the first entry will be saved.', + 'import_upload_rule_limit' => 'Files are limited to 10 MB.', + 'import_upload_rule_time' => 'It might take up to a minute to upload the contacts and process them. Please be patient.', + 'import_upload_rule_cant_revert' => 'Please make sure data is accurate before uploading, as you can’t undo the upload.', + 'import_upload_form_file' => 'Your .vcf or .vCard file:', + 'import_upload_behaviour' => 'Import behaviour:', + 'import_upload_behaviour_add' => 'Add new contacts and skip existing', + 'import_upload_behaviour_replace' => 'Replace existing contacts', + 'import_upload_behaviour_help' => 'Replacing will replace all data found in the vCard, but will keep existing contact fields.', + 'import_report_title' => 'Importing report', + 'import_report_date' => 'Date of the import', + 'import_report_type' => 'Type of import', + 'import_report_number_contacts' => 'Number of contacts in the file', + 'import_report_number_contacts_imported' => 'Number of imported contacts', + 'import_report_number_contacts_skipped' => 'Number of skipped contacts', + 'import_report_status_imported' => 'Imported', + 'import_report_status_skipped' => 'Skipped', + 'import_vcard_parse_error' => 'Error when parsing the vCard entry', + 'import_vcard_contact_exist' => 'Contact already exists', + 'import_vcard_contact_no_firstname' => 'No first name (mandatory)', + 'import_vcard_file_not_found' => 'File not found', + 'import_vcard_unknown_entry' => 'Unknown contact name', + 'import_vcard_file_no_entries' => 'File contains no entries', + 'import_blank_title' => 'You haven’t imported any contacts yet.', + 'import_blank_question' => 'Would you like to import contacts now?', + 'import_blank_description' => 'We can import vCard files that you can get from Google Contacts or your Contact manager.', + 'import_blank_cta' => 'Import vCard', + 'import_need_subscription' => 'Importing data requires a subscription.', + + 'tags_list_title' => 'Tags', + 'tags_list_description' => 'You can organize your contacts by setting up tags. Tags work like folders, but you can add more than one tag to a contact. To add a new tag, add it on the contact itself.', + 'tags_list_contact_number' => '1 contact|:count contacts', + 'tags_list_delete_success' => 'The tag has been successfully deleted', + 'tags_list_edit_success' => 'The tag has been successfully updated', + 'tags_list_delete_confirmation' => 'Are you sure you want to delete the tag? No contacts will be deleted, only the tag.', + 'tags_blank_title' => 'Tags are a great way of categorizing your contacts.', + 'tags_blank_description' => 'Tags work like folders, but you can add more than one tag to a contact. Go to a contact and tag a friend, right below the name. Once a contact is tagged, come back here to manage all the tags in your account.', + + 'api_title' => 'API access', + 'api_description' => 'The API can be used to manipulate Monica’s data from an external application, like a mobile application for instance.', + 'api_help' => 'To use the API, a token is mandatory. You can either create a personal access token (Bearer authentication), or authorize an OAuth client to create it for you. See API documentation.', + 'api_endpoint' => 'The API endpoint for this Monica instance is:', + + 'api_personal_access_tokens' => 'Personal access tokens', + 'api_pao_description' => 'Make sure you give this token to a source you trust – as they allow you to access all your data.', + 'api_token_title' => 'Personal Access Tokens', + 'api_token_create_new' => 'Create New Token', + 'api_token_not_created' => 'You have not created any personal access tokens.', + 'api_token_name' => 'Token name', + 'api_token_expire' => 'Expires at {date}', + 'api_token_delete' => 'Delete', + 'api_token_create' => 'Create Token', + 'api_token_scopes' => 'Scopes', + 'api_token_help' => 'Here is your new personal access token. This is the only time it will be shown so don’t lose it! You may now use this token to make API requests.', + + 'api_oauth_clients' => 'Your OAuth clients', + 'api_oauth_clients_desc' => 'This section lets you register your own OAuth clients.', + 'api_oauth_clients_desc2' => 'Use this client id to request a new token, and convert authorization codes to access tokens. See Laravel Passport documentation for more information.', + 'api_oauth_title' => 'OAuth Clients', + 'api_oauth_create_new' => 'Create New Client', + 'api_oauth_edit' => 'Edit Client', + 'api_oauth_not_created' => 'You have not created any OAuth clients.', + 'api_oauth_clientid' => 'Client ID', + 'api_oauth_name' => 'Name', + 'api_oauth_name_help' => 'Something your users will recognize and trust.', + 'api_oauth_secret' => 'Secret', + 'api_oauth_create' => 'Create Client', + 'api_oauth_redirecturl' => 'Redirect URL', + 'api_oauth_redirecturl_help' => 'Your application’s authorization callback URL.', + + 'api_authorized_clients' => 'List of authorized clients', + 'api_authorized_clients_desc' => 'This section lists all the clients you’ve authorized to access your application data. You can revoke this authorization at anytime.', + 'api_authorized_clients_title' => 'Authorized Applications', + 'api_authorized_clients_none' => 'There are no authorized clients yet.', + 'api_authorized_clients_name' => 'Name', + 'api_authorized_clients_scopes' => 'Scopes', + + 'personalization_tab_title' => 'Personalize your account', + + 'personalization_title' => 'Here you will find different settings to configure your account. These features are intended for “power users” who want maximum control over Monica.', + 'personalization_contact_field_type_title' => 'Contact field types', + 'personalization_contact_field_type_add' => 'Add new field type', + 'personalization_contact_field_type_description' => 'You can configure all the different types of contact fields that you can associate to all your contacts. For example, if a new social network appears in the future, you will be able to add this new way of communicating with your contacts right here.', + 'personalization_contact_field_type_table_name' => 'Name', + 'personalization_contact_field_type_table_protocol' => 'Protocol', + 'personalization_contact_field_type_table_actions' => 'Actions', + 'personalization_contact_field_type_modal_title' => 'Add a new contact field type', + 'personalization_contact_field_type_modal_edit_title' => 'Edit an existing contact field type', + 'personalization_contact_field_type_modal_delete_title' => 'Delete an existing contact field type', + 'personalization_contact_field_type_modal_delete_description' => 'Are you sure you want to delete this contact field type? Deleting this type of contact field will delete ALL the data with this type for all of your contacts.', + 'personalization_contact_field_type_modal_name' => 'Name', + 'personalization_contact_field_type_modal_protocol' => 'Protocol (optional)', + 'personalization_contact_field_type_modal_protocol_help' => 'Each new contact field type can be clickable. If a protocol is set, we will use it to trigger the action that is set.', + 'personalization_contact_field_type_modal_icon' => 'Icon (optional)', + 'personalization_contact_field_type_modal_icon_help' => 'You can associate an icon with this contact field type. You need to add a reference to a Font Awesome icon.', + 'personalization_contact_field_type_delete_success' => 'The contact field type has been successfully deleted.', + 'personalization_contact_field_type_add_success' => 'The contact field type has been successfully added.', + 'personalization_contact_field_type_edit_success' => 'The contact field type has been successfully updated.', + + 'personalization_genders_title' => 'Gender types', + 'personalization_genders_add' => 'Add new gender type', + 'personalization_genders_desc' => 'You can define as many genders as you need to. You need at least one gender type in your account.', + 'personalization_genders_modal_add' => 'Add gender type', + 'personalization_genders_modal_edit' => 'Update gender type', + 'personalization_genders_modal_name' => 'Name', + 'personalization_genders_modal_name_help' => 'The name used to display the gender on a contact page.', + 'personalization_genders_modal_sex' => 'Sex', + 'personalization_genders_modal_sex_help' => 'Used to define the relationships, and during the VCard import/export process.', + 'personalization_genders_modal_default' => 'Select the default gender for a new contact', + 'personalization_genders_modal_delete' => 'Delete gender type', + 'personalization_genders_modal_delete_desc' => 'Are you sure you want to delete the gender “{name}”?', + 'personalization_genders_modal_delete_question' => 'You currently have {count} contact with this gender. If you delete this gender, what gender should this contact have?|You currently have {count} contacts with this gender. If you delete this gender, what gender should these contacts have?', + 'personalization_genders_modal_delete_question_default' => 'This gender is the default one. If you delete this gender, which one will be the new default?', + 'personalization_genders_modal_error' => 'Please choose a gender from the list.', + 'personalization_genders_list_contact_number' => '{count} contact|{count} contacts', + 'personalization_genders_table_name' => 'Name', + 'personalization_genders_table_sex' => 'Sex', + 'personalization_genders_table_default' => 'Default', + 'personalization_genders_default' => 'Default gender', + 'personalization_genders_make_default' => 'Change default gender', + 'personalization_genders_select_default' => 'Select default gender', + 'personalization_genders_m' => 'Male', + 'personalization_genders_f' => 'Female', + 'personalization_genders_o' => 'Other', + 'personalization_genders_u' => 'Unknown', + 'personalization_genders_n' => 'None or not applicable', + + 'personalization_reminder_rule_save' => 'The change has been saved', + 'personalization_reminder_rule_title' => 'Reminder rules', + 'personalization_reminder_rule_line' => '{count} day before|{count} days before', + 'personalization_reminder_rule_desc' => 'For every reminder that you set, Monica can send you an email a number of days before the event happens. You can adjust these notification settings here. These notifications only apply to monthly and yearly reminders.', + + 'personalization_module_save' => 'The change has been saved', + 'personalization_module_title' => 'Features', + 'personalization_module_desc' => 'You may not need all of Monica’s features. Below you can toggle specific features that are used on a contact sheet. This change will affect ALL your contacts. Turning off a feature does not delete any data, it simply hides the feature.', + + 'personalisation_paid_upgrade' => 'This is a premium feature that requires a Paid subscription to be active. Upgrade your account by visiting Settings > Subscription.', + 'personalisation_paid_upgrade_vue' => 'This is a premium feature that requires a Paid subscription to be active. Upgrade your account by visiting Settings > Subscription.', + + 'reminder_time_to_send' => 'Time of the day reminders will be sent', + 'reminder_time_to_send_help' => 'Your next reminder is scheduled to be sent on {dateTime}.', + + 'personalization_activity_type_category_title' => 'Activity type categories', + 'personalization_activity_type_category_add' => 'Add a new activity type category', + 'personalization_activity_type_category_table_name' => 'Name', + 'personalization_activity_type_category_description' => 'An activity with one of your contacts can have a type and a category type. Your account comes with a set of predefined category types by default, but you can customize these here.', + 'personalization_activity_type_category_table_actions' => 'Actions', + 'personalization_activity_type_category_modal_add' => 'Add a new activity type category', + 'personalization_activity_type_category_modal_edit' => 'Edit an activity type category', + 'personalization_activity_type_category_modal_question' => 'What should we name this new category?', + 'personalization_activity_type_add_button' => 'Add a new activity type', + 'personalization_activity_type_modal_add' => 'Add a new activity type', + 'personalization_activity_type_modal_question' => 'What should we name this new activity type?', + 'personalization_activity_type_modal_edit' => 'Edit an activity type', + 'personalization_activity_type_category_modal_delete' => 'Delete an activity type category', + 'personalization_activity_type_category_modal_delete_desc' => 'Are you sure you want to delete this category? Deleting it will delete all associated activity types. Activities that belong to this category will not be affected by this deletion.', + 'personalization_activity_type_modal_delete' => 'Delete an activity type', + 'personalization_activity_type_modal_delete_desc' => 'Are you sure you want to delete this activity type? Activities that belong to this category will not be affected by this deletion.', + 'personalization_activity_type_modal_delete_error' => 'We can’t find this activity type.', + 'personalization_activity_type_category_modal_delete_error' => 'We can’t find this activity type category.', + + 'personalization_life_event_category_title' => 'Life event categories', + 'personalization_live_event_category_table_name' => 'Name', + 'personalization_life_event_category_description' => 'A life event can have a type and a category. Your account comes with a set of predefined categories and types by default, but you can customize life event types here.', + 'personalization_live_event_category_table_actions' => 'Actions', + 'personalization_life_event_type_add_button' => 'Add a new life event type', + 'personalization_life_event_type_modal_add' => 'Add a new life event type', + 'personalization_life_event_type_modal_question' => 'What should we name this new life event type?', + 'personalization_life_event_type_modal_edit' => 'Edit a life event type', + 'personalization_life_event_type_modal_delete' => 'Delete a life event type', + 'personalization_life_event_type_modal_delete_desc' => 'Are you sure you want to delete this life event type? Life events that belong to this type will be deleted by performing this action.', + 'personalization_life_event_type_modal_delete_error' => 'We can’t find this life event type.', + + 'personalization_life_event_category_work_education' => 'Work & education', + 'personalization_life_event_category_family_relationships' => 'Family & relationships', + 'personalization_life_event_category_home_living' => 'Home & living', + 'personalization_life_event_category_travel_experiences' => 'Travel & experiences', + 'personalization_life_event_category_health_wellness' => 'Health & wellness', + + 'personalization_life_event_type_new_job' => 'New job', + 'personalization_life_event_type_retirement' => 'Retirement', + 'personalization_life_event_type_new_school' => 'New school', + 'personalization_life_event_type_study_abroad' => 'Study abroad', + 'personalization_life_event_type_volunteer_work' => 'Volunteer work', + 'personalization_life_event_type_published_book_or_paper' => 'Published a book or paper', + 'personalization_life_event_type_military_service' => 'Military service', + 'personalization_life_event_type_first_met' => 'First met', + 'personalization_life_event_type_new_relationship' => 'New relationship', + 'personalization_life_event_type_engagement' => 'Engagement', + 'personalization_life_event_type_marriage' => 'Marriage', + 'personalization_life_event_type_anniversary' => 'Anniversary', + 'personalization_life_event_type_expecting_a_baby' => 'Expecting a baby', + 'personalization_life_event_type_new_child' => 'New child', + 'personalization_life_event_type_new_family_member' => 'New family member', + 'personalization_life_event_type_new_pet' => 'New pet', + 'personalization_life_event_type_end_of_relationship' => 'End of relationship', + 'personalization_life_event_type_loss_of_a_loved_one' => 'Loss of a loved one', + 'personalization_life_event_type_moved' => 'Moved', + 'personalization_life_event_type_bought_a_home' => 'Bought a home', + 'personalization_life_event_type_home_improvement' => 'Home improvement', + 'personalization_life_event_type_holidays' => 'Holidays', + 'personalization_life_event_type_new_vehicle' => 'New vehicle', + 'personalization_life_event_type_new_roommate' => 'New roommate', + 'personalization_life_event_type_overcame_an_illness' => 'Overcame an illness', + 'personalization_life_event_type_quit_a_habit' => 'Quit a habit', + 'personalization_life_event_type_new_eating_habits' => 'New eating habits', + 'personalization_life_event_type_weight_loss' => 'Weight loss', + 'personalization_life_event_type_wear_glass_or_contact' => 'Started wearing glasses or contacts', + 'personalization_life_event_type_broken_bone' => 'Broke a bone', + 'personalization_life_event_type_removed_braces' => 'Had braces removed', + 'personalization_life_event_type_surgery' => 'Had surgery', + 'personalization_life_event_type_dentist' => 'Had dental treatment', + 'personalization_life_event_type_new_sport' => 'Started playing a new sport', + 'personalization_life_event_type_new_hobby' => 'Took up a new hobby', + 'personalization_life_event_type_new_instrument' => 'Started learning a new instrument', + 'personalization_life_event_type_new_language' => 'Started learning a new language', + 'personalization_life_event_type_tattoo_or_piercing' => 'Tattoo or piercing', + 'personalization_life_event_type_new_license' => 'New license', + 'personalization_life_event_type_travel' => 'Travel', + 'personalization_life_event_type_achievement_or_award' => 'Achievement or award', + 'personalization_life_event_type_changed_beliefs' => 'Changed beliefs', + 'personalization_life_event_type_first_word' => 'First word', + 'personalization_life_event_type_first_kiss' => 'First kiss', + + 'storage_title' => 'Storage', + 'storage_account_info' => 'Your account limit is :accountLimit MB. Your current usage is :currentAccountSize MB (about :percentUsage%).', + 'storage_upgrade_notice' => 'Upgrade your account to be able to upload documents and photos.', + 'storage_description' => 'Here you can see all the documents and photos uploaded about your contacts.', + + 'dav_title' => 'WebDAV', + 'dav_description' => 'Here you can find all settings to use WebDAV resources for CardDAV and CalDAV exports.', + 'dav_copy_help' => 'Copy into your clipboard', + 'dav_clipboard_copied' => 'Value copied into your clipboard', + 'dav_url_base' => 'Base url for all CardDAV and CalDAV resources:', + 'dav_connect_help' => 'You can connect your contacts and/or calendars with this base url on you phone or computer.', + 'dav_connect_help2' => 'Use your login (email) and create an API token as the password to authenticate.', + 'dav_url_carddav' => 'CardDAV url for Contacts resource:', + 'dav_url_caldav_birthdays' => 'CalDAV url for Birthdays resources:', + 'dav_url_caldav_tasks' => 'CalDAV url for Tasks resources:', + 'dav_title_carddav' => 'CardDAV', + 'dav_title_caldav' => 'CalDAV', + 'dav_carddav_export' => 'Export all contacts in one file', + 'dav_caldav_birthdays_export' => 'Export all birthdays in one file', + 'dav_caldav_tasks_export' => 'Export all tasks in one file', + + 'archive_title' => 'Archive all of the contacts in your account', + 'archive_desc' => 'This will archive all of the contacts in your account.', + 'archive_cta' => 'Archive all of your contacts', + + 'logs_title' => 'Everything that has happened to this account', + 'logs_actor' => 'Actor', + 'logs_timestamp' => 'Timestamp', + 'logs_description' => 'Description', + 'logs_subject' => 'Subject', + 'logs_size' => 'Size (Kb)', + 'logs_object' => 'Object', +]; diff --git a/resources/lang/fa/validation.php b/resources/lang/fa/validation.php new file mode 100644 index 0000000..4e2b462 --- /dev/null +++ b/resources/lang/fa/validation.php @@ -0,0 +1,166 @@ + ':attribute باید پذیرفته شده باشد.', + 'active_url' => 'ویژگی :attribute معتبری نیست.', + 'after' => 'ویژگی باید در تاریخی بعد از تاریخ باشد.', + 'after_or_equal' => 'attribute باید یک تاریخ بعد باشد یا برابر باشد: date.', + 'alpha' => ':attribute باید فقط حروف الفبا باشد.', + 'alpha_dash' => ':attribute باید فقط حروف الفبا، اعداد، خط تیره و زیرخط باشد.', + 'alpha_num' => ':attribute باید فقط حروف الفبا و اعداد باشد.', + 'array' => ':attribute باید آرایه باشد.', + 'before' => 'ویژگی باید در تاریخی قبل از تاریخ باشد.', + 'before_or_equal' => 'attribute باید تاریخ قبل یا برابر باشد: date.', + 'between' => [ + 'numeric' => 'مقدار :attribute باید بین :min و :max باشد.', + 'file' => ':attribute باید بین :min و :max کیلوبایت باشد.', + 'string' => 'ویژگی باید بین حداقل حداکثر کاراکتر باشد.', + 'array' => ':attribute باید بین :min و :max آیتم باشد.', + ], + 'boolean' => 'مقدار :attribute باید true یا false باشد.', + 'confirmed' => ':attribute با فیلد تکرار مطابقت ندارد.', + 'date' => ':attribute یک تاریخ معتبر نیست.', + 'date_equals' => ':attribute باید برابر با تاریخ :date باشد.', + 'date_format' => ':attribute با الگوی :format مطابقت ندارد.', + 'different' => ':attribute و :other باید از یکدیگر متفاوت باشند.', + 'digits' => ':attribute باید :digits رقم باشد.', + 'digits_between' => 'تعداد ارقام :attribute باید بین :min و :max رقم باشد.', + 'dimensions' => 'attribute: ابعاد تصویر نامعتبر است.', + 'distinct' => 'فیلد attribute دارای مقدار تکراری است.', + 'email' => 'مقدار :attribute باید یک آدرس ایمیل معتبر باشد.', + 'ends_with' => 'فیلد :attribute باید با یکی از مقادیر زیر خاتمه یابد: :values', + 'exists' => ':attribute انتخاب شده نامعتبر است.', + 'file' => ':attribute باید یک عدد باشد.', + 'filled' => 'فیلد :attribute باید مقدار داشته باشد.', + 'gt' => [ + 'numeric' => ':attribute باید بزرگتر از :value باشد.', + 'file' => ':attribute باید بزرگتر از :value کیلوبایت باشد.', + 'string' => ':attribute باید بیشتر از :value کاراکتر داشته باشد.', + 'array' => ':attribute باید بیشتر از :value آیتم داشته باشد.', + ], + 'gte' => [ + 'numeric' => ':attribute باید بزرگتر یا مساوی :value باشد.', + 'file' => ':attribute باید بزرگتر یا مساوی :value کیلوبایت باشد.', + 'string' => ':attribute باید بیشتر یا مساوی :value کاراکتر داشته باشد.', + 'array' => ':attribute باید بیشتر یا مساوی :value آیتم داشته باشد.', + ], + 'image' => ':attribute باید یک تصویر معتبر باشد.', + 'in' => ':attribute انتخاب شده نامعتبر است.', + 'in_array' => 'مقدار :attribute در :other وجود ندارد.', + 'integer' => 'مقدار :attribute باید یک عدد باشد.', + 'ip' => 'مقدار :attribute باید یک آدرس IP معتبر باشد.', + 'ipv4' => ':attribute باید یک آدرس معتبر از نوع IPv4 باشد.', + 'ipv6' => ':attribute باید یک آدرس معتبر از نوع IPv6 باشد.', + 'json' => 'فیلد :attribute باید یک رشته از نوع JSON باشد.', + 'lt' => [ + 'numeric' => ':attribute باید کوچکتر از :value باشد.', + 'file' => ':attribute باید کوچکتر از :value کیلوبایت باشد.', + 'string' => ':attribute باید کمتر از :value کاراکتر داشته باشد.', + 'array' => ':attribute باید کمتر از :value آیتم داشته باشد.', + ], + 'lte' => [ + 'numeric' => ':attribute باید کوچکتر یا مساوی :value باشد.', + 'file' => ':attribute باید کوچکتر یا مساوی :value کیلوبایت باشد.', + 'string' => ':attribute باید کمتر یا مساوی :value کاراکتر داشته باشد.', + 'array' => ':attribute باید کمتر یا مساوی :value آیتم داشته باشد.', + ], + 'max' => [ + 'numeric' => ':attribute نباید بزرگتر از :max باشد.', + 'file' => ':attribute نباید بزرگتر از :max کیلوبایت باشد.', + 'string' => ':attribute نباید بیشتر از :max کاراکتر داشته باشد.', + 'array' => 'ویژگی: ممکن است بیش از موارد حداکثر داشته باشد.', + ], + 'mimes' => ':attribute باید یک فایل از نوع :values باشد.', + 'mimetypes' => ':attribute باید یک فایل از نوع :values باشد.', + 'min' => [ + 'numeric' => ':attribute نباید کوچکتر از :min باشد.', + 'file' => 'حجم :attribute باید حداقل :min کیلوبایت باشد.', + 'string' => ':attribute حداقل باید دارای :min کاراکتر باشد.', + 'array' => ':attribute باید حداقل دارای :min آیتم باشد.', + ], + 'not_in' => ':attribute انتخاب شده نامعتبر است.', + 'not_regex' => 'فرمت :attribute نامعتبر می‌باشد.', + 'numeric' => ':attribute باید یک عدد باشد.', + 'password' => 'کلمه عبور صحیح نیست.', + 'present' => ':attribute باید وجود داشته باشد.', + 'regex' => 'فرمت :attribute نامعتبر می‌باشد.', + 'required' => 'فیلد :attribute باید مقدار داشته باشد.', + 'required_if' => 'فیلد :attribute اجباری است تا زمانی که :other در :values باشد.', + 'required_unless' => 'فیلد :attribute اجباری است تا زمانی که :other در :values باشد.', + 'required_with' => 'فیلد :attribute اجباری است تا زمانی که :values وجود داشته باشد.', + 'required_with_all' => 'فیلد :attribute اجباری است تا زمانی که :values وجود داشته باشد.', + 'required_without' => 'فیلد :attribute اجباری است تا زمانی که :values وجود نداشته باشد.', + 'required_without_all' => 'در صورت عدم وجود هر یک از فیلدهای :values، فیلد :attribute الزامی است.', + 'same' => ':attribute و :other باید همانند هم باشند.', + 'size' => [ + 'numeric' => ':attribute باید برابر با :size باشد.', + 'file' => 'حجم :attribute باید به اندازه :size کیلوبایت باشد.', + 'string' => ':attribute باید برابر با :size کاراکتر باشد.', + 'array' => ':attribute باید شامل :size آیتم باشد.', + ], + 'starts_with' => 'فیلد :attribute باید با یکی از مقادیر زیر شروع شود: :values', + 'string' => 'فیلد :attribute باید متن باشد.', + 'timezone' => 'فیلد :attribute باید یک منطقه زمانی معتبر باشد.', + 'unique' => ':attribute قبلا انتخاب شده است.', + 'uploaded' => ':attribute آپلود نشد.', + 'url' => 'فرمت :attribute نامعتبر می‌باشد.', + 'uuid' => 'فیلد :attribute باید یک UUID معتبر باشد.', + + /* + |-------------------------------------------------------------------------- + | Custom Validation Language Lines + |-------------------------------------------------------------------------- + | + | Here you may specify custom validation messages for attributes using the + | convention "attribute.rule" to name the lines. This makes it quick to + | specify a specific custom language line for a given attribute rule. + | + */ + + 'custom' => [ + 'attribute-name' => [ + 'rule-name' => 'custom-message', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Custom Validation Attributes + |-------------------------------------------------------------------------- + | + | The following language lines are used to swap our attribute placeholder + | with something more reader friendly such as "E-Mail Address" instead + | of "email". This simply helps us make our message more expressive. + | + */ + + 'attributes' => [], + + 'vue' => [ + 'max' => [ + 'numeric' => '{field} نباید بزرگتر از {max} باشد.', + 'string' => '{field} نباید بزرگتر از {max} کارکتر باشد.', + ], + 'required' => '{field} الزامیست.', + 'url' => '{field} یک URL معتبر نیست.', + ], + +]; diff --git a/resources/lang/fi.json b/resources/lang/fi.json new file mode 100644 index 0000000..10414cb --- /dev/null +++ b/resources/lang/fi.json @@ -0,0 +1,7 @@ +{ + "The :attribute must contain at least one uppercase and one lowercase letter.": ":attribute n täytyy sisältää vähintään yksi iso kirjain ja yksi pieni kirjain.", + "The :attribute must contain at least one letter.": ":attribute n täytyy sisältää ainakin yksi kirjain.", + "The :attribute must contain at least one symbol.": ":attribute n täytyy sisältää ainakin yksi symboli.", + "The :attribute must contain at least one number.": ":attribute n täytyy sisältää ainakin yksi numero.", + "The given :attribute has appeared in a data leak. Please choose a different :attribute.": ":attribute on esiintynyt tietovuodossa. Ole hyvä ja valitse toinen :attribute." +} diff --git a/resources/lang/fi/app.php b/resources/lang/fi/app.php new file mode 100644 index 0000000..6339522 --- /dev/null +++ b/resources/lang/fi/app.php @@ -0,0 +1,571 @@ + 'Kyllä', + 'no' => 'Ei', + 'update' => 'Päivitä', + 'save' => 'Tallenna', + 'add' => 'Lisää', + 'cancel' => 'Peruuta', + 'confirm' => 'Vahvista', + 'delete_confirm' => 'Oletko varma?', + 'delete' => 'Poista', + 'edit' => 'Muokkaa', + 'upload' => 'Lähetä', + 'download' => 'Lataa', + 'save_close' => 'Tallenna ja sulje', + 'close' => 'Sulje', + 'copy' => 'Kopioi', + 'create' => 'Luo', + 'remove' => 'Poista', + 'revoke' => 'Kumoa', + 'done' => 'Valmis', + 'back' => 'Takaisin', + 'verify' => 'Vahvista', + 'new' => 'uusi', + 'unknown' => 'Minä en tiedä', + 'load_more' => 'Lataa lisää', + 'loading' => 'Ladataan…', + 'with' => 'with', + 'today' => 'tänään', + 'yesterday' => 'eilen', + 'another_day' => 'toinen päivä', + 'date' => 'Päivämäärä', + 'type' => 'Tyyppi', + 'zoom' => 'Zoomaus', + 'upgrade' => 'Päivitä avataksesi', + 'percent_uploaded' => '{percent}% lähetetty', + 'retry' => 'Yritä uudelleen', + 'filter' => 'Suodata lista', + 'go_back' => 'Mene takaisin', + 'file_selected' => 'Yksi tiedosto valittuna…{count} tiedostoa valittu…', + + 'application_title' => 'Monica – henkilökohtainen suhdepäällikkö', + 'application_description' => 'Monica on työkalu hallita vuorovaikutustasi rakkaiden, ystävien ja perheen kanssa.', + 'application_og_title' => 'Have better relations with your loved ones. Free online CRM for friends and family.', + + 'markdown_description' => 'Haluatko muotoilla tekstiäsi hienosti? Tuemme Markdownia lisätäksemme lihavointia, listoja ja paljon muuta.', + 'markdown_link' => 'Lue dokumentaatio', + + 'header_settings_link' => 'Asetukset', + 'header_logout_link' => 'Kirjaudu ulos', + 'header_changelog_link' => 'Tuotteiden muutokset', + + 'main_nav_cta' => 'Lisää ihmisiä', + 'main_nav_dashboard' => 'Hallintapaneeli', + 'main_nav_family' => 'Yhteystiedot', + 'main_nav_journal' => 'Päiväkirja', + 'main_nav_activities' => 'Activities', + 'main_nav_tasks' => 'Tasks', + + 'footer_remarks' => 'Comments?', + 'footer_send_email' => 'Send us an email', + 'footer_privacy' => 'Privacy policy', + 'footer_release' => 'Release notes', + 'footer_newsletter' => 'Newsletter', + 'footer_source_code' => 'Contribute', + 'footer_version' => 'Version: :version', + 'footer_new_version' => 'A new version of Monica is available', + + 'footer_modal_version_whats_new' => 'What’s new', + 'footer_modal_version_release_away' => 'You are 1 release behind the latest version available. You should update your instance.|You are :number releases behind the latest version available. You should update your instance.', + + 'breadcrumb_dashboard' => 'Dashboard', + 'breadcrumb_list_contacts' => 'List of people', + 'breadcrumb_archived_contacts' => 'Archived contacts', + 'breadcrumb_journal' => 'Journal', + 'breadcrumb_settings' => 'Settings', + 'breadcrumb_settings_export' => 'Export', + 'breadcrumb_settings_users' => 'Users', + 'breadcrumb_settings_users_add' => 'Add a user', + 'breadcrumb_settings_subscriptions' => 'Subscription', + 'breadcrumb_settings_import' => 'Import', + 'breadcrumb_settings_import_report' => 'Import report', + 'breadcrumb_settings_import_upload' => 'Upload', + 'breadcrumb_settings_tags' => 'Tags', + 'breadcrumb_add_significant_other' => 'Add significant other', + 'breadcrumb_edit_significant_other' => 'Edit significant other', + 'breadcrumb_add_note' => 'Add a note', + 'breadcrumb_edit_note' => 'Edit a note', + 'breadcrumb_api' => 'API', + 'breadcrumb_dav' => 'DAV Resources', + 'breadcrumb_edit_introductions' => 'How did you meet', + 'breadcrumb_settings_personalization' => 'Personalization', + 'breadcrumb_settings_security' => 'Security', + 'breadcrumb_settings_security_2fa' => 'Two Factor Authentication', + 'breadcrumb_profile' => 'Profile of :name', + + 'gender_male' => 'Man', + 'gender_female' => 'Woman', + 'gender_none' => 'Rather not say', + 'gender_no_gender' => 'No gender', + + 'error_title' => 'Whoops! Something went wrong.', + 'error_unauthorized' => 'You don’t have the right to edit this resource.', + 'error_user_account' => 'This user does not belong to the given account.', + 'error_save' => 'We had an error trying to save the data.', + 'error_try_again' => 'Something went wrong. Please try again.', + 'error_id' => 'Error ID: :id', + 'error_unavailable' => 'Service unavailable', + 'error_maintenance' => 'Maintenance in progress. We’ll be right back.', + 'error_help' => 'Tulemme tuota pikaa takaisin.', + 'error_twitter' => 'Seuraa meidän Twitter-tiliämme saadaksesi tiedon kun se on toiminnassa jälleen.', + 'error_no_term' => 'There is no policy for this instance yet.', + + 'default_save_success' => 'Tiedot on tallennettu.', + + 'compliance_title' => 'Pahoittelemme keskeytystä.', + 'compliance_desc' => 'Olemme muuttaneet Käyttöehtojamme ja Yksityisyyden suojaa. Lain mukaan meidän on pyydettävä sinua tarkistamaan ne ja hyväksymään ne, jotta voit edelleen käyttää tiliäsi.', + 'compliance_desc_end' => 'Emme tee mitään ilkeää tietojesi tai tilisi kanssa emmekä koskaan tule tekemään.', + 'compliance_terms' => 'Hyväksy uudet ehdot ja yksityisyyden suoja', + + // Relationship types + // Yes, each relationship type has 8 strings associated with it. + // This is because we need to indicate the name of the relationship type, + // and also the name of the opposite side of this relationship (father/son), + // and then, the feminine version of the string. Finally, in some sentences + // in the UI, we need to include the name of the person we add the relationship + // to. + 'relationship_type_group_love' => 'Rakkaus suhteet', + 'relationship_type_group_family' => 'Perheen suhteet', + 'relationship_type_group_friend' => 'Ystävien suhteet', + 'relationship_type_group_work' => 'Työn suhteet', + 'relationship_type_group_other' => 'Muut suhteet', + + 'relationship_type_partner' => 'kumppani', + 'relationship_type_partner_female' => 'tyttöystävä / naisystävä', + 'relationship_type_partner_male' => 'kumppani', + 'relationship_type_partner_with_name' => ':name:n kumppani', + 'relationship_type_partner_female_with_name' => ':name:n naisystävä', + 'relationship_type_partner_male_with_name' => ':name:n kumppani', + + 'relationship_type_spouse' => 'puoliso', + 'relationship_type_spouse_female' => 'vaimo', + 'relationship_type_spouse_male' => 'aviomies', + 'relationship_type_spouse_with_name' => ':name puoliso', + 'relationship_type_spouse_female_with_name' => ':name vaimo', + 'relationship_type_spouse_male_with_name' => ':name aviomies', + + 'relationship_type_date' => 'päivämäärä', + 'relationship_type_date_female' => 'päivämäärä', + 'relationship_type_date_male' => 'päivämäärä', + 'relationship_type_date_with_name' => ':name päivämäärä', + 'relationship_type_date_female_with_name' => ':name päivämäärä', + 'relationship_type_date_male_with_name' => ':name päivämäärä', + + 'relationship_type_lover' => 'rakastaja', + 'relationship_type_lover_female' => 'rakastaja', + 'relationship_type_lover_male' => 'rakastaja', + 'relationship_type_lover_with_name' => ':name rakastaja', + 'relationship_type_lover_female_with_name' => ':name rakastaja', + 'relationship_type_lover_male_with_name' => ':name rakastaja', + + 'relationship_type_inlovewith' => 'rakastunut kanssa', + 'relationship_type_inlovewith_female' => 'rakastunut kanssa', + 'relationship_type_inlovewith_male' => 'rakastunut kanssa', + 'relationship_type_inlovewith_with_name' => 'joku :name on rakastunut', + 'relationship_type_inlovewith_female_with_name' => 'joku :name on rakastunut', + 'relationship_type_inlovewith_male_with_name' => 'joku :name on rakastunut', + + 'relationship_type_lovedby' => 'loved by', + 'relationship_type_lovedby_female' => 'loved by', + 'relationship_type_lovedby_male' => 'loved by', + 'relationship_type_lovedby_with_name' => ':name’s secret lover', + 'relationship_type_lovedby_female_with_name' => ':name’s secret lover', + 'relationship_type_lovedby_male_with_name' => ':name’s secret lover', + + 'relationship_type_ex' => 'ex-kumppani', + 'relationship_type_ex_female' => 'ex-girlfriend', + 'relationship_type_ex_male' => 'ex-boyfriend', + 'relationship_type_ex_with_name' => ':name’s ex-partner', + 'relationship_type_ex_female_with_name' => ':name’s ex-girlfriend', + 'relationship_type_ex_male_with_name' => ':name’s ex-boyfriend', + + 'relationship_type_parent' => 'parent', + 'relationship_type_parent_female' => 'mother', + 'relationship_type_parent_male' => 'father', + 'relationship_type_parent_with_name' => ':name’s parent', + 'relationship_type_parent_female_with_name' => ':name’s mother', + 'relationship_type_parent_male_with_name' => ':name’s father', + + 'relationship_type_child' => 'child', + 'relationship_type_child_female' => 'daughter', + 'relationship_type_child_male' => 'son', + 'relationship_type_child_with_name' => ':name’s child', + 'relationship_type_child_female_with_name' => ':name’s daughter', + 'relationship_type_child_male_with_name' => ':name’s son', + + 'relationship_type_stepparent' => 'step-parent', + 'relationship_type_stepparent_female' => 'stepmother', + 'relationship_type_stepparent_male' => 'stepfather', + 'relationship_type_stepparent_with_name' => ':name’s step-parent', + 'relationship_type_stepparent_female_with_name' => ':name’s stepmother', + 'relationship_type_stepparent_male_with_name' => ':name’s stepfather', + + 'relationship_type_stepchild' => 'stepchild', + 'relationship_type_stepchild_female' => 'stepdaughter', + 'relationship_type_stepchild_male' => 'stepson', + 'relationship_type_stepchild_with_name' => ':name’s stepchild', + 'relationship_type_stepchild_female_with_name' => ':name’s stepdaughter', + 'relationship_type_stepchild_male_with_name' => ':name’s stepson', + + 'relationship_type_sibling' => 'sibling', + 'relationship_type_sibling_female' => 'sister', + 'relationship_type_sibling_male' => 'brother', + 'relationship_type_sibling_with_name' => ':name’s sibling', + 'relationship_type_sibling_female_with_name' => ':name’s sister', + 'relationship_type_sibling_male_with_name' => ':name’s brother', + + 'relationship_type_grandparent' => 'grandparent', + 'relationship_type_grandparent_female' => 'grandmother', + 'relationship_type_grandparent_male' => 'grandfather', + 'relationship_type_grandparent_with_name' => ':name’s grandparent', + 'relationship_type_grandparent_female_with_name' => ':name’s grandmother', + 'relationship_type_grandparent_male_with_name' => ':name’s grandfather', + + 'relationship_type_grandchild' => 'grandchild', + 'relationship_type_grandchild_female' => 'granddaughter', + 'relationship_type_grandchild_male' => 'lapsenpoika', + 'relationship_type_grandchild_with_name' => ':name lapsenlapsi', + 'relationship_type_grandchild_female_with_name' => ':name tyttärentytär', + 'relationship_type_grandchild_male_with_name' => ':name pojanpoika', + + 'relationship_type_uncle' => 'setä', + 'relationship_type_uncle_female' => 'täti', + 'relationship_type_uncle_male' => 'setä', + 'relationship_type_uncle_with_name' => ':name setä', + 'relationship_type_uncle_female_with_name' => ':name täti', + 'relationship_type_uncle_male_with_name' => ':name setä', + + 'relationship_type_nephew' => 'veljenpoika', + 'relationship_type_nephew_female' => 'sisarentytär', + 'relationship_type_nephew_male' => 'veljenpoika', + 'relationship_type_nephew_with_name' => ':name veljenpoika', + 'relationship_type_nephew_female_with_name' => ':name veljentytär', + 'relationship_type_nephew_male_with_name' => ':name veljenpoika', + + 'relationship_type_cousin' => 'serkku', + 'relationship_type_cousin_female' => 'serkku', + 'relationship_type_cousin_male' => 'serkku', + 'relationship_type_cousin_with_name' => ':name serkku', + 'relationship_type_cousin_female_with_name' => ':name serkku', + 'relationship_type_cousin_male_with_name' => ':name serkku', + + 'relationship_type_godfather' => 'kummi', + 'relationship_type_godfather_female' => 'kummiäiti', + 'relationship_type_godfather_male' => 'kummisetä', + 'relationship_type_godfather_with_name' => ':name kummi', + 'relationship_type_godfather_female_with_name' => ':name kummitäti', + 'relationship_type_godfather_male_with_name' => ':name kummisetä', + + 'relationship_type_godson' => 'kummilapsi', + 'relationship_type_godson_female' => 'kummitytär', + 'relationship_type_godson_male' => 'kummipoika', + 'relationship_type_godson_with_name' => ':name kummilapsi', + 'relationship_type_godson_female_with_name' => ':name kummityttö', + 'relationship_type_godson_male_with_name' => ':name kummipoika', + + 'relationship_type_friend' => 'kaveri', + 'relationship_type_friend_female' => 'kaveri', + 'relationship_type_friend_male' => 'kaveri', + 'relationship_type_friend_with_name' => ':name kaveri', + 'relationship_type_friend_female_with_name' => ':name kaveri', + 'relationship_type_friend_male_with_name' => ':name kaveri', + + 'relationship_type_bestfriend' => 'paras kavari', + 'relationship_type_bestfriend_female' => 'paras kaveri', + 'relationship_type_bestfriend_male' => 'paras kaveri', + 'relationship_type_bestfriend_with_name' => ':name paras kaveri', + 'relationship_type_bestfriend_female_with_name' => ':name paras kaveri', + 'relationship_type_bestfriend_male_with_name' => ':name paras kaveri', + + 'relationship_type_colleague' => 'kollega', + 'relationship_type_colleague_female' => 'kollega', + 'relationship_type_colleague_male' => 'kollega', + 'relationship_type_colleague_with_name' => ':name kollega', + 'relationship_type_colleague_female_with_name' => ':name’s colleague', + 'relationship_type_colleague_male_with_name' => ':name’s colleague', + + 'relationship_type_boss' => 'boss', + 'relationship_type_boss_female' => 'boss', + 'relationship_type_boss_male' => 'boss', + 'relationship_type_boss_with_name' => ':name’s boss', + 'relationship_type_boss_female_with_name' => ':name’s boss', + 'relationship_type_boss_male_with_name' => ':name’s boss', + + 'relationship_type_subordinate' => 'subordinate', + 'relationship_type_subordinate_female' => 'subordinate', + 'relationship_type_subordinate_male' => 'subordinate', + 'relationship_type_subordinate_with_name' => ':name’s subordinate', + 'relationship_type_subordinate_female_with_name' => ':name’s subordinate', + 'relationship_type_subordinate_male_with_name' => ':name’s subordinate', + + 'relationship_type_mentor' => 'mentor', + 'relationship_type_mentor_female' => 'mentor', + 'relationship_type_mentor_male' => 'mentor', + 'relationship_type_mentor_with_name' => ':name’s mentor', + 'relationship_type_mentor_female_with_name' => ':name’s mentor', + 'relationship_type_mentor_male_with_name' => ':name’s mentor', + + 'relationship_type_protege' => 'protégé', + 'relationship_type_protege_female' => 'protégé', + 'relationship_type_protege_male' => 'protégé', + 'relationship_type_protege_with_name' => ':name’s protégé', + 'relationship_type_protege_female_with_name' => ':name’s protégé', + 'relationship_type_protege_male_with_name' => ':name’s protégé', + + 'relationship_type_ex_husband' => 'ex-spouse', + 'relationship_type_ex_husband_female' => 'ex-wife', + 'relationship_type_ex_husband_male' => 'ex-husband', + 'relationship_type_ex_husband_with_name' => ':name’s ex-spouse', + 'relationship_type_ex_husband_female_with_name' => ':name’s ex-wife', + 'relationship_type_ex_husband_male_with_name' => ':name’s ex-husband', + + // emotions + 'emotion_primary_love' => 'Love', + 'emotion_primary_joy' => 'Joy', + 'emotion_primary_surprise' => 'Surprise', + 'emotion_primary_anger' => 'Anger', + 'emotion_primary_sadness' => 'Sadness', + 'emotion_primary_fear' => 'Fear', + + 'emotion_secondary_affection' => 'Affection', + 'emotion_secondary_lust' => 'Lust', + 'emotion_secondary_longing' => 'Longing', + 'emotion_secondary_cheerfulness' => 'Cheerfulness', + 'emotion_secondary_zest' => 'Zest', + 'emotion_secondary_contentment' => 'Contentment', + 'emotion_secondary_pride' => 'Pride', + 'emotion_secondary_optimism' => 'Optimism', + 'emotion_secondary_enthrallment' => 'Enthrallment', + 'emotion_secondary_relief' => 'Relief', + 'emotion_secondary_surprise' => 'Surprise', + 'emotion_secondary_irritation' => 'Irritation', + 'emotion_secondary_exasperation' => 'Ärsytys', + 'emotion_secondary_rage' => 'Raivostunut', + 'emotion_secondary_disgust' => 'Inho', + 'emotion_secondary_envy' => 'Kateellinen', + 'emotion_secondary_suffering' => 'Kärsimys', + 'emotion_secondary_sadness' => 'Surullisuus', + 'emotion_secondary_disappointment' => 'Pettymys', + 'emotion_secondary_shame' => 'Häpeä', + 'emotion_secondary_neglect' => 'Laiminlyönti', + 'emotion_secondary_sympathy' => 'Sympatia', + 'emotion_secondary_horror' => 'Kauhu', + 'emotion_secondary_nervousness' => 'Hermostuneisuus', + + 'emotion_adoration' => 'Ihailu', + 'emotion_affection' => 'Mieltymys', + 'emotion_love' => 'Rakkaus', + 'emotion_fondness' => 'Kiintymys', + 'emotion_liking' => 'Tykkääminen', + 'emotion_attraction' => 'Houkutus', + 'emotion_caring' => 'Välittävä', + 'emotion_tenderness' => 'Arkuus', + 'emotion_compassion' => 'Kompassio', + 'emotion_sentimentality' => 'Sentimentaalisuus', + 'emotion_arousal' => 'Arousal', + 'emotion_desire' => 'Desire', + 'emotion_lust' => 'Lust', + 'emotion_passion' => 'Passion', + 'emotion_infatuation' => 'Infatuation', + 'emotion_longing' => 'Longing', + 'emotion_amusement' => 'Amusement', + 'emotion_bliss' => 'Bliss', + 'emotion_cheerfulness' => 'Cheerfulness', + 'emotion_gaiety' => 'Gaiety', + 'emotion_glee' => 'Glee', + 'emotion_jolliness' => 'Jolliness', + 'emotion_joviality' => 'Joviality', + 'emotion_joy' => 'Joy', + 'emotion_delight' => 'Delight', + 'emotion_enjoyment' => 'Enjoyment', + 'emotion_gladness' => 'Gladness', + 'emotion_happiness' => 'Happiness', + 'emotion_jubilation' => 'Jubilation', + 'emotion_elation' => 'Elation', + 'emotion_satisfaction' => 'Satisfaction', + 'emotion_ecstasy' => 'Ecstasy', + 'emotion_euphoria' => 'Euphoria', + 'emotion_enthusiasm' => 'Enthusiasm', + 'emotion_zeal' => 'Zeal', + 'emotion_zest' => 'Zest', + 'emotion_excitement' => 'Excitement', + 'emotion_thrill' => 'Thrill', + 'emotion_exhilaration' => 'Exhilaration', + 'emotion_contentment' => 'Contentment', + 'emotion_pleasure' => 'Pleasure', + 'emotion_pride' => 'Pride', + 'emotion_eagerness' => 'Eagerness', + 'emotion_hope' => 'Hope', + 'emotion_optimism' => 'Optimism', + 'emotion_enthrallment' => 'Enthrallment', + 'emotion_rapture' => 'Rapture', + 'emotion_relief' => 'Relief', + 'emotion_amazement' => 'Amazement', + 'emotion_surprise' => 'Surprise', + 'emotion_astonishment' => 'Astonishment', + 'emotion_aggravation' => 'Aggravation', + 'emotion_irritation' => 'Irritation', + 'emotion_agitation' => 'Agitation', + 'emotion_annoyance' => 'Annoyance', + 'emotion_grouchiness' => 'Grouchiness', + 'emotion_grumpiness' => 'Grumpiness', + 'emotion_exasperation' => 'Exasperation', + 'emotion_frustration' => 'Frustration', + 'emotion_anger' => 'Anger', + 'emotion_rage' => 'Rage', + 'emotion_outrage' => 'Outrage', + 'emotion_fury' => 'Fury', + 'emotion_wrath' => 'Wrath', + 'emotion_hostility' => 'Hostility', + 'emotion_ferocity' => 'Ferocity', + 'emotion_bitterness' => 'Bitterness', + 'emotion_hate' => 'Hate', + 'emotion_loathing' => 'Loathing', + 'emotion_scorn' => 'Scorn', + 'emotion_spite' => 'Spite', + 'emotion_vengefulness' => 'Vengefulness', + 'emotion_dislike' => 'Dislike', + 'emotion_resentment' => 'Resentment', + 'emotion_disgust' => 'Disgust', + 'emotion_revulsion' => 'Revulsion', + 'emotion_contempt' => 'Contempt', + 'emotion_envy' => 'Envy', + 'emotion_jealousy' => 'Jealousy', + 'emotion_agony' => 'Agony', + 'emotion_suffering' => 'Suffering', + 'emotion_hurt' => 'Hurt', + 'emotion_anguish' => 'Anguish', + 'emotion_depression' => 'Depression', + 'emotion_despair' => 'Despair', + 'emotion_hopelessness' => 'Hopelessness', + 'emotion_gloom' => 'Gloom', + 'emotion_glumness' => 'Glumness', + 'emotion_sadness' => 'Sadness', + 'emotion_unhappiness' => 'Unhappiness', + 'emotion_grief' => 'Grief', + 'emotion_sorrow' => 'Sorrow', + 'emotion_woe' => 'Woe', + 'emotion_misery' => 'Misery', + 'emotion_melancholy' => 'Melancholy', + 'emotion_dismay' => 'Dismay', + 'emotion_disappointment' => 'Disappointment', + 'emotion_displeasure' => 'Displeasure', + 'emotion_guilt' => 'Guilt', + 'emotion_shame' => 'Shame', + 'emotion_regret' => 'Regret', + 'emotion_remorse' => 'Remorse', + 'emotion_alienation' => 'Alienation', + 'emotion_isolation' => 'Isolation', + 'emotion_neglect' => 'Neglect', + 'emotion_loneliness' => 'Loneliness', + 'emotion_rejection' => 'Rejection', + 'emotion_homesickness' => 'Homesickness', + 'emotion_defeat' => 'Defeat', + 'emotion_dejection' => 'Dejection', + 'emotion_insecurity' => 'Insecurity', + 'emotion_embarrassment' => 'Embarrassment', + 'emotion_humiliation' => 'Humiliation', + 'emotion_insult' => 'Insult', + 'emotion_pity' => 'Pity', + 'emotion_sympathy' => 'Sympathy', + 'emotion_alarm' => 'Alarm', + 'emotion_shock' => 'Shock', + 'emotion_fear' => 'Fear', + 'emotion_fright' => 'Fright', + 'emotion_horror' => 'Horror', + 'emotion_terror' => 'Terror', + 'emotion_panic' => 'Panic', + 'emotion_hysteria' => 'Hysteria', + 'emotion_mortification' => 'Mortification', + 'emotion_anxiety' => 'Anxiety', + 'emotion_nervousness' => 'Nervousness', + 'emotion_tenseness' => 'Tenseness', + 'emotion_uneasiness' => 'Uneasiness', + 'emotion_apprehension' => 'Apprehension', + 'emotion_worry' => 'Worry', + 'emotion_distress' => 'Distress', + 'emotion_dread' => 'Dread', + + // weather + 'weather_sunny' => 'Sunny', + 'weather_clear' => 'Clear', + 'weather_clear-day' => 'Clear', + 'weather_clear-night' => 'Clear night', + 'weather_light-drizzle' => 'Light drizzle', + 'weather_patchy-light-drizzle' => 'Patchy light drizzle', + 'weather_patchy-light-rain' => 'Patchy light rain', + 'weather_light-rain' => 'Light rain', + 'weather_moderate-rain-at-times' => 'Moderate rain at times', + 'weather_moderate-rain' => 'Moderate rain', + 'weather_patchy-rain-possible' => 'Patchy rain possible', + 'weather_heavy-rain-at-times' => 'Heavy rain at times', + 'weather_heavy-rain' => 'Heavy rain', + 'weather_light-freezing-rain' => 'Light freezing rain', + 'weather_moderate-or-heavy-freezing-rain' => 'Moderate or heavy freezing rain', + 'weather_light-sleet' => 'Light sleet', + 'weather_moderate-or-heavy-rain-shower' => 'Moderate or heavy rain shower', + 'weather_light-rain-shower' => 'Light rain shower', + 'weather_torrential-rain-shower' => 'Torrential rain shower', + 'weather_rain' => 'Rain', + 'weather_snow' => 'Snow', + 'weather_blowing-snow' => 'Blowing snow', + 'weather_patchy-light-snow' => 'Patchy light snow', + 'weather_light-snow' => 'Light snow', + 'weather_patchy-moderate-snow' => 'Patchy moderate snow', + 'weather_moderate-snow' => 'Moderate snow', + 'weather_patchy-heavy-snow' => 'Patchy heavy snow', + 'weather_heavy-snow' => 'Heavy snow', + 'weather_light-snow-showers' => 'Light snow showers', + 'weather_moderate-or-heavy-snow-showers' => 'Moderate or heavy snow showers', + 'weather_patchy-snow-possible' => 'Patchy snow possible', + 'weather_patchy-sleet-possible' => 'Patchy sleet possible', + 'weather_moderate-or-heavy-sleet' => 'Moderate or heavy sleet', + 'weather_light-sleet-showers' => 'Light sleet showers', + 'weather_moderate-or-heavy-sleet-showers' => 'Moderate or heavy sleet showers', + 'weather_sleet' => 'Sleet', + 'weather_wind' => 'Wind', + 'weather_fog' => 'Fog', + 'weather_freezing-fog' => 'Freezing fog', + 'weather_mist' => 'Mist', + 'weather_blizzard' => 'Blizzard', + 'weather_overcast' => 'Overcast', + 'weather_cloudy' => 'Cloudy', + 'weather_partly-cloudy-day' => 'Partly cloudy', + 'weather_partly-cloudy-night' => 'Partly cloudy', + 'weather_freezing-drizzle' => 'Freezing drizzle', + 'weather_heavy-freezing-drizzle' => 'Heavy freezing drizzle', + 'weather_patchy-freezing-drizzle-possible' => 'Patchy freezing drizzle possible', + 'weather_ice-pellets' => 'Ice pellets', + 'weather_light-showers-of-ice-pellets' => 'Light showers of ice pellets', + 'weather_moderate-or-heavy-showers-of-ice-pellets' => 'Moderate or heavy showers of ice pellets', + 'weather_thundery-outbreaks-possible' => 'Thundery outbreaks possible', + 'weather_patchy-light-rain-with-thunder' => 'Patchy light rain with thunder', + 'weather_moderate-or-heavy-rain-with-thunder' => 'Moderate or heavy rain with thunder', + 'weather_patchy-light-snow-with-thunder' => 'Patchy light snow with thunder', + 'weather_moderate-or-heavy-snow-with-thunder' => 'Moderate or heavy snow with thunder', + 'weather_current_temperature_celsius' => ':temperature °C', + 'weather_current_temperature_fahrenheit' => ':temperature °F', + 'weather_current_title' => 'Current weather', + + // dav + 'dav_contacts' => 'Contacts', + 'dav_contacts_description' => ':name’s contacts', + 'dav_birthdays' => 'Birthdays', + 'dav_birthdays_description' => ':name’s contact’s birthdays', + 'dav_tasks' => 'Tasks', + 'dav_tasks_description' => ':name’s tasks', + + // contact list + 'contact_list_avatar' => 'Avatar', + 'contact_list_name' => 'Contact', + 'contact_list_description' => 'Description', + +]; diff --git a/resources/lang/fi/auth.php b/resources/lang/fi/auth.php new file mode 100644 index 0000000..7398f73 --- /dev/null +++ b/resources/lang/fi/auth.php @@ -0,0 +1,89 @@ + 'Nämä tiedot eivät vastaa tietojamme.', + 'throttle' => 'Liian monta kirjautumisyritystä. Yritä uudelleen :seconds sekunnin kuluttua.', + 'not_authorized' => 'Sinulla ei ole oikeutta suorittaa tätä toimintoa', + 'signup_disabled' => 'Rekisteröinti on poistettu käytöstä', + 'signup_error' => 'Tapahtui virhe yritettäessä rekisteröidä käyttäjää', + 'back_homepage' => 'Takaisin kotisivulle', + 'mfa_auth_otp' => 'Todenna kaksivaiheisella laitteellasi', + 'mfa_auth_webauthn' => 'Todenna turvaavaimella (WebAuthn)', + '2fa_title' => 'Kaksivaiheinen tunnistautuminen', + '2fa_wrong_validation' => 'Kaksitasoinen todennus epäonnistui.', + '2fa_one_time_password' => 'Kaksivaiheisen todennuksen koodi', + '2fa_recuperation_code' => 'Kirjoita kaksivaiheisen todennuksen palautuskoodi', + '2fa_one_time_or_recuperation' => 'Syötä kaksivaiheinen tunnistautumiskoodi tai palautuskoodi', + '2fa_otp_help' => 'Avaa kaksivaiheinen autentikointi mobiilisovellus ja kopioi koodi', + + 'login_to_account' => 'Kirjaudu tilillesi', + 'login_with_recovery' => 'Kirjaudu käyttäen palautuskoodia', + 'login_again' => 'Ole hyvä ja kirjaudu uudelleen tilillesi', + 'email' => 'Sähköposti', + 'password' => 'Salasana', + 'recovery' => 'Palautuskoodi', + 'login' => 'Kirjaudu', + 'button_remember' => 'Muista minut', + 'password_forget' => 'Unohditko salasanasi?', + 'password_reset' => 'Nollaa salasanasi', + 'use_recovery' => 'Tai voit käyttää palautuskoodia', + 'signup_no_account' => 'Eikö sinulla ole tiliä?', + 'signup' => 'Rekisteröidy nyt', + 'create_account' => 'Luo ensimmäinen tili rekisteröitymällä', + 'change_language_title' => 'Vaihda kieltä:', + 'change_language' => 'Vaihda kieli :lang', + + 'password_reset_title' => 'Nollaa Salasana', + 'password_reset_email' => 'Sähköpostiosoite', + 'password_reset_send_link' => 'Lähetä Salasanan Nollauslinkki', + 'password_reset_password' => 'Salasana', + 'password_reset_password_confirm' => 'Vahvista Salasana', + 'password_reset_action' => 'Resetoi salasana', + 'password_reset_email_content' => 'Klikkaa tästä nollataksesi salasanan:', + + 'register_title_welcome' => 'Tervetuloa juuri asennettuun Monica instanssiin', + 'register_create_account' => 'Sinun täytyy luoda tili käyttääksesi Monicaa', + 'register_title_create' => 'Luo Monica tilisi', + 'register_login' => 'Kirjaudu sisään jos sinulla on jo tili.', + 'register_email' => 'Syötä toimiva sähköpostiosoite', + 'register_email_example' => 'sinä@koti', + 'register_firstname' => 'Etunimi', + 'register_firstname_example' => 'esim. Joni', + 'register_lastname' => 'Sukunimi', + 'register_lastname_example' => 'esim. Räikkönen', + 'register_password' => 'Salasana', + 'register_password_example' => 'Syötä turvallinen salasana', + 'register_password_confirmation' => 'Salasanan vahvistus', + 'register_action' => 'Register', + 'register_policy' => 'Signing up signifies you’ve read and agree to our Privacy Policy and Terms of use.', + 'register_invitation_email' => 'For security purposes, please indicate the email of the person who’ve invited you to join this account. This information is provided in the invitation email.', + + 'confirmation_title' => 'Verify Your Email Address', + 'confirmation_fresh' => 'A fresh verification link has been sent to your email address.', + 'confirmation_check' => 'Before proceeding, please check your email for a verification link.', + 'confirmation_request_another' => 'If you did not receive the email click here to request another.', + + 'confirmation_again' => 'If you want to change your email address you can click here.', + 'email_change_current_email' => 'Current email address:', + 'email_change_title' => 'Change your email address', + 'email_change_new' => 'New email address', + 'email_changed' => 'Your email address has been changed. Check your mailbox to validate it.', +]; diff --git a/resources/lang/fi/changelog.php b/resources/lang/fi/changelog.php new file mode 100644 index 0000000..981b018 --- /dev/null +++ b/resources/lang/fi/changelog.php @@ -0,0 +1,12 @@ + 'Product changes', + 'note' => 'Note: unfortunately, this page is only in English.', +]; diff --git a/resources/lang/fi/dashboard.php b/resources/lang/fi/dashboard.php new file mode 100644 index 0000000..5190352 --- /dev/null +++ b/resources/lang/fi/dashboard.php @@ -0,0 +1,42 @@ + 'Welcome to your account!', + 'dashboard_blank_description' => 'Monica is the place to organize all the interactions you have with the people you care about.', + 'dashboard_blank_cta' => 'Add your first contact', + 'dashboard_blank_illustration' => 'Illustration by Freepik', + + 'notes_title' => 'You don’t have any starred notes yet.', + + 'tab_recent_calls' => 'Recent calls', + 'tab_favorite_notes' => 'Favorite notes', + 'tab_calls_blank' => 'You haven’t logged any calls yet.', + 'tab_debts' => 'Debts', + 'tab_debts_blank' => 'You haven’t logged any debts yet.', + 'tab_tasks' => 'Tasks', + 'tab_tasks_blank' => 'You haven’t any tasks yet.', + + 'tasks_add_task_placeholder' => 'What is this task about?', + 'tasks_tab_your_contacts' => 'Tasks related to your contacts', + 'tasks_tab_your_tasks' => 'Your tasks', + 'tasks_add_note' => 'Press Enter to add the task.', + 'task_add_cta' => 'Add a task', + + 'debts_you_owe' => 'You owe', + + 'statistics_contacts' => 'Contacts', + 'statistics_activities' => 'Activities', + 'statistics_gifts' => 'Gifts', + + 'reminders_next_months' => 'Events in the next 3 months', + 'reminders_none' => 'No reminders for this month.', + + 'product_changes' => 'Product changes', + 'product_view_details' => 'View details', +]; diff --git a/resources/lang/fi/format.php b/resources/lang/fi/format.php new file mode 100644 index 0000000..a70a6ba --- /dev/null +++ b/resources/lang/fi/format.php @@ -0,0 +1,36 @@ + 'M d, Y H:i', + 'short_date_year' => 'M d, Y', + 'short_date' => 'M d', + 'short_month' => 'M', + 'short_month_year' => 'M Y', + 'short_day' => 'D', + 'full_date_year' => 'F d, Y', + 'full_month' => 'F', + 'full_month_year' => 'F Y', + 'full_hour' => 'h.i A', + + 'short_text' => '{text}…', +]; diff --git a/resources/lang/fi/journal.php b/resources/lang/fi/journal.php new file mode 100644 index 0000000..9b1f0be --- /dev/null +++ b/resources/lang/fi/journal.php @@ -0,0 +1,38 @@ + 'How was your day? You can rate it once a day.', + 'journal_come_back' => 'Thanks. Come back tomorrow to rate your day again.', + 'journal_description' => 'Note: the journal lists both manual journal entries, and automatic entries like Activities done with your contacts. While you can delete journal entries manually, you’ll have to delete the activity directly on the contact page.', + 'journal_add' => 'Add a journal entry', + 'journal_edit' => 'Edit a journal entry', + 'journal_empty' => 'Empty journal', + 'journal_created_at' => 'Created at {date}', + 'journal_created_automatically' => 'Created automatically', + 'journal_entry_type_journal' => 'Journal entry', + 'journal_entry_type_activity' => 'Activity', + 'journal_entry_rate' => 'You rated your day.', + 'journal_add_comment' => 'Care to add a comment (optional)?', + 'journal_show_comment' => 'Show comment', + 'entry_delete_success' => 'The journal entry has been successfully deleted.', + 'journal_add_title' => 'Title (optional)', + 'journal_add_date' => 'Date', + 'journal_add_post' => 'Entry', + 'journal_add_cta' => 'Save', + 'journal_blank_cta' => 'Add your first journal entry', + 'journal_blank_description' => 'The journal lets you write events that happened to you, and remember them.', + 'delete_confirmation' => 'Are you sure you want to delete this journal entry?', + 'apply_filter' => 'Apply filter', + 'start_date' => 'Start Date', + 'end_date' => 'End Date', + 'per_page' => 'Per Page', + 'sort_order' => 'Sort By Created At', + 'ascending' => 'Ascending', + 'descending' => 'Descending', +]; diff --git a/resources/lang/fi/logs.php b/resources/lang/fi/logs.php new file mode 100644 index 0000000..7b6654b --- /dev/null +++ b/resources/lang/fi/logs.php @@ -0,0 +1,29 @@ + 'Created the contact.', + 'settings_log_contact_created_with_name' => 'Added :name as a contact.', + + // contat description update + 'contact_log_contact_description_updated' => 'Updated the description.', + 'settings_log_contact_description_updated_with_name' => 'Updated the description of :name.', + + // contact description clear + 'contact_log_contact_description_cleared' => 'Cleared the description.', + 'settings_log_contact_description_cleared_with_name' => 'Cleared the description of :name.', + + // contact work information update + 'contact_log_contact_work_updated' => 'Updated work information.', + 'settings_log_contact_work_updated_with_name' => 'Updated work information of :name.', + + // company created + 'settings_log_company_created' => 'Created a company called :name.', +]; diff --git a/resources/lang/fi/mail.php b/resources/lang/fi/mail.php new file mode 100644 index 0000000..749f3d1 --- /dev/null +++ b/resources/lang/fi/mail.php @@ -0,0 +1,53 @@ + 'Reminder for :contact', + 'greetings' => 'Hi :username', + 'want_reminded_of' => 'You wanted to be reminded of :reason', + 'for' => 'For: :name', + 'comment' => 'Comment: :comment', + 'footer_contact_info' => 'Add, view, complete, and change information about this contact:', + 'footer_contact_info2' => 'See :name’s profile', + 'footer_contact_info2_link' => 'See :name’s profile: :url', + + 'notification_subject_line' => 'You have an upcoming event', + 'notification_description' => 'In :count days (on :date), the following event will happen:', + + 'stay_in_touch_subject_line' => 'Stay in touch with :name', + 'stay_in_touch_subject_description' => 'You asked to be reminded to stay in touch with :name every :frequency day.|You asked to be reminded to stay in touch with :name every :frequency days.', + + 'notifications_whoops' => 'Whoops!', + 'notifications_hello' => 'Hello!', + 'notifications_regards' => 'Regards', + 'notifications_footer' => 'If you’re having trouble clicking the ":actionText" button, copy and paste the URL below into your web browser: [:actionURL](:actionURL)', + 'notifications_rights' => 'All rights reserved', + + 'confirmation_email_title' => 'Monica – Email verification', + 'confirmation_email_intro'=> 'To validate your email click on the button below', + 'confirmation_email_button' => 'Verify email address', + 'confirmation_email_bottom' => 'If you did not create an account, no further action is required.', + + 'password_reset_title' => 'Monica – Reset Password Notification', + 'password_reset_intro' => 'You are receiving this email because we received a password reset request for your account.', + 'password_reset_button' => 'Reset Password', + 'password_reset_expiration' => 'This password reset link will expire in :count minutes.', + 'password_reset_bottom' => 'If you did not request a password reset, no further action is required.', + + 'invitation_title' => 'Monica – You are invited by :name', + 'invitation_intro' => 'You’ve been invited by :name (:email) to use Monica, a nice Personal Relationship Management tool.', + 'invitation_link' => 'To accept the invitation, click on the link below:', + 'invitation_button' => 'Accept invitation', + 'invitation_expiration' => 'This link will expire in :count days.', + + 'export_title' => 'Your export is ready', + 'export_description' => 'You requested a data export on :date. It is now ready to download.', + 'export_download' => 'Download export', + +]; diff --git a/resources/lang/fi/pagination.php b/resources/lang/fi/pagination.php new file mode 100644 index 0000000..d663041 --- /dev/null +++ b/resources/lang/fi/pagination.php @@ -0,0 +1,25 @@ + '❮ Previous', + 'next' => 'Next ❯', + +]; diff --git a/resources/lang/fi/passwords.php b/resources/lang/fi/passwords.php new file mode 100644 index 0000000..1487bb9 --- /dev/null +++ b/resources/lang/fi/passwords.php @@ -0,0 +1,30 @@ + 'Your password has been reset!', + 'sent' => 'If the email you entered exists in our records, you’ve been sent a password reset link.', + 'token' => 'This password reset token is invalid.', + 'user' => 'If the email you entered exists in our records, you’ve been sent a password reset link.', + 'changed' => 'Password changed successfully.', + 'invalid' => 'Current password you entered is not correct.', + 'throttled' => 'Please wait before retrying.', + +]; diff --git a/resources/lang/fi/people.php b/resources/lang/fi/people.php new file mode 100644 index 0000000..17d5eca --- /dev/null +++ b/resources/lang/fi/people.php @@ -0,0 +1,539 @@ + 'Yhteystietoa ei löytynyt', + 'people_list_number_kids' => ':count lapsi:count lapsia', + 'people_list_last_updated' => 'Last consulted:', + 'people_list_number_reminders' => ':count reminder|:count reminders', + 'people_list_blank_title' => 'You don’t have anyone in your account yet', + 'people_list_blank_cta' => 'Lisää joku', + 'people_list_sort' => 'Järjestä', + 'people_list_stats' => ':count yhteistieto|:count yhteystietoja', + 'people_list_firstnameAZ' => 'Lajittele etunimen A -> Z mukaan', + 'people_list_firstnameZA' => 'Järjestä etunimen mukaan Z → A', + 'people_list_lastnameAZ' => 'Järjestä sukunimen A → Z mukaan', + 'people_list_lastnameZA' => 'Järjestä sukunimen mukaan Z → A', + 'people_list_lastactivitydateNewtoOld' => 'Järjestä viimeisimmän aktiviteettipäivämäärän mukaan, uusimmasta vanhimpaan', + 'people_list_lastactivitydateOldtoNew' => 'Järjestä viimeisimmän aktiviteettipäivämäärän mukaan, vanhimmista uusimpiin', + 'people_list_filter_tag' => 'Näyttää kaikki yhteystiedot, jotka on merkitty', + 'people_list_clear_filter' => 'Tyhjennä suodatin', + 'people_list_contacts_per_tags' => ':count yhteistiedot|:count yhteystietoja', + 'people_list_show_dead' => 'Näytä kuolleet ihmiset (:count)', + 'people_list_hide_dead' => 'Piilota kuolleet ihmiset (:count)', + 'people_search' => 'Etsi yhteystietoja…', + 'people_search_no_results' => 'Tuloksia ei löytynyt', + 'people_search_next' => 'Seuraava', + 'people_search_prev' => 'Edellinen', + 'people_search_rows_per_page' => 'Rivejä per sivu', + 'people_search_of' => 'of', + 'people_search_page' => 'Sivu', + 'people_search_all' => 'Kaikki', + 'people_add_new' => 'Lisää uusi henkilö', + 'people_list_account_usage' => 'Tilisi käyttö: :current/:limit yhteistietoa', + 'people_list_account_upgrade_title' => 'Päivitä tilisi avataksesi sen täyteen potentiaaliin.', + 'people_list_account_upgrade_cta' => 'Päivitä nyt', + 'people_list_untagged' => 'View untagged contacts', + 'people_list_filter_untag' => 'Showing all untagged contacts', + 'archived_contact_readonly' => 'Arkistoituja yhteystietoja ei voi muokata, poista arkistointi ensin.', + + // people add + 'people_add_title' => 'Lisää uusi henkilö', + 'people_add_missing' => 'Ketään ei löytynyt – lisää uusi henkilö nyt', + 'people_add_firstname' => 'Etunimi', + 'people_add_middlename' => 'Toinen nimi (valinnainen)', + 'people_add_lastname' => 'Sukunimi (valinnainen)', + 'people_add_email' => 'Sähköposti (valinnainen)', + 'people_add_nickname' => 'Nimimerkki (valinnainen)', + 'people_add_cta' => 'Lisää', + 'people_save_and_add_another_cta' => 'Lähetä ja lisää joku muu', + 'people_add_success' => ':name on luotu onnistuneesti', + 'people_add_gender' => 'Sukupuoli', + 'people_delete_success' => 'Yhteystieto on poistettu', + 'people_delete_message' => 'Poista yhteystieto', + 'people_delete_confirmation' => 'Oletko varma, että haluat poistaa :name:n yhteystiedon? Poistaminen on välitön ja pysyvä.', + 'people_add_birthday_reminder' => 'Toivota hyvää syntymäpäivää henkilölle :name', + 'people_add_birthday_reminder_deceased' => 'Tänä päivänä :name olisi juhlinut syntymäpäiväänsä', + 'people_add_import' => 'Do you want to import your contacts?', + 'people_edit_email_error' => 'There is already a contact in your account with this email address. Please choose another one.', + 'people_export' => 'Export as vCard', + 'people_add_reminder_for_birthday' => 'Create an annual birthday reminder', + + // show + 'section_contact_information' => 'Contact information', + 'section_personal_activities' => 'Activities', + 'section_personal_reminders' => 'Reminders', + 'section_personal_tasks' => 'Tasks', + 'section_personal_gifts' => 'Gifts', + 'section_personal_notes' => 'Notes', + + // archived contacts + 'list_link_to_active_contacts' => 'You are viewing archived contacts. See the list of active contacts instead.', + 'list_link_to_archived_contacts' => 'List of archived contacts', + + // Header + 'me' => 'This is you', + 'edit_contact_information' => 'Edit contact information', + 'contact_archive' => 'Archive contact', + 'contact_unarchive' => 'Unarchive contact', + 'contact_archive_help' => 'Archived contacts are not be shown on the contact list, but still appear in search results.', + 'call_button' => 'Log a call', + 'set_favorite' => 'Favorite contacts are placed at the top of the contact list', + + // Stay in touch + 'stay_in_touch' => 'Stay in touch', + 'stay_in_touch_frequency' => 'Stay in touch every day|Stay in touch every {count} days', + 'stay_in_touch_next_date' => 'Next due: {date}', + 'stay_in_touch_invalid' => 'The frequency must be a number greater than 0.', + 'stay_in_touch_premium' => 'You need to upgrade your account to make use of this feature', + 'stay_in_touch_modal_title' => 'Stay in touch', + 'stay_in_touch_modal_desc' => 'We can remind you by email to keep in touch with {firstname} at a regular interval.', + 'stay_in_touch_modal_label' => 'Send me an email every… {count} day|Send me an email every… {count} days', + + // Calls + 'modal_call_title' => 'Log a call', + 'modal_call_comment' => 'What did you talk about? (optional)', + 'modal_call_exact_date' => 'The phone call happened on', + 'modal_call_who_called' => 'Who called?', + 'modal_call_emotion' => 'Do you want to log how you felt during this call? (optional)', + 'calls_add_success' => 'The phone call has been saved.', + 'call_delete_confirmation' => 'Are you sure you want to delete this call?', + 'call_delete_success' => 'The call has been deleted successfully', + 'call_title' => 'Phone calls', + 'call_empty_comment' => 'No details', + 'call_blank_title' => 'Keep track of the phone calls you’ve done with {name}', + 'call_blank_desc' => 'You called {name}', + 'call_you_called' => 'You called', + 'call_he_called' => '{name} called', + 'call_emotions' => 'Emotions:', + + // Conversation + 'conversation_blank' => 'Record conversations you have with :name on social media, SMS…', + 'conversation_delete_link' => 'Delete the conversation', + 'conversation_edit_title' => 'Muokkaa keskustelua', + 'conversation_edit_delete' => 'Oletko varma, että haluat poistaa tämän keskustelun? Poisto on pysyvä.', + 'conversation_add_success' => 'Keskustelu on lisätty onnistuneesti.', + 'conversation_edit_success' => 'Keskustelu on päivitetty onnistuneesti.', + 'conversation_delete_success' => 'Keskustelu on poistettu onnistuneesti.', + 'conversation_add_title' => 'Tallenna uusi keskustelu', + 'conversation_add_when' => 'Milloin kävit tämän keskustelun?', + 'conversation_add_who_wrote' => 'Kuka lähetti tämän viestin?', + 'conversation_add_how' => 'Miten sinä kommunikoit?', + 'conversation_add_you' => 'Sinä', + 'conversation_add_content' => 'Kirjoita muistiin mitä sanottiin', + 'conversation_add_what_was_said' => 'Mitä sinä sanoit?', + 'conversation_add_another' => 'Lisää toinen viesti', + 'conversation_add_error' => 'Sinun tulee lisätä vähintään yksi viesti.', + 'conversation_list_table_messages' => 'Viestit', + 'conversation_list_table_content' => 'Osittainen sisältö (viimeisin viesti)', + 'conversation_list_title' => 'Keskustelut', + 'conversation_list_cta' => 'Kirjaa keskustelu', + + // age - birthday + 'birthdate_not_set' => 'Syntymäpäivää ei ole asetettu', + 'age_approximate_in_years' => 'noin :age vuotta vanha', + 'age_exact_in_years' => ':age vuotta vanha', + 'age_exact_birthdate' => 'syntynyt :date', + + // Last called + 'last_called' => 'Viimeksi soitettu: :date', + 'last_talked_to' => 'Viimeksi soitettu: {date}', + 'last_called_empty' => 'Viimeksi soitettu: tuntematon', + 'last_activity_date' => 'Viimeisin aktiviteetti yhdessä: :date', + 'last_activity_date_empty' => 'Viimeisin aktiviteetti yhdessä: tuntematon', + + // additional information + 'information_edit_success' => 'Profiili on päivitetty onnistuneesti', + 'information_edit_title' => 'Muokkaa :name:n henkilökohtaisia tietoja', + 'information_edit_max_size' => 'Maksimissaan :size Kb.', + 'information_edit_max_size2' => 'Maksimi {size} kt.', + 'information_edit_firstname' => 'Etunimi', + 'information_edit_lastname' => 'Sukunimi (valinnainen)', + 'information_edit_description' => 'Kuvaus (valinnainen)', + 'information_edit_description_help' => 'Käytetään yhteystietoluettelossa lisätäkseen tarvittaessa jotain asiayhteyttä.', + 'information_edit_unknown' => 'En tiedä tämän henkilön ikää', + 'information_edit_probably' => 'Tämä henkilö on luultavasti…', + 'information_edit_not_year' => 'Tiedän tämän henkilön syntymäpäivän päivän ja kuukauden, mutta en vuotta…', + 'information_edit_exact' => 'Tiedän tämän henkilön tarkan syntymäpäivän…', + 'information_edit_birthdate_label' => 'Syntymäpäivä', + 'information_no_work_defined' => 'Työtietoja ei ole määritelty', + 'information_work_at' => 'at :company', + 'work_add_cta' => 'Päivitä työtiedot', + 'work_edit_success' => 'Työn tiedot päivitetty', + 'work_edit_title' => 'Päivitä :name:n työ tietoja', + 'work_edit_job' => 'Työn otsikko (valinnainen)', + 'work_edit_company' => 'Yritys (valinnainen)', + 'work_information' => 'Työtä koskevat tiedot', + + // food preferences + 'food_preferences_add_success' => 'Ruokamieltymykset on tallennettu', + 'food_preferences_edit_description' => 'Ehkä :firstname tai jollain :family perheessä on allergia. Tai ei pidä tietystä pullosta viiniä. Anna ne täällä, jotta muistat sen seuraavalla kerralla, kun kutsut heidät illalliselle', + 'food_preferences_edit_description_no_last_name' => 'Perhaps :firstname has an allergy. Or doesn’t like a specific bottle of wine. Indicate them here so you will remember it next time you invite them for dinner', + 'food_preferences_edit_title' => 'Indicate food preferences', + 'food_preferences_edit_cta' => 'Save food preferences', + 'food_preferences_title' => 'Food preferences', + 'food_preferences_cta' => 'Add food preferences', + + // reminders + 'reminders_blank_title' => 'Is there something you want to be reminded of about :name?', + 'reminders_blank_add_activity' => 'Add a reminder', + 'reminders_add_title' => 'What would you like to be reminded of about :name?', + 'reminders_add_description' => 'Please remind me to…', + 'reminders_add_next_time' => 'When is the next time you would like to be reminded about this?', + 'reminders_add_once' => 'Remind me about this just once', + 'reminders_add_recurrent' => 'Remind me about this every', + 'reminders_add_starting_from' => 'starting from the date specified above', + 'reminders_add_cta' => 'Add reminder', + 'reminders_edit_update_cta' => 'Update reminder', + 'reminders_add_error_custom_text' => 'You need to indicate a text for this reminder', + 'reminders_create_success' => 'The reminder has been added successfully', + 'reminders_delete_success' => 'The reminder has been deleted successfully', + 'reminders_update_success' => 'The reminder has been updated successfully', + 'reminders_add_optional_comment' => 'Optional comment', + + 'reminder_frequency_day' => 'every day|every :number days', + 'reminder_frequency_week' => 'every week|every :number weeks', + 'reminder_frequency_month' => 'every month|every :number months', + 'reminder_frequency_year' => 'every year|every :number year', + 'reminder_frequency_one_time' => 'on :date', + 'reminders_delete_confirmation' => 'Are you sure you want to delete this reminder?', + 'reminders_delete_cta' => 'Delete', + 'reminders_next_expected_date' => 'on', + 'reminders_cta' => 'Add a reminder', + 'reminders_description' => 'We will send an email for each one of the reminders below. Reminders are sent every morning the day events will happen. Reminders automatically added for birthdays can not be deleted. If you want to change those dates, edit the birthday of the contacts.', + 'reminders_one_time' => 'One time', + 'reminders_type_week' => 'week', + 'reminders_type_month' => 'month', + 'reminders_type_year' => 'year', + 'reminders_birthday' => 'Birthday of :name', + 'reminders_free_plan_warning' => 'You are on the Free plan. No emails are sent on this plan. To receive your reminders by email, upgrade your account.', + + // relationships + 'relationship_form_add' => 'Add a new relationship', + 'relationship_form_edit' => 'Edit an existing relationship', + 'relationship_form_is_with' => 'This person is…', + 'relationship_form_is_with_name' => ':name is…', + 'relationship_form_add_choice' => 'Who is the relationship with?', + 'relationship_form_create_contact' => 'Add a new person', + 'relationship_form_associate_contact' => 'An existing contact', + 'relationship_form_associate_dropdown' => 'Search and select an existing contact from the dropdown below', + 'relationship_form_associate_dropdown_placeholder' => 'Search and select an existing contact', + 'relationship_form_also_create_contact' => 'Create a Contact entry for this person.', + 'relationship_form_add_description' => 'This will let you treat this person like any other contact.', + 'relationship_form_add_no_existing_contact' => 'You don’t have any contacts who can be related to :name at the moment.', + 'relationship_delete_confirmation' => 'Are you sure you want to delete this relationship? Deletion is permanent.', + 'relationship_unlink_confirmation' => 'Oletko varma, että haluat poistaa tämän suhteen? Tätä henkilöä ei poisteta – vain näiden kahden välinen suhde.', + 'relationship_form_add_success' => 'Suhde on asetettu onnistuneesti.', + 'relationship_form_deletion_success' => 'Suhde on poistettu.', + + // tasks + 'tasks_title' => 'Tehtävät', + 'tasks_blank_title' => 'Sinulla ei ole vielä tehtäviä.', + 'tasks_form_title' => 'Otsikko', + 'tasks_form_description' => 'Kuvaus (valinnainen)', + 'tasks_add_task' => 'Lisää tehtävä', + 'tasks_delete_success' => 'Tehtävä on poistettu onnistuneesti', + 'tasks_complete_success' => 'Tehtävän tila on muuttunut onnistuneesti', + + // activities + 'activity_title' => 'Activities', + 'activity_type_category_simple_activities' => 'Simple activities', + 'activity_type_category_sport' => 'Urheilu', + 'activity_type_category_food' => 'Ruoka', + 'activity_type_category_cultural_activities' => 'Kulttuurinen toiminta', + 'activity_type_just_hung_out' => 'just hung out', + 'activity_type_watched_movie_at_home' => 'katsottiin elokuvaa kotona', + 'activity_type_talked_at_home' => 'puhuttiin kotona', + 'activity_type_did_sport_activities_together' => 'pelattiin urheilua yhdessä', + 'activity_type_ate_at_his_place' => 'syötiin heidän kotonaan', + 'activity_type_went_bar' => 'mentiin baariin', + 'activity_type_ate_at_home' => 'syötiin kotona', + 'activity_type_picnicked' => 'picnicked', + 'activity_type_ate_restaurant' => 'syötiin ravintolassa', + 'activity_type_went_theater' => 'käytiin teatterissa', + 'activity_type_went_concert' => 'mentiin konserttiin', + 'activity_type_went_play' => 'mentiin pelamaan', + 'activity_type_went_museum' => 'mentiin museoon', + 'activities_add_activity' => 'Lisää aktiviteetti', + 'activities_add_more_details' => 'Lisää lisätietoja', + 'activities_add_emotions' => 'Lisää tunteita', + 'activities_add_category' => 'Anna kategoria', + 'activities_add_participants_cta' => 'Lisää osallistujia', + 'activities_item_information' => ':Activity. Tapahtui :date', + 'activities_add_title' => 'Mitä sinä teit {name} kanssa?', + 'activities_summary' => 'Kuvaile mitä teit', + 'activities_add_pick_activity' => 'Haluatko luokitella tämän aktiviteetin? Sinun ei tarvitse luokitella, mutta se antaa sinulle tilastoja myöhemmin (valinnainen)', + 'activities_add_date_occured' => 'Aktiviteetti tapahtui...', + 'activities_add_participants' => 'Kuka osallistui tähän aktiviteettiin {name} lukuun ottamatta? (valinnainen)', + 'activities_add_emotions_title' => 'Haluatko kirjata, miltä tuntui tämän aktiviteetin aikana? (valinnainen)', + 'activities_blank_title' => 'Pidä kirjaa siitä, mitä olet tehnyt {name} kanssa menneisyydessä, ja mistä olet puhunut', + 'activities_blank_add_activity' => 'Lisää aktiviteetti', + 'activities_add_success' => 'Aktiviteetti on lisätty onnistuneesti', + 'activities_add_error' => 'Aktiviteetin lisäyksessä tapahtui virhe', + 'activities_update_success' => 'Aktiviteetti on onnistuneesti päivitetty', + 'activities_delete_success' => 'Aktiviteetti on poistettu onnistuneesti', + 'activities_who_was_involved' => 'Ketkä oli mukana?', + 'activities_activity' => 'Aktiviteetin kategoria', + 'activities_view_activities_report' => 'Näytä toimintaraportti', + 'activities_profile_title' => 'Aktiviteetti raportti välillä :name ja sinä', + 'activities_profile_subtitle' => 'You’ve logged :total_activities activity with :name in total and :activities_last_twelve_months in the last 12 months so far.|You’ve logged :total_activities activities with :name in total and :activities_last_twelve_months in the last 12 months so far.', + 'activities_profile_year_summary_activity_types' => 'Here is a breakdown of the type of activities you’ve done together in :year', + 'activities_profile_year_summary' => 'Here is what you two have done in :year', + 'activities_profile_number_occurences' => ':value activity|:value activities', + 'activities_list_participants' => 'Participants ({total}):', + 'activities_list_emotions' => 'Emotions felt:', + 'activities_list_date' => 'Happened on', + 'activities_list_category' => 'Category:', + + // notes + 'notes_create_success' => 'The note has been created successfully', + 'notes_update_success' => 'The note has been saved successfully', + 'notes_delete_success' => 'The note has been deleted successfully', + 'notes_add_cta' => 'Add note', + 'notes_favorite' => 'Add/remove from favorites', + 'notes_delete_title' => 'Delete a note', + 'notes_delete_confirmation' => 'Are you sure you want to delete this note? Deletion is permanent', + + // gifts + 'gifts_title' => 'Gifts', + 'gifts_add_success' => 'The gift has been added successfully', + 'gifts_delete_success' => 'The gift has been deleted successfully', + 'gifts_delete_confirmation' => 'Are you sure you want to delete this gift?', + 'gifts_add_gift' => 'Add a gift', + 'gifts_link' => 'Link', + 'gifts_for' => 'For: {name}', + 'gifts_delete_cta' => 'Delete', + 'gifts_add_title' => 'Gift management for :name', + 'gifts_add_gift_idea' => 'Gift idea', + 'gifts_add_gift_already_offered' => 'Gift given', + 'gifts_add_gift_received' => 'Gift received', + 'gifts_add_gift_title' => 'What is this gift?', + 'gifts_add_gift_name' => 'Gift name', + 'gifts_add_link' => 'Link to the web page (optional)', + 'gifts_add_value' => 'Value (optional)', + 'gifts_add_comment' => 'Comment (optional)', + 'gifts_add_recipient' => 'Recipient (optional)', + 'gifts_add_recipient_field' => 'Recipient', + 'gifts_add_photo' => 'Photo (optional)', + 'gifts_add_photo_title' => 'Add a photo for this gift', + 'gifts_add_someone' => 'This gift is for someone in {name}’s family in particular', + 'gifts_delete_title' => 'Delete a gift', + 'gifts_ideas' => 'Gift ideas', + 'gifts_offered' => 'Gifts given', + 'gifts_offered_as_an_idea' => 'Mark as an idea', + 'gifts_received' => 'Gifts received', + 'gifts_view_comment' => 'View comment', + 'gifts_mark_offered' => 'Mark as given', + 'gifts_update_success' => 'The gift has been updated successfully', + 'gifts_add_date' => 'Date (optional)', + + // debts + 'debt_delete_confirmation' => 'Haluatko varmasti poistaa tämän velan?', + 'debt_delete_success' => 'Tämä velka on onnistuneesti poistettu', + 'debt_add_success' => 'Tämä velka on lisätty onnistuneesti', + 'debt_title' => 'Velat', + 'debt_add_cta' => 'Lisää velka', + 'debt_you_owe' => 'Olet velkaa :amount', + 'debt_they_owe' => ':name on sinulla :amount velkaa', + 'debt_add_title' => 'Velkojen hallinta', + 'debt_add_you_owe' => 'Olet velkaa :name', + 'debt_add_they_owe' => ':name on sinulle velkaa', + 'debt_add_amount' => 'summa', + 'debt_add_reason' => 'seuraavasta syystä (valinnainen)', + 'debt_add_add_cta' => 'Lisää velka', + 'debt_edit_update_cta' => 'Päivitä velkaa', + 'debt_edit_success' => 'Velkaa on päivitetty onnistuneesti', + 'debts_blank_title' => 'Hallitse velkoja, jotka sinä olet velkaa :name:lle tai :name on velkaa sinulle', + + // tags + 'tag_edit' => 'Muokka tagia', + 'tag_add' => 'Lisää tunnisteita', + 'tag_add_search' => 'Lisää tai hae tageja', + 'tag_no_tags' => 'Ei vielä tageja', + + // Introductions + 'introductions_sidebar_title' => 'Miten tapasit', + 'introductions_blank_cta' => 'Ilmoita, miten tapasit :name', + 'introductions_title_edit' => 'Kuinka tapasitte :name:n kanssa?', + 'introductions_additional_info' => 'Selitä, miten ja missä tapasitte', + 'introductions_edit_met_through' => 'Onko joku esitellyt sinut tälle henkilölle?', + 'introductions_no_met_through' => 'Ei kukaan', + 'introductions_first_met_date' => 'Päivä, jona tapasit', + 'introductions_no_first_met_date' => 'En tiedä päivämäärää jolloin tapasimme', + 'introductions_first_met_date_known' => 'Tämä on päivämäärä, jolloin olemme tavanneet', + 'introductions_add_reminder' => 'Lisää muistutus juhlistaaksesi tätä kohtaamista vuosipäivänä kun tämä tapahtuma tapahtui', + 'introductions_update_success' => 'Olet onnistuneesti päivittänyt tietoja siitä, miten tapasit tämän henkilön', + 'introductions_met_through' => 'Met through :name', + 'introductions_met_date' => 'Met on :date', + 'introductions_reminder_title' => 'Anniversary of the day you first met', + + // Deceased + 'deceased_reminder_title' => 'Anniversary of the death of :name', + 'deceased_mark_person_deceased' => 'Merkitse tämä kuolleeksi', + 'deceased_know_date' => 'Tiedän päivämäärän milloin tämä henkilö kuoli', + 'deceased_add_reminder' => 'Lisää muistutus tälle päivälle', + 'deceased_label' => 'Menehtynyt', + 'deceased_date_label' => 'Menehtynyt pvm', + 'deceased_label_with_date' => 'Menehtynyt :date', + 'deceased_age' => 'Ikä menehtyessään', + + // Contact information + 'contact_info_title' => 'Yhteystiedot', + 'contact_info_form_content' => 'Sisältö', + 'contact_info_form_contact_type' => 'Yhteystiedon tyyppi', + 'contact_info_form_personalize' => 'Mukauta', + 'contact_info_address' => 'Asuu paikassa', + + // Addresses + 'contact_address_title' => 'Osoitteet', + 'contact_address_form_name' => 'Nimike (valinnainen)', + 'contact_address_form_street' => 'Katuosoite (valinnainen)', + 'contact_address_form_city' => 'City (optional)', + 'contact_address_form_province' => 'Province (optional)', + 'contact_address_form_postal_code' => 'Postal code (optional)', + 'contact_address_form_country' => 'Country (optional)', + 'contact_address_form_latitude' => 'Latitude (numbers only) (optional)', + 'contact_address_form_longitude' => 'Longitude (numbers only) (optional)', + + // Pets + 'pets_kind' => 'Kind of pet', + 'pets_name' => 'Name (optional)', + 'pets_create_success' => 'The pet has been successfully added', + 'pets_update_success' => 'The pet has been updated', + 'pets_delete_success' => 'The pet has been deleted', + 'pets_title' => 'Pets', + 'pets_reptile' => 'Reptile', + 'pets_bird' => 'Bird', + 'pets_cat' => 'Cat', + 'pets_dog' => 'Dog', + 'pets_fish' => 'Fish', + 'pets_hamster' => 'Hamster', + 'pets_horse' => 'Horse', + 'pets_rabbit' => 'Rabbit', + 'pets_rat' => 'Rat', + 'pets_small_animal' => 'Small animal', + 'pets_other' => 'Other', + + // life events + 'life_event_list_tab_life_events' => 'Life events', + 'life_event_list_tab_other' => 'Notes, reminders, …', + 'life_event_list_title' => 'Life events', + 'life_event_blank' => 'Log what happens to the life of {name} for your future reference.', + 'life_event_list_cta' => 'Add life event', + 'life_event_create_category' => 'All categories', + 'life_event_create_life_event' => 'Add life event', + 'life_event_create_default_title' => 'Title (optional)', + 'life_event_create_default_story' => 'Story (optional)', + 'life_event_create_date' => 'You do not need to indicate a month or a day – only the year is mandatory.', + 'life_event_create_default_description' => 'Add information about what you know', + 'life_event_create_add_yearly_reminder' => 'Add a yearly reminder for this event', + 'life_event_create_success' => 'The life event has been added', + 'life_event_delete_title' => 'Delete a life event', + 'life_event_delete_description' => 'Are you sure you want to delete this life event? Deletion is permanent.', + 'life_event_delete_success' => 'The life event has been deleted', + 'life_event_date_it_happened' => 'Date it happened', + 'life_event_category_work_education' => 'Work & education', + 'life_event_category_family_relationships' => 'Family & relationships', + 'life_event_category_home_living' => 'Home & living', + 'life_event_category_health_wellness' => 'Health & wellness', + 'life_event_category_travel_experiences' => 'Travel & experiences', + 'life_event_sentence_new_job' => 'Started a new job', + 'life_event_sentence_retirement' => 'Eläkkeellä', + 'life_event_sentence_new_school' => 'Aloittanut koulun', + 'life_event_sentence_study_abroad' => 'Opiskeli ulkomailla', + 'life_event_sentence_volunteer_work' => 'Aloitti vapaaehtoistyön', + 'life_event_sentence_published_book_or_paper' => 'Julkaisi paperin', + 'life_event_sentence_military_service' => 'Aloitti sotilaspalvelun', + 'life_event_sentence_new_relationship' => 'Aloitti suhteen', + 'life_event_sentence_engagement' => 'Meni kihloihin', + 'life_event_sentence_marriage' => 'Meni naimisiin', + 'life_event_sentence_anniversary' => 'Vuosipäivä', + 'life_event_sentence_expecting_a_baby' => 'Odottaa vauvaa', + 'life_event_sentence_new_child' => 'Sai lapsen', + 'life_event_sentence_new_family_member' => 'Sai uuden perheenjäsenen', + 'life_event_sentence_new_pet' => 'Sai lemmikin', + 'life_event_sentence_end_of_relationship' => 'Lopetti suhteen', + 'life_event_sentence_loss_of_a_loved_one' => 'Menetti läheisen', + 'life_event_sentence_moved' => 'Muutti', + 'life_event_sentence_bought_a_home' => 'Osti talon', + 'life_event_sentence_home_improvement' => 'Teki kodin parannuksia', + 'life_event_sentence_holidays' => 'Meni lomalle', + 'life_event_sentence_new_vehicle' => 'Sai uuden ajoneuvon', + 'life_event_sentence_new_roommate' => 'Sai kämppiksen', + 'life_event_sentence_overcame_an_illness' => 'Selvityi sairaudesta', + 'life_event_sentence_quit_a_habit' => 'Lopetti tavan', + 'life_event_sentence_new_eating_habits' => 'Alotti uusia ruokailutottumuksia', + 'life_event_sentence_weight_loss' => 'Pudotti painoa', + 'life_event_sentence_wear_glass_or_contact' => 'Alkoi käyttää laseja tai piilolinssejä', + 'life_event_sentence_broken_bone' => 'Luu murtui', + 'life_event_sentence_removed_braces' => 'Poistettiin hammasraudat', + 'life_event_sentence_surgery' => 'Oli leikkauksessa', + 'life_event_sentence_dentist' => 'Meni hammaslääkärille', + 'life_event_sentence_new_sport' => 'Aloitti urheilun', + 'life_event_sentence_new_hobby' => 'Aloitti harrastuksen', + 'life_event_sentence_new_instrument' => 'Oppi uuden musiiki instrumentin', + 'life_event_sentence_new_language' => 'Oppi uuden kielen', + 'life_event_sentence_tattoo_or_piercing' => 'Sai uuden tatioinnin tai lävistyksen', + 'life_event_sentence_new_license' => 'Got a license', + 'life_event_sentence_travel' => 'Matkustanut', + 'life_event_sentence_achievement_or_award' => 'Sai saavutuksen tai palkinnon', + 'life_event_sentence_changed_beliefs' => 'Vaihtanut uskoa', + 'life_event_sentence_first_word' => 'Puhui ensimmäistä kertaa', + 'life_event_sentence_first_kiss' => 'Pussasi ensimmäistä kertaa', + + // documents + 'document_list_title' => 'Dokumentit', + 'document_list_cta' => 'Lataa dokumentti', + 'document_list_blank_desc' => 'Täällä voit tallentaa tähän henkilöön liittyviä asiakirjoja.', + 'document_upload_zone_cta' => 'Lähetä tiedosto', + 'document_upload_zone_progress' => 'Lähetetään asiakirjaa…', + 'document_upload_zone_error' => 'Tiedoston lähetys epäonnistui. Ole hyvä ja yritä uudelleen alla.', + + // Photos + 'photo_title' => 'Valokuvat', + 'photo_list_title' => 'Liittyvät kuvat', + 'photo_list_cta' => 'Upload photo', + 'photo_list_blank_desc' => 'You can store images about this contact. Upload one now!', + 'photo_upload_zone_cta' => 'Upload a photo', + 'photo_current_profile_pic' => 'Current profile picture', + 'photo_make_profile_pic' => 'Make profile picture', + 'photo_delete' => 'Delete photo', + 'photo_next' => 'Next photo ❯', + 'photo_previous' => '❮ Previous photo', + + // Avatars + 'avatar_change_title' => 'Change your avatar', + 'avatar_question' => 'Which avatar would you like to use?', + 'avatar_default_avatar' => 'The default avatar', + 'avatar_adorable_avatar' => 'The Adorable avatar', + 'avatar_gravatar' => 'The Gravatar associated with the email address of this person. Gravatar is a global system that lets users associate email addresses with photos.', + 'avatar_current' => 'Keep the current avatar', + 'avatar_photo' => 'From a photo that you upload', + 'avatar_crop_new_avatar_photo' => 'Crop new avatar photo', + + // emotions + 'emotion_this_made_me_feel' => 'This made you feel…', + + // logs + 'auditlogs_link' => 'History', + 'auditlogs_title' => 'Everything that happened to :name', + 'auditlogs_breadcrumb' => 'History', + 'auditlogs_author' => 'By :name on :date', + + // contact field label + 'contact_field_label_home' => 'Home', + 'contact_field_label_work' => 'Work', + 'contact_field_label_cell' => 'Mobile', + 'contact_field_label_fax' => 'Fax', + 'contact_field_label_pager' => 'Pager', + 'contact_field_label_main' => 'Main', + 'contact_field_label_other' => 'Other', + 'contact_field_label_personal' => 'Personal', +]; diff --git a/resources/lang/fi/reminder.php b/resources/lang/fi/reminder.php new file mode 100644 index 0000000..bcab17c --- /dev/null +++ b/resources/lang/fi/reminder.php @@ -0,0 +1,16 @@ + 'Wish happy birthday to', + 'type_phone_call' => 'Call', + 'type_lunch' => 'Lunch with', + 'type_hangout' => 'Hangout with', + 'type_email' => 'Email', + 'type_birthday_kid' => 'Wish happy birthday to the child of', +]; diff --git a/resources/lang/fi/settings.php b/resources/lang/fi/settings.php new file mode 100644 index 0000000..f725bfe --- /dev/null +++ b/resources/lang/fi/settings.php @@ -0,0 +1,557 @@ + 'Tilin asetukset', + 'sidebar_personalization' => 'Mukauttaminen', + 'sidebar_settings_storage' => 'Tallennustila', + 'sidebar_settings_export' => 'Vie tiedot', + 'sidebar_settings_users' => 'Käyttäjät', + 'sidebar_settings_subscriptions' => 'Tilaus', + 'sidebar_settings_import' => 'Tuo tiedot', + 'sidebar_settings_tags' => 'Tagien hallinta', + 'sidebar_settings_api' => 'Rajapinta / API', + 'sidebar_settings_dav' => 'Dav- Resurssit', + 'sidebar_settings_security' => 'Turvallisuus', + 'sidebar_settings_auditlogs' => 'Tarkastuslokit', + + 'title_general' => 'Yleiset tiedot', + 'title_i18n' => 'Kansainväliset asetukset', + 'title_layout' => 'Ulkoasu', + + 'me_title' => 'Minä kontaktina', + 'me_help' => 'Tämä on kontakti, joka edustaa sinua Monicassa', + 'me_select' => 'Valitse yhteystieto', + 'me_no_contact' => 'Yhteystietoa ei ole vielä valittu.', + 'me_select_click' => 'Klikkaa tästä valitaksesi yhteystiedon.', + 'me_remove_contact' => 'Poista tämä yhteys', + 'me_choose' => 'Valitse itsesi', + 'me_choose_placeholder' => 'Valitse itsesi', + + 'export_title' => 'Vie tilisi tiedot', + 'export_be_patient' => 'Napsauta painiketta aloittaaksesi viennin. Vienti voi kestää useita minuutteja – ole kärsivällinen ja älä spämmää nappia.', + 'export_title_sql' => 'Vie SQL:ään', + 'export_sql_explanation' => 'Viedään tietoja SQL-muodossa voit ottaa tiedot ja tuoda ne omaan Monica instance. Tämä on arvokasta vain, jos sinulla on oma palvelin.', + 'export_sql_cta' => 'Vie SQL:ään', + 'export_sql_link_instructions' => 'Huomautus: lue ohjeet saadaksesi lisätietoja tämän tiedoston tuonnista instanssiisi.', + 'export_title_json' => 'Vie Jsoniin', + 'export_submitted' => 'Vienti on lähetetty, se on saatavilla muutaman hetken kuluttua…', + 'export_json_explanation' => 'Viedään tietojasi Jsonin muodossa varmuuskopiointia varten.', + 'export_json_beta' => 'Jsonin vienti on esikatselutilassa. Kerro meille mitä mieltä olet siitä:', + 'export_json_cta' => 'Vie Jsoniin', + 'export_header_type' => 'Tyyppi', + 'export_header_timestamp' => 'Luontipäivämäärä', + 'export_header_status' => 'Tila', + 'export_header_actions' => 'Toiminnot', + 'export_last_title' => 'Viimeisin vienti', + 'export_empty_title' => 'Ei vielä vientiä', + 'export_type_json' => 'JSON:n vienti', + 'export_type_sql' => 'SQL vienti', + 'export_status_todo' => 'Lähetetty', + 'export_status_doing' => 'Tehdään', + 'export_status_done' => 'Valmis', + 'export_status_failed' => 'Epäonnistui', + 'export_not_done' => 'Lataus mahdotonta, tämä vienti ei ole vielä valmis.', + + 'firstname' => 'Etunimi', + 'lastname' => 'Sukunimi', + 'name_order' => 'Nimen järjestys', + 'name_order_firstname_lastname' => ' – John Doe', + 'name_order_lastname_firstname' => ' – Doe John', + 'name_order_firstname_lastname_nickname' => ' () – John Doe (Rambo)', + 'name_order_firstname_nickname_lastname' => ' () – John (Rambo) Doe', + 'name_order_lastname_firstname_nickname' => ' () – Doe John (Rambo)', + 'name_order_lastname_nickname_firstname' => ' () – Doe (Rambo) John', + 'name_order_nickname_firstname_lastname' => ' ( ) – Rambo (John Doe)', + 'name_order_nickname_lastname_firstname' => ' ( ) – Rambo (Doe John)', + 'name_order_nickname' => ' – Rambo', + 'currency' => 'Currency', + 'name' => 'Your name: :name', + 'email' => 'Email address', + 'email_placeholder' => 'Enter email', + 'email_help' => 'This is the email used to login, and this is where Monica will send your reminders.', + 'timezone' => 'Timezone', + 'temperature_scale' => 'Temperature scale', + 'temperature_scale_fahrenheit' => 'Fahrenheit (°F)', + 'temperature_scale_celsius' => 'Celsius (°C)', + 'layout' => 'Layout', + 'layout_small' => 'Maximum 1200 pixels wide', + 'layout_big' => 'Full width of the browser', + 'save' => 'Update preferences', + 'delete_title' => 'Delete your account', + 'delete_desc' => 'Do you wish to delete your account? Deletion is permanent and all of your data will be erased permanently. If you have a subscription, it will be cancelled immediately.', + 'delete_other_desc' => 'Your data in the main database will be deleted immediately. As described in our privacy policy, we carry out securely encrypted backups of the database every day. These backups are kept for 30 days after which they are completely deleted. We cannot delete specific data from the backups we hold any earlier than this. All of your data will be completely deleted no later than 31 days after your account’s deletion.', + 'reset_desc' => 'Do you wish to reset your account? This will remove all your contacts, and all of the data associated with them. Your account will not be deleted.', + 'reset_title' => 'Reset your account', + 'reset_cta' => 'Reset account', + 'reset_notice' => 'Are you sure to reset your account? This is permanent and cannot be undone.', + 'reset_success' => 'Your account has been reset successfully.', + 'delete_notice' => 'Are you sure you want to delete your account? This is permanent and cannot be undone. All of your data will be deleted and will not be recoverable.', + 'delete_cta' => 'Delete account', + 'settings_success' => 'Preferences updated!', + 'locale' => 'Language used in the app', + 'locale_help' => 'Do you want to help translating Monica or add a new language? Please follow this link for more information.', + 'locale_ar' => 'Arabic', + 'locale_cs' => 'Czech', + 'locale_de' => 'German', + 'locale_el' => 'Greek', + 'locale_en' => 'English', + 'locale_en-GB' => 'English (United Kingdom)', + 'locale_es' => 'Spanish', + 'locale_fr' => 'French', + 'locale_he' => 'Hebrew', + 'locale_hr' => 'Croatian', + 'locale_id' => 'Indonesian', + 'locale_it' => 'Italian', + 'locale_ja' => 'Japanese', + 'locale_nl' => 'Dutch', + 'locale_pt' => 'Portuguese', + 'locale_pt-BR' => 'Brazilian Portuguese', + 'locale_ru' => 'Venäjä', + 'locale_sv' => 'Ruotsi', + 'locale_vi' => 'Vietnamese', + 'locale_zh' => 'Chinese Simplified', + 'locale_zh-TW' => 'Chinese Traditional', + 'locale_tr' => 'Turkish', + + 'security_title' => 'Turvallisuus', + 'security_help' => 'Muuta tilillesi liittyviä turvallisuuskysymyksiä.', + 'password_change' => 'Vaihda salasanasi', + 'password_current' => 'Nykyinen salasana', + 'password_current_placeholder' => 'Anna nykyinen salasanasi', + 'password_new1' => 'Uusi salasana', + 'password_new1_placeholder' => 'Anna uusi salasana', + 'password_new2' => 'Vahvista uusi salasana', + 'password_new2_placeholder' => 'Kirjoita uusi salasanasi uudelleen', + 'password_btn' => 'Vaihda salasana', + '2fa_title' => 'Kaksivaiheinen tunnistautuminen', + '2fa_otp_title' => 'Two Factor Authentication mobile application', + '2fa_enable_title' => 'Ota kaksivaiheinen tunnistautuminen käyttöön', + '2fa_enable_description' => 'Ota käyttöön kaksivaiheinen todennus lisätäksesi tilisi turvallisuutta.', + '2fa_enable_otp' => 'Open up your Two Factor Authentication mobile app and scan the following QR barcode:', + '2fa_enable_otp_help' => 'If your Two Factor Authentication mobile app does not support QR barcodes, enter in the following code:', + '2fa_enable_otp_validate' => 'Please validate the new device you’ve just set up:', + '2fa_enable_success' => 'Two Factor Authentication activated', + '2fa_enable_error' => 'Error when trying to activate Two Factor Authentication', + '2fa_enable_error_already_set' => 'Two Factor Authentication is already activated', + '2fa_disable_title' => 'Disable Two Factor Authentication', + '2fa_disable_description' => 'Disable Two Factor Authentication for your account. Be careful, your account will be much less secure!', + '2fa_disable_success' => 'Two Factor Authentication disabled', + '2fa_disable_error' => 'Error when trying to disable Two Factor Authentication', + + 'webauthn_title' => 'Security key — WebAuthn protocol', + 'webauthn_enable_description' => 'Add a new security key', + 'webauthn_key_name_help' => 'Anna avaimellesi nimi.', + 'webauthn_key_name' => 'Avaimen nimi:', + 'webauthn_success' => 'Avaimesi on havaittu ja vahvistettu.', + 'webauthn_last_use' => 'Viimeisin käyttö: {timestamp}', + 'webauthn_delete_confirmation' => 'Oletko varma, että haluat poistaa tämän avaimen?', + 'webauthn_delete_success' => 'Avain poistettu', + 'webauthn_insertKey' => 'Insert your security key.', + 'webauthn_buttonAdvise' => 'If your security key has a button, press it.', + 'webauthn_noButtonAdvise' => 'If it does not, remove it and insert it again.', + 'webauthn_not_supported' => 'Your browser doesn’t currently support WebAuthn.', + 'webauthn_not_secured' => 'WebAuthn only supports secure connections. Please load this page with https scheme.', + 'webauthn_error_already_used' => 'This key is already registered. It’s not necessary to register it again.', + 'webauthn_error_not_allowed' => 'The operation either timed out or was not allowed.', + + 'recovery_title' => 'Palautuskoodit', + 'recovery_show' => 'Hanki palautuskoodit', + 'recovery_copy_help' => 'Kopioi koodit leikepöydälle', + 'recovery_help_intro' => 'Nämä ovat palautuskoodisi:', + 'recovery_help_information' => 'You can use each recovery code once.', + 'recovery_clipboard' => 'Codes copied to the clipboard.', + 'recovery_generate' => 'Generate new codes…', + 'recovery_generate_help' => 'Generating new codes will invalidate previously generated codes.', + 'recovery_already_used_help' => 'This code has already been used.', + + 'users_list_title' => 'Users with access to your account', + 'users_list_add_user' => 'Invite a new user', + 'users_list_you' => 'That’s you', + 'users_list_invitations_title' => 'Pending invitations', + 'users_list_invitations_explanation' => 'Below are the people you’ve invited to join Monica as a collaborator.', + 'users_list_invitations_invited_by' => 'invited by :name', + 'users_list_invitations_sent_date' => 'sent on :date', + 'users_blank_title' => 'You are the only one who has access to this account.', + 'users_blank_add_title' => 'Would you like to invite someone else?', + 'users_blank_description' => 'This person will have the same access that you have, and will be able to add, edit or delete contact information.', + 'users_blank_cta' => 'Invite someone', + 'users_add_title' => 'Invite a new user to your account by email', + 'users_add_description' => 'This person will have the same access as you do, including inviting or deleting other users, including you. Make sure you trust this person before giving them access.', + 'users_add_email_field' => 'Enter the email of the person you want to invite', + 'users_add_confirmation' => 'I confirm that I want to invite this user to my account. I understand that this person will have access to ALL of my data and see exactly what I see.', + 'users_add_cta' => 'Invite user by email', + 'users_accept_title' => 'Accept invitation and create a new account', + 'users_error_please_confirm' => 'Please confirm that you want to invite this user before proceeding with the invitation', + 'users_error_email_already_taken' => 'This email is already taken. Please choose another one', + 'users_error_already_invited' => 'You already have invited this user. Please choose another email address.', + 'users_error_email_not_similar' => 'Tämä ei ole sen henkilön sähköposti, joka on kutsunut sinut.', + 'users_invitation_deleted_confirmation_message' => 'Kutsu on poistettu onnistuneesti', + 'users_invitations_delete_confirmation' => 'Haluatko varmasti poistaa tämän kutsun?', + 'users_list_delete_confirmation' => 'Haluatko varmasti poistaa tämän käyttäjän tililtäsi?', + 'users_invitation_need_subscription' => 'Useampien käyttäjien lisääminen vaatii tilauksen.', + + 'subscriptions_account_current_plan' => 'Nykyinen tilauksesi', + 'subscriptions_account_current_legacy' => 'Nykyinen suunnitelma, ei enää valittavissa:', + 'subscriptions_account_current_paid_plan' => 'You are on the :name plan. Thanks so much for being a subscriber.', + + 'subscriptions_account_next_billing_title' => 'Next bill', + 'subscriptions_account_next_billing' => 'Your subscription will auto-renew on :date.', + 'subscriptions_account_bill_monthly' => 'We’ll bill you :price for another month.', + 'subscriptions_account_bill_annual' => 'We’ll bill you :price for another year.', + 'subscriptions_account_change' => 'Change plan', + + 'subscriptions_account_cancel_title' => 'Cancel subscription', + 'subscriptions_account_cancel_action' => 'Cancel subscription', + 'subscriptions_account_cancel' => 'You can cancel your subscription at any time.', + 'subscriptions_account_free_plan' => 'You are on the free plan.', + 'subscriptions_account_free_plan_upgrade' => 'You can upgrade your account to the :name plan, which costs $:price per month. Here are the advantages:', + 'subscriptions_account_free_plan_benefits_users' => 'Unlimited number of users', + 'subscriptions_account_free_plan_benefits_reminders' => 'Reminders by email', + 'subscriptions_account_free_plan_benefits_import_data_vcard' => 'Import your contacts with vCard', + 'subscriptions_account_free_plan_benefits_support' => 'Support the project in the long run, so we can introduce more great features.', + 'subscriptions_account_upgrade' => 'Upgrade your account', + 'subscriptions_account_upgrade_title' => 'Upgrade Monica today and have more meaningful relationships.', + 'subscriptions_account_upgrade_choice' => 'Pick a plan below and join over :customers persons who upgraded their Monica.', + 'subscriptions_account_update_title' => 'Update Monica subscription', + 'subscriptions_account_update_description' => 'You can change your subscription’s frequency here.', + 'subscriptions_account_update_information' => 'You will be billed immediately for the new amount. Your subscription will extend to the new period, depending on your choice.', + 'subscriptions_account_invoices' => 'Laskut', + 'subscriptions_account_invoices_download' => 'Lataa', + 'subscriptions_account_invoices_subscription' => 'Tilaus :startDate päivään :endDate', + 'subscriptions_account_payment' => 'Mikä maksuvaihtoehto sopii sinulle parhaiten?', + 'subscriptions_account_confirm_payment' => 'Maksu on tällä hetkellä kesken, ole hyvä vahvista maksusi.', + 'subscriptions_downgrade_title' => 'Alenna tilisi maksuttomaan tilaukseen', + 'subscriptions_downgrade_limitations' => 'Ilmaisessa versiossa on rajoituksia. Jotta voit alentaa tilisi, sinun täytyy läpäistä tarkistuslista alla:', + 'subscriptions_downgrade_rule_users' => 'Sinulla tulee olla vain 1 käyttäjä tililläsi', + 'subscriptions_downgrade_rule_users_constraint' => 'Sinulla on tällä hetkellä 1 käyttäjä tililläsi. Sinulla on tällä hetkellä :count käyttäjiä tililläsi.', + 'subscriptions_downgrade_rule_invitations' => 'Sinulla ei saa olla odottavia kutsuja', + 'subscriptions_downgrade_rule_invitations_constraint' => 'Sinulla on 1 odottava kutsu. Olet tällä hetkellä :count odottavia kutsuja.', + 'subscriptions_downgrade_rule_contacts' => 'Sinulla ei saa olla enempää kuin :number aktiivisia yhteystietoja', + 'subscriptions_downgrade_rule_contacts_constraint' => 'Sinulla on tällä hetkellä 1 yhteystieto.| Sinulla on tällä hetkellä :count yhteystietoja.', + 'subscriptions_downgrade_rule_contacts_archive' => 'Voimme myös arkistoida kaikki yhteystietosi sinua varten – se poistaisi tämän säännön ja antaa sinun jatkaa tilisi ilmaisversioon siirtymis prosessia.', + 'subscriptions_downgrade_cta' => 'Alenna tasoa', + 'subscriptions_downgrade_success' => 'Olet palannut takaisin ilmaiseen versioon!', + 'subscriptions_downgrade_thanks' => 'Kiitos tosi paljon kun koitit maksullista versioa. Me lisäämme uusia ominaisuuksia Monicaan kokoajan, joten voit haluta palata takaisin maksullisen version käyttäjäksi.', + 'subscriptions_back' => 'Takaisin asetuksiin', + 'subscriptions_upgrade_title' => 'Päivitä tilisi', + 'subscriptions_upgrade_choose' => 'Valitsitte :plan suunnitelman.', + 'subscriptions_upgrade_infos' => 'Emme voisi olla tyytyväisempiä. Syötä maksutietosi alla.', + 'subscriptions_upgrade_name' => 'Nimi kortissa', + 'subscriptions_upgrade_zip' => 'ZIP or postal code', + 'subscriptions_upgrade_credit' => 'Credit or debit card', + 'subscriptions_upgrade_submit' => 'Pay {amount}', + 'subscriptions_upgrade_charge' => 'We’ll charge your card :price now. The next charge will be on :date. If you ever change your mind, you can cancel at any time, no questions asked.', + 'subscriptions_upgrade_charge_handled' => 'The payment is handled by Stripe. No card information touches our server.', + 'subscriptions_upgrade_success' => 'Thank you! You are now subscribed.', + 'subscriptions_upgrade_thanks' => 'Welcome to the community of people who try to make the world a better place.', + + 'subscriptions_payment_confirm_title' => 'Confirm your :amount payment', + 'subscriptions_payment_confirm_information' => 'Extra confirmation is needed to process your payment. Please confirm your payment by filling out your payment details below.', + 'subscriptions_payment_succeeded_title' => 'Payment Successful', + 'subscriptions_payment_succeeded' => 'This payment was already successfully confirmed.', + 'subscriptions_payment_cancelled_title' => 'Payment Cancelled', + 'subscriptions_payment_cancelled' => 'This payment was cancelled.', + 'subscriptions_payment_error_name' => 'Please provide your name.', + 'subscriptions_payment_success' => 'The payment was successful.', + + 'subscriptions_pdf_title' => 'Your :name monthly subscription', + 'subscriptions_plan_frequency_year' => ':amount / year', + 'subscriptions_plan_frequency_month' => ':amount / month', + 'subscriptions_plan_choose' => 'Choose this plan', + 'subscriptions_plan_year_title' => 'Pay annually', + 'subscriptions_plan_year_bonus' => 'Peace of mind for a whole year', + 'subscriptions_plan_month_title' => 'Pay monthly', + 'subscriptions_plan_month_bonus' => 'Cancel any time', + 'subscriptions_plan_include1' => 'Included with your upgrade:', + 'subscriptions_plan_include2' => 'Unlimited number of contacts • Unlimited number of users • Reminders by email • Import with vCard • Personalization of the contact sheet', + 'subscriptions_plan_include3' => '100% of the profits go the development of this great open source project.', + 'subscriptions_help_title' => 'Additional details you may be curious about', + 'subscriptions_help_opensource_title' => 'What is an open source project?', + 'subscriptions_help_opensource_desc' => 'Monica is an open source project. This means it is built by a community who wants to build a great tool for the greater good. Being open source means the code is publicly available on GitHub, and everyone can inspect it, modify it or enhance it. All the money we raise is dedicated to building better features, paying for more powerful servers, and paying other costs. Thanks for your help. We couldn’t do it without you.', + 'subscriptions_help_limits_title' => 'Is there a limit to the number of contacts we can have on the free plan?', + 'subscriptions_help_limits_plan' => 'Kyllä. Ilmaisen version avulla voit hallita :number yhteystietoja.', + 'subscriptions_help_discounts_title' => 'Do you have discounts for non-profits and education?', + 'subscriptions_help_discounts_desc' => 'We do! Monica is free for students, and free for non-profits and charities. Just contact the support with a proof of your status and we’ll apply this special status in your account.', + 'subscriptions_help_change_title' => 'What if I change my mind?', + 'subscriptions_help_change_desc' => 'You can cancel anytime, no questions asked, and all by yourself – no need to contact support. However, you will not be refunded for the current period.', + + 'stripe_error_card' => 'Your card was declined. Decline message is: :message', + 'stripe_error_api_connection' => 'Network communication with Stripe failed. Try again later.', + 'stripe_error_rate_limit' => 'Too many requests with Stripe right now. Try again later.', + 'stripe_error_invalid_request' => 'Invalid parameters. Try again later.', + 'stripe_error_authentication' => 'Wrong authentication with Stripe', + + 'import_title' => 'Import contacts in your account', + 'import_cta' => 'Upload contacts', + 'import_stat' => 'You’ve imported :number files so far.', + 'import_result_stat' => 'Uploaded vCard with 1 contact (:total_imported imported, :total_skipped skipped)|Uploaded vCard with :total_contacts contacts (:total_imported imported, :total_skipped skipped)', + 'import_view_report' => 'View report', + 'import_in_progress' => 'The import is in progress. Reload the page in one minute.', + 'import_upload_title' => 'Import your contacts from a vCard file', + 'import_upload_rules_desc' => 'We do however have some rules:', + 'import_upload_rule_format' => 'We support .vcard and .vcf files.', + 'import_upload_rule_vcard' => 'We support the vCard 3.0 format, which is the default format for macOS’s Contacts.app and Google Contacts.', + 'import_upload_rule_instructions' => 'Export instructions for macOS Contacts.app and Google Contacts.', + 'import_upload_rule_multiple' => 'If your contacts have multiple email addresses or phone numbers, only the first entry will be saved.', + 'import_upload_rule_limit' => 'Files are limited to 10 MB.', + 'import_upload_rule_time' => 'It might take up to a minute to upload the contacts and process them. Please be patient.', + 'import_upload_rule_cant_revert' => 'Please make sure data is accurate before uploading, as you can’t undo the upload.', + 'import_upload_form_file' => 'Your .vcf or .vCard file:', + 'import_upload_behaviour' => 'Import behaviour:', + 'import_upload_behaviour_add' => 'Add new contacts and skip existing', + 'import_upload_behaviour_replace' => 'Replace existing contacts', + 'import_upload_behaviour_help' => 'Replacing will replace all data found in the vCard, but will keep existing contact fields.', + 'import_report_title' => 'Importing report', + 'import_report_date' => 'Date of the import', + 'import_report_type' => 'Type of import', + 'import_report_number_contacts' => 'Number of contacts in the file', + 'import_report_number_contacts_imported' => 'Number of imported contacts', + 'import_report_number_contacts_skipped' => 'Number of skipped contacts', + 'import_report_status_imported' => 'Imported', + 'import_report_status_skipped' => 'Skipped', + 'import_vcard_parse_error' => 'Error when parsing the vCard entry', + 'import_vcard_contact_exist' => 'Contact already exists', + 'import_vcard_contact_no_firstname' => 'No first name (mandatory)', + 'import_vcard_file_not_found' => 'Tiedostoa ei löydy', + 'import_vcard_unknown_entry' => 'Tuntematon yhteystiedon nimi', + 'import_vcard_file_no_entries' => 'Tiedosto ei sisällä tietueita', + 'import_blank_title' => 'Et ole vielä tuonut yhtään yhteystietoa.', + 'import_blank_question' => 'Haluatko tuoda yhteystietoja nyt?', + 'import_blank_description' => 'Voimme tuoda vCard-tiedostoja, joita voit saada Googlen yhteystiedoista tai yhteystietojen hallinnasta.', + 'import_blank_cta' => 'Tuo vCard-yhteystietoja', + 'import_need_subscription' => 'Tietojen tuominen vaatii tilauksen.', + + 'tags_list_title' => 'Tags', + 'tags_list_description' => 'You can organize your contacts by setting up tags. Tags work like folders, but you can add more than one tag to a contact. To add a new tag, add it on the contact itself.', + 'tags_list_contact_number' => '1 contact|:count contacts', + 'tags_list_delete_success' => 'The tag has been successfully deleted', + 'tags_list_edit_success' => 'The tag has been successfully updated', + 'tags_list_delete_confirmation' => 'Are you sure you want to delete the tag? No contacts will be deleted, only the tag.', + 'tags_blank_title' => 'Tags are a great way of categorizing your contacts.', + 'tags_blank_description' => 'Tags work like folders, but you can add more than one tag to a contact. Go to a contact and tag a friend, right below the name. Once a contact is tagged, come back here to manage all the tags in your account.', + + 'api_title' => 'API access', + 'api_description' => 'The API can be used to manipulate Monica’s data from an external application, like a mobile application for instance.', + 'api_help' => 'API:n käyttä vaatii tokenin. Voit joko luoda henkilökohtaisen tokenin (Bearer authenication) tai valtuuttaa OAuth asiakkaan luoda sen sinulle. Katso API-dokumentaatio.', + 'api_endpoint' => 'Tämän Monica instanssin API osoite on:', + + 'api_personal_access_tokens' => 'Henkilökohtaiset pääsymerkit', + 'api_pao_description' => 'Make sure you give this token to a source you trust – as they allow you to access all your data.', + 'api_token_title' => 'Personal Access Tokens', + 'api_token_create_new' => 'Luo uusi API-valtuustunnus', + 'api_token_not_created' => 'You have not created any personal access tokens.', + 'api_token_name' => 'Token name', + 'api_token_expire' => 'Expires at {date}', + 'api_token_delete' => 'Poista', + 'api_token_create' => 'Create Token', + 'api_token_scopes' => 'Soveltamisalueet', + 'api_token_help' => 'Tässä on uusi henkilökohtainen pääsytunnus. Tämä on ainoa kerta kuin se näytetään joten älä menetä sitä! Voit nyt käyttää tätä tunnusta API pyyntöjen tekemiseen.', + + 'api_oauth_clients' => 'OAuth asiakkaasi', + 'api_oauth_clients_desc' => 'Tämän osion avulla voit rekisteröidä omia OAuth asiakkaita.', + 'api_oauth_clients_desc2' => 'Käytä tätä asiakastunnusta uuden tunnuksen pyytämiseen ja muunna valtuutuskoodit poletteihin. Katso Laravel Passport dokumentaatio saadaksesi lisätietoja.', + 'api_oauth_title' => 'OAuth Asiakkaat', + 'api_oauth_create_new' => 'Luo Uusi Asiakas', + 'api_oauth_edit' => 'Muokkaa Asiakasta', + 'api_oauth_not_created' => 'Et ole luonut yhtään OAuth-asiakasta.', + 'api_oauth_clientid' => 'Client ID', + 'api_oauth_name' => 'Nimi', + 'api_oauth_name_help' => 'Something your users will recognize and trust.', + 'api_oauth_secret' => 'Secret', + 'api_oauth_create' => 'Create Client', + 'api_oauth_redirecturl' => 'Redirect URL', + 'api_oauth_redirecturl_help' => 'Your application’s authorization callback URL.', + + 'api_authorized_clients' => 'List of authorized clients', + 'api_authorized_clients_desc' => 'This section lists all the clients you’ve authorized to access your application data. You can revoke this authorization at anytime.', + 'api_authorized_clients_title' => 'Authorized Applications', + 'api_authorized_clients_none' => 'There are no authorized clients yet.', + 'api_authorized_clients_name' => 'Name', + 'api_authorized_clients_scopes' => 'Scopes', + + 'personalization_tab_title' => 'Personalize your account', + + 'personalization_title' => 'Here you will find different settings to configure your account. These features are intended for “power users” who want maximum control over Monica.', + 'personalization_contact_field_type_title' => 'Contact field types', + 'personalization_contact_field_type_add' => 'Add new field type', + 'personalization_contact_field_type_description' => 'You can configure all the different types of contact fields that you can associate to all your contacts. For example, if a new social network appears in the future, you will be able to add this new way of communicating with your contacts right here.', + 'personalization_contact_field_type_table_name' => 'Name', + 'personalization_contact_field_type_table_protocol' => 'Protocol', + 'personalization_contact_field_type_table_actions' => 'Actions', + 'personalization_contact_field_type_modal_title' => 'Add a new contact field type', + 'personalization_contact_field_type_modal_edit_title' => 'Edit an existing contact field type', + 'personalization_contact_field_type_modal_delete_title' => 'Delete an existing contact field type', + 'personalization_contact_field_type_modal_delete_description' => 'Are you sure you want to delete this contact field type? Deleting this type of contact field will delete ALL the data with this type for all of your contacts.', + 'personalization_contact_field_type_modal_name' => 'Name', + 'personalization_contact_field_type_modal_protocol' => 'Protocol (optional)', + 'personalization_contact_field_type_modal_protocol_help' => 'Each new contact field type can be clickable. If a protocol is set, we will use it to trigger the action that is set.', + 'personalization_contact_field_type_modal_icon' => 'Icon (optional)', + 'personalization_contact_field_type_modal_icon_help' => 'You can associate an icon with this contact field type. You need to add a reference to a Font Awesome icon.', + 'personalization_contact_field_type_delete_success' => 'The contact field type has been successfully deleted.', + 'personalization_contact_field_type_add_success' => 'The contact field type has been successfully added.', + 'personalization_contact_field_type_edit_success' => 'The contact field type has been successfully updated.', + + 'personalization_genders_title' => 'Gender types', + 'personalization_genders_add' => 'Add new gender type', + 'personalization_genders_desc' => 'You can define as many genders as you need to. You need at least one gender type in your account.', + 'personalization_genders_modal_add' => 'Add gender type', + 'personalization_genders_modal_edit' => 'Update gender type', + 'personalization_genders_modal_name' => 'Name', + 'personalization_genders_modal_name_help' => 'The name used to display the gender on a contact page.', + 'personalization_genders_modal_sex' => 'Sex', + 'personalization_genders_modal_sex_help' => 'Used to define the relationships, and during the VCard import/export process.', + 'personalization_genders_modal_default' => 'Select the default gender for a new contact', + 'personalization_genders_modal_delete' => 'Delete gender type', + 'personalization_genders_modal_delete_desc' => 'Are you sure you want to delete the gender “{name}”?', + 'personalization_genders_modal_delete_question' => 'You currently have {count} contact with this gender. If you delete this gender, what gender should this contact have?|You currently have {count} contacts with this gender. If you delete this gender, what gender should these contacts have?', + 'personalization_genders_modal_delete_question_default' => 'This gender is the default one. If you delete this gender, which one will be the new default?', + 'personalization_genders_modal_error' => 'Please choose a gender from the list.', + 'personalization_genders_list_contact_number' => '{count} contact|{count} contacts', + 'personalization_genders_table_name' => 'Name', + 'personalization_genders_table_sex' => 'Sex', + 'personalization_genders_table_default' => 'Default', + 'personalization_genders_default' => 'Default gender', + 'personalization_genders_make_default' => 'Change default gender', + 'personalization_genders_select_default' => 'Select default gender', + 'personalization_genders_m' => 'Male', + 'personalization_genders_f' => 'Female', + 'personalization_genders_o' => 'Other', + 'personalization_genders_u' => 'Unknown', + 'personalization_genders_n' => 'None or not applicable', + + 'personalization_reminder_rule_save' => 'The change has been saved', + 'personalization_reminder_rule_title' => 'Reminder rules', + 'personalization_reminder_rule_line' => '{count} day before|{count} days before', + 'personalization_reminder_rule_desc' => 'For every reminder that you set, Monica can send you an email a number of days before the event happens. You can adjust these notification settings here. These notifications only apply to monthly and yearly reminders.', + + 'personalization_module_save' => 'The change has been saved', + 'personalization_module_title' => 'Features', + 'personalization_module_desc' => 'You may not need all of Monica’s features. Below you can toggle specific features that are used on a contact sheet. This change will affect ALL your contacts. Turning off a feature does not delete any data, it simply hides the feature.', + + 'personalisation_paid_upgrade' => 'This is a premium feature that requires a Paid subscription to be active. Upgrade your account by visiting Settings > Subscription.', + 'personalisation_paid_upgrade_vue' => 'This is a premium feature that requires a Paid subscription to be active. Upgrade your account by visiting Settings > Subscription.', + + 'reminder_time_to_send' => 'Time of the day reminders will be sent', + 'reminder_time_to_send_help' => 'Your next reminder is scheduled to be sent on {dateTime}.', + + 'personalization_activity_type_category_title' => 'Activity type categories', + 'personalization_activity_type_category_add' => 'Add a new activity type category', + 'personalization_activity_type_category_table_name' => 'Name', + 'personalization_activity_type_category_description' => 'An activity with one of your contacts can have a type and a category type. Your account comes with a set of predefined category types by default, but you can customize these here.', + 'personalization_activity_type_category_table_actions' => 'Actions', + 'personalization_activity_type_category_modal_add' => 'Add a new activity type category', + 'personalization_activity_type_category_modal_edit' => 'Edit an activity type category', + 'personalization_activity_type_category_modal_question' => 'What should we name this new category?', + 'personalization_activity_type_add_button' => 'Add a new activity type', + 'personalization_activity_type_modal_add' => 'Add a new activity type', + 'personalization_activity_type_modal_question' => 'What should we name this new activity type?', + 'personalization_activity_type_modal_edit' => 'Edit an activity type', + 'personalization_activity_type_category_modal_delete' => 'Delete an activity type category', + 'personalization_activity_type_category_modal_delete_desc' => 'Are you sure you want to delete this category? Deleting it will delete all associated activity types. Activities that belong to this category will not be affected by this deletion.', + 'personalization_activity_type_modal_delete' => 'Delete an activity type', + 'personalization_activity_type_modal_delete_desc' => 'Are you sure you want to delete this activity type? Activities that belong to this category will not be affected by this deletion.', + 'personalization_activity_type_modal_delete_error' => 'We can’t find this activity type.', + 'personalization_activity_type_category_modal_delete_error' => 'We can’t find this activity type category.', + + 'personalization_life_event_category_title' => 'Life event categories', + 'personalization_live_event_category_table_name' => 'Name', + 'personalization_life_event_category_description' => 'A life event can have a type and a category. Your account comes with a set of predefined categories and types by default, but you can customize life event types here.', + 'personalization_live_event_category_table_actions' => 'Actions', + 'personalization_life_event_type_add_button' => 'Add a new life event type', + 'personalization_life_event_type_modal_add' => 'Add a new life event type', + 'personalization_life_event_type_modal_question' => 'What should we name this new life event type?', + 'personalization_life_event_type_modal_edit' => 'Edit a life event type', + 'personalization_life_event_type_modal_delete' => 'Delete a life event type', + 'personalization_life_event_type_modal_delete_desc' => 'Are you sure you want to delete this life event type? Life events that belong to this type will be deleted by performing this action.', + 'personalization_life_event_type_modal_delete_error' => 'We can’t find this life event type.', + + 'personalization_life_event_category_work_education' => 'Work & education', + 'personalization_life_event_category_family_relationships' => 'Family & relationships', + 'personalization_life_event_category_home_living' => 'Home & living', + 'personalization_life_event_category_travel_experiences' => 'Travel & experiences', + 'personalization_life_event_category_health_wellness' => 'Health & wellness', + + 'personalization_life_event_type_new_job' => 'New job', + 'personalization_life_event_type_retirement' => 'Retirement', + 'personalization_life_event_type_new_school' => 'New school', + 'personalization_life_event_type_study_abroad' => 'Study abroad', + 'personalization_life_event_type_volunteer_work' => 'Volunteer work', + 'personalization_life_event_type_published_book_or_paper' => 'Published a book or paper', + 'personalization_life_event_type_military_service' => 'Military service', + 'personalization_life_event_type_first_met' => 'First met', + 'personalization_life_event_type_new_relationship' => 'New relationship', + 'personalization_life_event_type_engagement' => 'Engagement', + 'personalization_life_event_type_marriage' => 'Marriage', + 'personalization_life_event_type_anniversary' => 'Anniversary', + 'personalization_life_event_type_expecting_a_baby' => 'Expecting a baby', + 'personalization_life_event_type_new_child' => 'New child', + 'personalization_life_event_type_new_family_member' => 'New family member', + 'personalization_life_event_type_new_pet' => 'New pet', + 'personalization_life_event_type_end_of_relationship' => 'End of relationship', + 'personalization_life_event_type_loss_of_a_loved_one' => 'Loss of a loved one', + 'personalization_life_event_type_moved' => 'Moved', + 'personalization_life_event_type_bought_a_home' => 'Bought a home', + 'personalization_life_event_type_home_improvement' => 'Home improvement', + 'personalization_life_event_type_holidays' => 'Holidays', + 'personalization_life_event_type_new_vehicle' => 'New vehicle', + 'personalization_life_event_type_new_roommate' => 'New roommate', + 'personalization_life_event_type_overcame_an_illness' => 'Overcame an illness', + 'personalization_life_event_type_quit_a_habit' => 'Quit a habit', + 'personalization_life_event_type_new_eating_habits' => 'New eating habits', + 'personalization_life_event_type_weight_loss' => 'Weight loss', + 'personalization_life_event_type_wear_glass_or_contact' => 'Started wearing glasses or contacts', + 'personalization_life_event_type_broken_bone' => 'Broke a bone', + 'personalization_life_event_type_removed_braces' => 'Had braces removed', + 'personalization_life_event_type_surgery' => 'Had surgery', + 'personalization_life_event_type_dentist' => 'Had dental treatment', + 'personalization_life_event_type_new_sport' => 'Started playing a new sport', + 'personalization_life_event_type_new_hobby' => 'Took up a new hobby', + 'personalization_life_event_type_new_instrument' => 'Started learning a new instrument', + 'personalization_life_event_type_new_language' => 'Started learning a new language', + 'personalization_life_event_type_tattoo_or_piercing' => 'Tattoo or piercing', + 'personalization_life_event_type_new_license' => 'New license', + 'personalization_life_event_type_travel' => 'Travel', + 'personalization_life_event_type_achievement_or_award' => 'Achievement or award', + 'personalization_life_event_type_changed_beliefs' => 'Changed beliefs', + 'personalization_life_event_type_first_word' => 'First word', + 'personalization_life_event_type_first_kiss' => 'First kiss', + + 'storage_title' => 'Storage', + 'storage_account_info' => 'Your account limit is :accountLimit MB. Your current usage is :currentAccountSize MB (about :percentUsage%).', + 'storage_upgrade_notice' => 'Upgrade your account to be able to upload documents and photos.', + 'storage_description' => 'Here you can see all the documents and photos uploaded about your contacts.', + + 'dav_title' => 'WebDAV', + 'dav_description' => 'Here you can find all settings to use WebDAV resources for CardDAV and CalDAV exports.', + 'dav_copy_help' => 'Copy into your clipboard', + 'dav_clipboard_copied' => 'Value copied into your clipboard', + 'dav_url_base' => 'Base url for all CardDAV and CalDAV resources:', + 'dav_connect_help' => 'You can connect your contacts and/or calendars with this base url on you phone or computer.', + 'dav_connect_help2' => 'Use your login (email) and create an API token as the password to authenticate.', + 'dav_url_carddav' => 'CardDAV url for Contacts resource:', + 'dav_url_caldav_birthdays' => 'CalDAV url for Birthdays resources:', + 'dav_url_caldav_tasks' => 'CalDAV url for Tasks resources:', + 'dav_title_carddav' => 'CardDAV', + 'dav_title_caldav' => 'CalDAV', + 'dav_carddav_export' => 'Export all contacts in one file', + 'dav_caldav_birthdays_export' => 'Export all birthdays in one file', + 'dav_caldav_tasks_export' => 'Export all tasks in one file', + + 'archive_title' => 'Archive all of the contacts in your account', + 'archive_desc' => 'This will archive all of the contacts in your account.', + 'archive_cta' => 'Archive all of your contacts', + + 'logs_title' => 'Everything that has happened to this account', + 'logs_actor' => 'Actor', + 'logs_timestamp' => 'Timestamp', + 'logs_description' => 'Description', + 'logs_subject' => 'Subject', + 'logs_size' => 'Size (Kb)', + 'logs_object' => 'Object', +]; diff --git a/resources/lang/fi/validation.php b/resources/lang/fi/validation.php new file mode 100644 index 0000000..0153365 --- /dev/null +++ b/resources/lang/fi/validation.php @@ -0,0 +1,166 @@ + 'The :attribute must be accepted.', + 'active_url' => 'The :attribute is not a valid URL.', + 'after' => 'The :attribute must be a date after :date.', + 'after_or_equal' => 'The :attribute must be a date after or equal to :date.', + 'alpha' => 'The :attribute may only contain letters.', + 'alpha_dash' => 'The :attribute may only contain letters, numbers, dashes and underscores.', + 'alpha_num' => 'The :attribute may only contain letters and numbers.', + 'array' => 'The :attribute must be an array.', + 'before' => 'The :attribute must be a date before :date.', + 'before_or_equal' => 'The :attribute must be a date before or equal to :date.', + 'between' => [ + 'numeric' => 'The :attribute must be between :min and :max.', + 'file' => 'The :attribute must be between :min and :max kilobytes.', + 'string' => 'The :attribute must be between :min and :max characters.', + 'array' => 'The :attribute must have between :min and :max items.', + ], + 'boolean' => 'The :attribute field must be true or false.', + 'confirmed' => 'The :attribute confirmation does not match.', + 'date' => 'The :attribute is not a valid date.', + 'date_equals' => 'The :attribute must be a date equal to :date.', + 'date_format' => 'The :attribute does not match the format :format.', + 'different' => 'The :attribute and :other must be different.', + 'digits' => 'The :attribute must be :digits digits.', + 'digits_between' => 'The :attribute must be between :min and :max digits.', + 'dimensions' => 'The :attribute has invalid image dimensions.', + 'distinct' => 'The :attribute field has a duplicate value.', + 'email' => 'The :attribute must be a valid email address.', + 'ends_with' => 'The :attribute must end with one of the following: :values.', + 'exists' => 'The selected :attribute is invalid.', + 'file' => 'The :attribute must be a file.', + 'filled' => 'The :attribute field must have a value.', + 'gt' => [ + 'numeric' => 'The :attribute must be greater than :value.', + 'file' => 'The :attribute must be greater than :value kilobytes.', + 'string' => 'The :attribute must be greater than :value characters.', + 'array' => 'The :attribute must have more than :value items.', + ], + 'gte' => [ + 'numeric' => 'The :attribute must be greater than or equal :value.', + 'file' => 'The :attribute must be greater than or equal :value kilobytes.', + 'string' => 'The :attribute must be greater than or equal :value characters.', + 'array' => 'The :attribute must have :value items or more.', + ], + 'image' => 'The :attribute must be an image.', + 'in' => 'The selected :attribute is invalid.', + 'in_array' => 'The :attribute field does not exist in :other.', + 'integer' => 'The :attribute must be an integer.', + 'ip' => 'The :attribute must be a valid IP address.', + 'ipv4' => 'The :attribute must be a valid IPv4 address.', + 'ipv6' => 'The :attribute must be a valid IPv6 address.', + 'json' => 'The :attribute must be a valid JSON string.', + 'lt' => [ + 'numeric' => 'The :attribute must be less than :value.', + 'file' => 'The :attribute must be less than :value kilobytes.', + 'string' => 'The :attribute must be less than :value characters.', + 'array' => 'The :attribute must have less than :value items.', + ], + 'lte' => [ + 'numeric' => 'The :attribute must be less than or equal :value.', + 'file' => 'The :attribute must be less than or equal :value kilobytes.', + 'string' => 'The :attribute must be less than or equal :value characters.', + 'array' => 'The :attribute must not have more than :value items.', + ], + 'max' => [ + 'numeric' => 'The :attribute may not be greater than :max.', + 'file' => 'The :attribute may not be greater than :max kilobytes.', + 'string' => 'The :attribute may not be greater than :max characters.', + 'array' => 'The :attribute may not have more than :max items.', + ], + 'mimes' => 'The :attribute must be a file of type: :values.', + 'mimetypes' => 'The :attribute must be a file of type: :values.', + 'min' => [ + 'numeric' => 'The :attribute must be at least :min.', + 'file' => 'The :attribute must be at least :min kilobytes.', + 'string' => 'The :attribute must be at least :min characters.', + 'array' => 'The :attribute must have at least :min items.', + ], + 'not_in' => 'The selected :attribute is invalid.', + 'not_regex' => 'The :attribute format is invalid.', + 'numeric' => 'The :attribute must be a number.', + 'password' => 'The password is incorrect.', + 'present' => 'The :attribute field must be present.', + 'regex' => 'The :attribute format is invalid.', + 'required' => 'The :attribute field is required.', + 'required_if' => 'The :attribute field is required when :other is :value.', + 'required_unless' => 'The :attribute field is required unless :other is in :values.', + 'required_with' => 'The :attribute field is required when :values is present.', + 'required_with_all' => 'The :attribute field is required when :values are present.', + 'required_without' => 'The :attribute field is required when :values is not present.', + 'required_without_all' => 'The :attribute field is required when none of :values are present.', + 'same' => 'The :attribute and :other must match.', + 'size' => [ + 'numeric' => 'The :attribute must be :size.', + 'file' => 'The :attribute must be :size kilobytes.', + 'string' => 'The :attribute must be :size characters.', + 'array' => 'The :attribute must contain :size items.', + ], + 'starts_with' => 'The :attribute must start with one of the following: :values.', + 'string' => 'The :attribute must be a string.', + 'timezone' => 'The :attribute must be a valid zone.', + 'unique' => 'The :attribute has already been taken.', + 'uploaded' => 'The :attribute failed to upload.', + 'url' => 'The :attribute format is invalid.', + 'uuid' => 'The :attribute must be a valid UUID.', + + /* + |-------------------------------------------------------------------------- + | Custom Validation Language Lines + |-------------------------------------------------------------------------- + | + | Here you may specify custom validation messages for attributes using the + | convention "attribute.rule" to name the lines. This makes it quick to + | specify a specific custom language line for a given attribute rule. + | + */ + + 'custom' => [ + 'attribute-name' => [ + 'rule-name' => 'custom-message', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Custom Validation Attributes + |-------------------------------------------------------------------------- + | + | The following language lines are used to swap our attribute placeholder + | with something more reader friendly such as "E-Mail Address" instead + | of "email". This simply helps us make our message more expressive. + | + */ + + 'attributes' => [], + + 'vue' => [ + 'max' => [ + 'numeric' => '{field} may not be greater than {max}.', + 'string' => '{field} may not be greater than {max} characters.', + ], + 'required' => '{field} is required.', + 'url' => '{field} is not a valid URL.', + ], + +]; diff --git a/resources/lang/fr.json b/resources/lang/fr.json new file mode 100644 index 0000000..34b40e3 --- /dev/null +++ b/resources/lang/fr.json @@ -0,0 +1,7 @@ +{ + "The :attribute must contain at least one uppercase and one lowercase letter.": "Le champ :attribute doit avoir au moins une lettre majuscule et une lettre minuscule.", + "The :attribute must contain at least one letter.": "Le champ :attribute doit avoir au moins une lettre.", + "The :attribute must contain at least one symbol.": "Le champ :attribute doit avoir au moins un symbole.", + "The :attribute must contain at least one number.": "Le champ :attribute doit avoir au moins un numéro.", + "The given :attribute has appeared in a data leak. Please choose a different :attribute.": "La valeur du champ :attribute est apparue dans une fuite de données. Veuillez choisir une valeur différente." +} diff --git a/resources/lang/fr/app.php b/resources/lang/fr/app.php new file mode 100644 index 0000000..5318346 --- /dev/null +++ b/resources/lang/fr/app.php @@ -0,0 +1,571 @@ + 'Oui', + 'no' => 'Non', + 'update' => 'Mettre à jour', + 'save' => 'Sauver', + 'add' => 'Ajouter', + 'cancel' => 'Annuler', + 'confirm' => 'Confirmer', + 'delete_confirm' => 'Êtes-vous sûr(e) ?', + 'delete' => 'Supprimer', + 'edit' => 'Éditer', + 'upload' => 'Envoyer', + 'download' => 'Télécharger', + 'save_close' => 'Enregistrer et fermer', + 'close' => 'Fermer', + 'copy' => 'Copier', + 'create' => 'Créer', + 'remove' => 'Enlever', + 'revoke' => 'Révoquer', + 'done' => 'Terminé', + 'back' => 'Précédent', + 'verify' => 'Vérifier', + 'new' => 'nouveau', + 'unknown' => 'Je ne sais pas', + 'load_more' => 'Charger plus', + 'loading' => 'Chargement…', + 'with' => 'avec', + 'today' => 'aujourd’hui', + 'yesterday' => 'hier', + 'another_day' => 'un autre jour', + 'date' => 'Date', + 'type' => 'Type', + 'zoom' => 'Zoom', + 'upgrade' => 'Mettre à jour pour débloquer', + 'percent_uploaded' => '{percent}% téléchargés', + 'retry' => 'Réessayer', + 'filter' => 'Filtrer la liste', + 'go_back' => 'Revenir en arrière', + 'file_selected' => '{count} fichier sélectionné…|{count} fichiers sélectionnés…', + + 'application_title' => 'Monica – gestionnaire de relations personnelles', + 'application_description' => 'Monica est un outil pour gérer vos interactions avec vos proches, vos amis et votre famille.', + 'application_og_title' => 'Ayez de meilleures relations avec vos proches. GRC gratuit en ligne pour les amis et la famille.', + + 'markdown_description' => 'Souhaitez-vous formater votre texte d’une belle manière ? Nous supportons le format Markdown pour ajouter du gras, de l’italique, des listes et plus encore.', + 'markdown_link' => 'Lire la documentation', + + 'header_settings_link' => 'Paramètres', + 'header_logout_link' => 'Déconnexion', + 'header_changelog_link' => 'Évolutions du produit', + + 'main_nav_cta' => 'Ajouter des gens', + 'main_nav_dashboard' => 'Tableau de bord', + 'main_nav_family' => 'Contacts', + 'main_nav_journal' => 'Journal', + 'main_nav_activities' => 'Activités', + 'main_nav_tasks' => 'Tâches', + + 'footer_remarks' => 'Commentaires ?', + 'footer_send_email' => 'Envoyez nous un courriel', + 'footer_privacy' => 'Politique de confidentialité', + 'footer_release' => 'Notes de version', + 'footer_newsletter' => 'Infolettre', + 'footer_source_code' => 'Contribuer', + 'footer_version' => 'Version : :version', + 'footer_new_version' => 'Une nouvelle version de Monica est disponible', + + 'footer_modal_version_whats_new' => 'Quoi de neuf ?', + 'footer_modal_version_release_away' => 'Vous avez une version de retard par rapport à la dernière version disponible.|Vous avez :number versions de retard par rapport à la dernière version disponible. Vous devriez mettre à jour votre instance.', + + 'breadcrumb_dashboard' => 'Tableau de bord', + 'breadcrumb_list_contacts' => 'Liste de contacts', + 'breadcrumb_archived_contacts' => 'Contacts archivés', + 'breadcrumb_journal' => 'Journal', + 'breadcrumb_settings' => 'Paramètres', + 'breadcrumb_settings_export' => 'Exporter', + 'breadcrumb_settings_users' => 'Utilisateurs', + 'breadcrumb_settings_users_add' => 'Ajouter un utilisateur', + 'breadcrumb_settings_subscriptions' => 'Abonnement', + 'breadcrumb_settings_import' => 'Importer', + 'breadcrumb_settings_import_report' => 'Rapport d’import', + 'breadcrumb_settings_import_upload' => 'Téléversez', + 'breadcrumb_settings_tags' => 'Étiquettes', + 'breadcrumb_add_significant_other' => 'Ajouter un partenaire', + 'breadcrumb_edit_significant_other' => 'Mettre à jour un partenaire', + 'breadcrumb_add_note' => 'Ajouter une note', + 'breadcrumb_edit_note' => 'Modifier la note', + 'breadcrumb_api' => 'API', + 'breadcrumb_dav' => 'Ressources DAV', + 'breadcrumb_edit_introductions' => 'Comment vous vous êtes rencontrés', + 'breadcrumb_settings_personalization' => 'Personnalisation', + 'breadcrumb_settings_security' => 'Sécurité', + 'breadcrumb_settings_security_2fa' => 'Authentification à deux facteurs', + 'breadcrumb_profile' => 'Profil de :name', + + 'gender_male' => 'Homme', + 'gender_female' => 'Femme', + 'gender_none' => 'Aucun', + 'gender_no_gender' => 'Aucun genre', + + 'error_title' => 'Oups ! Une erreur est survenue.', + 'error_unauthorized' => 'Vous n’avez pas le droit de modifier cette ressource.', + 'error_user_account' => 'Cet utilisateur n’appartient pas au compte donné.', + 'error_save' => 'Une erreur est intervenue pendant la sauvegarde des données.', + 'error_try_again' => 'Une erreur s’est produite. Merci d’essayer à nouveau.', + 'error_id' => 'Erreur numéro : :id', + 'error_unavailable' => 'Service indisponible', + 'error_maintenance' => 'Maintenance en cours. On revient vite !', + 'error_help' => 'On revient tout de suite.', + 'error_twitter' => 'Suivez notre compte Twitter pour être alerté de l’évolution de la situation.', + 'error_no_term' => 'Il n’y a pas encore de politique pour cette instance.', + + 'default_save_success' => 'Les modifications ont été enregistrées.', + + 'compliance_title' => 'Désolé pour l’interruption.', + 'compliance_desc' => 'Nous avons changé nos Conditions d’Utilisation et notre Politique de Confidentialité. Nous devons vous demander de les consulter et les accepter si vous voulez continuer à utiliser votre compte.', + 'compliance_desc_end' => 'Nous ne faisons rien de méchant avec vos données ou votre compte et nous ne le ferons jamais.', + 'compliance_terms' => 'Accepter les nouvelles conditions et politique de confidentialité', + + // Relationship types + // Yes, each relationship type has 8 strings associated with it. + // This is because we need to indicate the name of the relationship type, + // and also the name of the opposite side of this relationship (father/son), + // and then, the feminine version of the string. Finally, in some sentences + // in the UI, we need to include the name of the person we add the relationship + // to. + 'relationship_type_group_love' => 'Relations amoureuses', + 'relationship_type_group_family' => 'Relations familiales', + 'relationship_type_group_friend' => 'Relations amicales', + 'relationship_type_group_work' => 'Relations de travail', + 'relationship_type_group_other' => 'Autre type de relations', + + 'relationship_type_partner' => 'conjoint', + 'relationship_type_partner_female' => 'conjointe', + 'relationship_type_partner_male' => 'conjoint', + 'relationship_type_partner_with_name' => 'conjoint de :name', + 'relationship_type_partner_female_with_name' => 'conjointe de :name', + 'relationship_type_partner_male_with_name' => 'conjoint de :name', + + 'relationship_type_spouse' => 'épouse', + 'relationship_type_spouse_female' => 'femme', + 'relationship_type_spouse_male' => 'mari', + 'relationship_type_spouse_with_name' => 'épouse de :name', + 'relationship_type_spouse_female_with_name' => 'femme de :name', + 'relationship_type_spouse_male_with_name' => 'mari de :name', + + 'relationship_type_date' => 'rendez-vous', + 'relationship_type_date_female' => 'rendez-vous', + 'relationship_type_date_male' => 'rendez-vous', + 'relationship_type_date_with_name' => 'rendez-vous de :name', + 'relationship_type_date_female_with_name' => 'rendez-vous de :name', + 'relationship_type_date_male_with_name' => 'rendez-vous de :name', + + 'relationship_type_lover' => 'amant', + 'relationship_type_lover_female' => 'amante', + 'relationship_type_lover_male' => 'amant', + 'relationship_type_lover_with_name' => 'amant de :name', + 'relationship_type_lover_female_with_name' => 'amante de :name', + 'relationship_type_lover_male_with_name' => 'amant de :name', + + 'relationship_type_inlovewith' => 'amoureux', + 'relationship_type_inlovewith_female' => 'amoureuse', + 'relationship_type_inlovewith_male' => 'amoureux', + 'relationship_type_inlovewith_with_name' => 'une personne dont :name est amoureux', + 'relationship_type_inlovewith_female_with_name' => 'une personne dont :name est amoureuse', + 'relationship_type_inlovewith_male_with_name' => 'une personne dont :name est amoureux', + + 'relationship_type_lovedby' => 'aimé par', + 'relationship_type_lovedby_female' => 'aimée par', + 'relationship_type_lovedby_male' => 'aimé par', + 'relationship_type_lovedby_with_name' => 'amant secret de :name', + 'relationship_type_lovedby_female_with_name' => 'amante secrète de :name', + 'relationship_type_lovedby_male_with_name' => 'amant secret de :name', + + 'relationship_type_ex' => 'ex-partenaire', + 'relationship_type_ex_female' => 'ex-petite amie', + 'relationship_type_ex_male' => 'ex-petit ami', + 'relationship_type_ex_with_name' => 'ex-partenaire de :name', + 'relationship_type_ex_female_with_name' => 'ex-petite amie de :name', + 'relationship_type_ex_male_with_name' => 'ex-petit ami de :name', + + 'relationship_type_parent' => 'parent', + 'relationship_type_parent_female' => 'mère', + 'relationship_type_parent_male' => 'père', + 'relationship_type_parent_with_name' => 'parent de :name', + 'relationship_type_parent_female_with_name' => 'mère de :name', + 'relationship_type_parent_male_with_name' => 'père de :name', + + 'relationship_type_child' => 'enfant', + 'relationship_type_child_female' => 'fille', + 'relationship_type_child_male' => 'fils', + 'relationship_type_child_with_name' => 'enfant de :name', + 'relationship_type_child_female_with_name' => 'fille de :name', + 'relationship_type_child_male_with_name' => 'fils de :name', + + 'relationship_type_stepparent' => 'beau-parent', + 'relationship_type_stepparent_female' => 'belle mère', + 'relationship_type_stepparent_male' => 'beau-père', + 'relationship_type_stepparent_with_name' => 'beau-parent de :name', + 'relationship_type_stepparent_female_with_name' => 'belle mère de :name', + 'relationship_type_stepparent_male_with_name' => 'beau-père de :name', + + 'relationship_type_stepchild' => 'beau-fils/fille', + 'relationship_type_stepchild_female' => 'belle fille', + 'relationship_type_stepchild_male' => 'beau-fils', + 'relationship_type_stepchild_with_name' => 'beau-fils/fille de :name', + 'relationship_type_stepchild_female_with_name' => 'belle fille de :name', + 'relationship_type_stepchild_male_with_name' => 'beau-fils de :name', + + 'relationship_type_sibling' => 'frère ou sœur', + 'relationship_type_sibling_female' => 'sœur', + 'relationship_type_sibling_male' => 'frère', + 'relationship_type_sibling_with_name' => 'frère ou sœur de :name', + 'relationship_type_sibling_female_with_name' => 'sœur de :name', + 'relationship_type_sibling_male_with_name' => 'frère de :name', + + 'relationship_type_grandparent' => 'grand-parent', + 'relationship_type_grandparent_female' => 'grand-mère', + 'relationship_type_grandparent_male' => 'grand-père', + 'relationship_type_grandparent_with_name' => 'grand-parent de :name', + 'relationship_type_grandparent_female_with_name' => 'grand-mère de :name', + 'relationship_type_grandparent_male_with_name' => 'grand-père de :name', + + 'relationship_type_grandchild' => 'petit-enfant', + 'relationship_type_grandchild_female' => 'petite-fille', + 'relationship_type_grandchild_male' => 'petit-fils', + 'relationship_type_grandchild_with_name' => 'petit-enfant de :name', + 'relationship_type_grandchild_female_with_name' => 'petite-fille de :name', + 'relationship_type_grandchild_male_with_name' => 'petit-fils de :name', + + 'relationship_type_uncle' => 'oncle', + 'relationship_type_uncle_female' => 'tante', + 'relationship_type_uncle_male' => 'oncle', + 'relationship_type_uncle_with_name' => 'oncle de :name', + 'relationship_type_uncle_female_with_name' => 'tante de :name', + 'relationship_type_uncle_male_with_name' => 'oncle de :name', + + 'relationship_type_nephew' => 'neveu', + 'relationship_type_nephew_female' => 'nièce', + 'relationship_type_nephew_male' => 'neveu', + 'relationship_type_nephew_with_name' => 'neveu de :name', + 'relationship_type_nephew_female_with_name' => 'nièce de :name', + 'relationship_type_nephew_male_with_name' => 'neveu de :name', + + 'relationship_type_cousin' => 'cousin', + 'relationship_type_cousin_female' => 'cousine', + 'relationship_type_cousin_male' => 'cousin', + 'relationship_type_cousin_with_name' => 'cousin de :name', + 'relationship_type_cousin_female_with_name' => 'cousine de :name', + 'relationship_type_cousin_male_with_name' => 'cousin de :name', + + 'relationship_type_godfather' => 'parrain ou marraine', + 'relationship_type_godfather_female' => 'marraine', + 'relationship_type_godfather_male' => 'parrain', + 'relationship_type_godfather_with_name' => 'parrain ou marraine de :name', + 'relationship_type_godfather_female_with_name' => 'marraine de :name', + 'relationship_type_godfather_male_with_name' => 'parrain de :name', + + 'relationship_type_godson' => 'filleul⋅e', + 'relationship_type_godson_female' => 'filleule', + 'relationship_type_godson_male' => 'filleul', + 'relationship_type_godson_with_name' => 'filleul⋅e de :name', + 'relationship_type_godson_female_with_name' => 'filleule de :name', + 'relationship_type_godson_male_with_name' => 'filleul de :name', + + 'relationship_type_friend' => 'ami', + 'relationship_type_friend_female' => 'amie', + 'relationship_type_friend_male' => 'ami', + 'relationship_type_friend_with_name' => 'ami de :name', + 'relationship_type_friend_female_with_name' => 'amie de :name', + 'relationship_type_friend_male_with_name' => 'ami de :name', + + 'relationship_type_bestfriend' => 'meilleur ami', + 'relationship_type_bestfriend_female' => 'meilleure amie', + 'relationship_type_bestfriend_male' => 'meilleur ami', + 'relationship_type_bestfriend_with_name' => 'meilleur ami de :name', + 'relationship_type_bestfriend_female_with_name' => 'meilleure amie de :name', + 'relationship_type_bestfriend_male_with_name' => 'meilleur ami de :name', + + 'relationship_type_colleague' => 'collègue', + 'relationship_type_colleague_female' => 'collègue', + 'relationship_type_colleague_male' => 'collègue', + 'relationship_type_colleague_with_name' => 'collègue de :name', + 'relationship_type_colleague_female_with_name' => 'collègue de :name', + 'relationship_type_colleague_male_with_name' => 'collègue de :name', + + 'relationship_type_boss' => 'patron', + 'relationship_type_boss_female' => 'patronne', + 'relationship_type_boss_male' => 'patron', + 'relationship_type_boss_with_name' => 'patron de :name', + 'relationship_type_boss_female_with_name' => 'patronne de :name', + 'relationship_type_boss_male_with_name' => 'patron de :name', + + 'relationship_type_subordinate' => 'employé', + 'relationship_type_subordinate_female' => 'employée', + 'relationship_type_subordinate_male' => 'employé', + 'relationship_type_subordinate_with_name' => 'employé de :name', + 'relationship_type_subordinate_female_with_name' => 'employée de :name', + 'relationship_type_subordinate_male_with_name' => 'employé de :name', + + 'relationship_type_mentor' => 'mentor', + 'relationship_type_mentor_female' => 'mentore', + 'relationship_type_mentor_male' => 'mentor', + 'relationship_type_mentor_with_name' => 'mentor de :name', + 'relationship_type_mentor_female_with_name' => 'mentore de :name', + 'relationship_type_mentor_male_with_name' => 'mentor de :name', + + 'relationship_type_protege' => 'protégé', + 'relationship_type_protege_female' => 'protégée', + 'relationship_type_protege_male' => 'protégé', + 'relationship_type_protege_with_name' => 'protégé de :name', + 'relationship_type_protege_female_with_name' => 'protégée de :name', + 'relationship_type_protege_male_with_name' => 'protégé de :name', + + 'relationship_type_ex_husband' => 'ex-épouse', + 'relationship_type_ex_husband_female' => 'ex-femme', + 'relationship_type_ex_husband_male' => 'ex-mari', + 'relationship_type_ex_husband_with_name' => 'ex-épouse de :name', + 'relationship_type_ex_husband_female_with_name' => 'ex-femme de :name', + 'relationship_type_ex_husband_male_with_name' => 'ex-mari de :name', + + // emotions + 'emotion_primary_love' => 'Amour', + 'emotion_primary_joy' => 'Joie', + 'emotion_primary_surprise' => 'Surprise', + 'emotion_primary_anger' => 'Colère', + 'emotion_primary_sadness' => 'Tristesse', + 'emotion_primary_fear' => 'Peur', + + 'emotion_secondary_affection' => 'Affection', + 'emotion_secondary_lust' => 'Luxure', + 'emotion_secondary_longing' => 'Nostalgie', + 'emotion_secondary_cheerfulness' => 'Bonne humeur', + 'emotion_secondary_zest' => 'Entrain', + 'emotion_secondary_contentment' => 'Satisfaction', + 'emotion_secondary_pride' => 'Fierté', + 'emotion_secondary_optimism' => 'Optimisme', + 'emotion_secondary_enthrallment' => 'Captivation', + 'emotion_secondary_relief' => 'Soulagement', + 'emotion_secondary_surprise' => 'Surprise', + 'emotion_secondary_irritation' => 'Irritation', + 'emotion_secondary_exasperation' => 'Exaspération', + 'emotion_secondary_rage' => 'Rage', + 'emotion_secondary_disgust' => 'Dégoût', + 'emotion_secondary_envy' => 'Envie', + 'emotion_secondary_suffering' => 'Souffrance', + 'emotion_secondary_sadness' => 'Tristesse', + 'emotion_secondary_disappointment' => 'Déception', + 'emotion_secondary_shame' => 'Honte', + 'emotion_secondary_neglect' => 'Négligence', + 'emotion_secondary_sympathy' => 'Sympathie', + 'emotion_secondary_horror' => 'Horreur', + 'emotion_secondary_nervousness' => 'Nervosité', + + 'emotion_adoration' => 'Adoration', + 'emotion_affection' => 'Affection', + 'emotion_love' => 'Amour', + 'emotion_fondness' => 'Tendresse', + 'emotion_liking' => 'Affectation', + 'emotion_attraction' => 'Attraction', + 'emotion_caring' => 'Soucieux', + 'emotion_tenderness' => 'Tendresse', + 'emotion_compassion' => 'Sympathie', + 'emotion_sentimentality' => 'Sentimentalité', + 'emotion_arousal' => 'Excitation', + 'emotion_desire' => 'Désir', + 'emotion_lust' => 'Luxure', + 'emotion_passion' => 'Passion', + 'emotion_infatuation' => 'Engouement', + 'emotion_longing' => 'Nostalgie', + 'emotion_amusement' => 'Amusement', + 'emotion_bliss' => 'Béatitude', + 'emotion_cheerfulness' => 'Bonne humeur', + 'emotion_gaiety' => 'Gaieté', + 'emotion_glee' => 'Allégresse', + 'emotion_jolliness' => 'Gaîté', + 'emotion_joviality' => 'Jovialité', + 'emotion_joy' => 'Joie', + 'emotion_delight' => 'Plaisir', + 'emotion_enjoyment' => 'Jouissance', + 'emotion_gladness' => 'Allégresse', + 'emotion_happiness' => 'Bonheur', + 'emotion_jubilation' => 'Jubilation', + 'emotion_elation' => 'Exultation', + 'emotion_satisfaction' => 'Satisfaction', + 'emotion_ecstasy' => 'Extase', + 'emotion_euphoria' => 'Euphorie', + 'emotion_enthusiasm' => 'Enthousiasme', + 'emotion_zeal' => 'Ferveur', + 'emotion_zest' => 'Entrain', + 'emotion_excitement' => 'Excitation', + 'emotion_thrill' => 'Frisson', + 'emotion_exhilaration' => 'Euphorie', + 'emotion_contentment' => 'Satisfaction', + 'emotion_pleasure' => 'Plaisir', + 'emotion_pride' => 'Fierté', + 'emotion_eagerness' => 'Ardeur', + 'emotion_hope' => 'Espoir', + 'emotion_optimism' => 'Optimisme', + 'emotion_enthrallment' => 'Captivation', + 'emotion_rapture' => 'Ravissement', + 'emotion_relief' => 'Soulagement', + 'emotion_amazement' => 'Stupéfaction', + 'emotion_surprise' => 'Surprise', + 'emotion_astonishment' => 'Étonnement', + 'emotion_aggravation' => 'Aggravation', + 'emotion_irritation' => 'Irritation', + 'emotion_agitation' => 'Agitation', + 'emotion_annoyance' => 'Gêne', + 'emotion_grouchiness' => 'Rogne', + 'emotion_grumpiness' => 'Mauvaise humeur', + 'emotion_exasperation' => 'Exaspération', + 'emotion_frustration' => 'Frustration', + 'emotion_anger' => 'Colère', + 'emotion_rage' => 'Rage', + 'emotion_outrage' => 'Indignation', + 'emotion_fury' => 'Fureur', + 'emotion_wrath' => 'Rage', + 'emotion_hostility' => 'Hostilité', + 'emotion_ferocity' => 'Férocité', + 'emotion_bitterness' => 'Amertume', + 'emotion_hate' => 'Haine', + 'emotion_loathing' => 'Répugnance', + 'emotion_scorn' => 'Mépris', + 'emotion_spite' => 'Dépit', + 'emotion_vengefulness' => 'Vengeance', + 'emotion_dislike' => 'Désamour', + 'emotion_resentment' => 'Ressentiment', + 'emotion_disgust' => 'Dégoût', + 'emotion_revulsion' => 'Répulsion', + 'emotion_contempt' => 'Mépris', + 'emotion_envy' => 'Envie', + 'emotion_jealousy' => 'Jalousie', + 'emotion_agony' => 'Agonie', + 'emotion_suffering' => 'Souffrance', + 'emotion_hurt' => 'Douloureux', + 'emotion_anguish' => 'Angoisse', + 'emotion_depression' => 'Dépression', + 'emotion_despair' => 'Désespoir', + 'emotion_hopelessness' => 'Désolation', + 'emotion_gloom' => 'Désespérance', + 'emotion_glumness' => 'Morose', + 'emotion_sadness' => 'Tristesse', + 'emotion_unhappiness' => 'Malheur', + 'emotion_grief' => 'Afflicion', + 'emotion_sorrow' => 'Chagrin', + 'emotion_woe' => 'Malheur', + 'emotion_misery' => 'Misère', + 'emotion_melancholy' => 'Mélancolie', + 'emotion_dismay' => 'Consternation', + 'emotion_disappointment' => 'Déception', + 'emotion_displeasure' => 'Mécontentement', + 'emotion_guilt' => 'Culpabilité', + 'emotion_shame' => 'Honte', + 'emotion_regret' => 'Regret', + 'emotion_remorse' => 'Remords', + 'emotion_alienation' => 'Aliénation', + 'emotion_isolation' => 'Isolement', + 'emotion_neglect' => 'Négligence', + 'emotion_loneliness' => 'Solitude', + 'emotion_rejection' => 'Rejet', + 'emotion_homesickness' => 'Mal du pays', + 'emotion_defeat' => 'Échec', + 'emotion_dejection' => 'Découragement', + 'emotion_insecurity' => 'Insécurité', + 'emotion_embarrassment' => 'Embarras', + 'emotion_humiliation' => 'Humiliation', + 'emotion_insult' => 'Insulte', + 'emotion_pity' => 'Dommage', + 'emotion_sympathy' => 'Sympathie', + 'emotion_alarm' => 'Inquiet', + 'emotion_shock' => 'Choc', + 'emotion_fear' => 'Peur', + 'emotion_fright' => 'Frayeur', + 'emotion_horror' => 'Horreur', + 'emotion_terror' => 'Terreur', + 'emotion_panic' => 'Panique', + 'emotion_hysteria' => 'Hystérie', + 'emotion_mortification' => 'Humilié', + 'emotion_anxiety' => 'Anxiété', + 'emotion_nervousness' => 'Nervosité', + 'emotion_tenseness' => 'Tension', + 'emotion_uneasiness' => 'Malaise', + 'emotion_apprehension' => 'Appréhension', + 'emotion_worry' => 'Inquiétude', + 'emotion_distress' => 'Détresse', + 'emotion_dread' => 'Effroi', + + // weather + 'weather_sunny' => 'Ensoleillé', + 'weather_clear' => 'Clair', + 'weather_clear-day' => 'Clair', + 'weather_clear-night' => 'Nuit claire', + 'weather_light-drizzle' => 'Légere bruine', + 'weather_patchy-light-drizzle' => 'Bruine légère par endroit', + 'weather_patchy-light-rain' => 'Faible pluie éparse', + 'weather_light-rain' => 'Légère pluie', + 'weather_moderate-rain-at-times' => 'Pluie modérée par moment', + 'weather_moderate-rain' => 'Pluie modérée', + 'weather_patchy-rain-possible' => 'Pluie éparse possible', + 'weather_heavy-rain-at-times' => 'Forte pluie épisodique', + 'weather_heavy-rain' => 'Forte pluie', + 'weather_light-freezing-rain' => 'Légère pluie verglaçante', + 'weather_moderate-or-heavy-freezing-rain' => 'Pluie verglaçante modérée ou forte', + 'weather_light-sleet' => 'Neige légèrement fondue', + 'weather_moderate-or-heavy-rain-shower' => 'Averse modérée ou forte', + 'weather_light-rain-shower' => 'Légere averse de pluie', + 'weather_torrential-rain-shower' => 'Averse torrentielle', + 'weather_rain' => 'Pluie', + 'weather_snow' => 'Neige', + 'weather_blowing-snow' => 'Neige poudreuse', + 'weather_patchy-light-snow' => 'Neige légère éparse', + 'weather_light-snow' => 'Neige légère', + 'weather_patchy-moderate-snow' => 'Neige modérée inégale', + 'weather_moderate-snow' => 'Neige modérée', + 'weather_patchy-heavy-snow' => 'Forte neige éparse', + 'weather_heavy-snow' => 'Forte neige', + 'weather_light-snow-showers' => 'Légères averses de neige', + 'weather_moderate-or-heavy-snow-showers' => 'Averses de neige modérées a fortes', + 'weather_patchy-snow-possible' => 'Neige éparse possible', + 'weather_patchy-sleet-possible' => 'Neige fondue éparse possible', + 'weather_moderate-or-heavy-sleet' => 'Neige modérée ou forte', + 'weather_light-sleet-showers' => 'Légeres averses de neige fondue', + 'weather_moderate-or-heavy-sleet-showers' => 'Averses de neige fondue modérées a fortes', + 'weather_sleet' => 'Neige fondue', + 'weather_wind' => 'Vent', + 'weather_fog' => 'Brouillard', + 'weather_freezing-fog' => 'Brouillard givrant', + 'weather_mist' => 'Brouillard', + 'weather_blizzard' => 'Tempête de neige', + 'weather_overcast' => 'Couvert', + 'weather_cloudy' => 'Nuageux', + 'weather_partly-cloudy-day' => 'Partiellement nuageux', + 'weather_partly-cloudy-night' => 'Partiellement nuageux', + 'weather_freezing-drizzle' => 'Bruine verglaçante', + 'weather_heavy-freezing-drizzle' => 'Forte bruine verglaçante', + 'weather_patchy-freezing-drizzle-possible' => 'Bruine verglaçante par endroit', + 'weather_ice-pellets' => 'Grêle', + 'weather_light-showers-of-ice-pellets' => 'Averses éparses de grêle', + 'weather_moderate-or-heavy-showers-of-ice-pellets' => 'Averses modérées ou fortes de grêle', + 'weather_thundery-outbreaks-possible' => 'Orages possibles', + 'weather_patchy-light-rain-with-thunder' => 'Pluie légère éparse avec tonnerre', + 'weather_moderate-or-heavy-rain-with-thunder' => 'Pluie modérée ou forte avec tonnerre', + 'weather_patchy-light-snow-with-thunder' => 'Pluie légère éparse avec tonnerre', + 'weather_moderate-or-heavy-snow-with-thunder' => 'Neige modérée ou forte avec tonnerre', + 'weather_current_temperature_celsius' => ':temperature °C', + 'weather_current_temperature_fahrenheit' => ':temperature °F', + 'weather_current_title' => 'Météo actuelle', + + // dav + 'dav_contacts' => 'Contacts', + 'dav_contacts_description' => 'Contacts de :name', + 'dav_birthdays' => 'Anniversaires', + 'dav_birthdays_description' => 'Anniversaires des contacts de :name', + 'dav_tasks' => 'Tâches', + 'dav_tasks_description' => 'Tâches de :name', + + // contact list + 'contact_list_avatar' => 'Avatar', + 'contact_list_name' => 'Contact', + 'contact_list_description' => 'Description', + +]; diff --git a/resources/lang/fr/auth.php b/resources/lang/fr/auth.php new file mode 100644 index 0000000..18e2744 --- /dev/null +++ b/resources/lang/fr/auth.php @@ -0,0 +1,89 @@ + 'Ces identifiants ne correspondent pas à nos enregistrements.', + 'throttle' => 'Tentatives de connexion trop nombreuses. Veuillez essayer de nouveau dans :seconds secondes.', + 'not_authorized' => 'Vous n’êtes pas autorisé à exécuter cette action', + 'signup_disabled' => 'L’inscription est actuellement désactivée', + 'signup_error' => 'Une erreur est survenue lors de l’ajout de l’utilisateur', + 'back_homepage' => 'Retour à la page d’accueil', + 'mfa_auth_otp' => 'S’authentifier avec votre dispositif à deux facteurs', + 'mfa_auth_webauthn' => 'Authentifier avec une clé de sécurité (WebAuthn)', + '2fa_title' => 'Authentification à deux facteurs', + '2fa_wrong_validation' => 'L’authentification à deux facteurs a échoué.', + '2fa_one_time_password' => 'Code d’authentification à deux facteurs', + '2fa_recuperation_code' => 'Entrez le code de récupération de deux facteurs', + '2fa_one_time_or_recuperation' => 'Entrez un code d’authentification à deux facteurs ou un code de récupération', + '2fa_otp_help' => 'Ouvrez votre application mobile pour l’authentification à deux facteurs et copiez le Qr code suivant', + + 'login_to_account' => 'Connectez-vous à votre compte', + 'login_with_recovery' => 'Connexion avec un code de récupération', + 'login_again' => 'Merci de vous connecter à nouveau à votre compte', + 'email' => 'Courriel', + 'password' => 'Mot de passe', + 'recovery' => 'Code de récupération', + 'login' => 'Connexion', + 'button_remember' => 'Se souvenir de moi', + 'password_forget' => 'Mot de passe oublié ?', + 'password_reset' => 'Réinitialisez votre mot de passe', + 'use_recovery' => 'Ou vous pouvez utiliser un code de récupération', + 'signup_no_account' => 'Vous n’avez pas de compte ?', + 'signup' => 'S’inscrire', + 'create_account' => 'Créer le premier compte en vous enregistrant', + 'change_language_title' => 'Changer la langue :', + 'change_language' => 'Afficher la page en :lang', + + 'password_reset_title' => 'Réinitialiser le mot de passe', + 'password_reset_email' => 'Adresse courriel', + 'password_reset_send_link' => 'Envoyer un lien pour réinitialiser le mot de passe', + 'password_reset_password' => 'Mot de passe', + 'password_reset_password_confirm' => 'Confirmez le mot de passe', + 'password_reset_action' => 'Réinitialiser le mot de passe', + 'password_reset_email_content' => 'Cliquez ici pour réinitialiser votre mot de passe :', + + 'register_title_welcome' => 'Bienvenue à votre nouvelle instance Monica', + 'register_create_account' => 'Vous devez créer un compte pour utiliser Monica', + 'register_title_create' => 'Créez votre compte Monica', + 'register_login' => 'Connectez-vous si vous avez déjà un compte.', + 'register_email' => 'Entrez une adresse courriel valide', + 'register_email_example' => 'vous@maison', + 'register_firstname' => 'Prénom', + 'register_firstname_example' => 'ex : Pierre', + 'register_lastname' => 'Nom de famille', + 'register_lastname_example' => 'ex : Dupont', + 'register_password' => 'Mot de passe', + 'register_password_example' => 'Entrez un mot de passe sécurisé', + 'register_password_confirmation' => 'Confirmez le mot de passe', + 'register_action' => 'Enregistrement', + 'register_policy' => 'L’inscription signifie vous avez lu et acceptez notre Politique de Confidentialité et nos Conditions d’Utilisation.', + 'register_invitation_email' => 'Pour des raisons de sécurité, merci d’indiquer l’adresse courriel de la personne qui vous a invité à joindre son compte. Cette information est indiquée dans le courriel d’invitation.', + + 'confirmation_title' => 'Vérifiez votre adresse courriel', + 'confirmation_fresh' => 'Un nouveau lien de vérification a été envoyé à votre adresse courriel.', + 'confirmation_check' => 'Avant de continuer, veuillez vérifier votre boîte mail pour un lien de vérification.', + 'confirmation_request_another' => 'Si vous n’avez pas reçu le courriel cliquez ici pour en demander un autre.', + + 'confirmation_again' => 'Si vous souhaitez modifier votre adresse courriel vous pouvez cliquer ici.', + 'email_change_current_email' => 'Adresse courriel actuelle :', + 'email_change_title' => 'Modifier votre adresse courriel', + 'email_change_new' => 'Nouvelle adresse courriel', + 'email_changed' => 'Votre adresse courriel a été modifée. Vérifiez votre boîte aux lettres pour la valider.', +]; diff --git a/resources/lang/fr/changelog.php b/resources/lang/fr/changelog.php new file mode 100644 index 0000000..0e68e45 --- /dev/null +++ b/resources/lang/fr/changelog.php @@ -0,0 +1,12 @@ + 'Évolutions du produit', + 'note' => 'Remarque : malheureusement, cette page est uniquement en Anglais.', +]; diff --git a/resources/lang/fr/dashboard.php b/resources/lang/fr/dashboard.php new file mode 100644 index 0000000..759ee0d --- /dev/null +++ b/resources/lang/fr/dashboard.php @@ -0,0 +1,42 @@ + 'Bienvenue chez vous !', + 'dashboard_blank_description' => 'Monica est l’endroit pour organiser toutes les interactions que vous avez avec ceux qui vous sont chers.', + 'dashboard_blank_cta' => 'Ajoutez votre premier contact', + 'dashboard_blank_illustration' => 'Illustration par Freepik', + + 'notes_title' => 'Vous n’avez pas encore de note favorite.', + + 'tab_recent_calls' => 'Appels récents', + 'tab_favorite_notes' => 'Notes favorites', + 'tab_calls_blank' => 'Vous n’avez encore enregistré aucun appel.', + 'tab_debts' => 'Dettes', + 'tab_debts_blank' => 'Vous n’avez encore enregistré aucune dette.', + 'tab_tasks' => 'Tâches', + 'tab_tasks_blank' => 'Vous n’avez encore aucune tâche.', + + 'tasks_add_task_placeholder' => 'En quoi consiste cette tâche ?', + 'tasks_tab_your_contacts' => 'Tâches liées à vos contacts', + 'tasks_tab_your_tasks' => 'Vos tâches', + 'tasks_add_note' => 'Appuyez sur Entrée pour ajouter la tâche.', + 'task_add_cta' => 'Ajouter une tâche', + + 'debts_you_owe' => 'Vous devez', + + 'statistics_contacts' => 'Contacts', + 'statistics_activities' => 'Activités', + 'statistics_gifts' => 'Cadeaux', + + 'reminders_next_months' => 'Évènements dans les 3 prochains mois', + 'reminders_none' => 'Aucun rappel pour ce mois-ci.', + + 'product_changes' => 'Évolutions du produit', + 'product_view_details' => 'Afficher les détails', +]; diff --git a/resources/lang/fr/format.php b/resources/lang/fr/format.php new file mode 100644 index 0000000..e6a0ad7 --- /dev/null +++ b/resources/lang/fr/format.php @@ -0,0 +1,36 @@ + 'd M Y H:i', + 'short_date_year' => 'd M Y', + 'short_date' => 'd M', + 'short_month' => 'M', + 'short_month_year' => 'M Y', + 'short_day' => 'D', + 'full_date_year' => 'd F Y', + 'full_month' => 'F', + 'full_month_year' => 'F Y', + 'full_hour' => 'H:i', + + 'short_text' => '{text}…', +]; diff --git a/resources/lang/fr/journal.php b/resources/lang/fr/journal.php new file mode 100644 index 0000000..097b39d --- /dev/null +++ b/resources/lang/fr/journal.php @@ -0,0 +1,38 @@ + 'Comment s’est passé votre journée ? Vous pouvez voter une fois par jour.', + 'journal_come_back' => 'Merci. Revenez demain pour voter à nouveau.', + 'journal_description' => 'Note : le journal liste les entrées manuelles, ainsi que les entrées automatiques comme les activités que vous faites avec vos contacts. Bien que vous puissiez supprimer les entrées manuelles, vous devrez supprimer les activités directement de la page du contact pour les supprimer du journal.', + 'journal_add' => 'Ajouter une entrée', + 'journal_edit' => 'Éditer une entrée de journal', + 'journal_empty' => 'Journal vide', + 'journal_created_at' => 'Créé le {date}', + 'journal_created_automatically' => 'Créée automatiquement', + 'journal_entry_type_journal' => 'Note de journal', + 'journal_entry_type_activity' => 'Activité', + 'journal_entry_rate' => 'Vous avez évalué votre journée.', + 'journal_add_comment' => 'Ajouter un commentaire (optionnel) ?', + 'journal_show_comment' => 'Afficher le commentaire', + 'entry_delete_success' => 'L’entrée a été supprimée avec succès.', + 'journal_add_title' => 'Titre (optionnel)', + 'journal_add_date' => 'Date', + 'journal_add_post' => 'Entrée', + 'journal_add_cta' => 'Sauvegarder', + 'journal_blank_cta' => 'Ajouter votre première entrée dans le journal', + 'journal_blank_description' => 'Le journal vous permet de vous rappeler d’évènements passés, ou à venir.', + 'delete_confirmation' => 'Êtes-vous sûr de vouloir supprimer cette entrée ?', + 'apply_filter' => 'Apply filter', + 'start_date' => 'Start Date', + 'end_date' => 'End Date', + 'per_page' => 'Per Page', + 'sort_order' => 'Sort By Created At', + 'ascending' => 'Ascending', + 'descending' => 'Descending', +]; diff --git a/resources/lang/fr/logs.php b/resources/lang/fr/logs.php new file mode 100644 index 0000000..86f8fa9 --- /dev/null +++ b/resources/lang/fr/logs.php @@ -0,0 +1,29 @@ + 'A créé un contact.', + 'settings_log_contact_created_with_name' => 'Ajout de :name en tant que contact.', + + // contat description update + 'contact_log_contact_description_updated' => 'Modification de la description.', + 'settings_log_contact_description_updated_with_name' => 'Modification de la description de :name.', + + // contact description clear + 'contact_log_contact_description_cleared' => 'Suppression de la description.', + 'settings_log_contact_description_cleared_with_name' => 'Suppression de la description de :name.', + + // contact work information update + 'contact_log_contact_work_updated' => 'Modification des informations professionnelles.', + 'settings_log_contact_work_updated_with_name' => 'Modification des informations professionnelles de :name.', + + // company created + 'settings_log_company_created' => 'Création d’une société nommée :name.', +]; diff --git a/resources/lang/fr/mail.php b/resources/lang/fr/mail.php new file mode 100644 index 0000000..ecc620b --- /dev/null +++ b/resources/lang/fr/mail.php @@ -0,0 +1,53 @@ + 'Rappel pour :contact', + 'greetings' => 'Bonjour :username', + 'want_reminded_of' => 'Vous souhaitez être rappelé de :reason', + 'for' => 'Pour : :name', + 'comment' => 'Commentaire : :comment', + 'footer_contact_info' => 'Ajouter, afficher, compléter et modifier les informations sur ce contact :', + 'footer_contact_info2' => 'Voir le profil de :name', + 'footer_contact_info2_link' => 'Voir le profil de :name : :url', + + 'notification_subject_line' => 'Vous avez un évènement à venir', + 'notification_description' => 'Dans :count jours (le :date), l’évènement suivant se produira :', + + 'stay_in_touch_subject_line' => 'Rester en contact avec :name', + 'stay_in_touch_subject_description' => 'Vous avez demandé à être rappelé de rester en contact avec :name tous les :frequency jour.|Vous avez demandé à être rappelé de rester en contact avec :name tous les :frequency jours.', + + 'notifications_whoops' => 'Oups !', + 'notifications_hello' => 'Bonjour !', + 'notifications_regards' => 'Cordialement', + 'notifications_footer' => 'Si vous rencontrez des problèmes en cliquant sur le bouton « :actionText », copiez et collez l’URL ci-dessous dans votre navigateur web : [:actionURL](:actionURL)', + 'notifications_rights' => 'Tous droits réservés', + + 'confirmation_email_title' => 'Monica – vérification d’adresse courriel', + 'confirmation_email_intro'=> 'Pour valider votre adresse courriel, cliquez sur le bouton ci-dessous', + 'confirmation_email_button' => 'Vérifiez l’adresse courriel', + 'confirmation_email_bottom' => 'Si vous n’avez pas créé de compte, aucune autre action n’est requise.', + + 'password_reset_title' => 'Monica – Réinitialisation du mot de passe', + 'password_reset_intro' => 'Vous recevez ce courriel car nous avons reçu une demande de réinitialisation de mot de passe pour votre compte.', + 'password_reset_button' => 'Réinitialiser le mot de passe', + 'password_reset_expiration' => 'Ce lien de réinitialisation du mot de passe expirera dans :count minutes.', + 'password_reset_bottom' => 'Si vous n’avez pas demandé de réinitialisation du mot de passe, aucune autre action n’est requise.', + + 'invitation_title' => 'Monica – Vous êtes invité par :name', + 'invitation_intro' => 'Vous avez été invité par :name (:email) à utiliser Monica, un outil de gestion de relations personnelles.', + 'invitation_link' => 'Pour accepter l’invitation, cliquez sur le lien ci-dessous :', + 'invitation_button' => 'Accepter l’invitation', + 'invitation_expiration' => 'Ce lien expirera dans :count jours.', + + 'export_title' => 'Votre export est prêt', + 'export_description' => 'Vous avez demandé un export de données le :date. Il est maintenant prêt à être téléchargé.', + 'export_download' => 'Télécharger l’export', + +]; diff --git a/resources/lang/fr/pagination.php b/resources/lang/fr/pagination.php new file mode 100644 index 0000000..9ba269a --- /dev/null +++ b/resources/lang/fr/pagination.php @@ -0,0 +1,25 @@ + '❮ Précédent', + 'next' => 'Suivant ❯', + +]; diff --git a/resources/lang/fr/passwords.php b/resources/lang/fr/passwords.php new file mode 100644 index 0000000..08491a6 --- /dev/null +++ b/resources/lang/fr/passwords.php @@ -0,0 +1,30 @@ + 'Votre mot de passe a été réinitialisé !', + 'sent' => 'Nous vous avons envoyé par courriel le lien de réinitialisation du mot de passe.', + 'token' => 'Ce jeton de réinitialisation du mot de passe n’est pas valide.', + 'user' => 'Si l’adresse email que vous avez entrée existe dans notre base de données, un couriel de réinitialisation du mot de passe vous a été envoyé.', + 'changed' => 'Mot de passe changé avec succès.', + 'invalid' => 'Le mot de passe que vous avez saisi est incorrect.', + 'throttled' => 'Merci de patienter avant de réessayer.', + +]; diff --git a/resources/lang/fr/people.php b/resources/lang/fr/people.php new file mode 100644 index 0000000..1c7df10 --- /dev/null +++ b/resources/lang/fr/people.php @@ -0,0 +1,539 @@ + 'Contact non trouvé', + 'people_list_number_kids' => ':count enfant|:count enfants', + 'people_list_last_updated' => 'Dernière consultation :', + 'people_list_number_reminders' => ':count rappel|:count rappels', + 'people_list_blank_title' => 'Vous n’avez encore ajouté aucun contact', + 'people_list_blank_cta' => 'Ajouter quelqu’un', + 'people_list_sort' => 'Tri', + 'people_list_stats' => ':count contact|:count contacts', + 'people_list_firstnameAZ' => 'Tri par prénom A → Z', + 'people_list_firstnameZA' => 'Tri par prénom Z → A', + 'people_list_lastnameAZ' => 'Tri par nom de famille A → Z', + 'people_list_lastnameZA' => 'Tri par nom de famille Z → A', + 'people_list_lastactivitydateNewtoOld' => 'Trier par date de dernière activité, du plus récent au plus ancien', + 'people_list_lastactivitydateOldtoNew' => 'Trier par date de dernière activité, du plus ancien au plus récent', + 'people_list_filter_tag' => 'Affichage des contacts avec l’étiquette', + 'people_list_clear_filter' => 'Enlever le filtre', + 'people_list_contacts_per_tags' => ':count contact|:count contacts', + 'people_list_show_dead' => 'Afficher les contacts décédés (:count)', + 'people_list_hide_dead' => 'Masquer les contacts décédés (:count)', + 'people_search' => 'Recherchez dans vos contacts…', + 'people_search_no_results' => 'Aucun résultat', + 'people_search_next' => 'Suivant', + 'people_search_prev' => 'Précédent', + 'people_search_rows_per_page' => 'Résultats par page', + 'people_search_of' => 'sur', + 'people_search_page' => 'Page', + 'people_search_all' => 'Tous', + 'people_add_new' => 'Ajouter une nouvelle personne', + 'people_list_account_usage' => 'Votre utilisation de compte : :current/:limit contacts', + 'people_list_account_upgrade_title' => 'Passez au plan supérieur pour débloquer votre compte et l’amener à son plein potentiel.', + 'people_list_account_upgrade_cta' => 'Passez au plan supérieur', + 'people_list_untagged' => 'Afficher les contacts sans étiquette', + 'people_list_filter_untag' => 'Afficher les contacts sans aucune étiquette', + 'archived_contact_readonly' => 'Un contact archivé ne peut pas être modifié, veuillez d’abord le désarchiver.', + + // people add + 'people_add_title' => 'Ajouter une nouvelle personne', + 'people_add_missing' => 'Aucune personne trouvée – ajouter une nouvelle personne maintenant', + 'people_add_firstname' => 'Prénom', + 'people_add_middlename' => 'Deuxième prénom (optionnel)', + 'people_add_lastname' => 'Nom de famille (optionnel)', + 'people_add_email' => 'Courriel (optionnel)', + 'people_add_nickname' => 'Surnom (optionnel)', + 'people_add_cta' => 'Ajouter', + 'people_save_and_add_another_cta' => 'Sauver et ajouter un autre contact', + 'people_add_success' => ':name a été crée avec succès', + 'people_add_gender' => 'Genre', + 'people_delete_success' => 'Le contact a été supprimé', + 'people_delete_message' => 'Supprimer le contact', + 'people_delete_confirmation' => 'Êtes-vous sûr⋅e de vouloir supprimer le contact de :name ? La suppression est immédiate et permanente.', + 'people_add_birthday_reminder' => 'Souhaiter un bon anniversaire à :name', + 'people_add_birthday_reminder_deceased' => 'À cette date, :name aurait célébré son anniversaire', + 'people_add_import' => 'Souhaitez-vous importer vos contacts ?', + 'people_edit_email_error' => 'Il y a déjà quelqu’un dans votre compte avec cette adresse courriel. Merci d’en choisir une autre.', + 'people_export' => 'Exporter en tant que vCard', + 'people_add_reminder_for_birthday' => 'Créer un rappel annuel d’anniversaire', + + // show + 'section_contact_information' => 'Coordonnées', + 'section_personal_activities' => 'Activités', + 'section_personal_reminders' => 'Rappels', + 'section_personal_tasks' => 'Tâches', + 'section_personal_gifts' => 'Cadeaux', + 'section_personal_notes' => 'Notes', + + // archived contacts + 'list_link_to_active_contacts' => 'Vous visualisez les contacts archivés. Afficher la liste des contacts actifs à la place.', + 'list_link_to_archived_contacts' => 'Liste de contacts archivés', + + // Header + 'me' => 'C’est vous', + 'edit_contact_information' => 'Mettre à jour les coordonnées', + 'contact_archive' => 'Archiver le contact', + 'contact_unarchive' => 'Désarchiver le contact', + 'contact_archive_help' => 'Les contacts archivés n’apparaîtront pas sur la liste de contacts, mais apparaîtront toujours dans les résultats de recherches.', + 'call_button' => 'Enregistrer un appel téléphonique', + 'set_favorite' => 'Les contacts favoris sont placés en haut de la liste des contacts', + + // Stay in touch + 'stay_in_touch' => 'Restez en contact', + 'stay_in_touch_frequency' => 'Rester en contact chaque jour|Rester en contact chaque jour|Rester en contact tous les {count} jours', + 'stay_in_touch_next_date' => 'Prochaine échéance : {date}', + 'stay_in_touch_invalid' => 'La fréquence doit être un nombre supérieur à 0.', + 'stay_in_touch_premium' => 'Vous devez mettre à jour votre compte pour pouvoir profiter de cette fonctionnalité', + 'stay_in_touch_modal_title' => 'Restez en contact', + 'stay_in_touch_modal_desc' => 'Nous pouvons vous rappeler par courriel pour rester en contact avec {firstname} à intervalle régulier.', + 'stay_in_touch_modal_label' => 'Envoyez-moi un courriel tous les… {count} jour|Envoyez-moi un courriel tous les… {count} jours', + + // Calls + 'modal_call_title' => 'Enregistrer un appel téléphonique', + 'modal_call_comment' => 'De quoi avez-vous parlé ? (optionnel)', + 'modal_call_exact_date' => 'L’appel s’est passé le', + 'modal_call_who_called' => 'Qui a appelé ?', + 'modal_call_emotion' => 'Voulez vous enregistrer ce que vous avez ressenti au cours de cet appel ? (optionnel)', + 'calls_add_success' => 'L’appel téléphonique a été enregistré.', + 'call_delete_confirmation' => 'Êtes-vous sûr de vouloir supprimer cet appel ?', + 'call_delete_success' => 'L’appel a été supprimé avec succès', + 'call_title' => 'Appels téléphoniques', + 'call_empty_comment' => 'Aucun details', + 'call_blank_title' => 'Gardez la trace des appels téléphoniques que vous avez fait avec {name}', + 'call_blank_desc' => 'Vous avez appelé {name}', + 'call_you_called' => 'Vous avez appelé', + 'call_he_called' => '{name} a appelé', + 'call_emotions' => 'Émotions :', + + // Conversation + 'conversation_blank' => 'Enregistrer les discussions que vous avez avec :name sur les réseaux sociaux, par SMS, etc.', + 'conversation_delete_link' => 'Supprimer la discussion', + 'conversation_edit_title' => 'Éditer la discussion', + 'conversation_edit_delete' => 'Êtes-vous sûr de vouloir supprimer la discussion ? La suppression est permanente.', + 'conversation_add_success' => 'La discussion a été ajoutée avec succès.', + 'conversation_edit_success' => 'La discussion a été mise à jour avec succès.', + 'conversation_delete_success' => 'La discussion a été supprimée avec succès.', + 'conversation_add_title' => 'Enregistrer une discussion', + 'conversation_add_when' => 'Quand avez-vous eu cette discussion ?', + 'conversation_add_who_wrote' => 'Qui a écrit ce message ?', + 'conversation_add_how' => 'Comment avez-vous communiqué ?', + 'conversation_add_you' => 'Vous', + 'conversation_add_content' => 'Écrivez ce que vous avez dit', + 'conversation_add_what_was_said' => 'Qu’avez-vous dit ?', + 'conversation_add_another' => 'Ajoutez un nouveau message', + 'conversation_add_error' => 'Vous devez ajouter au moins un message.', + 'conversation_list_table_messages' => 'Messages', + 'conversation_list_table_content' => 'Contenu partiel (dernier message)', + 'conversation_list_title' => 'Discussions', + 'conversation_list_cta' => 'Journal de conversation', + + // age - birthday + 'birthdate_not_set' => 'La date de naissance n’est pas définie', + 'age_approximate_in_years' => 'env. :age ans', + 'age_exact_in_years' => ':age ans', + 'age_exact_birthdate' => 'né le :date', + + // Last called + 'last_called' => 'Dernier appel : :date', + 'last_talked_to' => 'Dernier appel : {date}', + 'last_called_empty' => 'Dernier appel : inconnu', + 'last_activity_date' => 'Dernière activité ensemble : :date', + 'last_activity_date_empty' => 'Dernière activité ensemble : inconnu', + + // additional information + 'information_edit_success' => 'Le profil a été mis à jour avec succès', + 'information_edit_title' => 'Mettre à jour les informations personnelles de :name', + 'information_edit_max_size' => 'Maximum :size Ko.', + 'information_edit_max_size2' => 'Maximum {size} Ko.', + 'information_edit_firstname' => 'Prénom', + 'information_edit_lastname' => 'Nom de famille (optionnel)', + 'information_edit_description' => 'Description (optionnel)', + 'information_edit_description_help' => 'Utilisé sur la liste de contacts pour ajouter un contexte, si nécessaire.', + 'information_edit_unknown' => 'Je ne connais pas son âge', + 'information_edit_probably' => 'Cette personne a probablement…', + 'information_edit_not_year' => 'Je connais le jour et le mois de l’anniversaire de cette personne, mais pas l\'année…', + 'information_edit_exact' => 'Je connais la date d’anniversaire exacte de cette personne…', + 'information_edit_birthdate_label' => 'Date d’anniversaire', + 'information_no_work_defined' => 'Aucune information professionnelle définie', + 'information_work_at' => 'chez :company', + 'work_add_cta' => 'Mettre à jour les informations professionnelles', + 'work_edit_success' => 'Informations professionnelles mises à jour', + 'work_edit_title' => 'Mettre à jour les informations professionnelles de :name', + 'work_edit_job' => 'Poste (optionnel)', + 'work_edit_company' => 'Entreprise (optionnel)', + 'work_information' => 'Information sur le travail', + + // food preferences + 'food_preferences_add_success' => 'Les préférences alimentaires ont été mises à jour.', + 'food_preferences_edit_description' => 'Peut-être que :firstname ou quelqu’un dans la famille :family a une allergie. Ou peut-être qu’il n’aime pas un vin spécifique. Indiquez ici ses préférences alimentaires afin que vous vous en rappeliez la prochaine fois que vous l’inviterez à dîner', + 'food_preferences_edit_description_no_last_name' => 'Peut-être que :firstname a une allergie. Ou peut-être qu’il n’aime pas un vin spécifique. Indiquez ici ses préférences alimentaires afin que vous vous en rappeliez la prochaine fois que vous l’inviterez à dîner', + 'food_preferences_edit_title' => 'Modification des préférences alimentaires', + 'food_preferences_edit_cta' => 'Enregistrer les préférences alimentaires', + 'food_preferences_title' => 'Préférences alimentaires', + 'food_preferences_cta' => 'Ajouter des préférences alimentaires', + + // reminders + 'reminders_blank_title' => 'De quoi souhaitez-vous être rappelé à propos de :name ?', + 'reminders_blank_add_activity' => 'Ajouter un rappel', + 'reminders_add_title' => 'De quoi souhaitez-vous être rappelé à propos de :name ?', + 'reminders_add_description' => 'Merci de me tenir informé de…', + 'reminders_add_next_time' => 'Quand voulez-vous être rappelé à propos de ceci ?', + 'reminders_add_once' => 'Rappelez-moi juste une fois', + 'reminders_add_recurrent' => 'Rappelez-moi tous les', + 'reminders_add_starting_from' => 'à compter de la date définie ci-après', + 'reminders_add_cta' => 'Ajouter le rappel', + 'reminders_edit_update_cta' => 'Mettre à jour le rappel', + 'reminders_add_error_custom_text' => 'Vous devez indiquer un texte pour ce rappel.', + 'reminders_create_success' => 'Le rappel a été ajouté avec succès.', + 'reminders_delete_success' => 'Le rappel a été supprimé avec succès.', + 'reminders_update_success' => 'Le rappel a été mis à jour avec succès', + 'reminders_add_optional_comment' => 'Commentaire (optionnel)', + + 'reminder_frequency_day' => 'chaque jour|chaque :number jours', + 'reminder_frequency_week' => 'chaque semaine|chaque :number semaines', + 'reminder_frequency_month' => 'chaque mois|chaque :number mois', + 'reminder_frequency_year' => 'chaque année|chaque :number années', + 'reminder_frequency_one_time' => 'le :date', + 'reminders_delete_confirmation' => 'Êtes-vous sûr de vouloir supprimer ce rappel ?', + 'reminders_delete_cta' => 'Supprimer', + 'reminders_next_expected_date' => 'le', + 'reminders_cta' => 'Ajouter un rappel', + 'reminders_description' => 'Nous vous enverrons un courriel pour chacun des rappels ci-dessous. Les rappels sont envoyés le matin du jour où l’événement se passe. Les rappels ajoutés automatiquement pour les anniversaires ne peuvent pas être effacés. Si vous désirez changer ces derniers, modifiez la date d’anniversaire de ces contacts.', + 'reminders_one_time' => 'Unique', + 'reminders_type_week' => 'semaine', + 'reminders_type_month' => 'mois', + 'reminders_type_year' => 'année', + 'reminders_birthday' => 'Anniversaire de :name', + 'reminders_free_plan_warning' => 'Vous êtes sur le plan gratuit. Aucun courriel ne sera envoyé avec ce plan. Pour recevoir vos rappels par courriel, passez au plan supérieur.', + + // relationships + 'relationship_form_add' => 'Ajouter une relation', + 'relationship_form_edit' => 'Modifier une relation existante', + 'relationship_form_is_with' => 'Cette personne est…', + 'relationship_form_is_with_name' => ':name est…', + 'relationship_form_add_choice' => 'Quelle est cette relation ?', + 'relationship_form_create_contact' => 'Ajouter une nouvelle personne', + 'relationship_form_associate_contact' => 'Un contact existant', + 'relationship_form_associate_dropdown' => 'Recherchez et sélectionnez un contact existant dans la liste déroulante ci-dessous', + 'relationship_form_associate_dropdown_placeholder' => 'Recherchez et sélectionnez un contact existant', + 'relationship_form_also_create_contact' => 'Créer un contact pour cette personne.', + 'relationship_form_add_description' => 'Ceci vous permettra de traiter cette personne comme tous les autres contacts de votre compte.', + 'relationship_form_add_no_existing_contact' => 'Vous n’avez aucun contact qui puisse être associé à :name pour le moment.', + 'relationship_delete_confirmation' => 'Êtes-vous sûr de vouloir supprimer cette relation ? La suppression est permanente.', + 'relationship_unlink_confirmation' => 'Êtes-vous sûr de vouloir supprimer cette relation ? La personne ne sera pas supprimée – seulement la relation entre les deux.', + 'relationship_form_add_success' => 'La relation a été créée avec succès.', + 'relationship_form_deletion_success' => 'La relation a été supprimée.', + + // tasks + 'tasks_title' => 'Tâches', + 'tasks_blank_title' => 'Vous n’avez aucune tâche pour le moment.', + 'tasks_form_title' => 'Titre', + 'tasks_form_description' => 'Description (optionnel)', + 'tasks_add_task' => 'Ajouter une tâche', + 'tasks_delete_success' => 'La tâche a été supprimée avec succès.', + 'tasks_complete_success' => 'La tâche a été mise à jour avec succès', + + // activities + 'activity_title' => 'Activités', + 'activity_type_category_simple_activities' => 'Activités simples', + 'activity_type_category_sport' => 'Sport', + 'activity_type_category_food' => 'Gastronomie', + 'activity_type_category_cultural_activities' => 'Activités culturelles', + 'activity_type_just_hung_out' => 'traîner ensemble', + 'activity_type_watched_movie_at_home' => 'regarder un film à la maison ensemble', + 'activity_type_talked_at_home' => 'parler ensemble à la maison', + 'activity_type_did_sport_activities_together' => 'fait du sport ensemble', + 'activity_type_ate_at_his_place' => 'mangé chez lui·elle', + 'activity_type_went_bar' => 'aller dans un bar', + 'activity_type_ate_at_home' => 'manger à la maison', + 'activity_type_picnicked' => 'pique-niqué', + 'activity_type_ate_restaurant' => 'aller au restaurant', + 'activity_type_went_theater' => 'aller au cinéma', + 'activity_type_went_concert' => 'aller à un concert', + 'activity_type_went_play' => 'aller au théâtre', + 'activity_type_went_museum' => 'aller au musée', + 'activities_add_activity' => 'Ajouter une activité', + 'activities_add_more_details' => 'Ajouter plus de détails', + 'activities_add_emotions' => 'Ajouter une émotion', + 'activities_add_category' => 'Indiquer une catégorie', + 'activities_add_participants_cta' => 'Ajouter des participants', + 'activities_item_information' => ':Activity. S’est passée le :date', + 'activities_add_title' => 'Qu’avez-vous fait avec {name} ?', + 'activities_summary' => 'Décrivez ce que vous avez fait', + 'activities_add_pick_activity' => 'Souhaitez-vous catégoriser cette activité ? Vous n’avez pas à le faire, mais cela nous permettra de faire des statistiques plus tard (optionnel)', + 'activities_add_date_occured' => 'Cette activité s\'est produite le…', + 'activities_add_participants' => 'Qui, à part {name}, a participé à l’activité ? (optionnel)', + 'activities_add_emotions_title' => 'Voulez vous enregistrer ce que vous avez ressenti au cours de cette activité ? (optionnel)', + 'activities_blank_title' => 'Gardez une trace de ce que vous avez fait avec {name} dans le passé, et de ce dont vous avez parlé', + 'activities_blank_add_activity' => 'Ajouter une activité', + 'activities_add_success' => 'L’activité a été ajoutée avec succès', + 'activities_add_error' => 'Erreur lors de l’ajout de l’activité', + 'activities_update_success' => 'L’activité a été mise à jour avec succès', + 'activities_delete_success' => 'L’activité a été supprimée avec succès', + 'activities_who_was_involved' => 'Qui était impliqué ?', + 'activities_activity' => 'Catégorie d’activité', + 'activities_view_activities_report' => 'Afficher les rapports d’activités', + 'activities_profile_title' => 'Rapports d’activités entre :name et vous', + 'activities_profile_subtitle' => 'Vous avez enregistré :total_activities activité avec :name au total et :activities_last_twelve_months au cours des 12 derniers mois.|Vous avez enregistré :total_activities activités avec :name au total et :activities_last_twelve_months au cours des 12 derniers mois.', + 'activities_profile_year_summary_activity_types' => 'Voici un aperçu du type d\'activités que vous avez réalisées ensemble en :year', + 'activities_profile_year_summary' => 'Voici ce que vous avez fait ensemble en :year', + 'activities_profile_number_occurences' => ':value activité|:value activités', + 'activities_list_participants' => 'Participants ({total}) :', + 'activities_list_emotions' => 'Émotions ressenties :', + 'activities_list_date' => 'Arrivé le', + 'activities_list_category' => 'Catégorie :', + + // notes + 'notes_create_success' => 'La note a été ajoutée avec succès', + 'notes_update_success' => 'La note a été modifiée avec succès', + 'notes_delete_success' => 'La note a été supprimée avec succès', + 'notes_add_cta' => 'Ajouter une note', + 'notes_favorite' => 'Ajouter/retirer des favoris', + 'notes_delete_title' => 'Supprimer une note', + 'notes_delete_confirmation' => 'Êtes-vous sûr de vouloir supprimer cette note ? La suppression est permanente', + + // gifts + 'gifts_title' => 'Cadeaux', + 'gifts_add_success' => 'Le cadeau a été ajouté avec succès', + 'gifts_delete_success' => 'Le cadeau a été supprimé', + 'gifts_delete_confirmation' => 'Etes-vous sûr de vouloir supprimer ce cadeau ?', + 'gifts_add_gift' => 'Ajouter un cadeau', + 'gifts_link' => 'Lien', + 'gifts_for' => 'Pour : {name}', + 'gifts_delete_cta' => 'Supprimer', + 'gifts_add_title' => 'Gestion des cadeaux pour :name', + 'gifts_add_gift_idea' => 'Idée de cadeau', + 'gifts_add_gift_already_offered' => 'Cadeau déjà offert', + 'gifts_add_gift_received' => 'Cadeau reçu', + 'gifts_add_gift_title' => 'Quel est ce cadeau ?', + 'gifts_add_gift_name' => 'Cadeau', + 'gifts_add_link' => 'Lien de la page web (optionnel)', + 'gifts_add_value' => 'Valeur (optionnel)', + 'gifts_add_comment' => 'Commentaire (optionnel)', + 'gifts_add_recipient' => 'Destinataire (optionnel)', + 'gifts_add_recipient_field' => 'Destinataire', + 'gifts_add_photo' => 'Photo (optionnelle)', + 'gifts_add_photo_title' => 'Ajouter une photo pour ce cadeau', + 'gifts_add_someone' => 'Ce cadeau est destiné à quelqu’un de la famille de {name} en particulier', + 'gifts_delete_title' => 'Supprimer un cadeau', + 'gifts_ideas' => 'Idées cadeaux', + 'gifts_offered' => 'Cadeaux déjà offerts', + 'gifts_offered_as_an_idea' => 'Marquer comme idée', + 'gifts_received' => 'Cadeaux reçus', + 'gifts_view_comment' => 'Voir commentaire', + 'gifts_mark_offered' => 'Marquer comme offert', + 'gifts_update_success' => 'Le cadeau a été mis à jour avec succès', + 'gifts_add_date' => 'Date (facultatif)', + + // debts + 'debt_delete_confirmation' => 'Êtes-vous sûr de vouloir effacer cette dette ?', + 'debt_delete_success' => 'La dette a été effacée avec succès', + 'debt_add_success' => 'La dette a été ajoutée avec succès', + 'debt_title' => 'Dettes', + 'debt_add_cta' => 'Ajouter une dette', + 'debt_you_owe' => 'Vous devez :amount', + 'debt_they_owe' => ':name vous doit :amount', + 'debt_add_title' => 'Gestion des dettes', + 'debt_add_you_owe' => 'Vous devez à :name', + 'debt_add_they_owe' => ':name vous doit', + 'debt_add_amount' => 'la somme de', + 'debt_add_reason' => 'pour la raison suivante (optionnelle)', + 'debt_add_add_cta' => 'Ajouter la dette', + 'debt_edit_update_cta' => 'Mettre à jour la dette', + 'debt_edit_success' => 'La dette a été modifiée avec succès', + 'debts_blank_title' => 'Gérez les dettes que vous devez à :name ou que :name vous doit', + + // tags + 'tag_edit' => 'Modifier le tag', + 'tag_add' => 'Ajouter des étiquettes', + 'tag_add_search' => 'Ajouter ou rechercher une étiquette', + 'tag_no_tags' => 'Aucune étiquette', + + // Introductions + 'introductions_sidebar_title' => 'Comment vous vous êtes rencontré', + 'introductions_blank_cta' => 'Indiquez comment vous avez rencontré :name', + 'introductions_title_edit' => 'Comment avez-vous rencontré :name ?', + 'introductions_additional_info' => 'Expliquez quand et comment vous vous êtes rencontrés', + 'introductions_edit_met_through' => 'Est-ce que quelqu’un vous a introduit à cette personne ?', + 'introductions_no_met_through' => 'Personne', + 'introductions_first_met_date' => 'Date de la rencontre', + 'introductions_no_first_met_date' => 'Je ne connais pas la date de cette rencontre', + 'introductions_first_met_date_known' => 'Voici la date de notre rencontre', + 'introductions_add_reminder' => 'Ajouter un rappel pour célébrer la rencontre à la date anniversaire, rappelant chaque année quand cet évènement s’est passé', + 'introductions_update_success' => 'Vous avez mis à jour avec succès vos informations de rencontre', + 'introductions_met_through' => 'Rencontré·e via :name', + 'introductions_met_date' => 'Rencontré le :date', + 'introductions_reminder_title' => 'Anniversaire de la date de la première rencontre', + + // Deceased + 'deceased_reminder_title' => 'Anniversaire de la mort de :name', + 'deceased_mark_person_deceased' => 'Indiquer cette personne comme décédée', + 'deceased_know_date' => 'Je connais la date de décès de cette personne', + 'deceased_add_reminder' => 'Ajouter un rappel pour cette date', + 'deceased_label' => 'Décédé', + 'deceased_date_label' => 'Date de décès', + 'deceased_label_with_date' => 'Décédé le :date', + 'deceased_age' => 'Age au moment du décès', + + // Contact information + 'contact_info_title' => 'Coordonnées', + 'contact_info_form_content' => 'Contenu', + 'contact_info_form_contact_type' => 'Type de contact', + 'contact_info_form_personalize' => 'Personaliser', + 'contact_info_address' => 'Habite à', + + // Addresses + 'contact_address_title' => 'Adresses', + 'contact_address_form_name' => 'Nom (optionnel)', + 'contact_address_form_street' => 'Rue et numéro (optionnel)', + 'contact_address_form_city' => 'Ville (optionnel)', + 'contact_address_form_province' => 'Province (optionnel)', + 'contact_address_form_postal_code' => 'Code postal (optionnel)', + 'contact_address_form_country' => 'Pays (optionnel)', + 'contact_address_form_latitude' => 'Latitude (chiffres uniquement) (optionnel)', + 'contact_address_form_longitude' => 'Longitude (chiffres uniquement) (optionnel)', + + // Pets + 'pets_kind' => 'Sorte d’animal', + 'pets_name' => 'Nom (optionnel)', + 'pets_create_success' => 'L’animal a été ajouté avec succès', + 'pets_update_success' => 'L’animal a été mis à jour', + 'pets_delete_success' => 'L’animal a été supprimé', + 'pets_title' => 'Animaux de compagnie', + 'pets_reptile' => 'Reptile', + 'pets_bird' => 'Oiseau', + 'pets_cat' => 'Chat', + 'pets_dog' => 'Chien', + 'pets_fish' => 'Poisson', + 'pets_hamster' => 'Hamster', + 'pets_horse' => 'Cheval', + 'pets_rabbit' => 'Lapin', + 'pets_rat' => 'Rat', + 'pets_small_animal' => 'Petit animal', + 'pets_other' => 'Autre', + + // life events + 'life_event_list_tab_life_events' => 'Évènements marquants', + 'life_event_list_tab_other' => 'Notes, rappels…', + 'life_event_list_title' => 'Évènements marquants', + 'life_event_blank' => 'Prenez des notes sur ce qui arrive dans la vie de {name} pour votre référence future.', + 'life_event_list_cta' => 'Ajouter un évènement marquant', + 'life_event_create_category' => 'Toutes les catégories', + 'life_event_create_life_event' => 'Ajouter l’évènement marquant', + 'life_event_create_default_title' => 'Titre (optionnel)', + 'life_event_create_default_story' => 'Histoire (optionnel)', + 'life_event_create_date' => 'Vous n’avez pas à renseigner le mois ou le jour – seule l’année est obligatoire.', + 'life_event_create_default_description' => 'Ajouter des informations sur ce que vous savez', + 'life_event_create_add_yearly_reminder' => 'Ajouter un rappel annuel pour cet évènement', + 'life_event_create_success' => 'L’évènement a été sauvegardé', + 'life_event_delete_title' => 'Supprimer l’évènement', + 'life_event_delete_description' => 'Êtes-vous sûr de vouloir supprimer cet évènement marquant ? La suppression est permanente.', + 'life_event_delete_success' => 'L’évènement marquant a été supprimé', + 'life_event_date_it_happened' => 'Date de l’évènement', + 'life_event_category_work_education' => 'Travail & formation', + 'life_event_category_family_relationships' => 'Famille & relations', + 'life_event_category_home_living' => 'Foyer & vie domestique', + 'life_event_category_health_wellness' => 'Santé & bien-être', + 'life_event_category_travel_experiences' => 'Voyages & expériences', + 'life_event_sentence_new_job' => 'A commencé un nouveau travail', + 'life_event_sentence_retirement' => 'A pris sa retraite', + 'life_event_sentence_new_school' => 'A commencé l’école', + 'life_event_sentence_study_abroad' => 'A étudier à l’étranger', + 'life_event_sentence_volunteer_work' => 'A commencé à faire du bénévolat', + 'life_event_sentence_published_book_or_paper' => 'A publié un document', + 'life_event_sentence_military_service' => 'A démarré le service militaire', + 'life_event_sentence_new_relationship' => 'A commencé une relation', + 'life_event_sentence_engagement' => 'S’est fiancé', + 'life_event_sentence_marriage' => 'S’est marié', + 'life_event_sentence_anniversary' => 'Anniversaire', + 'life_event_sentence_expecting_a_baby' => 'Attend un bébé', + 'life_event_sentence_new_child' => 'A eu un enfant', + 'life_event_sentence_new_family_member' => 'Nouveau membre dans la famille', + 'life_event_sentence_new_pet' => 'A eu un animal de compagnie', + 'life_event_sentence_end_of_relationship' => 'Fin d’une relation', + 'life_event_sentence_loss_of_a_loved_one' => 'A perdu un être cher', + 'life_event_sentence_moved' => 'A déménagé', + 'life_event_sentence_bought_a_home' => 'A acheté une maison', + 'life_event_sentence_home_improvement' => 'A fait des rénovations', + 'life_event_sentence_holidays' => 'Est allé en vacances', + 'life_event_sentence_new_vehicle' => 'Acquisition dʼun nouveau véhicule', + 'life_event_sentence_new_roommate' => 'A eu un colocataire', + 'life_event_sentence_overcame_an_illness' => 'A surmonté une maladie', + 'life_event_sentence_quit_a_habit' => 'A quitté une habitude', + 'life_event_sentence_new_eating_habits' => 'A commencé de nouvelles habitudes alimentaires', + 'life_event_sentence_weight_loss' => 'A perdu du poids', + 'life_event_sentence_wear_glass_or_contact' => 'A commencé à porter des lunettes ou des lentilles de contact', + 'life_event_sentence_broken_bone' => 'S’est cassé un os', + 'life_event_sentence_removed_braces' => 'S’est fait retiré un appareil dentaire', + 'life_event_sentence_surgery' => 'A eu une chirurgie', + 'life_event_sentence_dentist' => 'Est allé chez le dentiste', + 'life_event_sentence_new_sport' => 'A commencé un sport', + 'life_event_sentence_new_hobby' => 'A commencé un passe-temps', + 'life_event_sentence_new_instrument' => 'A commencé à apprendre un nouvel instrument', + 'life_event_sentence_new_language' => 'A commencé à apprendre une nouvelle langue', + 'life_event_sentence_tattoo_or_piercing' => 'S’est fait tatoué ou percé', + 'life_event_sentence_new_license' => 'A eu le permis', + 'life_event_sentence_travel' => 'A voyagé', + 'life_event_sentence_achievement_or_award' => 'A eu une récompense ou un prix', + 'life_event_sentence_changed_beliefs' => 'A changé de croyances', + 'life_event_sentence_first_word' => 'A parlé pour la première fois', + 'life_event_sentence_first_kiss' => 'A eu son premier baiser', + + // documents + 'document_list_title' => 'Documents', + 'document_list_cta' => 'Télécharger un document', + 'document_list_blank_desc' => 'Ici vous pouvez stocker les documents reliés à cette personne.', + 'document_upload_zone_cta' => 'Télécharger un fichier', + 'document_upload_zone_progress' => 'Téléchargement du document…', + 'document_upload_zone_error' => 'Une erreur est survenue durant le téléchargement du document. Veuillez réessayer plus tard.', + + // Photos + 'photo_title' => 'Photos', + 'photo_list_title' => 'Photos associées', + 'photo_list_cta' => 'Télécharger une photo', + 'photo_list_blank_desc' => 'Vous pouvez enregistrer des images sur ce contact. Téléchargez-en une maintenant !', + 'photo_upload_zone_cta' => 'Télécharger une photo', + 'photo_current_profile_pic' => 'Photo de profil actuelle', + 'photo_make_profile_pic' => 'Utiliser comme photo de profil', + 'photo_delete' => 'Supprimer la photo', + 'photo_next' => 'Photo suivante ❯', + 'photo_previous' => '❮ Photo précédente', + + // Avatars + 'avatar_change_title' => 'Changer de photo de profil', + 'avatar_question' => 'Quelle photo de profil souhaitez-vous utiliser ?', + 'avatar_default_avatar' => 'La photo de profil par défaut', + 'avatar_adorable_avatar' => 'La photo de profil Adorable', + 'avatar_gravatar' => 'Le Gravatar associé à l’adresse courriel de cette personne. Gravatar est un système global qui permet aux utilisateurs d’associer des adresses courriel avec des photos.', + 'avatar_current' => 'Conserver la photo de profil actuelle', + 'avatar_photo' => 'À partir d’une photo que vous téléchargez', + 'avatar_crop_new_avatar_photo' => 'Recadrer la nouvelle photo de profil', + + // emotions + 'emotion_this_made_me_feel' => 'Cela vous fait sentir …', + + // logs + 'auditlogs_link' => 'Historique', + 'auditlogs_title' => 'Tout ce qui est arrivé à :name', + 'auditlogs_breadcrumb' => 'Historique', + 'auditlogs_author' => 'Par :name le :date', + + // contact field label + 'contact_field_label_home' => 'Domicile', + 'contact_field_label_work' => 'Bureau', + 'contact_field_label_cell' => 'Portable', + 'contact_field_label_fax' => 'Fax', + 'contact_field_label_pager' => 'Bipeur', + 'contact_field_label_main' => 'Principal', + 'contact_field_label_other' => 'Autre', + 'contact_field_label_personal' => 'Personnalisé', +]; diff --git a/resources/lang/fr/reminder.php b/resources/lang/fr/reminder.php new file mode 100644 index 0000000..d51b65b --- /dev/null +++ b/resources/lang/fr/reminder.php @@ -0,0 +1,16 @@ + 'Souhait d’anniversaire pour', + 'type_phone_call' => 'Appeler', + 'type_lunch' => 'Manger avec', + 'type_hangout' => 'Aller voir', + 'type_email' => 'Envoyer un email à', + 'type_birthday_kid' => 'Souhaiter un joyeux anniversaire à l’enfant de', +]; diff --git a/resources/lang/fr/settings.php b/resources/lang/fr/settings.php new file mode 100644 index 0000000..a80587a --- /dev/null +++ b/resources/lang/fr/settings.php @@ -0,0 +1,557 @@ + 'Paramètres du compte', + 'sidebar_personalization' => 'Personnalisation', + 'sidebar_settings_storage' => 'Espace de stockage', + 'sidebar_settings_export' => 'Exporter les données', + 'sidebar_settings_users' => 'Utilisateurs', + 'sidebar_settings_subscriptions' => 'Abonnement', + 'sidebar_settings_import' => 'Importation de données', + 'sidebar_settings_tags' => 'Gestion des étiquettes', + 'sidebar_settings_api' => 'API', + 'sidebar_settings_dav' => 'Ressources DAV', + 'sidebar_settings_security' => 'Sécurité', + 'sidebar_settings_auditlogs' => 'Journal d’audit', + + 'title_general' => 'Informations générales', + 'title_i18n' => 'Paramètres internationaux', + 'title_layout' => 'Disposition', + + 'me_title' => 'Moi en tant que contact', + 'me_help' => 'Ceci est le contact qui vous représente sur Monica', + 'me_select' => 'Sélectionner un contact', + 'me_no_contact' => 'Aucun contact sélectionné.', + 'me_select_click' => 'Cliquer ici pour sélectionner un contact.', + 'me_remove_contact' => 'Supprimer l’association', + 'me_choose' => 'Choisissez votre contact', + 'me_choose_placeholder' => 'Choisissez votre contact', + + 'export_title' => 'Exporter les données de votre compte', + 'export_be_patient' => 'Cliquez sur le bouton pour commencer l’export. Cela peut prendre plusieurs minutes pour préparer l’export – merci d’être patient et de ne pas spammer le bouton.', + 'export_title_sql' => 'Exporter en SQL', + 'export_sql_explanation' => 'L’exportation de vos données au format SQL vous permet de prendre vos données et de les importer dans votre propre instance Monica. Ceci n’est utile que si vous avez votre propre serveur.', + 'export_sql_cta' => 'Exporter en SQL', + 'export_sql_link_instructions' => 'Remarque : lisez les instructions pour en savoir plus sur l’importation de ce fichier dans votre instance.', + 'export_title_json' => 'Exporter en Json', + 'export_submitted' => 'Votre exportation a été envoyée, elle sera disponible dans quelques instants…', + 'export_json_explanation' => 'Exportation de vos données au format Json pour la sauvegarde.', + 'export_json_beta' => 'L’exportation en Json est en phase de test. Dites-nous ce que vous en pensez :', + 'export_json_cta' => 'Exporter en Json', + 'export_header_type' => 'Type', + 'export_header_timestamp' => 'Date de création', + 'export_header_status' => 'Statut', + 'export_header_actions' => 'Actions', + 'export_last_title' => 'Dernières exportations', + 'export_empty_title' => 'Aucune exportation pour le moment', + 'export_type_json' => 'Export Json', + 'export_type_sql' => 'Export SQL', + 'export_status_todo' => 'Soumis', + 'export_status_doing' => 'En cours', + 'export_status_done' => 'Terminé', + 'export_status_failed' => 'Echoué', + 'export_not_done' => 'Téléchargement impossible, cet export n’est pas encore terminé.', + + 'firstname' => 'Prénom', + 'lastname' => 'Nom de famille', + 'name_order' => 'Ordre des noms', + 'name_order_firstname_lastname' => ' – Jean Dupont', + 'name_order_lastname_firstname' => ' – Dupont Jean', + 'name_order_firstname_lastname_nickname' => ' () – Jean Dupont (Jojo)', + 'name_order_firstname_nickname_lastname' => ' () – Jean (Jojo) Dupont', + 'name_order_lastname_firstname_nickname' => ' () – Dupont Jean (Jojo)', + 'name_order_lastname_nickname_firstname' => ' () – Dupont (Jojo) Jean', + 'name_order_nickname_firstname_lastname' => ' ( ) – Jojo (Jean Dupont)', + 'name_order_nickname_lastname_firstname' => ' ( ) – Jojo (Dupont Jean)', + 'name_order_nickname' => ' – Jojo', + 'currency' => 'Devise', + 'name' => 'Votre nom : :name', + 'email' => 'Adresse courriel', + 'email_placeholder' => 'Entrez l’adresse courriel', + 'email_help' => 'Cette adresse courriel est utilisée pour vous connecter à votre compte, et c\'est aussi l\'adresse à laquelle Monica enverra vos rappels.', + 'timezone' => 'Fuseau horaire', + 'temperature_scale' => 'Échelle de température', + 'temperature_scale_fahrenheit' => 'Fahrenheit (°F)', + 'temperature_scale_celsius' => 'Celsius (°C)', + 'layout' => 'Disposition', + 'layout_small' => 'Maximum de 1200 pixels de large', + 'layout_big' => 'Largeur maximale du navigateur', + 'save' => 'Mettre à jour', + 'delete_title' => 'Supprimer votre compte', + 'delete_desc' => 'Voulez-vous supprimer votre compte ? La suppression est permanente et toutes vos données seront définitivement supprimées. Si vous avez un abonnement, il sera immédiatement annulé.', + 'delete_other_desc' => 'Vos données dans la base de données principale seront supprimées immédiatement. Comme décrit dans notre politique de confidentialité, nous faisons des sauvegardes quotidiennes et sécurisées de la base de données. Ces sauvegardes sont conservées pendant 30 jours, puis elles sont complètement supprimées. Nous ne pouvons pas supprimer des données spécifiques des sauvegardes que nous effectuons auparavant. Toutes vos données seront complètement supprimées dans les 31 jours suivant la suppression de votre compte.', + 'reset_desc' => 'Souhaitez-vous remettre à zéro votre compte ? Ceci effacera tous les contacts ainsi que les données associées. Votre compte ne sera pas effacé.', + 'reset_title' => 'Remettre à zéro votre compte', + 'reset_cta' => 'Remettre à zéro', + 'reset_notice' => 'Êtes-vous sûr de vouloir réinitialiser votre compte ? Ceci est permanent et ne peut pas être annulé.', + 'reset_success' => 'Votre compte a été réinitialisé.', + 'delete_notice' => 'Êtes-vous sûr de vouloir supprimer votre compte ? Cette action est permanente et ne peut pas être annulée. Toutes vos données seront supprimées et ne seront pas récupérables.', + 'delete_cta' => 'Effacer le compte', + 'settings_success' => 'Préférences mises à jour', + 'locale' => 'Langue', + 'locale_help' => 'Voulez-vous aider à traduire Monica ou ajouter une nouvelle langue ? Veuillez suivre ce lien pour plus d’informations.', + 'locale_ar' => 'Arabe', + 'locale_cs' => 'Tchèque', + 'locale_de' => 'Allemand', + 'locale_el' => 'Grec', + 'locale_en' => 'Anglais', + 'locale_en-GB' => 'Anglais (Royaume-Uni)', + 'locale_es' => 'Espagnol', + 'locale_fr' => 'Francais', + 'locale_he' => 'Hébreu', + 'locale_hr' => 'Croate', + 'locale_id' => 'Indonésien', + 'locale_it' => 'Italien', + 'locale_ja' => 'Japonais', + 'locale_nl' => 'Néerlandais', + 'locale_pt' => 'Portugais', + 'locale_pt-BR' => 'Portugais du Brésil', + 'locale_ru' => 'Russe', + 'locale_sv' => 'Suédois', + 'locale_vi' => 'Vietnamien', + 'locale_zh' => 'Chinois Simplifié', + 'locale_zh-TW' => 'Chinois Traditionnel', + 'locale_tr' => 'Turc', + + 'security_title' => 'Sécurité', + 'security_help' => 'Changer les questions de sécurité pour votre compte.', + 'password_change' => 'Modifier votre mot de passe', + 'password_current' => 'Mot de passe actuel', + 'password_current_placeholder' => 'Entrez votre mot de passe actuel', + 'password_new1' => 'Nouveau mot de passe', + 'password_new1_placeholder' => 'Entrez votre nouveau mot de passe', + 'password_new2' => 'Confirmez votre nouveau mot de passe', + 'password_new2_placeholder' => 'Retapez votre nouveau mot de passe', + 'password_btn' => 'Changer votre mot de passe', + '2fa_title' => 'Authentification à deux facteurs', + '2fa_otp_title' => 'Application mobile d’authentification à deux facteurs', + '2fa_enable_title' => 'Activer l’authentification à deux facteurs', + '2fa_enable_description' => 'Activer l’authentification à deux facteurs pour renforcer la sécurité de votre compte.', + '2fa_enable_otp' => 'Ouvrez votre application mobile pour l’authentification à deux facteurs et scannez le QR code suivant :', + '2fa_enable_otp_help' => 'Si votre application mobile pour l’authentification à deux facteurs ne supporte pas les QR codes, entrez le code suivant :', + '2fa_enable_otp_validate' => 'Merci de valider le nouvel appareil que vous venez de configurer :', + '2fa_enable_success' => 'L’authentification à deux facteurs est active', + '2fa_enable_error' => 'Erreur lors de l’activation de l’authentification à deux facteurs', + '2fa_enable_error_already_set' => 'L’authentification à deux facteurs est déjà activé', + '2fa_disable_title' => 'Désactiver l’authentification à deux facteurs', + '2fa_disable_description' => 'Désactiver l’authentification à deux facteurs pour votre compte. Attention, votre compte ne sera plus sécurisé !', + '2fa_disable_success' => 'L’authentification à deux facteurs a été désactivée', + '2fa_disable_error' => 'Erreur lors de la désactivation de l’authentification à deux facteurs', + + 'webauthn_title' => 'Clé de sécurité — Protocole WebAuthn', + 'webauthn_enable_description' => 'Ajouter une nouvelle clé de sécurité', + 'webauthn_key_name_help' => 'Donnez un nom à votre clé.', + 'webauthn_key_name' => 'Nom de la clé :', + 'webauthn_success' => 'Votre clé est détectée et validée.', + 'webauthn_last_use' => 'Dernière utilisation : {timestamp}', + 'webauthn_delete_confirmation' => 'Êtes-vous sûr de vouloir supprimer cette clé ?', + 'webauthn_delete_success' => 'Clé supprimée', + 'webauthn_insertKey' => 'Insérer votre clé de sécurité.', + 'webauthn_buttonAdvise' => 'Si votre clé de sécurité dispose d’un bouton, appuyez dessus.', + 'webauthn_noButtonAdvise' => 'Si ce n’est pas le cas, enlevez-la et insérez là à nouveau.', + 'webauthn_not_supported' => 'Votre navigateur ne supporte pas encore WebAuthn.', + 'webauthn_not_secured' => 'WebAuthn ne supporte que les connexions sécurisées. Veuillez charger cette page en https.', + 'webauthn_error_already_used' => 'Cette clé est déjà enregistrée. Il n’est pas nécessaire de l’enregistrer à nouveau.', + 'webauthn_error_not_allowed' => 'L’opération a expiré ou n’a pas été autorisée.', + + 'recovery_title' => 'Codes de récupération', + 'recovery_show' => 'Obtenez des codes de récupération', + 'recovery_copy_help' => 'Copier les codes dans votre presse-papiers', + 'recovery_help_intro' => 'Voici vos codes de récupération :', + 'recovery_help_information' => 'Vous pouvez utiliser chaque code de récupération une fois.', + 'recovery_clipboard' => 'Codes copiés dans le presse-papiers.', + 'recovery_generate' => 'Générer de nouveaux codes …', + 'recovery_generate_help' => 'Générez de nouveaux codes invalidera les codes générés précédemment.', + 'recovery_already_used_help' => 'Ce code a déjà été utilisé.', + + 'users_list_title' => 'Utilisateurs avec accès à votre compte', + 'users_list_add_user' => 'Inviter un nouvel utilisateur', + 'users_list_you' => 'C’est vous', + 'users_list_invitations_title' => 'Invitations en attente', + 'users_list_invitations_explanation' => 'Voici les personnes que vous avez invité à rejoindre Monica comme collaborateurs.', + 'users_list_invitations_invited_by' => 'invité par :name', + 'users_list_invitations_sent_date' => 'envoyé le :date', + 'users_blank_title' => 'Vous êtes la seule personne qui a accès à ce compte.', + 'users_blank_add_title' => 'Souhaitez-vous inviter quelqu’un d’autre ?', + 'users_blank_description' => 'Cette personne aura le même accès que vous et sera en mesure d’ajouter, modifier ou supprimer les informations de contact.', + 'users_blank_cta' => 'Inviter quelqu’un', + 'users_add_title' => 'Invitez un nouvel utilisateur à votre compte par courriel', + 'users_add_description' => 'Cette personne aura le même accès que vous, et pourra également inviter ou supprimer d’autres utilisateurs, vous compris. Assurez-vous de faire confiance à cette personne avant de lui donner ces accès.', + 'users_add_email_field' => 'Entrez le courriel de la personne que vous souhaitez inviter', + 'users_add_confirmation' => 'Je confirme que je veux inviter cet utilisateur dans mon compte. Je comprends que cette personne aura accès à toutes mes données et verra exactement ce que je vois.', + 'users_add_cta' => 'Inviter l’utilisateur par courriel', + 'users_accept_title' => 'Accepter l’invitation et créer un nouveau compte', + 'users_error_please_confirm' => 'Merci de confirmer que vous souhaitez bien inviter cette personne avant d’envoyer l’invitation', + 'users_error_email_already_taken' => 'Cette adresse courriel est déjà prise. Veuillez en choisir une autre', + 'users_error_already_invited' => 'Vous avez déjà invité cet utilisateur. Veuillez choisir une autre adresse courriel.', + 'users_error_email_not_similar' => 'Ce n’est pas le courriel de la personne qui vous a invité.', + 'users_invitation_deleted_confirmation_message' => 'L’invitation a été supprimée avec succès', + 'users_invitations_delete_confirmation' => 'Êtes-vous sûr de vouloir supprimer de cette invitation ?', + 'users_list_delete_confirmation' => 'Êtes-vous sûr de vouloir supprimer cet utilisateur de votre compte ?', + 'users_invitation_need_subscription' => 'L’ajout d’utilisateurs nécessite un abonnement.', + + 'subscriptions_account_current_plan' => 'Votre offre actuelle', + 'subscriptions_account_current_legacy' => 'Plan actuel, plus sélectionnable :', + 'subscriptions_account_current_paid_plan' => 'Vous êtes sur l’offre :name. Merci beaucoup pour votre inscription.', + + 'subscriptions_account_next_billing_title' => 'Prochaine facturation', + 'subscriptions_account_next_billing' => 'Votre abonnement va être renouvelé automatiquement le :date.', + 'subscriptions_account_bill_monthly' => 'Nous vous facturerons :price pour un mois de plus.', + 'subscriptions_account_bill_annual' => 'Nous vous facturerons :price pour une année de plus.', + 'subscriptions_account_change' => 'Changer d’offre', + + 'subscriptions_account_cancel_title' => 'Annuler votre abonnement', + 'subscriptions_account_cancel_action' => 'Annuler votre abonnement', + 'subscriptions_account_cancel' => 'Vous pouvez annuler votre abonnement à tout moment.', + 'subscriptions_account_free_plan' => 'Vous êtes sur l’offre gratuite.', + 'subscriptions_account_free_plan_upgrade' => 'Vous pouvez mettre à niveau votre compte en passant à l’offre :name, qui coûte :price $ par mois. En voici les avantages :', + 'subscriptions_account_free_plan_benefits_users' => 'Nombre illimité d’utilisateurs', + 'subscriptions_account_free_plan_benefits_reminders' => 'Rappels par courriel', + 'subscriptions_account_free_plan_benefits_import_data_vcard' => 'Import de vos contacts au format vCard', + 'subscriptions_account_free_plan_benefits_support' => 'Supporter le projet sur le long terme, afin de pouvoir vous proposer plus de fonctionnalités.', + 'subscriptions_account_upgrade' => 'Mettre à jour votre compte', + 'subscriptions_account_upgrade_title' => 'Mettez à niveau Monica aujourd’hui et ayez des relations encore plus significatives.', + 'subscriptions_account_upgrade_choice' => 'Choisissez une offre ci-dessous et rejoignez plus de :customers personnes qui ont mis à niveau leur Monica.', + 'subscriptions_account_update_title' => 'Modifier l’abonnement à Monica', + 'subscriptions_account_update_description' => 'Vous pouvez modifier la fréquence de votre abonnement ici.', + 'subscriptions_account_update_information' => 'Vous serez facturé immédiatement pour le nouveau montant. Votre abonnement sera prolongé jusqu\'à la nouvelle période, selon votre choix.', + 'subscriptions_account_invoices' => 'Factures', + 'subscriptions_account_invoices_download' => 'Télécharger', + 'subscriptions_account_invoices_subscription' => 'Abonnement du :startDate au :endDate', + 'subscriptions_account_payment' => 'Quelle option de paiement vous convient le mieux ?', + 'subscriptions_account_confirm_payment' => 'Votre paiement est actuellement incomplet, merci de confirmer votre paiement.', + 'subscriptions_downgrade_title' => 'Passez votre compte sur l’offre gratuite', + 'subscriptions_downgrade_limitations' => 'L’offre gratuite a des limitations. Afin de pouvoir passer à cette offre, vous devez passer les points suivants :', + 'subscriptions_downgrade_rule_users' => 'Vous devez avoir un seul utilisateur dans votre compte', + 'subscriptions_downgrade_rule_users_constraint' => 'Vous avez actuellement :count utilisateur dans votre compte.|Vous avez actuellement :count utilisateurs dans votre compte.', + 'subscriptions_downgrade_rule_invitations' => 'Vous ne devez pas avoir d’invitation en attente', + 'subscriptions_downgrade_rule_invitations_constraint' => 'Vous avez actuellement 1 invitiation en attente.|Vous avez actuellement :count invitations en attente.', + 'subscriptions_downgrade_rule_contacts' => 'Vous ne devez pas avoir plus de :number contacts actifs', + 'subscriptions_downgrade_rule_contacts_constraint' => 'Vous avez actuellement :count contact.|Vous avez actuellement :count contacts.', + 'subscriptions_downgrade_rule_contacts_archive' => 'Nous pouvons également archiver tous vos contacts pour vous – cela effacerait cette règle et vous laisserait passer le processus de rétrogradation de votre compte.', + 'subscriptions_downgrade_cta' => 'Passer au plan inférieur', + 'subscriptions_downgrade_success' => 'Vous êtes de retour sur l’offre gratuite !', + 'subscriptions_downgrade_thanks' => 'Merci beaucoup d’avoir essayé l’offre payante. Nous continuons à apporter de nouvelles fonctionnalités sur Monica tout le temps – vous pouvez donc revenir à l’occasion pour voir si vous pourriez à nouveau être intéressé·e pour prendre un abonnement.', + 'subscriptions_back' => 'Retourner aux paramètres', + 'subscriptions_upgrade_title' => 'Passer au plan supérieur', + 'subscriptions_upgrade_choose' => 'Vous avez choisi l’offre :plan.', + 'subscriptions_upgrade_infos' => 'Nous ne pourrions être plus heureux. Entrez vos informations de paiement ci-dessous.', + 'subscriptions_upgrade_name' => 'Nom sur la carte', + 'subscriptions_upgrade_zip' => 'Code postal', + 'subscriptions_upgrade_credit' => 'Carte de crédit ou de débit', + 'subscriptions_upgrade_submit' => 'Payer {amount}', + 'subscriptions_upgrade_charge' => 'Nous débiterons votre carte de :price maintenant. Le prochain paiement aura lieu le :date. Si jamais vous changez d’avis, vous pourrez annuler à tout moment, sans poser de questions.', + 'subscriptions_upgrade_charge_handled' => 'Le paiement est géré par Stripe. Aucune information bancaire n’arrive sur notre serveur.', + 'subscriptions_upgrade_success' => 'Merci ! Vous êtes maintenant inscrit.', + 'subscriptions_upgrade_thanks' => 'Bienvenue dans la communauté de personnes qui essaient de rendre le monde un peu meilleur.', + + 'subscriptions_payment_confirm_title' => 'Confirmez votre paiement de :amount', + 'subscriptions_payment_confirm_information' => 'Une confirmation supplémentaire est nécessaire pour traiter votre paiement. Veuillez confirmer votre paiement en remplissant vos informations de paiement ci-dessous.', + 'subscriptions_payment_succeeded_title' => 'Paiement validé', + 'subscriptions_payment_succeeded' => 'Ce paiement a déjà été confirmé avec succès.', + 'subscriptions_payment_cancelled_title' => 'Paiement annulé', + 'subscriptions_payment_cancelled' => 'Ce paiement a été annulé.', + 'subscriptions_payment_error_name' => 'Veuillez fournir votre nom.', + 'subscriptions_payment_success' => 'Votre paiement a été effectué.', + + 'subscriptions_pdf_title' => 'Votre abonnement :name mensuel', + 'subscriptions_plan_frequency_year' => ':amount / an', + 'subscriptions_plan_frequency_month' => ':amount / mois', + 'subscriptions_plan_choose' => 'Choisir cette offre', + 'subscriptions_plan_year_title' => 'Payer annuellement', + 'subscriptions_plan_year_bonus' => 'Tranquillité d’esprit pendant toute une année', + 'subscriptions_plan_month_title' => 'Payer tous les mois', + 'subscriptions_plan_month_bonus' => 'Annuler à tout moment', + 'subscriptions_plan_include1' => 'Inclus avec votre mise à niveau :', + 'subscriptions_plan_include2' => 'Nombre illimité de contacts • Nombre illimité d’utilisateurs • Rappels par courriel • Importer des vCard • Personnalisation de la vue d’un contact', + 'subscriptions_plan_include3' => '100% des bénéfices vont à l’élaboration de ce beau projet open source.', + 'subscriptions_help_title' => 'Détails supplémentaires, qui peuvent attiser votre curiosité', + 'subscriptions_help_opensource_title' => 'Qu’est-ce qu’un projet open source ?', + 'subscriptions_help_opensource_desc' => 'Monica est un projet open source. Il est le fruit du travail d’une communauté de bénévoles qui veulent juste créer un outil pour le bien de tous. Être un projet open source signifie que le code est disponible pour tous sur GitHub, et que tout le monde peut l’inspecter, le modifier et l’améliorer. Tout l’argent que nous récoltons sert uniquement à créer des fonctionnalités, payer de nouveaux serveurs puissants, et autres coûts. Merci pour votre aide. Nous ne pourrions rien faire sans vous.', + 'subscriptions_help_limits_title' => 'Y a-t-il une limite au nombre de contacts que nous pouvons avoir sur l’offre gratuite ?', + 'subscriptions_help_limits_plan' => 'Oui. L’offre gratuite vous permet de gérer :number contacts.', + 'subscriptions_help_discounts_title' => 'Avez-vous des réductions pour les organismes sans but lucratif et les organismes d’éducation ?', + 'subscriptions_help_discounts_desc' => 'En effet ! Monica est gratuit pour les étudiants, les organismes sans but lucratif et les organismes de bienfaisance. Il suffit de contacter le support avec un justificatif de votre statut et nous allons appliquer ce statut spécial dans votre compte.', + 'subscriptions_help_change_title' => 'Que se passe-t-il si je change d’avis ?', + 'subscriptions_help_change_desc' => 'Vous pouvez annuler à tout moment, sans question, et par vous-même – aucun support requis. Toutefois, vous ne serez pas remboursé pour la période en cours.', + + 'stripe_error_card' => 'Votre carte est refusée. Le message de refus est : :message', + 'stripe_error_api_connection' => 'Problèmes de communication avec Stripe. Veuillez réessayer plus tard.', + 'stripe_error_rate_limit' => 'Trop de requêtes avec Stripe actuellement. Veuillez réessayer plus tard.', + 'stripe_error_invalid_request' => 'Paramètres invalides. Réessayez plus tard.', + 'stripe_error_authentication' => 'Mauvaise authentification avec Stripe', + + 'import_title' => 'Importer les contacts dans votre compte', + 'import_cta' => 'Importer des contacts', + 'import_stat' => 'Vous avez importé :number fichiers jusqu’à présent.', + 'import_result_stat' => ':count contact vCard envoyé (:total_imported importé, :total_skipped ignoré)|:count contacts vCard envoyés (:total_imported importés, :total_skipped ignorés)', + 'import_view_report' => 'Voir le rapport', + 'import_in_progress' => 'L’import est en cours. Veuillez recharger la page dans quelques minutes.', + 'import_upload_title' => 'Importer vos contacts depuis un fichier vCard', + 'import_upload_rules_desc' => 'Nous avons toutefois quelques règles :', + 'import_upload_rule_format' => 'Nous supportons les formats .vcard et .vcf.', + 'import_upload_rule_vcard' => 'Nous supportons le format vCard 3.0, qui est le format par défaut de l’application macOS Contacts.app et Google Contacts.', + 'import_upload_rule_instructions' => 'Instructions d’export pour macOS Contacts.app et Google Contacts.', + 'import_upload_rule_multiple' => 'Si vos contacts ont plusieurs adresses courriels ou numéros de téléphone, seule la première entrée sera sauvegardée.', + 'import_upload_rule_limit' => 'Les fichiers sont limités à 10 Mo.', + 'import_upload_rule_time' => 'Cela peut prendre une minute pour importer les contacts et les traiter. Merci de votre patience.', + 'import_upload_rule_cant_revert' => 'Veuillez vous assurer que les données sont fiables avant d’importer, car l’import ne peut être annulé.', + 'import_upload_form_file' => 'Votre fichier .vcf ou .vCard :', + 'import_upload_behaviour' => 'Comportement pour l’importation :', + 'import_upload_behaviour_add' => 'Ajouter les nouveaux contacts et passer les contacts existants', + 'import_upload_behaviour_replace' => 'Remplacer les contacts existants', + 'import_upload_behaviour_help' => 'Remplacer les contacts mettra à jour toutes les données trouvées dans la vCard, mais gardera les autre valeurs existantes du contact.', + 'import_report_title' => 'Rapport d’import', + 'import_report_date' => 'Date de l’import', + 'import_report_type' => 'Type d’import', + 'import_report_number_contacts' => 'Nombre de contacts dans le fichier', + 'import_report_number_contacts_imported' => 'Nombre de contacts importés', + 'import_report_number_contacts_skipped' => 'Nombre de contacts ignorés', + 'import_report_status_imported' => 'Importés', + 'import_report_status_skipped' => 'Ignorés', + 'import_vcard_parse_error' => 'Erreur lors de l’analyse de l’entrée vCard', + 'import_vcard_contact_exist' => 'Le contact existe déjà', + 'import_vcard_contact_no_firstname' => 'Pas de prénom (obligatoire)', + 'import_vcard_file_not_found' => 'Fichier non trouvé', + 'import_vcard_unknown_entry' => 'Nom de contact inconnu', + 'import_vcard_file_no_entries' => 'Le fichier ne contient pas de donnée', + 'import_blank_title' => 'Vous n’avez encore importé aucun contact.', + 'import_blank_question' => 'Souhaitez-vous importer vos contacts maintenant ?', + 'import_blank_description' => 'Nous pouvons importer les fichiers vCard que vous avez dans votre Google Contacts ou votre gestionnaire de contacts.', + 'import_blank_cta' => 'Importer une vCard', + 'import_need_subscription' => 'L’importation de données nécessite une souscription.', + + 'tags_list_title' => 'Étiquettes', + 'tags_list_description' => 'Vous pouvez organiser vos contact avec des étiquettes. Les étiquettes sont comme des dossiers, mais vous pouvez avoir autant d’étiquettes que vous le souhaitez par contact.', + 'tags_list_contact_number' => ':count contact|:count contacts', + 'tags_list_delete_success' => 'L’étiquette a été supprimée avec succès', + 'tags_list_edit_success' => 'The tag has been successfully updated', + 'tags_list_delete_confirmation' => 'Êtes-vous sûr de vouloir supprimer cette étiquette ? Aucun contact ne sera supprimé, seulement l’étiquette.', + 'tags_blank_title' => 'Les étiquettes sont une excellente manière de catégoriser vos contacts.', + 'tags_blank_description' => 'Les étiquettent fonctionnent comme des dossiers, mais vous pouvez ajouter plus d\'une étiquette à un contact. Allez à un contact et taguez un ami, juste en dessous du nom. Une fois qu\'un contact est étiqueté, revenez ici pour gérer toutes les étiquettes de votre compte.', + + 'api_title' => 'Accès avec l’API', + 'api_description' => 'L’API peut être utilisée pour manipuler les données de Monica depuis une application externe, comme une application mobile par exemple.', + 'api_help' => 'Pour utiliser l’API, un jeton est obligatoire. Vous pouvez soit créer un jeton d’accès personnel (authentification Bearer), soit autoriser un client OAuth à le créer pour vous. Voir la documentation de l’API.', + 'api_endpoint' => 'Le point de terminaison de l’API pour cette instance Monica est :', + + 'api_personal_access_tokens' => 'Jeton d’accès personnel', + 'api_pao_description' => 'Faites attention à ne fournir ce jeton qu’à des personnes de confiance – elles pourront accéder à toutes vos données.', + 'api_token_title' => 'Jetons d’accès personnels', + 'api_token_create_new' => 'Créer un nouveau jeton', + 'api_token_not_created' => 'Vous n’avez pas encore créé de jeton d’accès personnel.', + 'api_token_name' => 'Nom du jeton', + 'api_token_expire' => 'Expire le {date}', + 'api_token_delete' => 'Supprimer', + 'api_token_create' => 'Créer un jeton', + 'api_token_scopes' => 'Périmètres', + 'api_token_help' => 'Voici votre nouveau jeton d’accès personnel. Ceci est la seule fois où vous pourrez le voir, ne le perdez pas ! Vous pouvez dès à présent utiliser ce jeton pour lancer des requêtes avec l’API.', + + 'api_oauth_clients' => 'Vos clients OAuth', + 'api_oauth_clients_desc' => 'Cette section vous permet d’enregistrer votre propre client OAuth.', + 'api_oauth_clients_desc2' => 'Utilisez cet identifiant de client pour demander un nouveau jeton et convertir les codes d’autorisation pour accéder aux jetons. Voir la documentation de Laravel Passport pour plus d’informations.', + 'api_oauth_title' => 'Clients OAuth', + 'api_oauth_create_new' => 'Créer de nouveaux clients', + 'api_oauth_edit' => 'Modifier le client', + 'api_oauth_not_created' => 'Vous n’avez pas encore créé de client OAuth.', + 'api_oauth_clientid' => 'Identifiant du client', + 'api_oauth_name' => 'Nom', + 'api_oauth_name_help' => 'Quelque chose que vos utilisateurs reconnaîtront et inspirera la confiance.', + 'api_oauth_secret' => 'Secret', + 'api_oauth_create' => 'Créer un client', + 'api_oauth_redirecturl' => 'URL de redirection', + 'api_oauth_redirecturl_help' => 'URL de rappel d’autorisation de votre application.', + + 'api_authorized_clients' => 'Liste de clients autorisés', + 'api_authorized_clients_desc' => 'Cette section liste tous les clients que vous avez autorisé à accéder aux données de votre demande. Vous pouvez révoquer cette autorisation à tout moment.', + 'api_authorized_clients_title' => 'Applications autorisées', + 'api_authorized_clients_none' => 'Il n’y a pas encore de client autorisé.', + 'api_authorized_clients_name' => 'Nom', + 'api_authorized_clients_scopes' => 'Périmètres', + + 'personalization_tab_title' => 'Personnalisez votre compte', + + 'personalization_title' => 'Ici vous pouvez configurer les différents paramètres de votre compte. Ces fonctionnalités sont pour les « utilisateurs avancés » qui veulent un contrôle maximal sur Monica.', + 'personalization_contact_field_type_title' => 'Types de champs de contact', + 'personalization_contact_field_type_add' => 'Ajouter un nouveau type de champ', + 'personalization_contact_field_type_description' => 'Vous pouvez configurer les différents type de champs de contact que vous pouvez associer à tous vos contacts. Par exemple si un nouveau réseau social apparaît, vous pourrez ajouter un nouveau type de communication pour vos contacts ici.', + 'personalization_contact_field_type_table_name' => 'Nom', + 'personalization_contact_field_type_table_protocol' => 'Protocole', + 'personalization_contact_field_type_table_actions' => 'Actions', + 'personalization_contact_field_type_modal_title' => 'Ajouter un nouveau type de champ', + 'personalization_contact_field_type_modal_edit_title' => 'Editer un type de champ existant', + 'personalization_contact_field_type_modal_delete_title' => 'Supprimer un type de champ existant', + 'personalization_contact_field_type_modal_delete_description' => 'Êtes-vous sûr de vouloir supprimer ce type de champ de contact ? Supprimer ce type de contact effacera TOUTES les données avec ce type de champ pour tous vos contacts.', + 'personalization_contact_field_type_modal_name' => 'Nom', + 'personalization_contact_field_type_modal_protocol' => 'Protocole (optionnel)', + 'personalization_contact_field_type_modal_protocol_help' => 'Chaque nouveau type de champ de contact peut être cliquable. Si un protocole est défini, nous l’utiliserons pour lancer l’action indiquée par le navigateur.', + 'personalization_contact_field_type_modal_icon' => 'Icone (optionnel)', + 'personalization_contact_field_type_modal_icon_help' => 'Vous pouvez associer un icône pour ce champ. Vous devez utiliser une référence vers une icône FontAwesome.', + 'personalization_contact_field_type_delete_success' => 'Le type de champ de contact a été supprimé avec succès.', + 'personalization_contact_field_type_add_success' => 'Le type de champ de contact a été ajouté avec succès.', + 'personalization_contact_field_type_edit_success' => 'Le type de champ de contact a été mis à jour avec succès.', + + 'personalization_genders_title' => 'Types de genre', + 'personalization_genders_add' => 'Ajouter un nouveau type de genre', + 'personalization_genders_desc' => 'Vous pouvez définir autant de genres dont vous avez besoin. Il vous faut avoir au moins un type de genre dans votre compte.', + 'personalization_genders_modal_add' => 'Ajouter un nouveau type de genre', + 'personalization_genders_modal_edit' => 'Mettre à jour le type de genre', + 'personalization_genders_modal_name' => 'Nom', + 'personalization_genders_modal_name_help' => 'Nom utilisé pour afficher le genre sur la page d’un contact.', + 'personalization_genders_modal_sex' => 'Sexe', + 'personalization_genders_modal_sex_help' => 'Utilisé pour définir les relations, et pendant le processus d’importation/exportation VCard.', + 'personalization_genders_modal_default' => 'Sélectionnez le genre par défaut pour un nouveau contact', + 'personalization_genders_modal_delete' => 'Supprimer le type de genre', + 'personalization_genders_modal_delete_desc' => 'Voulez-vous vraiment supprimer le genre « {name} » ?', + 'personalization_genders_modal_delete_question' => 'Vous avez actuellement {count} contact utilisant ce genre. Si vous supprimez ce genre, quel genre ce contact devrait avoir ?|Vous avez actuellement {count} contacts utilisant ce genre. Si vous supprimez ce genre, quel genre ces contacts devraient avoir ?', + 'personalization_genders_modal_delete_question_default' => 'Ce genre est celui par défaut. Si vous le supprimez, quel sera le prochain genre par défaut ?', + 'personalization_genders_modal_error' => 'Merci de choisir un genre depuis cette liste.', + 'personalization_genders_list_contact_number' => '{count} contact|{count} contacts', + 'personalization_genders_table_name' => 'Nom', + 'personalization_genders_table_sex' => 'Sexe', + 'personalization_genders_table_default' => 'Défaut', + 'personalization_genders_default' => 'Genre par défaut', + 'personalization_genders_make_default' => 'Modifier le genre par défaut', + 'personalization_genders_select_default' => 'Choisir le genre par défaut', + 'personalization_genders_m' => 'Masculin', + 'personalization_genders_f' => 'Féminin', + 'personalization_genders_o' => 'Autre', + 'personalization_genders_u' => 'Inconnu', + 'personalization_genders_n' => 'Aucun ou non applicable', + + 'personalization_reminder_rule_save' => 'Les modifications ont été enregistrées', + 'personalization_reminder_rule_title' => 'Règles de rappel', + 'personalization_reminder_rule_line' => '{count} jour avant|{count} jours avant', + 'personalization_reminder_rule_desc' => 'Pour chaque rappel que vous mettez en place, Monica peut envoyer un courriel plusieurs jours avant que l’évènement se passe. Vous pouvez ajuster ces préférences de notifications ici. Ces notifications ne s’appliquent qu’aux rappels mensuels et annuels.', + + 'personalization_module_save' => 'Les modifications ont été enregistrées', + 'personalization_module_title' => 'Fonctionnalités', + 'personalization_module_desc' => 'Vous n’avez peut-être pas besoin de toutes ces fonctionnalités. Ci-dessous vous pouvez activer ou désactiver des fonctionnalités spécifiques qui sont utilisées sur la vue d’un contact. Ces modifications s’appliqueront à tous vos contacts. Désactiver une fonctionnalité ne supprime aucune donnée, cela masque juste la fonctionnalité.', + + 'personalisation_paid_upgrade' => 'Il s’agit d’une fonctionnalité premium qui nécessite un abonnement payant pour être activée. Mettez à niveau votre compte en visitant Paramètres > Abonnement.', + 'personalisation_paid_upgrade_vue' => 'Il s’agit d’une fonctionnalité premium qui nécessite un abonnement payant pour être activée. Mettez à niveau votre compte en visitant Paramètres > Abonnement.', + + 'reminder_time_to_send' => 'Heure du jour à laquelle les rappels doivent être envoyés', + 'reminder_time_to_send_help' => 'Votre prochain rappel sera envoyé le {dateTime}.', + + 'personalization_activity_type_category_title' => 'Catégories de types d’activité', + 'personalization_activity_type_category_add' => 'Ajouter une nouvelle catégorie de type d’activité', + 'personalization_activity_type_category_table_name' => 'Nom', + 'personalization_activity_type_category_description' => 'Une activité avec l’un de vos contacts peut avoir un type et un type de catégorie. Votre compte est configuré par défaut avec un ensemble de types de catégories prédéfinies, mais vous pouvez les personnaliser ici.', + 'personalization_activity_type_category_table_actions' => 'Actions', + 'personalization_activity_type_category_modal_add' => 'Ajouter une nouvelle catégorie de type d’activité', + 'personalization_activity_type_category_modal_edit' => 'Modifier une catégorie de type d’activité', + 'personalization_activity_type_category_modal_question' => 'Comment nommer cette nouvelle catégorie ?', + 'personalization_activity_type_add_button' => 'Ajouter un nouveau type d’activité', + 'personalization_activity_type_modal_add' => 'Ajouter un nouveau type d’activité', + 'personalization_activity_type_modal_question' => 'Comment nommer ce nouveau type d’activité ?', + 'personalization_activity_type_modal_edit' => 'Modifier un type d’activité', + 'personalization_activity_type_category_modal_delete' => 'Supprimer une catégorie de type d’activité', + 'personalization_activity_type_category_modal_delete_desc' => 'Êtes-vous sûr de vouloir supprimer cette catégorie ? La suppression entraînera la suppression de tous les types d’activités associées. Les activités qui appartiennent à cette catégorie ne seront pas affectées par cette suppression.', + 'personalization_activity_type_modal_delete' => 'Supprimer un type d’activité', + 'personalization_activity_type_modal_delete_desc' => 'Êtes-vous sûr de vouloir supprimer ce type d’activité ? Les activités qui appartiennent à cette catégorie ne seront pas affectées par cette suppression.', + 'personalization_activity_type_modal_delete_error' => 'Impossible de trouver ce type d’activité.', + 'personalization_activity_type_category_modal_delete_error' => 'Impossible de trouver cette catégorie de type d’activité.', + + 'personalization_life_event_category_title' => 'Catégories d’évènements marquants', + 'personalization_live_event_category_table_name' => 'Nom', + 'personalization_life_event_category_description' => 'Un évènement marquant peut avoir un type et une catégorie. Votre compte est fourni par défaut avec un ensemble de catégories et types prédéfinis, mais vous pouvez personnaliser les types d’évènements marquants ici.', + 'personalization_live_event_category_table_actions' => 'Actions', + 'personalization_life_event_type_add_button' => 'Ajouter un nouveau type d’évènement', + 'personalization_life_event_type_modal_add' => 'Ajouter un nouveau type d’évènement', + 'personalization_life_event_type_modal_question' => 'Comment nommer ce nouveau type d’évènement ?', + 'personalization_life_event_type_modal_edit' => 'Éditer un type d’évènement', + 'personalization_life_event_type_modal_delete' => 'Supprimer un type d’évènement', + 'personalization_life_event_type_modal_delete_desc' => 'Êtes-vous sûr de vouloir supprimer ce type d’événement ? Les évènements qui appartiennent à ce type seront supprimés en effectuant cette action.', + 'personalization_life_event_type_modal_delete_error' => 'Impossible de trouver ce type d’évènement.', + + 'personalization_life_event_category_work_education' => 'Travail & formation', + 'personalization_life_event_category_family_relationships' => 'Famille & relations', + 'personalization_life_event_category_home_living' => 'Foyer & vie domestique', + 'personalization_life_event_category_travel_experiences' => 'Voyages & expériences', + 'personalization_life_event_category_health_wellness' => 'Santé & bien-être', + + 'personalization_life_event_type_new_job' => 'Nouveau travail', + 'personalization_life_event_type_retirement' => 'Retraite', + 'personalization_life_event_type_new_school' => 'Nouvelle école', + 'personalization_life_event_type_study_abroad' => 'Allé étudier à l’étranger', + 'personalization_life_event_type_volunteer_work' => 'Travail bénévole', + 'personalization_life_event_type_published_book_or_paper' => 'Publication d’un livre ou d’un papier', + 'personalization_life_event_type_military_service' => 'Service militaire', + 'personalization_life_event_type_first_met' => 'Première rencontre', + 'personalization_life_event_type_new_relationship' => 'Nouvelle relation', + 'personalization_life_event_type_engagement' => 'Fiançailles', + 'personalization_life_event_type_marriage' => 'Mariage', + 'personalization_life_event_type_anniversary' => 'Anniversaire', + 'personalization_life_event_type_expecting_a_baby' => 'Attend un bébé', + 'personalization_life_event_type_new_child' => 'Nouvel enfant', + 'personalization_life_event_type_new_family_member' => 'Nouveau membre dans la famille', + 'personalization_life_event_type_new_pet' => 'Nouvel animal de compagnie', + 'personalization_life_event_type_end_of_relationship' => 'Fin de relation', + 'personalization_life_event_type_loss_of_a_loved_one' => 'Perte d’un être cher', + 'personalization_life_event_type_moved' => 'Déménagement', + 'personalization_life_event_type_bought_a_home' => 'Nouvelle maison', + 'personalization_life_event_type_home_improvement' => 'Améliorations de la maison', + 'personalization_life_event_type_holidays' => 'Vacances', + 'personalization_life_event_type_new_vehicle' => 'Nouveau véhicule', + 'personalization_life_event_type_new_roommate' => 'Nouveau colocataire', + 'personalization_life_event_type_overcame_an_illness' => 'A surmonté une maladie', + 'personalization_life_event_type_quit_a_habit' => 'Perte d’une habitude', + 'personalization_life_event_type_new_eating_habits' => 'Nouvelles habitudes alimentaires', + 'personalization_life_event_type_weight_loss' => 'Perte de poids', + 'personalization_life_event_type_wear_glass_or_contact' => 'A commencé à porter des lunettes ou des lentilles de contact', + 'personalization_life_event_type_broken_bone' => 'S’est cassé un os', + 'personalization_life_event_type_removed_braces' => 'S’est fait retiré son appareil dentaire', + 'personalization_life_event_type_surgery' => 'A eu une opération chirurgicale', + 'personalization_life_event_type_dentist' => 'A eu un traitement dentaire', + 'personalization_life_event_type_new_sport' => 'A commencé un nouveau sport', + 'personalization_life_event_type_new_hobby' => 'A commencé un nouveau passe-temps', + 'personalization_life_event_type_new_instrument' => 'A commencé à jouer d’un nouvel instrument', + 'personalization_life_event_type_new_language' => 'A commencé à apprendre une nouvelle langue', + 'personalization_life_event_type_tattoo_or_piercing' => 'Tatouage ou piercing', + 'personalization_life_event_type_new_license' => 'Nouveau permis', + 'personalization_life_event_type_travel' => 'Voyage', + 'personalization_life_event_type_achievement_or_award' => 'Récompense ou prix', + 'personalization_life_event_type_changed_beliefs' => 'Changement de croyances', + 'personalization_life_event_type_first_word' => 'Premier mot', + 'personalization_life_event_type_first_kiss' => 'Premier baiser', + + 'storage_title' => 'Espace de stockage', + 'storage_account_info' => 'La limite de votre compte est : :accountLimit Mo. Votre utilisation actuelle est : :currentAccountSize Mo (environ :percentUsage %).', + 'storage_upgrade_notice' => 'Mettez à niveau votre compte pour pouvoir télécharger des documents et des photos.', + 'storage_description' => 'Ici vous pouvez voir tous les documents et photos téléchargés sur vos contacts.', + + 'dav_title' => 'WebDAV', + 'dav_description' => 'Vous trouverez ici tous les paramètres pour utiliser les ressources WebDAV pour CardDAV et CalDAV.', + 'dav_copy_help' => 'Copier dans votre presse-papier', + 'dav_clipboard_copied' => 'Valeur copiée dans le presse-papier', + 'dav_url_base' => 'Url de base pour toutes les ressources CardDAV et CalDAV :', + 'dav_connect_help' => 'Vous pouvez connecter vos contacts et/ou calendriers avec cette url de base sur votre téléphone ou ordinateur.', + 'dav_connect_help2' => 'Utilisez votre login (email) et créez un jeton API en tant que mot de passe pour vous authentifier.', + 'dav_url_carddav' => 'Url CardDAV pour les Contacts :', + 'dav_url_caldav_birthdays' => 'Url CalDAV pour les Anniversaires :', + 'dav_url_caldav_tasks' => 'Url CalDAV pour les Tâches :', + 'dav_title_carddav' => 'CardDAV', + 'dav_title_caldav' => 'CalDAV', + 'dav_carddav_export' => 'Exporter tous les contacts dans un seul fichier', + 'dav_caldav_birthdays_export' => 'Exporter tous les anniversaires dans un seul fichier', + 'dav_caldav_tasks_export' => 'Exporter toutes les tâches dans un seul fichier', + + 'archive_title' => 'Archiver tous les contacts de votre compte', + 'archive_desc' => 'Ceci va archiver tous les contacts de votre compte.', + 'archive_cta' => 'Archiver tous vos contacts', + + 'logs_title' => 'Tout ce qui est arrivé à ce compte', + 'logs_actor' => 'Acteur', + 'logs_timestamp' => 'Horodatage', + 'logs_description' => 'Description', + 'logs_subject' => 'Sujet', + 'logs_size' => 'Taille (Ko)', + 'logs_object' => 'Objet', +]; diff --git a/resources/lang/fr/validation.php b/resources/lang/fr/validation.php new file mode 100644 index 0000000..b3d9bd8 --- /dev/null +++ b/resources/lang/fr/validation.php @@ -0,0 +1,166 @@ + 'Le champ :attribute doit être accepté.', + 'active_url' => 'Le champ :attribute n’est pas une URL valide.', + 'after' => 'Le champ :attribute doit être une date postérieure au :date.', + 'after_or_equal' => ':attribute doit être une date postérieure ou égale à :date.', + 'alpha' => 'Le champ :attribute doit seulement contenir des lettres.', + 'alpha_dash' => 'Le champ :attribute doit contenir uniquement des lettres, des chiffres et des tirets.', + 'alpha_num' => 'Le champ :attribute doit seulement contenir des chiffres et des lettres.', + 'array' => 'Le champ :attribute doit être un tableau.', + 'before' => 'Le champ :attribute doit être une date antérieure au :date.', + 'before_or_equal' => ':attribute doit être une date antérieure ou égale à :date.', + 'between' => [ + 'numeric' => 'La valeur de :attribute doit être comprise entre :min et :max.', + 'file' => 'La taille du fichier de :attribute doit être comprise entre :min et :max kilo-octets.', + 'string' => 'Le texte :attribute doit contenir entre :min et :max caractères.', + 'array' => 'Le tableau :attribute doit contenir entre :min et :max éléments.', + ], + 'boolean' => 'Le champ :attribute doit être vrai ou faux.', + 'confirmed' => 'Le champ de confirmation :attribute ne correspond pas.', + 'date' => 'Le champ :attribute n’est pas une date valide.', + 'date_equals' => 'Le champ :attribute doit être une date égale à :date.', + 'date_format' => 'Le champ :attribute ne correspond pas au format :format.', + 'different' => 'Les champs :attribute et :other doivent être différents.', + 'digits' => 'Le champ :attribute doit contenir :digits chiffres.', + 'digits_between' => 'Le champ :attribute doit contenir entre :min et :max chiffres.', + 'dimensions' => ':attribute a des dimensions d’image invalides.', + 'distinct' => 'Le champ :attribute a une valeur dupliquée.', + 'email' => 'Le champ :attribute doit être une adresse e-mail valide.', + 'ends_with' => 'Le champ :attribute doit se terminer par une des valeurs suivantes : :values', + 'exists' => 'Le champ :attribute sélectionné est invalide.', + 'file' => ':attribute doit être un fichier.', + 'filled' => 'Le champ :attribute doit avoir une valeur.', + 'gt' => [ + 'numeric' => 'La valeur de :attribute doit être supérieure à :value.', + 'file' => 'La taille du fichier de :attribute doit être supérieure à :value kilo-octets.', + 'string' => 'Le texte :attribute doit contenir plus de :value caractères.', + 'array' => 'Le tableau :attribute doit contenir plus de :value éléments.', + ], + 'gte' => [ + 'numeric' => 'La valeur de :attribute doit être supérieure ou égale à :value.', + 'file' => 'La taille du fichier de :attribute doit être supérieure ou égale à :value kilo-octets.', + 'string' => 'Le texte :attribute doit contenir au moins :value caractères.', + 'array' => 'Le tableau :attribute doit contenir au moins :value éléments.', + ], + 'image' => 'Le champ :attribute doit être une image.', + 'in' => 'Le champ :attribute est invalide.', + 'in_array' => 'Le champ :attribute n’existe pas dans :other.', + 'integer' => 'Le champ :attribute doit être un entier.', + 'ip' => 'Le champ :attribute doit être une adresse IP valide.', + 'ipv4' => ':attribute doit être une adresse IPv4 valide.', + 'ipv6' => ':attribute doit être une adresse IPv6 valide.', + 'json' => 'Le champ :attribute doit être un document JSON valide.', + 'lt' => [ + 'numeric' => 'La valeur de :attribute doit être inférieure à :value.', + 'file' => 'La taille du fichier de :attribute doit être inférieure à :value kilo-octets.', + 'string' => 'Le texte :attribute doit contenir moins de :value caractères.', + 'array' => 'Le tableau :attribute doit contenir moins de :value éléments.', + ], + 'lte' => [ + 'numeric' => 'La valeur de :attribute doit être inférieure ou égale à :value.', + 'file' => 'La taille du fichier de :attribute doit être inférieure ou égale à :value kilo-octets.', + 'string' => 'Le texte :attribute doit contenir au plus :value caractères.', + 'array' => 'Le tableau :attribute doit contenir au plus :value éléments.', + ], + 'max' => [ + 'numeric' => 'La valeur de :attribute ne peut être supérieure à :max.', + 'file' => 'La taille du fichier de :attribute ne peut pas dépasser :max kilo-octets.', + 'string' => 'Le texte de :attribute ne peut contenir plus de :max caractères.', + 'array' => 'Le tableau :attribute ne peut contenir plus de :max éléments.', + ], + 'mimes' => 'Le champ :attribute doit être un fichier de type : :values.', + 'mimetypes' => ':attribute doit être un fichier de type : :values.', + 'min' => [ + 'numeric' => 'La valeur de :attribute doit être supérieure ou égale à :min.', + 'file' => 'La taille du fichier de :attribute doit être supérieure à :min kilo-octets.', + 'string' => 'Le texte :attribute doit contenir au moins :min caractères.', + 'array' => 'Le tableau :attribute doit contenir au moins :min éléments.', + ], + 'not_in' => 'Le champ :attribute sélectionné n’est pas valide.', + 'not_regex' => 'Le format du champ :attribute est invalide.', + 'numeric' => 'Le champ :attribute doit contenir un nombre.', + 'password' => 'Le mot de passe est incorrect.', + 'present' => 'Le champ :attribute doit être présent.', + 'regex' => 'Le format du champ :attribute est invalide.', + 'required' => 'Le champ :attribute est obligatoire.', + 'required_if' => 'Le champ :attribute est obligatoire quand la valeur de :other est :value.', + 'required_unless' => 'Le champ :attribute est obligatoire sauf si :other est :values.', + 'required_with' => 'Le champ :attribute est obligatoire quand :values est présent.', + 'required_with_all' => 'Le champ :attribute est obligatoire quand :values sont présents.', + 'required_without' => 'Le champ :attribute est obligatoire quand :values n’est pas présent.', + 'required_without_all' => 'Le champ :attribute est requis quand aucun de :values n’est présent.', + 'same' => 'Les champs :attribute et :other doivent être identiques.', + 'size' => [ + 'numeric' => 'La valeur de :attribute doit être :size.', + 'file' => 'La taille du fichier de :attribute doit être de :size kilo-octets.', + 'string' => 'Le texte de :attribute doit contenir :size caractères.', + 'array' => 'Le tableau :attribute doit contenir :size éléments.', + ], + 'starts_with' => 'Le champ :attribute doit commencer avec une des valeurs suivantes : :values', + 'string' => 'Le champ :attribute doit être une chaîne de caractères.', + 'timezone' => 'Le champ :attribute doit être un fuseau horaire valide.', + 'unique' => 'La valeur du champ :attribute est déjà utilisée.', + 'uploaded' => ':attribute n’a pas pu être téléversé.', + 'url' => 'Le format de l’Url de :attribute n’est pas valide.', + 'uuid' => 'Le champ :attribute doit être un UUID valide', + + /* + |-------------------------------------------------------------------------- + | Custom Validation Language Lines + |-------------------------------------------------------------------------- + | + | Here you may specify custom validation messages for attributes using the + | convention "attribute.rule" to name the lines. This makes it quick to + | specify a specific custom language line for a given attribute rule. + | + */ + + 'custom' => [ + 'attribute-name' => [ + 'rule-name' => 'custom-message', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Custom Validation Attributes + |-------------------------------------------------------------------------- + | + | The following language lines are used to swap our attribute placeholder + | with something more reader friendly such as "E-Mail Address" instead + | of "email". This simply helps us make our message more expressive. + | + */ + + 'attributes' => [], + + 'vue' => [ + 'max' => [ + 'numeric' => '{field} ne peut pas être plus grand que {max}.', + 'string' => '{field} ne peut pas avoir plus de {max} caractères.', + ], + 'required' => '{field} est obligatoire.', + 'url' => '{field} n’est pas une URL valide.', + ], + +]; diff --git a/resources/lang/he.json b/resources/lang/he.json new file mode 100644 index 0000000..609756d --- /dev/null +++ b/resources/lang/he.json @@ -0,0 +1,7 @@ +{ + "The :attribute must contain at least one uppercase and one lowercase letter.": ":attribute חייב להכיל לפחות אות גדולה ואות קטנה באנגלית.", + "The :attribute must contain at least one letter.": ":attribute חייב להכיל לפחות אות אחת.", + "The :attribute must contain at least one symbol.": ":attribute חייב להכיל לפחות סימן אחד.", + "The :attribute must contain at least one number.": ":attribute חייב להכיל לפחות ספרה אחת.", + "The given :attribute has appeared in a data leak. Please choose a different :attribute.": ":attribute הופיע בדליפת נתונים. נא לבחור ב:attribute אחר." +} diff --git a/resources/lang/he/app.php b/resources/lang/he/app.php new file mode 100644 index 0000000..5605183 --- /dev/null +++ b/resources/lang/he/app.php @@ -0,0 +1,571 @@ + 'כן', + 'no' => 'לא', + 'update' => 'עדכון', + 'save' => 'שמירה', + 'add' => 'הוספה', + 'cancel' => 'ביטול', + 'confirm' => 'אישור', + 'delete_confirm' => 'להמשיך?', + 'delete' => 'מחיקה', + 'edit' => 'עריכה', + 'upload' => 'העלאה', + 'download' => 'הורדה', + 'save_close' => 'שמירה וסגירה', + 'close' => 'סגירה', + 'copy' => 'העתקה', + 'create' => 'יצירה', + 'remove' => 'הסרה', + 'revoke' => 'שלילה', + 'done' => 'סיום', + 'back' => 'חזרה', + 'verify' => 'אימות', + 'new' => 'חדש/ה', + 'unknown' => 'לא ידוע לי', + 'load_more' => 'לטעון עוד', + 'loading' => 'בטעינה…', + 'with' => 'עם', + 'today' => 'היום', + 'yesterday' => 'אתמול', + 'another_day' => 'יום אחר', + 'date' => 'תאריך', + 'type' => 'סוג', + 'zoom' => 'תקריב', + 'upgrade' => 'יש לשדרג כדי לשחרר', + 'percent_uploaded' => '{percent}% נשלחו', + 'retry' => 'לנסות שוב', + 'filter' => 'סינון הרשימה', + 'go_back' => 'חזרה', + 'file_selected' => 'נבחר קובץ אחד…|{count} קבצים נבחרו…', + + 'application_title' => 'מוניקה - ניהול יחסים בינאישיים', + 'application_description' => 'מוניקה היא כלי לניהול הקשרים החברתיים שלך עם אהוביך, חבריך ומשפחתך.', + 'application_og_title' => 'חיזוק הקשר עם אהוביך. מערכת ניהול קשרים עם חברים ומשפחה, מקוונת ובחינם.', + + 'markdown_description' => 'רוצה להוסיף קצת עניין לטקסט שלך? במערכת זו קיימת תמיכה ב־Markdown כדי להוסיף הדגשה, הטיה, רשימות ועוד.', + 'markdown_link' => 'קריאת התיעוד', + + 'header_settings_link' => 'הגדרות', + 'header_logout_link' => 'יציאה', + 'header_changelog_link' => 'שינויים במוצר', + + 'main_nav_cta' => 'הוספת אנשים', + 'main_nav_dashboard' => 'לוח מחוונים', + 'main_nav_family' => 'אנשי קשר', + 'main_nav_journal' => 'יומן', + 'main_nav_activities' => 'פעילויות', + 'main_nav_tasks' => 'משימות', + + 'footer_remarks' => 'הערות?', + 'footer_send_email' => 'ניתן לשלוח לנו דוא״ל', + 'footer_privacy' => 'מדיניות פרטיות', + 'footer_release' => 'הערות הוצאה לאור', + 'footer_newsletter' => 'רשימת דיוור', + 'footer_source_code' => 'תרומה', + 'footer_version' => 'גרסה: :version', + 'footer_new_version' => 'גרסה חדשה של מוניקה זמינה', + + 'footer_modal_version_whats_new' => 'מה חדש', + 'footer_modal_version_release_away' => 'גרסה זו יצאה לאור גרסה אחת לפני הגרסה העדכנית הנוכחית. עליך לעדכן את העותק שלך.|גרסה זו יצאה לאור :number גרסאות לפני הגרסה העדכנית הנוכחית. עליך לעדכן את העותק שלך.', + + 'breadcrumb_dashboard' => 'לוח מחוונים', + 'breadcrumb_list_contacts' => 'רשימת אנשים', + 'breadcrumb_archived_contacts' => 'אנשי קשר בארכיון', + 'breadcrumb_journal' => 'יומן', + 'breadcrumb_settings' => 'הגדרות', + 'breadcrumb_settings_export' => 'יצוא', + 'breadcrumb_settings_users' => 'משתמשים', + 'breadcrumb_settings_users_add' => 'הוספת משתמש', + 'breadcrumb_settings_subscriptions' => 'הרשמה', + 'breadcrumb_settings_import' => 'יבוא', + 'breadcrumb_settings_import_report' => 'דוח יבוא', + 'breadcrumb_settings_import_upload' => 'העלאה', + 'breadcrumb_settings_tags' => 'תגיות', + 'breadcrumb_add_significant_other' => 'הוספת קשר זוגי', + 'breadcrumb_edit_significant_other' => 'עריכת קשר זוגי', + 'breadcrumb_add_note' => 'הוספת הערה', + 'breadcrumb_edit_note' => 'עריכת הערה', + 'breadcrumb_api' => 'API', + 'breadcrumb_dav' => 'משאבי DAV', + 'breadcrumb_edit_introductions' => 'איך הכרתם', + 'breadcrumb_settings_personalization' => 'התאמה אישית', + 'breadcrumb_settings_security' => 'אבטחה', + 'breadcrumb_settings_security_2fa' => 'אימות דו־שלבי', + 'breadcrumb_profile' => 'הפרופיל של :name', + + 'gender_male' => 'גבר', + 'gender_female' => 'אישה', + 'gender_none' => 'שמור במערכת', + 'gender_no_gender' => 'אין מגדר', + + 'error_title' => 'אופס! משהו השתבש.', + 'error_unauthorized' => 'אין לך את ההרשאה לערוך את המשאב הזה.', + 'error_user_account' => 'משתמש זה אינו שייך לחשבון שצוין.', + 'error_save' => 'אירעה שגיאה בעת שמירת הנתונים.', + 'error_try_again' => 'משהו השתבש. נא לנסות שוב.', + 'error_id' => 'מזהה שגיאה: :id', + 'error_unavailable' => 'השירות אינו זמין', + 'error_maintenance' => 'מתבצעות עבודות תחזוקה. תכף נשוב.', + 'error_help' => 'מיד נשוב.', + 'error_twitter' => 'ניתן לעקוב אחר חשבון הטוויטר שלנו כדי להתעדכן אם השירות שב לפעילות.', + 'error_no_term' => 'עדיין אין מדיניות עבור העותק הזה.', + + 'default_save_success' => 'הנתונים נשמרו.', + + 'compliance_title' => 'סליחה על ההפרעה.', + 'compliance_desc' => 'ערכנו את תנאי השימוש ואת מדיניות הפרטיות שלנו. מכוח החוק עלינו לבקש ממך לעיין בשינויים ולאשר את הסכמתך להם כדי להמשיך לאפשר לך להשתמש בחשבונך.', + 'compliance_desc_end' => 'אנו לא משתמשים בנתונים או בחשבון שלך לאף מטרה זדונית וגם לא נעשה זאת בעתיד.', + 'compliance_terms' => 'קבלת התנאים ומדיניות הפרטיות החדשים', + + // Relationship types + // Yes, each relationship type has 8 strings associated with it. + // This is because we need to indicate the name of the relationship type, + // and also the name of the opposite side of this relationship (father/son), + // and then, the feminine version of the string. Finally, in some sentences + // in the UI, we need to include the name of the person we add the relationship + // to. + 'relationship_type_group_love' => 'קשרים רומנטיים', + 'relationship_type_group_family' => 'קשרים משפחתיים', + 'relationship_type_group_friend' => 'קשרים חברתיים', + 'relationship_type_group_work' => 'קשרי עבודה', + 'relationship_type_group_other' => 'סוגי קשר אחרים', + + 'relationship_type_partner' => 'בן זוג', + 'relationship_type_partner_female' => 'בת זוג', + 'relationship_type_partner_male' => 'בזוגיות עם', + 'relationship_type_partner_with_name' => 'בן הזוג של :name', + 'relationship_type_partner_female_with_name' => 'בת הזוג של :name', + 'relationship_type_partner_male_with_name' => 'הזוגיות של :name', + + 'relationship_type_spouse' => 'בעל', + 'relationship_type_spouse_female' => 'אישה', + 'relationship_type_spouse_male' => 'בעל', + 'relationship_type_spouse_with_name' => 'בעלה של :name', + 'relationship_type_spouse_female_with_name' => 'אשתו של :name', + 'relationship_type_spouse_male_with_name' => 'בעלה של :name', + + 'relationship_type_date' => 'ליציאה', + 'relationship_type_date_female' => 'יוצאת קבוע', + 'relationship_type_date_male' => 'דייט', + 'relationship_type_date_with_name' => 'יוצא קבוע עם :name', + 'relationship_type_date_female_with_name' => 'יוצאת קבוע עם :name', + 'relationship_type_date_male_with_name' => 'הדייט של :name', + + 'relationship_type_lover' => 'מאהב', + 'relationship_type_lover_female' => 'מאהבת', + 'relationship_type_lover_male' => 'מאהב', + 'relationship_type_lover_with_name' => 'מאהב של :name', + 'relationship_type_lover_female_with_name' => 'מאהבת של :name', + 'relationship_type_lover_male_with_name' => 'המאהב של :name', + + 'relationship_type_inlovewith' => 'מאוהב', + 'relationship_type_inlovewith_female' => 'מאוהבת', + 'relationship_type_inlovewith_male' => 'מאוהב', + 'relationship_type_inlovewith_with_name' => 'מושא אהבתו של :name', + 'relationship_type_inlovewith_female_with_name' => 'מושא אהבתה של :name', + 'relationship_type_inlovewith_male_with_name' => 'מישהו ש־:name מאוהבת בו', + + 'relationship_type_lovedby' => 'נאהב על ידי', + 'relationship_type_lovedby_female' => 'נאהבת על ידי', + 'relationship_type_lovedby_male' => 'נאהב על ידי', + 'relationship_type_lovedby_with_name' => 'מאהב סודי של :name', + 'relationship_type_lovedby_female_with_name' => 'מאהבת סודית של :name', + 'relationship_type_lovedby_male_with_name' => 'מאהב סודי של :name', + + 'relationship_type_ex' => 'שותף לשעבר לחיים', + 'relationship_type_ex_female' => 'חברה לשעבר', + 'relationship_type_ex_male' => 'חבר לשעבר', + 'relationship_type_ex_with_name' => 'שותף לחיים לשעבר של :name', + 'relationship_type_ex_female_with_name' => 'חברה לשעבר של :name', + 'relationship_type_ex_male_with_name' => 'חבר לשעבר של :name', + + 'relationship_type_parent' => 'הורה', + 'relationship_type_parent_female' => 'אימא', + 'relationship_type_parent_male' => 'אבא', + 'relationship_type_parent_with_name' => 'הורה של :name', + 'relationship_type_parent_female_with_name' => 'אימא של :name', + 'relationship_type_parent_male_with_name' => 'אבא של :name', + + 'relationship_type_child' => 'ילד', + 'relationship_type_child_female' => 'בת', + 'relationship_type_child_male' => 'בן', + 'relationship_type_child_with_name' => 'הילד של :name', + 'relationship_type_child_female_with_name' => 'בת של :name', + 'relationship_type_child_male_with_name' => 'בן של :name', + + 'relationship_type_stepparent' => 'הורה חורג', + 'relationship_type_stepparent_female' => 'אם חורגת', + 'relationship_type_stepparent_male' => 'אב חורג', + 'relationship_type_stepparent_with_name' => 'הורה חורג של :name', + 'relationship_type_stepparent_female_with_name' => 'אם חורגת של :name', + 'relationship_type_stepparent_male_with_name' => 'אב חורג של :name', + + 'relationship_type_stepchild' => 'ילד חורג', + 'relationship_type_stepchild_female' => 'בת חורגת', + 'relationship_type_stepchild_male' => 'בן חורג', + 'relationship_type_stepchild_with_name' => 'הילד החורג של :name', + 'relationship_type_stepchild_female_with_name' => 'בת חורגת של :name', + 'relationship_type_stepchild_male_with_name' => 'בן חורג של :name', + + 'relationship_type_sibling' => 'אחאי', + 'relationship_type_sibling_female' => 'אחות', + 'relationship_type_sibling_male' => 'אח', + 'relationship_type_sibling_with_name' => 'אחאי של :name', + 'relationship_type_sibling_female_with_name' => 'אחות של :name', + 'relationship_type_sibling_male_with_name' => 'אח של :name', + + 'relationship_type_grandparent' => 'סב', + 'relationship_type_grandparent_female' => 'סבתא', + 'relationship_type_grandparent_male' => 'סבא', + 'relationship_type_grandparent_with_name' => 'הסבא או הסבתא של :name', + 'relationship_type_grandparent_female_with_name' => 'סבתא של :name', + 'relationship_type_grandparent_male_with_name' => 'סבא של :name', + + 'relationship_type_grandchild' => 'נכד/ה', + 'relationship_type_grandchild_female' => 'נכדה', + 'relationship_type_grandchild_male' => 'נכד', + 'relationship_type_grandchild_with_name' => 'הנכד או הנכדה של :name', + 'relationship_type_grandchild_female_with_name' => 'נכדה של :name', + 'relationship_type_grandchild_male_with_name' => 'נכד של :name', + + 'relationship_type_uncle' => 'דוד', + 'relationship_type_uncle_female' => 'דודה', + 'relationship_type_uncle_male' => 'דוד', + 'relationship_type_uncle_with_name' => 'דוד של :name', + 'relationship_type_uncle_female_with_name' => 'דודה של :name', + 'relationship_type_uncle_male_with_name' => 'דוד של :name', + + 'relationship_type_nephew' => 'אחיין', + 'relationship_type_nephew_female' => 'אחיינית', + 'relationship_type_nephew_male' => 'אחיין', + 'relationship_type_nephew_with_name' => 'אחיין של :name', + 'relationship_type_nephew_female_with_name' => 'אחיינית של :name', + 'relationship_type_nephew_male_with_name' => 'אחיין של :name', + + 'relationship_type_cousin' => 'בן דוד', + 'relationship_type_cousin_female' => 'בת דודה', + 'relationship_type_cousin_male' => 'בן דוד', + 'relationship_type_cousin_with_name' => 'בן דוד של :name', + 'relationship_type_cousin_female_with_name' => 'בת דודה של :name', + 'relationship_type_cousin_male_with_name' => 'בן דוד של :name', + + 'relationship_type_godfather' => 'אפוטרופוס', + 'relationship_type_godfather_female' => 'סנדקית', + 'relationship_type_godfather_male' => 'סנדק', + 'relationship_type_godfather_with_name' => 'האפוטרופוס/ית של :name', + 'relationship_type_godfather_female_with_name' => 'הסנדקית של :name', + 'relationship_type_godfather_male_with_name' => 'הסנדק של :name', + + 'relationship_type_godson' => 'godchild', + 'relationship_type_godson_female' => 'בת סנדקות', + 'relationship_type_godson_male' => 'בן סנדקות', + 'relationship_type_godson_with_name' => ':name’s godchild', + 'relationship_type_godson_female_with_name' => 'בת הסנדקות של :name', + 'relationship_type_godson_male_with_name' => 'בן הסנדקות של :name', + + 'relationship_type_friend' => 'חבר', + 'relationship_type_friend_female' => 'חברה', + 'relationship_type_friend_male' => 'חבר', + 'relationship_type_friend_with_name' => 'חבר של :name', + 'relationship_type_friend_female_with_name' => 'חברה של :name', + 'relationship_type_friend_male_with_name' => 'חבר של :name', + + 'relationship_type_bestfriend' => 'החבר הטוב ביותר', + 'relationship_type_bestfriend_female' => 'החברה הטובה ביותר', + 'relationship_type_bestfriend_male' => 'החבר הטוב ביותר', + 'relationship_type_bestfriend_with_name' => 'החבר הכי טוב של :name', + 'relationship_type_bestfriend_female_with_name' => 'החברה הכי טובה של :name', + 'relationship_type_bestfriend_male_with_name' => 'החבר הכי טוב של :name', + + 'relationship_type_colleague' => 'עמית לעבודה', + 'relationship_type_colleague_female' => 'עמיתה לעבודה', + 'relationship_type_colleague_male' => 'עמית לעבודה', + 'relationship_type_colleague_with_name' => 'עמית לעבודה של :name', + 'relationship_type_colleague_female_with_name' => 'עמיתה לעבודה של :name', + 'relationship_type_colleague_male_with_name' => 'עמית לעבודה של :name', + + 'relationship_type_boss' => 'מנהל', + 'relationship_type_boss_female' => 'מנהלת', + 'relationship_type_boss_male' => 'מנהל', + 'relationship_type_boss_with_name' => 'מנהל של :name', + 'relationship_type_boss_female_with_name' => 'מנהלת של :name', + 'relationship_type_boss_male_with_name' => 'מנהל של :name', + + 'relationship_type_subordinate' => 'כפוף', + 'relationship_type_subordinate_female' => 'כפופה', + 'relationship_type_subordinate_male' => 'כפוף', + 'relationship_type_subordinate_with_name' => 'כפוף ל:name', + 'relationship_type_subordinate_female_with_name' => 'כפופה ל:name', + 'relationship_type_subordinate_male_with_name' => 'כפוף ל:name', + + 'relationship_type_mentor' => 'חונך', + 'relationship_type_mentor_female' => 'חונכת', + 'relationship_type_mentor_male' => 'חונך', + 'relationship_type_mentor_with_name' => 'חונך של :name', + 'relationship_type_mentor_female_with_name' => 'חונך של :name', + 'relationship_type_mentor_male_with_name' => 'חונך של :name', + + 'relationship_type_protege' => 'protégé', + 'relationship_type_protege_female' => 'חניכה', + 'relationship_type_protege_male' => 'protégé', + 'relationship_type_protege_with_name' => ':name’s protégé', + 'relationship_type_protege_female_with_name' => 'חניכה של :name', + 'relationship_type_protege_male_with_name' => ':name’s protégé', + + 'relationship_type_ex_husband' => 'ex-spouse', + 'relationship_type_ex_husband_female' => 'גרושה', + 'relationship_type_ex_husband_male' => 'גרוש', + 'relationship_type_ex_husband_with_name' => ':name’s ex-spouse', + 'relationship_type_ex_husband_female_with_name' => 'הגרושה של :name', + 'relationship_type_ex_husband_male_with_name' => 'הגרוש של :name', + + // emotions + 'emotion_primary_love' => 'אהבה', + 'emotion_primary_joy' => 'שמחה', + 'emotion_primary_surprise' => 'הפתעה', + 'emotion_primary_anger' => 'כעס', + 'emotion_primary_sadness' => 'עצב', + 'emotion_primary_fear' => 'פחד', + + 'emotion_secondary_affection' => 'אהדה', + 'emotion_secondary_lust' => 'תשוקה', + 'emotion_secondary_longing' => 'געגועים', + 'emotion_secondary_cheerfulness' => 'עליזות', + 'emotion_secondary_zest' => 'להט', + 'emotion_secondary_contentment' => 'סיפוק', + 'emotion_secondary_pride' => 'גאווה', + 'emotion_secondary_optimism' => 'אופטימיות', + 'emotion_secondary_enthrallment' => 'קסם', + 'emotion_secondary_relief' => 'רגיעה', + 'emotion_secondary_surprise' => 'הפתעה', + 'emotion_secondary_irritation' => 'קנטרנות', + 'emotion_secondary_exasperation' => 'מרמור', + 'emotion_secondary_rage' => 'זעם', + 'emotion_secondary_disgust' => 'גועל', + 'emotion_secondary_envy' => 'קנאה', + 'emotion_secondary_suffering' => 'סבל', + 'emotion_secondary_sadness' => 'עצב', + 'emotion_secondary_disappointment' => 'אכזבה', + 'emotion_secondary_shame' => 'בושה', + 'emotion_secondary_neglect' => 'הזנחה', + 'emotion_secondary_sympathy' => 'אהדה', + 'emotion_secondary_horror' => 'אימה', + 'emotion_secondary_nervousness' => 'עצבנות', + + 'emotion_adoration' => 'הערצה', + 'emotion_affection' => 'אהדה', + 'emotion_love' => 'אהבה', + 'emotion_fondness' => 'חיבה', + 'emotion_liking' => 'התחבבות', + 'emotion_attraction' => 'משיכה', + 'emotion_caring' => 'אכפתיות', + 'emotion_tenderness' => 'רוך', + 'emotion_compassion' => 'חמלה', + 'emotion_sentimentality' => 'רגשנות', + 'emotion_arousal' => 'גירוי', + 'emotion_desire' => 'שקיקה', + 'emotion_lust' => 'תאווה', + 'emotion_passion' => 'חשקנות', + 'emotion_infatuation' => 'אהבה עיוורת', + 'emotion_longing' => 'געגועים', + 'emotion_amusement' => 'שעשוע', + 'emotion_bliss' => 'אושר עילאי', + 'emotion_cheerfulness' => 'עליזות', + 'emotion_gaiety' => 'עליצות', + 'emotion_glee' => 'צהלה', + 'emotion_jolliness' => 'דיצה', + 'emotion_joviality' => 'גילה', + 'emotion_joy' => 'שמחה', + 'emotion_delight' => 'עונג', + 'emotion_enjoyment' => 'שעשוע', + 'emotion_gladness' => 'רינה', + 'emotion_happiness' => 'אושר', + 'emotion_jubilation' => 'ששון', + 'emotion_elation' => 'התעלות', + 'emotion_satisfaction' => 'סיפוק', + 'emotion_ecstasy' => 'אקסטזה', + 'emotion_euphoria' => 'זחיחות', + 'emotion_enthusiasm' => 'התלהבות', + 'emotion_zeal' => 'מתלהב', + 'emotion_zest' => 'להט', + 'emotion_excitement' => 'התרגשות', + 'emotion_thrill' => 'מתרגש', + 'emotion_exhilaration' => 'הרננה', + 'emotion_contentment' => 'שביעות רצון', + 'emotion_pleasure' => 'הנאה', + 'emotion_pride' => 'גאווה', + 'emotion_eagerness' => 'מתלהב', + 'emotion_hope' => 'תקווה', + 'emotion_optimism' => 'אופטימיות', + 'emotion_enthrallment' => 'ריגוש', + 'emotion_rapture' => 'התלהבות', + 'emotion_relief' => 'רגיעה', + 'emotion_amazement' => 'פליאה', + 'emotion_surprise' => 'הפתעה', + 'emotion_astonishment' => 'תדהמה', + 'emotion_aggravation' => 'עגמת נפש', + 'emotion_irritation' => 'הרגזה', + 'emotion_agitation' => 'סערת נפש', + 'emotion_annoyance' => 'מטרד', + 'emotion_grouchiness' => 'נרגנות', + 'emotion_grumpiness' => 'רגזנות', + 'emotion_exasperation' => 'מרמור', + 'emotion_frustration' => 'תסכול', + 'emotion_anger' => 'כעס', + 'emotion_rage' => 'זעם', + 'emotion_outrage' => 'עלבון חמור', + 'emotion_fury' => 'זעף', + 'emotion_wrath' => 'חרון', + 'emotion_hostility' => 'עוינות', + 'emotion_ferocity' => 'אכזריות', + 'emotion_bitterness' => 'מרירות', + 'emotion_hate' => 'שנאה', + 'emotion_loathing' => 'תיעוב', + 'emotion_scorn' => 'בוז', + 'emotion_spite' => 'הקנטה', + 'emotion_vengefulness' => 'נקמה', + 'emotion_dislike' => 'סלידה', + 'emotion_resentment' => 'טינה', + 'emotion_disgust' => 'גועל', + 'emotion_revulsion' => 'בחילה', + 'emotion_contempt' => 'בוז', + 'emotion_envy' => 'קנאה', + 'emotion_jealousy' => 'קנאה', + 'emotion_agony' => 'ייסורים', + 'emotion_suffering' => 'סבל', + 'emotion_hurt' => 'פגיעה', + 'emotion_anguish' => 'חלחלה', + 'emotion_depression' => 'דיכאון', + 'emotion_despair' => 'ייאוש', + 'emotion_hopelessness' => 'חוסר תקווה', + 'emotion_gloom' => 'יגון', + 'emotion_glumness' => 'דכדוך', + 'emotion_sadness' => 'עצב', + 'emotion_unhappiness' => 'העדר שמחה', + 'emotion_grief' => 'תוגה', + 'emotion_sorrow' => 'צער', + 'emotion_woe' => 'צער עמוק', + 'emotion_misery' => 'אומללות', + 'emotion_melancholy' => 'דכאון', + 'emotion_dismay' => 'רפיון ידיים', + 'emotion_disappointment' => 'אכזבה', + 'emotion_displeasure' => 'אי שביעות רצון', + 'emotion_guilt' => 'אשמה', + 'emotion_shame' => 'בושה', + 'emotion_regret' => 'חרטה', + 'emotion_remorse' => 'נקיפת לב', + 'emotion_alienation' => 'ניכור', + 'emotion_isolation' => 'בידוד', + 'emotion_neglect' => 'הזנחה', + 'emotion_loneliness' => 'בדידות', + 'emotion_rejection' => 'דחייה', + 'emotion_homesickness' => 'געגועים הביתה', + 'emotion_defeat' => 'תבוסה', + 'emotion_dejection' => 'עצבות', + 'emotion_insecurity' => 'חוסר ביטחון', + 'emotion_embarrassment' => 'מבוכה', + 'emotion_humiliation' => 'השפלה', + 'emotion_insult' => 'עלבון', + 'emotion_pity' => 'רחמים', + 'emotion_sympathy' => 'אהדה', + 'emotion_alarm' => 'דריכות', + 'emotion_shock' => 'הלם', + 'emotion_fear' => 'פחד', + 'emotion_fright' => 'בעתה', + 'emotion_horror' => 'אימה', + 'emotion_terror' => 'טרור', + 'emotion_panic' => 'פאניקה', + 'emotion_hysteria' => 'היסטריה', + 'emotion_mortification' => 'המתה', + 'emotion_anxiety' => 'חרדה', + 'emotion_nervousness' => 'עצבנות', + 'emotion_tenseness' => 'מתיחות', + 'emotion_uneasiness' => 'חוסר נוחות', + 'emotion_apprehension' => 'חשש', + 'emotion_worry' => 'דאגה', + 'emotion_distress' => 'מצוקה', + 'emotion_dread' => 'מורא', + + // weather + 'weather_sunny' => 'שמשי', + 'weather_clear' => 'בהיר', + 'weather_clear-day' => 'בהיר', + 'weather_clear-night' => 'לילה בהיר', + 'weather_light-drizzle' => 'טפטוף קל', + 'weather_patchy-light-drizzle' => 'Patchy light drizzle', + 'weather_patchy-light-rain' => 'Patchy light rain', + 'weather_light-rain' => 'גשם קל', + 'weather_moderate-rain-at-times' => 'Moderate rain at times', + 'weather_moderate-rain' => 'Moderate rain', + 'weather_patchy-rain-possible' => 'Patchy rain possible', + 'weather_heavy-rain-at-times' => 'גשם כבד במקטעים', + 'weather_heavy-rain' => 'גשם כבד', + 'weather_light-freezing-rain' => 'Light freezing rain', + 'weather_moderate-or-heavy-freezing-rain' => 'Moderate or heavy freezing rain', + 'weather_light-sleet' => 'Light sleet', + 'weather_moderate-or-heavy-rain-shower' => 'Moderate or heavy rain shower', + 'weather_light-rain-shower' => 'Light rain shower', + 'weather_torrential-rain-shower' => 'Torrential rain shower', + 'weather_rain' => 'גשם', + 'weather_snow' => 'שלג', + 'weather_blowing-snow' => 'משבי שלג', + 'weather_patchy-light-snow' => 'Patchy light snow', + 'weather_light-snow' => 'שלג קל', + 'weather_patchy-moderate-snow' => 'Patchy moderate snow', + 'weather_moderate-snow' => 'שלג מתון', + 'weather_patchy-heavy-snow' => 'Patchy heavy snow', + 'weather_heavy-snow' => 'שלג כבד', + 'weather_light-snow-showers' => 'ממטרי שלג קלים', + 'weather_moderate-or-heavy-snow-showers' => 'Moderate or heavy snow showers', + 'weather_patchy-snow-possible' => 'Patchy snow possible', + 'weather_patchy-sleet-possible' => 'Patchy sleet possible', + 'weather_moderate-or-heavy-sleet' => 'Moderate or heavy sleet', + 'weather_light-sleet-showers' => 'Light sleet showers', + 'weather_moderate-or-heavy-sleet-showers' => 'Moderate or heavy sleet showers', + 'weather_sleet' => 'גשם שלג', + 'weather_wind' => 'רוח', + 'weather_fog' => 'ערפל', + 'weather_freezing-fog' => 'Freezing fog', + 'weather_mist' => 'ערפל', + 'weather_blizzard' => 'סופת שלג', + 'weather_overcast' => 'קודר', + 'weather_cloudy' => 'מעונן', + 'weather_partly-cloudy-day' => 'מעונן חלקית', + 'weather_partly-cloudy-night' => 'מעונן חלקית', + 'weather_freezing-drizzle' => 'טפטוף קפוא', + 'weather_heavy-freezing-drizzle' => 'Heavy freezing drizzle', + 'weather_patchy-freezing-drizzle-possible' => 'Patchy freezing drizzle possible', + 'weather_ice-pellets' => 'Ice pellets', + 'weather_light-showers-of-ice-pellets' => 'Light showers of ice pellets', + 'weather_moderate-or-heavy-showers-of-ice-pellets' => 'Moderate or heavy showers of ice pellets', + 'weather_thundery-outbreaks-possible' => 'Thundery outbreaks possible', + 'weather_patchy-light-rain-with-thunder' => 'Patchy light rain with thunder', + 'weather_moderate-or-heavy-rain-with-thunder' => 'Moderate or heavy rain with thunder', + 'weather_patchy-light-snow-with-thunder' => 'Patchy light snow with thunder', + 'weather_moderate-or-heavy-snow-with-thunder' => 'Moderate or heavy snow with thunder', + 'weather_current_temperature_celsius' => ':temperature °C', + 'weather_current_temperature_fahrenheit' => ':temperature °F', + 'weather_current_title' => 'מזג אוויר נוכחי', + + // dav + 'dav_contacts' => 'אנשי קשר', + 'dav_contacts_description' => 'אנשי הקשר של :name', + 'dav_birthdays' => 'ימי הולדת', + 'dav_birthdays_description' => 'ימי ההולדת של אנשי הקשר של :name', + 'dav_tasks' => 'משימות', + 'dav_tasks_description' => 'המשימות של :name', + + // contact list + 'contact_list_avatar' => 'תמונה ייצוגית', + 'contact_list_name' => 'איש קשר', + 'contact_list_description' => 'תיאור', + +]; diff --git a/resources/lang/he/auth.php b/resources/lang/he/auth.php new file mode 100644 index 0000000..6b4802a --- /dev/null +++ b/resources/lang/he/auth.php @@ -0,0 +1,89 @@ + 'פרטי הזהות האלה אינם תואמים את רישומינו.', + 'throttle' => 'בוצעו יותר מדי ניסיונות כניסה כושלים. נא לנסות שוב בעוד :seconds שניות.', + 'not_authorized' => 'אין לך הרשאה להריץ את הפעולה הזאת', + 'signup_disabled' => 'ההרשמה מושבתת כרגע', + 'signup_error' => 'אירעה שגיאה בעת רישום המשתמש', + 'back_homepage' => 'חזרה לדף הבית', + 'mfa_auth_otp' => 'אימות עם ההתקן שלך לאימות דו־שלבי', + 'mfa_auth_webauthn' => 'אימות עם מפתח אבטחה (WebAuthn)', + '2fa_title' => 'אימות דו־שלבי', + '2fa_wrong_validation' => 'האימות הדו־שלבי נכשל.', + '2fa_one_time_password' => 'קוד אימות דו־שלבי', + '2fa_recuperation_code' => 'נא להקליד את קוד השחזור לאימות הדו־שלבי', + '2fa_one_time_or_recuperation' => 'לא למלא קוד אימות דו־שלבי או קוד שחזור', + '2fa_otp_help' => 'יש לפתוח את יישומון האימות הדו־שלבי שלך ולהעתיק את הקוד', + + 'login_to_account' => 'כניסה לחשבון שלך', + 'login_with_recovery' => 'כניסה עם קוד שחזור', + 'login_again' => 'נא להיכנס לחשבונך פעם נוספת', + 'email' => 'דוא״ל', + 'password' => 'ססמה', + 'recovery' => 'קוד שחזור', + 'login' => 'כניסה', + 'button_remember' => 'לשמור את הפרטים שלי', + 'password_forget' => 'שכחת את הססמה שלך?', + 'password_reset' => 'איפוס הססמה שלך', + 'use_recovery' => 'באפשרותך להשתמש גם בקוד שחזור', + 'signup_no_account' => 'אין לך חשבון?', + 'signup' => 'הרשמה', + 'create_account' => 'ניתן ליצור את החשבון הראשון על ידי הרשמה', + 'change_language_title' => 'החלפת שפה:', + 'change_language' => 'החלפת השפה ל:lang', + + 'password_reset_title' => 'איפוס ססמה', + 'password_reset_email' => 'כתובת דוא״ל', + 'password_reset_send_link' => 'שליחת קישור לאיפוס הססמה', + 'password_reset_password' => 'ססמה', + 'password_reset_password_confirm' => 'אימות הססמה', + 'password_reset_action' => 'איפוס ססמה', + 'password_reset_email_content' => 'יש ללחוץ כאן כדי לאפס את הססמה שלך:', + + 'register_title_welcome' => 'ברוך בואך לעותק החדש של מוניקה שזה עתה התקנת', + 'register_create_account' => 'עליך ליצור חשבון כדי להשתמש במוניקה', + 'register_title_create' => 'יצירת חשבון אצל מוניקה', + 'register_login' => 'ניתן להיכנס אם כבר יש לך חשבון.', + 'register_email' => 'נא להקליד כתובת דוא״ל תקנית', + 'register_email_example' => 'you@home', + 'register_firstname' => 'שם פרטי', + 'register_firstname_example' => 'למשל: ירון', + 'register_lastname' => 'שם משפחה', + 'register_lastname_example' => 'למשל: כהן', + 'register_password' => 'ססמה', + 'register_password_example' => 'נא להקליד ססמה מאובטחת', + 'register_password_confirmation' => 'אימות ססמה', + 'register_action' => 'רישום', + 'register_policy' => 'הרשמה מאמתת שקראת והסכמת למדיניות הפרטיות ולתנאי השימוש שלנו.', + 'register_invitation_email' => 'מטעמי אבטחה, נא לציין את כתובת הדוא״ל של מי שהזמין אותך להצטרף לחשבון הזה. המידע הזה מופיע בהודעת ההזמנה.', + + 'confirmation_title' => 'אימות כתובת הדוא״ל שלך', + 'confirmation_fresh' => 'נשלח קישור אימות טרי לכתובת הדוא״ל שלך.', + 'confirmation_check' => 'בטרם המשך התהליך, נא לחפש את קישור האימות בתיבת הדוא״ל שלך.', + 'confirmation_request_another' => 'אם לא קיבלת את ההודעה בדוא״ל יש ללחוץ כאן כדי לבקש אחת נוספת.', + + 'confirmation_again' => 'כדי לשנות את כתובת הדוא״ל שלך נא ללחוץ כאן.', + 'email_change_current_email' => 'כתובת הדוא״ל הנוכחית:', + 'email_change_title' => 'החלפת כתובת הדוא״ל שלך', + 'email_change_new' => 'כתובת דוא״ל חדשה', + 'email_changed' => 'כתובת הדוא״ל שלך הוחלפה. נא לבדוק בתיבת הדוא״ל שלך כדי לאמת אותה.', +]; diff --git a/resources/lang/he/changelog.php b/resources/lang/he/changelog.php new file mode 100644 index 0000000..d5c1f69 --- /dev/null +++ b/resources/lang/he/changelog.php @@ -0,0 +1,12 @@ + 'שינויים במוצר', + 'note' => 'הערה: לרוע המזל, עמוד זה הוא באנגלית בלבד.', +]; diff --git a/resources/lang/he/dashboard.php b/resources/lang/he/dashboard.php new file mode 100644 index 0000000..b8fd3e7 --- /dev/null +++ b/resources/lang/he/dashboard.php @@ -0,0 +1,42 @@ + 'ברוך בואך לחשבון שלך!', + 'dashboard_blank_description' => 'מוניקה הוא המקום לארגן את כל המגע החברתי שלך עם אלו שאכפת לך מהם.', + 'dashboard_blank_cta' => 'נא להוסיף את איש הקשר הראשון שלך', + 'dashboard_blank_illustration' => 'ציור מאת Freepik', + + 'notes_title' => 'אין לך הערות שסימנת בכוכב עדיין.', + + 'tab_recent_calls' => 'שיחות אחרונות', + 'tab_favorite_notes' => 'הערות מועדפות', + 'tab_calls_blank' => 'לא תיעדת אף שיחה עדיין.', + 'tab_debts' => 'חובות', + 'tab_debts_blank' => 'לא תיעדת חובות עדיין.', + 'tab_tasks' => 'משימות', + 'tab_tasks_blank' => 'אין לך משימות עדיין.', + + 'tasks_add_task_placeholder' => 'מה מהות המשימה הזאת?', + 'tasks_tab_your_contacts' => 'משימות שקשורות לאנשי הקשר שלך', + 'tasks_tab_your_tasks' => 'המשימות שלך', + 'tasks_add_note' => 'נא ללחוץ על Enter כדי להוסיף את המשימה.', + 'task_add_cta' => 'הוספת משימה', + + 'debts_you_owe' => 'חובך הוא', + + 'statistics_contacts' => 'אנשי קשר', + 'statistics_activities' => 'פעילויות', + 'statistics_gifts' => 'מתנות', + + 'reminders_next_months' => 'אירועים ב־3 החודשים הקרובים', + 'reminders_none' => 'אין תזכורת לחודש הזה.', + + 'product_changes' => 'שינויים במוצר', + 'product_view_details' => 'הצגת פרטים', +]; diff --git a/resources/lang/he/format.php b/resources/lang/he/format.php new file mode 100644 index 0000000..f27f140 --- /dev/null +++ b/resources/lang/he/format.php @@ -0,0 +1,36 @@ + 'd בM,‏ Y H:i', + 'short_date_year' => 'd בM, Y', + 'short_date' => 'd בM', + 'short_month' => 'M', + 'short_month_year' => 'M Y', + 'short_day' => 'D', + 'full_date_year' => 'd בF, Y', + 'full_month' => 'F', + 'full_month_year' => 'F Y', + 'full_hour' => 'H.i', + + 'short_text' => '{text}…', +]; diff --git a/resources/lang/he/journal.php b/resources/lang/he/journal.php new file mode 100644 index 0000000..bb79a39 --- /dev/null +++ b/resources/lang/he/journal.php @@ -0,0 +1,38 @@ + 'איך עבר עליך היום? ניתן לדרג פעם ביום.', + 'journal_come_back' => 'תודה. מזמינים אותך לדרג את יומך גם מחר.', + 'journal_description' => 'לתשומת לבך: ביומן מופיעים רשומות ידניות לצד רשומות אוטומטיות כגון פעילויות שקיימת עם אנשי הקשר שלך. בעוד שניתן למחוק רשומות ביומן ידנית, יהיה עליך למחוק את הפעילות ישירות בעמוד של איש הקשר.', + 'journal_add' => 'הוספת רשומה ביומן', + 'journal_edit' => 'עריכת רשומה ביומן', + 'journal_empty' => 'יומן ריק', + 'journal_created_at' => 'נוצר ב־{date}', + 'journal_created_automatically' => 'נוצרה אוטומטית', + 'journal_entry_type_journal' => 'רשומה ביומן', + 'journal_entry_type_activity' => 'פעילות', + 'journal_entry_rate' => 'דירגת את היום שלך.', + 'journal_add_comment' => 'מעניין אותך להוסיף הערה (רשות)?', + 'journal_show_comment' => 'הצגת הערה', + 'entry_delete_success' => 'הרשומה ביומן נמחקה בהצלחה.', + 'journal_add_title' => 'כותרת (רשות)', + 'journal_add_date' => 'תאריך', + 'journal_add_post' => 'רשומה', + 'journal_add_cta' => 'שמירה', + 'journal_blank_cta' => 'ניתן להוסיף את רשומת היומן הראשונה שלך', + 'journal_blank_description' => 'היומן מאפשר לך לכתוב אירועים שעברו עליך ולזכור אותם.', + 'delete_confirmation' => 'למחוק את הרשומה הזאת ביומן?', + 'apply_filter' => 'Apply filter', + 'start_date' => 'Start Date', + 'end_date' => 'End Date', + 'per_page' => 'Per Page', + 'sort_order' => 'Sort By Created At', + 'ascending' => 'Ascending', + 'descending' => 'Descending', +]; diff --git a/resources/lang/he/logs.php b/resources/lang/he/logs.php new file mode 100644 index 0000000..da8d543 --- /dev/null +++ b/resources/lang/he/logs.php @@ -0,0 +1,29 @@ + 'איש הקשר נוצר.', + 'settings_log_contact_created_with_name' => 'הוספה של :name כאיש קשר.', + + // contat description update + 'contact_log_contact_description_updated' => 'התיאור עודכן.', + 'settings_log_contact_description_updated_with_name' => 'התיאור של :name עודכן.', + + // contact description clear + 'contact_log_contact_description_cleared' => 'התיאור נמחק.', + 'settings_log_contact_description_cleared_with_name' => 'התיאור של :name נמחק.', + + // contact work information update + 'contact_log_contact_work_updated' => 'פרטי העבודה עודכנו.', + 'settings_log_contact_work_updated_with_name' => 'פרטי העבודה של :name עודכנו.', + + // company created + 'settings_log_company_created' => 'נוצרה חברה בשם :name.', +]; diff --git a/resources/lang/he/mail.php b/resources/lang/he/mail.php new file mode 100644 index 0000000..0b205d1 --- /dev/null +++ b/resources/lang/he/mail.php @@ -0,0 +1,53 @@ + 'תזכורת עבור :contact', + 'greetings' => 'היי :username', + 'want_reminded_of' => 'רצית שאזכיר לך את :reason', + 'for' => 'עבור: :name', + 'comment' => 'הערה: :comment', + 'footer_contact_info' => 'הוספה, צפייה, השלמה ושינוי מידע על איש הקשר הזה:', + 'footer_contact_info2' => 'הצגת הפרופיל של :profile', + 'footer_contact_info2_link' => 'הצגת הפרופיל של :name‏ :url', + + 'notification_subject_line' => 'יש לך אירוע קרב', + 'notification_description' => 'בעוד :count ימים (ב־:date), יתרחש האירוע הבא:', + + 'stay_in_touch_subject_line' => 'לשמור על קשר עם :name', + 'stay_in_touch_subject_description' => '{1}ביקשת לקבל תזכורת ליצור קשר עם :name כל יום.|{2}ביקשת לקבל תזכורת ליצור קשר עם :name כל יומיים.|{3,n}ביקשת לקבל תזכורת ליצור קשר עם :name כל :frequency ימים.', + + 'notifications_whoops' => 'אופס!', + 'notifications_hello' => 'שלום!', + 'notifications_regards' => 'בברכה', + 'notifications_footer' => 'אם נתקלת בבעיה בלחיצה על הכפתור „:actionText”, יש להעתיק ולהדביק את הכתובת שלהלן לתוך הדפדפן שלך: [:actionURL](:actionURL)', + 'notifications_rights' => 'כל הזכויות שמורות', + + 'confirmation_email_title' => 'מוניקה – אימות דוא״ל', + 'confirmation_email_intro'=> 'כדי לאמת את כתובת הדוא״ל שלך נא ללחוץ על הכפתור שלהלן', + 'confirmation_email_button' => 'אימות כתובת דוא״ל', + 'confirmation_email_bottom' => 'אם לא יצרת חשבון, לא נדרשות פעולות נוספות.', + + 'password_reset_title' => 'מוניקה - התראת איפוס ססמה', + 'password_reset_intro' => 'הודעה זו נשלחה אליך כי התקבלה בקשה לאיפוס הססמה בחשבונך.', + 'password_reset_button' => 'איפוס ססמה', + 'password_reset_expiration' => 'קישור זה לאיפוס הססמה יפוג בעוד :count דקות.', + 'password_reset_bottom' => 'אם לא ביקשת לאפס את הססמה, לא נדרשות פעולות נוספות.', + + 'invitation_title' => 'מוניקה – קיבלת הזמנה מאת :name', + 'invitation_intro' => 'הוזמנת על ידי :name (:email) להשתמש במוניקה, כלי נחמד לניהול קשרים אישיים.', + 'invitation_link' => 'כדי לקבל את ההזמנה, יש ללחוץ על הקישור להלן:', + 'invitation_button' => 'קבלת ההזמנה', + 'invitation_expiration' => 'הקישור יפוג תוך :count ימים.', + + 'export_title' => 'הייצוא שלך מוכן', + 'export_description' => 'ביקשת ייצוא נתונים ב־:date. הוא מוכן כעת להורדה.', + 'export_download' => 'הורדת הייצוא', + +]; diff --git a/resources/lang/he/pagination.php b/resources/lang/he/pagination.php new file mode 100644 index 0000000..ff666be --- /dev/null +++ b/resources/lang/he/pagination.php @@ -0,0 +1,25 @@ + '❮ הקודם', + 'next' => 'הבא ❯', + +]; diff --git a/resources/lang/he/passwords.php b/resources/lang/he/passwords.php new file mode 100644 index 0000000..93c878a --- /dev/null +++ b/resources/lang/he/passwords.php @@ -0,0 +1,30 @@ + 'הססמה שלך אופסה!', + 'sent' => 'אם כתובת הדוא״ל שהזנת קיימת ברישומים שלנו, נשלח אליך קישור לאיפוס הססמה.', + 'token' => 'אסימון איפוס הססמה הזאת שגוי.', + 'user' => 'אם כתובת הדוא״ל שהזנת קיימת ברישומים שלנו, נשלח אליך קישור לאיפוס הססמה.', + 'changed' => 'הססמה הוחלפה בהצלחה.', + 'invalid' => 'הססמה הנוכחית שהקלדת שגויה.', + 'throttled' => 'נא להמתין לפני ביצוע ניסיון נוסף.', + +]; diff --git a/resources/lang/he/people.php b/resources/lang/he/people.php new file mode 100644 index 0000000..9ae5424 --- /dev/null +++ b/resources/lang/he/people.php @@ -0,0 +1,539 @@ + 'איש הקשר לא נמצא', + 'people_list_number_kids' => 'צאצא אחד|:count צאצאים', + 'people_list_last_updated' => 'יעוץ אחרון:', + 'people_list_number_reminders' => 'תזכורת אחת|:count תזכורות', + 'people_list_blank_title' => 'אין אף אחד בחשבון שלך עדיין', + 'people_list_blank_cta' => 'להוסיף מישהו', + 'people_list_sort' => 'מיון', + 'people_list_stats' => 'איש קשר אחד|:count אנשי קשר', + 'people_list_firstnameAZ' => 'מיון לפי שם פרטי א ← ת', + 'people_list_firstnameZA' => 'מיון לפי שם פרטי ת ← א', + 'people_list_lastnameAZ' => 'מיון לפי שם משפחה א ← ת', + 'people_list_lastnameZA' => 'מיון לפי שם משפחה ת ← א', + 'people_list_lastactivitydateNewtoOld' => 'מיון לפי מועד הפעילות האחרונה, מהחדשה לישנה', + 'people_list_lastactivitydateOldtoNew' => 'מיון לפי מועד הפעילות האחרונה, מהישנה לחדשה', + 'people_list_filter_tag' => 'מוצגים כל אנשי הקשר עם התגית', + 'people_list_clear_filter' => 'ניקוי מסנן', + 'people_list_contacts_per_tags' => 'איש קשר אחד|:count אנשי קשר', + 'people_list_show_dead' => 'הצגת מנוחים (:count)', + 'people_list_hide_dead' => 'הסתרת מנוחים (:count)', + 'people_search' => 'חיפוש באנשי הקשר שלך…', + 'people_search_no_results' => 'לא נמצאו תוצאות', + 'people_search_next' => 'הבא', + 'people_search_prev' => 'הקודם', + 'people_search_rows_per_page' => 'שורות בכל עמוד', + 'people_search_of' => 'מתוך', + 'people_search_page' => 'עמוד', + 'people_search_all' => 'הכול', + 'people_add_new' => 'הוספת אדם חדש', + 'people_list_account_usage' => 'ניצולת החשבון שלך: :current/:limit אנשי קשר', + 'people_list_account_upgrade_title' => 'ניתן לשדרג את החשבון שלך כדי ליהנות משפע התכונות שיש לנו להציע.', + 'people_list_account_upgrade_cta' => 'לשדרג כעת', + 'people_list_untagged' => 'הצגת אנשי קשר ללא תיוג', + 'people_list_filter_untag' => 'מוצגים כל אנשי קשר ללא תיוג', + 'archived_contact_readonly' => 'לא ניתן לערוך אנשי קשר בארכיון, נא להוציא מהארכיון תחילה.', + + // people add + 'people_add_title' => 'הוספת אדם חדש', + 'people_add_missing' => 'לא נמצא אף אחד - ניתן להוסיף אחד כעת', + 'people_add_firstname' => 'שם פרטי', + 'people_add_middlename' => 'שם אמצעי (רשות)', + 'people_add_lastname' => 'שם משפחה (רשות)', + 'people_add_email' => 'דוא״ל (רשות)', + 'people_add_nickname' => 'כינוי (רשות)', + 'people_add_cta' => 'הוספה', + 'people_save_and_add_another_cta' => 'הגשה והוספת עוד מישהו', + 'people_add_success' => 'היצירה של :name הושלמה בהצלחה', + 'people_add_gender' => 'מגדר', + 'people_delete_success' => 'איש הקשר נמחק', + 'people_delete_message' => 'מחיקת איש קשר', + 'people_delete_confirmation' => 'למחוק את פרטי הקשר של :name? מחיקה היא מיידית ולצמיתות.', + 'people_add_birthday_reminder' => 'נא לאחל יום הולדת שמח ל־:name', + 'people_add_birthday_reminder_deceased' => 'בתאריך הזה, אמור היה להחגג יום ההולדת של :name', + 'people_add_import' => 'ברצונך לייבא את אנשי הקשר שלך?', + 'people_edit_email_error' => 'כבר יש איש קשר בחשבון שלך עם כתובת הדוא״ל הזו. נא לבחור באחד אחר.', + 'people_export' => 'ייצוא כ־vCard', + 'people_add_reminder_for_birthday' => 'יצירת תזכורת שנתית ליומולדת', + + // show + 'section_contact_information' => 'פרטי קשר', + 'section_personal_activities' => 'פעילויות', + 'section_personal_reminders' => 'תזכורות', + 'section_personal_tasks' => 'משימות', + 'section_personal_gifts' => 'מתנות', + 'section_personal_notes' => 'הערות', + + // archived contacts + 'list_link_to_active_contacts' => 'הרשימה המוצגת היא אנשי קשר בארכיון. עליך לצפות ברשימת אנשי הקשר הפעילים במקום.', + 'list_link_to_archived_contacts' => 'הצגת אנשי קשר בארכיון', + + // Header + 'me' => 'מדובר בך', + 'edit_contact_information' => 'עריכת פרטים ליצירת קשר', + 'contact_archive' => 'העברת איש קשר לארכיון', + 'contact_unarchive' => 'הוצאת איש קשר מהארכיון', + 'contact_archive_help' => 'אנשי קשר בארכיון לא יופיעו ברשימת אנשי הקשר אך עדיין יופיעו בתוצאות החיפוש.', + 'call_button' => 'תיעוד שיחה', + 'set_favorite' => 'אנשי קשר מועדפים עולים לראש רשימת אני הקשר', + + // Stay in touch + 'stay_in_touch' => 'לשמור על קשר', + 'stay_in_touch_frequency' => 'להישאר בקשר כל יום|להישאר בקשר כל יומיים|להישאר בקשר כל {count} ימים', + 'stay_in_touch_next_date' => 'המועד הבא: {date}', + 'stay_in_touch_invalid' => 'התדירות חייבת להיות מספר גדול מ־0.', + 'stay_in_touch_premium' => 'עליך לשדרג את החשבון שלך כדי להשתמש בתכונה זו', + 'stay_in_touch_modal_title' => 'לשמור על קשר', + 'stay_in_touch_modal_desc' => 'נוכל להזכיר לך בהודעה בדוא״ל לשמור על קשר עם {firstname} במרווחי זמן קבועים.', + 'stay_in_touch_modal_label' => 'לשלוח לי הודעה בדוא״ל כל יום|לשלוח לי הודעה בדוא״ל כל יומיים|לשלוח לי הודעה בדוא״ל כל… {count} ימים|לשלוח לי הודעה בדוא״ל כל… {count} ימים', + + // Calls + 'modal_call_title' => 'תיעוד שיחה', + 'modal_call_comment' => 'על מה דיברתם? (רשות)', + 'modal_call_exact_date' => 'שיחת הטלפון התקיימה ב־', + 'modal_call_who_called' => 'מי היה בטלפון?', + 'modal_call_emotion' => 'מעניין אותך לתעד איך הרגשת במהלך השיחה? (רשות)', + 'calls_add_success' => 'שיחת הטלפון נשמרה.', + 'call_delete_confirmation' => 'למחוק את השיחה הזאת?', + 'call_delete_success' => 'שיחת הטלפון נמחקה בהצלחה', + 'call_title' => 'שיחות טלפון', + 'call_empty_comment' => 'אין פרטים', + 'call_blank_title' => 'מעקב אחר שיחות הטלפון שקיימת עם {name}', + 'call_blank_desc' => 'התקשרת אל {name}', + 'call_you_called' => 'התקשרת', + 'call_he_called' => 'קיבלת שיחה מאת {name}', + 'call_emotions' => 'רגשות:', + + // Conversation + 'conversation_blank' => 'תיעוד דיונים שערכת עם :name ברשתות חברתיות, מסרונים וכו׳…', + 'conversation_delete_link' => 'מחיקת הדיון', + 'conversation_edit_title' => 'עריכת הדיון', + 'conversation_edit_delete' => 'למחוק את הדיון? מחיקה היא לצמיתות.', + 'conversation_add_success' => 'הדיון נוסף בהצלחה.', + 'conversation_edit_success' => 'הדיון עודכן בהצלחה.', + 'conversation_delete_success' => 'הדיון נמחק בהצלחה.', + 'conversation_add_title' => 'תיעוד דיון חדש', + 'conversation_add_when' => 'מתי הדיון הזה התרחש?', + 'conversation_add_who_wrote' => 'למי שייכת ההודעה הזו?', + 'conversation_add_how' => 'איך תקשרתם?', + 'conversation_add_you' => 'אני', + 'conversation_add_content' => 'נא לכתוב את מה שאמרת', + 'conversation_add_what_was_said' => 'מה אמרת?', + 'conversation_add_another' => 'הוספת הודעה נוספת', + 'conversation_add_error' => 'עליך להוסיף הודעה אחת לפחות.', + 'conversation_list_table_messages' => 'הודעות', + 'conversation_list_table_content' => 'תוכן חלקי (הודעה אחרונה)', + 'conversation_list_title' => 'דיונים', + 'conversation_list_cta' => 'תיעוד דיון ביומן', + + // age - birthday + 'birthdate_not_set' => 'תאריך הלידה לא הוגדר', + 'age_approximate_in_years' => 'הגיל הוא בערך :age', + 'age_exact_in_years' => '{1}בגיל שנה|{2}בגיל שנתיים|[3,*]בגיל :age שנים', + 'age_exact_birthdate' => 'לידה ב־:date', + + // Last called + 'last_called' => 'שיחת הטלפון האחרונה: :date', + 'last_talked_to' => 'שיחה אחרונה: {date}', + 'last_called_empty' => 'שיחת הטלפון האחרונה: לא ידוע', + 'last_activity_date' => 'פעילות אחרונה יחד: :date', + 'last_activity_date_empty' => 'פעילות אחרונה יחד: לא ידוע', + + // additional information + 'information_edit_success' => 'הפרופיל עודכן בהצלחה', + 'information_edit_title' => 'עריכת הפרטים האישיים של :name', + 'information_edit_max_size' => ':size ק״ב לכל היותר.', + 'information_edit_max_size2' => '{size} קילוסיביות לכל היותר.', + 'information_edit_firstname' => 'שם פרטי', + 'information_edit_lastname' => 'שם משפחה (רשות)', + 'information_edit_description' => 'תיאור (רשות)', + 'information_edit_description_help' => 'משמש עבור רשימת אנשי הקשר כדי להוסיף הקשר אם יש צורך בכך.', + 'information_edit_unknown' => 'גיל האדם הזה לא ידוע לי', + 'information_edit_probably' => 'הגיל של איש הקשר הוא כנראה…', + 'information_edit_not_year' => 'היום והחודש בהם חל יום ההולדת של אדם זה ידועים לי, אך לא השנה…', + 'information_edit_exact' => 'יום ההולדת שלהם ידוע לי במדויק…', + 'information_edit_birthdate_label' => 'יום הולדת', + 'information_no_work_defined' => 'לא צוינו פרטי עבודה', + 'information_work_at' => 'ב:company', + 'work_add_cta' => 'עדכון פרטי עבודה', + 'work_edit_success' => 'פרטי העבודה עודכנו', + 'work_edit_title' => 'עדכון פרטי העבודה של :name', + 'work_edit_job' => 'תפקיד (רשות)', + 'work_edit_company' => 'חברה (רשות)', + 'work_information' => 'פרטי תעסוקה', + + // food preferences + 'food_preferences_add_success' => 'העדפות המזון נשמרו', + 'food_preferences_edit_description' => 'אולי ל:firstname או למישהו ממשפחת :family יש אלרגיה. או איזה סוג יין לא אהוב במיוחד. ניתן לציין את אלה כאן כדי להיזכר בהם בהזמנה הבאה לארוחת ערב', + 'food_preferences_edit_description_no_last_name' => 'אולי ל־:firstname יש אלרגיה. או איזה סוג יין לא אהוב במיוחד. ניתן לציין את אלה כאן כדי להיזכר בהם בהזמנה הבאה לארוחת ערב', + 'food_preferences_edit_title' => 'ציון העדפות מזון', + 'food_preferences_edit_cta' => 'שמירת העדפות מזון', + 'food_preferences_title' => 'העדפות מזון', + 'food_preferences_cta' => 'הוספת העדפות מזון', + + // reminders + 'reminders_blank_title' => 'יש משהו שברצונך לקבל עליו תזכורת בנוגע ל־:name?', + 'reminders_blank_add_activity' => 'הוספת תזכורת', + 'reminders_add_title' => 'מה להזכיר לך בנוגע ל־:name?', + 'reminders_add_description' => 'נא להזכיר לי לעשות…', + 'reminders_add_next_time' => 'מה הפעם הבאה שברצונך לקבל על כך תזכורת?', + 'reminders_add_once' => 'להזכיר לי על כך פעם אחת בלבד', + 'reminders_add_recurrent' => 'להזכיר לי על כך כל', + 'reminders_add_starting_from' => 'החל מהיום שצוין להלן', + 'reminders_add_cta' => 'הוספת תזכורת', + 'reminders_edit_update_cta' => 'עדכון תזכורת', + 'reminders_add_error_custom_text' => 'עליך לציין טקסט לתזכורת הזו', + 'reminders_create_success' => 'התזכורת נוספה בהצלחה', + 'reminders_delete_success' => 'התזכורת נמחקה בהצלחה', + 'reminders_update_success' => 'התזכורת עודכנה בהצלחה', + 'reminders_add_optional_comment' => 'הערת רשות', + + 'reminder_frequency_day' => '{1} כל יום|{2} כל יומיים|[3,*] כל :number ימים', + 'reminder_frequency_week' => '{1} כל שבוע| {2} כל שבועיים|[3,*] כל :number שבועות', + 'reminder_frequency_month' => '{1} כל חודש| {2} כל חודשיים|[3,*] כל :number חודשים', + 'reminder_frequency_year' => '{1} כל שנה| {2} כל שנתיים|[3,*] כל :number שנים', + 'reminder_frequency_one_time' => 'ב־:date', + 'reminders_delete_confirmation' => 'למחוק את התזכורת הזו?', + 'reminders_delete_cta' => 'מחיקה', + 'reminders_next_expected_date' => 'ב־', + 'reminders_cta' => 'הוספת תזכורת', + 'reminders_description' => 'אנו נשלח דוא״ל עבור כל אחת מהתזכורות שלהלן. תזכורות נשלחות כל בוקר ביום בו מתקיים האירוע. תזכורות נוספות אוטומטית לימי הולדת ולא ניתן למחוק אותן. לשינוי התאריכים האלה יש לשנות את תאריכי הלידה של אנשי הקשר.', + 'reminders_one_time' => 'חד פעמי', + 'reminders_type_week' => 'שבוע', + 'reminders_type_month' => 'חודש', + 'reminders_type_year' => 'שנה', + 'reminders_birthday' => 'יום ההולדת של :name', + 'reminders_free_plan_warning' => 'התכנית שלך היא התכנית החינמית. בתכנית הזאת לא נשלחות הודעות בדוא״ל. כדי לקבל תזכורות בדוא״ל יש לשדרג את החשבון שלך.', + + // relationships + 'relationship_form_add' => 'הוספת קשר חדש', + 'relationship_form_edit' => 'עריכת קשר קיים', + 'relationship_form_is_with' => 'איש הקשר הוא…', + 'relationship_form_is_with_name' => ':name…', + 'relationship_form_add_choice' => 'עם מי הקשר הזה מתקיים?', + 'relationship_form_create_contact' => 'הוספת אדם חדש', + 'relationship_form_associate_contact' => 'איש קשר קיים', + 'relationship_form_associate_dropdown' => 'ניתן לחפש ולבחור איש קשר קיים מהרשימה הנגללת שלהלן', + 'relationship_form_associate_dropdown_placeholder' => 'חיפוש ובחירה באיש קשר קיים', + 'relationship_form_also_create_contact' => 'יצירת רשומת איש קשר לאדם זה.', + 'relationship_form_add_description' => 'בחירה זו תאפשר לך להתייחס לאדם כמו לכל איש קשר אחר.', + 'relationship_form_add_no_existing_contact' => 'אין לך אנשי קשר שיכולים לקיים איזשהו קשר מול :name כרגע.', + 'relationship_delete_confirmation' => 'למחוק את הקשר הזה? מחיקה היא בלתי הפיכה.', + 'relationship_unlink_confirmation' => 'למחוק את הקשר הזה? האדם לא יימחק – רק הקשר בין השניים.', + 'relationship_form_add_success' => 'הקשר הוגדר בהצלחה.', + 'relationship_form_deletion_success' => 'הקשר נמחק.', + + // tasks + 'tasks_title' => 'משימות', + 'tasks_blank_title' => 'אין לך משימות עדיין.', + 'tasks_form_title' => 'כותרת', + 'tasks_form_description' => 'תיאור (רשות)', + 'tasks_add_task' => 'הוספת משימה', + 'tasks_delete_success' => 'המשימה נמחקה בהצלחה', + 'tasks_complete_success' => 'מצב המשימה השתנה בהצלחה', + + // activities + 'activity_title' => 'פעילויות', + 'activity_type_category_simple_activities' => 'פעילויות פשוטות', + 'activity_type_category_sport' => 'ספורט', + 'activity_type_category_food' => 'אוכל', + 'activity_type_category_cultural_activities' => 'פעילויות תרבותיות', + 'activity_type_just_hung_out' => 'בילוי משותף', + 'activity_type_watched_movie_at_home' => 'צפיתם בסרט בבית', + 'activity_type_talked_at_home' => 'דיברתם בבית', + 'activity_type_did_sport_activities_together' => 'עסקתם בפעילות ספורטיבית משותפת', + 'activity_type_ate_at_his_place' => 'אכלתם אצלם', + 'activity_type_went_bar' => 'הלכת לבר', + 'activity_type_ate_at_home' => 'אכלתם בבית', + 'activity_type_picnicked' => 'פיקניק', + 'activity_type_ate_restaurant' => 'אכלתם במסעדה', + 'activity_type_went_theater' => 'הלכתם לתיאטרון', + 'activity_type_went_concert' => 'הלכתם להופעה', + 'activity_type_went_play' => 'הלכתם להצגה', + 'activity_type_went_museum' => 'הלכתם למוזיאון', + 'activities_add_activity' => 'הוספת פעילות', + 'activities_add_more_details' => 'הוספת פרטים נוספים', + 'activities_add_emotions' => 'הוספת רגשות', + 'activities_add_category' => 'ציון קטגוריה', + 'activities_add_participants_cta' => 'הוספת משתתפים', + 'activities_item_information' => ':Activity. התקיימה ב־:date', + 'activities_add_title' => 'מה עשית עם {name}?', + 'activities_summary' => 'נא לתאר את אופן הפעילות', + 'activities_add_pick_activity' => 'לסווג את הפעילות הזאת? לא חובה אך סיווג יאפשר לך לערוך סטטיסטיקה בהמשך (רשות)', + 'activities_add_date_occured' => 'הפעילות התרחשה ב…', + 'activities_add_participants' => 'מי, למעט {name}, השתתף בפעילות הזאת? (רשות)', + 'activities_add_emotions_title' => 'מעניין אותך לתעד איך הרגשת במהלך הפעילות? (רשות)', + 'activities_blank_title' => 'מעקב אחר מה שעשית עם {name} בעבר ועל מה דיברתם', + 'activities_blank_add_activity' => 'הוספת פעילות', + 'activities_add_success' => 'הפעילות נוספה בהצלחה', + 'activities_add_error' => 'אירעה שגיאה בעת הוספת הפעילות', + 'activities_update_success' => 'הפעילות עודכנה בהצלחה', + 'activities_delete_success' => 'הפעילות נמחקה בהצלחה', + 'activities_who_was_involved' => 'מי היה מעורב?', + 'activities_activity' => 'קטגוריית הפעילות', + 'activities_view_activities_report' => 'הצגת דוח פעילות', + 'activities_profile_title' => 'דוח פעילות עבורך ועבור :name', + 'activities_profile_subtitle' => 'תיעדת פעילות אחת עם :name בסך הכול ו־:activities_last_twelve_months ב־12 החודשים האחרונים עד כה.|תיעדת :total_activities עם :name בסך הכול ו־:activities_last_twelve_months ב־12 החודשים האחרונים עד כה.', + 'activities_profile_year_summary_activity_types' => 'להלן פילוח של סוגי הפעילויות אותן ביצעתם יחדיו ב־:year', + 'activities_profile_year_summary' => 'הנה מה שעשיתם יחד ב־:year', + 'activities_profile_number_occurences' => 'פעילות אחת|:value פעילויות', + 'activities_list_participants' => 'משתתפים ({total}):', + 'activities_list_emotions' => 'רגשות שהרגשת:', + 'activities_list_date' => 'מועד הפעילות', + 'activities_list_category' => 'קטגוריה:', + + // notes + 'notes_create_success' => 'ההערה נוצרה בהצלחה', + 'notes_update_success' => 'ההערה נשמרה בהצלחה', + 'notes_delete_success' => 'ההערה נמחקה בהצלחה', + 'notes_add_cta' => 'הוספת הערה', + 'notes_favorite' => 'הוספה/הסרה מהמועדפים', + 'notes_delete_title' => 'מחיקת הערה', + 'notes_delete_confirmation' => 'למחוק את ההערה הזאת? מחיקה אינה הפיכה', + + // gifts + 'gifts_title' => 'מתנות', + 'gifts_add_success' => 'המתנה הזאת נוספה בהצלחה', + 'gifts_delete_success' => 'המתנה הזאת נמחקה בהצלחה', + 'gifts_delete_confirmation' => 'למחוק את המתנה הזאת?', + 'gifts_add_gift' => 'הוספת מתנה', + 'gifts_link' => 'קישור', + 'gifts_for' => 'עבור: {name}', + 'gifts_delete_cta' => 'מחיקה', + 'gifts_add_title' => 'ניהול מתנות עבור :name', + 'gifts_add_gift_idea' => 'רעיון למתנה', + 'gifts_add_gift_already_offered' => 'הוענקה מתנה', + 'gifts_add_gift_received' => 'מתנה שהתקבלה', + 'gifts_add_gift_title' => 'מה זו המתנה הזו?', + 'gifts_add_gift_name' => 'שם המתנה', + 'gifts_add_link' => 'קישור לאתר אינטרנט (רשות)', + 'gifts_add_value' => 'ערך (רשות)', + 'gifts_add_comment' => 'הערה (רשות)', + 'gifts_add_recipient' => 'למי מיועדת המתנה (רשות)', + 'gifts_add_recipient_field' => 'נמען', + 'gifts_add_photo' => 'תמונה (רשות)', + 'gifts_add_photo_title' => 'הוספת תמונה למתנה הזאת', + 'gifts_add_someone' => 'מתנה זו מיועדת במיוחד למישהו מהמשפחה של {name}', + 'gifts_delete_title' => 'מחיקת מתנה', + 'gifts_ideas' => 'רעיונות למתנות', + 'gifts_offered' => 'הוענקו מתנות', + 'gifts_offered_as_an_idea' => 'סימון כרעיון', + 'gifts_received' => 'מתנות שהתקבלו', + 'gifts_view_comment' => 'צפייה בהערה', + 'gifts_mark_offered' => 'סימון שניתנה', + 'gifts_update_success' => 'המתנה עודכנה בהצלחה', + 'gifts_add_date' => 'תאריך (רשות)', + + // debts + 'debt_delete_confirmation' => 'למחוק את החוב הזה?', + 'debt_delete_success' => 'החוב נמחק בהצלחה', + 'debt_add_success' => 'החוב נוסף בהצלחה', + 'debt_title' => 'חובות', + 'debt_add_cta' => 'הוספת חוב', + 'debt_you_owe' => 'החוב שלך הוא :amount', + 'debt_they_owe' => 'החוב של :name כלפיך הוא :amount', + 'debt_add_title' => 'ניהול חובות', + 'debt_add_you_owe' => 'יש לך חוב מול :name', + 'debt_add_they_owe' => 'ל־:name יש חוב מולך', + 'debt_add_amount' => 'על סך של', + 'debt_add_reason' => 'מהסיבה הבאה (רשות)', + 'debt_add_add_cta' => 'הוספת חוב', + 'debt_edit_update_cta' => 'עדכון חוב', + 'debt_edit_success' => 'החוב עודכן בהצלחה', + 'debts_blank_title' => 'ניהול חובות מול :name או חוב של :name מולך', + + // tags + 'tag_edit' => 'עריכת תגית', + 'tag_add' => 'הוספת תגיות', + 'tag_add_search' => 'הוספה או חיפוש תגיות', + 'tag_no_tags' => 'אין תגיות עדיין', + + // Introductions + 'introductions_sidebar_title' => 'איך נפגשתם', + 'introductions_blank_cta' => 'ציון כיצד פגשת את :name', + 'introductions_title_edit' => 'איך פגשת את :name?', + 'introductions_additional_info' => 'ניתן להסביר איך ואיפה נפגשתם', + 'introductions_edit_met_through' => 'נערכה לך היכרות על ידי מישהו עם האדם הזה?', + 'introductions_no_met_through' => 'אף אחד', + 'introductions_first_met_date' => 'תאריך המפגש', + 'introductions_no_first_met_date' => 'תאריך המפגש אינו ידוע לי', + 'introductions_first_met_date_known' => 'זה התאריך בו נפגשנו', + 'introductions_add_reminder' => 'הוספת תזכורת לחגוג את יום השנה להיכרותכם', + 'introductions_update_success' => 'עדכנת בהצלחה את המידע בנוגע לאופי המפגש שלך עם אדם זה', + 'introductions_met_through' => 'הכרתם דרך :name', + 'introductions_met_date' => 'נפגשתם ב־:date', + 'introductions_reminder_title' => 'יום השנה למועד ההיכרות ביניכם', + + // Deceased + 'deceased_reminder_title' => 'האזכרה של :name', + 'deceased_mark_person_deceased' => 'סימון פטירה של זה', + 'deceased_know_date' => 'מועד הפטירה של האדם הזה ידוע לי', + 'deceased_add_reminder' => 'הוספת תזכורת לתאריך הזה', + 'deceased_label' => 'פטירה', + 'deceased_date_label' => 'מועד הפטירה', + 'deceased_label_with_date' => 'פטירה ב־:date', + 'deceased_age' => 'גיל בעת הפטירה', + + // Contact information + 'contact_info_title' => 'פרטי קשר', + 'contact_info_form_content' => 'תוכן', + 'contact_info_form_contact_type' => 'סוג איש קשר', + 'contact_info_form_personalize' => 'התאמה אישית', + 'contact_info_address' => 'מקום מגורים', + + // Addresses + 'contact_address_title' => 'כתובות', + 'contact_address_form_name' => 'תווית (רשות)', + 'contact_address_form_street' => 'רחוב (רשות)', + 'contact_address_form_city' => 'עיר (רשות)', + 'contact_address_form_province' => 'מחוז (רשות)', + 'contact_address_form_postal_code' => 'מיקוד (רשות)', + 'contact_address_form_country' => 'ארץ (רשות)', + 'contact_address_form_latitude' => 'רוחב (מספרים בלבד) (רשות)', + 'contact_address_form_longitude' => 'אורך (מספרים בלבד) (רשות)', + + // Pets + 'pets_kind' => 'סוג חיית מחמד', + 'pets_name' => 'שם (רשות)', + 'pets_create_success' => 'חיית המחמד נוספה בהצלחה', + 'pets_update_success' => 'חיית המחמד עודכנה', + 'pets_delete_success' => 'חיית המחמד נמחקה', + 'pets_title' => 'חיות מחמד', + 'pets_reptile' => 'זוחל', + 'pets_bird' => 'ציפור', + 'pets_cat' => 'חתול', + 'pets_dog' => 'כלב', + 'pets_fish' => 'דג', + 'pets_hamster' => 'אוגר', + 'pets_horse' => 'סוס', + 'pets_rabbit' => 'ארנב', + 'pets_rat' => 'חולדה', + 'pets_small_animal' => 'חיה קטנה', + 'pets_other' => 'אחר', + + // life events + 'life_event_list_tab_life_events' => 'אירועים משמעותיים', + 'life_event_list_tab_other' => 'פתקיות, תזכורות, …', + 'life_event_list_title' => 'אירועים משמעותיים', + 'life_event_blank' => 'תיעוד אירועים משמעותיים בחיים של {name} להפניה עתידית.', + 'life_event_list_cta' => 'הוספת אירוע משמעותי', + 'life_event_create_category' => 'כל הקטגוריות', + 'life_event_create_life_event' => 'הוספת אירוע משמעותי', + 'life_event_create_default_title' => 'כותרת (רשות)', + 'life_event_create_default_story' => 'סיפור (רשות)', + 'life_event_create_date' => 'אין צורך לציין חודש או יום - חובה לציין שנה בלבד.', + 'life_event_create_default_description' => 'הוספת המידע כפי שידוע לך', + 'life_event_create_add_yearly_reminder' => 'הוספת תזכורת שנתית לאירוע הזה', + 'life_event_create_success' => 'האירוע המשמעותי נוסף', + 'life_event_delete_title' => 'מחיקת אירוע משמעותי', + 'life_event_delete_description' => 'למחוק את האירוע המשמעותי הזה? מחיקה היא לצמיתות.', + 'life_event_delete_success' => 'האירוע המשמעותי נמחק', + 'life_event_date_it_happened' => 'התאריך בו זה התרחש', + 'life_event_category_work_education' => 'עבודה והשכלה', + 'life_event_category_family_relationships' => 'משפחה ויחסים', + 'life_event_category_home_living' => 'בית ומחייה', + 'life_event_category_health_wellness' => 'בריאות ורווחה', + 'life_event_category_travel_experiences' => 'טיול וחוויות', + 'life_event_sentence_new_job' => 'התחלת עבודה חדשה', + 'life_event_sentence_retirement' => 'יציאה לפנסיה', + 'life_event_sentence_new_school' => 'התחלת לימודים', + 'life_event_sentence_study_abroad' => 'יציאה ללימודים בחו״ל', + 'life_event_sentence_volunteer_work' => 'התחלת התנדבות', + 'life_event_sentence_published_book_or_paper' => 'פרסום מאמר', + 'life_event_sentence_military_service' => 'התחלת שירות צבאי', + 'life_event_sentence_new_relationship' => 'התחלת קשר', + 'life_event_sentence_engagement' => 'אירוסין', + 'life_event_sentence_marriage' => 'נישואין', + 'life_event_sentence_anniversary' => 'יום השנה', + 'life_event_sentence_expecting_a_baby' => 'ציפייה לתינוק', + 'life_event_sentence_new_child' => 'הצטרפות ילד/ה לחיים', + 'life_event_sentence_new_family_member' => 'התווספות חבר/ה למשפחה', + 'life_event_sentence_new_pet' => 'אימוץ חיית מחמד', + 'life_event_sentence_end_of_relationship' => 'סיום קשר', + 'life_event_sentence_loss_of_a_loved_one' => 'אובדן של אדם קרוב', + 'life_event_sentence_moved' => 'מעבר דירה', + 'life_event_sentence_bought_a_home' => 'רכישת דירה', + 'life_event_sentence_home_improvement' => 'עריכת שיפוץ', + 'life_event_sentence_holidays' => 'יציאה לחופש', + 'life_event_sentence_new_vehicle' => 'קבלת כלי רכב חדש', + 'life_event_sentence_new_roommate' => 'התווספות שותפות', + 'life_event_sentence_overcame_an_illness' => 'החלמה ממחלה', + 'life_event_sentence_quit_a_habit' => 'גמילה', + 'life_event_sentence_new_eating_habits' => 'אימוץ הרגלי אכילה חדשים', + 'life_event_sentence_weight_loss' => 'ירידה במשקל', + 'life_event_sentence_wear_glass_or_contact' => 'הרכבת משקפיים או עדשות מגע לראשונה', + 'life_event_sentence_broken_bone' => 'שבירת עצם', + 'life_event_sentence_removed_braces' => 'הסרת גשר', + 'life_event_sentence_surgery' => 'לאחר ניתוח', + 'life_event_sentence_dentist' => 'טיפול שיניים', + 'life_event_sentence_new_sport' => 'התחלה של ספורט', + 'life_event_sentence_new_hobby' => 'התחלת תחביב', + 'life_event_sentence_new_instrument' => 'למידת כלי נגינה חדש', + 'life_event_sentence_new_language' => 'למידת שפה חדשה', + 'life_event_sentence_tattoo_or_piercing' => 'קעקוע או פירסינג חדש', + 'life_event_sentence_new_license' => 'הוצאת רישיון', + 'life_event_sentence_travel' => 'טיול', + 'life_event_sentence_achievement_or_award' => 'קבלת הישג או פרס', + 'life_event_sentence_changed_beliefs' => 'שינוי אמונה', + 'life_event_sentence_first_word' => 'דיבור בפעם הראשונה', + 'life_event_sentence_first_kiss' => 'נשיקה ראשונה', + + // documents + 'document_list_title' => 'מסמכים', + 'document_list_cta' => 'העלאת מסמך', + 'document_list_blank_desc' => 'כאן ניתן לאחסן מסמכים שקשורים לאדם זה.', + 'document_upload_zone_cta' => 'העלאת קובץ', + 'document_upload_zone_progress' => 'המסמך נשלח…', + 'document_upload_zone_error' => 'אירעה שגיאה בעת העלאת המסמך. נא לנסות שוב להלן.', + + // Photos + 'photo_title' => 'תמונות', + 'photo_list_title' => 'תמונות קשורות', + 'photo_list_cta' => 'העלאת תמונה', + 'photo_list_blank_desc' => 'ניתן לאחסן תמונות של איש הקשר הזה. אפילו ממש ברגע זה!', + 'photo_upload_zone_cta' => 'העלאת תמונה', + 'photo_current_profile_pic' => 'תמונת פרופיל נוכחית', + 'photo_make_profile_pic' => 'הכנת תמונת פרופיל', + 'photo_delete' => 'מחיקת תמונה', + 'photo_next' => 'לתמונה הבאה ❯', + 'photo_previous' => '❮ לתמונה הקודמת', + + // Avatars + 'avatar_change_title' => 'החלפת התמונה הייצוגית שלך', + 'avatar_question' => 'באיזו תמונה ייצוגית להשתמש?', + 'avatar_default_avatar' => 'התמונה הייצוגית כבררת מחדל', + 'avatar_adorable_avatar' => 'התמונה הייצוגית המקסימה', + 'avatar_gravatar' => 'ה־Gravatar שמשויך לכתובת הדוא״ל של משתמש זה. Gravatar היא מערכת גלובלית שמאפשרת למשתמשים לשייך כתובות דוא״ל לתמונות.', + 'avatar_current' => 'להשאיר את התמונה הייצוגית הנוכחית', + 'avatar_photo' => 'מתמונה שהעלית', + 'avatar_crop_new_avatar_photo' => 'חיתוך התמונה הייצוגית החדשה', + + // emotions + 'emotion_this_made_me_feel' => 'התחושה שקיבלת היא…', + + // logs + 'auditlogs_link' => 'היסטוריה', + 'auditlogs_title' => 'כלה מה שקרה ל:name', + 'auditlogs_breadcrumb' => 'היסטוריה', + 'auditlogs_author' => 'מאת :name ב־:date', + + // contact field label + 'contact_field_label_home' => 'בית', + 'contact_field_label_work' => 'עבודה', + 'contact_field_label_cell' => 'נייד', + 'contact_field_label_fax' => 'פקס', + 'contact_field_label_pager' => 'זימונית', + 'contact_field_label_main' => 'עיקרי', + 'contact_field_label_other' => 'אחר', + 'contact_field_label_personal' => 'אישי', +]; diff --git a/resources/lang/he/reminder.php b/resources/lang/he/reminder.php new file mode 100644 index 0000000..35674a8 --- /dev/null +++ b/resources/lang/he/reminder.php @@ -0,0 +1,16 @@ + 'נא לאחל יום הולדת שמח ל־', + 'type_phone_call' => 'שיחה', + 'type_lunch' => 'ארוחת צהריים עם', + 'type_hangout' => 'בילוי משותף עם', + 'type_email' => 'דוא״ל', + 'type_birthday_kid' => 'נא לאחל יום הולדת שמח לילד של', +]; diff --git a/resources/lang/he/settings.php b/resources/lang/he/settings.php new file mode 100644 index 0000000..70ef9b3 --- /dev/null +++ b/resources/lang/he/settings.php @@ -0,0 +1,557 @@ + 'הגדרות חשבון', + 'sidebar_personalization' => 'התאמה אישית', + 'sidebar_settings_storage' => 'אחסון', + 'sidebar_settings_export' => 'יצוא נתונים', + 'sidebar_settings_users' => 'משתמשים', + 'sidebar_settings_subscriptions' => 'הרשמה', + 'sidebar_settings_import' => 'יבוא נתונים', + 'sidebar_settings_tags' => 'ניהול תגיות', + 'sidebar_settings_api' => 'API', + 'sidebar_settings_dav' => 'משאבי DAV', + 'sidebar_settings_security' => 'אבטחה', + 'sidebar_settings_auditlogs' => 'יומני ביקורת', + + 'title_general' => 'מידע כללי', + 'title_i18n' => 'הגדרות בינלאומיות', + 'title_layout' => 'פריסה', + + 'me_title' => 'אותי כאיש קשר', + 'me_help' => 'איש קשר זה או הייצוג שלך במוניקה', + 'me_select' => 'נא לבחור איש קשר', + 'me_no_contact' => 'טרם נבחרו אנשי קשר.', + 'me_select_click' => 'יש ללחוץ כאן כדי לבחור אנשי קשר.', + 'me_remove_contact' => 'הסרת השיוך', + 'me_choose' => 'בחירה עצמית', + 'me_choose_placeholder' => 'בחירה עצמית', + + 'export_title' => 'יצוא נתוני החשבון שלך', + 'export_be_patient' => 'יש ללחוץ על הכפתור כדי להתחיל את הייצוא. עיבוד הייצוא עשוי לארוך מספר דקות - נא להתאזר בסבלנות ולא ללחוץ על הכפתור שוב.', + 'export_title_sql' => 'ייצוא ל־SQL', + 'export_sql_explanation' => 'יצוא הנתונים שלך במבנה SQL מאפשר לך לקחת את הנתונים שלך ולייבא אותם לעותק של מוניקה משלך. לתכונה זו יש ערך רק אם יש לך שרת משלך.', + 'export_sql_cta' => 'ייצוא ל־SQL', + 'export_sql_link_instructions' => 'לתשומת לבך: ניתן לקרוא את ההנחיות כדי ללמוד יותר על יבוא הקובץ הזה לעותק שלך.', + 'export_title_json' => 'ייצוא ל־Json', + 'export_submitted' => 'הייצוא שלך הוגש, הוא יהיה זמין בעוד מספר רגעים…', + 'export_json_explanation' => 'הנתונים שלך מיוצאים בתצורת Json למטרות גיבוי.', + 'export_json_beta' => 'ייצוא Json הוא במצב תצוגה מקדימה. נא לספר לנו מה דעתך עליו:', + 'export_json_cta' => 'ייצוא ל־Json', + 'export_header_type' => 'סוג', + 'export_header_timestamp' => 'מועד יצירה', + 'export_header_status' => 'מצב', + 'export_header_actions' => 'פעולות', + 'export_last_title' => 'Last exports', + 'export_empty_title' => 'אין נתונים שיוצאו עדיין', + 'export_type_json' => 'ייצוא Json', + 'export_type_sql' => 'ייצוא SQL', + 'export_status_todo' => 'הוגש', + 'export_status_doing' => 'מתבצע', + 'export_status_done' => 'בוצע', + 'export_status_failed' => 'נכשל', + 'export_not_done' => 'אי אפשר להוריד, הייצוא עדיין לא בוצע.', + + 'firstname' => 'שם פרטי', + 'lastname' => 'שם משפחה', + 'name_order' => 'סדר השמות', + 'name_order_firstname_lastname' => '<שם פרטי> <שם משפחה> – ישראל ישראלי', + 'name_order_lastname_firstname' => '<שם משפחה> <שם פרטי> – ישראלי ישראל', + 'name_order_firstname_lastname_nickname' => '<שם פרטי> <שם משפחה> (<כינוי>) – ישראל ישראלי (שרוליק)', + 'name_order_firstname_nickname_lastname' => '<שם פרטי> (<כינוי>) <שם משפחה> – ישראל (שרוליק) ישראלי', + 'name_order_lastname_firstname_nickname' => ' () – ישראלי ישראל (שרוליק)', + 'name_order_lastname_nickname_firstname' => ' () – ישראל (שרוליק) ישראלי', + 'name_order_nickname_firstname_lastname' => ' ( ) – שרוליק (ישראל ישראלי)', + 'name_order_nickname_lastname_firstname' => '<כינוי>‏ (<שם משפחה> <שם פרטי>) – שרוליק (ישראלי ישראל)', + 'name_order_nickname' => '<כינוי> – שרוליק', + 'currency' => 'מטבע', + 'name' => 'שמך: :name', + 'email' => 'כתובת דוא״ל', + 'email_placeholder' => 'נא להקליד כתובת דוא״ל', + 'email_help' => 'זו היא כתובת הדוא״ל המשמשת לכניסה ולשם גם תשלח מוניקה את התזכורות שלך.', + 'timezone' => 'אזור זמן', + 'temperature_scale' => 'יחידות טמפרטורה', + 'temperature_scale_fahrenheit' => 'פרנהייט (‎°F)', + 'temperature_scale_celsius' => 'צלזיוס (‎°C)', + 'layout' => 'פריסה', + 'layout_small' => 'רוחב של 1200 פיקסלים לכל היותר', + 'layout_big' => 'הרוחב המלא של הדפדפן', + 'save' => 'עדכון העדפות', + 'delete_title' => 'מחיקת החשבון שלך', + 'delete_desc' => 'למחוק את החשבון שלך? מחיקה היא לצמיתות וכל הנתונים שלך יימחקו לצמיתות גם הם. אם יש לך מינוי, הוא יבוטל מיידית.', + 'delete_other_desc' => 'Your data in the main database will be deleted immediately. As described in our privacy policy, we carry out securely encrypted backups of the database every day. These backups are kept for 30 days after which they are completely deleted. We cannot delete specific data from the backups we hold any earlier than this. All of your data will be completely deleted no later than 31 days after your account’s deletion.', + 'reset_desc' => 'לאפס את החשבון שלך? פעולה זו תסיר את כל אנשי הקשר שלך ואת כל המידע המשויך להם. החשבון שלך לא יימחק.', + 'reset_title' => 'איפוס החשבון שלך', + 'reset_cta' => 'איפוס חשבון', + 'reset_notice' => 'לאפס את החשבון שלך? זאת פעולה בלתי הפיכה.', + 'reset_success' => 'החשבון שלך אופס בהצלחה.', + 'delete_notice' => 'למחוק את החשבון שלך? פעולה זו היא לצמיתות ולא ניתן להשתקם ממנה. כל הנתונים שלך יימחקו ולא ניתן יהיה לשחזר אותם.', + 'delete_cta' => 'מחיקת חשבון', + 'settings_success' => 'ההעדפות עודכנו!', + 'locale' => 'שפה בה נעשה שימוש ביישום', + 'locale_help' => 'מעניין אותך לסייע בתרגום מוניקה לשפה חדשה? נא להיכנס לקישור הבא לקבלת מידע נוסף.', + 'locale_ar' => 'ערבית', + 'locale_cs' => 'צ׳כית', + 'locale_de' => 'גרמנית', + 'locale_el' => 'יוונית', + 'locale_en' => 'אנגלית', + 'locale_en-GB' => 'אנגלית בריטית', + 'locale_es' => 'ספרדית', + 'locale_fr' => 'צרפתית', + 'locale_he' => 'עברית', + 'locale_hr' => 'קרואטית', + 'locale_id' => 'אינדונזית', + 'locale_it' => 'איטלקית', + 'locale_ja' => 'יפנית', + 'locale_nl' => 'הולנדית', + 'locale_pt' => 'פורטוגלית', + 'locale_pt-BR' => 'פורטוגלית ברזילאית', + 'locale_ru' => 'רוסית', + 'locale_sv' => 'שוודית', + 'locale_vi' => 'וייטנאמית', + 'locale_zh' => 'סינית מפושטת', + 'locale_zh-TW' => 'סינית מסורתית', + 'locale_tr' => 'טורקית', + + 'security_title' => 'אבטחה', + 'security_help' => 'שינוי נדבכי האבטחה של החשבון שלך.', + 'password_change' => 'להחליף את הססמה שלך', + 'password_current' => 'הססמה הנוכחית', + 'password_current_placeholder' => 'נא להקליד את הססמה הנוכחית שלך', + 'password_new1' => 'ססמה חדשה', + 'password_new1_placeholder' => 'למלא את הססמה החדשה שלך', + 'password_new2' => 'אישור הססמה החדשה שלך', + 'password_new2_placeholder' => 'נא להקליד את הססמה החדשה שלך שוב', + 'password_btn' => 'החלפת ססמה', + '2fa_title' => 'אימות דו־שלבי', + '2fa_otp_title' => 'יישומון לנייד לאימות דו־שלבי', + '2fa_enable_title' => 'הפעלת אימות דו־שלבי', + '2fa_enable_description' => 'ניתן להפעיל אימות דו־שלבי כדי להגביר את האבטחה של החשבון שלך.', + '2fa_enable_otp' => 'יש לפתוח את יישומון האימות הדו־שלבי שלך ולסרוק את ברקוד ה־QR שלהלן:', + '2fa_enable_otp_help' => 'אם יישומון האימות הדו־שלבי שלך אינו תומך בקודים מסוג QR, יש להקליד את הקוד שלהלן:', + '2fa_enable_otp_validate' => 'נא לאמת את ההתקן החדש שזה עתה הגדרת:', + '2fa_enable_success' => 'הופעל אימות דו־שלבי', + '2fa_enable_error' => 'אירעה שגיאה בעת הניסיון להפעיל אימות דו־שלבי', + '2fa_enable_error_already_set' => 'האימות הדו־שלבי כבר מופעל', + '2fa_disable_title' => 'השבתת אימות דו־שלבי', + '2fa_disable_description' => 'השבתת האימות הדו־שלבי לחשבון שלך. נא להיזהר, החשבון שלך יהיה הרבה פחות מאובטח!', + '2fa_disable_success' => 'אימות דו־שלבי מושבת', + '2fa_disable_error' => 'אירעה שגיאה בעת הניסיון להשבית את האימות הדו־שלבי', + + 'webauthn_title' => 'מפתח אבטחה — פרוטוקול WebAuthn', + 'webauthn_enable_description' => 'הוספת מפתח אבטחה חדש', + 'webauthn_key_name_help' => 'נא לתת למפתח שלך שם.', + 'webauthn_key_name' => 'שם מפתח:', + 'webauthn_success' => 'המפתח שלך מזוהה ועובר וידוא.', + 'webauthn_last_use' => 'שימוש אחרון: {timestamp}', + 'webauthn_delete_confirmation' => 'למחוק את המפתח הזה?', + 'webauthn_delete_success' => 'המפתח נמחק', + 'webauthn_insertKey' => 'נא להכניס את מפתח האבטחה שלך.', + 'webauthn_buttonAdvise' => 'אם למפתח האבטחה שלך יש כפתור, יש ללחוץ עליו.', + 'webauthn_noButtonAdvise' => 'אם אין לו, יש להסיר אותו ולהכניס שוב.', + 'webauthn_not_supported' => 'בדפדפן שלך עדיין אין תמיכה ב־WebAuthn.', + 'webauthn_not_secured' => 'WebAuthn נתמך עם חיבורים מאובטחים בלבד. נא לטעון את העמוד הזה בתצורת https.', + 'webauthn_error_already_used' => 'המפתח הזה כבר רשום. אין זה הכרחי לרשום אותו שוב.', + 'webauthn_error_not_allowed' => 'הזמן שהוקצב לפעולה פג או שאין אישור.', + + 'recovery_title' => 'קודים לשחזור', + 'recovery_show' => 'קבלת קודים לשחזור', + 'recovery_copy_help' => 'העתקת קודים ללוח הגזירים שלך', + 'recovery_help_intro' => 'אלו הקודים שלך לטובת שחזור:', + 'recovery_help_information' => 'ניתן להשתמש בכל קוד שחזור פעם אחת בלבד.', + 'recovery_clipboard' => 'הקודים הועתקו ללוח הגזירים.', + 'recovery_generate' => 'יצירת קודים חדשים…', + 'recovery_generate_help' => 'יצירת קודים חדשים תשלול את הקודים שנוצרו לפניהם.', + 'recovery_already_used_help' => 'כבר נעשה שימוש בקוד הזה.', + + 'users_list_title' => 'משתמשים עם גישה לחשבון שלך', + 'users_list_add_user' => 'הזמנת משתמש חדש', + 'users_list_you' => 'מדובר בך', + 'users_list_invitations_title' => 'הזמנות ממתינות', + 'users_list_invitations_explanation' => 'להלן מופיעים האנשים שהזמנת להשתמש במוניקה כשותפים.', + 'users_list_invitations_invited_by' => 'הזמנה מאת :name', + 'users_list_invitations_sent_date' => 'נשלח ב־:date', + 'users_blank_title' => 'רק לך יש גישה לחשבון הזה.', + 'users_blank_add_title' => 'להזמין מישהו נוסף?', + 'users_blank_description' => 'לאדם הזה יש את אותה רמת הגישה כמוך ותהיה לו אפשרות להוסיף, לערוך או למחוק פרטי קשר.', + 'users_blank_cta' => 'להזמין מישהו', + 'users_add_title' => 'ניתן להזמין משתמש חדש לחשבון שלך בדוא״ל', + 'users_add_description' => 'לאדם זה יש את אותה רמת גישה כמוך, לרבות הזמנת או מחיקת משתמשים אחרים, כולל אותך. נא לוודא שמדובר באדם מהימן בטרם מתן הגישה.', + 'users_add_email_field' => 'נא להקליד את כתובת הדוא״ל של האדם שברצונך להזמין', + 'users_add_confirmation' => 'מוסכם עלי להזמין את המשתמש הזה לחשבון שלי. ברור לי שלאדם הזה תהיה גישה לכל המידע שלי ויוכל לראות בדיוק מה שאני רואה.', + 'users_add_cta' => 'הזמנת משתמש דרך דוא״ל', + 'users_accept_title' => 'קבלת ההזמנה ויצירת חשבון חדש', + 'users_error_please_confirm' => 'נא להסכים להזמנת המשתמש הזה בטרם המשך תהליך ההזמנה', + 'users_error_email_already_taken' => 'כתובת דוא״ל זו כבר תפוסה. נא לבחור באחת אחרת', + 'users_error_already_invited' => 'כבר הזמנת את המשתמש הזה. נא לבחור בכתובת דוא״ל אחרת.', + 'users_error_email_not_similar' => 'זאת לא כתובת הדוא״ל של האדם שהזמנת.', + 'users_invitation_deleted_confirmation_message' => 'ההזמנה נמחקה בהצלחה', + 'users_invitations_delete_confirmation' => 'למחוק את ההזמנה הזאת?', + 'users_list_delete_confirmation' => 'למחוק את המשתמש הזה מהחשבון שלך?', + 'users_invitation_need_subscription' => 'הוספת משתמשים נוספים דורשת רישום.', + + 'subscriptions_account_current_plan' => 'התכנית הנוכחית שלך', + 'subscriptions_account_current_legacy' => 'תכנית נוכחית, אינה זמינה עוד לבחירה:', + 'subscriptions_account_current_paid_plan' => 'תכנית העבודה שלך כרגע היא :name. תודה לך על ההרשמה.', + + 'subscriptions_account_next_billing_title' => 'החשבון הבא', + 'subscriptions_account_next_billing' => 'המינוי שלך יחודש אוטומטית ב־:date.', + 'subscriptions_account_bill_monthly' => 'נחייב אותך ב־:price למשך חודש נוסף.', + 'subscriptions_account_bill_annual' => 'נחייב אותך ב־:price למשך שנה נוספת.', + 'subscriptions_account_change' => 'החלפת תכנית', + + 'subscriptions_account_cancel_title' => 'ביטול מינוי', + 'subscriptions_account_cancel_action' => 'ביטול מינוי', + 'subscriptions_account_cancel' => 'ניתן לבטל את המינוי בכל עת.', + 'subscriptions_account_free_plan' => 'התכנית שלך היא החינמית.', + 'subscriptions_account_free_plan_upgrade' => 'ניתן לשדרג את החשבון שלך לתכנית :name, שעולה $:price לחודש. להלן היתרונות:', + 'subscriptions_account_free_plan_benefits_users' => 'מספר בלתי מוגבל של משתמשים', + 'subscriptions_account_free_plan_benefits_reminders' => 'תזכורות לפי דוא״ל', + 'subscriptions_account_free_plan_benefits_import_data_vcard' => 'יבוא אנשי הקשר שלך עם vCard', + 'subscriptions_account_free_plan_benefits_support' => 'כדאי לתמוך במיזם לטווח הרחוק כדי שנוכל להשיק תכונות נפלאות נוספות.', + 'subscriptions_account_upgrade' => 'שדרוג החשבון שלך', + 'subscriptions_account_upgrade_title' => 'ניתן לשדרג את מוניקה כדי להעשיר את הקשרים הבין אישיים שלך.', + 'subscriptions_account_upgrade_choice' => 'נא לבחור בתכנית להלן כדי להצטרף ל־:customers לקוחות ששדרגו את המוניקה שלהם.', + 'subscriptions_account_update_title' => 'עדכון המינוי למוניקה', + 'subscriptions_account_update_description' => 'ניתן לשנות את תדירות המינוי שלך כאן.', + 'subscriptions_account_update_information' => 'החיוב שלך יבוצע מיידית עבור הסכום החדש. המינוי שלך יורחב לתקופה החדשה בהתאם לבחירה שלך.', + 'subscriptions_account_invoices' => 'חשבוניות', + 'subscriptions_account_invoices_download' => 'הורדה', + 'subscriptions_account_invoices_subscription' => 'מינוי מ־:startDate עד :endDate', + 'subscriptions_account_payment' => 'מה דרך התשלום המועדפת עליך?', + 'subscriptions_account_confirm_payment' => 'התשלום שלך לא הושלם, נא לאשר את התשלום שלך.', + 'subscriptions_downgrade_title' => 'ניתן לשנמך את החשבון שלך לתכנית החינמית', + 'subscriptions_downgrade_limitations' => 'לתכנית החופשית יש מגבלות. כדי לשנמך, עליך לעבור ולאמת את הפריטים ברשימה שלהלן:', + 'subscriptions_downgrade_rule_users' => 'חייב להיות לך רק משתמש אחד בחשבון', + 'subscriptions_downgrade_rule_users_constraint' => 'יש לך משתמש אחד בחשבון כרגע.|יש לך :count משתמשים בחשבון כרגע.', + 'subscriptions_downgrade_rule_invitations' => 'אסור שתהיינה לך הזמנות ממתינות כלשהן', + 'subscriptions_downgrade_rule_invitations_constraint' => 'יש לך כרגע הזמנה אחת בהמתנה.|יש לך כרגע :count הזמנות בהמתנה.', + 'subscriptions_downgrade_rule_contacts' => 'לא יכולים להיות לך יותר מ־:number אנשי קשר פעילים', + 'subscriptions_downgrade_rule_contacts_constraint' => 'כרגע יש לך איש קשר יחיד.|כרגע יש לך :count אנשי קשר.', + 'subscriptions_downgrade_rule_contacts_archive' => 'נוכל גם להעביר את כל אנשי הקשר שלך לארכיון עבורך - פעולה זו תמחק את הכלל הזה ותאפשר לך להמשיך בתהליך שנמוך החשבון שלך.', + 'subscriptions_downgrade_cta' => 'שנמוך', + 'subscriptions_downgrade_success' => 'חזרת לתכנית החינמית!', + 'subscriptions_downgrade_thanks' => 'תודה לך על ההתנסות בתכנית בתשלום. אנו ממשיכים ומוסיפים תכונות חדשות למוניקה כל הזמן - יכול להיות שכדאי לך לקפוץ בהמשך כדי לראות אם מעניין אותך להירשם מחדש.', + 'subscriptions_back' => 'חזרה להגדרות', + 'subscriptions_upgrade_title' => 'שדרוג החשבון שלך', + 'subscriptions_upgrade_choose' => 'בחרת בתכנית :plan.', + 'subscriptions_upgrade_infos' => 'אין מאושרים מאתנו. נא להקליד את פרטי התשלום שלך להלן.', + 'subscriptions_upgrade_name' => 'השם על הכרטיס', + 'subscriptions_upgrade_zip' => 'מיקוד או תא דואר', + 'subscriptions_upgrade_credit' => 'כרטיס אשראי או חיוב', + 'subscriptions_upgrade_submit' => 'לשלם {amount}', + 'subscriptions_upgrade_charge' => 'אנו נחייב את הכרטיס שלך בסכום של :price כעת. החיוב הבא יהיה ב־:date. במקרה ששינית את דעתך, ניתן לבטל בכל עת, בלי שאלות מיותרות.', + 'subscriptions_upgrade_charge_handled' => 'הסליקה מבוצעת על ידי Stripe. המידע על הכרטיס לא עובר דרך השרת שלנו.', + 'subscriptions_upgrade_success' => 'תודה לך! נרשמת כעת.', + 'subscriptions_upgrade_thanks' => 'ברוך בואך לקהילה של אנשים שמנסים להפוך את העולם למקום טוב יותר.', + + 'subscriptions_payment_confirm_title' => 'אישור התשלום שלך על סך :amount', + 'subscriptions_payment_confirm_information' => 'נדרש אימות נוסף כדי לעבד את התשלום שלך. נא לאשר את התשלום על ידי מילוי פרטי התשלום שלהלן.', + 'subscriptions_payment_succeeded_title' => 'התשלום עבר בהצלחה', + 'subscriptions_payment_succeeded' => 'תשלום זה כבר אושר בהצלחה.', + 'subscriptions_payment_cancelled_title' => 'התשלום בוטל', + 'subscriptions_payment_cancelled' => 'התשלום בוטל.', + 'subscriptions_payment_error_name' => 'נא לציין את שמך.', + 'subscriptions_payment_success' => 'התשלום בוצע בהצלחה.', + + 'subscriptions_pdf_title' => 'המינוי החודשי שלך מסוג :name', + 'subscriptions_plan_frequency_year' => ':amount לשנה', + 'subscriptions_plan_frequency_month' => ':amount לחודש', + 'subscriptions_plan_choose' => 'בחירה בתכנית הזאת', + 'subscriptions_plan_year_title' => 'תשלום שנתי', + 'subscriptions_plan_year_bonus' => 'שקט נפשי לשנה שלמה', + 'subscriptions_plan_month_title' => 'תשלום חודשי', + 'subscriptions_plan_month_bonus' => 'ניתן לבטל בכל עת', + 'subscriptions_plan_include1' => 'כלול בשדרוג שלך:', + 'subscriptions_plan_include2' => 'מספר בלתי מוגבל של אנשי קשר • מספר בלתי מוגבל של משתמשים • תזכורות בדוא״ל • ייבוא עם vCard • התאמה אישית של גיליון אנשי הקשר', + 'subscriptions_plan_include3' => '100% מהרווחים מושקעים בפיתוח מיזם הקוד הפתוח הנהדר הזה.', + 'subscriptions_help_title' => 'פרטים נוספים שעשויים לעניין אותך', + 'subscriptions_help_opensource_title' => 'מה הוא מיזם בקוד פתוח?', + 'subscriptions_help_opensource_desc' => 'מוניקה הוא מיזם בקוד פתוח. משמעות הדבר היא שהוא נבנה על ידי קהילה שרוצה לספק כלי נהדר לטובת הכלל. קוד פתוח משמעו שהקוד גלוי לעיני הציבור ב־GitHub וכולם יכולים לבחון, לשנות או לשפר אותו. כל הכסף שמגויס מושקע לטובת פיתוח תכונות טובות יותר, תשלום על שרתים חזקים יותר ותשלום הוצאות שונות. תודה לך על הסיוע. לא היינו מצליחים לעשות זאת בלעדיך.', + 'subscriptions_help_limits_title' => 'האם יש מגבלה כלשהי על מספר אנשי הקשר שאוכל לנהל בתכנית החינמית?', + 'subscriptions_help_limits_plan' => 'כן. התכניות החינמיות מאפשרת לך לנהל :number אנשי קשר.', + 'subscriptions_help_discounts_title' => 'יש לכם הנחה לארגונים ללא מטרות רווח ומוסדות חינוך?', + 'subscriptions_help_discounts_desc' => 'יש לנו! מוניקה מוצעת בחינם לתלמידים ולארגוני צדקה ללא מטרות רווח. עליך רק ליצור קשר עם התמיכה עם הוכחה על המצב שלך ואנו נחיל המצב המיוחד הזה על החשבון שלך.', + 'subscriptions_help_change_title' => 'מה אם התחרטתי?', + 'subscriptions_help_change_desc' => 'ניתן לבטל בכל עת, ללא שאלות מיותרות ובאופן עצמאי לחלוטין - אין צורך ליצור קשר עם התמיכה. עם זאת, לא יבוצע זיכוי על התקופה הנוכחית.', + + 'stripe_error_card' => 'הכרטיס שלך נדחה. הודעה הדחייה היא: :message', + 'stripe_error_api_connection' => 'התקשורת עם Stripe נכשלה. נא לנסות שוב מאוחר יותר.', + 'stripe_error_rate_limit' => 'הגיעו יותר מדי בקשות אל Stripe כרגע. נא לנסות שוב מאוחר יותר.', + 'stripe_error_invalid_request' => 'משתנים שגויים. נא לנסות שוב מאוחר יותר.', + 'stripe_error_authentication' => 'אימות שגוי מול Stripe', + + 'import_title' => 'יבוא אנשי קשר לחשבון שלך', + 'import_cta' => 'עדכון אנשי קשר', + 'import_stat' => 'ייבאת :number קבצים עד כה.', + 'import_result_stat' => 'הועלה vCard עם איש קשר אחד (:total_imported יובא, :total_skipped לא יובא)|הועלה vCard עם :total_contacts אנשי קשר (:total_imported יובאו, :total_skipped לא יובאו)', + 'import_view_report' => 'צפייה בדו״ח', + 'import_in_progress' => 'הייבוא מתבצע כעת. יש לרענן את העמוד בעוד דקה.', + 'import_upload_title' => 'ניתן לייבא את אנשי הקשר שלך מקובץ vCard', + 'import_upload_rules_desc' => 'עם זאת, חלים כאן חוקים כלשהם:', + 'import_upload_rule_format' => 'אנו תומכים בקבצים מהסוגים .vcard ו־.vcf', + 'import_upload_rule_vcard' => 'אנו תומכים ב־vCard גרסה 3.0, שהיא גרסת בררת המחדל עבור Contacts.app של macOS ועבור Google Contacts.', + 'import_upload_rule_instructions' => 'הנחיות יצוא עבור Contacts.app של macOS ועבור אנשי הקשר של Google.', + 'import_upload_rule_multiple' => 'אם לאנשי הקשר שלך יש מגוון כתובות דוא״ל או מספרי טלפון, רק הרשומה הראשונה תישמר.', + 'import_upload_rule_limit' => 'קבצים מוגבלים ל־10 מ״ב.', + 'import_upload_rule_time' => 'ההעלאה ועיבוד אנשי הקשר עשויים לארוך עד דקה. נא להתאזר בסבלנות.', + 'import_upload_rule_cant_revert' => 'נא לוודא שהנתונים מדויקים בטרם ההעלאה, כיוון שלא ניתן לבטל העלאה.', + 'import_upload_form_file' => 'קובץ ה־.vcf או ה־.vCard שלך:', + 'import_upload_behaviour' => 'התנהגות הייבוא:', + 'import_upload_behaviour_add' => 'הוספת אנשי קשר חדשים ודילוג על הקיימים', + 'import_upload_behaviour_replace' => 'החלפת אנשי קשר קיימים', + 'import_upload_behaviour_help' => 'החלפה תוביל להחלפת כל הנתונים שנמצאים ב־vCard אך השדות הקיימים יישמרו.', + 'import_report_title' => 'דוח יבוא', + 'import_report_date' => 'תאריך הייבוא', + 'import_report_type' => 'סוג הייבוא', + 'import_report_number_contacts' => 'מספר אנשי הקשר בקובץ', + 'import_report_number_contacts_imported' => 'מספר אנשי הקשר שייובאו', + 'import_report_number_contacts_skipped' => 'מספר אנשי הקשר שדולגו', + 'import_report_status_imported' => 'יובאו', + 'import_report_status_skipped' => 'דולגו', + 'import_vcard_parse_error' => 'שגיאה בעת ניתוח רשומת vCard', + 'import_vcard_contact_exist' => 'איש הקשר כבר קיים', + 'import_vcard_contact_no_firstname' => 'אין שם פרטי (חובה)', + 'import_vcard_file_not_found' => 'הקובץ לא נמצא', + 'import_vcard_unknown_entry' => 'שם איש הקשר אינו מוכר', + 'import_vcard_file_no_entries' => 'אין רשומות בקובץ', + 'import_blank_title' => 'לא ייבאת אנשי קשר עדיין.', + 'import_blank_question' => 'לייבא אנשי קשר כעת?', + 'import_blank_description' => 'יש לנו אפשרות לייבא קובצי vCard שניתן לקבל מאנשי הקשר ב־Google או ממנהל אנשי הקשר שלך.', + 'import_blank_cta' => 'יבוא vCard', + 'import_need_subscription' => 'יבוא נתונים דורש מינוי.', + + 'tags_list_title' => 'תגיות', + 'tags_list_description' => 'ניתן לארגן את אנשי הקשר שלך על ידי הגדרת תגיות. תגיות עובדות כמו תיקיות אך יש לך אפשרות להוסיף יותר מתגית אחת לאיש קשר. כדי להוסיף תגית חדשה, ניתן להוסיף אותה על איש הקשר עצמו.', + 'tags_list_contact_number' => 'איש קשר אחד|:count אנשי קשר', + 'tags_list_delete_success' => 'התגית נמחקה בהצלחה', + 'tags_list_edit_success' => 'The tag has been successfully updated', + 'tags_list_delete_confirmation' => 'למחוק את התגית? לא יימחקו אנשי קשר, רק התגית.', + 'tags_blank_title' => 'תגיות הן דרך נהדרת לארגון אנשי הקשר שלך.', + 'tags_blank_description' => 'תגיות עובדות כמו תיקיות, אך ניתן להוסיף יותר מתגית אחת לאיש קשר. יש לגשת לאיש קשר ולתייג כחבר, מיד מתחת לשם. לאחר תיוג איש קשר, ניתן לחזור לכאן כדי לנהל את כל התגיות בחשבון שלך.', + + 'api_title' => 'גישת API', + 'api_description' => 'ניתן להשתמש ב־API כדי לשנות את הנתונים של מוניקה דרך יישום חיצוני, כגון יישומון לנייד למשל.', + 'api_help' => 'כדי להשתמש ב־API, חובה להשתמש באסימון. ניתן ליצור אסימון גישה אישי (אימות חשוף יותר) או לאמת לקוח OAuth כדי שייצור אותו עבורך. כדאי לעיין בתיעוד של ה־API.', + 'api_endpoint' => 'נקודת הגישה ל־API של העותק הזה של מוניקה היא:', + + 'api_personal_access_tokens' => 'אסימון לגישה אישית', + 'api_pao_description' => 'נא לוודא שהאסימון הזה מועבר לידיים מהימנות - כיוון שהאסימון מעניק גישה לכל הנתונים שלך.', + 'api_token_title' => 'אסימונים לגישה אישית', + 'api_token_create_new' => 'יצירת אסימון חדש', + 'api_token_not_created' => 'לא יצרת אסימונים לגישה אישית.', + 'api_token_name' => 'שם האסימון', + 'api_token_expire' => 'יפוג ב־{date}', + 'api_token_delete' => 'מחיקה', + 'api_token_create' => 'יצירת אסימון', + 'api_token_scopes' => 'תחומים', + 'api_token_help' => 'הנה אסימון הגישה האישית החדש שלך. זאת הפעם היחידה שהוא יופיע כך שעדיף לא לאבד אותו! כעת ניתן להשתמש באסימון הזה כדי לבצע בקשות API.', + + 'api_oauth_clients' => 'לקוחות ה־OAuth שלך', + 'api_oauth_clients_desc' => 'אגף זה מסייע לך לרשום לקוחות OAuth משלך.', + 'api_oauth_clients_desc2' => 'יש להשתמש במזהה לקוח זה כדי לבקש אסימון חדש ולהמיר את הקודים למטרת גישה לאסימוני גישה. יש לעיין בתיעוד שלLaravel Passport לקבלת מידע נוסף.', + 'api_oauth_title' => 'לקוחות OAuth', + 'api_oauth_create_new' => 'יצירת לקוח חדש', + 'api_oauth_edit' => 'עריכת לקוח', + 'api_oauth_not_created' => 'לא יצרת לקוחות OAuth כלשהם.', + 'api_oauth_clientid' => 'מזהה לקוח', + 'api_oauth_name' => 'שם', + 'api_oauth_name_help' => 'משהו שהמשתמשים שלך יזהו ויאמינו בו.', + 'api_oauth_secret' => 'סוד', + 'api_oauth_create' => 'יצירת לקוח', + 'api_oauth_redirecturl' => 'כתובת הפניה', + 'api_oauth_redirecturl_help' => 'כתובת הקריאה החוזרת לאימות היישום שלך.', + + 'api_authorized_clients' => 'רשימת לקוחות מורשים', + 'api_authorized_clients_desc' => 'אגף זה מציג את כל הלקוחות להם הענקת גישה לנתוני היישומונים שלך. ניתן לשלול את האישור הזה בכל עת.', + 'api_authorized_clients_title' => 'יישומים מאושרים', + 'api_authorized_clients_none' => 'עדיין אין לקוחות מאומתים.', + 'api_authorized_clients_name' => 'שם', + 'api_authorized_clients_scopes' => 'תחומים', + + 'personalization_tab_title' => 'התאמת החשבון שלך', + + 'personalization_title' => 'להלן ניתן למצוא הגדרות שונות להתאמת החשבון שלך. התכונות האלו מיועדות למשתמשים מתקדמים שרוצים לקבל שליטה מלאה במוניקה.', + 'personalization_contact_field_type_title' => 'סוגי שדות אנשי קשר', + 'personalization_contact_field_type_add' => 'הוספת סוג שדה נתונים חדש', + 'personalization_contact_field_type_description' => 'ניתן להגדיר את כל הסוגים השונים של שדות שניתן לשייך לכל אנשי הקשר שלך. למשל, אם בעתיד תופיע רשת חברתית חדשה, תהיה לך האפשרות להוסיף את דרך התקשורת החדשה הזאת מול אנשי הקשר שלך ממש מכאן.', + 'personalization_contact_field_type_table_name' => 'שם', + 'personalization_contact_field_type_table_protocol' => 'פרוטוקול', + 'personalization_contact_field_type_table_actions' => 'פעולות', + 'personalization_contact_field_type_modal_title' => 'הוספת סוג שדה חדש לאנשי קשר', + 'personalization_contact_field_type_modal_edit_title' => 'עריכת סוג שדה קיים לאנשי קשר', + 'personalization_contact_field_type_modal_delete_title' => 'מחיקת סוג שדה קיים לאנשי קשר', + 'personalization_contact_field_type_modal_delete_description' => 'למחוק את סוג השדה הזה של אנשי הקשר? מחיקת סוג זה של שדה לאנשי קשר ימחק את כל הנתונים מסוג זה עבור כל אנשי הקשר שלך.', + 'personalization_contact_field_type_modal_name' => 'שם', + 'personalization_contact_field_type_modal_protocol' => 'פרוטוקול (רשות)', + 'personalization_contact_field_type_modal_protocol_help' => 'כל סוג חדש של שדה איש קשר אמור לתמוך בלחיצה עליו. אם מוגדר פרוטוקול, אנו נשתמש בלחיצה כדי להפעיל את הפעולה שהוגדרה.', + 'personalization_contact_field_type_modal_icon' => 'סמל (רשות)', + 'personalization_contact_field_type_modal_icon_help' => 'ניתן לשייך סמל עם סוג שדה איש קשר זה. יהיה עליך להוסיף הפניה לסמל מתוך Font Awesome.', + 'personalization_contact_field_type_delete_success' => 'סוג שדה איש הקשר נמחק בהצלחה.', + 'personalization_contact_field_type_add_success' => 'סוג שדה איש הקשר נוסף בהצלחה.', + 'personalization_contact_field_type_edit_success' => 'סוג שדה איש הקשר עודכן בהצלחה.', + + 'personalization_genders_title' => 'סוגי מגדר', + 'personalization_genders_add' => 'הוספת סוג מגדר חדש', + 'personalization_genders_desc' => 'ניתן להגדיר כמה מגדרים שיש לך צורך בהם. צריך לפחות סוג מגדר אחד בחשבון שלך.', + 'personalization_genders_modal_add' => 'הוספת סוג מגדר', + 'personalization_genders_modal_edit' => 'עדכון סוג מגדר', + 'personalization_genders_modal_name' => 'שם', + 'personalization_genders_modal_name_help' => 'השם המשמש להצגת המגדר בעמוד יצירת הקשר.', + 'personalization_genders_modal_sex' => 'מגדר', + 'personalization_genders_modal_sex_help' => 'משמש לציון יחסים ומהלך ייבוא/ייצוא של כרטיס ביקור - vCard.', + 'personalization_genders_modal_default' => 'נא לבחור את מגדר בררת המחדל לאנשי קשר חדשים', + 'personalization_genders_modal_delete' => 'מחיקת סוג מגדר', + 'personalization_genders_modal_delete_desc' => 'למחוק את המגדר „{name}”?', + 'personalization_genders_modal_delete_question' => 'לאחד מאנשי הקשר שלך מוגדר המגדר הזה. אם המגדר יימחק, לאיזה מגדר לשייך את איש הקשר?|ל־{count} מאנשי הקשר שלך מוגדר המגדר הזה. אם המגדר יימחק, לאיזה מגדר לשייך את אנשי הקשר?', + 'personalization_genders_modal_delete_question_default' => 'מגדר זה הוא בררת המחדל. לאחר מחיקת מגדר זה, איזה מגדר יוגדר כבררת המחדל החדשה?', + 'personalization_genders_modal_error' => 'נא לבחור במגדר מהרשימה.', + 'personalization_genders_list_contact_number' => 'איש קשר אחד|{count} אנשי קשר', + 'personalization_genders_table_name' => 'שם', + 'personalization_genders_table_sex' => 'מגדר', + 'personalization_genders_table_default' => 'בררת מחדל', + 'personalization_genders_default' => 'מגדר בררת המחדל', + 'personalization_genders_make_default' => 'החלפת מגדר בררת המחדל', + 'personalization_genders_select_default' => 'בחירת מגדר בררת מחדל', + 'personalization_genders_m' => 'זכר', + 'personalization_genders_f' => 'נקבה', + 'personalization_genders_o' => 'אחר', + 'personalization_genders_u' => 'לא ידוע', + 'personalization_genders_n' => 'אין או שלא משנה', + + 'personalization_reminder_rule_save' => 'השינוי נשמר', + 'personalization_reminder_rule_title' => 'כללי תזכורות', + 'personalization_reminder_rule_line' => 'יום לפני|יומיים לפני|{count} ימים לפני', + 'personalization_reminder_rule_desc' => 'לכל תזכורת שמוגדרת, מוניקה יכולה לשלוח לך הודעה כמה ימים לפני שהאירוע מתרחש. ניתן להתאים את הגדרות ההתראות האלו כאן. ההתראות חלות על תזכורות חודשיות ושנתיות.', + + 'personalization_module_save' => 'השינוי נשמר', + 'personalization_module_title' => 'תכונות', + 'personalization_module_desc' => 'יכול להיות שאין לך צורך בכל התכונות של מוניקה. להלן ניתן לכבות או להפעיל תכונות מסוימות שמשמשות אותך בגיליון אנשי קשר. השינוי הזה ישפיע על כל אנשי הקשר שלך. כיבוי התכונה הזאת לא מוחק נתונים, הוא פשוט מסתיר אותה.', + + 'personalisation_paid_upgrade' => 'תכונה זו היא תכונת פרמיום שדורשת הפעלת מינוי בתשלום. ניתן לשדרג את החשבון שלך על ידי מעבר להגדרות > מינוי.', + 'personalisation_paid_upgrade_vue' => 'תכונה זו היא תכונת פרמיום שדורשת הפעלת מינוי בתשלום. ניתן לשדרג את החשבון שלך על ידי מעבר להגדרות > מינוי.', + + 'reminder_time_to_send' => 'השעה ביום בה תישלחנה התזכורות', + 'reminder_time_to_send_help' => 'התזכורת הבאה שלך מתוזמנת להישלח ב־{dateTime}.', + + 'personalization_activity_type_category_title' => 'קטגוריות סוגי פעילות', + 'personalization_activity_type_category_add' => 'הוספת קטגוריית סוג פעילות חדשה', + 'personalization_activity_type_category_table_name' => 'שם', + 'personalization_activity_type_category_description' => 'פעילות עם אחד מאנשי הקשר שלך יכולה להיות מסוג וסוג קטגוריה מסוימים. החשבון שלך מוגדר מראש עם סוגי קטגוריות כלשהם כבררת מחדל, אך ניתן להתאים אותם דרך כאן.', + 'personalization_activity_type_category_table_actions' => 'פעולות', + 'personalization_activity_type_category_modal_add' => 'הוספת קטגוריית סוג פעילות חדשה', + 'personalization_activity_type_category_modal_edit' => 'עריכת קטגוריית סוג פעילות', + 'personalization_activity_type_category_modal_question' => 'איך יש לקרוא לקטגוריה החדשה?', + 'personalization_activity_type_add_button' => 'הוספת סוג פעילות חדש', + 'personalization_activity_type_modal_add' => 'הוספת סוג פעילות חדש', + 'personalization_activity_type_modal_question' => 'איך יש לקרוא לסוג הקטגוריה החדש?', + 'personalization_activity_type_modal_edit' => 'עריכת סוג פעילות', + 'personalization_activity_type_category_modal_delete' => 'מחיקת קטגוריית סוג פעילות', + 'personalization_activity_type_category_modal_delete_desc' => 'למחוק את הקטגוריה הזאת? מחיקתה תמחק את כל סוגי הפעילות המשויכים. פעילויות שאינן שייכות לקטגוריה הזאת לא תושפענה מהמחיקה.', + 'personalization_activity_type_modal_delete' => 'מחיקת סוג פעילות', + 'personalization_activity_type_modal_delete_desc' => 'למחוק את סוג הפעילות הזה? פעילויות ששייכות לקטגוריה זו לא תושפענה מהמחיקה הזאת.', + 'personalization_activity_type_modal_delete_error' => 'אין לנו אפשרות למצוא את סוג הפעילות.', + 'personalization_activity_type_category_modal_delete_error' => 'אין לנו אפשרות למצוא את קטגוריית סוג הפעילות הזו.', + + 'personalization_life_event_category_title' => 'קטגוריות אירועי חיים', + 'personalization_live_event_category_table_name' => 'שם', + 'personalization_life_event_category_description' => 'לאירוע חיים יכול להיות סוג וקטגוריה. בחשבון שלך כבר נוצרו קטגוריות וסוגים כבררת מחדל אך ניתן לערוך את סוגי אירועי החיים להלן.', + 'personalization_live_event_category_table_actions' => 'פעולות', + 'personalization_life_event_type_add_button' => 'הוספת סוג חדש של אירוע חיים', + 'personalization_life_event_type_modal_add' => 'הוספת סוג חדש של אירוע חיים', + 'personalization_life_event_type_modal_question' => 'איך יש לקרוא לסוג אירוע החיים החדש?', + 'personalization_life_event_type_modal_edit' => 'עריכת סוג אירוע חיים', + 'personalization_life_event_type_modal_delete' => 'מחיקת סוג אירוע חיים', + 'personalization_life_event_type_modal_delete_desc' => 'למחוק סוג אירוע חיים? אירועי חיים ששייכים לסוג הזה יימחקו עם ביצוע הפעולה.', + 'personalization_life_event_type_modal_delete_error' => 'לא הצלחנו למצוא את סוג אירוע חיים זה.', + + 'personalization_life_event_category_work_education' => 'עבודה והשכלה', + 'personalization_life_event_category_family_relationships' => 'משפחה ויחסים', + 'personalization_life_event_category_home_living' => 'בית ומחייה', + 'personalization_life_event_category_travel_experiences' => 'טיול וחוויות', + 'personalization_life_event_category_health_wellness' => 'בריאות ורווחה', + + 'personalization_life_event_type_new_job' => 'עבודה חדשה', + 'personalization_life_event_type_retirement' => 'פרישה', + 'personalization_life_event_type_new_school' => 'בית ספר חדש', + 'personalization_life_event_type_study_abroad' => 'לימודים בחו״ל', + 'personalization_life_event_type_volunteer_work' => 'עבודה התנדבותית', + 'personalization_life_event_type_published_book_or_paper' => 'פרסום של ספר או מאמר', + 'personalization_life_event_type_military_service' => 'שירות צבאי', + 'personalization_life_event_type_first_met' => 'פגישה ראשונה', + 'personalization_life_event_type_new_relationship' => 'קשר חדש', + 'personalization_life_event_type_engagement' => 'אירוסין', + 'personalization_life_event_type_marriage' => 'נישואין', + 'personalization_life_event_type_anniversary' => 'יום השנה', + 'personalization_life_event_type_expecting_a_baby' => 'בציפייה לתינוק', + 'personalization_life_event_type_new_child' => 'ילד חדש', + 'personalization_life_event_type_new_family_member' => 'חבר חדש במשפחה', + 'personalization_life_event_type_new_pet' => 'חיית מחמד חדשה', + 'personalization_life_event_type_end_of_relationship' => 'סיום קשר', + 'personalization_life_event_type_loss_of_a_loved_one' => 'אובדן של אדם קרוב', + 'personalization_life_event_type_moved' => 'מעבר דירה', + 'personalization_life_event_type_bought_a_home' => 'רכישת דירה', + 'personalization_life_event_type_home_improvement' => 'שיפוץ הבית', + 'personalization_life_event_type_holidays' => 'חגים', + 'personalization_life_event_type_new_vehicle' => 'כלי רכב חדש', + 'personalization_life_event_type_new_roommate' => 'שותף/שותפה חדש/ה', + 'personalization_life_event_type_overcame_an_illness' => 'החלמה ממחלה', + 'personalization_life_event_type_quit_a_habit' => 'גמילה', + 'personalization_life_event_type_new_eating_habits' => 'הרגלי אכילה חדשים', + 'personalization_life_event_type_weight_loss' => 'ירידה במשקל', + 'personalization_life_event_type_wear_glass_or_contact' => 'התחלת הרכבת משקפיים או עדשות מגע', + 'personalization_life_event_type_broken_bone' => 'שבירת עצם', + 'personalization_life_event_type_removed_braces' => 'הסרת גשר בשיניים', + 'personalization_life_event_type_surgery' => 'לאחר ניתוח', + 'personalization_life_event_type_dentist' => 'לאחר טיפול שיניים', + 'personalization_life_event_type_new_sport' => 'עיסוק בספורט חדש', + 'personalization_life_event_type_new_hobby' => 'אימוץ תחביב חדש', + 'personalization_life_event_type_new_instrument' => 'התחלת למידת כלי נגינה חדש', + 'personalization_life_event_type_new_language' => 'התחלת רכישת שפה חדשה', + 'personalization_life_event_type_tattoo_or_piercing' => 'קעקוע או פירסינג', + 'personalization_life_event_type_new_license' => 'רישיון חדש', + 'personalization_life_event_type_travel' => 'טיול', + 'personalization_life_event_type_achievement_or_award' => 'הישג או פרס', + 'personalization_life_event_type_changed_beliefs' => 'שינוי אמונה', + 'personalization_life_event_type_first_word' => 'מילה ראשונה', + 'personalization_life_event_type_first_kiss' => 'נשיקה ראשונה', + + 'storage_title' => 'אחסון', + 'storage_account_info' => 'מגבלת החשבון שלך היא: :accountLimit מגה בתים. הניצולת הנוכחית שלך היא: :currentAccountSize מגה בתים (בערך :percentUsage%).', + 'storage_upgrade_notice' => 'עליך לשדרג את החשבון שלך כדי שתהיה לך אפשרות להעלות מסמכים ותמונות.', + 'storage_description' => 'כאן ניתן לצפות בכל המסמכים והתמונות שהועלו לטובת אנשי הקשר שלך.', + + 'dav_title' => 'WebDAV', + 'dav_description' => 'להלן ניתן למצוא את כל ההגדרות לשימוש במשאבי WebDAV עבור נתונים מיוצאים מסוג CardDAV ו־CalDAV.', + 'dav_copy_help' => 'העתקה ללוח הגזירים שלך', + 'dav_clipboard_copied' => 'הערך הועתק ללוח הגזירים שלך', + 'dav_url_base' => 'כתובת בסיס לכל המשאבים מסוג CardDAV ו־CalDAV:', + 'dav_connect_help' => 'ניתן להתחבר לאנשי הקשר ו/או ללוחות השנה שלך עם כתובת בסיס זו מהטלפון או המחשב שלך.', + 'dav_connect_help2' => 'עליך להשתמש בשם הכניסה שלך (כתובת דוא״ל) וליצור אסימון API בתור ססמה לאימות.', + 'dav_url_carddav' => 'כתובת CardDAV למשאבי אנשי קשר:', + 'dav_url_caldav_birthdays' => 'כתובת CalDAV למשאבי ימי הולדת:', + 'dav_url_caldav_tasks' => 'כתובת CalDAV עבור משאבי משימות:', + 'dav_title_carddav' => 'CardDAV', + 'dav_title_caldav' => 'CalDAV', + 'dav_carddav_export' => 'ייצוא כל אנשי הקשר בקובץ אחד', + 'dav_caldav_birthdays_export' => 'ייצוא כל ימי ההולדת בקובץ אחד', + 'dav_caldav_tasks_export' => 'ייצוא כל המשימות בקובץ אחד', + + 'archive_title' => 'העברת כל אנשי הקשר בחשבון שלך לארכיון', + 'archive_desc' => 'פעולה זו תעביר את כל אנשי הקשר בחשבון שלך לארכיון.', + 'archive_cta' => 'העברת כל אנשי הקשר לארכיון', + + 'logs_title' => 'כל מה שקרה לחשבון הזה', + 'logs_actor' => 'גורם', + 'logs_timestamp' => 'חותמת זמן', + 'logs_description' => 'תיאור', + 'logs_subject' => 'נושא', + 'logs_size' => 'גודל (ק״ס)', + 'logs_object' => 'עצם', +]; diff --git a/resources/lang/he/validation.php b/resources/lang/he/validation.php new file mode 100644 index 0000000..67bdc7f --- /dev/null +++ b/resources/lang/he/validation.php @@ -0,0 +1,166 @@ + ':attribute חייב להיות מסומן.', + 'active_url' => ':attribute אינה כתובת תקנית.', + 'after' => ':attribute חייב להיות תאריך לאחר :date.', + 'after_or_equal' => ':attribute חייב להיות התאריך :date או אחריו.', + 'alpha' => ':attribute יכול להכיל אותיות בלבד.', + 'alpha_dash' => 'שדה :attribute יכול להכיל אותיות, מספרים ומקפים בלבד.', + 'alpha_num' => ':attribute יכול להכיל אותיות ומספרים בלבד.', + 'array' => ':attribute חייב להיות מערך.', + 'before' => ':attribute חייב להיות תאריך לפני :date.', + 'before_or_equal' => ':attribute חייב להיות התאריך :date או לפניו.', + 'between' => [ + 'numeric' => ':attribute חייב להיות בין :min לבין :max.', + 'file' => ':attribute חייב להיות בין :min לבין :max קילובתים.', + 'string' => ':attribute חייב להיות בין :min לבין :max תווים.', + 'array' => ':attribute חייב להיות בין :min לבין :max פריטים.', + ], + 'boolean' => 'השדה :attribute חייב להיות אמת או שקר.', + 'confirmed' => 'האימות של :attribute לא תואם.', + 'date' => ':attribute אינו תאריך תקני.', + 'date_equals' => 'על ה :attribute להיות תאריך שווה ל- :date.', + 'date_format' => ':attribute לא תואם את המבנה :format.', + 'different' => ':attribute וגם :other חייבים להיות שונים זה מזה.', + 'digits' => ':attribute חייב להיות באורך :digits ספרות.', + 'digits_between' => ':attribute חייב להיות בין :min ל־:max ספרות.', + 'dimensions' => 'ממדי התמונה של :attribute שגויים.', + 'distinct' => 'לשדה :attribute יש ערך כפול.', + 'email' => ':attribute חייב להיות כתובת דוא״ל תקנית.', + 'ends_with' => 'שדה :attribute חייב להסתיים באחד מהבאים: :values', + 'exists' => ':attribute הנבחר שגוי.', + 'file' => ':attribute חייב להיות קובץ.', + 'filled' => 'השדה :attribute חייב להכיל לערך.', + 'gt' => [ + 'numeric' => 'על ה :attribute להיות גדול יותר מ- :value.', + 'file' => 'על ה :attribute להיות גדול יותר מ- :value קילו-בתים.', + 'string' => 'על ה :attribute להיות גדול יותר מ- :value תווים.', + 'array' => 'על ה :attribute לכלול יותר מ- :value פריטים.', + ], + 'gte' => [ + 'numeric' => 'על ה :attribute להיות גדול יותר או שווה ל- :value.', + 'file' => 'על ה :attribute להיות גדול יותר או שווה ל- :value קילו-בתים.', + 'string' => 'על ה :attribute להיות גדול יותר או שווה ל- :value תווים.', + 'array' => 'ה :attribute חייב לכלול :value פריטים או יותר.', + ], + 'image' => ':attribute חייב להיות תמונה.', + 'in' => ':attribute הנבחר שגוי.', + 'in_array' => 'השדה :attribute לא קיים תחת :other.', + 'integer' => ':attribute חייב להיות מספר שלם וחיובי.', + 'ip' => ':attribute חייב להיות כתובת IP תקנית.', + 'ipv4' => ':attribute חייב להיות כתובת IPv4 תקנית.', + 'ipv6' => ':attribute חייב להיות כתובת IPv6 תקנית.', + 'json' => ':attribute חייב להיות מחרוזת JSON תקנית.', + 'lt' => [ + 'numeric' => 'על ה :attribute להיות נמוך יותר מ- :value.', + 'file' => ':attribute חייב להיות קטן מ־:value קילובתים.', + 'string' => 'על ה :attribute להכיל פחות מ- :value תווים.', + 'array' => 'על ה :attribute לכלול פחות מ- :value פריטים.', + ], + 'lte' => [ + 'numeric' => 'על ה :attribute להיות נמוך או שווה ל- :value.', + 'file' => 'על ה :attribute להיות קטן יותר או שווה ל- :value קילו-בתים.', + 'string' => 'על ה :attribute להכיל :value תווים או פחות.', + 'array' => 'ה :attribute לא יכול לכלול יותר מאשר :value פריטים.', + ], + 'max' => [ + 'numeric' => ':attribute לא יכול להיות יותר גדול מאשר :max.', + 'file' => ':attribute לא יכול להיות גדול מ־:max קילובתים.', + 'string' => ':attribute לא יכול להיות גדול מ־:max תווים.', + 'array' => 'תחת :attribute לא יכולים להיות יותר מ־:max פריטים.', + ], + 'mimes' => ':attribute חייב להיות קובץ מסוג: :values.', + 'mimetypes' => ':attribute חייב להיות קובץ מסוג: :values.', + 'min' => [ + 'numeric' => ':attribute חייב להיות לפחות :min.', + 'file' => ':attribute חייב להיות בגודל של לפחות :min קילובתים.', + 'string' => ':attribute חייב להיות באורך של לפחות :min תווים.', + 'array' => 'תחת :attribute חייבים להיות לפחות :min פריטים.', + ], + 'not_in' => ':attribute הנבחר שגוי.', + 'not_regex' => 'התבנית :attribute שגויה.', + 'numeric' => ':attribute חייב להיות מספר.', + 'password' => 'הססמה שגויה.', + 'present' => 'השדה :attribute חייב להיות נוכח.', + 'regex' => 'המבנה :attribute שגוי.', + 'required' => 'השדה :attribute נחוץ.', + 'required_if' => 'השדה :attribute נחוץ כאשר :other הוא :value.', + 'required_unless' => 'השדה :attribute נחוץ אלמלא :other קיים בתוך :values.', + 'required_with' => 'השדה :attribute נחוץ כאשר :values קיימים.', + 'required_with_all' => 'שדה :attribute נחוץ כאשר :values נמצא.', + 'required_without' => 'השדה :attribute נחוץ כאשר :values אינם קיימים.', + 'required_without_all' => 'השדה :attribute נחוץ כאשר אף אחד מבין :values קיים.', + 'same' => ':attribute וגם :other חייבים להיות תואמים.', + 'size' => [ + 'numeric' => ':attribute חייב להיות בגודל :size.', + 'file' => ':attribute חייב להיות בגודל של :size קילובתים.', + 'string' => ':attribute חייב להיות באורך של :size תווים.', + 'array' => ':attribute חייב להכיל :size פריטים.', + ], + 'starts_with' => 'ה :attribute חייב להתחיל עם אחד מהבאים: :values', + 'string' => ':attribute חייב להיות קובץ.', + 'timezone' => ':attribute חייב להיות אזור תקני.', + 'unique' => ':attribute כבר תפוס.', + 'uploaded' => 'העלאת :attribute נכשלה.', + 'url' => 'התבנית :attribute שגויה.', + 'uuid' => 'ה :attribute חייב להיות מזהה ייחודי אוניברסלי (UUID) חוקי.', + + /* + |-------------------------------------------------------------------------- + | Custom Validation Language Lines + |-------------------------------------------------------------------------- + | + | Here you may specify custom validation messages for attributes using the + | convention "attribute.rule" to name the lines. This makes it quick to + | specify a specific custom language line for a given attribute rule. + | + */ + + 'custom' => [ + 'attribute-name' => [ + 'rule-name' => 'custom-message', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Custom Validation Attributes + |-------------------------------------------------------------------------- + | + | The following language lines are used to swap our attribute placeholder + | with something more reader friendly such as "E-Mail Address" instead + | of "email". This simply helps us make our message more expressive. + | + */ + + 'attributes' => [], + + 'vue' => [ + 'max' => [ + 'numeric' => '{field} לא יכול להיות גדול מהערך {max}.', + 'string' => '{field} לא יכול לחרוג מעבר ל־{max} תווים.', + ], + 'required' => '{field} הוא שדה חובה.', + 'url' => '{field} אינה כתובת אתר תקנית.', + ], + +]; diff --git a/resources/lang/hr.json b/resources/lang/hr.json new file mode 100644 index 0000000..ddea72e --- /dev/null +++ b/resources/lang/hr.json @@ -0,0 +1,7 @@ +{ + "The :attribute must contain at least one uppercase and one lowercase letter.": "The :attribute must contain at least one uppercase and one lowercase letter.", + "The :attribute must contain at least one letter.": "The :attribute must contain at least one letter.", + "The :attribute must contain at least one symbol.": "The :attribute must contain at least one symbol.", + "The :attribute must contain at least one number.": "The :attribute must contain at least one number.", + "The given :attribute has appeared in a data leak. Please choose a different :attribute.": "The given :attribute has appeared in a data leak. Please choose a different :attribute." +} diff --git a/resources/lang/hr/app.php b/resources/lang/hr/app.php new file mode 100644 index 0000000..6bdf913 --- /dev/null +++ b/resources/lang/hr/app.php @@ -0,0 +1,571 @@ + 'Yes', + 'no' => 'No', + 'update' => 'Ažuriraj', + 'save' => 'Spremi', + 'add' => 'Dodaj', + 'cancel' => 'Poništi', + 'confirm' => 'Confirm', + 'delete_confirm' => 'Are you sure?', + 'delete' => 'Obriši', + 'edit' => 'Uredi', + 'upload' => 'Uploadaj', + 'download' => 'Preuzmi', + 'save_close' => 'Spremi i zatvori', + 'close' => 'Zatvori', + 'copy' => 'Copy', + 'create' => 'Dodaj', + 'remove' => 'Izbriši', + 'revoke' => 'Opozovi', + 'done' => 'Gotovo', + 'back' => 'Back', + 'verify' => 'Potvrdi', + 'new' => 'novo', + 'unknown' => 'Nepoznato', + 'load_more' => 'Učitaj više', + 'loading' => 'Loading…', + 'with' => 's', + 'today' => 'danas', + 'yesterday' => 'jučer', + 'another_day' => 'drugi dan', + 'date' => 'Datum', + 'type' => 'Vrsta', + 'zoom' => 'Zoom', + 'upgrade' => 'Upgrade to unlock', + 'percent_uploaded' => '{percent}% uploaded', + 'retry' => 'Retry', + 'filter' => 'Filter the list', + 'go_back' => 'Go back', + 'file_selected' => 'One file selected…|{count} files selected…', + + 'application_title' => 'Monica – upravljanje osobnim odnosima', + 'application_description' => 'Monica is a tool to manage your interactions with your loved ones, friends and family.', + 'application_og_title' => 'Have better relations with your loved ones. Free online CRM for friends and family.', + + 'markdown_description' => 'Want to format your text in a nice way? We support Markdown to add bold, italic, lists and more.', + 'markdown_link' => 'Pročitajte dokumentaciju', + + 'header_settings_link' => 'Postavke', + 'header_logout_link' => 'Odjava', + 'header_changelog_link' => 'Product changes', + + 'main_nav_cta' => 'Dodajte osobu', + 'main_nav_dashboard' => 'Nadzorna ploča', + 'main_nav_family' => 'Kontakti', + 'main_nav_journal' => 'Dnevnik', + 'main_nav_activities' => 'Aktivnosti', + 'main_nav_tasks' => 'Zadaci', + + 'footer_remarks' => 'Comments?', + 'footer_send_email' => 'Send us an email', + 'footer_privacy' => 'Pravila privatnosti', + 'footer_release' => 'Release notes', + 'footer_newsletter' => 'Newsletter', + 'footer_source_code' => 'Pridonesite', + 'footer_version' => 'Verzija :version', + 'footer_new_version' => 'A new version of Monica is available', + + 'footer_modal_version_whats_new' => 'Što je novo', + 'footer_modal_version_release_away' => 'You are 1 release behind the latest version available. You should update your instance.|You are :number releases behind the latest version available. You should update your instance.', + + 'breadcrumb_dashboard' => 'Dashboard', + 'breadcrumb_list_contacts' => 'List of people', + 'breadcrumb_archived_contacts' => 'Archived contacts', + 'breadcrumb_journal' => 'Journal', + 'breadcrumb_settings' => 'Settings', + 'breadcrumb_settings_export' => 'Export', + 'breadcrumb_settings_users' => 'Users', + 'breadcrumb_settings_users_add' => 'Add a user', + 'breadcrumb_settings_subscriptions' => 'Subscription', + 'breadcrumb_settings_import' => 'Import', + 'breadcrumb_settings_import_report' => 'Import report', + 'breadcrumb_settings_import_upload' => 'Upload', + 'breadcrumb_settings_tags' => 'Tags', + 'breadcrumb_add_significant_other' => 'Add significant other', + 'breadcrumb_edit_significant_other' => 'Edit significant other', + 'breadcrumb_add_note' => 'Add a note', + 'breadcrumb_edit_note' => 'Edit a note', + 'breadcrumb_api' => 'API', + 'breadcrumb_dav' => 'DAV Resources', + 'breadcrumb_edit_introductions' => 'How did you meet', + 'breadcrumb_settings_personalization' => 'Personalization', + 'breadcrumb_settings_security' => 'Security', + 'breadcrumb_settings_security_2fa' => 'Two Factor Authentication', + 'breadcrumb_profile' => 'Profile of :name', + + 'gender_male' => 'Man', + 'gender_female' => 'Woman', + 'gender_none' => 'Rather not say', + 'gender_no_gender' => 'No gender', + + 'error_title' => 'Whoops! Something went wrong.', + 'error_unauthorized' => 'You don’t have the right to edit this resource.', + 'error_user_account' => 'This user does not belong to the given account.', + 'error_save' => 'We had an error trying to save the data.', + 'error_try_again' => 'Something went wrong. Please try again.', + 'error_id' => 'Error ID: :id', + 'error_unavailable' => 'Service unavailable', + 'error_maintenance' => 'Maintenance in progress. We’ll be right back.', + 'error_help' => 'We’ll be right back.', + 'error_twitter' => 'Follow our Twitter account to be alerted when it’s up again.', + 'error_no_term' => 'There is no policy for this instance yet.', + + 'default_save_success' => 'The data has been saved.', + + 'compliance_title' => 'Sorry for the interruption.', + 'compliance_desc' => 'We have changed our Terms of Use and Privacy Policy. By law we have to ask you to review them and accept them so you can continue to use your account.', + 'compliance_desc_end' => 'We don’t do anything nasty with your data or account and will never do.', + 'compliance_terms' => 'Accept new terms and privacy policy', + + // Relationship types + // Yes, each relationship type has 8 strings associated with it. + // This is because we need to indicate the name of the relationship type, + // and also the name of the opposite side of this relationship (father/son), + // and then, the feminine version of the string. Finally, in some sentences + // in the UI, we need to include the name of the person we add the relationship + // to. + 'relationship_type_group_love' => 'Love relationships', + 'relationship_type_group_family' => 'Family relationships', + 'relationship_type_group_friend' => 'Friend relationships', + 'relationship_type_group_work' => 'Work relationships', + 'relationship_type_group_other' => 'druga vrsta odnosa', + + 'relationship_type_partner' => 'partner', + 'relationship_type_partner_female' => 'partnerica', + 'relationship_type_partner_male' => 'significant other', + 'relationship_type_partner_with_name' => ':name’s significant other', + 'relationship_type_partner_female_with_name' => ':name’s significant other', + 'relationship_type_partner_male_with_name' => ':name’s significant other', + + 'relationship_type_spouse' => 'spouse', + 'relationship_type_spouse_female' => 'wife', + 'relationship_type_spouse_male' => 'husband', + 'relationship_type_spouse_with_name' => ':name’s spouse', + 'relationship_type_spouse_female_with_name' => ':name’s wife', + 'relationship_type_spouse_male_with_name' => ':name’s husband', + + 'relationship_type_date' => 'date', + 'relationship_type_date_female' => 'date', + 'relationship_type_date_male' => 'date', + 'relationship_type_date_with_name' => ':name’s date', + 'relationship_type_date_female_with_name' => ':name’s date', + 'relationship_type_date_male_with_name' => ':name’s date', + + 'relationship_type_lover' => 'lover', + 'relationship_type_lover_female' => 'lover', + 'relationship_type_lover_male' => 'lover', + 'relationship_type_lover_with_name' => ':name’s lover', + 'relationship_type_lover_female_with_name' => ':name’s lover', + 'relationship_type_lover_male_with_name' => ':name’s lover', + + 'relationship_type_inlovewith' => 'in love with', + 'relationship_type_inlovewith_female' => 'in love with', + 'relationship_type_inlovewith_male' => 'in love with', + 'relationship_type_inlovewith_with_name' => 'someone :name is in love with', + 'relationship_type_inlovewith_female_with_name' => 'someone :name is in love with', + 'relationship_type_inlovewith_male_with_name' => 'someone :name is in love with', + + 'relationship_type_lovedby' => 'loved by', + 'relationship_type_lovedby_female' => 'loved by', + 'relationship_type_lovedby_male' => 'loved by', + 'relationship_type_lovedby_with_name' => ':name’s secret lover', + 'relationship_type_lovedby_female_with_name' => ':name’s secret lover', + 'relationship_type_lovedby_male_with_name' => ':name’s secret lover', + + 'relationship_type_ex' => 'ex-partner', + 'relationship_type_ex_female' => 'ex-girlfriend', + 'relationship_type_ex_male' => 'ex-boyfriend', + 'relationship_type_ex_with_name' => ':name’s ex-partner', + 'relationship_type_ex_female_with_name' => ':name’s ex-girlfriend', + 'relationship_type_ex_male_with_name' => ':name’s ex-boyfriend', + + 'relationship_type_parent' => 'parent', + 'relationship_type_parent_female' => 'mother', + 'relationship_type_parent_male' => 'father', + 'relationship_type_parent_with_name' => ':name’s parent', + 'relationship_type_parent_female_with_name' => ':name’s mother', + 'relationship_type_parent_male_with_name' => ':name’s father', + + 'relationship_type_child' => 'child', + 'relationship_type_child_female' => 'daughter', + 'relationship_type_child_male' => 'son', + 'relationship_type_child_with_name' => ':name’s child', + 'relationship_type_child_female_with_name' => ':name’s daughter', + 'relationship_type_child_male_with_name' => ':name’s son', + + 'relationship_type_stepparent' => 'step-parent', + 'relationship_type_stepparent_female' => 'stepmother', + 'relationship_type_stepparent_male' => 'stepfather', + 'relationship_type_stepparent_with_name' => ':name’s step-parent', + 'relationship_type_stepparent_female_with_name' => ':name’s stepmother', + 'relationship_type_stepparent_male_with_name' => ':name’s stepfather', + + 'relationship_type_stepchild' => 'stepchild', + 'relationship_type_stepchild_female' => 'stepdaughter', + 'relationship_type_stepchild_male' => 'stepson', + 'relationship_type_stepchild_with_name' => ':name’s stepchild', + 'relationship_type_stepchild_female_with_name' => ':name’s stepdaughter', + 'relationship_type_stepchild_male_with_name' => ':name’s stepson', + + 'relationship_type_sibling' => 'sibling', + 'relationship_type_sibling_female' => 'sister', + 'relationship_type_sibling_male' => 'brother', + 'relationship_type_sibling_with_name' => ':name’s sibling', + 'relationship_type_sibling_female_with_name' => ':name’s sister', + 'relationship_type_sibling_male_with_name' => ':name’s brother', + + 'relationship_type_grandparent' => 'grandparent', + 'relationship_type_grandparent_female' => 'grandmother', + 'relationship_type_grandparent_male' => 'grandfather', + 'relationship_type_grandparent_with_name' => ':name’s grandparent', + 'relationship_type_grandparent_female_with_name' => ':name’s grandmother', + 'relationship_type_grandparent_male_with_name' => ':name’s grandfather', + + 'relationship_type_grandchild' => 'grandchild', + 'relationship_type_grandchild_female' => 'granddaughter', + 'relationship_type_grandchild_male' => 'grandson', + 'relationship_type_grandchild_with_name' => ':name’s grandchild', + 'relationship_type_grandchild_female_with_name' => ':name’s granddauther', + 'relationship_type_grandchild_male_with_name' => ':name’s grandson', + + 'relationship_type_uncle' => 'uncle', + 'relationship_type_uncle_female' => 'aunt', + 'relationship_type_uncle_male' => 'uncle', + 'relationship_type_uncle_with_name' => ':name’s uncle', + 'relationship_type_uncle_female_with_name' => ':name’s aunt', + 'relationship_type_uncle_male_with_name' => ':name’s uncle', + + 'relationship_type_nephew' => 'nephew', + 'relationship_type_nephew_female' => 'niece', + 'relationship_type_nephew_male' => 'nephew', + 'relationship_type_nephew_with_name' => ':name’s nephew', + 'relationship_type_nephew_female_with_name' => ':name’s niece', + 'relationship_type_nephew_male_with_name' => ':name’s nephew', + + 'relationship_type_cousin' => 'cousin', + 'relationship_type_cousin_female' => 'cousin', + 'relationship_type_cousin_male' => 'cousin', + 'relationship_type_cousin_with_name' => ':name’s cousin', + 'relationship_type_cousin_female_with_name' => ':name’s cousin', + 'relationship_type_cousin_male_with_name' => ':name’s cousin', + + 'relationship_type_godfather' => 'godparent', + 'relationship_type_godfather_female' => 'godmother', + 'relationship_type_godfather_male' => 'godfather', + 'relationship_type_godfather_with_name' => ':name’s godparent', + 'relationship_type_godfather_female_with_name' => ':name’s godmother', + 'relationship_type_godfather_male_with_name' => ':name’s godfather', + + 'relationship_type_godson' => 'godchild', + 'relationship_type_godson_female' => 'goddaughter', + 'relationship_type_godson_male' => 'godson', + 'relationship_type_godson_with_name' => ':name’s godchild', + 'relationship_type_godson_female_with_name' => ':name’s goddaughter', + 'relationship_type_godson_male_with_name' => ':name’s godson', + + 'relationship_type_friend' => 'friend', + 'relationship_type_friend_female' => 'friend', + 'relationship_type_friend_male' => 'friend', + 'relationship_type_friend_with_name' => ':name’s friend', + 'relationship_type_friend_female_with_name' => ':name’s friend', + 'relationship_type_friend_male_with_name' => ':name’s friend', + + 'relationship_type_bestfriend' => 'best friend', + 'relationship_type_bestfriend_female' => 'best friend', + 'relationship_type_bestfriend_male' => 'best friend', + 'relationship_type_bestfriend_with_name' => ':name’s best friend', + 'relationship_type_bestfriend_female_with_name' => ':name’s best friend', + 'relationship_type_bestfriend_male_with_name' => ':name’s best friend', + + 'relationship_type_colleague' => 'colleague', + 'relationship_type_colleague_female' => 'colleague', + 'relationship_type_colleague_male' => 'colleague', + 'relationship_type_colleague_with_name' => ':name’s colleague', + 'relationship_type_colleague_female_with_name' => ':name’s colleague', + 'relationship_type_colleague_male_with_name' => ':name’s colleague', + + 'relationship_type_boss' => 'boss', + 'relationship_type_boss_female' => 'boss', + 'relationship_type_boss_male' => 'boss', + 'relationship_type_boss_with_name' => ':name’s boss', + 'relationship_type_boss_female_with_name' => ':name’s boss', + 'relationship_type_boss_male_with_name' => ':name’s boss', + + 'relationship_type_subordinate' => 'subordinate', + 'relationship_type_subordinate_female' => 'subordinate', + 'relationship_type_subordinate_male' => 'subordinate', + 'relationship_type_subordinate_with_name' => ':name’s subordinate', + 'relationship_type_subordinate_female_with_name' => ':name’s subordinate', + 'relationship_type_subordinate_male_with_name' => ':name’s subordinate', + + 'relationship_type_mentor' => 'mentor', + 'relationship_type_mentor_female' => 'mentor', + 'relationship_type_mentor_male' => 'mentor', + 'relationship_type_mentor_with_name' => ':name’s mentor', + 'relationship_type_mentor_female_with_name' => ':name’s mentor', + 'relationship_type_mentor_male_with_name' => ':name’s mentor', + + 'relationship_type_protege' => 'protégé', + 'relationship_type_protege_female' => 'protégé', + 'relationship_type_protege_male' => 'protégé', + 'relationship_type_protege_with_name' => ':name’s protégé', + 'relationship_type_protege_female_with_name' => ':name’s protégé', + 'relationship_type_protege_male_with_name' => ':name’s protégé', + + 'relationship_type_ex_husband' => 'ex-spouse', + 'relationship_type_ex_husband_female' => 'ex wife', + 'relationship_type_ex_husband_male' => 'ex-husband', + 'relationship_type_ex_husband_with_name' => ':name’s ex-spouse', + 'relationship_type_ex_husband_female_with_name' => ':name’s ex wife', + 'relationship_type_ex_husband_male_with_name' => ':name’s ex-husband', + + // emotions + 'emotion_primary_love' => 'Love', + 'emotion_primary_joy' => 'Joy', + 'emotion_primary_surprise' => 'Surprise', + 'emotion_primary_anger' => 'Anger', + 'emotion_primary_sadness' => 'Sadness', + 'emotion_primary_fear' => 'Fear', + + 'emotion_secondary_affection' => 'Affection', + 'emotion_secondary_lust' => 'Lust', + 'emotion_secondary_longing' => 'Longing', + 'emotion_secondary_cheerfulness' => 'Cheerfulness', + 'emotion_secondary_zest' => 'Zest', + 'emotion_secondary_contentment' => 'Contentment', + 'emotion_secondary_pride' => 'Pride', + 'emotion_secondary_optimism' => 'Optimism', + 'emotion_secondary_enthrallment' => 'Enthrallment', + 'emotion_secondary_relief' => 'Relief', + 'emotion_secondary_surprise' => 'Surprise', + 'emotion_secondary_irritation' => 'Irritation', + 'emotion_secondary_exasperation' => 'Exasperation', + 'emotion_secondary_rage' => 'Rage', + 'emotion_secondary_disgust' => 'Disgust', + 'emotion_secondary_envy' => 'Envy', + 'emotion_secondary_suffering' => 'Suffering', + 'emotion_secondary_sadness' => 'Sadness', + 'emotion_secondary_disappointment' => 'Disappointment', + 'emotion_secondary_shame' => 'Shame', + 'emotion_secondary_neglect' => 'Neglect', + 'emotion_secondary_sympathy' => 'Sympathy', + 'emotion_secondary_horror' => 'Horror', + 'emotion_secondary_nervousness' => 'Nervousness', + + 'emotion_adoration' => 'Adoration', + 'emotion_affection' => 'Affection', + 'emotion_love' => 'Love', + 'emotion_fondness' => 'Fondness', + 'emotion_liking' => 'Liking', + 'emotion_attraction' => 'Attraction', + 'emotion_caring' => 'Caring', + 'emotion_tenderness' => 'Tenderness', + 'emotion_compassion' => 'Compassion', + 'emotion_sentimentality' => 'Sentimentality', + 'emotion_arousal' => 'Arousal', + 'emotion_desire' => 'Desire', + 'emotion_lust' => 'Lust', + 'emotion_passion' => 'Passion', + 'emotion_infatuation' => 'Infatuation', + 'emotion_longing' => 'Longing', + 'emotion_amusement' => 'Amusement', + 'emotion_bliss' => 'Bliss', + 'emotion_cheerfulness' => 'Cheerfulness', + 'emotion_gaiety' => 'Gaiety', + 'emotion_glee' => 'Glee', + 'emotion_jolliness' => 'Jolliness', + 'emotion_joviality' => 'Joviality', + 'emotion_joy' => 'Joy', + 'emotion_delight' => 'Delight', + 'emotion_enjoyment' => 'Enjoyment', + 'emotion_gladness' => 'Gladness', + 'emotion_happiness' => 'Happiness', + 'emotion_jubilation' => 'Jubilation', + 'emotion_elation' => 'Elation', + 'emotion_satisfaction' => 'Satisfaction', + 'emotion_ecstasy' => 'Ecstasy', + 'emotion_euphoria' => 'Euphoria', + 'emotion_enthusiasm' => 'Enthusiasm', + 'emotion_zeal' => 'Zeal', + 'emotion_zest' => 'Zest', + 'emotion_excitement' => 'Excitement', + 'emotion_thrill' => 'Thrill', + 'emotion_exhilaration' => 'Exhilaration', + 'emotion_contentment' => 'Contentment', + 'emotion_pleasure' => 'Pleasure', + 'emotion_pride' => 'Pride', + 'emotion_eagerness' => 'Eagerness', + 'emotion_hope' => 'Hope', + 'emotion_optimism' => 'Optimism', + 'emotion_enthrallment' => 'Enthrallment', + 'emotion_rapture' => 'Rapture', + 'emotion_relief' => 'Relief', + 'emotion_amazement' => 'Amazement', + 'emotion_surprise' => 'Surprise', + 'emotion_astonishment' => 'Astonishment', + 'emotion_aggravation' => 'Aggravation', + 'emotion_irritation' => 'Irritation', + 'emotion_agitation' => 'Agitation', + 'emotion_annoyance' => 'Annoyance', + 'emotion_grouchiness' => 'Grouchiness', + 'emotion_grumpiness' => 'Grumpiness', + 'emotion_exasperation' => 'Exasperation', + 'emotion_frustration' => 'Frustration', + 'emotion_anger' => 'Anger', + 'emotion_rage' => 'Rage', + 'emotion_outrage' => 'Outrage', + 'emotion_fury' => 'Fury', + 'emotion_wrath' => 'Wrath', + 'emotion_hostility' => 'Hostility', + 'emotion_ferocity' => 'Ferocity', + 'emotion_bitterness' => 'Bitterness', + 'emotion_hate' => 'Hate', + 'emotion_loathing' => 'Loathing', + 'emotion_scorn' => 'Scorn', + 'emotion_spite' => 'Spite', + 'emotion_vengefulness' => 'Vengefulness', + 'emotion_dislike' => 'Dislike', + 'emotion_resentment' => 'Resentment', + 'emotion_disgust' => 'Disgust', + 'emotion_revulsion' => 'Revulsion', + 'emotion_contempt' => 'Contempt', + 'emotion_envy' => 'Envy', + 'emotion_jealousy' => 'Jealousy', + 'emotion_agony' => 'Agony', + 'emotion_suffering' => 'Suffering', + 'emotion_hurt' => 'Hurt', + 'emotion_anguish' => 'Anguish', + 'emotion_depression' => 'Depression', + 'emotion_despair' => 'Despair', + 'emotion_hopelessness' => 'Hopelessness', + 'emotion_gloom' => 'Gloom', + 'emotion_glumness' => 'Glumness', + 'emotion_sadness' => 'Sadness', + 'emotion_unhappiness' => 'Unhappiness', + 'emotion_grief' => 'Grief', + 'emotion_sorrow' => 'Sorrow', + 'emotion_woe' => 'Woe', + 'emotion_misery' => 'Misery', + 'emotion_melancholy' => 'Melancholy', + 'emotion_dismay' => 'Dismay', + 'emotion_disappointment' => 'Disappointment', + 'emotion_displeasure' => 'Displeasure', + 'emotion_guilt' => 'Guilt', + 'emotion_shame' => 'Shame', + 'emotion_regret' => 'Regret', + 'emotion_remorse' => 'Remorse', + 'emotion_alienation' => 'Alienation', + 'emotion_isolation' => 'Isolation', + 'emotion_neglect' => 'Neglect', + 'emotion_loneliness' => 'Loneliness', + 'emotion_rejection' => 'Rejection', + 'emotion_homesickness' => 'Homesickness', + 'emotion_defeat' => 'Defeat', + 'emotion_dejection' => 'Dejection', + 'emotion_insecurity' => 'Insecurity', + 'emotion_embarrassment' => 'Embarrassment', + 'emotion_humiliation' => 'Humiliation', + 'emotion_insult' => 'Insult', + 'emotion_pity' => 'Pity', + 'emotion_sympathy' => 'Sympathy', + 'emotion_alarm' => 'Alarm', + 'emotion_shock' => 'Shock', + 'emotion_fear' => 'Fear', + 'emotion_fright' => 'Fright', + 'emotion_horror' => 'Horror', + 'emotion_terror' => 'Terror', + 'emotion_panic' => 'Panic', + 'emotion_hysteria' => 'Hysteria', + 'emotion_mortification' => 'Mortification', + 'emotion_anxiety' => 'Anxiety', + 'emotion_nervousness' => 'Nervousness', + 'emotion_tenseness' => 'Tenseness', + 'emotion_uneasiness' => 'Uneasiness', + 'emotion_apprehension' => 'Apprehension', + 'emotion_worry' => 'Worry', + 'emotion_distress' => 'Distress', + 'emotion_dread' => 'Dread', + + // weather + 'weather_sunny' => 'Sunny', + 'weather_clear' => 'Clear', + 'weather_clear-day' => 'Clear', + 'weather_clear-night' => 'Clear night', + 'weather_light-drizzle' => 'Light drizzle', + 'weather_patchy-light-drizzle' => 'Patchy light drizzle', + 'weather_patchy-light-rain' => 'Patchy light rain', + 'weather_light-rain' => 'Light rain', + 'weather_moderate-rain-at-times' => 'Moderate rain at times', + 'weather_moderate-rain' => 'Moderate rain', + 'weather_patchy-rain-possible' => 'Patchy rain possible', + 'weather_heavy-rain-at-times' => 'Heavy rain at times', + 'weather_heavy-rain' => 'Heavy rain', + 'weather_light-freezing-rain' => 'Light freezing rain', + 'weather_moderate-or-heavy-freezing-rain' => 'Moderate or heavy freezing rain', + 'weather_light-sleet' => 'Light sleet', + 'weather_moderate-or-heavy-rain-shower' => 'Moderate or heavy rain shower', + 'weather_light-rain-shower' => 'Light rain shower', + 'weather_torrential-rain-shower' => 'Torrential rain shower', + 'weather_rain' => 'Rain', + 'weather_snow' => 'Snow', + 'weather_blowing-snow' => 'Blowing snow', + 'weather_patchy-light-snow' => 'Patchy light snow', + 'weather_light-snow' => 'Light snow', + 'weather_patchy-moderate-snow' => 'Patchy moderate snow', + 'weather_moderate-snow' => 'Moderate snow', + 'weather_patchy-heavy-snow' => 'Patchy heavy snow', + 'weather_heavy-snow' => 'Heavy snow', + 'weather_light-snow-showers' => 'Light snow showers', + 'weather_moderate-or-heavy-snow-showers' => 'Moderate or heavy snow showers', + 'weather_patchy-snow-possible' => 'Patchy snow possible', + 'weather_patchy-sleet-possible' => 'Patchy sleet possible', + 'weather_moderate-or-heavy-sleet' => 'Moderate or heavy sleet', + 'weather_light-sleet-showers' => 'Light sleet showers', + 'weather_moderate-or-heavy-sleet-showers' => 'Moderate or heavy sleet showers', + 'weather_sleet' => 'Sleet', + 'weather_wind' => 'Wind', + 'weather_fog' => 'Fog', + 'weather_freezing-fog' => 'Freezing fog', + 'weather_mist' => 'Mist', + 'weather_blizzard' => 'Blizzard', + 'weather_overcast' => 'Overcast', + 'weather_cloudy' => 'Cloudy', + 'weather_partly-cloudy-day' => 'Partly cloudy', + 'weather_partly-cloudy-night' => 'Partly cloudy', + 'weather_freezing-drizzle' => 'Freezing drizzle', + 'weather_heavy-freezing-drizzle' => 'Heavy freezing drizzle', + 'weather_patchy-freezing-drizzle-possible' => 'Patchy freezing drizzle possible', + 'weather_ice-pellets' => 'Ice pellets', + 'weather_light-showers-of-ice-pellets' => 'Light showers of ice pellets', + 'weather_moderate-or-heavy-showers-of-ice-pellets' => 'Moderate or heavy showers of ice pellets', + 'weather_thundery-outbreaks-possible' => 'Thundery outbreaks possible', + 'weather_patchy-light-rain-with-thunder' => 'Patchy light rain with thunder', + 'weather_moderate-or-heavy-rain-with-thunder' => 'Moderate or heavy rain with thunder', + 'weather_patchy-light-snow-with-thunder' => 'Patchy light snow with thunder', + 'weather_moderate-or-heavy-snow-with-thunder' => 'Moderate or heavy snow with thunder', + 'weather_current_temperature_celsius' => ':temperature °C', + 'weather_current_temperature_fahrenheit' => ':temperature °F', + 'weather_current_title' => 'Current weather', + + // dav + 'dav_contacts' => 'Contacts', + 'dav_contacts_description' => ':name’s contacts', + 'dav_birthdays' => 'Birthdays', + 'dav_birthdays_description' => ':name’s contact’s birthdays', + 'dav_tasks' => 'Tasks', + 'dav_tasks_description' => ':name’s tasks', + + // contact list + 'contact_list_avatar' => 'Avatar', + 'contact_list_name' => 'Contact', + 'contact_list_description' => 'Description', + +]; diff --git a/resources/lang/hr/auth.php b/resources/lang/hr/auth.php new file mode 100644 index 0000000..c99296f --- /dev/null +++ b/resources/lang/hr/auth.php @@ -0,0 +1,89 @@ + 'Ovi podaci ne odgovaraju našima.', + 'throttle' => 'Previše pokušaja prijave. Molim Vas pokušajte ponovno za :seconds sekundi.', + 'not_authorized' => 'You are not authorized to execute this action', + 'signup_disabled' => 'Registration is currently disabled', + 'signup_error' => 'An error occured trying to register the user', + 'back_homepage' => 'Back to homepage', + 'mfa_auth_otp' => 'Authenticate with your two factor device', + 'mfa_auth_webauthn' => 'Authenticate with a security key (WebAuthn)', + '2fa_title' => 'Two Factor Authentication', + '2fa_wrong_validation' => 'The two factor authentication has failed.', + '2fa_one_time_password' => 'Two factor authentication code', + '2fa_recuperation_code' => 'Enter a two factor recovery code', + '2fa_one_time_or_recuperation' => 'Enter a two factor authentication code or a recovery code', + '2fa_otp_help' => 'Open up your two factor authentication mobile app and copy the code', + + 'login_to_account' => 'Login to your account', + 'login_with_recovery' => 'Login with a recovery code', + 'login_again' => 'Please login again to your account', + 'email' => 'Email', + 'password' => 'Password', + 'recovery' => 'Recovery code', + 'login' => 'Login', + 'button_remember' => 'Remember Me', + 'password_forget' => 'Forget your password?', + 'password_reset' => 'Reset your password', + 'use_recovery' => 'Or you can use a recovery code', + 'signup_no_account' => 'Don’t have an account?', + 'signup' => 'Sign up', + 'create_account' => 'Create the first account by signing up', + 'change_language_title' => 'Change language:', + 'change_language' => 'Change language to :lang', + + 'password_reset_title' => 'Reset Password', + 'password_reset_email' => 'E-Mail Address', + 'password_reset_send_link' => 'Send Password Reset Link', + 'password_reset_password' => 'Password', + 'password_reset_password_confirm' => 'Confirm Password', + 'password_reset_action' => 'Reset Password', + 'password_reset_email_content' => 'Click here to reset your password:', + + 'register_title_welcome' => 'Welcome to your newly installed Monica instance', + 'register_create_account' => 'You need to create an account to use Monica', + 'register_title_create' => 'Create your Monica account', + 'register_login' => 'Log in if you already have an account.', + 'register_email' => 'Enter a valid email address', + 'register_email_example' => 'you@home', + 'register_firstname' => 'First name', + 'register_firstname_example' => 'eg. John', + 'register_lastname' => 'Last name', + 'register_lastname_example' => 'eg. Doe', + 'register_password' => 'Password', + 'register_password_example' => 'Enter a secure password', + 'register_password_confirmation' => 'Password confirmation', + 'register_action' => 'Register', + 'register_policy' => 'Signing up signifies you’ve read and agree to our Privacy Policy and Terms of use.', + 'register_invitation_email' => 'For security purposes, please indicate the email of the person who’ve invited you to join this account. This information is provided in the invitation email.', + + 'confirmation_title' => 'Verify Your Email Address', + 'confirmation_fresh' => 'A fresh verification link has been sent to your email address.', + 'confirmation_check' => 'Before proceeding, please check your email for a verification link.', + 'confirmation_request_another' => 'If you did not receive the email click here to request another.', + + 'confirmation_again' => 'If you want to change your email address you can click here.', + 'email_change_current_email' => 'Current email address:', + 'email_change_title' => 'Change your email address', + 'email_change_new' => 'New email address', + 'email_changed' => 'Your email address has been changed. Check your mailbox to validate it.', +]; diff --git a/resources/lang/hr/changelog.php b/resources/lang/hr/changelog.php new file mode 100644 index 0000000..981b018 --- /dev/null +++ b/resources/lang/hr/changelog.php @@ -0,0 +1,12 @@ + 'Product changes', + 'note' => 'Note: unfortunately, this page is only in English.', +]; diff --git a/resources/lang/hr/dashboard.php b/resources/lang/hr/dashboard.php new file mode 100644 index 0000000..5190352 --- /dev/null +++ b/resources/lang/hr/dashboard.php @@ -0,0 +1,42 @@ + 'Welcome to your account!', + 'dashboard_blank_description' => 'Monica is the place to organize all the interactions you have with the people you care about.', + 'dashboard_blank_cta' => 'Add your first contact', + 'dashboard_blank_illustration' => 'Illustration by Freepik', + + 'notes_title' => 'You don’t have any starred notes yet.', + + 'tab_recent_calls' => 'Recent calls', + 'tab_favorite_notes' => 'Favorite notes', + 'tab_calls_blank' => 'You haven’t logged any calls yet.', + 'tab_debts' => 'Debts', + 'tab_debts_blank' => 'You haven’t logged any debts yet.', + 'tab_tasks' => 'Tasks', + 'tab_tasks_blank' => 'You haven’t any tasks yet.', + + 'tasks_add_task_placeholder' => 'What is this task about?', + 'tasks_tab_your_contacts' => 'Tasks related to your contacts', + 'tasks_tab_your_tasks' => 'Your tasks', + 'tasks_add_note' => 'Press Enter to add the task.', + 'task_add_cta' => 'Add a task', + + 'debts_you_owe' => 'You owe', + + 'statistics_contacts' => 'Contacts', + 'statistics_activities' => 'Activities', + 'statistics_gifts' => 'Gifts', + + 'reminders_next_months' => 'Events in the next 3 months', + 'reminders_none' => 'No reminders for this month.', + + 'product_changes' => 'Product changes', + 'product_view_details' => 'View details', +]; diff --git a/resources/lang/hr/format.php b/resources/lang/hr/format.php new file mode 100644 index 0000000..a70a6ba --- /dev/null +++ b/resources/lang/hr/format.php @@ -0,0 +1,36 @@ + 'M d, Y H:i', + 'short_date_year' => 'M d, Y', + 'short_date' => 'M d', + 'short_month' => 'M', + 'short_month_year' => 'M Y', + 'short_day' => 'D', + 'full_date_year' => 'F d, Y', + 'full_month' => 'F', + 'full_month_year' => 'F Y', + 'full_hour' => 'h.i A', + + 'short_text' => '{text}…', +]; diff --git a/resources/lang/hr/journal.php b/resources/lang/hr/journal.php new file mode 100644 index 0000000..9b1f0be --- /dev/null +++ b/resources/lang/hr/journal.php @@ -0,0 +1,38 @@ + 'How was your day? You can rate it once a day.', + 'journal_come_back' => 'Thanks. Come back tomorrow to rate your day again.', + 'journal_description' => 'Note: the journal lists both manual journal entries, and automatic entries like Activities done with your contacts. While you can delete journal entries manually, you’ll have to delete the activity directly on the contact page.', + 'journal_add' => 'Add a journal entry', + 'journal_edit' => 'Edit a journal entry', + 'journal_empty' => 'Empty journal', + 'journal_created_at' => 'Created at {date}', + 'journal_created_automatically' => 'Created automatically', + 'journal_entry_type_journal' => 'Journal entry', + 'journal_entry_type_activity' => 'Activity', + 'journal_entry_rate' => 'You rated your day.', + 'journal_add_comment' => 'Care to add a comment (optional)?', + 'journal_show_comment' => 'Show comment', + 'entry_delete_success' => 'The journal entry has been successfully deleted.', + 'journal_add_title' => 'Title (optional)', + 'journal_add_date' => 'Date', + 'journal_add_post' => 'Entry', + 'journal_add_cta' => 'Save', + 'journal_blank_cta' => 'Add your first journal entry', + 'journal_blank_description' => 'The journal lets you write events that happened to you, and remember them.', + 'delete_confirmation' => 'Are you sure you want to delete this journal entry?', + 'apply_filter' => 'Apply filter', + 'start_date' => 'Start Date', + 'end_date' => 'End Date', + 'per_page' => 'Per Page', + 'sort_order' => 'Sort By Created At', + 'ascending' => 'Ascending', + 'descending' => 'Descending', +]; diff --git a/resources/lang/hr/logs.php b/resources/lang/hr/logs.php new file mode 100644 index 0000000..7b6654b --- /dev/null +++ b/resources/lang/hr/logs.php @@ -0,0 +1,29 @@ + 'Created the contact.', + 'settings_log_contact_created_with_name' => 'Added :name as a contact.', + + // contat description update + 'contact_log_contact_description_updated' => 'Updated the description.', + 'settings_log_contact_description_updated_with_name' => 'Updated the description of :name.', + + // contact description clear + 'contact_log_contact_description_cleared' => 'Cleared the description.', + 'settings_log_contact_description_cleared_with_name' => 'Cleared the description of :name.', + + // contact work information update + 'contact_log_contact_work_updated' => 'Updated work information.', + 'settings_log_contact_work_updated_with_name' => 'Updated work information of :name.', + + // company created + 'settings_log_company_created' => 'Created a company called :name.', +]; diff --git a/resources/lang/hr/mail.php b/resources/lang/hr/mail.php new file mode 100644 index 0000000..749f3d1 --- /dev/null +++ b/resources/lang/hr/mail.php @@ -0,0 +1,53 @@ + 'Reminder for :contact', + 'greetings' => 'Hi :username', + 'want_reminded_of' => 'You wanted to be reminded of :reason', + 'for' => 'For: :name', + 'comment' => 'Comment: :comment', + 'footer_contact_info' => 'Add, view, complete, and change information about this contact:', + 'footer_contact_info2' => 'See :name’s profile', + 'footer_contact_info2_link' => 'See :name’s profile: :url', + + 'notification_subject_line' => 'You have an upcoming event', + 'notification_description' => 'In :count days (on :date), the following event will happen:', + + 'stay_in_touch_subject_line' => 'Stay in touch with :name', + 'stay_in_touch_subject_description' => 'You asked to be reminded to stay in touch with :name every :frequency day.|You asked to be reminded to stay in touch with :name every :frequency days.', + + 'notifications_whoops' => 'Whoops!', + 'notifications_hello' => 'Hello!', + 'notifications_regards' => 'Regards', + 'notifications_footer' => 'If you’re having trouble clicking the ":actionText" button, copy and paste the URL below into your web browser: [:actionURL](:actionURL)', + 'notifications_rights' => 'All rights reserved', + + 'confirmation_email_title' => 'Monica – Email verification', + 'confirmation_email_intro'=> 'To validate your email click on the button below', + 'confirmation_email_button' => 'Verify email address', + 'confirmation_email_bottom' => 'If you did not create an account, no further action is required.', + + 'password_reset_title' => 'Monica – Reset Password Notification', + 'password_reset_intro' => 'You are receiving this email because we received a password reset request for your account.', + 'password_reset_button' => 'Reset Password', + 'password_reset_expiration' => 'This password reset link will expire in :count minutes.', + 'password_reset_bottom' => 'If you did not request a password reset, no further action is required.', + + 'invitation_title' => 'Monica – You are invited by :name', + 'invitation_intro' => 'You’ve been invited by :name (:email) to use Monica, a nice Personal Relationship Management tool.', + 'invitation_link' => 'To accept the invitation, click on the link below:', + 'invitation_button' => 'Accept invitation', + 'invitation_expiration' => 'This link will expire in :count days.', + + 'export_title' => 'Your export is ready', + 'export_description' => 'You requested a data export on :date. It is now ready to download.', + 'export_download' => 'Download export', + +]; diff --git a/resources/lang/hr/pagination.php b/resources/lang/hr/pagination.php new file mode 100644 index 0000000..f9a31ac --- /dev/null +++ b/resources/lang/hr/pagination.php @@ -0,0 +1,25 @@ + '❮ Prethodna', + 'next' => 'Sljedeća ❯', + +]; diff --git a/resources/lang/hr/passwords.php b/resources/lang/hr/passwords.php new file mode 100644 index 0000000..4f93817 --- /dev/null +++ b/resources/lang/hr/passwords.php @@ -0,0 +1,30 @@ + 'Lozinka je postavljena!', + 'sent' => 'Poveznica za ponovono postavljanje lozinke je poslana!', + 'token' => 'Oznaka za ponovno postavljanje lozinke više nije važeća.', + 'user' => 'Korisnik nije pronađen.', + 'changed' => 'Password changed successfully.', + 'invalid' => 'Lozinka koju ste unijeli nije točna.', + 'throttled' => 'Please wait before retrying.', + +]; diff --git a/resources/lang/hr/people.php b/resources/lang/hr/people.php new file mode 100644 index 0000000..5f4a12f --- /dev/null +++ b/resources/lang/hr/people.php @@ -0,0 +1,539 @@ + 'Contact not found', + 'people_list_number_kids' => ':count child|:count children', + 'people_list_last_updated' => 'Posljednji ažurirani:', + 'people_list_number_reminders' => ':count reminder|:count reminders', + 'people_list_blank_title' => 'Još nemate unesenih kontakata', + 'people_list_blank_cta' => 'Novi kontakt', + 'people_list_sort' => 'Sortiraj', + 'people_list_stats' => ':count contact|:count contacts', + 'people_list_firstnameAZ' => 'Sortiraj po imenu A → Z', + 'people_list_firstnameZA' => 'Sortiraj po imenu Z → A', + 'people_list_lastnameAZ' => 'Sortiraj po prezimenu A → Z', + 'people_list_lastnameZA' => 'Sortiraj po prezimenu Z → A', + 'people_list_lastactivitydateNewtoOld' => 'Sort by last activity date, newest to oldest', + 'people_list_lastactivitydateOldtoNew' => 'Sort by last activity date, oldest to newest', + 'people_list_filter_tag' => 'Prikazuju se svi kontakti označeni sa', + 'people_list_clear_filter' => 'Očisti filter', + 'people_list_contacts_per_tags' => ':count contact|:count contacts', + 'people_list_show_dead' => 'Pokaži umrle osobe (:count)', + 'people_list_hide_dead' => 'Sakrij umrle osobe (:count)', + 'people_search' => 'Search your contacts…', + 'people_search_no_results' => 'No results found', + 'people_search_next' => 'Next', + 'people_search_prev' => 'Previous', + 'people_search_rows_per_page' => 'Rows per page', + 'people_search_of' => 'of', + 'people_search_page' => 'Page', + 'people_search_all' => 'All', + 'people_add_new' => 'Add new person', + 'people_list_account_usage' => 'Your account usage: :current/:limit contacts', + 'people_list_account_upgrade_title' => 'Upgrade your account to unlock it to its full potential.', + 'people_list_account_upgrade_cta' => 'Nadogradi sada', + 'people_list_untagged' => 'View untagged contacts', + 'people_list_filter_untag' => 'Showing all untagged contacts', + 'archived_contact_readonly' => 'Archived contact can’t be edited, please unarchive it first.', + + // people add + 'people_add_title' => 'Dodajte novu osobu', + 'people_add_missing' => 'No person found – add a new one now', + 'people_add_firstname' => 'Ime', + 'people_add_middlename' => 'Middle name (optional)', + 'people_add_lastname' => 'Last name (optional)', + 'people_add_email' => 'Email (optional)', + 'people_add_nickname' => 'Nickname (optional)', + 'people_add_cta' => 'Dodaj', + 'people_save_and_add_another_cta' => 'Unesi pa dodaj drugu osobu', + 'people_add_success' => 'Kontakt :name je uspješno unesen', + 'people_add_gender' => 'Spol', + 'people_delete_success' => 'Kontakt je obrisan', + 'people_delete_message' => 'Delete contact', + 'people_delete_confirmation' => 'Are you sure you want to delete :name’s contact? Deletion is immediate and permanent.', + 'people_add_birthday_reminder' => 'Zaželi sretan rođendan :name', + 'people_add_birthday_reminder_deceased' => 'On this date, :name would have celebrated their birthday', + 'people_add_import' => 'Želite li uvesti svoje kontakte?', + 'people_edit_email_error' => 'Već postoji kontakt s ovom email adresom. Molimo unesite drugu.', + 'people_export' => 'Izvezi kao vCard', + 'people_add_reminder_for_birthday' => 'Create an annual birthday reminder', + + // show + 'section_contact_information' => 'Informacije o kontaktu', + 'section_personal_activities' => 'Aktivnosti', + 'section_personal_reminders' => 'Podsjetnici', + 'section_personal_tasks' => 'Zadaci', + 'section_personal_gifts' => 'Pokloni', + 'section_personal_notes' => 'Bilješke', + + // archived contacts + 'list_link_to_active_contacts' => 'You are viewing archived contacts. See the list of active contacts instead.', + 'list_link_to_archived_contacts' => 'List of archived contacts', + + // Header + 'me' => 'This is you', + 'edit_contact_information' => 'Uredi kontakt', + 'contact_archive' => 'Archive contact', + 'contact_unarchive' => 'Unarchive contact', + 'contact_archive_help' => 'Archived contacts are not be shown on the contact list, but still appear in search results.', + 'call_button' => 'Zabilježi poziv', + 'set_favorite' => 'Omiljeni kontakti smješteni su na vrhu popisa', + + // Stay in touch + 'stay_in_touch' => 'Ostani u kontaktu', + 'stay_in_touch_frequency' => 'Ostanite u kontaktu svaki dan|Ostanite u kontaktu svaka/ih {count} dana', + 'stay_in_touch_next_date' => 'Next due: {date}', + 'stay_in_touch_invalid' => 'Učestalost treba biti broj veći od 0.', + 'stay_in_touch_premium' => 'Potrebno je nadograditi račun za korištenje ove opcije', + 'stay_in_touch_modal_title' => 'Ostani u kontaktu', + 'stay_in_touch_modal_desc' => 'Možemo vas mailom podsjetiti da radovito ostenete u kontaktu sa {firstname}.', + 'stay_in_touch_modal_label' => 'Send me an email every… {count} day|Send me an email every… {count} days', + + // Calls + 'modal_call_title' => 'Zabilježi poziv', + 'modal_call_comment' => 'O čemu ste razgovarali? (opcionalno)', + 'modal_call_exact_date' => 'Poziv se dogodio', + 'modal_call_who_called' => 'Who called?', + 'modal_call_emotion' => 'Do you want to log how you felt during this call? (optional)', + 'calls_add_success' => 'Poziv je uspješno unesen.', + 'call_delete_confirmation' => 'Jeste li sigurni da želite izbrisati ovaj poziv?', + 'call_delete_success' => 'Poziv je uspješno obrisan', + 'call_title' => 'Telefonski pozivi', + 'call_empty_comment' => 'Nema pojedinosti', + 'call_blank_title' => 'Keep track of the phone calls you’ve done with {name}', + 'call_blank_desc' => 'You called {name}', + 'call_you_called' => 'You called', + 'call_he_called' => '{name} called', + 'call_emotions' => 'Emotions:', + + // Conversation + 'conversation_blank' => 'Record conversations you have with :name on social media, SMS…', + 'conversation_delete_link' => 'Izbriši razgovor', + 'conversation_edit_title' => 'Uredi razgovor', + 'conversation_edit_delete' => 'Jeste li sigurni da želite izbrisati razgovor? Brisanje je trajno.', + 'conversation_add_success' => 'The conversation has been successfully added.', + 'conversation_edit_success' => 'The conversation has been successfully updated.', + 'conversation_delete_success' => 'Razgovor je uspješno izbrisan.', + 'conversation_add_title' => 'Unesi novi razgovor', + 'conversation_add_when' => 'Kada ste imali ovaj razgovor?', + 'conversation_add_who_wrote' => 'Who sent this message?', + 'conversation_add_how' => 'Kako ste komunicirali?', + 'conversation_add_you' => 'Vi', + 'conversation_add_content' => 'Zapišite ono što je rečeno', + 'conversation_add_what_was_said' => 'Što ste vi rekli?', + 'conversation_add_another' => 'Dodajte još jednu poruku', + 'conversation_add_error' => 'You must add at least one message.', + 'conversation_list_table_messages' => 'Poruke', + 'conversation_list_table_content' => 'Djelomični sadržaj (zadnja poruka)', + 'conversation_list_title' => 'Razgovori', + 'conversation_list_cta' => 'Zabilježi razgovor', + + // age - birthday + 'birthdate_not_set' => 'Birthday is not set', + 'age_approximate_in_years' => 'oko :age godina', + 'age_exact_in_years' => ':age years old', + 'age_exact_birthdate' => 'rođen/a :date', + + // Last called + 'last_called' => 'Last called: :date', + 'last_talked_to' => 'Last called: {date}', + 'last_called_empty' => 'Last called: unknown', + 'last_activity_date' => 'Last activity together: :date', + 'last_activity_date_empty' => 'Last activity together: unknown', + + // additional information + 'information_edit_success' => 'The profile has been updated successfully', + 'information_edit_title' => 'Edit :name’s personal information', + 'information_edit_max_size' => 'Max :size Kb.', + 'information_edit_max_size2' => 'Max {size} Kb.', + 'information_edit_firstname' => 'Ime', + 'information_edit_lastname' => 'Last name (optional)', + 'information_edit_description' => 'Description (optional)', + 'information_edit_description_help' => 'Used on the contact list to add some context, if necessary.', + 'information_edit_unknown' => 'Ne znam starost ove osobe', + 'information_edit_probably' => 'This person is probably…', + 'information_edit_not_year' => 'I know the day and month of this person’s birthday, but not the year…', + 'information_edit_exact' => 'I know this person’s exact birthday…', + 'information_edit_birthdate_label' => 'Birthday', + 'information_no_work_defined' => 'No work information defined', + 'information_work_at' => 'at :company', + 'work_add_cta' => 'Update work information', + 'work_edit_success' => 'Work information updated', + 'work_edit_title' => 'Update :name’s job information', + 'work_edit_job' => 'Job title (optional)', + 'work_edit_company' => 'Company (optional)', + 'work_information' => 'Work information', + + // food preferences + 'food_preferences_add_success' => 'Food preferences have been saved', + 'food_preferences_edit_description' => 'Perhaps :firstname or someone in the :family’s family has an allergy. Or doesn’t like a specific bottle of wine. Indicate them here so you will remember it next time you invite them for dinner', + 'food_preferences_edit_description_no_last_name' => 'Perhaps :firstname has an allergy. Or doesn’t like a specific bottle of wine. Indicate them here so you will remember it next time you invite them for dinner', + 'food_preferences_edit_title' => 'Indicate food preferences', + 'food_preferences_edit_cta' => 'Save food preferences', + 'food_preferences_title' => 'Food preferences', + 'food_preferences_cta' => 'Add food preferences', + + // reminders + 'reminders_blank_title' => 'Is there something you want to be reminded of about :name?', + 'reminders_blank_add_activity' => 'Dodaj podsjetnik', + 'reminders_add_title' => 'Što želiš zapamtiti za :name?', + 'reminders_add_description' => 'Please remind me to…', + 'reminders_add_next_time' => 'Kada sljedeći put želite dobiti podsjetnik?', + 'reminders_add_once' => 'Remind me about this just once', + 'reminders_add_recurrent' => 'Remind me about this every', + 'reminders_add_starting_from' => 'starting from the date specified above', + 'reminders_add_cta' => 'Add reminder', + 'reminders_edit_update_cta' => 'Update reminder', + 'reminders_add_error_custom_text' => 'You need to indicate a text for this reminder', + 'reminders_create_success' => 'The reminder has been added successfully', + 'reminders_delete_success' => 'The reminder has been deleted successfully', + 'reminders_update_success' => 'The reminder has been updated successfully', + 'reminders_add_optional_comment' => 'Optional comment', + + 'reminder_frequency_day' => 'every day|every :number days', + 'reminder_frequency_week' => 'every week|every :number weeks', + 'reminder_frequency_month' => 'every month|every :number months', + 'reminder_frequency_year' => 'every year|every :number year', + 'reminder_frequency_one_time' => 'on :date', + 'reminders_delete_confirmation' => 'Are you sure you want to delete this reminder?', + 'reminders_delete_cta' => 'Delete', + 'reminders_next_expected_date' => 'on', + 'reminders_cta' => 'Add a reminder', + 'reminders_description' => 'We will send an email for each one of the reminders below. Reminders are sent every morning the day events will happen. Reminders automatically added for birthdays can not be deleted. If you want to change those dates, edit the birthday of the contacts.', + 'reminders_one_time' => 'One time', + 'reminders_type_week' => 'tjedan', + 'reminders_type_month' => 'mjesec', + 'reminders_type_year' => 'godina', + 'reminders_birthday' => 'Rođendan :name', + 'reminders_free_plan_warning' => 'You are on the Free plan. No emails are sent on this plan. To receive your reminders by email, upgrade your account.', + + // relationships + 'relationship_form_add' => 'Dodaj novi odnos', + 'relationship_form_edit' => 'Uredi postojeći odnos', + 'relationship_form_is_with' => 'This person is…', + 'relationship_form_is_with_name' => ':name is…', + 'relationship_form_add_choice' => 'Who is the relationship with?', + 'relationship_form_create_contact' => 'Dodajte novu osobu', + 'relationship_form_associate_contact' => 'Dodaj postojeći kontakt', + 'relationship_form_associate_dropdown' => 'Pretraži i odaberi postojeći kontakt iz padajućeg izbornika', + 'relationship_form_associate_dropdown_placeholder' => 'Pretraži i odaberi postojeći kontakt', + 'relationship_form_also_create_contact' => 'Create a Contact entry for this person.', + 'relationship_form_add_description' => 'This will let you treat this person like any other contact.', + 'relationship_form_add_no_existing_contact' => 'You don’t have any contacts who can be related to :name at the moment.', + 'relationship_delete_confirmation' => 'Are you sure you want to delete this relationship? Deletion is permanent.', + 'relationship_unlink_confirmation' => 'Are you sure you want to delete this relationship? This person will not be deleted – only the relationship between the two.', + 'relationship_form_add_success' => 'The relationship has been successfully set.', + 'relationship_form_deletion_success' => 'The relationship has been deleted.', + + // tasks + 'tasks_title' => 'Tasks', + 'tasks_blank_title' => 'You don’t have any tasks yet.', + 'tasks_form_title' => 'Title', + 'tasks_form_description' => 'Description (optional)', + 'tasks_add_task' => 'Add a task', + 'tasks_delete_success' => 'The task has been deleted successfully', + 'tasks_complete_success' => 'The task has changed status successfully', + + // activities + 'activity_title' => 'Activities', + 'activity_type_category_simple_activities' => 'Simple activities', + 'activity_type_category_sport' => 'Sport', + 'activity_type_category_food' => 'hrana', + 'activity_type_category_cultural_activities' => 'Kultura', + 'activity_type_just_hung_out' => 'druženje', + 'activity_type_watched_movie_at_home' => 'gledali film doma', + 'activity_type_talked_at_home' => 'razgovarali doma', + 'activity_type_did_sport_activities_together' => 'played a sport together', + 'activity_type_ate_at_his_place' => 'jeli u gostima', + 'activity_type_went_bar' => 'bili u kafiću', + 'activity_type_ate_at_home' => 'jeli doma', + 'activity_type_picnicked' => 'picnicked', + 'activity_type_ate_restaurant' => 'ate at a restaurant', + 'activity_type_went_theater' => 'went to the theater', + 'activity_type_went_concert' => 'went to a concert', + 'activity_type_went_play' => 'went to a play', + 'activity_type_went_museum' => 'went to the museum', + 'activities_add_activity' => 'Add activity', + 'activities_add_more_details' => 'Add more details', + 'activities_add_emotions' => 'Add emotions', + 'activities_add_category' => 'Indicate a category', + 'activities_add_participants_cta' => 'Add participants', + 'activities_item_information' => ':Activity. Happened on :date', + 'activities_add_title' => 'What did you do with {name}?', + 'activities_summary' => 'Describe what you did', + 'activities_add_pick_activity' => 'Would you like to categorize this activity? You don’t have to, but it will give you statistics later on (optional)', + 'activities_add_date_occured' => 'The activity happened on…', + 'activities_add_participants' => 'Who, apart from {name}, participated in this activity? (optional)', + 'activities_add_emotions_title' => 'Do you want to log how you felt during this activity? (optional)', + 'activities_blank_title' => 'Keep track of what you’ve done with {name} in the past, and what you’ve talked about', + 'activities_blank_add_activity' => 'Add an activity', + 'activities_add_success' => 'The activity has been added successfully', + 'activities_add_error' => 'Error when adding the activity', + 'activities_update_success' => 'Uspješno ažuriranje aktivnosti', + 'activities_delete_success' => 'Uspješno brisanje aktivnosti', + 'activities_who_was_involved' => 'Tko je sudjelovao?', + 'activities_activity' => 'Kategorija aktivnosti', + 'activities_view_activities_report' => 'Izvještaj aktivnosti', + 'activities_profile_title' => 'Izvještaj aktivnosti između tebe i osobe: :name ', + 'activities_profile_subtitle' => 'You’ve logged :total_activities activity with :name in total and :activities_last_twelve_months in the last 12 months so far.|You’ve logged :total_activities activities with :name in total and :activities_last_twelve_months in the last 12 months so far.', + 'activities_profile_year_summary_activity_types' => 'Here is a breakdown of the type of activities you’ve done together in :year', + 'activities_profile_year_summary' => 'Pregled aktivnosti u godini :year', + 'activities_profile_number_occurences' => ':value aktivnost|:value aktivnosti', + 'activities_list_participants' => 'Participants ({total}):', + 'activities_list_emotions' => 'Emotions felt:', + 'activities_list_date' => 'Happened on', + 'activities_list_category' => 'Category:', + + // notes + 'notes_create_success' => 'Uspješno dodavanje bilješke', + 'notes_update_success' => 'Uspješno spremanje bilješke', + 'notes_delete_success' => 'Bilješka je uspješno obrisana', + 'notes_add_cta' => 'Dodaj bilješku', + 'notes_favorite' => 'Dodaj/ukloni iz favorita', + 'notes_delete_title' => 'Izbriši bilješku', + 'notes_delete_confirmation' => 'Jeste li sigurni da želite izbrisati ovu bilješku? Brisanje je trajno', + + // gifts + 'gifts_title' => 'Pokloni', + 'gifts_add_success' => 'Uspješno dodavanje poklona', + 'gifts_delete_success' => 'Uspješno brisanje poklona', + 'gifts_delete_confirmation' => 'Jeste li sigurni da želite izbrisati ovaj poklon?', + 'gifts_add_gift' => 'Dodaj poklon', + 'gifts_link' => 'Poveznica', + 'gifts_for' => 'For: {name}', + 'gifts_delete_cta' => 'Obriši', + 'gifts_add_title' => 'Uređivanje poklona za osobu: :name', + 'gifts_add_gift_idea' => 'Ideja za poklon', + 'gifts_add_gift_already_offered' => 'Već poklonjeno', + 'gifts_add_gift_received' => 'Primljen poklon', + 'gifts_add_gift_title' => 'Što je bio ovaj poklon?', + 'gifts_add_gift_name' => 'Gift name', + 'gifts_add_link' => 'Poveznica na web stranicu (opcionalno)', + 'gifts_add_value' => 'Vrijednost (opcionalno)', + 'gifts_add_comment' => 'Komentar (opcionalno)', + 'gifts_add_recipient' => 'Recipient (optional)', + 'gifts_add_recipient_field' => 'Recipient', + 'gifts_add_photo' => 'Photo (optional)', + 'gifts_add_photo_title' => 'Add a photo for this gift', + 'gifts_add_someone' => 'This gift is for someone in {name}’s family in particular', + 'gifts_delete_title' => 'Delete a gift', + 'gifts_ideas' => 'Ideje za poklone', + 'gifts_offered' => 'Već poklonjeno', + 'gifts_offered_as_an_idea' => 'Označi kao ideju', + 'gifts_received' => 'Primljeni pokloni', + 'gifts_view_comment' => 'Pogledaj komentar', + 'gifts_mark_offered' => 'Označi kao poklonjeno', + 'gifts_update_success' => 'Uspješno ažuriranje poklona', + 'gifts_add_date' => 'Date (optional)', + + // debts + 'debt_delete_confirmation' => 'Jeste li sigurni da želite izbrisati ovaj dug?', + 'debt_delete_success' => 'Uspješno brisanje duga', + 'debt_add_success' => 'Uspješno dodavanje duga', + 'debt_title' => 'Dugovi', + 'debt_add_cta' => 'Dodaj dug', + 'debt_you_owe' => 'Duguješ :amount', + 'debt_they_owe' => ':name duguje tebi :amount', + 'debt_add_title' => 'Uređivanje dugova za osobu: :name', + 'debt_add_you_owe' => 'Ti duguješ osobi: :name', + 'debt_add_they_owe' => ':name owes you', + 'debt_add_amount' => 'the sum of', + 'debt_add_reason' => 'for the following reason (optional)', + 'debt_add_add_cta' => 'Add debt', + 'debt_edit_update_cta' => 'Update debt', + 'debt_edit_success' => 'The debt has been updated successfully', + 'debts_blank_title' => 'Manage debts you owe to :name or :name owes you', + + // tags + 'tag_edit' => 'Edit tag', + 'tag_add' => 'Add tags', + 'tag_add_search' => 'Add or search tags', + 'tag_no_tags' => 'No tags yet', + + // Introductions + 'introductions_sidebar_title' => 'How you met', + 'introductions_blank_cta' => 'Indicate how you met :name', + 'introductions_title_edit' => 'How did you meet :name?', + 'introductions_additional_info' => 'Explain how and where you met', + 'introductions_edit_met_through' => 'Has someone introduced you to this person?', + 'introductions_no_met_through' => 'No one', + 'introductions_first_met_date' => 'Date you met', + 'introductions_no_first_met_date' => 'I don’t know the date we met', + 'introductions_first_met_date_known' => 'This is the date we met', + 'introductions_add_reminder' => 'Add a reminder to celebrate this encounter on the anniversary this event happened', + 'introductions_update_success' => 'You’ve successfully updated the information about how you met this person', + 'introductions_met_through' => 'Met through :name', + 'introductions_met_date' => 'Met on :date', + 'introductions_reminder_title' => 'Anniversary of the day you first met', + + // Deceased + 'deceased_reminder_title' => 'Anniversary of the death of :name', + 'deceased_mark_person_deceased' => 'Mark this as deceased', + 'deceased_know_date' => 'I know the date that this person died', + 'deceased_add_reminder' => 'Add a reminder for this date', + 'deceased_label' => 'Deceased', + 'deceased_date_label' => 'Deceased date', + 'deceased_label_with_date' => 'Deceased on :date', + 'deceased_age' => 'Age at death', + + // Contact information + 'contact_info_title' => 'Contact information', + 'contact_info_form_content' => 'Content', + 'contact_info_form_contact_type' => 'Contact type', + 'contact_info_form_personalize' => 'Personalize', + 'contact_info_address' => 'Lives in', + + // Addresses + 'contact_address_title' => 'Addresses', + 'contact_address_form_name' => 'Label (optional)', + 'contact_address_form_street' => 'Street (optional)', + 'contact_address_form_city' => 'City (optional)', + 'contact_address_form_province' => 'Province (optional)', + 'contact_address_form_postal_code' => 'Postal code (optional)', + 'contact_address_form_country' => 'Country (optional)', + 'contact_address_form_latitude' => 'Latitude (numbers only) (optional)', + 'contact_address_form_longitude' => 'Longitude (numbers only) (optional)', + + // Pets + 'pets_kind' => 'Kind of pet', + 'pets_name' => 'Name (optional)', + 'pets_create_success' => 'The pet has been successfully added', + 'pets_update_success' => 'The pet has been updated', + 'pets_delete_success' => 'The pet has been deleted', + 'pets_title' => 'Pets', + 'pets_reptile' => 'Reptile', + 'pets_bird' => 'Bird', + 'pets_cat' => 'Cat', + 'pets_dog' => 'Dog', + 'pets_fish' => 'Fish', + 'pets_hamster' => 'Hamster', + 'pets_horse' => 'Horse', + 'pets_rabbit' => 'Rabbit', + 'pets_rat' => 'Rat', + 'pets_small_animal' => 'Small animal', + 'pets_other' => 'Other', + + // life events + 'life_event_list_tab_life_events' => 'Life events', + 'life_event_list_tab_other' => 'Notes, reminders, …', + 'life_event_list_title' => 'Life events', + 'life_event_blank' => 'Log what happens to the life of {name} for your future reference.', + 'life_event_list_cta' => 'Add life event', + 'life_event_create_category' => 'All categories', + 'life_event_create_life_event' => 'Add life event', + 'life_event_create_default_title' => 'Title (optional)', + 'life_event_create_default_story' => 'Story (optional)', + 'life_event_create_date' => 'You do not need to indicate a month or a day – only the year is mandatory.', + 'life_event_create_default_description' => 'Add information about what you know', + 'life_event_create_add_yearly_reminder' => 'Add a yearly reminder for this event', + 'life_event_create_success' => 'The life event has been added', + 'life_event_delete_title' => 'Delete a life event', + 'life_event_delete_description' => 'Are you sure you want to delete this life event? Deletion is permanent.', + 'life_event_delete_success' => 'The life event has been deleted', + 'life_event_date_it_happened' => 'Date it happened', + 'life_event_category_work_education' => 'Work & education', + 'life_event_category_family_relationships' => 'Family & relationships', + 'life_event_category_home_living' => 'Home & living', + 'life_event_category_health_wellness' => 'Health & wellness', + 'life_event_category_travel_experiences' => 'Travel & experiences', + 'life_event_sentence_new_job' => 'Started a new job', + 'life_event_sentence_retirement' => 'Retired', + 'life_event_sentence_new_school' => 'Started school', + 'life_event_sentence_study_abroad' => 'Studied abroad', + 'life_event_sentence_volunteer_work' => 'Started volunteering', + 'life_event_sentence_published_book_or_paper' => 'Published a paper', + 'life_event_sentence_military_service' => 'Started military service', + 'life_event_sentence_new_relationship' => 'Started a relationship', + 'life_event_sentence_engagement' => 'Got engaged', + 'life_event_sentence_marriage' => 'Got married', + 'life_event_sentence_anniversary' => 'Anniversary', + 'life_event_sentence_expecting_a_baby' => 'Expects a baby', + 'life_event_sentence_new_child' => 'Had a child', + 'life_event_sentence_new_family_member' => 'Added a family member', + 'life_event_sentence_new_pet' => 'Got a pet', + 'life_event_sentence_end_of_relationship' => 'Ended a relationship', + 'life_event_sentence_loss_of_a_loved_one' => 'Lost a loved one', + 'life_event_sentence_moved' => 'Moved', + 'life_event_sentence_bought_a_home' => 'Bought a home', + 'life_event_sentence_home_improvement' => 'Made a home improvement', + 'life_event_sentence_holidays' => 'Went on holidays', + 'life_event_sentence_new_vehicle' => 'Got a new vehicle', + 'life_event_sentence_new_roommate' => 'Got a roommate', + 'life_event_sentence_overcame_an_illness' => 'Overcame an illness', + 'life_event_sentence_quit_a_habit' => 'Quit a habit', + 'life_event_sentence_new_eating_habits' => 'Started new eating habits', + 'life_event_sentence_weight_loss' => 'Lost weight', + 'life_event_sentence_wear_glass_or_contact' => 'Started to wear glass or contact lenses', + 'life_event_sentence_broken_bone' => 'Broke a bone', + 'life_event_sentence_removed_braces' => 'Removed braces', + 'life_event_sentence_surgery' => 'Had surgery', + 'life_event_sentence_dentist' => 'Went to the dentist', + 'life_event_sentence_new_sport' => 'Started a sport', + 'life_event_sentence_new_hobby' => 'Started a hobby', + 'life_event_sentence_new_instrument' => 'Learned a new instrument', + 'life_event_sentence_new_language' => 'Learned a new language', + 'life_event_sentence_tattoo_or_piercing' => 'Got a tattoo or piercing', + 'life_event_sentence_new_license' => 'Got a license', + 'life_event_sentence_travel' => 'Traveled', + 'life_event_sentence_achievement_or_award' => 'Got an achievement or award', + 'life_event_sentence_changed_beliefs' => 'Changed beliefs', + 'life_event_sentence_first_word' => 'Spoke for the first time', + 'life_event_sentence_first_kiss' => 'Kissed for the first time', + + // documents + 'document_list_title' => 'Documents', + 'document_list_cta' => 'Upload document', + 'document_list_blank_desc' => 'Here you can store documents related to this person.', + 'document_upload_zone_cta' => 'Upload a file', + 'document_upload_zone_progress' => 'Uploading the document…', + 'document_upload_zone_error' => 'There was an error uploading the document. Please try again below.', + + // Photos + 'photo_title' => 'Photos', + 'photo_list_title' => 'Related photos', + 'photo_list_cta' => 'Upload photo', + 'photo_list_blank_desc' => 'You can store images about this contact. Upload one now!', + 'photo_upload_zone_cta' => 'Upload a photo', + 'photo_current_profile_pic' => 'Current profile picture', + 'photo_make_profile_pic' => 'Make profile picture', + 'photo_delete' => 'Delete photo', + 'photo_next' => 'Next photo ❯', + 'photo_previous' => '❮ Previous photo', + + // Avatars + 'avatar_change_title' => 'Change your avatar', + 'avatar_question' => 'Which avatar would you like to use?', + 'avatar_default_avatar' => 'The default avatar', + 'avatar_adorable_avatar' => 'The Adorable avatar', + 'avatar_gravatar' => 'The Gravatar associated with the email address of this person. Gravatar is a global system that lets users associate email addresses with photos.', + 'avatar_current' => 'Keep the current avatar', + 'avatar_photo' => 'From a photo that you upload', + 'avatar_crop_new_avatar_photo' => 'Crop new avatar photo', + + // emotions + 'emotion_this_made_me_feel' => 'This made you feel…', + + // logs + 'auditlogs_link' => 'History', + 'auditlogs_title' => 'Everything that happened to :name', + 'auditlogs_breadcrumb' => 'History', + 'auditlogs_author' => 'By :name on :date', + + // contact field label + 'contact_field_label_home' => 'Home', + 'contact_field_label_work' => 'Work', + 'contact_field_label_cell' => 'Mobile', + 'contact_field_label_fax' => 'Fax', + 'contact_field_label_pager' => 'Pager', + 'contact_field_label_main' => 'Main', + 'contact_field_label_other' => 'Other', + 'contact_field_label_personal' => 'Personal', +]; diff --git a/resources/lang/hr/reminder.php b/resources/lang/hr/reminder.php new file mode 100644 index 0000000..bcab17c --- /dev/null +++ b/resources/lang/hr/reminder.php @@ -0,0 +1,16 @@ + 'Wish happy birthday to', + 'type_phone_call' => 'Call', + 'type_lunch' => 'Lunch with', + 'type_hangout' => 'Hangout with', + 'type_email' => 'Email', + 'type_birthday_kid' => 'Wish happy birthday to the child of', +]; diff --git a/resources/lang/hr/settings.php b/resources/lang/hr/settings.php new file mode 100644 index 0000000..316cf93 --- /dev/null +++ b/resources/lang/hr/settings.php @@ -0,0 +1,557 @@ + 'Account settings', + 'sidebar_personalization' => 'Personalization', + 'sidebar_settings_storage' => 'Storage', + 'sidebar_settings_export' => 'Export data', + 'sidebar_settings_users' => 'Users', + 'sidebar_settings_subscriptions' => 'Subscription', + 'sidebar_settings_import' => 'Import data', + 'sidebar_settings_tags' => 'Tag management', + 'sidebar_settings_api' => 'API', + 'sidebar_settings_dav' => 'DAV Resources', + 'sidebar_settings_security' => 'Security', + 'sidebar_settings_auditlogs' => 'Audit logs', + + 'title_general' => 'General Information', + 'title_i18n' => 'International settings', + 'title_layout' => 'Layout', + + 'me_title' => 'Me as a contact', + 'me_help' => 'This is the contact that represents you in Monica', + 'me_select' => 'Select a contact', + 'me_no_contact' => 'No contact selected yet.', + 'me_select_click' => 'Click here to select a contact.', + 'me_remove_contact' => 'Remove the association', + 'me_choose' => 'Choose yourself', + 'me_choose_placeholder' => 'Choose yourself', + + 'export_title' => 'Export your account data', + 'export_be_patient' => 'Click the button to start the export. It might take several minutes to process the export – please be patient and do not spam the button.', + 'export_title_sql' => 'Export to SQL', + 'export_sql_explanation' => 'Exporting your data in SQL format allows you to take your data and import it to your own Monica instance. This is only valuable if you do have your own server.', + 'export_sql_cta' => 'Export to SQL', + 'export_sql_link_instructions' => 'Note: read the instructions to learn more about importing this file to your instance.', + 'export_title_json' => 'Export to Json', + 'export_submitted' => 'Your export has been submitted, it will be available in a few moment…', + 'export_json_explanation' => 'Exporting your data in Json format for backup.', + 'export_json_beta' => 'Json export is in preview mode. Tell us what you think about it:', + 'export_json_cta' => 'Export to Json', + 'export_header_type' => 'Type', + 'export_header_timestamp' => 'Creation date', + 'export_header_status' => 'Status', + 'export_header_actions' => 'Actions', + 'export_last_title' => 'Last exports', + 'export_empty_title' => 'No exports yet', + 'export_type_json' => 'Json export', + 'export_type_sql' => 'SQL export', + 'export_status_todo' => 'Submitted', + 'export_status_doing' => 'Doing', + 'export_status_done' => 'Done', + 'export_status_failed' => 'Failed', + 'export_not_done' => 'Download impossible, this export is not done yet.', + + 'firstname' => 'First name', + 'lastname' => 'Last name', + 'name_order' => 'Name order', + 'name_order_firstname_lastname' => ' – John Doe', + 'name_order_lastname_firstname' => ' – Doe John', + 'name_order_firstname_lastname_nickname' => ' () – John Doe (Rambo)', + 'name_order_firstname_nickname_lastname' => ' () – John (Rambo) Doe', + 'name_order_lastname_firstname_nickname' => ' () – Doe John (Rambo)', + 'name_order_lastname_nickname_firstname' => ' () – Doe (Rambo) John', + 'name_order_nickname_firstname_lastname' => ' ( ) – Rambo (John Doe)', + 'name_order_nickname_lastname_firstname' => ' ( ) – Rambo (Doe John)', + 'name_order_nickname' => ' – Rambo', + 'currency' => 'Currency', + 'name' => 'Your name: :name', + 'email' => 'Email address', + 'email_placeholder' => 'Enter email', + 'email_help' => 'This is the email used to login, and this is where Monica will send your reminders.', + 'timezone' => 'Timezone', + 'temperature_scale' => 'Temperature scale', + 'temperature_scale_fahrenheit' => 'Fahrenheit (°F)', + 'temperature_scale_celsius' => 'Celsius (°C)', + 'layout' => 'Layout', + 'layout_small' => 'Maximum 1200 pixels wide', + 'layout_big' => 'Full width of the browser', + 'save' => 'Update preferences', + 'delete_title' => 'Delete your account', + 'delete_desc' => 'Do you wish to delete your account? Deletion is permanent and all of your data will be erased permanently. If you have a subscription, it will be cancelled immediately.', + 'delete_other_desc' => 'Your data in the main database will be deleted immediately. As described in our privacy policy, we carry out securely encrypted backups of the database every day. These backups are kept for 30 days after which they are completely deleted. We cannot delete specific data from the backups we hold any earlier than this. All of your data will be completely deleted no later than 31 days after your account’s deletion.', + 'reset_desc' => 'Do you wish to reset your account? This will remove all your contacts, and all of the data associated with them. Your account will not be deleted.', + 'reset_title' => 'Reset your account', + 'reset_cta' => 'Reset account', + 'reset_notice' => 'Are you sure to reset your account? This is permanent and cannot be undone.', + 'reset_success' => 'Your account has been reset successfully.', + 'delete_notice' => 'Are you sure you want to delete your account? This is permanent and cannot be undone. All of your data will be deleted and will not be recoverable.', + 'delete_cta' => 'Delete account', + 'settings_success' => 'Preferences updated!', + 'locale' => 'Language used in the app', + 'locale_help' => 'Do you want to help translating Monica or add a new language? Please follow this link for more information.', + 'locale_ar' => 'Arabic', + 'locale_cs' => 'Czech', + 'locale_de' => 'German', + 'locale_el' => 'Greek', + 'locale_en' => 'English', + 'locale_en-GB' => 'English (United Kingdom)', + 'locale_es' => 'Spanish', + 'locale_fr' => 'French', + 'locale_he' => 'Hebrew', + 'locale_hr' => 'Croatian', + 'locale_id' => 'Indonesian', + 'locale_it' => 'Italian', + 'locale_ja' => 'Japanese', + 'locale_nl' => 'Dutch', + 'locale_pt' => 'Portuguese', + 'locale_pt-BR' => 'Portuguese, Brazil', + 'locale_ru' => 'Russian', + 'locale_sv' => 'Swedish', + 'locale_vi' => 'Vietnamese', + 'locale_zh' => 'Chinese Simplified', + 'locale_zh-TW' => 'Chinese Traditional', + 'locale_tr' => 'Turkish', + + 'security_title' => 'Security', + 'security_help' => 'Change security matters for your account.', + 'password_change' => 'Change your password', + 'password_current' => 'Current password', + 'password_current_placeholder' => 'Enter your current password', + 'password_new1' => 'New password', + 'password_new1_placeholder' => 'Enter your new password', + 'password_new2' => 'Confirm your new password', + 'password_new2_placeholder' => 'Retype your new password', + 'password_btn' => 'Change password', + '2fa_title' => 'Two Factor Authentication', + '2fa_otp_title' => 'Two Factor Authentication mobile application', + '2fa_enable_title' => 'Enable Two Factor Authentication', + '2fa_enable_description' => 'Enable Two Factor Authentication to increase the security of your account.', + '2fa_enable_otp' => 'Open up your Two Factor Authentication mobile app and scan the following QR barcode:', + '2fa_enable_otp_help' => 'If your Two Factor Authentication mobile app does not support QR barcodes, enter in the following code:', + '2fa_enable_otp_validate' => 'Please validate the new device you’ve just set up:', + '2fa_enable_success' => 'Two Factor Authentication activated', + '2fa_enable_error' => 'Error when trying to activate Two Factor Authentication', + '2fa_enable_error_already_set' => 'Two Factor Authentication is already activated', + '2fa_disable_title' => 'Disable Two Factor Authentication', + '2fa_disable_description' => 'Disable Two Factor Authentication for your account. Be careful, your account will be much less secure!', + '2fa_disable_success' => 'Two Factor Authentication disabled', + '2fa_disable_error' => 'Error when trying to disable Two Factor Authentication', + + 'webauthn_title' => 'Security key — WebAuthn protocol', + 'webauthn_enable_description' => 'Add a new security key', + 'webauthn_key_name_help' => 'Give your key a name.', + 'webauthn_key_name' => 'Key name:', + 'webauthn_success' => 'Your key is detected and validated.', + 'webauthn_last_use' => 'Last use: {timestamp}', + 'webauthn_delete_confirmation' => 'Are you sure you want to delete this key?', + 'webauthn_delete_success' => 'Key deleted', + 'webauthn_insertKey' => 'Insert your security key.', + 'webauthn_buttonAdvise' => 'If your security key has a button, press it.', + 'webauthn_noButtonAdvise' => 'If it does not, remove it and insert it again.', + 'webauthn_not_supported' => 'Your browser doesn’t currently support WebAuthn.', + 'webauthn_not_secured' => 'WebAuthn only supports secure connections. Please load this page with https scheme.', + 'webauthn_error_already_used' => 'This key is already registered. It’s not necessary to register it again.', + 'webauthn_error_not_allowed' => 'The operation either timed out or was not allowed.', + + 'recovery_title' => 'Recovery codes', + 'recovery_show' => 'Get recovery codes', + 'recovery_copy_help' => 'Copy codes in your clipboard', + 'recovery_help_intro' => 'These are your recovery codes:', + 'recovery_help_information' => 'You can use each recovery code once.', + 'recovery_clipboard' => 'Codes copied to the clipboard.', + 'recovery_generate' => 'Generate new codes…', + 'recovery_generate_help' => 'Generating new codes will invalidate previously generated codes.', + 'recovery_already_used_help' => 'This code has already been used.', + + 'users_list_title' => 'Users with access to your account', + 'users_list_add_user' => 'Invite a new user', + 'users_list_you' => 'That’s you', + 'users_list_invitations_title' => 'Pending invitations', + 'users_list_invitations_explanation' => 'Below are the people you’ve invited to join Monica as a collaborator.', + 'users_list_invitations_invited_by' => 'invited by :name', + 'users_list_invitations_sent_date' => 'sent on :date', + 'users_blank_title' => 'You are the only one who has access to this account.', + 'users_blank_add_title' => 'Would you like to invite someone else?', + 'users_blank_description' => 'This person will have the same access that you have, and will be able to add, edit or delete contact information.', + 'users_blank_cta' => 'Invite someone', + 'users_add_title' => 'Invite a new user to your account by email', + 'users_add_description' => 'This person will have the same access as you do, including inviting or deleting other users, including you. Make sure you trust this person before giving them access.', + 'users_add_email_field' => 'Enter the email of the person you want to invite', + 'users_add_confirmation' => 'I confirm that I want to invite this user to my account. I understand that this person will have access to ALL of my data and see exactly what I see.', + 'users_add_cta' => 'Invite user by email', + 'users_accept_title' => 'Accept invitation and create a new account', + 'users_error_please_confirm' => 'Please confirm that you want to invite this user before proceeding with the invitation', + 'users_error_email_already_taken' => 'This email is already taken. Please choose another one', + 'users_error_already_invited' => 'You already have invited this user. Please choose another email address.', + 'users_error_email_not_similar' => 'This is not the email of the person who’ve invited you.', + 'users_invitation_deleted_confirmation_message' => 'The invitation has been successfully deleted', + 'users_invitations_delete_confirmation' => 'Are you sure you want to delete this invitation?', + 'users_list_delete_confirmation' => 'Are you sure to delete this user from your account?', + 'users_invitation_need_subscription' => 'Adding more users requires a subscription.', + + 'subscriptions_account_current_plan' => 'Your current plan', + 'subscriptions_account_current_legacy' => 'Current plan, not selectable anymore:', + 'subscriptions_account_current_paid_plan' => 'You are on the :name plan. Thanks so much for being a subscriber.', + + 'subscriptions_account_next_billing_title' => 'Next bill', + 'subscriptions_account_next_billing' => 'Your subscription will auto-renew on :date.', + 'subscriptions_account_bill_monthly' => 'We’ll bill you :price for another month.', + 'subscriptions_account_bill_annual' => 'We’ll bill you :price for another year.', + 'subscriptions_account_change' => 'Change plan', + + 'subscriptions_account_cancel_title' => 'Cancel subscription', + 'subscriptions_account_cancel_action' => 'Cancel subscription', + 'subscriptions_account_cancel' => 'You can cancel subscription anytime.', + 'subscriptions_account_free_plan' => 'You are on the free plan.', + 'subscriptions_account_free_plan_upgrade' => 'You can upgrade your account to the :name plan, which costs $:price per month. Here are the advantages:', + 'subscriptions_account_free_plan_benefits_users' => 'Unlimited number of users', + 'subscriptions_account_free_plan_benefits_reminders' => 'Reminders by email', + 'subscriptions_account_free_plan_benefits_import_data_vcard' => 'Import your contacts with vCard', + 'subscriptions_account_free_plan_benefits_support' => 'Support the project in the long run, so we can introduce more great features.', + 'subscriptions_account_upgrade' => 'Upgrade your account', + 'subscriptions_account_upgrade_title' => 'Upgrade Monica today and have more meaningful relationships.', + 'subscriptions_account_upgrade_choice' => 'Pick a plan below and join over :customers persons who upgraded their Monica.', + 'subscriptions_account_update_title' => 'Update Monica subscription', + 'subscriptions_account_update_description' => 'You can change your subscription’s frequency here.', + 'subscriptions_account_update_information' => 'You will be billed immediately for the new amount. Your subscription will extend to the new period, depending on your choice.', + 'subscriptions_account_invoices' => 'Invoices', + 'subscriptions_account_invoices_download' => 'Download', + 'subscriptions_account_invoices_subscription' => 'Subscription from :startDate to :endDate', + 'subscriptions_account_payment' => 'Which payment option fits you best?', + 'subscriptions_account_confirm_payment' => 'Your payment is currently incomplete, please confirm your payment.', + 'subscriptions_downgrade_title' => 'Downgrade your account to the free plan', + 'subscriptions_downgrade_limitations' => 'The free plan has limitations. In order to be able to downgrade, you need to pass the checklist below:', + 'subscriptions_downgrade_rule_users' => 'You must have only 1 user in your account', + 'subscriptions_downgrade_rule_users_constraint' => 'You currently have 1 user in your account.|You currently have :count users in your account.', + 'subscriptions_downgrade_rule_invitations' => 'You must not have any pending invitations', + 'subscriptions_downgrade_rule_invitations_constraint' => 'You currently have 1 pending invitation.|You currently have :count pending invitations.', + 'subscriptions_downgrade_rule_contacts' => 'You must not have more than :number active contacts', + 'subscriptions_downgrade_rule_contacts_constraint' => 'You currently have 1 contact.|You currently have :count contacts.', + 'subscriptions_downgrade_rule_contacts_archive' => 'We can also archive all your contacts for you – that would clear this rule and let you proceed with your account’s downgrade process.', + 'subscriptions_downgrade_cta' => 'Downgrade', + 'subscriptions_downgrade_success' => 'You are back to the Free plan!', + 'subscriptions_downgrade_thanks' => 'Thanks so much for trying the paid plan. We keep adding new features on Monica all the time – so you might want to come back in the future to see if you might be interested in taking a subscription again.', + 'subscriptions_back' => 'Back to settings', + 'subscriptions_upgrade_title' => 'Upgrade your account', + 'subscriptions_upgrade_choose' => 'You picked the :plan plan.', + 'subscriptions_upgrade_infos' => 'We couldn’t be happier. Enter your payment info below.', + 'subscriptions_upgrade_name' => 'Name on card', + 'subscriptions_upgrade_zip' => 'ZIP or postal code', + 'subscriptions_upgrade_credit' => 'Credit or debit card', + 'subscriptions_upgrade_submit' => 'Pay {amount}', + 'subscriptions_upgrade_charge' => 'We’ll charge your card :price now. The next charge will be on :date. If you ever change your mind, you can cancel at any time, no questions asked.', + 'subscriptions_upgrade_charge_handled' => 'The payment is handled by Stripe. No card information touches our server.', + 'subscriptions_upgrade_success' => 'Thank you! You are now subscribed.', + 'subscriptions_upgrade_thanks' => 'Welcome to the community of people who try to make the world a better place.', + + 'subscriptions_payment_confirm_title' => 'Confirm your :amount payment', + 'subscriptions_payment_confirm_information' => 'Extra confirmation is needed to process your payment. Please confirm your payment by filling out your payment details below.', + 'subscriptions_payment_succeeded_title' => 'Payment Successful', + 'subscriptions_payment_succeeded' => 'This payment was already successfully confirmed.', + 'subscriptions_payment_cancelled_title' => 'Payment Cancelled', + 'subscriptions_payment_cancelled' => 'This payment was cancelled.', + 'subscriptions_payment_error_name' => 'Please provide your name.', + 'subscriptions_payment_success' => 'The payment was successful.', + + 'subscriptions_pdf_title' => 'Your :name monthly subscription', + 'subscriptions_plan_frequency_year' => ':amount / year', + 'subscriptions_plan_frequency_month' => ':amount / month', + 'subscriptions_plan_choose' => 'Choose this plan', + 'subscriptions_plan_year_title' => 'Pay annually', + 'subscriptions_plan_year_bonus' => 'Peace of mind for a whole year', + 'subscriptions_plan_month_title' => 'Pay monthly', + 'subscriptions_plan_month_bonus' => 'Cancel any time', + 'subscriptions_plan_include1' => 'Included with your upgrade:', + 'subscriptions_plan_include2' => 'Unlimited number of contacts • Unlimited number of users • Reminders by email • Import with vCard • Personalization of the contact sheet', + 'subscriptions_plan_include3' => '100% of the profits go the development of this great open source project.', + 'subscriptions_help_title' => 'Additional details you may be curious about', + 'subscriptions_help_opensource_title' => 'What is an open source project?', + 'subscriptions_help_opensource_desc' => 'Monica is an open source project. This means it is built by a community who wants to build a great tool for the greater good. Being open source means the code is publicly available on GitHub, and everyone can inspect it, modify it or enhance it. All the money we raise is dedicated to building better features, paying for more powerful servers, and paying other costs. Thanks for your help. We couldn’t do it without you.', + 'subscriptions_help_limits_title' => 'Is there a limit to the number of contacts we can have on the free plan?', + 'subscriptions_help_limits_plan' => 'Yes. Free plans let you manage :number contacts.', + 'subscriptions_help_discounts_title' => 'Do you have discounts for non-profits and education?', + 'subscriptions_help_discounts_desc' => 'We do! Monica is free for students, and free for non-profits and charities. Just contact the support with a proof of your status and we’ll apply this special status in your account.', + 'subscriptions_help_change_title' => 'What if I change my mind?', + 'subscriptions_help_change_desc' => 'You can cancel anytime, no questions asked, and all by yourself – no need to contact support. However, you will not be refunded for the current period.', + + 'stripe_error_card' => 'Your card was declined. Decline message is: :message', + 'stripe_error_api_connection' => 'Network communication with Stripe failed. Try again later.', + 'stripe_error_rate_limit' => 'Too many requests with Stripe right now. Try again later.', + 'stripe_error_invalid_request' => 'Invalid parameters. Try again later.', + 'stripe_error_authentication' => 'Wrong authentication with Stripe', + + 'import_title' => 'Import contacts in your account', + 'import_cta' => 'Upload contacts', + 'import_stat' => 'You’ve imported :number files so far.', + 'import_result_stat' => 'Uploaded vCard with 1 contact (:total_imported imported, :total_skipped skipped)|Uploaded vCard with :total_contacts contacts (:total_imported imported, :total_skipped skipped)', + 'import_view_report' => 'View report', + 'import_in_progress' => 'The import is in progress. Reload the page in one minute.', + 'import_upload_title' => 'Import your contacts from a vCard file', + 'import_upload_rules_desc' => 'We do however have some rules:', + 'import_upload_rule_format' => 'We support .vcard and .vcf files.', + 'import_upload_rule_vcard' => 'We support the vCard 3.0 format, which is the default format for macOS’s Contacts.app and Google Contacts.', + 'import_upload_rule_instructions' => 'Export instructions for macOS Contacts.app and Google Contacts.', + 'import_upload_rule_multiple' => 'If your contacts have multiple email addresses or phone numbers, only the first entry will be saved.', + 'import_upload_rule_limit' => 'Files are limited to 10 MB.', + 'import_upload_rule_time' => 'It might take up to a minute to upload the contacts and process them. Please be patient.', + 'import_upload_rule_cant_revert' => 'Please make sure data is accurate before uploading, as you can’t undo the upload.', + 'import_upload_form_file' => 'Your .vcf or .vCard file:', + 'import_upload_behaviour' => 'Import behaviour:', + 'import_upload_behaviour_add' => 'Add new contacts and skip existing', + 'import_upload_behaviour_replace' => 'Replace existing contacts', + 'import_upload_behaviour_help' => 'Replacing will replace all data found in the vCard, but will keep existing contact fields.', + 'import_report_title' => 'Importing report', + 'import_report_date' => 'Date of the import', + 'import_report_type' => 'Type of import', + 'import_report_number_contacts' => 'Number of contacts in the file', + 'import_report_number_contacts_imported' => 'Number of imported contacts', + 'import_report_number_contacts_skipped' => 'Number of skipped contacts', + 'import_report_status_imported' => 'Imported', + 'import_report_status_skipped' => 'Skipped', + 'import_vcard_parse_error' => 'Error when parsing the vCard entry', + 'import_vcard_contact_exist' => 'Contact already exists', + 'import_vcard_contact_no_firstname' => 'No first name (mandatory)', + 'import_vcard_file_not_found' => 'File not found', + 'import_vcard_unknown_entry' => 'Unknown contact name', + 'import_vcard_file_no_entries' => 'File contains no entries', + 'import_blank_title' => 'You haven’t imported any contacts yet.', + 'import_blank_question' => 'Would you like to import contacts now?', + 'import_blank_description' => 'We can import vCard files that you can get from Google Contacts or your Contact manager.', + 'import_blank_cta' => 'Import vCard', + 'import_need_subscription' => 'Importing data requires a subscription.', + + 'tags_list_title' => 'Tags', + 'tags_list_description' => 'You can organize your contacts by setting up tags. Tags work like folders, but you can add more than one tag to a contact. To add a new tag, add it on the contact itself.', + 'tags_list_contact_number' => '1 contact|:count contacts', + 'tags_list_delete_success' => 'The tag has been successfully deleted', + 'tags_list_edit_success' => 'The tag has been successfully updated', + 'tags_list_delete_confirmation' => 'Are you sure you want to delete the tag? No contacts will be deleted, only the tag.', + 'tags_blank_title' => 'Tags are a great way of categorizing your contacts.', + 'tags_blank_description' => 'Tags work like folders, but you can add more than one tag to a contact. Go to a contact and tag a friend, right below the name. Once a contact is tagged, come back here to manage all the tags in your account.', + + 'api_title' => 'API access', + 'api_description' => 'The API can be used to manipulate Monica’s data from an external application, like a mobile application for instance.', + 'api_help' => 'To use the API, a token is mandatory. You can either create a personal access token (Bearer authentication), or authorize an OAuth client to create it for you. See API documentation.', + 'api_endpoint' => 'The API endpoint for this Monica instance is:', + + 'api_personal_access_tokens' => 'Personal access tokens', + 'api_pao_description' => 'Make sure you give this token to a source you trust – as they allow you to access all your data.', + 'api_token_title' => 'Personal Access Tokens', + 'api_token_create_new' => 'Create New Token', + 'api_token_not_created' => 'You have not created any personal access tokens.', + 'api_token_name' => 'Token name', + 'api_token_expire' => 'Expires at {date}', + 'api_token_delete' => 'Delete', + 'api_token_create' => 'Create Token', + 'api_token_scopes' => 'Scopes', + 'api_token_help' => 'Here is your new personal access token. This is the only time it will be shown so don’t lose it! You may now use this token to make API requests.', + + 'api_oauth_clients' => 'Your OAuth clients', + 'api_oauth_clients_desc' => 'This section lets you register your own OAuth clients.', + 'api_oauth_clients_desc2' => 'Use this client id to request a new token, and convert authorization codes to access tokens. See Laravel Passport documentation for more information.', + 'api_oauth_title' => 'OAuth Clients', + 'api_oauth_create_new' => 'Create New Client', + 'api_oauth_edit' => 'Edit Client', + 'api_oauth_not_created' => 'You have not created any OAuth clients.', + 'api_oauth_clientid' => 'Client ID', + 'api_oauth_name' => 'Name', + 'api_oauth_name_help' => 'Something your users will recognize and trust.', + 'api_oauth_secret' => 'Secret', + 'api_oauth_create' => 'Create Client', + 'api_oauth_redirecturl' => 'Redirect URL', + 'api_oauth_redirecturl_help' => 'Your application’s authorization callback URL.', + + 'api_authorized_clients' => 'List of authorized clients', + 'api_authorized_clients_desc' => 'This section lists all the clients you’ve authorized to access your application data. You can revoke this authorization at anytime.', + 'api_authorized_clients_title' => 'Authorized Applications', + 'api_authorized_clients_none' => 'There are no authorized clients yet.', + 'api_authorized_clients_name' => 'Name', + 'api_authorized_clients_scopes' => 'Scopes', + + 'personalization_tab_title' => 'Personalize your account', + + 'personalization_title' => 'Here you will find different settings to configure your account. These features are intended for “power users” who want maximum control over Monica.', + 'personalization_contact_field_type_title' => 'Contact field types', + 'personalization_contact_field_type_add' => 'Add new field type', + 'personalization_contact_field_type_description' => 'You can configure all the different types of contact fields that you can associate to all your contacts. For example, if a new social network appears in the future, you will be able to add this new way of communicating with your contacts right here.', + 'personalization_contact_field_type_table_name' => 'Name', + 'personalization_contact_field_type_table_protocol' => 'Protocol', + 'personalization_contact_field_type_table_actions' => 'Actions', + 'personalization_contact_field_type_modal_title' => 'Add a new contact field type', + 'personalization_contact_field_type_modal_edit_title' => 'Edit an existing contact field type', + 'personalization_contact_field_type_modal_delete_title' => 'Delete an existing contact field type', + 'personalization_contact_field_type_modal_delete_description' => 'Are you sure you want to delete this contact field type? Deleting this type of contact field will delete ALL the data with this type for all of your contacts.', + 'personalization_contact_field_type_modal_name' => 'Name', + 'personalization_contact_field_type_modal_protocol' => 'Protocol (optional)', + 'personalization_contact_field_type_modal_protocol_help' => 'Each new contact field type can be clickable. If a protocol is set, we will use it to trigger the action that is set.', + 'personalization_contact_field_type_modal_icon' => 'Icon (optional)', + 'personalization_contact_field_type_modal_icon_help' => 'You can associate an icon with this contact field type. You need to add a reference to a Font Awesome icon.', + 'personalization_contact_field_type_delete_success' => 'The contact field type has been successfully deleted.', + 'personalization_contact_field_type_add_success' => 'The contact field type has been successfully added.', + 'personalization_contact_field_type_edit_success' => 'The contact field type has been successfully updated.', + + 'personalization_genders_title' => 'Gender types', + 'personalization_genders_add' => 'Add new gender type', + 'personalization_genders_desc' => 'You can define as many genders as you need to. You need at least one gender type in your account.', + 'personalization_genders_modal_add' => 'Add gender type', + 'personalization_genders_modal_edit' => 'Update gender type', + 'personalization_genders_modal_name' => 'Name', + 'personalization_genders_modal_name_help' => 'The name used to display the gender on a contact page.', + 'personalization_genders_modal_sex' => 'Sex', + 'personalization_genders_modal_sex_help' => 'Used to define the relationships, and during the VCard import/export process.', + 'personalization_genders_modal_default' => 'Select the default gender for a new contact', + 'personalization_genders_modal_delete' => 'Delete gender type', + 'personalization_genders_modal_delete_desc' => 'Are you sure you want to delete the gender “{name}”?', + 'personalization_genders_modal_delete_question' => 'You currently have {count} contact with this gender. If you delete this gender, what gender should this contact have?|You currently have {count} contacts with this gender. If you delete this gender, what gender should these contacts have?', + 'personalization_genders_modal_delete_question_default' => 'This gender is the default one. If you delete this gender, which one will be the new default?', + 'personalization_genders_modal_error' => 'Please choose a gender from the list.', + 'personalization_genders_list_contact_number' => '{count} contact|{count} contacts', + 'personalization_genders_table_name' => 'Name', + 'personalization_genders_table_sex' => 'Sex', + 'personalization_genders_table_default' => 'Default', + 'personalization_genders_default' => 'Default gender', + 'personalization_genders_make_default' => 'Change default gender', + 'personalization_genders_select_default' => 'Select default gender', + 'personalization_genders_m' => 'Male', + 'personalization_genders_f' => 'Female', + 'personalization_genders_o' => 'Other', + 'personalization_genders_u' => 'Unknown', + 'personalization_genders_n' => 'None or not applicable', + + 'personalization_reminder_rule_save' => 'The change has been saved', + 'personalization_reminder_rule_title' => 'Reminder rules', + 'personalization_reminder_rule_line' => '{count} day before|{count} days before', + 'personalization_reminder_rule_desc' => 'For every reminder that you set, Monica can send you an email a number of days before the event happens. You can adjust these notification settings here. These notifications only apply to monthly and yearly reminders.', + + 'personalization_module_save' => 'The change has been saved', + 'personalization_module_title' => 'Features', + 'personalization_module_desc' => 'You may not need all of Monica’s features. Below you can toggle specific features that are used on a contact sheet. This change will affect ALL your contacts. Turning off a feature does not delete any data, it simply hides the feature.', + + 'personalisation_paid_upgrade' => 'This is a premium feature that requires a Paid subscription to be active. Upgrade your account by visiting Settings > Subscription.', + 'personalisation_paid_upgrade_vue' => 'This is a premium feature that requires a Paid subscription to be active. Upgrade your account by visiting Settings > Subscription.', + + 'reminder_time_to_send' => 'Time of the day reminders will be sent', + 'reminder_time_to_send_help' => 'Your next reminder is scheduled to be sent on {dateTime}.', + + 'personalization_activity_type_category_title' => 'Activity type categories', + 'personalization_activity_type_category_add' => 'Add a new activity type category', + 'personalization_activity_type_category_table_name' => 'Name', + 'personalization_activity_type_category_description' => 'An activity with one of your contacts can have a type and a category type. Your account comes with a set of predefined category types by default, but you can customize these here.', + 'personalization_activity_type_category_table_actions' => 'Actions', + 'personalization_activity_type_category_modal_add' => 'Add a new activity type category', + 'personalization_activity_type_category_modal_edit' => 'Edit an activity type category', + 'personalization_activity_type_category_modal_question' => 'What should we name this new category?', + 'personalization_activity_type_add_button' => 'Add a new activity type', + 'personalization_activity_type_modal_add' => 'Add a new activity type', + 'personalization_activity_type_modal_question' => 'What should we name this new activity type?', + 'personalization_activity_type_modal_edit' => 'Edit an activity type', + 'personalization_activity_type_category_modal_delete' => 'Delete an activity type category', + 'personalization_activity_type_category_modal_delete_desc' => 'Are you sure you want to delete this category? Deleting it will delete all associated activity types. Activities that belong to this category will not be affected by this deletion.', + 'personalization_activity_type_modal_delete' => 'Delete an activity type', + 'personalization_activity_type_modal_delete_desc' => 'Are you sure you want to delete this activity type? Activities that belong to this category will not be affected by this deletion.', + 'personalization_activity_type_modal_delete_error' => 'We can’t find this activity type.', + 'personalization_activity_type_category_modal_delete_error' => 'We can’t find this activity type category.', + + 'personalization_life_event_category_title' => 'Life event categories', + 'personalization_live_event_category_table_name' => 'Name', + 'personalization_life_event_category_description' => 'A life event can have a type and a category. Your account comes with a set of predefined categories and types by default, but you can customize life event types here.', + 'personalization_live_event_category_table_actions' => 'Actions', + 'personalization_life_event_type_add_button' => 'Add a new life event type', + 'personalization_life_event_type_modal_add' => 'Add a new life event type', + 'personalization_life_event_type_modal_question' => 'What should we name this new life event type?', + 'personalization_life_event_type_modal_edit' => 'Edit a life event type', + 'personalization_life_event_type_modal_delete' => 'Delete a life event type', + 'personalization_life_event_type_modal_delete_desc' => 'Are you sure you want to delete this life event type? Life events that belong to this type will be deleted by performing this action.', + 'personalization_life_event_type_modal_delete_error' => 'We can’t find this life event type.', + + 'personalization_life_event_category_work_education' => 'Work & education', + 'personalization_life_event_category_family_relationships' => 'Family & relationships', + 'personalization_life_event_category_home_living' => 'Home & living', + 'personalization_life_event_category_travel_experiences' => 'Travel & experiences', + 'personalization_life_event_category_health_wellness' => 'Health & wellness', + + 'personalization_life_event_type_new_job' => 'New job', + 'personalization_life_event_type_retirement' => 'Retirement', + 'personalization_life_event_type_new_school' => 'New school', + 'personalization_life_event_type_study_abroad' => 'Study abroad', + 'personalization_life_event_type_volunteer_work' => 'Volunteer work', + 'personalization_life_event_type_published_book_or_paper' => 'Published a book or paper', + 'personalization_life_event_type_military_service' => 'Military service', + 'personalization_life_event_type_first_met' => 'First met', + 'personalization_life_event_type_new_relationship' => 'New relationship', + 'personalization_life_event_type_engagement' => 'Engagement', + 'personalization_life_event_type_marriage' => 'Marriage', + 'personalization_life_event_type_anniversary' => 'Anniversary', + 'personalization_life_event_type_expecting_a_baby' => 'Expecting a baby', + 'personalization_life_event_type_new_child' => 'New child', + 'personalization_life_event_type_new_family_member' => 'New family member', + 'personalization_life_event_type_new_pet' => 'New pet', + 'personalization_life_event_type_end_of_relationship' => 'End of relationship', + 'personalization_life_event_type_loss_of_a_loved_one' => 'Loss of a loved one', + 'personalization_life_event_type_moved' => 'Moved', + 'personalization_life_event_type_bought_a_home' => 'Bought a home', + 'personalization_life_event_type_home_improvement' => 'Home improvement', + 'personalization_life_event_type_holidays' => 'Holidays', + 'personalization_life_event_type_new_vehicle' => 'New vehicle', + 'personalization_life_event_type_new_roommate' => 'New roommate', + 'personalization_life_event_type_overcame_an_illness' => 'Overcame an illness', + 'personalization_life_event_type_quit_a_habit' => 'Quit a habit', + 'personalization_life_event_type_new_eating_habits' => 'New eating habits', + 'personalization_life_event_type_weight_loss' => 'Weight loss', + 'personalization_life_event_type_wear_glass_or_contact' => 'Started wearing glasses or contacts', + 'personalization_life_event_type_broken_bone' => 'Broke a bone', + 'personalization_life_event_type_removed_braces' => 'Had braces removed', + 'personalization_life_event_type_surgery' => 'Had surgery', + 'personalization_life_event_type_dentist' => 'Had dental treatment', + 'personalization_life_event_type_new_sport' => 'Started playing a new sport', + 'personalization_life_event_type_new_hobby' => 'Took up a new hobby', + 'personalization_life_event_type_new_instrument' => 'Started learning a new instrument', + 'personalization_life_event_type_new_language' => 'Started learning a new language', + 'personalization_life_event_type_tattoo_or_piercing' => 'Tattoo or piercing', + 'personalization_life_event_type_new_license' => 'New license', + 'personalization_life_event_type_travel' => 'Travel', + 'personalization_life_event_type_achievement_or_award' => 'Achievement or award', + 'personalization_life_event_type_changed_beliefs' => 'Changed beliefs', + 'personalization_life_event_type_first_word' => 'First word', + 'personalization_life_event_type_first_kiss' => 'First kiss', + + 'storage_title' => 'Storage', + 'storage_account_info' => 'Your account limit is :accountLimit MB. Your current usage is :currentAccountSize MB (about :percentUsage%).', + 'storage_upgrade_notice' => 'Upgrade your account to be able to upload documents and photos.', + 'storage_description' => 'Here you can see all the documents and photos uploaded about your contacts.', + + 'dav_title' => 'WebDAV', + 'dav_description' => 'Here you can find all settings to use WebDAV resources for CardDAV and CalDAV exports.', + 'dav_copy_help' => 'Copy into your clipboard', + 'dav_clipboard_copied' => 'Value copied into your clipboard', + 'dav_url_base' => 'Base url for all CardDAV and CalDAV resources:', + 'dav_connect_help' => 'You can connect your contacts and/or calendars with this base url on you phone or computer.', + 'dav_connect_help2' => 'Use your login (email) and create an API token as the password to authenticate.', + 'dav_url_carddav' => 'CardDAV url for Contacts resource:', + 'dav_url_caldav_birthdays' => 'CalDAV url for Birthdays resources:', + 'dav_url_caldav_tasks' => 'CalDAV url for Tasks resources:', + 'dav_title_carddav' => 'CardDAV', + 'dav_title_caldav' => 'CalDAV', + 'dav_carddav_export' => 'Export all contacts in one file', + 'dav_caldav_birthdays_export' => 'Export all birthdays in one file', + 'dav_caldav_tasks_export' => 'Export all tasks in one file', + + 'archive_title' => 'Archive all of the contacts in your account', + 'archive_desc' => 'This will archive all of the contacts in your account.', + 'archive_cta' => 'Archive all of your contacts', + + 'logs_title' => 'Everything that has happened to this account', + 'logs_actor' => 'Actor', + 'logs_timestamp' => 'Timestamp', + 'logs_description' => 'Description', + 'logs_subject' => 'Subject', + 'logs_size' => 'Size (Kb)', + 'logs_object' => 'Object', +]; diff --git a/resources/lang/hr/validation.php b/resources/lang/hr/validation.php new file mode 100644 index 0000000..3a246f9 --- /dev/null +++ b/resources/lang/hr/validation.php @@ -0,0 +1,166 @@ + 'Polje :attribute mora biti prihvaćeno.', + 'active_url' => 'Polje :attribute nije ispravan URL.', + 'after' => 'Polje :attribute mora biti datum nakon :date.', + 'after_or_equal' => 'Polje :attribute mora biti datum veći ili jednak :date.', + 'alpha' => 'Polje :attribute smije sadržavati samo slova.', + 'alpha_dash' => 'Polje :attribute smije sadržavati samo slova, brojeve i crtice.', + 'alpha_num' => 'Polje :attribute smije sadržavati samo slova i brojeve.', + 'array' => 'Polje :attribute mora biti niz.', + 'before' => 'Polje :attribute mora biti datum prije :date.', + 'before_or_equal' => 'Polje :attribute mora biti datum manji ili jednak :date.', + 'between' => [ + 'numeric' => 'Polje :attribute mora biti između :min - :max.', + 'file' => 'Polje :attribute mora biti između :min - :max kilobajta.', + 'string' => 'Polje :attribute mora biti između :min - :max znakova.', + 'array' => 'Polje :attribute mora imati između :min - :max stavki.', + ], + 'boolean' => 'Polje :attribute mora biti false ili true.', + 'confirmed' => 'Potvrda polja :attribute se ne podudara.', + 'date' => 'Polje :attribute nije ispravan datum.', + 'date_equals' => 'Stavka :attribute mora biti jednaka :date.', + 'date_format' => 'Polje :attribute ne podudara s formatom :format.', + 'different' => 'Polja :attribute i :other moraju biti različita.', + 'digits' => 'Polje :attribute mora sadržavati :digits znamenki.', + 'digits_between' => 'Polje :attribute mora imati između :min i :max znamenki.', + 'dimensions' => 'Polje :attribute ima neispravne dimenzije slike.', + 'distinct' => 'Polje :attribute ima dupliciranu vrijednost.', + 'email' => 'Polje :attribute mora biti ispravna e-mail adresa.', + 'ends_with' => 'The :attribute must end with one of the following: :values.', + 'exists' => 'Odabrano polje :attribute nije ispravno.', + 'file' => 'Polje :attribute mora biti datoteka.', + 'filled' => 'The :attribute field is required.', + 'gt' => [ + 'numeric' => 'Polje :attribute mora biti veće od :value.', + 'file' => 'Polje :attribute mora biti veće od :value kilobajta.', + 'string' => 'Polje :attribute mora biti veće od :value karaktera.', + 'array' => 'Polje :attribute mora biti veće od :value stavki.', + ], + 'gte' => [ + 'numeric' => 'Polje :attribute mora biti veće ili jednako :value.', + 'file' => 'Polje :attribute mora biti veće ili jednako :value kilobajta.', + 'string' => 'Polje :attribute mora biti veće ili jednako :value znakova.', + 'array' => 'Polje :attribute mora imati :value stavki ili više.', + ], + 'image' => 'Polje :attribute mora biti slika.', + 'in' => 'Odabrano polje :attribute nije ispravno.', + 'in_array' => 'Polje :attribute ne postoji u :other.', + 'integer' => 'Polje :attribute mora biti broj.', + 'ip' => 'Polje :attribute mora biti ispravna IP adresa.', + 'ipv4' => 'Polje :attribute mora biti ispravna IPv4 adresa.', + 'ipv6' => 'Polje :attribute mora biti ispravna IPv6 adresa.', + 'json' => 'Polje :attribute mora biti ispravan JSON string.', + 'lt' => [ + 'numeric' => 'Polje :attribute mora biti manje od :value.', + 'file' => 'Polje :attribute mora biti manje od :value kilobajta.', + 'string' => 'Polje :attribute mora biti manje od :value znakova.', + 'array' => 'Polje :attribute mora biti manje od :value stavki.', + ], + 'lte' => [ + 'numeric' => 'Polje :attribute mora biti manje ili jednako :value.', + 'file' => 'Polje :attribute mora biti manje ili jednako :value kilobajta.', + 'string' => 'Polje :attribute mora biti manje ili jednako :value znakova.', + 'array' => 'Polje :attribute ne smije imati više od :value stavki.', + ], + 'max' => [ + 'numeric' => 'Polje :attribute mora biti manje od :max.', + 'file' => 'Polje :attribute mora biti manje od :max kilobajta.', + 'string' => 'Polje :attribute mora sadržavati manje od :max znakova.', + 'array' => 'Polje :attribute ne smije imati više od :max stavki.', + ], + 'mimes' => 'Polje :attribute mora biti datoteka tipa: :values.', + 'mimetypes' => 'Polje :attribute mora biti datoteka tipa: :values.', + 'min' => [ + 'numeric' => 'Polje :attribute mora biti najmanje :min.', + 'file' => 'Polje :attribute mora biti najmanje :min kilobajta.', + 'string' => 'Polje :attribute mora sadržavati najmanje :min znakova.', + 'array' => 'Polje :attribute mora sadržavati najmanje :min stavki.', + ], + 'not_in' => 'Odabrano polje :attribute nije ispravno.', + 'not_regex' => 'Format polja :attribute je neispravan.', + 'numeric' => 'Polje :attribute mora biti broj.', + 'password' => 'The password is incorrect.', + 'present' => 'Polje :attribute mora biti prisutno.', + 'regex' => 'Polje :attribute se ne podudara s formatom.', + 'required' => 'Polje :attribute je obavezno.', + 'required_if' => 'Polje :attribute je obavezno kada polje :other sadrži :value.', + 'required_unless' => 'Polje :attribute je obavezno osim :other je u :values.', + 'required_with' => 'Polje :attribute je obavezno kada postoji polje :values.', + 'required_with_all' => 'Polje :attribute je obavezno kada postje polja :values.', + 'required_without' => 'Polje :attribute je obavezno kada ne postoji polje :values.', + 'required_without_all' => 'Polje :attribute je obavezno kada nijedno od polja :values ne postoji.', + 'same' => 'Polja :attribute i :other se moraju podudarati.', + 'size' => [ + 'numeric' => 'Polje :attribute mora biti :size.', + 'file' => 'Polje :attribute mora biti :size kilobajta.', + 'string' => 'Polje :attribute mora biti :size znakova.', + 'array' => 'Polje :attribute mora sadržavati :size stavki.', + ], + 'starts_with' => 'Stavka :attribute mora započinjati jednom od narednih stavki: :values', + 'string' => 'Polje :attribute mora biti string.', + 'timezone' => 'Polje :attribute mora biti ispravna vremenska zona.', + 'unique' => 'Polje :attribute već postoji.', + 'uploaded' => 'Polje :attribute nije uspešno učitano.', + 'url' => 'Polje :attribute nije ispravnog formata.', + 'uuid' => 'Stavka :attribute mora biti valjani UUID.', + + /* + |-------------------------------------------------------------------------- + | Custom Validation Language Lines + |-------------------------------------------------------------------------- + | + | Here you may specify custom validation messages for attributes using the + | convention "attribute.rule" to name the lines. This makes it quick to + | specify a specific custom language line for a given attribute rule. + | + */ + + 'custom' => [ + 'attribute-name' => [ + 'rule-name' => 'custom-message', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Custom Validation Attributes + |-------------------------------------------------------------------------- + | + | The following language lines are used to swap our attribute placeholder + | with something more reader friendly such as "E-Mail Address" instead + | of "email". This simply helps us make our message more expressive. + | + */ + + 'attributes' => [], + + 'vue' => [ + 'max' => [ + 'numeric' => '{field} may not be greater than {max}.', + 'string' => '{field} may not be greater than {max} characters.', + ], + 'required' => '{field} is required.', + 'url' => '{field} is not a valid URL.', + ], + +]; diff --git a/resources/lang/id.json b/resources/lang/id.json new file mode 100644 index 0000000..e10d4ed --- /dev/null +++ b/resources/lang/id.json @@ -0,0 +1,7 @@ +{ + "The :attribute must contain at least one uppercase and one lowercase letter.": ":attribute harus mengandung setidaknya satu huruf besar dan satu huruf kecil.", + "The :attribute must contain at least one letter.": ":attribute harus mengandung setidaknya satu huruf.", + "The :attribute must contain at least one symbol.": ":attribute harus mengandung setidaknya satu simbol.", + "The :attribute must contain at least one number.": ":attribute harus mengandung setidaknya satu angka.", + "The given :attribute has appeared in a data leak. Please choose a different :attribute.": ":attribute yang diberikan telah muncul dalam kebocoran data. Silahkan pilih :attribute yang lain." +} diff --git a/resources/lang/id/app.php b/resources/lang/id/app.php new file mode 100644 index 0000000..3c251ed --- /dev/null +++ b/resources/lang/id/app.php @@ -0,0 +1,571 @@ + 'Ya', + 'no' => 'Tidak', + 'update' => 'Perbarui', + 'save' => 'Simpan', + 'add' => 'Tambah', + 'cancel' => 'Batalkan', + 'confirm' => 'Konfirmasi', + 'delete_confirm' => 'Apakah Anda yakin?', + 'delete' => 'Hapus', + 'edit' => 'Sunting', + 'upload' => 'Unggah', + 'download' => 'Unduh', + 'save_close' => 'Simpan dan tutup dialog', + 'close' => 'Tutup', + 'copy' => 'Salin', + 'create' => 'Buat', + 'remove' => 'Hapus', + 'revoke' => 'Cabut', + 'done' => 'Selesai', + 'back' => 'Kembali', + 'verify' => 'Verifikasi', + 'new' => 'baru', + 'unknown' => 'Saya tidak tau', + 'load_more' => 'Muat lebih banyak', + 'loading' => 'Memuat…', + 'with' => 'dengan', + 'today' => 'hari ini', + 'yesterday' => 'kemarin', + 'another_day' => 'hari yang lain', + 'date' => 'Tanggal', + 'type' => 'Jenis', + 'zoom' => 'Perbesar', + 'upgrade' => 'Perbarui untuk membuka', + 'percent_uploaded' => '{percent}% diunggah', + 'retry' => 'Coba Lagi', + 'filter' => 'Filter daftar', + 'go_back' => 'Kembali', + 'file_selected' => '{count} berkas dipilih…', + + 'application_title' => 'Monica – pengelola kontak relasi pribadi', + 'application_description' => 'Monica adalah sebuah alat untuk mengelola interaksi Anda dengan orang yang Anda sayangi, teman, dan keluarga.', + 'application_og_title' => 'Bangun hubungan yang lebih baik dengan orang yang Anda sayangi. CRM bebas untuk teman dan keluarga.', + + 'markdown_description' => 'Ingin memformat teks Anda lebih bagus? Kami mendukung format Markdown untuk menambahkan tekstebal, italik, daftar, dan banyak lagi.', + 'markdown_link' => 'Baca dokumentasi', + + 'header_settings_link' => 'Pengaturan', + 'header_logout_link' => 'Keluar', + 'header_changelog_link' => 'Perubahan produk', + + 'main_nav_cta' => 'Tambah orang', + 'main_nav_dashboard' => 'Dasbor', + 'main_nav_family' => 'Kontak', + 'main_nav_journal' => 'Jurnal', + 'main_nav_activities' => 'Aktifitas', + 'main_nav_tasks' => 'Tugas', + + 'footer_remarks' => 'Komentar?', + 'footer_send_email' => 'Kirim kami sebuah email', + 'footer_privacy' => 'Kebijakan privasi', + 'footer_release' => 'Catatan rilis', + 'footer_newsletter' => 'Newsletter', + 'footer_source_code' => 'Kontribusi', + 'footer_version' => 'Versi: :version', + 'footer_new_version' => 'Sebuah versi baru dari Monica tersedia', + + 'footer_modal_version_whats_new' => 'Apa yang baru', + 'footer_modal_version_release_away' => 'Anda tertinggal :number rilisan dibelakang versi baru yang tersedia. Anda harus memperbarui pemasangan Anda.', + + 'breadcrumb_dashboard' => 'Dasbor', + 'breadcrumb_list_contacts' => 'Daftar orang', + 'breadcrumb_archived_contacts' => 'Kontak yang diarsipkan', + 'breadcrumb_journal' => 'Jurnal', + 'breadcrumb_settings' => 'Pengaturan', + 'breadcrumb_settings_export' => 'Ekspor', + 'breadcrumb_settings_users' => 'Pengguna', + 'breadcrumb_settings_users_add' => 'Tambah seorang pengguna', + 'breadcrumb_settings_subscriptions' => 'Berlangganan', + 'breadcrumb_settings_import' => 'Impor', + 'breadcrumb_settings_import_report' => 'Impor laporan', + 'breadcrumb_settings_import_upload' => 'Unggah', + 'breadcrumb_settings_tags' => 'Tag', + 'breadcrumb_add_significant_other' => 'Tambahkan orang yang berarti', + 'breadcrumb_edit_significant_other' => 'Sunting orang yang berarti', + 'breadcrumb_add_note' => 'Tambah sebuah catatan', + 'breadcrumb_edit_note' => 'Sunting sebuah catatan', + 'breadcrumb_api' => 'API', + 'breadcrumb_dav' => 'Sumberdaya DAV', + 'breadcrumb_edit_introductions' => 'Bagaimana Anda bertemu', + 'breadcrumb_settings_personalization' => 'Personalisasi', + 'breadcrumb_settings_security' => 'Keamanan', + 'breadcrumb_settings_security_2fa' => 'Otentikasi Dua Faktor', + 'breadcrumb_profile' => 'Profil dari :name', + + 'gender_male' => 'Laki-laki', + 'gender_female' => 'Perempuan', + 'gender_none' => 'Lebih baik tidak mengatakan', + 'gender_no_gender' => 'Tidak ada jenis kelamin', + + 'error_title' => 'Uppss Kakak! Suatu kesalahan telah terjadi.', + 'error_unauthorized' => 'Anda tidak punya izin untuk menyunting sumber daya ini.', + 'error_user_account' => 'Pengguna ini bukan termasuk dalam akun yang diberikan.', + 'error_save' => 'Kami mengalami kesalahan ketika mencoba menyimpan data.', + 'error_try_again' => 'Terjadi sesuatu kesalahan. Silahkan coba lagi.', + 'error_id' => 'ID Kesalahan: :id', + 'error_unavailable' => 'Layanan tidak tersedia', + 'error_maintenance' => 'Sedang dalam mode pemeliharaan. Kami akan segera kembali.', + 'error_help' => 'Kami akan segera kembali lagi.', + 'error_twitter' => 'Ikuti akun Twitter kami untuk pemberitahuan ketika sudah berjalan kembali.', + 'error_no_term' => 'Belum ada kebijakan untuk contoh ini.', + + 'default_save_success' => 'Data telah disimpan.', + + 'compliance_title' => 'Maaf untuk gangguan ini.', + 'compliance_desc' => 'Kami telah merubah Ketentuan Penggunaan dan Kebijakan Privasi kami. Berdasarkan hukum, kami harus meminta Anda untuk meninjau kebijakan tersebut dan menyetujuinya agar Anda dapat melanjutkan untuk menggunakan akun Anda.', + 'compliance_desc_end' => 'Kami tidak melakukan sesuatu yang buruk terhadap data atau akun Anda dan kami tidak akan pernah melakukannya.', + 'compliance_terms' => 'Setujui ketentuan dan kebijakan privasi yang baru', + + // Relationship types + // Yes, each relationship type has 8 strings associated with it. + // This is because we need to indicate the name of the relationship type, + // and also the name of the opposite side of this relationship (father/son), + // and then, the feminine version of the string. Finally, in some sentences + // in the UI, we need to include the name of the person we add the relationship + // to. + 'relationship_type_group_love' => 'Hubungan percintaan', + 'relationship_type_group_family' => 'Hubungan keluarga', + 'relationship_type_group_friend' => 'Hubungan pertemanan', + 'relationship_type_group_work' => 'Hubungan pekerjaan', + 'relationship_type_group_other' => 'Jenis hubungan lainnya', + + 'relationship_type_partner' => 'orang yang berarti', + 'relationship_type_partner_female' => 'orang yang berarti', + 'relationship_type_partner_male' => 'significant other', + 'relationship_type_partner_with_name' => ':name orang yang berarti lainnya', + 'relationship_type_partner_female_with_name' => ':name orang yang berarti lainnya', + 'relationship_type_partner_male_with_name' => ':name’s significant other', + + 'relationship_type_spouse' => 'pasangan', + 'relationship_type_spouse_female' => 'wife', + 'relationship_type_spouse_male' => 'husband', + 'relationship_type_spouse_with_name' => 'pasangan :name', + 'relationship_type_spouse_female_with_name' => ':name’s wife', + 'relationship_type_spouse_male_with_name' => ':name’s husband', + + 'relationship_type_date' => 'tanggal', + 'relationship_type_date_female' => 'tanggal', + 'relationship_type_date_male' => 'date', + 'relationship_type_date_with_name' => 'Tanggal :name', + 'relationship_type_date_female_with_name' => 'Tanggal :name', + 'relationship_type_date_male_with_name' => ':name’s date', + + 'relationship_type_lover' => 'kekasih', + 'relationship_type_lover_female' => 'kekasih', + 'relationship_type_lover_male' => 'lover', + 'relationship_type_lover_with_name' => 'kekasih :name', + 'relationship_type_lover_female_with_name' => 'kekasih :name', + 'relationship_type_lover_male_with_name' => ':name’s lover', + + 'relationship_type_inlovewith' => 'jatuh cinta dengan', + 'relationship_type_inlovewith_female' => 'jatuh cinta dengan', + 'relationship_type_inlovewith_male' => 'in love with', + 'relationship_type_inlovewith_with_name' => 'seseorang :name jatuh cinta dengan', + 'relationship_type_inlovewith_female_with_name' => 'seseorang :name jatuh cinta dengan', + 'relationship_type_inlovewith_male_with_name' => 'someone :name is in love with', + + 'relationship_type_lovedby' => 'dicintai oleh', + 'relationship_type_lovedby_female' => 'dicintai oleh', + 'relationship_type_lovedby_male' => 'loved by', + 'relationship_type_lovedby_with_name' => 'Kekasih rahasia :name', + 'relationship_type_lovedby_female_with_name' => 'Kekasih rahasia :name', + 'relationship_type_lovedby_male_with_name' => ':name’s secret lover', + + 'relationship_type_ex' => 'ex-partner', + 'relationship_type_ex_female' => 'mantan pacar', + 'relationship_type_ex_male' => 'ex-boyfriend', + 'relationship_type_ex_with_name' => ':name’s ex-partner', + 'relationship_type_ex_female_with_name' => 'Mantan pacar :name', + 'relationship_type_ex_male_with_name' => ':name’s ex-boyfriend', + + 'relationship_type_parent' => 'parent', + 'relationship_type_parent_female' => 'ibu', + 'relationship_type_parent_male' => 'father', + 'relationship_type_parent_with_name' => ':name’s parent', + 'relationship_type_parent_female_with_name' => 'Ibu :name', + 'relationship_type_parent_male_with_name' => ':name’s father', + + 'relationship_type_child' => 'child', + 'relationship_type_child_female' => 'putri', + 'relationship_type_child_male' => 'son', + 'relationship_type_child_with_name' => ':name’s child', + 'relationship_type_child_female_with_name' => 'Putri :name', + 'relationship_type_child_male_with_name' => ':name’s son', + + 'relationship_type_stepparent' => 'step-parent', + 'relationship_type_stepparent_female' => 'ibu tiri', + 'relationship_type_stepparent_male' => 'stepfather', + 'relationship_type_stepparent_with_name' => ':name’s step-parent', + 'relationship_type_stepparent_female_with_name' => 'Ibu tiri :name', + 'relationship_type_stepparent_male_with_name' => ':name’s stepfather', + + 'relationship_type_stepchild' => 'stepchild', + 'relationship_type_stepchild_female' => 'putri tiri', + 'relationship_type_stepchild_male' => 'stepson', + 'relationship_type_stepchild_with_name' => ':name’s stepchild', + 'relationship_type_stepchild_female_with_name' => 'Putri tiri :name', + 'relationship_type_stepchild_male_with_name' => ':name’s stepson', + + 'relationship_type_sibling' => 'sibling', + 'relationship_type_sibling_female' => 'saudara perempuan', + 'relationship_type_sibling_male' => 'brother', + 'relationship_type_sibling_with_name' => ':name’s sibling', + 'relationship_type_sibling_female_with_name' => 'Saudara perempuan :name', + 'relationship_type_sibling_male_with_name' => ':name’s brother', + + 'relationship_type_grandparent' => 'grandparent', + 'relationship_type_grandparent_female' => 'grandmother', + 'relationship_type_grandparent_male' => 'grandfather', + 'relationship_type_grandparent_with_name' => ':name’s grandparent', + 'relationship_type_grandparent_female_with_name' => ':name’s grandmother', + 'relationship_type_grandparent_male_with_name' => ':name’s grandfather', + + 'relationship_type_grandchild' => 'grandchild', + 'relationship_type_grandchild_female' => 'granddaughter', + 'relationship_type_grandchild_male' => 'grandson', + 'relationship_type_grandchild_with_name' => ':name’s grandchild', + 'relationship_type_grandchild_female_with_name' => ':name’s granddauther', + 'relationship_type_grandchild_male_with_name' => ':name’s grandson', + + 'relationship_type_uncle' => 'paman', + 'relationship_type_uncle_female' => 'bibi', + 'relationship_type_uncle_male' => 'uncle', + 'relationship_type_uncle_with_name' => 'Paman :name', + 'relationship_type_uncle_female_with_name' => 'Bibi :name', + 'relationship_type_uncle_male_with_name' => ':name’s uncle', + + 'relationship_type_nephew' => 'keponakan', + 'relationship_type_nephew_female' => 'ponakan', + 'relationship_type_nephew_male' => 'nephew', + 'relationship_type_nephew_with_name' => 'Keponakan :name', + 'relationship_type_nephew_female_with_name' => 'Ponakan :name', + 'relationship_type_nephew_male_with_name' => ':name’s nephew', + + 'relationship_type_cousin' => 'saudara', + 'relationship_type_cousin_female' => 'saudara', + 'relationship_type_cousin_male' => 'cousin', + 'relationship_type_cousin_with_name' => 'saudara :name', + 'relationship_type_cousin_female_with_name' => 'saudara :name', + 'relationship_type_cousin_male_with_name' => ':name’s cousin', + + 'relationship_type_godfather' => 'godparent', + 'relationship_type_godfather_female' => 'wali perempuan', + 'relationship_type_godfather_male' => 'godfather', + 'relationship_type_godfather_with_name' => ':name’s godparent', + 'relationship_type_godfather_female_with_name' => 'Wali perempuan :name', + 'relationship_type_godfather_male_with_name' => ':name’s godfather', + + 'relationship_type_godson' => 'godchild', + 'relationship_type_godson_female' => 'anak perempuan wali', + 'relationship_type_godson_male' => 'godson', + 'relationship_type_godson_with_name' => ':name’s godchild', + 'relationship_type_godson_female_with_name' => 'Anak perempuan wali :name', + 'relationship_type_godson_male_with_name' => ':name’s godson', + + 'relationship_type_friend' => 'teman', + 'relationship_type_friend_female' => 'teman', + 'relationship_type_friend_male' => 'friend', + 'relationship_type_friend_with_name' => 'Teman :name', + 'relationship_type_friend_female_with_name' => 'Teman :name', + 'relationship_type_friend_male_with_name' => ':name’s friend', + + 'relationship_type_bestfriend' => 'teman akrab', + 'relationship_type_bestfriend_female' => 'teman akrab', + 'relationship_type_bestfriend_male' => 'best friend', + 'relationship_type_bestfriend_with_name' => 'Sahabat :name', + 'relationship_type_bestfriend_female_with_name' => 'Sahabat :name', + 'relationship_type_bestfriend_male_with_name' => ':name’s best friend', + + 'relationship_type_colleague' => 'kolega', + 'relationship_type_colleague_female' => 'kolega', + 'relationship_type_colleague_male' => 'colleague', + 'relationship_type_colleague_with_name' => 'Kolega :name', + 'relationship_type_colleague_female_with_name' => 'Kolega :name', + 'relationship_type_colleague_male_with_name' => ':name’s colleague', + + 'relationship_type_boss' => 'bos', + 'relationship_type_boss_female' => 'bos', + 'relationship_type_boss_male' => 'boss', + 'relationship_type_boss_with_name' => 'bos :name', + 'relationship_type_boss_female_with_name' => 'bos :name', + 'relationship_type_boss_male_with_name' => ':name’s boss', + + 'relationship_type_subordinate' => 'bawahan', + 'relationship_type_subordinate_female' => 'bawahan', + 'relationship_type_subordinate_male' => 'subordinate', + 'relationship_type_subordinate_with_name' => 'bawahan :name', + 'relationship_type_subordinate_female_with_name' => 'bawahan :name', + 'relationship_type_subordinate_male_with_name' => ':name’s subordinate', + + 'relationship_type_mentor' => 'pelatih', + 'relationship_type_mentor_female' => 'pelatih', + 'relationship_type_mentor_male' => 'mentor', + 'relationship_type_mentor_with_name' => 'Pelatih :name', + 'relationship_type_mentor_female_with_name' => 'Pelatih :name', + 'relationship_type_mentor_male_with_name' => ':name’s mentor', + + 'relationship_type_protege' => 'protégé', + 'relationship_type_protege_female' => 'protégé', + 'relationship_type_protege_male' => 'protégé', + 'relationship_type_protege_with_name' => ':name’s protégé', + 'relationship_type_protege_female_with_name' => ':name’s protégé', + 'relationship_type_protege_male_with_name' => ':name’s protégé', + + 'relationship_type_ex_husband' => 'ex-spouse', + 'relationship_type_ex_husband_female' => 'mantan istri', + 'relationship_type_ex_husband_male' => 'ex-husband', + 'relationship_type_ex_husband_with_name' => ':name’s ex-spouse', + 'relationship_type_ex_husband_female_with_name' => 'Mantan istri :name', + 'relationship_type_ex_husband_male_with_name' => ':name’s ex-husband', + + // emotions + 'emotion_primary_love' => 'Percintaan', + 'emotion_primary_joy' => 'Gembira', + 'emotion_primary_surprise' => 'Kejutan', + 'emotion_primary_anger' => 'Marah', + 'emotion_primary_sadness' => 'Sedih', + 'emotion_primary_fear' => 'Takut', + + 'emotion_secondary_affection' => 'Kasih sayang', + 'emotion_secondary_lust' => 'Nafsu', + 'emotion_secondary_longing' => 'Kangen', + 'emotion_secondary_cheerfulness' => 'Kegembiraan', + 'emotion_secondary_zest' => 'Gembira', + 'emotion_secondary_contentment' => 'Nafsu', + 'emotion_secondary_pride' => 'Kebanggaan', + 'emotion_secondary_optimism' => 'Optimisme', + 'emotion_secondary_enthrallment' => 'Kegirangan', + 'emotion_secondary_relief' => 'Kelegaan', + 'emotion_secondary_surprise' => 'Kejutan', + 'emotion_secondary_irritation' => 'Iritasi', + 'emotion_secondary_exasperation' => 'Kegembiraan', + 'emotion_secondary_rage' => 'Kemarahan', + 'emotion_secondary_disgust' => 'Kenajisan', + 'emotion_secondary_envy' => 'Iri', + 'emotion_secondary_suffering' => 'Kesakitan', + 'emotion_secondary_sadness' => 'Kesedihan', + 'emotion_secondary_disappointment' => 'Kecewa', + 'emotion_secondary_shame' => 'Malu', + 'emotion_secondary_neglect' => 'Ditinggalkan', + 'emotion_secondary_sympathy' => 'Simpati', + 'emotion_secondary_horror' => 'Kengerian', + 'emotion_secondary_nervousness' => 'Gerogi', + + 'emotion_adoration' => 'Pemujaan', + 'emotion_affection' => 'Kasih sayang', + 'emotion_love' => 'Percintaan', + 'emotion_fondness' => 'Kesukaan', + 'emotion_liking' => 'Kesukaan', + 'emotion_attraction' => 'Daya Tarik', + 'emotion_caring' => 'Kepedulian', + 'emotion_tenderness' => 'Kelembutan', + 'emotion_compassion' => 'Belas Kasih', + 'emotion_sentimentality' => 'Sentimental', + 'emotion_arousal' => 'Gairah', + 'emotion_desire' => 'Keinginan', + 'emotion_lust' => 'Nafsu', + 'emotion_passion' => 'Semangat', + 'emotion_infatuation' => 'Jatuh Hati', + 'emotion_longing' => 'Kangen', + 'emotion_amusement' => 'Hiburan', + 'emotion_bliss' => 'Kebahagiaan', + 'emotion_cheerfulness' => 'Kegembiraan', + 'emotion_gaiety' => 'Keriangan', + 'emotion_glee' => 'Keriaan', + 'emotion_jolliness' => 'Kegirangan', + 'emotion_joviality' => 'Keriangan', + 'emotion_joy' => 'Gembira', + 'emotion_delight' => 'Kegembiraan', + 'emotion_enjoyment' => 'Kenikmatan', + 'emotion_gladness' => 'Kepuasan', + 'emotion_happiness' => 'Kebahagiaan', + 'emotion_jubilation' => 'Sorak Sorai', + 'emotion_elation' => 'Kegirangan Hati', + 'emotion_satisfaction' => 'Kepuasan', + 'emotion_ecstasy' => 'Sukacita', + 'emotion_euphoria' => 'Uforia', + 'emotion_enthusiasm' => 'Antusiasme', + 'emotion_zeal' => 'Semangat', + 'emotion_zest' => 'Gembira', + 'emotion_excitement' => 'Kegembiraan', + 'emotion_thrill' => 'Sensasi', + 'emotion_exhilaration' => 'Kegembiraan', + 'emotion_contentment' => 'Nafsu', + 'emotion_pleasure' => 'Kesenangan', + 'emotion_pride' => 'Kebanggaan', + 'emotion_eagerness' => 'Keinginan', + 'emotion_hope' => 'Harapan', + 'emotion_optimism' => 'Optimisme', + 'emotion_enthrallment' => 'Kegirangan', + 'emotion_rapture' => 'Kegirangan', + 'emotion_relief' => 'Kelegaan', + 'emotion_amazement' => 'Kekaguman', + 'emotion_surprise' => 'Kejutan', + 'emotion_astonishment' => 'Keheranan', + 'emotion_aggravation' => 'Kejengkelan', + 'emotion_irritation' => 'Iritasi', + 'emotion_agitation' => 'Agitasi', + 'emotion_annoyance' => 'Gangguan', + 'emotion_grouchiness' => 'Menggerutu', + 'emotion_grumpiness' => 'Sifat galak', + 'emotion_exasperation' => 'Kegembiraan', + 'emotion_frustration' => 'Frustrasi', + 'emotion_anger' => 'Marah', + 'emotion_rage' => 'Marah', + 'emotion_outrage' => 'Kekejaman', + 'emotion_fury' => 'Marah Besar', + 'emotion_wrath' => 'Wrath', + 'emotion_hostility' => 'Perseteruan', + 'emotion_ferocity' => 'Keganasan', + 'emotion_bitterness' => 'Kepahitan', + 'emotion_hate' => 'Benci', + 'emotion_loathing' => 'Kebencian', + 'emotion_scorn' => 'Cemooh', + 'emotion_spite' => 'Dendam', + 'emotion_vengefulness' => 'Rasa Dendam', + 'emotion_dislike' => 'Tidak suka', + 'emotion_resentment' => 'Kebencian', + 'emotion_disgust' => 'Kenajisan', + 'emotion_revulsion' => 'Rasa Muka', + 'emotion_contempt' => 'Penghinaan', + 'emotion_envy' => 'Iri', + 'emotion_jealousy' => 'Kecemburuan', + 'emotion_agony' => 'Penderitaan Mendalam', + 'emotion_suffering' => 'Kesakitan', + 'emotion_hurt' => 'Tersakiti', + 'emotion_anguish' => 'Penderitaan Berat', + 'emotion_depression' => 'Depresi', + 'emotion_despair' => 'Putus Asa', + 'emotion_hopelessness' => 'Keputusasan', + 'emotion_gloom' => 'Suram', + 'emotion_glumness' => 'Kesuraman', + 'emotion_sadness' => 'Kesedihan', + 'emotion_unhappiness' => 'Ketidakbahagiaan', + 'emotion_grief' => 'Duka', + 'emotion_sorrow' => 'Nestapa', + 'emotion_woe' => 'Duka', + 'emotion_misery' => 'Penderitaan', + 'emotion_melancholy' => 'Melankolis', + 'emotion_dismay' => 'Kecemasan', + 'emotion_disappointment' => 'Kecewa', + 'emotion_displeasure' => 'Ketidaksenangan', + 'emotion_guilt' => 'Rasa Bersalah', + 'emotion_shame' => 'Malu', + 'emotion_regret' => 'Penyesalan', + 'emotion_remorse' => 'Penyesalan', + 'emotion_alienation' => 'Pengasingan', + 'emotion_isolation' => 'Isolasi', + 'emotion_neglect' => 'Ditinggalkan', + 'emotion_loneliness' => 'Kesendirian', + 'emotion_rejection' => 'Penolakan', + 'emotion_homesickness' => 'Rindu Rumah', + 'emotion_defeat' => 'Kekalahan', + 'emotion_dejection' => 'Kekesalan', + 'emotion_insecurity' => 'Gelisah', + 'emotion_embarrassment' => 'Rasa Malu', + 'emotion_humiliation' => 'Penghinaan', + 'emotion_insult' => 'Menghina', + 'emotion_pity' => 'Mengasihani', + 'emotion_sympathy' => 'Simpati', + 'emotion_alarm' => 'Khawatir', + 'emotion_shock' => 'Syok', + 'emotion_fear' => 'Takut', + 'emotion_fright' => 'Ketakutan', + 'emotion_horror' => 'Kengerian', + 'emotion_terror' => 'Teror', + 'emotion_panic' => 'Panik', + 'emotion_hysteria' => 'Histeris', + 'emotion_mortification' => 'Malu', + 'emotion_anxiety' => 'Kegelisahan', + 'emotion_nervousness' => 'Gerogi', + 'emotion_tenseness' => 'Ketegangan', + 'emotion_uneasiness' => 'Rasa Gelisah', + 'emotion_apprehension' => 'Prihatin', + 'emotion_worry' => 'Khawatir', + 'emotion_distress' => 'Kesulitan', + 'emotion_dread' => 'Ketakutan', + + // weather + 'weather_sunny' => 'Sunny', + 'weather_clear' => 'Clear', + 'weather_clear-day' => 'Clear', + 'weather_clear-night' => 'Malam yang cerah', + 'weather_light-drizzle' => 'Light drizzle', + 'weather_patchy-light-drizzle' => 'Patchy light drizzle', + 'weather_patchy-light-rain' => 'Patchy light rain', + 'weather_light-rain' => 'Light rain', + 'weather_moderate-rain-at-times' => 'Moderate rain at times', + 'weather_moderate-rain' => 'Moderate rain', + 'weather_patchy-rain-possible' => 'Patchy rain possible', + 'weather_heavy-rain-at-times' => 'Heavy rain at times', + 'weather_heavy-rain' => 'Heavy rain', + 'weather_light-freezing-rain' => 'Light freezing rain', + 'weather_moderate-or-heavy-freezing-rain' => 'Moderate or heavy freezing rain', + 'weather_light-sleet' => 'Light sleet', + 'weather_moderate-or-heavy-rain-shower' => 'Moderate or heavy rain shower', + 'weather_light-rain-shower' => 'Light rain shower', + 'weather_torrential-rain-shower' => 'Torrential rain shower', + 'weather_rain' => 'Hujan', + 'weather_snow' => 'Salju', + 'weather_blowing-snow' => 'Blowing snow', + 'weather_patchy-light-snow' => 'Patchy light snow', + 'weather_light-snow' => 'Light snow', + 'weather_patchy-moderate-snow' => 'Patchy moderate snow', + 'weather_moderate-snow' => 'Moderate snow', + 'weather_patchy-heavy-snow' => 'Patchy heavy snow', + 'weather_heavy-snow' => 'Heavy snow', + 'weather_light-snow-showers' => 'Light snow showers', + 'weather_moderate-or-heavy-snow-showers' => 'Moderate or heavy snow showers', + 'weather_patchy-snow-possible' => 'Patchy snow possible', + 'weather_patchy-sleet-possible' => 'Patchy sleet possible', + 'weather_moderate-or-heavy-sleet' => 'Moderate or heavy sleet', + 'weather_light-sleet-showers' => 'Light sleet showers', + 'weather_moderate-or-heavy-sleet-showers' => 'Moderate or heavy sleet showers', + 'weather_sleet' => 'Hujan Es', + 'weather_wind' => 'Angin', + 'weather_fog' => 'Kabut', + 'weather_freezing-fog' => 'Freezing fog', + 'weather_mist' => 'Mist', + 'weather_blizzard' => 'Blizzard', + 'weather_overcast' => 'Overcast', + 'weather_cloudy' => 'Berawan', + 'weather_partly-cloudy-day' => 'Partly cloudy', + 'weather_partly-cloudy-night' => 'Partly cloudy', + 'weather_freezing-drizzle' => 'Freezing drizzle', + 'weather_heavy-freezing-drizzle' => 'Heavy freezing drizzle', + 'weather_patchy-freezing-drizzle-possible' => 'Patchy freezing drizzle possible', + 'weather_ice-pellets' => 'Ice pellets', + 'weather_light-showers-of-ice-pellets' => 'Light showers of ice pellets', + 'weather_moderate-or-heavy-showers-of-ice-pellets' => 'Moderate or heavy showers of ice pellets', + 'weather_thundery-outbreaks-possible' => 'Thundery outbreaks possible', + 'weather_patchy-light-rain-with-thunder' => 'Patchy light rain with thunder', + 'weather_moderate-or-heavy-rain-with-thunder' => 'Moderate or heavy rain with thunder', + 'weather_patchy-light-snow-with-thunder' => 'Patchy light snow with thunder', + 'weather_moderate-or-heavy-snow-with-thunder' => 'Moderate or heavy snow with thunder', + 'weather_current_temperature_celsius' => ':temperatur °C', + 'weather_current_temperature_fahrenheit' => ':temperature °F', + 'weather_current_title' => 'Cuaca saat ini', + + // dav + 'dav_contacts' => 'Kontak', + 'dav_contacts_description' => 'Kontak :name', + 'dav_birthdays' => 'Ulang tahun', + 'dav_birthdays_description' => 'Ulang tahun kontak :name', + 'dav_tasks' => 'Tugas', + 'dav_tasks_description' => 'Tugas :name', + + // contact list + 'contact_list_avatar' => 'Avatar', + 'contact_list_name' => 'Kontak', + 'contact_list_description' => 'Deskripsi', + +]; diff --git a/resources/lang/id/auth.php b/resources/lang/id/auth.php new file mode 100644 index 0000000..9e10dcd --- /dev/null +++ b/resources/lang/id/auth.php @@ -0,0 +1,89 @@ + 'Kredensial ini tidak cocok dengan catatan kami.', + 'throttle' => 'Terlalu banyak upaya login. Silakan coba lagi dalam :seconds detik.', + 'not_authorized' => 'Anda tidak memiliki izin untuk menjalankan tindakan ini', + 'signup_disabled' => 'Pendaftaran saat ini dinonaktifkan', + 'signup_error' => 'Terjadi kesalahan saat mencoba mendaftarkan pengguna', + 'back_homepage' => 'Kembali ke halaman beranda', + 'mfa_auth_otp' => 'Otentikasi dengan perangkat dua faktor Anda', + 'mfa_auth_webauthn' => 'Otentikasi dengan sebuah kunci keamanan (WebAuthn)', + '2fa_title' => 'Otentikasi Dua Faktor', + '2fa_wrong_validation' => 'Otentikasi dua faktor telah gagal.', + '2fa_one_time_password' => 'Kode otentikasi dua faktor', + '2fa_recuperation_code' => 'Masukkan sebuah kode pemulihan dua faktor', + '2fa_one_time_or_recuperation' => 'Masukkan sebuah kode otentikasi dua faktor atau sebuah kode pemulihan', + '2fa_otp_help' => 'Buka aplikasi seluler otentikasi dua faktor Anda dan salin kode tersebut', + + 'login_to_account' => 'Masuk ke akun Anda', + 'login_with_recovery' => 'Masuk dengan sebuah kode pemulihan', + 'login_again' => 'Silakan masuk lagi ke akun Anda', + 'email' => 'Email', + 'password' => 'Kata sandi', + 'recovery' => 'Kode pemulihan', + 'login' => 'Masuk', + 'button_remember' => 'Ingat Saya', + 'password_forget' => 'Lupa kata sandi Anda?', + 'password_reset' => 'Atur ulang kata sandi Anda', + 'use_recovery' => 'Atau kamu bisa menggunakan sebuah kode pemulihan', + 'signup_no_account' => 'Tidak punya akun?', + 'signup' => 'Daftar', + 'create_account' => 'Buat akun pertama dengan mendaftar', + 'change_language_title' => 'Ganti bahasa:', + 'change_language' => 'Ganti bahasa ke :lang', + + 'password_reset_title' => 'Atur ulang kata sandi', + 'password_reset_email' => 'Alamat Email', + 'password_reset_send_link' => 'Kirim Tautan Atur Ulang Kata Sandi', + 'password_reset_password' => 'Kata sandi', + 'password_reset_password_confirm' => 'Komfirmasi Kata Sandi', + 'password_reset_action' => 'Atur ulang kata sandi', + 'password_reset_email_content' => 'Klik di sini untuk mengatur ulang kata sandi Anda:', + + 'register_title_welcome' => 'Wilujeng sumping ke contoh pemasangan Monica Anda yang baru saja dipasang', + 'register_create_account' => 'Anda harus membuat sebuah akun untuk menggunakan Monica', + 'register_title_create' => 'Buat Akun Monica Anda', + 'register_login' => 'Masuk jika Anda sudah memiliki akun.', + 'register_email' => 'Masukkan sebuah alamat email yang valid', + 'register_email_example' => 'kamu@beranda', + 'register_firstname' => 'Nama depan', + 'register_firstname_example' => 'mis. Asep', + 'register_lastname' => 'Nama keluarga', + 'register_lastname_example' => 'mis. Surasep', + 'register_password' => 'Kata sandi', + 'register_password_example' => 'Masukkan sebuah kata sandi yang aman', + 'register_password_confirmation' => 'Konfirmasi kata sandi', + 'register_action' => 'Daftar', + 'register_policy' => 'Dengan mendaftar menandakan Anda telah membaca dan menyetujui Kebijakan Privasi dan Ketentuan penggunaan.', + 'register_invitation_email' => 'Untuk tujuan keamanan, silahkan cantumkan email orang yang telah mengundang Anda untuk bergabung dengan akun ini. Informasi ini disediakan dalam email invitasi.', + + 'confirmation_title' => 'Verifikasi Alamat Email Anda', + 'confirmation_fresh' => 'Tautan verifikasi baru telah dikirimkan ke alamat email Anda.', + 'confirmation_check' => 'Sebelum melanjutkan, silakan periksa email Anda untuk sebuah tautan verifikasi.', + 'confirmation_request_another' => 'Jika Anda tidak menerima email >klik di sini untuk meminta lagi.', + + 'confirmation_again' => 'Jika Anda ingin mengganti alamat email Anda, Anda bisa klik di sini.', + 'email_change_current_email' => 'Alamat email saat ini:', + 'email_change_title' => 'Ganti alamat email Anda', + 'email_change_new' => 'Alamat email baru', + 'email_changed' => 'Alamat email Anda telah diubah. Periksa kotak surat Anda untuk memvalidasinya.', +]; diff --git a/resources/lang/id/changelog.php b/resources/lang/id/changelog.php new file mode 100644 index 0000000..7850ed1 --- /dev/null +++ b/resources/lang/id/changelog.php @@ -0,0 +1,12 @@ + 'Perubahan produk', + 'note' => 'Catatan: Sangat disayangkan, halaman ini hanya dalam bahasa Inggris.', +]; diff --git a/resources/lang/id/dashboard.php b/resources/lang/id/dashboard.php new file mode 100644 index 0000000..28104fa --- /dev/null +++ b/resources/lang/id/dashboard.php @@ -0,0 +1,42 @@ + 'Wilujeng sumping ke akun Anda!', + 'dashboard_blank_description' => 'Monica adalah tempat untuk mengatur semua interaksi yang Anda miliki dengan orang-orang yang Anda sayangi.', + 'dashboard_blank_cta' => 'Tambahkan kontak pertama Anda', + 'dashboard_blank_illustration' => 'Ilustrasi oleh Freepik', + + 'notes_title' => 'Anda belum punya catatan apa pun yang telah dilihat.', + + 'tab_recent_calls' => 'Panggilan terbaru', + 'tab_favorite_notes' => 'Catatan favorit', + 'tab_calls_blank' => 'Anda belum mencatat panggilan.', + 'tab_debts' => 'Hutang', + 'tab_debts_blank' => 'Anda belum mencatat hutang apa pun.', + 'tab_tasks' => 'Tugas', + 'tab_tasks_blank' => 'Anda belum punya tugas apa pun.', + + 'tasks_add_task_placeholder' => 'Tugas ini tentang apa?', + 'tasks_tab_your_contacts' => 'Tugas yang berhubungan dengan kontak Anda', + 'tasks_tab_your_tasks' => 'Tugas Anda', + 'tasks_add_note' => 'Tekan Enter untuk menambahkan tugas.', + 'task_add_cta' => 'Tambah sebuah tugas', + + 'debts_you_owe' => 'Anda berhutang', + + 'statistics_contacts' => 'Kontak', + 'statistics_activities' => 'Aktifitas', + 'statistics_gifts' => 'Hadiah', + + 'reminders_next_months' => 'Peristiwa dalam 3 bulan ke depan', + 'reminders_none' => 'Tidak ada pengingat untuk bulan ini.', + + 'product_changes' => 'Perubahan produk', + 'product_view_details' => 'Lihat rincian', +]; diff --git a/resources/lang/id/format.php b/resources/lang/id/format.php new file mode 100644 index 0000000..39c1a48 --- /dev/null +++ b/resources/lang/id/format.php @@ -0,0 +1,36 @@ + 'd M, Y H:i', + 'short_date_year' => 'd M, Y', + 'short_date' => 'd M', + 'short_month' => 'M', + 'short_month_year' => 'M Y', + 'short_day' => 'D', + 'full_date_year' => 'd F, Y', + 'full_month' => 'F', + 'full_month_year' => 'F Y', + 'full_hour' => 'h.i A', + + 'short_text' => '{text}…', +]; diff --git a/resources/lang/id/journal.php b/resources/lang/id/journal.php new file mode 100644 index 0000000..6cd1469 --- /dev/null +++ b/resources/lang/id/journal.php @@ -0,0 +1,38 @@ + 'Bagaimana hari Anda? Anda bisa menilainya sekali setiap hari.', + 'journal_come_back' => 'Terima kasih. Kembalilah besok untuk menilai hari Anda lagi.', + 'journal_description' => 'Catatan: Jurnal mencantumkan entri jurnal mandiri, dan entri otomatis seperti Aktifitas yang telah selesai dilakukan dengan kontak Anda. Meskipun Anda dapat menghapus entri jurnal secara mandiri, Anda harus menghapus aktifitas secara langsung pada halaman kontak.', + 'journal_add' => 'Tambahkan sebuah entri jurnal', + 'journal_edit' => 'Sunting sebuah entri jurnal', + 'journal_empty' => 'Jurnal kosong', + 'journal_created_at' => 'Dibuat pada {date}', + 'journal_created_automatically' => 'Dibuat secara otomatis', + 'journal_entry_type_journal' => 'Entri jurnal', + 'journal_entry_type_activity' => 'Aktifitas', + 'journal_entry_rate' => 'Anda telah menilai hari Anda.', + 'journal_add_comment' => 'Apakah Anda ingin untuk menambahkan komentar (opsional)?', + 'journal_show_comment' => 'Tampilkan komentar', + 'entry_delete_success' => 'Entri jurnal telah berhasil dihapus.', + 'journal_add_title' => 'Judul (opsional)', + 'journal_add_date' => 'Tanggal', + 'journal_add_post' => 'Entri', + 'journal_add_cta' => 'Simpan', + 'journal_blank_cta' => 'Tambahkan entri jurnal pertama Anda', + 'journal_blank_description' => 'Jurnal memungkinkan Anda menulis peristiwa yang terjadi pada Anda, dan mengingatnya.', + 'delete_confirmation' => 'Apakah Anda yakin ingin menghapus entri jurnal ini?', + 'apply_filter' => 'Apply filter', + 'start_date' => 'Start Date', + 'end_date' => 'End Date', + 'per_page' => 'Per Page', + 'sort_order' => 'Sort By Created At', + 'ascending' => 'Ascending', + 'descending' => 'Descending', +]; diff --git a/resources/lang/id/logs.php b/resources/lang/id/logs.php new file mode 100644 index 0000000..efe0473 --- /dev/null +++ b/resources/lang/id/logs.php @@ -0,0 +1,29 @@ + 'Membuat kontak.', + 'settings_log_contact_created_with_name' => 'Menambahkan :name sebagai sebuah kontak.', + + // contat description update + 'contact_log_contact_description_updated' => 'Memperbarui deskripsi.', + 'settings_log_contact_description_updated_with_name' => 'Memperbarui deskripsi :name.', + + // contact description clear + 'contact_log_contact_description_cleared' => 'Membersihkan deskripsi.', + 'settings_log_contact_description_cleared_with_name' => 'Membersihkan deskripsi :name.', + + // contact work information update + 'contact_log_contact_work_updated' => 'Memperbarui informasi pekerjaan.', + 'settings_log_contact_work_updated_with_name' => 'Memperbarui informasi pekerjaan dari :name.', + + // company created + 'settings_log_company_created' => 'Membuat sebuah perusahaan bernama :name.', +]; diff --git a/resources/lang/id/mail.php b/resources/lang/id/mail.php new file mode 100644 index 0000000..c2dd343 --- /dev/null +++ b/resources/lang/id/mail.php @@ -0,0 +1,53 @@ + 'Pengingat untuk :contact', + 'greetings' => 'Hai :username', + 'want_reminded_of' => 'Anda ingin diingatkan tentang :reason', + 'for' => 'Untuk: :name', + 'comment' => 'Komentar: :comment', + 'footer_contact_info' => 'Tambah, lihat, lengkapi, dan ubah informasi tentang kontak ini:', + 'footer_contact_info2' => 'Lihat profil :name', + 'footer_contact_info2_link' => 'Lihat profil :name: :url', + + 'notification_subject_line' => 'Anda memiliki sebuah peristiwa didepan', + 'notification_description' => 'Di :count hari (pada :date), peristiwa berikut akan terjadi:', + + 'stay_in_touch_subject_line' => 'Tetap berhubungan dengan :name', + 'stay_in_touch_subject_description' => 'Anda meminta untuk diingatkan untuk tetap berhubungan dengan :name setiap :frequency hari.', + + 'notifications_whoops' => 'Waduh!', + 'notifications_hello' => 'Halo!', + 'notifications_regards' => 'Salam', + 'notifications_footer' => 'Jika Anda mengalami masalah meng-klik tombol ":actionText", salin dan tempel alamat URL dibawah ini ke peramban web Anda : [:actionURL](:actionURL)', + 'notifications_rights' => 'Hak cipta', + + 'confirmation_email_title' => 'Monica – Verifikasi email', + 'confirmation_email_intro'=> 'Untuk memvalidasi email Anda klik pada tombol di bawah ini', + 'confirmation_email_button' => 'Verifikasi alamat email', + 'confirmation_email_bottom' => 'Jika Anda tidak membuat sebuah akun, tidak diperlukan tindakan lebih lanjut.', + + 'password_reset_title' => 'Monica – Pemberitahuan Atur Ulang Kata Sandi', + 'password_reset_intro' => 'Anda menerima email ini karena kami menerima permintaan pengaturan ulang kata sandi untuk akun Anda.', + 'password_reset_button' => 'Atur Ulang Kata Sandi', + 'password_reset_expiration' => 'Tautan pengaturan ulang kata sandi ini akan beerakhir dalam :count menit.', + 'password_reset_bottom' => 'Jika Anda tidak meminta pengaturan ulang kata sandi, tidak diperlukan tindakan lebih lanjut.', + + 'invitation_title' => 'Monica – Anda diundang oleh :name', + 'invitation_intro' => 'Kamu telah diundang oleh :name (:email) untuk menggunakan Monica, sebuah alat Pengelola Kontak Relasi Pribadi yang bagus.', + 'invitation_link' => 'Untuk menerima undangan, klik tautan di bawah ini:', + 'invitation_button' => 'Terima undangan', + 'invitation_expiration' => 'Tautan ini akan berakhir pada :count hari.', + + 'export_title' => 'Your export is ready', + 'export_description' => 'You requested a data export on :date. It is now ready to download.', + 'export_download' => 'Download export', + +]; diff --git a/resources/lang/id/pagination.php b/resources/lang/id/pagination.php new file mode 100644 index 0000000..cee0c26 --- /dev/null +++ b/resources/lang/id/pagination.php @@ -0,0 +1,25 @@ + '❮ Sebelumnya', + 'next' => 'Selanjutnya ❯', + +]; diff --git a/resources/lang/id/passwords.php b/resources/lang/id/passwords.php new file mode 100644 index 0000000..94203b1 --- /dev/null +++ b/resources/lang/id/passwords.php @@ -0,0 +1,30 @@ + 'Kata sandi Anda telah diatur ulang!', + 'sent' => 'Jika email yang Anda masukkan ada/tersedia dalam catatan kami, Anda telah dikirimkan sebuah tautan pengaturan ulang kata sandi.', + 'token' => 'Token pengaturan ulang kata sandi ini tidak valid.', + 'user' => 'Jika email yang Anda masukkan ada/tersedia dalam catatan kami, Anda telah dikirimkan sebuah tautan pengaturan ulang kata sandi.', + 'changed' => 'Kata sandi berhasil diganti.', + 'invalid' => 'Kata sandi yang Anda masukkan saat ini tidak benar.', + 'throttled' => 'Silahkan tunggu sebelum mencoba lagi.', + +]; diff --git a/resources/lang/id/people.php b/resources/lang/id/people.php new file mode 100644 index 0000000..2245918 --- /dev/null +++ b/resources/lang/id/people.php @@ -0,0 +1,539 @@ + 'Kontak tidak ditemukan', + 'people_list_number_kids' => ':count anak', + 'people_list_last_updated' => 'Terakhir konsultasi:', + 'people_list_number_reminders' => ':count pengingat', + 'people_list_blank_title' => 'Anda tidak mempunyai siapapun di akun Anda', + 'people_list_blank_cta' => 'Tambahkan seseorang', + 'people_list_sort' => 'Urutkan', + 'people_list_stats' => ':count kontak', + 'people_list_firstnameAZ' => 'Urutkan berdasarkan nama depan A → Z', + 'people_list_firstnameZA' => 'Urutkan berdasarkan nama depan Z → A', + 'people_list_lastnameAZ' => 'Urutkan berdasarkan nama belakang A → Z', + 'people_list_lastnameZA' => 'Urutkan berdasarkan nama belakang Z → A', + 'people_list_lastactivitydateNewtoOld' => 'Urutkan berdasarkan tanggal aktivitas, terbaru sampai terlama', + 'people_list_lastactivitydateOldtoNew' => 'Urutkan berdasarkan tanggal aktivitas, terlama sampai terbaru', + 'people_list_filter_tag' => 'Menampilkan semua kontak dengan tag', + 'people_list_clear_filter' => 'Hapus filter', + 'people_list_contacts_per_tags' => ':count kontak', + 'people_list_show_dead' => 'Tampilkan orang yang telah tiada (:count)', + 'people_list_hide_dead' => 'Sembunyikan orang yang telah tiada (:count)', + 'people_search' => 'Cari kontak Anda…', + 'people_search_no_results' => 'Tidak ada hasil ditemukan', + 'people_search_next' => 'Berikutnya', + 'people_search_prev' => 'Sebelumnya', + 'people_search_rows_per_page' => 'Baris per halaman', + 'people_search_of' => 'dari', + 'people_search_page' => 'Halaman', + 'people_search_all' => 'Semua', + 'people_add_new' => 'Tambah orang baru', + 'people_list_account_usage' => 'Pemakaian akun Anda: :current/:limit kontak', + 'people_list_account_upgrade_title' => 'Perbarui akun Anda untuk membukanya dengan potensial penuh.', + 'people_list_account_upgrade_cta' => 'Perbarui sekarang', + 'people_list_untagged' => 'Tampilkan kontak belum mempunyai tag', + 'people_list_filter_untag' => 'Menampilkan semua kontak tanpa tag', + 'archived_contact_readonly' => 'Kontak yang diarsipkan tidak dapat disunting, silahkan buka arsip terlebih dahulu.', + + // people add + 'people_add_title' => 'Tambah orang baru', + 'people_add_missing' => 'Tidak ada orang ditemukan – tambahkan satu yang baru sekarang', + 'people_add_firstname' => 'Nama depan', + 'people_add_middlename' => 'Nama tengah (opsional)', + 'people_add_lastname' => 'Nama belakang (opsional)', + 'people_add_email' => 'Email (opsional)', + 'people_add_nickname' => 'Nama panggilan (opsional)', + 'people_add_cta' => 'Tambah', + 'people_save_and_add_another_cta' => 'Kirimkan dan tambah orang yang lain', + 'people_add_success' => ':name telah berhasil dibuat', + 'people_add_gender' => 'Jenis kelamin', + 'people_delete_success' => 'Kontak telah dihapus', + 'people_delete_message' => 'Hapus kontak', + 'people_delete_confirmation' => 'Apakah Anda yakin ingin menghapus kontak :name ini? Penghapusan bersifat langsung dan permanen.', + 'people_add_birthday_reminder' => 'Ucapkan selamat ulang tahun ke :name', + 'people_add_birthday_reminder_deceased' => 'Pada tanggal ini, :name telah merayakan hari ulang tahun mereka', + 'people_add_import' => 'Apakah Anda ingin mengimpor kontak Anda?', + 'people_edit_email_error' => 'Telah ada sebuah kontak dengan alamat email ini pada kontak Anda. Silahkan pilih yang lain.', + 'people_export' => 'Ekspor sebagai vCard', + 'people_add_reminder_for_birthday' => 'Buat sebuah pengingat ulang tahun tahunan', + + // show + 'section_contact_information' => 'Informasi kontak', + 'section_personal_activities' => 'Aktifitas', + 'section_personal_reminders' => 'Pengingat', + 'section_personal_tasks' => 'Tugas', + 'section_personal_gifts' => 'Hadiah', + 'section_personal_notes' => 'Catatan', + + // archived contacts + 'list_link_to_active_contacts' => 'Anda sedang melihat kontak yang diarsipkan. Lihat daftar kontak aktif sebagai gantinya.', + 'list_link_to_archived_contacts' => 'Daftar kontak yang diarsipkan', + + // Header + 'me' => 'Ini adalah Anda', + 'edit_contact_information' => 'Sunting informasi kontak', + 'contact_archive' => 'Arsipkan kontak', + 'contact_unarchive' => 'Batal Arsipkan kontak', + 'contact_archive_help' => 'Kontak yang diarsipkan tidak ditampilkan pada daftar kontak, tetapi masih tetap muncul pada hasil pencarian.', + 'call_button' => 'Catat sebuah panggilan', + 'set_favorite' => 'Kontak favorit ditempatkan di bagian atas dari daftar kontak', + + // Stay in touch + 'stay_in_touch' => 'Tetap berhubungan', + 'stay_in_touch_frequency' => 'Tetap berhubungan setiap {count} hari', + 'stay_in_touch_next_date' => 'Next due: {date}', + 'stay_in_touch_invalid' => 'Frekuensinya harus berupa angka yang lebih besar dari 0.', + 'stay_in_touch_premium' => 'Anda perlu meningkatkan akun Anda untuk memanfaatkan fitur ini', + 'stay_in_touch_modal_title' => 'Tetap berhubungan', + 'stay_in_touch_modal_desc' => 'Kami dapat mengingatkan Anda melalui email untuk tetap berhubungan dengan {firstname} pada sebuah interval reguler.', + 'stay_in_touch_modal_label' => 'Kirimkan saya email setiap… {count} hari', + + // Calls + 'modal_call_title' => 'Catat sebuah panggilan', + 'modal_call_comment' => 'Yang Anda bicarakan tentang apa? (opsional)', + 'modal_call_exact_date' => 'Panggilan telepon tersebut terjadi pada', + 'modal_call_who_called' => 'Siapa yang memanggil?', + 'modal_call_emotion' => 'Apakah Anda ingin mencatat bagaimana perasaan Anda selama panggilan ini? (opsional)', + 'calls_add_success' => 'Panggilan telepon telah disimpan.', + 'call_delete_confirmation' => 'Apakah Anda ingin menghapus panggilan ini?', + 'call_delete_success' => 'Panggilan telah berhasil dihapus', + 'call_title' => 'Panggilan telepon', + 'call_empty_comment' => 'Tanpa rincian', + 'call_blank_title' => 'Pantau panggilan telepon yang telah Anda lakukan dengan {name}', + 'call_blank_desc' => 'Anda memanggil {name}', + 'call_you_called' => 'Anda memanggil', + 'call_he_called' => '{name} memanggil', + 'call_emotions' => 'Perasaan:', + + // Conversation + 'conversation_blank' => 'Rekam percakapan yang Anda miliki dengan :name di media sosial, SMS…', + 'conversation_delete_link' => 'Hapus percakapan', + 'conversation_edit_title' => 'Sunting percakapan', + 'conversation_edit_delete' => 'Apakah kamu yakin ingin menghapus percakapan ini? Penghapusan bersifat permanen.', + 'conversation_add_success' => 'Percakapan telah berhasil ditambahkan.', + 'conversation_edit_success' => 'Percakapan telah berhasil diperbarui.', + 'conversation_delete_success' => 'Percakapan telah berhasil dihapus.', + 'conversation_add_title' => 'Rekam sebuah percakapan baru', + 'conversation_add_when' => 'Kapan Anda melakukan percakapan ini?', + 'conversation_add_who_wrote' => 'Siapa yang mengirim pesan ini?', + 'conversation_add_how' => 'Bagaimana Anda berkomunikasi?', + 'conversation_add_you' => 'Anda', + 'conversation_add_content' => 'Tulis apa yang telah dikatakan', + 'conversation_add_what_was_said' => 'Apa yang Anda katakan?', + 'conversation_add_another' => 'Tambah pesan lainnya', + 'conversation_add_error' => 'Anda harus menambahkan setidaknya satu pesan.', + 'conversation_list_table_messages' => 'Pesan', + 'conversation_list_table_content' => 'Konten sebagian (pesan terakhir)', + 'conversation_list_title' => 'Percakapan', + 'conversation_list_cta' => 'Catat percakapan', + + // age - birthday + 'birthdate_not_set' => 'Ulang tahun tidak diatur', + 'age_approximate_in_years' => 'sekitar :age tahun', + 'age_exact_in_years' => ':age tahun', + 'age_exact_birthdate' => 'lahir :date', + + // Last called + 'last_called' => 'Terakhir memanggil: :date', + 'last_talked_to' => 'Last called: {date}', + 'last_called_empty' => 'Terakhir memanggil: :date', + 'last_activity_date' => 'Aktifitas terakhir bersama: :date', + 'last_activity_date_empty' => 'Aktifitas terakhir bersama: :date', + + // additional information + 'information_edit_success' => 'Profil telah berhasil diperbarui', + 'information_edit_title' => 'Sunting informasi pribadi :name', + 'information_edit_max_size' => 'Maks :size Kb.', + 'information_edit_max_size2' => 'Max {size} Kb.', + 'information_edit_firstname' => 'Nama depan', + 'information_edit_lastname' => 'Nama belakang (opsional)', + 'information_edit_description' => 'Deskripsi (opsional)', + 'information_edit_description_help' => 'Digunakan pada daftar kontak untuk menambahkan beberapa konteks, jika diperlukan.', + 'information_edit_unknown' => 'Saya tidak tau umur orang ini', + 'information_edit_probably' => 'Orang ini mungkin…', + 'information_edit_not_year' => 'Saya mengetahui hari dan bulan ulang tahun orang ini, tetapi tidak tahunnya…', + 'information_edit_exact' => 'Saya tau dengan tepat hari ulang tahun orang ini…', + 'information_edit_birthdate_label' => 'Ulang tahun', + 'information_no_work_defined' => 'Tidak ada informasi pekerjaan yang ditentukan', + 'information_work_at' => 'di :company', + 'work_add_cta' => 'Perbarui informasi pekerjaan', + 'work_edit_success' => 'Informasi pekerjaan diperbarui', + 'work_edit_title' => 'Perbarui informasi pekerjaan :name', + 'work_edit_job' => 'Judul pekerjaan (opsional)', + 'work_edit_company' => 'Perusahaan (opsional)', + 'work_information' => 'Informasi pekerjaan', + + // food preferences + 'food_preferences_add_success' => 'Preferensi makanan telah disimpan', + 'food_preferences_edit_description' => 'Mungkin :firstname atau seseorang di keluarga :family memiliki sebuah alergi. Atau tidak suka sebotol anggur tertentu. Cantumkan mereka di sini sehingga Anda akan mengingatnya lain kali Anda mengundang mereka untuk makan malam', + 'food_preferences_edit_description_no_last_name' => 'Mungkin :firstname memiliki alergi. Atau tidak suka sebotol anggur tertentu. Cantumkan mereka di sini sehingga Anda akan mengingatnya lain kali Anda mengundang mereka untuk makan malam', + 'food_preferences_edit_title' => 'Cantumkan preferensi makanan', + 'food_preferences_edit_cta' => 'Simpan preferensi makanan', + 'food_preferences_title' => 'Preferensi makanan', + 'food_preferences_cta' => 'Tambahkan preferensi makanan', + + // reminders + 'reminders_blank_title' => 'Apakah ada sesuatu yang Anda ingin diingatkan tentang :name?', + 'reminders_blank_add_activity' => 'Tambahkan sebuah pengingat', + 'reminders_add_title' => 'Apa yang Anda ingin diingatkan tentang :name?', + 'reminders_add_description' => 'Tolong ingatkan saya untuk…', + 'reminders_add_next_time' => 'Kapan Anda ingin diingatkan tentang hal ini lain kali?', + 'reminders_add_once' => 'Ingatkan saya tentang ini sekali saja', + 'reminders_add_recurrent' => 'Ingatkan saya tentang ini setiap', + 'reminders_add_starting_from' => 'mulai dari tanggal yang ditentukan di atas', + 'reminders_add_cta' => 'Tambahkan pengingat', + 'reminders_edit_update_cta' => 'Perbarui pengingat', + 'reminders_add_error_custom_text' => 'Anda perlu mencantumkan sebuah teks untuk pengingat ini', + 'reminders_create_success' => 'Pengingat telah berhasil ditambahkan', + 'reminders_delete_success' => 'Pengingat telah berhasil dihapus', + 'reminders_update_success' => 'Pengingat telah berhasil diperbarui', + 'reminders_add_optional_comment' => 'Komentar tambahan', + + 'reminder_frequency_day' => 'setiap :number hari', + 'reminder_frequency_week' => 'setiap :number minggu', + 'reminder_frequency_month' => 'setiap :number bulan', + 'reminder_frequency_year' => 'setiap :number tahun', + 'reminder_frequency_one_time' => 'pada :date', + 'reminders_delete_confirmation' => 'Apakah Anda yakin ingin menghapus pengingat ini?', + 'reminders_delete_cta' => 'Hapus', + 'reminders_next_expected_date' => 'pada', + 'reminders_cta' => 'Tambahkan sebuah pengingat', + 'reminders_description' => 'Kami akan mengirim sebuah email untuk setiap pengingat di bawah ini. Pengingat dikirim setiap pagi saat hari peristiwa akan terjadi. Pengingat yang ditambahkan untuk ulang tahun secara otomatis tidak dapat dihapus. Jika Anda ingin mengganti tanggal tersebut, sunting ulang tahun kontak.', + 'reminders_one_time' => 'Satu kali', + 'reminders_type_week' => 'minggu', + 'reminders_type_month' => 'bulan', + 'reminders_type_year' => 'tahun', + 'reminders_birthday' => 'Ulang tahun :name', + 'reminders_free_plan_warning' => 'Anda berada di paket rencana gratis. Tidak ada email yang dikirimkan pada paket ini. Untuk menerima pengingat Anda melalui email, perbarui/tingkatkan akun Anda.', + + // relationships + 'relationship_form_add' => 'Tambahkan sebuah hubungan baru', + 'relationship_form_edit' => 'Sunting sebuah hubungan yang telah tersedia', + 'relationship_form_is_with' => 'Orang ini adalah…', + 'relationship_form_is_with_name' => ':name adalah…', + 'relationship_form_add_choice' => 'Dengan siapa hubungannya?', + 'relationship_form_create_contact' => 'Tambah seseorang yang baru', + 'relationship_form_associate_contact' => 'Sebuah kontak yang tersedia', + 'relationship_form_associate_dropdown' => 'Cari dan pilih sebuah kontak yang tersedia dari dropdown di bawah ini', + 'relationship_form_associate_dropdown_placeholder' => 'Cari dan pilih sebuah kontak yang telah tersedia', + 'relationship_form_also_create_contact' => 'Buat sebuah entri Kontak untuk orang ini.', + 'relationship_form_add_description' => 'Ini akan mengizinkan Anda untuk memperlakukan orang ini seperti kontak lainnya.', + 'relationship_form_add_no_existing_contact' => 'Anda tidak memiliki kontak apapun yang bisa dikaitkan dengan :name saat ini.', + 'relationship_delete_confirmation' => 'Apakah Anda yakin ingin menghapus hubungan ini? Penghapusan bersifat permanen.', + 'relationship_unlink_confirmation' => 'Apakah Anda yakin ingin menghapus hubungan ini? Orang ini tidak akan dihapus - hanya hubungan antara keduanya.', + 'relationship_form_add_success' => 'Hubungan telah berhasil diatur.', + 'relationship_form_deletion_success' => 'Hubungan telah dihapus.', + + // tasks + 'tasks_title' => 'Tugas', + 'tasks_blank_title' => 'Anda belum punya tugas apapun.', + 'tasks_form_title' => 'Gelar', + 'tasks_form_description' => 'Deskripsi (opsional)', + 'tasks_add_task' => 'Tambahkan sebuah tugas', + 'tasks_delete_success' => 'Tugas telah berhasil dihapus', + 'tasks_complete_success' => 'Tugas telah berhasil berubah status', + + // activities + 'activity_title' => 'Aktifitas', + 'activity_type_category_simple_activities' => 'Aktifitas sederhana', + 'activity_type_category_sport' => 'Olahraga', + 'activity_type_category_food' => 'Makanan', + 'activity_type_category_cultural_activities' => 'Aktifitas budaya', + 'activity_type_just_hung_out' => 'hanya nongkrong', + 'activity_type_watched_movie_at_home' => 'menonton sebuah film di rumah', + 'activity_type_talked_at_home' => 'baru saja ngobrol di rumah', + 'activity_type_did_sport_activities_together' => 'bersama memainkan sebuah olahraga', + 'activity_type_ate_at_his_place' => 'makan di tempat mereka', + 'activity_type_went_bar' => 'pergi ke sebuah kios indomie :)', + 'activity_type_ate_at_home' => 'makan di rumah', + 'activity_type_picnicked' => 'berpiknik', + 'activity_type_ate_restaurant' => 'makan di sebuah restoran', + 'activity_type_went_theater' => 'pergi ke bioskop', + 'activity_type_went_concert' => 'pergi nonton sebuah konser', + 'activity_type_went_play' => 'pergi ke sebuah pertunjukan', + 'activity_type_went_museum' => 'pergi ke museum', + 'activities_add_activity' => 'Tambah aktifitas', + 'activities_add_more_details' => 'Tambah lebih banyak rincian', + 'activities_add_emotions' => 'Tambahkan perasaan', + 'activities_add_category' => 'Cantumkan sebuah kategori', + 'activities_add_participants_cta' => 'Tambah orang/peserta', + 'activities_item_information' => ':Activity. Terjadi pada :date', + 'activities_add_title' => 'Apa yang telah Anda lakukan dengan {name}?', + 'activities_summary' => 'Jelaskan apa yang Anda telah lakukan', + 'activities_add_pick_activity' => 'Apakah Anda ingin mengkategorikan aktifitas ini? Anda tidak perlu melakukannya, tetapi hal itu akan memberi Anda statistik nantinya (optional)', + 'activities_add_date_occured' => 'Aktifitas terjadi pada…', + 'activities_add_participants' => 'Siapa, selain dari {name}, yang berpartisipasi dalam aktifitas ini? (opsional)', + 'activities_add_emotions_title' => 'Apakah Anda ingin mencatat bagaimana perasaan Anda selama aktifitas ini? (opsional)', + 'activities_blank_title' => 'Pantau apa yang telah Anda lakukan dengan {name} di masa lalu, dan apa yang telah Anda bicarakan', + 'activities_blank_add_activity' => 'Tambah sebuah aktifitas', + 'activities_add_success' => 'Aktifitas telah berhasil ditambahkan', + 'activities_add_error' => 'Kesalahan saat menambahkan aktifitas tersebut', + 'activities_update_success' => 'Aktifitas telah berhasil diperbarui', + 'activities_delete_success' => 'Aktifitas telas berhasil dihapus', + 'activities_who_was_involved' => 'Siapa yang terlibat?', + 'activities_activity' => 'Kategori Aktifitas', + 'activities_view_activities_report' => 'Lihat laporan aktifitas', + 'activities_profile_title' => 'Laporan aktifitas antara :name dan Anda', + 'activities_profile_subtitle' => 'Anda telah mencatat aktifitas :total_activities dengan :name secara total dan :activities_last_twelve_months dalam 12 bulan terakhir sejauh ini.', + 'activities_profile_year_summary_activity_types' => 'Ini adalah rincian dari jenis aktifitas yang telah Anda lakukan bersama dalam :year', + 'activities_profile_year_summary' => 'Inilah yang telah kalian lakukan dalam :year', + 'activities_profile_number_occurences' => 'Aktifitas :value', + 'activities_list_participants' => 'Participants ({total}):', + 'activities_list_emotions' => 'Perasaan yang dirasakan:', + 'activities_list_date' => 'Terjadi pada', + 'activities_list_category' => 'Kategori:', + + // notes + 'notes_create_success' => 'Catatan telah berhasil dibuat', + 'notes_update_success' => 'Catatan telah berhasil disimpan', + 'notes_delete_success' => 'Catatan telah berhasil dihapus', + 'notes_add_cta' => 'Tambah catatan', + 'notes_favorite' => 'Tambah/hapus dari favorit', + 'notes_delete_title' => 'Hapus sebuah catatan', + 'notes_delete_confirmation' => 'Apakah Anda yakin ingin menghapus catatan ini? Penghapusan bersifat permanen', + + // gifts + 'gifts_title' => 'Hadiah', + 'gifts_add_success' => 'Hadiah telah berhasil ditambahkan', + 'gifts_delete_success' => 'Hadiah telah berhasil dihapus', + 'gifts_delete_confirmation' => 'Apakah Anda yakin ingin menghapus hadiah ini?', + 'gifts_add_gift' => 'Tambah sebuah hadiah', + 'gifts_link' => 'Tautan', + 'gifts_for' => 'Untuk: {name}', + 'gifts_delete_cta' => 'Hapus', + 'gifts_add_title' => 'Pengelolaan hadiah untuk :name', + 'gifts_add_gift_idea' => 'Ide hadiah', + 'gifts_add_gift_already_offered' => 'Hadiah yang telah diberikan', + 'gifts_add_gift_received' => 'Hadiah yang telah diterima', + 'gifts_add_gift_title' => 'Hadiah ini apa?', + 'gifts_add_gift_name' => 'Nama hadiah', + 'gifts_add_link' => 'Tautan ke halaman web (opsional)', + 'gifts_add_value' => 'Nilai (opsional)', + 'gifts_add_comment' => 'Komentar (opsional)', + 'gifts_add_recipient' => 'Penerima (opsional)', + 'gifts_add_recipient_field' => 'Penerima', + 'gifts_add_photo' => 'Foto (opsional)', + 'gifts_add_photo_title' => 'Tambahkan sebuah foto untuk hadiah ini', + 'gifts_add_someone' => 'Hadiah ini khususnya untuk seseorang dalam keluarga {name}', + 'gifts_delete_title' => 'Hapus sebuah hadiah', + 'gifts_ideas' => 'Ide hadiah', + 'gifts_offered' => 'Hadiah yang telah diberikan', + 'gifts_offered_as_an_idea' => 'Tandai sebagai sebuah ide', + 'gifts_received' => 'Hadiah yang telah diterima', + 'gifts_view_comment' => 'Lihat komentar', + 'gifts_mark_offered' => 'Tandai sebagai telah diberikan', + 'gifts_update_success' => 'Hadiah telah berhasil diperbarui', + 'gifts_add_date' => 'Tanggal (opsional)', + + // debts + 'debt_delete_confirmation' => 'Apakah Anda yakin ingin menghapus hutang ini?', + 'debt_delete_success' => 'Hutang telah berhasil dihapus', + 'debt_add_success' => 'Hutang telah berhasil ditambahkan', + 'debt_title' => 'Hutang', + 'debt_add_cta' => 'Perbarui hutang', + 'debt_you_owe' => 'Anda berhutang sebesar :amount', + 'debt_they_owe' => ':name berutang pada Anda sebesar :amount', + 'debt_add_title' => 'Pengelolaan hutang', + 'debt_add_you_owe' => 'Anda berutang pada :name', + 'debt_add_they_owe' => ':name berhutang pada Anda', + 'debt_add_amount' => 'jumlah dari', + 'debt_add_reason' => 'untuk alasan berikut (opsional)', + 'debt_add_add_cta' => 'Tambah hutang', + 'debt_edit_update_cta' => 'Perbarui hutang', + 'debt_edit_success' => 'Hutang telah berhasil diperbarui', + 'debts_blank_title' => 'Kelola hutang yang Anda punya kepada :name atau hutang :name pada Anda', + + // tags + 'tag_edit' => 'Sunting tag', + 'tag_add' => 'Tambahkan tag', + 'tag_add_search' => 'Tambah atau cari tag', + 'tag_no_tags' => 'Belum ada tag', + + // Introductions + 'introductions_sidebar_title' => 'Bagaimana Anda bertemu', + 'introductions_blank_cta' => 'Cantumkan bagaimana Anda bertemu :name', + 'introductions_title_edit' => 'Bagaimana Anda bertemu :name?', + 'introductions_additional_info' => 'Jelaskan bagaimana dan di mana Anda bertemu', + 'introductions_edit_met_through' => 'Apakah seseorang memperkenalkan Anda kepada orang ini?', + 'introductions_no_met_through' => 'Tidak seorang pun', + 'introductions_first_met_date' => 'Tanggal Anda bertemu', + 'introductions_no_first_met_date' => 'Saya tidak tau tanggal kita bertemu', + 'introductions_first_met_date_known' => 'Ini adalah tanggal kami bertemu', + 'introductions_add_reminder' => 'Tambahkan sebuah pengingat untuk merayakan pertemuan ini pada hari peristiwa ini terjadi', + 'introductions_update_success' => 'Anda telah berhasil memperbarui informasi tentang bagaimana Anda bertemu orang ini', + 'introductions_met_through' => 'Dikenalkan oleh :name', + 'introductions_met_date' => 'Bertemu pada :date', + 'introductions_reminder_title' => 'Hari peringatan dimana Anda pertama kali bertemu', + + // Deceased + 'deceased_reminder_title' => 'Hari peringatan meninggal dunia :name', + 'deceased_mark_person_deceased' => 'Tandai ini sebagai almarhum', + 'deceased_know_date' => 'Saya tau kapan tanggal orang ini meninggal', + 'deceased_add_reminder' => 'Tambahkan sebuah pengingat untuk tanggal ini', + 'deceased_label' => 'Almarhum', + 'deceased_date_label' => 'Tanggal meninggal', + 'deceased_label_with_date' => 'Almarhum pada :date', + 'deceased_age' => 'Usia saat meninggal', + + // Contact information + 'contact_info_title' => 'Informasi kontak', + 'contact_info_form_content' => 'Konten', + 'contact_info_form_contact_type' => 'Jenis kontak', + 'contact_info_form_personalize' => 'Personalisasi', + 'contact_info_address' => 'Tinggal di', + + // Addresses + 'contact_address_title' => 'Alamat', + 'contact_address_form_name' => 'Label (opsional)', + 'contact_address_form_street' => 'Jalan (opsional)', + 'contact_address_form_city' => 'Kota (opsional)', + 'contact_address_form_province' => 'Provinsi (opsional)', + 'contact_address_form_postal_code' => 'Kode pos (opsional)', + 'contact_address_form_country' => 'Negara (opsional)', + 'contact_address_form_latitude' => 'Lintang peta (hanya angka) (opsional)', + 'contact_address_form_longitude' => 'Bujur peta (angka saja) (opsional)', + + // Pets + 'pets_kind' => 'Jenis hewan peliharaan', + 'pets_name' => 'Nama (opsional)', + 'pets_create_success' => 'Hewan peliharaan telah berhasil ditambahkan', + 'pets_update_success' => 'Hewan peliharaan telah diperbarui', + 'pets_delete_success' => 'Hewan peliharaan telah dihapus', + 'pets_title' => 'Hewan peliharaan', + 'pets_reptile' => 'Reptil', + 'pets_bird' => 'Burung', + 'pets_cat' => 'Kucing', + 'pets_dog' => 'Anjing', + 'pets_fish' => 'Ikan', + 'pets_hamster' => 'Hamster', + 'pets_horse' => 'Kuda', + 'pets_rabbit' => 'Kelinci', + 'pets_rat' => 'Tikus', + 'pets_small_animal' => 'Hewan kecil', + 'pets_other' => 'Lainnya', + + // life events + 'life_event_list_tab_life_events' => 'Peristiwa kehidupan', + 'life_event_list_tab_other' => 'Catatan, pengingat, …', + 'life_event_list_title' => 'Peristiwa kehidupan', + 'life_event_blank' => 'Catat apa yang terjadi pada kehidupan {name} untuk referensi mendatang Anda.', + 'life_event_list_cta' => 'Tambahkan peristiwa kehidupan', + 'life_event_create_category' => 'Semua kategori', + 'life_event_create_life_event' => 'Tambahkan peristiwa kehidupan', + 'life_event_create_default_title' => 'Judul (opsional)', + 'life_event_create_default_story' => 'Cerita (opsional)', + 'life_event_create_date' => 'Anda tidak perlu mencantumkan bulan atau hari – hanya tahun saja yang bersifat wajib.', + 'life_event_create_default_description' => 'Tambahkan informasi tentang apa yang Anda ketahui', + 'life_event_create_add_yearly_reminder' => 'Tambah sebuah pengingat tahunan untuk peristiwa ini', + 'life_event_create_success' => 'Peristiwa kehidupan telah ditambahkan', + 'life_event_delete_title' => 'Hapus sebuah peristiwa kehidupan', + 'life_event_delete_description' => 'Apakah Anda yakin ingin menghapus peristiwa kehidupan ini? Penghapusan bersifat permanen.', + 'life_event_delete_success' => 'Peristiwa kehidupan telah dihapus', + 'life_event_date_it_happened' => 'Tanggal hal itu terjadi', + 'life_event_category_work_education' => 'Pekerjaan & edukasi', + 'life_event_category_family_relationships' => 'Keluarga & hubungan relasi', + 'life_event_category_home_living' => 'Rumah & kehidupan', + 'life_event_category_health_wellness' => 'Kesehatan & kebugaran', + 'life_event_category_travel_experiences' => 'Wisata & pengalaman', + 'life_event_sentence_new_job' => 'Memulai sebuah pekerjaan baru', + 'life_event_sentence_retirement' => 'Pensiun', + 'life_event_sentence_new_school' => 'Memulai sekolah', + 'life_event_sentence_study_abroad' => 'Belajar di luar negeri', + 'life_event_sentence_volunteer_work' => 'Menjadi sukarelawan', + 'life_event_sentence_published_book_or_paper' => 'Menerbitkan sebuah makalah', + 'life_event_sentence_military_service' => 'Memulai dinas militer', + 'life_event_sentence_new_relationship' => 'Memulai sebuah hubungan', + 'life_event_sentence_engagement' => 'Bertunangan', + 'life_event_sentence_marriage' => 'Menikah', + 'life_event_sentence_anniversary' => 'Perayaan Hari Pernikahan', + 'life_event_sentence_expecting_a_baby' => 'Mengharapkan seorang bayi', + 'life_event_sentence_new_child' => 'Mempunyai seorang anak', + 'life_event_sentence_new_family_member' => 'Menambahkan seorang anggota keluarga', + 'life_event_sentence_new_pet' => 'Mempunyai seekor hewan peliharaan', + 'life_event_sentence_end_of_relationship' => 'Mengakhiri suatu hubungan', + 'life_event_sentence_loss_of_a_loved_one' => 'Kehilangan orang yang dicintai', + 'life_event_sentence_moved' => 'Pindah', + 'life_event_sentence_bought_a_home' => 'Membeli sebuah rumah', + 'life_event_sentence_home_improvement' => 'Melakukan sebuah perbaikan rumah', + 'life_event_sentence_holidays' => 'Pergi berlibur', + 'life_event_sentence_new_vehicle' => 'Mempunyai sebuah kendaraan baru', + 'life_event_sentence_new_roommate' => 'Mempunyai teman sekamar', + 'life_event_sentence_overcame_an_illness' => 'Sembuh dari sebuah penyakit', + 'life_event_sentence_quit_a_habit' => 'Berhenti dari kebiasaan', + 'life_event_sentence_new_eating_habits' => 'Memulai kebiasaan makan yang baru', + 'life_event_sentence_weight_loss' => 'Turun berat', + 'life_event_sentence_wear_glass_or_contact' => 'Memulai memakai kacamata atau lensa kontak', + 'life_event_sentence_broken_bone' => 'Patah tulang', + 'life_event_sentence_removed_braces' => 'Melepaskan kawat gigi', + 'life_event_sentence_surgery' => 'Menjalani operasi', + 'life_event_sentence_dentist' => 'Pergi ke dokter gigi', + 'life_event_sentence_new_sport' => 'Memulai sebuah olahraga', + 'life_event_sentence_new_hobby' => 'Memulai sebuah hobi', + 'life_event_sentence_new_instrument' => 'Belajar sebuah instrumen baru', + 'life_event_sentence_new_language' => 'Belajar sebuah bahasa baru', + 'life_event_sentence_tattoo_or_piercing' => 'Memiliki sebuah tato atau tindik', + 'life_event_sentence_new_license' => 'Mempunyai sebuah SIM', + 'life_event_sentence_travel' => 'Berwisata', + 'life_event_sentence_achievement_or_award' => 'Mempunyai sebuah prestasi atau penghargaan', + 'life_event_sentence_changed_beliefs' => 'Berubah keyakinan', + 'life_event_sentence_first_word' => 'Berbicara untuk pertama kalinya', + 'life_event_sentence_first_kiss' => 'Berciuman untuk pertama kalinya', + + // documents + 'document_list_title' => 'Dokumen', + 'document_list_cta' => 'Unggah dokumen', + 'document_list_blank_desc' => 'Di sini Anda dapat menyimpan dokumen yang berkaitan dengan orang ini.', + 'document_upload_zone_cta' => 'Unggah sebuah berkas', + 'document_upload_zone_progress' => 'Mengunggah dokumen…', + 'document_upload_zone_error' => 'Terdapat kesalahan saat mengunggah dokumen. Silakan coba lagi dibawah ini.', + + // Photos + 'photo_title' => 'Foto', + 'photo_list_title' => 'Foto terkait', + 'photo_list_cta' => 'Unggah foto', + 'photo_list_blank_desc' => 'Anda dapat menyimpan gambar tentang kontak ini. Unggah satu sekarang!', + 'photo_upload_zone_cta' => 'Unggah sebuah foto', + 'photo_current_profile_pic' => 'Gambar profil saat ini', + 'photo_make_profile_pic' => 'Jadikan gambar profil', + 'photo_delete' => 'Hapus foto', + 'photo_next' => 'Foto berikutnya ❯', + 'photo_previous' => '❮ Foto sebelumnya', + + // Avatars + 'avatar_change_title' => 'Ganti avatar Anda', + 'avatar_question' => 'Avatar mana yang ingin Anda gunakan?', + 'avatar_default_avatar' => 'Avatar standar', + 'avatar_adorable_avatar' => 'Avatar Menggemaskan', + 'avatar_gravatar' => 'Gravatar diasosiasikan dengan alamat email orang ini. Gravatar adalah sebuah sistem global yang mengizinkan pengguna mengasosiasikan alamat email dengan foto.', + 'avatar_current' => 'Simpan avatar saat ini', + 'avatar_photo' => 'Dari sebuah foto yang Anda unggah', + 'avatar_crop_new_avatar_photo' => 'Pangkas foto avatar baru', + + // emotions + 'emotion_this_made_me_feel' => 'Ini membuat Anda merasa…', + + // logs + 'auditlogs_link' => 'Riwayat', + 'auditlogs_title' => 'Segala sesuatu yang terjadi pada :name', + 'auditlogs_breadcrumb' => 'Riwayat', + 'auditlogs_author' => 'Oleh :name pada :date', + + // contact field label + 'contact_field_label_home' => 'Rumah', + 'contact_field_label_work' => 'Pekerjaan', + 'contact_field_label_cell' => 'Seluler', + 'contact_field_label_fax' => 'Faksimili', + 'contact_field_label_pager' => 'Pager', + 'contact_field_label_main' => 'Utama', + 'contact_field_label_other' => 'Lainnya', + 'contact_field_label_personal' => 'Pribadi', +]; diff --git a/resources/lang/id/reminder.php b/resources/lang/id/reminder.php new file mode 100644 index 0000000..9f59924 --- /dev/null +++ b/resources/lang/id/reminder.php @@ -0,0 +1,16 @@ + 'Ucapkan selamat ulang tahun kepada', + 'type_phone_call' => 'Panggil', + 'type_lunch' => 'Makan siang dengan', + 'type_hangout' => 'Nongkrong dengan', + 'type_email' => 'Email', + 'type_birthday_kid' => 'Wish happy birthday to the child of', +]; diff --git a/resources/lang/id/settings.php b/resources/lang/id/settings.php new file mode 100644 index 0000000..7d6fda9 --- /dev/null +++ b/resources/lang/id/settings.php @@ -0,0 +1,557 @@ + 'Pengaturan akun', + 'sidebar_personalization' => 'Personalisasi', + 'sidebar_settings_storage' => 'Ruang Peyimpanan', + 'sidebar_settings_export' => 'Ekspor data', + 'sidebar_settings_users' => 'Pengguna', + 'sidebar_settings_subscriptions' => 'Berlangganan', + 'sidebar_settings_import' => 'Impor data', + 'sidebar_settings_tags' => 'Pengelolaan tag', + 'sidebar_settings_api' => 'API', + 'sidebar_settings_dav' => 'Sumberdaya DAV', + 'sidebar_settings_security' => 'Keamanan', + 'sidebar_settings_auditlogs' => 'Catatan audit', + + 'title_general' => 'Informasi Umum', + 'title_i18n' => 'Pengaturan internasional', + 'title_layout' => 'Tata letak', + + 'me_title' => 'Saya sebagai sebuah kontak', + 'me_help' => 'Ini adalah kontak yang mewakili Anda dalam Monica', + 'me_select' => 'Pilih sebuah kontak', + 'me_no_contact' => 'Belum ada kontak yang dipilih.', + 'me_select_click' => 'Klik di sini untuk memilih sebuah kontak.', + 'me_remove_contact' => 'Hapus pengkaitan', + 'me_choose' => 'Pilih diri Anda sendiri', + 'me_choose_placeholder' => 'Pilih diri Anda sendiri', + + 'export_title' => 'Ekspor data akun Anda', + 'export_be_patient' => 'Click the button to start the export. It might take several minutes to process the export – please be patient and do not spam the button.', + 'export_title_sql' => 'Export to SQL', + 'export_sql_explanation' => 'Exporting your data in SQL format allows you to take your data and import it to your own Monica instance. This is only valuable if you do have your own server.', + 'export_sql_cta' => 'Export to SQL', + 'export_sql_link_instructions' => 'Note: read the instructions to learn more about importing this file to your instance.', + 'export_title_json' => 'Export to Json', + 'export_submitted' => 'Your export has been submitted, it will be available in a few moment…', + 'export_json_explanation' => 'Exporting your data in Json format for backup.', + 'export_json_beta' => 'Json export is in preview mode. Tell us what you think about it:', + 'export_json_cta' => 'Export to Json', + 'export_header_type' => 'Type', + 'export_header_timestamp' => 'Creation date', + 'export_header_status' => 'Status', + 'export_header_actions' => 'Actions', + 'export_last_title' => 'Last exports', + 'export_empty_title' => 'No exports yet', + 'export_type_json' => 'Json export', + 'export_type_sql' => 'SQL export', + 'export_status_todo' => 'Submitted', + 'export_status_doing' => 'Doing', + 'export_status_done' => 'Done', + 'export_status_failed' => 'Failed', + 'export_not_done' => 'Download impossible, this export is not done yet.', + + 'firstname' => 'Nama depan', + 'lastname' => 'Nama belakang', + 'name_order' => 'Urutan nama', + 'name_order_firstname_lastname' => ' – Asep Surasep', + 'name_order_lastname_firstname' => ' – Surasep Asep', + 'name_order_firstname_lastname_nickname' => ' () – Asep Surasep (Kabayan)', + 'name_order_firstname_nickname_lastname' => ' () – Asep (Kabayan) Surasep', + 'name_order_lastname_firstname_nickname' => ' () – Surasep Asep (Kabayan)', + 'name_order_lastname_nickname_firstname' => ' () – Surasep (Kabayan) Asep', + 'name_order_nickname_firstname_lastname' => ' ( ) – Kabayan (Asep Surasep)', + 'name_order_nickname_lastname_firstname' => ' ( ) – Kabayan (Surasep Asep)', + 'name_order_nickname' => ' - Kabayan', + 'currency' => 'Mata uang', + 'name' => 'Nama Anda: :name', + 'email' => 'Alamat email', + 'email_placeholder' => 'Masukkan email', + 'email_help' => 'Ini adalah email yang digunakan untuk masuk, dan di alamat inilah Monica akan mengirim pengingat Anda.', + 'timezone' => 'Zona waktu', + 'temperature_scale' => 'Skala suhu', + 'temperature_scale_fahrenheit' => 'Fahrenheit (°F)', + 'temperature_scale_celsius' => 'Celsius (°C)', + 'layout' => 'Tata letak', + 'layout_small' => 'Maksimal lebar 1200 piksel', + 'layout_big' => 'Lebar penuh dari peramban web', + 'save' => 'Perbarui preferensi', + 'delete_title' => 'Hapus akun Anda', + 'delete_desc' => 'Apakah Anda ingin menghapus akun Anda? Penghapusan bersifat permanen dan semua data Anda akan dihapus secara permanen. Jika Anda berlangganan, itu akan segera dibatalkan.', + 'delete_other_desc' => 'Your data in the main database will be deleted immediately. As described in our privacy policy, we carry out securely encrypted backups of the database every day. These backups are kept for 30 days after which they are completely deleted. We cannot delete specific data from the backups we hold any earlier than this. All of your data will be completely deleted no later than 31 days after your account’s deletion.', + 'reset_desc' => 'Apakah Anda ingin mengatur ulang akun Anda? Ini akan menghapus semua kontak Anda, dan semua data yang terkait dengannya. Akun Anda tidak akan dihapus.', + 'reset_title' => 'Atur ulang akun Anda', + 'reset_cta' => 'Atur ulang akun', + 'reset_notice' => 'Apakah Anda yakin untuk mengatur ulang akun Anda? Ini bersifat permanen dan tidak dapat dibatalkan.', + 'reset_success' => 'Akun Anda telah berhasil diatur ulang.', + 'delete_notice' => 'Apakah Anda yakin ingin menghapus akun Anda? Ini bersifat permanen dan tidak bisa dibatalkan. Semua data Anda akan dihapus dan tidak akan dapat dipulihkan.', + 'delete_cta' => 'Hapus akun', + 'settings_success' => 'Preferensi diperbarui!', + 'locale' => 'Bahasa yang digunakan dalam aplikasi', + 'locale_help' => 'Apakah Anda ingin membantu menerjemahkan Monica atau menambahkan bahasa baru? Silakan ikuti tautan ini untuk informasi lebih lanjut.', + 'locale_ar' => 'Arab', + 'locale_cs' => 'Ceko', + 'locale_de' => 'Jerman', + 'locale_el' => 'Greek', + 'locale_en' => 'Inggris', + 'locale_en-GB' => 'Inggris (Britania Raya)', + 'locale_es' => 'Spanyol', + 'locale_fr' => 'Perancis', + 'locale_he' => 'Ibrani', + 'locale_hr' => 'Kroasia', + 'locale_id' => 'Indonesia', + 'locale_it' => 'Italia', + 'locale_ja' => 'Jepang', + 'locale_nl' => 'Belanda', + 'locale_pt' => 'Portugis', + 'locale_pt-BR' => 'Portuguese, Brazil', + 'locale_ru' => 'Rusia', + 'locale_sv' => 'Swedia', + 'locale_vi' => 'Vietnam', + 'locale_zh' => 'Cina Sederhana', + 'locale_zh-TW' => 'Cina Tradisional', + 'locale_tr' => 'Turki', + + 'security_title' => 'Keamanan', + 'security_help' => 'Ubah hal tentang keamanan untuk akun Anda.', + 'password_change' => 'Ganti kata sandi Anda', + 'password_current' => 'Kata sandi saat ini', + 'password_current_placeholder' => 'Masukkan kata sandi Anda', + 'password_new1' => 'Kata sandi baru', + 'password_new1_placeholder' => 'Masukkan kata sandi baru Anda', + 'password_new2' => 'Konfirmasi kata sandi baru Anda', + 'password_new2_placeholder' => 'Ketik ulang kata sandi baru Anda', + 'password_btn' => 'Ganti kata sandi', + '2fa_title' => 'Otentikasi Dua Faktor', + '2fa_otp_title' => 'Aplikasi seluler Otentikasi Dua Faktor', + '2fa_enable_title' => 'Aktifkan Otentikasi Dua Faktor', + '2fa_enable_description' => 'Aktifkan Otentikasi Dua Faktor untuk meningkatkan keamanan akun Anda.', + '2fa_enable_otp' => 'Buka aplikasi seluler Otentikasi Dua Faktor Anda dan pindai barcode QR berikut:', + '2fa_enable_otp_help' => 'Jika aplikasi seluler Otentikasi Dua Faktor Anda tidak mendukung barcode QR, masukkan kode berikut:', + '2fa_enable_otp_validate' => 'Silakan validasi perangkat baru yang baru saja Anda atur:', + '2fa_enable_success' => 'Otentikasi Dua Faktor diaktifkan', + '2fa_enable_error' => 'Kesalahan saat mencoba mengaktifkan Otentikasi Dua Faktor', + '2fa_enable_error_already_set' => 'Otentikasi Dua Faktor telah diaktifkan sebelumnya', + '2fa_disable_title' => 'Nonaktifkan Otentikasi Dua Faktor', + '2fa_disable_description' => 'Nonaktifkan Otentikasi Dua Faktor untuk akun Anda. Harap berhati-hati, akun Anda akan jauh lebih kurang aman!', + '2fa_disable_success' => 'Otentikasi Dua Faktor dinonaktifkan', + '2fa_disable_error' => 'Kesalahan saat mencoba menonaktifkan Otentikasi Dua Faktor', + + 'webauthn_title' => 'Kunci keamanan - protokol WebAuthn', + 'webauthn_enable_description' => 'Tambahkan sebuah kunci keamanan baru', + 'webauthn_key_name_help' => 'Berikan sebuah nama ke kunci Anda.', + 'webauthn_key_name' => 'Nama kunci:', + 'webauthn_success' => 'Kunci Anda terdeteksi dan telah divalidasi.', + 'webauthn_last_use' => 'Penggunaan terakhir: {timestamp}', + 'webauthn_delete_confirmation' => 'Apakah Anda yakin ingin menghapus kunci ini?', + 'webauthn_delete_success' => 'Kunci dihapus', + 'webauthn_insertKey' => 'Masukkan kunci keamanan Anda.', + 'webauthn_buttonAdvise' => 'Jika kunci keamanan Anda memiliki sebuah tombol, tekan tombol itu.', + 'webauthn_noButtonAdvise' => 'Jika tidak, hapus dan masukkan lagi kunci tersebut.', + 'webauthn_not_supported' => 'Peramban web Anda saat ini tidak mendukung WebAuthn.', + 'webauthn_not_secured' => 'WebAuthn hanya mendukung koneksi yang aman. Silakan muat halaman ini dengan skema https.', + 'webauthn_error_already_used' => 'Kunci ini telah didaftarkan sebelumnya. Tidak diperlukan untuk didaftarkannya lagi.', + 'webauthn_error_not_allowed' => 'Operasi tersebut habis waktu atau tidak diizinkan.', + + 'recovery_title' => 'Kode pemulihan', + 'recovery_show' => 'Dapatkan kode pemulihan', + 'recovery_copy_help' => 'Salin kode di papanklip Anda', + 'recovery_help_intro' => 'Ini adalah kode pemulihan Anda:', + 'recovery_help_information' => 'Anda dapat menggunakan setiap kode pemulihan hanya sekali.', + 'recovery_clipboard' => 'Kode disalin ke papanklip.', + 'recovery_generate' => 'Hasilkan kode baru…', + 'recovery_generate_help' => 'Menghasilkan kode baru akan membatalkan kode yang dihasilkan sebelumnya/tidak valid.', + 'recovery_already_used_help' => 'Kode ini telah digunakan sebelumnya.', + + 'users_list_title' => 'Pengguna dengan akses ke akun Anda', + 'users_list_add_user' => 'Undang seorang pengguna baru', + 'users_list_you' => 'Itu Anda', + 'users_list_invitations_title' => 'Undangan yang tertunda', + 'users_list_invitations_explanation' => 'Di bawah ini adalah orang yang telah Anda undang untuk bergabung dengan Monica sebagai kolaborator.', + 'users_list_invitations_invited_by' => 'diundang oleh :name', + 'users_list_invitations_sent_date' => 'dikirim pada :date', + 'users_blank_title' => 'Anda adalah satu-satunya yang memiliki akses ke akun ini.', + 'users_blank_add_title' => 'Apakah Anda ingin mengundang orang lain?', + 'users_blank_description' => 'Orang ini akan memiliki akses yang sama yang Anda miliki, dan akan dapat menambah, mengedit, atau menghapus informasi kontak.', + 'users_blank_cta' => 'Undang seseorang', + 'users_add_title' => 'Undang seorang pengguna baru ke akun Anda melalui email', + 'users_add_description' => 'Orang ini akan memiliki akses yang sama seperti yang Anda punya, termasuk mengundang atau menghapus pengguna lain, termasuk Anda. Pastikan Anda mempercayai orang ini sebelum memberi mereka akses.', + 'users_add_email_field' => 'Masukkan alamat email orang yang ingin Anda undang', + 'users_add_confirmation' => 'Saya mengkonfirmasi bahwa saya ingin mengundang pengguna ini ke akun saya. Saya mengerti bahwa orang ini akan memiliki akses ke semua data saya dan melihat apa yang saya lihat dengan sama.', + 'users_add_cta' => 'Undang pengguna melalui email', + 'users_accept_title' => 'Terima undangan dan buat sebuah akun baru', + 'users_error_please_confirm' => 'Harap konfirmasi bahwa Anda ingin mengundang pengguna ini sebelum melanjutkan dengan undangan tersebut', + 'users_error_email_already_taken' => 'Email ini telah dipakai. Silakan pilih yang lain', + 'users_error_already_invited' => 'Anda telah mengundang pengguna ini sebelumnya. Silakan pilih alamat email lain.', + 'users_error_email_not_similar' => 'Ini bukan alamat email dari orang yang telah mengundang Anda.', + 'users_invitation_deleted_confirmation_message' => 'Undangan telah berhasil dihapus', + 'users_invitations_delete_confirmation' => 'Apakah Anda yakin ingin menghapus undangan ini?', + 'users_list_delete_confirmation' => 'Apakah Anda yakin ingin menghapus pengguna ini dari akun Anda?', + 'users_invitation_need_subscription' => 'Menambahkan lebih banyak pengguna membutuhkan sebuah langganan.', + + 'subscriptions_account_current_plan' => 'Paket rencana Anda saat ini', + 'subscriptions_account_current_legacy' => 'Current plan, not selectable anymore:', + 'subscriptions_account_current_paid_plan' => 'Anda berada di paket rencana :name. Terima kasih banyak telah menjadi seorang pelanggan.', + + 'subscriptions_account_next_billing_title' => 'Next bill', + 'subscriptions_account_next_billing' => 'Langganan Anda akan diperbarui secara otomatis pada :date.', + 'subscriptions_account_bill_monthly' => 'We’ll bill you :price for another month.', + 'subscriptions_account_bill_annual' => 'We’ll bill you :price for another year.', + 'subscriptions_account_change' => 'Change plan', + + 'subscriptions_account_cancel_title' => 'Cancel subscription', + 'subscriptions_account_cancel_action' => 'Cancel subscription', + 'subscriptions_account_cancel' => 'Anda dapat membatalkan langganan kapan saja.', + 'subscriptions_account_free_plan' => 'Anda berada dalam paket rencana gratis.', + 'subscriptions_account_free_plan_upgrade' => 'Anda dapat meningkatkan akun Anda ke paket rencana :name, dengan biaya $:price per bulan. Berikut ini adalah kelebihannya:', + 'subscriptions_account_free_plan_benefits_users' => 'Jumlah pengguna yang tidak terbatas', + 'subscriptions_account_free_plan_benefits_reminders' => 'Pengingat melalui email', + 'subscriptions_account_free_plan_benefits_import_data_vcard' => 'Impor kontak Anda dengan vCard', + 'subscriptions_account_free_plan_benefits_support' => 'Mendukung proyek dalam jangka panjang, sehingga kami dapat memperkenalkan lebih banyak lagi fitur hebat.', + 'subscriptions_account_upgrade' => 'Tingkatkan akun Anda', + 'subscriptions_account_upgrade_title' => 'Tingkatkan Monica hari ini dan memiliki hubungan yang lebih bermakna.', + 'subscriptions_account_upgrade_choice' => 'Pilih sebuah paket rencana di bawah ini dan bergabunglah dengan :customers pelanggan yang meningkatkan aplikasi Monica mereka.', + 'subscriptions_account_update_title' => 'Update Monica subscription', + 'subscriptions_account_update_description' => 'You can change your subscription’s frequency here.', + 'subscriptions_account_update_information' => 'You will be billed immediately for the new amount. Your subscription will extend to the new period, depending on your choice.', + 'subscriptions_account_invoices' => 'Faktur', + 'subscriptions_account_invoices_download' => 'Unduh', + 'subscriptions_account_invoices_subscription' => 'Berlangganan dari :startDate ke sampai :endDate', + 'subscriptions_account_payment' => 'Opsi pembayaran mana yang paling cocok untuk Anda?', + 'subscriptions_account_confirm_payment' => 'Pembayaran Anda saat ini tidak lengkap, silakan Konfirmasi pembayaran Anda.', + 'subscriptions_downgrade_title' => 'Turunkan akun Anda ke paket rencana gratis', + 'subscriptions_downgrade_limitations' => 'Paket rencana gratis memiliki keterbatasan. Untuk dapat menurunkan versi, Anda harus lulus daftar periksa di bawah ini:', + 'subscriptions_downgrade_rule_users' => 'Anda harus memiliki hanya 1 pengguna di akun Anda', + 'subscriptions_downgrade_rule_users_constraint' => 'Anda saat ini memiliki :count pengguna pada akun Anda.', + 'subscriptions_downgrade_rule_invitations' => 'Anda tidak boleh memiliki undangan yang tertunda', + 'subscriptions_downgrade_rule_invitations_constraint' => 'Anda saat ini memiliki :count undangan tertunda.', + 'subscriptions_downgrade_rule_contacts' => 'Anda tidak boleh memiliki lebih dari :number kontak aktif', + 'subscriptions_downgrade_rule_contacts_constraint' => 'Saat ini Anda memiliki :count kontak.', + 'subscriptions_downgrade_rule_contacts_archive' => 'We can also archive all your contacts for you – that would clear this rule and let you proceed with your account’s downgrade process.', + 'subscriptions_downgrade_cta' => 'Turun tingkat', + 'subscriptions_downgrade_success' => 'Anda kembali ke paket rencana Gratis!', + 'subscriptions_downgrade_thanks' => 'Terima kasih banyak telah mencoba paket rencana berbayar. Kami terus menambahkan fitur baru di Monica sepanjang waktu - sehingga Anda mungkin ingin kembali pada kesempatan mendatang untuk melihat apakah Anda mungkin tertarik untuk berlangganan lagi.', + 'subscriptions_back' => 'Kembali ke pengaturan', + 'subscriptions_upgrade_title' => 'Tingkatkan akun Anda', + 'subscriptions_upgrade_choose' => 'Kamu memilih paket rencana :plan.', + 'subscriptions_upgrade_infos' => 'Kami tidak bisa lebih bahagia. Masukkan informasi pembayaran Anda di bawah ini.', + 'subscriptions_upgrade_name' => 'Nama di kartu', + 'subscriptions_upgrade_zip' => 'ZIP atau kode pos', + 'subscriptions_upgrade_credit' => 'Kartu kredit atau debit', + 'subscriptions_upgrade_submit' => 'Bayar {amount}', + 'subscriptions_upgrade_charge' => 'Kami akan menagih kartu Anda :price sekarang. Tagihan berikutnya akan dibebankan pada :date. Jika Anda berubah pikiran, Anda dapat membatalkannya kapan saja, tanpa pertanyaan.', + 'subscriptions_upgrade_charge_handled' => 'Pembayaran ditangani oleh Stripe. Tidak ada informasi kartu yang melalui server kami.', + 'subscriptions_upgrade_success' => 'Terima kasih Kakak! Anda sekarang berlangganan.', + 'subscriptions_upgrade_thanks' => 'Wilujeng sumping ke komunitas kumpulan orang yang berusaha menjadikan dunia tempat yang lebih baik.', + + 'subscriptions_payment_confirm_title' => 'Konfirmasi pembayaran :amount Anda', + 'subscriptions_payment_confirm_information' => 'Konfirmasi tambahan diperlukan untuk memproses pembayaran Anda. Silahkan konfirmasi pembayaran Anda dengan mengisi rincian pembayaran Anda di bawah ini.', + 'subscriptions_payment_succeeded_title' => 'Pembayaran berhasil', + 'subscriptions_payment_succeeded' => 'Pembayaran ini sudah berhasil dikonfirmasi sebelumnya.', + 'subscriptions_payment_cancelled_title' => 'Pembayaran Dibatalkan', + 'subscriptions_payment_cancelled' => 'Pembayaran ini dibatalkan.', + 'subscriptions_payment_error_name' => 'Tolong sediakan nama Anda.', + 'subscriptions_payment_success' => 'Pembayaran berhasil.', + + 'subscriptions_pdf_title' => 'Langganan bulanan :name Anda', + 'subscriptions_plan_frequency_year' => ':amount / year', + 'subscriptions_plan_frequency_month' => ':amount / month', + 'subscriptions_plan_choose' => 'Pilih paket rencana ini', + 'subscriptions_plan_year_title' => 'Bayar pertahun', + 'subscriptions_plan_year_bonus' => 'Ketenangan pikiran untuk setahun penuh', + 'subscriptions_plan_month_title' => 'Bayar perbulan', + 'subscriptions_plan_month_bonus' => 'Batalkan kapan saja', + 'subscriptions_plan_include1' => 'Termasuk dalam peningkatan Anda:', + 'subscriptions_plan_include2' => 'Jumlah kontak yang tidak terbatas • Jumlah pengguna yang tidak terbatas • Pengingat melalui email • Impor dengan vCard • Personalisasi lembar kontak', + 'subscriptions_plan_include3' => '100% dari keuntungan digunakan untuk pengembangan proyek sumber terbuka yang hebat ini.', + 'subscriptions_help_title' => 'Rincian tambahan yang mungkin Anda ingin tau', + 'subscriptions_help_opensource_title' => 'Apa itu proyek sumber terbuka?', + 'subscriptions_help_opensource_desc' => 'Monica adalah proyek sumber terbuka. Ini berarti dibangun oleh sebuah komunitas yang ingin membuat alat yang hebat untuk kebaikan yang lebih besar. Menjadi sumber terbuka berarti kode aplikasi ini tersedia untuk umum di GitHub, dan semua orang dapat memeriksa, memodifikasi, atau meningkatkannya. Semua dana yang kami kumpulkan didedikasikan untuk membangun fitur yang lebih baik, membayar untuk server yang lebih kuat, dan membayar biaya lainnnya. Terima kasih atas bantuan Anda. Kami tidak bisa melakukannya tanpa Anda.', + 'subscriptions_help_limits_title' => 'Apakah ada sebuah batasan jumlah kontak yang bisa dimiliki pada paket rencana gratis?', + 'subscriptions_help_limits_plan' => 'Ya. Paket rencana gratis mengizinkan Anda mengelola :number kontak.', + 'subscriptions_help_discounts_title' => 'Apakah Anda memiliki diskon untuk organisasi non-profit dan edukasi?', + 'subscriptions_help_discounts_desc' => 'Kami punya! Monica gratis untuk siswa, dan gratis untuk organisasi non-profit dan amal. Cukup hubungidukungan dengan bukti status Anda dan kami akan terapkan status khusus ini pada akun Anda.', + 'subscriptions_help_change_title' => 'Bagaimana jika saya berubah pikiran?', + 'subscriptions_help_change_desc' => 'Anda dapat membatalkannya kapan saja, tanpa pertanyaan, dan semua oleh Anda sendiri - tidak perlu menghubungi dukungan. Namun, dana Anda tidak akan dikembalikan untuk periode tersebut.', + + 'stripe_error_card' => 'Kartu Anda ditolak. Pesan penolakan: :message', + 'stripe_error_api_connection' => 'Komunikasi jaringan dengan Stripe gagal. Coba lagi nanti.', + 'stripe_error_rate_limit' => 'Terlalu banyak permintaan dengan Stripe saat ini. Coba lagi nanti.', + 'stripe_error_invalid_request' => 'Parameter tidak valid. Coba lagi nanti.', + 'stripe_error_authentication' => 'Otentikasi salah dengan Stripe', + + 'import_title' => 'Impor kontak di akun Anda', + 'import_cta' => 'Unggah kontak', + 'import_stat' => 'Anda sudah mengimpor :number berkas sejauh ini.', + 'import_result_stat' => 'Unggahan vCard dengan :total_contacts kontak (:total_imported yang diimpor, :total_skipped dilewatkan)', + 'import_view_report' => 'Lihat laporan', + 'import_in_progress' => 'Impor sedang berlangsung. Muat ulang halaman dalam satu menit.', + 'import_upload_title' => 'Impor kontak Anda dari sebuah berkas vCard', + 'import_upload_rules_desc' => 'Kami tetapi memiliki beberapa aturan:', + 'import_upload_rule_format' => 'Kami mendukung berkas .vcard dan .vcf.', + 'import_upload_rule_vcard' => 'Kami mendukung format vCard 3.0, yang merupakan format standar untuk Contacts.app MacOS dan Google Contacts.', + 'import_upload_rule_instructions' => 'Petunjuk ekspor untuk Contacts.app macOS dan Google Contacts.', + 'import_upload_rule_multiple' => 'Jika kontak Anda memiliki beberapa alamat email atau nomor telepon, hanya entri pertama yang akan disimpan.', + 'import_upload_rule_limit' => 'Berkas dibatasi hingga 10 MB.', + 'import_upload_rule_time' => 'Mungkin membutuhkan waktu hingga satu menit untuk mengunggah kontak dan memprosesnya. Harap bersabar.', + 'import_upload_rule_cant_revert' => 'Pastikan data dalam vCard tersebut akurat sebelum mengunggah, karena Anda tidak dapat membatalkan unggahan.', + 'import_upload_form_file' => 'Berkas .vcf atau .vCard Anda:', + 'import_upload_behaviour' => 'Perilaku impor:', + 'import_upload_behaviour_add' => 'Tambahkan kontak baru dan lewati yang telah tersedia', + 'import_upload_behaviour_replace' => 'Ganti kontak yang telah tersedia', + 'import_upload_behaviour_help' => 'Mengganti akan mengganti semua data yang ditemukan di vCard, tetapi akan menyimpan kontak yang telah tersedia.', + 'import_report_title' => 'Mengimpor laporan', + 'import_report_date' => 'Tanggal impor', + 'import_report_type' => 'Jenis impor', + 'import_report_number_contacts' => 'Jumlah kontak dalam berkas', + 'import_report_number_contacts_imported' => 'Jumlah kontak yang telah diimpor', + 'import_report_number_contacts_skipped' => 'Jumlah kontak yang dilewati', + 'import_report_status_imported' => 'Diimpor', + 'import_report_status_skipped' => 'Dilewati', + 'import_vcard_parse_error' => 'Kesalahan saat menguraikan entri vCard', + 'import_vcard_contact_exist' => 'Kontak telah tersedia', + 'import_vcard_contact_no_firstname' => 'Tanpa nama depan (wajib)', + 'import_vcard_file_not_found' => 'Berkas tidak ditemukan', + 'import_vcard_unknown_entry' => 'Nama kontak tidak diketahui', + 'import_vcard_file_no_entries' => 'Berkas tidak memiliki entri', + 'import_blank_title' => 'Anda belum mengimpor kontak apapun.', + 'import_blank_question' => 'Apakah Ànda ingin mengimpor kontak sekarang?', + 'import_blank_description' => 'Kami dapat mengimpor berks vCard yang bisa Anda dapatkan dari Google Contacts atau pengelola Kontak Anda.', + 'import_blank_cta' => 'Impor vCard', + 'import_need_subscription' => 'Mengimpor data memerlukan sebuah langganan.', + + 'tags_list_title' => 'Tag', + 'tags_list_description' => 'Anda dapat mengkategorikan kontak Anda dengan mengatur tag. Tag berfungsi seperti folder, tetapi Anda dapat menambahkan lebih dari satu tag ke kontak. Untuk menambahkan sebuah tag baru, tambahkan tag pada kontak tersebut.', + 'tags_list_contact_number' => ':count kontak', + 'tags_list_delete_success' => 'Tag telah berhasil dihapus', + 'tags_list_edit_success' => 'The tag has been successfully updated', + 'tags_list_delete_confirmation' => 'Apakah Anda yakin ingin menghapus tag? Tidak ada kontak yang akan dihapus, hanya tag saja.', + 'tags_blank_title' => 'Tag adalah sebuah cara yang bagus untuk mengkategorikan kontak Anda.', + 'tags_blank_description' => 'Tag bekerja seperti folder, tetapi Anda dapat menambahkan lebih dari satu tag ke kontak. Cari sebuah kontak dan tag seorang teman, tepat di bawah namanya. Setelah sebuah kontak ditambhkan tag, kembali ke sini untuk mengelola semua tag di akun Anda.', + + 'api_title' => 'Akses API', + 'api_description' => 'API dapat digunakan untuk memanipulasi data Monica dari aplikasi eksternal, seperti aplikasi seluler misalnya.', + 'api_help' => 'Untuk menggunakan API, sebuah token bersifat wajib. Anda dapat membuat token akses pribadi (Bearer authentication), atau mengotorisasi sebuah klien OAuth untuk membuatnya untuk Anda. Lihat Dokumentasi API.', + 'api_endpoint' => 'Endpoint API untuk contoh pemasangan Monica ini adalah:', + + 'api_personal_access_tokens' => 'Token akses pribadi', + 'api_pao_description' => 'Pastikan Anda memberikan token ini kepada sebuah sumber yang Anda percayai - karena token tersebut mengizinkan Anda untuk mengakses semua data Anda.', + 'api_token_title' => 'Token Akses Pribadi', + 'api_token_create_new' => 'Buat Token Baru', + 'api_token_not_created' => 'Anda belum membuat token akses pribadi apapun.', + 'api_token_name' => 'Nama token', + 'api_token_expire' => 'Kadaluarsa pada {date}', + 'api_token_delete' => 'Hapus', + 'api_token_create' => 'Buat Token', + 'api_token_scopes' => 'Lingkup', + 'api_token_help' => 'Ini adalah token akses pribadi baru Anda. Ditampilkan hanya sekali ini saja, jadi jangan hilangkan token tersebut! Anda sekarang dapat menggunakan token ini untuk membuat permintaan API.', + + 'api_oauth_clients' => 'Klien OAuth Anda', + 'api_oauth_clients_desc' => 'Bagian ini memungkinkan Anda mendaftarkan klien OAuth Anda sendiri.', + 'api_oauth_clients_desc2' => 'Gunakan ID klien ini untuk meminta sebuah token baru, dan mengkonversi kode otorisasi untuk mengakses token. Lihat Dokumentasi Passport Laravel untuk informasi lebih lanjut.', + 'api_oauth_title' => 'Klien OAuth', + 'api_oauth_create_new' => 'Buat Klien Baru', + 'api_oauth_edit' => 'Sunting Klien', + 'api_oauth_not_created' => 'Anda belum membuat klien OAuth apapun.', + 'api_oauth_clientid' => 'ID Klien', + 'api_oauth_name' => 'Nama', + 'api_oauth_name_help' => 'Sesuatu yang akan dikenali dan dipercaya oleh pengguna Anda.', + 'api_oauth_secret' => 'Kunci Rahasia', + 'api_oauth_create' => 'Buat Klien', + 'api_oauth_redirecturl' => 'URL Pengalihan', + 'api_oauth_redirecturl_help' => 'URL panggilan balik otorisasi aplikasi Anda.', + + 'api_authorized_clients' => 'Daftar klien resmi', + 'api_authorized_clients_desc' => 'Bagian ini mencantumkan semua klien yang telah Anda otorisasi untuk mengakses data aplikasi Anda. Anda dapat mencabut otorisasi ini kapan saja.', + 'api_authorized_clients_title' => 'Aplikasi Resmi', + 'api_authorized_clients_none' => 'Belum ada klien resmi.', + 'api_authorized_clients_name' => 'Nama', + 'api_authorized_clients_scopes' => 'Lingkup', + + 'personalization_tab_title' => 'Personalisasi akun Anda', + + 'personalization_title' => 'Di sini Anda akan menemukan pengaturan yang berbeda untuk mengkonfigurasi akun Anda. Fitur ini ditujukan untuk "pengguna super" yang menginginkan kontrol maksimum atas Monica.', + 'personalization_contact_field_type_title' => 'Jenis baris kontak', + 'personalization_contact_field_type_add' => 'Tambah jenis baris baru', + 'personalization_contact_field_type_description' => 'Anda dapat mengkonfigurasi semua jenis baris kontak yang berbeda yang dapat Anda kaitkan ke semua kontak Anda. Misalnya, jika sebuah jejaring sosial baru muncul di masa mendatang, Anda akan dapat menambahkan cara baru berkomunikasi ini dengan kontak Anda di sini.', + 'personalization_contact_field_type_table_name' => 'Nama', + 'personalization_contact_field_type_table_protocol' => 'Protokol', + 'personalization_contact_field_type_table_actions' => 'Tindakan', + 'personalization_contact_field_type_modal_title' => 'Tambah jenis baris kontak baru', + 'personalization_contact_field_type_modal_edit_title' => 'Sunting jenis bidang kontak yang telah tersedia', + 'personalization_contact_field_type_modal_delete_title' => 'Hapus jenis bidang kontak yang telah tersedia', + 'personalization_contact_field_type_modal_delete_description' => 'Apakah Anda yakin ingin menghapus jenis baris kontak ini? Menghapus jenis baris kontak ini akan menghapus SEMUA data dengan jenis ini untuk semua kontak Anda.', + 'personalization_contact_field_type_modal_name' => 'Nama', + 'personalization_contact_field_type_modal_protocol' => 'Protokol (opsional)', + 'personalization_contact_field_type_modal_protocol_help' => 'Setiap jenis baris kontak yang baru dapat diklik. Jika sebuah protokol diatur, kami akan menggunakannya untuk memicu tindakan yang diatur.', + 'personalization_contact_field_type_modal_icon' => 'Ikon (opsional)', + 'personalization_contact_field_type_modal_icon_help' => 'Anda dapat mengaitkan sebuah ikon dengan jenis baris kontak ini. Anda perlu menambahkan sebuah referensi ke sebuah ikon Font Awesome.', + 'personalization_contact_field_type_delete_success' => 'Jenis baris kontak telah berhasil dihapus.', + 'personalization_contact_field_type_add_success' => 'Jenis baris kontak telah berhasil ditambahkan.', + 'personalization_contact_field_type_edit_success' => 'Jenis baris kontak telah berhasil diperbarui.', + + 'personalization_genders_title' => 'Jenis Kelamin', + 'personalization_genders_add' => 'Tambahkan jenis kelamin baru', + 'personalization_genders_desc' => 'Anda dapat mendefinisikan jenis kelamin yang Anda butuhkan sebanyak mungkin. Anda memerlukan setidaknya satu jenis kelamin di akun Anda.', + 'personalization_genders_modal_add' => 'Tambah jenis kelamin', + 'personalization_genders_modal_edit' => 'Perbarui jenis kelamin', + 'personalization_genders_modal_name' => 'Nama', + 'personalization_genders_modal_name_help' => 'Nama yang digunakan untuk menampilkan jenis kelamin pada sebuah halaman kontak.', + 'personalization_genders_modal_sex' => 'Jenis Kelamin', + 'personalization_genders_modal_sex_help' => 'Digunakan untuk mendefinisikan hubungan relasi, dan selama proses impor/ekspor vCard.', + 'personalization_genders_modal_default' => 'Pilih jenis kelamin standar untuk kontak baru', + 'personalization_genders_modal_delete' => 'Hapus jenis kelamin', + 'personalization_genders_modal_delete_desc' => 'Apakah Anda yakin ingin menghapus jenis kelamin "{name}"?', + 'personalization_genders_modal_delete_question' => 'Anda saat ini memiliki {count} kontak dengan jenis kelamin ini. Jika Anda menghapus jenis kelamin ini, jenis kelamin apa yang harus dimiliki kontak tersebut?', + 'personalization_genders_modal_delete_question_default' => 'Jenis kelamin ini adalah yang standar. Jika Anda menghapus jenis kelamin ini, yang mana yang akan menjadi standar yang baru?', + 'personalization_genders_modal_error' => 'Silakan pilih sebuah jenis kelamin dari daftar.', + 'personalization_genders_list_contact_number' => '{count} kontak', + 'personalization_genders_table_name' => 'Nama', + 'personalization_genders_table_sex' => 'Jenis Kelamin', + 'personalization_genders_table_default' => 'Standar', + 'personalization_genders_default' => 'Jenis kelamin standar', + 'personalization_genders_make_default' => 'Ubah jenis kelamin standar', + 'personalization_genders_select_default' => 'Pilih jenis kelamin standar', + 'personalization_genders_m' => 'Pria', + 'personalization_genders_f' => 'Wanita', + 'personalization_genders_o' => 'Lainnya', + 'personalization_genders_u' => 'Tidak Diketahui', + 'personalization_genders_n' => 'Tidak ada atau tidak berlaku', + + 'personalization_reminder_rule_save' => 'Perubahan telah disimpan', + 'personalization_reminder_rule_title' => 'Aturan pengingat', + 'personalization_reminder_rule_line' => '{count} hari sebelumnya', + 'personalization_reminder_rule_desc' => 'Untuk setiap pengingat yang Anda telah atur, Monica dapat mengirimkan email kepada Anda beberapa hari sebelum peristiwa terjadi. Anda dapat menyesuaikan pengaturan pemberitahuan ini di sini. Pemberitahuan ini hanya berlaku untuk pengingat bulanan dan tahunan.', + + 'personalization_module_save' => 'Perubahan telah disimpan', + 'personalization_module_title' => 'Fitur', + 'personalization_module_desc' => 'Anda mungkin tidak memerlukan semua fitur Monica. Di bawah ini Anda dapat mengaktifkan fitur spesifik yang digunakan pada sebuah lembar kontak. Perubahan ini akan memengaruhi SEMUA kontak Anda. Menonaktifkan sebuah fitur tidak akan menghapus data apapun, hanya menyembunyikan fitur tersebut.', + + 'personalisation_paid_upgrade' => 'Ini adalah fitur premium yang memerlukan langganan Berbayar untuk dapat diaktifkan. Tingkatkan akun Anda dengan mengunjungi Pengaturan > Berlangganan.', + 'personalisation_paid_upgrade_vue' => 'Ini adalah fitur premium yang memerlukan sebuah langganan Berbayar untuk dapat diaktifkan. Tingkatkan akun Anda dengan mengunjungi Pengaturan > Berlangganan.', + + 'reminder_time_to_send' => 'Pengingat waktu dalam hari akan dikirim', + 'reminder_time_to_send_help' => 'Pengingat Anda berikutnya dijadwalkan untuk dikirimkan pada {dateTime}.', + + 'personalization_activity_type_category_title' => 'Kategori jenis aktifitas', + 'personalization_activity_type_category_add' => 'Tambahkan sebuah kategori jenis aktifitas baru', + 'personalization_activity_type_category_table_name' => 'Nama', + 'personalization_activity_type_category_description' => 'Sebuah aktifitas dengan salah satu kontak Anda dapat memiliki sebuah jenis dan sebuah jenis kategori. Akun Anda dilengkapi dengan satu set jenis kategori yang telah ditentukan secara standar, tetapi Anda dapat menyesuaikan ini di sini.', + 'personalization_activity_type_category_table_actions' => 'Tindakan', + 'personalization_activity_type_category_modal_add' => 'Tambahkan sebuah kategori jenis aktifitas baru', + 'personalization_activity_type_category_modal_edit' => 'Sunting sebuah kategori jenis aktifitas', + 'personalization_activity_type_category_modal_question' => 'Harus kita namakan apa kategori baru ini?', + 'personalization_activity_type_add_button' => 'Tambahkan sebuah jenis aktifitas baru', + 'personalization_activity_type_modal_add' => 'Tambahkan sebuah jenis aktifitas baru', + 'personalization_activity_type_modal_question' => 'Harus kita namakan apa aktifitas baru ini?', + 'personalization_activity_type_modal_edit' => 'Sunting sebuah jenis aktifitas', + 'personalization_activity_type_category_modal_delete' => 'Hapus sebuah kategori jenis aktifitas', + 'personalization_activity_type_category_modal_delete_desc' => 'Apakah Anda yakin ingin menghapus kategori ini? Menghapusnya akan menghapus semua jenis aktifitas terkait. Aktifitas yang termasuk kedalam kategori ini tidak akan terpengaruh oleh penghapusan ini.', + 'personalization_activity_type_modal_delete' => 'Hapus sebuah jenis aktifitas', + 'personalization_activity_type_modal_delete_desc' => 'Apakah Anda yakin ingin menghapus jenis aktifitas ini? Aktifitas yang termasuk kedalam kategori ini tidak akan terpengaruh oleh penghapusan ini.', + 'personalization_activity_type_modal_delete_error' => 'Kami tidak dapat menemukan jenis aktifitas ini.', + 'personalization_activity_type_category_modal_delete_error' => 'Kami tidak dapat menemukan kategori jenis aktifitas ini.', + + 'personalization_life_event_category_title' => 'Kategori peristiwa Kehidupan', + 'personalization_live_event_category_table_name' => 'Nama', + 'personalization_life_event_category_description' => 'Sebuah peristiwa kehidupan dapat memiliki sebuah jenis dan kategori. Akun Anda dilengkapi dengan serangkaian kategori dan jenis yang telah ditentukan secara standar, tetapi Anda dapat menyesuaikan jenis peritiswa kehidupan di sini.', + 'personalization_live_event_category_table_actions' => 'Tindakan', + 'personalization_life_event_type_add_button' => 'Tambahkan sebuah jenis peristiwa kehidupan baru', + 'personalization_life_event_type_modal_add' => 'Tambahkan sebuah jenis peristiwa kehidupan baru', + 'personalization_life_event_type_modal_question' => 'Harus kita namakan apa jenis peristiwa kehidupan baru ini?', + 'personalization_life_event_type_modal_edit' => 'Sunting sebuah jenis peristiwa kehidupan', + 'personalization_life_event_type_modal_delete' => 'Hapus sebuah jenis peristiwa kehidupan', + 'personalization_life_event_type_modal_delete_desc' => 'Apakah Anda yakin ingin menghapus jenis peristiwa kehidupan ini? Peristiwa kehidupan yang termasuk dalam jenis ini akan dihapus dengan melakukan tindakan ini.', + 'personalization_life_event_type_modal_delete_error' => 'Kami tidak dapat menemukan jenis peristiwa kehidupan ini.', + + 'personalization_life_event_category_work_education' => 'Pekerjaan & edukasi', + 'personalization_life_event_category_family_relationships' => 'Keluarga & hubungan relasi', + 'personalization_life_event_category_home_living' => 'Rumah & kehidupan', + 'personalization_life_event_category_travel_experiences' => 'Wisata & pengalaman', + 'personalization_life_event_category_health_wellness' => 'Kesehatan & kebugaran', + + 'personalization_life_event_type_new_job' => 'Pekerjaan baru', + 'personalization_life_event_type_retirement' => 'Pensiunan', + 'personalization_life_event_type_new_school' => 'Sekolah baru', + 'personalization_life_event_type_study_abroad' => 'Belajar diluar negeri', + 'personalization_life_event_type_volunteer_work' => 'Pekerjaan sukarela', + 'personalization_life_event_type_published_book_or_paper' => 'Menerbitkan sebuah buku atau makalah', + 'personalization_life_event_type_military_service' => 'Pelayanan militer', + 'personalization_life_event_type_first_met' => 'Pertama kali bertemu', + 'personalization_life_event_type_new_relationship' => 'Hubungan baru', + 'personalization_life_event_type_engagement' => 'Bertunangan', + 'personalization_life_event_type_marriage' => 'Pernikahan', + 'personalization_life_event_type_anniversary' => 'Perayaan Hari Pernikahan', + 'personalization_life_event_type_expecting_a_baby' => 'Mengharapkan seorang bayi', + 'personalization_life_event_type_new_child' => 'Anak baru', + 'personalization_life_event_type_new_family_member' => 'Anggota keluarga baru', + 'personalization_life_event_type_new_pet' => 'Hewan peliharan baru', + 'personalization_life_event_type_end_of_relationship' => 'Akhir dari hubungan', + 'personalization_life_event_type_loss_of_a_loved_one' => 'Kehilangan orang yang dicintai', + 'personalization_life_event_type_moved' => 'Pindah', + 'personalization_life_event_type_bought_a_home' => 'Membeli sebuah rumah', + 'personalization_life_event_type_home_improvement' => 'Perbaikan rumah', + 'personalization_life_event_type_holidays' => 'Liburan', + 'personalization_life_event_type_new_vehicle' => 'Kendaraan baru', + 'personalization_life_event_type_new_roommate' => 'Teman sekamar baru', + 'personalization_life_event_type_overcame_an_illness' => 'Sembuh dari sebuah penyakit', + 'personalization_life_event_type_quit_a_habit' => 'Berhenti dari kebiasaan', + 'personalization_life_event_type_new_eating_habits' => 'Kebiasaan makan baru', + 'personalization_life_event_type_weight_loss' => 'Turun berat badan', + 'personalization_life_event_type_wear_glass_or_contact' => 'Mulai mengenakan kacamata atau kontak', + 'personalization_life_event_type_broken_bone' => 'Patah tulang', + 'personalization_life_event_type_removed_braces' => 'Melepaskan kawat gigi', + 'personalization_life_event_type_surgery' => 'Menjalani operasi', + 'personalization_life_event_type_dentist' => 'Melakukan perawatan gigi', + 'personalization_life_event_type_new_sport' => 'Memulai memainkan sebuah olahraga baru', + 'personalization_life_event_type_new_hobby' => 'Mengambil sebuah hobi baru', + 'personalization_life_event_type_new_instrument' => 'Memulai belajar sebuah instrumen baru', + 'personalization_life_event_type_new_language' => 'Memulai belajar sebuah bahasa baru', + 'personalization_life_event_type_tattoo_or_piercing' => 'Tato atau tindik', + 'personalization_life_event_type_new_license' => 'SIM baru', + 'personalization_life_event_type_travel' => 'Wisata', + 'personalization_life_event_type_achievement_or_award' => 'Prestasi atau penghargaan', + 'personalization_life_event_type_changed_beliefs' => 'Berubah keyakinan', + 'personalization_life_event_type_first_word' => 'Kata pertama', + 'personalization_life_event_type_first_kiss' => 'Ciuman pertama', + + 'storage_title' => 'Ruang Peyimpanan', + 'storage_account_info' => 'Batas akun Anda adalah :accountLimit MB. Penggunaan Anda saat ini adalah :currentAccountSize MB (sekitar :percentUsage%).', + 'storage_upgrade_notice' => 'Tingkatkan akun Anda àgar dapat mengunggah dokumen dan foto.', + 'storage_description' => 'Di sini Anda dapat melihat semua dokumen dan foto yang telah diunggah tentang kontak Anda.', + + 'dav_title' => 'WebDAV', + 'dav_description' => 'Di sini Anda dapat menemukan semua pengaturan untuk menggunakan sumber daya WebDAV untuk ekspor CardDAV dan CalDAV.', + 'dav_copy_help' => 'Salin ke papanklip Anda', + 'dav_clipboard_copied' => 'Nilai disalin ke dalam papanklip Anda', + 'dav_url_base' => 'Url Dasar untuk semua sumber daya CardDAV dan CalDAV:', + 'dav_connect_help' => 'Anda dapat menghubungkan kontak dan/atau kalender Anda dengan url dasar ini pada ponsel atau komputer Anda.', + 'dav_connect_help2' => 'Gunakan login Anda (email) dan buat sebuah token API sebagai kata sandi untuk mengotentikasi.', + 'dav_url_carddav' => 'Url CardDAV untuk sumber daya Kontak:', + 'dav_url_caldav_birthdays' => 'Url CalDAV untuk sumber daya Ulang Tahun:', + 'dav_url_caldav_tasks' => 'Url CalDAV untuk sumber daya Tugas:', + 'dav_title_carddav' => 'CardDAV', + 'dav_title_caldav' => 'CalDAV', + 'dav_carddav_export' => 'Ekspor semua kontak dalam satu berkas', + 'dav_caldav_birthdays_export' => 'Ekspor semua ulang tahun dalam satu berkas', + 'dav_caldav_tasks_export' => 'Ekspor semua tugas dalam satu berkas', + + 'archive_title' => 'Arsipkan semua kontak di akun Anda', + 'archive_desc' => 'Ini akan mengarsipkan semua kontak di akun Anda.', + 'archive_cta' => 'Arsipkan semua kontak Anda', + + 'logs_title' => 'Segala sesuatu yang telah terjadi pada akun ini', + 'logs_actor' => 'Aktor', + 'logs_timestamp' => 'Timestamp', + 'logs_description' => 'Deskripsi', + 'logs_subject' => 'Subyek', + 'logs_size' => 'Ukuran (Kb)', + 'logs_object' => 'Obyek', +]; diff --git a/resources/lang/id/validation.php b/resources/lang/id/validation.php new file mode 100644 index 0000000..e6c67b0 --- /dev/null +++ b/resources/lang/id/validation.php @@ -0,0 +1,166 @@ + ':attribute harus diterima.', + 'active_url' => ':atrribute bukan sebuah URL yang valid.', + 'after' => ':attribute harus merupakan tanggal setelah :date.', + 'after_or_equal' => ':attribute harus merupakan sebuah tanggal setelah atau sama dengan :date.', + 'alpha' => ':attribute hanya boleh berisi huruf.', + 'alpha_dash' => ':attribute hanya dapat berisi huruf, angka, tanda hubung dan garis bawah.', + 'alpha_num' => ':attribute hanya boleh berisi huruf dan angka.', + 'array' => ':attribute harus merupakan array.', + 'before' => ':attribute harus merupakan seubah tanggal sebelum :date.', + 'before_or_equal' => ':attribute harus merupakan sebuah tanggal sebelum atau sama dengan :date.', + 'between' => [ + 'numeric' => 'attribute: harus berada antara :min dan :max.', + 'file' => 'attribute: harus berada antara :min dan :max kilobytes.', + 'string' => 'attribute: harus berada antara :min dan :max karakter.', + 'array' => ':attribute harus memiliki item antara :min dan :max.', + ], + 'boolean' => 'Baris :attribute harus true atau false.', + 'confirmed' => 'Konfirmasi :attribute tidak cocok.', + 'date' => ':attribute bukan sebuah tanggal yang valid.', + 'date_equals' => ':attribute harus berupa tanggal yang sama dengan :date.', + 'date_format' => ':attribute tidak cocok dengan format :format.', + 'different' => ':attribute dan :other harus berbeda.', + 'digits' => ':attribute harus berupa digit :digits.', + 'digits_between' => ':attribute harus berada diantara :min dan :max digit.', + 'dimensions' => ':attribute memiliki dimensi gambar tidak valid.', + 'distinct' => 'Baris :attribute memiliki sebuah nilai duplikat.', + 'email' => ':attribute harus berupa alamat email yang valid.', + 'ends_with' => ':attribute harus diakhiri dengan salah satu dari hal berikut: :values.', + 'exists' => ':attribute yang dipilih tidak valid.', + 'file' => ':attribute harus berupa sebuah berkas.', + 'filled' => 'Baris :attribute harus memiliki sebuah nilai.', + 'gt' => [ + 'numeric' => ':attribute harus lebih besar dari :value.', + 'file' => ':attribute harus lebih besar dari :value kilobytes.', + 'string' => ':attribute harus lebih besar dari :value karakter.', + 'array' => ':attribute harus lebih besar dari :value item.', + ], + 'gte' => [ + 'numeric' => ':attribute harus lebih besar dari atau sama dengan :value.', + 'file' => ':attribute harus lebih besar dari atau sama dengan :value kilobytes.', + 'string' => ':attribute harus lebih besar dari atau sama dengan :value karakter.', + 'array' => ':attribute harus memiliki item :value atau lebih.', + ], + 'image' => ':attribute harus berupa sebuah gambar.', + 'in' => ':attribute yang dipilih tidak valid.', + 'in_array' => 'Baris :attribute tidak tersedia di :other.', + 'integer' => ':attribute harus berupa bilangan bulat.', + 'ip' => ':attribute harus berupa sebuah alamat IP yang valid.', + 'ipv4' => ':attribute harus berupa sebuah alamat IPv4 yang valid.', + 'ipv6' => ':attribute harus berupa sebuah alamat IPv6 yang valid.', + 'json' => ':attribute harus berupa sebuah string JSON yang valid.', + 'lt' => [ + 'numeric' => ':attribute harus kurang dari :value.', + 'file' => ':attribute harus kurang dari :value kilobytes.', + 'string' => ':attribute harus kurang dari :value karakter.', + 'array' => ':attribute harus kurang dari :value item.', + ], + 'lte' => [ + 'numeric' => ':attribute harus kurang dari atau sama dengan :value.', + 'file' => ':attribute harus kurang dari atau sama dengan :value kilobytes.', + 'string' => ':attribute harus kurang dari atau sama dengan :value karakter.', + 'array' => ':attribute harus kurang dari atau sama dengan :value item.', + ], + 'max' => [ + 'numeric' => ':attribute tidak boleh lebih besar dari :max.', + 'file' => ':attribute tidak boleh lebih besar dari :max kilobytes.', + 'string' => ':attribute tidak boleh lebih besar dari :max karakter.', + 'array' => ':attribute tidak boleh lebih besar dari :max item.', + ], + 'mimes' => ':attribute harus berupa sebuah berkas berjenis :values.', + 'mimetypes' => ':attribute harus berupa sebuah berkas berjenis :values.', + 'min' => [ + 'numeric' => ':attribute harus setidaknya :min.', + 'file' => ':attribute harus setidaknya :min kilobytes.', + 'string' => ':attribute harus setidaknya :min karakter.', + 'array' => ':attribute harus setidaknya :min item.', + ], + 'not_in' => ':attribute yang dipilih tidak valid.', + 'not_regex' => 'Format :attribute tidak benar.', + 'numeric' => ':attribute harus berupa angka.', + 'password' => 'Kata sandi tidak benar.', + 'present' => 'Baris :attribute harus tersedia.', + 'regex' => 'Format :attribute tidak valid.', + 'required' => 'Baris :attribute diperlukan.', + 'required_if' => 'Baris :attribute diperlukan ketika :other adalah :value.', + 'required_unless' => 'Baris :attribute diperlukan kecuali :other di dalam :values.', + 'required_with' => 'Baris :attribute diperlukan ketika :values tersedia.', + 'required_with_all' => 'Baris :attribute diperlukan ketika :values tersedia.', + 'required_without' => 'Baris :attribute diperlukan ketika :values tidak tersedia.', + 'required_without_all' => 'Baris :attribute diperlukan ketika tidak ada dari :values tersedia.', + 'same' => ':attribute dan :other harus cocok.', + 'size' => [ + 'numeric' => ':attribute harus :size.', + 'file' => ':attribute harus :size kilobytes.', + 'string' => ':attribute harus :size karakter.', + 'array' => ':attribute harus :size item.', + ], + 'starts_with' => ':attribute harus dimulai dengan salah satu dari berikut: :values.', + 'string' => ':attribute harus berupa sebuah string.', + 'timezone' => ':attribute harus berupa sebuah zona yang valid.', + 'unique' => ':attribute telah diambil/dipakai.', + 'uploaded' => ':attribute gagal mengunggah.', + 'url' => 'Format :attribute tidak valid.', + 'uuid' => ':attribute harus berupa sebuah UUID yang valid.', + + /* + |-------------------------------------------------------------------------- + | Custom Validation Language Lines + |-------------------------------------------------------------------------- + | + | Here you may specify custom validation messages for attributes using the + | convention "attribute.rule" to name the lines. This makes it quick to + | specify a specific custom language line for a given attribute rule. + | + */ + + 'custom' => [ + 'attribute-name' => [ + 'rule-name' => 'custom-message', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Custom Validation Attributes + |-------------------------------------------------------------------------- + | + | The following language lines are used to swap our attribute placeholder + | with something more reader friendly such as "E-Mail Address" instead + | of "email". This simply helps us make our message more expressive. + | + */ + + 'attributes' => [], + + 'vue' => [ + 'max' => [ + 'numeric' => '{field} tidak boleh lebih dari {max}.', + 'string' => '{field} tidak boleh lebih dari {max} karakter.', + ], + 'required' => '{field} diperlukan.', + 'url' => '{field} bukan sebuah alamat URL yang valid.', + ], + +]; diff --git a/resources/lang/it.json b/resources/lang/it.json new file mode 100644 index 0000000..ad05e5d --- /dev/null +++ b/resources/lang/it.json @@ -0,0 +1,7 @@ +{ + "The :attribute must contain at least one uppercase and one lowercase letter.": ":attribute deve contenere almeno un carattere maiuscolo ed uno minuscolo.", + "The :attribute must contain at least one letter.": ":attribute deve contenere almeno una lettera.", + "The :attribute must contain at least one symbol.": ":attribute deve contenere almeno un carattere speciale.", + "The :attribute must contain at least one number.": ":attribute deve contenere almeno un numero.", + "The given :attribute has appeared in a data leak. Please choose a different :attribute.": ":attribute sembra che faccia parte di un archivio con dati rubati. Per piacere, utilizza un valore differente." +} diff --git a/resources/lang/it/app.php b/resources/lang/it/app.php new file mode 100644 index 0000000..28fa3eb --- /dev/null +++ b/resources/lang/it/app.php @@ -0,0 +1,571 @@ + 'Sì', + 'no' => 'No', + 'update' => 'Aggiorna', + 'save' => 'Salva', + 'add' => 'Aggiungi', + 'cancel' => 'Annulla', + 'confirm' => 'Conferma', + 'delete_confirm' => 'Sei sicuro?', + 'delete' => 'Elimina', + 'edit' => 'Modifica', + 'upload' => 'Carica', + 'download' => 'Scarica', + 'save_close' => 'Salva e chiudi', + 'close' => 'Chiudi', + 'copy' => 'Copia', + 'create' => 'Crea', + 'remove' => 'Elimina', + 'revoke' => 'Revoca', + 'done' => 'Fatto', + 'back' => 'Indietro', + 'verify' => 'Verifica', + 'new' => 'nuovo', + 'unknown' => 'Non so', + 'load_more' => 'Carica altro', + 'loading' => 'Caricamento…', + 'with' => 'con', + 'today' => 'oggi', + 'yesterday' => 'ieri', + 'another_day' => 'un altro giorno', + 'date' => 'Data', + 'type' => 'Tipo', + 'zoom' => 'Zoom', + 'upgrade' => 'Effettua l\'upgrade per sbloccare', + 'percent_uploaded' => '{percent}% caricato', + 'retry' => 'Riprova', + 'filter' => 'Filtra la lista', + 'go_back' => 'Torna indietro', + 'file_selected' => 'Un file selezionato…|{count} file selezionati…', + + 'application_title' => 'Monica – personal relationship manager', + 'application_description' => 'Monica è uno strumento per gestire le interazioni con i vostri cari, amici e familiari.', + 'application_og_title' => 'Stabilisci relazioni migliori con i tuoi cari. CRM gratuito online per amici e famiglia.', + + 'markdown_description' => 'Vuoi formattare il tuo testo? Supportiamo Markdown per grassetto, corsivo, liste, e altro ancora.', + 'markdown_link' => 'Leggi documentazione', + + 'header_settings_link' => 'Impostazioni', + 'header_logout_link' => 'Disconnettiti', + 'header_changelog_link' => 'Modifiche di prodotto', + + 'main_nav_cta' => 'Aggiungi contatti', + 'main_nav_dashboard' => 'Home', + 'main_nav_family' => 'Contatti', + 'main_nav_journal' => 'Diario', + 'main_nav_activities' => 'Attività', + 'main_nav_tasks' => 'Compiti', + + 'footer_remarks' => 'Commenti?', + 'footer_send_email' => 'Inviaci un\'email', + 'footer_privacy' => 'Privacy', + 'footer_release' => 'Note di rilascio', + 'footer_newsletter' => 'Newsletter', + 'footer_source_code' => 'Monica su GitHub', + 'footer_version' => 'Versione: :version', + 'footer_new_version' => 'È disponibile una nuova versione di Monica', + + 'footer_modal_version_whats_new' => 'Novità', + 'footer_modal_version_release_away' => 'La tua versione è 1 versione indietro rispetto all\'ultima disponibile. Dovresti aggiornare Monica.|La tua versione è :number versioni indietro rispetto all\'ultima disponibile. Dovresti aggiornare Monica.', + + 'breadcrumb_dashboard' => 'Home', + 'breadcrumb_list_contacts' => 'Lista dei contatti', + 'breadcrumb_archived_contacts' => 'Contatti archiviati', + 'breadcrumb_journal' => 'Diario', + 'breadcrumb_settings' => 'Impostazioni', + 'breadcrumb_settings_export' => 'Esporta', + 'breadcrumb_settings_users' => 'Utenti', + 'breadcrumb_settings_users_add' => 'Aggiungi un utente', + 'breadcrumb_settings_subscriptions' => 'Sottoscrizioni', + 'breadcrumb_settings_import' => 'Importa', + 'breadcrumb_settings_import_report' => 'Resoconto dell\'importazione', + 'breadcrumb_settings_import_upload' => 'Carica', + 'breadcrumb_settings_tags' => 'Etichette', + 'breadcrumb_add_significant_other' => 'Aggiungi partner', + 'breadcrumb_edit_significant_other' => 'Modifica partner', + 'breadcrumb_add_note' => 'Aggiungi una nota', + 'breadcrumb_edit_note' => 'Modifica nota', + 'breadcrumb_api' => 'API', + 'breadcrumb_dav' => 'Risorse DAV', + 'breadcrumb_edit_introductions' => 'Come vi siete conosciuti', + 'breadcrumb_settings_personalization' => 'Personalizzazione', + 'breadcrumb_settings_security' => 'Sicurezza', + 'breadcrumb_settings_security_2fa' => 'Autenticazione due fattori', + 'breadcrumb_profile' => 'Profilo di :name', + + 'gender_male' => 'Uomo', + 'gender_female' => 'Donna', + 'gender_none' => 'Preferisco non specificarlo', + 'gender_no_gender' => 'Nessun genere', + + 'error_title' => 'Ops! Qualcosa è andato storto.', + 'error_unauthorized' => 'Non hai il permesso di aggiornare questa risorsa.', + 'error_user_account' => 'Questo utente non appartiene all\'account corrente.', + 'error_save' => 'Abbiamo avuto un errore cercando di salvare i dati.', + 'error_try_again' => 'Qualcosa è andato storto. Riprova.', + 'error_id' => 'ID errore: :id', + 'error_unavailable' => 'Servizio non disponibile', + 'error_maintenance' => 'Manutenzione in corso. Torneremo presto.', + 'error_help' => 'Torneremo presto.', + 'error_twitter' => 'Seguici sul nostro account Twitter per venire notificato quando saremo di nuovo online.', + 'error_no_term' => 'Non ci sono ancora regole per questa istanza.', + + 'default_save_success' => 'I dati sono stati salvati.', + + 'compliance_title' => 'Ci scusiamo per l\'interruzione.', + 'compliance_desc' => 'Abbiamo cambiato i nostri Termini di Utilizzo e Privacy Policy. Per legge dobbiamo chiederti di controllarli e accettarli prima di poter continuare a utilizzare il tuo account.', + 'compliance_desc_end' => 'Non facciamo nulla di losco con i tuoi dati, nè lo faremo mai.', + 'compliance_terms' => 'Accetta i nuovi termini e privacy policy', + + // Relationship types + // Yes, each relationship type has 8 strings associated with it. + // This is because we need to indicate the name of the relationship type, + // and also the name of the opposite side of this relationship (father/son), + // and then, the feminine version of the string. Finally, in some sentences + // in the UI, we need to include the name of the person we add the relationship + // to. + 'relationship_type_group_love' => 'Relazioni d\'amore', + 'relationship_type_group_family' => 'Relazioni familiari', + 'relationship_type_group_friend' => 'Rapporti di amicizia', + 'relationship_type_group_work' => 'Rapporti di lavoro', + 'relationship_type_group_other' => 'Altri tipi di relazioni', + + 'relationship_type_partner' => 'partner', + 'relationship_type_partner_female' => 'partner', + 'relationship_type_partner_male' => 'partner', + 'relationship_type_partner_with_name' => 'partner di :name', + 'relationship_type_partner_female_with_name' => 'partner di :name', + 'relationship_type_partner_male_with_name' => 'partner di :name', + + 'relationship_type_spouse' => 'marito', + 'relationship_type_spouse_female' => 'moglie', + 'relationship_type_spouse_male' => 'marito', + 'relationship_type_spouse_with_name' => 'marito di :name', + 'relationship_type_spouse_female_with_name' => 'Moglie di :name', + 'relationship_type_spouse_male_with_name' => 'Marito di :name', + + 'relationship_type_date' => 'impegnato', + 'relationship_type_date_female' => 'impegnata', + 'relationship_type_date_male' => 'data', + 'relationship_type_date_with_name' => 'impegnato con :name', + 'relationship_type_date_female_with_name' => 'impegnata con :name', + 'relationship_type_date_male_with_name' => 'ragazzo di :name', + + 'relationship_type_lover' => 'amante', + 'relationship_type_lover_female' => 'amante', + 'relationship_type_lover_male' => 'amante', + 'relationship_type_lover_with_name' => ': nome amante', + 'relationship_type_lover_female_with_name' => ': nome amante', + 'relationship_type_lover_male_with_name' => 'amante di :name', + + 'relationship_type_inlovewith' => 'innamorati di', + 'relationship_type_inlovewith_female' => 'innamorati di', + 'relationship_type_inlovewith_male' => 'innamorato di', + 'relationship_type_inlovewith_with_name' => 'qualcuno: è innamorato di', + 'relationship_type_inlovewith_female_with_name' => 'qualcuno di cui :name è innamorata', + 'relationship_type_inlovewith_male_with_name' => 'qualcuno di cui :name è innamorato', + + 'relationship_type_lovedby' => 'amato da', + 'relationship_type_lovedby_female' => 'amata da', + 'relationship_type_lovedby_male' => 'amato da', + 'relationship_type_lovedby_with_name' => 'amante segreto di :name', + 'relationship_type_lovedby_female_with_name' => 'amante segreto di :name', + 'relationship_type_lovedby_male_with_name' => 'amante segreto di :name', + + 'relationship_type_ex' => 'ex compagno/a', + 'relationship_type_ex_female' => 'ex-fidanzata', + 'relationship_type_ex_male' => 'ex-fidanzato', + 'relationship_type_ex_with_name' => 'Ex compagno/a di :name', + 'relationship_type_ex_female_with_name' => 'ex-fidanzata di :name', + 'relationship_type_ex_male_with_name' => 'Ex fidanzato di :name', + + 'relationship_type_parent' => 'genitore', + 'relationship_type_parent_female' => 'madre', + 'relationship_type_parent_male' => 'padre', + 'relationship_type_parent_with_name' => 'Genitore di :name', + 'relationship_type_parent_female_with_name' => 'madre di :name', + 'relationship_type_parent_male_with_name' => 'Padre di :name', + + 'relationship_type_child' => 'bambino/a', + 'relationship_type_child_female' => 'figlia', + 'relationship_type_child_male' => 'figlio', + 'relationship_type_child_with_name' => 'Bambino/a di :name', + 'relationship_type_child_female_with_name' => 'figlia di :name', + 'relationship_type_child_male_with_name' => 'Figlio di :name', + + 'relationship_type_stepparent' => 'genitore adottivo', + 'relationship_type_stepparent_female' => 'madrigna', + 'relationship_type_stepparent_male' => 'padre adottivo', + 'relationship_type_stepparent_with_name' => 'Genitore adottivo di :name', + 'relationship_type_stepparent_female_with_name' => ':name della matrigna', + 'relationship_type_stepparent_male_with_name' => 'Padre adottivo di :name', + + 'relationship_type_stepchild' => 'figlio/a adottivo', + 'relationship_type_stepchild_female' => 'figliastra', + 'relationship_type_stepchild_male' => 'figlio adottivo', + 'relationship_type_stepchild_with_name' => 'Figlio/a adottivo/a di :name', + 'relationship_type_stepchild_female_with_name' => ':name della figliastra', + 'relationship_type_stepchild_male_with_name' => 'Figlio adottivo di :name', + + 'relationship_type_sibling' => 'fratello/sorella', + 'relationship_type_sibling_female' => 'sorella', + 'relationship_type_sibling_male' => 'fratello', + 'relationship_type_sibling_with_name' => 'Fratello/Sorella di :name', + 'relationship_type_sibling_female_with_name' => 'sorella di :name', + 'relationship_type_sibling_male_with_name' => 'Fratello di :name', + + 'relationship_type_grandparent' => 'nonno/a', + 'relationship_type_grandparent_female' => 'nonna', + 'relationship_type_grandparent_male' => 'nonno', + 'relationship_type_grandparent_with_name' => 'Nonno/a di :name', + 'relationship_type_grandparent_female_with_name' => 'Nonna di :name', + 'relationship_type_grandparent_male_with_name' => 'Nonno di :name', + + 'relationship_type_grandchild' => 'nipote', + 'relationship_type_grandchild_female' => 'nipote', + 'relationship_type_grandchild_male' => 'nipote', + 'relationship_type_grandchild_with_name' => 'Nipote di :name', + 'relationship_type_grandchild_female_with_name' => 'Nipotina di :name', + 'relationship_type_grandchild_male_with_name' => 'Nipotino di :name', + + 'relationship_type_uncle' => 'zio', + 'relationship_type_uncle_female' => 'zia', + 'relationship_type_uncle_male' => 'zio', + 'relationship_type_uncle_with_name' => 'zio di :name', + 'relationship_type_uncle_female_with_name' => 'zia di :name', + 'relationship_type_uncle_male_with_name' => 'zio di :name', + + 'relationship_type_nephew' => 'nipote', + 'relationship_type_nephew_female' => 'nipote', + 'relationship_type_nephew_male' => 'nipote', + 'relationship_type_nephew_with_name' => 'nipote di :name', + 'relationship_type_nephew_female_with_name' => 'nipote di :name', + 'relationship_type_nephew_male_with_name' => 'nipote di :name', + + 'relationship_type_cousin' => 'cugino', + 'relationship_type_cousin_female' => 'cugino', + 'relationship_type_cousin_male' => 'cugino', + 'relationship_type_cousin_with_name' => 'Cugino di :name', + 'relationship_type_cousin_female_with_name' => 'Cugina di :name', + 'relationship_type_cousin_male_with_name' => 'cugino di :name', + + 'relationship_type_godfather' => 'padrino', + 'relationship_type_godfather_female' => 'madrina', + 'relationship_type_godfather_male' => 'padrino', + 'relationship_type_godfather_with_name' => 'Padrino/Madrina di :name', + 'relationship_type_godfather_female_with_name' => ': nome della madrina', + 'relationship_type_godfather_male_with_name' => 'Padrino di :name', + + 'relationship_type_godson' => 'figlioccio/a', + 'relationship_type_godson_female' => 'figlioccia', + 'relationship_type_godson_male' => 'figlioccio', + 'relationship_type_godson_with_name' => 'Figlioccio/a di :name', + 'relationship_type_godson_female_with_name' => 'figlioccia di :name', + 'relationship_type_godson_male_with_name' => 'Figlioccio di :name', + + 'relationship_type_friend' => 'amico', + 'relationship_type_friend_female' => 'amico', + 'relationship_type_friend_male' => 'amico', + 'relationship_type_friend_with_name' => 'amico di :name', + 'relationship_type_friend_female_with_name' => 'amica di :name', + 'relationship_type_friend_male_with_name' => 'amico di :name', + + 'relationship_type_bestfriend' => 'miglior amico', + 'relationship_type_bestfriend_female' => 'miglior amico', + 'relationship_type_bestfriend_male' => 'migliore amico', + 'relationship_type_bestfriend_with_name' => 'miglior amico di :name', + 'relationship_type_bestfriend_female_with_name' => 'miglior amica di :name', + 'relationship_type_bestfriend_male_with_name' => 'migliore amico di :name', + + 'relationship_type_colleague' => 'collega', + 'relationship_type_colleague_female' => 'collega', + 'relationship_type_colleague_male' => 'collega', + 'relationship_type_colleague_with_name' => 'collega di :name', + 'relationship_type_colleague_female_with_name' => 'collega di :name', + 'relationship_type_colleague_male_with_name' => 'collega di :name', + + 'relationship_type_boss' => 'capo', + 'relationship_type_boss_female' => 'capo', + 'relationship_type_boss_male' => 'capo', + 'relationship_type_boss_with_name' => 'capo di :name', + 'relationship_type_boss_female_with_name' => 'capo di :name', + 'relationship_type_boss_male_with_name' => 'capo di :name', + + 'relationship_type_subordinate' => 'dipendente', + 'relationship_type_subordinate_female' => 'dipendente', + 'relationship_type_subordinate_male' => 'subordinate', + 'relationship_type_subordinate_with_name' => 'dipendente di :name', + 'relationship_type_subordinate_female_with_name' => 'dipendente di :name', + 'relationship_type_subordinate_male_with_name' => ':name’s subordinate', + + 'relationship_type_mentor' => 'mentore', + 'relationship_type_mentor_female' => 'mentrice', + 'relationship_type_mentor_male' => 'mentore', + 'relationship_type_mentor_with_name' => 'mentore di :name', + 'relationship_type_mentor_female_with_name' => 'mentrice di :name', + 'relationship_type_mentor_male_with_name' => 'mentore di :name', + + 'relationship_type_protege' => 'protetto', + 'relationship_type_protege_female' => 'protetta', + 'relationship_type_protege_male' => 'protégé', + 'relationship_type_protege_with_name' => 'Protetto di :name', + 'relationship_type_protege_female_with_name' => 'Protetta di :name', + 'relationship_type_protege_male_with_name' => ':name’s protégé', + + 'relationship_type_ex_husband' => 'ex coniuge', + 'relationship_type_ex_husband_female' => 'ex moglie', + 'relationship_type_ex_husband_male' => 'ex marito', + 'relationship_type_ex_husband_with_name' => 'Ex coniuge di :name', + 'relationship_type_ex_husband_female_with_name' => 'ex moglie di :name', + 'relationship_type_ex_husband_male_with_name' => 'Ex marito di :name', + + // emotions + 'emotion_primary_love' => 'Amore', + 'emotion_primary_joy' => 'Felicità', + 'emotion_primary_surprise' => 'Sorpresa', + 'emotion_primary_anger' => 'Rabbia', + 'emotion_primary_sadness' => 'Tristezza', + 'emotion_primary_fear' => 'Paura', + + 'emotion_secondary_affection' => 'Affetto', + 'emotion_secondary_lust' => 'Lussuria', + 'emotion_secondary_longing' => 'Bramosia', + 'emotion_secondary_cheerfulness' => 'Allegria', + 'emotion_secondary_zest' => 'Gusto', + 'emotion_secondary_contentment' => 'Soddisfazione', + 'emotion_secondary_pride' => 'Orgoglio', + 'emotion_secondary_optimism' => 'Ottimismo', + 'emotion_secondary_enthrallment' => 'Fascino', + 'emotion_secondary_relief' => 'Sollievo', + 'emotion_secondary_surprise' => 'Sorpresa', + 'emotion_secondary_irritation' => 'Irritazione', + 'emotion_secondary_exasperation' => 'Esasperazione', + 'emotion_secondary_rage' => 'Rabbia', + 'emotion_secondary_disgust' => 'Disgusto', + 'emotion_secondary_envy' => 'Invidia', + 'emotion_secondary_suffering' => 'Sofferenza', + 'emotion_secondary_sadness' => 'Tristezza', + 'emotion_secondary_disappointment' => 'Disappunto', + 'emotion_secondary_shame' => 'Vergogna', + 'emotion_secondary_neglect' => 'Negligenza', + 'emotion_secondary_sympathy' => 'Simpatia', + 'emotion_secondary_horror' => 'Orrore', + 'emotion_secondary_nervousness' => 'Nervosismo', + + 'emotion_adoration' => 'Adorazione', + 'emotion_affection' => 'Affetto', + 'emotion_love' => 'Amore', + 'emotion_fondness' => 'Passione', + 'emotion_liking' => 'Simpatia', + 'emotion_attraction' => 'Attrazione', + 'emotion_caring' => 'Cura', + 'emotion_tenderness' => 'Tenerezza', + 'emotion_compassion' => 'Compassione', + 'emotion_sentimentality' => 'Sentimentalità', + 'emotion_arousal' => 'Eccitazione', + 'emotion_desire' => 'Desiderio', + 'emotion_lust' => 'Lussuria', + 'emotion_passion' => 'Passione', + 'emotion_infatuation' => 'Infatuazione', + 'emotion_longing' => 'Bramosia', + 'emotion_amusement' => 'Divertimento', + 'emotion_bliss' => 'Beatitudine', + 'emotion_cheerfulness' => 'Allegria', + 'emotion_gaiety' => 'Giocondità', + 'emotion_glee' => 'Gioia', + 'emotion_jolliness' => 'Gaietà', + 'emotion_joviality' => 'Giovialità', + 'emotion_joy' => 'Felicità', + 'emotion_delight' => 'Delizia', + 'emotion_enjoyment' => 'Godimento', + 'emotion_gladness' => 'Contentezza', + 'emotion_happiness' => 'Felicità', + 'emotion_jubilation' => 'Esultanza', + 'emotion_elation' => 'Esaltazione', + 'emotion_satisfaction' => 'Soddisfazione', + 'emotion_ecstasy' => 'Estasi', + 'emotion_euphoria' => 'Euforia', + 'emotion_enthusiasm' => 'Entusiasmo', + 'emotion_zeal' => 'Zelo', + 'emotion_zest' => 'Gusto', + 'emotion_excitement' => 'Eccitamento', + 'emotion_thrill' => 'Fremito', + 'emotion_exhilaration' => 'Esilarante', + 'emotion_contentment' => 'Appagamento', + 'emotion_pleasure' => 'Piacere', + 'emotion_pride' => 'Orgoglio', + 'emotion_eagerness' => 'Impazienza', + 'emotion_hope' => 'Speranza', + 'emotion_optimism' => 'Ottimismo', + 'emotion_enthrallment' => 'Divertimento', + 'emotion_rapture' => 'Estasi', + 'emotion_relief' => 'Sollievo', + 'emotion_amazement' => 'Stupore', + 'emotion_surprise' => 'Sorpresa', + 'emotion_astonishment' => 'Stupefacente', + 'emotion_aggravation' => 'Peggioramento', + 'emotion_irritation' => 'Irritazione', + 'emotion_agitation' => 'Agitazione', + 'emotion_annoyance' => 'Fastidio', + 'emotion_grouchiness' => 'Cattivo umore', + 'emotion_grumpiness' => 'Irritabilità', + 'emotion_exasperation' => 'Esasperazione', + 'emotion_frustration' => 'Frustrazione', + 'emotion_anger' => 'Rabbia', + 'emotion_rage' => 'Collera', + 'emotion_outrage' => 'Oltraggio', + 'emotion_fury' => 'Furia', + 'emotion_wrath' => 'Ira', + 'emotion_hostility' => 'Ostilità', + 'emotion_ferocity' => 'Ferocità', + 'emotion_bitterness' => 'Amarezza', + 'emotion_hate' => 'Odio', + 'emotion_loathing' => 'Ripugnanza', + 'emotion_scorn' => 'Disprezzo', + 'emotion_spite' => 'Rancore', + 'emotion_vengefulness' => 'Vendicatività', + 'emotion_dislike' => 'Antipatia', + 'emotion_resentment' => 'Risentimento', + 'emotion_disgust' => 'Disgusto', + 'emotion_revulsion' => 'Repulsione', + 'emotion_contempt' => 'Disprezzo', + 'emotion_envy' => 'Invidia', + 'emotion_jealousy' => 'Gelosia', + 'emotion_agony' => 'Agonia', + 'emotion_suffering' => 'Sofferenza', + 'emotion_hurt' => 'Dolore', + 'emotion_anguish' => 'Angoscia', + 'emotion_depression' => 'Depressione', + 'emotion_despair' => 'Disperazione', + 'emotion_hopelessness' => 'Senza speranza', + 'emotion_gloom' => 'Maliconia', + 'emotion_glumness' => 'Cupezza', + 'emotion_sadness' => 'Tristezza', + 'emotion_unhappiness' => 'Infelicità', + 'emotion_grief' => 'Afflizione', + 'emotion_sorrow' => 'Pena', + 'emotion_woe' => 'Calamità', + 'emotion_misery' => 'Miseria', + 'emotion_melancholy' => 'Malinconia', + 'emotion_dismay' => 'Sgomento', + 'emotion_disappointment' => 'Disappunto', + 'emotion_displeasure' => 'Scontento', + 'emotion_guilt' => 'Colpevolezza', + 'emotion_shame' => 'Vergogna', + 'emotion_regret' => 'Rimpianto', + 'emotion_remorse' => 'Rimorso', + 'emotion_alienation' => 'Alienazione', + 'emotion_isolation' => 'Isolamento', + 'emotion_neglect' => 'Negligenza', + 'emotion_loneliness' => 'Solitudine', + 'emotion_rejection' => 'Rifiuto', + 'emotion_homesickness' => 'Nostalgia', + 'emotion_defeat' => 'Sconfitta', + 'emotion_dejection' => 'Sconforto', + 'emotion_insecurity' => 'Insicurezza', + 'emotion_embarrassment' => 'Imbarazzo', + 'emotion_humiliation' => 'Umiliazione', + 'emotion_insult' => 'Insulto', + 'emotion_pity' => 'Peccato', + 'emotion_sympathy' => 'Simpatia', + 'emotion_alarm' => 'Allarme', + 'emotion_shock' => 'Shock', + 'emotion_fear' => 'Paura', + 'emotion_fright' => 'Spavento', + 'emotion_horror' => 'Orrore', + 'emotion_terror' => 'Terrore', + 'emotion_panic' => 'Panico', + 'emotion_hysteria' => 'Isteria', + 'emotion_mortification' => 'Mortificazione', + 'emotion_anxiety' => 'Ansia', + 'emotion_nervousness' => 'Nervosismo', + 'emotion_tenseness' => 'Tensione', + 'emotion_uneasiness' => 'Disagio', + 'emotion_apprehension' => 'Apprensione', + 'emotion_worry' => 'Preoccupazione', + 'emotion_distress' => 'Angoscia', + 'emotion_dread' => 'Terrore', + + // weather + 'weather_sunny' => 'Soleggiato', + 'weather_clear' => 'Chiaro', + 'weather_clear-day' => 'Chiara', + 'weather_clear-night' => 'Notte limpida', + 'weather_light-drizzle' => 'Pioggia leggera', + 'weather_patchy-light-drizzle' => 'Pioggerellina irregolare', + 'weather_patchy-light-rain' => 'Pioggia leggera e irregolare', + 'weather_light-rain' => 'Pioggia leggera', + 'weather_moderate-rain-at-times' => 'Pioggia moderata e intermittente', + 'weather_moderate-rain' => 'Pioggia moderata', + 'weather_patchy-rain-possible' => 'Possibile pioggia a tratti', + 'weather_heavy-rain-at-times' => 'Forte pioggia intermittente', + 'weather_heavy-rain' => 'Forte pioggia', + 'weather_light-freezing-rain' => 'Light freezing rain', + 'weather_moderate-or-heavy-freezing-rain' => 'Moderate or heavy freezing rain', + 'weather_light-sleet' => 'Light sleet', + 'weather_moderate-or-heavy-rain-shower' => 'Moderate or heavy rain shower', + 'weather_light-rain-shower' => 'Light rain shower', + 'weather_torrential-rain-shower' => 'Torrential rain shower', + 'weather_rain' => 'Pioggia', + 'weather_snow' => 'Neve', + 'weather_blowing-snow' => 'Blowing snow', + 'weather_patchy-light-snow' => 'Patchy light snow', + 'weather_light-snow' => 'Light snow', + 'weather_patchy-moderate-snow' => 'Patchy moderate snow', + 'weather_moderate-snow' => 'Moderate snow', + 'weather_patchy-heavy-snow' => 'Patchy heavy snow', + 'weather_heavy-snow' => 'Forte nevicata', + 'weather_light-snow-showers' => 'Nevicate leggere', + 'weather_moderate-or-heavy-snow-showers' => 'Moderate or heavy snow showers', + 'weather_patchy-snow-possible' => 'Patchy snow possible', + 'weather_patchy-sleet-possible' => 'Patchy sleet possible', + 'weather_moderate-or-heavy-sleet' => 'Moderate or heavy sleet', + 'weather_light-sleet-showers' => 'Light sleet showers', + 'weather_moderate-or-heavy-sleet-showers' => 'Moderate or heavy sleet showers', + 'weather_sleet' => 'Nevischio', + 'weather_wind' => 'Vento', + 'weather_fog' => 'Nebbia', + 'weather_freezing-fog' => 'Freezing fog', + 'weather_mist' => 'Mist', + 'weather_blizzard' => 'Blizzard', + 'weather_overcast' => 'Overcast', + 'weather_cloudy' => 'Nuvoloso', + 'weather_partly-cloudy-day' => 'Parzialmente nuvoloso', + 'weather_partly-cloudy-night' => 'Parzialmente nuvolosa', + 'weather_freezing-drizzle' => 'Freezing drizzle', + 'weather_heavy-freezing-drizzle' => 'Heavy freezing drizzle', + 'weather_patchy-freezing-drizzle-possible' => 'Patchy freezing drizzle possible', + 'weather_ice-pellets' => 'Ice pellets', + 'weather_light-showers-of-ice-pellets' => 'Light showers of ice pellets', + 'weather_moderate-or-heavy-showers-of-ice-pellets' => 'Moderate or heavy showers of ice pellets', + 'weather_thundery-outbreaks-possible' => 'Thundery outbreaks possible', + 'weather_patchy-light-rain-with-thunder' => 'Patchy light rain with thunder', + 'weather_moderate-or-heavy-rain-with-thunder' => 'Moderate or heavy rain with thunder', + 'weather_patchy-light-snow-with-thunder' => 'Patchy light snow with thunder', + 'weather_moderate-or-heavy-snow-with-thunder' => 'Moderate or heavy snow with thunder', + 'weather_current_temperature_celsius' => ':temperature °C', + 'weather_current_temperature_fahrenheit' => ':temperature °F', + 'weather_current_title' => 'Meteo attuale', + + // dav + 'dav_contacts' => 'Contatti', + 'dav_contacts_description' => 'contatti di :name', + 'dav_birthdays' => 'Compleanni', + 'dav_birthdays_description' => 'compleanno del contatto di :name', + 'dav_tasks' => 'Compiti', + 'dav_tasks_description' => 'attività di :name', + + // contact list + 'contact_list_avatar' => 'Avatar', + 'contact_list_name' => 'Contatto', + 'contact_list_description' => 'Descrizione', + +]; diff --git a/resources/lang/it/auth.php b/resources/lang/it/auth.php new file mode 100644 index 0000000..f50506b --- /dev/null +++ b/resources/lang/it/auth.php @@ -0,0 +1,89 @@ + 'Queste credenziali non combaciano con i nostri archivi.', + 'throttle' => 'Troppi tentativi di accesso. Ti preghiamo di ritentare in :seconds secondi.', + 'not_authorized' => 'Non sei autorizzato a eseguire questa azione.', + 'signup_disabled' => 'La registrazione è al momento disattivata', + 'signup_error' => 'Si è verificato un errore provando a registrare l\'utente', + 'back_homepage' => 'Ritorna alla Home', + 'mfa_auth_otp' => 'Autenticati con il tuo dispositivo secondo fattore', + 'mfa_auth_webauthn' => 'Autenticazione con una chiave di sicurezza (WebAuthn)', + '2fa_title' => 'Autenticazione due fattori', + '2fa_wrong_validation' => 'Autenticazione due fattori fallita.', + '2fa_one_time_password' => 'Codice di autenticazione a due fattori', + '2fa_recuperation_code' => 'Inserisci il codice di recupero dell\'Autenticazione a due Fattori', + '2fa_one_time_or_recuperation' => 'Inserisci un codice d\'autenticazione a due fattori o un codice di recupero', + '2fa_otp_help' => 'Apri la tua app di autenticazione a due fattori e copia il codice', + + 'login_to_account' => 'Accedi al tuo conto', + 'login_with_recovery' => 'Login con un codice di recupero', + 'login_again' => 'Per favore, effettua di nuovo il login', + 'email' => 'Email', + 'password' => 'Password', + 'recovery' => 'Codice di recupero', + 'login' => 'Accedi', + 'button_remember' => 'Ricordami', + 'password_forget' => 'Hai dimenticato la password?', + 'password_reset' => 'Reimposta la Tua password', + 'use_recovery' => 'Oppure utilizza un codice di recupero', + 'signup_no_account' => 'Non hai ancora un account?', + 'signup' => 'Iscriviti', + 'create_account' => 'Creare il primo account di firma', + 'change_language_title' => 'Cambia lingua:', + 'change_language' => 'Imposta lingua su :lang', + + 'password_reset_title' => 'Reimposta Password', + 'password_reset_email' => 'Indirizzo E-Mail', + 'password_reset_send_link' => 'Invia un collegamento di ripristino password', + 'password_reset_password' => 'Password', + 'password_reset_password_confirm' => 'Conferma password', + 'password_reset_action' => 'Reimposta Password', + 'password_reset_email_content' => 'Clicca qui per ripristinare la tua password:', + + 'register_title_welcome' => 'Benvenuto alla tua nuova istanza di Monica', + 'register_create_account' => 'È necessario creare un account per utilizzare Monica', + 'register_title_create' => 'Crea il tuo account di Monica', + 'register_login' => 'Accedi se hai già un account.', + 'register_email' => 'Inserire un indirizzo email valido', + 'register_email_example' => 'you@Home', + 'register_firstname' => 'Nome', + 'register_firstname_example' => 'es. Giovanni', + 'register_lastname' => 'Cognome', + 'register_lastname_example' => 'es. Bianchi', + 'register_password' => 'Password', + 'register_password_example' => 'Inserire una password sicura', + 'register_password_confirmation' => 'Conferma password', + 'register_action' => 'Registrati', + 'register_policy' => 'La registrazione implica che tu abbia letto e accettato la nostra Privacy Policy e i Termini di Utilizzo.', + 'register_invitation_email' => 'Per ragioni di sicurezza, inserisci l\'indirizzo email della persona che ti ha invitato. Trovi questa informazione nella mail di invito.', + + 'confirmation_title' => 'Verifica il tuo indirizzo Email', + 'confirmation_fresh' => 'Un nuovo link di verifica è stato mandato al tuo indirizzo email.', + 'confirmation_check' => 'Prima di procedere, controlla il link che ti abbiamo mandato al tuo indirizzo email.', + 'confirmation_request_another' => 'Se non hai ricevuto l\'email clicca qui per richiederne un\'altra.', + + 'confirmation_again' => 'Se vuoi cambiare il tuo indirizzo email clicca qui.', + 'email_change_current_email' => 'Indirizzo email attuale:', + 'email_change_title' => 'Modifica il tuo indirizzo email', + 'email_change_new' => 'Nuovo indirizzo email', + 'email_changed' => 'Il tuo indirizzo email è stato cambiato. Controlla la tua casella per verificarlo.', +]; diff --git a/resources/lang/it/changelog.php b/resources/lang/it/changelog.php new file mode 100644 index 0000000..edd3150 --- /dev/null +++ b/resources/lang/it/changelog.php @@ -0,0 +1,12 @@ + 'Changelog', + 'note' => 'Nota: questa pagina è disponibile solo in inglese.', +]; diff --git a/resources/lang/it/dashboard.php b/resources/lang/it/dashboard.php new file mode 100644 index 0000000..c01cde7 --- /dev/null +++ b/resources/lang/it/dashboard.php @@ -0,0 +1,42 @@ + 'Benvenuto nel tuo account!', + 'dashboard_blank_description' => 'Monica è il luogo per organizzare tutte le interazioni che hai con le persone a cui tieni.', + 'dashboard_blank_cta' => 'Aggiungi il tuo primo contatto', + 'dashboard_blank_illustration' => 'Illustrazione by Freepik', + + 'notes_title' => 'Non hai alcuna nota.', + + 'tab_recent_calls' => 'Chiamate recenti', + 'tab_favorite_notes' => 'Note preferite', + 'tab_calls_blank' => 'Non hai ancora registrato alcuna chiamata.', + 'tab_debts' => 'Debiti', + 'tab_debts_blank' => 'Non hai ancora registrato alcun debito.', + 'tab_tasks' => 'Promemoria', + 'tab_tasks_blank' => 'Non hai ancora alcuna attività.', + + 'tasks_add_task_placeholder' => 'Cosa vuoi ricordarti?', + 'tasks_tab_your_contacts' => 'Promemoria riguardanti i tuoi contatti', + 'tasks_tab_your_tasks' => 'I tuoi promemoria', + 'tasks_add_note' => 'Premi Invio per creare il promemoria.', + 'task_add_cta' => 'Aggiungi un promemoria', + + 'debts_you_owe' => 'Devi', + + 'statistics_contacts' => 'Contatti', + 'statistics_activities' => 'Attività', + 'statistics_gifts' => 'Regali', + + 'reminders_next_months' => 'Eventi nei prossimi 3 mesi', + 'reminders_none' => 'Nessun promemoria per questo mese.', + + 'product_changes' => 'Changelog', + 'product_view_details' => 'Mostra dettagli', +]; diff --git a/resources/lang/it/format.php b/resources/lang/it/format.php new file mode 100644 index 0000000..eb6c636 --- /dev/null +++ b/resources/lang/it/format.php @@ -0,0 +1,36 @@ + 'd M Y H:i', + 'short_date_year' => 'd M Y', + 'short_date' => 'd M', + 'short_month' => 'M', + 'short_month_year' => 'M Y', + 'short_day' => 'D', + 'full_date_year' => 'd F Y', + 'full_month' => 'F', + 'full_month_year' => 'F Y', + 'full_hour' => 'h.i A', + + 'short_text' => '{text}…', +]; diff --git a/resources/lang/it/journal.php b/resources/lang/it/journal.php new file mode 100644 index 0000000..22e5d16 --- /dev/null +++ b/resources/lang/it/journal.php @@ -0,0 +1,38 @@ + 'Com\'è andata la tua giornata? Puoi votare una volta al giorno.', + 'journal_come_back' => 'Grazie. Ritorna domani per valutare di nuovo la tua giornata.', + 'journal_description' => 'Nota: il diario mostra sia voci inserite manualmente che voci generate automaticamente come le Attività svolte con i tuoi contatti. Puoi eliminare le voci manuali, ma per quelle automatiche devi eliminarla direttamente dalla pagina del contatto.', + 'journal_add' => 'Scrivi nel diario', + 'journal_edit' => 'Modifica una voce del diario', + 'journal_empty' => 'Diario vuoto', + 'journal_created_at' => 'Creato il {date}', + 'journal_created_automatically' => 'Creata automaticamente', + 'journal_entry_type_journal' => 'Voce del diario', + 'journal_entry_type_activity' => 'Attività', + 'journal_entry_rate' => 'Hai votato la tua giornata.', + 'journal_add_comment' => 'Ti interessa aggiungere un commento? (facoltativo)', + 'journal_show_comment' => 'Visualizza commento', + 'entry_delete_success' => 'La pagina del diario è stata rimossa.', + 'journal_add_title' => 'Titolo (facoltativo)', + 'journal_add_date' => 'Data', + 'journal_add_post' => 'Testo', + 'journal_add_cta' => 'Salva', + 'journal_blank_cta' => 'Scrivi qualcosa nel diario', + 'journal_blank_description' => 'Il diario ti permette di appuntare cose che ti succedono, e ricordarle.', + 'delete_confirmation' => 'Sei sicuro di voler rimuovere questa pagina dal diario?', + 'apply_filter' => 'Apply filter', + 'start_date' => 'Start Date', + 'end_date' => 'End Date', + 'per_page' => 'Per Page', + 'sort_order' => 'Sort By Created At', + 'ascending' => 'Ascending', + 'descending' => 'Descending', +]; diff --git a/resources/lang/it/logs.php b/resources/lang/it/logs.php new file mode 100644 index 0000000..334d83a --- /dev/null +++ b/resources/lang/it/logs.php @@ -0,0 +1,29 @@ + 'Creato il contatto.', + 'settings_log_contact_created_with_name' => 'Aggiunto :name come contatto.', + + // contat description update + 'contact_log_contact_description_updated' => 'Aggiornata la descrizione.', + 'settings_log_contact_description_updated_with_name' => 'Aggiornata la descrizione di :name.', + + // contact description clear + 'contact_log_contact_description_cleared' => 'Rimosso la descrizione.', + 'settings_log_contact_description_cleared_with_name' => 'Rimosso la descrizione di :name.', + + // contact work information update + 'contact_log_contact_work_updated' => 'Informazioni di lavoro aggiornate.', + 'settings_log_contact_work_updated_with_name' => 'Aggiornato informazioni di lavoro di :name.', + + // company created + 'settings_log_company_created' => 'Creata un\'azienda chiamata :name.', +]; diff --git a/resources/lang/it/mail.php b/resources/lang/it/mail.php new file mode 100644 index 0000000..59ec09e --- /dev/null +++ b/resources/lang/it/mail.php @@ -0,0 +1,53 @@ + 'Promemoria per :contact', + 'greetings' => 'Ciao :username', + 'want_reminded_of' => 'Volevi che ti ricordassi :reason', + 'for' => 'Per: :name', + 'comment' => 'Commento: :comment', + 'footer_contact_info' => 'Aggiungi, consulta, completa e cambia le informazioni di questo contatto:', + 'footer_contact_info2' => 'Apri il profilo di :name', + 'footer_contact_info2_link' => 'Apri il profilo di :name: :url', + + 'notification_subject_line' => 'Hai un evento in programma', + 'notification_description' => 'Tra :count giorni (il :date), avverrà questo evento:', + + 'stay_in_touch_subject_line' => 'Rimani in contatto con :name', + 'stay_in_touch_subject_description' => 'Volevi che ti ricordassi di rimanere in contatto con :name ogni giorno.|Volevi che ti ricordassi di rimanere in contatto con :name ogni :frequency giorni.', + + 'notifications_whoops' => 'Ops!', + 'notifications_hello' => 'Ciao!', + 'notifications_regards' => 'Saluti', + 'notifications_footer' => 'Se hai problemi nel cliccare sul bottone ":actionText", copia e incolla il seguente indirizzo direttamente nel tuo browser web: [:actionURL](:actionURL)', + 'notifications_rights' => 'Tutti i diritti riservati', + + 'confirmation_email_title' => 'Monica – Verifica Email', + 'confirmation_email_intro'=> 'Per verificare il tuo indirizzo email clicca il bottone qui sotto', + 'confirmation_email_button' => 'Verifica indirizzo email', + 'confirmation_email_bottom' => 'Se non hai creato un account, non è richiesta alcuna azione ulteriore.', + + 'password_reset_title' => 'Monica – Reimposta la tua password', + 'password_reset_intro' => 'Hai ricevuto questa email perché dal tuo account é partita una richiesta di reimpostazione della password.', + 'password_reset_button' => 'Reimposta Password', + 'password_reset_expiration' => 'Il link per reimpostare la password scadrà tra :count minuti.', + 'password_reset_bottom' => 'Se non hai richiesto di reimpostare la password, non sono necessarie ulteriori azioni.', + + 'invitation_title' => 'Monica – Sei stato invitato da :name', + 'invitation_intro' => 'Sei stato invitato da :name (:email) a usare Monica, un buon strumento per la gestione delle relazioni personali.', + 'invitation_link' => 'Per accettare l\'invito, clicca sul link sottostante:', + 'invitation_button' => 'Accetta l\'invito', + 'invitation_expiration' => 'Questo link scadrà tra :count giorni.', + + 'export_title' => 'L\'esportazione è pronta', + 'export_description' => 'Hai richiesto un\'esportazione di dati il :date. Ora è pronto per il download.', + 'export_download' => 'Scarica l\'esportazione', + +]; diff --git a/resources/lang/it/pagination.php b/resources/lang/it/pagination.php new file mode 100644 index 0000000..fefbe26 --- /dev/null +++ b/resources/lang/it/pagination.php @@ -0,0 +1,25 @@ + '❮ Precedente', + 'next' => 'Seguente ❯', + +]; diff --git a/resources/lang/it/passwords.php b/resources/lang/it/passwords.php new file mode 100644 index 0000000..b59c6ad --- /dev/null +++ b/resources/lang/it/passwords.php @@ -0,0 +1,30 @@ + 'La password è stata reimpostata!', + 'sent' => 'Se l\'email inserita esiste nei nostri archivi vi é stato inviato il link per reimpostare la tua password.', + 'token' => 'Questo token per reimpostare la password non è valido.', + 'user' => 'Se l\'email inserita esiste nei nostri archivi vi é stato inviato il link per reimpostare la tua password.', + 'changed' => 'Password modificata con successo.', + 'invalid' => 'La password inserita non è corretta.', + 'throttled' => 'Per favore attendere prima di riprovare.', + +]; diff --git a/resources/lang/it/people.php b/resources/lang/it/people.php new file mode 100644 index 0000000..de463c4 --- /dev/null +++ b/resources/lang/it/people.php @@ -0,0 +1,539 @@ + 'Contatto non trovato', + 'people_list_number_kids' => ':count bambino|:count bambini', + 'people_list_last_updated' => 'Consultato l\'ultima volta:', + 'people_list_number_reminders' => ':count promemoria|:count promemoria', + 'people_list_blank_title' => 'Non ci sono contatti nel tuo account', + 'people_list_blank_cta' => 'Aggiungi qualcuno', + 'people_list_sort' => 'Ordina', + 'people_list_stats' => ':count contatto|:count contatti', + 'people_list_firstnameAZ' => 'Ordina per nome A → Z', + 'people_list_firstnameZA' => 'Ordina per nome Z → A', + 'people_list_lastnameAZ' => 'Ordina per cognome A → Z', + 'people_list_lastnameZA' => 'Ordina per cognome Z → A', + 'people_list_lastactivitydateNewtoOld' => 'Ordina per data di ultima attività, da più recente a meno recente', + 'people_list_lastactivitydateOldtoNew' => 'Ordina per data di ultima attività, dalla meno recente alla più recente', + 'people_list_filter_tag' => 'Tutti i contatti etichettati con', + 'people_list_clear_filter' => 'Rimuovi filtro', + 'people_list_contacts_per_tags' => ':count contatto|:count contatti', + 'people_list_show_dead' => 'Mostra persone decedute (:count)', + 'people_list_hide_dead' => 'Nascondi persone decedute (:count)', + 'people_search' => 'Cerca i tuoi contatti…', + 'people_search_no_results' => 'Nessun risultato trovato', + 'people_search_next' => 'Successivo', + 'people_search_prev' => 'Precedente', + 'people_search_rows_per_page' => 'Righe per pagina', + 'people_search_of' => 'di', + 'people_search_page' => 'Pagina', + 'people_search_all' => 'Tutto', + 'people_add_new' => 'Aggiungi una persona', + 'people_list_account_usage' => 'Utilizzo account: :current/:limit contatti', + 'people_list_account_upgrade_title' => 'Effettua l\'upgrade del tuo account per poter usufruire delle sue piene funzionalitá.', + 'people_list_account_upgrade_cta' => 'Effettua l\'upgrade ora', + 'people_list_untagged' => 'Mostra contatti senza etichette', + 'people_list_filter_untag' => 'Tutti i contatti senza etichette', + 'archived_contact_readonly' => 'Il contatto archiviato non è modificabile, sei pregato di de archiviarlo prima.', + + // people add + 'people_add_title' => 'Aggiungi una nuova persona', + 'people_add_missing' => 'Nessuna persona trovata, aggiungine ora una nuova', + 'people_add_firstname' => 'Nome', + 'people_add_middlename' => 'Secondo nome (facoltativo)', + 'people_add_lastname' => 'Cognome (facoltativo)', + 'people_add_email' => 'Email (facoltativa)', + 'people_add_nickname' => 'Nickname (facoltativo)', + 'people_add_cta' => 'Aggiungi questa persona', + 'people_save_and_add_another_cta' => 'Salva e aggiungi un\'altra persona', + 'people_add_success' => 'Contatto creato con successo', + 'people_add_gender' => 'Sesso', + 'people_delete_success' => 'Il contatto è stato rimosso', + 'people_delete_message' => 'Elimina contatto', + 'people_delete_confirmation' => 'Sei sicuro di voler eliminare il contatto di :name? L\'eliminazione è immediata e permanente.', + 'people_add_birthday_reminder' => 'Fai gli auguri di compleanno a :name', + 'people_add_birthday_reminder_deceased' => 'In questa data, :name avrebbe celebrato il suo compleanno', + 'people_add_import' => 'Vuoi importare i tuoi contatti?', + 'people_edit_email_error' => 'Esiste già un contatto nel tuo account con questo indirizzo email. Scegline un altro, per favore.', + 'people_export' => 'Esporta in formato vCard', + 'people_add_reminder_for_birthday' => 'Crea un promemoria annuale del compleanno', + + // show + 'section_contact_information' => 'Informazioni sul contatto', + 'section_personal_activities' => 'Attività', + 'section_personal_reminders' => 'Promemoria', + 'section_personal_tasks' => 'Cose da fare', + 'section_personal_gifts' => 'Regali', + 'section_personal_notes' => 'Note', + + // archived contacts + 'list_link_to_active_contacts' => 'Stai visualizzando i contatti archiviati. Mostra i contatti attivi invece.', + 'list_link_to_archived_contacts' => 'Lista di contatti archiviati', + + // Header + 'me' => 'Questo sei tu', + 'edit_contact_information' => 'Modifica informazioni del contatto', + 'contact_archive' => 'Archivia contatto', + 'contact_unarchive' => 'Ripristina contatto', + 'contact_archive_help' => 'I contatti archiviati non sono mostrati sull\'elenco di contatti, ma appaiono comunque nei risultati della ricerca.', + 'call_button' => 'Aggiungi chiamata', + 'set_favorite' => 'I contatti preferiti vengono mostrati per primi nella lista', + + // Stay in touch + 'stay_in_touch' => 'Rimani in contatto', + 'stay_in_touch_frequency' => 'Rimani in contatto ogni giorno|Rimani in contatto ogni {count} giorni', + 'stay_in_touch_next_date' => 'Prossima scadenza: {date}', + 'stay_in_touch_invalid' => 'La frequenza dev\'essere un numero maggiore di 0.', + 'stay_in_touch_premium' => 'Devi fare l\'upgrade al tuo account per usare questa funzione', + 'stay_in_touch_modal_title' => 'Rimani in contatto', + 'stay_in_touch_modal_desc' => 'Possiamo ricordarti di rimanere in contatto con {firstname} tramite email a intervalli regolari.', + 'stay_in_touch_modal_label' => 'Inviami un\'email ogni… {count} giorno| Inviami un\'email ogni… {count} giorni', + + // Calls + 'modal_call_title' => 'Aggiungi chiamata', + 'modal_call_comment' => 'Di cosa avete parlato? (facoltativo)', + 'modal_call_exact_date' => 'La chiamata é stata fatta il', + 'modal_call_who_called' => 'Chi ha chiamato?', + 'modal_call_emotion' => 'Vuoi registrare come ti sei sentito durante questa chiamata? (facoltativo)', + 'calls_add_success' => 'La chiamata é stata salvata.', + 'call_delete_confirmation' => 'Rimuovere questa chiamata?', + 'call_delete_success' => 'La chiamata é stata rimossa', + 'call_title' => 'Chiamate', + 'call_empty_comment' => 'Nessuna informazione', + 'call_blank_title' => 'Tieni traccia delle chiamate effettuate con {name}', + 'call_blank_desc' => 'Hai chiamato {name}', + 'call_you_called' => 'Hai chiamato', + 'call_he_called' => '{name} ti ha chiamato', + 'call_emotions' => 'Emozioni:', + + // Conversation + 'conversation_blank' => 'Registra le conversazioni che hai con :name sui social, SMS…', + 'conversation_delete_link' => 'Elimina la conversazione', + 'conversation_edit_title' => 'Modifica conversazione', + 'conversation_edit_delete' => 'Sei sicuro di voler eliminare questa conversazione? Non si può annullare.', + 'conversation_add_success' => 'Conversazione aggiunta con successo.', + 'conversation_edit_success' => 'Conversazione aggiornata con successo.', + 'conversation_delete_success' => 'Conversazione eliminata con successo.', + 'conversation_add_title' => 'Registra una nuova conversazione', + 'conversation_add_when' => 'Quando hai avuto questa conversazione?', + 'conversation_add_who_wrote' => 'Chi ha inviato questo messaggio?', + 'conversation_add_how' => 'Come avete comunicato?', + 'conversation_add_you' => 'Tu', + 'conversation_add_content' => 'Scrivi cos\'è stato detto', + 'conversation_add_what_was_said' => 'Che cosa hai detto?', + 'conversation_add_another' => 'Aggiungi un altro messaggio', + 'conversation_add_error' => 'Devi aggiungere almeno un messaggio.', + 'conversation_list_table_messages' => 'Messaggi', + 'conversation_list_table_content' => 'Contenuto parziale (ultimo messaggio)', + 'conversation_list_title' => 'Conversazioni', + 'conversation_list_cta' => 'Registra conversazione', + + // age - birthday + 'birthdate_not_set' => 'Il compleanno non è impostato', + 'age_approximate_in_years' => 'circa :age anni', + 'age_exact_in_years' => ':age anni', + 'age_exact_birthdate' => 'nato :date', + + // Last called + 'last_called' => 'Ultima chiamata: :date', + 'last_talked_to' => 'Ultima chiamata: {date}', + 'last_called_empty' => 'Ultima chiamata: sconosciuta', + 'last_activity_date' => 'Ultima attività insieme: :date', + 'last_activity_date_empty' => 'Ultima attività insieme: sconosciuta', + + // additional information + 'information_edit_success' => 'Il profilo è stato aggiornato', + 'information_edit_title' => 'Modifica le informazioni personali di :name', + 'information_edit_max_size' => 'Massimo :size Kb.', + 'information_edit_max_size2' => 'Massimo {size} Kb.', + 'information_edit_firstname' => 'Nome', + 'information_edit_lastname' => 'Cognome (facoltativo)', + 'information_edit_description' => 'Descrizione (facoltativa)', + 'information_edit_description_help' => 'Usato nella lista dei contatti per aggiungere contesto, se necessario.', + 'information_edit_unknown' => 'Non conosco l\'età di questa persona', + 'information_edit_probably' => 'Questa persona è probabilmente…', + 'information_edit_not_year' => 'Conosco il giorno e il mese del compleanno di questa persona, ma non l\'anno…', + 'information_edit_exact' => 'Conosco il compleanno esatto di questa persona…', + 'information_edit_birthdate_label' => 'Compleanno', + 'information_no_work_defined' => 'Nessuna informazione professionale', + 'information_work_at' => 'alla :company', + 'work_add_cta' => 'Aggiorna informazioni professionali', + 'work_edit_success' => 'Informazioni lavorative aggiornate', + 'work_edit_title' => 'Aggiorna informazioni professionali di :name', + 'work_edit_job' => 'Titolo (facoltativo)', + 'work_edit_company' => 'Azienda (facoltativa)', + 'work_information' => 'Informazioni professionali', + + // food preferences + 'food_preferences_add_success' => 'Le preferenze alimentari sono state salvate', + 'food_preferences_edit_description' => 'Magari :firstname o qualcuno nella famiglia :family ha un\'allergia. O non gli piace un certo vino. Indica queste cose qui così da ricordarle la prossima volta che li inviti a cena', + 'food_preferences_edit_description_no_last_name' => 'Magari :firstname ha un\'allergia. O non gli piace un certo vino. Indica queste cose qui così da ricordarle la prossima volta che li inviti a cena', + 'food_preferences_edit_title' => 'Indica le preferenze alimentari', + 'food_preferences_edit_cta' => 'Salva preferenze alimentari', + 'food_preferences_title' => 'Preferenze alimentari', + 'food_preferences_cta' => 'Aggiunti preferenze alimentari', + + // reminders + 'reminders_blank_title' => 'C\'è qualcosa di cui ti vuoi ricordare riguardo a :name?', + 'reminders_blank_add_activity' => 'Aggiungi un promemoria', + 'reminders_add_title' => 'Cosa vorresti ricordare a proposito di :name?', + 'reminders_add_description' => 'Per favore ricordami di…', + 'reminders_add_next_time' => 'Quando vorresti ti fosse ricordato?', + 'reminders_add_once' => 'Ricordamelo una sola volta', + 'reminders_add_recurrent' => 'Ricordamelo ogni', + 'reminders_add_starting_from' => 'a partire dalla data specificata qui sopra', + 'reminders_add_cta' => 'Aggiungi promemoria', + 'reminders_edit_update_cta' => 'Aggiorna promemoria', + 'reminders_add_error_custom_text' => 'Devi scrivere qualcosa per questo promemoria', + 'reminders_create_success' => 'Il promemoria è stato creato', + 'reminders_delete_success' => 'Il promemoria è stato rimosso', + 'reminders_update_success' => 'Il promemoria è stato aggiornato', + 'reminders_add_optional_comment' => 'Informazioni aggiuntive', + + 'reminder_frequency_day' => 'ogni giorno|ogni :number giorni', + 'reminder_frequency_week' => 'ogni settimana|ogni :number settimane', + 'reminder_frequency_month' => 'ogni mese|ogni :number mesi', + 'reminder_frequency_year' => 'ogni anno|ogni :number anni', + 'reminder_frequency_one_time' => 'il :date', + 'reminders_delete_confirmation' => 'Rimuovere questo promemoria?', + 'reminders_delete_cta' => 'Rimuovi', + 'reminders_next_expected_date' => 'il', + 'reminders_cta' => 'Aggiungi un promemoria', + 'reminders_description' => 'Invieremo un\'email per ognuno dei seguenti promemoria. I promemoria sono inviati ogni mattina in cui si verificherà l\'evento. I promemoria aggiunti automaticamente per i compleanno non sono eliminabili. Se vuoi modificare queste date, modifica il compleanno dei contatti.', + 'reminders_one_time' => 'Una volta', + 'reminders_type_week' => 'settimana', + 'reminders_type_month' => 'mese', + 'reminders_type_year' => 'anno', + 'reminders_birthday' => 'Compleanno di :name', + 'reminders_free_plan_warning' => 'Nella versione gratuita di Monica non vengono inviate email. Per ricevere promemoria via email, effettua l\'upgrade.', + + // relationships + 'relationship_form_add' => 'Aggiungi relazione', + 'relationship_form_edit' => 'Modifica una relazione esistente', + 'relationship_form_is_with' => 'Questa persona è…', + 'relationship_form_is_with_name' => ':name è…', + 'relationship_form_add_choice' => 'Con chi è la relazione?', + 'relationship_form_create_contact' => 'Aggiungi persona', + 'relationship_form_associate_contact' => 'Un contatto esistente', + 'relationship_form_associate_dropdown' => 'Cerca e seleziona un contatto dalla lista', + 'relationship_form_associate_dropdown_placeholder' => 'Cerca e seleziona un contatto esistente', + 'relationship_form_also_create_contact' => 'Aggiungi questa persona anche come Contatto.', + 'relationship_form_add_description' => 'Ti permetterà di trattare questa persona come ogni altro contatto.', + 'relationship_form_add_no_existing_contact' => 'Al momento non hai contatti che possono essere una relazione :name.', + 'relationship_delete_confirmation' => 'Sei sicuro di voler eliminare questa relazione? L\'eliminazione è permanente.', + 'relationship_unlink_confirmation' => 'Rimuovere questa relazione? Il contatto non sará cancellato – solo la relazione.', + 'relationship_form_add_success' => 'Relazione impostata correttamente.', + 'relationship_form_deletion_success' => 'La relazione è stata eliminata.', + + // tasks + 'tasks_title' => 'Cose da fare', + 'tasks_blank_title' => 'Nulla da fare.', + 'tasks_form_title' => 'Titolo', + 'tasks_form_description' => 'Descrizione (facoltativa)', + 'tasks_add_task' => 'Aggiungi compito', + 'tasks_delete_success' => 'Compito rimosso', + 'tasks_complete_success' => 'Compito completato', + + // activities + 'activity_title' => 'Attività', + 'activity_type_category_simple_activities' => 'Attività semplici', + 'activity_type_category_sport' => 'Sport', + 'activity_type_category_food' => 'Cibo', + 'activity_type_category_cultural_activities' => 'Attività culturali', + 'activity_type_just_hung_out' => 'siamo usciti', + 'activity_type_watched_movie_at_home' => 'visto un film, a casa', + 'activity_type_talked_at_home' => 'parlato, a casa', + 'activity_type_did_sport_activities_together' => 'hanno giocato insieme a uno sport', + 'activity_type_ate_at_his_place' => 'mangiato a casa sua/loro', + 'activity_type_went_bar' => 'andati al bar', + 'activity_type_ate_at_home' => 'mangiato a casa', + 'activity_type_picnicked' => 'abbiamo fatto un picnic', + 'activity_type_ate_restaurant' => 'mangiato al ristorante', + 'activity_type_went_theater' => 'andati a teatro', + 'activity_type_went_concert' => 'andati a un concerto', + 'activity_type_went_play' => 'andati a una rappresentazione teatrale', + 'activity_type_went_museum' => 'andati al museo', + 'activities_add_activity' => 'Aggiungi attività', + 'activities_add_more_details' => 'Aggiungi ulteriori dettagli', + 'activities_add_emotions' => 'Aggiungi Umori/emozioni', + 'activities_add_category' => 'Indica una categoria', + 'activities_add_participants_cta' => 'Aggiungi partecipanti', + 'activities_item_information' => ':Activity il :date', + 'activities_add_title' => 'Cosa hai fatto con {name}?', + 'activities_summary' => 'Descrivi cosa avete fatto', + 'activities_add_pick_activity' => 'Vorresti categorizzare quest\'attività? Non devi, ma ti darà le statistiche in seguito (facoltativo)', + 'activities_add_date_occured' => 'L\'attività è avvenuta il…', + 'activities_add_participants' => 'Chi, a parte {name}, ha partecipato a questa attività? (opzionale)', + 'activities_add_emotions_title' => 'Vuoi registrare come ti sei sentito durante questa attivitá? (facoltativo)', + 'activities_blank_title' => 'Tieni traccia di quello che tu e {name} avete fatto, e ciò di cui avete parlato', + 'activities_blank_add_activity' => 'Agginugi attività', + 'activities_add_success' => 'Attività aggiunta', + 'activities_add_error' => 'Errore durante l\'aggiunta dell\'attività', + 'activities_update_success' => 'Attività aggiornata', + 'activities_delete_success' => 'Attività rimossa', + 'activities_who_was_involved' => 'Chi era coinvolto?', + 'activities_activity' => 'Categoria dell\'attività', + 'activities_view_activities_report' => 'Visualizza resoconti attività', + 'activities_profile_title' => 'Attività tra tu e :name', + 'activities_profile_subtitle' => 'Hai registrato :total_activities attività con :name e :activities_last_twelve_months negli ultimi 12 mesi.|Hai registrato :total_activities attività con :name e :activities_last_twelve_months negli ultimi 12 mesi.', + 'activities_profile_year_summary_activity_types' => 'Ecco un resoconto dei tipi di attività svolte nel :year', + 'activities_profile_year_summary' => 'Ecco cosa avete fatto insieme nel :year', + 'activities_profile_number_occurences' => ':value attività|:value attività', + 'activities_list_participants' => 'Partecipanti ({total}):', + 'activities_list_emotions' => 'Emozioni provate:', + 'activities_list_date' => 'Accaduto il', + 'activities_list_category' => 'Categoria:', + + // notes + 'notes_create_success' => 'Nota creata', + 'notes_update_success' => 'Nota aggiornata', + 'notes_delete_success' => 'Nota rimossa', + 'notes_add_cta' => 'Aggiungi nota', + 'notes_favorite' => 'Aggiungi/rimuovi dalle note preferite', + 'notes_delete_title' => 'Rimuovi nota', + 'notes_delete_confirmation' => 'Rimuovere nota? Questo cambio è permanente.', + + // gifts + 'gifts_title' => 'Regali', + 'gifts_add_success' => 'Regalo aggiunto', + 'gifts_delete_success' => 'Regalo rimosso', + 'gifts_delete_confirmation' => 'Rimuovere regalo?', + 'gifts_add_gift' => 'Aggiungi regalo', + 'gifts_link' => 'Collegamento', + 'gifts_for' => 'Per: {name}', + 'gifts_delete_cta' => 'Rimuovi', + 'gifts_add_title' => 'Gestione dei regali a :name', + 'gifts_add_gift_idea' => 'Idea regalo', + 'gifts_add_gift_already_offered' => 'Regalo dato', + 'gifts_add_gift_received' => 'Regalo ricevuto', + 'gifts_add_gift_title' => 'Cos\'è questo regalo?', + 'gifts_add_gift_name' => 'Nome del regalo', + 'gifts_add_link' => 'Link alla pagina web (facoltativo)', + 'gifts_add_value' => 'Valore (facoltativo)', + 'gifts_add_comment' => 'Commenti (facoltativo)', + 'gifts_add_recipient' => 'Destinatario (opzionale)', + 'gifts_add_recipient_field' => 'Destinatario', + 'gifts_add_photo' => 'Foto (opzionale)', + 'gifts_add_photo_title' => 'Aggiungi una foto per questo regalo', + 'gifts_add_someone' => 'Questo regalo é per qualcuno in particolare nella famiglia di {name}', + 'gifts_delete_title' => 'Rimuovi un regalo', + 'gifts_ideas' => 'Idee regalo', + 'gifts_offered' => 'Regali dati', + 'gifts_offered_as_an_idea' => 'Segna come idea', + 'gifts_received' => 'Regali ricevuti', + 'gifts_view_comment' => 'Visualizza commento', + 'gifts_mark_offered' => 'Segna come dato', + 'gifts_update_success' => 'Regalo modificato', + 'gifts_add_date' => 'Data (opzionale)', + + // debts + 'debt_delete_confirmation' => 'Rimuovere questo debito?', + 'debt_delete_success' => 'Debito rimosso', + 'debt_add_success' => 'Debito aggiunto', + 'debt_title' => 'Debiti', + 'debt_add_cta' => 'Aggiungi debito', + 'debt_you_owe' => 'Devi :amount', + 'debt_they_owe' => ':name ti deve :amount', + 'debt_add_title' => 'Gestione dei debiti', + 'debt_add_you_owe' => 'devi a :name', + 'debt_add_they_owe' => ':name ti deve', + 'debt_add_amount' => 'l\'ammontare di', + 'debt_add_reason' => 'per questo motivo (facoltativo)', + 'debt_add_add_cta' => 'Aggiungi debito', + 'debt_edit_update_cta' => 'Aggiorna debito', + 'debt_edit_success' => 'Debito aggiornato', + 'debts_blank_title' => 'Gestisci ciò che devi a :name e quello che :name ti deve', + + // tags + 'tag_edit' => 'Modifica etichetta', + 'tag_add' => 'Aggiungi etichette', + 'tag_add_search' => 'Aggiungi o cerca etichette', + 'tag_no_tags' => 'Nessuna etichetta', + + // Introductions + 'introductions_sidebar_title' => 'Come vi siete conosciuti', + 'introductions_blank_cta' => 'Indica come hai conosciuto :name', + 'introductions_title_edit' => 'Come hai conosciuto :name?', + 'introductions_additional_info' => 'Spiega come e dove vi siete conosciuti', + 'introductions_edit_met_through' => 'Qualcuno ti ha presentato a questa persona?', + 'introductions_no_met_through' => 'Nessuno', + 'introductions_first_met_date' => 'Data in cui vi siete conosciuti', + 'introductions_no_first_met_date' => 'Non ricordo la data in cui ci siamo conosciuti', + 'introductions_first_met_date_known' => 'Questo é il giorno in cui si siamo conosciuti', + 'introductions_add_reminder' => 'Aggiungi un promemoria per celebrare questo incontro nel suo anniversario', + 'introductions_update_success' => 'Informazioni sull\'incontro con questa persona aggiornate', + 'introductions_met_through' => 'Conosciuto/a attraverso :name', + 'introductions_met_date' => 'Incontrato/a il :date', + 'introductions_reminder_title' => 'Anniversario del giorno in cui vi siete conosciuti', + + // Deceased + 'deceased_reminder_title' => 'Anniversario della morte di :name', + 'deceased_mark_person_deceased' => 'Segna come deceduto', + 'deceased_know_date' => 'Conosco la data di decesso di questa persona', + 'deceased_add_reminder' => 'Aggiungi un promemoria per questa data', + 'deceased_label' => 'Deceduto/a', + 'deceased_date_label' => 'Data morte', + 'deceased_label_with_date' => 'Deceduto/a il :date', + 'deceased_age' => 'Età di decesso', + + // Contact information + 'contact_info_title' => 'Informazioni di contatto', + 'contact_info_form_content' => 'Contenuti', + 'contact_info_form_contact_type' => 'Tipo di contatto', + 'contact_info_form_personalize' => 'Personalizza', + 'contact_info_address' => 'Vive in', + + // Addresses + 'contact_address_title' => 'Indirizzi', + 'contact_address_form_name' => 'Etichetta (facoltativa)', + 'contact_address_form_street' => 'Via (facoltativa)', + 'contact_address_form_city' => 'Cittá (facoltativa)', + 'contact_address_form_province' => 'Provincia (facoltativa)', + 'contact_address_form_postal_code' => 'Codice postale (facoltativa)', + 'contact_address_form_country' => 'Regione (facoltativa)', + 'contact_address_form_latitude' => 'Latitudine (solo numeri) (facoltativa)', + 'contact_address_form_longitude' => 'Longitudine (solo numeri) (facoltativa)', + + // Pets + 'pets_kind' => 'Tipo di animale domestico', + 'pets_name' => 'Nome (facoltativo)', + 'pets_create_success' => 'Animale domestico aggiunto con successo', + 'pets_update_success' => 'Animale domestico modificato', + 'pets_delete_success' => 'Animale domestico rimosso', + 'pets_title' => 'Animali domestici', + 'pets_reptile' => 'Rettile', + 'pets_bird' => 'Uccello', + 'pets_cat' => 'Gatto', + 'pets_dog' => 'Cane', + 'pets_fish' => 'Pesce', + 'pets_hamster' => 'Criceto', + 'pets_horse' => 'Cavallo', + 'pets_rabbit' => 'Coniglio', + 'pets_rat' => 'Topo/Ratto', + 'pets_small_animal' => 'Animale di piccole dimensioni', + 'pets_other' => 'Altro', + + // life events + 'life_event_list_tab_life_events' => 'Eventi della vita', + 'life_event_list_tab_other' => 'Note, promemoria, …', + 'life_event_list_title' => 'Eventi della vita', + 'life_event_blank' => 'Memorizza gli eventi importanti della vita di {name} per riferimento futuro.', + 'life_event_list_cta' => 'Aggiungi evento', + 'life_event_create_category' => 'Tutte le categorie', + 'life_event_create_life_event' => 'Aggiungi evento', + 'life_event_create_default_title' => 'Titolo (facoltativo)', + 'life_event_create_default_story' => 'Storia (facoltativo)', + 'life_event_create_date' => 'Non devi indicare un giorno o un mese, solo l\'anno è obbligatorio.', + 'life_event_create_default_description' => 'Aggiungi informazioni su quello che sai', + 'life_event_create_add_yearly_reminder' => 'Aggiungi un promemoria per l\'anniversario di questo evento', + 'life_event_create_success' => 'Evento aggiunto', + 'life_event_delete_title' => 'Elimina un evento', + 'life_event_delete_description' => 'Sei sicuro di eliminare questo evento? Non si può annullare.', + 'life_event_delete_success' => 'Evento eliminato con successo', + 'life_event_date_it_happened' => 'Data di avvenimento', + 'life_event_category_work_education' => 'Lavoro e Istruzione', + 'life_event_category_family_relationships' => 'Famiglia e Relazioni', + 'life_event_category_home_living' => 'Casa e Vita', + 'life_event_category_health_wellness' => 'Salute e Benessere', + 'life_event_category_travel_experiences' => 'Viaggi ed Esperienze', + 'life_event_sentence_new_job' => 'Inizio di un nuovo lavoro', + 'life_event_sentence_retirement' => 'Pensionamento', + 'life_event_sentence_new_school' => 'Inizio scuola', + 'life_event_sentence_study_abroad' => 'Studi all\'estero', + 'life_event_sentence_volunteer_work' => 'Inizio volontariato', + 'life_event_sentence_published_book_or_paper' => 'Pubblicato un articolo', + 'life_event_sentence_military_service' => 'Inizio servizio militare', + 'life_event_sentence_new_relationship' => 'Inizio di una relazione', + 'life_event_sentence_engagement' => 'Fidanzamento', + 'life_event_sentence_marriage' => 'Matrimonio', + 'life_event_sentence_anniversary' => 'Anniversario', + 'life_event_sentence_expecting_a_baby' => 'Aspetta un bambino', + 'life_event_sentence_new_child' => 'Ha avuto un bambino', + 'life_event_sentence_new_family_member' => 'Nuovo membro in famiglia', + 'life_event_sentence_new_pet' => 'Ha preso un animale domestico', + 'life_event_sentence_end_of_relationship' => 'Fine di una relazione', + 'life_event_sentence_loss_of_a_loved_one' => 'Perdita di un caro', + 'life_event_sentence_moved' => 'Trasferimento', + 'life_event_sentence_bought_a_home' => 'Comprato una casa', + 'life_event_sentence_home_improvement' => 'Miglioramento alla casa', + 'life_event_sentence_holidays' => 'Andato in vacanza', + 'life_event_sentence_new_vehicle' => 'Nuovo veicolo', + 'life_event_sentence_new_roommate' => 'Nuovo coinquilino', + 'life_event_sentence_overcame_an_illness' => 'Superamento di una malattia', + 'life_event_sentence_quit_a_habit' => 'Fine di un vizio', + 'life_event_sentence_new_eating_habits' => 'Inizio di nuove abitudini alimentari', + 'life_event_sentence_weight_loss' => 'Perso peso', + 'life_event_sentence_wear_glass_or_contact' => 'Occhiali o lenti a contatto', + 'life_event_sentence_broken_bone' => 'Rotto un osso', + 'life_event_sentence_removed_braces' => 'Levato l\'apparecchio', + 'life_event_sentence_surgery' => 'Ha subito un intervento', + 'life_event_sentence_dentist' => 'Andato dal dentista', + 'life_event_sentence_new_sport' => 'Iniziato uno sport', + 'life_event_sentence_new_hobby' => 'Iniziato un hobby', + 'life_event_sentence_new_instrument' => 'Imparato un nuovo strumento', + 'life_event_sentence_new_language' => 'Imparato una nuova lingua', + 'life_event_sentence_tattoo_or_piercing' => 'Fatto un piercing o un tatuaggio', + 'life_event_sentence_new_license' => 'Preso una patente', + 'life_event_sentence_travel' => 'Viaggiato', + 'life_event_sentence_achievement_or_award' => 'Preso un premio o un riconoscimento', + 'life_event_sentence_changed_beliefs' => 'Cambiato credo', + 'life_event_sentence_first_word' => 'Parlato per la prima volta', + 'life_event_sentence_first_kiss' => 'Primo bacio', + + // documents + 'document_list_title' => 'Documenti', + 'document_list_cta' => 'Carica documento', + 'document_list_blank_desc' => 'Qui puoi archiviare documenti relativi a questa persona.', + 'document_upload_zone_cta' => 'Carica un file', + 'document_upload_zone_progress' => 'Caricando il documento…', + 'document_upload_zone_error' => 'Si è verificato un errore. Per favore, riprova a caricare il documento.', + + // Photos + 'photo_title' => 'Foto', + 'photo_list_title' => 'Foto', + 'photo_list_cta' => 'Carica foto', + 'photo_list_blank_desc' => 'Qui puoi salvare foto relative a questa persona, caricane una adesso!', + 'photo_upload_zone_cta' => 'Carica una foto', + 'photo_current_profile_pic' => 'Attuale immagine del profilo', + 'photo_make_profile_pic' => 'Rendi questa foto immagine del profilo', + 'photo_delete' => 'Elimina foto', + 'photo_next' => 'Prossima foto ❯', + 'photo_previous' => '❮ Foto precedente', + + // Avatars + 'avatar_change_title' => 'Cambia il tuo avatar', + 'avatar_question' => 'Quale account preferisci usare?', + 'avatar_default_avatar' => 'Avatar predefinito', + 'avatar_adorable_avatar' => 'L\'avatar adorabile', + 'avatar_gravatar' => 'Il Gravatar associato all\'indirizzo email di questa persona. Gravatar è un sistema globale che permette agli utenti di associare indirizzi email con foto.', + 'avatar_current' => 'Mantieni l\'avatar attuale', + 'avatar_photo' => 'Da una foto che carichi', + 'avatar_crop_new_avatar_photo' => 'Ritaglia nuova foto dell\'avatar', + + // emotions + 'emotion_this_made_me_feel' => 'Questo mi ha fatto sentire…', + + // logs + 'auditlogs_link' => 'Cronologia', + 'auditlogs_title' => 'Tutto ciò che è accaduto a :name', + 'auditlogs_breadcrumb' => 'Cronologia', + 'auditlogs_author' => 'Da :name il :date', + + // contact field label + 'contact_field_label_home' => 'Casa', + 'contact_field_label_work' => 'Lavoro', + 'contact_field_label_cell' => 'Cellulare', + 'contact_field_label_fax' => 'Fax', + 'contact_field_label_pager' => 'Cercapersone', + 'contact_field_label_main' => 'Principale', + 'contact_field_label_other' => 'Altro', + 'contact_field_label_personal' => 'Personale', +]; diff --git a/resources/lang/it/reminder.php b/resources/lang/it/reminder.php new file mode 100644 index 0000000..68d6d5a --- /dev/null +++ b/resources/lang/it/reminder.php @@ -0,0 +1,16 @@ + 'Augura buon compleanno a', + 'type_phone_call' => 'Chiama', + 'type_lunch' => 'Pranzo con', + 'type_hangout' => 'Incontro con', + 'type_email' => 'Email', + 'type_birthday_kid' => 'Augura buon compleanno al figlio di', +]; diff --git a/resources/lang/it/settings.php b/resources/lang/it/settings.php new file mode 100644 index 0000000..ec981e2 --- /dev/null +++ b/resources/lang/it/settings.php @@ -0,0 +1,557 @@ + 'Impostazioni accounto', + 'sidebar_personalization' => 'Personalizzazione', + 'sidebar_settings_storage' => 'Archiviazione', + 'sidebar_settings_export' => 'Esporta dati', + 'sidebar_settings_users' => 'Utenti', + 'sidebar_settings_subscriptions' => 'Sottoscrizioni', + 'sidebar_settings_import' => 'Importa dati', + 'sidebar_settings_tags' => 'Gestione dei tag', + 'sidebar_settings_api' => 'API', + 'sidebar_settings_dav' => 'Risorse DAV', + 'sidebar_settings_security' => 'Sicurezza', + 'sidebar_settings_auditlogs' => 'Verifica logs', + + 'title_general' => 'Informazioni generali', + 'title_i18n' => 'Impostazioni internazionali', + 'title_layout' => 'Impaginazione', + + 'me_title' => 'Me come contatto', + 'me_help' => 'Questo è il contatto che rappresenta te a Monica', + 'me_select' => 'Seleziona un contatto', + 'me_no_contact' => 'Ancora nessun contatto selezionato.', + 'me_select_click' => 'Clicca qui per selezionare un contatto.', + 'me_remove_contact' => 'Rimuovi l\'associazione', + 'me_choose' => 'Scegli te stesso', + 'me_choose_placeholder' => 'Scegli te stesso', + + 'export_title' => 'Esporta i dati del tuo account', + 'export_be_patient' => 'Clicca il pulsante per iniziare l\'esportazione. Potrebbe volerci qualche minuto – ti chiediamo di portare pazienza e non premere il pulsante a ripetizione.', + 'export_title_sql' => 'Esporta in SQL', + 'export_sql_explanation' => 'Esportare i dati in formato SQL permette di importarli nella propria istanza di Monica. Questo è da considerare solo se si possiede un server.', + 'export_sql_cta' => 'Esporta in SQL', + 'export_sql_link_instructions' => 'Nota: leggi le istruzioni per capire come importare questo file nella tua istanza di Monica.', + 'export_title_json' => 'Esporta in Json', + 'export_submitted' => 'La tua esportazione è stata richiesta, sarà disponibile tra qualche istante…', + 'export_json_explanation' => 'Esportare i dati in formato Json per il backup.', + 'export_json_beta' => 'L\'esportazione di Json è in modalità anteprima. Dicci cosa ne pensi:', + 'export_json_cta' => 'Esporta in Json', + 'export_header_type' => 'Tipo', + 'export_header_timestamp' => 'Data di creazione', + 'export_header_status' => 'Stato', + 'export_header_actions' => 'Azioni', + 'export_last_title' => 'Ultime esportazioni', + 'export_empty_title' => 'Ancora nessuna esportazione', + 'export_type_json' => 'Esportazione Json', + 'export_type_sql' => 'Esportazione SQL', + 'export_status_todo' => 'Inviato', + 'export_status_doing' => 'Facendo', + 'export_status_done' => 'Fatto', + 'export_status_failed' => 'Fallito', + 'export_not_done' => 'Download impossibile, questa esportazione non è ancora terminata.', + + 'firstname' => 'Nome', + 'lastname' => 'Cognome', + 'name_order' => 'Ordine del nome', + 'name_order_firstname_lastname' => ' – John Doe', + 'name_order_lastname_firstname' => ' – Doe John', + 'name_order_firstname_lastname_nickname' => ' () – John Doe (Rambo)', + 'name_order_firstname_nickname_lastname' => ' () – John (Rambo) Doe', + 'name_order_lastname_firstname_nickname' => ' () – Doe John (Rambo)', + 'name_order_lastname_nickname_firstname' => ' () – Doe (Rambo) John', + 'name_order_nickname_firstname_lastname' => ' ( ) – Rambo (John Doe)', + 'name_order_nickname_lastname_firstname' => ' ( ) – Rambo (Mario Rossi)', + 'name_order_nickname' => ' – Rambo', + 'currency' => 'Valuta', + 'name' => 'Il tuo nome: :name', + 'email' => 'Email', + 'email_placeholder' => 'Insersci un\'email', + 'email_help' => 'Questa è l\'email usata per accedere e dove Monica invierà i tuoi promemoria.', + 'timezone' => 'Fuso orario', + 'temperature_scale' => 'Unità temperatura', + 'temperature_scale_fahrenheit' => 'Fahrenheit (°F)', + 'temperature_scale_celsius' => 'Celsius (°C)', + 'layout' => 'Impaginazione', + 'layout_small' => 'Massimo 1200 pixel di larghezza', + 'layout_big' => 'Larghezza intera del browser', + 'save' => 'Aggiorna impostazioni', + 'delete_title' => 'Rimuovi il tuo account', + 'delete_desc' => 'Desideri eliminare il tuo profilo? L\'eliminazione è permanente e tutti i tuoi dati saranno eliminati permanentemente. Se hai un abbonamento, sarà annullato immediatamente.', + 'delete_other_desc' => 'I tuoi dati nel database principale saranno immediatamente eliminati. Come descritto nella nostra politica della privacy, effettuiamo backup giornalieri, backup crittografati in sicurezza del database, mantenuti per 30 giorni dopo cui sono completamente eliminati. Non possiamo eliminare dati specifici dai backup che manteniamo prima di questo periodo. Tutti i tuoi dati saranno completamente eliminati entro 31 giorni dall\'eliminazione del tuo profilo.', + 'reset_desc' => 'Desideri ripristinare il tuo profilo? Questo rimuoverà tutti i tuoi contatti e tutti i dati a essi associati. Il tuo profilo non sarà eliminato.', + 'reset_title' => 'Reimposta il tuo account', + 'reset_cta' => 'Reimposta il tuo account', + 'reset_notice' => 'Sei sicuro di voler ripristinare il tuo profilo? Ciò è permanente e non annullabile.', + 'reset_success' => 'Il tuo profilo è stato correttamente ripristinato.', + 'delete_notice' => 'Sei sicuro di voler eliminare il tuo profilo? Ciò è permanente e non annullabile. Tutti i tuoi dati saranno eliminati e non saranno recuperabili.', + 'delete_cta' => 'Rimuovi account', + 'settings_success' => 'Impostazioni aggiornate', + 'locale' => 'Lingua', + 'locale_help' => 'Vuoi aiutare a tradurre Monica o ad aggiungere una nuova lingua? Segui questo link per ulteriori informazioni.', + 'locale_ar' => 'Arabo', + 'locale_cs' => 'Ceco', + 'locale_de' => 'Tedesco', + 'locale_el' => 'Greco', + 'locale_en' => 'Inglese', + 'locale_en-GB' => 'Inglese (Regno Unito)', + 'locale_es' => 'Spagnolo', + 'locale_fr' => 'Francese', + 'locale_he' => 'Ebraico', + 'locale_hr' => 'Croato', + 'locale_id' => 'Indonesiano', + 'locale_it' => 'Italiano', + 'locale_ja' => 'Giapponese', + 'locale_nl' => 'Olandese', + 'locale_pt' => 'Portoghese', + 'locale_pt-BR' => 'Portoghese Brasiliano', + 'locale_ru' => 'Russo', + 'locale_sv' => 'Svedese', + 'locale_vi' => 'Vietnamita', + 'locale_zh' => 'Cinese semplificato', + 'locale_zh-TW' => 'Cinese Tradizionale', + 'locale_tr' => 'Turco', + + 'security_title' => 'Sicurezza', + 'security_help' => 'Modifica le impostazioni di sicurezza relative al tuo account', + 'password_change' => 'Modifica la tua password', + 'password_current' => 'Password attuale', + 'password_current_placeholder' => 'Inserisci la tua password corrente', + 'password_new1' => 'Nuova password', + 'password_new1_placeholder' => 'Inserisci la tua nuova password', + 'password_new2' => 'Conferma la tua nuova password', + 'password_new2_placeholder' => 'Digita di nuovo la tua nuova password', + 'password_btn' => 'Modifica password', + '2fa_title' => 'Autenticazione a due fattori', + '2fa_otp_title' => 'App di autenticazione a due fattori', + '2fa_enable_title' => 'Abilita autenticazione a due fattori', + '2fa_enable_description' => 'Abilita l\'Autenticazione a Due Fattori per aumentare la sicurezza del tuo profilo.', + '2fa_enable_otp' => 'Apri la tua app mobile dell\'Autenticazione a Due Fattori e scansiona il seguente codice QR:', + '2fa_enable_otp_help' => 'Se la tua app mobile di Autenticazione a Due Fattori non supporta i codici QR, inserisci il seguente codice:', + '2fa_enable_otp_validate' => 'Sei pregato di validare il nuovo dispositivo appena configurato:', + '2fa_enable_success' => 'Autenticazione a due fattori attivata.', + '2fa_enable_error' => 'Errore durante l\'attivazione dell\'autenticazione a due fattori.', + '2fa_enable_error_already_set' => 'Autenticazione a due fattori già attiva', + '2fa_disable_title' => 'Disabilita autenticazione a due fattori', + '2fa_disable_description' => 'Disabilita l\'Autenticazione a Due Fattori per il tuo profilo. Attenzione, il tuo profilo sarà molto meno sicuro!', + '2fa_disable_success' => 'Autenticazione a due fattori disattivata', + '2fa_disable_error' => 'Errore durante la disattivazione dell\'autenticazione a due fattori', + + 'webauthn_title' => 'Chiave di sicurezza — Protocollo WebAuthn', + 'webauthn_enable_description' => 'Aggiungi una nuova chiave di sicurezza', + 'webauthn_key_name_help' => 'Dai un nome alla tua chiave.', + 'webauthn_key_name' => 'Nome della chiave:', + 'webauthn_success' => 'Chiave rilevata e confermata.', + 'webauthn_last_use' => 'Ultimo uso: {timestamp}', + 'webauthn_delete_confirmation' => 'Sei sicuro di voler cancellare questa chiave?', + 'webauthn_delete_success' => 'Chiave eliminata', + 'webauthn_insertKey' => 'Inserisci la tua chiave di sicurezza.', + 'webauthn_buttonAdvise' => 'Se la tua chiave ha un bottone, premilo.', + 'webauthn_noButtonAdvise' => 'Se non ce l\'ha, rimuovila e reinseriscila.', + 'webauthn_not_supported' => 'Il tuo browser non supporta WebAuthn.', + 'webauthn_not_secured' => 'WebAuthn supporta solo connessioni sicure. Si prega di caricare questa pagina con lo schema https.', + 'webauthn_error_already_used' => 'Questa chiave è già registrata. Non è necessario registrarla nuovamente.', + 'webauthn_error_not_allowed' => 'L\'operazione è scaduta o non è stata consentita.', + + 'recovery_title' => 'Codici di recupero', + 'recovery_show' => 'Ottieni codici di recupero', + 'recovery_copy_help' => 'Copia i codici nella clipboard', + 'recovery_help_intro' => 'Ecco i tuoi codici di recupero:', + 'recovery_help_information' => 'Puoi usare ciascun codice una volta soltanto.', + 'recovery_clipboard' => 'Codici copiati negli appunti.', + 'recovery_generate' => 'Genera nuovi codici…', + 'recovery_generate_help' => 'Generare nuovi codici invaliderà quelli precedentemente generati.', + 'recovery_already_used_help' => 'Questo codice è già stato usato.', + + 'users_list_title' => 'Utenti con accesso al tuo account', + 'users_list_add_user' => 'Invita un nouvo utente', + 'users_list_you' => 'Sei tu', + 'users_list_invitations_title' => 'Inviti in attesa di risposta', + 'users_list_invitations_explanation' => 'Qui sotto trovi gli inviti a Monica come collaboratori.', + 'users_list_invitations_invited_by' => 'invitato da :name', + 'users_list_invitations_sent_date' => 'il :date', + 'users_blank_title' => 'Sei l\'unica persona che ha accesso a questo account.', + 'users_blank_add_title' => 'Vuoi invitare qualcun altro ?', + 'users_blank_description' => 'Questa persona avrà il tuo stesso accesso, e potrà aggiungere, modificare o rimuovere qualsiasi contatto.', + 'users_blank_cta' => 'Invita qualcuno', + 'users_add_title' => 'Invita un nuovo utente al tuo profilo via email', + 'users_add_description' => 'Questa persona avrà il tuo stesso accesso, inclusi l\'invito o l\'eliminazione di altri utenti, tu incluso. Assicurati di avere fiducia in questa persona prima di dargli accesso.', + 'users_add_email_field' => 'Inserisci l\'email della persona che vuoi invitare', + 'users_add_confirmation' => 'Confermo che voglio invitare quest\'utente al mio profilo. Sono consapevole che questa persona avrà accesso a TUTTI i miei dati e vedrà esattamente ciò che vedo.', + 'users_add_cta' => 'Invita utente tramite email', + 'users_accept_title' => 'Accetta l\'invito e crea un account', + 'users_error_please_confirm' => 'Ti preghiamo di confermare di voler invitare questo utente prima di procedere', + 'users_error_email_already_taken' => 'Questa email è già assegnata. Ti preghiamo di sceglierne un\'altra', + 'users_error_already_invited' => 'Hai già invitato questo utente. Ti preghiamo di scegliere un\'altro indirizzo email.', + 'users_error_email_not_similar' => 'Questa non è l\'email della persona che ti ha invitato.', + 'users_invitation_deleted_confirmation_message' => 'Invito rimosso', + 'users_invitations_delete_confirmation' => 'Rimuovere invito?', + 'users_list_delete_confirmation' => 'Rimuovere questo utente dal tuo account?', + 'users_invitation_need_subscription' => 'Aggiungere altri utenti richiede una sottoscrizione.', + + 'subscriptions_account_current_plan' => 'Il tuo piano attuale', + 'subscriptions_account_current_legacy' => 'Piano corrente, non più selezionabile:', + 'subscriptions_account_current_paid_plan' => 'Stai usando il piano :name. Grazie infinite per essere abbonato.', + + 'subscriptions_account_next_billing_title' => 'Prossima fattura', + 'subscriptions_account_next_billing' => 'Il tuo abbonamento verrà automaticamente rinnovato il :date.', + 'subscriptions_account_bill_monthly' => 'Ti addebiteremo :price per un altro mese.', + 'subscriptions_account_bill_annual' => 'Ti addebiteremo :price per un altro anno.', + 'subscriptions_account_change' => 'Cambia piano', + + 'subscriptions_account_cancel_title' => 'Annulla abbonamento', + 'subscriptions_account_cancel_action' => 'Annulla abbonamento', + 'subscriptions_account_cancel' => 'Puoi annullare il tuo abbonamento in qualsiasi momento.', + 'subscriptions_account_free_plan' => 'Stai usando il piano gratuito.', + 'subscriptions_account_free_plan_upgrade' => 'Puoi promuovere il tuo piano al livello :name, che costa $:price al mese. I vantaggi sono:', + 'subscriptions_account_free_plan_benefits_users' => 'Numero di utenti illimitato', + 'subscriptions_account_free_plan_benefits_reminders' => 'Promemoria via email', + 'subscriptions_account_free_plan_benefits_import_data_vcard' => 'Importa i tuoi contatti con vCard', + 'subscriptions_account_free_plan_benefits_support' => 'Supporta il progetto a lungo termine, così che possiamo introdurre grandi nuove funzionalità.', + 'subscriptions_account_upgrade' => 'Promuovi il tuo account', + 'subscriptions_account_upgrade_title' => 'Aggiorna Monica oggi e ottieni relazioni più significative.', + 'subscriptions_account_upgrade_choice' => 'Scegli un piano e unisciti alle :customers persone abbonate a Monica.', + 'subscriptions_account_update_title' => 'Aggiorna l\'abbonamento di Monica', + 'subscriptions_account_update_description' => 'Puoi cambiare la frequenza del tuo abbonamento qui.', + 'subscriptions_account_update_information' => 'Sarai addebitato immediatamente per il nuovo importo. Il tuo abbonamento si estenderà al nuovo periodo, in base alla tua scelta.', + 'subscriptions_account_invoices' => 'Ricevute', + 'subscriptions_account_invoices_download' => 'Scarica', + 'subscriptions_account_invoices_subscription' => 'Abbonamento da :startDate a :endDate', + 'subscriptions_account_payment' => 'Quale opzione di pagamento preferisci?', + 'subscriptions_account_confirm_payment' => 'Il tuo pagamento è attualmente incompleto, per favore conferma il tuo pagamento.', + 'subscriptions_downgrade_title' => 'Retrocedi il tuo piano a quello gratuito', + 'subscriptions_downgrade_limitations' => 'Il piano gratuito è limitato. Per poter retrocedere il tuo account al piano gratuito, devi soddisfare questi requisiti:', + 'subscriptions_downgrade_rule_users' => 'Devi avere un solo utente nel tuo account', + 'subscriptions_downgrade_rule_users_constraint' => 'Al momento hai 1 utente nel tuo account.|Al momento hai :count utenti nel tuo account.', + 'subscriptions_downgrade_rule_invitations' => 'Non devi avere alcun invito in attesa', + 'subscriptions_downgrade_rule_invitations_constraint' => 'Correntemente hai 1 invito in attesa.|Correntemente hai :count inviti in attesa.', + 'subscriptions_downgrade_rule_contacts' => 'Non puoi avere più di :number contatti attivi', + 'subscriptions_downgrade_rule_contacts_constraint' => 'Al momento hai 1 contatto.|Al momento hai :count contatti.', + 'subscriptions_downgrade_rule_contacts_archive' => 'Possiamo anche archiviare tutti i tuoi contatti per te: ciò cancellerebbe questa regola e ti consentirebbe di procedere con il processo di downgrade del tuo profilo.', + 'subscriptions_downgrade_cta' => 'Retrocedi', + 'subscriptions_downgrade_success' => 'Sei tornato al piano gratuito!', + 'subscriptions_downgrade_thanks' => 'Grazie mille per aver provato il piano a pagamento. Continuiamo sempre ad aggiungere nuove funzionalità su Monica, quindi potresti voler tornare in futuro per vedere se potresti esser interessato ad abbonarti di nuovo.', + 'subscriptions_back' => 'Torna alle impostazioni', + 'subscriptions_upgrade_title' => 'Promuovi il tuo account', + 'subscriptions_upgrade_choose' => 'Hai scelto il piano :plan.', + 'subscriptions_upgrade_infos' => 'Non potremmo essere più felici. Inserisci le informazioni sul pagamento qui sotto.', + 'subscriptions_upgrade_name' => 'Nome sulla carta', + 'subscriptions_upgrade_zip' => 'CAP', + 'subscriptions_upgrade_credit' => 'Carta di credito o debito', + 'subscriptions_upgrade_submit' => 'Paga {amount}', + 'subscriptions_upgrade_charge' => 'Addebiteremo ora :price alla tua carta. Il prossimo addebito sarà il :date. Se dovessi cambiare idea, potrai annullarlo in ogni momento, senza spiegazioni.', + 'subscriptions_upgrade_charge_handled' => 'Il pagamento è gestito da Stripe. Nessuna informazione sulla tua carta arriva ai nostri server.', + 'subscriptions_upgrade_success' => 'Grazie! Adesso sei abbonato.', + 'subscriptions_upgrade_thanks' => 'Benvenuto nella community di persone che tenta di migliorare il mondo.', + + 'subscriptions_payment_confirm_title' => 'Conferma il tuo pagamento per :amount', + 'subscriptions_payment_confirm_information' => 'È necessaria una conferma ulteriore per elaborare il pagamento. Conferma il pagamento compilando i dettagli di pagamento qui sotto.', + 'subscriptions_payment_succeeded_title' => 'Pagamento riuscito', + 'subscriptions_payment_succeeded' => 'Questo pagamento è già stato confermato con successo.', + 'subscriptions_payment_cancelled_title' => 'Pagamento annullato', + 'subscriptions_payment_cancelled' => 'Questo pagamento è stato annullato.', + 'subscriptions_payment_error_name' => 'Per favore inserisci il tuo nome.', + 'subscriptions_payment_success' => 'Pagamento effettuato con successo.', + + 'subscriptions_pdf_title' => 'Sottoscrizione mensile a :name', + 'subscriptions_plan_frequency_year' => ':amount / anno', + 'subscriptions_plan_frequency_month' => ':amount / mese', + 'subscriptions_plan_choose' => 'Scegli questo piano', + 'subscriptions_plan_year_title' => 'Paga annualmente', + 'subscriptions_plan_year_bonus' => 'Nessun pensiero per un anno', + 'subscriptions_plan_month_title' => 'Paga mensilmente', + 'subscriptions_plan_month_bonus' => 'Cancella in qualsiasi momento', + 'subscriptions_plan_include1' => 'Incluso nell\'abbonamento:', + 'subscriptions_plan_include2' => 'Numero di contatti illimitato • Numero di utenti illimitato • Promemoria via email • Importazione da vCard • Personalizzazione della pagina dei contatti', + 'subscriptions_plan_include3' => 'Il 100% dei profitti va nello sviluppo di questo progetto grande e open source.', + 'subscriptions_help_title' => 'Altri dettagli che potrebbero interessarti', + 'subscriptions_help_opensource_title' => 'Cosa significa open source?', + 'subscriptions_help_opensource_desc' => 'Monica è un progetto open source. Ciò significa che è costruito da una community che vuole costruire un buono strumento per il bene maggiore. Essere open source significa che il codice è disponibile pubblicamente su GitHub e che tutti possono ispezionarlo, modificarlo o migliorarlo. Tutto il denaro che raccogliamo è dedicato a costruire funzionalità migliori, pagare per server più potenti e pagare altri costi. Grazie per il tuo aiuto. Non potremmo farlo senza di te.', + 'subscriptions_help_limits_title' => 'C\'è un limite al numero di contatti che posso avere sul piano gratuito?', + 'subscriptions_help_limits_plan' => 'Sì. Il piano gratuito ti permette di gestire :number contatti.', + 'subscriptions_help_discounts_title' => 'Avete sconti per organizzazioni no-profit e studenti?', + 'subscriptions_help_discounts_desc' => 'Sì! Monica è gratuita per studenti, no-profit e organizzazioni di beneficienza. Basta contattare il supporto clienti con una prova del tuo status, e aggiorneremo il tuo account.', + 'subscriptions_help_change_title' => 'Che succede se cambio idea?', + 'subscriptions_help_change_desc' => 'Puoi annullare quando vuoi, senza spiegazioni, e puoi farlo da solo, non serve contattare il supporto. Tuttavia, non sarai rimborsato per il periodo corrente.', + + 'stripe_error_card' => 'La tua carta è stata declinata. Il messaggio ricevuto è: :message', + 'stripe_error_api_connection' => 'Comunicazione con Stripe fallita. Riprova tra poco.', + 'stripe_error_rate_limit' => 'Troppe richieste a Stripe in questo momento. Riprova tra poco.', + 'stripe_error_invalid_request' => 'Parametri non validi. Riprova più tardi.', + 'stripe_error_authentication' => 'Autenticazione con Stripe non valida', + + 'import_title' => 'Importa contatti nel tuo account', + 'import_cta' => 'Carica contatti', + 'import_stat' => 'Hai importato :number file fino ad ora.', + 'import_result_stat' => 'vCard caricata con 1 contatto (:total_imported importati, :total_skipped saltati)|vCard caricata con :total_contacts contatti (:total_imported importati, :total_skipped saltati)', + 'import_view_report' => 'Vedi resoconto', + 'import_in_progress' => 'Importazione in corso. Ricarica la pagina in un minuto.', + 'import_upload_title' => 'Importa i contatti da un file vCard', + 'import_upload_rules_desc' => 'Ci sono alcune regole:', + 'import_upload_rule_format' => 'Supportiamo file .vcard e .vcf.', + 'import_upload_rule_vcard' => 'Supportiamo il formato vCard 3.0, il formato predefinito per Contacts.app di macOS e Google Contacts.', + 'import_upload_rule_instructions' => 'Esporta le istruzioni per Contacts.app di macOS e Google Contacts.', + 'import_upload_rule_multiple' => 'Se i tuoi contatti hanno indirizzi email o numeri telefonici multipli, solo i primi saranno salvati.', + 'import_upload_rule_limit' => 'I file sono limitati a 10 MB.', + 'import_upload_rule_time' => 'Potrebbe volerci fino a un minuto per caricare ed elaborare i contatti. Sei pregato di esser paziente.', + 'import_upload_rule_cant_revert' => 'Sei pregato di assicurarti che i dati siano accurati prima di caricarli, poiché non puoi annullarne il caricamento.', + 'import_upload_form_file' => 'Il tuo file .vcf o .vCard:', + 'import_upload_behaviour' => 'Comportamento:', + 'import_upload_behaviour_add' => 'Aggiungi nuovi contatti e salta esistenti', + 'import_upload_behaviour_replace' => 'Sovrascrivi i contatti già esistenti', + 'import_upload_behaviour_help' => 'La sostituzione rimpiazzerà tutti i dati trovati nella vCard, ma manterrà i campi di contatto esistenti.', + 'import_report_title' => 'Resoconto dell\'importazione', + 'import_report_date' => 'Data dell\'importazione', + 'import_report_type' => 'Tipo di importazione', + 'import_report_number_contacts' => 'Numero di contatti nel file', + 'import_report_number_contacts_imported' => 'Numero di contatti importati', + 'import_report_number_contacts_skipped' => 'Numero di contatti omessi', + 'import_report_status_imported' => 'Importati', + 'import_report_status_skipped' => 'Omessi', + 'import_vcard_parse_error' => 'Errore nel parsing della vCard', + 'import_vcard_contact_exist' => 'Contatto già esistente', + 'import_vcard_contact_no_firstname' => 'Nome mancante (obbligatorio)', + 'import_vcard_file_not_found' => 'File non trovato', + 'import_vcard_unknown_entry' => 'Nome di contatto sconosciuto', + 'import_vcard_file_no_entries' => 'Il file non contiene contatti', + 'import_blank_title' => 'Non hai importato nessun contatto per ora.', + 'import_blank_question' => 'Importare contatti?', + 'import_blank_description' => 'Possiamo importare file vCard ottenibili da Google Contacts o dal tuo gestore di contatti.', + 'import_blank_cta' => 'Importa vCard', + 'import_need_subscription' => 'Importare dati richiede una sottoscrizione.', + + 'tags_list_title' => 'Etichette', + 'tags_list_description' => 'Puoi organizzare i tuoi contatti attraverso le etichette. Le etichette funzionano come delle cartelle, ma puoi aggiungere più di un\'etichetta a ogni contatto. Per aggiungere una nuova etichetta, aggiungila al contatto stesso.', + 'tags_list_contact_number' => '1 contatto|:count contatti', + 'tags_list_delete_success' => 'Etichetta rimossa', + 'tags_list_edit_success' => 'The tag has been successfully updated', + 'tags_list_delete_confirmation' => 'Rimuovere etichetta? Nessun contatto verrà rimosso, solo l\'etichetta.', + 'tags_blank_title' => 'Le etichette sono un buon modo di organizzare i tuoi contatti.', + 'tags_blank_description' => 'I tag funzionano come cartelle, ma puoi aggiungerne più di uno a un contatto. Vai a un contatto e tagga un amico, proprio sotto al nome. Una volta taggato un contatto, torna qui per gestire tutti i tag nel tuo profilo.', + + 'api_title' => 'Accesso all\'API', + 'api_description' => 'L\'API puó essere usata per manipolare le informazioni in Monica da un\'applicazione esterna, ad esempio da un\'applicazione per smartphone.', + 'api_help' => 'Per utilizzare le API, é obbligatorio l\'uso di un token. È possibile creare un token di accesso personale (autenticazione Bearer), o autorizzare un client OAuth per farlo creare al posto vostro. Vedi documentazione riguardo le API per maggiori informazioni.', + 'api_endpoint' => 'L\'endpoint API per questa istanza Monica è:', + + 'api_personal_access_tokens' => 'Personal access token', + 'api_pao_description' => 'Assicurati di dare questo token a fonti fidate, giá che danno accesso a tutti i tuoi dati.', + 'api_token_title' => 'Token di Acceso personale', + 'api_token_create_new' => 'Crea nuovo token', + 'api_token_not_created' => 'Non hai creato nessun token di accesso.', + 'api_token_name' => 'Nome token', + 'api_token_expire' => 'Scade il {date}', + 'api_token_delete' => 'Rimuovi', + 'api_token_create' => 'Crea Token', + 'api_token_scopes' => 'Visibilità', + 'api_token_help' => 'Ecco il tuo nuovo token. Questa è l\'unica volta in cui viene mostrato, per cui segnatelo! Da ora in poi puoi utilizzarlo per fare richieste alle API.', + + 'api_oauth_clients' => 'I tuoi client Oauth', + 'api_oauth_clients_desc' => 'Questa sezione ti permette di registrare i tuoi client OAuth.', + 'api_oauth_clients_desc2' => 'Usa questo Id Client per richiedere un nuovo token e convertire i codici di autorizzazione a token di accesso. Vedi la documentazione di Laravel Passport per ulteriori informazioni.', + 'api_oauth_title' => 'Client OAuth', + 'api_oauth_create_new' => 'Crea nuovo client', + 'api_oauth_edit' => 'Modifica client', + 'api_oauth_not_created' => 'Non hai ancora creato nessun client OAuth.', + 'api_oauth_clientid' => 'ID Cliente', + 'api_oauth_name' => 'Nome', + 'api_oauth_name_help' => 'Qualcosa di riconoscibile per i tuoi utenti.', + 'api_oauth_secret' => 'Segreto', + 'api_oauth_create' => 'Crea client', + 'api_oauth_redirecturl' => 'URL di reindirizzamento', + 'api_oauth_redirecturl_help' => 'Indirizzo della callback di autorizzazione della tua applicazione.', + + 'api_authorized_clients' => 'Lista di client autorizzati', + 'api_authorized_clients_desc' => 'Questa sezione elenca tutti i client che hai autorizzato ad accedere all\'applicazione. Puoi revocare questa autorizzazione in qualsiasi momento.', + 'api_authorized_clients_title' => 'Applicazioni autorizzate', + 'api_authorized_clients_none' => 'Ancora non c\'è alcun client autorizzato.', + 'api_authorized_clients_name' => 'Nome', + 'api_authorized_clients_scopes' => 'Visibilità', + + 'personalization_tab_title' => 'Personalizza il tuo account', + + 'personalization_title' => 'Qui puoi trovare diverse impostazioni per configurare il tuo profilo. Queste funzionalità sono intese per "utenti esperti" che vogliono il massimo controllo su Monica.', + 'personalization_contact_field_type_title' => 'Forme di contatto', + 'personalization_contact_field_type_add' => 'Aggiungi una nuova forma di contatto', + 'personalization_contact_field_type_description' => 'Puoi configurare tutti i diversi tipi di campi di contatto che puoi associare a tutti i tuoi contatti. Ad esempio, se comparisse un nuovo social network in futuro, potrai aggiungere questo nuovo mezzo di comunicazione ai tuoi contatti, proprio qui.', + 'personalization_contact_field_type_table_name' => 'Nome', + 'personalization_contact_field_type_table_protocol' => 'Protocollo', + 'personalization_contact_field_type_table_actions' => 'Azioni', + 'personalization_contact_field_type_modal_title' => 'Aggiungi una nova forma di contatto', + 'personalization_contact_field_type_modal_edit_title' => 'Aggiorna una forma di contatto esistente', + 'personalization_contact_field_type_modal_delete_title' => 'Rimuovi una forma di contatto esistente', + 'personalization_contact_field_type_modal_delete_description' => 'Sei sicuro di voler eliminare questo tipo di campo di contatto? Eliminandolo, cancellerai TUTTI i dati di questo tipo per tutti i tuoi contatti.', + 'personalization_contact_field_type_modal_name' => 'Nome', + 'personalization_contact_field_type_modal_protocol' => 'Protocollo (facoltativo)', + 'personalization_contact_field_type_modal_protocol_help' => 'Si puó cliccare su ogni forma di contatto. Se é impostato un protocollo, useremo quello.', + 'personalization_contact_field_type_modal_icon' => 'Icona (facoltativa)', + 'personalization_contact_field_type_modal_icon_help' => 'Puoi associare un\'icona a questa forma di contatto. Dev\'essere un\'icona di Font Awesome.', + 'personalization_contact_field_type_delete_success' => 'Il tipo di campo di contatto è stato correttamente eliminato.', + 'personalization_contact_field_type_add_success' => 'Forma di contatto aggiunta.', + 'personalization_contact_field_type_edit_success' => 'Forma di contatto aggiornata.', + + 'personalization_genders_title' => 'Tipi di sesso', + 'personalization_genders_add' => 'Aggiungi un nuovo sesso', + 'personalization_genders_desc' => 'Puoi definire tutti i sessi che vuoi. Nel tuo account deve essere presente almeno un tipo di sesso.', + 'personalization_genders_modal_add' => 'Aggiungi sesso', + 'personalization_genders_modal_edit' => 'Aggiorna sesso', + 'personalization_genders_modal_name' => 'Nome', + 'personalization_genders_modal_name_help' => 'Il nome utilizzato per visualizzare il genere in una pagina di contatto.', + 'personalization_genders_modal_sex' => 'Sesso', + 'personalization_genders_modal_sex_help' => 'Usato per definire le relazioni, e durante il processo di importazione/esportazione della VCard.', + 'personalization_genders_modal_default' => 'Seleziona il sesso predefinito per un nuovo contatto', + 'personalization_genders_modal_delete' => 'Elimina sesso', + 'personalization_genders_modal_delete_desc' => 'Sei sicuro di voler eliminare il genere "{name}"?', + 'personalization_genders_modal_delete_question' => 'Correntemente hai {count} contatto con questo genere. Se elimini questo genere, questo contatto quale dovrebbe avere?|Correntemente hai {count} contatti con questo genere. Se elimini questo genere, questi contatti quale dovrebbe avere?', + 'personalization_genders_modal_delete_question_default' => 'Questo genere è predefinito. Se elimini questo genere, quale sarà il nuovo predefinito?', + 'personalization_genders_modal_error' => 'Sei pregato di scegliere un genere dall\'elenco.', + 'personalization_genders_list_contact_number' => '{count} contatto|{count} contatti', + 'personalization_genders_table_name' => 'Nome', + 'personalization_genders_table_sex' => 'Sesso', + 'personalization_genders_table_default' => 'Predefinito', + 'personalization_genders_default' => 'Genere predefinito', + 'personalization_genders_make_default' => 'Cambia genere predefinito', + 'personalization_genders_select_default' => 'Seleziona genere predefinito', + 'personalization_genders_m' => 'Maschio', + 'personalization_genders_f' => 'Femmina', + 'personalization_genders_o' => 'Altro', + 'personalization_genders_u' => 'Sconosciuto', + 'personalization_genders_n' => 'Nessuno o non applicabile', + + 'personalization_reminder_rule_save' => 'Cambiamenti salvati', + 'personalization_reminder_rule_title' => 'Regole per i promemoria', + 'personalization_reminder_rule_line' => '{count} giorno prima|{count} giorni prima', + 'personalization_reminder_rule_desc' => 'Per ogni promemoria che imposti, Monica ti invierà un\'email un certo numero di giorni prima dell\'evento. Puoi regolare qui queste impostazioni di notifica. Queste notifiche si applicano solo a promemoria mensili e annuali.', + + 'personalization_module_save' => 'Cambiamenti salvati', + 'personalization_module_title' => 'Funzionalità', + 'personalization_module_desc' => 'Potresti non necessitare di tutte le funzionalità di Monica. Sotto puoi attivare/disattivare funzionalità specifiche usate su una rubrica. Questa modifica influenzerà TUTTI i tuoi contatti. Disattivare una funzionalità non ne elimina tutti i dati, nasconde semplicemente la funzionalità.', + + 'personalisation_paid_upgrade' => 'Questa è una funzionalità premium che richiede un abbonamento a pagamento per essere attivo. Aggiorna il tuo account visitando Impostazioni > Abbonamento.', + 'personalisation_paid_upgrade_vue' => 'Questa è una funzionalità premium che richiede un abbonamento a pagamento per essere attiva. Aggiorna il tuo account visitando Impostazioni > Abbonamento.', + + 'reminder_time_to_send' => 'Orario del giorno di invio dei promemoria', + 'reminder_time_to_send_help' => 'Il tuo prossimo promemoria è pianificato per l\'invio alle {dateTime}.', + + 'personalization_activity_type_category_title' => 'Categorie per le attività', + 'personalization_activity_type_category_add' => 'Aggiungi una nuova categoria di attività', + 'personalization_activity_type_category_table_name' => 'Nome', + 'personalization_activity_type_category_description' => 'Un\'attività con uno dei tuoi contatti può avere un tipo e un tipo di categoria. Il tuo profilo è fornito con una serie di tipi di categoria predefiniti di default, ma puoi personalizzarli qui.', + 'personalization_activity_type_category_table_actions' => 'Azioni', + 'personalization_activity_type_category_modal_add' => 'Aggiungi nuova categoria', + 'personalization_activity_type_category_modal_edit' => 'Modifica una categoria', + 'personalization_activity_type_category_modal_question' => 'Come dovremmo denominare questa nuova categoria?', + 'personalization_activity_type_add_button' => 'Aggiungi categoria', + 'personalization_activity_type_modal_add' => 'Aggiungi una nuova categoria', + 'personalization_activity_type_modal_question' => 'Come dovremmo denominare questo nuovo tipo d\'attività?', + 'personalization_activity_type_modal_edit' => 'Aggiorna una categoria esistente', + 'personalization_activity_type_category_modal_delete' => 'Elimina una categoria', + 'personalization_activity_type_category_modal_delete_desc' => 'Sei sicuro di voler eliminare questa categoria? Eliminarla cancellerà tutti i tipi d\'attività associati. Le attività appartenenti a questa categoria non saranno influenzate da quest\'eliminazione.', + 'personalization_activity_type_modal_delete' => 'Elimina un tipo di attività', + 'personalization_activity_type_modal_delete_desc' => 'Sei sicuro di eliminare questo tipo di attività? Le attività che appartengono a questa categoria non saranno eliminate.', + 'personalization_activity_type_modal_delete_error' => 'Impossibile trovare questo tipo.', + 'personalization_activity_type_category_modal_delete_error' => 'Impossibile trovare questa categoria.', + + 'personalization_life_event_category_title' => 'Categorie dell\'evento della vita', + 'personalization_live_event_category_table_name' => 'Nome', + 'personalization_life_event_category_description' => 'Un evento importante può avere un tipo e una categoria. Il tuo profilo è fornito di una serie di categorie predefinite e tipi di default, ma puoi personalizzare qui i tipi di evento importante.', + 'personalization_live_event_category_table_actions' => 'Azioni', + 'personalization_life_event_type_add_button' => 'Aggiungi un nuovo tipo di evento della vita', + 'personalization_life_event_type_modal_add' => 'Aggiungi un nuovo tipo di evento della vita', + 'personalization_life_event_type_modal_question' => 'Come dovremmo denominare questo nuovo tipo di evento importante?', + 'personalization_life_event_type_modal_edit' => 'Modifica un tipo di evento della vita', + 'personalization_life_event_type_modal_delete' => 'Elimina un tipo di evento della vita', + 'personalization_life_event_type_modal_delete_desc' => 'Sei sicuro di voler eliminare questo tipo di evento della vita? Gli eventi della vita che appartengono a questo tipo saranno eliminati eseguendo quest\'azione.', + 'personalization_life_event_type_modal_delete_error' => 'Impossibile trovare questo tipo di evento della vita.', + + 'personalization_life_event_category_work_education' => 'Lavoro e educazione', + 'personalization_life_event_category_family_relationships' => 'Famiglia e relazioni', + 'personalization_life_event_category_home_living' => 'Casa e vita', + 'personalization_life_event_category_travel_experiences' => 'Viaggi e esperienze', + 'personalization_life_event_category_health_wellness' => 'Salute e benessere', + + 'personalization_life_event_type_new_job' => 'Nuovo lavoro', + 'personalization_life_event_type_retirement' => 'Pensionamento', + 'personalization_life_event_type_new_school' => 'Nuova scuola', + 'personalization_life_event_type_study_abroad' => 'Studio all\'estero', + 'personalization_life_event_type_volunteer_work' => 'Volontariato', + 'personalization_life_event_type_published_book_or_paper' => 'Pubblicazione di un libro o articolo', + 'personalization_life_event_type_military_service' => 'Servizio militare', + 'personalization_life_event_type_first_met' => 'Primo incontro', + 'personalization_life_event_type_new_relationship' => 'Nuova relazione', + 'personalization_life_event_type_engagement' => 'Fidanzamento', + 'personalization_life_event_type_marriage' => 'Matrimonio', + 'personalization_life_event_type_anniversary' => 'Anniversario', + 'personalization_life_event_type_expecting_a_baby' => 'Attesa di un bambino', + 'personalization_life_event_type_new_child' => 'Nuovo bambino', + 'personalization_life_event_type_new_family_member' => 'Nuovo membro di famiglia', + 'personalization_life_event_type_new_pet' => 'Nuovo animale domestico', + 'personalization_life_event_type_end_of_relationship' => 'Fine di una relazione', + 'personalization_life_event_type_loss_of_a_loved_one' => 'Perdita di un caro', + 'personalization_life_event_type_moved' => 'Trasferimento', + 'personalization_life_event_type_bought_a_home' => 'Comprato una casa', + 'personalization_life_event_type_home_improvement' => 'Miglioramento per la casa', + 'personalization_life_event_type_holidays' => 'Vacanze', + 'personalization_life_event_type_new_vehicle' => 'Nuovo veicolo', + 'personalization_life_event_type_new_roommate' => 'Nuovo coinquilino', + 'personalization_life_event_type_overcame_an_illness' => 'Superamento di una malattia', + 'personalization_life_event_type_quit_a_habit' => 'Fine di un vizio', + 'personalization_life_event_type_new_eating_habits' => 'Nuove abitudini alimentari', + 'personalization_life_event_type_weight_loss' => 'Perdita di peso', + 'personalization_life_event_type_wear_glass_or_contact' => 'Iniziato a indossare occhiali o lenti', + 'personalization_life_event_type_broken_bone' => 'Rotto un osso', + 'personalization_life_event_type_removed_braces' => 'Tolto l\'apparecchio', + 'personalization_life_event_type_surgery' => 'Subito un intervento', + 'personalization_life_event_type_dentist' => 'Trattamento odontoiatrico', + 'personalization_life_event_type_new_sport' => 'Iniziato a praticare un nuovo sport', + 'personalization_life_event_type_new_hobby' => 'Iniziato un nuovo hobby', + 'personalization_life_event_type_new_instrument' => 'Iniziato ad apprendere un nuovo strumento', + 'personalization_life_event_type_new_language' => 'Iniziato ad apprendere una nuova lingua', + 'personalization_life_event_type_tattoo_or_piercing' => 'Piercing o tatuaggio', + 'personalization_life_event_type_new_license' => 'Nuova patente', + 'personalization_life_event_type_travel' => 'Viaggio', + 'personalization_life_event_type_achievement_or_award' => 'Premio o riconoscimento', + 'personalization_life_event_type_changed_beliefs' => 'Cambio di credo', + 'personalization_life_event_type_first_word' => 'Prima parola', + 'personalization_life_event_type_first_kiss' => 'Primo bacio', + + 'storage_title' => 'Memoria', + 'storage_account_info' => 'Il limite del tuo profilo è :accountLimit MB. Il tuo uso corrente è :currentAccountSize MB (circa :percentUsage%).', + 'storage_upgrade_notice' => 'Effettua l\'upgrade del tuo account per caricare foto e documenti.', + 'storage_description' => 'Qui puoi trovare tutti i documenti e le foto relativi ai tuoi contatti.', + + 'dav_title' => 'WebDAV', + 'dav_description' => 'Qui puoi trovare tutte le impostazioni per utilizzare le risorse WebDAV per le esportazioni di CardDAV e CalDAV.', + 'dav_copy_help' => 'Copia negli appunti', + 'dav_clipboard_copied' => 'Valore copiato negli appunti', + 'dav_url_base' => 'Url di base per tutte le risorse CardDAV e CalDAV:', + 'dav_connect_help' => 'Puoi collegare i tuoi contatti e/o calendari con questo url di base sul tuo telefono o computer.', + 'dav_connect_help2' => 'Usa il tuo accesso (email) e crea un token API come password per autenticarsi.', + 'dav_url_carddav' => 'Url CardDAV per la risorsa Contatti:', + 'dav_url_caldav_birthdays' => 'Url CalDAV per le risorse di compleanni:', + 'dav_url_caldav_tasks' => 'Url CalDAV per le risorse di task:', + 'dav_title_carddav' => 'CardDAV', + 'dav_title_caldav' => 'CalDAV', + 'dav_carddav_export' => 'Esporta tutti i contatti in un file', + 'dav_caldav_birthdays_export' => 'Esporta tutti i compleanni in un file', + 'dav_caldav_tasks_export' => 'Esporta tutte le attività in un file', + + 'archive_title' => 'Archivia tutti i contatti nel tuo profilo', + 'archive_desc' => 'Questo archivierà tutti i contatti nel tuo profilo.', + 'archive_cta' => 'Archivia tutti i tuoi contatti', + + 'logs_title' => 'Tutto ciò che è successo a questo profilo', + 'logs_actor' => 'Attore', + 'logs_timestamp' => 'Data e ora', + 'logs_description' => 'Descrizione', + 'logs_subject' => 'Soggetto', + 'logs_size' => 'Dimensione (Kb)', + 'logs_object' => 'Oggetto', +]; diff --git a/resources/lang/it/validation.php b/resources/lang/it/validation.php new file mode 100644 index 0000000..ce4a50e --- /dev/null +++ b/resources/lang/it/validation.php @@ -0,0 +1,166 @@ + ':attribute deve essere accettato.', + 'active_url' => ':attribute non è un URL valido.', + 'after' => ':attribute deve essere una data successiva al :date.', + 'after_or_equal' => ':attribute deve essere una data successiva o uguale al :date.', + 'alpha' => ':attribute può contenere solo lettere.', + 'alpha_dash' => ':attribute può contenere solo lettere, numeri e trattini.', + 'alpha_num' => ':attribute può contenere solo lettere e numeri.', + 'array' => ':attribute deve essere un array.', + 'before' => ':attribute deve essere una data precedente al :date.', + 'before_or_equal' => ':attribute deve essere una data precedente o uguale al :date.', + 'between' => [ + 'numeric' => ':attribute deve trovarsi tra :min - :max.', + 'file' => ':attribute deve trovarsi tra :min - :max kilobyte.', + 'string' => ':attribute deve trovarsi tra :min - :max caratteri.', + 'array' => ':attribute deve avere tra :min - :max elementi.', + ], + 'boolean' => 'Il campo :attribute deve essere vero o falso.', + 'confirmed' => 'Il campo di conferma per :attribute non coincide.', + 'date' => ':attribute non è una data valida.', + 'date_equals' => ':attribute deve essere una data e uguale a :date.', + 'date_format' => ':attribute non coincide con il formato :format.', + 'different' => ':attribute e :other devono essere differenti.', + 'digits' => ':attribute deve essere di :digits cifre.', + 'digits_between' => ':attribute deve essere tra :min e :max cifre.', + 'dimensions' => 'Le dimensioni dell\'immagine di :attribute non sono valide.', + 'distinct' => ':attribute contiene un valore duplicato.', + 'email' => ':attribute non è valido.', + 'ends_with' => ':attribute deve finire con uno dei seguenti valori: :values.', + 'exists' => ':attribute selezionato non è valido.', + 'file' => ':attribute deve essere un file.', + 'filled' => 'Il campo :attribute deve contenere un valore.', + 'gt' => [ + 'numeric' => ':attribute deve essere maggiore di :value.', + 'file' => ':attribute deve essere maggiore di :value kilobyte.', + 'string' => ':attribute deve contenere più di :value caratteri.', + 'array' => ':attribute deve contenere più di :value elementi.', + ], + 'gte' => [ + 'numeric' => ':attribute deve essere uguale o maggiore di :value.', + 'file' => ':attribute deve essere uguale o maggiore di :value kilobyte.', + 'string' => ':attribute deve contenere un numero di caratteri uguale o maggiore di :value.', + 'array' => ':attribute deve contenere un numero di elementi uguale o maggiore di :value.', + ], + 'image' => ':attribute deve essere un\'immagine.', + 'in' => ':attribute selezionato non è valido.', + 'in_array' => 'Il valore del campo :attribute non esiste in :other.', + 'integer' => ':attribute deve essere un numero intero.', + 'ip' => ':attribute deve essere un indirizzo IP valido.', + 'ipv4' => ':attribute deve essere un indirizzo IPv4 valido.', + 'ipv6' => ':attribute deve essere un indirizzo IPv6 valido.', + 'json' => ':attribute deve essere una stringa JSON valida.', + 'lt' => [ + 'numeric' => ':attribute deve essere minore di :value.', + 'file' => ':attribute deve essere minore di :value kilobyte.', + 'string' => ':attribute deve contenere meno di :value caratteri.', + 'array' => ':attribute deve contenere meno di :value elementi.', + ], + 'lte' => [ + 'numeric' => ':attribute deve essere minore o uguale a :value.', + 'file' => ':attribute deve essere minore o uguale a :value kilobyte.', + 'string' => ':attribute deve contenere un numero di caratteri minore o uguale a :value.', + 'array' => ':attribute deve contenere un numero di elementi minore o uguale a :value.', + ], + 'max' => [ + 'numeric' => ':attribute non può essere superiore a :max.', + 'file' => ':attribute non può essere superiore a :max kilobyte.', + 'string' => ':attribute non può contenere più di :max caratteri.', + 'array' => ':attribute non può avere più di :max elementi.', + ], + 'mimes' => ':attribute deve essere del tipo: :values.', + 'mimetypes' => ':attribute deve essere del tipo: :values.', + 'min' => [ + 'numeric' => ':attribute deve essere almeno :min.', + 'file' => ':attribute deve essere almeno di :min kilobyte.', + 'string' => ':attribute deve contenere almeno :min caratteri.', + 'array' => ':attribute deve avere almeno :min elementi.', + ], + 'not_in' => 'Il valore selezionato per :attribute non è valido.', + 'not_regex' => 'Il formato di :attribute non è valido.', + 'numeric' => ':attribute deve essere un numero.', + 'password' => 'La password non è corretta.', + 'present' => 'Il campo :attribute deve essere presente.', + 'regex' => 'Il formato del campo :attribute non è valido.', + 'required' => 'Il campo :attribute è richiesto.', + 'required_if' => 'Il campo :attribute è richiesto quando :other è :value.', + 'required_unless' => 'Il campo :attribute è richiesto a meno che :other sia in :values.', + 'required_with' => 'Il campo :attribute è richiesto quando :values è presente.', + 'required_with_all' => 'Il campo :attribute è richiesto quando :values sono presenti.', + 'required_without' => 'Il campo :attribute è richiesto quando :values non è presente.', + 'required_without_all' => 'Il campo :attribute è richiesto quando nessuno di :values è presente.', + 'same' => ':attribute e :other devono coincidere.', + 'size' => [ + 'numeric' => ':attribute deve essere :size.', + 'file' => ':attribute deve essere :size kilobyte.', + 'string' => ':attribute deve contenere :size caratteri.', + 'array' => ':attribute deve contenere :size elementi.', + ], + 'starts_with' => ':attribute deve iniziare con uno dei seguenti: :values.', + 'string' => ':attribute deve essere una stringa.', + 'timezone' => ':attribute deve essere una zona valida.', + 'unique' => ':attribute è stato già utilizzato.', + 'uploaded' => ':attribute non è stato caricato.', + 'url' => 'Il formato del campo :attribute non è valido.', + 'uuid' => ':attribute deve essere un UUID valido.', + + /* + |-------------------------------------------------------------------------- + | Custom Validation Language Lines + |-------------------------------------------------------------------------- + | + | Here you may specify custom validation messages for attributes using the + | convention "attribute.rule" to name the lines. This makes it quick to + | specify a specific custom language line for a given attribute rule. + | + */ + + 'custom' => [ + 'attribute-name' => [ + 'rule-name' => 'custom-message', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Custom Validation Attributes + |-------------------------------------------------------------------------- + | + | The following language lines are used to swap our attribute placeholder + | with something more reader friendly such as "E-Mail Address" instead + | of "email". This simply helps us make our message more expressive. + | + */ + + 'attributes' => [], + + 'vue' => [ + 'max' => [ + 'numeric' => '{field} non può essere maggiore di {max}.', + 'string' => '{field} non può essere maggiore di {max} caratteri.', + ], + 'required' => '{field} è obbligatorio.', + 'url' => '{field} non è un URL valido.', + ], + +]; diff --git a/resources/lang/ja.json b/resources/lang/ja.json new file mode 100644 index 0000000..7dd6a49 --- /dev/null +++ b/resources/lang/ja.json @@ -0,0 +1,7 @@ +{ + "The :attribute must contain at least one uppercase and one lowercase letter.": ":attributeは大文字と小文字をそれぞれ1文字以上含めなければなりません。", + "The :attribute must contain at least one letter.": ":attributeは文字を1文字以上含めなければなりません。", + "The :attribute must contain at least one symbol.": ":attributeは記号を1文字以上含めなければなりません。", + "The :attribute must contain at least one number.": ":attributeは数字を1文字以上含めなければなりません。", + "The given :attribute has appeared in a data leak. Please choose a different :attribute.": ":attributeはデータ漏洩の対象だった可能性があります。別の:attributeを選んでください。" +} diff --git a/resources/lang/ja/app.php b/resources/lang/ja/app.php new file mode 100644 index 0000000..d330167 --- /dev/null +++ b/resources/lang/ja/app.php @@ -0,0 +1,571 @@ + 'はい', + 'no' => 'いいえ', + 'update' => '更新', + 'save' => '保存', + 'add' => '追加', + 'cancel' => 'キャンセル', + 'confirm' => '確定する', + 'delete_confirm' => 'Are you sure?', + 'delete' => '削除', + 'edit' => '編集', + 'upload' => 'アップロード', + 'download' => 'ダウンロード', + 'save_close' => '保存して閉じる', + 'close' => '閉じる', + 'copy' => 'コピー', + 'create' => '作成する', + 'remove' => '削除する', + 'revoke' => '取り消し', + 'done' => '完了', + 'back' => '戻る', + 'verify' => '検証', + 'new' => '新規', + 'unknown' => 'わかりません', + 'load_more' => '更に読み込む', + 'loading' => 'Loading…', + 'with' => 'with', + 'today' => '今日', + 'yesterday' => '昨日', + 'another_day' => 'another day', + 'date' => '日付', + 'type' => 'タイプ', + 'zoom' => 'Zoom', + 'upgrade' => 'アップグレードしてロックを解除する', + 'percent_uploaded' => '{percent}% アップロード', + 'retry' => '再試行', + 'filter' => 'リストをフィルタ', + 'go_back' => '戻る', + 'file_selected' => 'One file selected…|{count} files selected…', + + 'application_title' => 'Monica – personal relationship manager', + 'application_description' => 'Monica(モニカ)はあなたの交流・家族・友人の情報を記録するツールです。', + 'application_og_title' => 'Have better relations with your loved ones. Free online CRM for friends and family.', + + 'markdown_description' => 'Want to format your text nicely? We support Markdown to add bold, italic, lists, and more.', + 'markdown_link' => 'ドキュメントを読む', + + 'header_settings_link' => '設定', + 'header_logout_link' => 'ログアウト', + 'header_changelog_link' => '機能の変更', + + 'main_nav_cta' => '人を追加します', + 'main_nav_dashboard' => 'ダッシュボード', + 'main_nav_family' => '連絡先', + 'main_nav_journal' => '日記', + 'main_nav_activities' => 'アクテビティ', + 'main_nav_tasks' => 'タスク', + + 'footer_remarks' => 'Comments?', + 'footer_send_email' => 'Send us an email', + 'footer_privacy' => 'プライバシーポリシー', + 'footer_release' => '更新情報', + 'footer_newsletter' => 'ニュースレター', + 'footer_source_code' => 'Contribute', + 'footer_version' => 'バージョン: :version', + 'footer_new_version' => 'A new version of Monica is available', + + 'footer_modal_version_whats_new' => '新着情報', + 'footer_modal_version_release_away' => 'You are 1 release behind the latest version available. You should update your instance.|You are :number releases behind the latest version available. You should update your instance.', + + 'breadcrumb_dashboard' => 'ダッシュボード', + 'breadcrumb_list_contacts' => '連絡先のリスト', + 'breadcrumb_archived_contacts' => 'Archived contacts', + 'breadcrumb_journal' => '日記', + 'breadcrumb_settings' => '設定', + 'breadcrumb_settings_export' => 'エクスポート', + 'breadcrumb_settings_users' => 'ユーザー', + 'breadcrumb_settings_users_add' => 'ユーザーを追加', + 'breadcrumb_settings_subscriptions' => 'Subscription', + 'breadcrumb_settings_import' => 'インポート', + 'breadcrumb_settings_import_report' => 'Import report', + 'breadcrumb_settings_import_upload' => 'アップロード', + 'breadcrumb_settings_tags' => 'タグ', + 'breadcrumb_add_significant_other' => 'Add significant other', + 'breadcrumb_edit_significant_other' => 'Edit significant other', + 'breadcrumb_add_note' => 'Add a note', + 'breadcrumb_edit_note' => 'Edit a note', + 'breadcrumb_api' => 'API', + 'breadcrumb_dav' => 'DAV Resources', + 'breadcrumb_edit_introductions' => 'どうやって会いましたか?', + 'breadcrumb_settings_personalization' => 'Personalization', + 'breadcrumb_settings_security' => 'セキュリティ', + 'breadcrumb_settings_security_2fa' => 'Two Factor Authentication', + 'breadcrumb_profile' => 'プロフィール :name', + + 'gender_male' => '男', + 'gender_female' => '女', + 'gender_none' => '言いたくない', + 'gender_no_gender' => '性別無し', + + 'error_title' => 'Whoops! Something went wrong.', + 'error_unauthorized' => 'You don’t have the right to edit this resource.', + 'error_user_account' => 'This user does not belong to the given account.', + 'error_save' => 'We had an error trying to save the data.', + 'error_try_again' => 'Something went wrong. Please try again.', + 'error_id' => 'エラー ID: :id', + 'error_unavailable' => 'サービスを利用できません', + 'error_maintenance' => 'Maintenance in progress. We’ll be right back.', + 'error_help' => 'メンテナンスはすぐに終わります。', + 'error_twitter' => 'Follow our Twitter account to be alerted when it’s up again.', + 'error_no_term' => 'There is no policy for this instance yet.', + + 'default_save_success' => 'The data has been saved.', + + 'compliance_title' => 'Sorry for the interruption.', + 'compliance_desc' => 'We have changed our Terms of Use and Privacy Policy. By law we have to ask you to review them and accept them so you can continue to use your account.', + 'compliance_desc_end' => 'We don’t do anything nasty with your data or your account and we never will.', + 'compliance_terms' => 'Accept new terms and privacy policy', + + // Relationship types + // Yes, each relationship type has 8 strings associated with it. + // This is because we need to indicate the name of the relationship type, + // and also the name of the opposite side of this relationship (father/son), + // and then, the feminine version of the string. Finally, in some sentences + // in the UI, we need to include the name of the person we add the relationship + // to. + 'relationship_type_group_love' => '恋人関係', + 'relationship_type_group_family' => '家族構成', + 'relationship_type_group_friend' => '友人関係', + 'relationship_type_group_work' => '仕事関係', + 'relationship_type_group_other' => 'その他の人間関係', + + 'relationship_type_partner' => 'significant other', + 'relationship_type_partner_female' => 'significant other', + 'relationship_type_partner_male' => 'significant other', + 'relationship_type_partner_with_name' => ':name’s significant other', + 'relationship_type_partner_female_with_name' => ':name’s significant other', + 'relationship_type_partner_male_with_name' => ':name’s significant other', + + 'relationship_type_spouse' => '配偶者', + 'relationship_type_spouse_female' => 'wife', + 'relationship_type_spouse_male' => 'husband', + 'relationship_type_spouse_with_name' => ':name’s spouse', + 'relationship_type_spouse_female_with_name' => ':name’s wife', + 'relationship_type_spouse_male_with_name' => ':name’s husband', + + 'relationship_type_date' => 'date', + 'relationship_type_date_female' => 'date', + 'relationship_type_date_male' => 'date', + 'relationship_type_date_with_name' => ':name’s date', + 'relationship_type_date_female_with_name' => ':name’s date', + 'relationship_type_date_male_with_name' => ':name’s date', + + 'relationship_type_lover' => '恋人', + 'relationship_type_lover_female' => '恋人', + 'relationship_type_lover_male' => 'lover', + 'relationship_type_lover_with_name' => ':name’s lover', + 'relationship_type_lover_female_with_name' => ':name’s lover', + 'relationship_type_lover_male_with_name' => ':name’s lover', + + 'relationship_type_inlovewith' => 'in love with', + 'relationship_type_inlovewith_female' => 'in love with', + 'relationship_type_inlovewith_male' => 'in love with', + 'relationship_type_inlovewith_with_name' => 'someone :name is in love with', + 'relationship_type_inlovewith_female_with_name' => 'someone :name is in love with', + 'relationship_type_inlovewith_male_with_name' => 'someone :name is in love with', + + 'relationship_type_lovedby' => 'loved by', + 'relationship_type_lovedby_female' => 'loved by', + 'relationship_type_lovedby_male' => 'loved by', + 'relationship_type_lovedby_with_name' => ':name’s secret lover', + 'relationship_type_lovedby_female_with_name' => ':name’s secret lover', + 'relationship_type_lovedby_male_with_name' => ':name’s secret lover', + + 'relationship_type_ex' => 'ex-partner', + 'relationship_type_ex_female' => 'ex-girlfriend', + 'relationship_type_ex_male' => 'ex-boyfriend', + 'relationship_type_ex_with_name' => ':name’s ex-partner', + 'relationship_type_ex_female_with_name' => ':name’s ex-girlfriend', + 'relationship_type_ex_male_with_name' => ':name’s ex-boyfriend', + + 'relationship_type_parent' => 'parent', + 'relationship_type_parent_female' => '母', + 'relationship_type_parent_male' => 'father', + 'relationship_type_parent_with_name' => ':name’s parent', + 'relationship_type_parent_female_with_name' => ':name’s mother', + 'relationship_type_parent_male_with_name' => ':name’s father', + + 'relationship_type_child' => 'child', + 'relationship_type_child_female' => 'daughter', + 'relationship_type_child_male' => 'son', + 'relationship_type_child_with_name' => ':name’s child', + 'relationship_type_child_female_with_name' => ':name’s daughter', + 'relationship_type_child_male_with_name' => ':name’s son', + + 'relationship_type_stepparent' => 'step-parent', + 'relationship_type_stepparent_female' => 'stepmother', + 'relationship_type_stepparent_male' => 'stepfather', + 'relationship_type_stepparent_with_name' => ':name’s step-parent', + 'relationship_type_stepparent_female_with_name' => ':name’s stepmother', + 'relationship_type_stepparent_male_with_name' => ':name’s stepfather', + + 'relationship_type_stepchild' => 'stepchild', + 'relationship_type_stepchild_female' => 'stepdaughter', + 'relationship_type_stepchild_male' => 'stepson', + 'relationship_type_stepchild_with_name' => ':name’s stepchild', + 'relationship_type_stepchild_female_with_name' => ':name’s stepdaughter', + 'relationship_type_stepchild_male_with_name' => ':name’s stepson', + + 'relationship_type_sibling' => 'sibling', + 'relationship_type_sibling_female' => 'sister', + 'relationship_type_sibling_male' => 'brother', + 'relationship_type_sibling_with_name' => ':name’s sibling', + 'relationship_type_sibling_female_with_name' => ':name’s sister', + 'relationship_type_sibling_male_with_name' => ':name’s brother', + + 'relationship_type_grandparent' => 'grandparent', + 'relationship_type_grandparent_female' => 'grandmother', + 'relationship_type_grandparent_male' => 'grandfather', + 'relationship_type_grandparent_with_name' => ':name’s grandparent', + 'relationship_type_grandparent_female_with_name' => ':name’s grandmother', + 'relationship_type_grandparent_male_with_name' => ':name’s grandfather', + + 'relationship_type_grandchild' => 'grandchild', + 'relationship_type_grandchild_female' => 'granddaughter', + 'relationship_type_grandchild_male' => 'grandson', + 'relationship_type_grandchild_with_name' => ':name’s grandchild', + 'relationship_type_grandchild_female_with_name' => ':name’s granddauther', + 'relationship_type_grandchild_male_with_name' => ':name’s grandson', + + 'relationship_type_uncle' => 'uncle', + 'relationship_type_uncle_female' => 'aunt', + 'relationship_type_uncle_male' => 'uncle', + 'relationship_type_uncle_with_name' => ':name’s uncle', + 'relationship_type_uncle_female_with_name' => ':name’s aunt', + 'relationship_type_uncle_male_with_name' => ':name’s uncle', + + 'relationship_type_nephew' => 'nephew', + 'relationship_type_nephew_female' => 'niece', + 'relationship_type_nephew_male' => 'nephew', + 'relationship_type_nephew_with_name' => ':name’s nephew', + 'relationship_type_nephew_female_with_name' => ':name’s niece', + 'relationship_type_nephew_male_with_name' => ':name’s nephew', + + 'relationship_type_cousin' => 'cousin', + 'relationship_type_cousin_female' => 'cousin', + 'relationship_type_cousin_male' => 'cousin', + 'relationship_type_cousin_with_name' => ':name’s cousin', + 'relationship_type_cousin_female_with_name' => ':name’s cousin', + 'relationship_type_cousin_male_with_name' => ':name’s cousin', + + 'relationship_type_godfather' => 'godparent', + 'relationship_type_godfather_female' => 'godmother', + 'relationship_type_godfather_male' => 'godfather', + 'relationship_type_godfather_with_name' => ':name’s godparent', + 'relationship_type_godfather_female_with_name' => ':name’s godmother', + 'relationship_type_godfather_male_with_name' => ':name’s godfather', + + 'relationship_type_godson' => 'godchild', + 'relationship_type_godson_female' => 'goddaughter', + 'relationship_type_godson_male' => 'godson', + 'relationship_type_godson_with_name' => ':name’s godchild', + 'relationship_type_godson_female_with_name' => ':name’s goddaughter', + 'relationship_type_godson_male_with_name' => ':name’s godson', + + 'relationship_type_friend' => 'friend', + 'relationship_type_friend_female' => 'friend', + 'relationship_type_friend_male' => 'friend', + 'relationship_type_friend_with_name' => ':name’s friend', + 'relationship_type_friend_female_with_name' => ':name’s friend', + 'relationship_type_friend_male_with_name' => ':name’s friend', + + 'relationship_type_bestfriend' => 'best friend', + 'relationship_type_bestfriend_female' => 'best friend', + 'relationship_type_bestfriend_male' => 'best friend', + 'relationship_type_bestfriend_with_name' => ':name’s best friend', + 'relationship_type_bestfriend_female_with_name' => ':name’s best friend', + 'relationship_type_bestfriend_male_with_name' => ':name’s best friend', + + 'relationship_type_colleague' => 'colleague', + 'relationship_type_colleague_female' => 'colleague', + 'relationship_type_colleague_male' => 'colleague', + 'relationship_type_colleague_with_name' => ':name’s colleague', + 'relationship_type_colleague_female_with_name' => ':name’s colleague', + 'relationship_type_colleague_male_with_name' => ':name’s colleague', + + 'relationship_type_boss' => 'boss', + 'relationship_type_boss_female' => 'boss', + 'relationship_type_boss_male' => 'boss', + 'relationship_type_boss_with_name' => ':name’s boss', + 'relationship_type_boss_female_with_name' => ':name’s boss', + 'relationship_type_boss_male_with_name' => ':name’s boss', + + 'relationship_type_subordinate' => 'subordinate', + 'relationship_type_subordinate_female' => 'subordinate', + 'relationship_type_subordinate_male' => 'subordinate', + 'relationship_type_subordinate_with_name' => ':name’s subordinate', + 'relationship_type_subordinate_female_with_name' => ':name’s subordinate', + 'relationship_type_subordinate_male_with_name' => ':name’s subordinate', + + 'relationship_type_mentor' => 'mentor', + 'relationship_type_mentor_female' => 'mentor', + 'relationship_type_mentor_male' => 'mentor', + 'relationship_type_mentor_with_name' => ':name’s mentor', + 'relationship_type_mentor_female_with_name' => ':name’s mentor', + 'relationship_type_mentor_male_with_name' => ':name’s mentor', + + 'relationship_type_protege' => 'protégé', + 'relationship_type_protege_female' => 'protégé', + 'relationship_type_protege_male' => 'protégé', + 'relationship_type_protege_with_name' => ':name’s protégé', + 'relationship_type_protege_female_with_name' => ':name’s protégé', + 'relationship_type_protege_male_with_name' => ':name’s protégé', + + 'relationship_type_ex_husband' => 'ex-spouse', + 'relationship_type_ex_husband_female' => 'ex-wife', + 'relationship_type_ex_husband_male' => 'ex-husband', + 'relationship_type_ex_husband_with_name' => ':name’s ex-spouse', + 'relationship_type_ex_husband_female_with_name' => ':name’s ex-wife', + 'relationship_type_ex_husband_male_with_name' => ':name’s ex-husband', + + // emotions + 'emotion_primary_love' => 'Love', + 'emotion_primary_joy' => 'Joy', + 'emotion_primary_surprise' => 'Surprise', + 'emotion_primary_anger' => 'Anger', + 'emotion_primary_sadness' => 'Sadness', + 'emotion_primary_fear' => 'Fear', + + 'emotion_secondary_affection' => 'Affection', + 'emotion_secondary_lust' => 'Lust', + 'emotion_secondary_longing' => 'Longing', + 'emotion_secondary_cheerfulness' => 'Cheerfulness', + 'emotion_secondary_zest' => 'Zest', + 'emotion_secondary_contentment' => 'Contentment', + 'emotion_secondary_pride' => 'Pride', + 'emotion_secondary_optimism' => 'Optimism', + 'emotion_secondary_enthrallment' => 'Enthrallment', + 'emotion_secondary_relief' => 'Relief', + 'emotion_secondary_surprise' => 'Surprise', + 'emotion_secondary_irritation' => 'Irritation', + 'emotion_secondary_exasperation' => 'Exasperation', + 'emotion_secondary_rage' => 'Rage', + 'emotion_secondary_disgust' => 'Disgust', + 'emotion_secondary_envy' => 'Envy', + 'emotion_secondary_suffering' => 'Suffering', + 'emotion_secondary_sadness' => 'Sadness', + 'emotion_secondary_disappointment' => 'Disappointment', + 'emotion_secondary_shame' => 'Shame', + 'emotion_secondary_neglect' => 'Neglect', + 'emotion_secondary_sympathy' => 'Sympathy', + 'emotion_secondary_horror' => 'Horror', + 'emotion_secondary_nervousness' => 'Nervousness', + + 'emotion_adoration' => 'Adoration', + 'emotion_affection' => 'Affection', + 'emotion_love' => 'Love', + 'emotion_fondness' => 'Fondness', + 'emotion_liking' => 'Liking', + 'emotion_attraction' => 'Attraction', + 'emotion_caring' => 'Caring', + 'emotion_tenderness' => 'Tenderness', + 'emotion_compassion' => 'Compassion', + 'emotion_sentimentality' => 'Sentimentality', + 'emotion_arousal' => 'Arousal', + 'emotion_desire' => 'Desire', + 'emotion_lust' => 'Lust', + 'emotion_passion' => 'Passion', + 'emotion_infatuation' => 'Infatuation', + 'emotion_longing' => 'Longing', + 'emotion_amusement' => 'Amusement', + 'emotion_bliss' => 'Bliss', + 'emotion_cheerfulness' => 'Cheerfulness', + 'emotion_gaiety' => 'Gaiety', + 'emotion_glee' => 'Glee', + 'emotion_jolliness' => 'Jolliness', + 'emotion_joviality' => 'Joviality', + 'emotion_joy' => 'Joy', + 'emotion_delight' => 'Delight', + 'emotion_enjoyment' => 'Enjoyment', + 'emotion_gladness' => 'Gladness', + 'emotion_happiness' => 'Happiness', + 'emotion_jubilation' => 'Jubilation', + 'emotion_elation' => 'Elation', + 'emotion_satisfaction' => 'Satisfaction', + 'emotion_ecstasy' => 'Ecstasy', + 'emotion_euphoria' => 'Euphoria', + 'emotion_enthusiasm' => 'Enthusiasm', + 'emotion_zeal' => 'Zeal', + 'emotion_zest' => 'Zest', + 'emotion_excitement' => 'Excitement', + 'emotion_thrill' => 'Thrill', + 'emotion_exhilaration' => 'Exhilaration', + 'emotion_contentment' => 'Contentment', + 'emotion_pleasure' => 'Pleasure', + 'emotion_pride' => 'Pride', + 'emotion_eagerness' => 'Eagerness', + 'emotion_hope' => 'Hope', + 'emotion_optimism' => 'Optimism', + 'emotion_enthrallment' => 'Enthrallment', + 'emotion_rapture' => 'Rapture', + 'emotion_relief' => 'Relief', + 'emotion_amazement' => 'Amazement', + 'emotion_surprise' => 'Surprise', + 'emotion_astonishment' => 'Astonishment', + 'emotion_aggravation' => 'Aggravation', + 'emotion_irritation' => 'Irritation', + 'emotion_agitation' => 'Agitation', + 'emotion_annoyance' => 'Annoyance', + 'emotion_grouchiness' => 'Grouchiness', + 'emotion_grumpiness' => 'Grumpiness', + 'emotion_exasperation' => 'Exasperation', + 'emotion_frustration' => 'Frustration', + 'emotion_anger' => 'Anger', + 'emotion_rage' => 'Rage', + 'emotion_outrage' => 'Outrage', + 'emotion_fury' => 'Fury', + 'emotion_wrath' => 'Wrath', + 'emotion_hostility' => 'Hostility', + 'emotion_ferocity' => 'Ferocity', + 'emotion_bitterness' => 'Bitterness', + 'emotion_hate' => 'Hate', + 'emotion_loathing' => 'Loathing', + 'emotion_scorn' => 'Scorn', + 'emotion_spite' => 'Spite', + 'emotion_vengefulness' => 'Vengefulness', + 'emotion_dislike' => 'Dislike', + 'emotion_resentment' => 'Resentment', + 'emotion_disgust' => 'Disgust', + 'emotion_revulsion' => 'Revulsion', + 'emotion_contempt' => 'Contempt', + 'emotion_envy' => 'Envy', + 'emotion_jealousy' => 'Jealousy', + 'emotion_agony' => 'Agony', + 'emotion_suffering' => 'Suffering', + 'emotion_hurt' => 'Hurt', + 'emotion_anguish' => 'Anguish', + 'emotion_depression' => 'Depression', + 'emotion_despair' => 'Despair', + 'emotion_hopelessness' => 'Hopelessness', + 'emotion_gloom' => 'Gloom', + 'emotion_glumness' => 'Glumness', + 'emotion_sadness' => 'Sadness', + 'emotion_unhappiness' => 'Unhappiness', + 'emotion_grief' => 'Grief', + 'emotion_sorrow' => 'Sorrow', + 'emotion_woe' => 'Woe', + 'emotion_misery' => 'Misery', + 'emotion_melancholy' => 'Melancholy', + 'emotion_dismay' => 'Dismay', + 'emotion_disappointment' => 'Disappointment', + 'emotion_displeasure' => 'Displeasure', + 'emotion_guilt' => 'Guilt', + 'emotion_shame' => 'Shame', + 'emotion_regret' => 'Regret', + 'emotion_remorse' => 'Remorse', + 'emotion_alienation' => 'Alienation', + 'emotion_isolation' => 'Isolation', + 'emotion_neglect' => 'Neglect', + 'emotion_loneliness' => 'Loneliness', + 'emotion_rejection' => 'Rejection', + 'emotion_homesickness' => 'Homesickness', + 'emotion_defeat' => 'Defeat', + 'emotion_dejection' => 'Dejection', + 'emotion_insecurity' => 'Insecurity', + 'emotion_embarrassment' => 'Embarrassment', + 'emotion_humiliation' => 'Humiliation', + 'emotion_insult' => 'Insult', + 'emotion_pity' => 'Pity', + 'emotion_sympathy' => 'Sympathy', + 'emotion_alarm' => 'Alarm', + 'emotion_shock' => 'Shock', + 'emotion_fear' => 'Fear', + 'emotion_fright' => 'Fright', + 'emotion_horror' => 'Horror', + 'emotion_terror' => 'Terror', + 'emotion_panic' => 'Panic', + 'emotion_hysteria' => 'Hysteria', + 'emotion_mortification' => 'Mortification', + 'emotion_anxiety' => 'Anxiety', + 'emotion_nervousness' => 'Nervousness', + 'emotion_tenseness' => 'Tenseness', + 'emotion_uneasiness' => 'Uneasiness', + 'emotion_apprehension' => 'Apprehension', + 'emotion_worry' => 'Worry', + 'emotion_distress' => 'Distress', + 'emotion_dread' => 'Dread', + + // weather + 'weather_sunny' => 'Sunny', + 'weather_clear' => 'Clear', + 'weather_clear-day' => 'Clear', + 'weather_clear-night' => 'Clear night', + 'weather_light-drizzle' => 'Light drizzle', + 'weather_patchy-light-drizzle' => 'Patchy light drizzle', + 'weather_patchy-light-rain' => 'Patchy light rain', + 'weather_light-rain' => 'Light rain', + 'weather_moderate-rain-at-times' => 'Moderate rain at times', + 'weather_moderate-rain' => 'Moderate rain', + 'weather_patchy-rain-possible' => 'Patchy rain possible', + 'weather_heavy-rain-at-times' => 'Heavy rain at times', + 'weather_heavy-rain' => 'Heavy rain', + 'weather_light-freezing-rain' => 'Light freezing rain', + 'weather_moderate-or-heavy-freezing-rain' => 'Moderate or heavy freezing rain', + 'weather_light-sleet' => 'Light sleet', + 'weather_moderate-or-heavy-rain-shower' => 'Moderate or heavy rain shower', + 'weather_light-rain-shower' => 'Light rain shower', + 'weather_torrential-rain-shower' => 'Torrential rain shower', + 'weather_rain' => 'Rain', + 'weather_snow' => 'Snow', + 'weather_blowing-snow' => 'Blowing snow', + 'weather_patchy-light-snow' => 'Patchy light snow', + 'weather_light-snow' => 'Light snow', + 'weather_patchy-moderate-snow' => 'Patchy moderate snow', + 'weather_moderate-snow' => 'Moderate snow', + 'weather_patchy-heavy-snow' => 'Patchy heavy snow', + 'weather_heavy-snow' => 'Heavy snow', + 'weather_light-snow-showers' => 'Light snow showers', + 'weather_moderate-or-heavy-snow-showers' => 'Moderate or heavy snow showers', + 'weather_patchy-snow-possible' => 'Patchy snow possible', + 'weather_patchy-sleet-possible' => 'Patchy sleet possible', + 'weather_moderate-or-heavy-sleet' => 'Moderate or heavy sleet', + 'weather_light-sleet-showers' => 'Light sleet showers', + 'weather_moderate-or-heavy-sleet-showers' => 'Moderate or heavy sleet showers', + 'weather_sleet' => 'Sleet', + 'weather_wind' => 'Wind', + 'weather_fog' => 'Fog', + 'weather_freezing-fog' => 'Freezing fog', + 'weather_mist' => 'Mist', + 'weather_blizzard' => 'Blizzard', + 'weather_overcast' => 'Overcast', + 'weather_cloudy' => 'Cloudy', + 'weather_partly-cloudy-day' => 'Partly cloudy', + 'weather_partly-cloudy-night' => 'Partly cloudy', + 'weather_freezing-drizzle' => 'Freezing drizzle', + 'weather_heavy-freezing-drizzle' => 'Heavy freezing drizzle', + 'weather_patchy-freezing-drizzle-possible' => 'Patchy freezing drizzle possible', + 'weather_ice-pellets' => 'Ice pellets', + 'weather_light-showers-of-ice-pellets' => 'Light showers of ice pellets', + 'weather_moderate-or-heavy-showers-of-ice-pellets' => 'Moderate or heavy showers of ice pellets', + 'weather_thundery-outbreaks-possible' => 'Thundery outbreaks possible', + 'weather_patchy-light-rain-with-thunder' => 'Patchy light rain with thunder', + 'weather_moderate-or-heavy-rain-with-thunder' => 'Moderate or heavy rain with thunder', + 'weather_patchy-light-snow-with-thunder' => 'Patchy light snow with thunder', + 'weather_moderate-or-heavy-snow-with-thunder' => 'Moderate or heavy snow with thunder', + 'weather_current_temperature_celsius' => ':temperature °C', + 'weather_current_temperature_fahrenheit' => ':temperature °F', + 'weather_current_title' => 'Current weather', + + // dav + 'dav_contacts' => '連絡先', + 'dav_contacts_description' => ':name’s contacts', + 'dav_birthdays' => 'Birthdays', + 'dav_birthdays_description' => ':name’s contact’s birthdays', + 'dav_tasks' => 'タスク', + 'dav_tasks_description' => ':name’s tasks', + + // contact list + 'contact_list_avatar' => 'Avatar', + 'contact_list_name' => 'Contact', + 'contact_list_description' => 'Description', + +]; diff --git a/resources/lang/ja/auth.php b/resources/lang/ja/auth.php new file mode 100644 index 0000000..abc4631 --- /dev/null +++ b/resources/lang/ja/auth.php @@ -0,0 +1,89 @@ + '認証情報と一致するレコードがありません。', + 'throttle' => 'ログインの試行回数が多すぎます。:seconds 秒後にお試しください。', + 'not_authorized' => 'You are not authorized to execute this action', + 'signup_disabled' => 'Registration is currently disabled', + 'signup_error' => 'An error occured trying to register the user', + 'back_homepage' => 'Back to homepage', + 'mfa_auth_otp' => 'Authenticate with your two factor device', + 'mfa_auth_webauthn' => 'Authenticate with a security key (WebAuthn)', + '2fa_title' => 'Two Factor Authentication', + '2fa_wrong_validation' => 'The two factor authentication has failed.', + '2fa_one_time_password' => 'Two factor authentication code', + '2fa_recuperation_code' => 'Enter a two factor recovery code', + '2fa_one_time_or_recuperation' => 'Enter a two factor authentication code or a recovery code', + '2fa_otp_help' => 'Open up your two factor authentication mobile app and copy the code', + + 'login_to_account' => '貴方のアカウントにログインしてください。', + 'login_with_recovery' => 'Login with a recovery code', + 'login_again' => 'Please login again to your account', + 'email' => 'Email', + 'password' => 'パスワード', + 'recovery' => 'Recovery code', + 'login' => 'ログイン', + 'button_remember' => 'Remember Me', + 'password_forget' => 'Forget your password?', + 'password_reset' => 'Reset your password', + 'use_recovery' => 'Or you can use a recovery code', + 'signup_no_account' => 'Don’t have an account?', + 'signup' => '新規登録', + 'create_account' => 'Create the first account by signing up', + 'change_language_title' => '言語を切り替える', + 'change_language' => 'Change language to :lang', + + 'password_reset_title' => 'パスワードをリセット', + 'password_reset_email' => 'メール アドレス', + 'password_reset_send_link' => 'Send Password Reset Link', + 'password_reset_password' => 'パスワード', + 'password_reset_password_confirm' => 'パスワードの確認', + 'password_reset_action' => 'Reset Password', + 'password_reset_email_content' => 'Click here to reset your password:', + + 'register_title_welcome' => 'Welcome to your newly installed Monica instance', + 'register_create_account' => 'You need to create an account to use Monica', + 'register_title_create' => 'アカウント作成', + 'register_login' => 'Log in if you already have an account.', + 'register_email' => 'Enter a valid email address', + 'register_email_example' => 'you@home', + 'register_firstname' => '名', + 'register_firstname_example' => '例:ジョン', + 'register_lastname' => '姓', + 'register_lastname_example' => 'eg. Doe', + 'register_password' => 'パスワード', + 'register_password_example' => 'Enter a secure password', + 'register_password_confirmation' => 'Password confirmation', + 'register_action' => 'Register', + 'register_policy' => 'Signing up signifies you’ve read and agree to our Privacy Policy and Terms of use.', + 'register_invitation_email' => 'For security purposes, please indicate the email of the person who’ve invited you to join this account. This information is provided in the invitation email.', + + 'confirmation_title' => 'Verify Your Email Address', + 'confirmation_fresh' => 'A fresh verification link has been sent to your email address.', + 'confirmation_check' => 'Before proceeding, please check your email for a verification link.', + 'confirmation_request_another' => 'If you did not receive the email click here to request another.', + + 'confirmation_again' => 'If you want to change your email address you can click here.', + 'email_change_current_email' => 'Current email address:', + 'email_change_title' => 'Change your email address', + 'email_change_new' => '新しいEメールアドレス', + 'email_changed' => 'Your email address has been changed. Check your mailbox to validate it.', +]; diff --git a/resources/lang/ja/changelog.php b/resources/lang/ja/changelog.php new file mode 100644 index 0000000..bac906d --- /dev/null +++ b/resources/lang/ja/changelog.php @@ -0,0 +1,12 @@ + '機能の変更', + 'note' => 'Note: unfortunately, this page is only in English.', +]; diff --git a/resources/lang/ja/dashboard.php b/resources/lang/ja/dashboard.php new file mode 100644 index 0000000..ef5c689 --- /dev/null +++ b/resources/lang/ja/dashboard.php @@ -0,0 +1,42 @@ + 'あなたのアカウントにようこそ!', + 'dashboard_blank_description' => 'Monica is the place to organize all the interactions you have with the people you care about.', + 'dashboard_blank_cta' => 'とりあえず連絡先を追加する', + 'dashboard_blank_illustration' => 'Illustration by Freepik', + + 'notes_title' => 'You don’t have any starred notes yet.', + + 'tab_recent_calls' => '最近の通話:', + 'tab_favorite_notes' => 'Favorite notes', + 'tab_calls_blank' => 'You haven’t logged any calls yet.', + 'tab_debts' => 'Debts', + 'tab_debts_blank' => 'You haven’t logged any debts yet.', + 'tab_tasks' => 'タスク', + 'tab_tasks_blank' => 'You haven’t any tasks yet.', + + 'tasks_add_task_placeholder' => 'What is this task about?', + 'tasks_tab_your_contacts' => 'Tasks related to your contacts', + 'tasks_tab_your_tasks' => 'あなたのタスク', + 'tasks_add_note' => 'Press Enter to add the task.', + 'task_add_cta' => 'Add a task', + + 'debts_you_owe' => 'You owe', + + 'statistics_contacts' => '連絡先', + 'statistics_activities' => 'Activities', + 'statistics_gifts' => 'ギフト', + + 'reminders_next_months' => '3ヶ月以内のイベント', + 'reminders_none' => '今月のリマインダーはありません.', + + 'product_changes' => '機能の変更', + 'product_view_details' => '詳細を表示', +]; diff --git a/resources/lang/ja/format.php b/resources/lang/ja/format.php new file mode 100644 index 0000000..a70a6ba --- /dev/null +++ b/resources/lang/ja/format.php @@ -0,0 +1,36 @@ + 'M d, Y H:i', + 'short_date_year' => 'M d, Y', + 'short_date' => 'M d', + 'short_month' => 'M', + 'short_month_year' => 'M Y', + 'short_day' => 'D', + 'full_date_year' => 'F d, Y', + 'full_month' => 'F', + 'full_month_year' => 'F Y', + 'full_hour' => 'h.i A', + + 'short_text' => '{text}…', +]; diff --git a/resources/lang/ja/journal.php b/resources/lang/ja/journal.php new file mode 100644 index 0000000..9b1f0be --- /dev/null +++ b/resources/lang/ja/journal.php @@ -0,0 +1,38 @@ + 'How was your day? You can rate it once a day.', + 'journal_come_back' => 'Thanks. Come back tomorrow to rate your day again.', + 'journal_description' => 'Note: the journal lists both manual journal entries, and automatic entries like Activities done with your contacts. While you can delete journal entries manually, you’ll have to delete the activity directly on the contact page.', + 'journal_add' => 'Add a journal entry', + 'journal_edit' => 'Edit a journal entry', + 'journal_empty' => 'Empty journal', + 'journal_created_at' => 'Created at {date}', + 'journal_created_automatically' => 'Created automatically', + 'journal_entry_type_journal' => 'Journal entry', + 'journal_entry_type_activity' => 'Activity', + 'journal_entry_rate' => 'You rated your day.', + 'journal_add_comment' => 'Care to add a comment (optional)?', + 'journal_show_comment' => 'Show comment', + 'entry_delete_success' => 'The journal entry has been successfully deleted.', + 'journal_add_title' => 'Title (optional)', + 'journal_add_date' => 'Date', + 'journal_add_post' => 'Entry', + 'journal_add_cta' => 'Save', + 'journal_blank_cta' => 'Add your first journal entry', + 'journal_blank_description' => 'The journal lets you write events that happened to you, and remember them.', + 'delete_confirmation' => 'Are you sure you want to delete this journal entry?', + 'apply_filter' => 'Apply filter', + 'start_date' => 'Start Date', + 'end_date' => 'End Date', + 'per_page' => 'Per Page', + 'sort_order' => 'Sort By Created At', + 'ascending' => 'Ascending', + 'descending' => 'Descending', +]; diff --git a/resources/lang/ja/logs.php b/resources/lang/ja/logs.php new file mode 100644 index 0000000..7b6654b --- /dev/null +++ b/resources/lang/ja/logs.php @@ -0,0 +1,29 @@ + 'Created the contact.', + 'settings_log_contact_created_with_name' => 'Added :name as a contact.', + + // contat description update + 'contact_log_contact_description_updated' => 'Updated the description.', + 'settings_log_contact_description_updated_with_name' => 'Updated the description of :name.', + + // contact description clear + 'contact_log_contact_description_cleared' => 'Cleared the description.', + 'settings_log_contact_description_cleared_with_name' => 'Cleared the description of :name.', + + // contact work information update + 'contact_log_contact_work_updated' => 'Updated work information.', + 'settings_log_contact_work_updated_with_name' => 'Updated work information of :name.', + + // company created + 'settings_log_company_created' => 'Created a company called :name.', +]; diff --git a/resources/lang/ja/mail.php b/resources/lang/ja/mail.php new file mode 100644 index 0000000..b123f3d --- /dev/null +++ b/resources/lang/ja/mail.php @@ -0,0 +1,53 @@ + 'Reminder for :contact', + 'greetings' => 'Hi :username', + 'want_reminded_of' => 'You wanted to be reminded of :reason', + 'for' => 'For: :name', + 'comment' => 'Comment: :comment', + 'footer_contact_info' => '連絡を取る方法', + 'footer_contact_info2' => 'See :name’s profile', + 'footer_contact_info2_link' => 'See :name’s profile: :url', + + 'notification_subject_line' => 'You have an upcoming event', + 'notification_description' => 'In :count days (on :date), the following event will happen:', + + 'stay_in_touch_subject_line' => 'Stay in touch with :name', + 'stay_in_touch_subject_description' => 'You asked to be reminded to stay in touch with :name every :frequency day.|You asked to be reminded to stay in touch with :name every :frequency days.', + + 'notifications_whoops' => 'Whoops!', + 'notifications_hello' => 'Hello!', + 'notifications_regards' => 'Regards', + 'notifications_footer' => 'If you’re having trouble clicking the ":actionText" button, copy and paste the URL below into your web browser: [:actionURL](:actionURL)', + 'notifications_rights' => 'All rights reserved', + + 'confirmation_email_title' => 'Monica – Email verification', + 'confirmation_email_intro'=> 'To validate your email click on the button below', + 'confirmation_email_button' => 'Verify email address', + 'confirmation_email_bottom' => 'If you did not create an account, no further action is required.', + + 'password_reset_title' => 'Monica – Reset Password Notification', + 'password_reset_intro' => 'You are receiving this email because we received a password reset request for your account.', + 'password_reset_button' => 'Reset Password', + 'password_reset_expiration' => 'This password reset link will expire in :count minutes.', + 'password_reset_bottom' => 'If you did not request a password reset, no further action is required.', + + 'invitation_title' => 'Monica – You are invited by :name', + 'invitation_intro' => 'You’ve been invited by :name (:email) to use Monica, a nice Personal Relationship Management tool.', + 'invitation_link' => 'To accept the invitation, click on the link below:', + 'invitation_button' => 'Accept invitation', + 'invitation_expiration' => 'This link will expire in :count days.', + + 'export_title' => 'Your export is ready', + 'export_description' => 'You requested a data export on :date. It is now ready to download.', + 'export_download' => 'Download export', + +]; diff --git a/resources/lang/ja/pagination.php b/resources/lang/ja/pagination.php new file mode 100644 index 0000000..f257e2a --- /dev/null +++ b/resources/lang/ja/pagination.php @@ -0,0 +1,25 @@ + '❮ 前', + 'next' => '次 ❯', + +]; diff --git a/resources/lang/ja/passwords.php b/resources/lang/ja/passwords.php new file mode 100644 index 0000000..828f589 --- /dev/null +++ b/resources/lang/ja/passwords.php @@ -0,0 +1,30 @@ + 'パスワードをリセットしました。', + 'sent' => 'パスワードリマインダーを送信しました。', + 'token' => 'このパスワードリセットトークンは無効です。', + 'user' => 'このメールアドレスに一致するユーザーを見つけることが出来ませんでした。', + 'changed' => 'Password changed successfully.', + 'invalid' => 'Current password you entered is not correct.', + 'throttled' => '時間を置いて再度お試しください。', + +]; diff --git a/resources/lang/ja/people.php b/resources/lang/ja/people.php new file mode 100644 index 0000000..d7ef0f1 --- /dev/null +++ b/resources/lang/ja/people.php @@ -0,0 +1,539 @@ + 'Contact not found', + 'people_list_number_kids' => ':count child|:count children', + 'people_list_last_updated' => 'Last consulted:', + 'people_list_number_reminders' => ':count reminder|:count reminders', + 'people_list_blank_title' => 'You don’t have anyone in your account yet', + 'people_list_blank_cta' => '連絡先を追加する', + 'people_list_sort' => '並べ替え', + 'people_list_stats' => ':count contact|:count contacts', + 'people_list_firstnameAZ' => '名前でソート(A→Z)', + 'people_list_firstnameZA' => '名前でソート(Z→A)', + 'people_list_lastnameAZ' => '姓でソート(A→Z)', + 'people_list_lastnameZA' => '姓でソート(Z→A)', + 'people_list_lastactivitydateNewtoOld' => 'Sort by last activity date, newest to oldest', + 'people_list_lastactivitydateOldtoNew' => 'Sort by last activity date, oldest to newest', + 'people_list_filter_tag' => 'Showing all the contacts tagged with', + 'people_list_clear_filter' => 'フィルターを解除', + 'people_list_contacts_per_tags' => ':count contact|:count contacts', + 'people_list_show_dead' => 'Show deceased people (:count)', + 'people_list_hide_dead' => 'Hide deceased people (:count)', + 'people_search' => 'Search your contacts…', + 'people_search_no_results' => 'No results found', + 'people_search_next' => '次', + 'people_search_prev' => 'Previous', + 'people_search_rows_per_page' => 'Rows per page', + 'people_search_of' => 'of', + 'people_search_page' => 'ページ', + 'people_search_all' => 'All', + 'people_add_new' => 'Add new person', + 'people_list_account_usage' => 'Your account usage: :current/:limit contacts', + 'people_list_account_upgrade_title' => 'Upgrade your account to unlock it to its full potential.', + 'people_list_account_upgrade_cta' => 'Upgrade now', + 'people_list_untagged' => 'View untagged contacts', + 'people_list_filter_untag' => 'Showing all untagged contacts', + 'archived_contact_readonly' => 'Archived contact can’t be edited, please unarchive it first.', + + // people add + 'people_add_title' => '新しく人を作成する', + 'people_add_missing' => 'No person found – add a new one now', + 'people_add_firstname' => '名', + 'people_add_middlename' => 'Middle name (optional)', + 'people_add_lastname' => 'Last name (optional)', + 'people_add_email' => 'Email (optional)', + 'people_add_nickname' => 'Nickname (optional)', + 'people_add_cta' => '追加', + 'people_save_and_add_another_cta' => 'Submit and add someone else', + 'people_add_success' => ':name has been successfully created', + 'people_add_gender' => '性別', + 'people_delete_success' => 'The contact has been deleted', + 'people_delete_message' => '連絡先の削除', + 'people_delete_confirmation' => 'Are you sure you want to delete :name’s contact? Deletion is immediate and permanent.', + 'people_add_birthday_reminder' => 'Wish happy birthday to :name', + 'people_add_birthday_reminder_deceased' => 'On this date, :name would have celebrated their birthday', + 'people_add_import' => '連絡先を インポートしますか?', + 'people_edit_email_error' => 'There is already a contact in your account with this email address. Please choose another one.', + 'people_export' => 'Export as vCard', + 'people_add_reminder_for_birthday' => 'Create an annual birthday reminder', + + // show + 'section_contact_information' => 'Contact information', + 'section_personal_activities' => 'アクテビティ', + 'section_personal_reminders' => 'リマインダー', + 'section_personal_tasks' => 'タスク', + 'section_personal_gifts' => 'ギフト', + 'section_personal_notes' => 'ノート', + + // archived contacts + 'list_link_to_active_contacts' => 'You are viewing archived contacts. See the list of active contacts instead.', + 'list_link_to_archived_contacts' => 'List of archived contacts', + + // Header + 'me' => 'これはあなたです。', + 'edit_contact_information' => 'Edit contact information', + 'contact_archive' => 'Archive contact', + 'contact_unarchive' => 'Unarchive contact', + 'contact_archive_help' => 'Archived contacts are not be shown on the contact list, but still appear in search results.', + 'call_button' => 'Log a call', + 'set_favorite' => 'Favorite contacts are placed at the top of the contact list', + + // Stay in touch + 'stay_in_touch' => 'Stay in touch', + 'stay_in_touch_frequency' => 'Stay in touch every day|Stay in touch every {count} days', + 'stay_in_touch_next_date' => 'Next due: {date}', + 'stay_in_touch_invalid' => 'The frequency must be a number greater than 0.', + 'stay_in_touch_premium' => 'You need to upgrade your account to make use of this feature', + 'stay_in_touch_modal_title' => 'Stay in touch', + 'stay_in_touch_modal_desc' => 'We can remind you by email to keep in touch with {firstname} at a regular interval.', + 'stay_in_touch_modal_label' => 'Send me an email every… {count} day|Send me an email every… {count} days', + + // Calls + 'modal_call_title' => 'Log a call', + 'modal_call_comment' => 'What did you talk about? (optional)', + 'modal_call_exact_date' => '電話をかけたのは', + 'modal_call_who_called' => 'Who called?', + 'modal_call_emotion' => 'Do you want to log how you felt during this call? (optional)', + 'calls_add_success' => 'The phone call has been saved.', + 'call_delete_confirmation' => 'Are you sure you want to delete this call?', + 'call_delete_success' => 'The call has been deleted successfully', + 'call_title' => 'Phone calls', + 'call_empty_comment' => 'No details', + 'call_blank_title' => 'Keep track of the phone calls you’ve done with {name}', + 'call_blank_desc' => 'You called {name}', + 'call_you_called' => 'You called', + 'call_he_called' => '{name} called', + 'call_emotions' => 'Emotions:', + + // Conversation + 'conversation_blank' => 'Record conversations you have with :name on social media, SMS…', + 'conversation_delete_link' => 'Delete the conversation', + 'conversation_edit_title' => 'Edit conversation', + 'conversation_edit_delete' => 'Are you sure you want to delete this conversation? Deletion is permanent.', + 'conversation_add_success' => 'The conversation has been successfully added.', + 'conversation_edit_success' => 'The conversation has been successfully updated.', + 'conversation_delete_success' => 'The conversation has been successfully deleted.', + 'conversation_add_title' => 'Record a new conversation', + 'conversation_add_when' => 'When did you have this conversation?', + 'conversation_add_who_wrote' => 'Who sent this message?', + 'conversation_add_how' => 'How did you communicate?', + 'conversation_add_you' => 'You', + 'conversation_add_content' => 'Write down what was said', + 'conversation_add_what_was_said' => 'What did you say?', + 'conversation_add_another' => 'Add another message', + 'conversation_add_error' => 'You must add at least one message.', + 'conversation_list_table_messages' => 'Messages', + 'conversation_list_table_content' => 'Partial content (last message)', + 'conversation_list_title' => '会話', + 'conversation_list_cta' => 'Log conversation', + + // age - birthday + 'birthdate_not_set' => 'Birthday is not set', + 'age_approximate_in_years' => 'around :age years old', + 'age_exact_in_years' => ':age years old', + 'age_exact_birthdate' => 'born :date', + + // Last called + 'last_called' => 'Last called: :date', + 'last_talked_to' => 'Last called: {date}', + 'last_called_empty' => 'Last called: unknown', + 'last_activity_date' => 'Last activity together: :date', + 'last_activity_date_empty' => 'Last activity together: unknown', + + // additional information + 'information_edit_success' => 'プロフィールを更新しました', + 'information_edit_title' => ':name の個人情報の編集', + 'information_edit_max_size' => 'Max :size Kb.', + 'information_edit_max_size2' => 'Max {size} Kb.', + 'information_edit_firstname' => '名', + 'information_edit_lastname' => 'Last name (optional)', + 'information_edit_description' => 'Description (optional)', + 'information_edit_description_help' => 'Used on the contact list to add some context, if necessary.', + 'information_edit_unknown' => '年齢は不明です', + 'information_edit_probably' => 'This person is probably…', + 'information_edit_not_year' => 'I know the day and month of this person’s birthday, but not the year…', + 'information_edit_exact' => 'I know this person’s exact birthday…', + 'information_edit_birthdate_label' => 'Birthday', + 'information_no_work_defined' => 'No work information defined', + 'information_work_at' => 'at :company', + 'work_add_cta' => '仕事の情報を更新する', + 'work_edit_success' => 'Work information updated', + 'work_edit_title' => '更新する :name の職業', + 'work_edit_job' => '職業(任意)', + 'work_edit_company' => 'Company (optional)', + 'work_information' => '仕事', + + // food preferences + 'food_preferences_add_success' => '「食べ物の好み」は保存されました。', + 'food_preferences_edit_description' => 'Perhaps :firstname or someone in the :family’s family has an allergy. Or doesn’t like a specific bottle of wine. Indicate them here so you will remember it next time you invite them for dinner', + 'food_preferences_edit_description_no_last_name' => 'Perhaps :firstname has an allergy. Or doesn’t like a specific bottle of wine. Indicate them here so you will remember it next time you invite them for dinner', + 'food_preferences_edit_title' => '「食べ物の好み」を記述する', + 'food_preferences_edit_cta' => '「食べ物の好み」を保存する', + 'food_preferences_title' => '食べ物の好み', + 'food_preferences_cta' => '「食べ物の好み」を追加する', + + // reminders + 'reminders_blank_title' => 'Is there something you want to be reminded of about :name?', + 'reminders_blank_add_activity' => 'Add a reminder', + 'reminders_add_title' => 'What would you like to be reminded of about :name?', + 'reminders_add_description' => 'Please remind me to…', + 'reminders_add_next_time' => 'When is the next time you would like to be reminded about this?', + 'reminders_add_once' => 'Remind me about this just once', + 'reminders_add_recurrent' => 'Remind me about this every', + 'reminders_add_starting_from' => 'starting from the date specified above', + 'reminders_add_cta' => 'Add reminder', + 'reminders_edit_update_cta' => 'Update reminder', + 'reminders_add_error_custom_text' => 'You need to indicate a text for this reminder', + 'reminders_create_success' => 'The reminder has been added successfully', + 'reminders_delete_success' => 'The reminder has been deleted successfully', + 'reminders_update_success' => 'The reminder has been updated successfully', + 'reminders_add_optional_comment' => 'Optional comment', + + 'reminder_frequency_day' => 'every day|every :number days', + 'reminder_frequency_week' => 'every week|every :number weeks', + 'reminder_frequency_month' => 'every month|every :number months', + 'reminder_frequency_year' => 'every year|every :number year', + 'reminder_frequency_one_time' => 'on :date', + 'reminders_delete_confirmation' => 'Are you sure you want to delete this reminder?', + 'reminders_delete_cta' => '削除', + 'reminders_next_expected_date' => 'on', + 'reminders_cta' => 'Add a reminder', + 'reminders_description' => 'We will send an email for each one of the reminders below. Reminders are sent every morning the day events will happen. Reminders automatically added for birthdays can not be deleted. If you want to change those dates, edit the birthday of the contacts.', + 'reminders_one_time' => 'One time', + 'reminders_type_week' => 'week', + 'reminders_type_month' => 'month', + 'reminders_type_year' => 'year', + 'reminders_birthday' => 'Birthday of :name', + 'reminders_free_plan_warning' => 'You are on the Free plan. No emails are sent on this plan. To receive your reminders by email, upgrade your account.', + + // relationships + 'relationship_form_add' => 'Add a new relationship', + 'relationship_form_edit' => 'Edit an existing relationship', + 'relationship_form_is_with' => 'This person is…', + 'relationship_form_is_with_name' => ':name is…', + 'relationship_form_add_choice' => '関係者を選んでください', + 'relationship_form_create_contact' => '新しく人を作成する', + 'relationship_form_associate_contact' => '連絡先から選ぶ', + 'relationship_form_associate_dropdown' => 'Search and select an existing contact from the dropdown below', + 'relationship_form_associate_dropdown_placeholder' => 'Search and select an existing contact', + 'relationship_form_also_create_contact' => 'Create a Contact entry for this person.', + 'relationship_form_add_description' => 'This will let you treat this person like any other contact.', + 'relationship_form_add_no_existing_contact' => 'You don’t have any contacts who can be related to :name at the moment.', + 'relationship_delete_confirmation' => 'Are you sure you want to delete this relationship? Deletion is permanent.', + 'relationship_unlink_confirmation' => 'Are you sure you want to delete this relationship? This person will not be deleted – only the relationship between the two.', + 'relationship_form_add_success' => 'The relationship has been successfully set.', + 'relationship_form_deletion_success' => 'The relationship has been deleted.', + + // tasks + 'tasks_title' => 'タスク', + 'tasks_blank_title' => 'You don’t have any tasks yet.', + 'tasks_form_title' => 'Title', + 'tasks_form_description' => '説明 (任意)', + 'tasks_add_task' => 'タスクの追加', + 'tasks_delete_success' => 'The task has been deleted successfully', + 'tasks_complete_success' => 'The task has changed status successfully', + + // activities + 'activity_title' => 'Activities', + 'activity_type_category_simple_activities' => 'Simple activities', + 'activity_type_category_sport' => 'Sport', + 'activity_type_category_food' => 'Food', + 'activity_type_category_cultural_activities' => 'Cultural activities', + 'activity_type_just_hung_out' => 'just hung out', + 'activity_type_watched_movie_at_home' => 'watched a movie at home', + 'activity_type_talked_at_home' => 'just talked at home', + 'activity_type_did_sport_activities_together' => 'played a sport together', + 'activity_type_ate_at_his_place' => 'ate at their place', + 'activity_type_went_bar' => 'went to a bar', + 'activity_type_ate_at_home' => 'ate at home', + 'activity_type_picnicked' => 'picnicked', + 'activity_type_ate_restaurant' => 'ate at a restaurant', + 'activity_type_went_theater' => 'went to the theater', + 'activity_type_went_concert' => 'went to a concert', + 'activity_type_went_play' => 'went to a play', + 'activity_type_went_museum' => 'went to the museum', + 'activities_add_activity' => 'Add activity', + 'activities_add_more_details' => 'Add more details', + 'activities_add_emotions' => 'Add emotions', + 'activities_add_category' => 'Indicate a category', + 'activities_add_participants_cta' => 'Add participants', + 'activities_item_information' => ':Activity. Happened on :date', + 'activities_add_title' => 'What did you do with {name}?', + 'activities_summary' => 'Describe what you did', + 'activities_add_pick_activity' => 'Would you like to categorize this activity? You don’t have to, but it will give you statistics later on (optional)', + 'activities_add_date_occured' => 'The activity happened on…', + 'activities_add_participants' => 'Who, apart from {name}, participated in this activity? (optional)', + 'activities_add_emotions_title' => 'Do you want to log how you felt during this activity? (optional)', + 'activities_blank_title' => 'Keep track of what you’ve done with {name} in the past, and what you’ve talked about', + 'activities_blank_add_activity' => 'Add an activity', + 'activities_add_success' => 'The activity has been added successfully', + 'activities_add_error' => 'Error when adding the activity', + 'activities_update_success' => 'The activity has been updated successfully', + 'activities_delete_success' => 'The activity has been deleted successfully', + 'activities_who_was_involved' => 'Who was involved?', + 'activities_activity' => 'Activity Category', + 'activities_view_activities_report' => 'View activities report', + 'activities_profile_title' => 'Activities report between :name and you', + 'activities_profile_subtitle' => 'You’ve logged :total_activities activity with :name in total and :activities_last_twelve_months in the last 12 months so far.|You’ve logged :total_activities activities with :name in total and :activities_last_twelve_months in the last 12 months so far.', + 'activities_profile_year_summary_activity_types' => 'Here is a breakdown of the type of activities you’ve done together in :year', + 'activities_profile_year_summary' => 'Here is what you two have done in :year', + 'activities_profile_number_occurences' => ':value activity|:value activities', + 'activities_list_participants' => 'Participants ({total}):', + 'activities_list_emotions' => 'Emotions felt:', + 'activities_list_date' => 'Happened on', + 'activities_list_category' => 'Category:', + + // notes + 'notes_create_success' => 'The note has been created successfully', + 'notes_update_success' => 'The note has been saved successfully', + 'notes_delete_success' => 'The note has been deleted successfully', + 'notes_add_cta' => 'Add note', + 'notes_favorite' => 'Add/remove from favorites', + 'notes_delete_title' => 'Delete a note', + 'notes_delete_confirmation' => 'Are you sure you want to delete this note? Deletion is permanent', + + // gifts + 'gifts_title' => 'ギフト', + 'gifts_add_success' => 'The gift has been added successfully', + 'gifts_delete_success' => 'The gift has been deleted successfully', + 'gifts_delete_confirmation' => 'Are you sure you want to delete this gift?', + 'gifts_add_gift' => 'Add a gift', + 'gifts_link' => 'Link', + 'gifts_for' => 'For: {name}', + 'gifts_delete_cta' => 'Delete', + 'gifts_add_title' => 'Gift management for :name', + 'gifts_add_gift_idea' => 'ギフトのアイデア', + 'gifts_add_gift_already_offered' => 'Gift given', + 'gifts_add_gift_received' => 'Gift received', + 'gifts_add_gift_title' => 'What is this gift?', + 'gifts_add_gift_name' => 'Gift name', + 'gifts_add_link' => 'Link to the web page (optional)', + 'gifts_add_value' => 'Value (optional)', + 'gifts_add_comment' => 'Comment (optional)', + 'gifts_add_recipient' => 'Recipient (optional)', + 'gifts_add_recipient_field' => 'Recipient', + 'gifts_add_photo' => 'Photo (optional)', + 'gifts_add_photo_title' => 'Add a photo for this gift', + 'gifts_add_someone' => 'This gift is for someone in {name}’s family in particular', + 'gifts_delete_title' => 'Delete a gift', + 'gifts_ideas' => 'Gift ideas', + 'gifts_offered' => 'Gifts given', + 'gifts_offered_as_an_idea' => 'Mark as an idea', + 'gifts_received' => 'ギフトの受け取り', + 'gifts_view_comment' => 'View comment', + 'gifts_mark_offered' => 'Mark as given', + 'gifts_update_success' => 'The gift has been updated successfully', + 'gifts_add_date' => 'Date (optional)', + + // debts + 'debt_delete_confirmation' => 'Are you sure you want to delete this debt?', + 'debt_delete_success' => 'The debt has been deleted successfully', + 'debt_add_success' => 'The debt has been added successfully', + 'debt_title' => 'Debts', + 'debt_add_cta' => 'Add debt', + 'debt_you_owe' => 'You owe :amount', + 'debt_they_owe' => ':name owes you :amount', + 'debt_add_title' => 'Debt management', + 'debt_add_you_owe' => 'You owe :name', + 'debt_add_they_owe' => ':name owes you', + 'debt_add_amount' => 'the sum of', + 'debt_add_reason' => 'for the following reason (optional)', + 'debt_add_add_cta' => 'Add debt', + 'debt_edit_update_cta' => 'Update debt', + 'debt_edit_success' => 'The debt has been updated successfully', + 'debts_blank_title' => 'Manage debts you owe to :name or :name owes you', + + // tags + 'tag_edit' => 'Edit tag', + 'tag_add' => 'Add tags', + 'tag_add_search' => 'Add or search tags', + 'tag_no_tags' => 'No tags yet', + + // Introductions + 'introductions_sidebar_title' => 'How you met', + 'introductions_blank_cta' => 'Indicate how you met :name', + 'introductions_title_edit' => 'How did you meet :name?', + 'introductions_additional_info' => 'Explain how and where you met', + 'introductions_edit_met_through' => 'Has someone introduced you to this person?', + 'introductions_no_met_through' => 'No one', + 'introductions_first_met_date' => 'Date you met', + 'introductions_no_first_met_date' => 'I don’t know the date we met', + 'introductions_first_met_date_known' => 'This is the date we met', + 'introductions_add_reminder' => 'Add a reminder to celebrate this encounter on the anniversary this event happened', + 'introductions_update_success' => 'You’ve successfully updated the information about how you met this person', + 'introductions_met_through' => 'Met through :name', + 'introductions_met_date' => 'Met on :date', + 'introductions_reminder_title' => 'Anniversary of the day you first met', + + // Deceased + 'deceased_reminder_title' => 'Anniversary of the death of :name', + 'deceased_mark_person_deceased' => 'Mark this as deceased', + 'deceased_know_date' => 'I know the date that this person died', + 'deceased_add_reminder' => 'Add a reminder for this date', + 'deceased_label' => 'Deceased', + 'deceased_date_label' => 'Deceased date', + 'deceased_label_with_date' => 'Deceased on :date', + 'deceased_age' => 'Age at death', + + // Contact information + 'contact_info_title' => 'Contact information', + 'contact_info_form_content' => 'Content', + 'contact_info_form_contact_type' => 'Contact type', + 'contact_info_form_personalize' => '個人設定', + 'contact_info_address' => 'Lives in', + + // Addresses + 'contact_address_title' => '住所:', + 'contact_address_form_name' => 'Label (optional)', + 'contact_address_form_street' => 'Street (optional)', + 'contact_address_form_city' => 'City (optional)', + 'contact_address_form_province' => 'Province (optional)', + 'contact_address_form_postal_code' => '郵便番号(任意)', + 'contact_address_form_country' => '国名(任意)', + 'contact_address_form_latitude' => '緯度(数字のみ、任意)', + 'contact_address_form_longitude' => '経度(数字のみ、任意)', + + // Pets + 'pets_kind' => 'ペットの種類', + 'pets_name' => 'Name (optional)', + 'pets_create_success' => 'The pet has been successfully added', + 'pets_update_success' => 'The pet has been updated', + 'pets_delete_success' => 'The pet has been deleted', + 'pets_title' => 'ペット', + 'pets_reptile' => '爬虫類', + 'pets_bird' => '鳥', + 'pets_cat' => '猫', + 'pets_dog' => '犬', + 'pets_fish' => '魚', + 'pets_hamster' => 'ハムスター', + 'pets_horse' => '馬', + 'pets_rabbit' => 'ウサギ', + 'pets_rat' => 'ネズミ', + 'pets_small_animal' => '小動物', + 'pets_other' => 'その他', + + // life events + 'life_event_list_tab_life_events' => '出来事', + 'life_event_list_tab_other' => 'Notes, reminders, …', + 'life_event_list_title' => '出来事', + 'life_event_blank' => 'Log what happens to the life of {name} for your future reference.', + 'life_event_list_cta' => '出来事を追加する', + 'life_event_create_category' => 'All categories', + 'life_event_create_life_event' => '出来事を追加する', + 'life_event_create_default_title' => 'Title (optional)', + 'life_event_create_default_story' => 'Story (optional)', + 'life_event_create_date' => 'You do not need to indicate a month or a day – only the year is mandatory.', + 'life_event_create_default_description' => 'Add information about what you know', + 'life_event_create_add_yearly_reminder' => 'Add a yearly reminder for this event', + 'life_event_create_success' => 'The life event has been added', + 'life_event_delete_title' => 'Delete a life event', + 'life_event_delete_description' => 'Are you sure you want to delete this life event? Deletion is permanent.', + 'life_event_delete_success' => 'The life event has been deleted', + 'life_event_date_it_happened' => 'Date it happened', + 'life_event_category_work_education' => 'Work & education', + 'life_event_category_family_relationships' => 'Family & relationships', + 'life_event_category_home_living' => 'Home & living', + 'life_event_category_health_wellness' => 'Health & wellness', + 'life_event_category_travel_experiences' => 'Travel & experiences', + 'life_event_sentence_new_job' => 'Started a new job', + 'life_event_sentence_retirement' => 'Retired', + 'life_event_sentence_new_school' => 'Started school', + 'life_event_sentence_study_abroad' => 'Studied abroad', + 'life_event_sentence_volunteer_work' => 'Started volunteering', + 'life_event_sentence_published_book_or_paper' => 'Published a paper', + 'life_event_sentence_military_service' => 'Started military service', + 'life_event_sentence_new_relationship' => 'Started a relationship', + 'life_event_sentence_engagement' => 'Got engaged', + 'life_event_sentence_marriage' => 'Got married', + 'life_event_sentence_anniversary' => 'Anniversary', + 'life_event_sentence_expecting_a_baby' => 'Expects a baby', + 'life_event_sentence_new_child' => 'Had a child', + 'life_event_sentence_new_family_member' => 'Added a family member', + 'life_event_sentence_new_pet' => 'Got a pet', + 'life_event_sentence_end_of_relationship' => 'Ended a relationship', + 'life_event_sentence_loss_of_a_loved_one' => 'Lost a loved one', + 'life_event_sentence_moved' => 'Moved', + 'life_event_sentence_bought_a_home' => 'Bought a home', + 'life_event_sentence_home_improvement' => 'Made a home improvement', + 'life_event_sentence_holidays' => 'Went on holidays', + 'life_event_sentence_new_vehicle' => 'Got a new vehicle', + 'life_event_sentence_new_roommate' => 'Got a roommate', + 'life_event_sentence_overcame_an_illness' => 'Overcame an illness', + 'life_event_sentence_quit_a_habit' => 'Quit a habit', + 'life_event_sentence_new_eating_habits' => 'Started new eating habits', + 'life_event_sentence_weight_loss' => 'Lost weight', + 'life_event_sentence_wear_glass_or_contact' => 'Started to wear glass or contact lenses', + 'life_event_sentence_broken_bone' => 'Broke a bone', + 'life_event_sentence_removed_braces' => 'Removed braces', + 'life_event_sentence_surgery' => 'Had surgery', + 'life_event_sentence_dentist' => 'Went to the dentist', + 'life_event_sentence_new_sport' => 'Started a sport', + 'life_event_sentence_new_hobby' => 'Started a hobby', + 'life_event_sentence_new_instrument' => 'Learned a new instrument', + 'life_event_sentence_new_language' => 'Learned a new language', + 'life_event_sentence_tattoo_or_piercing' => 'Got a tattoo or piercing', + 'life_event_sentence_new_license' => 'Got a license', + 'life_event_sentence_travel' => 'Traveled', + 'life_event_sentence_achievement_or_award' => 'Got an achievement or award', + 'life_event_sentence_changed_beliefs' => 'Changed beliefs', + 'life_event_sentence_first_word' => 'Spoke for the first time', + 'life_event_sentence_first_kiss' => 'Kissed for the first time', + + // documents + 'document_list_title' => 'ドキュメント', + 'document_list_cta' => 'Upload document', + 'document_list_blank_desc' => 'Here you can store documents related to this person.', + 'document_upload_zone_cta' => 'Upload a file', + 'document_upload_zone_progress' => 'Uploading the document…', + 'document_upload_zone_error' => 'There was an error uploading the document. Please try again below.', + + // Photos + 'photo_title' => '写真', + 'photo_list_title' => '関連する写真', + 'photo_list_cta' => '写真をアップロード', + 'photo_list_blank_desc' => 'この連絡先ページに写真を保存することができます。', + 'photo_upload_zone_cta' => '写真をアップロード', + 'photo_current_profile_pic' => 'Current profile picture', + 'photo_make_profile_pic' => 'Make profile picture', + 'photo_delete' => '写真を削除', + 'photo_next' => 'Next photo ❯', + 'photo_previous' => '❮ Previous photo', + + // Avatars + 'avatar_change_title' => 'アバターを変更', + 'avatar_question' => 'Which avatar would you like to use?', + 'avatar_default_avatar' => 'The default avatar', + 'avatar_adorable_avatar' => 'The Adorable avatar', + 'avatar_gravatar' => 'The Gravatar associated with the email address of this person. Gravatar is a global system that lets users associate email addresses with photos.', + 'avatar_current' => 'Keep the current avatar', + 'avatar_photo' => 'From a photo that you upload', + 'avatar_crop_new_avatar_photo' => 'Crop new avatar photo', + + // emotions + 'emotion_this_made_me_feel' => 'This made you feel…', + + // logs + 'auditlogs_link' => 'History', + 'auditlogs_title' => 'Everything that happened to :name', + 'auditlogs_breadcrumb' => 'History', + 'auditlogs_author' => 'By :name on :date', + + // contact field label + 'contact_field_label_home' => 'Home', + 'contact_field_label_work' => 'Work', + 'contact_field_label_cell' => 'Mobile', + 'contact_field_label_fax' => 'Fax', + 'contact_field_label_pager' => 'Pager', + 'contact_field_label_main' => 'Main', + 'contact_field_label_other' => 'Other', + 'contact_field_label_personal' => 'Personal', +]; diff --git a/resources/lang/ja/reminder.php b/resources/lang/ja/reminder.php new file mode 100644 index 0000000..380796a --- /dev/null +++ b/resources/lang/ja/reminder.php @@ -0,0 +1,16 @@ + 'Wish happy birthday to', + 'type_phone_call' => 'Call', + 'type_lunch' => 'Lunch with', + 'type_hangout' => 'Hangout with', + 'type_email' => 'メールアドレス', + 'type_birthday_kid' => 'Wish happy birthday to the child of', +]; diff --git a/resources/lang/ja/settings.php b/resources/lang/ja/settings.php new file mode 100644 index 0000000..85c8f47 --- /dev/null +++ b/resources/lang/ja/settings.php @@ -0,0 +1,557 @@ + 'アカウントの設定', + 'sidebar_personalization' => 'Personalization', + 'sidebar_settings_storage' => 'ストレージ', + 'sidebar_settings_export' => 'Export data', + 'sidebar_settings_users' => 'Users', + 'sidebar_settings_subscriptions' => 'Subscription', + 'sidebar_settings_import' => 'Import data', + 'sidebar_settings_tags' => 'Tag management', + 'sidebar_settings_api' => 'API', + 'sidebar_settings_dav' => 'DAV Resources', + 'sidebar_settings_security' => 'Security', + 'sidebar_settings_auditlogs' => 'Audit logs', + + 'title_general' => 'General Information', + 'title_i18n' => 'International settings', + 'title_layout' => 'レイアウト', + + 'me_title' => 'Me as a contact', + 'me_help' => 'This is the contact that represents you in Monica', + 'me_select' => 'Select a contact', + 'me_no_contact' => 'No contact selected yet.', + 'me_select_click' => 'Click here to select a contact.', + 'me_remove_contact' => 'Remove the association', + 'me_choose' => 'Choose yourself', + 'me_choose_placeholder' => 'Choose yourself', + + 'export_title' => 'Export your account data', + 'export_be_patient' => 'Click the button to start the export. It might take several minutes to process the export – please be patient and do not spam the button.', + 'export_title_sql' => 'Export to SQL', + 'export_sql_explanation' => 'Exporting your data in SQL format allows you to take your data and import it to your own Monica instance. This is only valuable if you do have your own server.', + 'export_sql_cta' => 'Export to SQL', + 'export_sql_link_instructions' => 'Note: read the instructions to learn more about importing this file to your instance.', + 'export_title_json' => 'Export to Json', + 'export_submitted' => 'Your export has been submitted, it will be available in a few moment…', + 'export_json_explanation' => 'Exporting your data in Json format for backup.', + 'export_json_beta' => 'Json export is in preview mode. Tell us what you think about it:', + 'export_json_cta' => 'Export to Json', + 'export_header_type' => 'Type', + 'export_header_timestamp' => 'Creation date', + 'export_header_status' => 'Status', + 'export_header_actions' => 'Actions', + 'export_last_title' => 'Last exports', + 'export_empty_title' => 'No exports yet', + 'export_type_json' => 'Json export', + 'export_type_sql' => 'SQL export', + 'export_status_todo' => 'Submitted', + 'export_status_doing' => 'Doing', + 'export_status_done' => 'Done', + 'export_status_failed' => 'Failed', + 'export_not_done' => 'Download impossible, this export is not done yet.', + + 'firstname' => '名', + 'lastname' => '姓', + 'name_order' => '姓と名の並び', + 'name_order_firstname_lastname' => ' – John Doe', + 'name_order_lastname_firstname' => ' – Doe John', + 'name_order_firstname_lastname_nickname' => ' () – John Doe (Rambo)', + 'name_order_firstname_nickname_lastname' => ' () – John (Rambo) Doe', + 'name_order_lastname_firstname_nickname' => ' () – Doe John (Rambo)', + 'name_order_lastname_nickname_firstname' => ' () – Doe (Rambo) John', + 'name_order_nickname_firstname_lastname' => ' ( ) – Rambo (John Doe)', + 'name_order_nickname_lastname_firstname' => ' ( ) – Rambo (Doe John)', + 'name_order_nickname' => ' – Rambo', + 'currency' => '通貨', + 'name' => 'Your name: :name', + 'email' => 'メールアドレス', + 'email_placeholder' => 'Enter email', + 'email_help' => 'This is the email used to login, and this is where Monica will send your reminders.', + 'timezone' => 'タイムゾーン', + 'temperature_scale' => 'Temperature scale', + 'temperature_scale_fahrenheit' => 'Fahrenheit (°F)', + 'temperature_scale_celsius' => 'Celsius (°C)', + 'layout' => 'レイアウト', + 'layout_small' => '横幅1200ピクセル', + 'layout_big' => 'ブラウザに合わせる', + 'save' => 'Update preferences', + 'delete_title' => 'Delete your account', + 'delete_desc' => 'Do you wish to delete your account? Deletion is permanent and all of your data will be erased permanently. If you have a subscription, it will be cancelled immediately.', + 'delete_other_desc' => 'Your data in the main database will be deleted immediately. As described in our privacy policy, we carry out securely encrypted backups of the database every day. These backups are kept for 30 days after which they are completely deleted. We cannot delete specific data from the backups we hold any earlier than this. All of your data will be completely deleted no later than 31 days after your account’s deletion.', + 'reset_desc' => 'Do you wish to reset your account? This will remove all your contacts, and all of the data associated with them. Your account will not be deleted.', + 'reset_title' => 'Reset your account', + 'reset_cta' => 'Reset account', + 'reset_notice' => 'Are you sure to reset your account? This is permanent and cannot be undone.', + 'reset_success' => 'Your account has been reset successfully.', + 'delete_notice' => 'Are you sure you want to delete your account? This is permanent and cannot be undone. All of your data will be deleted and will not be recoverable.', + 'delete_cta' => 'Delete account', + 'settings_success' => 'Preferences updated!', + 'locale' => '使用する言語', + 'locale_help' => 'Do you want to help translating Monica or add a new language? Please follow this link for more information.', + 'locale_ar' => 'Arabic', + 'locale_cs' => 'Czech', + 'locale_de' => 'ドイツ語', + 'locale_el' => 'Greek', + 'locale_en' => 'English', + 'locale_en-GB' => 'English (United Kingdom)', + 'locale_es' => 'Spanish', + 'locale_fr' => 'French', + 'locale_he' => 'Hebrew', + 'locale_hr' => 'Croatian', + 'locale_id' => 'Indonesian', + 'locale_it' => 'Italian', + 'locale_ja' => '日本語', + 'locale_nl' => 'Dutch', + 'locale_pt' => 'Portuguese', + 'locale_pt-BR' => 'Portuguese, Brazil', + 'locale_ru' => 'Russian', + 'locale_sv' => 'Swedish', + 'locale_vi' => 'Vietnamese', + 'locale_zh' => 'Chinese Simplified', + 'locale_zh-TW' => 'Chinese Traditional', + 'locale_tr' => 'Turkish', + + 'security_title' => 'Security', + 'security_help' => 'Change security matters for your account.', + 'password_change' => 'Change your password', + 'password_current' => 'Current password', + 'password_current_placeholder' => 'Enter your current password', + 'password_new1' => 'New password', + 'password_new1_placeholder' => 'Enter your new password', + 'password_new2' => 'Confirm your new password', + 'password_new2_placeholder' => 'Retype your new password', + 'password_btn' => 'パスワードを変更', + '2fa_title' => 'Two Factor Authentication', + '2fa_otp_title' => 'Two Factor Authentication mobile application', + '2fa_enable_title' => 'Enable Two Factor Authentication', + '2fa_enable_description' => 'Enable Two Factor Authentication to increase the security of your account.', + '2fa_enable_otp' => 'Open up your Two Factor Authentication mobile app and scan the following QR barcode:', + '2fa_enable_otp_help' => 'If your Two Factor Authentication mobile app does not support QR barcodes, enter in the following code:', + '2fa_enable_otp_validate' => 'Please validate the new device you’ve just set up:', + '2fa_enable_success' => 'Two Factor Authentication activated', + '2fa_enable_error' => 'Error when trying to activate Two Factor Authentication', + '2fa_enable_error_already_set' => 'Two Factor Authentication is already activated', + '2fa_disable_title' => 'Disable Two Factor Authentication', + '2fa_disable_description' => 'Disable Two Factor Authentication for your account. Be careful, your account will be much less secure!', + '2fa_disable_success' => 'Two Factor Authentication disabled', + '2fa_disable_error' => 'Error when trying to disable Two Factor Authentication', + + 'webauthn_title' => 'Security key — WebAuthn protocol', + 'webauthn_enable_description' => 'Add a new security key', + 'webauthn_key_name_help' => 'Give your key a name.', + 'webauthn_key_name' => 'Key name:', + 'webauthn_success' => 'Your key is detected and validated.', + 'webauthn_last_use' => 'Last use: {timestamp}', + 'webauthn_delete_confirmation' => 'Are you sure you want to delete this key?', + 'webauthn_delete_success' => 'Key deleted', + 'webauthn_insertKey' => 'Insert your security key.', + 'webauthn_buttonAdvise' => 'If your security key has a button, press it.', + 'webauthn_noButtonAdvise' => 'If it does not, remove it and insert it again.', + 'webauthn_not_supported' => 'Your browser doesn’t currently support WebAuthn.', + 'webauthn_not_secured' => 'WebAuthn only supports secure connections. Please load this page with https scheme.', + 'webauthn_error_already_used' => 'This key is already registered. It’s not necessary to register it again.', + 'webauthn_error_not_allowed' => 'The operation either timed out or was not allowed.', + + 'recovery_title' => 'Recovery codes', + 'recovery_show' => 'Get recovery codes', + 'recovery_copy_help' => 'Copy codes in your clipboard', + 'recovery_help_intro' => 'These are your recovery codes:', + 'recovery_help_information' => 'You can use each recovery code once.', + 'recovery_clipboard' => 'Codes copied to the clipboard.', + 'recovery_generate' => 'Generate new codes…', + 'recovery_generate_help' => 'Generating new codes will invalidate previously generated codes.', + 'recovery_already_used_help' => 'This code has already been used.', + + 'users_list_title' => 'Users with access to your account', + 'users_list_add_user' => 'Invite a new user', + 'users_list_you' => 'That’s you', + 'users_list_invitations_title' => 'Pending invitations', + 'users_list_invitations_explanation' => 'Below are the people you’ve invited to join Monica as a collaborator.', + 'users_list_invitations_invited_by' => 'invited by :name', + 'users_list_invitations_sent_date' => 'sent on :date', + 'users_blank_title' => 'You are the only one who has access to this account.', + 'users_blank_add_title' => 'Would you like to invite someone else?', + 'users_blank_description' => 'This person will have the same access that you have, and will be able to add, edit or delete contact information.', + 'users_blank_cta' => 'Invite someone', + 'users_add_title' => 'Invite a new user to your account by email', + 'users_add_description' => 'This person will have the same access as you do, including inviting or deleting other users, including you. Make sure you trust this person before giving them access.', + 'users_add_email_field' => 'Enter the email of the person you want to invite', + 'users_add_confirmation' => 'I confirm that I want to invite this user to my account. I understand that this person will have access to ALL of my data and see exactly what I see.', + 'users_add_cta' => 'Invite user by email', + 'users_accept_title' => 'Accept invitation and create a new account', + 'users_error_please_confirm' => 'Please confirm that you want to invite this user before proceeding with the invitation', + 'users_error_email_already_taken' => 'This email is already taken. Please choose another one', + 'users_error_already_invited' => 'You already have invited this user. Please choose another email address.', + 'users_error_email_not_similar' => 'This is not the email of the person who’ve invited you.', + 'users_invitation_deleted_confirmation_message' => 'The invitation has been successfully deleted', + 'users_invitations_delete_confirmation' => 'Are you sure you want to delete this invitation?', + 'users_list_delete_confirmation' => 'Are you sure to delete this user from your account?', + 'users_invitation_need_subscription' => 'Adding more users requires a subscription.', + + 'subscriptions_account_current_plan' => 'Your current plan', + 'subscriptions_account_current_legacy' => 'Current plan, not selectable anymore:', + 'subscriptions_account_current_paid_plan' => 'You are on the :name plan. Thanks so much for being a subscriber.', + + 'subscriptions_account_next_billing_title' => 'Next bill', + 'subscriptions_account_next_billing' => 'Your subscription will auto-renew on :date.', + 'subscriptions_account_bill_monthly' => 'We’ll bill you :price for another month.', + 'subscriptions_account_bill_annual' => 'We’ll bill you :price for another year.', + 'subscriptions_account_change' => 'Change plan', + + 'subscriptions_account_cancel_title' => 'Cancel subscription', + 'subscriptions_account_cancel_action' => 'Cancel subscription', + 'subscriptions_account_cancel' => 'You can cancel subscription anytime.', + 'subscriptions_account_free_plan' => 'You are on the free plan.', + 'subscriptions_account_free_plan_upgrade' => 'You can upgrade your account to the :name plan, which costs $:price per month. Here are the advantages:', + 'subscriptions_account_free_plan_benefits_users' => 'Unlimited number of users', + 'subscriptions_account_free_plan_benefits_reminders' => 'Reminders by email', + 'subscriptions_account_free_plan_benefits_import_data_vcard' => 'Import your contacts with vCard', + 'subscriptions_account_free_plan_benefits_support' => 'Support the project in the long run, so we can introduce more great features.', + 'subscriptions_account_upgrade' => 'Upgrade your account', + 'subscriptions_account_upgrade_title' => 'Upgrade Monica today and have more meaningful relationships.', + 'subscriptions_account_upgrade_choice' => 'Pick a plan below and join over :customers persons who upgraded their Monica.', + 'subscriptions_account_update_title' => 'Update Monica subscription', + 'subscriptions_account_update_description' => 'You can change your subscription’s frequency here.', + 'subscriptions_account_update_information' => 'You will be billed immediately for the new amount. Your subscription will extend to the new period, depending on your choice.', + 'subscriptions_account_invoices' => 'Invoices', + 'subscriptions_account_invoices_download' => 'Download', + 'subscriptions_account_invoices_subscription' => 'Subscription from :startDate to :endDate', + 'subscriptions_account_payment' => 'Which payment option fits you best?', + 'subscriptions_account_confirm_payment' => 'Your payment is currently incomplete, please confirm your payment.', + 'subscriptions_downgrade_title' => 'Downgrade your account to the free plan', + 'subscriptions_downgrade_limitations' => 'The free plan has limitations. In order to be able to downgrade, you need to pass the checklist below:', + 'subscriptions_downgrade_rule_users' => 'You must have only 1 user in your account', + 'subscriptions_downgrade_rule_users_constraint' => 'You currently have 1 user in your account.|You currently have :count users in your account.', + 'subscriptions_downgrade_rule_invitations' => 'You must not have any pending invitations', + 'subscriptions_downgrade_rule_invitations_constraint' => 'You currently have 1 pending invitation.|You currently have :count pending invitations.', + 'subscriptions_downgrade_rule_contacts' => 'You must not have more than :number active contacts', + 'subscriptions_downgrade_rule_contacts_constraint' => 'You currently have 1 contact.|You currently have :count contacts.', + 'subscriptions_downgrade_rule_contacts_archive' => 'We can also archive all your contacts for you – that would clear this rule and let you proceed with your account’s downgrade process.', + 'subscriptions_downgrade_cta' => 'Downgrade', + 'subscriptions_downgrade_success' => 'You are back to the Free plan!', + 'subscriptions_downgrade_thanks' => 'Thanks so much for trying the paid plan. We keep adding new features on Monica all the time – so you might want to come back in the future to see if you might be interested in taking a subscription again.', + 'subscriptions_back' => 'Back to settings', + 'subscriptions_upgrade_title' => 'Upgrade your account', + 'subscriptions_upgrade_choose' => 'You picked the :plan plan.', + 'subscriptions_upgrade_infos' => 'We couldn’t be happier. Enter your payment info below.', + 'subscriptions_upgrade_name' => 'Name on card', + 'subscriptions_upgrade_zip' => 'ZIP or postal code', + 'subscriptions_upgrade_credit' => 'Credit or debit card', + 'subscriptions_upgrade_submit' => 'Pay {amount}', + 'subscriptions_upgrade_charge' => 'We’ll charge your card :price now. The next charge will be on :date. If you ever change your mind, you can cancel at any time, no questions asked.', + 'subscriptions_upgrade_charge_handled' => 'The payment is handled by Stripe. No card information touches our server.', + 'subscriptions_upgrade_success' => 'Thank you! You are now subscribed.', + 'subscriptions_upgrade_thanks' => 'Welcome to the community of people who try to make the world a better place.', + + 'subscriptions_payment_confirm_title' => 'Confirm your :amount payment', + 'subscriptions_payment_confirm_information' => 'Extra confirmation is needed to process your payment. Please confirm your payment by filling out your payment details below.', + 'subscriptions_payment_succeeded_title' => 'Payment Successful', + 'subscriptions_payment_succeeded' => 'This payment was already successfully confirmed.', + 'subscriptions_payment_cancelled_title' => 'Payment Cancelled', + 'subscriptions_payment_cancelled' => 'This payment was cancelled.', + 'subscriptions_payment_error_name' => 'Please provide your name.', + 'subscriptions_payment_success' => 'The payment was successful.', + + 'subscriptions_pdf_title' => 'Your :name monthly subscription', + 'subscriptions_plan_frequency_year' => ':amount / year', + 'subscriptions_plan_frequency_month' => ':amount / month', + 'subscriptions_plan_choose' => 'Choose this plan', + 'subscriptions_plan_year_title' => 'Pay annually', + 'subscriptions_plan_year_bonus' => 'Peace of mind for a whole year', + 'subscriptions_plan_month_title' => 'Pay monthly', + 'subscriptions_plan_month_bonus' => 'Cancel any time', + 'subscriptions_plan_include1' => 'Included with your upgrade:', + 'subscriptions_plan_include2' => 'Unlimited number of contacts • Unlimited number of users • Reminders by email • Import with vCard • Personalization of the contact sheet', + 'subscriptions_plan_include3' => '100% of the profits go the development of this great open source project.', + 'subscriptions_help_title' => 'Additional details you may be curious about', + 'subscriptions_help_opensource_title' => 'What is an open source project?', + 'subscriptions_help_opensource_desc' => 'Monica is an open source project. This means it is built by a community who wants to build a great tool for the greater good. Being open source means the code is publicly available on GitHub, and everyone can inspect it, modify it or enhance it. All the money we raise is dedicated to building better features, paying for more powerful servers, and paying other costs. Thanks for your help. We couldn’t do it without you.', + 'subscriptions_help_limits_title' => 'Is there a limit to the number of contacts we can have on the free plan?', + 'subscriptions_help_limits_plan' => 'Yes. Free plans let you manage :number contacts.', + 'subscriptions_help_discounts_title' => 'Do you have discounts for non-profits and education?', + 'subscriptions_help_discounts_desc' => 'We do! Monica is free for students, and free for non-profits and charities. Just contact the support with a proof of your status and we’ll apply this special status in your account.', + 'subscriptions_help_change_title' => 'What if I change my mind?', + 'subscriptions_help_change_desc' => 'You can cancel anytime, no questions asked, and all by yourself – no need to contact support. However, you will not be refunded for the current period.', + + 'stripe_error_card' => 'Your card was declined. Decline message is: :message', + 'stripe_error_api_connection' => 'Network communication with Stripe failed. Try again later.', + 'stripe_error_rate_limit' => 'Too many requests with Stripe right now. Try again later.', + 'stripe_error_invalid_request' => 'Invalid parameters. Try again later.', + 'stripe_error_authentication' => 'Wrong authentication with Stripe', + + 'import_title' => 'Import contacts in your account', + 'import_cta' => '連絡先をアップロード', + 'import_stat' => 'You’ve imported :number files so far.', + 'import_result_stat' => 'Uploaded vCard with 1 contact (:total_imported imported, :total_skipped skipped)|Uploaded vCard with :total_contacts contacts (:total_imported imported, :total_skipped skipped)', + 'import_view_report' => 'View report', + 'import_in_progress' => 'The import is in progress. Reload the page in one minute.', + 'import_upload_title' => 'Import your contacts from a vCard file', + 'import_upload_rules_desc' => 'We do however have some rules:', + 'import_upload_rule_format' => 'We support .vcard and .vcf files.', + 'import_upload_rule_vcard' => 'We support the vCard 3.0 format, which is the default format for macOS’s Contacts.app and Google Contacts.', + 'import_upload_rule_instructions' => 'Export instructions for macOS Contacts.app and Google Contacts.', + 'import_upload_rule_multiple' => 'If your contacts have multiple email addresses or phone numbers, only the first entry will be saved.', + 'import_upload_rule_limit' => 'Files are limited to 10 MB.', + 'import_upload_rule_time' => 'It might take up to a minute to upload the contacts and process them. Please be patient.', + 'import_upload_rule_cant_revert' => 'Please make sure data is accurate before uploading, as you can’t undo the upload.', + 'import_upload_form_file' => 'Your .vcf or .vCard file:', + 'import_upload_behaviour' => 'Import behaviour:', + 'import_upload_behaviour_add' => 'Add new contacts and skip existing', + 'import_upload_behaviour_replace' => 'Replace existing contacts', + 'import_upload_behaviour_help' => 'Replacing will replace all data found in the vCard, but will keep existing contact fields.', + 'import_report_title' => 'Importing report', + 'import_report_date' => 'Date of the import', + 'import_report_type' => 'Type of import', + 'import_report_number_contacts' => 'Number of contacts in the file', + 'import_report_number_contacts_imported' => 'Number of imported contacts', + 'import_report_number_contacts_skipped' => 'Number of skipped contacts', + 'import_report_status_imported' => 'Imported', + 'import_report_status_skipped' => 'Skipped', + 'import_vcard_parse_error' => 'Error when parsing the vCard entry', + 'import_vcard_contact_exist' => 'Contact already exists', + 'import_vcard_contact_no_firstname' => 'No first name (mandatory)', + 'import_vcard_file_not_found' => 'File not found', + 'import_vcard_unknown_entry' => 'Unknown contact name', + 'import_vcard_file_no_entries' => 'File contains no entries', + 'import_blank_title' => 'You haven’t imported any contacts yet.', + 'import_blank_question' => 'Would you like to import contacts now?', + 'import_blank_description' => 'We can import vCard files that you can get from Google Contacts or your Contact manager.', + 'import_blank_cta' => 'vCardをインポートする', + 'import_need_subscription' => 'Importing data requires a subscription.', + + 'tags_list_title' => 'Tags', + 'tags_list_description' => 'You can organize your contacts by setting up tags. Tags work like folders, but you can add more than one tag to a contact. To add a new tag, add it on the contact itself.', + 'tags_list_contact_number' => '1 contact|:count contacts', + 'tags_list_delete_success' => 'The tag has been successfully deleted', + 'tags_list_edit_success' => 'The tag has been successfully updated', + 'tags_list_delete_confirmation' => 'Are you sure you want to delete the tag? No contacts will be deleted, only the tag.', + 'tags_blank_title' => 'Tags are a great way of categorizing your contacts.', + 'tags_blank_description' => 'Tags work like folders, but you can add more than one tag to a contact. Go to a contact and tag a friend, right below the name. Once a contact is tagged, come back here to manage all the tags in your account.', + + 'api_title' => 'API access', + 'api_description' => 'The API can be used to manipulate Monica’s data from an external application, like a mobile application for instance.', + 'api_help' => 'To use the API, a token is mandatory. You can either create a personal access token (Bearer authentication), or authorize an OAuth client to create it for you. See API documentation.', + 'api_endpoint' => 'The API endpoint for this Monica instance is:', + + 'api_personal_access_tokens' => 'Personal access tokens', + 'api_pao_description' => 'Make sure you give this token to a source you trust – as they allow you to access all your data.', + 'api_token_title' => 'Personal Access Tokens', + 'api_token_create_new' => 'Create New Token', + 'api_token_not_created' => 'You have not created any personal access tokens.', + 'api_token_name' => 'Token name', + 'api_token_expire' => 'Expires at {date}', + 'api_token_delete' => 'Delete', + 'api_token_create' => 'Create Token', + 'api_token_scopes' => 'Scopes', + 'api_token_help' => 'Here is your new personal access token. This is the only time it will be shown so don’t lose it! You may now use this token to make API requests.', + + 'api_oauth_clients' => 'Your OAuth clients', + 'api_oauth_clients_desc' => 'This section lets you register your own OAuth clients.', + 'api_oauth_clients_desc2' => 'Use this client id to request a new token, and convert authorization codes to access tokens. See Laravel Passport documentation for more information.', + 'api_oauth_title' => 'OAuth Clients', + 'api_oauth_create_new' => 'Create New Client', + 'api_oauth_edit' => 'Edit Client', + 'api_oauth_not_created' => 'You have not created any OAuth clients.', + 'api_oauth_clientid' => 'Client ID', + 'api_oauth_name' => 'Name', + 'api_oauth_name_help' => 'Something your users will recognize and trust.', + 'api_oauth_secret' => 'Secret', + 'api_oauth_create' => 'Create Client', + 'api_oauth_redirecturl' => 'Redirect URL', + 'api_oauth_redirecturl_help' => 'Your application’s authorization callback URL.', + + 'api_authorized_clients' => 'List of authorized clients', + 'api_authorized_clients_desc' => 'This section lists all the clients you’ve authorized to access your application data. You can revoke this authorization at anytime.', + 'api_authorized_clients_title' => 'Authorized Applications', + 'api_authorized_clients_none' => 'There are no authorized clients yet.', + 'api_authorized_clients_name' => 'Name', + 'api_authorized_clients_scopes' => 'Scopes', + + 'personalization_tab_title' => 'アカウントをカスタマイズ', + + 'personalization_title' => 'Here you will find different settings to configure your account. These features are intended for “power users” who want maximum control over Monica.', + 'personalization_contact_field_type_title' => 'Contact field types', + 'personalization_contact_field_type_add' => 'Add new field type', + 'personalization_contact_field_type_description' => 'You can configure all the different types of contact fields that you can associate to all your contacts. For example, if a new social network appears in the future, you will be able to add this new way of communicating with your contacts right here.', + 'personalization_contact_field_type_table_name' => 'Name', + 'personalization_contact_field_type_table_protocol' => 'Protocol', + 'personalization_contact_field_type_table_actions' => 'Actions', + 'personalization_contact_field_type_modal_title' => 'Add a new contact field type', + 'personalization_contact_field_type_modal_edit_title' => 'Edit an existing contact field type', + 'personalization_contact_field_type_modal_delete_title' => 'Delete an existing contact field type', + 'personalization_contact_field_type_modal_delete_description' => 'Are you sure you want to delete this contact field type? Deleting this type of contact field will delete ALL the data with this type for all of your contacts.', + 'personalization_contact_field_type_modal_name' => 'Name', + 'personalization_contact_field_type_modal_protocol' => 'Protocol (optional)', + 'personalization_contact_field_type_modal_protocol_help' => 'Each new contact field type can be clickable. If a protocol is set, we will use it to trigger the action that is set.', + 'personalization_contact_field_type_modal_icon' => 'Icon (optional)', + 'personalization_contact_field_type_modal_icon_help' => 'You can associate an icon with this contact field type. You need to add a reference to a Font Awesome icon.', + 'personalization_contact_field_type_delete_success' => 'The contact field type has been successfully deleted.', + 'personalization_contact_field_type_add_success' => 'The contact field type has been successfully added.', + 'personalization_contact_field_type_edit_success' => 'The contact field type has been successfully updated.', + + 'personalization_genders_title' => 'Gender types', + 'personalization_genders_add' => 'Add new gender type', + 'personalization_genders_desc' => 'You can define as many genders as you need to. You need at least one gender type in your account.', + 'personalization_genders_modal_add' => 'Add gender type', + 'personalization_genders_modal_edit' => 'Update gender type', + 'personalization_genders_modal_name' => 'Name', + 'personalization_genders_modal_name_help' => 'The name used to display the gender on a contact page.', + 'personalization_genders_modal_sex' => 'Sex', + 'personalization_genders_modal_sex_help' => 'Used to define the relationships, and during the VCard import/export process.', + 'personalization_genders_modal_default' => 'Select the default gender for a new contact', + 'personalization_genders_modal_delete' => 'Delete gender type', + 'personalization_genders_modal_delete_desc' => 'Are you sure you want to delete the gender “{name}”?', + 'personalization_genders_modal_delete_question' => 'You currently have {count} contact with this gender. If you delete this gender, what gender should this contact have?|You currently have {count} contacts with this gender. If you delete this gender, what gender should these contacts have?', + 'personalization_genders_modal_delete_question_default' => 'This gender is the default one. If you delete this gender, which one will be the new default?', + 'personalization_genders_modal_error' => 'Please choose a gender from the list.', + 'personalization_genders_list_contact_number' => '{count} の連絡先', + 'personalization_genders_table_name' => 'Name', + 'personalization_genders_table_sex' => 'Sex', + 'personalization_genders_table_default' => 'Default', + 'personalization_genders_default' => 'Default gender', + 'personalization_genders_make_default' => 'Change default gender', + 'personalization_genders_select_default' => 'Select default gender', + 'personalization_genders_m' => 'Male', + 'personalization_genders_f' => 'Female', + 'personalization_genders_o' => 'Other', + 'personalization_genders_u' => 'Unknown', + 'personalization_genders_n' => 'None or not applicable', + + 'personalization_reminder_rule_save' => 'The change has been saved', + 'personalization_reminder_rule_title' => 'Reminder rules', + 'personalization_reminder_rule_line' => '{count} day before|{count} days before', + 'personalization_reminder_rule_desc' => 'For every reminder that you set, Monica can send you an email a number of days before the event happens. You can adjust these notification settings here. These notifications only apply to monthly and yearly reminders.', + + 'personalization_module_save' => 'The change has been saved', + 'personalization_module_title' => 'Features', + 'personalization_module_desc' => 'You may not need all of Monica’s features. Below you can toggle specific features that are used on a contact sheet. This change will affect ALL your contacts. Turning off a feature does not delete any data, it simply hides the feature.', + + 'personalisation_paid_upgrade' => 'This is a premium feature that requires a Paid subscription to be active. Upgrade your account by visiting Settings > Subscription.', + 'personalisation_paid_upgrade_vue' => 'This is a premium feature that requires a Paid subscription to be active. Upgrade your account by visiting Settings > Subscription.', + + 'reminder_time_to_send' => 'Time of the day reminders will be sent', + 'reminder_time_to_send_help' => 'Your next reminder is scheduled to be sent on {dateTime}.', + + 'personalization_activity_type_category_title' => 'Activity type categories', + 'personalization_activity_type_category_add' => 'Add a new activity type category', + 'personalization_activity_type_category_table_name' => 'Name', + 'personalization_activity_type_category_description' => 'An activity with one of your contacts can have a type and a category type. Your account comes with a set of predefined category types by default, but you can customize these here.', + 'personalization_activity_type_category_table_actions' => 'Actions', + 'personalization_activity_type_category_modal_add' => 'Add a new activity type category', + 'personalization_activity_type_category_modal_edit' => 'Edit an activity type category', + 'personalization_activity_type_category_modal_question' => 'What should we name this new category?', + 'personalization_activity_type_add_button' => 'Add a new activity type', + 'personalization_activity_type_modal_add' => 'Add a new activity type', + 'personalization_activity_type_modal_question' => 'What should we name this new activity type?', + 'personalization_activity_type_modal_edit' => 'Edit an activity type', + 'personalization_activity_type_category_modal_delete' => 'Delete an activity type category', + 'personalization_activity_type_category_modal_delete_desc' => 'Are you sure you want to delete this category? Deleting it will delete all associated activity types. Activities that belong to this category will not be affected by this deletion.', + 'personalization_activity_type_modal_delete' => 'Delete an activity type', + 'personalization_activity_type_modal_delete_desc' => 'Are you sure you want to delete this activity type? Activities that belong to this category will not be affected by this deletion.', + 'personalization_activity_type_modal_delete_error' => 'We can’t find this activity type.', + 'personalization_activity_type_category_modal_delete_error' => 'We can’t find this activity type category.', + + 'personalization_life_event_category_title' => 'Life event categories', + 'personalization_live_event_category_table_name' => 'Name', + 'personalization_life_event_category_description' => 'A life event can have a type and a category. Your account comes with a set of predefined categories and types by default, but you can customize life event types here.', + 'personalization_live_event_category_table_actions' => 'Actions', + 'personalization_life_event_type_add_button' => 'Add a new life event type', + 'personalization_life_event_type_modal_add' => 'Add a new life event type', + 'personalization_life_event_type_modal_question' => 'What should we name this new life event type?', + 'personalization_life_event_type_modal_edit' => 'Edit a life event type', + 'personalization_life_event_type_modal_delete' => 'Delete a life event type', + 'personalization_life_event_type_modal_delete_desc' => 'Are you sure you want to delete this life event type? Life events that belong to this type will be deleted by performing this action.', + 'personalization_life_event_type_modal_delete_error' => 'We can’t find this life event type.', + + 'personalization_life_event_category_work_education' => 'Work & education', + 'personalization_life_event_category_family_relationships' => 'Family & relationships', + 'personalization_life_event_category_home_living' => 'Home & living', + 'personalization_life_event_category_travel_experiences' => 'Travel & experiences', + 'personalization_life_event_category_health_wellness' => 'Health & wellness', + + 'personalization_life_event_type_new_job' => 'New job', + 'personalization_life_event_type_retirement' => 'Retirement', + 'personalization_life_event_type_new_school' => 'New school', + 'personalization_life_event_type_study_abroad' => 'Study abroad', + 'personalization_life_event_type_volunteer_work' => 'Volunteer work', + 'personalization_life_event_type_published_book_or_paper' => 'Published a book or paper', + 'personalization_life_event_type_military_service' => 'Military service', + 'personalization_life_event_type_first_met' => 'First met', + 'personalization_life_event_type_new_relationship' => 'New relationship', + 'personalization_life_event_type_engagement' => 'Engagement', + 'personalization_life_event_type_marriage' => 'Marriage', + 'personalization_life_event_type_anniversary' => 'Anniversary', + 'personalization_life_event_type_expecting_a_baby' => 'Expecting a baby', + 'personalization_life_event_type_new_child' => 'New child', + 'personalization_life_event_type_new_family_member' => 'New family member', + 'personalization_life_event_type_new_pet' => 'New pet', + 'personalization_life_event_type_end_of_relationship' => 'End of relationship', + 'personalization_life_event_type_loss_of_a_loved_one' => 'Loss of a loved one', + 'personalization_life_event_type_moved' => 'Moved', + 'personalization_life_event_type_bought_a_home' => 'Bought a home', + 'personalization_life_event_type_home_improvement' => 'Home improvement', + 'personalization_life_event_type_holidays' => 'Holidays', + 'personalization_life_event_type_new_vehicle' => 'New vehicle', + 'personalization_life_event_type_new_roommate' => 'New roommate', + 'personalization_life_event_type_overcame_an_illness' => 'Overcame an illness', + 'personalization_life_event_type_quit_a_habit' => 'Quit a habit', + 'personalization_life_event_type_new_eating_habits' => 'New eating habits', + 'personalization_life_event_type_weight_loss' => 'Weight loss', + 'personalization_life_event_type_wear_glass_or_contact' => 'Started wearing glasses or contacts', + 'personalization_life_event_type_broken_bone' => 'Broke a bone', + 'personalization_life_event_type_removed_braces' => 'Had braces removed', + 'personalization_life_event_type_surgery' => 'Had surgery', + 'personalization_life_event_type_dentist' => 'Had dental treatment', + 'personalization_life_event_type_new_sport' => 'Started playing a new sport', + 'personalization_life_event_type_new_hobby' => 'Took up a new hobby', + 'personalization_life_event_type_new_instrument' => 'Started learning a new instrument', + 'personalization_life_event_type_new_language' => 'Started learning a new language', + 'personalization_life_event_type_tattoo_or_piercing' => 'Tattoo or piercing', + 'personalization_life_event_type_new_license' => 'New license', + 'personalization_life_event_type_travel' => 'Travel', + 'personalization_life_event_type_achievement_or_award' => 'Achievement or award', + 'personalization_life_event_type_changed_beliefs' => 'Changed beliefs', + 'personalization_life_event_type_first_word' => 'First word', + 'personalization_life_event_type_first_kiss' => 'First kiss', + + 'storage_title' => 'ストレージ', + 'storage_account_info' => 'Your account limit is :accountLimit MB. Your current usage is :currentAccountSize MB (about :percentUsage%).', + 'storage_upgrade_notice' => 'Upgrade your account to be able to upload documents and photos.', + 'storage_description' => 'Here you can see all the documents and photos uploaded about your contacts.', + + 'dav_title' => 'WebDAV', + 'dav_description' => 'Here you can find all settings to use WebDAV resources for CardDAV and CalDAV exports.', + 'dav_copy_help' => 'Copy into your clipboard', + 'dav_clipboard_copied' => 'Value copied into your clipboard', + 'dav_url_base' => 'Base url for all CardDAV and CalDAV resources:', + 'dav_connect_help' => 'You can connect your contacts and/or calendars with this base url on you phone or computer.', + 'dav_connect_help2' => 'Use your login (email) and create an API token as the password to authenticate.', + 'dav_url_carddav' => 'CardDAV url for Contacts resource:', + 'dav_url_caldav_birthdays' => 'CalDAV url for Birthdays resources:', + 'dav_url_caldav_tasks' => 'CalDAV url for Tasks resources:', + 'dav_title_carddav' => 'CardDAV', + 'dav_title_caldav' => 'CalDAV', + 'dav_carddav_export' => 'Export all contacts in one file', + 'dav_caldav_birthdays_export' => 'Export all birthdays in one file', + 'dav_caldav_tasks_export' => 'Export all tasks in one file', + + 'archive_title' => 'Archive all of the contacts in your account', + 'archive_desc' => 'This will archive all of the contacts in your account.', + 'archive_cta' => 'Archive all of your contacts', + + 'logs_title' => 'Everything that has happened to this account', + 'logs_actor' => 'Actor', + 'logs_timestamp' => 'Timestamp', + 'logs_description' => 'Description', + 'logs_subject' => 'Subject', + 'logs_size' => 'Size (Kb)', + 'logs_object' => 'Object', +]; diff --git a/resources/lang/ja/validation.php b/resources/lang/ja/validation.php new file mode 100644 index 0000000..d451897 --- /dev/null +++ b/resources/lang/ja/validation.php @@ -0,0 +1,166 @@ + ':attributeを承認してください。', + 'active_url' => ':attributeは、有効なURLではありません。', + 'after' => ':attributeには、:dateより後の日付を指定してください。', + 'after_or_equal' => ':attributeには、:date以降の日付を指定してください。', + 'alpha' => ':attributeには、アルファベッドのみ使用できます。', + 'alpha_dash' => ':attributeには、英数字(\'A-Z\',\'a-z\',\'0-9\')とハイフンと下線(\'-\',\'_\')が使用できます。', + 'alpha_num' => ':attributeには、英数字(\'A-Z\',\'a-z\',\'0-9\')が使用できます。', + 'array' => ':attributeには、配列を指定してください。', + 'before' => ':attributeには、:dateより前の日付を指定してください。', + 'before_or_equal' => ':attributeには、:date以前の日付を指定してください。', + 'between' => [ + 'numeric' => ':attributeには、:minから、:maxまでの数字を指定してください。', + 'file' => ':attributeには、:min KBから:max KBまでのサイズのファイルを指定してください。', + 'string' => ':attributeは、:min文字から:max文字にしてください。', + 'array' => ':attributeの項目は、:min個から:max個にしてください。', + ], + 'boolean' => ':attributeには、\'true\'か\'false\'を指定してください。', + 'confirmed' => ':attributeと:attribute確認が一致しません。', + 'date' => ':attributeは、正しい日付ではありません。', + 'date_equals' => ':attributeは:dateに等しい日付でなければなりません。', + 'date_format' => ':attributeの形式は、\':format\'と合いません。', + 'different' => ':attributeと:otherには、異なるものを指定してください。', + 'digits' => ':attributeは、:digits桁にしてください。', + 'digits_between' => ':attributeは、:min桁から:max桁にしてください。', + 'dimensions' => ':attributeの画像サイズが無効です', + 'distinct' => ':attributeの値が重複しています。', + 'email' => ':attributeは、有効なメールアドレス形式で指定してください。', + 'ends_with' => ':attributeは、次のうちのいずれかで終わらなければなりません。: :values', + 'exists' => '選択された:attributeは、有効ではありません。', + 'file' => ':attributeはファイルでなければいけません。', + 'filled' => ':attributeは必須です。', + 'gt' => [ + 'numeric' => ':attributeは、:valueより大きくなければなりません。', + 'file' => ':attributeは、:value KBより大きくなければなりません。', + 'string' => ':attributeは、:value文字より大きくなければなりません。', + 'array' => ':attributeの項目数は、:value個より大きくなければなりません。', + ], + 'gte' => [ + 'numeric' => ':attributeは、:value以上でなければなりません。', + 'file' => ':attributeは、:value KB以上でなければなりません。', + 'string' => ':attributeは、:value文字以上でなければなりません。', + 'array' => ':attributeの項目数は、:value個以上でなければなりません。', + ], + 'image' => ':attributeには、画像を指定してください。', + 'in' => '選択された:attributeは、有効ではありません。', + 'in_array' => ':attributeが:otherに存在しません。', + 'integer' => ':attributeには、整数を指定してください。', + 'ip' => ':attributeには、有効なIPアドレスを指定してください。', + 'ipv4' => ':attributeはIPv4アドレスを指定してください。', + 'ipv6' => ':attributeはIPv6アドレスを指定してください。', + 'json' => ':attributeには、有効なJSON文字列を指定してください。', + 'lt' => [ + 'numeric' => ':attributeは、:valueより小さくなければなりません。', + 'file' => ':attributeは、:value KBより小さくなければなりません。', + 'string' => ':attributeは、:value文字より小さくなければなりません。', + 'array' => ':attributeの項目数は、:value個より小さくなければなりません。', + ], + 'lte' => [ + 'numeric' => ':attributeは、:value以下でなければなりません。', + 'file' => ':attributeは、:value KB以下でなければなりません。', + 'string' => ':attributeは、:value文字以下でなければなりません。', + 'array' => ':attributeの項目数は、:value個以下でなければなりません。', + ], + 'max' => [ + 'numeric' => ':attributeには、:max以下の数字を指定してください。', + 'file' => ':attributeには、:max KB以下のファイルを指定してください。', + 'string' => ':attributeは、:max文字以下にしてください。', + 'array' => ':attributeの項目は、:max個以下にしてください。', + ], + 'mimes' => ':attributeには、:valuesタイプのファイルを指定してください。', + 'mimetypes' => ':attributeには、:valuesタイプのファイルを指定してください。', + 'min' => [ + 'numeric' => ':attributeには、:min以上の数字を指定してください。', + 'file' => ':attributeには、:min KB以上のファイルを指定してください。', + 'string' => ':attributeは、:min文字以上にしてください。', + 'array' => ':attributeの項目は、:min個以上にしてください。', + ], + 'not_in' => '選択された:attributeは、有効ではありません。', + 'not_regex' => ':attributeの形式が無効です。', + 'numeric' => ':attributeには、数字を指定してください。', + 'password' => 'パスワードが正しくありません。', + 'present' => ':attributeが存在している必要があります。', + 'regex' => ':attributeには、有効な正規表現を指定してください。', + 'required' => ':attributeは、必ず指定してください。', + 'required_if' => ':otherが:valueの場合、:attributeを指定してください。', + 'required_unless' => ':otherが:values以外の場合、:attributeを指定してください。', + 'required_with' => ':valuesが指定されている場合、:attributeも指定してください。', + 'required_with_all' => ':valuesが全て指定されている場合、:attributeも指定してください。', + 'required_without' => ':valuesが指定されていない場合、:attributeを指定してください。', + 'required_without_all' => ':valuesが全て指定されていない場合、:attributeを指定してください。', + 'same' => ':attributeと:otherが一致しません。', + 'size' => [ + 'numeric' => ':attributeには、:sizeを指定してください。', + 'file' => ':attributeには、:size KBのファイルを指定してください。', + 'string' => ':attributeは、:size文字にしてください。', + 'array' => ':attributeの項目は、:size個にしてください。', + ], + 'starts_with' => ':attributeは、次のいずれかで始まる必要があります。:values', + 'string' => ':attributeには、文字を指定してください。', + 'timezone' => ':attributeには、有効なタイムゾーンを指定してください。', + 'unique' => '指定の:attributeは既に使用されています。', + 'uploaded' => ':attributeのアップロードに失敗しました。', + 'url' => ':attributeは、有効なURL形式で指定してください。', + 'uuid' => ':attributeは、有効なUUIDでなければなりません。', + + /* + |-------------------------------------------------------------------------- + | Custom Validation Language Lines + |-------------------------------------------------------------------------- + | + | Here you may specify custom validation messages for attributes using the + | convention "attribute.rule" to name the lines. This makes it quick to + | specify a specific custom language line for a given attribute rule. + | + */ + + 'custom' => [ + 'attribute-name' => [ + 'rule-name' => 'custom-message', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Custom Validation Attributes + |-------------------------------------------------------------------------- + | + | The following language lines are used to swap our attribute placeholder + | with something more reader friendly such as "E-Mail Address" instead + | of "email". This simply helps us make our message more expressive. + | + */ + + 'attributes' => [], + + 'vue' => [ + 'max' => [ + 'numeric' => '{field} may not be greater than {max}.', + 'string' => '{field} may not be greater than {max} characters.', + ], + 'required' => '{field} is required.', + 'url' => '{field} is not a valid URL.', + ], + +]; diff --git a/resources/lang/nl.json b/resources/lang/nl.json new file mode 100644 index 0000000..e5cc4c4 --- /dev/null +++ b/resources/lang/nl.json @@ -0,0 +1,7 @@ +{ + "The :attribute must contain at least one uppercase and one lowercase letter.": "Het :attribute moet minimaal één hoofdletter en één kleine letter bevatten.", + "The :attribute must contain at least one letter.": "Het :attribute moet minimaal één letter bevatten.", + "The :attribute must contain at least one symbol.": "Het :attribute moet minimaal één symbool bevatten.", + "The :attribute must contain at least one number.": "Het :attribute moet minimaal één cijfer bevatten .", + "The given :attribute has appeared in a data leak. Please choose a different :attribute.": "Het :attribute is aangetroffen in een datalek. Geef een ander :attribute." +} diff --git a/resources/lang/nl/app.php b/resources/lang/nl/app.php new file mode 100644 index 0000000..9ce1990 --- /dev/null +++ b/resources/lang/nl/app.php @@ -0,0 +1,571 @@ + 'Ja', + 'no' => 'Nee', + 'update' => 'Bijwerken', + 'save' => 'Opslaan', + 'add' => 'Toevoegen', + 'cancel' => 'Annuleren', + 'confirm' => 'Bevestigen', + 'delete_confirm' => 'Zeker weten?', + 'delete' => 'Verwijderen', + 'edit' => 'Bewerken', + 'upload' => 'Uploaden', + 'download' => 'Download', + 'save_close' => 'Opslaan & sluiten', + 'close' => 'Sluiten', + 'copy' => 'Kopieer', + 'create' => 'Maak', + 'remove' => 'Verwijderen', + 'revoke' => 'Intrekken', + 'done' => 'Gereed', + 'back' => 'Terug', + 'verify' => 'Bevestigen', + 'new' => 'nieuw', + 'unknown' => 'Ik weet het niet', + 'load_more' => 'Meer laden', + 'loading' => 'Laden…', + 'with' => 'met', + 'today' => 'vandaag', + 'yesterday' => 'gisteren', + 'another_day' => 'een andere dag', + 'date' => 'Datum', + 'type' => 'Soort', + 'zoom' => 'Inzoomen', + 'upgrade' => 'Upgrade om te ontgrendelen', + 'percent_uploaded' => '{percent}% geüpload', + 'retry' => 'Opnieuw Proberen', + 'filter' => 'Filter de lijst', + 'go_back' => 'Terug', + 'file_selected' => 'Één bestand geselecteerd…|{count} bestanden geselecteerd…', + + 'application_title' => 'Monica – persoonlijke relatie manager', + 'application_description' => 'Monica is een app voor het beheren van interacties met uw geliefden, vrienden en familie.', + 'application_og_title' => 'Heb betere relaties met je geliefden. Gratis online CRM voor familie en vrienden.', + + 'markdown_description' => 'Wilt u uw tekst opmaken op een leuke manier? Wij ondersteunen Markdown om vet, cursief, lijsten en meer toe te voegen.', + 'markdown_link' => 'Lees documentatie', + + 'header_settings_link' => 'Instellingen', + 'header_logout_link' => 'Uitloggen', + 'header_changelog_link' => 'Productwijzigingen', + + 'main_nav_cta' => 'Personen toevoegen', + 'main_nav_dashboard' => 'Dashboard', + 'main_nav_family' => 'Contacten', + 'main_nav_journal' => 'Dagboek', + 'main_nav_activities' => 'Activiteiten', + 'main_nav_tasks' => 'Taken', + + 'footer_remarks' => 'Opmerkingen?', + 'footer_send_email' => 'Stuur ons een e-mail', + 'footer_privacy' => 'Privacybeleid', + 'footer_release' => 'Releaseopmerkingen', + 'footer_newsletter' => 'Nieuwsbrief', + 'footer_source_code' => 'Bijdragen', + 'footer_version' => 'Versie: :version', + 'footer_new_version' => 'Er is een nieuwe versie van Monica beschikbaar', + + 'footer_modal_version_whats_new' => 'Wat is er nieuw', + 'footer_modal_version_release_away' => 'Je loopt 1 versie achter op de laatst beschikbare versie. Je zou je applicatie moeten bijwerken.|Je loopt :number versies achter op de laatst beschikbare versie. Je zou je applicatie moeten bijwerken.', + + 'breadcrumb_dashboard' => 'Dashboard', + 'breadcrumb_list_contacts' => 'Lijst van mensen', + 'breadcrumb_archived_contacts' => 'Gearchiveerde contacten', + 'breadcrumb_journal' => 'Dagboek', + 'breadcrumb_settings' => 'Instellingen', + 'breadcrumb_settings_export' => 'Exporteren', + 'breadcrumb_settings_users' => 'Gebruikers', + 'breadcrumb_settings_users_add' => 'Gebruiker toevoegen', + 'breadcrumb_settings_subscriptions' => 'Abonnement', + 'breadcrumb_settings_import' => 'Importeren', + 'breadcrumb_settings_import_report' => 'Importrapport', + 'breadcrumb_settings_import_upload' => 'Uploaden', + 'breadcrumb_settings_tags' => 'Labels', + 'breadcrumb_add_significant_other' => 'Partner toevoegen', + 'breadcrumb_edit_significant_other' => 'Partner bewerken', + 'breadcrumb_add_note' => 'Notitie toevoegen', + 'breadcrumb_edit_note' => 'Notitie bewerken', + 'breadcrumb_api' => 'API', + 'breadcrumb_dav' => 'DAV-bronnen', + 'breadcrumb_edit_introductions' => 'Hoe hebben jullie elkaar ontmoet', + 'breadcrumb_settings_personalization' => 'Personalisatie', + 'breadcrumb_settings_security' => 'Beveiliging', + 'breadcrumb_settings_security_2fa' => 'Tweestapsverificatie', + 'breadcrumb_profile' => 'Profiel van :name', + + 'gender_male' => 'Man', + 'gender_female' => 'Vrouw', + 'gender_none' => 'Zeg ik liever niet', + 'gender_no_gender' => 'Geen geslacht', + + 'error_title' => 'Oeps! Er is iets misgegaan.', + 'error_unauthorized' => 'Je hebt niet de rechten om dit onderdeel te bewerken.', + 'error_user_account' => 'Deze gebruiker behoort niet tot het opgegeven account.', + 'error_save' => 'Er is een fout opgetreden bij het opslaan van de gegevens.', + 'error_try_again' => 'Er ging iets mis. Probeer opnieuw.', + 'error_id' => 'Fout-ID: :id', + 'error_unavailable' => 'Service niet beschikbaar', + 'error_maintenance' => 'Werkzaamheden zijn bezig. Een ogenblik graag.', + 'error_help' => 'We zijn zo terug.', + 'error_twitter' => 'Volg ons op Twitter als je gewaarschuwd wilt worden als we terug zijn.', + 'error_no_term' => 'Er zijn nog geen voorwaarden opgesteld voor deze server.', + + 'default_save_success' => 'De gegevens zijn opgeslagen.', + + 'compliance_title' => 'Sorry voor de onderbreking.', + 'compliance_desc' => 'We hebben onze gebruiksvoorwaarden en ons privacybeleid aangepast. We zijn verplicht u te vragen deze opnieuw te lezen en goed te keuren, om je account te kunnen blijven gebruiken.', + 'compliance_desc_end' => 'Wij doen niets vervelends met uw gegevens of account en zullen dit ook nooit doen.', + 'compliance_terms' => 'Accepteer de nieuwe voorwaarden en privacybeleid', + + // Relationship types + // Yes, each relationship type has 8 strings associated with it. + // This is because we need to indicate the name of the relationship type, + // and also the name of the opposite side of this relationship (father/son), + // and then, the feminine version of the string. Finally, in some sentences + // in the UI, we need to include the name of the person we add the relationship + // to. + 'relationship_type_group_love' => 'Liefdesrelaties', + 'relationship_type_group_family' => 'Familierelaties', + 'relationship_type_group_friend' => 'Vriendschappen', + 'relationship_type_group_work' => 'Collega’s', + 'relationship_type_group_other' => 'Andere relaties', + + 'relationship_type_partner' => 'partner', + 'relationship_type_partner_female' => 'partner', + 'relationship_type_partner_male' => 'partner', + 'relationship_type_partner_with_name' => ':name’s van partner', + 'relationship_type_partner_female_with_name' => ':name’s partner', + 'relationship_type_partner_male_with_name' => ':name’s partner', + + 'relationship_type_spouse' => 'echtgenoot', + 'relationship_type_spouse_female' => 'vrouw', + 'relationship_type_spouse_male' => 'echtgenoot', + 'relationship_type_spouse_with_name' => ':name’s van echtgeno(o)t(e)', + 'relationship_type_spouse_female_with_name' => ':name’s vrouw', + 'relationship_type_spouse_male_with_name' => 'array[\'relationship_type_spouse_male_with_name\']', + + 'relationship_type_date' => 'date', + 'relationship_type_date_female' => 'date', + 'relationship_type_date_male' => 'date', + 'relationship_type_date_with_name' => ':name’s date', + 'relationship_type_date_female_with_name' => ':name’s date', + 'relationship_type_date_male_with_name' => ':name’s date', + + 'relationship_type_lover' => 'geliefde', + 'relationship_type_lover_female' => 'geliefde', + 'relationship_type_lover_male' => 'geliefde', + 'relationship_type_lover_with_name' => ':name’s geliefde', + 'relationship_type_lover_female_with_name' => ':name’s geliefde', + 'relationship_type_lover_male_with_name' => ':name’s geliefde', + + 'relationship_type_inlovewith' => 'verliefd op', + 'relationship_type_inlovewith_female' => 'verliefd op', + 'relationship_type_inlovewith_male' => 'verliefd op', + 'relationship_type_inlovewith_with_name' => 'iemand :name is verliefd op', + 'relationship_type_inlovewith_female_with_name' => 'iemand :name is verliefd op', + 'relationship_type_inlovewith_male_with_name' => 'iemand :name is verliefd op', + + 'relationship_type_lovedby' => 'begeert door', + 'relationship_type_lovedby_female' => 'begeert door', + 'relationship_type_lovedby_male' => 'geliefd door', + 'relationship_type_lovedby_with_name' => ':name’s geheime minnaar', + 'relationship_type_lovedby_female_with_name' => ':name’s geheime minnaar', + 'relationship_type_lovedby_male_with_name' => ':name’s geheime minnaar', + + 'relationship_type_ex' => 'ex-partner', + 'relationship_type_ex_female' => 'ex-vriendin', + 'relationship_type_ex_male' => 'ex-vriend', + 'relationship_type_ex_with_name' => ':name’s ex-partner', + 'relationship_type_ex_female_with_name' => ':name’s ex-vriendinnetje', + 'relationship_type_ex_male_with_name' => ':name’s ex-vriend', + + 'relationship_type_parent' => 'ouder', + 'relationship_type_parent_female' => 'moeder', + 'relationship_type_parent_male' => 'vader', + 'relationship_type_parent_with_name' => ':name’s ouder', + 'relationship_type_parent_female_with_name' => ':name’s moeder', + 'relationship_type_parent_male_with_name' => ':name’s vader', + + 'relationship_type_child' => 'kind', + 'relationship_type_child_female' => 'dochter', + 'relationship_type_child_male' => 'zoon', + 'relationship_type_child_with_name' => ':name’s kind', + 'relationship_type_child_female_with_name' => ':name’s dochter', + 'relationship_type_child_male_with_name' => ':name’s zoon', + + 'relationship_type_stepparent' => 'stiefouder', + 'relationship_type_stepparent_female' => 'stiefmoeder', + 'relationship_type_stepparent_male' => 'stiefvader', + 'relationship_type_stepparent_with_name' => ':name’s stiefouder', + 'relationship_type_stepparent_female_with_name' => ':name’s stiefmoeder', + 'relationship_type_stepparent_male_with_name' => ':name’s stiefvader', + + 'relationship_type_stepchild' => 'stiefkind', + 'relationship_type_stepchild_female' => 'stiefdochter', + 'relationship_type_stepchild_male' => 'stiefzoon', + 'relationship_type_stepchild_with_name' => ':name’s stiefkind', + 'relationship_type_stepchild_female_with_name' => ':name’s stiefdochter', + 'relationship_type_stepchild_male_with_name' => ':name’s stiefzoon', + + 'relationship_type_sibling' => 'broer of zus', + 'relationship_type_sibling_female' => 'zus', + 'relationship_type_sibling_male' => 'broer', + 'relationship_type_sibling_with_name' => ':name’s broer of zus', + 'relationship_type_sibling_female_with_name' => ':name’s zus', + 'relationship_type_sibling_male_with_name' => ':name’s broer', + + 'relationship_type_grandparent' => 'grootouder', + 'relationship_type_grandparent_female' => 'oma', + 'relationship_type_grandparent_male' => 'opa', + 'relationship_type_grandparent_with_name' => ':name’s grootouder', + 'relationship_type_grandparent_female_with_name' => ':name’s oma', + 'relationship_type_grandparent_male_with_name' => ':name’s opa', + + 'relationship_type_grandchild' => 'kleinkind', + 'relationship_type_grandchild_female' => 'kleindochter', + 'relationship_type_grandchild_male' => 'kleinzoon', + 'relationship_type_grandchild_with_name' => ':name’s kleinkind', + 'relationship_type_grandchild_female_with_name' => ':name’s kleindochter', + 'relationship_type_grandchild_male_with_name' => ':name\'s kleinzoon', + + 'relationship_type_uncle' => 'oom', + 'relationship_type_uncle_female' => 'tante', + 'relationship_type_uncle_male' => 'oom', + 'relationship_type_uncle_with_name' => ':name’s oom', + 'relationship_type_uncle_female_with_name' => ':name’s tante', + 'relationship_type_uncle_male_with_name' => ':name’s oom', + + 'relationship_type_nephew' => 'neef', + 'relationship_type_nephew_female' => 'nicht', + 'relationship_type_nephew_male' => 'neef', + 'relationship_type_nephew_with_name' => ':name’s neef', + 'relationship_type_nephew_female_with_name' => ':name’s nicht', + 'relationship_type_nephew_male_with_name' => ':name’s neef', + + 'relationship_type_cousin' => 'neef', + 'relationship_type_cousin_female' => 'nicht', + 'relationship_type_cousin_male' => 'neef', + 'relationship_type_cousin_with_name' => ':name’s neef', + 'relationship_type_cousin_female_with_name' => ':name’s nicht', + 'relationship_type_cousin_male_with_name' => ':name’s neef', + + 'relationship_type_godfather' => 'peetouder', + 'relationship_type_godfather_female' => 'peet moeder', + 'relationship_type_godfather_male' => 'peetoom', + 'relationship_type_godfather_with_name' => ':name’s peetoom', + 'relationship_type_godfather_female_with_name' => ':name’s peetmoeder', + 'relationship_type_godfather_male_with_name' => ':name\'s peetoom', + + 'relationship_type_godson' => 'petekind', + 'relationship_type_godson_female' => 'peetdochter', + 'relationship_type_godson_male' => 'peetzoon', + 'relationship_type_godson_with_name' => ':name’s petekind', + 'relationship_type_godson_female_with_name' => ':name’s schoondochter', + 'relationship_type_godson_male_with_name' => ':name’s peetzoon', + + 'relationship_type_friend' => 'vriend', + 'relationship_type_friend_female' => 'vriend', + 'relationship_type_friend_male' => 'vriend', + 'relationship_type_friend_with_name' => ':name’s vriend', + 'relationship_type_friend_female_with_name' => ':name’s vriend', + 'relationship_type_friend_male_with_name' => ':name’s vriend', + + 'relationship_type_bestfriend' => 'beste vriend', + 'relationship_type_bestfriend_female' => 'beste vriend', + 'relationship_type_bestfriend_male' => 'beste vriend', + 'relationship_type_bestfriend_with_name' => ':name’s beste vriend', + 'relationship_type_bestfriend_female_with_name' => ':name’s beste vriend', + 'relationship_type_bestfriend_male_with_name' => ':name’s beste vriend', + + 'relationship_type_colleague' => 'collega', + 'relationship_type_colleague_female' => 'collega', + 'relationship_type_colleague_male' => 'collega', + 'relationship_type_colleague_with_name' => ':name’s collega', + 'relationship_type_colleague_female_with_name' => ':name’s collega', + 'relationship_type_colleague_male_with_name' => ':name’s collega', + + 'relationship_type_boss' => 'baas', + 'relationship_type_boss_female' => 'baas', + 'relationship_type_boss_male' => 'baas', + 'relationship_type_boss_with_name' => ':name’s baas', + 'relationship_type_boss_female_with_name' => ':name’s baas', + 'relationship_type_boss_male_with_name' => ':name’s baas', + + 'relationship_type_subordinate' => 'ondergeschikte', + 'relationship_type_subordinate_female' => 'ondergeschikte', + 'relationship_type_subordinate_male' => 'ondergeschikte', + 'relationship_type_subordinate_with_name' => ':name’s ondergeschikte', + 'relationship_type_subordinate_female_with_name' => ':name’s ondergeschikte', + 'relationship_type_subordinate_male_with_name' => ':name’s ondergeschikte', + + 'relationship_type_mentor' => 'mentor', + 'relationship_type_mentor_female' => 'mentor', + 'relationship_type_mentor_male' => 'mentor', + 'relationship_type_mentor_with_name' => ':name’s mentor', + 'relationship_type_mentor_female_with_name' => ':name’s mentor', + 'relationship_type_mentor_male_with_name' => ':name’s mentor', + + 'relationship_type_protege' => 'pupil', + 'relationship_type_protege_female' => 'pupil', + 'relationship_type_protege_male' => 'pupil', + 'relationship_type_protege_with_name' => ':name\'s pupil', + 'relationship_type_protege_female_with_name' => ':name\'s pupil', + 'relationship_type_protege_male_with_name' => ':name\'s pupil', + + 'relationship_type_ex_husband' => 'ex-man', + 'relationship_type_ex_husband_female' => 'ex-vrouw', + 'relationship_type_ex_husband_male' => 'ex-man', + 'relationship_type_ex_husband_with_name' => ':naam ex-echtgenoot', + 'relationship_type_ex_husband_female_with_name' => ':name’s ex-vrouw', + 'relationship_type_ex_husband_male_with_name' => ':naam ex-man', + + // emotions + 'emotion_primary_love' => 'Liefde', + 'emotion_primary_joy' => 'Blijdschap', + 'emotion_primary_surprise' => 'Verrast', + 'emotion_primary_anger' => 'Boos', + 'emotion_primary_sadness' => 'Verdriet', + 'emotion_primary_fear' => 'Angst', + + 'emotion_secondary_affection' => 'Genegenheid', + 'emotion_secondary_lust' => 'Lust', + 'emotion_secondary_longing' => 'Verlangen', + 'emotion_secondary_cheerfulness' => 'Opgewekt', + 'emotion_secondary_zest' => 'Vol vuur', + 'emotion_secondary_contentment' => 'Tevreden', + 'emotion_secondary_pride' => 'Trots', + 'emotion_secondary_optimism' => 'Optimisme', + 'emotion_secondary_enthrallment' => 'Betoverend', + 'emotion_secondary_relief' => 'Opgelucht', + 'emotion_secondary_surprise' => 'Verrast', + 'emotion_secondary_irritation' => 'Irritatie', + 'emotion_secondary_exasperation' => 'Wrevel', + 'emotion_secondary_rage' => 'Woedend', + 'emotion_secondary_disgust' => 'Afschuw', + 'emotion_secondary_envy' => 'Afgunst', + 'emotion_secondary_suffering' => 'Pijn', + 'emotion_secondary_sadness' => 'Verdriet', + 'emotion_secondary_disappointment' => 'Teleurstelling', + 'emotion_secondary_shame' => 'Schaamte', + 'emotion_secondary_neglect' => 'Verwaarloosd', + 'emotion_secondary_sympathy' => 'Meelevend', + 'emotion_secondary_horror' => 'Afgrijzen', + 'emotion_secondary_nervousness' => 'Zenuwachtig', + + 'emotion_adoration' => 'Aanbidding', + 'emotion_affection' => 'Genegenheid', + 'emotion_love' => 'Liefde', + 'emotion_fondness' => 'Warmte', + 'emotion_liking' => 'Voorliefde', + 'emotion_attraction' => 'Aantrekking', + 'emotion_caring' => 'Zorgzaamheid', + 'emotion_tenderness' => 'Tederheid', + 'emotion_compassion' => 'Compassie', + 'emotion_sentimentality' => 'Sentimenteel', + 'emotion_arousal' => 'Opwinding', + 'emotion_desire' => 'Verlangen', + 'emotion_lust' => 'Lust', + 'emotion_passion' => 'Passie', + 'emotion_infatuation' => 'Bevlieging', + 'emotion_longing' => 'Verlangen', + 'emotion_amusement' => 'Plezier', + 'emotion_bliss' => 'Gelukzaligheid', + 'emotion_cheerfulness' => 'Opgewekt', + 'emotion_gaiety' => 'Pret', + 'emotion_glee' => 'Vrolijk', + 'emotion_jolliness' => 'Jolig', + 'emotion_joviality' => 'Joviaal', + 'emotion_joy' => 'Vreugde', + 'emotion_delight' => 'Verrukking', + 'emotion_enjoyment' => 'Genot', + 'emotion_gladness' => 'Verheugd', + 'emotion_happiness' => 'Blijdschap', + 'emotion_jubilation' => 'Vervoering', + 'emotion_elation' => 'Opgetogen', + 'emotion_satisfaction' => 'Genoegen', + 'emotion_ecstasy' => 'Vervoering', + 'emotion_euphoria' => 'Euforie', + 'emotion_enthusiasm' => 'Enthousiasme', + 'emotion_zeal' => 'Geestdrift', + 'emotion_zest' => 'Vol vuur', + 'emotion_excitement' => 'Opwinding', + 'emotion_thrill' => 'Sensatie', + 'emotion_exhilaration' => 'Opbeuring', + 'emotion_contentment' => 'Voldoening', + 'emotion_pleasure' => 'Genoegen', + 'emotion_pride' => 'Trots', + 'emotion_eagerness' => 'Gretig', + 'emotion_hope' => 'Hoopvol', + 'emotion_optimism' => 'Optimistisch', + 'emotion_enthrallment' => 'Betoverend', + 'emotion_rapture' => 'Extase', + 'emotion_relief' => 'Opgelucht', + 'emotion_amazement' => 'Verbazing', + 'emotion_surprise' => 'Verrast', + 'emotion_astonishment' => 'Verwonderd', + 'emotion_aggravation' => 'Vervelend', + 'emotion_irritation' => 'Irritatie', + 'emotion_agitation' => 'Geërgerd', + 'emotion_annoyance' => 'Ergernis', + 'emotion_grouchiness' => 'Humeurig', + 'emotion_grumpiness' => 'Mopperend', + 'emotion_exasperation' => 'Wrevel', + 'emotion_frustration' => 'Frustratie', + 'emotion_anger' => 'Boosheid', + 'emotion_rage' => 'Woede', + 'emotion_outrage' => 'Verbolgenheid', + 'emotion_fury' => 'Razend', + 'emotion_wrath' => 'Toorn', + 'emotion_hostility' => 'Vijandig', + 'emotion_ferocity' => 'Wreed', + 'emotion_bitterness' => 'Bitter', + 'emotion_hate' => 'Haat', + 'emotion_loathing' => 'Afkeer', + 'emotion_scorn' => 'Verachting', + 'emotion_spite' => 'Wrok', + 'emotion_vengefulness' => 'Wraakzuchtig', + 'emotion_dislike' => 'Aversie', + 'emotion_resentment' => 'Verontwaardiging', + 'emotion_disgust' => 'Afschuw', + 'emotion_revulsion' => 'Walging', + 'emotion_contempt' => 'Minachting', + 'emotion_envy' => 'Afgunst', + 'emotion_jealousy' => 'Jaloezie', + 'emotion_agony' => 'Pijn', + 'emotion_suffering' => 'Lijden', + 'emotion_hurt' => 'Kwelling', + 'emotion_anguish' => 'Leed', + 'emotion_depression' => 'Depressief', + 'emotion_despair' => 'Wanhoop', + 'emotion_hopelessness' => 'Hopeloos', + 'emotion_gloom' => 'Zwaarmoedig', + 'emotion_glumness' => 'Mistroostig', + 'emotion_sadness' => 'Verdrietig', + 'emotion_unhappiness' => 'Ongelukkig', + 'emotion_grief' => 'Droevig', + 'emotion_sorrow' => 'Rouw', + 'emotion_woe' => 'Smart', + 'emotion_misery' => 'Ellendig', + 'emotion_melancholy' => 'Neerslachtig', + 'emotion_dismay' => 'Verbijstering', + 'emotion_disappointment' => 'Teleurstellend', + 'emotion_displeasure' => 'Ongenoegen', + 'emotion_guilt' => 'Schuldig', + 'emotion_shame' => 'Schaamte', + 'emotion_regret' => 'Spijt', + 'emotion_remorse' => 'Wroeging', + 'emotion_alienation' => 'Vervreemding', + 'emotion_isolation' => 'Afzondering', + 'emotion_neglect' => 'Verwaarlozing', + 'emotion_loneliness' => 'Eenzaam', + 'emotion_rejection' => 'Verworpen', + 'emotion_homesickness' => 'Heimwee', + 'emotion_defeat' => 'Verslagen', + 'emotion_dejection' => 'Mismoedig', + 'emotion_insecurity' => 'Onzeker', + 'emotion_embarrassment' => 'Verlegenheid', + 'emotion_humiliation' => 'Vernederd', + 'emotion_insult' => 'Beledigd', + 'emotion_pity' => 'Medelijden', + 'emotion_sympathy' => 'Meelevend', + 'emotion_alarm' => 'Geschrokken', + 'emotion_shock' => 'Geschokt', + 'emotion_fear' => 'Angst', + 'emotion_fright' => 'Vrees', + 'emotion_horror' => 'Afgrijzen', + 'emotion_terror' => 'Verschrikking', + 'emotion_panic' => 'Paniek', + 'emotion_hysteria' => 'Hysterie', + 'emotion_mortification' => 'Gekrenkt', + 'emotion_anxiety' => 'Berzorgd', + 'emotion_nervousness' => 'Nerveus', + 'emotion_tenseness' => 'Gespannen', + 'emotion_uneasiness' => 'Onbehaagelijk', + 'emotion_apprehension' => 'Vrees', + 'emotion_worry' => 'Bezorgdheid', + 'emotion_distress' => 'Ontsteltenis', + 'emotion_dread' => 'Doodsangst', + + // weather + 'weather_sunny' => 'Zonnig', + 'weather_clear' => 'Helder', + 'weather_clear-day' => 'Helder', + 'weather_clear-night' => 'Heldere nacht', + 'weather_light-drizzle' => 'Lichte motregen', + 'weather_patchy-light-drizzle' => 'Af en toe motregen', + 'weather_patchy-light-rain' => 'Af en toe regenval', + 'weather_light-rain' => 'Lichte regen', + 'weather_moderate-rain-at-times' => 'Nu en dan matige regenval', + 'weather_moderate-rain' => 'Matige regenval', + 'weather_patchy-rain-possible' => 'Plaatselijk regen mogelijk', + 'weather_heavy-rain-at-times' => 'Nu en dan hevige regen', + 'weather_heavy-rain' => 'Heavy rain', + 'weather_light-freezing-rain' => 'Light freezing rain', + 'weather_moderate-or-heavy-freezing-rain' => 'Moderate or heavy freezing rain', + 'weather_light-sleet' => 'Light sleet', + 'weather_moderate-or-heavy-rain-shower' => 'Moderate or heavy rain shower', + 'weather_light-rain-shower' => 'Light rain shower', + 'weather_torrential-rain-shower' => 'Torrential rain shower', + 'weather_rain' => 'Regen', + 'weather_snow' => 'Sneeuw', + 'weather_blowing-snow' => 'Blowing snow', + 'weather_patchy-light-snow' => 'Patchy light snow', + 'weather_light-snow' => 'Light snow', + 'weather_patchy-moderate-snow' => 'Patchy moderate snow', + 'weather_moderate-snow' => 'Moderate snow', + 'weather_patchy-heavy-snow' => 'Patchy heavy snow', + 'weather_heavy-snow' => 'Heavy snow', + 'weather_light-snow-showers' => 'Light snow showers', + 'weather_moderate-or-heavy-snow-showers' => 'Moderate or heavy snow showers', + 'weather_patchy-snow-possible' => 'Patchy snow possible', + 'weather_patchy-sleet-possible' => 'Patchy sleet possible', + 'weather_moderate-or-heavy-sleet' => 'Moderate or heavy sleet', + 'weather_light-sleet-showers' => 'Light sleet showers', + 'weather_moderate-or-heavy-sleet-showers' => 'Moderate or heavy sleet showers', + 'weather_sleet' => 'Natte sneeuw', + 'weather_wind' => 'Wind', + 'weather_fog' => 'Mist', + 'weather_freezing-fog' => 'Freezing fog', + 'weather_mist' => 'Mist', + 'weather_blizzard' => 'Blizzard', + 'weather_overcast' => 'Overcast', + 'weather_cloudy' => 'Bewolkt', + 'weather_partly-cloudy-day' => 'Partly cloudy', + 'weather_partly-cloudy-night' => 'Partly cloudy', + 'weather_freezing-drizzle' => 'Freezing drizzle', + 'weather_heavy-freezing-drizzle' => 'Heavy freezing drizzle', + 'weather_patchy-freezing-drizzle-possible' => 'Patchy freezing drizzle possible', + 'weather_ice-pellets' => 'Ice pellets', + 'weather_light-showers-of-ice-pellets' => 'Light showers of ice pellets', + 'weather_moderate-or-heavy-showers-of-ice-pellets' => 'Moderate or heavy showers of ice pellets', + 'weather_thundery-outbreaks-possible' => 'Thundery outbreaks possible', + 'weather_patchy-light-rain-with-thunder' => 'Patchy light rain with thunder', + 'weather_moderate-or-heavy-rain-with-thunder' => 'Moderate or heavy rain with thunder', + 'weather_patchy-light-snow-with-thunder' => 'Patchy light snow with thunder', + 'weather_moderate-or-heavy-snow-with-thunder' => 'Moderate or heavy snow with thunder', + 'weather_current_temperature_celsius' => ':temperature °C', + 'weather_current_temperature_fahrenheit' => ':temperature °F', + 'weather_current_title' => 'Huidig weer', + + // dav + 'dav_contacts' => 'Contacten', + 'dav_contacts_description' => 'Contacten van :name', + 'dav_birthdays' => 'Verjaardagen', + 'dav_birthdays_description' => 'Verjaardagen van de contacten van :name', + 'dav_tasks' => 'Taken', + 'dav_tasks_description' => ':name’s taken', + + // contact list + 'contact_list_avatar' => 'Avatar', + 'contact_list_name' => 'Contact', + 'contact_list_description' => 'Beschrijving', + +]; diff --git a/resources/lang/nl/auth.php b/resources/lang/nl/auth.php new file mode 100644 index 0000000..6525565 --- /dev/null +++ b/resources/lang/nl/auth.php @@ -0,0 +1,89 @@ + 'Deze gegevens zijn niet correct.', + 'throttle' => 'Te veel inlogpogingen. Probeer opnieuw in :seconds seconden.', + 'not_authorized' => 'Je bent niet gemachtigd om dit te doen', + 'signup_disabled' => 'Registratie is momenteel uitgeschakeld', + 'signup_error' => 'Er is een fout opgetreden bij het registreren van de gebruiker', + 'back_homepage' => 'Terug naar homepage', + 'mfa_auth_otp' => 'Verifieer met je tweestapsverificatie apparaat', + 'mfa_auth_webauthn' => 'Authenticeer met een beveiligingssleutel (WebAuthn)', + '2fa_title' => 'Tweestapsverificatie', + '2fa_wrong_validation' => 'De tweestapsverificatie is mislukt.', + '2fa_one_time_password' => 'Tweestapsverificatiecode', + '2fa_recuperation_code' => 'Voer een tweestapsverificatiecode in', + '2fa_one_time_or_recuperation' => 'Voer een tweestapsverificatiecode of een herstelcode in', + '2fa_otp_help' => 'Open je tweestapsverificatiecode-app en kopieer de code', + + 'login_to_account' => 'Inloggen op je account', + 'login_with_recovery' => 'Inloggen met een herstelcode', + 'login_again' => 'Gelieve nogmaals in te loggen op je account', + 'email' => 'E-mail', + 'password' => 'Wachtwoord', + 'recovery' => 'Herstelcode', + 'login' => 'Aanmelden', + 'button_remember' => 'Onthoud Mij', + 'password_forget' => 'Wachtwoord vergeten?', + 'password_reset' => 'Wachtwoord resetten', + 'use_recovery' => 'Of je kan een herstelcode gebruiken', + 'signup_no_account' => 'Heb je nog geen account?', + 'signup' => 'Registreren', + 'create_account' => 'Maak het eerste account aan door je te registreren', + 'change_language_title' => 'Verander taal:', + 'change_language' => 'Verander taal naar :lang', + + 'password_reset_title' => 'Wachtwoord resetten', + 'password_reset_email' => 'E-mailadres', + 'password_reset_send_link' => 'Stuur een wachtwoord reset link', + 'password_reset_password' => 'Wachtwoord', + 'password_reset_password_confirm' => 'Bevestig wachtwoord', + 'password_reset_action' => 'Wachtwoord resetten', + 'password_reset_email_content' => 'Klik hier om je wachtwoord te resetten:', + + 'register_title_welcome' => 'Welkom bij je nieuwe Monica-installatie', + 'register_create_account' => 'Je moet een account aanmaken om Monica te kunnen gebruiken', + 'register_title_create' => 'Maak jouw Monica account aan', + 'register_login' => 'Inloggen als je al een account hebt.', + 'register_email' => 'Voor een geldig e-mailadres in', + 'register_email_example' => 'jij@jouwdomein', + 'register_firstname' => 'Voornaam', + 'register_firstname_example' => 'bv. Simone', + 'register_lastname' => 'Achternaam', + 'register_lastname_example' => 'bv. Schutterman', + 'register_password' => 'Wachtwoord', + 'register_password_example' => 'Voer een veilig wachtwoord in', + 'register_password_confirmation' => 'Wachtwoordbevestiging', + 'register_action' => 'Registreren', + 'register_policy' => 'Door te registreren bevestig je dat je ons Privacybeleid en onze Algemene Voorwaarden hebt gelezen en daarmee akkoord bent.', + 'register_invitation_email' => 'Wegens beveiligingsdoeleinden vragen wij je om het e-mailadres op te geven van de persoon die je heeft uitgenodigd voor dit account. Deze informatie staat in de uitnodigingse-mail.', + + 'confirmation_title' => 'Verifieer je e-mailadres', + 'confirmation_fresh' => 'Een nieuwe verificatie e-mail is verstuurd naar jouw e-mailadres.', + 'confirmation_check' => 'Voordat je verdergaat, controleer alsjeblieft je e-mail voor een verificatie e-mail.', + 'confirmation_request_another' => 'Heb je de e-mail niet ontvangen? Klik hier om er nog een te sturen.', + + 'confirmation_again' => 'Als je jouw e-mailadres wilt wijzigen kun je hier klikken.', + 'email_change_current_email' => 'Huidige e-mailadres:', + 'email_change_title' => 'E-mailadres wijzigen', + 'email_change_new' => 'Nieuw e-mailadres', + 'email_changed' => 'Je e-mailadres is gewijzigd. Kijk in je inbox om het te bevestigen.', +]; diff --git a/resources/lang/nl/changelog.php b/resources/lang/nl/changelog.php new file mode 100644 index 0000000..4e44522 --- /dev/null +++ b/resources/lang/nl/changelog.php @@ -0,0 +1,12 @@ + 'Productwijzigingen', + 'note' => 'Opmerking: Helaas, deze pagina is alleen beschikbaar in het Engels.', +]; diff --git a/resources/lang/nl/dashboard.php b/resources/lang/nl/dashboard.php new file mode 100644 index 0000000..d8456e5 --- /dev/null +++ b/resources/lang/nl/dashboard.php @@ -0,0 +1,42 @@ + 'Welkom bij jouw account!', + 'dashboard_blank_description' => 'Monica is de plaats om alle interacties met de mensen waar je om geeft te organiseren.', + 'dashboard_blank_cta' => 'Voeg je eerste contact toe', + 'dashboard_blank_illustration' => 'Illustratie door Freepik', + + 'notes_title' => 'Je hebt nog geen notities met een ster.', + + 'tab_recent_calls' => 'Recente oproepen', + 'tab_favorite_notes' => 'Favoriete notities', + 'tab_calls_blank' => 'Je hebt nog geen oproepen opgeslagen.', + 'tab_debts' => 'Schulden', + 'tab_debts_blank' => 'Je hebt nog geen schulden opgegeven.', + 'tab_tasks' => 'Taken', + 'tab_tasks_blank' => 'Je hebt nog geen taken.', + + 'tasks_add_task_placeholder' => 'Waar gaat deze taak over?', + 'tasks_tab_your_contacts' => 'Taken met betrekking tot je contacten', + 'tasks_tab_your_tasks' => 'Jouw taken', + 'tasks_add_note' => 'Druk op Enter om de taak toe te voegen.', + 'task_add_cta' => 'Taak toevoegen', + + 'debts_you_owe' => 'U bent verschuldigd', + + 'statistics_contacts' => 'Contacten', + 'statistics_activities' => 'Activiteiten', + 'statistics_gifts' => 'Cadeaus', + + 'reminders_next_months' => 'Gebeurtenissen de komende 3 maanden', + 'reminders_none' => 'Geen herinnering voor deze maand.', + + 'product_changes' => 'Productwijzigingen', + 'product_view_details' => 'Details weergeven', +]; diff --git a/resources/lang/nl/format.php b/resources/lang/nl/format.php new file mode 100644 index 0000000..8e60df5 --- /dev/null +++ b/resources/lang/nl/format.php @@ -0,0 +1,36 @@ + 'd M Y H:i', + 'short_date_year' => 'd M Y', + 'short_date' => 'd M', + 'short_month' => 'M', + 'short_month_year' => 'M Y', + 'short_day' => 'D', + 'full_date_year' => 'd M Y', + 'full_month' => 'F', + 'full_month_year' => 'F Y', + 'full_hour' => 'H:i', + + 'short_text' => '{text}…', +]; diff --git a/resources/lang/nl/journal.php b/resources/lang/nl/journal.php new file mode 100644 index 0000000..804b4cd --- /dev/null +++ b/resources/lang/nl/journal.php @@ -0,0 +1,38 @@ + 'Hoe was je dag? Je kunt hem eens per dag beoordelen.', + 'journal_come_back' => 'Bedankt. Kom morgen terug om je dag opnieuw te beoordelen.', + 'journal_description' => 'Opmerking: het dagboek toont zowel handmatige invoeren als automatische berichten, zoals Activiteiten die je gedaan hebt met je contacten. Je kunt dagboek-invoeren handmatig wissen, maar Activiteiten kunnen alleen op de contact pagina verwijderd worden.', + 'journal_add' => 'Voeg een dagboek-invoer toe', + 'journal_edit' => 'Bewerk dagboek-invoer', + 'journal_empty' => 'Leeg dagboek', + 'journal_created_at' => 'Aangemaakt op {date}', + 'journal_created_automatically' => 'Automatisch aangemaakt', + 'journal_entry_type_journal' => 'Dagboek-invoer', + 'journal_entry_type_activity' => 'Activiteit', + 'journal_entry_rate' => 'Je hebt je dag beoordeeld.', + 'journal_add_comment' => 'Wil je een (optionele) opmerking toevoegen?', + 'journal_show_comment' => 'Toon opmerking', + 'entry_delete_success' => 'De dagboek-invoer is succesvol verwijderd.', + 'journal_add_title' => 'Titel (optioneel)', + 'journal_add_date' => 'Datum', + 'journal_add_post' => 'Invoer', + 'journal_add_cta' => 'Opslaan', + 'journal_blank_cta' => 'Voeg je eerst dagboek-invoer toe', + 'journal_blank_description' => 'Het dagboek laat je gebeurtenissen registreren, zodat je ze kunt onthouden.', + 'delete_confirmation' => 'Weet je zeker dat je deze dagboek-invoer wilt verwijderen?', + 'apply_filter' => 'Apply filter', + 'start_date' => 'Start Date', + 'end_date' => 'End Date', + 'per_page' => 'Per Page', + 'sort_order' => 'Sort By Created At', + 'ascending' => 'Ascending', + 'descending' => 'Descending', +]; diff --git a/resources/lang/nl/logs.php b/resources/lang/nl/logs.php new file mode 100644 index 0000000..bf3b9ed --- /dev/null +++ b/resources/lang/nl/logs.php @@ -0,0 +1,29 @@ + 'Contact is aangemaakt.', + 'settings_log_contact_created_with_name' => ':name is toegevoegd als contact.', + + // contat description update + 'contact_log_contact_description_updated' => 'Beschrijving geüpdatet.', + 'settings_log_contact_description_updated_with_name' => 'Beschrijving van :name geüpdatet.', + + // contact description clear + 'contact_log_contact_description_cleared' => 'De beschrijving is gewist.', + 'settings_log_contact_description_cleared_with_name' => 'De beschrijving van :name is gewist.', + + // contact work information update + 'contact_log_contact_work_updated' => 'Werkinformatie bijgewerkt.', + 'settings_log_contact_work_updated_with_name' => 'Werkinformatie van :name geüpdatet.', + + // company created + 'settings_log_company_created' => 'Bedrijf genaamd :name aangemaakt.', +]; diff --git a/resources/lang/nl/mail.php b/resources/lang/nl/mail.php new file mode 100644 index 0000000..c06b8c8 --- /dev/null +++ b/resources/lang/nl/mail.php @@ -0,0 +1,53 @@ + 'Herinnering voor :contact', + 'greetings' => 'Hi :username', + 'want_reminded_of' => 'Je wilde herinnerd worden aan :reason', + 'for' => 'Voor: :name', + 'comment' => 'Opmerking: :comment', + 'footer_contact_info' => 'Toevoegen, bekijken, afmaken en aanpassen van information over dit contact:', + 'footer_contact_info2' => 'Bekijk :name\'s profiel', + 'footer_contact_info2_link' => 'Zie :name\'s profiel: :url', + + 'notification_subject_line' => 'Je hebt een aankomende gebeurtenis', + 'notification_description' => 'Over :count dagen (op :date), zal de volgende gebeurtenis plaatsvinden:', + + 'stay_in_touch_subject_line' => 'Blijf in contact met :name', + 'stay_in_touch_subject_description' => 'Je hebt gevraagd om in contact te blijven met :name elke :frequency dag.| Je hebt gevraagd om in contact te blijven met :name elke :frequency dagen.', + + 'notifications_whoops' => 'Oeps!', + 'notifications_hello' => 'Hallo!', + 'notifications_regards' => 'Met vriendelijke groet', + 'notifications_footer' => 'Kopieer en plak de volgende URL in je browser als de ":actionText" knop niet werkt: [:actionURL](:actionURL)', + 'notifications_rights' => 'Alle rechten voorbehouden', + + 'confirmation_email_title' => 'Monica – E-mailverificatie', + 'confirmation_email_intro'=> 'Klik op de knop hieronder om je e-mailadres te valideren', + 'confirmation_email_button' => 'Verifieer het e-mailadres', + 'confirmation_email_bottom' => 'Als je geen account hebt gemaakt, kun je deze email negeren.', + + 'password_reset_title' => 'Monica – Wachtwoord Herstellen', + 'password_reset_intro' => 'Je ontvangt deze e-mail omdat we een verzoek hebben ontvangen om het wachtwoord van je account te herstellen.', + 'password_reset_button' => 'Herstel wachtwoord', + 'password_reset_expiration' => 'Deze link verloopt over :count minuten.', + 'password_reset_bottom' => 'Als je ons niet hebt verzocht om je wachtwoord te herstellen, kun je deze e-mail negeren.', + + 'invitation_title' => 'Monica – Je bent uitgenodigd door :name', + 'invitation_intro' => 'Je bent door :name (:email) uitgenodigd voor Monica, een handige Personal Relationship Management webapp.', + 'invitation_link' => 'Klik op de onderstaande link om de uitnodiging te accepteren:', + 'invitation_button' => 'Uitnodiging accepteren', + 'invitation_expiration' => 'Deze link verloopt over :count dagen.', + + 'export_title' => 'Your export is ready', + 'export_description' => 'You requested a data export on :date. It is now ready to download.', + 'export_download' => 'Download export', + +]; diff --git a/resources/lang/nl/pagination.php b/resources/lang/nl/pagination.php new file mode 100644 index 0000000..06526ee --- /dev/null +++ b/resources/lang/nl/pagination.php @@ -0,0 +1,25 @@ + '❮ Vorige', + 'next' => 'Volgende ❯', + +]; diff --git a/resources/lang/nl/passwords.php b/resources/lang/nl/passwords.php new file mode 100644 index 0000000..e67be65 --- /dev/null +++ b/resources/lang/nl/passwords.php @@ -0,0 +1,30 @@ + 'Je wachtwoord is gereset!', + 'sent' => 'We hebben een e-mail verstuurd met instructies om een nieuw wachtwoord in te stellen.', + 'token' => 'Deze wachtwoord reset token is ongeldig.', + 'user' => 'Geen gebruiker bekend met het e-mailadres.', + 'changed' => 'Wachtwoord succesvol gewijzigd.', + 'invalid' => 'Het ingevoerde wachtwoord is niet correct.', + 'throttled' => 'Wacht alsjeblieft even voor je het opnieuw probeert.', + +]; diff --git a/resources/lang/nl/people.php b/resources/lang/nl/people.php new file mode 100644 index 0000000..9ecfb8a --- /dev/null +++ b/resources/lang/nl/people.php @@ -0,0 +1,539 @@ + 'Contactpersoon niet gevonden', + 'people_list_number_kids' => ':count kind|:count kinderen', + 'people_list_last_updated' => 'Laatst bekeken:', + 'people_list_number_reminders' => ':count herinnering|:count herinneringen', + 'people_list_blank_title' => 'Je hebt nog niemand in je account', + 'people_list_blank_cta' => 'Voeg iemand toe', + 'people_list_sort' => 'Sorteer', + 'people_list_stats' => ':count contact|:count contacten', + 'people_list_firstnameAZ' => 'Sorteer op voornaam A → Z', + 'people_list_firstnameZA' => 'Sorteer op voornaam Z → A', + 'people_list_lastnameAZ' => 'Sorteer op achternaam A → Z', + 'people_list_lastnameZA' => 'Sorteer op achternaam Z → A', + 'people_list_lastactivitydateNewtoOld' => 'Sorteer activiteit op datum, nieuw naar oud', + 'people_list_lastactivitydateOldtoNew' => 'Sorteren op laatste activiteit datum, oudste naar nieuwste', + 'people_list_filter_tag' => 'Tonen alle contactpersonen gelabeld met', + 'people_list_clear_filter' => 'Filter wissen', + 'people_list_contacts_per_tags' => ':count contact|:count contacten', + 'people_list_show_dead' => 'Toon overleden personen (:count)', + 'people_list_hide_dead' => 'Verberg overleden personen (:count)', + 'people_search' => 'Zoek in je contacten…', + 'people_search_no_results' => 'Geen resultaten gevonden', + 'people_search_next' => 'Volgende', + 'people_search_prev' => 'Vorige', + 'people_search_rows_per_page' => 'Rijen per pagina', + 'people_search_of' => 'van', + 'people_search_page' => 'Pagina', + 'people_search_all' => 'Iedereen', + 'people_add_new' => 'Voeg nieuw persoon toe', + 'people_list_account_usage' => 'Je huidige gebruik: :current/:limit contacten', + 'people_list_account_upgrade_title' => 'Upgrade je account om alle functies te kunnen gebruiken.', + 'people_list_account_upgrade_cta' => 'Nu upgraden', + 'people_list_untagged' => 'Bekijken contacten zonder labels', + 'people_list_filter_untag' => 'All contacten zonder labels', + 'archived_contact_readonly' => 'Gearchiveerd contactpersoon kan niet worden bewerkt, verwijder deze eerst.', + + // people add + 'people_add_title' => 'Voeg een nieuwe persoon toe', + 'people_add_missing' => 'Geen persoon gevonden – voeg nu een nieuwe toe', + 'people_add_firstname' => 'Voornaam', + 'people_add_middlename' => 'Tussenvoegsel (optioneel)', + 'people_add_lastname' => 'Achternaam (optioneel)', + 'people_add_email' => 'E-mailadres (optioneel)', + 'people_add_nickname' => 'Alias (optioneel)', + 'people_add_cta' => 'Toevoegen', + 'people_save_and_add_another_cta' => 'Opslaan en nog iemand toevoegen', + 'people_add_success' => ':name is succesvol toegevoegd', + 'people_add_gender' => 'Geslacht', + 'people_delete_success' => 'De contactpersoon is verwijderd', + 'people_delete_message' => 'Contact verwijderen', + 'people_delete_confirmation' => 'Weet je zeker dat je :name\'s contactgegevens wilt verwijderen? Verwijderen is onmiddellijk en permanent.', + 'people_add_birthday_reminder' => 'Feliciteer :name met zijn/haar verjaardag', + 'people_add_birthday_reminder_deceased' => 'Op deze datum zou :name zijn verjaardag hebben gevierd', + 'people_add_import' => 'Wil je contacten importeren?', + 'people_edit_email_error' => 'Er is al een contactpersoon in jouw account met dit e-mailadres. Kies alsjeblieft een ander.', + 'people_export' => 'Exporteer als vCard', + 'people_add_reminder_for_birthday' => 'Maak een jaarlijkse verjaardag herinnering', + + // show + 'section_contact_information' => 'Contactinformatie', + 'section_personal_activities' => 'Activiteiten', + 'section_personal_reminders' => 'Herinneringen', + 'section_personal_tasks' => 'Taken', + 'section_personal_gifts' => 'Cadeaus', + 'section_personal_notes' => 'Notities', + + // archived contacts + 'list_link_to_active_contacts' => 'Je bekijkt gearchiveerde contacten. Bekijk in plaats daarvan de lijst van actieve contacten.', + 'list_link_to_archived_contacts' => 'Lijst van gearchiveerde contacten', + + // Header + 'me' => 'Dit ben jij', + 'edit_contact_information' => 'Bewerk contactinformatie', + 'contact_archive' => 'Archiveer contact', + 'contact_unarchive' => 'Dearchiveer contact', + 'contact_archive_help' => 'Gearchiveerde contacten worden niet getoond in de lijst met contactpersonen, maar worden nog steeds weergegeven in zoekresultaten.', + 'call_button' => 'Telefoongesprek registreren', + 'set_favorite' => 'Favoriete contacten worden bovenaan de lijst met contactpersonen geplaatst', + + // Stay in touch + 'stay_in_touch' => 'Blijf in contact', + 'stay_in_touch_frequency' => 'Blijf elke dag in contact | Blijf elke {count} dagen in contact', + 'stay_in_touch_next_date' => 'Volgende keer: {date}', + 'stay_in_touch_invalid' => 'De frequentie moet groter zijn dan 0.', + 'stay_in_touch_premium' => 'Je moet je account upgraden om gebruik te maken van deze functie', + 'stay_in_touch_modal_title' => 'Blijf in contact', + 'stay_in_touch_modal_desc' => 'We kunnen je herinneren via e-mail om regelmatig in contact te blijven met {firstname}.', + 'stay_in_touch_modal_label' => 'Stuur me iedere… {count} dag|Stuur me een e-mail iedere… {count} dagen', + + // Calls + 'modal_call_title' => 'Telefoongesprek registreren', + 'modal_call_comment' => 'Waar hebben jullie het over gehad? (optioneel)', + 'modal_call_exact_date' => 'Het telefoongesprek gebeurde op', + 'modal_call_who_called' => 'Wie heeft gebeld?', + 'modal_call_emotion' => 'Wil je opslaan hoe jij je voelde tijdens dit gesprek? (optioneel)', + 'calls_add_success' => 'Het telefoongesprek is opgeslagen.', + 'call_delete_confirmation' => 'Weet je zeker dat je deze oproep wil wissen?', + 'call_delete_success' => 'Deze oproep is succesvol verwijderd', + 'call_title' => 'Telefoongesprekken', + 'call_empty_comment' => 'Geen details', + 'call_blank_title' => 'Hou de telefoongesprekken bij die je met {name} hebt gevoerd', + 'call_blank_desc' => 'Jij hebt {name} gebeld', + 'call_you_called' => 'Jij belde', + 'call_he_called' => '{name} belde', + 'call_emotions' => 'Emoties:', + + // Conversation + 'conversation_blank' => 'Registreer gesprekken die je hebt met :name op sociale media, SMS…', + 'conversation_delete_link' => 'Verwijder het gesprek', + 'conversation_edit_title' => 'Gesprek bewerken', + 'conversation_edit_delete' => 'Weet je zeker dat je dit gesprek wil verwijderen? Het wordt definitief verwijderd.', + 'conversation_add_success' => 'Het gesprek is succesvol toegevoegd.', + 'conversation_edit_success' => 'Het gesprek is succesvol bijgewerkt.', + 'conversation_delete_success' => 'Het gesprek is succesvol verwijderd.', + 'conversation_add_title' => 'Nieuw gesprek registreren', + 'conversation_add_when' => 'Wanneer heb je dit gesprek gehad?', + 'conversation_add_who_wrote' => 'Wie heeft dit bericht verzonden?', + 'conversation_add_how' => 'Hoe heb je dit gesprek gevoerd?', + 'conversation_add_you' => 'Jij', + 'conversation_add_content' => 'Schrijf hier wat er is gezegd', + 'conversation_add_what_was_said' => 'Wat was de gespreksinhoud?', + 'conversation_add_another' => 'Nog een bericht toevoegen', + 'conversation_add_error' => 'Je moet tenminste één bericht toevoegen.', + 'conversation_list_table_messages' => 'Berichten', + 'conversation_list_table_content' => 'Gedeeltelijke inhoud (laatste bericht)', + 'conversation_list_title' => 'Gesprekken', + 'conversation_list_cta' => 'Gesprek toevoegen', + + // age - birthday + 'birthdate_not_set' => 'Geboortedatum is niet ingesteld', + 'age_approximate_in_years' => 'ongeveer :age jaren oud', + 'age_exact_in_years' => ':age jaren oud', + 'age_exact_birthdate' => 'geboren op :date', + + // Last called + 'last_called' => 'Laatst gebeld op: :date', + 'last_talked_to' => 'Laatst gebeld op: {date}', + 'last_called_empty' => 'Laatst gebeld op: onbekend', + 'last_activity_date' => 'Laatste activiteit samen: :date', + 'last_activity_date_empty' => 'Laatste activiteit samen: onbekend', + + // additional information + 'information_edit_success' => 'Het profiel is succesvol bijgewerkt', + 'information_edit_title' => 'Bewerk :name\'s persoonlijke informatie', + 'information_edit_max_size' => 'Maximaal :size Kb.', + 'information_edit_max_size2' => 'Maximaal {size} Kb.', + 'information_edit_firstname' => 'Voornaam', + 'information_edit_lastname' => 'Achternaam (optioneel)', + 'information_edit_description' => 'Beschrijving (optioneel)', + 'information_edit_description_help' => 'Dit wordt gebruikt in de contactenlijst om context toe te voegen, indien nodig.', + 'information_edit_unknown' => 'Ik weet de leeftijd van deze persoon niet', + 'information_edit_probably' => 'Deze persoon is waarschijnlijk…', + 'information_edit_not_year' => 'Ik weet de dag en de maand van de verjaardag van deze persoon, maar niet het jaar…', + 'information_edit_exact' => 'Ik weet precies wanneer deze persoon jarig is…', + 'information_edit_birthdate_label' => 'Geboortedatum', + 'information_no_work_defined' => 'Geen werkgegevens gedefinieerd', + 'information_work_at' => 'bij :company', + 'work_add_cta' => 'Werk informatie bijwerken', + 'work_edit_success' => 'Werkinformatie bijgewerkt', + 'work_edit_title' => 'Update :name\'s baan', + 'work_edit_job' => 'Functietitel (optioneel)', + 'work_edit_company' => 'Bedrijf (optioneel)', + 'work_information' => 'Werk informatie', + + // food preferences + 'food_preferences_add_success' => 'Voedsel voorkeuren zijn opgeslagen', + 'food_preferences_edit_description' => 'Misschien heeft :firstname of iemand in de :familiy\'s familie een allergie. Of houdt niet van een specifieke fles wijn. Vul dat hier in zodat je er bij een volgend diner aan denkt', + 'food_preferences_edit_description_no_last_name' => 'Misschien heeft :firstname een allergie. Of houdt niet van een specifieke fles wijn. Vul dat hier in zodat je er bij een volgend diner aan denkt', + 'food_preferences_edit_title' => 'Voedselvoorkeuren', + 'food_preferences_edit_cta' => 'Voedselvoorkeuren opslaan', + 'food_preferences_title' => 'Voedselvoorkeuren', + 'food_preferences_cta' => 'Voeg voedsel voorkeur toe', + + // reminders + 'reminders_blank_title' => 'Is er iets over :name waar je aan herinnert wilt worden?', + 'reminders_blank_add_activity' => 'Voeg een herinnering toe', + 'reminders_add_title' => 'Waar zou je over :name aan herinnert willen worden?', + 'reminders_add_description' => 'Herinner me om…', + 'reminders_add_next_time' => 'Wanneer is de volgende keer dat je hier aan herinnert wilt worden?', + 'reminders_add_once' => 'Herinner me slechts eenmaal hieraan', + 'reminders_add_recurrent' => 'Herinner me hier aan elke', + 'reminders_add_starting_from' => 'beginnend op bovenstaande datum', + 'reminders_add_cta' => 'Herinnering toevoegen', + 'reminders_edit_update_cta' => 'Update herinnering', + 'reminders_add_error_custom_text' => 'Je moet tekst toevoegen voor deze herinnering', + 'reminders_create_success' => 'De herinnering is met succes toegevoegd', + 'reminders_delete_success' => 'De herinnering is verwijderd', + 'reminders_update_success' => 'De herinnering is succesvol bijgewerkt', + 'reminders_add_optional_comment' => 'Opmerking (optioneel)', + + 'reminder_frequency_day' => 'dagelijks | elke :number dagen', + 'reminder_frequency_week' => 'wekelijks | elke :number weken', + 'reminder_frequency_month' => 'elke maand | elke :number maanden', + 'reminder_frequency_year' => 'jaarlijks | elke :number jaren', + 'reminder_frequency_one_time' => 'op :date', + 'reminders_delete_confirmation' => 'Weet u zeker dat u deze herinnering wilt verwijderen?', + 'reminders_delete_cta' => 'Verwijderen', + 'reminders_next_expected_date' => 'op', + 'reminders_cta' => 'Voeg een herinnering toe', + 'reminders_description' => 'Wij sturen een e-mail voor elke herinnering hieronder. Herinneringen worden op de ochtend van de dag waarop de gebeurtenis zal plaatsvinden verzonden. Automatisch toegevoegde herinneringen voor verjaardagen kunnen niet worden verwijderd. Wilt u deze herinneringen wijzigen, bewerk dan de geboortedatum van de betreffende contactpersoon.', + 'reminders_one_time' => 'Eenmalig', + 'reminders_type_week' => 'week', + 'reminders_type_month' => 'maand', + 'reminders_type_year' => 'jaar', + 'reminders_birthday' => 'Verjaardag van :name', + 'reminders_free_plan_warning' => 'Je hebt een gratis abonnement. Hiermee worden geen e-mails verzonden. Als je herinneringen per e-mail wilt ontvangen, upgrade dan je account.', + + // relationships + 'relationship_form_add' => 'Voeg een nieuwe relatie toe', + 'relationship_form_edit' => 'Bewerk bestaande relatie', + 'relationship_form_is_with' => 'Deze persoon is…', + 'relationship_form_is_with_name' => ':name is…', + 'relationship_form_add_choice' => 'Wie is de relatie met?', + 'relationship_form_create_contact' => 'Voeg een nieuwe persoon toe', + 'relationship_form_associate_contact' => 'Toevoegen aan bestaand persoon', + 'relationship_form_associate_dropdown' => 'Zoek en selecteer een bestaande contactpersoon met het dropdown menu hieronder', + 'relationship_form_associate_dropdown_placeholder' => 'Zoek en selecteer een bestaande contactpersoon', + 'relationship_form_also_create_contact' => 'Een contact-kaart maken voor deze persoon.', + 'relationship_form_add_description' => 'Hierdoor kan je dit persoon bewerken, net als elk ander contact.', + 'relationship_form_add_no_existing_contact' => 'Je hebt nog geen contacten die een :name gekoppeld kunnen worden.', + 'relationship_delete_confirmation' => 'Weet je zeker dat je deze relatie wilt verwijderen? Dit is permanent.', + 'relationship_unlink_confirmation' => 'Weet je zeker dat je deze relatie wilt verwijderen? Deze persoon wordt niet verwijdert, alleen de relatie tussen de twee personen.', + 'relationship_form_add_success' => 'De relatie is succesvol toegevoegd.', + 'relationship_form_deletion_success' => 'De relatie is verwijdert.', + + // tasks + 'tasks_title' => 'Taken', + 'tasks_blank_title' => 'Je hebt nog geen taken.', + 'tasks_form_title' => 'Titel', + 'tasks_form_description' => 'Beschrijving (optioneel)', + 'tasks_add_task' => 'Voeg taak toe', + 'tasks_delete_success' => 'De taak is succesvol verwijderd', + 'tasks_complete_success' => 'De taak is met succes aangepast', + + // activities + 'activity_title' => 'Activiteiten', + 'activity_type_category_simple_activities' => 'Eenvoudige activiteiten', + 'activity_type_category_sport' => 'Sport', + 'activity_type_category_food' => 'Eten', + 'activity_type_category_cultural_activities' => 'Culturele activiteiten', + 'activity_type_just_hung_out' => 'gewoon een beetje gehangen', + 'activity_type_watched_movie_at_home' => 'thuis een film gekeken', + 'activity_type_talked_at_home' => 'thuis gekletst', + 'activity_type_did_sport_activities_together' => 'samen gesport', + 'activity_type_ate_at_his_place' => 'bij hun gegeten', + 'activity_type_went_bar' => 'naar een bar gegaan', + 'activity_type_ate_at_home' => 'thuis gegeten', + 'activity_type_picnicked' => 'gepicknickt', + 'activity_type_ate_restaurant' => 'naar een restaurant gegaan', + 'activity_type_went_theater' => 'naar het theater gegaan', + 'activity_type_went_concert' => 'naar een concert gegaan', + 'activity_type_went_play' => 'naar een toneelstuk gegaan', + 'activity_type_went_museum' => 'naar het museum gegaan', + 'activities_add_activity' => 'Voeg activiteit toe', + 'activities_add_more_details' => 'Voeg meer details toe', + 'activities_add_emotions' => 'Voeg emoties toe', + 'activities_add_category' => 'Categorie toevoegen', + 'activities_add_participants_cta' => 'Deelnemers toevoegen', + 'activities_item_information' => 'Deze :activity was op :date', + 'activities_add_title' => 'Wat heb je met {name} gedaan?', + 'activities_summary' => 'Beschrijf wat je deed', + 'activities_add_pick_activity' => 'Wil je deze activiteit categoriseren? Het hoeft niet, maar levert je later statistieken op. (optioneel)', + 'activities_add_date_occured' => 'De activiteit vond plaats op…', + 'activities_add_participants' => 'Wie nam, naast {name}, nog meer deel aan deze activiteit? (optioneel)', + 'activities_add_emotions_title' => 'Wil je opslaan hoe je je voelde tijdens deze activiteit? (optioneel)', + 'activities_blank_title' => 'Hou bij wat je samen met {name} gedaan hebt en waarover jullie gesproken hebben', + 'activities_blank_add_activity' => 'Voeg activiteit toe', + 'activities_add_success' => 'De activiteit is met succes toegevoegd', + 'activities_add_error' => 'Fout bij toevoegen van activiteit', + 'activities_update_success' => 'Deze activiteit is succesvol bijgewerkt', + 'activities_delete_success' => 'Deze activiteit is succesvol verwijdert', + 'activities_who_was_involved' => 'Wie was erbij?', + 'activities_activity' => 'Activiteit categorie', + 'activities_view_activities_report' => 'Bekijk activiteiten rapport', + 'activities_profile_title' => 'Activiteiten rapport voor :name', + 'activities_profile_subtitle' => 'Je hebt, tot nu toe, in totaal :total_activities activiteit met :name vastgelegd, waarvan :activities_last_twelve_months in de afgelopen twaalf maanden.|Je hebt, tot nu toe, in totaal :total_activities activiteiten met :name vastgelegd, waarvan :activities_last_twelve_months in de afgelopen twaalf maanden.', + 'activities_profile_year_summary_activity_types' => 'Hier is een overzicht van de soort activiteiten die jullie samen hebben gedaan in :year', + 'activities_profile_year_summary' => 'Dit is wat jullie samen hebben gedaan in :year', + 'activities_profile_number_occurences' => ':value activiteit|:value activiteiten', + 'activities_list_participants' => 'Deelnemers ({total}):', + 'activities_list_emotions' => 'Gevoelde emoties:', + 'activities_list_date' => 'Vond plaats op', + 'activities_list_category' => 'Categorie:', + + // notes + 'notes_create_success' => 'Nieuwe notitie succesvol toegevoegd', + 'notes_update_success' => 'De notitie is succesvol opgeslagen', + 'notes_delete_success' => 'De notitie is succesvol verwijderd', + 'notes_add_cta' => 'Notitie toevoegen', + 'notes_favorite' => 'Toevoegen/verwijderen uit favorieten', + 'notes_delete_title' => 'Notitie verwijderen', + 'notes_delete_confirmation' => 'Weet je zeker dat je deze notitie wil verwijderen? Het wordt definitief verwijderd', + + // gifts + 'gifts_title' => 'Cadeaus', + 'gifts_add_success' => 'Het cadeau is succesvol toegevoegd', + 'gifts_delete_success' => 'Het cadeau is succesvol verwijderd', + 'gifts_delete_confirmation' => 'Weet je zeker dat je dit cadeau wil verwijderen?', + 'gifts_add_gift' => 'Cadeau toevoegen', + 'gifts_link' => 'Link', + 'gifts_for' => 'Voor: {name}', + 'gifts_delete_cta' => 'Verwijderen', + 'gifts_add_title' => 'Cadeaubeheer voor :name', + 'gifts_add_gift_idea' => 'Cadeau idee', + 'gifts_add_gift_already_offered' => 'Cadeau aangeboden', + 'gifts_add_gift_received' => 'Cadeau ontvangen', + 'gifts_add_gift_title' => 'Wat is dit voor een cadeau?', + 'gifts_add_gift_name' => 'Titel cadeau', + 'gifts_add_link' => 'Link naar de webpagina (optioneel)', + 'gifts_add_value' => 'Waarde (optioneel)', + 'gifts_add_comment' => 'Opmerking (optioneel)', + 'gifts_add_recipient' => 'Ontvanger (optioneel)', + 'gifts_add_recipient_field' => 'Ontvanger', + 'gifts_add_photo' => 'Foto (optioneel)', + 'gifts_add_photo_title' => 'Voeg een foto toe aan dit cadeau', + 'gifts_add_someone' => 'Dit cadeau is voor iemand in {name}\'s familie', + 'gifts_delete_title' => 'Een cadeau verwijderen', + 'gifts_ideas' => 'Cadeau-ideeën', + 'gifts_offered' => 'Cadeau aangeboden', + 'gifts_offered_as_an_idea' => 'Als idee markeren', + 'gifts_received' => 'Ontvangen cadeaus', + 'gifts_view_comment' => 'Opmerking bekijken', + 'gifts_mark_offered' => 'Als aangeboden markeren', + 'gifts_update_success' => 'Het cadeau is succesvol bijgewerkt', + 'gifts_add_date' => 'Datum (optioneel)', + + // debts + 'debt_delete_confirmation' => 'Weet je zeker dat je deze schuld wil verwijderen?', + 'debt_delete_success' => 'De schuld is succesvol verwijderd', + 'debt_add_success' => 'De schuld is succesvol toegevoegd', + 'debt_title' => 'Schulden', + 'debt_add_cta' => 'Schuld toevoegen', + 'debt_you_owe' => 'Jij verschuldigd :amount', + 'debt_they_owe' => ':name verschuldigd jou :amount', + 'debt_add_title' => 'Schuldenbeheer', + 'debt_add_you_owe' => 'Je bent :name verschuldigd', + 'debt_add_they_owe' => ':name is jou verschuldigd', + 'debt_add_amount' => 'een totaal van', + 'debt_add_reason' => 'om de volgende reden (optioneel)', + 'debt_add_add_cta' => 'Schuld toevoegen', + 'debt_edit_update_cta' => 'Schuld bijwerken', + 'debt_edit_success' => 'De schuld is succesvol bijgewerkt', + 'debts_blank_title' => 'Beheer schulden die je bent verschuldigd aan :name of :name jou is verschuldigd', + + // tags + 'tag_edit' => 'Label bewerken', + 'tag_add' => 'Labels toevoegen', + 'tag_add_search' => 'Labels toevoegen of zoeken', + 'tag_no_tags' => 'Nog geen labels', + + // Introductions + 'introductions_sidebar_title' => 'Hoe jullie elkaar ontmoet hebben', + 'introductions_blank_cta' => 'Geef aan hoe je :name hebt ontmoet', + 'introductions_title_edit' => 'Hoe heb je :name leren kennen?', + 'introductions_additional_info' => 'Leg uit hoe en waar jullie elkaar hebben ontmoet', + 'introductions_edit_met_through' => 'Heeft iemand je voorgesteld?', + 'introductions_no_met_through' => 'Niemand', + 'introductions_first_met_date' => 'Datum eerste ontmoeting', + 'introductions_no_first_met_date' => 'Ik weet meer wanneer wij elkaar voor het eerst ontmoet hebben', + 'introductions_first_met_date_known' => 'Dit is de datum dat wij elkaar ontmoet hebben', + 'introductions_add_reminder' => 'Voeg een herinnering toe voor het jubileum van deze ontmoeting', + 'introductions_update_success' => 'Eerste ontmoeting is succesvol bijgewerkt', + 'introductions_met_through' => 'Voorgesteld door :name', + 'introductions_met_date' => 'Ontmoet op :date', + 'introductions_reminder_title' => 'Jubileum van jullie eerste ontmoeting', + + // Deceased + 'deceased_reminder_title' => 'Sterfdag van :name', + 'deceased_mark_person_deceased' => 'Markeer dit als overleden', + 'deceased_know_date' => 'Ik weet de datum waarop deze persoon is gestorven', + 'deceased_add_reminder' => 'Stel een herinnering in voor de sterfdag', + 'deceased_label' => 'Overleden', + 'deceased_date_label' => 'Datum van overlijden', + 'deceased_label_with_date' => 'Overleden op :date', + 'deceased_age' => 'Leeftijd bij overlijden', + + // Contact information + 'contact_info_title' => 'Contactinformatie', + 'contact_info_form_content' => 'Inhoud', + 'contact_info_form_contact_type' => 'Contactsoort', + 'contact_info_form_personalize' => 'Personaliseer', + 'contact_info_address' => 'Woont in', + + // Addresses + 'contact_address_title' => 'Adressen', + 'contact_address_form_name' => 'Label (optioneel)', + 'contact_address_form_street' => 'Straat (optioneel)', + 'contact_address_form_city' => 'Stad (optioneel)', + 'contact_address_form_province' => 'Provincie (optioneel)', + 'contact_address_form_postal_code' => 'Postcode (optioneel)', + 'contact_address_form_country' => 'Land (optioneel)', + 'contact_address_form_latitude' => 'Breedtegraad (alleen cijfers) (optioneel)', + 'contact_address_form_longitude' => 'Lengtegraad (alleen cijfers) (optioneel)', + + // Pets + 'pets_kind' => 'Soort huisdier', + 'pets_name' => 'Naam (optioneel)', + 'pets_create_success' => 'Het huisdier is succesvol toegevoegd', + 'pets_update_success' => 'Het huisdier is succesvol bijgewerkt', + 'pets_delete_success' => 'Het huisdier is succesvol verwijderd', + 'pets_title' => 'Huisdieren', + 'pets_reptile' => 'Reptiel', + 'pets_bird' => 'Vogel', + 'pets_cat' => 'Kat', + 'pets_dog' => 'Hond', + 'pets_fish' => 'Vis', + 'pets_hamster' => 'Hamster', + 'pets_horse' => 'Paard', + 'pets_rabbit' => 'Konijn', + 'pets_rat' => 'Rat', + 'pets_small_animal' => 'Klein dier', + 'pets_other' => 'Andere', + + // life events + 'life_event_list_tab_life_events' => 'Levensgebeurtenissen', + 'life_event_list_tab_other' => 'Notities, herinneringen, …', + 'life_event_list_title' => 'Levensgebeurtenissen', + 'life_event_blank' => 'Leg vast wat er in het leven van {name} gebeurd voor toekomstige referentie.', + 'life_event_list_cta' => 'Levensgebeurtenis toevoegen', + 'life_event_create_category' => 'Alle categorieën', + 'life_event_create_life_event' => 'Levensgebeurtenis toevoegen', + 'life_event_create_default_title' => 'Titel (optioneel)', + 'life_event_create_default_story' => 'Verhaal (optioneel)', + 'life_event_create_date' => 'Je hoeft geen maand of dag aan te geven - alleen het jaar is verplicht.', + 'life_event_create_default_description' => 'Voeg toe wat je hierover weet', + 'life_event_create_add_yearly_reminder' => 'Voeg een jaarlijkse herinnering toe voor deze gebeurtenis', + 'life_event_create_success' => 'De levensgebeurtenis is toegevoegd', + 'life_event_delete_title' => 'Verwijder levensgebeurtenis', + 'life_event_delete_description' => 'Weet je zeker dat je deze levensgebeurtenis wil verwijderen? Dit is permanent.', + 'life_event_delete_success' => 'De levensgebeurtenis is verwijderd', + 'life_event_date_it_happened' => 'Datum van de gebeurtenis', + 'life_event_category_work_education' => 'Werk & onderwijs', + 'life_event_category_family_relationships' => 'Familie & relaties', + 'life_event_category_home_living' => 'Thuis & leven', + 'life_event_category_health_wellness' => 'Gezondheid & welzijn', + 'life_event_category_travel_experiences' => 'Reizen & ervaringen', + 'life_event_sentence_new_job' => 'Nieuwe baan gekregen', + 'life_event_sentence_retirement' => 'Met pensioen gegaan', + 'life_event_sentence_new_school' => 'Begonnen met school', + 'life_event_sentence_study_abroad' => 'Gestudeerd in het buitenland', + 'life_event_sentence_volunteer_work' => 'Begonnen met vrijwilligerswerk', + 'life_event_sentence_published_book_or_paper' => 'Werd gepubliceerd', + 'life_event_sentence_military_service' => 'Begonnen in militaire dienst', + 'life_event_sentence_new_relationship' => 'Begon een relatie', + 'life_event_sentence_engagement' => 'Verloofd', + 'life_event_sentence_marriage' => 'Getrouwd', + 'life_event_sentence_anniversary' => 'Jubileum', + 'life_event_sentence_expecting_a_baby' => 'Verwacht een baby', + 'life_event_sentence_new_child' => 'Kreeg een kind', + 'life_event_sentence_new_family_member' => 'Familielid toegevoegd', + 'life_event_sentence_new_pet' => 'Kreeg een huisdier', + 'life_event_sentence_end_of_relationship' => 'Relatie beëindigd', + 'life_event_sentence_loss_of_a_loved_one' => 'Een geliefde verloren', + 'life_event_sentence_moved' => 'Verhuisd', + 'life_event_sentence_bought_a_home' => 'Huis gekocht', + 'life_event_sentence_home_improvement' => 'Verbouwd', + 'life_event_sentence_holidays' => 'Op vakantie geweest', + 'life_event_sentence_new_vehicle' => 'Kreeg een nieuw voertuig', + 'life_event_sentence_new_roommate' => 'Kreeg een huisgenoot', + 'life_event_sentence_overcame_an_illness' => 'Overwon een ziekte', + 'life_event_sentence_quit_a_habit' => 'Stopte met slechte gewoonte', + 'life_event_sentence_new_eating_habits' => 'Begon met nieuw voedingspatroon', + 'life_event_sentence_weight_loss' => 'Afgevallen', + 'life_event_sentence_wear_glass_or_contact' => 'Begon met dragen van bril of contactlenzen', + 'life_event_sentence_broken_bone' => 'Bot gebroken', + 'life_event_sentence_removed_braces' => 'Beugel verwijderd', + 'life_event_sentence_surgery' => 'Operatie ondergaan', + 'life_event_sentence_dentist' => 'Ging naar de tandarts', + 'life_event_sentence_new_sport' => 'Begon met een sport', + 'life_event_sentence_new_hobby' => 'Begon met een hobby', + 'life_event_sentence_new_instrument' => 'Nieuw instrument geleerd', + 'life_event_sentence_new_language' => 'Nieuwe taal geleerd', + 'life_event_sentence_tattoo_or_piercing' => 'Kreeg een tatoeage of piercing', + 'life_event_sentence_new_license' => 'Kreeg een diploma', + 'life_event_sentence_travel' => 'Heeft gereisd', + 'life_event_sentence_achievement_or_award' => 'Ontving een prestatie of prijs', + 'life_event_sentence_changed_beliefs' => 'Van overtuiging veranderd', + 'life_event_sentence_first_word' => 'Voor het eerst gesproken', + 'life_event_sentence_first_kiss' => 'De eerste kus', + + // documents + 'document_list_title' => 'Documenten', + 'document_list_cta' => 'Document uploaden', + 'document_list_blank_desc' => 'Hier kan je documenten opslaan gerelateerd aan deze persoon.', + 'document_upload_zone_cta' => 'Bestand uploaden', + 'document_upload_zone_progress' => 'Document uploaden…', + 'document_upload_zone_error' => 'Er is een fout opgetreden tijdens uploaden van het document, probeer het a.u.b. opnieuw.', + + // Photos + 'photo_title' => 'Foto\'s', + 'photo_list_title' => 'Gerelateerde foto\'s', + 'photo_list_cta' => 'Foto uploaden', + 'photo_list_blank_desc' => 'Je kan afbeeldingen van dit contact opslaan. Upload er nu eentje!', + 'photo_upload_zone_cta' => 'Upload een foto', + 'photo_current_profile_pic' => 'Huidige profielfoto', + 'photo_make_profile_pic' => 'Stel in als profielfoto', + 'photo_delete' => 'Foto verwijderen', + 'photo_next' => 'Volgende foto ❯', + 'photo_previous' => '❮ Vorige foto', + + // Avatars + 'avatar_change_title' => 'Wijzig je profielfoto', + 'avatar_question' => 'Welke avatar wil je gebruiken?', + 'avatar_default_avatar' => 'De standaard-avatar', + 'avatar_adorable_avatar' => 'De Schattige avatar', + 'avatar_gravatar' => 'The Gravatar dat geassocieerd is met dit contact. Gravatar is een wereldwijd systeem dat gebruikers hun emailadres laat associëren met hun foto\'s.', + 'avatar_current' => 'Huidige avatar houden', + 'avatar_photo' => 'Van een foto die je upload', + 'avatar_crop_new_avatar_photo' => 'Snij nieuwe avatar foto bij', + + // emotions + 'emotion_this_made_me_feel' => 'Je voelde je…', + + // logs + 'auditlogs_link' => 'Geschiedenis', + 'auditlogs_title' => 'Alles wat er met :name is gebeurd', + 'auditlogs_breadcrumb' => 'Geschiedenis', + 'auditlogs_author' => 'Door :name op :date', + + // contact field label + 'contact_field_label_home' => 'Home', + 'contact_field_label_work' => 'Werk', + 'contact_field_label_cell' => 'Mobiel', + 'contact_field_label_fax' => 'Fax', + 'contact_field_label_pager' => 'Pieper', + 'contact_field_label_main' => 'Algemeen', + 'contact_field_label_other' => 'Ander', + 'contact_field_label_personal' => 'Persoonlijk', +]; diff --git a/resources/lang/nl/reminder.php b/resources/lang/nl/reminder.php new file mode 100644 index 0000000..376ff8d --- /dev/null +++ b/resources/lang/nl/reminder.php @@ -0,0 +1,16 @@ + 'Wens een gelukkige verjaardag aan', + 'type_phone_call' => 'Bel', + 'type_lunch' => 'Lunchen met', + 'type_hangout' => 'Ontmoeten met', + 'type_email' => 'E-mail', + 'type_birthday_kid' => 'Wish happy birthday to the child of', +]; diff --git a/resources/lang/nl/settings.php b/resources/lang/nl/settings.php new file mode 100644 index 0000000..e5e7ed0 --- /dev/null +++ b/resources/lang/nl/settings.php @@ -0,0 +1,557 @@ + 'Accountinstellingen', + 'sidebar_personalization' => 'Personalisatie', + 'sidebar_settings_storage' => 'Opslag', + 'sidebar_settings_export' => 'Exporteer gegevens', + 'sidebar_settings_users' => 'Gebruikers', + 'sidebar_settings_subscriptions' => 'Abonnement', + 'sidebar_settings_import' => 'Importeer gegevens', + 'sidebar_settings_tags' => 'Labelbeheer', + 'sidebar_settings_api' => 'API', + 'sidebar_settings_dav' => 'DAV bronnen', + 'sidebar_settings_security' => 'Beveiliging', + 'sidebar_settings_auditlogs' => 'Auditlog', + + 'title_general' => 'Algemene Informatie', + 'title_i18n' => 'Internationaliseringsinstellingen', + 'title_layout' => 'Lay-out', + + 'me_title' => 'Ik als contact', + 'me_help' => 'Dit is het contact dat jou vertegenwoordigt in Monica', + 'me_select' => 'Selecteer een contactpersoon', + 'me_no_contact' => 'Nog geen contact geselecteerd.', + 'me_select_click' => 'Klik hier om een contact te selecteren.', + 'me_remove_contact' => 'Koppeling verwijderen', + 'me_choose' => 'Kies jezelf', + 'me_choose_placeholder' => 'Kies jezelf', + + 'export_title' => 'Exporteer je accountgegevens', + 'export_be_patient' => 'Click the button to start the export. It might take several minutes to process the export – please be patient and do not spam the button.', + 'export_title_sql' => 'Export to SQL', + 'export_sql_explanation' => 'Exporting your data in SQL format allows you to take your data and import it to your own Monica instance. This is only valuable if you do have your own server.', + 'export_sql_cta' => 'Export to SQL', + 'export_sql_link_instructions' => 'Note: read the instructions to learn more about importing this file to your instance.', + 'export_title_json' => 'Export to Json', + 'export_submitted' => 'Your export has been submitted, it will be available in a few moment…', + 'export_json_explanation' => 'Exporting your data in Json format for backup.', + 'export_json_beta' => 'Json export is in preview mode. Tell us what you think about it:', + 'export_json_cta' => 'Export to Json', + 'export_header_type' => 'Soort', + 'export_header_timestamp' => 'Aanmaak datum', + 'export_header_status' => 'Status', + 'export_header_actions' => 'Acties', + 'export_last_title' => 'Laatste exports', + 'export_empty_title' => 'No exports yet', + 'export_type_json' => 'Json export', + 'export_type_sql' => 'SQL export', + 'export_status_todo' => 'Ingediend', + 'export_status_doing' => 'Bezig', + 'export_status_done' => 'Gereed', + 'export_status_failed' => 'Mislukt', + 'export_not_done' => 'Download impossible, this export is not done yet.', + + 'firstname' => 'Voornaam', + 'lastname' => 'Achternaam', + 'name_order' => 'Naamvolgorde', + 'name_order_firstname_lastname' => ' – Klaas Bakker', + 'name_order_lastname_firstname' => ' – Bakker Klaas', + 'name_order_firstname_lastname_nickname' => ' () – Klaas Bakker (Broodje)', + 'name_order_firstname_nickname_lastname' => ' () – Klaas (Broodje) Bakker', + 'name_order_lastname_firstname_nickname' => ' () – Bakker Klaas (Broodje)', + 'name_order_lastname_nickname_firstname' => ' () – Bakker (Broodje) Klaas', + 'name_order_nickname_firstname_lastname' => ' ( ) – Rambo (John Doe)', + 'name_order_nickname_lastname_firstname' => ' ( ) – Rambo (Doe John)', + 'name_order_nickname' => ' – Rambo', + 'currency' => 'Valuta', + 'name' => 'Jouw naam: :name', + 'email' => 'E-mailadres', + 'email_placeholder' => 'E-mailadres invoeren', + 'email_help' => 'Dit is de e-mail die gebruikt wordt om in te loggen, en dit is waar naar toe Monica uw herinneringen zal sturen.', + 'timezone' => 'Tijdzone', + 'temperature_scale' => 'Temperatuurschaal', + 'temperature_scale_fahrenheit' => 'Fahrenheit (°F)', + 'temperature_scale_celsius' => 'Celsius (°C)', + 'layout' => 'Lay-out', + 'layout_small' => 'Maximaal 1200 pixels breed', + 'layout_big' => 'Volledige breedte van de browser', + 'save' => 'Voorkeuren bijwerken', + 'delete_title' => 'Verwijder je account', + 'delete_desc' => 'Wilt u uw account verwijderen? Verwijderen is permanent en al uw gegevens zullen permanent worden verwijderd. Als u een abonnement hebt, wordt het onmiddellijk geannuleerd.', + 'delete_other_desc' => 'Your data in the main database will be deleted immediately. As described in our privacy policy, we carry out securely encrypted backups of the database every day. These backups are kept for 30 days after which they are completely deleted. We cannot delete specific data from the backups we hold any earlier than this. All of your data will be completely deleted no later than 31 days after your account’s deletion.', + 'reset_desc' => 'Wil je jouw account resetten? Hiermee verwijder je alle contactpersonen en de gegevens die hieraan zijn gekoppeld. Je account zal niet worden verwijderd.', + 'reset_title' => 'Reset je account', + 'reset_cta' => 'Account resetten', + 'reset_notice' => 'Weet je zeker dat je je account wilt resetten? Dit is permanent en kan niet ongedaan worden gemaakt.', + 'reset_success' => 'Uw account is succesvol gereset.', + 'delete_notice' => 'Weet je zeker dat je je account wilt verwijderen? Dit is permanent en kan niet ongedaan worden gemaakt. Al uw gegevens zullen worden verwijderd en kunnen niet worden hersteld.', + 'delete_cta' => 'Account verwijderen', + 'settings_success' => 'Voorkeuren bijgewerkt!', + 'locale' => 'Taal', + 'locale_help' => 'Wil je helpen met het vertalen van Monica of een nieuwe taal toevoegen? Klik hier voor meer informatie.', + 'locale_ar' => 'Arabisch', + 'locale_cs' => 'Tsjechisch', + 'locale_de' => 'Duits', + 'locale_el' => 'Greek', + 'locale_en' => 'Engels', + 'locale_en-GB' => 'Engels (Verenigd Koninkrijk)', + 'locale_es' => 'Spaans', + 'locale_fr' => 'Frans', + 'locale_he' => 'Hebreeuws', + 'locale_hr' => 'Kroatisch', + 'locale_id' => 'Indonesisch', + 'locale_it' => 'Italiaans', + 'locale_ja' => 'Japans', + 'locale_nl' => 'Nederlands', + 'locale_pt' => 'Portugees', + 'locale_pt-BR' => 'Portuguese, Brazil', + 'locale_ru' => 'Russisch', + 'locale_sv' => 'Zweeds', + 'locale_vi' => 'Vietnamese', + 'locale_zh' => 'Chinees (vereenvoudigd)', + 'locale_zh-TW' => 'Traditioneel Chinees', + 'locale_tr' => 'Turks', + + 'security_title' => 'Beveiliging', + 'security_help' => 'Verander de beveiligingsinstellingen voor je account.', + 'password_change' => 'Verander je wachtwoord', + 'password_current' => 'Huidige wachtwoord', + 'password_current_placeholder' => 'Voer je huidige wachtwoord in', + 'password_new1' => 'Nieuw wachtwoord', + 'password_new1_placeholder' => 'Voer je nieuwe wachtwoord in', + 'password_new2' => 'Bevestig nieuw wachtwoord', + 'password_new2_placeholder' => 'Herhaal je nieuwe wachtwoord', + 'password_btn' => 'Wachtwoord wijzigen', + '2fa_title' => 'Tweestapsverificatie', + '2fa_otp_title' => 'Tweestapsverificatie mobiele applicatie', + '2fa_enable_title' => 'Tweestapsverificatie inschakelen', + '2fa_enable_description' => 'Schakel tweestapsverificatie in om de beveiliging van je account te verbeteren.', + '2fa_enable_otp' => 'Open je tweestapsverificatie app en scan de volgende QR code:', + '2fa_enable_otp_help' => 'Als je tweestapsverificatie app geen QR codes ondersteund, voer dan de volgende code in:', + '2fa_enable_otp_validate' => 'Controleer of je app goed is ingesteld:', + '2fa_enable_success' => 'Tweestapsverificatie geactiveerd', + '2fa_enable_error' => 'Foutmelding tijdens het activeren van tweestapsverificatie', + '2fa_enable_error_already_set' => 'Tweestapsverificatie is al geactiveerd', + '2fa_disable_title' => 'Tweestapsverificatie uitschakelen', + '2fa_disable_description' => 'Tweestapsverificatie uitschakelen voor je account. Wees voorzichtig, je account zal minder beveiligd zijn!', + '2fa_disable_success' => 'Tweestapsverificatie uitgeschakeld', + '2fa_disable_error' => 'Foutmelding tijdens het uitschakelen van tweestapsverificatie', + + 'webauthn_title' => 'Beveiligingssleutel — WebAuthn protocol', + 'webauthn_enable_description' => 'Voeg een nieuwe beveiligingssleutel toe', + 'webauthn_key_name_help' => 'Kies een naam voor je sleutel.', + 'webauthn_key_name' => 'Sleutelnaam:', + 'webauthn_success' => 'Je sleutel is gedetecteerd en bevestigd.', + 'webauthn_last_use' => 'Laatst gebruikt: {timestamp}', + 'webauthn_delete_confirmation' => 'Weet je zeker dat je deze sleutel wilt verwijderen?', + 'webauthn_delete_success' => 'Sleutel verwijderd', + 'webauthn_insertKey' => 'Voer je beveiligingssleutel in.', + 'webauthn_buttonAdvise' => 'Druk op de knop van de beveiligingssleutel, als deze er een heeft.', + 'webauthn_noButtonAdvise' => 'Als dat niet zo is, verwijder de sleutel dan en verbind deze opnieuw.', + 'webauthn_not_supported' => 'Je browser ondersteunt op dit moment geen WebAuthn.', + 'webauthn_not_secured' => 'WebAuthn ondersteunt alleen beveiligde verbindingen. Laad deze pagina via https.', + 'webauthn_error_already_used' => 'Deze sleutel is al geregistreerd. Je hoeft deze niet opnieuw te registreren.', + 'webauthn_error_not_allowed' => 'De bewerking is niet toegestaan, of duurde te lang en is geannuleerd.', + + 'recovery_title' => 'Herstelcodes', + 'recovery_show' => 'Herstelcodes opvragen', + 'recovery_copy_help' => 'Codes naar klembord kopiëren', + 'recovery_help_intro' => 'Dit zijn je herstelcodes:', + 'recovery_help_information' => 'Je kan elke herstelcode eenmalig gebruiken.', + 'recovery_clipboard' => 'Codes gekopieerd naar het klembord.', + 'recovery_generate' => 'Genereer nieuwe codes…', + 'recovery_generate_help' => 'Het genereren van nieuwe codes zal eerder gegenereerde codes ongeldig maken.', + 'recovery_already_used_help' => 'Deze code is al gebruikt.', + + 'users_list_title' => 'Gebruikers met toegang tot je account', + 'users_list_add_user' => 'Nieuwe gebruiker uitnodigen', + 'users_list_you' => 'Dat ben jij', + 'users_list_invitations_title' => 'Openstaande uitnodigingen', + 'users_list_invitations_explanation' => 'Hieronder staan de mensen die je hebt uitgenodigd om mee samen te werken.', + 'users_list_invitations_invited_by' => 'uitgenodigd door :name', + 'users_list_invitations_sent_date' => 'verzonden op :date', + 'users_blank_title' => 'Jij bent de enige met toegang tot dit account.', + 'users_blank_add_title' => 'Wil je iemand anders uitnodigen?', + 'users_blank_description' => 'Deze persoon zal dezelfde toegang hebben als jij en zal contactinformatie kunnen toevoegen, bewerken of verwijderen.', + 'users_blank_cta' => 'Iemand uitnodigen', + 'users_add_title' => 'Nodig een nieuwe gebruiker uit voor je account via e-mail', + 'users_add_description' => 'Deze persoon heeft dezelfde toegang als jij, inclusief het uitnodigen of verwijderen van andere gebruikers, waaronder jij. Zorg ervoor dat je deze persoon vertrouwt voordat je hem toegang geeft.', + 'users_add_email_field' => 'Voer het e-mailadres in van de persoon die je wil uitnodigen', + 'users_add_confirmation' => 'Ik bevestig dat ik deze gebruiker wil uitnodigen voor mijn account. Ik begrijp dat deze persoon toegang zal hebben tot AL mijn gegevens en precies zal zien wat ik zie.', + 'users_add_cta' => 'Uitnodigen via e-mail', + 'users_accept_title' => 'Uitnodiging accepteren en nieuw account aanmaken', + 'users_error_please_confirm' => 'Bevestig alsjeblieft, dat je deze gebruiker wilt uitnodigen', + 'users_error_email_already_taken' => 'Dit e-mailadres is al in gebruik. Gebruik een andere', + 'users_error_already_invited' => 'Je hebt deze gebruiker al uitgenodigd. Kies alsjeblieft een ander e-mailadres.', + 'users_error_email_not_similar' => 'Dit is niet het e-mailadres van de persoon die jou heeft uitgenodigd.', + 'users_invitation_deleted_confirmation_message' => 'De uitnodiging is succesvol verwijderd', + 'users_invitations_delete_confirmation' => 'Weet je zeker dat je deze uitnodiging wilt verwijderen?', + 'users_list_delete_confirmation' => 'Weet je zeker dat je deze gebruiker uit je account wil verwijderen?', + 'users_invitation_need_subscription' => 'Het toevoegen van meer gebruikers vereist een abonnement.', + + 'subscriptions_account_current_plan' => 'Je huidige abonnement', + 'subscriptions_account_current_legacy' => 'Current plan, not selectable anymore:', + 'subscriptions_account_current_paid_plan' => 'Jij gebruikt het :name abonnement. Bedankt voor het aanmelden.', + + 'subscriptions_account_next_billing_title' => 'Volgende betaling', + 'subscriptions_account_next_billing' => 'Je abonnement wordt automatisch verlengd op :date.', + 'subscriptions_account_bill_monthly' => 'We’ll bill you :price for another month.', + 'subscriptions_account_bill_annual' => 'We’ll bill you :price for another year.', + 'subscriptions_account_change' => 'Change plan', + + 'subscriptions_account_cancel_title' => 'Cancel subscription', + 'subscriptions_account_cancel_action' => 'Cancel subscription', + 'subscriptions_account_cancel' => 'Je kunt je abonnement op elk moment annuleren.', + 'subscriptions_account_free_plan' => 'Je hebt het gratis abonnement.', + 'subscriptions_account_free_plan_upgrade' => 'Je kan je account upgraden naar het :name abonnement, dat $:price per maand kost. Hier zijn de voordelen:', + 'subscriptions_account_free_plan_benefits_users' => 'Onbeperkt aantal gebruikers', + 'subscriptions_account_free_plan_benefits_reminders' => 'E-mail herinneringen', + 'subscriptions_account_free_plan_benefits_import_data_vcard' => 'Importeer contacten via vCard', + 'subscriptions_account_free_plan_benefits_support' => 'Steun het project op de lange termijn, zodat we meer geweldige functies kunnen introduceren.', + 'subscriptions_account_upgrade' => 'Account upgraden', + 'subscriptions_account_upgrade_title' => 'Upgrade Monica vandaag en maak je persoonlijke relaties betekenisvoller.', + 'subscriptions_account_upgrade_choice' => 'Kies hieronder een abonnement en sluit je aan bij meer dan :customers personen die reeds de premium versie van Monica gebruiken.', + 'subscriptions_account_update_title' => 'Update Monica subscription', + 'subscriptions_account_update_description' => 'You can change your subscription’s frequency here.', + 'subscriptions_account_update_information' => 'You will be billed immediately for the new amount. Your subscription will extend to the new period, depending on your choice.', + 'subscriptions_account_invoices' => 'Facturen', + 'subscriptions_account_invoices_download' => 'Download', + 'subscriptions_account_invoices_subscription' => 'Abonnement van :startDate tot :endDate', + 'subscriptions_account_payment' => 'Welke betaalmethode past je het beste?', + 'subscriptions_account_confirm_payment' => 'Je betaling is nog niet helemaal afgerond, bevestig je betaling alsjeblieft.', + 'subscriptions_downgrade_title' => 'Account naar de gratis variant downgraden', + 'subscriptions_downgrade_limitations' => 'De gratis versie heeft beperkingen. Om te kunnen downgraden moet je voldoen aan de volgende voorwaarden:', + 'subscriptions_downgrade_rule_users' => 'Je mag slechts één gebruiker in je account hebben', + 'subscriptions_downgrade_rule_users_constraint' => 'Je hebt op dit moment 1 gebruiker in jouw account.|Je hebt op dit moment :count gebruikers in jouw account.', + 'subscriptions_downgrade_rule_invitations' => 'Je mag geen openstaande uitnodigingen hebben', + 'subscriptions_downgrade_rule_invitations_constraint' => 'Op dit moment heb je 1 openstaande uitnodiging. Op dit moment heb je :count openstaande uitnodigingen.', + 'subscriptions_downgrade_rule_contacts' => 'Je mag niet meer dan :number actieve contacten hebben', + 'subscriptions_downgrade_rule_contacts_constraint' => 'Op dit moment heb je 1 contact.|Op dit moment heb je :count contacten.', + 'subscriptions_downgrade_rule_contacts_archive' => 'We can also archive all your contacts for you – that would clear this rule and let you proceed with your account’s downgrade process.', + 'subscriptions_downgrade_cta' => 'Downgraden', + 'subscriptions_downgrade_success' => 'Je hebt nu het gratis abonnement!', + 'subscriptions_downgrade_thanks' => 'Bedankt dat je het betaalde abonnement geprobeerd hebt. We zijn voortdurend bezig om nieuwe functionaliteiten toe voegen. Misschien wil je dit in de gaten houden en wie weet heb je in de toekomst weer interesse in een betaald abonnement.', + 'subscriptions_back' => 'Terug naar instellingen', + 'subscriptions_upgrade_title' => 'Account upgraden', + 'subscriptions_upgrade_choose' => 'Je hebt het :plan abonnement gekozen.', + 'subscriptions_upgrade_infos' => 'Wij zijn erg blij. Voer hieronder je betaalgegevens in.', + 'subscriptions_upgrade_name' => 'Naam op de kaart', + 'subscriptions_upgrade_zip' => 'Postcode', + 'subscriptions_upgrade_credit' => 'Creditcard', + 'subscriptions_upgrade_submit' => 'Betaal {amount}', + 'subscriptions_upgrade_charge' => 'We nemen nu :price in rekening. Het volgende afschrift is op :date. Als je ooit van gedachten verandert, kun je op elk moment annuleren, geen vragen.', + 'subscriptions_upgrade_charge_handled' => 'De betaling wordt afgehandeld via Stripe. Er worden geen creditcardgegevens bij ons opgeslagen.', + 'subscriptions_upgrade_success' => 'Dankjewel! Je hebt nu een abonnement.', + 'subscriptions_upgrade_thanks' => 'Welkom in de community van mensen die proberen om de wereld een betere plek te maken.', + + 'subscriptions_payment_confirm_title' => 'Bevestig je betaling van :amount', + 'subscriptions_payment_confirm_information' => 'Er is een extra bevestiging nodig om je betaling te kunnen verwerken. Bevestig je betaling door hieronder je betalingsgegevens in te vullen.', + 'subscriptions_payment_succeeded_title' => 'Betaling succesvol', + 'subscriptions_payment_succeeded' => 'Deze betaling is al met succes bevestigd.', + 'subscriptions_payment_cancelled_title' => 'Betaling geannuleerd', + 'subscriptions_payment_cancelled' => 'De betaling is geannuleerd.', + 'subscriptions_payment_error_name' => 'Voer alsjeblieft je naam in.', + 'subscriptions_payment_success' => 'De betaling is voltooid.', + + 'subscriptions_pdf_title' => 'Jouw :name maandelijkse abonnement', + 'subscriptions_plan_frequency_year' => ':amount / year', + 'subscriptions_plan_frequency_month' => ':amount / month', + 'subscriptions_plan_choose' => 'Kies een abonnement', + 'subscriptions_plan_year_title' => 'Jaarlijks betalen', + 'subscriptions_plan_year_bonus' => 'Gemoedsrust voor een heel jaar', + 'subscriptions_plan_month_title' => 'Maandelijks betalen', + 'subscriptions_plan_month_bonus' => 'Altijd opzegbaar', + 'subscriptions_plan_include1' => 'Bij je upgrade inbegrepen:', + 'subscriptions_plan_include2' => 'Onbeperkt aantal contacten • Onbeperkt aantal gebruikers • Herinneringen via e-mail • Importeren via vCard • Personaliseren van het contactoverzicht', + 'subscriptions_plan_include3' => '100% van de inkomsten gaan naar de ontwikkeling van dit fantastische open source project.', + 'subscriptions_help_title' => 'Extra details waar je wellicht interesse in hebt', + 'subscriptions_help_opensource_title' => 'Wat is een open source project?', + 'subscriptions_help_opensource_desc' => 'Monica is een open source project. Dat betekend dat het volledig wordt gebouwd door een welwillende groep mensen die slechts een geweldige applicatie willen maken voor het algemeen belang. Open source betekend dat de code vrij beschikbaar is op Github, iederheen het kan inspecteren, het kan aanpassen of het kan verbeteren. Al het geld dat wij binnenkrijgen is bedoeld om betere functionaliteiten te ontwikkelen, krachtigere servers in te kunnen zetten en het helpt om de rekeningen te betalen. Bedankt voor je hulp. We zouden het niet zonder jou kunnen doen - werkelijk.', + 'subscriptions_help_limits_title' => 'Is er een limiet aan het aantal contacten dat je kan hebben in het gratis abonnement?', + 'subscriptions_help_limits_plan' => 'Ja. Een gratis abonnement stelt je in staat om :number contacten te beheren.', + 'subscriptions_help_discounts_title' => 'Zijn er kortingen voor non-profitorganisaties en onderwijs?', + 'subscriptions_help_discounts_desc' => 'Jazeker! Monica is gratis voor studenten en goede doelen. Neem contact op met support met een passend bewijs (van inschrijving) en wij zullen deze speciale status aan je account toekennen.', + 'subscriptions_help_change_title' => 'Wat als ik van gedachten verander?', + 'subscriptions_help_change_desc' => 'Je kan op elk moment volledig zelfstandig opzeggen, zonder verdere voorwaarden. Er is geen noodzaak om contact te zoeken met support. Je lopende abonnementsperiode kan echter niet terugbetaald worden.', + + 'stripe_error_card' => 'Je creditcard is geweigerd met de volgende mededeling: :message', + 'stripe_error_api_connection' => 'Netwerkcommunicatie met Stripe is mislukt. Probeer het later opnieuw.', + 'stripe_error_rate_limit' => 'Teveel verzoeken met Stripe op dit moment. Probeer het later opnieuw.', + 'stripe_error_invalid_request' => 'Ongeldige parameters. Probeer het later opnieuw.', + 'stripe_error_authentication' => 'Verkeerde authenticatie met Stripe', + + 'import_title' => 'Contacten importeren in jouw account', + 'import_cta' => 'Contacten uploaden', + 'import_stat' => 'Je heb tot nu toe :number bestanden geïmporteerd.', + 'import_result_stat' => 'vCard met 1 contact geüpload(:total_imported geïmporteerd,:total_skipped overgeslagen)|vCard met :total_contacts contacten geüpload(:total_imported geïmporteerd,:total_skipped overgeslagen)', + 'import_view_report' => 'Importrapport bekijken', + 'import_in_progress' => 'Het importeren is bezig. Ververs de pagina over één minuut.', + 'import_upload_title' => 'Contacten uit een vCard bestand importeren', + 'import_upload_rules_desc' => 'Er zijn restricties:', + 'import_upload_rule_format' => 'Wij ondersteunen .vcard en .vcf bestanden.', + 'import_upload_rule_vcard' => 'Wij ondersteunen het vCard 3.0 formaat, wat het standaardformaat is voor macOS Contacts.app en Google Contacts.', + 'import_upload_rule_instructions' => 'Export instructions for macOS Contacts.app and Google Contacts.', + 'import_upload_rule_multiple' => 'If your contacts have multiple email addresses or phone numbers, only the first entry will be saved.', + 'import_upload_rule_limit' => 'Bestanden mogen niet groter dan 10MB zijn.', + 'import_upload_rule_time' => 'It might take up to a minute to upload the contacts and process them. Please be patient.', + 'import_upload_rule_cant_revert' => 'Please make sure data is accurate before uploading, as you can’t undo the upload.', + 'import_upload_form_file' => 'Jouw .vcf of .vCard bestand:', + 'import_upload_behaviour' => 'Importgedrag:', + 'import_upload_behaviour_add' => 'Nieuwe contacten toevoegen en bestaande overslaan', + 'import_upload_behaviour_replace' => 'Overschrijf bestaande contacten', + 'import_upload_behaviour_help' => 'Replacing will replace all data found in the vCard, but will keep existing contact fields.', + 'import_report_title' => 'Importrapport', + 'import_report_date' => 'Importdatum', + 'import_report_type' => 'Importtype', + 'import_report_number_contacts' => 'Aantal contacten in het bestand', + 'import_report_number_contacts_imported' => 'Aantal geïmporteerde contacten', + 'import_report_number_contacts_skipped' => 'Aantal overgeslagen contacten', + 'import_report_status_imported' => 'Geïmporteerd', + 'import_report_status_skipped' => 'Overgeslagen', + 'import_vcard_parse_error' => 'Fout tijdens het verwerken van de vCard', + 'import_vcard_contact_exist' => 'Contactpersoon bestaat al', + 'import_vcard_contact_no_firstname' => 'Geen voornaam (verplicht)', + 'import_vcard_file_not_found' => 'Bestand niet gevonden', + 'import_vcard_unknown_entry' => 'Onbekende contactpersoon', + 'import_vcard_file_no_entries' => 'Bestand bevat geen items', + 'import_blank_title' => 'Je hebt nog geen contacten geïmporteerd.', + 'import_blank_question' => 'Wil je nu contacten importeren?', + 'import_blank_description' => 'Wij kunnen vCard-bestanden importeren die je kan krijgen uit Google Contacts of je contactenmanager.', + 'import_blank_cta' => 'Importeer vCard', + 'import_need_subscription' => 'Gegevens importeren vereist een abonnement.', + + 'tags_list_title' => 'Labels', + 'tags_list_description' => 'Je kan je contacten organiseren door labels toe te voegen. Labels werken als mappen maar je kan meer dan één label toevoegen aan een contact. Om een nieuw label toe te voegen moet je dat bij een contactpersoon zelf doen.', + 'tags_list_contact_number' => '1 contact|:count contacten', + 'tags_list_delete_success' => 'Het label is succesvol verwijderd', + 'tags_list_edit_success' => 'The tag has been successfully updated', + 'tags_list_delete_confirmation' => 'Weet je zeker dat je het label wil verwijderen? Alleen het label zal worden verwijderd, contactpersonen blijven behouden.', + 'tags_blank_title' => 'Labels zijn een geweldige methode om je contacten te organiseren.', + 'tags_blank_description' => 'Tags work like folders, but you can add more than one tag to a contact. Go to a contact and tag a friend, right below the name. Once a contact is tagged, come back here to manage all the tags in your account.', + + 'api_title' => 'API-toegang', + 'api_description' => 'De API kan worden gebruikt om de gegevens van Monica te gebruiken in een externe applicatie. Bijvoorbeeld een app op je mobiele telefoon.', + 'api_help' => 'Om de API te gebruiken, is een token noodzakelijk. Je kunt een persoonlijke token aanmaken (zgn. \'Bearer authentication\'), of een OAuth client toestaan om dit voor je te doen. Zie ook de API documentatie.', + 'api_endpoint' => 'De \'API endpoint\' voor deze Monica-server is:', + + 'api_personal_access_tokens' => 'Persoonlijke toegangscodes', + 'api_pao_description' => 'Zorg dat je deze code alleen toevertrouwt aan een bron die je vertrouwd gezien deze code toegang geeft tot al jouw gegevens.', + 'api_token_title' => 'Persoonlijke Tokens', + 'api_token_create_new' => 'Nieuwe code aanmaken', + 'api_token_not_created' => 'Je hebt nog geen persoonlijke toegangscodes aangemaakt.', + 'api_token_name' => 'Naam token', + 'api_token_expire' => 'Verloopt op {date}', + 'api_token_delete' => 'Verwijderen', + 'api_token_create' => 'Code aanmaken', + 'api_token_scopes' => 'Bereik', + 'api_token_help' => 'Hier is je nieuwe persoonlijke toegangscode. Dit is de enige keer dat deze getoond wordt dus verlies deze niet! Je kan deze toegangscode nu gebruiken om API-aanvragen te maken.', + + 'api_oauth_clients' => 'Jouw OAuth clients', + 'api_oauth_clients_desc' => 'Hier kun je jouw eigen OAuth clients registreren.', + 'api_oauth_clients_desc2' => 'Gebruik deze client-id om een nieuwe token aan te vragen en autorisatiecodes om te zetten naar tokens. Zie Laravel Passport documentatie voor meer informatie.', + 'api_oauth_title' => 'OAuth Clients', + 'api_oauth_create_new' => 'Nieuwe client aanmaken', + 'api_oauth_edit' => 'Client bewerken', + 'api_oauth_not_created' => 'Je hebt nog geen OAuth-clients aangemaakt.', + 'api_oauth_clientid' => 'Client-ID', + 'api_oauth_name' => 'Naam', + 'api_oauth_name_help' => 'Iets dat jouw gebruikers herkennen en kunnen vertrouwen.', + 'api_oauth_secret' => 'Secret', + 'api_oauth_create' => 'Client aanmaken', + 'api_oauth_redirecturl' => 'Redirect-URL', + 'api_oauth_redirecturl_help' => 'De authorisatie-callback-url van jouw applicatie.', + + 'api_authorized_clients' => 'Lijst van geautoriseerde clients', + 'api_authorized_clients_desc' => 'Deze lijst bevat alle Clients die je hebt gemachtigd om toegang te krijgen tot je applicatie. Je kan deze machtiging te allen tijde intrekken.', + 'api_authorized_clients_title' => 'Geautoriseerde applicaties', + 'api_authorized_clients_none' => 'Er zijn nog geen geautoriseerde clients.', + 'api_authorized_clients_name' => 'Naam', + 'api_authorized_clients_scopes' => 'Bereik', + + 'personalization_tab_title' => 'Personaliseer je account', + + 'personalization_title' => 'Here you will find different settings to configure your account. These features are intended for “power users” who want maximum control over Monica.', + 'personalization_contact_field_type_title' => 'Contactvelden', + 'personalization_contact_field_type_add' => 'Nieuw type contactveld toevoegen', + 'personalization_contact_field_type_description' => 'You can configure all the different types of contact fields that you can associate to all your contacts. For example, if a new social network appears in the future, you will be able to add this new way of communicating with your contacts right here.', + 'personalization_contact_field_type_table_name' => 'Naam', + 'personalization_contact_field_type_table_protocol' => 'Protocol', + 'personalization_contact_field_type_table_actions' => 'Acties', + 'personalization_contact_field_type_modal_title' => 'Nieuw soort contactveld toevoegen', + 'personalization_contact_field_type_modal_edit_title' => 'Een bestaand type contactveld bewerken', + 'personalization_contact_field_type_modal_delete_title' => 'Een bestaand type contactveld verwijderen', + 'personalization_contact_field_type_modal_delete_description' => 'Are you sure you want to delete this contact field type? Deleting this type of contact field will delete ALL the data with this type for all of your contacts.', + 'personalization_contact_field_type_modal_name' => 'Naam', + 'personalization_contact_field_type_modal_protocol' => 'Protocol (optioneel)', + 'personalization_contact_field_type_modal_protocol_help' => 'Als er een protocol voor een type contactveld is ingesteld wordt bij een muisklik op dat veld de actie uitgevoerd die bij dat protocol hoort.', + 'personalization_contact_field_type_modal_icon' => 'Pictogram (optioneel)', + 'personalization_contact_field_type_modal_icon_help' => 'Je kan een pictogram koppelen aan dit type contactveld. Het moet een referentie zijn naar een Font Awesome pictogram.', + 'personalization_contact_field_type_delete_success' => 'The contact field type has been successfully deleted.', + 'personalization_contact_field_type_add_success' => 'Het type contactveld is met succes toegevoegd.', + 'personalization_contact_field_type_edit_success' => 'Het type contactveld is met succes bijgewerkt.', + + 'personalization_genders_title' => 'Genderidentiteiten', + 'personalization_genders_add' => 'Nieuwe genderidentiteit toevoegen', + 'personalization_genders_desc' => 'Je kan zoveel genderidentiteiten definiëren als je nodig acht. Je hebt ten minste één genderidentiteit nodig in je account.', + 'personalization_genders_modal_add' => 'Genderidentiteit toevoegen', + 'personalization_genders_modal_edit' => 'Genderidentiteit bewerken', + 'personalization_genders_modal_name' => 'Naam', + 'personalization_genders_modal_name_help' => 'De titel voor het aangeven van het geslacht op een contactpagina.', + 'personalization_genders_modal_sex' => 'Geslacht', + 'personalization_genders_modal_sex_help' => 'Wordt gebruikt voor het definiëren van relaties, o.a. tijdens het importeren en exporteren van VCards.', + 'personalization_genders_modal_default' => 'Selecteer het standaardgeslacht voor nieuwe contacten', + 'personalization_genders_modal_delete' => 'Genderidentiteit verwijderen', + 'personalization_genders_modal_delete_desc' => 'Weet u zeker dat je het geslacht "{name} " wilt verwijderen?', + 'personalization_genders_modal_delete_question' => 'You currently have {count} contact with this gender. If you delete this gender, what gender should this contact have?|You currently have {count} contacts with this gender. If you delete this gender, what gender should these contacts have?', + 'personalization_genders_modal_delete_question_default' => 'This gender is the default one. If you delete this gender, which one will be the new default?', + 'personalization_genders_modal_error' => 'Kies een geslacht uit de lijst.', + 'personalization_genders_list_contact_number' => '{count} contact|{count} contacten', + 'personalization_genders_table_name' => 'Titel', + 'personalization_genders_table_sex' => 'Geslacht', + 'personalization_genders_table_default' => 'Standaard', + 'personalization_genders_default' => 'Standaardgeslacht', + 'personalization_genders_make_default' => 'Standaardgeslacht wijzigen', + 'personalization_genders_select_default' => 'Selecteer standaardgeslacht', + 'personalization_genders_m' => 'Man', + 'personalization_genders_f' => 'Vrouw', + 'personalization_genders_o' => 'Anders', + 'personalization_genders_u' => 'Onbekend', + 'personalization_genders_n' => 'Geen of niet van toepassing', + + 'personalization_reminder_rule_save' => 'De instellingen zijn opgeslagen', + 'personalization_reminder_rule_title' => 'Herinneringen', + 'personalization_reminder_rule_line' => '{count} dag ervoor|{count} dagen ervoor', + 'personalization_reminder_rule_desc' => 'For every reminder that you set, Monica can send you an email a number of days before the event happens. You can adjust these notification settings here. These notifications only apply to monthly and yearly reminders.', + + 'personalization_module_save' => 'De wijziging is opgeslagen', + 'personalization_module_title' => 'Functionaliteiten', + 'personalization_module_desc' => 'You may not need all of Monica’s features. Below you can toggle specific features that are used on a contact sheet. This change will affect ALL your contacts. Turning off a feature does not delete any data, it simply hides the feature.', + + 'personalisation_paid_upgrade' => 'Dit is een betaalde functionaliteit en vereist dat je een betaald abonnement hebt. Upgrade je account door naar Instellingen > Abonnement te gaan.', + 'personalisation_paid_upgrade_vue' => 'Dit is een betaalde functionaliteit en vereist dat je een betaald abonnement hebt. Upgrade je account door naar Instellingen > Abonnement te gaan.', + + 'reminder_time_to_send' => 'Tijd van de dag herinneringen zullen worden verzonden', + 'reminder_time_to_send_help' => 'Uw volgende herinnering is gepland te worden verzonden op {dateTime}.', + + 'personalization_activity_type_category_title' => 'Activiteit-categorieën', + 'personalization_activity_type_category_add' => 'Voeg een nieuwe categorie activiteiten toe', + 'personalization_activity_type_category_table_name' => 'Naam', + 'personalization_activity_type_category_description' => 'An activity with one of your contacts can have a type and a category type. Your account comes with a set of predefined category types by default, but you can customize these here.', + 'personalization_activity_type_category_table_actions' => 'Acties', + 'personalization_activity_type_category_modal_add' => 'Voeg een nieuwe categorie activiteiten toe', + 'personalization_activity_type_category_modal_edit' => 'Bewerk een categorie activiteiten', + 'personalization_activity_type_category_modal_question' => 'Hoe moeten we deze nieuwe categorie noemen?', + 'personalization_activity_type_add_button' => 'Nieuw type activiteit toevoegen', + 'personalization_activity_type_modal_add' => 'Nieuw type activiteit toevoegen', + 'personalization_activity_type_modal_question' => 'Hoe moeten we deze nieuwe type activiteit noemen?', + 'personalization_activity_type_modal_edit' => 'Bewerk type activiteit', + 'personalization_activity_type_category_modal_delete' => 'Verwijder een categorie activiteiten', + 'personalization_activity_type_category_modal_delete_desc' => 'Are you sure you want to delete this category? Deleting it will delete all associated activity types. Activities that belong to this category will not be affected by this deletion.', + 'personalization_activity_type_modal_delete' => 'Verwijder type activiteit', + 'personalization_activity_type_modal_delete_desc' => 'Weet je zeker dat je dit activiteitstype wil verwijderen? Activiteiten die behoren tot dit type worden niet beïnvloed door deze verwijdering.', + 'personalization_activity_type_modal_delete_error' => 'We kunnen dit type activiteit niet vinden.', + 'personalization_activity_type_category_modal_delete_error' => 'We kunnen deze categorie niet vinden.', + + 'personalization_life_event_category_title' => 'Levensgebeurtenis categorieën', + 'personalization_live_event_category_table_name' => 'Naam', + 'personalization_life_event_category_description' => 'A life event can have a type and a category. Your account comes with a set of predefined categories and types by default, but you can customize life event types here.', + 'personalization_live_event_category_table_actions' => 'Acties', + 'personalization_life_event_type_add_button' => 'Een nieuw levensgebeurtenis type toevoegen', + 'personalization_life_event_type_modal_add' => 'Een nieuw levensgebeurtenis type toevoegen', + 'personalization_life_event_type_modal_question' => 'Hoe moeten we dit nieuwe levensevenement noemen?', + 'personalization_life_event_type_modal_edit' => 'Bewerk een levensgebeurtenis type', + 'personalization_life_event_type_modal_delete' => 'Verwijder levensgebeurtenis type', + 'personalization_life_event_type_modal_delete_desc' => 'Are you sure you want to delete this life event type? Life events that belong to this type will be deleted by performing this action.', + 'personalization_life_event_type_modal_delete_error' => 'We can’t find this life event type.', + + 'personalization_life_event_category_work_education' => 'Werk & onderwijs', + 'personalization_life_event_category_family_relationships' => 'Familie & relaties', + 'personalization_life_event_category_home_living' => 'Huis & leven', + 'personalization_life_event_category_travel_experiences' => 'Reizen & ervaringen', + 'personalization_life_event_category_health_wellness' => 'Gezondheid & welzijn', + + 'personalization_life_event_type_new_job' => 'Nieuwe baan', + 'personalization_life_event_type_retirement' => 'Pensioen', + 'personalization_life_event_type_new_school' => 'Nieuwe school', + 'personalization_life_event_type_study_abroad' => 'Studie in het buitenland', + 'personalization_life_event_type_volunteer_work' => 'Vrijwilligerswerk', + 'personalization_life_event_type_published_book_or_paper' => 'Publicatie van boek of artikel', + 'personalization_life_event_type_military_service' => 'Militaire dienst', + 'personalization_life_event_type_first_met' => 'Eerste ontmoeting', + 'personalization_life_event_type_new_relationship' => 'Nieuwe relatie', + 'personalization_life_event_type_engagement' => 'Verloving', + 'personalization_life_event_type_marriage' => 'Huwelijk', + 'personalization_life_event_type_anniversary' => 'Jubileum', + 'personalization_life_event_type_expecting_a_baby' => 'Verwacht een baby', + 'personalization_life_event_type_new_child' => 'Nieuw kind', + 'personalization_life_event_type_new_family_member' => 'Nieuw gezinslid', + 'personalization_life_event_type_new_pet' => 'Nieuw huisdier', + 'personalization_life_event_type_end_of_relationship' => 'Einde van relatie', + 'personalization_life_event_type_loss_of_a_loved_one' => 'Verlies van een dierbare', + 'personalization_life_event_type_moved' => 'Verhuisd', + 'personalization_life_event_type_bought_a_home' => 'Huis gekocht', + 'personalization_life_event_type_home_improvement' => 'Verbouwing', + 'personalization_life_event_type_holidays' => 'Vakantie', + 'personalization_life_event_type_new_vehicle' => 'Nieuw voertuig', + 'personalization_life_event_type_new_roommate' => 'Nieuwe kamergenoot', + 'personalization_life_event_type_overcame_an_illness' => 'Overwon een ziekte', + 'personalization_life_event_type_quit_a_habit' => 'Gestopt met gewoonte', + 'personalization_life_event_type_new_eating_habits' => 'Nieuw voedingspatroon', + 'personalization_life_event_type_weight_loss' => 'Gewichtsverlies', + 'personalization_life_event_type_wear_glass_or_contact' => 'Begonnen met dragen van bril of contactlenzen', + 'personalization_life_event_type_broken_bone' => 'Bot gebroken', + 'personalization_life_event_type_removed_braces' => 'Had braces removed', + 'personalization_life_event_type_surgery' => 'Operatie ondergaan', + 'personalization_life_event_type_dentist' => 'Had dental treatment', + 'personalization_life_event_type_new_sport' => 'Begonnen met het spelen van een nieuwe sport', + 'personalization_life_event_type_new_hobby' => 'Took up a new hobby', + 'personalization_life_event_type_new_instrument' => 'Begonnen met leren van een nieuw instrument', + 'personalization_life_event_type_new_language' => 'Begonnen met leren van een nieuwe taal', + 'personalization_life_event_type_tattoo_or_piercing' => 'Tatoeage of piercing', + 'personalization_life_event_type_new_license' => 'Nieuw diploma', + 'personalization_life_event_type_travel' => 'Reizen', + 'personalization_life_event_type_achievement_or_award' => 'Prestatie of prijs', + 'personalization_life_event_type_changed_beliefs' => 'Van overtuiging veranderd', + 'personalization_life_event_type_first_word' => 'Eerste woord', + 'personalization_life_event_type_first_kiss' => 'Eerste kus', + + 'storage_title' => 'Opslag', + 'storage_account_info' => 'Je accountlimit is :accountlimit MB. Je huidige gebruik is :currentAccountSize MB (ongeveer :percentUsage%).', + 'storage_upgrade_notice' => 'Upgrade je account om documenten en foto\'s te kunnen uploaden.', + 'storage_description' => 'Hier kun je alle documenten en foto\'s zien die bij als bijlage bij je contacten zijn geüpload.', + + 'dav_title' => 'WebDAV', + 'dav_description' => 'Hier kun je alle instellingen vinden om WebDAV te gebruiken voor CardDAV en CalDAV export.', + 'dav_copy_help' => 'Naar klembord kopiëren', + 'dav_clipboard_copied' => 'Waarde gekopieerd naar klembord', + 'dav_url_base' => 'Basis-url voor alle CardDAV en CalDAV bronnen:', + 'dav_connect_help' => 'Je kan jouw contactpersonen en/of kalenders verbinden met deze basis-url op je telefoon of computer.', + 'dav_connect_help2' => 'Use your login (email) and create an API token as the password to authenticate.', + 'dav_url_carddav' => 'CardDAV url voor Contacten:', + 'dav_url_caldav_birthdays' => 'CalDAV url voor verjaardagen:', + 'dav_url_caldav_tasks' => 'CalDAV url voor taken:', + 'dav_title_carddav' => 'CardDAV', + 'dav_title_caldav' => 'CalDAV', + 'dav_carddav_export' => 'Alle contacten exporteren in één bestand', + 'dav_caldav_birthdays_export' => 'Alle verjaardagen exporteren in één bestand', + 'dav_caldav_tasks_export' => 'Alle taken exporteren in één bestand', + + 'archive_title' => 'Archiveer alle contacten in uw account', + 'archive_desc' => 'Dit zal alle contacten in uw account archiveren.', + 'archive_cta' => 'Archiveer al je contacten', + + 'logs_title' => 'Alles wat met dit account is gebeurd', + 'logs_actor' => 'Actor', + 'logs_timestamp' => 'Timestamp', + 'logs_description' => 'Beschrijving', + 'logs_subject' => 'Onderwerp', + 'logs_size' => 'Grootte (kB)', + 'logs_object' => 'Object', +]; diff --git a/resources/lang/nl/validation.php b/resources/lang/nl/validation.php new file mode 100644 index 0000000..08112e8 --- /dev/null +++ b/resources/lang/nl/validation.php @@ -0,0 +1,166 @@ + ':attribute moet geaccepteerd zijn.', + 'active_url' => ':attribute is geen geldige URL.', + 'after' => ':attribute moet een datum na :date zijn.', + 'after_or_equal' => ':attribute moet een datum na of gelijk aan :date zijn.', + 'alpha' => ':attribute mag alleen letters bevatten.', + 'alpha_dash' => ':attribute mag alleen letters, nummers, underscores (_) en streepjes (-) bevatten.', + 'alpha_num' => ':attribute mag alleen letters en nummers bevatten.', + 'array' => ':attribute moet geselecteerde elementen bevatten.', + 'before' => ':attribute moet een datum voor :date zijn.', + 'before_or_equal' => ':attribute moet een datum voor of gelijk aan :date zijn.', + 'between' => [ + 'numeric' => ':attribute moet tussen :min en :max zijn.', + 'file' => ':attribute moet tussen :min en :max kilobytes zijn.', + 'string' => ':attribute moet tussen :min en :max karakters zijn.', + 'array' => ':attribute moet tussen :min en :max items bevatten.', + ], + 'boolean' => ':attribute moet ja of nee zijn.', + 'confirmed' => ':attribute bevestiging komt niet overeen.', + 'date' => ':attribute moet een datum bevatten.', + 'date_equals' => ':attribute mag alleen letters, nummers, underscores (_) en streepjes (-) bevatten.', + 'date_format' => ':attribute moet een geldig datum formaat bevatten.', + 'different' => ':attribute en :other moeten verschillend zijn.', + 'digits' => ':attribute moet bestaan uit :digits cijfers.', + 'digits_between' => ':attribute moet bestaan uit minimaal :min en maximaal :max cijfers.', + 'dimensions' => ':attribute heeft geen geldige afmetingen voor afbeeldingen.', + 'distinct' => ':attribute heeft een dubbele waarde.', + 'email' => ':attribute is geen geldig e-mailadres.', + 'ends_with' => ':attribute moet met één van de volgende waarden eindigen: :values.', + 'exists' => ':attribute bestaat niet.', + 'file' => ':attribute moet een bestand zijn.', + 'filled' => ':attribute is verplicht.', + 'gt' => [ + 'numeric' => 'De :attribute moet groter zijn dan :value.', + 'file' => 'De :attribute moet groter zijn dan :value kilobytes.', + 'string' => 'De :attribute moet meer dan :value tekens bevatten.', + 'array' => 'De :attribute moet meer dan :value waardes bevatten.', + ], + 'gte' => [ + 'numeric' => 'De :attribute moet groter of gelijk zijn aan :value.', + 'file' => 'De :attribute moet groter of gelijk zijn aan :value kilobytes.', + 'string' => 'De :attribute moet minimaal :value tekens bevatten.', + 'array' => 'De :attribute moet :value waardes of meer bevatten.', + ], + 'image' => ':attribute moet een afbeelding zijn.', + 'in' => ':attribute is ongeldig.', + 'in_array' => ':attribute bestaat niet in :other.', + 'integer' => ':attribute moet een getal zijn.', + 'ip' => ':attribute moet een geldig IP-adres zijn.', + 'ipv4' => ':attribute moet een geldig IPv4-adres zijn.', + 'ipv6' => ':attribute moet een geldig IPv6-adres zijn.', + 'json' => ':attribute moet een geldige JSON-string zijn.', + 'lt' => [ + 'numeric' => 'De :attribute moet kleiner zijn dan :value.', + 'file' => 'De :attribute moet kleiner zijn dan :value kilobytes.', + 'string' => 'De :attribute moet minder dan :value tekens bevatten.', + 'array' => 'De :attribute moet minder dan :value waardes bevatten.', + ], + 'lte' => [ + 'numeric' => 'De :attribute moet kleiner of gelijk zijn aan :value.', + 'file' => 'De :attribute moet kleiner of gelijk zijn aan :value kilobytes.', + 'string' => 'De :attribute moet maximaal :value tekens bevatten.', + 'array' => 'De :attribute moet :value waardes of minder bevatten.', + ], + 'max' => [ + 'numeric' => ':attribute mag niet hoger dan :max zijn.', + 'file' => ':attribute mag niet meer dan :max kilobytes zijn.', + 'string' => ':attribute mag niet uit meer dan :max karakters bestaan.', + 'array' => ':attribute mag niet meer dan :max items bevatten.', + ], + 'mimes' => ':attribute moet een bestand zijn van het bestandstype :values.', + 'mimetypes' => ':attribute moet een bestand zijn van het bestandstype :values.', + 'min' => [ + 'numeric' => ':attribute moet minimaal :min zijn.', + 'file' => ':attribute moet minimaal :min kilobytes zijn.', + 'string' => ':attribute moet minimaal :min karakters zijn.', + 'array' => ':attribute moet minimaal :min items bevatten.', + ], + 'not_in' => 'Het formaat van :attribute is ongeldig.', + 'not_regex' => 'De :attribute formaat is ongeldig.', + 'numeric' => ':attribute moet een nummer zijn.', + 'password' => 'Het wachtwoord is incorrect.', + 'present' => ':attribute moet bestaan.', + 'regex' => ':attribute formaat is ongeldig.', + 'required' => ':attribute is verplicht.', + 'required_if' => ':attribute is verplicht indien :other gelijk is aan :value.', + 'required_unless' => ':attribute is verplicht tenzij :other gelijk is aan :values.', + 'required_with' => ':attribute is verplicht i.c.m. :values', + 'required_with_all' => ':attribute is verplicht i.c.m. :values.', + 'required_without' => ':attribute is verplicht als :values niet ingevuld is.', + 'required_without_all' => ':attribute is verplicht als :values niet ingevuld zijn.', + 'same' => ':attribute en :other moeten overeenkomen.', + 'size' => [ + 'numeric' => ':attribute moet :size zijn.', + 'file' => ':attribute moet :size kilobyte zijn.', + 'string' => ':attribute moet :size karakters zijn.', + 'array' => ':attribute moet :size items bevatten.', + ], + 'starts_with' => ':attribute moet starten met een van de volgende: :values.', + 'string' => ':attribute moet een tekenreeks zijn.', + 'timezone' => ':attribute moet een geldige tijdzone zijn.', + 'unique' => ':attribute is al in gebruik.', + 'uploaded' => 'Het uploaden van :attribute is mislukt.', + 'url' => ':attribute is geen geldige URL.', + 'uuid' => ':attribute moet een geldig UUID zijn.', + + /* + |-------------------------------------------------------------------------- + | Custom Validation Language Lines + |-------------------------------------------------------------------------- + | + | Here you may specify custom validation messages for attributes using the + | convention "attribute.rule" to name the lines. This makes it quick to + | specify a specific custom language line for a given attribute rule. + | + */ + + 'custom' => [ + 'attribute-name' => [ + 'rule-name' => 'custom-message', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Custom Validation Attributes + |-------------------------------------------------------------------------- + | + | The following language lines are used to swap our attribute placeholder + | with something more reader friendly such as "E-Mail Address" instead + | of "email". This simply helps us make our message more expressive. + | + */ + + 'attributes' => [], + + 'vue' => [ + 'max' => [ + 'numeric' => '{field} mag niet groter zijn dan {max}.', + 'string' => '{field} mag niet uit meer dan {max} karakters bestaan.', + ], + 'required' => '{field} is vereist.', + 'url' => '{field} is geen geldige URL.', + ], + +]; diff --git a/resources/lang/no.json b/resources/lang/no.json new file mode 100644 index 0000000..ddea72e --- /dev/null +++ b/resources/lang/no.json @@ -0,0 +1,7 @@ +{ + "The :attribute must contain at least one uppercase and one lowercase letter.": "The :attribute must contain at least one uppercase and one lowercase letter.", + "The :attribute must contain at least one letter.": "The :attribute must contain at least one letter.", + "The :attribute must contain at least one symbol.": "The :attribute must contain at least one symbol.", + "The :attribute must contain at least one number.": "The :attribute must contain at least one number.", + "The given :attribute has appeared in a data leak. Please choose a different :attribute.": "The given :attribute has appeared in a data leak. Please choose a different :attribute." +} diff --git a/resources/lang/no/app.php b/resources/lang/no/app.php new file mode 100644 index 0000000..64e3ac3 --- /dev/null +++ b/resources/lang/no/app.php @@ -0,0 +1,571 @@ + 'Ja', + 'no' => 'Nei', + 'update' => 'Oppdater', + 'save' => 'Lagre', + 'add' => 'Legg til', + 'cancel' => 'Avbryt', + 'confirm' => 'Bekreft', + 'delete_confirm' => 'Er du sikker på at du vil slette?', + 'delete' => 'Slett', + 'edit' => 'Endre', + 'upload' => 'Last opp', + 'download' => 'Last ned', + 'save_close' => 'Lagre og lukk', + 'close' => 'Lukk', + 'copy' => 'Kopier', + 'create' => 'Opprett', + 'remove' => 'Fjern', + 'revoke' => 'Tilbakekall', + 'done' => 'Utført', + 'back' => 'Tilbake', + 'verify' => 'Bekreft', + 'new' => 'ny', + 'unknown' => 'Jeg vet ikke', + 'load_more' => 'Last flere', + 'loading' => 'Laster inn…', + 'with' => 'med', + 'today' => 'i dag', + 'yesterday' => 'i går', + 'another_day' => 'en annen dag', + 'date' => 'Dato', + 'type' => 'Type', + 'zoom' => 'Zoom', + 'upgrade' => 'Oppgrader for å låse opp', + 'percent_uploaded' => '{percent} % lastet opp', + 'retry' => 'Prøv igjen', + 'filter' => 'Filtrer listen', + 'go_back' => 'Tilbake', + 'file_selected' => 'Én fil valgt…|{count} filer valgt…', + + 'application_title' => 'Monica – personlig relasjonsassistent', + 'application_description' => 'Monica er et verktøy for å holde oversikt over forholdet til dine kjæreste, venner, og familie.', + 'application_og_title' => 'Få bedre forhold til dine kjære. Gratis CRM for venner og familie.', + + 'markdown_description' => 'Vil du pynte på teksten din? Vi støtter Markdown for å legge til fete typer, kursiv, lister, og mer.', + 'markdown_link' => 'Les dokumentasjon', + + 'header_settings_link' => 'Innstillinger', + 'header_logout_link' => 'Logg av', + 'header_changelog_link' => 'Produktendringer', + + 'main_nav_cta' => 'Legg til personer', + 'main_nav_dashboard' => 'Oversikt', + 'main_nav_family' => 'Kontakter', + 'main_nav_journal' => 'Journal', + 'main_nav_activities' => 'Aktiviteter', + 'main_nav_tasks' => 'Oppgaver', + + 'footer_remarks' => 'Kommentarer?', + 'footer_send_email' => 'Send oss en e-post', + 'footer_privacy' => 'Personvernerklæring', + 'footer_release' => 'Merknader om programvareutgaven', + 'footer_newsletter' => 'Nyhetsbrev', + 'footer_source_code' => 'Bidra', + 'footer_version' => 'Versjon: :version', + 'footer_new_version' => 'En ny versjon av Monica er tilgjengelig', + + 'footer_modal_version_whats_new' => 'Hva er nytt', + 'footer_modal_version_release_away' => 'Du er 1 versjon bak den siste tilgjengelige versjonen. Du burde oppdatere ditt system.|Du er :number versjoner bak den siste tilgjengelige versjonen. Du burde oppdatere ditt system.', + + 'breadcrumb_dashboard' => 'Oversikt', + 'breadcrumb_list_contacts' => 'Kontakter', + 'breadcrumb_archived_contacts' => 'Arkiverte kontakter', + 'breadcrumb_journal' => 'Dagbok', + 'breadcrumb_settings' => 'Innstillinger', + 'breadcrumb_settings_export' => 'Eksporter', + 'breadcrumb_settings_users' => 'Brukere', + 'breadcrumb_settings_users_add' => 'Legg til bruker', + 'breadcrumb_settings_subscriptions' => 'Abonner', + 'breadcrumb_settings_import' => 'Importer', + 'breadcrumb_settings_import_report' => 'Importrapport', + 'breadcrumb_settings_import_upload' => 'Last opp', + 'breadcrumb_settings_tags' => 'Tagger', + 'breadcrumb_add_significant_other' => 'Legg til kjæreste', + 'breadcrumb_edit_significant_other' => 'Rediger kjæreste', + 'breadcrumb_add_note' => 'Legg til notat', + 'breadcrumb_edit_note' => 'Rediger notat', + 'breadcrumb_api' => 'API', + 'breadcrumb_dav' => 'DAV-ressurser', + 'breadcrumb_edit_introductions' => 'Hvordan møttes dere', + 'breadcrumb_settings_personalization' => 'Tilpass', + 'breadcrumb_settings_security' => 'Sikkerhet', + 'breadcrumb_settings_security_2fa' => 'To-faktor-autentisering', + 'breadcrumb_profile' => 'Profil for :name', + + 'gender_male' => 'Mann', + 'gender_female' => 'Kvinne', + 'gender_none' => 'Vil ikke oppgi', + 'gender_no_gender' => 'Ingen kjønn', + + 'error_title' => 'Oops! Noe gikk galt.', + 'error_unauthorized' => 'Du har ikke rettigheter til å redigere dette.', + 'error_user_account' => 'Denne brukeren tilhører ikke den oppgitte kontoen.', + 'error_save' => 'Lagringen av opplysninger feilet.', + 'error_try_again' => 'Noe gikk galt. Prøv igjen.', + 'error_id' => 'Feilmelding: :id', + 'error_unavailable' => 'Tjenesten er ikke tilgjengelig', + 'error_maintenance' => 'Vedlikehold pågår. Vi er straks tilbake.', + 'error_help' => 'Vi er straks tilbake.', + 'error_twitter' => 'Følg oss på Twitter for å få beskjed om når vi er tilbake.', + 'error_no_term' => 'Det finnes ingen policy for denne instansen ennå.', + + 'default_save_success' => 'Data er lagret.', + + 'compliance_title' => 'Beklager avbrytelsen.', + 'compliance_desc' => 'Vi har endret våre brukervilkår og personvernerklæringen. Vi er pålagt å be deg om å gå gjennom dem og samtykke før du kan fortsette å bruke kontoen din.', + 'compliance_desc_end' => 'Vi gjør ikke noe ondsinnet med dine opplysninger eller din konto, og vi vil heller aldri gjøre det.', + 'compliance_terms' => 'Godta nye brukervilkår og personvernpolicy', + + // Relationship types + // Yes, each relationship type has 8 strings associated with it. + // This is because we need to indicate the name of the relationship type, + // and also the name of the opposite side of this relationship (father/son), + // and then, the feminine version of the string. Finally, in some sentences + // in the UI, we need to include the name of the person we add the relationship + // to. + 'relationship_type_group_love' => 'Kjærlighetsforhold', + 'relationship_type_group_family' => 'Familieforhold', + 'relationship_type_group_friend' => 'Venneforhold', + 'relationship_type_group_work' => 'Kollegaforhold', + 'relationship_type_group_other' => 'Andre typer forhold', + + 'relationship_type_partner' => 'kjæreste', + 'relationship_type_partner_female' => 'kjæreste', + 'relationship_type_partner_male' => 'significant other', + 'relationship_type_partner_with_name' => 'kjæresten til :name', + 'relationship_type_partner_female_with_name' => 'kjæresten til :name', + 'relationship_type_partner_male_with_name' => ':name’s significant other', + + 'relationship_type_spouse' => 'ektefelle', + 'relationship_type_spouse_female' => 'kone', + 'relationship_type_spouse_male' => 'mann', + 'relationship_type_spouse_with_name' => 'ektefellen til :name', + 'relationship_type_spouse_female_with_name' => ':name’s kone', + 'relationship_type_spouse_male_with_name' => ':name\'s mann', + + 'relationship_type_date' => 'date', + 'relationship_type_date_female' => 'date', + 'relationship_type_date_male' => 'date', + 'relationship_type_date_with_name' => 'date med :name', + 'relationship_type_date_female_with_name' => 'date med :name', + 'relationship_type_date_male_with_name' => ':name’s date', + + 'relationship_type_lover' => 'elsker', + 'relationship_type_lover_female' => 'elskerinne', + 'relationship_type_lover_male' => 'lover', + 'relationship_type_lover_with_name' => 'elskeren til :name', + 'relationship_type_lover_female_with_name' => 'elskerinnen til :name', + 'relationship_type_lover_male_with_name' => ':name’s lover', + + 'relationship_type_inlovewith' => 'forelsket i', + 'relationship_type_inlovewith_female' => 'forelsket i', + 'relationship_type_inlovewith_male' => 'in love with', + 'relationship_type_inlovewith_with_name' => 'noen :name er forelsket i', + 'relationship_type_inlovewith_female_with_name' => 'noen :name er forelsket i', + 'relationship_type_inlovewith_male_with_name' => 'someone :name is in love with', + + 'relationship_type_lovedby' => 'elsket av', + 'relationship_type_lovedby_female' => 'elsket av', + 'relationship_type_lovedby_male' => 'loved by', + 'relationship_type_lovedby_with_name' => ':name sin hemmelige elsker', + 'relationship_type_lovedby_female_with_name' => ':name sin hemmelige elskerinne', + 'relationship_type_lovedby_male_with_name' => ':name’s secret lover', + + 'relationship_type_ex' => 'ex-partner', + 'relationship_type_ex_female' => 'ekskjæreste', + 'relationship_type_ex_male' => 'ekskjæreste', + 'relationship_type_ex_with_name' => ':name’s sin ex-partner', + 'relationship_type_ex_female_with_name' => 'ekskjæresten til :name', + 'relationship_type_ex_male_with_name' => 'ekskjæresten til :name’s', + + 'relationship_type_parent' => 'foreldre', + 'relationship_type_parent_female' => 'mor', + 'relationship_type_parent_male' => 'far', + 'relationship_type_parent_with_name' => ':name’s sin forelder', + 'relationship_type_parent_female_with_name' => 'mor til :name', + 'relationship_type_parent_male_with_name' => ':name’s far', + + 'relationship_type_child' => 'barn', + 'relationship_type_child_female' => 'datter', + 'relationship_type_child_male' => 'sønn', + 'relationship_type_child_with_name' => ':name’s barn', + 'relationship_type_child_female_with_name' => 'datter til :name', + 'relationship_type_child_male_with_name' => ':name’s sin sønn', + + 'relationship_type_stepparent' => 'ste-forelder', + 'relationship_type_stepparent_female' => 'stemor', + 'relationship_type_stepparent_male' => 'stefar', + 'relationship_type_stepparent_with_name' => ':name’s step-parent', + 'relationship_type_stepparent_female_with_name' => 'Stemoren til :name', + 'relationship_type_stepparent_male_with_name' => ':name’s stepfather', + + 'relationship_type_stepchild' => 'stebarn', + 'relationship_type_stepchild_female' => 'stedatter', + 'relationship_type_stepchild_male' => 'stesønn', + 'relationship_type_stepchild_with_name' => ':name’s stepchild', + 'relationship_type_stepchild_female_with_name' => 'Stedatteren til :name', + 'relationship_type_stepchild_male_with_name' => ':name’s stepson', + + 'relationship_type_sibling' => 'sibling', + 'relationship_type_sibling_female' => 'søster', + 'relationship_type_sibling_male' => 'bror', + 'relationship_type_sibling_with_name' => ':name’s sibling', + 'relationship_type_sibling_female_with_name' => 'Søsteren til :name', + 'relationship_type_sibling_male_with_name' => ':name’s sin bror', + + 'relationship_type_grandparent' => 'besteforelder', + 'relationship_type_grandparent_female' => 'bestemor', + 'relationship_type_grandparent_male' => 'bestefar', + 'relationship_type_grandparent_with_name' => ':name’s sin beseforelder', + 'relationship_type_grandparent_female_with_name' => ':name’s bestemor', + 'relationship_type_grandparent_male_with_name' => ':name’s bestemor', + + 'relationship_type_grandchild' => 'barnebarn', + 'relationship_type_grandchild_female' => 'granddaughter', + 'relationship_type_grandchild_male' => 'grandson', + 'relationship_type_grandchild_with_name' => ':name’s grandchild', + 'relationship_type_grandchild_female_with_name' => ':name’s granddauther', + 'relationship_type_grandchild_male_with_name' => ':name’s grandson', + + 'relationship_type_uncle' => 'onkel', + 'relationship_type_uncle_female' => 'tante', + 'relationship_type_uncle_male' => 'uncle', + 'relationship_type_uncle_with_name' => 'Onkelen til :name', + 'relationship_type_uncle_female_with_name' => 'Tanten til :name', + 'relationship_type_uncle_male_with_name' => ':name’s uncle', + + 'relationship_type_nephew' => 'nevø', + 'relationship_type_nephew_female' => 'niese', + 'relationship_type_nephew_male' => 'nephew', + 'relationship_type_nephew_with_name' => 'Nevøen til :name', + 'relationship_type_nephew_female_with_name' => 'Niesen til :name', + 'relationship_type_nephew_male_with_name' => ':name’s nephew', + + 'relationship_type_cousin' => 'fetter', + 'relationship_type_cousin_female' => 'kusine', + 'relationship_type_cousin_male' => 'cousin', + 'relationship_type_cousin_with_name' => 'Fetteren til :name', + 'relationship_type_cousin_female_with_name' => 'Kusinen til :name', + 'relationship_type_cousin_male_with_name' => ':name’s cousin', + + 'relationship_type_godfather' => 'godparent', + 'relationship_type_godfather_female' => 'gudmor', + 'relationship_type_godfather_male' => 'godfather', + 'relationship_type_godfather_with_name' => ':name’s godparent', + 'relationship_type_godfather_female_with_name' => 'Gudmoren til :name', + 'relationship_type_godfather_male_with_name' => ':name’s godfather', + + 'relationship_type_godson' => 'godchild', + 'relationship_type_godson_female' => 'gudbarn', + 'relationship_type_godson_male' => 'godson', + 'relationship_type_godson_with_name' => ':name’s godchild', + 'relationship_type_godson_female_with_name' => 'Gudbarnet til :name', + 'relationship_type_godson_male_with_name' => ':name’s godson', + + 'relationship_type_friend' => 'venn', + 'relationship_type_friend_female' => 'venninne', + 'relationship_type_friend_male' => 'friend', + 'relationship_type_friend_with_name' => 'Venn av :name', + 'relationship_type_friend_female_with_name' => 'Venn av :name', + 'relationship_type_friend_male_with_name' => ':name’s friend', + + 'relationship_type_bestfriend' => 'bestevenn', + 'relationship_type_bestfriend_female' => 'bestevenninne', + 'relationship_type_bestfriend_male' => 'best friend', + 'relationship_type_bestfriend_with_name' => 'Bestevenn av :name', + 'relationship_type_bestfriend_female_with_name' => 'Bestevenn av :name', + 'relationship_type_bestfriend_male_with_name' => ':name’s best friend', + + 'relationship_type_colleague' => 'kollega', + 'relationship_type_colleague_female' => 'kollega', + 'relationship_type_colleague_male' => 'colleague', + 'relationship_type_colleague_with_name' => 'Kollega av :name', + 'relationship_type_colleague_female_with_name' => 'Kollega av :name', + 'relationship_type_colleague_male_with_name' => ':name’s colleague', + + 'relationship_type_boss' => 'sjef', + 'relationship_type_boss_female' => 'sjef', + 'relationship_type_boss_male' => 'boss', + 'relationship_type_boss_with_name' => 'Sjefen til :name', + 'relationship_type_boss_female_with_name' => 'Sjefen til :name', + 'relationship_type_boss_male_with_name' => ':name’s boss', + + 'relationship_type_subordinate' => 'underordnet', + 'relationship_type_subordinate_female' => 'underordnet', + 'relationship_type_subordinate_male' => 'subordinate', + 'relationship_type_subordinate_with_name' => 'Underordnet til :name', + 'relationship_type_subordinate_female_with_name' => 'Underordnet til :name', + 'relationship_type_subordinate_male_with_name' => ':name’s subordinate', + + 'relationship_type_mentor' => 'mentor', + 'relationship_type_mentor_female' => 'mentor', + 'relationship_type_mentor_male' => 'mentor', + 'relationship_type_mentor_with_name' => 'Mentor til :name', + 'relationship_type_mentor_female_with_name' => 'Mentor til :name', + 'relationship_type_mentor_male_with_name' => ':name’s mentor', + + 'relationship_type_protege' => 'protégé', + 'relationship_type_protege_female' => 'protégé', + 'relationship_type_protege_male' => 'protégé', + 'relationship_type_protege_with_name' => ':name’s protégé', + 'relationship_type_protege_female_with_name' => ':name’s protégé', + 'relationship_type_protege_male_with_name' => ':name’s protégé', + + 'relationship_type_ex_husband' => 'ex-spouse', + 'relationship_type_ex_husband_female' => 'ekskone', + 'relationship_type_ex_husband_male' => 'eksmann', + 'relationship_type_ex_husband_with_name' => ':name’s ex-spouse', + 'relationship_type_ex_husband_female_with_name' => 'Ekskona til :name', + 'relationship_type_ex_husband_male_with_name' => ':name’s eksmann', + + // emotions + 'emotion_primary_love' => 'Kjærlighet', + 'emotion_primary_joy' => 'Glede', + 'emotion_primary_surprise' => 'Overraskelse', + 'emotion_primary_anger' => 'Sinne', + 'emotion_primary_sadness' => 'Sorg', + 'emotion_primary_fear' => 'Frykt', + + 'emotion_secondary_affection' => 'Affeksjon', + 'emotion_secondary_lust' => 'Lyst', + 'emotion_secondary_longing' => 'Lengsel', + 'emotion_secondary_cheerfulness' => 'Oppstemt', + 'emotion_secondary_zest' => 'Glede', + 'emotion_secondary_contentment' => 'Fornøyd', + 'emotion_secondary_pride' => 'Stolthet', + 'emotion_secondary_optimism' => 'Optimisme', + 'emotion_secondary_enthrallment' => 'Fengslet', + 'emotion_secondary_relief' => 'Lettet', + 'emotion_secondary_surprise' => 'Overraskelse', + 'emotion_secondary_irritation' => 'Irritasjon', + 'emotion_secondary_exasperation' => 'Irritasjon', + 'emotion_secondary_rage' => 'Raseri', + 'emotion_secondary_disgust' => 'Ekkel', + 'emotion_secondary_envy' => 'Misunnelse', + 'emotion_secondary_suffering' => 'Lidelse', + 'emotion_secondary_sadness' => 'Sorg', + 'emotion_secondary_disappointment' => 'Skuffelse', + 'emotion_secondary_shame' => 'Skam', + 'emotion_secondary_neglect' => 'Forsømt', + 'emotion_secondary_sympathy' => 'Sympati', + 'emotion_secondary_horror' => 'Skrekk', + 'emotion_secondary_nervousness' => 'Nervøsitet', + + 'emotion_adoration' => 'Beundring', + 'emotion_affection' => 'Affeksjon', + 'emotion_love' => 'Kjærlighet', + 'emotion_fondness' => 'Forkjærlighet', + 'emotion_liking' => 'Liker', + 'emotion_attraction' => 'Tiltrukket', + 'emotion_caring' => 'Omsorgsfull', + 'emotion_tenderness' => 'Ømhet', + 'emotion_compassion' => 'Medlidenhet', + 'emotion_sentimentality' => 'Sentimentalitet', + 'emotion_arousal' => 'Opphisset', + 'emotion_desire' => 'Begjær', + 'emotion_lust' => 'Lyst', + 'emotion_passion' => 'Lidenskapelig', + 'emotion_infatuation' => 'Blindt forelsket', + 'emotion_longing' => 'Lengsel', + 'emotion_amusement' => 'Fornøyd', + 'emotion_bliss' => 'Overlykkelig', + 'emotion_cheerfulness' => 'Gledet', + 'emotion_gaiety' => 'Munter', + 'emotion_glee' => 'Munter', + 'emotion_jolliness' => 'Overlykkelig', + 'emotion_joviality' => 'Jovial', + 'emotion_joy' => 'Glede', + 'emotion_delight' => 'Fryd', + 'emotion_enjoyment' => 'Nytelse', + 'emotion_gladness' => 'Gladhet', + 'emotion_happiness' => 'Lykke', + 'emotion_jubilation' => 'Jubel', + 'emotion_elation' => 'Oppspilt', + 'emotion_satisfaction' => 'Fornøyd', + 'emotion_ecstasy' => 'Ekstase', + 'emotion_euphoria' => 'Eufori', + 'emotion_enthusiasm' => 'Entusiasme', + 'emotion_zeal' => 'Iver', + 'emotion_zest' => 'Iver', + 'emotion_excitement' => 'Spenning', + 'emotion_thrill' => 'Spennende', + 'emotion_exhilaration' => 'Opprømt', + 'emotion_contentment' => 'Fornøyd', + 'emotion_pleasure' => 'Nytelse', + 'emotion_pride' => 'Stolthet', + 'emotion_eagerness' => 'Ivrig', + 'emotion_hope' => 'Håp', + 'emotion_optimism' => 'Optimisme', + 'emotion_enthrallment' => 'Fengslet', + 'emotion_rapture' => 'Henførelse', + 'emotion_relief' => 'Lettet', + 'emotion_amazement' => 'Overveldet', + 'emotion_surprise' => 'Overrasket', + 'emotion_astonishment' => 'Overraskelse', + 'emotion_aggravation' => 'Forsvær', + 'emotion_irritation' => 'Irritasjon', + 'emotion_agitation' => 'Agitert', + 'emotion_annoyance' => 'Irritert', + 'emotion_grouchiness' => 'Grettenhet', + 'emotion_grumpiness' => 'Grettenhet', + 'emotion_exasperation' => 'Oppbrakt', + 'emotion_frustration' => 'Frustrasjon', + 'emotion_anger' => 'Sinne', + 'emotion_rage' => 'Raseri', + 'emotion_outrage' => 'Skandaløs', + 'emotion_fury' => 'Furisk', + 'emotion_wrath' => 'Vrede', + 'emotion_hostility' => 'Fiendtlighet', + 'emotion_ferocity' => 'Innbitt', + 'emotion_bitterness' => 'Bitterhet', + 'emotion_hate' => 'Hat', + 'emotion_loathing' => 'Avsky', + 'emotion_scorn' => 'Forakt', + 'emotion_spite' => 'Uvilje', + 'emotion_vengefulness' => 'Hevnlyst', + 'emotion_dislike' => 'Misnøye', + 'emotion_resentment' => 'Bitterhet', + 'emotion_disgust' => 'Ekkel', + 'emotion_revulsion' => 'Frastøtende', + 'emotion_contempt' => 'Forakt', + 'emotion_envy' => 'Misunnelse', + 'emotion_jealousy' => 'Sjalusi', + 'emotion_agony' => 'Smerte', + 'emotion_suffering' => 'Lidelse', + 'emotion_hurt' => 'Vondt', + 'emotion_anguish' => 'Plaget', + 'emotion_depression' => 'Deprimert', + 'emotion_despair' => 'Fortvilet', + 'emotion_hopelessness' => 'Håpløshet', + 'emotion_gloom' => 'Dyster', + 'emotion_glumness' => 'Dysterhet', + 'emotion_sadness' => 'Tristhet', + 'emotion_unhappiness' => 'Ulykkelig', + 'emotion_grief' => 'Sorg', + 'emotion_sorrow' => 'Sorg', + 'emotion_woe' => 'Ve', + 'emotion_misery' => 'Miserere', + 'emotion_melancholy' => 'Melankoli', + 'emotion_dismay' => 'Vantro', + 'emotion_disappointment' => 'Skuffelse', + 'emotion_displeasure' => 'Misnøye', + 'emotion_guilt' => 'Skyld', + 'emotion_shame' => 'Skam', + 'emotion_regret' => 'Anger', + 'emotion_remorse' => 'Anger', + 'emotion_alienation' => 'Fremmedgjort', + 'emotion_isolation' => 'Isolert', + 'emotion_neglect' => 'Forsømt', + 'emotion_loneliness' => 'Ensomhet', + 'emotion_rejection' => 'Avvisning', + 'emotion_homesickness' => 'Hjemlengsel', + 'emotion_defeat' => 'Beseiret', + 'emotion_dejection' => 'Nedstemt', + 'emotion_insecurity' => 'Usikkerhet', + 'emotion_embarrassment' => 'Flauhet', + 'emotion_humiliation' => 'Ydmykelse', + 'emotion_insult' => 'Fornærmelse', + 'emotion_pity' => 'Medynk', + 'emotion_sympathy' => 'Sympati', + 'emotion_alarm' => 'Bekymret', + 'emotion_shock' => 'Sjokk', + 'emotion_fear' => 'Frykt', + 'emotion_fright' => 'Skrekk', + 'emotion_horror' => 'Skrekk', + 'emotion_terror' => 'Terror', + 'emotion_panic' => 'Panikk', + 'emotion_hysteria' => 'Hysteri', + 'emotion_mortification' => 'Fortred', + 'emotion_anxiety' => 'Engstelig', + 'emotion_nervousness' => 'Nervøs', + 'emotion_tenseness' => 'Spent', + 'emotion_uneasiness' => 'Ubehag', + 'emotion_apprehension' => 'Usikkerhet', + 'emotion_worry' => 'Bekymret', + 'emotion_distress' => 'Nød', + 'emotion_dread' => 'Frykt', + + // weather + 'weather_sunny' => 'Sol', + 'weather_clear' => 'Klart', + 'weather_clear-day' => 'Klart', + 'weather_clear-night' => 'Klart (natt)', + 'weather_light-drizzle' => 'Lett yr', + 'weather_patchy-light-drizzle' => 'Stedvis lett yr', + 'weather_patchy-light-rain' => 'Stedvis lett regn', + 'weather_light-rain' => 'Lett regn', + 'weather_moderate-rain-at-times' => 'Moderat regn til tider', + 'weather_moderate-rain' => 'Moderat regn', + 'weather_patchy-rain-possible' => 'Stedvis regn mulig', + 'weather_heavy-rain-at-times' => 'Kraftig regn til tider', + 'weather_heavy-rain' => 'Kraftig regn', + 'weather_light-freezing-rain' => 'Lett underkjølt regn', + 'weather_moderate-or-heavy-freezing-rain' => 'Moderat eller mye underkjølt regn', + 'weather_light-sleet' => 'Lett sludd', + 'weather_moderate-or-heavy-rain-shower' => 'Moderate eller kraftige regnbyger', + 'weather_light-rain-shower' => 'Lette regnbyger', + 'weather_torrential-rain-shower' => 'Styrtregn', + 'weather_rain' => 'Regn', + 'weather_snow' => 'Snø', + 'weather_blowing-snow' => 'Snødrev', + 'weather_patchy-light-snow' => 'Stedvis lett snøvær', + 'weather_light-snow' => 'Lett snøvær', + 'weather_patchy-moderate-snow' => 'Stedvis moderat snøvær', + 'weather_moderate-snow' => 'Moderat snøvær', + 'weather_patchy-heavy-snow' => 'Stedvis kraftig snøvær', + 'weather_heavy-snow' => 'Kraftig snøvær', + 'weather_light-snow-showers' => 'Lette snøbyger', + 'weather_moderate-or-heavy-snow-showers' => 'Moderate eller kraftige snøbyger', + 'weather_patchy-snow-possible' => 'Stedvis regn mulig', + 'weather_patchy-sleet-possible' => 'Stedvis sludd mulig', + 'weather_moderate-or-heavy-sleet' => 'Moderat eller kraftig sludd', + 'weather_light-sleet-showers' => 'Lette sluddbyger', + 'weather_moderate-or-heavy-sleet-showers' => 'Moderate eller kraftige sluddbyger', + 'weather_sleet' => 'Sludd', + 'weather_wind' => 'Vind', + 'weather_fog' => 'Tåke', + 'weather_freezing-fog' => 'Tåke som fryser', + 'weather_mist' => 'Lett tåke', + 'weather_blizzard' => 'Snøstorm', + 'weather_overcast' => 'Overskyet', + 'weather_cloudy' => 'Overskyet', + 'weather_partly-cloudy-day' => 'Delvis overskyet', + 'weather_partly-cloudy-night' => 'Delvis overskyet', + 'weather_freezing-drizzle' => 'Underkjølt yr', + 'weather_heavy-freezing-drizzle' => 'Kraftig underkjølt yr', + 'weather_patchy-freezing-drizzle-possible' => 'Stedvis underkjølt yr mulig', + 'weather_ice-pellets' => 'Hagl', + 'weather_light-showers-of-ice-pellets' => 'Lette haglbyger', + 'weather_moderate-or-heavy-showers-of-ice-pellets' => 'Moderate eller kraftige haglbyger', + 'weather_thundery-outbreaks-possible' => 'Mulig torden utbrudd', + 'weather_patchy-light-rain-with-thunder' => 'Lett regn i området, med torden', + 'weather_moderate-or-heavy-rain-with-thunder' => 'Moderat eller kraftig regn i området, med torden', + 'weather_patchy-light-snow-with-thunder' => 'Lett snø i området, med torden', + 'weather_moderate-or-heavy-snow-with-thunder' => 'Moderat eller kraftig snø i området, med torden', + 'weather_current_temperature_celsius' => ':temperature °C', + 'weather_current_temperature_fahrenheit' => ':temperature °F', + 'weather_current_title' => 'Været', + + // dav + 'dav_contacts' => 'Kontakter', + 'dav_contacts_description' => 'Kontaktene til :name', + 'dav_birthdays' => 'Bursdager', + 'dav_birthdays_description' => 'Bursdager for kontaktene til :name', + 'dav_tasks' => 'Oppgaver', + 'dav_tasks_description' => 'Oppgavene til :name', + + // contact list + 'contact_list_avatar' => 'Profilbilde', + 'contact_list_name' => 'Kontakt', + 'contact_list_description' => 'Beskrivelse', + +]; diff --git a/resources/lang/no/auth.php b/resources/lang/no/auth.php new file mode 100644 index 0000000..57adcc1 --- /dev/null +++ b/resources/lang/no/auth.php @@ -0,0 +1,89 @@ + 'Brukernavn eller passord stemmer ikke.', + 'throttle' => 'Du har forsøkt å logge inn for mange ganger. Prøv igjen om :seconds sekunder.', + 'not_authorized' => 'Du har ikke tilgang', + 'signup_disabled' => 'Beklager, men nye registreringer er for tiden ikke tillatt', + 'signup_error' => 'Det oppstod en feil ved forsøk på å registrere brukeren', + 'back_homepage' => 'Tilbake til hjemmesiden', + 'mfa_auth_otp' => 'Autentiser med din to-faktor enhet', + 'mfa_auth_webauthn' => 'Autentiser med en sikkerhetsnøkkel (WebAuthn)', + '2fa_title' => 'To-faktor-autentisering', + '2fa_wrong_validation' => 'To-faktor autentisering mislyktes.', + '2fa_one_time_password' => 'Tofaktorautentiseringskode', + '2fa_recuperation_code' => 'Skriv inn din tofaktorgjenopprettingskode', + '2fa_one_time_or_recuperation' => 'Skriv inn en to-faktor autentiseringskode eller en gjenopprettingskode', + '2fa_otp_help' => 'Åpne din To-faktor autentiseringsapp og kopier koden', + + 'login_to_account' => 'Logg på kontoen din', + 'login_with_recovery' => 'Logg inn med en gjenopprettingskode', + 'login_again' => 'Vennligst logg inn på kontoen din igjen', + 'email' => 'E-post', + 'password' => 'Passord', + 'recovery' => 'Gjenopprettingskode', + 'login' => 'Logg på', + 'button_remember' => 'Husk meg', + 'password_forget' => 'Glemt passord?', + 'password_reset' => 'Tilbakestill passordet ditt', + 'use_recovery' => 'Eller du kan bruke en gjenopprettingskode', + 'signup_no_account' => 'Mangler du konto?', + 'signup' => 'Registrer deg', + 'create_account' => 'Opprett den første kontoen ved å registrere deg', + 'change_language_title' => 'Bytt språk:', + 'change_language' => 'Endre språk til :lang', + + 'password_reset_title' => 'Tilbakestill passord', + 'password_reset_email' => 'E-postadresse', + 'password_reset_send_link' => 'Send lenke for tilbakestilling av passord', + 'password_reset_password' => 'Passord', + 'password_reset_password_confirm' => 'Bekreft passord', + 'password_reset_action' => 'Tilbakestill passord', + 'password_reset_email_content' => 'Klikk her for å tilbakestille passordet:', + + 'register_title_welcome' => 'Velkommen til din nyinstallerte Monica-instans', + 'register_create_account' => 'Du må opprette en konto for å bruke Monica', + 'register_title_create' => 'Opprett konto', + 'register_login' => 'Logg inn hvis du allerede har en konto.', + 'register_email' => 'Oppgi en gyldig e-postadresse', + 'register_email_example' => 'du@hjem', + 'register_firstname' => 'Fornavn', + 'register_firstname_example' => 'f.eks. Jon', + 'register_lastname' => 'Etternavn', + 'register_lastname_example' => 'f.eks. Smith', + 'register_password' => 'Passord', + 'register_password_example' => 'Skriv inn et sikkert passord', + 'register_password_confirmation' => 'Passord (bekreft)', + 'register_action' => 'Registrer', + 'register_policy' => 'Registrering bekrefter at du har lest og godtar vår personvernerklæring og våre brukervilkår.', + 'register_invitation_email' => 'Av sikkerhetsgrunner kan du oppgi e-postadressen til personen som har invitert deg til å delta i denne kontoen. Denne informasjonen er gitt i invitasjonse-posten.', + + 'confirmation_title' => 'Verifiser din e-postadresse', + 'confirmation_fresh' => 'En bekreftelseslenke har blitt sendt til din e-postadresse.', + 'confirmation_check' => 'Før du fortsetter må du sjekke e-posten din for verifiseringslenken.', + 'confirmation_request_another' => 'Hvis du ikke mottok e-posten, klikk her for å be om en ny.', + + 'confirmation_again' => 'Hvis du vil endre din e-postadresse kan du klikke her.', + 'email_change_current_email' => 'Nåværende e-postadresse:', + 'email_change_title' => 'Endre e-postadresse', + 'email_change_new' => 'Ny e-postadresse', + 'email_changed' => 'Din e-postadresse har blitt endret. Sjekk din e-postkasse for å validere den nye adressen.', +]; diff --git a/resources/lang/no/changelog.php b/resources/lang/no/changelog.php new file mode 100644 index 0000000..49423d5 --- /dev/null +++ b/resources/lang/no/changelog.php @@ -0,0 +1,12 @@ + 'Produktendringer', + 'note' => 'Merk: Denne siden er dessverre bare på engelsk.', +]; diff --git a/resources/lang/no/dashboard.php b/resources/lang/no/dashboard.php new file mode 100644 index 0000000..a49548b --- /dev/null +++ b/resources/lang/no/dashboard.php @@ -0,0 +1,42 @@ + 'Velkommen til din konto!', + 'dashboard_blank_description' => 'Monica er applikasjonen hvor du kan organisere alt du har å gjøre med personene du bryr deg om.', + 'dashboard_blank_cta' => 'Legg til din første kontakt', + 'dashboard_blank_illustration' => 'Illustrasjon av Freepik', + + 'notes_title' => 'Du har ingen uthevede notater ennå.', + + 'tab_recent_calls' => 'Nylige anrop', + 'tab_favorite_notes' => 'Favorittnotater', + 'tab_calls_blank' => 'Du har ikke logget noen samtaler enda.', + 'tab_debts' => 'Gjeld', + 'tab_debts_blank' => 'Du har ikke loggført noen gjeld ennå.', + 'tab_tasks' => 'Oppgaver', + 'tab_tasks_blank' => 'Du har ingen oppgaver enda.', + + 'tasks_add_task_placeholder' => 'Hva handler denne oppgaven om?', + 'tasks_tab_your_contacts' => 'Oppgaver knyttet til kontaktene dine', + 'tasks_tab_your_tasks' => 'Dine oppgaver', + 'tasks_add_note' => 'Trykk Enter for å legge til oppgave.', + 'task_add_cta' => 'Ny oppgave', + + 'debts_you_owe' => 'Du skylder', + + 'statistics_contacts' => 'Kontakter', + 'statistics_activities' => 'Aktiviteter', + 'statistics_gifts' => 'Gaver', + + 'reminders_next_months' => 'Hendelser de neste 3 månedene', + 'reminders_none' => 'Ingen påminnelser for denne måneden.', + + 'product_changes' => 'Produktendringer', + 'product_view_details' => 'Vis detaljer', +]; diff --git a/resources/lang/no/format.php b/resources/lang/no/format.php new file mode 100644 index 0000000..8cc99c9 --- /dev/null +++ b/resources/lang/no/format.php @@ -0,0 +1,36 @@ + 'd.m.Y H:i', + 'short_date_year' => 'd.m.Y', + 'short_date' => 'd.m', + 'short_month' => 'm', + 'short_month_year' => 'm.y', + 'short_day' => 'D', + 'full_date_year' => 'F d, Y', + 'full_month' => 'F', + 'full_month_year' => 'F Y', + 'full_hour' => 'h.i A', + + 'short_text' => '{text}…', +]; diff --git a/resources/lang/no/journal.php b/resources/lang/no/journal.php new file mode 100644 index 0000000..36d631e --- /dev/null +++ b/resources/lang/no/journal.php @@ -0,0 +1,38 @@ + 'Hvordan var dagen din? Du kan vurdere den én gang om dagen.', + 'journal_come_back' => 'Takk. Kom tilbake i morgen for å vurdere dagen din igjen.', + 'journal_description' => 'Merk: Journalen viser både manuelle journaloppføringer og automatiske innlegg som aktiviteter registrert på dine kontakter. Du kan slette journaloppføringer, men for å fjerne aktiviteter må du fjerne dem fra kontaktens side.', + 'journal_add' => 'Legg til journaloppføring', + 'journal_edit' => 'Rediger en journaloppføring', + 'journal_empty' => 'Tom journal', + 'journal_created_at' => 'Opprettet {date}', + 'journal_created_automatically' => 'Opprettet automatisk', + 'journal_entry_type_journal' => 'Journalinnlegg', + 'journal_entry_type_activity' => 'Aktivitet', + 'journal_entry_rate' => 'Du vurderte din dag.', + 'journal_add_comment' => 'Vil du utdype i en kommentar (valgfritt)?', + 'journal_show_comment' => 'Vis kommentar', + 'entry_delete_success' => 'Journaloppføringen har blitt slettet.', + 'journal_add_title' => 'Tittel (valgfritt)', + 'journal_add_date' => 'Dato', + 'journal_add_post' => 'Innlegg', + 'journal_add_cta' => 'Lagre', + 'journal_blank_cta' => 'Legg til din første journaloppføring', + 'journal_blank_description' => 'Journalen lar deg skrive innlegg slik at du husker hva som skjer i løpet av en dag.', + 'delete_confirmation' => 'Er du sikker på at du vil slette denne journaloppføringen?', + 'apply_filter' => 'Apply filter', + 'start_date' => 'Start Date', + 'end_date' => 'End Date', + 'per_page' => 'Per Page', + 'sort_order' => 'Sort By Created At', + 'ascending' => 'Ascending', + 'descending' => 'Descending', +]; diff --git a/resources/lang/no/logs.php b/resources/lang/no/logs.php new file mode 100644 index 0000000..ecf0af0 --- /dev/null +++ b/resources/lang/no/logs.php @@ -0,0 +1,29 @@ + 'Opprettet ny kontakt.', + 'settings_log_contact_created_with_name' => 'La til :name som kontakt.', + + // contat description update + 'contact_log_contact_description_updated' => 'Oppdatert kontaktbeskrivelsen.', + 'settings_log_contact_description_updated_with_name' => 'Oppdatert beskrivelsen av :name.', + + // contact description clear + 'contact_log_contact_description_cleared' => 'Fjernet beskrivelsen.', + 'settings_log_contact_description_cleared_with_name' => 'Fjernet beskrivelsen av :name.', + + // contact work information update + 'contact_log_contact_work_updated' => 'Oppdatert arbeidsinformasjon.', + 'settings_log_contact_work_updated_with_name' => 'Oppdatert arbeidsinformasjonen til :name.', + + // company created + 'settings_log_company_created' => 'Opprettet en bedrift kalt :name.', +]; diff --git a/resources/lang/no/mail.php b/resources/lang/no/mail.php new file mode 100644 index 0000000..364baf2 --- /dev/null +++ b/resources/lang/no/mail.php @@ -0,0 +1,53 @@ + 'Påminnelse for :contact', + 'greetings' => 'Hei :username', + 'want_reminded_of' => 'Du ønsket å bli minnet om :reason', + 'for' => 'For: :name', + 'comment' => 'Kommentar: :comment', + 'footer_contact_info' => 'Legg til, vis, suppler, og endre informasjon om denne kontakten:', + 'footer_contact_info2' => 'Se profilen til :name', + 'footer_contact_info2_link' => 'Se profilen til :name: :url', + + 'notification_subject_line' => 'Du har én kommende hendelse', + 'notification_description' => 'Om :count dager (på :date) vil følgende skje:', + + 'stay_in_touch_subject_line' => 'Hold kontakt med :name', + 'stay_in_touch_subject_description' => 'Du ba om å bli minnet om å holde kontakt med :name hver :frequency dag.|Du ba om å bli minnet om å holde kontakt med :name hver :frequency dag.', + + 'notifications_whoops' => 'Oops!', + 'notifications_hello' => 'Hallo!', + 'notifications_regards' => 'Vennlig hilsen', + 'notifications_footer' => 'Hvis du har problemer med å klikke på ":actionText"-knappen, kopier og lim inn URL-adressen nedenfor i din nettleser: [:actionURL](:actionURL)', + 'notifications_rights' => 'Alt innhold er opphavsrettslig beskyttet', + + 'confirmation_email_title' => 'Monica - verifiser e-post', + 'confirmation_email_intro'=> 'For å verifisere e-postadressen din, trykk på knappen nedenfor', + 'confirmation_email_button' => 'Verifiser e-postadressen', + 'confirmation_email_bottom' => 'Hvis du ikke opprettet noen konto hos oss trenger du ikke gjøre noe.', + + 'password_reset_title' => 'Monica - forespørsel om å nullstille passord', + 'password_reset_intro' => 'Du får denne e-posten fordi vi har fått en forespørsel om å tilbakestille passordet for din konto.', + 'password_reset_button' => 'Tilbakestill passord', + 'password_reset_expiration' => 'Denne lenken vil utløpe om :count minutter.', + 'password_reset_bottom' => 'Hvis du ikke ba om å nullstille passordet ditt trenger du ikke gjøre noe.', + + 'invitation_title' => 'Monica - :name har invitert deg', + 'invitation_intro' => 'Du har blitt invitert av :name (:email) til å bruke Monica, et hyggelig verktøy for å holde styr på dine forhold.', + 'invitation_link' => 'For å akseptere invitasjonen, trykk på lenken under:', + 'invitation_button' => 'Akseptert invitasjon', + 'invitation_expiration' => 'Lenken utløper om :count dager.', + + 'export_title' => 'Din eksport er klar', + 'export_description' => 'You requested a data export on :date. It is now ready to download.', + 'export_download' => 'Last ned eksport', + +]; diff --git a/resources/lang/no/pagination.php b/resources/lang/no/pagination.php new file mode 100644 index 0000000..cc99fd9 --- /dev/null +++ b/resources/lang/no/pagination.php @@ -0,0 +1,25 @@ + '❮ Forrige', + 'next' => 'Neste ❯', + +]; diff --git a/resources/lang/no/passwords.php b/resources/lang/no/passwords.php new file mode 100644 index 0000000..f1014d6 --- /dev/null +++ b/resources/lang/no/passwords.php @@ -0,0 +1,30 @@ + 'Ditt passord har blitt tilbakestilt!', + 'sent' => 'Hvis e-posten du skrev inn finnes i vårt brukerregister, så har du blitt sendt en lenke for å tilbakestille passordet.', + 'token' => 'Denne tilbakestillingsnøkkelen er ugyldig.', + 'user' => 'Hvis e-posten du skrev inn finnes i vårt brukerregister, så har du blitt sendt en lenke for å tilbakestille passordet.', + 'changed' => 'Passordet er endret.', + 'invalid' => 'Nåværende passord er ikke riktig.', + 'throttled' => 'Vennligst vent før du prøver igjen.', + +]; diff --git a/resources/lang/no/people.php b/resources/lang/no/people.php new file mode 100644 index 0000000..f7a09c2 --- /dev/null +++ b/resources/lang/no/people.php @@ -0,0 +1,539 @@ + 'Kontakten ble ikke funnet', + 'people_list_number_kids' => ':count barn:count barn', + 'people_list_last_updated' => 'Sist konsultert:', + 'people_list_number_reminders' => ':1 påminnelse|:count påminnelser', + 'people_list_blank_title' => 'Du har ikke noen i kontoen din ennå', + 'people_list_blank_cta' => 'Legg til noen', + 'people_list_sort' => 'Sorter', + 'people_list_stats' => ':1 kontakt|:count kontakter', + 'people_list_firstnameAZ' => 'Sorter etter fornavn A → Z', + 'people_list_firstnameZA' => 'Sorter etter fornavn Z → A', + 'people_list_lastnameAZ' => 'Sorter etter etternavn A → Z', + 'people_list_lastnameZA' => 'Sorter etter etternavn Z → A', + 'people_list_lastactivitydateNewtoOld' => 'Sorter etter siste aktivitetsdato nyeste til eldste', + 'people_list_lastactivitydateOldtoNew' => 'Sorter etter siste aktivitetsdato eldste til nyeste', + 'people_list_filter_tag' => 'Viser alle kontakter som er merket med', + 'people_list_clear_filter' => 'Tøm filter', + 'people_list_contacts_per_tags' => ':count måned|:count måneder', + 'people_list_show_dead' => 'Vis døde personer (:count)', + 'people_list_hide_dead' => 'Skjul døde personer (:count)', + 'people_search' => 'Søk i dine kontakter…', + 'people_search_no_results' => 'Ingen resultater funnet', + 'people_search_next' => 'Neste', + 'people_search_prev' => 'Forrige', + 'people_search_rows_per_page' => 'Rader per side', + 'people_search_of' => 'for', + 'people_search_page' => 'Side', + 'people_search_all' => 'Alle', + 'people_add_new' => 'Legg til ny person', + 'people_list_account_usage' => 'Din konto inneholder: :current/:limit kontakter', + 'people_list_account_upgrade_title' => 'Oppgrader din konto for å låse opp begrensninger.', + 'people_list_account_upgrade_cta' => 'Oppgrader nå', + 'people_list_untagged' => 'Vis kontakter uten tagger', + 'people_list_filter_untag' => 'Viser alle kontakter uten tags', + 'archived_contact_readonly' => 'Arkivert kontakt kan ikke redigeres, vennligst ta den bort fra arkiv først.', + + // people add + 'people_add_title' => 'Legg til ny kontakt', + 'people_add_missing' => 'Ingen person funnet - legg til ny nå', + 'people_add_firstname' => 'Fornavn', + 'people_add_middlename' => 'Mellomnavn (valgfritt)', + 'people_add_lastname' => 'Etternavn (valgfritt)', + 'people_add_email' => 'E-post (valgfritt)', + 'people_add_nickname' => 'Kallenavn (valgfritt)', + 'people_add_cta' => 'Legg til', + 'people_save_and_add_another_cta' => 'Lagre og legg til ny', + 'people_add_success' => ':name har blitt opprettet', + 'people_add_gender' => 'Kjønn', + 'people_delete_success' => 'Kontakten er blitt slettet', + 'people_delete_message' => 'Slett kontakt', + 'people_delete_confirmation' => 'Er du sikker på at du vil slette :name’s kontakt? Sletting er umiddelbar og permanent.', + 'people_add_birthday_reminder' => 'Gratuler :name med dagen', + 'people_add_birthday_reminder_deceased' => 'På denne datoen vil :name ha feiret sin bursdag', + 'people_add_import' => 'Vil du importere kontakter?', + 'people_edit_email_error' => 'Det finnes allerede en kontakt med denne e-postadressen. Velg en annen.', + 'people_export' => 'Eksporter som vCard', + 'people_add_reminder_for_birthday' => 'Opprett en årlig påminnelse om fødselsdagen', + + // show + 'section_contact_information' => 'Kontaktinformasjon', + 'section_personal_activities' => 'Aktiviteter', + 'section_personal_reminders' => 'Påminnelser', + 'section_personal_tasks' => 'Oppgaver', + 'section_personal_gifts' => 'Gaver', + 'section_personal_notes' => 'Notater', + + // archived contacts + 'list_link_to_active_contacts' => 'Du ser på arkiverte kontakter. Se listen over aktive kontakter i stedet.', + 'list_link_to_archived_contacts' => 'Vis arkiverte kontakter', + + // Header + 'me' => 'Dette er deg', + 'edit_contact_information' => 'Rediger kontaktinformasjon', + 'contact_archive' => 'Arkiver kontakt', + 'contact_unarchive' => 'Ta kontakt tilbake fra arkivet', + 'contact_archive_help' => 'Arkiverte kontakter vises ikke på kontaktlisten, men vises fortsatt i søkeresultatene.', + 'call_button' => 'Logg en samtale', + 'set_favorite' => 'Favorittkontakter er plassert på toppen av kontaktlisten', + + // Stay in touch + 'stay_in_touch' => 'Hold kontakten', + 'stay_in_touch_frequency' => 'Hold kontakten hver dag|Hold kontakten hver {count} dag', + 'stay_in_touch_next_date' => 'Neste forfall: {date}', + 'stay_in_touch_invalid' => 'Frekvensen må være et tall som er større enn 0.', + 'stay_in_touch_premium' => 'Du må oppgradere kontoen din for at denne funksjonen skal kunne brukes', + 'stay_in_touch_modal_title' => 'Hold kontakten', + 'stay_in_touch_modal_desc' => 'Vi kan sende en på minnelse til deg på e-post for å holde kontakten med {firstname} med jevne mellomrom.', + 'stay_in_touch_modal_label' => 'Send meg en e-post hver… {count} dag|Send meg en e-post hver… {count} dag', + + // Calls + 'modal_call_title' => 'Logg en samtale', + 'modal_call_comment' => 'Hva snakket du om? (valgfritt)', + 'modal_call_exact_date' => 'Telefonsamtalen skjedde den', + 'modal_call_who_called' => 'Hvem ringte?', + 'modal_call_emotion' => 'Vil du logge hvordan du følte under denne samtalen? (valgfritt)', + 'calls_add_success' => 'Telefonsamtalen har blitt lagret.', + 'call_delete_confirmation' => 'Er du sikker på at du vil slette denne samtalen?', + 'call_delete_success' => 'Samtalen har blitt slettet', + 'call_title' => 'Telefonsamtaler', + 'call_empty_comment' => 'Ingen detaljer', + 'call_blank_title' => 'Hold oversikt over telefonsamtaler du har hatt med {name}', + 'call_blank_desc' => 'Du ringte {name}', + 'call_you_called' => 'Du ringte', + 'call_he_called' => '{name} ringte', + 'call_emotions' => 'Følelser:', + + // Conversation + 'conversation_blank' => 'Ta opp samtaler du har med :name på sosiale medier, SMS…', + 'conversation_delete_link' => 'Slett samtalen', + 'conversation_edit_title' => 'Rediger samtalen', + 'conversation_edit_delete' => 'Er du sikker på at du vil slette denne samtalen? Sletting er permanent.', + 'conversation_add_success' => 'Samtalen har blitt lagt til.', + 'conversation_edit_success' => 'Temaet ble oppdatert.', + 'conversation_delete_success' => 'Samtalen ble slettet.', + 'conversation_add_title' => 'Ta opp en ny samtale', + 'conversation_add_when' => 'Når hadde du denne samtalen?', + 'conversation_add_who_wrote' => 'Hvem har sendt denne meldingen?', + 'conversation_add_how' => 'Hvordan kommuniserer du?', + 'conversation_add_you' => 'Deg', + 'conversation_add_content' => 'Skriv ned hva som ble sagt', + 'conversation_add_what_was_said' => 'Hva sa du?', + 'conversation_add_another' => 'Legg til ny melding', + 'conversation_add_error' => 'Du må legge til minst en melding.', + 'conversation_list_table_messages' => 'Meldinger', + 'conversation_list_table_content' => 'Delvis innhold (siste melding)', + 'conversation_list_title' => 'Samtaler', + 'conversation_list_cta' => 'Logg samtale', + + // age - birthday + 'birthdate_not_set' => 'Fødselsdag er ikke satt', + 'age_approximate_in_years' => 'rundt :age år gammel', + 'age_exact_in_years' => ':age år gammel', + 'age_exact_birthdate' => 'født :date', + + // Last called + 'last_called' => 'Sist ringt: :date', + 'last_talked_to' => 'Sist ringt: {date}', + 'last_called_empty' => 'Sist ringt: ukjent', + 'last_activity_date' => 'Siste aktivitet sammen: :date', + 'last_activity_date_empty' => 'Siste aktivitet sammen: ukjent', + + // additional information + 'information_edit_success' => 'Din profil har blitt oppdatert', + 'information_edit_title' => 'Endre :din personlige informasjon', + 'information_edit_max_size' => 'Maks :størrelse Kb.', + 'information_edit_max_size2' => 'Maks {size} Kb.', + 'information_edit_firstname' => 'Fornavn', + 'information_edit_lastname' => 'Etternavn (valgfritt)', + 'information_edit_description' => 'Beskrivelse (Valgfritt)', + 'information_edit_description_help' => 'Brukes i kontaktlisten for å legge til en kontekst, hvis nødvendig.', + 'information_edit_unknown' => 'Jeg vet ikke denne personens alder', + 'information_edit_probably' => 'Denne personen er sannsynligvis…', + 'information_edit_not_year' => 'Jeg kjenner dagen og måneden med denne personens bursdag, men ikke året…', + 'information_edit_exact' => 'Jeg kjenner denne personens eksakte fødselsdag…', + 'information_edit_birthdate_label' => 'Bursdag', + 'information_no_work_defined' => 'Ingen jobb informasjon er definert', + 'information_work_at' => 'på :arbeidssted', + 'work_add_cta' => 'Oppdater arbeidsinformasjon', + 'work_edit_success' => 'Arbeids informasjon oppdatert', + 'work_edit_title' => 'Oppdater :name sin jobbinformasjon', + 'work_edit_job' => 'Jobbtittel (valgfritt)', + 'work_edit_company' => 'Firmanavn (Valgfritt)', + 'work_information' => 'Informasjon om arbeidet', + + // food preferences + 'food_preferences_add_success' => 'Mat preferanser har blitt lagret', + 'food_preferences_edit_description' => 'Kanskje :firstname eller noen i :familys familie har en allergi. Eller liker ikke en spesifikk flaske med vin. Indikerer dem her, så du vil huske det neste gang du inviterer dem til middag', + 'food_preferences_edit_description_no_last_name' => 'Kanskje :firstname har en allergi. Eller liker ikke en spesifikk flaske med vin. Indikerer dem her, så du vil huske det neste gang du inviterer dem til middag', + 'food_preferences_edit_title' => 'Angi matpreferanser', + 'food_preferences_edit_cta' => 'Lagre mat preferanser', + 'food_preferences_title' => 'Mat preferanser', + 'food_preferences_cta' => 'Legg til matpreferanser', + + // reminders + 'reminders_blank_title' => 'Er det noe du ønsker å bli minnet om :name?', + 'reminders_blank_add_activity' => 'Legg til en påminnelse', + 'reminders_add_title' => 'Hva ønsker du å bli minnet om :name?', + 'reminders_add_description' => 'Påminn meg om…', + 'reminders_add_next_time' => 'Når er neste gang du ønsker å minnes om dette?', + 'reminders_add_once' => 'Påminn meg om dette en gang', + 'reminders_add_recurrent' => 'Påminn meg om dette hver', + 'reminders_add_starting_from' => 'starter fra den angitte datoen ovenfor', + 'reminders_add_cta' => 'Legg til påminnelse', + 'reminders_edit_update_cta' => 'Oppdater påminnelse', + 'reminders_add_error_custom_text' => 'Du må angi tekst for denne påminnelsen', + 'reminders_create_success' => 'En påminnelse er lagt til', + 'reminders_delete_success' => 'Påminnelsen har blitt slettet', + 'reminders_update_success' => 'Påminnelsen har blitt oppdatert', + 'reminders_add_optional_comment' => 'Valgfri kommentar', + + 'reminder_frequency_day' => 'hver dag|hver :number dager', + 'reminder_frequency_week' => 'hver uke|hver :number uker', + 'reminder_frequency_month' => 'hver måned|hver :number måneder', + 'reminder_frequency_year' => 'hvert år|hver :number år', + 'reminder_frequency_one_time' => 'den :date', + 'reminders_delete_confirmation' => 'Er du sikker på at du vil slette denne påminnelsen?', + 'reminders_delete_cta' => 'Slett', + 'reminders_next_expected_date' => 'på', + 'reminders_cta' => 'Legg til en påminnelse', + 'reminders_description' => 'Vi sender en e-post for hver av påminnelsene nedenfor. Påminnelser sendes hver morgen arrangementet skjer. Påminnelser legges automatisk i bursdager kan ikke slettes. Hvis du vil endre disse datoene, kan du redigere bursdagen til kontaktene.', + 'reminders_one_time' => 'En gang', + 'reminders_type_week' => 'uke', + 'reminders_type_month' => 'måned', + 'reminders_type_year' => 'år', + 'reminders_birthday' => 'Bursdag for :name', + 'reminders_free_plan_warning' => 'Du er på gratisplanen. Ingen e-post blir sendt på denne planen. For å motta påminnelser via e-post, oppgrader kontoen din.', + + // relationships + 'relationship_form_add' => 'Legg til et nytt forhold', + 'relationship_form_edit' => 'Rediger et eksisterende forhold', + 'relationship_form_is_with' => 'Denne personen er…', + 'relationship_form_is_with_name' => ':name er…', + 'relationship_form_add_choice' => 'Hvem er forholdet med?', + 'relationship_form_create_contact' => 'Legg til ny person', + 'relationship_form_associate_contact' => 'En eksisterende kontakt', + 'relationship_form_associate_dropdown' => 'Søk på og velg en eksisterende kontakt fra nedtrekkslisten nedenfor', + 'relationship_form_associate_dropdown_placeholder' => 'Søk og velg en eksisterende kontakt', + 'relationship_form_also_create_contact' => 'Opprett kontaktoppføring for denne personen.', + 'relationship_form_add_description' => 'Dette vil la deg behandle denne personen som en hvilken som helst annen kontakt.', + 'relationship_form_add_no_existing_contact' => 'Du har ingen kontakter som kan være relatert til :name for øyeblikket.', + 'relationship_delete_confirmation' => 'Er du sikker på at du vil slette denne relasjonen? Sletting er permanent.', + 'relationship_unlink_confirmation' => 'Er du sikker på at du vil slette denne relasjonen? Denne personen vil ikke bli slettet bare relasjonen mellom de to.', + 'relationship_form_add_success' => 'Forholdet har blitt lagt til.', + 'relationship_form_deletion_success' => 'Relasjonen har blitt slettet.', + + // tasks + 'tasks_title' => 'Oppgaver', + 'tasks_blank_title' => 'Du har ingen oppgaver enda.', + 'tasks_form_title' => 'Tittel', + 'tasks_form_description' => 'Beskrivelse (Valgfritt)', + 'tasks_add_task' => 'Ny oppgave', + 'tasks_delete_success' => 'Oppgaven har blitt slettet', + 'tasks_complete_success' => 'Oppgaven har endret status', + + // activities + 'activity_title' => 'Aktiviteter', + 'activity_type_category_simple_activities' => 'Enkle aktiviteter', + 'activity_type_category_sport' => 'Sport', + 'activity_type_category_food' => 'Mat', + 'activity_type_category_cultural_activities' => 'Kulturelle aktiviteter', + 'activity_type_just_hung_out' => 'bare møttes', + 'activity_type_watched_movie_at_home' => 'har sett en film hjemme', + 'activity_type_talked_at_home' => 'bare snakket hjemme', + 'activity_type_did_sport_activities_together' => 'gjorde idrett sammen', + 'activity_type_ate_at_his_place' => 'spiste på deres sted', + 'activity_type_went_bar' => 'gikk til en bar', + 'activity_type_ate_at_home' => 'spiste hjemme', + 'activity_type_picnicked' => 'var på pikk nikk', + 'activity_type_ate_restaurant' => 'spiste på en restaurant', + 'activity_type_went_theater' => 'gikk på teater', + 'activity_type_went_concert' => 'gikk på en konsert', + 'activity_type_went_play' => 'gikk for å spille', + 'activity_type_went_museum' => 'gikk til et museum', + 'activities_add_activity' => 'Legg til aktivitet', + 'activities_add_more_details' => 'Legg til flere detaljer', + 'activities_add_emotions' => 'Legg til følelser', + 'activities_add_category' => 'Angi en kategori', + 'activities_add_participants_cta' => 'Legg til deltakere', + 'activities_item_information' => ':Activity. Skjedde den :date', + 'activities_add_title' => 'Hva gjorde du med {name}?', + 'activities_summary' => 'Beskriv hva du har gjort', + 'activities_add_pick_activity' => 'Ønsker du å kategorisere denne aktiviteten? Du trenger ikke, men den vil gi deg statistikk senere (valgfritt)', + 'activities_add_date_occured' => 'Aktiviteten skjedde den…', + 'activities_add_participants' => 'Hvem, andre en {name}, deltok på denne aktiviteten? (valgfritt)', + 'activities_add_emotions_title' => 'Vil du logge hvordan du følte under denne aktiviteten? (valgfritt)', + 'activities_blank_title' => 'Hold oversikt over hva du har gjort med {name}, og hva du har snakket om', + 'activities_blank_add_activity' => 'Legg til en aktivitet', + 'activities_add_success' => 'Aktiviteten er lagt til', + 'activities_add_error' => 'Feil ved opprettelse av aktivitet', + 'activities_update_success' => 'Aktiviteten har blitt oppdatert', + 'activities_delete_success' => 'Aktiviteten har blitt slettet', + 'activities_who_was_involved' => 'Hvem var involvert?', + 'activities_activity' => 'Aktivitets kategori', + 'activities_view_activities_report' => 'Se aktivitets rapport', + 'activities_profile_title' => 'Rapport om aktiviteter mellom :name og deg', + 'activities_profile_subtitle' => 'Du har logget :total_activities med :name totalt og :activities_last_twelve_months de siste 12 månedene så langt. Du har logget :total_activities med :name totalt og :activities_last_twelve_months de siste 12 månedene så langt.', + 'activities_profile_year_summary_activity_types' => 'Her er et sammendrag av hva slags aktiviteter du har gjort sammen i :year', + 'activities_profile_year_summary' => 'Her er hva dere to har gjort i år :year', + 'activities_profile_number_occurences' => ':value aktivitet|:value aktiviteter', + 'activities_list_participants' => 'Deltakere ({total}):', + 'activities_list_emotions' => 'Følelser felt:', + 'activities_list_date' => 'Skjedde på', + 'activities_list_category' => 'Kategori:', + + // notes + 'notes_create_success' => 'Notatet er opprettet', + 'notes_update_success' => 'Notatet er lagret', + 'notes_delete_success' => 'Notatet har blitt slettet', + 'notes_add_cta' => 'Legg til notat', + 'notes_favorite' => 'Legg til/fjern fra favoritter', + 'notes_delete_title' => 'Slett et notat', + 'notes_delete_confirmation' => 'Er du sikker på at du vil slette denne merknaden? Sletting er permanent', + + // gifts + 'gifts_title' => 'Gaver', + 'gifts_add_success' => 'Gaven er lagt til', + 'gifts_delete_success' => 'Gaven er vellykket slettet', + 'gifts_delete_confirmation' => 'Er du sikker på at du vil slette denne gaven?', + 'gifts_add_gift' => 'Legg til en gave', + 'gifts_link' => 'Lenke', + 'gifts_for' => 'For: {name}', + 'gifts_delete_cta' => 'Slett', + 'gifts_add_title' => 'Gavehåndtering for :name', + 'gifts_add_gift_idea' => 'Gave idé', + 'gifts_add_gift_already_offered' => 'Gave gitt', + 'gifts_add_gift_received' => 'Gave mottatt', + 'gifts_add_gift_title' => 'Hva er denne gaven?', + 'gifts_add_gift_name' => 'Gave navn', + 'gifts_add_link' => 'Lenke til nettside (valgfritt)', + 'gifts_add_value' => 'Verdi (valgfritt)', + 'gifts_add_comment' => 'Kommentar (valgfritt)', + 'gifts_add_recipient' => 'Mottaker (valgfritt)', + 'gifts_add_recipient_field' => 'Mottaker', + 'gifts_add_photo' => 'Bilde (valgfritt)', + 'gifts_add_photo_title' => 'Legg til et bilde for denne gaven', + 'gifts_add_someone' => 'Denne gaven er for noen i {name} sin familie', + 'gifts_delete_title' => 'Slett en gave', + 'gifts_ideas' => 'Gave ideer', + 'gifts_offered' => 'Gaver gitt', + 'gifts_offered_as_an_idea' => 'Marker som en idé', + 'gifts_received' => 'Gaver mottatt', + 'gifts_view_comment' => 'Vis kommentar', + 'gifts_mark_offered' => 'Merk som gitt', + 'gifts_update_success' => 'Gaven er oppdatert', + 'gifts_add_date' => 'Dato (valgfritt)', + + // debts + 'debt_delete_confirmation' => 'Er du sikker på at du vil slette denne gjelden?', + 'debt_delete_success' => 'Gjeld er vellykket slettet', + 'debt_add_success' => 'Gjeld er lagt til', + 'debt_title' => 'Gjeld', + 'debt_add_cta' => 'Legg til gjeld', + 'debt_you_owe' => 'Du skylder :amount', + 'debt_they_owe' => ':name skylder deg :amount', + 'debt_add_title' => 'Styring av gjeld', + 'debt_add_you_owe' => 'Du skylder :name', + 'debt_add_they_owe' => ':name skylder deg', + 'debt_add_amount' => 'på en sum av', + 'debt_add_reason' => 'av følgende årsak (valgfritt)', + 'debt_add_add_cta' => 'Legg til gjeld', + 'debt_edit_update_cta' => 'Oppdater gjeld', + 'debt_edit_success' => 'Gjeld er oppdatert', + 'debts_blank_title' => 'Administrere gjeld du skylder til :name eller :name skylder deg', + + // tags + 'tag_edit' => 'Redigere Etiketter', + 'tag_add' => 'Legg til tagger', + 'tag_add_search' => 'Legg til eller søk tag', + 'tag_no_tags' => 'Ingen tagger enda', + + // Introductions + 'introductions_sidebar_title' => 'Slik møtte dere', + 'introductions_blank_cta' => 'Angi hvordan du møtte :name', + 'introductions_title_edit' => 'Hvordan møtte du :name?', + 'introductions_additional_info' => 'Forklar hvordan og hvor du møtte', + 'introductions_edit_met_through' => 'Har noen introduserte deg for denne personen?', + 'introductions_no_met_through' => 'Ingen', + 'introductions_first_met_date' => 'Dato dere møttes', + 'introductions_no_first_met_date' => 'Jeg vet ikke hvilken dato vi møttes', + 'introductions_first_met_date_known' => 'Dette er den datoen vi møttes', + 'introductions_add_reminder' => 'Legg til en påminnelse om å feire dette møtet på jubileumsarrangementet', + 'introductions_update_success' => 'Du har vellykket oppdatert informasjon om hvordan du møtte denne personen', + 'introductions_met_through' => 'Møttes igjennom :name', + 'introductions_met_date' => 'Møttes den :date', + 'introductions_reminder_title' => 'Jubileum for dagen du først møtte', + + // Deceased + 'deceased_reminder_title' => 'Jubileum for døden til :name', + 'deceased_mark_person_deceased' => 'Marker denne som utgått', + 'deceased_know_date' => 'Jeg vet at denne personen døde', + 'deceased_add_reminder' => 'Legg til en påminnelse for denne datoen', + 'deceased_label' => 'Avdød', + 'deceased_date_label' => 'Døds dato', + 'deceased_label_with_date' => 'Døde på :date', + 'deceased_age' => 'Alder ved død', + + // Contact information + 'contact_info_title' => 'Kontaktinformasjon', + 'contact_info_form_content' => 'Innhold', + 'contact_info_form_contact_type' => 'Kontakttype', + 'contact_info_form_personalize' => 'Personaliser', + 'contact_info_address' => 'Bor ved', + + // Addresses + 'contact_address_title' => 'Adresser', + 'contact_address_form_name' => 'Etikett (valgfritt)', + 'contact_address_form_street' => 'Gate (valgfritt)', + 'contact_address_form_city' => 'By (valgfritt)', + 'contact_address_form_province' => 'Provins (valgfritt)', + 'contact_address_form_postal_code' => 'Postkode (valgfritt)', + 'contact_address_form_country' => 'Land (valgfritt)', + 'contact_address_form_latitude' => 'Breddegrad (kun tall) (valgfritt)', + 'contact_address_form_longitude' => 'Lengdegrad (kun tall) (valgfritt)', + + // Pets + 'pets_kind' => 'Type kjæledyr', + 'pets_name' => 'Navn (valgfritt)', + 'pets_create_success' => 'Kjæledyret har blitt lagt til', + 'pets_update_success' => 'Kjæledyret er oppdatert', + 'pets_delete_success' => 'Kjæledyret er slettet', + 'pets_title' => 'Kjæledyr', + 'pets_reptile' => 'Reptil', + 'pets_bird' => 'Fugl', + 'pets_cat' => 'Katt', + 'pets_dog' => 'Hund', + 'pets_fish' => 'Fisk', + 'pets_hamster' => 'Hamster', + 'pets_horse' => 'Hest', + 'pets_rabbit' => 'Kanin', + 'pets_rat' => 'Rotte', + 'pets_small_animal' => 'Små dyr', + 'pets_other' => 'Annet', + + // life events + 'life_event_list_tab_life_events' => 'Livets hendelser', + 'life_event_list_tab_other' => 'Notater, påminnelser, …', + 'life_event_list_title' => 'Livet hendelser', + 'life_event_blank' => 'Logg hva som skjer med livet til {name} for din fremtidige referanse.', + 'life_event_list_cta' => 'Legg til livshendelse', + 'life_event_create_category' => 'Alle kategorier', + 'life_event_create_life_event' => 'Legg til livshendelse', + 'life_event_create_default_title' => 'Tittel (valgfritt)', + 'life_event_create_default_story' => 'Historie (valgfritt)', + 'life_event_create_date' => 'Du trenger ikke å angi en måned eller dag – bare året er obligatorisk.', + 'life_event_create_default_description' => 'Legg til informasjon om hva du vet', + 'life_event_create_add_yearly_reminder' => 'Legg til en årlig påminnelse om dette arrangementet', + 'life_event_create_success' => 'Livshendelsen har blitt lagt til', + 'life_event_delete_title' => 'Slett en livshendelse', + 'life_event_delete_description' => 'Er du sikker på at du vil slette denne livshendelsen? Sletting er permanent.', + 'life_event_delete_success' => 'Livshendelsen har blitt slettet', + 'life_event_date_it_happened' => 'Dato det skjedde', + 'life_event_category_work_education' => 'Arbeid og utdanning', + 'life_event_category_family_relationships' => 'Familier og relasjoner', + 'life_event_category_home_living' => 'Hjem og interiør', + 'life_event_category_health_wellness' => 'Helse og velvære', + 'life_event_category_travel_experiences' => 'Reiser og erfaringer', + 'life_event_sentence_new_job' => 'Startet en ny jobb', + 'life_event_sentence_retirement' => 'Pensjonert', + 'life_event_sentence_new_school' => 'Startet på skolen', + 'life_event_sentence_study_abroad' => 'Studiet i utlandet', + 'life_event_sentence_volunteer_work' => 'Startet frivillighetsarbeid', + 'life_event_sentence_published_book_or_paper' => 'Publiserte innlegg', + 'life_event_sentence_military_service' => 'Startet militærtjeneste', + 'life_event_sentence_new_relationship' => 'Startet et forhold', + 'life_event_sentence_engagement' => 'Blitt forlovet', + 'life_event_sentence_marriage' => 'Ble gift', + 'life_event_sentence_anniversary' => 'Jubileum', + 'life_event_sentence_expecting_a_baby' => 'Venter en baby', + 'life_event_sentence_new_child' => 'Fikk en baby', + 'life_event_sentence_new_family_member' => 'Lagt til et familiemedlem', + 'life_event_sentence_new_pet' => 'Har fått et kjæledyr', + 'life_event_sentence_end_of_relationship' => 'Endte et forhold', + 'life_event_sentence_loss_of_a_loved_one' => 'Mistet en kjær', + 'life_event_sentence_moved' => 'Flyttet', + 'life_event_sentence_bought_a_home' => 'Kjøpt et hjem', + 'life_event_sentence_home_improvement' => 'Gjort en forbedring i hjemmet', + 'life_event_sentence_holidays' => 'Var på ferie', + 'life_event_sentence_new_vehicle' => 'Fikk et nytt kjøretøy', + 'life_event_sentence_new_roommate' => 'Fikk en samboer', + 'life_event_sentence_overcame_an_illness' => 'Ble frisk fra en sykdom', + 'life_event_sentence_quit_a_habit' => 'Avslutt en vane', + 'life_event_sentence_new_eating_habits' => 'Startet nye spisevaner', + 'life_event_sentence_weight_loss' => 'Mistet vekt', + 'life_event_sentence_wear_glass_or_contact' => 'Begynte å bruke briller eller kontaktlinser', + 'life_event_sentence_broken_bone' => 'Knakk et bein', + 'life_event_sentence_removed_braces' => 'Fjernet tannregulering', + 'life_event_sentence_surgery' => 'Fikk kirurgi', + 'life_event_sentence_dentist' => 'Var til tannlegen', + 'life_event_sentence_new_sport' => 'Startet med en idrett', + 'life_event_sentence_new_hobby' => 'Begynte med en hobby', + 'life_event_sentence_new_instrument' => 'Lærte et nytt instrument', + 'life_event_sentence_new_language' => 'Lærte et nytt språk', + 'life_event_sentence_tattoo_or_piercing' => 'Fikk en tatovering eller piercing', + 'life_event_sentence_new_license' => 'Fikk et sertifikat', + 'life_event_sentence_travel' => 'Reiste', + 'life_event_sentence_achievement_or_award' => 'Fikk en pris', + 'life_event_sentence_changed_beliefs' => 'Endret tro', + 'life_event_sentence_first_word' => 'Snakket for første gang', + 'life_event_sentence_first_kiss' => 'Kysset for før første gang', + + // documents + 'document_list_title' => 'Dokumenter', + 'document_list_cta' => 'Last opp dokument', + 'document_list_blank_desc' => 'Her kan du lagre dokumenter som er relatert til denne personen.', + 'document_upload_zone_cta' => 'Laste opp en fil', + 'document_upload_zone_progress' => 'Laster opp dokumentet…', + 'document_upload_zone_error' => 'Det oppsto en feil ved opplasting av dokumentet. Vennligst prøv igjen nedenfor.', + + // Photos + 'photo_title' => 'Bilder', + 'photo_list_title' => 'Relaterte bilder', + 'photo_list_cta' => 'Last opp bilde', + 'photo_list_blank_desc' => 'Du kan lagre bilder om denne kontakten. Last opp en nå!', + 'photo_upload_zone_cta' => 'Last opp et bilde', + 'photo_current_profile_pic' => 'Nåværende profilbilde', + 'photo_make_profile_pic' => 'Lag profilbilde', + 'photo_delete' => 'Slett bilde', + 'photo_next' => 'Neste bilde ❯', + 'photo_previous' => '❮ Forge bilde', + + // Avatars + 'avatar_change_title' => 'Endre din avatar', + 'avatar_question' => 'Hvilken avatar vil du bruke?', + 'avatar_default_avatar' => 'Standard avatar', + 'avatar_adorable_avatar' => 'Den fantastiske avatar', + 'avatar_gravatar' => 'Gravatar tilknyttet e-postadressen til denne personen. Gravatar er et globalt system som lar brukere knytte e-postadresser til bilder.', + 'avatar_current' => 'Hold gjeldende avatar', + 'avatar_photo' => 'Fra et bilde som du laster opp', + 'avatar_crop_new_avatar_photo' => 'Beskjær nytt profilbilde', + + // emotions + 'emotion_this_made_me_feel' => 'Dette fikk deg til å føle…', + + // logs + 'auditlogs_link' => 'Historikk', + 'auditlogs_title' => 'Alt som skjedde med :name', + 'auditlogs_breadcrumb' => 'Historikk', + 'auditlogs_author' => 'Via :name den :date', + + // contact field label + 'contact_field_label_home' => 'Hjem', + 'contact_field_label_work' => 'Arbeid', + 'contact_field_label_cell' => 'Mobil', + 'contact_field_label_fax' => 'Faks', + 'contact_field_label_pager' => 'Personsøker', + 'contact_field_label_main' => 'Hoved', + 'contact_field_label_other' => 'Annet', + 'contact_field_label_personal' => 'Personlig', +]; diff --git a/resources/lang/no/reminder.php b/resources/lang/no/reminder.php new file mode 100644 index 0000000..be1bb7a --- /dev/null +++ b/resources/lang/no/reminder.php @@ -0,0 +1,16 @@ + 'Ønsk lykke til med dagen til', + 'type_phone_call' => 'Ring', + 'type_lunch' => 'Lunsj med', + 'type_hangout' => 'Heng med', + 'type_email' => 'E-post', + 'type_birthday_kid' => 'Wish happy birthday to the child of', +]; diff --git a/resources/lang/no/settings.php b/resources/lang/no/settings.php new file mode 100644 index 0000000..d899810 --- /dev/null +++ b/resources/lang/no/settings.php @@ -0,0 +1,557 @@ + 'Kontoinnstillinger', + 'sidebar_personalization' => 'Tilpass', + 'sidebar_settings_storage' => 'Lagring', + 'sidebar_settings_export' => 'Eksporter data', + 'sidebar_settings_users' => 'Brukere', + 'sidebar_settings_subscriptions' => 'Abonnementer', + 'sidebar_settings_import' => 'Importer data', + 'sidebar_settings_tags' => 'Tag-administrasjon', + 'sidebar_settings_api' => 'API', + 'sidebar_settings_dav' => 'DAV-ressurser', + 'sidebar_settings_security' => 'Sikkerhet', + 'sidebar_settings_auditlogs' => 'Overvåkningslogg', + + 'title_general' => 'Generell informasjon', + 'title_i18n' => 'Internasjonale innstillinger', + 'title_layout' => 'Visningsoppsett', + + 'me_title' => 'Meg som kontakt', + 'me_help' => 'Dette er den kontakten som representerer deg i Monica', + 'me_select' => 'Velg en kontakt', + 'me_no_contact' => 'Ingen kontakt valgt.', + 'me_select_click' => 'Klikk her for å velge en kontakt.', + 'me_remove_contact' => 'Fjern tilknytning', + 'me_choose' => 'Velg deg selv', + 'me_choose_placeholder' => 'Velg deg selv', + + 'export_title' => 'Eksporter dine kontodata', + 'export_be_patient' => 'Click the button to start the export. It might take several minutes to process the export – please be patient and do not spam the button.', + 'export_title_sql' => 'Export to SQL', + 'export_sql_explanation' => 'Exporting your data in SQL format allows you to take your data and import it to your own Monica instance. This is only valuable if you do have your own server.', + 'export_sql_cta' => 'Export to SQL', + 'export_sql_link_instructions' => 'Note: read the instructions to learn more about importing this file to your instance.', + 'export_title_json' => 'Export to Json', + 'export_submitted' => 'Your export has been submitted, it will be available in a few moment…', + 'export_json_explanation' => 'Exporting your data in Json format for backup.', + 'export_json_beta' => 'Json export is in preview mode. Tell us what you think about it:', + 'export_json_cta' => 'Export to Json', + 'export_header_type' => 'Type', + 'export_header_timestamp' => 'Creation date', + 'export_header_status' => 'Status', + 'export_header_actions' => 'Actions', + 'export_last_title' => 'Last exports', + 'export_empty_title' => 'No exports yet', + 'export_type_json' => 'Json export', + 'export_type_sql' => 'SQL export', + 'export_status_todo' => 'Submitted', + 'export_status_doing' => 'Doing', + 'export_status_done' => 'Done', + 'export_status_failed' => 'Failed', + 'export_not_done' => 'Download impossible, this export is not done yet.', + + 'firstname' => 'Fornavn', + 'lastname' => 'Etternavn', + 'name_order' => 'Rekkefølge på navn', + 'name_order_firstname_lastname' => ' – Jon Smith', + 'name_order_lastname_firstname' => ' – Smith Jon', + 'name_order_firstname_lastname_nickname' => ' () - Jon Smith (Rambo)', + 'name_order_firstname_nickname_lastname' => ' () – Jon (Rambo) Smith', + 'name_order_lastname_firstname_nickname' => ' () – Smith Jon (Rambo)', + 'name_order_lastname_nickname_firstname' => ' () - Smith (Rambo) Jon', + 'name_order_nickname_firstname_lastname' => ' ( ) – Rambo (Jon Smith)', + 'name_order_nickname_lastname_firstname' => ' ( ) – Rambo (Doe John)', + 'name_order_nickname' => ' – Rambo', + 'currency' => 'Valuta', + 'name' => 'Ditt navn: :name', + 'email' => 'E-postadresse', + 'email_placeholder' => 'Skriv e-postadresse', + 'email_help' => 'Dette er e-postadressen som brukes til å logge inn, og det er hit Monica sender deg påminnelser.', + 'timezone' => 'Tidssone', + 'temperature_scale' => 'Temperaturskala', + 'temperature_scale_fahrenheit' => 'Fahrenheit (°F)', + 'temperature_scale_celsius' => 'Celsius (°C)', + 'layout' => 'Visningsoppsett', + 'layout_small' => 'Maksimalt 1200 piksler bredt', + 'layout_big' => 'Full nettleserbredde', + 'save' => 'Oppdater innstillinger', + 'delete_title' => 'Slett kontoen din', + 'delete_desc' => 'Ønsker du å slette din konto? Sletting er permanent og alle dine data vil bli slettet permanent. Hvis du har et abonnement, blir det kansellert umiddelbart.', + 'delete_other_desc' => 'Your data in the main database will be deleted immediately. As described in our privacy policy, we carry out securely encrypted backups of the database every day. These backups are kept for 30 days after which they are completely deleted. We cannot delete specific data from the backups we hold any earlier than this. All of your data will be completely deleted no later than 31 days after your account’s deletion.', + 'reset_desc' => 'Ønsker du å tilbakestille kontoen din? Dette vil fjerne alle dine kontakter og alle dataene knyttet til dem. Kontoen din vil ikke bli slettet.', + 'reset_title' => 'Tilbakestill konto', + 'reset_cta' => 'Tilbakestill konto', + 'reset_notice' => 'Er du sikker på at du vil tilbakestille kontoen din? Dette er permanent og kan ikke angres.', + 'reset_success' => 'Din konto har blitt tilbakestilt.', + 'delete_notice' => 'Er du sikker på at du vil slette kontoen? Dette er permanent og kan ikke angres. Alle dataene dine vil bli slettet og vil ikke kunne gjenopprettes.', + 'delete_cta' => 'Slett konto', + 'settings_success' => 'Innstillinger oppdatert!', + 'locale' => 'Språk brukt i appen', + 'locale_help' => 'Vil du hjelpe med å oversette Monica eller legge til et nytt språk? Følg denne linken for mer informasjon.', + 'locale_ar' => 'Arabisk', + 'locale_cs' => 'Tsjekkisk', + 'locale_de' => 'Tysk', + 'locale_el' => 'Gresk', + 'locale_en' => 'Engelsk', + 'locale_en-GB' => 'English (United Kingdom)', + 'locale_es' => 'Spansk', + 'locale_fr' => 'Fransk', + 'locale_he' => 'Hebraisk', + 'locale_hr' => 'Kroatisk', + 'locale_id' => 'Indonesisk', + 'locale_it' => 'Italiensk', + 'locale_ja' => 'Japanese', + 'locale_nl' => 'Dutch', + 'locale_pt' => 'Portuguese', + 'locale_pt-BR' => 'Brasiliansk portugisisk', + 'locale_ru' => 'Russian', + 'locale_sv' => 'Swedish', + 'locale_vi' => 'Vietnamesisk', + 'locale_zh' => 'Chinese Simplified', + 'locale_zh-TW' => 'Chinese Traditional', + 'locale_tr' => 'Turkish', + + 'security_title' => 'Sikkerhet', + 'security_help' => 'Endre sikkerhetsinnstillinger for din konto.', + 'password_change' => 'Endre passord', + 'password_current' => 'Gjeldende passord', + 'password_current_placeholder' => 'Skriv inn ditt nåværende passord', + 'password_new1' => 'Nytt passord', + 'password_new1_placeholder' => 'Skriv inn det nye passordet ditt', + 'password_new2' => 'Bekreft nytt passord', + 'password_new2_placeholder' => 'Gjenta nytt passord', + 'password_btn' => 'Endre passord', + '2fa_title' => 'To-faktor-autentisering', + '2fa_otp_title' => 'To-faktor autentiserings mobilapp', + '2fa_enable_title' => 'Sett opp to-faktor-autentisering', + '2fa_enable_description' => 'Aktiver to-faktor autentisering for å øke sikkerheten på kontoen din.', + '2fa_enable_otp' => 'Åpne opp din To-faktor autentiserings mobilapp og skann følgende QR-strekkode:', + '2fa_enable_otp_help' => 'Hvis din to-faktor autentiseringsmobilapp ikke støtter QR-strekkoder, angi følgende kode:', + '2fa_enable_otp_validate' => 'Vennligst bekreft den nye enheten du akkurat har konfigurert:', + '2fa_enable_success' => 'To-faktor autentisering aktivert', + '2fa_enable_error' => 'Feil ved forsøk på å aktivere To-faktor autentisering', + '2fa_enable_error_already_set' => 'To-faktor autentisering er allerede aktivert', + '2fa_disable_title' => 'Deaktiver to-faktor autentisering', + '2fa_disable_description' => 'Deaktiver to-faktor autentisering for kontoen. Vær forsiktig, kontoen din blir mye mindre sikker!', + '2fa_disable_success' => 'To-faktor autentisering deaktivert', + '2fa_disable_error' => 'Feil ved forsøk på å deaktivere To-faktor autentisering', + + 'webauthn_title' => 'Sikkerhetsnøkkel – WebAuthn protokoll', + 'webauthn_enable_description' => 'Legg til en ny sikkerhetsnøkkel', + 'webauthn_key_name_help' => 'Gi nøkkelen din et navn.', + 'webauthn_key_name' => 'Nøkkelens navn:', + 'webauthn_success' => 'Nøkkelen din ble oppdaget og validert.', + 'webauthn_last_use' => 'Sist bruk: {timestamp}', + 'webauthn_delete_confirmation' => 'Er du sikker på at du vil slette denne nøkkelen?', + 'webauthn_delete_success' => 'Nøkkelen ble slettet', + 'webauthn_insertKey' => 'Sett inn sikkerhetsnøkkelen.', + 'webauthn_buttonAdvise' => 'Hvis sikkerhetsnøkkelen har en knapp, trykk på den.', + 'webauthn_noButtonAdvise' => 'Hvis den ikke fjernes, slett den og sett den inn igjen.', + 'webauthn_not_supported' => 'Nettleseren din støtter for øyeblikket ikke WebAuthn.', + 'webauthn_not_secured' => 'WebAuthn støtter kun sikre tilkoblinger. Last inn denne siden med https plan.', + 'webauthn_error_already_used' => 'Nøkkelen er allerede registrert. Det er ikke nødvendig å registrere den på nytt.', + 'webauthn_error_not_allowed' => 'Operasjonen ble enten tidsavbrutt eller ikke tillatt.', + + 'recovery_title' => 'Gjenopprettingskode', + 'recovery_show' => 'Få gjenopprettingskoder', + 'recovery_copy_help' => 'Kopier koder i din utklippstavle', + 'recovery_help_intro' => 'Dette er dine gjenopprettings koder:', + 'recovery_help_information' => 'Du kan bruke hver gjenopprettingskode en gang.', + 'recovery_clipboard' => 'Koder kopiert til utklippstavlen.', + 'recovery_generate' => 'Generer nye koder…', + 'recovery_generate_help' => 'Generering av nye koder vil ugyldiggjøre tidligere genererte koder.', + 'recovery_already_used_help' => 'Denne koden er allerede brukt.', + + 'users_list_title' => 'Brukere med tilgang til kontoen din', + 'users_list_add_user' => 'Inviter en ny bruker', + 'users_list_you' => 'Dette er deg', + 'users_list_invitations_title' => 'Ventende invitasjoner', + 'users_list_invitations_explanation' => 'Nedenfor er de du har invitert til å bli med i Monica som samarbeidspartner.', + 'users_list_invitations_invited_by' => 'invitert av :name', + 'users_list_invitations_sent_date' => 'sendt :date', + 'users_blank_title' => 'Du er den eneste som har tilgang til denne kontoen.', + 'users_blank_add_title' => 'Vil du invitere noen andre?', + 'users_blank_description' => 'Denne personen vil ha samme tilgang som du har, og vil kunne legge til, redigere eller slette kontaktinformasjon.', + 'users_blank_cta' => 'Inviter noen', + 'users_add_title' => 'Inviter en ny bruker til kontoen din via e-post', + 'users_add_description' => 'Denne personen vil ha samme tilgang som du gjør, inkludert å invitere eller slette andre brukere, inkludert deg. Vær sikker på at du stoler på denne personen før du gir dem tilgang.', + 'users_add_email_field' => 'Angi e-post til personen du ønsker å invitere', + 'users_add_confirmation' => 'Jeg bekrefter at jeg vil invitere denne brukeren til kontoen min. Jeg forstår at denne personen vil ha tilgang til ALLE data og se nøyaktig hva jeg ser.', + 'users_add_cta' => 'Inviter brukeren via e-post', + 'users_accept_title' => 'Godta invitasjon og opprett en ny konto', + 'users_error_please_confirm' => 'Vennligst bekreft at du vil invitere denne brukeren før du fortsetter med invitasjonen', + 'users_error_email_already_taken' => 'Denne e-post adressen er allerede registrert. Velg en annen', + 'users_error_already_invited' => 'Du har allerede invitert brukeren. Velg en annen e-postadresse.', + 'users_error_email_not_similar' => 'Dette er ikke e-posten til personen som har invitert deg.', + 'users_invitation_deleted_confirmation_message' => 'Invitasjonen er slettet', + 'users_invitations_delete_confirmation' => 'Er du sikker på at du vil slette denne invitasjonen?', + 'users_list_delete_confirmation' => 'Er du sikker på at du vil slette denne brukeren fra kontoen din?', + 'users_invitation_need_subscription' => 'For å legge til flere brukere må du ha et abonnement.', + + 'subscriptions_account_current_plan' => 'Din nåværende abonnementsplan', + 'subscriptions_account_current_legacy' => 'Nåværende plan, ikke valgbar lenger:', + 'subscriptions_account_current_paid_plan' => 'Du er på planen :name. Tusen takk for at du er abonnent.', + + 'subscriptions_account_next_billing_title' => 'Neste regning', + 'subscriptions_account_next_billing' => 'Abonnementet ditt blir automatisk fornyet :date.', + 'subscriptions_account_bill_monthly' => 'Vi fakturerer deg :price i måned.', + 'subscriptions_account_bill_annual' => 'Vi fakturerer deg :price for ytterligere år.', + 'subscriptions_account_change' => 'Endre plan', + + 'subscriptions_account_cancel_title' => 'Avslutt abonnement', + 'subscriptions_account_cancel_action' => 'Avslutt abonnement', + 'subscriptions_account_cancel' => 'Du kan avslutte abonnementet når som helst.', + 'subscriptions_account_free_plan' => 'Du er på en gratis abonnementsplan.', + 'subscriptions_account_free_plan_upgrade' => 'Du kan oppgradere kontoen din til planen :name, som koster $:price per måned. Her er fordelene:', + 'subscriptions_account_free_plan_benefits_users' => 'Ubegrenset antall brukere', + 'subscriptions_account_free_plan_benefits_reminders' => 'Påminnelser via e-post', + 'subscriptions_account_free_plan_benefits_import_data_vcard' => 'Importer kontaktene dine med vCard', + 'subscriptions_account_free_plan_benefits_support' => 'Støtt prosjektet på lang sikt, slik at vi kan introdusere mer flotte egenskaper.', + 'subscriptions_account_upgrade' => 'Oppgrader din konto', + 'subscriptions_account_upgrade_title' => 'Oppgrader Monica i dag og få mer meningsfylte forhold.', + 'subscriptions_account_upgrade_choice' => 'Velg en plan nedenfor og bli med :customers personer som har oppgradert sin Monica.', + 'subscriptions_account_update_title' => 'Oppdater Monica-abonnement', + 'subscriptions_account_update_description' => 'Du kan endre abonnementets frekvens her.', + 'subscriptions_account_update_information' => 'Du vil bli fakturert umiddelbart for det nye beløpet. Ditt abonnement vil forlenges til den nye perioden, avhengig av ditt valg.', + 'subscriptions_account_invoices' => 'Fakturaer', + 'subscriptions_account_invoices_download' => 'Nedlasting', + 'subscriptions_account_invoices_subscription' => 'Abonnement fra :startDate til :endDate', + 'subscriptions_account_payment' => 'Hvilket betalingsalternativ passer deg best?', + 'subscriptions_account_confirm_payment' => 'Betalingen er for øyeblikket ufullført, bekreft betalingen.', + 'subscriptions_downgrade_title' => 'Nedgrader din konto til gratisabonnementet', + 'subscriptions_downgrade_limitations' => 'Den frie planen har begrensninger. For å kunne nedgradere, må du bestå sjekklisten nedenfor:', + 'subscriptions_downgrade_rule_users' => 'Du må bare ha én bruker i kontoen din', + 'subscriptions_downgrade_rule_users_constraint' => 'Du har for tiden 1 bruker i din konto.εDu har for tiden :count brukere på kontoen din.', + 'subscriptions_downgrade_rule_invitations' => 'Du kan ikke ha noen ventende invitasjoner', + 'subscriptions_downgrade_rule_invitations_constraint' => 'Du har for tiden 1 ventende invitasjon.|Du har fortiden :count Ventende invitasjoner.', + 'subscriptions_downgrade_rule_contacts' => 'Du må ikke ha mer enn :number aktive kontakter', + 'subscriptions_downgrade_rule_contacts_constraint' => 'Du har 1 kontakt. You have for øyeblikket :count contacts.', + 'subscriptions_downgrade_rule_contacts_archive' => 'Vi kan også arkivere alle kontaktene dine for deg - det vil tømme denne regelen, og la deg gå videre med nedgraderingsprosessen til din konto.', + 'subscriptions_downgrade_cta' => 'Nedgrader', + 'subscriptions_downgrade_success' => 'Du er tilbake til gratisplanen!', + 'subscriptions_downgrade_thanks' => 'Takk så mye for å prøve den betalte planen. Vi har lagt til nye funksjoner på Monica hele tiden, slik at du kanskje vil komme tilbake i fremtiden for å se om du kanskje er interessert i å melde deg på igjen.', + 'subscriptions_back' => 'Tilbake til innstillinger', + 'subscriptions_upgrade_title' => 'Oppgrader din konto', + 'subscriptions_upgrade_choose' => 'Du valgte :plan planen.', + 'subscriptions_upgrade_infos' => 'Vi kunne ikke være lykkelige. Skriv inn din betalingsinformasjon under.', + 'subscriptions_upgrade_name' => 'Navn på kortet', + 'subscriptions_upgrade_zip' => 'Postnummer', + 'subscriptions_upgrade_credit' => 'Kreditt- eller debet kort', + 'subscriptions_upgrade_submit' => 'Betal {amount}', + 'subscriptions_upgrade_charge' => 'Vi tar betalt for kortet ditt :price nå. Neste trekk vil være den :date. Hvis du noen gang ombestemmer deg, kan du avbryte når som helst, ingen spørsmål blir stilt.', + 'subscriptions_upgrade_charge_handled' => 'Betalingen håndteres av Stripe. Ingen kortinformasjon berører vår server.', + 'subscriptions_upgrade_success' => 'Takk! Du er nå abonnent.', + 'subscriptions_upgrade_thanks' => 'Velkommen til samfunnet av mennesker som prøver å gjøre verden til et bedre sted.', + + 'subscriptions_payment_confirm_title' => 'Bekreft din :amount betaling', + 'subscriptions_payment_confirm_information' => 'Ekstra bekreftelse er nødvendig for å behandle betalingen. Vennligst bekreft betalingen din ved å fylle ut betalingdetaljene dine nedenfor.', + 'subscriptions_payment_succeeded_title' => 'Betaling vellykket', + 'subscriptions_payment_succeeded' => 'Denne betalingen var allerede bekreftet.', + 'subscriptions_payment_cancelled_title' => 'Betalingen ble avbrutt', + 'subscriptions_payment_cancelled' => 'Denne betalingen ble kansellert.', + 'subscriptions_payment_error_name' => 'Skriv inn navnet ditt.', + 'subscriptions_payment_success' => 'Betalingen var vellykket.', + + 'subscriptions_pdf_title' => 'Ditt :name månedlige abonnement', + 'subscriptions_plan_frequency_year' => ':amount / år', + 'subscriptions_plan_frequency_month' => ':amount / måned', + 'subscriptions_plan_choose' => 'Velg denne planen', + 'subscriptions_plan_year_title' => 'Betal årlig', + 'subscriptions_plan_year_bonus' => 'Bli med i ett år', + 'subscriptions_plan_month_title' => 'Betal månedlig', + 'subscriptions_plan_month_bonus' => 'Avbryt når som helst', + 'subscriptions_plan_include1' => 'Inkludert med oppgradering:', + 'subscriptions_plan_include2' => 'Ubegrenset antall kontakter • Ubegrenset antall brukere • Påminnelser via e-post • Importer med vCard • Personalisering av kontaktskjemaet', + 'subscriptions_plan_include3' => '100 % av gevinstene går til utviklingen av dette åpne kildekode-prosjektet.', + 'subscriptions_help_title' => 'Ytterligere opplysninger du kan være nysgjerrig på', + 'subscriptions_help_opensource_title' => 'Hva er et åpen kildekode-prosjekt?', + 'subscriptions_help_opensource_desc' => 'Monica er et åpen kildekode-prosjekt. Det betyr at det blir bygget av et samfunn som ønsker å bygge et flott verktøy for mer godt. Å være åpen kildekode betyr at koden er offentlig tilgjengelig på GitHub, og alle kan inspisere den, endre koden eller forbedre den. Alle pengene vi skaffer oss er opptatt av å bygge bedre funksjoner, betale for mer kraftfulle servere og betale andre kostnader. Takk for din hjelp. Vi kunne ikke gjøre det uten deg.', + 'subscriptions_help_limits_title' => 'Er det begrenset hvor mange kontakter vi har på "free plan"?', + 'subscriptions_help_limits_plan' => 'Ja. "free plans" lar deg administrere :number kontakter.', + 'subscriptions_help_discounts_title' => 'Har du rabatter til veldedighet og utdanning?', + 'subscriptions_help_discounts_desc' => 'Vi gjør! Monica er gratis for studenter, og gratis for ikke-fortjeneste og veldedighet. Bare kontakt brukerstøtte med bevis på din status, og vi bruker denne spesielle statusen i kontoen din.', + 'subscriptions_help_change_title' => 'Hva hvis jeg ombestemmer meg?', + 'subscriptions_help_change_desc' => 'Du kan kansellere når som helst, ingen spørsmål spurt– ingen grunn til å kontakte kundestøtte. Men du vil ikke bli refundert for denne perioden.', + + 'stripe_error_card' => 'Kortet ditt ble avslått. Meldingen er: :message', + 'stripe_error_api_connection' => 'Nettverkskommunikasjon med Stripe mislyktes. Prøv igjen senere.', + 'stripe_error_rate_limit' => 'For mange forespørsler med Stripe akkurat nå. Prøv igjen senere.', + 'stripe_error_invalid_request' => 'Ugyldige parametere. Prøv igjen senere.', + 'stripe_error_authentication' => 'Feil godkjenning med Stripe', + + 'import_title' => 'Importer kontakter til din konto', + 'import_cta' => 'Last opp kontakter', + 'import_stat' => 'Du har importert :number filer så langt.', + 'import_result_stat' => 'Opplastet vCard med 1 kontakt (:total_imported importen, :total_skippet hoppet over)) Opplastet vCard med :total_contacts (:total_imported importen, :total_skipped skipped)', + 'import_view_report' => 'Vis rapport', + 'import_in_progress' => 'Importen pågår. Oppdater siden om et minutt.', + 'import_upload_title' => 'Importer kontaktene dine fra en vCard-fil', + 'import_upload_rules_desc' => 'Vi har imidlertid noen regler:', + 'import_upload_rule_format' => 'Vi støtter .vcard og .vcf filer.', + 'import_upload_rule_vcard' => 'Vi støtter vCard 3.0 formatet, som er standardformat for macOSs Kontakter.app og Google Kontakter.', + 'import_upload_rule_instructions' => 'Eksporter instruksjonene for macOS Contacts.app og Google Kontakter.', + 'import_upload_rule_multiple' => 'Hvis kontaktene har flere e-postadresser eller telefonnummer, lagres bare den første oppføringen.', + 'import_upload_rule_limit' => 'Filene er begrenset til 10 MB.', + 'import_upload_rule_time' => 'Det kan ta opptil ett minutt å laste opp kontaktene og behandle dem. Vær tålmodig.', + 'import_upload_rule_cant_revert' => 'Kontroller at dataene er nøyaktige før opplasting, da du ikke kan angre opplastingen.', + 'import_upload_form_file' => 'Din .vcf eller .vCard fil:', + 'import_upload_behaviour' => 'Import oppførsel:', + 'import_upload_behaviour_add' => 'Legg til nye kontakter og hopp over eksisterende', + 'import_upload_behaviour_replace' => 'Erstatt eksisterende kontakter', + 'import_upload_behaviour_help' => 'Erstatning erstatter alle data funnet i vCard, men vil beholde eksisterende kontaktfelt.', + 'import_report_title' => 'Importerer rapport', + 'import_report_date' => 'Dato for import', + 'import_report_type' => 'Import type', + 'import_report_number_contacts' => 'Antall kontakter i filen', + 'import_report_number_contacts_imported' => 'Antall importerte kontakter', + 'import_report_number_contacts_skipped' => 'Antall kontakter hoppet over', + 'import_report_status_imported' => 'Importert', + 'import_report_status_skipped' => 'Utelatt', + 'import_vcard_parse_error' => 'Feil ved analyse av vCard-oppføringen', + 'import_vcard_contact_exist' => 'Kontakten finnes allerede', + 'import_vcard_contact_no_firstname' => 'Har ikke fornavn (obligatorisk)', + 'import_vcard_file_not_found' => 'Finner ikke filen', + 'import_vcard_unknown_entry' => 'Ukjent navn på kontakt', + 'import_vcard_file_no_entries' => 'Filen inneholder ingen oppføringer', + 'import_blank_title' => 'Du har ikke importert noen kontakter enda.', + 'import_blank_question' => 'Vil du importere kontakter nå?', + 'import_blank_description' => 'Vi kan importere vCard-filer som du kan få fra Google Kontakter eller kontakt-behandleren din.', + 'import_blank_cta' => 'Importer vCard', + 'import_need_subscription' => 'Importering av data krever et abonnement.', + + 'tags_list_title' => 'Tagger', + 'tags_list_description' => 'Du kan organisere kontaktene dine ved å sette opp tagger. Tagger fungerer som mapper, men du kan legge til flere enn ett tag til en kontakt. For å legge til en ny tag, legg den til selve kontakten.', + 'tags_list_contact_number' => '1 kontakt|:count kontakter', + 'tags_list_delete_success' => 'Taggen har blitt slettet', + 'tags_list_edit_success' => 'The tag has been successfully updated', + 'tags_list_delete_confirmation' => 'Er du sikker på at du vil slette taggen? Ingen kontakter vil bli slettet, bare taggen.', + 'tags_blank_title' => 'Tagger er en flott måte å kategorisere kontaktene på.', + 'tags_blank_description' => 'Merker fungerer som mapper, men du kan legge til flere enn en tag til en kontakt. Gå til kontakt og tagg en venn rett under navnet. Når en kontakt er tagget, kom tilbake hit for å administrere alle taggene på kontoen din.', + + 'api_title' => 'API tilgang', + 'api_description' => 'API-et kan brukes til å manipulere Monicas data fra en ekstern applikasjon, for eksempel en mobilapplikasjon.', + 'api_help' => 'For å bruke API er et token obligatorisk. Du kan enten opprette en personlig tilgangstoken (sikker autentisering), eller autorisere en OAuth-klient til å opprette den for deg. Se API dokumentasjon.', + 'api_endpoint' => 'API-endepunktet for dette Monica-eksempelet er:', + + 'api_personal_access_tokens' => 'Personlige tilgangsnøkler', + 'api_pao_description' => 'Sørg for at du gir denne tilgangsnøkkelen til en resurs du stoler på - da de gir deg tilgang til alle dine data.', + 'api_token_title' => 'Personlige tilgangsnøkler', + 'api_token_create_new' => 'Opprett Nytt Token', + 'api_token_not_created' => 'Du har ikke opprettet noen personlig tilgangsnøkkel.', + 'api_token_name' => 'Tokennavn', + 'api_token_expire' => 'Utløper den {date}', + 'api_token_delete' => 'Slett', + 'api_token_create' => 'Opprett Token', + 'api_token_scopes' => 'Omfang', + 'api_token_help' => 'Her er din nye personlige tilgangstoken. Dette er den eneste gangen den blir vist, ikke mist den! Nå kan du bruke dette tokenet til å lage API-forespørsler.', + + 'api_oauth_clients' => 'Dine OAuth-klienter', + 'api_oauth_clients_desc' => 'Denne lar deg registrere dine egne OAuth-klienter.', + 'api_oauth_clients_desc2' => 'Bruk denne klient-id til å be om en ny token, og konvertere autorisasjonskoder til å få tilgang til token. Se Laravel Passport-dokumentasjon for mer informasjon.', + 'api_oauth_title' => 'OAuth klienter', + 'api_oauth_create_new' => 'Opprett ny klient', + 'api_oauth_edit' => 'Rediger kunde', + 'api_oauth_not_created' => 'Du har ikke opprettet noen OAuth klienter.', + 'api_oauth_clientid' => 'Klient ID', + 'api_oauth_name' => 'Navn', + 'api_oauth_name_help' => 'Noe brukerne dine vil gjenkjenne og stole på.', + 'api_oauth_secret' => 'Hemmelig', + 'api_oauth_create' => 'Opprett kunde', + 'api_oauth_redirecturl' => 'Omdirigere URL', + 'api_oauth_redirecturl_help' => 'Appens autorisasjon tilbakeringing URL.', + + 'api_authorized_clients' => 'Liste over autoriserte klienter', + 'api_authorized_clients_desc' => 'Denne delen viser alle klientene du har autorisert til å få tilgang til dine programdata. Du kan når som helst tilbakekalle denne autorisasjonen.', + 'api_authorized_clients_title' => 'Autoriserte applikasjoner', + 'api_authorized_clients_none' => 'Det finnes ingen autoriserte klienter ennå.', + 'api_authorized_clients_name' => 'Navn', + 'api_authorized_clients_scopes' => 'Omfang', + + 'personalization_tab_title' => 'Tilpass din konto', + + 'personalization_title' => 'Her finner du forskjellige innstillinger for å konfigurere kontoen din. Disse funksjonene er ment for "avanserte brukere" som vil ha maksimal kontroll over Monica.', + 'personalization_contact_field_type_title' => 'Kontaktfelt typer', + 'personalization_contact_field_type_add' => 'Legg til ny felttype', + 'personalization_contact_field_type_description' => 'Du kan konfigurere alle de ulike typene kontaktfeltene som du kan knytte til alle kontaktene dine. For eksempel, hvis et nytt sosialt nettverk vises i fremtiden, du vil kunne legge til denne nye måten å kommunisere med kontaktene dine på her.', + 'personalization_contact_field_type_table_name' => 'Navn', + 'personalization_contact_field_type_table_protocol' => 'Protokoll', + 'personalization_contact_field_type_table_actions' => 'Handlinger', + 'personalization_contact_field_type_modal_title' => 'Legg til en ny kontaktfelttype', + 'personalization_contact_field_type_modal_edit_title' => 'Rediger en eksisterende kontaktfelttype', + 'personalization_contact_field_type_modal_delete_title' => 'Slette en eksisterende kontaktfelttype', + 'personalization_contact_field_type_modal_delete_description' => 'Er du sikker på at du vil slette denne kontakt felttypen? Slette denne typen kontaktfelt vil slette ALLE data med denne typen for alle kontaktene dine.', + 'personalization_contact_field_type_modal_name' => 'Navn', + 'personalization_contact_field_type_modal_protocol' => 'Protokoll (valgfritt)', + 'personalization_contact_field_type_modal_protocol_help' => 'Hver nye kontaktfelttype kan være klikkbare. Hvis en protokoll er satt vil vi bruke den til å utløse handlingen som er satt.', + 'personalization_contact_field_type_modal_icon' => 'Ikon (valgfritt)', + 'personalization_contact_field_type_modal_icon_help' => 'Du kan tilknytte et ikon med denne kontaktfelttypen. Du må legge til en referanse til skrifttype Awesome icon.', + 'personalization_contact_field_type_delete_success' => 'Kontaktfelt type har blitt slettet.', + 'personalization_contact_field_type_add_success' => 'Kontaktfelt typen har blitt opprettet.', + 'personalization_contact_field_type_edit_success' => 'Kontaktfelt type har nå blitt oppdatert.', + + 'personalization_genders_title' => 'Kjønn typer', + 'personalization_genders_add' => 'Legg til ny kjønnstype', + 'personalization_genders_desc' => 'Du kan definere så mange kjønn som du trenger det. Du trenger minst én kjønn type på kontoen din.', + 'personalization_genders_modal_add' => 'Legg til kjønn type', + 'personalization_genders_modal_edit' => 'Oppdater kjønnstype', + 'personalization_genders_modal_name' => 'Navn', + 'personalization_genders_modal_name_help' => 'Navnet som brukes til å vise kjønnet på en kontaktside.', + 'personalization_genders_modal_sex' => 'Kjønn', + 'personalization_genders_modal_sex_help' => 'Brukes til å definere forholdet og under VCard-import/eksport-prosessen.', + 'personalization_genders_modal_default' => 'Velg standard kjønn for en ny kontakt', + 'personalization_genders_modal_delete' => 'Slett kjønnstype', + 'personalization_genders_modal_delete_desc' => 'Er du sikker på at du vil slette kjønnet "{name}”?', + 'personalization_genders_modal_delete_question' => 'Du har for øyeblikket {count} kontakt med dette kjønnet Hvis du sletter denne kjønn, hvilket kjønn skal du ta bort denne kontakten? Du har for tiden {count} kontakter med dette kjønnet. Hvis du sletter denne kjønn, hvilket kjønn skal disse kontaktene ha?', + 'personalization_genders_modal_delete_question_default' => 'Dette kjønn er standard. Hvis du sletter denne kjønn som vil være den nye standarden?', + 'personalization_genders_modal_error' => 'Velg et kjønn fra listen.', + 'personalization_genders_list_contact_number' => '{count} kontakt|{count} kontakter', + 'personalization_genders_table_name' => 'Navn', + 'personalization_genders_table_sex' => 'Kjønn', + 'personalization_genders_table_default' => 'Standard', + 'personalization_genders_default' => 'Standard kjønn', + 'personalization_genders_make_default' => 'Endre standard kjønn', + 'personalization_genders_select_default' => 'Velg standard kjønn', + 'personalization_genders_m' => 'Mann', + 'personalization_genders_f' => 'Kvinne', + 'personalization_genders_o' => 'Annet', + 'personalization_genders_u' => 'Ukjent', + 'personalization_genders_n' => 'Ingen eller ikke relevant', + + 'personalization_reminder_rule_save' => 'Endringen er lagret', + 'personalization_reminder_rule_title' => 'Regler for påminnelse', + 'personalization_reminder_rule_line' => '{count} dag før{count} dager før', + 'personalization_reminder_rule_desc' => 'For hver påminnelse du angir, kan Monica sende deg en e-post med en rekke dager før arrangementet skjer. Du kan justere disse varslingsinnstillingene her. Disse varslene gjelder bare for månedlige og årlige påminnelser.', + + 'personalization_module_save' => 'Endringen er lagret', + 'personalization_module_title' => 'Funksjoner', + 'personalization_module_desc' => 'Det er mulig at du ikke trenger alle funksjonene til Monica. Nedenfor kan du endre bestemte funksjoner som brukes på et kontaktark. Denne endringen vil påvirke ALLE kontaktene dine. Ved å skru av en funksjon sletter du ikke data, vil den bare skjules funksjonen.', + + 'personalisation_paid_upgrade' => 'Dette er en premium-funksjon som krever at et betalt abonnement er aktiv. Oppgrader kontoen din ved å gå til Innstillinger > Abonnement.', + 'personalisation_paid_upgrade_vue' => 'Dette er en premium-funksjon som krever at et betalt abonnement er aktiv. Oppgrader kontoen din ved å gå til Innstillinger > Abonnement.', + + 'reminder_time_to_send' => 'Tidspunkt på dagen du ønsker påminnelser skal bli sendt', + 'reminder_time_to_send_help' => 'Din neste påminnelse er planlagt å bli sendt den {dateTime}.', + + 'personalization_activity_type_category_title' => 'Kategorier av aktivitetstype', + 'personalization_activity_type_category_add' => 'Legg til en ny aktivitetstype kategori', + 'personalization_activity_type_category_table_name' => 'Navn', + 'personalization_activity_type_category_description' => 'En aktivitet med en av kontaktene dine kan ha en type og kategori type. Kontoen din kommer med et sett med forhåndsdefinerte kategorityper som standard, men du kan tilpasse disse her.', + 'personalization_activity_type_category_table_actions' => 'Handlinger', + 'personalization_activity_type_category_modal_add' => 'Legg til en ny aktivitetstype kategori', + 'personalization_activity_type_category_modal_edit' => 'Endre en aktivitetstype kategori', + 'personalization_activity_type_category_modal_question' => 'Hva bør vi kalle denne nye kategorien?', + 'personalization_activity_type_add_button' => 'Legg til en ny aktivitetstype', + 'personalization_activity_type_modal_add' => 'Legg til en ny aktivitetstype', + 'personalization_activity_type_modal_question' => 'Hva bør vi kalle denne nye aktivitetstypen?', + 'personalization_activity_type_modal_edit' => 'Endre en aktivitetstype', + 'personalization_activity_type_category_modal_delete' => 'Slett aktivitetstype kategori', + 'personalization_activity_type_category_modal_delete_desc' => 'Er du sikker på at du vil slette denne kategorien? Sletting vil slette alle tilknyttede aktivitetstyper. Aktiviteter som tilhører denne kategorien vil ikke bli påvirket av denne slettingen.', + 'personalization_activity_type_modal_delete' => 'Slett en aktivitetstype', + 'personalization_activity_type_modal_delete_desc' => 'Er du sikker på at du vil slette denne aktivitetstypen? Aktiviteter som tilhører denne kategorien påvirkes ikke av denne slettingen.', + 'personalization_activity_type_modal_delete_error' => 'Vi finner ikke denne aktivitetstypen.', + 'personalization_activity_type_category_modal_delete_error' => 'Vi finner ikke denne aktivitetstype kategorien.', + + 'personalization_life_event_category_title' => 'Kategorier for livets hendelser', + 'personalization_live_event_category_table_name' => 'Navn', + 'personalization_life_event_category_description' => 'En livshendelse kan ha en type og en kategori. Kontoen din kommer med et sett av forhåndsdefinerte kategorier og typer som standard, men du kan tilpasse livstyper her.', + 'personalization_live_event_category_table_actions' => 'Handlinger', + 'personalization_life_event_type_add_button' => 'Legg til en ny hendelsestype', + 'personalization_life_event_type_modal_add' => 'Legg til en ny livs hendelsetype', + 'personalization_life_event_type_modal_question' => 'Hva skal vi kalle denne nye livshendelsestypen?', + 'personalization_life_event_type_modal_edit' => 'Rediger en livshendelsetype', + 'personalization_life_event_type_modal_delete' => 'Slette en livshendelse', + 'personalization_life_event_type_modal_delete_desc' => 'Er du sikker på at du vil slette denne livshendelsen? Livshendelser som tilhører denne typen vil bli slettet ved å utføre denne handlingen.', + 'personalization_life_event_type_modal_delete_error' => 'Vi kan ikke finne denne typen livshendelse.', + + 'personalization_life_event_category_work_education' => 'Arbeid og utdanning', + 'personalization_life_event_category_family_relationships' => 'Familier og relasjoner', + 'personalization_life_event_category_home_living' => 'Hjem og hendelser', + 'personalization_life_event_category_travel_experiences' => 'Reiser og opplevelser', + 'personalization_life_event_category_health_wellness' => 'Helse og velvære', + + 'personalization_life_event_type_new_job' => 'Ny jobb', + 'personalization_life_event_type_retirement' => 'Pensjonert', + 'personalization_life_event_type_new_school' => 'Ny skole', + 'personalization_life_event_type_study_abroad' => 'Studie i utlandet', + 'personalization_life_event_type_volunteer_work' => 'Frivillig arbeid', + 'personalization_life_event_type_published_book_or_paper' => 'Publiserte bok eller avisartikkel', + 'personalization_life_event_type_military_service' => 'Militær tjeneste', + 'personalization_life_event_type_first_met' => 'Første møte', + 'personalization_life_event_type_new_relationship' => 'Nytt forhold', + 'personalization_life_event_type_engagement' => 'Engasjement', + 'personalization_life_event_type_marriage' => 'Ekteskap', + 'personalization_life_event_type_anniversary' => 'Jubileum', + 'personalization_life_event_type_expecting_a_baby' => 'Forventer en baby', + 'personalization_life_event_type_new_child' => 'Nytt barn', + 'personalization_life_event_type_new_family_member' => 'Nytt familiemedlem', + 'personalization_life_event_type_new_pet' => 'Nytt kjæledyr', + 'personalization_life_event_type_end_of_relationship' => 'Avsluttet forholdet', + 'personalization_life_event_type_loss_of_a_loved_one' => 'Mistet et familiemedlem', + 'personalization_life_event_type_moved' => 'Flyttet', + 'personalization_life_event_type_bought_a_home' => 'Kjøpt et hjem', + 'personalization_life_event_type_home_improvement' => 'Oppussing', + 'personalization_life_event_type_holidays' => 'Ferier', + 'personalization_life_event_type_new_vehicle' => 'Nytt kjøretøy', + 'personalization_life_event_type_new_roommate' => 'Ny samboer', + 'personalization_life_event_type_overcame_an_illness' => 'Overkom en sykdom', + 'personalization_life_event_type_quit_a_habit' => 'Avslutt en vane', + 'personalization_life_event_type_new_eating_habits' => 'Nye spisevaner', + 'personalization_life_event_type_weight_loss' => 'Vekttap', + 'personalization_life_event_type_wear_glass_or_contact' => 'Startet å bruke briller eller kontaktlinser', + 'personalization_life_event_type_broken_bone' => 'Knakk et bein', + 'personalization_life_event_type_removed_braces' => 'Fikk fjernet regulering', + 'personalization_life_event_type_surgery' => 'Fikk kirurgi', + 'personalization_life_event_type_dentist' => 'Hadde tannbehandling', + 'personalization_life_event_type_new_sport' => 'Startet med en ny sport', + 'personalization_life_event_type_new_hobby' => 'Begynte med en ny hobby', + 'personalization_life_event_type_new_instrument' => 'Begynte å lære et nytt instrument', + 'personalization_life_event_type_new_language' => 'Startet å lære et nytt språk', + 'personalization_life_event_type_tattoo_or_piercing' => 'Tatovering eller piercing', + 'personalization_life_event_type_new_license' => 'Nytt sertifikat', + 'personalization_life_event_type_travel' => 'Reise', + 'personalization_life_event_type_achievement_or_award' => 'Prestasjon eller pris', + 'personalization_life_event_type_changed_beliefs' => 'Endret livs tro', + 'personalization_life_event_type_first_word' => 'Første ord', + 'personalization_life_event_type_first_kiss' => 'Første kyss', + + 'storage_title' => 'Lagring', + 'storage_account_info' => 'Begrensningen på kontoen din er :accountLimit MB. Din gjeldende bruk er :currentAccountSize MB (ca :percentUsage%).', + 'storage_upgrade_notice' => 'Oppgrader kontoen din for å kunne laste opp dokumenter og bilder.', + 'storage_description' => 'Her kan du se alle dokumenter og bilder som er lastet opp under kontaktene dine.', + + 'dav_title' => 'WebDAV', + 'dav_description' => 'Her finner du alle innstillingene til å bruke med WebDAV-ressurser til CardDAV-og CalDAV-eksport.', + 'dav_copy_help' => 'Kopier til utklippstavle', + 'dav_clipboard_copied' => 'Verdi kopiert til utklippstavle', + 'dav_url_base' => 'Basis url for alle CardDAV-og CalDAV-ressurser:', + 'dav_connect_help' => 'Du kan koble kontakter og/eller kalendere med denne base-url på din telefon eller datamaskin.', + 'dav_connect_help2' => 'Bruk innloggingen din (e-post) og opprett et API-token som passord for å godkjenne.', + 'dav_url_carddav' => 'CardDAV url for kontakt-ressurs:', + 'dav_url_caldav_birthdays' => 'CalDAV url for bursdags ressurser:', + 'dav_url_caldav_tasks' => 'CalDAV-url for oppgave-ressurser:', + 'dav_title_carddav' => 'CardDAV', + 'dav_title_caldav' => 'CalDAV', + 'dav_carddav_export' => 'Eksporter alle kontakter i en fil', + 'dav_caldav_birthdays_export' => 'Eksporter alle bursdager i en fil', + 'dav_caldav_tasks_export' => 'Eksporter alle oppgaver til en fil', + + 'archive_title' => 'Arkiver alle kontaktene i kontoen din', + 'archive_desc' => 'Dette vil arkivere alle kontaktene i kontoen din.', + 'archive_cta' => 'Arkiver alle kontaktene dine', + + 'logs_title' => 'Alt som har skjedd med denne kontoen', + 'logs_actor' => 'Aktør', + 'logs_timestamp' => 'Tidsstempel', + 'logs_description' => 'Beskrivelse', + 'logs_subject' => 'Emne', + 'logs_size' => 'Størrelse (Kb)', + 'logs_object' => 'Objekt', +]; diff --git a/resources/lang/no/validation.php b/resources/lang/no/validation.php new file mode 100644 index 0000000..2789a18 --- /dev/null +++ b/resources/lang/no/validation.php @@ -0,0 +1,166 @@ + ':attribute må bli godtatt.', + 'active_url' => ':attribute er ikke en gyldig URL.', + 'after' => ':attribute må være en dato etter :date.', + 'after_or_equal' => ':attribute må tidligst være datoen :date.', + 'alpha' => ':attribute kan kun inneholde bokstaver.', + 'alpha_dash' => ':attribute kan bare inneholde bokstaver, tall, bindestrek, og understrek.', + 'alpha_num' => ':attribute kan bare inneholde tall og bokstaver.', + 'array' => ':attribute må være en matrise.', + 'before' => ':attribute må være en dato tidligere enn :date.', + 'before_or_equal' => ':attribute må være en dato før eller lik :date.', + 'between' => [ + 'numeric' => ':attribute må være mellom :min og :max.', + 'file' => ':attribute må være mellom :min og :max kilobytes.', + 'string' => ':attribute må være mellom :min og :max tegn.', + 'array' => ':attribute må være mellom :min og :max elementer.', + ], + 'boolean' => ':attribute må være sann eller usann.', + 'confirmed' => ':attribute bekreftelsen stemmer ikke overens.', + 'date' => ':attribute er ikke en gyldig dato.', + 'date_equals' => ':attribute må være en dato samsvarende med :date.', + 'date_format' => ':attribute samsvarer ikke med formatet :format.', + 'different' => ':attribute og :other må være forskjellige.', + 'digits' => 'Attributtet :attribute må være :digits sifre.', + 'digits_between' => ':attribute må være mellom :min og :max sifre.', + 'dimensions' => ':attribute har ugyldig bildestørrelse.', + 'distinct' => ':attribute har en duplikatverdi.', + 'email' => ':attribute må være en gyldig e-postadresse.', + 'ends_with' => ':attribute må avsluttes med en av følgende: :values.', + 'exists' => 'Valgt :attribute er ugyldig.', + 'file' => ':attribute må være en fil.', + 'filled' => ':attribute må inneholde en verdi.', + 'gt' => [ + 'numeric' => ':attribute må være større enn :value.', + 'file' => ':attribute må være større enn :value kilobytes.', + 'string' => ':attribute må ha flere enn :value tegn.', + 'array' => ':attribute må inneholde flere enn :value elementer.', + ], + 'gte' => [ + 'numeric' => ':attribute må være større enn eller samsvarende med :value.', + 'file' => ':attribute må være større enn eller samsvarende med :value kilobytes.', + 'string' => ':attribute må være større enn eller samsvarende med :value tegn.', + 'array' => ':attribute må være :value elementer eller mer.', + ], + 'image' => ':attribute må være et bilde.', + 'in' => 'Valgt :attribute er ugyldig.', + 'in_array' => ':attribute feltet finnes ikke i :other.', + 'integer' => ':attribute må være ett helt tall.', + 'ip' => ':attribute må være en gyldig IP-adresse.', + 'ipv4' => ':attribute må være en gyldig IPv4-adresse.', + 'ipv6' => ':attribute må være en gyldig IPv6-adresse.', + 'json' => ':attribute må være en gyldig JSON-streng.', + 'lt' => [ + 'numeric' => ':attribute må være mindre enn :value.', + 'file' => ':attribute må være mindre enn :value kilobytes.', + 'string' => ':attribute må være færre enn :value tegn.', + 'array' => ':attribute må ha færre enn :value elementer.', + ], + 'lte' => [ + 'numeric' => ':attribute må være mindre enn eller samsvarende med :value.', + 'file' => ':attribute må være mindre enn eller samsvarende med :value kilobytes.', + 'string' => ':attribute må være mindre enn eller tilsvarende :value tegn.', + 'array' => ':attribute må ikke inneholde flere enn :value elementer.', + ], + 'max' => [ + 'numeric' => ':attribute kan ikke være større enn :max.', + 'file' => ':attribute kan ikke være større enn :max kilobytes.', + 'string' => ':attribute kan ikke være større enn :max tegn.', + 'array' => ':attribute kan ikke inneholde mer enn :max elementer.', + ], + 'mimes' => ':attribute må være av filtypen: :values.', + 'mimetypes' => ':attribute må være av filtypen: :values.', + 'min' => [ + 'numeric' => ':attribute må være minst :min.', + 'file' => ':attribute må være minimum :min kilobytes.', + 'string' => ':attribute må ha minst :min tegn.', + 'array' => ':attribute må inneholde :min elementer.', + ], + 'not_in' => 'Valgt :attribute er ugyldig.', + 'not_regex' => 'Formatet er ugyldig (:attribute).', + 'numeric' => ':attribute må være et tall.', + 'password' => 'Passordet er feil.', + 'present' => ':attribute må finnes.', + 'regex' => 'Formatet er ugyldig (:attribute).', + 'required' => ':attribute feltet er påkrevd.', + 'required_if' => ':attribute er påkrevd når :oher er :value.', + 'required_unless' => ':attribute feltet er påkrevd med mindre :other er i :values.', + 'required_with' => ':attribute er påkrevd når :values er tilstede.', + 'required_with_all' => ':attribute er påkrevd når :values er tilstede.', + 'required_without' => ':attribute kreves når ingen av :values er til stede.', + 'required_without_all' => ':attribute er påkrevd når ingen av :values er tilstede.', + 'same' => ':attribute og :other må være like.', + 'size' => [ + 'numeric' => ':attribute må være :size.', + 'file' => ':attribute må være :size kilobytes.', + 'string' => ':attribute må være :size tegn.', + 'array' => ':attribute må inneholde :size elementer.', + ], + 'starts_with' => ':attribute må begynne med en av de følgende: :values.', + 'string' => ':attribute må være en tekst.', + 'timezone' => ':attribute må være en gyldig tidssone.', + 'unique' => ':attribute er allerede brukt.', + 'uploaded' => ':attribute opplasting feilet.', + 'url' => 'Formatet er ugyldig (:attribute).', + 'uuid' => ':attribute må være en gyldig UUID.', + + /* + |-------------------------------------------------------------------------- + | Custom Validation Language Lines + |-------------------------------------------------------------------------- + | + | Here you may specify custom validation messages for attributes using the + | convention "attribute.rule" to name the lines. This makes it quick to + | specify a specific custom language line for a given attribute rule. + | + */ + + 'custom' => [ + 'attribute-name' => [ + 'rule-name' => 'custom-message', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Custom Validation Attributes + |-------------------------------------------------------------------------- + | + | The following language lines are used to swap our attribute placeholder + | with something more reader friendly such as "E-Mail Address" instead + | of "email". This simply helps us make our message more expressive. + | + */ + + 'attributes' => [], + + 'vue' => [ + 'max' => [ + 'numeric' => '{field} kan ikke være større enn {max}.', + 'string' => '{field} må ikke være lengre enn {max} tegn.', + ], + 'required' => '{field} er påkrevd.', + 'url' => '{field} er ikke en gyldig URL.', + ], + +]; diff --git a/resources/lang/pt-BR.json b/resources/lang/pt-BR.json new file mode 100644 index 0000000..ddea72e --- /dev/null +++ b/resources/lang/pt-BR.json @@ -0,0 +1,7 @@ +{ + "The :attribute must contain at least one uppercase and one lowercase letter.": "The :attribute must contain at least one uppercase and one lowercase letter.", + "The :attribute must contain at least one letter.": "The :attribute must contain at least one letter.", + "The :attribute must contain at least one symbol.": "The :attribute must contain at least one symbol.", + "The :attribute must contain at least one number.": "The :attribute must contain at least one number.", + "The given :attribute has appeared in a data leak. Please choose a different :attribute.": "The given :attribute has appeared in a data leak. Please choose a different :attribute." +} diff --git a/resources/lang/pt-BR/app.php b/resources/lang/pt-BR/app.php new file mode 100644 index 0000000..4a024b2 --- /dev/null +++ b/resources/lang/pt-BR/app.php @@ -0,0 +1,571 @@ + 'Sim', + 'no' => 'Não', + 'update' => 'Atualizar', + 'save' => 'Salvar', + 'add' => 'Adicionar', + 'cancel' => 'Cancelar', + 'confirm' => 'Confirmar', + 'delete_confirm' => 'Tem certeza?', + 'delete' => 'Excluir', + 'edit' => 'Editar', + 'upload' => 'Upload', + 'download' => 'Baixar', + 'save_close' => 'Salvar e fechar', + 'close' => 'Fechar', + 'copy' => 'Copiar', + 'create' => 'Criar', + 'remove' => 'Remover', + 'revoke' => 'Revogar', + 'done' => 'Concluído', + 'back' => 'Voltar', + 'verify' => 'Verificar', + 'new' => 'novo', + 'unknown' => 'Eu não sei', + 'load_more' => 'Carregar mais', + 'loading' => 'Carregando…', + 'with' => 'com', + 'today' => 'hoje', + 'yesterday' => 'ontem', + 'another_day' => 'outro dia', + 'date' => 'Data', + 'type' => 'Tipo', + 'zoom' => 'Zoom', + 'upgrade' => 'Assine para desbloquear', + 'percent_uploaded' => '{percent}% enviado', + 'retry' => 'Tentar novamente', + 'filter' => 'Filtrar lista', + 'go_back' => 'Voltar', + 'file_selected' => 'Um arquivo selecionado…|{count} arquivos selecionados…', + + 'application_title' => 'Monica – Gerenciador de relacionamento pessoal', + 'application_description' => 'Monica é uma ferramenta para gerenciar suas interações com seus amigos, familiares e pessoas queridas.', + 'application_og_title' => 'Fortaleça seu relacionamento com seus entes queridos. CRM online gratuito para amigos e família.', + + 'markdown_description' => 'Want to format your text in a nice way? We support Markdown to add bold, italic, lists and more.', + 'markdown_link' => 'Ler documentação', + + 'header_settings_link' => 'Configurações', + 'header_logout_link' => 'Sair', + 'header_changelog_link' => 'Atualizações de produtos', + + 'main_nav_cta' => 'Adicionar contatos', + 'main_nav_dashboard' => 'Dashboard', + 'main_nav_family' => 'Contatos', + 'main_nav_journal' => 'Diário', + 'main_nav_activities' => 'Atividades', + 'main_nav_tasks' => 'Tarefas', + + 'footer_remarks' => 'Comentários?', + 'footer_send_email' => 'Envie-nos um email', + 'footer_privacy' => 'Política de Privacidade', + 'footer_release' => 'Notas da versão', + 'footer_newsletter' => 'Newsletter', + 'footer_source_code' => 'Contribuir', + 'footer_version' => 'Versão: :version', + 'footer_new_version' => 'Uma nova versão de Monica está disponível', + + 'footer_modal_version_whats_new' => 'Novidades', + 'footer_modal_version_release_away' => 'Sua instalação está 1 versão atrás da versão mais recente. Atualize para aproveitar as novidades.|Sua instalação está :number versões atrás da versão mais recente. Atualize para aproveitar as novidades.', + + 'breadcrumb_dashboard' => 'Dashboard', + 'breadcrumb_list_contacts' => 'Lista de contatos', + 'breadcrumb_archived_contacts' => 'Contatos arquivados', + 'breadcrumb_journal' => 'Diário', + 'breadcrumb_settings' => 'Configurações', + 'breadcrumb_settings_export' => 'Exportar', + 'breadcrumb_settings_users' => 'Usuários', + 'breadcrumb_settings_users_add' => 'Adicionar usuário', + 'breadcrumb_settings_subscriptions' => 'Assinatura', + 'breadcrumb_settings_import' => 'Importar', + 'breadcrumb_settings_import_report' => 'Relatório de importação', + 'breadcrumb_settings_import_upload' => 'Enviar', + 'breadcrumb_settings_tags' => 'Etiquetas', + 'breadcrumb_add_significant_other' => 'Adicionar companheiro(a)', + 'breadcrumb_edit_significant_other' => 'Editar companheiro(a)', + 'breadcrumb_add_note' => 'Adicionar nota', + 'breadcrumb_edit_note' => 'Editar nota', + 'breadcrumb_api' => 'API', + 'breadcrumb_dav' => 'DAV Resources', + 'breadcrumb_edit_introductions' => 'Como se conheceram', + 'breadcrumb_settings_personalization' => 'Personalização', + 'breadcrumb_settings_security' => 'Segurança', + 'breadcrumb_settings_security_2fa' => 'Autenticação de dois fatores', + 'breadcrumb_profile' => 'Perfil de :name', + + 'gender_male' => 'Homem', + 'gender_female' => 'Mulher', + 'gender_none' => 'Prefiro não dizer', + 'gender_no_gender' => 'Sem gênero', + + 'error_title' => 'Ops! Algo deu errado.', + 'error_unauthorized' => 'Você não tem permissão para editar este recurso.', + 'error_user_account' => 'Esse usuário não pertence à conta fornecida.', + 'error_save' => 'Ocorreu um erro ao tentar salvar os dados.', + 'error_try_again' => 'Algo deu errado. Por favor, tente novamente.', + 'error_id' => 'Error ID: :id', + 'error_unavailable' => 'Serviço indisponível', + 'error_maintenance' => 'Manutenção em andamento. Voltaremos em breve.', + 'error_help' => 'Voltaremos em breve.', + 'error_twitter' => 'Siga-nos no Twitter para saber quando voltamos.', + 'error_no_term' => 'Não há nenhuma regra para esta instância até o momento.', + + 'default_save_success' => 'Dados salvos com sucesso!', + + 'compliance_title' => 'Desculpa pelo incômodo.', + 'compliance_desc' => 'Alteramos nossos Termos de Uso e Política de Privacidade. Por lei, solicitamos que você os revise e aceite ambos para que possa continuar usando sua conta.', + 'compliance_desc_end' => 'We don’t do anything nasty with your data or account and will never do.', + 'compliance_terms' => 'Aceitar os novos Termos e Política de Privacidade', + + // Relationship types + // Yes, each relationship type has 8 strings associated with it. + // This is because we need to indicate the name of the relationship type, + // and also the name of the opposite side of this relationship (father/son), + // and then, the feminine version of the string. Finally, in some sentences + // in the UI, we need to include the name of the person we add the relationship + // to. + 'relationship_type_group_love' => 'Relacionamentos amorosos', + 'relationship_type_group_family' => 'Relacionamentos familiares', + 'relationship_type_group_friend' => 'Relacionamentos de amizade', + 'relationship_type_group_work' => 'Relacionamentos profissionais', + 'relationship_type_group_other' => 'Outros tipos de relações', + + 'relationship_type_partner' => 'companheiro', + 'relationship_type_partner_female' => 'companheira', + 'relationship_type_partner_male' => 'significant other', + 'relationship_type_partner_with_name' => 'companheiro de :name', + 'relationship_type_partner_female_with_name' => 'companheira de :name', + 'relationship_type_partner_male_with_name' => ':name’s significant other', + + 'relationship_type_spouse' => 'esposo', + 'relationship_type_spouse_female' => 'esposa', + 'relationship_type_spouse_male' => 'marido', + 'relationship_type_spouse_with_name' => 'esposo de :name', + 'relationship_type_spouse_female_with_name' => 'esposa de :name', + 'relationship_type_spouse_male_with_name' => 'marido de :name', + + 'relationship_type_date' => 'namorado', + 'relationship_type_date_female' => 'namorada', + 'relationship_type_date_male' => 'date', + 'relationship_type_date_with_name' => 'namorado de :name', + 'relationship_type_date_female_with_name' => 'namorada de :name', + 'relationship_type_date_male_with_name' => ':name’s date', + + 'relationship_type_lover' => 'amante', + 'relationship_type_lover_female' => 'amante', + 'relationship_type_lover_male' => 'lover', + 'relationship_type_lover_with_name' => 'amante de :name', + 'relationship_type_lover_female_with_name' => 'amante de :name', + 'relationship_type_lover_male_with_name' => ':name’s lover', + + 'relationship_type_inlovewith' => 'apaixonado por', + 'relationship_type_inlovewith_female' => 'apaixonada por', + 'relationship_type_inlovewith_male' => 'in love with', + 'relationship_type_inlovewith_with_name' => 'alguém que :name está apaixonado', + 'relationship_type_inlovewith_female_with_name' => 'alguém que :name está apaixonada', + 'relationship_type_inlovewith_male_with_name' => 'someone :name is in love with', + + 'relationship_type_lovedby' => 'amado por', + 'relationship_type_lovedby_female' => 'amada por', + 'relationship_type_lovedby_male' => 'loved by', + 'relationship_type_lovedby_with_name' => 'amor secreto de :name', + 'relationship_type_lovedby_female_with_name' => 'amor secreto de :name', + 'relationship_type_lovedby_male_with_name' => ':name’s secret lover', + + 'relationship_type_ex' => 'ex-partner', + 'relationship_type_ex_female' => 'ex-namorada', + 'relationship_type_ex_male' => 'ex-namorado', + 'relationship_type_ex_with_name' => ':name’s ex-partner', + 'relationship_type_ex_female_with_name' => 'ex-namorada de :name', + 'relationship_type_ex_male_with_name' => 'ex-namorado de :name', + + 'relationship_type_parent' => 'pais', + 'relationship_type_parent_female' => 'mãe', + 'relationship_type_parent_male' => 'pai', + 'relationship_type_parent_with_name' => ':name’s parent', + 'relationship_type_parent_female_with_name' => 'mãe de :name', + 'relationship_type_parent_male_with_name' => ':name’s father', + + 'relationship_type_child' => 'child', + 'relationship_type_child_female' => 'filha', + 'relationship_type_child_male' => 'filho', + 'relationship_type_child_with_name' => ':name’s child', + 'relationship_type_child_female_with_name' => 'filha de :name', + 'relationship_type_child_male_with_name' => ':name’s son', + + 'relationship_type_stepparent' => 'step-parent', + 'relationship_type_stepparent_female' => 'madrasta', + 'relationship_type_stepparent_male' => 'stepfather', + 'relationship_type_stepparent_with_name' => ':name’s step-parent', + 'relationship_type_stepparent_female_with_name' => 'madrasta de :name', + 'relationship_type_stepparent_male_with_name' => ':name’s stepfather', + + 'relationship_type_stepchild' => 'stepchild', + 'relationship_type_stepchild_female' => 'enteada', + 'relationship_type_stepchild_male' => 'stepson', + 'relationship_type_stepchild_with_name' => ':name’s stepchild', + 'relationship_type_stepchild_female_with_name' => 'enteada de :name', + 'relationship_type_stepchild_male_with_name' => ':name’s stepson', + + 'relationship_type_sibling' => 'sibling', + 'relationship_type_sibling_female' => 'irmã', + 'relationship_type_sibling_male' => 'brother', + 'relationship_type_sibling_with_name' => ':name’s sibling', + 'relationship_type_sibling_female_with_name' => 'irmã de :name', + 'relationship_type_sibling_male_with_name' => ':name’s brother', + + 'relationship_type_grandparent' => 'grandparent', + 'relationship_type_grandparent_female' => 'grandmother', + 'relationship_type_grandparent_male' => 'grandfather', + 'relationship_type_grandparent_with_name' => ':name’s grandparent', + 'relationship_type_grandparent_female_with_name' => ':name’s grandmother', + 'relationship_type_grandparent_male_with_name' => ':name’s grandfather', + + 'relationship_type_grandchild' => 'grandchild', + 'relationship_type_grandchild_female' => 'granddaughter', + 'relationship_type_grandchild_male' => 'grandson', + 'relationship_type_grandchild_with_name' => ':name’s grandchild', + 'relationship_type_grandchild_female_with_name' => 'neta da :name’s', + 'relationship_type_grandchild_male_with_name' => 'neto do :name\'s', + + 'relationship_type_uncle' => 'tio', + 'relationship_type_uncle_female' => 'tia', + 'relationship_type_uncle_male' => 'uncle', + 'relationship_type_uncle_with_name' => 'tio de :name', + 'relationship_type_uncle_female_with_name' => 'tia de :name', + 'relationship_type_uncle_male_with_name' => ':name’s uncle', + + 'relationship_type_nephew' => 'sobrinho', + 'relationship_type_nephew_female' => 'sobrinha', + 'relationship_type_nephew_male' => 'nephew', + 'relationship_type_nephew_with_name' => 'sobrinho de :name', + 'relationship_type_nephew_female_with_name' => 'sobrinha de :name', + 'relationship_type_nephew_male_with_name' => ':name’s nephew', + + 'relationship_type_cousin' => 'primo', + 'relationship_type_cousin_female' => 'prima', + 'relationship_type_cousin_male' => 'cousin', + 'relationship_type_cousin_with_name' => 'primo de :name', + 'relationship_type_cousin_female_with_name' => 'prima de :name', + 'relationship_type_cousin_male_with_name' => ':name’s cousin', + + 'relationship_type_godfather' => 'godparent', + 'relationship_type_godfather_female' => 'madrinha', + 'relationship_type_godfather_male' => 'godfather', + 'relationship_type_godfather_with_name' => ':name’s godparent', + 'relationship_type_godfather_female_with_name' => 'madrinha de :name', + 'relationship_type_godfather_male_with_name' => ':name’s godfather', + + 'relationship_type_godson' => 'godchild', + 'relationship_type_godson_female' => 'afilhada', + 'relationship_type_godson_male' => 'godson', + 'relationship_type_godson_with_name' => ':name’s godchild', + 'relationship_type_godson_female_with_name' => 'afilhada de :name', + 'relationship_type_godson_male_with_name' => ':name’s godson', + + 'relationship_type_friend' => 'amigo', + 'relationship_type_friend_female' => 'amiga', + 'relationship_type_friend_male' => 'amigo', + 'relationship_type_friend_with_name' => 'amigo de :name', + 'relationship_type_friend_female_with_name' => 'amiga de :name', + 'relationship_type_friend_male_with_name' => ':name’s friend', + + 'relationship_type_bestfriend' => 'melhor amigo', + 'relationship_type_bestfriend_female' => 'melhor amiga', + 'relationship_type_bestfriend_male' => 'melhor amigo', + 'relationship_type_bestfriend_with_name' => 'melhor amigo de :name', + 'relationship_type_bestfriend_female_with_name' => 'melhor amiga de :name', + 'relationship_type_bestfriend_male_with_name' => ':name’s best friend', + + 'relationship_type_colleague' => 'colega', + 'relationship_type_colleague_female' => 'colega', + 'relationship_type_colleague_male' => 'colleague', + 'relationship_type_colleague_with_name' => 'colega de :name', + 'relationship_type_colleague_female_with_name' => 'colega de :name', + 'relationship_type_colleague_male_with_name' => ':name’s colleague', + + 'relationship_type_boss' => 'chefe', + 'relationship_type_boss_female' => 'chefa', + 'relationship_type_boss_male' => 'chefe', + 'relationship_type_boss_with_name' => 'chefe de :name', + 'relationship_type_boss_female_with_name' => ':name do chefe', + 'relationship_type_boss_male_with_name' => ':name’s boss', + + 'relationship_type_subordinate' => 'funcionário', + 'relationship_type_subordinate_female' => 'funcionário', + 'relationship_type_subordinate_male' => 'funcionário', + 'relationship_type_subordinate_with_name' => ':name do funcionário', + 'relationship_type_subordinate_female_with_name' => ':name do funcionario', + 'relationship_type_subordinate_male_with_name' => 'Funcionário do :name’s', + + 'relationship_type_mentor' => 'mentor', + 'relationship_type_mentor_female' => 'mentora', + 'relationship_type_mentor_male' => 'mentor', + 'relationship_type_mentor_with_name' => 'mentor de :name', + 'relationship_type_mentor_female_with_name' => 'mentora de :name', + 'relationship_type_mentor_male_with_name' => 'mentor de :name’s', + + 'relationship_type_protege' => 'protégé', + 'relationship_type_protege_female' => 'protégé', + 'relationship_type_protege_male' => 'protégé', + 'relationship_type_protege_with_name' => ':name’s protégé', + 'relationship_type_protege_female_with_name' => ':name’s protégé', + 'relationship_type_protege_male_with_name' => ':name’s protégé', + + 'relationship_type_ex_husband' => 'ex-spouse', + 'relationship_type_ex_husband_female' => 'ex wife', + 'relationship_type_ex_husband_male' => 'ex-husband', + 'relationship_type_ex_husband_with_name' => ':name’s ex-spouse', + 'relationship_type_ex_husband_female_with_name' => ':name’s ex wife', + 'relationship_type_ex_husband_male_with_name' => ':name’s ex-husband', + + // emotions + 'emotion_primary_love' => 'Amor', + 'emotion_primary_joy' => 'Alegria', + 'emotion_primary_surprise' => 'Surpresa', + 'emotion_primary_anger' => 'Raiva', + 'emotion_primary_sadness' => 'Tristeza', + 'emotion_primary_fear' => 'Medo', + + 'emotion_secondary_affection' => 'Afeição', + 'emotion_secondary_lust' => 'Cobiça', + 'emotion_secondary_longing' => 'Desejo', + 'emotion_secondary_cheerfulness' => 'Felicidade', + 'emotion_secondary_zest' => 'Entusiasmo', + 'emotion_secondary_contentment' => 'Satisfação', + 'emotion_secondary_pride' => 'Orgulho', + 'emotion_secondary_optimism' => 'Otimismo', + 'emotion_secondary_enthrallment' => 'Encantado', + 'emotion_secondary_relief' => 'Alívio', + 'emotion_secondary_surprise' => 'Surpreso', + 'emotion_secondary_irritation' => 'Irritação', + 'emotion_secondary_exasperation' => 'Fúria', + 'emotion_secondary_rage' => 'Raiva', + 'emotion_secondary_disgust' => 'Nojo', + 'emotion_secondary_envy' => 'Inveja', + 'emotion_secondary_suffering' => 'Dor', + 'emotion_secondary_sadness' => 'Tristeza', + 'emotion_secondary_disappointment' => 'Desapontado', + 'emotion_secondary_shame' => 'Vergonha', + 'emotion_secondary_neglect' => 'Esquecido', + 'emotion_secondary_sympathy' => 'Simpatia', + 'emotion_secondary_horror' => 'Repúdio', + 'emotion_secondary_nervousness' => 'Nervosismo', + + 'emotion_adoration' => 'Adorado', + 'emotion_affection' => 'Afeição', + 'emotion_love' => 'Amor', + 'emotion_fondness' => 'Apreço', + 'emotion_liking' => 'Carinho', + 'emotion_attraction' => 'Atração', + 'emotion_caring' => 'Cuidado', + 'emotion_tenderness' => 'Ternura', + 'emotion_compassion' => 'Compaixão', + 'emotion_sentimentality' => 'Sentimental', + 'emotion_arousal' => 'Agitação', + 'emotion_desire' => 'Desejo', + 'emotion_lust' => 'Cobiça', + 'emotion_passion' => 'Paixão', + 'emotion_infatuation' => 'Fascínio', + 'emotion_longing' => 'Desejo', + 'emotion_amusement' => 'Diversão', + 'emotion_bliss' => 'Euforia', + 'emotion_cheerfulness' => 'Felicidade', + 'emotion_gaiety' => 'Alegria', + 'emotion_glee' => 'Alegria', + 'emotion_jolliness' => 'Contente', + 'emotion_joviality' => 'Jovialidade', + 'emotion_joy' => 'Alegria', + 'emotion_delight' => 'Deleite', + 'emotion_enjoyment' => 'Prazer', + 'emotion_gladness' => 'Alegria', + 'emotion_happiness' => 'Felicidade', + 'emotion_jubilation' => 'Júbilo', + 'emotion_elation' => 'Exaltação', + 'emotion_satisfaction' => 'Satisfação', + 'emotion_ecstasy' => 'Êxtase', + 'emotion_euphoria' => 'Euforia', + 'emotion_enthusiasm' => 'Entusiasmo', + 'emotion_zeal' => 'Zelo', + 'emotion_zest' => 'Animação', + 'emotion_excitement' => 'Excitação', + 'emotion_thrill' => 'Emocionado/a', + 'emotion_exhilaration' => 'Alegria', + 'emotion_contentment' => 'Satisfação', + 'emotion_pleasure' => 'Prazer', + 'emotion_pride' => 'Orgulho', + 'emotion_eagerness' => 'Ansiedade', + 'emotion_hope' => 'Esperança', + 'emotion_optimism' => 'Otimismo', + 'emotion_enthrallment' => 'Encanto', + 'emotion_rapture' => 'Êxtase', + 'emotion_relief' => 'Alívio', + 'emotion_amazement' => 'Incrível', + 'emotion_surprise' => 'Surpresa', + 'emotion_astonishment' => 'Espanto', + 'emotion_aggravation' => 'Encrenca', + 'emotion_irritation' => 'Irritação', + 'emotion_agitation' => 'Agitação', + 'emotion_annoyance' => 'Aborrecimento', + 'emotion_grouchiness' => 'Resmungar', + 'emotion_grumpiness' => 'Mau humor', + 'emotion_exasperation' => 'Exasperação', + 'emotion_frustration' => 'Frustração', + 'emotion_anger' => 'Raiva', + 'emotion_rage' => 'Raiva', + 'emotion_outrage' => 'Ultraje', + 'emotion_fury' => 'Fúria', + 'emotion_wrath' => 'Ira', + 'emotion_hostility' => 'Hostilidade', + 'emotion_ferocity' => 'Ferocidade', + 'emotion_bitterness' => 'Amargura', + 'emotion_hate' => 'Ódio', + 'emotion_loathing' => 'Repulsa', + 'emotion_scorn' => 'Desprezo', + 'emotion_spite' => 'Rancor', + 'emotion_vengefulness' => 'Vingança', + 'emotion_dislike' => 'Desgosto', + 'emotion_resentment' => 'Ressentimento', + 'emotion_disgust' => 'Nojo', + 'emotion_revulsion' => 'Repulsa', + 'emotion_contempt' => 'Desdém', + 'emotion_envy' => 'Inveja', + 'emotion_jealousy' => 'Ciúme', + 'emotion_agony' => 'Agonia', + 'emotion_suffering' => 'Sofrimento', + 'emotion_hurt' => 'Magoar', + 'emotion_anguish' => 'Angústia', + 'emotion_depression' => 'Muito triste', + 'emotion_despair' => 'Desespero', + 'emotion_hopelessness' => 'Desesperança', + 'emotion_gloom' => 'Melancolia', + 'emotion_glumness' => 'Desânimo', + 'emotion_sadness' => 'Tristeza', + 'emotion_unhappiness' => 'Infelicidade', + 'emotion_grief' => 'Pesar', + 'emotion_sorrow' => 'Aflição', + 'emotion_woe' => 'Problemão', + 'emotion_misery' => 'Miséria', + 'emotion_melancholy' => 'Melancólico', + 'emotion_dismay' => 'Consternado', + 'emotion_disappointment' => 'Decepção', + 'emotion_displeasure' => 'Desgosto', + 'emotion_guilt' => 'Culpa', + 'emotion_shame' => 'Vergonha', + 'emotion_regret' => 'Arrepender', + 'emotion_remorse' => 'Remorso', + 'emotion_alienation' => 'Alienação', + 'emotion_isolation' => 'Isolamento', + 'emotion_neglect' => 'Negligência', + 'emotion_loneliness' => 'Solidão', + 'emotion_rejection' => 'Rejeição', + 'emotion_homesickness' => 'Saudade', + 'emotion_defeat' => 'Derrota', + 'emotion_dejection' => 'Abatido', + 'emotion_insecurity' => 'Insegurança', + 'emotion_embarrassment' => 'Constrangimento', + 'emotion_humiliation' => 'Humilhação', + 'emotion_insult' => 'Insulto', + 'emotion_pity' => 'Pena', + 'emotion_sympathy' => 'Simpatia', + 'emotion_alarm' => 'Alarme', + 'emotion_shock' => 'Choque', + 'emotion_fear' => 'Medo', + 'emotion_fright' => 'Susto', + 'emotion_horror' => 'Horror', + 'emotion_terror' => 'Terror', + 'emotion_panic' => 'Pânico', + 'emotion_hysteria' => 'Histeria', + 'emotion_mortification' => 'Mortificação', + 'emotion_anxiety' => 'Ansiedade', + 'emotion_nervousness' => 'Nervosismo', + 'emotion_tenseness' => 'Tensão', + 'emotion_uneasiness' => 'Inquietação', + 'emotion_apprehension' => 'Apreensão', + 'emotion_worry' => 'Preocupação', + 'emotion_distress' => 'Angústia', + 'emotion_dread' => 'Pavor', + + // weather + 'weather_sunny' => 'Ensolarado', + 'weather_clear' => 'Limpo', + 'weather_clear-day' => 'Limpo', + 'weather_clear-night' => 'Noite limpa', + 'weather_light-drizzle' => 'Chuvisco leve', + 'weather_patchy-light-drizzle' => 'Garoa leve irregular', + 'weather_patchy-light-rain' => 'Chuva leve irregular', + 'weather_light-rain' => 'Chuva leve', + 'weather_moderate-rain-at-times' => 'Chuva moderada ocasional', + 'weather_moderate-rain' => 'Chuva moderada', + 'weather_patchy-rain-possible' => 'Possibilidade de chuva', + 'weather_heavy-rain-at-times' => 'Chuva forte ocasional', + 'weather_heavy-rain' => 'Chuva forte', + 'weather_light-freezing-rain' => 'Frio com chuva leve', + 'weather_moderate-or-heavy-freezing-rain' => 'Frio com chuva moderada ou pesada', + 'weather_light-sleet' => 'Granizo fraco', + 'weather_moderate-or-heavy-rain-shower' => 'Chuvisco moderado ou pesado', + 'weather_light-rain-shower' => 'Chuva leve', + 'weather_torrential-rain-shower' => 'Chuva torrencial', + 'weather_rain' => 'Chuva', + 'weather_snow' => 'Neve', + 'weather_blowing-snow' => 'Nevando', + 'weather_patchy-light-snow' => 'Patchy light snow', + 'weather_light-snow' => 'Neve fraca', + 'weather_patchy-moderate-snow' => 'Patchy moderate snow', + 'weather_moderate-snow' => 'Neve moderada', + 'weather_patchy-heavy-snow' => 'Patchy heavy snow', + 'weather_heavy-snow' => 'Neve intensa', + 'weather_light-snow-showers' => 'Pancadas leves de neve', + 'weather_moderate-or-heavy-snow-showers' => 'Pancadas de neve moderadas ou fortes', + 'weather_patchy-snow-possible' => 'Patchy snow possible', + 'weather_patchy-sleet-possible' => 'Patchy sleet possible', + 'weather_moderate-or-heavy-sleet' => 'Granizo moderado ou forte', + 'weather_light-sleet-showers' => 'Pancadas leves de granizo', + 'weather_moderate-or-heavy-sleet-showers' => 'Pancadas de granizo moderadas ou fortes', + 'weather_sleet' => 'Granizo', + 'weather_wind' => 'Ventania', + 'weather_fog' => 'Neblina', + 'weather_freezing-fog' => 'Nevoeiro gelado', + 'weather_mist' => 'Neblina', + 'weather_blizzard' => 'Nevasca', + 'weather_overcast' => 'Nublado', + 'weather_cloudy' => 'Nublado', + 'weather_partly-cloudy-day' => 'Parcialmente nublado', + 'weather_partly-cloudy-night' => 'Parcialmente nublada', + 'weather_freezing-drizzle' => 'Garoa congelante', + 'weather_heavy-freezing-drizzle' => 'Heavy freezing drizzle', + 'weather_patchy-freezing-drizzle-possible' => 'Patchy freezing drizzle possible', + 'weather_ice-pellets' => 'Granizo', + 'weather_light-showers-of-ice-pellets' => 'Chuva leve de granizo', + 'weather_moderate-or-heavy-showers-of-ice-pellets' => 'Chuva moderada ou pesada de granizo', + 'weather_thundery-outbreaks-possible' => 'Possibilidade de tempestade', + 'weather_patchy-light-rain-with-thunder' => 'Chuva leve e irregular com trovoadas', + 'weather_moderate-or-heavy-rain-with-thunder' => 'Chuva moderada ou forte com trovoadas', + 'weather_patchy-light-snow-with-thunder' => 'Patchy light snow with thunder', + 'weather_moderate-or-heavy-snow-with-thunder' => 'Neve moderada ou forte com trovoadas', + 'weather_current_temperature_celsius' => ':temperature °C', + 'weather_current_temperature_fahrenheit' => ':temperature °F', + 'weather_current_title' => 'Tempo atual', + + // dav + 'dav_contacts' => 'Contatos', + 'dav_contacts_description' => 'Contatos de :name', + 'dav_birthdays' => 'Aniversários', + 'dav_birthdays_description' => 'Aniversários dos contatos :name\'s', + 'dav_tasks' => 'Tarefas', + 'dav_tasks_description' => 'tarefas de :name', + + // contact list + 'contact_list_avatar' => 'Avatar', + 'contact_list_name' => 'Contato', + 'contact_list_description' => 'Descrição', + +]; diff --git a/resources/lang/pt-BR/auth.php b/resources/lang/pt-BR/auth.php new file mode 100644 index 0000000..d3f09ee --- /dev/null +++ b/resources/lang/pt-BR/auth.php @@ -0,0 +1,89 @@ + 'Credenciais informadas não correspondem com nossos registros.', + 'throttle' => 'Você realizou muitas tentativas de login. Por favor, tente novamente em :seconds segundos.', + 'not_authorized' => 'Você não está autorizado a executar esta ação', + 'signup_disabled' => 'Cadastro de novas contas desativado no momento', + 'signup_error' => 'Um erro ocorreu ao tentar registrar o usuário', + 'back_homepage' => 'Voltar à página inicial', + 'mfa_auth_otp' => 'Autenticar com dois fatores', + 'mfa_auth_webauthn' => 'Autenticar com uma chave de segurança (WebAuthn)', + '2fa_title' => 'Autenticação de dois fatores', + '2fa_wrong_validation' => 'Falha na autenticação de dois fatores.', + '2fa_one_time_password' => 'Código de autenticação de dois fatores', + '2fa_recuperation_code' => 'Digite um código de recuperação de dois fatores', + '2fa_one_time_or_recuperation' => 'Digite um código de autenticação de dois fatores ou um código de recuperação', + '2fa_otp_help' => 'Abra seu aplicativo para autenticação de dois fatores e copie o código', + + 'login_to_account' => 'Entre na sua conta', + 'login_with_recovery' => 'Entrar com um código de recuperação', + 'login_again' => 'Por favor, entre novamente na sua conta', + 'email' => 'E-mail', + 'password' => 'Senha', + 'recovery' => 'Código de recuperação', + 'login' => 'Entrar', + 'button_remember' => 'Permanecer logado', + 'password_forget' => 'Esqueceu sua senha?', + 'password_reset' => 'Redefinir senha', + 'use_recovery' => 'Ou você pode usar um código de recuperação', + 'signup_no_account' => 'Não tem uma conta?', + 'signup' => 'Cadastre-se', + 'create_account' => 'Cadastre-se para criar a primeira conta', + 'change_language_title' => 'Mudar idioma:', + 'change_language' => 'Mudar idioma para :lang', + + 'password_reset_title' => 'Redefinir senha', + 'password_reset_email' => 'Endereço de e-mail', + 'password_reset_send_link' => 'Enviar e-mail para redefinição de senha', + 'password_reset_password' => 'Senha', + 'password_reset_password_confirm' => 'Confirmar senha', + 'password_reset_action' => 'Redefinir senha', + 'password_reset_email_content' => 'Clique aqui para redefinir sua senha:', + + 'register_title_welcome' => 'Bem-vindo à sua instância Monica recém instalada', + 'register_create_account' => 'Você precisa criar uma conta para usar o Monica', + 'register_title_create' => 'Crie sua conta Monica', + 'register_login' => 'Entre se você já tiver uma conta.', + 'register_email' => 'Insira um endereço de e-mail válido', + 'register_email_example' => 'joao@gmail.com', + 'register_firstname' => 'Nome', + 'register_firstname_example' => 'ex. João', + 'register_lastname' => 'Sobrenome', + 'register_lastname_example' => 'ex. Silva', + 'register_password' => 'Senha', + 'register_password_example' => 'Digite uma senha segura', + 'register_password_confirmation' => 'Confirmação de senha', + 'register_action' => 'Cadastrar', + 'register_policy' => 'Registrar-se significa que você leu e concordou com nossas Políticas de Privacidade e Termos de uso.', + 'register_invitation_email' => 'Por questões de segurança, favor indicar o email da pessoa que te convidou para fazer parte desta conta. Esta informação é fornecida no email do convite.', + + 'confirmation_title' => 'Verifique seu endereço de email', + 'confirmation_fresh' => 'Um novo link de verificação foi enviado para o seu endereço de email.', + 'confirmation_check' => 'Antes de prosseguir, verifique seu email para um link de verificação.', + 'confirmation_request_another' => 'Se você não recebeu o email clique aqui para solicitar outro.', + + 'confirmation_again' => 'Se você desejar alterar seu endereço de email, você pode clicar aqui.', + 'email_change_current_email' => 'Endereço de e-mail atual:', + 'email_change_title' => 'Alterar o seu endereço de e-mail', + 'email_change_new' => 'Novo endereço de e-mail', + 'email_changed' => 'Seu endereço de e-mail foi alterado. Verifique sua caixa de entrada para validá-lo.', +]; diff --git a/resources/lang/pt-BR/changelog.php b/resources/lang/pt-BR/changelog.php new file mode 100644 index 0000000..34f1a61 --- /dev/null +++ b/resources/lang/pt-BR/changelog.php @@ -0,0 +1,12 @@ + 'Atualizações de produtos', + 'note' => 'Observação: infelizmente, esta página só está disponível em inglês.', +]; diff --git a/resources/lang/pt-BR/dashboard.php b/resources/lang/pt-BR/dashboard.php new file mode 100644 index 0000000..050ff92 --- /dev/null +++ b/resources/lang/pt-BR/dashboard.php @@ -0,0 +1,42 @@ + 'Seja bem-vindo à sua conta!', + 'dashboard_blank_description' => 'Monica é o lugar para organizar todas as suas interações com pessoas importantes para você.', + 'dashboard_blank_cta' => 'Adicione seu primeiro contato', + 'dashboard_blank_illustration' => 'Ilustração por Freepik', + + 'notes_title' => 'Você ainda não tem nenhuma nota favorita.', + + 'tab_recent_calls' => 'Chamadas recentes', + 'tab_favorite_notes' => 'Notas favoritas', + 'tab_calls_blank' => 'Você ainda não registrou uma chamada.', + 'tab_debts' => 'Dívidas', + 'tab_debts_blank' => 'Você ainda não registrou nenhuma dívida.', + 'tab_tasks' => 'Tarefas', + 'tab_tasks_blank' => 'Você ainda não tem nenhuma tarefa.', + + 'tasks_add_task_placeholder' => 'De que se trata esta tarefa?', + 'tasks_tab_your_contacts' => 'Tarefas relacionadas aos seus contatos', + 'tasks_tab_your_tasks' => 'Suas tarefas', + 'tasks_add_note' => 'Pressione Enter para adicionar a tarefa.', + 'task_add_cta' => 'Adicionar tarefa', + + 'debts_you_owe' => 'Você deve', + + 'statistics_contacts' => 'Contatos', + 'statistics_activities' => 'Atividades', + 'statistics_gifts' => 'Presentes', + + 'reminders_next_months' => 'Eventos nos próximos 3 meses', + 'reminders_none' => 'Nenhum lembrete para este mês.', + + 'product_changes' => 'Atualizações de produtos', + 'product_view_details' => 'Ver detalhes', +]; diff --git a/resources/lang/pt-BR/format.php b/resources/lang/pt-BR/format.php new file mode 100644 index 0000000..8e60df5 --- /dev/null +++ b/resources/lang/pt-BR/format.php @@ -0,0 +1,36 @@ + 'd M Y H:i', + 'short_date_year' => 'd M Y', + 'short_date' => 'd M', + 'short_month' => 'M', + 'short_month_year' => 'M Y', + 'short_day' => 'D', + 'full_date_year' => 'd M Y', + 'full_month' => 'F', + 'full_month_year' => 'F Y', + 'full_hour' => 'H:i', + + 'short_text' => '{text}…', +]; diff --git a/resources/lang/pt-BR/journal.php b/resources/lang/pt-BR/journal.php new file mode 100644 index 0000000..8838af3 --- /dev/null +++ b/resources/lang/pt-BR/journal.php @@ -0,0 +1,38 @@ + 'Como foi o seu dia? Você pode avaliá-lo uma vez por dia.', + 'journal_come_back' => 'Obrigado. Volte amanhã para avaliar o seu dia novamente.', + 'journal_description' => 'Observação: o diário lista ambos os registros manuais e automáticos, como Atividades feitas com seus contatos. Embora você possa excluir lançamentos do diário manualmente, você terá que excluir a atividade diretamente na página de contato.', + 'journal_add' => 'Adicionar uma entrada no diário', + 'journal_edit' => 'Editar uma entrada no diário', + 'journal_empty' => 'Diário vazio', + 'journal_created_at' => 'Criado em {date}', + 'journal_created_automatically' => 'Criar automaticamente', + 'journal_entry_type_journal' => 'Entrada do diário', + 'journal_entry_type_activity' => 'Atividade', + 'journal_entry_rate' => 'Você avaliou seu dia.', + 'journal_add_comment' => 'Deseja adicionar um comentário (opcional)?', + 'journal_show_comment' => 'Mostrar comentário', + 'entry_delete_success' => 'A informação do diário foi excluída com sucesso.', + 'journal_add_title' => 'Título (opcional)', + 'journal_add_date' => 'Encontro', + 'journal_add_post' => 'Texto', + 'journal_add_cta' => 'Salvar', + 'journal_blank_cta' => 'Adicione sua primeira informação no diário', + 'journal_blank_description' => 'O diário permite que você escreva eventos que aconteceram com você para que lembre-se deles.', + 'delete_confirmation' => 'Tem certeza que deseja excluir este registro do diário?', + 'apply_filter' => 'Apply filter', + 'start_date' => 'Start Date', + 'end_date' => 'End Date', + 'per_page' => 'Per Page', + 'sort_order' => 'Sort By Created At', + 'ascending' => 'Ascending', + 'descending' => 'Descending', +]; diff --git a/resources/lang/pt-BR/logs.php b/resources/lang/pt-BR/logs.php new file mode 100644 index 0000000..f66cdb8 --- /dev/null +++ b/resources/lang/pt-BR/logs.php @@ -0,0 +1,29 @@ + 'Criou o contato.', + 'settings_log_contact_created_with_name' => 'Adicionado :name como um contato.', + + // contat description update + 'contact_log_contact_description_updated' => 'Descrição atualizada.', + 'settings_log_contact_description_updated_with_name' => 'Atualizada a descrição :name.', + + // contact description clear + 'contact_log_contact_description_cleared' => 'Descrição foi limpa.', + 'settings_log_contact_description_cleared_with_name' => 'Descrição limpa de :name.', + + // contact work information update + 'contact_log_contact_work_updated' => 'Informações de trabalho atualizadas.', + 'settings_log_contact_work_updated_with_name' => 'Informação de trabalho atualizada de :name.', + + // company created + 'settings_log_company_created' => 'Criou uma empresa chamada :name.', +]; diff --git a/resources/lang/pt-BR/mail.php b/resources/lang/pt-BR/mail.php new file mode 100644 index 0000000..a3b6f54 --- /dev/null +++ b/resources/lang/pt-BR/mail.php @@ -0,0 +1,53 @@ + 'Lembrete para :contact', + 'greetings' => 'Olá :username', + 'want_reminded_of' => 'Você queria ser lembrado de :reason', + 'for' => 'Para: :name', + 'comment' => 'Comentário: :comment', + 'footer_contact_info' => 'Adicionar, visualizar, completar e alterar informações sobre este contato:', + 'footer_contact_info2' => 'Ver o perfil de :name', + 'footer_contact_info2_link' => 'Ver perfil do :name: :url', + + 'notification_subject_line' => 'Você tem um evento futuro', + 'notification_description' => 'Em :count dias (no dia :date), o seguinte evento irá acontecer:', + + 'stay_in_touch_subject_line' => 'Mantenha contato com :name', + 'stay_in_touch_subject_description' => 'Você pediu que fosse lembrado para ficar em contato com :name a cada :frequency. Você pediu que fosse lembrado de manter contato com :name a cada :frequency.', + + 'notifications_whoops' => 'Ops!', + 'notifications_hello' => 'Olá!', + 'notifications_regards' => 'Cordialmente', + 'notifications_footer' => 'Se você estiver tendo problemas para clicar no botão ":actionText", copie e cole o URL abaixo em seu navegador da Web: [:actionURL](:actionURL)', + 'notifications_rights' => 'Todos os direitos reservados', + + 'confirmation_email_title' => 'Monica – Verificação de e-mail', + 'confirmation_email_intro'=> 'Para validar seu e-mail clique no botão abaixo', + 'confirmation_email_button' => 'Verificar endereço de e-mail', + 'confirmation_email_bottom' => 'Se você não criou uma conta, nenhuma ação adicional é necessária.', + + 'password_reset_title' => 'Monica - Notificação de redefinição de senha', + 'password_reset_intro' => 'Você está recebendo este e-mail porque recebemos uma solicitação de redefinição de senha para sua conta.', + 'password_reset_button' => 'Redefinir a senha', + 'password_reset_expiration' => 'Este link de redefinição de senha expirará em :count minutos.', + 'password_reset_bottom' => 'Se você não solicitou uma redefinição de senha, nenhuma ação adicional é necessária.', + + 'invitation_title' => 'Monica – Você foi convidado por :name', + 'invitation_intro' => 'Você foi convidado por :name (:email) para usar a Monica, uma ótima ferramenta Gerenciamento de Relacionamento Pessoal.', + 'invitation_link' => 'Para aceitar o convite, clique no link abaixo:', + 'invitation_button' => 'Aceitar convite', + 'invitation_expiration' => 'Este link irá expirar em :count dias.', + + 'export_title' => 'Sua exportação está pronta', + 'export_description' => 'Você solicitou uma exportação de dados em :date. Agora está pronto para baixar.', + 'export_download' => 'Download export', + +]; diff --git a/resources/lang/pt-BR/pagination.php b/resources/lang/pt-BR/pagination.php new file mode 100644 index 0000000..27031f5 --- /dev/null +++ b/resources/lang/pt-BR/pagination.php @@ -0,0 +1,25 @@ + '❮ Anterior', + 'next' => 'Próxima ❯', + +]; diff --git a/resources/lang/pt-BR/passwords.php b/resources/lang/pt-BR/passwords.php new file mode 100644 index 0000000..d414162 --- /dev/null +++ b/resources/lang/pt-BR/passwords.php @@ -0,0 +1,30 @@ + 'Sua senha foi redefinida!', + 'sent' => 'Enviamos um link para redefinir a sua senha por e-mail.', + 'token' => 'Esse código de redefinição de senha é inválido.', + 'user' => 'Não conseguimos encontrar nenhum usuário com o endereço de e-mail informado.', + 'changed' => 'Senha alterada com sucesso.', + 'invalid' => 'A senha que introduziu não está correta.', + 'throttled' => 'Por favor espere antes de tentar novamente.', + +]; diff --git a/resources/lang/pt-BR/people.php b/resources/lang/pt-BR/people.php new file mode 100644 index 0000000..19b87b8 --- /dev/null +++ b/resources/lang/pt-BR/people.php @@ -0,0 +1,539 @@ + 'Contato não encontrado', + 'people_list_number_kids' => ':count filho|:count filhos', + 'people_list_last_updated' => 'Últimas consultas:', + 'people_list_number_reminders' => ':count lembrete|:count lembretes', + 'people_list_blank_title' => 'Você ainda não adicionou ninguém', + 'people_list_blank_cta' => 'Adicionar contato', + 'people_list_sort' => 'Ordenar', + 'people_list_stats' => ':count contato|:count contatos', + 'people_list_firstnameAZ' => 'Ordenar por nome A → Z', + 'people_list_firstnameZA' => 'Ordenar por nome Z → A', + 'people_list_lastnameAZ' => 'Ordenar por sobrenome A → Z', + 'people_list_lastnameZA' => 'Ordenar por sobrenome Z → A', + 'people_list_lastactivitydateNewtoOld' => 'Ordenar pela última data de atividade, os mais recentes primeiro', + 'people_list_lastactivitydateOldtoNew' => 'Ordenar pela última data de atividade, os mais antigos primeiro', + 'people_list_filter_tag' => 'Exibindo todos os contatos etiquetados com', + 'people_list_clear_filter' => 'Limpar filtro', + 'people_list_contacts_per_tags' => ':count contatos|:count contatos', + 'people_list_show_dead' => 'Mostrar pessoas falecidas (:count)', + 'people_list_hide_dead' => 'Ocultar pessoas falecidas (:count)', + 'people_search' => 'Pesquisar seus contatos…', + 'people_search_no_results' => 'Nenhum resultado encontrado', + 'people_search_next' => 'Próximo', + 'people_search_prev' => 'Anterior', + 'people_search_rows_per_page' => 'Linhas por página', + 'people_search_of' => 'de', + 'people_search_page' => 'Página', + 'people_search_all' => 'Todos', + 'people_add_new' => 'Adicionar nova pessoa', + 'people_list_account_usage' => 'Uso da sua conta: :current/:limit contatos', + 'people_list_account_upgrade_title' => 'Assine para ter acesso a todos os recursos.', + 'people_list_account_upgrade_cta' => 'Assinar agora', + 'people_list_untagged' => 'Visualizar contatos sem etiqueta', + 'people_list_filter_untag' => 'Exibindo todos os contatos não etiquetados', + 'archived_contact_readonly' => 'Contatos arquivados não podem ser editados, desarquive-o primeiro.', + + // people add + 'people_add_title' => 'Adicionar novo contato', + 'people_add_missing' => 'Nenhuma pessoa encontrada – adicione uma nova agora', + 'people_add_firstname' => 'Nome', + 'people_add_middlename' => 'Nome do meio (opcional)', + 'people_add_lastname' => 'Último nome (opcional)', + 'people_add_email' => 'E-mail (opcional)', + 'people_add_nickname' => 'Apelido (opcional)', + 'people_add_cta' => 'Adicionar', + 'people_save_and_add_another_cta' => 'Enviar e adicionar outra pessoa', + 'people_add_success' => ':nome foi criado com sucesso', + 'people_add_gender' => 'Gênero', + 'people_delete_success' => 'O contato foi excluído', + 'people_delete_message' => 'Excluir contato', + 'people_delete_confirmation' => 'Você tem certeza que quer remover o contato de :name? A remoção é imediata e permanente.', + 'people_add_birthday_reminder' => 'Deseje feliz aniversário para :name', + 'people_add_birthday_reminder_deceased' => 'Nessa data, :name, teria comemorado seu aniversário', + 'people_add_import' => 'Você quer importar seus contatos?', + 'people_edit_email_error' => 'Já existe um contato em sua conta com esse e-mail. Por favor, escolha outro e-mail.', + 'people_export' => 'Exportar como vCard', + 'people_add_reminder_for_birthday' => 'Criar um lembrete de aniversário anual', + + // show + 'section_contact_information' => 'Informações de contato', + 'section_personal_activities' => 'Atividades', + 'section_personal_reminders' => 'Lembretes', + 'section_personal_tasks' => 'Tarefas', + 'section_personal_gifts' => 'Presentes', + 'section_personal_notes' => 'Notas', + + // archived contacts + 'list_link_to_active_contacts' => 'Você está visualizando os contatos arquivados. Em vez disso, veja a lista de contatos ativos.', + 'list_link_to_archived_contacts' => 'Lista de contatos arquivados', + + // Header + 'me' => 'Este é você', + 'edit_contact_information' => 'Editar informações de contato', + 'contact_archive' => 'Arquivar contato', + 'contact_unarchive' => 'Desarquivar contato', + 'contact_archive_help' => 'Os contatos arquivados não são mostrados na lista de contatos, mas ainda aparecem nos resultados de busca.', + 'call_button' => 'Registrar ligação', + 'set_favorite' => 'Contatos favoritos são colocados no topo da lista de contatos', + + // Stay in touch + 'stay_in_touch' => 'Manter contato', + 'stay_in_touch_frequency' => 'Manter contato todos os dias|Manter contato a cada {count} dias', + 'stay_in_touch_next_date' => 'Próximo vencimento: {date}', + 'stay_in_touch_invalid' => 'A frequência deve ser um número maior que 0.', + 'stay_in_touch_premium' => 'Você precisa de uma assinatura ativa para utilizar esse recurso', + 'stay_in_touch_modal_title' => 'Manter contato', + 'stay_in_touch_modal_desc' => 'Podemos lembrar você por e-mail para manter contato com {firstname} em um determinado intervalo.', + 'stay_in_touch_modal_label' => 'Envie-me um email a cada…{count} dia|Envie-me um email a cada… {count} dias', + + // Calls + 'modal_call_title' => 'Registrar ligação', + 'modal_call_comment' => 'Sobre o que falaram? (opcional)', + 'modal_call_exact_date' => 'O telefonema aconteceu em', + 'modal_call_who_called' => 'Quem ligou?', + 'modal_call_emotion' => 'Quer registrar como se sentiu durante esta chamada? (opcional)', + 'calls_add_success' => 'A chamada telefônica foi salva.', + 'call_delete_confirmation' => 'Tem certeza que deseja excluir esta ligação?', + 'call_delete_success' => 'A chamada telefônica foi excluída com sucesso', + 'call_title' => 'Ligações', + 'call_empty_comment' => 'Sem detalhes', + 'call_blank_title' => 'Registre todas as ligações realizadas com {name}', + 'call_blank_desc' => 'Você ligou para {name}', + 'call_you_called' => 'Você ligou', + 'call_he_called' => '{name} ligou', + 'call_emotions' => 'Emoções:', + + // Conversation + 'conversation_blank' => 'Grave conversas que você tem com :name em mídias sociais, SMS…', + 'conversation_delete_link' => 'Excluir conversa', + 'conversation_edit_title' => 'Editar conversa', + 'conversation_edit_delete' => 'Quer mesmo excluir esta conversa? Você não poderá voltar atrás.', + 'conversation_add_success' => 'Conversa registrada com sucesso!', + 'conversation_edit_success' => 'Conversa atualizada com sucesso!', + 'conversation_delete_success' => 'Conversa excluída com sucesso!', + 'conversation_add_title' => 'Registre uma nova conversa', + 'conversation_add_when' => 'Quando aconteceu essa conversa?', + 'conversation_add_who_wrote' => 'Quem enviou esta mensagem?', + 'conversation_add_how' => 'Como vocês se comunicaram?', + 'conversation_add_you' => 'Você', + 'conversation_add_content' => 'Escreva o que foi dito', + 'conversation_add_what_was_said' => 'O que você disse?', + 'conversation_add_another' => 'Adicionar outra mensagem', + 'conversation_add_error' => 'Você precisa adicionar pelo menos uma mensagem.', + 'conversation_list_table_messages' => 'Mensagens', + 'conversation_list_table_content' => 'Conteúdo parcial (última mensagem)', + 'conversation_list_title' => 'Conversas', + 'conversation_list_cta' => 'Registrar conversa', + + // age - birthday + 'birthdate_not_set' => 'O aniversário não foi definido', + 'age_approximate_in_years' => 'cerca de :age anos', + 'age_exact_in_years' => ':age anos', + 'age_exact_birthdate' => 'nasceu em :date', + + // Last called + 'last_called' => 'Última ligação: :date', + 'last_talked_to' => 'Última ligação: {date}', + 'last_called_empty' => 'Última ligação: desconhecido', + 'last_activity_date' => 'Última atividade juntos: :date', + 'last_activity_date_empty' => 'Última atividade juntos: desconhecido', + + // additional information + 'information_edit_success' => 'O perfil foi atualizado com sucesso', + 'information_edit_title' => 'Editar informações pessoais de :name', + 'information_edit_max_size' => 'Máx :size Kb.', + 'information_edit_max_size2' => 'Máx {size} Kb.', + 'information_edit_firstname' => 'Nome', + 'information_edit_lastname' => 'Último nome (opcional)', + 'information_edit_description' => 'Descrição (opcional)', + 'information_edit_description_help' => 'Usado na lista de contatos para adicionar algum contexto, se necessário.', + 'information_edit_unknown' => 'Não sei a idade desta pessoa', + 'information_edit_probably' => 'Esta pessoa é provavelmente…', + 'information_edit_not_year' => 'Eu conheço o dia e o mês de aniversário desta pessoa, mas não o ano…', + 'information_edit_exact' => 'Eu sei exatamente a data de aniversário dessa pessoa…', + 'information_edit_birthdate_label' => 'Aniversário', + 'information_no_work_defined' => 'Sem informação profissional', + 'information_work_at' => 'na :company', + 'work_add_cta' => 'Atualizar informação profissional', + 'work_edit_success' => 'Informações de trabalho atualizadas', + 'work_edit_title' => 'Atualizar trabalho de :name', + 'work_edit_job' => 'Função (Opcional)', + 'work_edit_company' => 'Empresa (Opcional)', + 'work_information' => 'Informação de trabalho', + + // food preferences + 'food_preferences_add_success' => 'Preferências alimentares salvas com sucesso', + 'food_preferences_edit_description' => 'Talvez :firstname ou alguém na família :family tenha algum tipo de alergia ou não goste de algo específico. Coloque tudo aqui para que possa lembrar na próxima vez que os convidar para jantar', + 'food_preferences_edit_description_no_last_name' => 'Talvez :firstname tenha algum tipo de alergia ou não goste de algo específico. Coloque tudo aqui para que possa lembrar na próxima vez que estiverem juntos', + 'food_preferences_edit_title' => 'Registre suas preferências alimentares', + 'food_preferences_edit_cta' => 'Salvar preferências', + 'food_preferences_title' => 'Preferências alimentares', + 'food_preferences_cta' => 'Adicionar preferências alimentares', + + // reminders + 'reminders_blank_title' => 'Há alguma coisa que você gostaria de lembrar sobre :name?', + 'reminders_blank_add_activity' => 'Adicionar lembrete', + 'reminders_add_title' => 'O que você gostaria de ser lembrado sobre :name?', + 'reminders_add_description' => 'Por favor, lembre-me de…', + 'reminders_add_next_time' => 'Quando você gostaria de ser lembrado sobre isso?', + 'reminders_add_once' => 'Lembre-me apenas uma vez', + 'reminders_add_recurrent' => 'Lembre-me a cada', + 'reminders_add_starting_from' => 'começando pela data selecionada acima', + 'reminders_add_cta' => 'Adicionar lembrete', + 'reminders_edit_update_cta' => 'Atualizar lembrete', + 'reminders_add_error_custom_text' => 'Você precisa indicar um texto para este lembrete', + 'reminders_create_success' => 'O lembrete foi adicionado com sucesso', + 'reminders_delete_success' => 'O lembrete foi excluído com sucesso', + 'reminders_update_success' => 'O lembrete foi atualizado com sucesso', + 'reminders_add_optional_comment' => 'Comentário opcional', + + 'reminder_frequency_day' => 'todos os dias|a cada :number dias', + 'reminder_frequency_week' => 'toda semana|a cada :number semanas', + 'reminder_frequency_month' => 'todo mês|a cada :number meses', + 'reminder_frequency_year' => 'todo ano|a cada :number ano(s)', + 'reminder_frequency_one_time' => 'em :date', + 'reminders_delete_confirmation' => 'Você quer mesmo excluir este lembrete?', + 'reminders_delete_cta' => 'Excluir', + 'reminders_next_expected_date' => 'em', + 'reminders_cta' => 'Adicionar lembrete', + 'reminders_description' => 'Enviaremos um e-mail para cada um dos lembretes abaixo. Os lembretes serão enviados na parte da manhãs do dia do evento. Lembretes adicionados automaticamente para aniversários não podem ser excluídos. Se você quiser alterar essas datas, edite a data de aniversário dos contatos.', + 'reminders_one_time' => 'Uma vez', + 'reminders_type_week' => 'semana', + 'reminders_type_month' => 'mês', + 'reminders_type_year' => 'ano', + 'reminders_birthday' => 'Aniversário de :name', + 'reminders_free_plan_warning' => 'Você está utilizando o Plano Gratuito. E-mails não são enviados neste plano. Por favor, assine para receber seus lembretes por e-mail.', + + // relationships + 'relationship_form_add' => 'Adicionar novo relacionamento', + 'relationship_form_edit' => 'Editar relacionamento', + 'relationship_form_is_with' => 'Esta pessoa é...', + 'relationship_form_is_with_name' => ':name é…', + 'relationship_form_add_choice' => 'Com quem é esse relacionamento?', + 'relationship_form_create_contact' => 'Adicionar nova pessoa', + 'relationship_form_associate_contact' => 'Um contato existente', + 'relationship_form_associate_dropdown' => 'Pesquise e selecione um contato existente no menu abaixo', + 'relationship_form_associate_dropdown_placeholder' => 'Pesquise e selecione um contato existente', + 'relationship_form_also_create_contact' => 'Criar um perfil de contato para esta pessoa.', + 'relationship_form_add_description' => 'Isto permitirá que você gerencie esta pessoa como qualquer outro contato.', + 'relationship_form_add_no_existing_contact' => 'Você não tem nenhum contato que possa ser relacionado a :name no momento.', + 'relationship_delete_confirmation' => 'Quer mesmo excluir este relacionamento? Você não pode voltar atrás.', + 'relationship_unlink_confirmation' => 'Quer mesmo excluir este relacionamento? Esta pessoa não será excluída, somente o relacionamento entre as duas.', + 'relationship_form_add_success' => 'O relacionamento foi estabelecido com sucesso.', + 'relationship_form_deletion_success' => 'O relacionamento foi excluído.', + + // tasks + 'tasks_title' => 'Tarefas', + 'tasks_blank_title' => 'Você ainda não tem nenhuma tarefa.', + 'tasks_form_title' => 'Título', + 'tasks_form_description' => 'Descrição (Opcional)', + 'tasks_add_task' => 'Adicionar tarefa', + 'tasks_delete_success' => 'A tarefa foi excluída com sucesso', + 'tasks_complete_success' => 'O status da tarefa foi alterado com sucesso', + + // activities + 'activity_title' => 'Atividades', + 'activity_type_category_simple_activities' => 'Atividades simples', + 'activity_type_category_sport' => 'Esporte', + 'activity_type_category_food' => 'Comida', + 'activity_type_category_cultural_activities' => 'Atividades culturais', + 'activity_type_just_hung_out' => 'acabou de desligar', + 'activity_type_watched_movie_at_home' => 'assistimos um filme em casa', + 'activity_type_talked_at_home' => 'só conversamos em casa', + 'activity_type_did_sport_activities_together' => 'praticamos um esporte juntos', + 'activity_type_ate_at_his_place' => 'como na casa deles', + 'activity_type_went_bar' => 'fui para um bar', + 'activity_type_ate_at_home' => 'comi em casa', + 'activity_type_picnicked' => 'fiz um piquenique', + 'activity_type_ate_restaurant' => 'comi em um restaurante', + 'activity_type_went_theater' => 'fui ao teatro', + 'activity_type_went_concert' => 'fui a um concerto', + 'activity_type_went_play' => 'fui a uma peça', + 'activity_type_went_museum' => 'fui ao museu', + 'activities_add_activity' => 'Acrescentar atividade', + 'activities_add_more_details' => 'Acrescentar mais detalhes', + 'activities_add_emotions' => 'Acrescentar emoções', + 'activities_add_category' => 'Indicar uma categoria', + 'activities_add_participants_cta' => 'Adicionar participantes', + 'activities_item_information' => ':Activity. Aconteceu em :date', + 'activities_add_title' => 'O que você fez com {name}?', + 'activities_summary' => 'Descreva o que fez', + 'activities_add_pick_activity' => 'Você gostaria de categorizar esta atividade? Não é obrigatório, mas fornecerá estatísticas posteriormente (opcional)', + 'activities_add_date_occured' => 'A atividade aconteceu em…', + 'activities_add_participants' => 'Quem, além de {name}, participou desta atividade? (opcional)', + 'activities_add_emotions_title' => 'Você quer registrar como se sentiu durante esta atividade? (opcional)', + 'activities_blank_title' => 'Mantenha controle do que fez com {name} no passado, e sobre o que falaram', + 'activities_blank_add_activity' => 'Acrescentar uma atividade', + 'activities_add_success' => 'A atividade foi adicionada com sucesso', + 'activities_add_error' => 'Erro ao adicionar a atividade', + 'activities_update_success' => 'A atividade foi atualizada com sucesso', + 'activities_delete_success' => 'A atividade foi deletada com sucesso', + 'activities_who_was_involved' => 'Quem estava envolvido?', + 'activities_activity' => 'Categoria da Atividade', + 'activities_view_activities_report' => 'Ver relatório de atividades', + 'activities_profile_title' => 'Relatório de atividades entre :nome e você', + 'activities_profile_subtitle' => 'Você registrou :total_activities atividade com :name no total e :activities_last_twelve_months nos últimos 12 meses até o momento.|Você registrou :total_activities atividades com :name no total e :activities_last_twelve_months nos últimos 12 meses até agora.', + 'activities_profile_year_summary_activity_types' => 'Aqui está um detalhamento do tipo de atividades que vocês fizeram juntos no :ano', + 'activities_profile_year_summary' => 'Aqui está o que vocês dois fizeram em :year', + 'activities_profile_number_occurences' => ':value atividade|:value atividades', + 'activities_list_participants' => 'Participantes ({total}):', + 'activities_list_emotions' => 'Emoções sentidas:', + 'activities_list_date' => 'Aconteceu em', + 'activities_list_category' => 'Categoria:', + + // notes + 'notes_create_success' => 'A nota foi criada com sucesso', + 'notes_update_success' => 'A nota foi salva com sucesso', + 'notes_delete_success' => 'A nota foi excluída com sucesso', + 'notes_add_cta' => 'Adicionar nota', + 'notes_favorite' => 'Adicionar/remover dos favoritos', + 'notes_delete_title' => 'Deletar nota', + 'notes_delete_confirmation' => 'Você tem certeza de que deseja excluir esta nota? Você não poderá voltar atrás', + + // gifts + 'gifts_title' => 'Presentes', + 'gifts_add_success' => 'O presente foi adicionado com sucesso', + 'gifts_delete_success' => 'O presente foi excluído com sucesso', + 'gifts_delete_confirmation' => 'Tem certeza de que deseja deletar este presente?', + 'gifts_add_gift' => 'Adicionar um presente', + 'gifts_link' => 'Link', + 'gifts_for' => 'Para: {name}', + 'gifts_delete_cta' => 'Excluir', + 'gifts_add_title' => 'Gerenciar presentes para :name', + 'gifts_add_gift_idea' => 'Ideia de presente', + 'gifts_add_gift_already_offered' => 'Presente dado', + 'gifts_add_gift_received' => 'Presente recebido', + 'gifts_add_gift_title' => 'Que presente é esse?', + 'gifts_add_gift_name' => 'Nome do presente', + 'gifts_add_link' => 'Link para a página da web (opcional)', + 'gifts_add_value' => 'Valor (opcional)', + 'gifts_add_comment' => 'Comentário (opcional)', + 'gifts_add_recipient' => 'Destinatário (opcional)', + 'gifts_add_recipient_field' => 'Destinatário', + 'gifts_add_photo' => 'Foto (opcional)', + 'gifts_add_photo_title' => 'Adicione uma foto para este presente', + 'gifts_add_someone' => 'Este presente é para alguém da família de {name} ', + 'gifts_delete_title' => 'Excluir um presente', + 'gifts_ideas' => 'Ideias de presente', + 'gifts_offered' => 'Presentes dados', + 'gifts_offered_as_an_idea' => 'Marcar como uma ideia', + 'gifts_received' => 'Presentes recebidos', + 'gifts_view_comment' => 'Ver comentário', + 'gifts_mark_offered' => 'Marcar como entregue', + 'gifts_update_success' => 'O presente foi atualizado com sucesso', + 'gifts_add_date' => 'Data (opcional)', + + // debts + 'debt_delete_confirmation' => 'Tem certeza de que deseja deletar esta dívida?', + 'debt_delete_success' => 'A dívida foi excluída com sucesso', + 'debt_add_success' => 'A dívida foi acrescentada com sucesso', + 'debt_title' => 'Dívidas', + 'debt_add_cta' => 'Adicionar dívida', + 'debt_you_owe' => 'Você deve :amount', + 'debt_they_owe' => ':name lhe deve :amount', + 'debt_add_title' => 'Gerenciamento de dívidas', + 'debt_add_you_owe' => 'Você deve a :name', + 'debt_add_they_owe' => ':name deve a você', + 'debt_add_amount' => 'a soma de', + 'debt_add_reason' => 'pela seguinte razão (opcional)', + 'debt_add_add_cta' => 'Adicionar dívida', + 'debt_edit_update_cta' => 'Atualizar débito', + 'debt_edit_success' => 'A dívida foi atualizada com sucesso', + 'debts_blank_title' => 'Gerenciar dívidas que você deve a :name ou :name deve a você', + + // tags + 'tag_edit' => 'Editar etiqueta', + 'tag_add' => 'Adicionar etiquetas', + 'tag_add_search' => 'Adicionar ou pesquisar etiquetas', + 'tag_no_tags' => 'Ainda sem etiquetas', + + // Introductions + 'introductions_sidebar_title' => 'Como você conheceu', + 'introductions_blank_cta' => 'Indique como você conheceu :name', + 'introductions_title_edit' => 'Como você conheceu :name?', + 'introductions_additional_info' => 'Explique como e onde vocês se conheceram', + 'introductions_edit_met_through' => 'Alguém te apresentou para essa pessoa?', + 'introductions_no_met_through' => 'Ninguém', + 'introductions_first_met_date' => 'Data em que vocês se conheceram', + 'introductions_no_first_met_date' => 'Não sei a data em que nos conhecemos', + 'introductions_first_met_date_known' => 'Esta é a data que nos conhecemos', + 'introductions_add_reminder' => 'Adicionar um lembrete para celebrar esse encontro no aniversário do evento ocorrido', + 'introductions_update_success' => 'Você atualizou com sucesso as informações sobre como conheceu essa pessoa', + 'introductions_met_through' => 'Combinar através de :name', + 'introductions_met_date' => 'Encontro em :date', + 'introductions_reminder_title' => 'Aniversário do dia em que nos conhecemos', + + // Deceased + 'deceased_reminder_title' => 'Aniversário da morte de :name', + 'deceased_mark_person_deceased' => 'Marcar como falecido', + 'deceased_know_date' => 'Eu sei a data em que esta pessoa morreu', + 'deceased_add_reminder' => 'Adicionar um lembrete para esta data', + 'deceased_label' => 'Falecido', + 'deceased_date_label' => 'Data do falecimento', + 'deceased_label_with_date' => 'Falecimento em :date', + 'deceased_age' => 'Idade na morte', + + // Contact information + 'contact_info_title' => 'Informações de contato', + 'contact_info_form_content' => 'Conteúdo', + 'contact_info_form_contact_type' => 'Tipo de contato', + 'contact_info_form_personalize' => 'Personalizar', + 'contact_info_address' => 'Vive em', + + // Addresses + 'contact_address_title' => 'Endereços', + 'contact_address_form_name' => 'Etiqueta (opcional)', + 'contact_address_form_street' => 'Nome da rua (opcional)', + 'contact_address_form_city' => 'Cidade (opcional)', + 'contact_address_form_province' => 'Estado (opcional)', + 'contact_address_form_postal_code' => 'CEP (opcional)', + 'contact_address_form_country' => 'País (opcional)', + 'contact_address_form_latitude' => 'Latitude (apenas números) (opcional)', + 'contact_address_form_longitude' => 'Latitude (apenas números) (opcional)', + + // Pets + 'pets_kind' => 'Tipo de animal', + 'pets_name' => 'Nome (opcional)', + 'pets_create_success' => 'Animal adicionado com sucesso', + 'pets_update_success' => 'O animal de estimação foi atualizado com sucesso', + 'pets_delete_success' => 'O animal de estimação foi apagado', + 'pets_title' => 'Animais de Estimação', + 'pets_reptile' => 'Répteis', + 'pets_bird' => 'Pássaro', + 'pets_cat' => 'Gato', + 'pets_dog' => 'Cachorro', + 'pets_fish' => 'Peixe', + 'pets_hamster' => 'Hamster', + 'pets_horse' => 'Cavalo', + 'pets_rabbit' => 'Coelho', + 'pets_rat' => 'Rato', + 'pets_small_animal' => 'Animal pequeno', + 'pets_other' => 'Outros', + + // life events + 'life_event_list_tab_life_events' => 'Eventos da Vida', + 'life_event_list_tab_other' => 'Anotações, lembretes, ...', + 'life_event_list_title' => 'Eventos da Vida', + 'life_event_blank' => 'Registre o que acontece com a vida de {name} para sua referência futura.', + 'life_event_list_cta' => 'Acrescentar evento de vida', + 'life_event_create_category' => 'Todas as categorias', + 'life_event_create_life_event' => 'Adicionar evento de vida', + 'life_event_create_default_title' => 'Título (opcional)', + 'life_event_create_default_story' => 'História (opcional)', + 'life_event_create_date' => 'Não é necessário indicar um mês ou um dia - apenas o ano é obrigatório.', + 'life_event_create_default_description' => 'Adicione informações sobre o que você sabe', + 'life_event_create_add_yearly_reminder' => 'Adicione um lembrete anual para este evento', + 'life_event_create_success' => 'O evento de vida foi acrescentado', + 'life_event_delete_title' => 'Excluir um evento de vida', + 'life_event_delete_description' => 'Tem certeza de que deseja excluir este evento de vida? Não poderá voltar atrás.', + 'life_event_delete_success' => 'O evento de vida foi excluído', + 'life_event_date_it_happened' => 'Data em que aconteceu', + 'life_event_category_work_education' => 'Trabalho e Educação', + 'life_event_category_family_relationships' => 'Família e Relacionamentos', + 'life_event_category_home_living' => 'Casa e Vida', + 'life_event_category_health_wellness' => 'Saúde e Bem-Estar', + 'life_event_category_travel_experiences' => 'Viagens e Experiências', + 'life_event_sentence_new_job' => 'Começou um novo trabalho', + 'life_event_sentence_retirement' => 'Se aposentou', + 'life_event_sentence_new_school' => 'Começou a estudar', + 'life_event_sentence_study_abroad' => 'Estudou no exterior', + 'life_event_sentence_volunteer_work' => 'Começou o voluntariado', + 'life_event_sentence_published_book_or_paper' => 'Publicou um documento', + 'life_event_sentence_military_service' => 'Começou o serviço militar', + 'life_event_sentence_new_relationship' => 'Entrou em um relacionamento', + 'life_event_sentence_engagement' => 'Noivou', + 'life_event_sentence_marriage' => 'Casou-se', + 'life_event_sentence_anniversary' => 'Aniversário', + 'life_event_sentence_expecting_a_baby' => 'Engravidou', + 'life_event_sentence_new_child' => 'Teve um filho', + 'life_event_sentence_new_family_member' => 'Adicionou um membro à família', + 'life_event_sentence_new_pet' => 'Obteve um animal de estimação', + 'life_event_sentence_end_of_relationship' => 'Terminou um relacionamento', + 'life_event_sentence_loss_of_a_loved_one' => 'Perdeu um ente querido', + 'life_event_sentence_moved' => 'Mudou-se', + 'life_event_sentence_bought_a_home' => 'Comprou uma casa', + 'life_event_sentence_home_improvement' => 'Fez uma reforma na casa', + 'life_event_sentence_holidays' => 'Saiu de férias', + 'life_event_sentence_new_vehicle' => 'Comprou um novo veículo', + 'life_event_sentence_new_roommate' => 'Encontrou um novo companheiro de quarto', + 'life_event_sentence_overcame_an_illness' => 'Superou uma doença', + 'life_event_sentence_quit_a_habit' => 'Abandonou um hábito', + 'life_event_sentence_new_eating_habits' => 'Começou novos hábitos alimentares', + 'life_event_sentence_weight_loss' => 'Perdeu peso', + 'life_event_sentence_wear_glass_or_contact' => 'Começou a usar óculos ou lentes de contato', + 'life_event_sentence_broken_bone' => 'Quebrou um osso', + 'life_event_sentence_removed_braces' => 'Tirou o aparelho ortodôntico', + 'life_event_sentence_surgery' => 'Fez uma cirurgia', + 'life_event_sentence_dentist' => 'Foi ao dentista', + 'life_event_sentence_new_sport' => 'Iniciou um esporte', + 'life_event_sentence_new_hobby' => 'Iniciou um passatempo', + 'life_event_sentence_new_instrument' => 'Aprendeu um novo instrumento', + 'life_event_sentence_new_language' => 'Aprendeu um novo idioma', + 'life_event_sentence_tattoo_or_piercing' => 'Fez uma tatuagem ou colocou um piercing', + 'life_event_sentence_new_license' => 'Tirou carteira de motorista', + 'life_event_sentence_travel' => 'Viajou', + 'life_event_sentence_achievement_or_award' => 'Obteve uma conquista ou prêmio', + 'life_event_sentence_changed_beliefs' => 'Mudança de crença', + 'life_event_sentence_first_word' => 'Falou pela primeira vez', + 'life_event_sentence_first_kiss' => 'Beijou pela primeira vez', + + // documents + 'document_list_title' => 'Documentos', + 'document_list_cta' => 'Enviar um documento', + 'document_list_blank_desc' => 'Aqui você pode armazenar documentos relacionados a esta pessoa.', + 'document_upload_zone_cta' => 'Enviar um arquivo', + 'document_upload_zone_progress' => 'Enviando o documento…', + 'document_upload_zone_error' => 'Houve um erro ao adicionar o arquivo. Por favor tente novamente.', + + // Photos + 'photo_title' => 'Fotos', + 'photo_list_title' => 'Fotos relacionadas', + 'photo_list_cta' => 'Enviar foto', + 'photo_list_blank_desc' => 'Você pode armazenar imagens sobre este contato. Carregue uma agora!', + 'photo_upload_zone_cta' => 'Carregar uma foto', + 'photo_current_profile_pic' => 'Foto de perfil', + 'photo_make_profile_pic' => 'Foto de perfil', + 'photo_delete' => 'Excluir foto', + 'photo_next' => 'Próxima foto', + 'photo_previous' => 'Foto anterior', + + // Avatars + 'avatar_change_title' => 'Alterar seu avatar', + 'avatar_question' => 'Qual foto você gostaria de usar?', + 'avatar_default_avatar' => 'Foto padrão', + 'avatar_adorable_avatar' => 'Uma foto Adorável', + 'avatar_gravatar' => 'O Gravatar associado ao endereço de e-mail desta pessoa. Gravatar é um sistema global que permite aos usuários associar endereços de e-mail com fotos.', + 'avatar_current' => 'Manter foto atual', + 'avatar_photo' => 'Fazer o upload de uma nova foto', + 'avatar_crop_new_avatar_photo' => 'Cortar a nova foto', + + // emotions + 'emotion_this_made_me_feel' => 'Isso fez você se sentir…', + + // logs + 'auditlogs_link' => 'História', + 'auditlogs_title' => 'Tudo que aconteceu com :name', + 'auditlogs_breadcrumb' => 'História', + 'auditlogs_author' => 'Por :name em :date', + + // contact field label + 'contact_field_label_home' => 'Casa', + 'contact_field_label_work' => 'Trabalho', + 'contact_field_label_cell' => 'Celular', + 'contact_field_label_fax' => 'Fax', + 'contact_field_label_pager' => 'Pager', + 'contact_field_label_main' => 'Principal', + 'contact_field_label_other' => 'Outro', + 'contact_field_label_personal' => 'Pessoal', +]; diff --git a/resources/lang/pt-BR/reminder.php b/resources/lang/pt-BR/reminder.php new file mode 100644 index 0000000..6b1920c --- /dev/null +++ b/resources/lang/pt-BR/reminder.php @@ -0,0 +1,16 @@ + 'Desejar feliz aniversário para :name', + 'type_phone_call' => 'Ligar para', + 'type_lunch' => 'Almoçar com', + 'type_hangout' => 'Sair com', + 'type_email' => 'Enviar e-mail para', + 'type_birthday_kid' => 'Wish happy birthday to the child of', +]; diff --git a/resources/lang/pt-BR/settings.php b/resources/lang/pt-BR/settings.php new file mode 100644 index 0000000..7380133 --- /dev/null +++ b/resources/lang/pt-BR/settings.php @@ -0,0 +1,557 @@ + 'Ajustes de conta', + 'sidebar_personalization' => 'Personalização', + 'sidebar_settings_storage' => 'Armazenamento', + 'sidebar_settings_export' => 'Exportar dados', + 'sidebar_settings_users' => 'Usuários', + 'sidebar_settings_subscriptions' => 'Assinatura', + 'sidebar_settings_import' => 'Importar dados', + 'sidebar_settings_tags' => 'Gerenciar tags', + 'sidebar_settings_api' => 'API', + 'sidebar_settings_dav' => 'DAV Resources', + 'sidebar_settings_security' => 'Segurança', + 'sidebar_settings_auditlogs' => 'Registros de Auditoria', + + 'title_general' => 'Informações Gerais', + 'title_i18n' => 'Configurações internacionais', + 'title_layout' => 'Layout', + + 'me_title' => 'Eu como contato', + 'me_help' => 'Este é o contato que te representa em Monica', + 'me_select' => 'Selecione um contato', + 'me_no_contact' => 'Nenhum contato selecionado.', + 'me_select_click' => 'Clique aqui para selecionar um contato.', + 'me_remove_contact' => 'Remover a associação', + 'me_choose' => 'Escolha-se', + 'me_choose_placeholder' => 'Escolha-se', + + 'export_title' => 'Exporte os dados da sua conta', + 'export_be_patient' => 'Click the button to start the export. It might take several minutes to process the export – please be patient and do not spam the button.', + 'export_title_sql' => 'Export to SQL', + 'export_sql_explanation' => 'Exporting your data in SQL format allows you to take your data and import it to your own Monica instance. This is only valuable if you do have your own server.', + 'export_sql_cta' => 'Export to SQL', + 'export_sql_link_instructions' => 'Note: read the instructions to learn more about importing this file to your instance.', + 'export_title_json' => 'Export to Json', + 'export_submitted' => 'Your export has been submitted, it will be available in a few moment…', + 'export_json_explanation' => 'Exporting your data in Json format for backup.', + 'export_json_beta' => 'Json export is in preview mode. Tell us what you think about it:', + 'export_json_cta' => 'Export to Json', + 'export_header_type' => 'Type', + 'export_header_timestamp' => 'Creation date', + 'export_header_status' => 'Status', + 'export_header_actions' => 'Actions', + 'export_last_title' => 'Last exports', + 'export_empty_title' => 'No exports yet', + 'export_type_json' => 'Json export', + 'export_type_sql' => 'SQL export', + 'export_status_todo' => 'Submitted', + 'export_status_doing' => 'Doing', + 'export_status_done' => 'Done', + 'export_status_failed' => 'Failed', + 'export_not_done' => 'Download impossible, this export is not done yet.', + + 'firstname' => 'Nome', + 'lastname' => 'Sobrenome', + 'name_order' => 'Ordem de nome', + 'name_order_firstname_lastname' => ' – João Silva', + 'name_order_lastname_firstname' => ' – Silva João', + 'name_order_firstname_lastname_nickname' => ' () – João Silva (Jão)', + 'name_order_firstname_nickname_lastname' => ' () – João (Jão) Silva', + 'name_order_lastname_firstname_nickname' => ' () – Silva João (Jão)', + 'name_order_lastname_nickname_firstname' => ' () – Silva (Jão) João', + 'name_order_nickname_firstname_lastname' => ' ( ) – Jão (João Silva)', + 'name_order_nickname_lastname_firstname' => ' ( ) – Rambo (Doe John)', + 'name_order_nickname' => ' – Jão', + 'currency' => 'Moeda', + 'name' => 'Seu nome: :name', + 'email' => 'Endereço de e-mail', + 'email_placeholder' => 'Digite o seu e-mail', + 'email_help' => 'Este é o email usado para fazer login, e é para cá que Monica enviará seus lembretes.', + 'timezone' => 'Fuso horário', + 'temperature_scale' => 'Escala de temperatura', + 'temperature_scale_fahrenheit' => 'Fahrenheit (°F)', + 'temperature_scale_celsius' => 'Celsius (°C)', + 'layout' => 'Layout', + 'layout_small' => 'Máximo 1200 pixels de largura', + 'layout_big' => 'Largura total do navegador', + 'save' => 'Atualizar preferências', + 'delete_title' => 'Excluir sua conta', + 'delete_desc' => 'Deseja excluir sua conta? A exclusão é permanente e todos os seus dados serão apagados permanentemente. Se você tiver uma assinatura, ela será cancelada imediatamente.', + 'delete_other_desc' => 'Your data in the main database will be deleted immediately. As described in our privacy policy, we carry out securely encrypted backups of the database every day. These backups are kept for 30 days after which they are completely deleted. We cannot delete specific data from the backups we hold any earlier than this. All of your data will be completely deleted no later than 31 days after your account’s deletion.', + 'reset_desc' => 'Gostaria de redefinir sua conta? Todos os seus contatos e dados serão removidos permanentemente, mas sua conta não será excluída.', + 'reset_title' => 'Redefinir sua conta', + 'reset_cta' => 'Redefinir conta', + 'reset_notice' => 'Tem certeza de que deseja redefinir sua conta? Isso é permanente e não pode ser desfeito.', + 'reset_success' => 'Sua conta foi redefinida com sucesso.', + 'delete_notice' => 'Tem certeza que deseja apagar sua conta? Isso é permanente e não pode ser desfeito. Todos os seus dados serão excluídos e não poderão ser recuperados.', + 'delete_cta' => 'Deletar conta', + 'settings_success' => 'Preferências atualizadas!', + 'locale' => 'Idioma usado no aplicativo', + 'locale_help' => 'Você quer ajudar a traduzir Monica ou adicionar um novo idioma? Por favor, siga este link para mais informações.', + 'locale_ar' => 'Árabe', + 'locale_cs' => 'Tcheco', + 'locale_de' => 'Alemão', + 'locale_el' => 'Grego', + 'locale_en' => 'Inglês', + 'locale_en-GB' => 'Inglês (Reino Unido)', + 'locale_es' => 'Espanhol', + 'locale_fr' => 'Francês', + 'locale_he' => 'Hebraico', + 'locale_hr' => 'Croata', + 'locale_id' => 'Indonésio', + 'locale_it' => 'Italiano', + 'locale_ja' => 'Japonês', + 'locale_nl' => 'Nederlands', + 'locale_pt' => 'Português', + 'locale_pt-BR' => 'Português do Brasil', + 'locale_ru' => 'Russo', + 'locale_sv' => 'Sueco', + 'locale_vi' => 'Vietnamita', + 'locale_zh' => 'Chinês (Simplificado)', + 'locale_zh-TW' => 'Chinês Tradicional', + 'locale_tr' => 'Turco', + + 'security_title' => 'Segurança', + 'security_help' => 'Altere as informações de segurança da sua conta.', + 'password_change' => 'Redefina sua senha', + 'password_current' => 'Senha atual', + 'password_current_placeholder' => 'Digite a sua senha atual', + 'password_new1' => 'Nova senha', + 'password_new1_placeholder' => 'Digite sua nova senha', + 'password_new2' => 'Confirme sua nova senha', + 'password_new2_placeholder' => 'Redigite sua nova senha', + 'password_btn' => 'Alterar senha', + '2fa_title' => 'Autenticação de dois fatores', + '2fa_otp_title' => 'Aplicativo para autenticação de dois fatores', + '2fa_enable_title' => 'Ativar Autenticação de dois fatores', + '2fa_enable_description' => 'Ative a autenticação de dois fatores para proteger sua conta.', + '2fa_enable_otp' => 'Abra o seu aplicativo de autenticação de dois fatores e aponte o celular para essa tela para capturar o código QR:', + '2fa_enable_otp_help' => 'Se o seu aplicativo móvel de Autenticação em Duas Etapas não suporte códigos de barras QR, digite o seguinte código:', + '2fa_enable_otp_validate' => 'Por favor, confirme o novo dispositivo que você acabou de configurar:', + '2fa_enable_success' => 'Autenticação de dois fatores ativada', + '2fa_enable_error' => 'Erro ao tentar ativar a autenticação de dois fatores', + '2fa_enable_error_already_set' => 'Autenticação de dois fatores já está ativada', + '2fa_disable_title' => 'Desativar autenticação de dois fatores', + '2fa_disable_description' => 'Desative a Autenticação em Duas Etapas para sua conta. Tenha cuidado, sua conta estará muito menos segura!', + '2fa_disable_success' => 'Autenticação de Dois Fatores desativada', + '2fa_disable_error' => 'Erro ao tentar desativar a Autenticação em Duas Etapas', + + 'webauthn_title' => 'Chave de segurança — protocolo WebAuthn', + 'webauthn_enable_description' => 'Adicionar uma nova regra de segurança', + 'webauthn_key_name_help' => 'Dê um nome à sua chave.', + 'webauthn_key_name' => 'Nome da chave:', + 'webauthn_success' => 'Sua chave foi detectada e validada.', + 'webauthn_last_use' => 'Último uso: {timestamp}', + 'webauthn_delete_confirmation' => 'Você tem certeza que deseja excluir esta chave?', + 'webauthn_delete_success' => 'Chave excluída', + 'webauthn_insertKey' => 'Insira a sua chave de segurança.', + 'webauthn_buttonAdvise' => 'Se a sua chave de segurança tiver um botão, pressione-o.', + 'webauthn_noButtonAdvise' => 'Se não, remova-o e insira-o novamente.', + 'webauthn_not_supported' => 'Seu navegador atualmente não oferece suporte ao WebAuthn.', + 'webauthn_not_secured' => 'WebAuthn suporta apenas conexões seguras. Por favor, carregue esta página com o padrão https.', + 'webauthn_error_already_used' => 'Esta chave já está registrada. Não é necessário registrá-la novamente.', + 'webauthn_error_not_allowed' => 'A operação expirou ou não foi permitida.', + + 'recovery_title' => 'Códigos de recuperação', + 'recovery_show' => 'Obter códigos de recuperação', + 'recovery_copy_help' => 'Copiar códigos para a área de transferência', + 'recovery_help_intro' => 'Estes são os seus códigos de recuperação:', + 'recovery_help_information' => 'Você pode usar cada código de recuperação uma única vez.', + 'recovery_clipboard' => 'Códigos copiados para a área de transferência.', + 'recovery_generate' => 'Gerar novos códigos…', + 'recovery_generate_help' => 'Gerar novos códigos invalidará os códigos gerados anteriormente.', + 'recovery_already_used_help' => 'Esse código já foi usado.', + + 'users_list_title' => 'Usuários com acesso à sua conta', + 'users_list_add_user' => 'Convidar um novo usuário', + 'users_list_you' => 'É isso', + 'users_list_invitations_title' => 'Convites pendentes', + 'users_list_invitations_explanation' => 'Abaixo estão as pessoas que você convidou para se juntar a Monica como colaborador.', + 'users_list_invitations_invited_by' => 'convidado por :name', + 'users_list_invitations_sent_date' => 'enviado em :date', + 'users_blank_title' => 'Você é o único que tem acesso a essa conta.', + 'users_blank_add_title' => 'Você gostaria de convidar outra pessoa?', + 'users_blank_description' => 'Essa pessoa terá o mesmo acesso que você tem e poderá adicionar, editar ou excluir informações de contato.', + 'users_blank_cta' => 'Convidar alguém', + 'users_add_title' => 'Convide um novo usuário para sua conta por e-mail', + 'users_add_description' => 'Essa pessoa terá o mesmo acesso que você, incluindo convidar ou excluir outros usuários, incluindo você. Certifique-se de que você confia nessa pessoa antes de lhe dar acesso.', + 'users_add_email_field' => 'Digite o e-mail da pessoa que você deseja convidar', + 'users_add_confirmation' => 'Eu confirmo que eu quero convidar esse usuário para minha conta. Compreendo que esta pessoa terá acesso a TODOS os meus dados e verá exatamente o que eu vejo.', + 'users_add_cta' => 'Convidar usuário por e-mail', + 'users_accept_title' => 'Aceite o convite e crie uma nova conta', + 'users_error_please_confirm' => 'Por favor, confirme que você deseja convidar este usuário antes de prosseguir com o convite', + 'users_error_email_already_taken' => 'Este e-mail já está em uso. Por favor, escolha outro', + 'users_error_already_invited' => 'Você já convidou este usuário. Por favor, escolha outro endereço de e-mail.', + 'users_error_email_not_similar' => 'Este não é o e-mail da pessoa que te convidou.', + 'users_invitation_deleted_confirmation_message' => 'O convite foi excluído com sucesso', + 'users_invitations_delete_confirmation' => 'Tem certeza de que deseja excluir este convite?', + 'users_list_delete_confirmation' => 'Tem certeza que deseja excluir este usuário da sua conta?', + 'users_invitation_need_subscription' => 'Adicionar mais usuários requer uma assinatura.', + + 'subscriptions_account_current_plan' => 'Seu plano atual', + 'subscriptions_account_current_legacy' => 'Plano atual, não pode mais ser selecionado:', + 'subscriptions_account_current_paid_plan' => 'Você está no plano :name. Muito obrigado por ser um assinante.', + + 'subscriptions_account_next_billing_title' => 'Próximo pagamento', + 'subscriptions_account_next_billing' => 'Sua assinatura será renovada automaticamente em :date.', + 'subscriptions_account_bill_monthly' => 'Vamos cobrar :price por mais um mês.', + 'subscriptions_account_bill_annual' => 'Vamos cobrar :price por mais um ano.', + 'subscriptions_account_change' => 'Alterar Plano', + + 'subscriptions_account_cancel_title' => 'Cancelar assinatura', + 'subscriptions_account_cancel_action' => 'Cancelar assinatura', + 'subscriptions_account_cancel' => 'Você pode cancelar a assinatura a qualquer momento.', + 'subscriptions_account_free_plan' => 'Você está no plano gratuito.', + 'subscriptions_account_free_plan_upgrade' => 'Você pode fazer um upgrade na sua conta para o plano :name, que custa $:price por mês. Aqui estão as vantagens:', + 'subscriptions_account_free_plan_benefits_users' => 'Número ilimitado de usuários', + 'subscriptions_account_free_plan_benefits_reminders' => 'Lembretes por email', + 'subscriptions_account_free_plan_benefits_import_data_vcard' => 'Importe seus contatos com vCard', + 'subscriptions_account_free_plan_benefits_support' => 'Apoie o projeto a longo prazo, para que possamos introduzir recursos mais importantes.', + 'subscriptions_account_upgrade' => 'Faça um upgrade na sua conta', + 'subscriptions_account_upgrade_title' => 'Faça um upgrade no Monica hoje e tenha relações mais significativas.', + 'subscriptions_account_upgrade_choice' => 'Escolha um plano abaixo e junte-se a :customers pessoas que fizeram upgrade em seu Monica.', + 'subscriptions_account_update_title' => 'Atualizar minha Assinatura', + 'subscriptions_account_update_description' => 'Aqui você pode alterar a frequência de pagamento da sua assinatura.', + 'subscriptions_account_update_information' => 'A cobrança do novo valor será feita imediatamente. Sua assinatura será estendida dependendo da sua escolha.', + 'subscriptions_account_invoices' => 'Faturas', + 'subscriptions_account_invoices_download' => 'Download', + 'subscriptions_account_invoices_subscription' => 'Assinatura de :startDate a :endDate', + 'subscriptions_account_payment' => 'Qual opção de pagamento melhor lhe agrada?', + 'subscriptions_account_confirm_payment' => 'Seu pagamento está incompleto, por favor confirme seu pagamento.', + 'subscriptions_downgrade_title' => 'Fazer downgrade da sua conta para o plano gratuito', + 'subscriptions_downgrade_limitations' => 'O plano gratuito tem limitações. Para poder fazer o downgrade, você precisa passar a lista de verificação abaixo:', + 'subscriptions_downgrade_rule_users' => 'Você deve ter apenas 1 usuário na sua conta', + 'subscriptions_downgrade_rule_users_constraint' => 'Você tem atualmente 1 usuário na sua conta. No momento, você tem :count usuários na sua conta.', + 'subscriptions_downgrade_rule_invitations' => 'Você não deve ter nenhum convite pendente', + 'subscriptions_downgrade_rule_invitations_constraint' => 'Você tem atualmente 1 convite pendente.➲ Você tem :count convites pendentes.', + 'subscriptions_downgrade_rule_contacts' => 'Você não pode ter mais de :number contatos ativos', + 'subscriptions_downgrade_rule_contacts_constraint' => 'Você tem atualmente 1 contato.├Você tem :count contatos.', + 'subscriptions_downgrade_rule_contacts_archive' => 'Você também pode arquivar todos os seus contatos – para poder prosseguir com o processo de downgrade.', + 'subscriptions_downgrade_cta' => 'Fazer downgrade', + 'subscriptions_downgrade_success' => 'Você voltou para o Plano Grátis!', + 'subscriptions_downgrade_thanks' => 'Muito obrigado por experimentar o plano pago. Continuamos adicionando novas funcionalidades à Monica o tempo todo - então você pode querer voltar no futuro para ver se pode estar interessado em fazer uma assinatura novamente.', + 'subscriptions_back' => 'Voltar para as configurações', + 'subscriptions_upgrade_title' => 'Faça um upgrade na sua conta', + 'subscriptions_upgrade_choose' => 'Você escolheu o plano :plan.', + 'subscriptions_upgrade_infos' => 'Não poderíamos estar mais felizes. Insira as suas informações de pagamento abaixo.', + 'subscriptions_upgrade_name' => 'Nome no cartão', + 'subscriptions_upgrade_zip' => 'CEP ou código postal', + 'subscriptions_upgrade_credit' => 'Cartão de crédito ou débito', + 'subscriptions_upgrade_submit' => 'Pagar {amount}', + 'subscriptions_upgrade_charge' => 'Agora, será cobrado o seu cartão :price A próxima cobrança será em :date. Se você mudar de ideia, você pode cancelar a qualquer momento, sem perguntas.', + 'subscriptions_upgrade_charge_handled' => 'O pagamento é processado por Stripe. Nenhuma informação do cartão fica em nosso servidor.', + 'subscriptions_upgrade_success' => 'Obrigado! Você agora é assinante.', + 'subscriptions_upgrade_thanks' => 'Bem-vindo à comunidade de pessoas que tentam fazer do mundo um lugar melhor.', + + 'subscriptions_payment_confirm_title' => 'Confirme o seu pagamento :amount', + 'subscriptions_payment_confirm_information' => 'A confirmação extra é necessária para processar o seu pagamento. Por favor, confirme seu pagamento preenchendo os detalhes de pagamento abaixo.', + 'subscriptions_payment_succeeded_title' => 'Pagamento realizado', + 'subscriptions_payment_succeeded' => 'Este pagamento já foi confirmado com sucesso.', + 'subscriptions_payment_cancelled_title' => 'Pagamento cancelado', + 'subscriptions_payment_cancelled' => 'Este pagamento foi cancelado.', + 'subscriptions_payment_error_name' => 'Por favor, forneça seu nome.', + 'subscriptions_payment_success' => 'O pagamento foi efetuado com sucesso.', + + 'subscriptions_pdf_title' => 'Sua assinatura mensal :name', + 'subscriptions_plan_frequency_year' => ':amount / ano', + 'subscriptions_plan_frequency_month' => ':amount / mês', + 'subscriptions_plan_choose' => 'Escolha este plano', + 'subscriptions_plan_year_title' => 'Pagar anualmente', + 'subscriptions_plan_year_bonus' => 'Tranquilidade para um ano inteiro', + 'subscriptions_plan_month_title' => 'Pagar mensalmente', + 'subscriptions_plan_month_bonus' => 'Cancele a qualquer momento', + 'subscriptions_plan_include1' => 'Incluído na sua atualização:', + 'subscriptions_plan_include2' => 'Número ilimitado de contatos • Número ilimitado de usuários • Lembretes por e-mail • Importar com vCard • Personalização da página de contatos', + 'subscriptions_plan_include3' => '100% dos lucros revertem a favor do desenvolvimento deste grande projeto de código aberto.', + 'subscriptions_help_title' => 'Detalhes adicionais sobre os quais você pode estar curioso', + 'subscriptions_help_opensource_title' => 'O que é um projeto de código aberto?', + 'subscriptions_help_opensource_desc' => 'Monica é um projeto open source. Isso significa que é feito por uma comunidade que quer construir uma ferramenta excelente para o bem maior. Ser de código aberto significa que o código está disponível publicamente no GitHub e todos podem inspecioná-lo, modificá-lo ou aprimorá-lo. Todo o dinheiro que angariamos é dedicado à construção de melhores recursos, ao pagamento de servidores mais poderosos e a outros custos. Obrigado pela sua ajuda. Não poderíamos fazer isso sem você.', + 'subscriptions_help_limits_title' => 'Existe algum limite para o número de contatos que podemos ter no plano gratuito?', + 'subscriptions_help_limits_plan' => 'Sim. Planos grátis permitem que você gerencie :number contatos.', + 'subscriptions_help_discounts_title' => 'Vocês oferecem descontos para uso educacional ou uso sem fins lucrativos?', + 'subscriptions_help_discounts_desc' => 'Sim! Monica é gratuita para estudantes e gratuita para instituições sem fins lucrativos e de caridade. Basta contatar o suporte com um comprovante de seu status e aplicaremos este status especial na sua conta.', + 'subscriptions_help_change_title' => 'E se eu mudar de ideia?', + 'subscriptions_help_change_desc' => 'Você pode cancelar a qualquer momento, sem perguntas e todas feitas por você – sem necessidade de entrar em contato com o suporte. No entanto, não será reembolsado no período em curso.', + + 'stripe_error_card' => 'Seu cartão foi recusado. A mensagem recusada é: :message', + 'stripe_error_api_connection' => 'A comunicação de rede com o Stripe falhou. Tente novamente mais tarde.', + 'stripe_error_rate_limit' => 'Muitas solicitações com o Stripe no momento. Tente novamente mais tarde.', + 'stripe_error_invalid_request' => 'Parâmetros inválidos. Tente novamente mais tarde.', + 'stripe_error_authentication' => 'Autenticação errada com Stripe', + + 'import_title' => 'Importar contatos da sua conta', + 'import_cta' => 'Carregar contatos', + 'import_stat' => 'Você importou :number arquivos até agora.', + 'import_result_stat' => 'Cartão vCard enviado com 1 contato (:total_imported importado, :total_skipped ignorado)|Uploaded vCard com :total_contacts (:total_imported importado, :total_skipped ignorado)', + 'import_view_report' => 'Ver relatório', + 'import_in_progress' => 'A importação está em andamento. Recarregue a página em um minuto.', + 'import_upload_title' => 'Importe seus contatos de um arquivo vCard', + 'import_upload_rules_desc' => 'Temos, no entanto, algumas regras:', + 'import_upload_rule_format' => 'Oferecemos suporte a arquivos .vcard e .vcf.', + 'import_upload_rule_vcard' => 'Oferecemos suporte ao formato vCard 3.0, que é o formato padrão do macOS Contacts.app e Google Contacts.', + 'import_upload_rule_instructions' => 'Exportar instruções para macOS Contacts.app e Google contacts.', + 'import_upload_rule_multiple' => 'Se os seus contatos tiverem vários endereços de e-mail ou números de telefone, apenas a primeira entrada será salva.', + 'import_upload_rule_limit' => 'Os arquivos estão limitados a 10 MB.', + 'import_upload_rule_time' => 'Pode demorar até um minuto para carregar os contatos e processá-los. Por favor, seja paciente.', + 'import_upload_rule_cant_revert' => 'Certifique-se de que os dados estão precisos antes de fazer o upload, pois você não pode desfazer o upload.', + 'import_upload_form_file' => 'Seu arquivo .vcf ou .vCard:', + 'import_upload_behaviour' => 'Comportamento da importação:', + 'import_upload_behaviour_add' => 'Adicionar novos contatos e pular os contatos existentes', + 'import_upload_behaviour_replace' => 'Substituir os contatos existentes', + 'import_upload_behaviour_help' => 'Substituir irá substituir todos os dados encontrados no vCard, mas irá manter campos de contato existentes.', + 'import_report_title' => 'Importando relatório', + 'import_report_date' => 'Data da importação', + 'import_report_type' => 'Tipo de importação', + 'import_report_number_contacts' => 'Número de contatos no arquivo', + 'import_report_number_contacts_imported' => 'Número de contatos importados', + 'import_report_number_contacts_skipped' => 'Número de contatos ignorados', + 'import_report_status_imported' => 'Importado', + 'import_report_status_skipped' => 'Ignorado', + 'import_vcard_parse_error' => 'Erro ao analisar a entrada do vCard', + 'import_vcard_contact_exist' => 'O contato já existe', + 'import_vcard_contact_no_firstname' => 'Nenhum nome (obrigatório)', + 'import_vcard_file_not_found' => 'Arquivo não encontrado', + 'import_vcard_unknown_entry' => 'Nome de contato desconhecido', + 'import_vcard_file_no_entries' => 'O arquivo não contém entradas', + 'import_blank_title' => 'Você não importou nenhum contato ainda.', + 'import_blank_question' => 'Você gostaria de importar os contatos agora?', + 'import_blank_description' => 'Podemos importar arquivos vCard que você pode obter a partir dos Contatos do Google ou do seu Gerenciador de Contato.', + 'import_blank_cta' => 'Importar vCard', + 'import_need_subscription' => 'A importação de dados requer uma assinatura.', + + 'tags_list_title' => 'Etiquetas', + 'tags_list_description' => 'Você pode organizar seus contatos configurando etiquetas (tags). As tags funcionam como pastas, mas você pode adicionar mais de uma tag a um contato. Para adicionar uma nova tag, adicione-a no próprio contato.', + 'tags_list_contact_number' => '1 contato|:count contatos', + 'tags_list_delete_success' => 'A tag foi excluída com sucesso', + 'tags_list_edit_success' => 'The tag has been successfully updated', + 'tags_list_delete_confirmation' => 'Tem certeza que deseja excluir a etiqueta? Nenhum contato será excluído, apenas a etiqueta.', + 'tags_blank_title' => 'As tags são uma ótima maneira de categorizar seus contatos.', + 'tags_blank_description' => 'As tags funcionam como pastas, mas você pode adicionar mais de uma tag a um contato. Vá para um contato e marque um amigo, logo abaixo do nome. Depois de adicionar uma tag ao contato, volte aqui para gerenciar todas as tags da sua conta.', + + 'api_title' => 'Acesso à API', + 'api_description' => 'A API pode ser usada para manipular os dados da Monica a partir de uma aplicação externa, como um aplicativo móvel por exemplo.', + 'api_help' => 'Para usar a API, um token é obrigatório. Você pode criar um token de acesso pessoal (autenticação pelo titular) ou autorizar um cliente OAuth para criá-lo para você. Veja a documentação da API.', + 'api_endpoint' => 'A API endpoint para esta instância de Monica é:', + + 'api_personal_access_tokens' => 'Tokens de acesso pessoal', + 'api_pao_description' => 'Certifique-se de dar esse token para uma fonte em que você confia – pois ele permite que você acesse todos os seus dados.', + 'api_token_title' => 'Tokens de acesso pessoal', + 'api_token_create_new' => 'Criar novo token', + 'api_token_not_created' => 'Você não criou nenhum token de acesso pessoal.', + 'api_token_name' => 'Nome do token', + 'api_token_expire' => 'Expira em {date}', + 'api_token_delete' => 'Excluir', + 'api_token_create' => 'Criar Token', + 'api_token_scopes' => 'Escopos', + 'api_token_help' => 'Aqui está seu novo token de acesso pessoal. Esta é a única vez que ele será mostrado, então, não o perca! Agora você pode usar este token para fazer solicitações de API.', + + 'api_oauth_clients' => 'Seus clientes OAuth', + 'api_oauth_clients_desc' => 'Esta seção permite registrar seus próprios clientes OAuth.', + 'api_oauth_clients_desc2' => 'Use este id de cliente para solicitar um novo token e converter códigos de autorização para tokens de acesso. Veja a documentação do Passaporte Laravel para mais informações.', + 'api_oauth_title' => 'Clientes OAuth', + 'api_oauth_create_new' => 'Criar Novo Cliente', + 'api_oauth_edit' => 'Editar Cliente', + 'api_oauth_not_created' => 'Você não criou nenhum cliente OAuth.', + 'api_oauth_clientid' => 'ID do Cliente', + 'api_oauth_name' => 'Nome', + 'api_oauth_name_help' => 'Algo que seus usuários reconhecerão e confiarão.', + 'api_oauth_secret' => 'Segredo', + 'api_oauth_create' => 'Criar Cliente', + 'api_oauth_redirecturl' => 'URL de redirecionamento', + 'api_oauth_redirecturl_help' => 'URL de retorno de chamada de autorização do seu aplicativo.', + + 'api_authorized_clients' => 'Lista de clientes autorizados', + 'api_authorized_clients_desc' => 'Esta seção lista todos os clientes que você tem autorização para acessar seus dados de aplicativos. Você pode revogar essa autorização a qualquer momento.', + 'api_authorized_clients_title' => 'Aplicativos autorizados', + 'api_authorized_clients_none' => 'Ainda não existem clientes autorizados.', + 'api_authorized_clients_name' => 'Nome', + 'api_authorized_clients_scopes' => 'Escopos', + + 'personalization_tab_title' => 'Personalize sua conta', + + 'personalization_title' => 'Aqui você encontrará configurações diferentes para configurar sua conta. Esses recursos são destinados a "usuários masters" que querem o controle máximo sobre a Monica.', + 'personalization_contact_field_type_title' => 'Tipos de campos de contato', + 'personalization_contact_field_type_add' => 'Adicionar novo tipo de campo', + 'personalization_contact_field_type_description' => 'Você pode configurar todos os diferentes tipos de campos de contato que você pode associar a todos os seus contatos. Por exemplo, se uma nova rede social surgir no futuro, você poderá adicionar esta nova forma de comunicação com os seus contatos aqui mesmo.', + 'personalization_contact_field_type_table_name' => 'Nome', + 'personalization_contact_field_type_table_protocol' => 'Protocolo', + 'personalization_contact_field_type_table_actions' => 'Ações', + 'personalization_contact_field_type_modal_title' => 'Adicionar um novo tipo de campo de contato', + 'personalization_contact_field_type_modal_edit_title' => 'Editar um tipo de campo de contato existente', + 'personalization_contact_field_type_modal_delete_title' => 'Excluir um tipo de campo de contato existente', + 'personalization_contact_field_type_modal_delete_description' => 'Tem certeza de que deseja excluir este tipo de campo de contato? Excluir este tipo de campo de contato irá excluir TODOS os dados com este tipo para todos os seus contatos.', + 'personalization_contact_field_type_modal_name' => 'Nome', + 'personalization_contact_field_type_modal_protocol' => 'Protocolo (opcional)', + 'personalization_contact_field_type_modal_protocol_help' => 'Cada novo tipo de campo de contato pode ser clicável. Se um protocolo for definido, nós o usaremos para acionar a ação definida.', + 'personalization_contact_field_type_modal_icon' => 'Ícone (opcional)', + 'personalization_contact_field_type_modal_icon_help' => 'Você pode associar um ícone com este tipo de campo de contato. Você precisa adicionar uma referência a um ícone do Font Awesome.', + 'personalization_contact_field_type_delete_success' => 'O tipo de campo de contato foi excluído com sucesso.', + 'personalization_contact_field_type_add_success' => 'O tipo de campo de contato foi adicionado com sucesso.', + 'personalization_contact_field_type_edit_success' => 'O tipo de campo de contato foi atualizado com sucesso.', + + 'personalization_genders_title' => 'Tipos de gêneros', + 'personalization_genders_add' => 'Adicionar novo tipo de gênero', + 'personalization_genders_desc' => 'Você pode definir quantos gêneros precisar. Você precisa de pelo menos um tipo de gênero na sua conta.', + 'personalization_genders_modal_add' => 'Adicionar tipo de gênero', + 'personalization_genders_modal_edit' => 'Atualizar tipo de gênero', + 'personalization_genders_modal_name' => 'Nome', + 'personalization_genders_modal_name_help' => 'O nome usado para exibir o gênero em uma página de contato.', + 'personalization_genders_modal_sex' => 'Sexo', + 'personalization_genders_modal_sex_help' => 'Usado para definir as relações, e durante o processo de importação/exportação de cartões VCard.', + 'personalization_genders_modal_default' => 'Selecione o gênero padrão para um novo contato', + 'personalization_genders_modal_delete' => 'Excluir tipo de gênero', + 'personalization_genders_modal_delete_desc' => 'Tem certeza de que deseja excluir o gênero “{name}”?', + 'personalization_genders_modal_delete_question' => 'Atualmente, você tem {count} contato com este gênero. Se você excluir este gênero, qual deve ser o gênero deste contato?|Atualmente você tem {count} contatos com este gênero. Se você excluir este gênero, qual deve ser o gênero destes contatos?', + 'personalization_genders_modal_delete_question_default' => 'Este gênero é o padrão. Se você excluir este gênero, qual será o novo padrão?', + 'personalization_genders_modal_error' => 'Por favor, escolha um gênero da lista.', + 'personalization_genders_list_contact_number' => '{count} contato|{count} contatos', + 'personalization_genders_table_name' => 'Nome', + 'personalization_genders_table_sex' => 'Sexo', + 'personalization_genders_table_default' => 'Padrão', + 'personalization_genders_default' => 'Gênero padrão', + 'personalization_genders_make_default' => 'Alterar gênero padrão', + 'personalization_genders_select_default' => 'Selecionar gênero padrão', + 'personalization_genders_m' => 'Masculino', + 'personalization_genders_f' => 'Feminino', + 'personalization_genders_o' => 'Outros', + 'personalization_genders_u' => 'Desconhecido', + 'personalization_genders_n' => 'Nenhum ou não aplicável', + + 'personalization_reminder_rule_save' => 'A alteração foi salva', + 'personalization_reminder_rule_title' => 'Regras do lembrete', + 'personalization_reminder_rule_line' => '{count} dia antes?|{count} dias antes', + 'personalization_reminder_rule_desc' => 'Para cada lembrete que você definiu, Monica pode enviar-lhe um e-mail por alguns dias antes do evento acontecer. Você pode ajustar estas configurações de notificação aqui. Estas notificações se aplicam apenas a lembretes mensais e anuais.', + + 'personalization_module_save' => 'A alteração foi salva', + 'personalization_module_title' => 'Funcionalidades', + 'personalization_module_desc' => 'Você pode não precisar de todas as funcionalidades da Monica. Abaixo você pode alternar as funcionalidades específicas que são utilizadas em uma lista de contatos. Esta alteração afetará TODOS os seus contatos. Desativar um recurso não exclui nenhum dado, simplesmente oculta o recurso.', + + 'personalisation_paid_upgrade' => 'Este é um recurso premium que requer uma assinatura paga para estar ativo. Atualize a sua conta visitando Configurações > Assinatura.', + 'personalisation_paid_upgrade_vue' => 'Este é um recurso premium que requer uma assinatura paga para estar ativo. Atualize a sua conta visitando Configurações > Assinatura.', + + 'reminder_time_to_send' => 'O horário do dia dos lembretes será enviado', + 'reminder_time_to_send_help' => 'Seu próximo lembrete está agendado para ser enviado no {dateTime}.', + + 'personalization_activity_type_category_title' => 'Categorias do tipo atividade', + 'personalization_activity_type_category_add' => 'Adicionar uma nova categoria de tipo de atividade', + 'personalization_activity_type_category_table_name' => 'Nome', + 'personalization_activity_type_category_description' => 'Uma atividade com um dos seus contatos pode ter um tipo e uma categoria. Sua conta vem com um conjunto de tipos de categoria predefinidos por padrão, mas você pode personalizá-los aqui.', + 'personalization_activity_type_category_table_actions' => 'Ações', + 'personalization_activity_type_category_modal_add' => 'Adicionar uma nova categoria de tipo de atividade', + 'personalization_activity_type_category_modal_edit' => 'Editar uma categoria de tipo de atividade', + 'personalization_activity_type_category_modal_question' => 'Como devemos nomear esta nova categoria?', + 'personalization_activity_type_add_button' => 'Adicionar um novo tipo de atividade', + 'personalization_activity_type_modal_add' => 'Adicionar um novo tipo de atividade', + 'personalization_activity_type_modal_question' => 'Como devemos chamar este novo tipo de atividade?', + 'personalization_activity_type_modal_edit' => 'Editar um tipo de atividade', + 'personalization_activity_type_category_modal_delete' => 'Apagar uma categoria de tipo de atividade', + 'personalization_activity_type_category_modal_delete_desc' => 'Tem certeza que deseja excluir esta categoria? Excluirá todos os tipos de atividades associadas. As atividades que pertencem a esta categoria não serão afetadas por esta exclusão.', + 'personalization_activity_type_modal_delete' => 'Apagar um tipo de atividade', + 'personalization_activity_type_modal_delete_desc' => 'Tem certeza que deseja excluir este tipo de atividade? Atividades que pertencem a esta categoria não serão afetadas por esta exclusão.', + 'personalization_activity_type_modal_delete_error' => 'Não encontramos este tipo de atividade.', + 'personalization_activity_type_category_modal_delete_error' => 'Não conseguimos encontrar esta categoria de tipo de atividade.', + + 'personalization_life_event_category_title' => 'Categorias de eventos pessoais', + 'personalization_live_event_category_table_name' => 'Nome', + 'personalization_life_event_category_description' => 'Um evento de vida pode ter um tipo e uma categoria. Sua conta vem com um conjunto de categorias e tipos predefinidos por padrão, mas você pode personalizar os tipos de eventos de vida aqui.', + 'personalization_live_event_category_table_actions' => 'Ações', + 'personalization_life_event_type_add_button' => 'Adicionar um novo tipo de evento de vida', + 'personalization_life_event_type_modal_add' => 'Adicionar um novo tipo de evento de vida', + 'personalization_life_event_type_modal_question' => 'Como devemos chamar este novo tipo de evento de vida?', + 'personalization_life_event_type_modal_edit' => 'Editar um tipo de evento de vida', + 'personalization_life_event_type_modal_delete' => 'Excluir um tipo de evento de vida', + 'personalization_life_event_type_modal_delete_desc' => 'Tem certeza que deseja excluir este tipo de evento de vida? Eventos de vida que pertencem a este tipo serão excluídos ao realizar esta ação.', + 'personalization_life_event_type_modal_delete_error' => 'Não foi possível encontrar este tipo de evento de vida.', + + 'personalization_life_event_category_work_education' => 'Trabalho e educação', + 'personalization_life_event_category_family_relationships' => 'Família e relacionamentos', + 'personalization_life_event_category_home_living' => 'Casa e vida', + 'personalization_life_event_category_travel_experiences' => 'Viagem e experiências', + 'personalization_life_event_category_health_wellness' => 'Saúde e bem-estar', + + 'personalization_life_event_type_new_job' => 'Novo trabalho', + 'personalization_life_event_type_retirement' => 'Aposentado', + 'personalization_life_event_type_new_school' => 'Nova escola', + 'personalization_life_event_type_study_abroad' => 'Estudos no exterior', + 'personalization_life_event_type_volunteer_work' => 'Trabalho voluntário', + 'personalization_life_event_type_published_book_or_paper' => 'Publicou um livro ou artigo', + 'personalization_life_event_type_military_service' => 'Serviço militar', + 'personalization_life_event_type_first_met' => 'Primeira reunião', + 'personalization_life_event_type_new_relationship' => 'Novo relacionamento', + 'personalization_life_event_type_engagement' => 'Envolvimento', + 'personalization_life_event_type_marriage' => 'Casamento', + 'personalization_life_event_type_anniversary' => 'Aniversário', + 'personalization_life_event_type_expecting_a_baby' => 'Esperando um bebê', + 'personalization_life_event_type_new_child' => 'Novo filho', + 'personalization_life_event_type_new_family_member' => 'Novo membro da família', + 'personalization_life_event_type_new_pet' => 'Novo pet', + 'personalization_life_event_type_end_of_relationship' => 'Fim do relacionamento', + 'personalization_life_event_type_loss_of_a_loved_one' => 'Perda de um ente querido', + 'personalization_life_event_type_moved' => 'Mudou-se', + 'personalization_life_event_type_bought_a_home' => 'Comprou uma casa', + 'personalization_life_event_type_home_improvement' => 'Melhoria na casa', + 'personalization_life_event_type_holidays' => 'Feriados', + 'personalization_life_event_type_new_vehicle' => 'Novo veículo', + 'personalization_life_event_type_new_roommate' => 'Novo colega de quarto', + 'personalization_life_event_type_overcame_an_illness' => 'Superou uma doença', + 'personalization_life_event_type_quit_a_habit' => 'Abandonou um hábito', + 'personalization_life_event_type_new_eating_habits' => 'Novos hábitos de alimentação', + 'personalization_life_event_type_weight_loss' => 'Perda de peso', + 'personalization_life_event_type_wear_glass_or_contact' => 'Começou a usar óculos ou lentes de contato', + 'personalization_life_event_type_broken_bone' => 'Quebrou um osso', + 'personalization_life_event_type_removed_braces' => 'Retirou o aparelho ortodôntico', + 'personalization_life_event_type_surgery' => 'Fez uma cirurgia', + 'personalization_life_event_type_dentist' => 'Fez um tratamento dental', + 'personalization_life_event_type_new_sport' => 'Começou a praticar um novo esporte', + 'personalization_life_event_type_new_hobby' => 'Começou um novo passatempo', + 'personalization_life_event_type_new_instrument' => 'Começou a aprender um novo instrumento', + 'personalization_life_event_type_new_language' => 'Começou a aprender um novo idioma', + 'personalization_life_event_type_tattoo_or_piercing' => 'Tatuagem ou piercing', + 'personalization_life_event_type_new_license' => 'Nova CNH', + 'personalization_life_event_type_travel' => 'Viagem', + 'personalization_life_event_type_achievement_or_award' => 'Conquista ou prêmio', + 'personalization_life_event_type_changed_beliefs' => 'Crenças alteradas', + 'personalization_life_event_type_first_word' => 'Primeira palavra', + 'personalization_life_event_type_first_kiss' => 'Primeiro beijo', + + 'storage_title' => 'Armazenamento', + 'storage_account_info' => 'O limite de sua conta é :accountLimit MB. Seu uso atual é :currentAccountSize MB (cerca de :percentUsage%).', + 'storage_upgrade_notice' => 'Faça um upgrade de sua conta para poder enviar documentos e fotos.', + 'storage_description' => 'Aqui você pode ver todos os documentos e fotos enviados sobre seus contatos.', + + 'dav_title' => 'WebDAV', + 'dav_description' => 'Aqui você pode encontrar todas as configurações para usar os recursos WebDAV para exportações CardDAV e CalDAV.', + 'dav_copy_help' => 'Copiar para a área de transferência', + 'dav_clipboard_copied' => 'Valor copiado para a área de transferência', + 'dav_url_base' => 'Url base para todos os recursos de CardDAV e CalDAV:', + 'dav_connect_help' => 'Você pode conectar seus contatos e/ou agendas com esta url base no seu telefone ou computador.', + 'dav_connect_help2' => 'Use seu login (e-mail) e crie um token API como a senha para autenticar.', + 'dav_url_carddav' => 'URL CardDAV para recurso de Contatos:', + 'dav_url_caldav_birthdays' => 'URL CalDAV para recursos de aniversário:', + 'dav_url_caldav_tasks' => 'URL CalDAV para recursos de Tarefas:', + 'dav_title_carddav' => 'CardDAV', + 'dav_title_caldav' => 'CalDAV', + 'dav_carddav_export' => 'Exportar contatos em um único arquivo', + 'dav_caldav_birthdays_export' => 'Exportar todos os aniversários em um só arquivo', + 'dav_caldav_tasks_export' => 'Exportar todas as tarefas em um único arquivo', + + 'archive_title' => 'Arquivar todos os contatos da sua conta', + 'archive_desc' => 'Isto irá arquivar todos os contatos da sua conta.', + 'archive_cta' => 'Arquivar todos os seus contatos', + + 'logs_title' => 'Tudo o que aconteceu com esta conta', + 'logs_actor' => 'Ator', + 'logs_timestamp' => 'Timestamp', + 'logs_description' => 'Descrição', + 'logs_subject' => 'Assunto', + 'logs_size' => 'Tamanho (Kb)', + 'logs_object' => 'Objeto', +]; diff --git a/resources/lang/pt-BR/validation.php b/resources/lang/pt-BR/validation.php new file mode 100644 index 0000000..c344b0b --- /dev/null +++ b/resources/lang/pt-BR/validation.php @@ -0,0 +1,166 @@ + 'O campo :attribute deve ser aceito.', + 'active_url' => 'O campo :attribute deve conter uma URL válida.', + 'after' => 'O campo :attribute deve conter uma data posterior a :date.', + 'after_or_equal' => 'O campo :attribute deve conter uma data superior ou igual a :date.', + 'alpha' => 'O campo :attribute deve conter apenas letras.', + 'alpha_dash' => 'O campo :attribute deve conter apenas letras, números e traços.', + 'alpha_num' => 'O campo :attribute deve conter apenas letras e números .', + 'array' => 'O campo :attribute deve conter um array.', + 'before' => 'O campo :attribute deve conter uma data anterior a :date.', + 'before_or_equal' => 'O campo :attribute deve conter uma data inferior ou igual a :date.', + 'between' => [ + 'numeric' => 'O campo :attribute deve conter um número entre :min e :max.', + 'file' => 'O campo :attribute deve conter um arquivo de :min a :max kilobytes.', + 'string' => 'O campo :attribute deve conter entre :min a :max caracteres.', + 'array' => 'O campo :attribute deve conter de :min a :max itens.', + ], + 'boolean' => 'O campo :attribute deve conter o valor verdadeiro ou falso.', + 'confirmed' => 'A confirmação para o campo :attribute não coincide.', + 'date' => 'O campo :attribute não contém uma data válida.', + 'date_equals' => 'O campo :attribute deve ser uma data igual a :date.', + 'date_format' => 'A data informada para o campo :attribute não respeita o formato :format.', + 'different' => 'Os campos :attribute e :other devem conter valores diferentes.', + 'digits' => 'O campo :attribute deve conter :digits dígitos.', + 'digits_between' => 'O campo :attribute deve conter entre :min a :max dígitos.', + 'dimensions' => 'O valor informado para o campo :attribute não é uma dimensão de imagem válida.', + 'distinct' => 'O campo :attribute contém um valor duplicado.', + 'email' => 'O campo :attribute não contém um endereço de email válido.', + 'ends_with' => 'O campo :attribute deve terminar com um dos seguintes valores: :values', + 'exists' => 'O valor selecionado para o campo :attribute é inválido.', + 'file' => 'O campo :attribute deve conter um arquivo.', + 'filled' => 'O campo :attribute é obrigatório.', + 'gt' => [ + 'numeric' => 'O campo :attribute deve ser maior que :value.', + 'file' => 'O arquivo :attribute deve ser maior que :value kilobytes.', + 'string' => 'O campo :attribute deve ser maior que :value caracteres.', + 'array' => 'O campo :attribute deve ter mais que :value itens.', + ], + 'gte' => [ + 'numeric' => 'O campo :attribute deve ser maior ou igual a :value.', + 'file' => 'O arquivo :attribute deve ser maior ou igual a :value kilobytes.', + 'string' => 'O campo :attribute deve ser maior ou igual a :value caracteres.', + 'array' => 'O campo :attribute deve ter :value itens ou mais.', + ], + 'image' => 'O campo :attribute deve conter uma imagem.', + 'in' => 'O campo :attribute não contém um valor válido.', + 'in_array' => 'O campo :attribute não existe em :other.', + 'integer' => 'O campo :attribute deve conter um número inteiro.', + 'ip' => 'O campo :attribute deve conter um IP válido.', + 'ipv4' => 'O campo :attribute deve conter um IPv4 válido.', + 'ipv6' => 'O campo :attribute deve conter um IPv6 válido.', + 'json' => 'O campo :attribute deve conter uma string JSON válida.', + 'lt' => [ + 'numeric' => 'O campo :attribute deve ser menor que :value.', + 'file' => 'O arquivo :attribute ser menor que :value kilobytes.', + 'string' => 'O campo :attribute deve ser menor que :value caracteres.', + 'array' => 'O campo :attribute deve ter menos que :value itens.', + ], + 'lte' => [ + 'numeric' => 'O campo :attribute deve ser menor ou igual a :value.', + 'file' => 'O arquivo :attribute ser menor ou igual a :value kilobytes.', + 'string' => 'O campo :attribute deve ser menor ou igual a :value caracteres.', + 'array' => 'O campo :attribute não deve ter mais que :value itens.', + ], + 'max' => [ + 'numeric' => 'O campo :attribute não pode conter um valor superior a :max.', + 'file' => 'O campo :attribute não pode conter um arquivo com mais de :max kilobytes.', + 'string' => 'O campo :attribute não pode conter mais de :max caracteres.', + 'array' => 'O campo :attribute deve conter no máximo :max itens.', + ], + 'mimes' => 'O campo :attribute deve conter um arquivo do tipo: :values.', + 'mimetypes' => 'O campo :attribute deve conter um arquivo do tipo: :values.', + 'min' => [ + 'numeric' => 'O campo :attribute deve conter um número superior ou igual a :min.', + 'file' => 'O campo :attribute deve conter um arquivo com no mínimo :min kilobytes.', + 'string' => 'O campo :attribute deve conter no mínimo :min caracteres.', + 'array' => 'O campo :attribute deve conter no mínimo :min itens.', + ], + 'not_in' => 'O campo :attribute contém um valor inválido.', + 'not_regex' => 'O formato do valor :attribute é inválido.', + 'numeric' => 'O campo :attribute deve conter um valor numérico.', + 'password' => 'A senha está incorreta.', + 'present' => 'O campo :attribute deve estar presente.', + 'regex' => 'O formato do valor informado no campo :attribute é inválido.', + 'required' => 'O campo :attribute é obrigatório.', + 'required_if' => 'O campo :attribute é obrigatório quando o valor do campo :other é igual a :value.', + 'required_unless' => 'O campo :attribute é obrigatório a menos que :other esteja presente em :values.', + 'required_with' => 'O campo :attribute é obrigatório quando :values está presente.', + 'required_with_all' => 'O campo :attribute é obrigatório quando um dos :values está presente.', + 'required_without' => 'O campo :attribute é obrigatório quando :values não está presente.', + 'required_without_all' => 'O campo :attribute é obrigatório quando nenhum dos :values está presente.', + 'same' => 'Os campos :attribute e :other devem conter valores iguais.', + 'size' => [ + 'numeric' => 'O campo :attribute deve conter o número :size.', + 'file' => 'O campo :attribute deve conter um arquivo com o tamanho de :size kilobytes.', + 'string' => 'O campo :attribute deve conter :size caracteres.', + 'array' => 'O campo :attribute deve conter :size itens.', + ], + 'starts_with' => 'O campo :attribute deve começar com um dos seguintes valores: :values', + 'string' => 'O campo :attribute deve ser uma string.', + 'timezone' => 'O campo :attribute deve conter um fuso horário válido.', + 'unique' => 'O valor informado para o campo :attribute já está em uso.', + 'uploaded' => 'Falha no Upload do arquivo :attribute.', + 'url' => 'O formato da URL informada para o campo :attribute é inválido.', + 'uuid' => 'O campo :attribute deve ser um UUID válido.', + + /* + |-------------------------------------------------------------------------- + | Custom Validation Language Lines + |-------------------------------------------------------------------------- + | + | Here you may specify custom validation messages for attributes using the + | convention "attribute.rule" to name the lines. This makes it quick to + | specify a specific custom language line for a given attribute rule. + | + */ + + 'custom' => [ + 'attribute-name' => [ + 'rule-name' => 'custom-message', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Custom Validation Attributes + |-------------------------------------------------------------------------- + | + | The following language lines are used to swap our attribute placeholder + | with something more reader friendly such as "E-Mail Address" instead + | of "email". This simply helps us make our message more expressive. + | + */ + + 'attributes' => [], + + 'vue' => [ + 'max' => [ + 'numeric' => '{field} não pode ser maior que {max}.', + 'string' => '{field} não pode ser maior que {max}.', + ], + 'required' => '{field} é obrigatório.', + 'url' => '{field} não é uma URL válida.', + ], + +]; diff --git a/resources/lang/pt.json b/resources/lang/pt.json new file mode 100644 index 0000000..ddea72e --- /dev/null +++ b/resources/lang/pt.json @@ -0,0 +1,7 @@ +{ + "The :attribute must contain at least one uppercase and one lowercase letter.": "The :attribute must contain at least one uppercase and one lowercase letter.", + "The :attribute must contain at least one letter.": "The :attribute must contain at least one letter.", + "The :attribute must contain at least one symbol.": "The :attribute must contain at least one symbol.", + "The :attribute must contain at least one number.": "The :attribute must contain at least one number.", + "The given :attribute has appeared in a data leak. Please choose a different :attribute.": "The given :attribute has appeared in a data leak. Please choose a different :attribute." +} diff --git a/resources/lang/pt/app.php b/resources/lang/pt/app.php new file mode 100644 index 0000000..5d847c4 --- /dev/null +++ b/resources/lang/pt/app.php @@ -0,0 +1,571 @@ + 'Sim', + 'no' => 'Não', + 'update' => 'Atualizar', + 'save' => 'Salvar', + 'add' => 'Adicionar', + 'cancel' => 'Cancelar', + 'confirm' => 'Confirm', + 'delete_confirm' => 'Are you sure?', + 'delete' => 'Deletar', + 'edit' => 'Editar', + 'upload' => 'Enviar', + 'download' => 'Transferir', + 'save_close' => 'Guardar e Fechar', + 'close' => 'Fechar', + 'copy' => 'Copy', + 'create' => 'Create', + 'remove' => 'Remover', + 'revoke' => 'Revogar', + 'done' => 'Concluído', + 'back' => 'Back', + 'verify' => 'Verificar', + 'new' => 'new', + 'unknown' => 'I don’t know', + 'load_more' => 'Carregar mais', + 'loading' => 'Loading…', + 'with' => 'com', + 'today' => 'hoje', + 'yesterday' => 'ontem', + 'another_day' => 'another day', + 'date' => 'Data', + 'type' => 'Type', + 'zoom' => 'Zoom', + 'upgrade' => 'Upgrade to unlock', + 'percent_uploaded' => '{percent}% uploaded', + 'retry' => 'Tentar novamente', + 'filter' => 'Filter the list', + 'go_back' => 'Retroceder', + 'file_selected' => 'One file selected…|{count} files selected…', + + 'application_title' => 'Monica – personal relationship manager', + 'application_description' => 'Monica is a tool to manage your interactions with your loved ones, friends, and family.', + 'application_og_title' => 'Have better relations with your loved ones. Free online CRM for friends and family.', + + 'markdown_description' => 'Want to format your text nicely? We support Markdown to add bold, italic, lists, and more.', + 'markdown_link' => 'Ler a documentação', + + 'header_settings_link' => 'Configurações', + 'header_logout_link' => 'Logout', + 'header_changelog_link' => 'Product changes', + + 'main_nav_cta' => 'Adicionar Pessoa', + 'main_nav_dashboard' => 'Painel', + 'main_nav_family' => 'Contatos', + 'main_nav_journal' => 'Diário', + 'main_nav_activities' => 'Atividades', + 'main_nav_tasks' => 'Tarefas', + + 'footer_remarks' => 'Comments?', + 'footer_send_email' => 'Send us an email', + 'footer_privacy' => 'Política de Privacidade', + 'footer_release' => 'Notas de versão', + 'footer_newsletter' => 'Boletim informativo', + 'footer_source_code' => 'Contribuir', + 'footer_version' => 'Versão: :version', + 'footer_new_version' => 'A new version of Monica is available', + + 'footer_modal_version_whats_new' => 'O que há de novo', + 'footer_modal_version_release_away' => 'A sua instalação encontra-se 1 versão atrás da versão mais recente disponível. É recomendado atualizar a sua instalação.|A sua instalação encontra-se :number versões atrás da versão mais recente disponível. É recomendado atualizar a sua instalação.', + + 'breadcrumb_dashboard' => 'Painel', + 'breadcrumb_list_contacts' => 'Lista de contatos', + 'breadcrumb_archived_contacts' => 'Archived contacts', + 'breadcrumb_journal' => 'Diário', + 'breadcrumb_settings' => 'Configurações', + 'breadcrumb_settings_export' => 'Exportar', + 'breadcrumb_settings_users' => 'Utilizadores', + 'breadcrumb_settings_users_add' => 'Adicionar um utilizador', + 'breadcrumb_settings_subscriptions' => 'Subscrição', + 'breadcrumb_settings_import' => 'Importar', + 'breadcrumb_settings_import_report' => 'Importar relatório', + 'breadcrumb_settings_import_upload' => 'Enviar', + 'breadcrumb_settings_tags' => 'Etiquetas', + 'breadcrumb_add_significant_other' => 'Adicionar companheiro(a)', + 'breadcrumb_edit_significant_other' => 'Editar companheiro(a)', + 'breadcrumb_add_note' => 'Adicionar uma nota', + 'breadcrumb_edit_note' => 'Editar nota', + 'breadcrumb_api' => 'API', + 'breadcrumb_dav' => 'DAV Resources', + 'breadcrumb_edit_introductions' => 'Como se conheceram', + 'breadcrumb_settings_personalization' => 'Personalização', + 'breadcrumb_settings_security' => 'Segurança', + 'breadcrumb_settings_security_2fa' => 'Autenticação de dois fatores', + 'breadcrumb_profile' => 'Profile of :name', + + 'gender_male' => 'Homem', + 'gender_female' => 'Mulher', + 'gender_none' => 'Prefiro não dizer', + 'gender_no_gender' => 'Sem género', + + 'error_title' => 'Ups! Algo correu mal.', + 'error_unauthorized' => 'Não tem permissões para editar este recurso.', + 'error_user_account' => 'This user does not belong to the given account.', + 'error_save' => 'Ocorreu um erro ao guardar os dados.', + 'error_try_again' => 'Something went wrong. Please try again.', + 'error_id' => 'Error ID: :id', + 'error_unavailable' => 'Service unavailable', + 'error_maintenance' => 'Maintenance in progress. We’ll be right back.', + 'error_help' => 'We’ll be right back.', + 'error_twitter' => 'Follow our Twitter account to be alerted when it’s up again.', + 'error_no_term' => 'There is no policy for this instance yet.', + + 'default_save_success' => 'Os dados foram guardados.', + + 'compliance_title' => 'Pedimos desculpa pela interrupção.', + 'compliance_desc' => 'We have changed our Terms of Use and Privacy Policy. By law we have to ask you to review them and accept them so you can continue to use your account.', + 'compliance_desc_end' => 'We don’t do anything nasty with your data or your account and we never will.', + 'compliance_terms' => 'Accept new terms and privacy policy', + + // Relationship types + // Yes, each relationship type has 8 strings associated with it. + // This is because we need to indicate the name of the relationship type, + // and also the name of the opposite side of this relationship (father/son), + // and then, the feminine version of the string. Finally, in some sentences + // in the UI, we need to include the name of the person we add the relationship + // to. + 'relationship_type_group_love' => 'Relações amorosas', + 'relationship_type_group_family' => 'Relações familiares', + 'relationship_type_group_friend' => 'Relações de amizade', + 'relationship_type_group_work' => 'Relações laborais', + 'relationship_type_group_other' => 'Other kind of relationships', + + 'relationship_type_partner' => 'companheiro', + 'relationship_type_partner_female' => 'companheira', + 'relationship_type_partner_male' => 'significant other', + 'relationship_type_partner_with_name' => ':name’s significant other', + 'relationship_type_partner_female_with_name' => ':name’s significant other', + 'relationship_type_partner_male_with_name' => ':name’s significant other', + + 'relationship_type_spouse' => 'spouse', + 'relationship_type_spouse_female' => 'wife', + 'relationship_type_spouse_male' => 'husband', + 'relationship_type_spouse_with_name' => ':name’s spouse', + 'relationship_type_spouse_female_with_name' => ':name’s wife', + 'relationship_type_spouse_male_with_name' => ':name’s husband', + + 'relationship_type_date' => 'date', + 'relationship_type_date_female' => 'date', + 'relationship_type_date_male' => 'date', + 'relationship_type_date_with_name' => ':name’s date', + 'relationship_type_date_female_with_name' => ':name’s date', + 'relationship_type_date_male_with_name' => ':name’s date', + + 'relationship_type_lover' => 'lover', + 'relationship_type_lover_female' => 'lover', + 'relationship_type_lover_male' => 'lover', + 'relationship_type_lover_with_name' => ':name’s lover', + 'relationship_type_lover_female_with_name' => ':name’s lover', + 'relationship_type_lover_male_with_name' => ':name’s lover', + + 'relationship_type_inlovewith' => 'in love with', + 'relationship_type_inlovewith_female' => 'in love with', + 'relationship_type_inlovewith_male' => 'in love with', + 'relationship_type_inlovewith_with_name' => 'someone :name is in love with', + 'relationship_type_inlovewith_female_with_name' => 'someone :name is in love with', + 'relationship_type_inlovewith_male_with_name' => 'someone :name is in love with', + + 'relationship_type_lovedby' => 'loved by', + 'relationship_type_lovedby_female' => 'loved by', + 'relationship_type_lovedby_male' => 'loved by', + 'relationship_type_lovedby_with_name' => ':name’s secret lover', + 'relationship_type_lovedby_female_with_name' => ':name’s secret lover', + 'relationship_type_lovedby_male_with_name' => ':name’s secret lover', + + 'relationship_type_ex' => 'ex-partner', + 'relationship_type_ex_female' => 'ex-girlfriend', + 'relationship_type_ex_male' => 'ex-boyfriend', + 'relationship_type_ex_with_name' => ':name’s ex-partner', + 'relationship_type_ex_female_with_name' => ':name’s ex-girlfriend', + 'relationship_type_ex_male_with_name' => ':name’s ex-boyfriend', + + 'relationship_type_parent' => 'parent', + 'relationship_type_parent_female' => 'mother', + 'relationship_type_parent_male' => 'father', + 'relationship_type_parent_with_name' => ':name’s parent', + 'relationship_type_parent_female_with_name' => ':name’s mother', + 'relationship_type_parent_male_with_name' => ':name’s father', + + 'relationship_type_child' => 'child', + 'relationship_type_child_female' => 'daughter', + 'relationship_type_child_male' => 'son', + 'relationship_type_child_with_name' => ':name’s child', + 'relationship_type_child_female_with_name' => ':name’s daughter', + 'relationship_type_child_male_with_name' => ':name’s son', + + 'relationship_type_stepparent' => 'step-parent', + 'relationship_type_stepparent_female' => 'stepmother', + 'relationship_type_stepparent_male' => 'stepfather', + 'relationship_type_stepparent_with_name' => ':name’s step-parent', + 'relationship_type_stepparent_female_with_name' => ':name’s stepmother', + 'relationship_type_stepparent_male_with_name' => ':name’s stepfather', + + 'relationship_type_stepchild' => 'stepchild', + 'relationship_type_stepchild_female' => 'stepdaughter', + 'relationship_type_stepchild_male' => 'stepson', + 'relationship_type_stepchild_with_name' => ':name’s stepchild', + 'relationship_type_stepchild_female_with_name' => ':name’s stepdaughter', + 'relationship_type_stepchild_male_with_name' => ':name’s stepson', + + 'relationship_type_sibling' => 'sibling', + 'relationship_type_sibling_female' => 'sister', + 'relationship_type_sibling_male' => 'brother', + 'relationship_type_sibling_with_name' => ':name’s sibling', + 'relationship_type_sibling_female_with_name' => ':name’s sister', + 'relationship_type_sibling_male_with_name' => ':name’s brother', + + 'relationship_type_grandparent' => 'grandparent', + 'relationship_type_grandparent_female' => 'grandmother', + 'relationship_type_grandparent_male' => 'grandfather', + 'relationship_type_grandparent_with_name' => ':name’s grandparent', + 'relationship_type_grandparent_female_with_name' => ':name’s grandmother', + 'relationship_type_grandparent_male_with_name' => ':name’s grandfather', + + 'relationship_type_grandchild' => 'grandchild', + 'relationship_type_grandchild_female' => 'granddaughter', + 'relationship_type_grandchild_male' => 'grandson', + 'relationship_type_grandchild_with_name' => ':name’s grandchild', + 'relationship_type_grandchild_female_with_name' => ':name’s granddauther', + 'relationship_type_grandchild_male_with_name' => ':name’s grandson', + + 'relationship_type_uncle' => 'uncle', + 'relationship_type_uncle_female' => 'aunt', + 'relationship_type_uncle_male' => 'uncle', + 'relationship_type_uncle_with_name' => ':name’s uncle', + 'relationship_type_uncle_female_with_name' => ':name’s aunt', + 'relationship_type_uncle_male_with_name' => ':name’s uncle', + + 'relationship_type_nephew' => 'nephew', + 'relationship_type_nephew_female' => 'niece', + 'relationship_type_nephew_male' => 'nephew', + 'relationship_type_nephew_with_name' => ':name’s nephew', + 'relationship_type_nephew_female_with_name' => ':name’s niece', + 'relationship_type_nephew_male_with_name' => ':name’s nephew', + + 'relationship_type_cousin' => 'cousin', + 'relationship_type_cousin_female' => 'cousin', + 'relationship_type_cousin_male' => 'cousin', + 'relationship_type_cousin_with_name' => ':name’s cousin', + 'relationship_type_cousin_female_with_name' => ':name’s cousin', + 'relationship_type_cousin_male_with_name' => ':name’s cousin', + + 'relationship_type_godfather' => 'godparent', + 'relationship_type_godfather_female' => 'godmother', + 'relationship_type_godfather_male' => 'godfather', + 'relationship_type_godfather_with_name' => ':name’s godparent', + 'relationship_type_godfather_female_with_name' => ':name’s godmother', + 'relationship_type_godfather_male_with_name' => ':name’s godfather', + + 'relationship_type_godson' => 'godchild', + 'relationship_type_godson_female' => 'goddaughter', + 'relationship_type_godson_male' => 'godson', + 'relationship_type_godson_with_name' => ':name’s godchild', + 'relationship_type_godson_female_with_name' => ':name’s goddaughter', + 'relationship_type_godson_male_with_name' => ':name’s godson', + + 'relationship_type_friend' => 'friend', + 'relationship_type_friend_female' => 'friend', + 'relationship_type_friend_male' => 'friend', + 'relationship_type_friend_with_name' => ':name’s friend', + 'relationship_type_friend_female_with_name' => ':name’s friend', + 'relationship_type_friend_male_with_name' => ':name’s friend', + + 'relationship_type_bestfriend' => 'best friend', + 'relationship_type_bestfriend_female' => 'best friend', + 'relationship_type_bestfriend_male' => 'best friend', + 'relationship_type_bestfriend_with_name' => ':name’s best friend', + 'relationship_type_bestfriend_female_with_name' => ':name’s best friend', + 'relationship_type_bestfriend_male_with_name' => ':name’s best friend', + + 'relationship_type_colleague' => 'colleague', + 'relationship_type_colleague_female' => 'colleague', + 'relationship_type_colleague_male' => 'colleague', + 'relationship_type_colleague_with_name' => ':name’s colleague', + 'relationship_type_colleague_female_with_name' => ':name’s colleague', + 'relationship_type_colleague_male_with_name' => ':name’s colleague', + + 'relationship_type_boss' => 'boss', + 'relationship_type_boss_female' => 'boss', + 'relationship_type_boss_male' => 'boss', + 'relationship_type_boss_with_name' => ':name’s boss', + 'relationship_type_boss_female_with_name' => ':name’s boss', + 'relationship_type_boss_male_with_name' => ':name’s boss', + + 'relationship_type_subordinate' => 'subordinate', + 'relationship_type_subordinate_female' => 'subordinate', + 'relationship_type_subordinate_male' => 'subordinate', + 'relationship_type_subordinate_with_name' => ':name’s subordinate', + 'relationship_type_subordinate_female_with_name' => ':name’s subordinate', + 'relationship_type_subordinate_male_with_name' => ':name’s subordinate', + + 'relationship_type_mentor' => 'mentor', + 'relationship_type_mentor_female' => 'mentor', + 'relationship_type_mentor_male' => 'mentor', + 'relationship_type_mentor_with_name' => ':name’s mentor', + 'relationship_type_mentor_female_with_name' => ':name’s mentor', + 'relationship_type_mentor_male_with_name' => ':name’s mentor', + + 'relationship_type_protege' => 'protégé', + 'relationship_type_protege_female' => 'protégé', + 'relationship_type_protege_male' => 'protégé', + 'relationship_type_protege_with_name' => ':name’s protégé', + 'relationship_type_protege_female_with_name' => ':name’s protégé', + 'relationship_type_protege_male_with_name' => ':name’s protégé', + + 'relationship_type_ex_husband' => 'ex-spouse', + 'relationship_type_ex_husband_female' => 'ex-wife', + 'relationship_type_ex_husband_male' => 'ex-husband', + 'relationship_type_ex_husband_with_name' => ':name’s ex-spouse', + 'relationship_type_ex_husband_female_with_name' => ':name’s ex-wife', + 'relationship_type_ex_husband_male_with_name' => ':name’s ex-husband', + + // emotions + 'emotion_primary_love' => 'Love', + 'emotion_primary_joy' => 'Joy', + 'emotion_primary_surprise' => 'Surprise', + 'emotion_primary_anger' => 'Anger', + 'emotion_primary_sadness' => 'Sadness', + 'emotion_primary_fear' => 'Fear', + + 'emotion_secondary_affection' => 'Affection', + 'emotion_secondary_lust' => 'Lust', + 'emotion_secondary_longing' => 'Longing', + 'emotion_secondary_cheerfulness' => 'Cheerfulness', + 'emotion_secondary_zest' => 'Zest', + 'emotion_secondary_contentment' => 'Contentment', + 'emotion_secondary_pride' => 'Pride', + 'emotion_secondary_optimism' => 'Optimism', + 'emotion_secondary_enthrallment' => 'Enthrallment', + 'emotion_secondary_relief' => 'Relief', + 'emotion_secondary_surprise' => 'Surprise', + 'emotion_secondary_irritation' => 'Irritation', + 'emotion_secondary_exasperation' => 'Exasperation', + 'emotion_secondary_rage' => 'Rage', + 'emotion_secondary_disgust' => 'Disgust', + 'emotion_secondary_envy' => 'Envy', + 'emotion_secondary_suffering' => 'Suffering', + 'emotion_secondary_sadness' => 'Sadness', + 'emotion_secondary_disappointment' => 'Disappointment', + 'emotion_secondary_shame' => 'Shame', + 'emotion_secondary_neglect' => 'Neglect', + 'emotion_secondary_sympathy' => 'Sympathy', + 'emotion_secondary_horror' => 'Horror', + 'emotion_secondary_nervousness' => 'Nervousness', + + 'emotion_adoration' => 'Adoration', + 'emotion_affection' => 'Affection', + 'emotion_love' => 'Love', + 'emotion_fondness' => 'Fondness', + 'emotion_liking' => 'Liking', + 'emotion_attraction' => 'Attraction', + 'emotion_caring' => 'Caring', + 'emotion_tenderness' => 'Tenderness', + 'emotion_compassion' => 'Compassion', + 'emotion_sentimentality' => 'Sentimentality', + 'emotion_arousal' => 'Arousal', + 'emotion_desire' => 'Desire', + 'emotion_lust' => 'Lust', + 'emotion_passion' => 'Passion', + 'emotion_infatuation' => 'Infatuation', + 'emotion_longing' => 'Longing', + 'emotion_amusement' => 'Amusement', + 'emotion_bliss' => 'Bliss', + 'emotion_cheerfulness' => 'Cheerfulness', + 'emotion_gaiety' => 'Gaiety', + 'emotion_glee' => 'Glee', + 'emotion_jolliness' => 'Jolliness', + 'emotion_joviality' => 'Joviality', + 'emotion_joy' => 'Joy', + 'emotion_delight' => 'Delight', + 'emotion_enjoyment' => 'Enjoyment', + 'emotion_gladness' => 'Gladness', + 'emotion_happiness' => 'Happiness', + 'emotion_jubilation' => 'Jubilation', + 'emotion_elation' => 'Elation', + 'emotion_satisfaction' => 'Satisfaction', + 'emotion_ecstasy' => 'Ecstasy', + 'emotion_euphoria' => 'Euphoria', + 'emotion_enthusiasm' => 'Enthusiasm', + 'emotion_zeal' => 'Zeal', + 'emotion_zest' => 'Zest', + 'emotion_excitement' => 'Excitement', + 'emotion_thrill' => 'Thrill', + 'emotion_exhilaration' => 'Exhilaration', + 'emotion_contentment' => 'Contentment', + 'emotion_pleasure' => 'Pleasure', + 'emotion_pride' => 'Pride', + 'emotion_eagerness' => 'Eagerness', + 'emotion_hope' => 'Hope', + 'emotion_optimism' => 'Optimism', + 'emotion_enthrallment' => 'Enthrallment', + 'emotion_rapture' => 'Rapture', + 'emotion_relief' => 'Relief', + 'emotion_amazement' => 'Amazement', + 'emotion_surprise' => 'Surprise', + 'emotion_astonishment' => 'Astonishment', + 'emotion_aggravation' => 'Aggravation', + 'emotion_irritation' => 'Irritation', + 'emotion_agitation' => 'Agitation', + 'emotion_annoyance' => 'Annoyance', + 'emotion_grouchiness' => 'Grouchiness', + 'emotion_grumpiness' => 'Grumpiness', + 'emotion_exasperation' => 'Exasperation', + 'emotion_frustration' => 'Frustration', + 'emotion_anger' => 'Anger', + 'emotion_rage' => 'Rage', + 'emotion_outrage' => 'Outrage', + 'emotion_fury' => 'Fury', + 'emotion_wrath' => 'Wrath', + 'emotion_hostility' => 'Hostility', + 'emotion_ferocity' => 'Ferocity', + 'emotion_bitterness' => 'Bitterness', + 'emotion_hate' => 'Hate', + 'emotion_loathing' => 'Loathing', + 'emotion_scorn' => 'Scorn', + 'emotion_spite' => 'Spite', + 'emotion_vengefulness' => 'Vengefulness', + 'emotion_dislike' => 'Dislike', + 'emotion_resentment' => 'Resentment', + 'emotion_disgust' => 'Disgust', + 'emotion_revulsion' => 'Revulsion', + 'emotion_contempt' => 'Contempt', + 'emotion_envy' => 'Envy', + 'emotion_jealousy' => 'Jealousy', + 'emotion_agony' => 'Agony', + 'emotion_suffering' => 'Suffering', + 'emotion_hurt' => 'Hurt', + 'emotion_anguish' => 'Anguish', + 'emotion_depression' => 'Depression', + 'emotion_despair' => 'Despair', + 'emotion_hopelessness' => 'Hopelessness', + 'emotion_gloom' => 'Gloom', + 'emotion_glumness' => 'Glumness', + 'emotion_sadness' => 'Sadness', + 'emotion_unhappiness' => 'Unhappiness', + 'emotion_grief' => 'Grief', + 'emotion_sorrow' => 'Sorrow', + 'emotion_woe' => 'Woe', + 'emotion_misery' => 'Misery', + 'emotion_melancholy' => 'Melancholy', + 'emotion_dismay' => 'Dismay', + 'emotion_disappointment' => 'Disappointment', + 'emotion_displeasure' => 'Displeasure', + 'emotion_guilt' => 'Guilt', + 'emotion_shame' => 'Shame', + 'emotion_regret' => 'Regret', + 'emotion_remorse' => 'Remorse', + 'emotion_alienation' => 'Alienation', + 'emotion_isolation' => 'Isolation', + 'emotion_neglect' => 'Neglect', + 'emotion_loneliness' => 'Loneliness', + 'emotion_rejection' => 'Rejection', + 'emotion_homesickness' => 'Homesickness', + 'emotion_defeat' => 'Defeat', + 'emotion_dejection' => 'Dejection', + 'emotion_insecurity' => 'Insecurity', + 'emotion_embarrassment' => 'Embarrassment', + 'emotion_humiliation' => 'Humiliation', + 'emotion_insult' => 'Insult', + 'emotion_pity' => 'Pity', + 'emotion_sympathy' => 'Sympathy', + 'emotion_alarm' => 'Alarm', + 'emotion_shock' => 'Shock', + 'emotion_fear' => 'Fear', + 'emotion_fright' => 'Fright', + 'emotion_horror' => 'Horror', + 'emotion_terror' => 'Terror', + 'emotion_panic' => 'Panic', + 'emotion_hysteria' => 'Hysteria', + 'emotion_mortification' => 'Mortification', + 'emotion_anxiety' => 'Anxiety', + 'emotion_nervousness' => 'Nervousness', + 'emotion_tenseness' => 'Tenseness', + 'emotion_uneasiness' => 'Uneasiness', + 'emotion_apprehension' => 'Apprehension', + 'emotion_worry' => 'Worry', + 'emotion_distress' => 'Distress', + 'emotion_dread' => 'Dread', + + // weather + 'weather_sunny' => 'Sunny', + 'weather_clear' => 'Clear', + 'weather_clear-day' => 'Clear', + 'weather_clear-night' => 'Clear night', + 'weather_light-drizzle' => 'Light drizzle', + 'weather_patchy-light-drizzle' => 'Patchy light drizzle', + 'weather_patchy-light-rain' => 'Patchy light rain', + 'weather_light-rain' => 'Light rain', + 'weather_moderate-rain-at-times' => 'Moderate rain at times', + 'weather_moderate-rain' => 'Moderate rain', + 'weather_patchy-rain-possible' => 'Patchy rain possible', + 'weather_heavy-rain-at-times' => 'Heavy rain at times', + 'weather_heavy-rain' => 'Heavy rain', + 'weather_light-freezing-rain' => 'Light freezing rain', + 'weather_moderate-or-heavy-freezing-rain' => 'Moderate or heavy freezing rain', + 'weather_light-sleet' => 'Light sleet', + 'weather_moderate-or-heavy-rain-shower' => 'Moderate or heavy rain shower', + 'weather_light-rain-shower' => 'Light rain shower', + 'weather_torrential-rain-shower' => 'Torrential rain shower', + 'weather_rain' => 'Rain', + 'weather_snow' => 'Snow', + 'weather_blowing-snow' => 'Blowing snow', + 'weather_patchy-light-snow' => 'Patchy light snow', + 'weather_light-snow' => 'Light snow', + 'weather_patchy-moderate-snow' => 'Patchy moderate snow', + 'weather_moderate-snow' => 'Moderate snow', + 'weather_patchy-heavy-snow' => 'Patchy heavy snow', + 'weather_heavy-snow' => 'Heavy snow', + 'weather_light-snow-showers' => 'Light snow showers', + 'weather_moderate-or-heavy-snow-showers' => 'Moderate or heavy snow showers', + 'weather_patchy-snow-possible' => 'Patchy snow possible', + 'weather_patchy-sleet-possible' => 'Patchy sleet possible', + 'weather_moderate-or-heavy-sleet' => 'Moderate or heavy sleet', + 'weather_light-sleet-showers' => 'Light sleet showers', + 'weather_moderate-or-heavy-sleet-showers' => 'Moderate or heavy sleet showers', + 'weather_sleet' => 'Sleet', + 'weather_wind' => 'Wind', + 'weather_fog' => 'Fog', + 'weather_freezing-fog' => 'Freezing fog', + 'weather_mist' => 'Mist', + 'weather_blizzard' => 'Blizzard', + 'weather_overcast' => 'Overcast', + 'weather_cloudy' => 'Cloudy', + 'weather_partly-cloudy-day' => 'Partly cloudy', + 'weather_partly-cloudy-night' => 'Partly cloudy', + 'weather_freezing-drizzle' => 'Freezing drizzle', + 'weather_heavy-freezing-drizzle' => 'Heavy freezing drizzle', + 'weather_patchy-freezing-drizzle-possible' => 'Patchy freezing drizzle possible', + 'weather_ice-pellets' => 'Ice pellets', + 'weather_light-showers-of-ice-pellets' => 'Light showers of ice pellets', + 'weather_moderate-or-heavy-showers-of-ice-pellets' => 'Moderate or heavy showers of ice pellets', + 'weather_thundery-outbreaks-possible' => 'Thundery outbreaks possible', + 'weather_patchy-light-rain-with-thunder' => 'Patchy light rain with thunder', + 'weather_moderate-or-heavy-rain-with-thunder' => 'Moderate or heavy rain with thunder', + 'weather_patchy-light-snow-with-thunder' => 'Patchy light snow with thunder', + 'weather_moderate-or-heavy-snow-with-thunder' => 'Moderate or heavy snow with thunder', + 'weather_current_temperature_celsius' => ':temperature °C', + 'weather_current_temperature_fahrenheit' => ':temperature °F', + 'weather_current_title' => 'Current weather', + + // dav + 'dav_contacts' => 'Contacts', + 'dav_contacts_description' => ':name’s contacts', + 'dav_birthdays' => 'Birthdays', + 'dav_birthdays_description' => ':name’s contact’s birthdays', + 'dav_tasks' => 'Tasks', + 'dav_tasks_description' => ':name’s tasks', + + // contact list + 'contact_list_avatar' => 'Avatar', + 'contact_list_name' => 'Contact', + 'contact_list_description' => 'Description', + +]; diff --git a/resources/lang/pt/auth.php b/resources/lang/pt/auth.php new file mode 100644 index 0000000..7301670 --- /dev/null +++ b/resources/lang/pt/auth.php @@ -0,0 +1,89 @@ + 'As informações de login não foram encontradas.', + 'throttle' => 'Muitas tentativas de login. Por favor tente novamente em :seconds segundos.', + 'not_authorized' => 'Você não está autorizado a executar esta ação', + 'signup_disabled' => 'Atualmente o registro está desativado', + 'signup_error' => 'An error occured trying to register the user', + 'back_homepage' => 'Voltar à página inicial', + 'mfa_auth_otp' => 'Authenticate with your two factor device', + 'mfa_auth_webauthn' => 'Authenticate with a security key (WebAuthn)', + '2fa_title' => 'Autenticação de dois fatores', + '2fa_wrong_validation' => 'Falha na autenticação de dois fatores.', + '2fa_one_time_password' => 'Two factor authentication code', + '2fa_recuperation_code' => 'Introduza um código de recuperação de dois fatores', + '2fa_one_time_or_recuperation' => 'Enter a two factor authentication code or a recovery code', + '2fa_otp_help' => 'Open up your two factor authentication mobile app and copy the code', + + 'login_to_account' => 'Login to your account', + 'login_with_recovery' => 'Login with a recovery code', + 'login_again' => 'Please login again to your account', + 'email' => 'Email', + 'password' => 'Password', + 'recovery' => 'Recovery code', + 'login' => 'Login', + 'button_remember' => 'Remember Me', + 'password_forget' => 'Forget your password?', + 'password_reset' => 'Reset your password', + 'use_recovery' => 'Or you can use a recovery code', + 'signup_no_account' => 'Don’t have an account?', + 'signup' => 'Sign up', + 'create_account' => 'Create the first account by signing up', + 'change_language_title' => 'Change language:', + 'change_language' => 'Change language to :lang', + + 'password_reset_title' => 'Reset Password', + 'password_reset_email' => 'E-Mail Address', + 'password_reset_send_link' => 'Send Password Reset Link', + 'password_reset_password' => 'Password', + 'password_reset_password_confirm' => 'Confirm Password', + 'password_reset_action' => 'Reset Password', + 'password_reset_email_content' => 'Click here to reset your password:', + + 'register_title_welcome' => 'Welcome to your newly installed Monica instance', + 'register_create_account' => 'You need to create an account to use Monica', + 'register_title_create' => 'Create your Monica account', + 'register_login' => 'Log in if you already have an account.', + 'register_email' => 'Enter a valid email address', + 'register_email_example' => 'you@home', + 'register_firstname' => 'First name', + 'register_firstname_example' => 'eg. John', + 'register_lastname' => 'Last name', + 'register_lastname_example' => 'eg. Doe', + 'register_password' => 'Password', + 'register_password_example' => 'Enter a secure password', + 'register_password_confirmation' => 'Password confirmation', + 'register_action' => 'Register', + 'register_policy' => 'Signing up signifies you’ve read and agree to our Privacy Policy and Terms of use.', + 'register_invitation_email' => 'For security purposes, please indicate the email of the person who’ve invited you to join this account. This information is provided in the invitation email.', + + 'confirmation_title' => 'Verify Your Email Address', + 'confirmation_fresh' => 'A fresh verification link has been sent to your email address.', + 'confirmation_check' => 'Before proceeding, please check your email for a verification link.', + 'confirmation_request_another' => 'If you did not receive the email click here to request another.', + + 'confirmation_again' => 'If you want to change your email address you can click here.', + 'email_change_current_email' => 'Current email address:', + 'email_change_title' => 'Change your email address', + 'email_change_new' => 'New email address', + 'email_changed' => 'Your email address has been changed. Check your mailbox to validate it.', +]; diff --git a/resources/lang/pt/changelog.php b/resources/lang/pt/changelog.php new file mode 100644 index 0000000..981b018 --- /dev/null +++ b/resources/lang/pt/changelog.php @@ -0,0 +1,12 @@ + 'Product changes', + 'note' => 'Note: unfortunately, this page is only in English.', +]; diff --git a/resources/lang/pt/dashboard.php b/resources/lang/pt/dashboard.php new file mode 100644 index 0000000..af729fa --- /dev/null +++ b/resources/lang/pt/dashboard.php @@ -0,0 +1,42 @@ + 'Seja bem-vindo à sua conta!', + 'dashboard_blank_description' => 'Com Monica pode organizar todas as suas interações com as pessoas que são importantes para si.', + 'dashboard_blank_cta' => 'Adicione o seu primeiro contacto', + 'dashboard_blank_illustration' => 'Illustration by Freepik', + + 'notes_title' => 'You don’t have any starred notes yet.', + + 'tab_recent_calls' => 'Chamadas recentes', + 'tab_favorite_notes' => 'Notas favoritas', + 'tab_calls_blank' => 'Você ainda não registou chamadas.', + 'tab_debts' => 'Debts', + 'tab_debts_blank' => 'You haven’t logged any debts yet.', + 'tab_tasks' => 'Tasks', + 'tab_tasks_blank' => 'You haven’t any tasks yet.', + + 'tasks_add_task_placeholder' => 'What is this task about?', + 'tasks_tab_your_contacts' => 'Tasks related to your contacts', + 'tasks_tab_your_tasks' => 'Your tasks', + 'tasks_add_note' => 'Press Enter to add the task.', + 'task_add_cta' => 'Add a task', + + 'debts_you_owe' => 'You owe', + + 'statistics_contacts' => 'Contatos', + 'statistics_activities' => 'Atividades', + 'statistics_gifts' => 'Presentes', + + 'reminders_next_months' => 'Events in the next 3 months', + 'reminders_none' => 'No reminders for this month.', + + 'product_changes' => 'Product changes', + 'product_view_details' => 'View details', +]; diff --git a/resources/lang/pt/format.php b/resources/lang/pt/format.php new file mode 100644 index 0000000..8e60df5 --- /dev/null +++ b/resources/lang/pt/format.php @@ -0,0 +1,36 @@ + 'd M Y H:i', + 'short_date_year' => 'd M Y', + 'short_date' => 'd M', + 'short_month' => 'M', + 'short_month_year' => 'M Y', + 'short_day' => 'D', + 'full_date_year' => 'd M Y', + 'full_month' => 'F', + 'full_month_year' => 'F Y', + 'full_hour' => 'H:i', + + 'short_text' => '{text}…', +]; diff --git a/resources/lang/pt/journal.php b/resources/lang/pt/journal.php new file mode 100644 index 0000000..5f1ad8c --- /dev/null +++ b/resources/lang/pt/journal.php @@ -0,0 +1,38 @@ + 'Como foi o seu dia? Você pode avaliá-lo uma vez por dia.', + 'journal_come_back' => 'Obrigado. Volte amanhã para avaliar o seu dia novamente.', + 'journal_description' => 'Nota: o diário agrega entradas manuais e entradas automáticas tais como Atividades feitas com seus contactos. Embora possa apagar manualmente as entradas de diário, no caso das Atividades terá que apagá-las na página de contacto.', + 'journal_add' => 'Adicionar um registro no diário', + 'journal_edit' => 'Edit a journal entry', + 'journal_empty' => 'Empty journal', + 'journal_created_at' => 'Created at {date}', + 'journal_created_automatically' => 'Criado automaticamente', + 'journal_entry_type_journal' => 'Entrada de diário', + 'journal_entry_type_activity' => 'Atividade', + 'journal_entry_rate' => 'Você avaliou o seu dia.', + 'journal_add_comment' => 'Care to add a comment (optional)?', + 'journal_show_comment' => 'Show comment', + 'entry_delete_success' => 'O registro no diário foi eliminada com sucesso.', + 'journal_add_title' => 'Título (Opcional)', + 'journal_add_date' => 'Date', + 'journal_add_post' => 'Registro', + 'journal_add_cta' => 'Salvar', + 'journal_blank_cta' => 'Adicione seu primeiro registro no diário', + 'journal_blank_description' => 'O diário permite que você escreva eventos que aconteceram com você, para te lembrar.', + 'delete_confirmation' => 'Tem certeza que quer apagar esta entrada de diário?', + 'apply_filter' => 'Apply filter', + 'start_date' => 'Start Date', + 'end_date' => 'End Date', + 'per_page' => 'Per Page', + 'sort_order' => 'Sort By Created At', + 'ascending' => 'Ascending', + 'descending' => 'Descending', +]; diff --git a/resources/lang/pt/logs.php b/resources/lang/pt/logs.php new file mode 100644 index 0000000..7b6654b --- /dev/null +++ b/resources/lang/pt/logs.php @@ -0,0 +1,29 @@ + 'Created the contact.', + 'settings_log_contact_created_with_name' => 'Added :name as a contact.', + + // contat description update + 'contact_log_contact_description_updated' => 'Updated the description.', + 'settings_log_contact_description_updated_with_name' => 'Updated the description of :name.', + + // contact description clear + 'contact_log_contact_description_cleared' => 'Cleared the description.', + 'settings_log_contact_description_cleared_with_name' => 'Cleared the description of :name.', + + // contact work information update + 'contact_log_contact_work_updated' => 'Updated work information.', + 'settings_log_contact_work_updated_with_name' => 'Updated work information of :name.', + + // company created + 'settings_log_company_created' => 'Created a company called :name.', +]; diff --git a/resources/lang/pt/mail.php b/resources/lang/pt/mail.php new file mode 100644 index 0000000..fdc001c --- /dev/null +++ b/resources/lang/pt/mail.php @@ -0,0 +1,53 @@ + 'Lembrete para :contact', + 'greetings' => 'Olá :username', + 'want_reminded_of' => 'You wanted to be reminded of :reason', + 'for' => 'Para: :name', + 'comment' => 'Comment: :comment', + 'footer_contact_info' => 'Adicionar, visualizar, completar e alterar informações sobre este contato:', + 'footer_contact_info2' => 'See :name’s profile', + 'footer_contact_info2_link' => 'See :name’s profile: :url', + + 'notification_subject_line' => 'Você tem um evento futuro', + 'notification_description' => 'Em :count dias (em :date), acontecerá o evento seguinte:', + + 'stay_in_touch_subject_line' => 'Stay in touch with :name', + 'stay_in_touch_subject_description' => 'You asked to be reminded to stay in touch with :name every :frequency day.|You asked to be reminded to stay in touch with :name every :frequency days.', + + 'notifications_whoops' => 'Oops!', + 'notifications_hello' => 'Olá!', + 'notifications_regards' => 'Regards', + 'notifications_footer' => 'If you’re having trouble clicking the ":actionText" button, copy and paste the URL below into your web browser: [:actionURL](:actionURL)', + 'notifications_rights' => 'Todos os direitos reservados', + + 'confirmation_email_title' => 'Monica - Verificação de e-mail', + 'confirmation_email_intro'=> 'To validate your email click on the button below', + 'confirmation_email_button' => 'Verify email address', + 'confirmation_email_bottom' => 'If you did not create an account, no further action is required.', + + 'password_reset_title' => 'Monica – Reset Password Notification', + 'password_reset_intro' => 'You are receiving this email because we received a password reset request for your account.', + 'password_reset_button' => 'Reset Password', + 'password_reset_expiration' => 'This password reset link will expire in :count minutes.', + 'password_reset_bottom' => 'If you did not request a password reset, no further action is required.', + + 'invitation_title' => 'Monica – You are invited by :name', + 'invitation_intro' => 'You’ve been invited by :name (:email) to use Monica, a nice Personal Relationship Management tool.', + 'invitation_link' => 'To accept the invitation, click on the link below:', + 'invitation_button' => 'Accept invitation', + 'invitation_expiration' => 'This link will expire in :count days.', + + 'export_title' => 'Your export is ready', + 'export_description' => 'You requested a data export on :date. It is now ready to download.', + 'export_download' => 'Download export', + +]; diff --git a/resources/lang/pt/pagination.php b/resources/lang/pt/pagination.php new file mode 100644 index 0000000..27031f5 --- /dev/null +++ b/resources/lang/pt/pagination.php @@ -0,0 +1,25 @@ + '❮ Anterior', + 'next' => 'Próxima ❯', + +]; diff --git a/resources/lang/pt/passwords.php b/resources/lang/pt/passwords.php new file mode 100644 index 0000000..8e6e50c --- /dev/null +++ b/resources/lang/pt/passwords.php @@ -0,0 +1,30 @@ + 'Sua senha foi redefinida!', + 'sent' => 'O link para redefinição de senha foi enviado para o seu e-mail.', + 'token' => 'Token para recuperação de senha inválido.', + 'user' => 'O link para redefinição de senha foi enviado para o seu e-mail.', + 'changed' => 'Password changed successfully.', + 'invalid' => 'A senha que introduziu não está correta.', + 'throttled' => 'Please wait before retrying.', + +]; diff --git a/resources/lang/pt/people.php b/resources/lang/pt/people.php new file mode 100644 index 0000000..086462e --- /dev/null +++ b/resources/lang/pt/people.php @@ -0,0 +1,539 @@ + 'Contacto não encontrado', + 'people_list_number_kids' => ':count criança|:count crianças', + 'people_list_last_updated' => 'Última consulta:', + 'people_list_number_reminders' => ':count lembrete|:count lembretes', + 'people_list_blank_title' => 'Você ainda não tem ninguém em sua conta', + 'people_list_blank_cta' => 'Adicionar uma pessoa', + 'people_list_sort' => 'Ordenar', + 'people_list_stats' => ':count contacto|:count contactos', + 'people_list_firstnameAZ' => 'Classificar por primeiro nome A → Z', + 'people_list_firstnameZA' => 'Classificar por primeiro nome Z → A', + 'people_list_lastnameAZ' => 'Classificar por sobrenome A → Z', + 'people_list_lastnameZA' => 'Classificar por sobrenome Z → A', + 'people_list_lastactivitydateNewtoOld' => 'Ordenar por data da última atividade, mais recente para mais antigo', + 'people_list_lastactivitydateOldtoNew' => 'Ordenar por data da última atividade, mais antigo para mais recente', + 'people_list_filter_tag' => 'A mostrar todos os contactos etiquetados com', + 'people_list_clear_filter' => 'Limpar filtro', + 'people_list_contacts_per_tags' => ':count contacto|:count contactos', + 'people_list_show_dead' => 'Show deceased people (:count)', + 'people_list_hide_dead' => 'Hide deceased people (:count)', + 'people_search' => 'Search your contacts…', + 'people_search_no_results' => 'No results found', + 'people_search_next' => 'Next', + 'people_search_prev' => 'Previous', + 'people_search_rows_per_page' => 'Rows per page', + 'people_search_of' => 'of', + 'people_search_page' => 'Page', + 'people_search_all' => 'All', + 'people_add_new' => 'Add new person', + 'people_list_account_usage' => 'Consumo da sua conta: :current/:limit contactos', + 'people_list_account_upgrade_title' => 'Upgrade your account to unlock it to its full potential.', + 'people_list_account_upgrade_cta' => 'Upgrade now', + 'people_list_untagged' => 'View untagged contacts', + 'people_list_filter_untag' => 'Showing all untagged contacts', + 'archived_contact_readonly' => 'Archived contact can’t be edited, please unarchive it first.', + + // people add + 'people_add_title' => 'Adicione uma nova pessoa', + 'people_add_missing' => 'No person found – add a new one now', + 'people_add_firstname' => 'Primeiro nome', + 'people_add_middlename' => 'Middle name (optional)', + 'people_add_lastname' => 'Last name (optional)', + 'people_add_email' => 'Email (optional)', + 'people_add_nickname' => 'Nickname (optional)', + 'people_add_cta' => 'Adicionar essa pessoa', + 'people_save_and_add_another_cta' => 'Enviar e adicionar outra pessoa', + 'people_add_success' => ':name foi criado com sucesso', + 'people_add_gender' => 'Gênero', + 'people_delete_success' => 'O contato foi excluído', + 'people_delete_message' => 'Delete contact', + 'people_delete_confirmation' => 'Are you sure you want to delete :name’s contact? Deletion is immediate and permanent.', + 'people_add_birthday_reminder' => 'Desejar feliz aniversário a :name', + 'people_add_birthday_reminder_deceased' => 'On this date, :name would have celebrated their birthday', + 'people_add_import' => 'Deseja importar os seus contatos?', + 'people_edit_email_error' => 'Já existe um contacto na sua conta com este endereço de e-mail. Por favor escolha outro.', + 'people_export' => 'Export as vCard', + 'people_add_reminder_for_birthday' => 'Create an annual birthday reminder', + + // show + 'section_contact_information' => 'Informações de contacto', + 'section_personal_activities' => 'Atividades', + 'section_personal_reminders' => 'Lembretes', + 'section_personal_tasks' => 'Tarefas', + 'section_personal_gifts' => 'Presentes', + 'section_personal_notes' => 'Notes', + + // archived contacts + 'list_link_to_active_contacts' => 'You are viewing archived contacts. See the list of active contacts instead.', + 'list_link_to_archived_contacts' => 'List of archived contacts', + + // Header + 'me' => 'This is you', + 'edit_contact_information' => 'Editar informação do contato', + 'contact_archive' => 'Archive contact', + 'contact_unarchive' => 'Unarchive contact', + 'contact_archive_help' => 'Archived contacts are not be shown on the contact list, but still appear in search results.', + 'call_button' => 'Registar uma chamada', + 'set_favorite' => 'Favorite contacts are placed at the top of the contact list', + + // Stay in touch + 'stay_in_touch' => 'Stay in touch', + 'stay_in_touch_frequency' => 'Stay in touch every day|Stay in touch every {count} days', + 'stay_in_touch_next_date' => 'Next due: {date}', + 'stay_in_touch_invalid' => 'The frequency must be a number greater than 0.', + 'stay_in_touch_premium' => 'You need to upgrade your account to make use of this feature', + 'stay_in_touch_modal_title' => 'Stay in touch', + 'stay_in_touch_modal_desc' => 'We can remind you by email to keep in touch with {firstname} at a regular interval.', + 'stay_in_touch_modal_label' => 'Send me an email every… {count} day|Send me an email every… {count} days', + + // Calls + 'modal_call_title' => 'Registar uma chamada', + 'modal_call_comment' => 'Sobre o que falaram? (opcional)', + 'modal_call_exact_date' => 'O telefonema aconteceu em', + 'modal_call_who_called' => 'Who called?', + 'modal_call_emotion' => 'Do you want to log how you felt during this call? (optional)', + 'calls_add_success' => 'O telefonema foi guardado.', + 'call_delete_confirmation' => 'Tem certeza que deseja eliminar esta chamada?', + 'call_delete_success' => 'A chamada foi eliminada com sucesso', + 'call_title' => 'Chamadas telefónicas', + 'call_empty_comment' => 'Sem detalhes', + 'call_blank_title' => 'Keep track of the phone calls you’ve done with {name}', + 'call_blank_desc' => 'You called {name}', + 'call_you_called' => 'You called', + 'call_he_called' => '{name} called', + 'call_emotions' => 'Emotions:', + + // Conversation + 'conversation_blank' => 'Record conversations you have with :name on social media, SMS…', + 'conversation_delete_link' => 'Delete the conversation', + 'conversation_edit_title' => 'Edit conversation', + 'conversation_edit_delete' => 'Are you sure you want to delete this conversation? Deletion is permanent.', + 'conversation_add_success' => 'The conversation has been successfully added.', + 'conversation_edit_success' => 'The conversation has been successfully updated.', + 'conversation_delete_success' => 'The conversation has been successfully deleted.', + 'conversation_add_title' => 'Record a new conversation', + 'conversation_add_when' => 'When did you have this conversation?', + 'conversation_add_who_wrote' => 'Who sent this message?', + 'conversation_add_how' => 'How did you communicate?', + 'conversation_add_you' => 'You', + 'conversation_add_content' => 'Write down what was said', + 'conversation_add_what_was_said' => 'What did you say?', + 'conversation_add_another' => 'Add another message', + 'conversation_add_error' => 'You must add at least one message.', + 'conversation_list_table_messages' => 'Messages', + 'conversation_list_table_content' => 'Partial content (last message)', + 'conversation_list_title' => 'Conversations', + 'conversation_list_cta' => 'Log conversation', + + // age - birthday + 'birthdate_not_set' => 'Birthday is not set', + 'age_approximate_in_years' => 'por volta de :age anos de idade', + 'age_exact_in_years' => ':age anos de idade', + 'age_exact_birthdate' => 'nascido :date', + + // Last called + 'last_called' => 'Last called: :date', + 'last_talked_to' => 'Last called: {date}', + 'last_called_empty' => 'Last called: unknown', + 'last_activity_date' => 'Last activity together: :date', + 'last_activity_date_empty' => 'Last activity together: unknown', + + // additional information + 'information_edit_success' => 'O perfil foi atualizado com sucesso', + 'information_edit_title' => 'Editar informações pessoais para :name', + 'information_edit_max_size' => 'Max :size Kb.', + 'information_edit_max_size2' => 'Max {size} Kb.', + 'information_edit_firstname' => 'Primeiro nome', + 'information_edit_lastname' => 'Last name (optional)', + 'information_edit_description' => 'Description (optional)', + 'information_edit_description_help' => 'Used on the contact list to add some context, if necessary.', + 'information_edit_unknown' => 'Eu não sei a idade desta pessoa', + 'information_edit_probably' => 'This person is probably…', + 'information_edit_not_year' => 'I know the day and month of this person’s birthday, but not the year…', + 'information_edit_exact' => 'I know this person’s exact birthday…', + 'information_edit_birthdate_label' => 'Birthday', + 'information_no_work_defined' => 'Nenhuma informação profissional definida', + 'information_work_at' => 'em :company', + 'work_add_cta' => 'Atualizar informação de trabalho', + 'work_edit_success' => 'Work information updated', + 'work_edit_title' => 'Atualizar a informação profissional de :name', + 'work_edit_job' => 'Cargo (opcional)', + 'work_edit_company' => 'Empresa (opcional)', + 'work_information' => 'Work information', + + // food preferences + 'food_preferences_add_success' => 'As preferências de alimentos foram salvas', + 'food_preferences_edit_description' => 'Talvez :firstname ou alguém na família de :family tenha uma alergia. Ou não gosta de uma garrafa específica de vinho. Indique-os aqui para que você lembre-se da próxima vez que você os convide para o jantar', + 'food_preferences_edit_description_no_last_name' => 'Talvez :firstname tenha uma alergia. Ou não gosta de uma garrafa específica de vinho. Indique-os aqui para que você lembre-se da próxima vez que você os convide para o jantar', + 'food_preferences_edit_title' => 'Indique preferências de alimentos', + 'food_preferences_edit_cta' => 'Guardar preferências de alimentos', + 'food_preferences_title' => 'Preferências alimentares', + 'food_preferences_cta' => 'Adicione preferências de alimentos', + + // reminders + 'reminders_blank_title' => 'Há algo sobre o qual você quer se lembrar :name?', + 'reminders_blank_add_activity' => 'Adicionar um lembrete', + 'reminders_add_title' => 'Sobre o que você gostaria de lembrar sobre :name?', + 'reminders_add_description' => 'Please remind me to…', + 'reminders_add_next_time' => 'Quando é a próxima vez que você gostaria de ser lembrado sobre isso?', + 'reminders_add_once' => 'Lembre-me sobre isso apenas uma vez', + 'reminders_add_recurrent' => 'Lembre-me sobre isso a todo momento', + 'reminders_add_starting_from' => 'começar a partir da data especificada acima', + 'reminders_add_cta' => 'Adicionar lembrete', + 'reminders_edit_update_cta' => 'Atualizar lembrete', + 'reminders_add_error_custom_text' => 'Você precisa indicar um texto para esse lembrete', + 'reminders_create_success' => 'O lembrete foi adicionado com sucesso', + 'reminders_delete_success' => 'O lembrete foi excluído com sucesso', + 'reminders_update_success' => 'O lembrete foi atualizado com sucesso', + 'reminders_add_optional_comment' => 'Optional comment', + + 'reminder_frequency_day' => 'todos os dias|a cada :number dias', + 'reminder_frequency_week' => 'toda semana|cada :number semanas', + 'reminder_frequency_month' => 'todo month|cada :number mêses', + 'reminder_frequency_year' => 'todo year|cada :number anos', + 'reminder_frequency_one_time' => 'em :date', + 'reminders_delete_confirmation' => 'em certeza de que deseja excluir esse lembrete?', + 'reminders_delete_cta' => 'Deletar', + 'reminders_next_expected_date' => 'em', + 'reminders_cta' => 'Adicionar um lembrete', + 'reminders_description' => 'We will send an email for each one of the reminders below. Reminders are sent every morning the day events will happen. Reminders automatically added for birthdays can not be deleted. If you want to change those dates, edit the birthday of the contacts.', + 'reminders_one_time' => 'Uma vez', + 'reminders_type_week' => 'semana', + 'reminders_type_month' => 'mês', + 'reminders_type_year' => 'ano', + 'reminders_birthday' => 'Birthdate of :name', + 'reminders_free_plan_warning' => 'You are on the Free plan. No emails are sent on this plan. To receive your reminders by email, upgrade your account.', + + // relationships + 'relationship_form_add' => 'Add a new relationship', + 'relationship_form_edit' => 'Edit an existing relationship', + 'relationship_form_is_with' => 'This person is…', + 'relationship_form_is_with_name' => ':name is…', + 'relationship_form_add_choice' => 'Who is the relationship with?', + 'relationship_form_create_contact' => 'Add a new person', + 'relationship_form_associate_contact' => 'An existing contact', + 'relationship_form_associate_dropdown' => 'Search and select an existing contact from the dropdown below', + 'relationship_form_associate_dropdown_placeholder' => 'Search and select an existing contact', + 'relationship_form_also_create_contact' => 'Create a Contact entry for this person.', + 'relationship_form_add_description' => 'This will let you treat this person like any other contact.', + 'relationship_form_add_no_existing_contact' => 'You don’t have any contacts who can be related to :name at the moment.', + 'relationship_delete_confirmation' => 'Are you sure you want to delete this relationship? Deletion is permanent.', + 'relationship_unlink_confirmation' => 'Are you sure you want to delete this relationship? This person will not be deleted – only the relationship between the two.', + 'relationship_form_add_success' => 'The relationship has been successfully set.', + 'relationship_form_deletion_success' => 'The relationship has been deleted.', + + // tasks + 'tasks_title' => 'Tasks', + 'tasks_blank_title' => 'You don’t have any tasks yet.', + 'tasks_form_title' => 'Título', + 'tasks_form_description' => 'Description (optional)', + 'tasks_add_task' => 'Adicionar uma tarefa', + 'tasks_delete_success' => 'A tarefa foi excluída com sucesso', + 'tasks_complete_success' => 'O status da tarefa foi alterado com sucesso', + + // activities + 'activity_title' => 'Atividades', + 'activity_type_category_simple_activities' => 'Simple activities', + 'activity_type_category_sport' => 'Sport', + 'activity_type_category_food' => 'Food', + 'activity_type_category_cultural_activities' => 'Cultural activities', + 'activity_type_just_hung_out' => 'apenas sai', + 'activity_type_watched_movie_at_home' => 'assisti um filme em casa', + 'activity_type_talked_at_home' => 'apenas fiquei em casa', + 'activity_type_did_sport_activities_together' => 'played a sport together', + 'activity_type_ate_at_his_place' => 'ate at their place', + 'activity_type_went_bar' => 'fui para um bar', + 'activity_type_ate_at_home' => 'comi em casa', + 'activity_type_picnicked' => 'picnicked', + 'activity_type_ate_restaurant' => 'comi em um restaurante', + 'activity_type_went_theater' => 'fui a um teatro', + 'activity_type_went_concert' => 'fui a um concerto', + 'activity_type_went_play' => 'fui jogar', + 'activity_type_went_museum' => 'fui a um museu', + 'activities_add_activity' => 'Adicionar atividade', + 'activities_add_more_details' => 'Add more details', + 'activities_add_emotions' => 'Add emotions', + 'activities_add_category' => 'Indicate a category', + 'activities_add_participants_cta' => 'Add participants', + 'activities_item_information' => ':Activity. Aconteceu em :date', + 'activities_add_title' => 'What did you do with {name}?', + 'activities_summary' => 'Descreva o que você fez', + 'activities_add_pick_activity' => 'Would you like to categorize this activity? You don’t have to, but it will give you statistics later on (optional)', + 'activities_add_date_occured' => 'The activity happened on…', + 'activities_add_participants' => 'Who, apart from {name}, participated in this activity? (optional)', + 'activities_add_emotions_title' => 'Do you want to log how you felt during this activity? (optional)', + 'activities_blank_title' => 'Keep track of what you’ve done with {name} in the past, and what you’ve talked about', + 'activities_blank_add_activity' => 'Adicionar uma atividade', + 'activities_add_success' => 'A atividade foi adicionada com sucesso', + 'activities_add_error' => 'Error when adding the activity', + 'activities_update_success' => 'A atividade foi atualizada com sucesso', + 'activities_delete_success' => 'A atividade foi excluída com sucesso', + 'activities_who_was_involved' => 'Quem estava envolvido?', + 'activities_activity' => 'Activity Category', + 'activities_view_activities_report' => 'View activities report', + 'activities_profile_title' => 'Activities report between :name and you', + 'activities_profile_subtitle' => 'You’ve logged :total_activities activity with :name in total and :activities_last_twelve_months in the last 12 months so far.|You’ve logged :total_activities activities with :name in total and :activities_last_twelve_months in the last 12 months so far.', + 'activities_profile_year_summary_activity_types' => 'Here is a breakdown of the type of activities you’ve done together in :year', + 'activities_profile_year_summary' => 'Here is what you two have done in :year', + 'activities_profile_number_occurences' => ':value activity|:value activities', + 'activities_list_participants' => 'Participants ({total}):', + 'activities_list_emotions' => 'Emotions felt:', + 'activities_list_date' => 'Happened on', + 'activities_list_category' => 'Category:', + + // notes + 'notes_create_success' => 'A nota foi adicionada com sucesso', + 'notes_update_success' => 'A nota foi guardada com sucesso', + 'notes_delete_success' => 'A nota foi excluída com sucesso', + 'notes_add_cta' => 'Adicionar nota', + 'notes_favorite' => 'Add/remove from favorites', + 'notes_delete_title' => 'Eliminar nota', + 'notes_delete_confirmation' => 'Tem certeza de que deseja excluir esta anotação? A exclusão é permanente', + + // gifts + 'gifts_title' => 'Gifts', + 'gifts_add_success' => 'O presente foi adicionado com sucesso', + 'gifts_delete_success' => 'O presente foi excluído com sucesso', + 'gifts_delete_confirmation' => 'Tem certeza de que deseja excluir esse presente?', + 'gifts_add_gift' => 'Adicionar um presente', + 'gifts_link' => 'Ligar', + 'gifts_for' => 'For: {name}', + 'gifts_delete_cta' => 'Deletar', + 'gifts_add_title' => 'Gerenciamento de presentes para :name', + 'gifts_add_gift_idea' => 'Ideia de presente', + 'gifts_add_gift_already_offered' => 'Presente já oferecido', + 'gifts_add_gift_received' => 'Gift received', + 'gifts_add_gift_title' => 'O que é esse presente?', + 'gifts_add_gift_name' => 'Gift name', + 'gifts_add_link' => 'Ligar com o site (Opcional)', + 'gifts_add_value' => 'Valor (Opcional)', + 'gifts_add_comment' => 'Comentário (Opcional)', + 'gifts_add_recipient' => 'Recipient (optional)', + 'gifts_add_recipient_field' => 'Recipient', + 'gifts_add_photo' => 'Photo (optional)', + 'gifts_add_photo_title' => 'Add a photo for this gift', + 'gifts_add_someone' => 'This gift is for someone in {name}’s family in particular', + 'gifts_delete_title' => 'Delete a gift', + 'gifts_ideas' => 'Gift ideas', + 'gifts_offered' => 'Gifts given', + 'gifts_offered_as_an_idea' => 'Mark as an idea', + 'gifts_received' => 'Gifts received', + 'gifts_view_comment' => 'Ver comentário', + 'gifts_mark_offered' => 'Mark as given', + 'gifts_update_success' => 'The gift has been updated successfully', + 'gifts_add_date' => 'Date (optional)', + + // debts + 'debt_delete_confirmation' => 'Tem certeza de que deseja excluir esta dívida?', + 'debt_delete_success' => 'A dívida foi excluída com sucesso', + 'debt_add_success' => 'A dívida foi adicionada com sucesso', + 'debt_title' => 'Dívidas', + 'debt_add_cta' => 'Adicionar dívida', + 'debt_you_owe' => 'Você deve :amount', + 'debt_they_owe' => ':name te deve :amount', + 'debt_add_title' => 'Debt management', + 'debt_add_you_owe' => 'Você deve a :name', + 'debt_add_they_owe' => ':name te deve', + 'debt_add_amount' => 'a soma de', + 'debt_add_reason' => 'Pelo seguinte motivo (Opcional)', + 'debt_add_add_cta' => 'Adicionar dívida', + 'debt_edit_update_cta' => 'Update debt', + 'debt_edit_success' => 'The debt has been updated successfully', + 'debts_blank_title' => 'Manage debts you owe to :name or :name owes you', + + // tags + 'tag_edit' => 'Edit tag', + 'tag_add' => 'Add tags', + 'tag_add_search' => 'Add or search tags', + 'tag_no_tags' => 'No tags yet', + + // Introductions + 'introductions_sidebar_title' => 'How you met', + 'introductions_blank_cta' => 'Indicate how you met :name', + 'introductions_title_edit' => 'How did you meet :name?', + 'introductions_additional_info' => 'Explain how and where you met', + 'introductions_edit_met_through' => 'Has someone introduced you to this person?', + 'introductions_no_met_through' => 'No one', + 'introductions_first_met_date' => 'Date you met', + 'introductions_no_first_met_date' => 'I don’t know the date we met', + 'introductions_first_met_date_known' => 'This is the date we met', + 'introductions_add_reminder' => 'Add a reminder to celebrate this encounter on the anniversary this event happened', + 'introductions_update_success' => 'You’ve successfully updated the information about how you met this person', + 'introductions_met_through' => 'Met through :name', + 'introductions_met_date' => 'Met on :date', + 'introductions_reminder_title' => 'Anniversary of the day you first met', + + // Deceased + 'deceased_reminder_title' => 'Anniversary of the death of :name', + 'deceased_mark_person_deceased' => 'Mark this as deceased', + 'deceased_know_date' => 'I know the date that this person died', + 'deceased_add_reminder' => 'Add a reminder for this date', + 'deceased_label' => 'Deceased', + 'deceased_date_label' => 'Deceased date', + 'deceased_label_with_date' => 'Deceased on :date', + 'deceased_age' => 'Age at death', + + // Contact information + 'contact_info_title' => 'Contact information', + 'contact_info_form_content' => 'Content', + 'contact_info_form_contact_type' => 'Contact type', + 'contact_info_form_personalize' => 'Personalize', + 'contact_info_address' => 'Lives in', + + // Addresses + 'contact_address_title' => 'Addresses', + 'contact_address_form_name' => 'Label (optional)', + 'contact_address_form_street' => 'Street (optional)', + 'contact_address_form_city' => 'City (optional)', + 'contact_address_form_province' => 'Province (optional)', + 'contact_address_form_postal_code' => 'Postal code (optional)', + 'contact_address_form_country' => 'Country (optional)', + 'contact_address_form_latitude' => 'Latitude (numbers only) (optional)', + 'contact_address_form_longitude' => 'Longitude (numbers only) (optional)', + + // Pets + 'pets_kind' => 'Kind of pet', + 'pets_name' => 'Name (optional)', + 'pets_create_success' => 'The pet has been successfully added', + 'pets_update_success' => 'The pet has been updated', + 'pets_delete_success' => 'The pet has been deleted', + 'pets_title' => 'Pets', + 'pets_reptile' => 'Réptil', + 'pets_bird' => 'Pássaro', + 'pets_cat' => 'Gato', + 'pets_dog' => 'Cão', + 'pets_fish' => 'Peixe', + 'pets_hamster' => 'Hamster', + 'pets_horse' => 'Cavalo', + 'pets_rabbit' => 'Coelho', + 'pets_rat' => 'Rato', + 'pets_small_animal' => 'Animal pequeno', + 'pets_other' => 'Outro', + + // life events + 'life_event_list_tab_life_events' => 'Life events', + 'life_event_list_tab_other' => 'Notes, reminders, …', + 'life_event_list_title' => 'Life events', + 'life_event_blank' => 'Log what happens to the life of {name} for your future reference.', + 'life_event_list_cta' => 'Add life event', + 'life_event_create_category' => 'All categories', + 'life_event_create_life_event' => 'Add life event', + 'life_event_create_default_title' => 'Title (optional)', + 'life_event_create_default_story' => 'Story (optional)', + 'life_event_create_date' => 'You do not need to indicate a month or a day – only the year is mandatory.', + 'life_event_create_default_description' => 'Add information about what you know', + 'life_event_create_add_yearly_reminder' => 'Add a yearly reminder for this event', + 'life_event_create_success' => 'The life event has been added', + 'life_event_delete_title' => 'Delete a life event', + 'life_event_delete_description' => 'Are you sure you want to delete this life event? Deletion is permanent.', + 'life_event_delete_success' => 'The life event has been deleted', + 'life_event_date_it_happened' => 'Date it happened', + 'life_event_category_work_education' => 'Work & education', + 'life_event_category_family_relationships' => 'Family & relationships', + 'life_event_category_home_living' => 'Home & living', + 'life_event_category_health_wellness' => 'Health & wellness', + 'life_event_category_travel_experiences' => 'Travel & experiences', + 'life_event_sentence_new_job' => 'Started a new job', + 'life_event_sentence_retirement' => 'Retired', + 'life_event_sentence_new_school' => 'Started school', + 'life_event_sentence_study_abroad' => 'Studied abroad', + 'life_event_sentence_volunteer_work' => 'Started volunteering', + 'life_event_sentence_published_book_or_paper' => 'Published a paper', + 'life_event_sentence_military_service' => 'Started military service', + 'life_event_sentence_new_relationship' => 'Started a relationship', + 'life_event_sentence_engagement' => 'Got engaged', + 'life_event_sentence_marriage' => 'Got married', + 'life_event_sentence_anniversary' => 'Anniversary', + 'life_event_sentence_expecting_a_baby' => 'Expects a baby', + 'life_event_sentence_new_child' => 'Had a child', + 'life_event_sentence_new_family_member' => 'Added a family member', + 'life_event_sentence_new_pet' => 'Got a pet', + 'life_event_sentence_end_of_relationship' => 'Ended a relationship', + 'life_event_sentence_loss_of_a_loved_one' => 'Lost a loved one', + 'life_event_sentence_moved' => 'Moved', + 'life_event_sentence_bought_a_home' => 'Bought a home', + 'life_event_sentence_home_improvement' => 'Made a home improvement', + 'life_event_sentence_holidays' => 'Went on holidays', + 'life_event_sentence_new_vehicle' => 'Got a new vehicle', + 'life_event_sentence_new_roommate' => 'Got a roommate', + 'life_event_sentence_overcame_an_illness' => 'Overcame an illness', + 'life_event_sentence_quit_a_habit' => 'Quit a habit', + 'life_event_sentence_new_eating_habits' => 'Started new eating habits', + 'life_event_sentence_weight_loss' => 'Lost weight', + 'life_event_sentence_wear_glass_or_contact' => 'Started to wear glass or contact lenses', + 'life_event_sentence_broken_bone' => 'Broke a bone', + 'life_event_sentence_removed_braces' => 'Removed braces', + 'life_event_sentence_surgery' => 'Had surgery', + 'life_event_sentence_dentist' => 'Went to the dentist', + 'life_event_sentence_new_sport' => 'Started a sport', + 'life_event_sentence_new_hobby' => 'Started a hobby', + 'life_event_sentence_new_instrument' => 'Learned a new instrument', + 'life_event_sentence_new_language' => 'Learned a new language', + 'life_event_sentence_tattoo_or_piercing' => 'Got a tattoo or piercing', + 'life_event_sentence_new_license' => 'Got a license', + 'life_event_sentence_travel' => 'Traveled', + 'life_event_sentence_achievement_or_award' => 'Got an achievement or award', + 'life_event_sentence_changed_beliefs' => 'Changed beliefs', + 'life_event_sentence_first_word' => 'Spoke for the first time', + 'life_event_sentence_first_kiss' => 'Kissed for the first time', + + // documents + 'document_list_title' => 'Documents', + 'document_list_cta' => 'Upload document', + 'document_list_blank_desc' => 'Here you can store documents related to this person.', + 'document_upload_zone_cta' => 'Upload a file', + 'document_upload_zone_progress' => 'Uploading the document…', + 'document_upload_zone_error' => 'There was an error uploading the document. Please try again below.', + + // Photos + 'photo_title' => 'Photos', + 'photo_list_title' => 'Related photos', + 'photo_list_cta' => 'Upload photo', + 'photo_list_blank_desc' => 'You can store images about this contact. Upload one now!', + 'photo_upload_zone_cta' => 'Upload a photo', + 'photo_current_profile_pic' => 'Current profile picture', + 'photo_make_profile_pic' => 'Make profile picture', + 'photo_delete' => 'Delete photo', + 'photo_next' => 'Next photo ❯', + 'photo_previous' => '❮ Previous photo', + + // Avatars + 'avatar_change_title' => 'Change your avatar', + 'avatar_question' => 'Which avatar would you like to use?', + 'avatar_default_avatar' => 'The default avatar', + 'avatar_adorable_avatar' => 'The Adorable avatar', + 'avatar_gravatar' => 'The Gravatar associated with the email address of this person. Gravatar is a global system that lets users associate email addresses with photos.', + 'avatar_current' => 'Keep the current avatar', + 'avatar_photo' => 'From a photo that you upload', + 'avatar_crop_new_avatar_photo' => 'Crop new avatar photo', + + // emotions + 'emotion_this_made_me_feel' => 'This made you feel…', + + // logs + 'auditlogs_link' => 'History', + 'auditlogs_title' => 'Everything that happened to :name', + 'auditlogs_breadcrumb' => 'History', + 'auditlogs_author' => 'By :name on :date', + + // contact field label + 'contact_field_label_home' => 'Home', + 'contact_field_label_work' => 'Work', + 'contact_field_label_cell' => 'Mobile', + 'contact_field_label_fax' => 'Fax', + 'contact_field_label_pager' => 'Pager', + 'contact_field_label_main' => 'Main', + 'contact_field_label_other' => 'Other', + 'contact_field_label_personal' => 'Personal', +]; diff --git a/resources/lang/pt/reminder.php b/resources/lang/pt/reminder.php new file mode 100644 index 0000000..acec64e --- /dev/null +++ b/resources/lang/pt/reminder.php @@ -0,0 +1,16 @@ + 'Desejar feliz aniversário para', + 'type_phone_call' => 'Ligar', + 'type_lunch' => 'Almoçar com', + 'type_hangout' => 'Sair com', + 'type_email' => 'Email', + 'type_birthday_kid' => 'Wish happy birthday to the child of', +]; diff --git a/resources/lang/pt/settings.php b/resources/lang/pt/settings.php new file mode 100644 index 0000000..2b4f012 --- /dev/null +++ b/resources/lang/pt/settings.php @@ -0,0 +1,557 @@ + 'Definições de conta', + 'sidebar_personalization' => 'Personalização', + 'sidebar_settings_storage' => 'Storage', + 'sidebar_settings_export' => 'Exportar dados', + 'sidebar_settings_users' => 'Utilizadores', + 'sidebar_settings_subscriptions' => 'Subscrição', + 'sidebar_settings_import' => 'Importar dados', + 'sidebar_settings_tags' => 'Tag management', + 'sidebar_settings_api' => 'API', + 'sidebar_settings_dav' => 'Recursos DAV', + 'sidebar_settings_security' => 'Segurança', + 'sidebar_settings_auditlogs' => 'Audit logs', + + 'title_general' => 'General Information', + 'title_i18n' => 'International settings', + 'title_layout' => 'Layout', + + 'me_title' => 'Me as a contact', + 'me_help' => 'This is the contact that represents you in Monica', + 'me_select' => 'Select a contact', + 'me_no_contact' => 'No contact selected yet.', + 'me_select_click' => 'Click here to select a contact.', + 'me_remove_contact' => 'Remove the association', + 'me_choose' => 'Choose yourself', + 'me_choose_placeholder' => 'Choose yourself', + + 'export_title' => 'Exportar os seus dados', + 'export_be_patient' => 'Click the button to start the export. It might take several minutes to process the export – please be patient and do not spam the button.', + 'export_title_sql' => 'Export to SQL', + 'export_sql_explanation' => 'Exporting your data in SQL format allows you to take your data and import it to your own Monica instance. This is only valuable if you do have your own server.', + 'export_sql_cta' => 'Export to SQL', + 'export_sql_link_instructions' => 'Note: read the instructions to learn more about importing this file to your instance.', + 'export_title_json' => 'Export to Json', + 'export_submitted' => 'Your export has been submitted, it will be available in a few moment…', + 'export_json_explanation' => 'Exporting your data in Json format for backup.', + 'export_json_beta' => 'Json export is in preview mode. Tell us what you think about it:', + 'export_json_cta' => 'Export to Json', + 'export_header_type' => 'Type', + 'export_header_timestamp' => 'Creation date', + 'export_header_status' => 'Status', + 'export_header_actions' => 'Actions', + 'export_last_title' => 'Last exports', + 'export_empty_title' => 'No exports yet', + 'export_type_json' => 'Json export', + 'export_type_sql' => 'SQL export', + 'export_status_todo' => 'Submitted', + 'export_status_doing' => 'Doing', + 'export_status_done' => 'Done', + 'export_status_failed' => 'Failed', + 'export_not_done' => 'Download impossible, this export is not done yet.', + + 'firstname' => 'Primeiro nome', + 'lastname' => 'Apelido', + 'name_order' => 'Name order', + 'name_order_firstname_lastname' => ' – John Doe', + 'name_order_lastname_firstname' => ' – Doe John', + 'name_order_firstname_lastname_nickname' => ' () – John Doe (Rambo)', + 'name_order_firstname_nickname_lastname' => ' () – John (Rambo) Doe', + 'name_order_lastname_firstname_nickname' => ' () – Doe John (Rambo)', + 'name_order_lastname_nickname_firstname' => ' () – Doe (Rambo) John', + 'name_order_nickname_firstname_lastname' => ' ( ) – Rambo (John Doe)', + 'name_order_nickname_lastname_firstname' => ' ( ) – Rambo (Doe John)', + 'name_order_nickname' => ' – Rambo', + 'currency' => 'Moneda', + 'name' => 'Seu nome: :name', + 'email' => 'Endereço de email', + 'email_placeholder' => 'Digite o email', + 'email_help' => 'This is the email used to login, and this is where Monica will send your reminders.', + 'timezone' => 'Fuso horário', + 'temperature_scale' => 'Temperature scale', + 'temperature_scale_fahrenheit' => 'Fahrenheit (°F)', + 'temperature_scale_celsius' => 'Celsius (°C)', + 'layout' => 'Layout', + 'layout_small' => 'Máximo 1200 pixels de largura', + 'layout_big' => 'Largura total do navegador', + 'save' => 'Salvar Preferências', + 'delete_title' => 'Delete your account', + 'delete_desc' => 'Do you wish to delete your account? Deletion is permanent and all of your data will be erased permanently. If you have a subscription, it will be cancelled immediately.', + 'delete_other_desc' => 'Your data in the main database will be deleted immediately. As described in our privacy policy, we carry out securely encrypted backups of the database every day. These backups are kept for 30 days after which they are completely deleted. We cannot delete specific data from the backups we hold any earlier than this. All of your data will be completely deleted no later than 31 days after your account’s deletion.', + 'reset_desc' => 'Do you wish to reset your account? This will remove all your contacts, and all of the data associated with them. Your account will not be deleted.', + 'reset_title' => 'Reset your account', + 'reset_cta' => 'Reset account', + 'reset_notice' => 'Are you sure to reset your account? This is permanent and cannot be undone.', + 'reset_success' => 'Your account has been reset successfully.', + 'delete_notice' => 'Are you sure you want to delete your account? This is permanent and cannot be undone. All of your data will be deleted and will not be recoverable.', + 'delete_cta' => 'Deletar conta', + 'settings_success' => 'Preferências atualizadas!', + 'locale' => 'Idioma usado no aplicativo', + 'locale_help' => 'Do you want to help translating Monica or add a new language? Please follow this link for more information.', + 'locale_ar' => 'Arabic', + 'locale_cs' => 'Checo', + 'locale_de' => 'Alemão', + 'locale_el' => 'Greek', + 'locale_en' => 'Inglês', + 'locale_en-GB' => 'English (United Kingdom)', + 'locale_es' => 'Espanhol', + 'locale_fr' => 'Francês', + 'locale_he' => 'Hebraico', + 'locale_hr' => 'Croata', + 'locale_id' => 'Indonesian', + 'locale_it' => 'Italiano', + 'locale_ja' => 'Japanese', + 'locale_nl' => 'Holandês', + 'locale_pt' => 'Português', + 'locale_pt-BR' => 'Portuguese, Brazil', + 'locale_ru' => 'Russo', + 'locale_sv' => 'Swedish', + 'locale_vi' => 'Vietnamese', + 'locale_zh' => 'Chinês (Simplificado)', + 'locale_zh-TW' => 'Chinese Traditional', + 'locale_tr' => 'Turco', + + 'security_title' => 'Segurança', + 'security_help' => 'Change security matters for your account.', + 'password_change' => 'Change your password', + 'password_current' => 'Current password', + 'password_current_placeholder' => 'Enter your current password', + 'password_new1' => 'New password', + 'password_new1_placeholder' => 'Enter your new password', + 'password_new2' => 'Confirm your new password', + 'password_new2_placeholder' => 'Retype your new password', + 'password_btn' => 'Change password', + '2fa_title' => 'Autenticação de dois fatores', + '2fa_otp_title' => 'Two Factor Authentication mobile application', + '2fa_enable_title' => 'Enable Two Factor Authentication', + '2fa_enable_description' => 'Enable Two Factor Authentication to increase the security of your account.', + '2fa_enable_otp' => 'Open up your Two Factor Authentication mobile app and scan the following QR barcode:', + '2fa_enable_otp_help' => 'If your Two Factor Authentication mobile app does not support QR barcodes, enter in the following code:', + '2fa_enable_otp_validate' => 'Please validate the new device you’ve just set up:', + '2fa_enable_success' => 'Two Factor Authentication activated', + '2fa_enable_error' => 'Error when trying to activate Two Factor Authentication', + '2fa_enable_error_already_set' => 'Two Factor Authentication is already activated', + '2fa_disable_title' => 'Disable Two Factor Authentication', + '2fa_disable_description' => 'Disable Two Factor Authentication for your account. Be careful, your account will be much less secure!', + '2fa_disable_success' => 'Two Factor Authentication disabled', + '2fa_disable_error' => 'Error when trying to disable Two Factor Authentication', + + 'webauthn_title' => 'Security key — WebAuthn protocol', + 'webauthn_enable_description' => 'Add a new security key', + 'webauthn_key_name_help' => 'Give your key a name.', + 'webauthn_key_name' => 'Key name:', + 'webauthn_success' => 'Your key is detected and validated.', + 'webauthn_last_use' => 'Last use: {timestamp}', + 'webauthn_delete_confirmation' => 'Are you sure you want to delete this key?', + 'webauthn_delete_success' => 'Key deleted', + 'webauthn_insertKey' => 'Insert your security key.', + 'webauthn_buttonAdvise' => 'If your security key has a button, press it.', + 'webauthn_noButtonAdvise' => 'If it does not, remove it and insert it again.', + 'webauthn_not_supported' => 'Your browser doesn’t currently support WebAuthn.', + 'webauthn_not_secured' => 'WebAuthn only supports secure connections. Please load this page with https scheme.', + 'webauthn_error_already_used' => 'This key is already registered. It’s not necessary to register it again.', + 'webauthn_error_not_allowed' => 'The operation either timed out or was not allowed.', + + 'recovery_title' => 'Recovery codes', + 'recovery_show' => 'Get recovery codes', + 'recovery_copy_help' => 'Copy codes in your clipboard', + 'recovery_help_intro' => 'These are your recovery codes:', + 'recovery_help_information' => 'You can use each recovery code once.', + 'recovery_clipboard' => 'Codes copied to the clipboard.', + 'recovery_generate' => 'Generate new codes…', + 'recovery_generate_help' => 'Generating new codes will invalidate previously generated codes.', + 'recovery_already_used_help' => 'This code has already been used.', + + 'users_list_title' => 'Users with access to your account', + 'users_list_add_user' => 'Invite a new user', + 'users_list_you' => 'That’s you', + 'users_list_invitations_title' => 'Pending invitations', + 'users_list_invitations_explanation' => 'Below are the people you’ve invited to join Monica as a collaborator.', + 'users_list_invitations_invited_by' => 'invited by :name', + 'users_list_invitations_sent_date' => 'sent on :date', + 'users_blank_title' => 'You are the only one who has access to this account.', + 'users_blank_add_title' => 'Would you like to invite someone else?', + 'users_blank_description' => 'This person will have the same access that you have, and will be able to add, edit or delete contact information.', + 'users_blank_cta' => 'Invite someone', + 'users_add_title' => 'Invite a new user to your account by email', + 'users_add_description' => 'This person will have the same access as you do, including inviting or deleting other users, including you. Make sure you trust this person before giving them access.', + 'users_add_email_field' => 'Enter the email of the person you want to invite', + 'users_add_confirmation' => 'I confirm that I want to invite this user to my account. I understand that this person will have access to ALL of my data and see exactly what I see.', + 'users_add_cta' => 'Invite user by email', + 'users_accept_title' => 'Accept invitation and create a new account', + 'users_error_please_confirm' => 'Please confirm that you want to invite this before proceeding with the invitation', + 'users_error_email_already_taken' => 'This email is already taken. Please choose another one', + 'users_error_already_invited' => 'You already have invited this user. Please choose another email address.', + 'users_error_email_not_similar' => 'This is not the email of the person who’ve invited you.', + 'users_invitation_deleted_confirmation_message' => 'The invitation has been successfully deleted', + 'users_invitations_delete_confirmation' => 'Are you sure you want to delete this invitation?', + 'users_list_delete_confirmation' => 'Are you sure to delete this user from your account?', + 'users_invitation_need_subscription' => 'Adding more users requires a subscription.', + + 'subscriptions_account_current_plan' => 'Your current plan', + 'subscriptions_account_current_legacy' => 'Current plan, not selectable anymore:', + 'subscriptions_account_current_paid_plan' => 'You are on the :name plan. Thanks so much for being a subscriber.', + + 'subscriptions_account_next_billing_title' => 'Next bill', + 'subscriptions_account_next_billing' => 'Your subscription will auto-renew on :date.', + 'subscriptions_account_bill_monthly' => 'We’ll bill you :price for another month.', + 'subscriptions_account_bill_annual' => 'We’ll bill you :price for another year.', + 'subscriptions_account_change' => 'Change plan', + + 'subscriptions_account_cancel_title' => 'Cancel subscription', + 'subscriptions_account_cancel_action' => 'Cancel subscription', + 'subscriptions_account_cancel' => 'You can cancel subscription anytime.', + 'subscriptions_account_free_plan' => 'You are on the free plan.', + 'subscriptions_account_free_plan_upgrade' => 'You can upgrade your account to the :name plan, which costs $:price per month. Here are the advantages:', + 'subscriptions_account_free_plan_benefits_users' => 'Unlimited number of users', + 'subscriptions_account_free_plan_benefits_reminders' => 'Reminders by email', + 'subscriptions_account_free_plan_benefits_import_data_vcard' => 'Import your contacts with vCard', + 'subscriptions_account_free_plan_benefits_support' => 'Support the project in the long run, so we can introduce more great features.', + 'subscriptions_account_upgrade' => 'Upgrade your account', + 'subscriptions_account_upgrade_title' => 'Upgrade Monica today and have more meaningful relationships.', + 'subscriptions_account_upgrade_choice' => 'Pick a plan below and join over :customers persons who upgraded their Monica.', + 'subscriptions_account_update_title' => 'Update Monica subscription', + 'subscriptions_account_update_description' => 'You can change your subscription’s frequency here.', + 'subscriptions_account_update_information' => 'You will be billed immediately for the new amount. Your subscription will extend to the new period, depending on your choice.', + 'subscriptions_account_invoices' => 'Invoices', + 'subscriptions_account_invoices_download' => 'Download', + 'subscriptions_account_invoices_subscription' => 'Subscription from :startDate to :endDate', + 'subscriptions_account_payment' => 'Which payment option fits you best?', + 'subscriptions_account_confirm_payment' => 'Your payment is currently incomplete, please confirm your payment.', + 'subscriptions_downgrade_title' => 'Downgrade your account to the free plan', + 'subscriptions_downgrade_limitations' => 'The free plan has limitations. In order to be able to downgrade, you need to pass the checklist below:', + 'subscriptions_downgrade_rule_users' => 'You must have only 1 user in your account', + 'subscriptions_downgrade_rule_users_constraint' => 'You currently have 1 user in your account.|You currently have :count users in your account.', + 'subscriptions_downgrade_rule_invitations' => 'You must not have any pending invitations', + 'subscriptions_downgrade_rule_invitations_constraint' => 'You currently have 1 pending invitation.|You currently have :count pending invitations.', + 'subscriptions_downgrade_rule_contacts' => 'You must not have more than :number active contacts', + 'subscriptions_downgrade_rule_contacts_constraint' => 'You currently have 1 contact.|You currently have :count contacts.', + 'subscriptions_downgrade_rule_contacts_archive' => 'We can also archive all your contacts for you – that would clear this rule and let you proceed with your account’s downgrade process.', + 'subscriptions_downgrade_cta' => 'Downgrade', + 'subscriptions_downgrade_success' => 'You are back to the Free plan!', + 'subscriptions_downgrade_thanks' => 'Thanks so much for trying the paid plan. We keep adding new features on Monica all the time – so you might want to come back in the future to see if you might be interested in taking a subscription again.', + 'subscriptions_back' => 'Back to settings', + 'subscriptions_upgrade_title' => 'Upgrade your account', + 'subscriptions_upgrade_choose' => 'You picked the :plan plan.', + 'subscriptions_upgrade_infos' => 'We couldn’t be happier. Enter your payment info below.', + 'subscriptions_upgrade_name' => 'Name on card', + 'subscriptions_upgrade_zip' => 'ZIP or postal code', + 'subscriptions_upgrade_credit' => 'Credit or debit card', + 'subscriptions_upgrade_submit' => 'Pay {amount}', + 'subscriptions_upgrade_charge' => 'We’ll charge your card :price now. The next charge will be on :date. If you ever change your mind, you can cancel at any time, no questions asked.', + 'subscriptions_upgrade_charge_handled' => 'The payment is handled by Stripe. No card information touches our server.', + 'subscriptions_upgrade_success' => 'Thank you! You are now subscribed.', + 'subscriptions_upgrade_thanks' => 'Welcome to the community of people who try to make the world a better place.', + + 'subscriptions_payment_confirm_title' => 'Confirm your :amount payment', + 'subscriptions_payment_confirm_information' => 'Extra confirmation is needed to process your payment. Please confirm your payment by filling out your payment details below.', + 'subscriptions_payment_succeeded_title' => 'Payment Successful', + 'subscriptions_payment_succeeded' => 'This payment was already successfully confirmed.', + 'subscriptions_payment_cancelled_title' => 'Payment Cancelled', + 'subscriptions_payment_cancelled' => 'This payment was cancelled.', + 'subscriptions_payment_error_name' => 'Please provide your name.', + 'subscriptions_payment_success' => 'The payment was successful.', + + 'subscriptions_pdf_title' => 'Your :name monthly subscription', + 'subscriptions_plan_frequency_year' => ':amount / year', + 'subscriptions_plan_frequency_month' => ':amount / month', + 'subscriptions_plan_choose' => 'Choose this plan', + 'subscriptions_plan_year_title' => 'Pay annually', + 'subscriptions_plan_year_bonus' => 'Peace of mind for a whole year', + 'subscriptions_plan_month_title' => 'Pay monthly', + 'subscriptions_plan_month_bonus' => 'Cancel any time', + 'subscriptions_plan_include1' => 'Included with your upgrade:', + 'subscriptions_plan_include2' => 'Unlimited number of contacts • Unlimited number of users • Reminders by email • Import with vCard • Personalization of the contact sheet', + 'subscriptions_plan_include3' => '100% of the profits go the development of this great open source project.', + 'subscriptions_help_title' => 'Additional details you may be curious about', + 'subscriptions_help_opensource_title' => 'What is an open source project?', + 'subscriptions_help_opensource_desc' => 'Monica is an open source project. This means it is built by a community who wants to build a great tool for the greater good. Being open source means the code is publicly available on GitHub, and everyone can inspect it, modify it or enhance it. All the money we raise is dedicated to building better features, paying for more powerful servers, and paying other costs. Thanks for your help. We couldn’t do it without you.', + 'subscriptions_help_limits_title' => 'Is there a limit to the number of contacts we can have on the free plan?', + 'subscriptions_help_limits_plan' => 'Yes. Free plans let you manage :number contacts.', + 'subscriptions_help_discounts_title' => 'Do you have discounts for non-profits and education?', + 'subscriptions_help_discounts_desc' => 'We do! Monica is free for students, and free for non-profits and charities. Just contact the support with a proof of your status and we’ll apply this special status in your account.', + 'subscriptions_help_change_title' => 'What if I change my mind?', + 'subscriptions_help_change_desc' => 'You can cancel anytime, no questions asked, and all by yourself – no need to contact support. However, you will not be refunded for the current period.', + + 'stripe_error_card' => 'Your card was declined. Decline message is: :message', + 'stripe_error_api_connection' => 'Network communication with Stripe failed. Try again later.', + 'stripe_error_rate_limit' => 'Too many requests with Stripe right now. Try again later.', + 'stripe_error_invalid_request' => 'Invalid parameters. Try again later.', + 'stripe_error_authentication' => 'Wrong authentication with Stripe', + + 'import_title' => 'Import contacts in your account', + 'import_cta' => 'Upload contacts', + 'import_stat' => 'You’ve imported :number files so far.', + 'import_result_stat' => 'Uploaded vCard with 1 contact (:total_imported imported, :total_skipped skipped)|Uploaded vCard with :total_contacts contacts (:total_imported imported, :total_skipped skipped)', + 'import_view_report' => 'View report', + 'import_in_progress' => 'The import is in progress. Reload the page in one minute.', + 'import_upload_title' => 'Import your contacts from a vCard file', + 'import_upload_rules_desc' => 'We do however have some rules:', + 'import_upload_rule_format' => 'We support .vcard and .vcf files.', + 'import_upload_rule_vcard' => 'We support the vCard 3.0 format, which is the default format for macOS’s Contacts.app and Google Contacts.', + 'import_upload_rule_instructions' => 'Export instructions for macOS Contacts.app and Google Contacts.', + 'import_upload_rule_multiple' => 'If your contacts have multiple email addresses or phone numbers, only the first entry will be saved.', + 'import_upload_rule_limit' => 'Files are limited to 10 MB.', + 'import_upload_rule_time' => 'It might take up to a minute to upload the contacts and process them. Please be patient.', + 'import_upload_rule_cant_revert' => 'Please make sure data is accurate before uploading, as you can’t undo the upload.', + 'import_upload_form_file' => 'Your .vcf or .vCard file:', + 'import_upload_behaviour' => 'Import behaviour:', + 'import_upload_behaviour_add' => 'Add new contacts and skip existing', + 'import_upload_behaviour_replace' => 'Replace existing contacts', + 'import_upload_behaviour_help' => 'Replacing will replace all data found in the vCard, but will keep existing contact fields.', + 'import_report_title' => 'Importing report', + 'import_report_date' => 'Date of the import', + 'import_report_type' => 'Type of import', + 'import_report_number_contacts' => 'Number of contacts in the file', + 'import_report_number_contacts_imported' => 'Number of imported contacts', + 'import_report_number_contacts_skipped' => 'Number of skipped contacts', + 'import_report_status_imported' => 'Imported', + 'import_report_status_skipped' => 'Skipped', + 'import_vcard_parse_error' => 'Error when parsing the vCard entry', + 'import_vcard_contact_exist' => 'Contact already exists', + 'import_vcard_contact_no_firstname' => 'No first name (mandatory)', + 'import_vcard_file_not_found' => 'File not found', + 'import_vcard_unknown_entry' => 'Unknown contact name', + 'import_vcard_file_no_entries' => 'File contains no entries', + 'import_blank_title' => 'You haven’t imported any contacts yet.', + 'import_blank_question' => 'Would you like to import contacts now?', + 'import_blank_description' => 'We can import vCard files that you can get from Google Contacts or your Contact manager.', + 'import_blank_cta' => 'Import vCard', + 'import_need_subscription' => 'Importing data requires a subscription.', + + 'tags_list_title' => 'Tags', + 'tags_list_description' => 'You can organize your contacts by setting up tags. Tags work like folders, but you can add more than one tag to a contact.', + 'tags_list_contact_number' => '1 contacto|:count contactos', + 'tags_list_delete_success' => 'The tag has been successfully with success', + 'tags_list_edit_success' => 'The tag has been successfully updated', + 'tags_list_delete_confirmation' => 'Are you sure you want to delete the tag? No contacts will be deleted, only the tag.', + 'tags_blank_title' => 'Tags are a great way of categorizing your contacts.', + 'tags_blank_description' => 'Tags work like folders, but you can add more than one tag to a contact. Go to a contact and tag a friend, right below the name. Once a contact is tagged, come back here to manage all the tags in your account.', + + 'api_title' => 'API access', + 'api_description' => 'The API can be used to manipulate Monica’s data from an external application, like a mobile application for instance.', + 'api_help' => 'To use the API, a token is mandatory. You can either create a personal access token (Bearer authentication), or authorize an OAuth client to create it for you. See API documentation.', + 'api_endpoint' => 'The API endpoint for this Monica instance is:', + + 'api_personal_access_tokens' => 'Personal access tokens', + 'api_pao_description' => 'Make sure you give this token to a source you trust – as they allow you to access all your data.', + 'api_token_title' => 'Personal Access Tokens', + 'api_token_create_new' => 'Create New Token', + 'api_token_not_created' => 'You have not created any personal access tokens.', + 'api_token_name' => 'Token name', + 'api_token_expire' => 'Expires at {date}', + 'api_token_delete' => 'Delete', + 'api_token_create' => 'Create Token', + 'api_token_scopes' => 'Scopes', + 'api_token_help' => 'Here is your new personal access token. This is the only time it will be shown so don’t lose it! You may now use this token to make API requests.', + + 'api_oauth_clients' => 'Your OAuth clients', + 'api_oauth_clients_desc' => 'This section lets you register your own OAuth clients.', + 'api_oauth_clients_desc2' => 'Use this client id to request a new token, and convert authorization codes to access tokens. See Laravel Passport documentation for more information.', + 'api_oauth_title' => 'OAuth Clients', + 'api_oauth_create_new' => 'Create New Client', + 'api_oauth_edit' => 'Edit Client', + 'api_oauth_not_created' => 'You have not created any OAuth clients.', + 'api_oauth_clientid' => 'Client ID', + 'api_oauth_name' => 'Name', + 'api_oauth_name_help' => 'Something your users will recognize and trust.', + 'api_oauth_secret' => 'Secret', + 'api_oauth_create' => 'Create Client', + 'api_oauth_redirecturl' => 'Redirect URL', + 'api_oauth_redirecturl_help' => 'Your application’s authorization callback URL.', + + 'api_authorized_clients' => 'List of authorized clients', + 'api_authorized_clients_desc' => 'This section lists all the clients you’ve authorized to access your application data. You can revoke this authorization at anytime.', + 'api_authorized_clients_title' => 'Authorized Applications', + 'api_authorized_clients_none' => 'There are no authorized clients yet.', + 'api_authorized_clients_name' => 'Name', + 'api_authorized_clients_scopes' => 'Scopes', + + 'personalization_tab_title' => 'Personalize your account', + + 'personalization_title' => 'Here you will find different settings to configure your account. These features are intended for “power users” who want maximum control over Monica.', + 'personalization_contact_field_type_title' => 'Contact field types', + 'personalization_contact_field_type_add' => 'Add new field type', + 'personalization_contact_field_type_description' => 'You can configure all the different types of contact fields that you can associate to all your contacts. For example, if a new social network appears in the future, you will be able to add this new way of communicating with your contacts right here.', + 'personalization_contact_field_type_table_name' => 'Name', + 'personalization_contact_field_type_table_protocol' => 'Protocol', + 'personalization_contact_field_type_table_actions' => 'Actions', + 'personalization_contact_field_type_modal_title' => 'Add a new contact field type', + 'personalization_contact_field_type_modal_edit_title' => 'Edit an existing contact field type', + 'personalization_contact_field_type_modal_delete_title' => 'Delete an existing contact field type', + 'personalization_contact_field_type_modal_delete_description' => 'Are you sure you want to delete this contact field type? Deleting this type of contact field will delete ALL the data with this type for all of your contacts.', + 'personalization_contact_field_type_modal_name' => 'Name', + 'personalization_contact_field_type_modal_protocol' => 'Protocol (optional)', + 'personalization_contact_field_type_modal_protocol_help' => 'Each new contact field type can be clickable. If a protocol is set, we will use it to trigger the action that is set.', + 'personalization_contact_field_type_modal_icon' => 'Icon (optional)', + 'personalization_contact_field_type_modal_icon_help' => 'You can associate an icon with this contact field type. You need to add a reference to a Font Awesome icon.', + 'personalization_contact_field_type_delete_success' => 'The contact field type has been successfully deleted.', + 'personalization_contact_field_type_add_success' => 'The contact field type has been successfully added.', + 'personalization_contact_field_type_edit_success' => 'The contact field type has been successfully updated.', + + 'personalization_genders_title' => 'Gender types', + 'personalization_genders_add' => 'Add new gender type', + 'personalization_genders_desc' => 'You can define as many genders as you need to. You need at least one gender type in your account.', + 'personalization_genders_modal_add' => 'Add gender type', + 'personalization_genders_modal_edit' => 'Update gender type', + 'personalization_genders_modal_name' => 'Name', + 'personalization_genders_modal_name_help' => 'The name used to display the gender on a contact page.', + 'personalization_genders_modal_sex' => 'Sex', + 'personalization_genders_modal_sex_help' => 'Used to define the relationships, and during the VCard import/export process.', + 'personalization_genders_modal_default' => 'Select the default gender for a new contact', + 'personalization_genders_modal_delete' => 'Delete gender type', + 'personalization_genders_modal_delete_desc' => 'Are you sure you want to delete the gender “{name}”?', + 'personalization_genders_modal_delete_question' => 'You currently have {count} contact with this gender. If you delete this gender, what gender should this contact have?|You currently have {count} contacts with this gender. If you delete this gender, what gender should these contacts have?', + 'personalization_genders_modal_delete_question_default' => 'This gender is the default one. If you delete this gender, which one will be the new default?', + 'personalization_genders_modal_error' => 'Please choose a gender from the list.', + 'personalization_genders_list_contact_number' => '{count} contact|{count} contacts', + 'personalization_genders_table_name' => 'Name', + 'personalization_genders_table_sex' => 'Sex', + 'personalization_genders_table_default' => 'Default', + 'personalization_genders_default' => 'Default gender', + 'personalization_genders_make_default' => 'Change default gender', + 'personalization_genders_select_default' => 'Select default gender', + 'personalization_genders_m' => 'Male', + 'personalization_genders_f' => 'Female', + 'personalization_genders_o' => 'Other', + 'personalization_genders_u' => 'Unknown', + 'personalization_genders_n' => 'None or not applicable', + + 'personalization_reminder_rule_save' => 'The change has been saved', + 'personalization_reminder_rule_title' => 'Reminder rules', + 'personalization_reminder_rule_line' => '{count} day before|{count} days before', + 'personalization_reminder_rule_desc' => 'For every reminder that you set, Monica can send you an email a number of days before the event happens. You can adjust these notification settings here. These notifications only apply to monthly and yearly reminders.', + + 'personalization_module_save' => 'The change has been saved', + 'personalization_module_title' => 'Features', + 'personalization_module_desc' => 'You may not need all of Monica’s features. Below you can toggle specific features that are used on a contact sheet. This change will affect ALL your contacts. Turning off a feature does not delete any data, it simply hides the feature.', + + 'personalisation_paid_upgrade' => 'This is a premium feature that requires a Paid subscription to be active. Upgrade your account by visiting Settings > Subscription.', + 'personalisation_paid_upgrade_vue' => 'This is a premium feature that requires a Paid subscription to be active. Upgrade your account by visiting Settings > Subscription.', + + 'reminder_time_to_send' => 'Time of the day reminders will be sent', + 'reminder_time_to_send_help' => 'Your next reminder is scheduled to be sent on {dateTime}.', + + 'personalization_activity_type_category_title' => 'Activity type categories', + 'personalization_activity_type_category_add' => 'Add a new activity type category', + 'personalization_activity_type_category_table_name' => 'Name', + 'personalization_activity_type_category_description' => 'An activity with one of your contacts can have a type and a category type. Your account comes with a set of predefined category types by default, but you can customize these here.', + 'personalization_activity_type_category_table_actions' => 'Actions', + 'personalization_activity_type_category_modal_add' => 'Add a new activity type category', + 'personalization_activity_type_category_modal_edit' => 'Edit an activity type category', + 'personalization_activity_type_category_modal_question' => 'What should we name this new category?', + 'personalization_activity_type_add_button' => 'Add a new activity type', + 'personalization_activity_type_modal_add' => 'Add a new activity type', + 'personalization_activity_type_modal_question' => 'What should we name this new activity type?', + 'personalization_activity_type_modal_edit' => 'Edit an activity type', + 'personalization_activity_type_category_modal_delete' => 'Delete an activity type category', + 'personalization_activity_type_category_modal_delete_desc' => 'Are you sure you want to delete this category? Deleting it will delete all associated activity types. Activities that belong to this category will not be affected by this deletion.', + 'personalization_activity_type_modal_delete' => 'Delete an activity type', + 'personalization_activity_type_modal_delete_desc' => 'Are you sure you want to delete this activity type? Activities that belong to this category will not be affected by this deletion.', + 'personalization_activity_type_modal_delete_error' => 'We can’t find this activity type.', + 'personalization_activity_type_category_modal_delete_error' => 'We can’t find this activity type category.', + + 'personalization_life_event_category_title' => 'Life event categories', + 'personalization_live_event_category_table_name' => 'Name', + 'personalization_life_event_category_description' => 'A life event can have a type and a category. Your account comes with a set of predefined categories and types by default, but you can customize life event types here.', + 'personalization_live_event_category_table_actions' => 'Actions', + 'personalization_life_event_type_add_button' => 'Add a new life event type', + 'personalization_life_event_type_modal_add' => 'Add a new life event type', + 'personalization_life_event_type_modal_question' => 'What should we name this new life event type?', + 'personalization_life_event_type_modal_edit' => 'Edit a life event type', + 'personalization_life_event_type_modal_delete' => 'Delete a life event type', + 'personalization_life_event_type_modal_delete_desc' => 'Are you sure you want to delete this life event type? Life events that belong to this type will be deleted by performing this action.', + 'personalization_life_event_type_modal_delete_error' => 'We can’t find this life event type.', + + 'personalization_life_event_category_work_education' => 'Work & education', + 'personalization_life_event_category_family_relationships' => 'Family & relationships', + 'personalization_life_event_category_home_living' => 'Home & living', + 'personalization_life_event_category_travel_experiences' => 'Travel & experiences', + 'personalization_life_event_category_health_wellness' => 'Health & wellness', + + 'personalization_life_event_type_new_job' => 'New job', + 'personalization_life_event_type_retirement' => 'Retirement', + 'personalization_life_event_type_new_school' => 'New school', + 'personalization_life_event_type_study_abroad' => 'Study abroad', + 'personalization_life_event_type_volunteer_work' => 'Volunteer work', + 'personalization_life_event_type_published_book_or_paper' => 'Published a book or paper', + 'personalization_life_event_type_military_service' => 'Military service', + 'personalization_life_event_type_first_met' => 'First met', + 'personalization_life_event_type_new_relationship' => 'New relationship', + 'personalization_life_event_type_engagement' => 'Engagement', + 'personalization_life_event_type_marriage' => 'Marriage', + 'personalization_life_event_type_anniversary' => 'Anniversary', + 'personalization_life_event_type_expecting_a_baby' => 'Expecting a baby', + 'personalization_life_event_type_new_child' => 'New child', + 'personalization_life_event_type_new_family_member' => 'New family member', + 'personalization_life_event_type_new_pet' => 'New pet', + 'personalization_life_event_type_end_of_relationship' => 'End of relationship', + 'personalization_life_event_type_loss_of_a_loved_one' => 'Loss of a loved one', + 'personalization_life_event_type_moved' => 'Moved', + 'personalization_life_event_type_bought_a_home' => 'Bought a home', + 'personalization_life_event_type_home_improvement' => 'Home improvement', + 'personalization_life_event_type_holidays' => 'Holidays', + 'personalization_life_event_type_new_vehicle' => 'New vehicle', + 'personalization_life_event_type_new_roommate' => 'New roommate', + 'personalization_life_event_type_overcame_an_illness' => 'Overcame an illness', + 'personalization_life_event_type_quit_a_habit' => 'Quit a habit', + 'personalization_life_event_type_new_eating_habits' => 'New eating habits', + 'personalization_life_event_type_weight_loss' => 'Weight loss', + 'personalization_life_event_type_wear_glass_or_contact' => 'Started wearing glasses or contacts', + 'personalization_life_event_type_broken_bone' => 'Broke a bone', + 'personalization_life_event_type_removed_braces' => 'Had braces removed', + 'personalization_life_event_type_surgery' => 'Had surgery', + 'personalization_life_event_type_dentist' => 'Had dental treatment', + 'personalization_life_event_type_new_sport' => 'Started playing a new sport', + 'personalization_life_event_type_new_hobby' => 'Took up a new hobby', + 'personalization_life_event_type_new_instrument' => 'Started learning a new instrument', + 'personalization_life_event_type_new_language' => 'Started learning a new language', + 'personalization_life_event_type_tattoo_or_piercing' => 'Tattoo or piercing', + 'personalization_life_event_type_new_license' => 'New license', + 'personalization_life_event_type_travel' => 'Travel', + 'personalization_life_event_type_achievement_or_award' => 'Achievement or award', + 'personalization_life_event_type_changed_beliefs' => 'Changed beliefs', + 'personalization_life_event_type_first_word' => 'First word', + 'personalization_life_event_type_first_kiss' => 'First kiss', + + 'storage_title' => 'Storage', + 'storage_account_info' => 'Your account limit is :accountLimit MB. Your current usage is :currentAccountSize MB (about :percentUsage%).', + 'storage_upgrade_notice' => 'Upgrade your account to be able to upload documents and photos.', + 'storage_description' => 'Here you can see all the documents and photos uploaded about your contacts.', + + 'dav_title' => 'WebDAV', + 'dav_description' => 'Here you can find all settings to use WebDAV resources for CardDAV and CalDAV exports.', + 'dav_copy_help' => 'Copy into your clipboard', + 'dav_clipboard_copied' => 'Value copied into your clipboard', + 'dav_url_base' => 'Base url for all CardDAV and CalDAV resources:', + 'dav_connect_help' => 'You can connect your contacts and/or calendars with this base url on you phone or computer.', + 'dav_connect_help2' => 'Use your login (email) and create an API token as the password to authenticate.', + 'dav_url_carddav' => 'CardDAV url for Contacts resource:', + 'dav_url_caldav_birthdays' => 'CalDAV url for Birthdays resources:', + 'dav_url_caldav_tasks' => 'CalDAV url for Tasks resources:', + 'dav_title_carddav' => 'CardDAV', + 'dav_title_caldav' => 'CalDAV', + 'dav_carddav_export' => 'Export all contacts in one file', + 'dav_caldav_birthdays_export' => 'Export all birthdays in one file', + 'dav_caldav_tasks_export' => 'Export all tasks in one file', + + 'archive_title' => 'Archive all of the contacts in your account', + 'archive_desc' => 'This will archive all of the contacts in your account.', + 'archive_cta' => 'Archive all of your contacts', + + 'logs_title' => 'Everything that has happened to this account', + 'logs_actor' => 'Actor', + 'logs_timestamp' => 'Timestamp', + 'logs_description' => 'Description', + 'logs_subject' => 'Subject', + 'logs_size' => 'Size (Kb)', + 'logs_object' => 'Object', +]; diff --git a/resources/lang/pt/validation.php b/resources/lang/pt/validation.php new file mode 100644 index 0000000..a86bedf --- /dev/null +++ b/resources/lang/pt/validation.php @@ -0,0 +1,166 @@ + 'O campo :attribute deve ser aceito.', + 'active_url' => 'O campo :attribute deve conter uma URL válida.', + 'after' => 'O campo :attribute deve conter uma data posterior a :date.', + 'after_or_equal' => 'O campo :attribute deverá conter uma data posterior ou igual a :date.', + 'alpha' => 'O campo :attribute deve conter apenas letras.', + 'alpha_dash' => 'The :attribute may only contain letters, numbers, dashes and underscores.', + 'alpha_num' => 'O campo :attribute deve conter apenas letras e números .', + 'array' => 'O campo :attribute deve conter um array.', + 'before' => 'O campo :attribute deve conter uma data anterior a :date.', + 'before_or_equal' => 'O Campo :attribute deverá conter uma data anterior ou igual a :date.', + 'between' => [ + 'numeric' => 'O campo :attribute deve conter um número entre :min e :max.', + 'file' => 'O campo :attribute deve conter um arquivo de :min a :max kilobytes.', + 'string' => 'O campo :attribute deve conter entre :min a :max caracteres.', + 'array' => 'O campo :attribute deve conter de :min a :max itens.', + ], + 'boolean' => 'O campo :attribute deve conter o valor verdadeiro ou falso.', + 'confirmed' => 'A confirmação para o campo :attribute não coincide.', + 'date' => 'O campo :attribute não contém uma data válida.', + 'date_equals' => 'The :attribute must be a date equal to :date.', + 'date_format' => 'A data informada para o campo :attribute não respeita o formato :format.', + 'different' => 'Os campos :attribute e :other devem conter valores diferentes.', + 'digits' => 'O campo :attribute deve conter :digits dígitos.', + 'digits_between' => 'O campo :attribute deve conter entre :min a :max dígitos.', + 'dimensions' => 'O campo :attribute deverá conter uma dimensão de imagem válida.', + 'distinct' => 'O campo :attribute contém um valor duplicado.', + 'email' => 'O campo :attribute não contém um endereço de email válido.', + 'ends_with' => 'The :attribute must end with one of the following: :values.', + 'exists' => 'O valor selecionado para o campo :attribute é inválido.', + 'file' => 'O campo :attribute deverá conter um ficheiro.', + 'filled' => 'É obrigatória a indicação de um valor para o campo :attribute.', + 'gt' => [ + 'numeric' => 'The :attribute must be greater than :value.', + 'file' => 'The :attribute must be greater than :value kilobytes.', + 'string' => 'The :attribute must be greater than :value characters.', + 'array' => 'The :attribute must have more than :value items.', + ], + 'gte' => [ + 'numeric' => 'The :attribute must be greater than or equal :value.', + 'file' => 'The :attribute must be greater than or equal :value kilobytes.', + 'string' => 'The :attribute must be greater than or equal :value characters.', + 'array' => 'The :attribute must have :value items or more.', + ], + 'image' => 'O campo :attribute deve conter uma imagem.', + 'in' => 'O campo :attribute não contém um valor válido.', + 'in_array' => 'O campo :attribute não existe em :other.', + 'integer' => 'O campo :attribute deve conter um número inteiro.', + 'ip' => 'O campo :attribute deve conter um IP válido.', + 'ipv4' => 'O campo :attribute deverá conter um IPv4 válido.', + 'ipv6' => 'O campo :attribute deverá conter um IPv6 válido.', + 'json' => 'O campo :attribute deve conter uma string JSON válida.', + 'lt' => [ + 'numeric' => 'The :attribute must be less than :value.', + 'file' => 'The :attribute must be less than :value kilobytes.', + 'string' => 'The :attribute must be less than :value characters.', + 'array' => 'The :attribute must have less than :value items.', + ], + 'lte' => [ + 'numeric' => 'The :attribute must be less than or equal :value.', + 'file' => 'The :attribute must be less than or equal :value kilobytes.', + 'string' => 'The :attribute must be less than or equal :value characters.', + 'array' => 'The :attribute must not have more than :value items.', + ], + 'max' => [ + 'numeric' => 'O campo :attribute não pode conter um valor superior a :max.', + 'file' => 'O campo :attribute não pode conter um arquivo com mais de :max kilobytes.', + 'string' => 'O campo :attribute não pode conter mais de :max caracteres.', + 'array' => 'O campo :attribute deve conter no máximo :max itens.', + ], + 'mimes' => 'O campo :attribute deve conter um arquivo do tipo: :values.', + 'mimetypes' => 'O campo :attribute deverá conter um ficheiro do tipo: :values.', + 'min' => [ + 'numeric' => 'O campo :attribute deve conter um número superior ou igual a :min.', + 'file' => 'O campo :attribute deve conter um arquivo com no mínimo :min kilobytes.', + 'string' => 'O campo :attribute deve conter no mínimo :min caracteres.', + 'array' => 'O campo :attribute deve conter no mínimo :min itens.', + ], + 'not_in' => 'O campo :attribute contém um valor inválido.', + 'not_regex' => 'The :attribute format is invalid.', + 'numeric' => 'O campo :attribute deve conter um valor numérico.', + 'password' => 'The password is incorrect.', + 'present' => 'O campo :attribute deve estar presente.', + 'regex' => 'O formato do valor informado no campo :attribute é inválido.', + 'required' => 'O campo :attribute é obrigatório.', + 'required_if' => 'O campo :attribute é obrigatório quando o valor do campo :other é igual a :value.', + 'required_unless' => 'O campo :attribute é obrigatório a menos que :other esteja presente em :values.', + 'required_with' => 'O campo :attribute é obrigatório quando :values está presente.', + 'required_with_all' => 'The :attribute field is required when :values are present.', + 'required_without' => 'O campo :attribute é obrigatório quando :values não está presente.', + 'required_without_all' => 'O campo :attribute é obrigatório quando nenhum dos :values está presente.', + 'same' => 'Os campos :attribute e :other devem conter valores iguais.', + 'size' => [ + 'numeric' => 'O campo :attribute deve conter o número :size.', + 'file' => 'O campo :attribute deve conter um arquivo com o tamanho de :size kilobytes.', + 'string' => 'O campo :attribute deve conter :size caracteres.', + 'array' => 'O campo :attribute deve conter :size itens.', + ], + 'starts_with' => 'The :attribute must start with one of the following: :values.', + 'string' => 'O campo :attribute deve ser uma string.', + 'timezone' => 'O campo :attribute deve conter um fuso horário válido.', + 'unique' => 'O valor informado para o campo :attribute já está em uso.', + 'uploaded' => 'O upload do ficheiro :attribute falhou.', + 'url' => 'O formato da URL informada para o campo :attribute é inválido.', + 'uuid' => 'The :attribute must be a valid UUID.', + + /* + |-------------------------------------------------------------------------- + | Custom Validation Language Lines + |-------------------------------------------------------------------------- + | + | Here you may specify custom validation messages for attributes using the + | convention "attribute.rule" to name the lines. This makes it quick to + | specify a specific custom language line for a given attribute rule. + | + */ + + 'custom' => [ + 'attribute-name' => [ + 'rule-name' => 'mensagem-personalizada', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Custom Validation Attributes + |-------------------------------------------------------------------------- + | + | The following language lines are used to swap our attribute placeholder + | with something more reader friendly such as "E-Mail Address" instead + | of "email". This simply helps us make our message more expressive. + | + */ + + 'attributes' => [], + + 'vue' => [ + 'max' => [ + 'numeric' => '{field} may not be greater than {max}.', + 'string' => '{field} may not be greater than {max} characters.', + ], + 'required' => '{field} is required.', + 'url' => '{field} is not a valid URL.', + ], + +]; diff --git a/resources/lang/ru.json b/resources/lang/ru.json new file mode 100644 index 0000000..4a34cf5 --- /dev/null +++ b/resources/lang/ru.json @@ -0,0 +1,7 @@ +{ + "The :attribute must contain at least one uppercase and one lowercase letter.": "Поле :attribute должно содержать как минимум по одному символу в нижнем и верхнем регистрах.", + "The :attribute must contain at least one letter.": "Поле :attribute должно содержать минимум одну букву.", + "The :attribute must contain at least one symbol.": "Поле :attribute должно содержать минимум один спец символ.", + "The :attribute must contain at least one number.": "Поле :attribute должно содержать минимум одну цифру.", + "The given :attribute has appeared in a data leak. Please choose a different :attribute.": "Значение поля :attribute обнаружено в утечке данных. Пожалуйста, укажите другое значение для :attribute." +} diff --git a/resources/lang/ru/app.php b/resources/lang/ru/app.php new file mode 100644 index 0000000..8a6a393 --- /dev/null +++ b/resources/lang/ru/app.php @@ -0,0 +1,571 @@ + 'Да', + 'no' => 'Нет', + 'update' => 'Обновить', + 'save' => 'Сохранить', + 'add' => 'Добавить', + 'cancel' => 'Отмена', + 'confirm' => 'Подтвердить', + 'delete_confirm' => 'Вы уверены?', + 'delete' => 'Удалить', + 'edit' => 'Редактировать', + 'upload' => 'Закачать', + 'download' => 'Загрузить', + 'save_close' => 'Сохранить и закрыть', + 'close' => 'Закрыть', + 'copy' => 'Копировать', + 'create' => 'Создать', + 'remove' => 'Убрать', + 'revoke' => 'Отозвать', + 'done' => 'Готово', + 'back' => 'Назад', + 'verify' => 'Подтвердить', + 'new' => 'новый', + 'unknown' => 'Я не знаю', + 'load_more' => 'Загрузить ещё', + 'loading' => 'Загрузка...', + 'with' => 'с', + 'today' => 'сегодня', + 'yesterday' => 'вчера', + 'another_day' => 'another day', + 'date' => 'Дата', + 'type' => 'Тип', + 'zoom' => 'Масштаб', + 'upgrade' => 'Upgrade to unlock', + 'percent_uploaded' => '{percent}% загружено', + 'retry' => 'Повторить', + 'filter' => 'Список фильтров', + 'go_back' => 'Назад', + 'file_selected' => 'One file selected…|{count} files selected…', + + 'application_title' => 'Monica – personal relationship manager', + 'application_description' => 'Monica is a tool to manage your interactions with your loved ones, friends and family.', + 'application_og_title' => 'Have better relations with your loved ones. Free online CRM for friends and family.', + + 'markdown_description' => 'Хотите форматировать ваш текст? Мы поддерживаем Markdown для добавления этих функций', + 'markdown_link' => 'Читать документацию', + + 'header_settings_link' => 'Настройки', + 'header_logout_link' => 'Выйти', + 'header_changelog_link' => 'Product changes', + + 'main_nav_cta' => 'Добавить людей', + 'main_nav_dashboard' => 'Обзор', + 'main_nav_family' => 'контакты', + 'main_nav_journal' => 'Журнал', + 'main_nav_activities' => 'Активности', + 'main_nav_tasks' => 'Задачи', + + 'footer_remarks' => 'Комментарии?', + 'footer_send_email' => 'Отправить нам email', + 'footer_privacy' => 'Политика конфиденциальности', + 'footer_release' => 'Примечания к выпуску', + 'footer_newsletter' => 'Рассылка', + 'footer_source_code' => 'Поддержать проект', + 'footer_version' => 'Версия: :version', + 'footer_new_version' => 'Доступна новая версия', + + 'footer_modal_version_whats_new' => 'Что нового', + 'footer_modal_version_release_away' => 'You are 1 release behind the latest version available. You should update your instance.|You are :number releases behind the latest version available. You should update your instance.', + + 'breadcrumb_dashboard' => 'Обзор', + 'breadcrumb_list_contacts' => 'Список контактов', + 'breadcrumb_archived_contacts' => 'Archived contacts', + 'breadcrumb_journal' => 'Журнал', + 'breadcrumb_settings' => 'Настройки', + 'breadcrumb_settings_export' => 'Экспорт', + 'breadcrumb_settings_users' => 'Пользователи', + 'breadcrumb_settings_users_add' => 'Добавить пользователя', + 'breadcrumb_settings_subscriptions' => 'Подписка', + 'breadcrumb_settings_import' => 'Импорт', + 'breadcrumb_settings_import_report' => 'Импортировать отчёт', + 'breadcrumb_settings_import_upload' => 'Закачать', + 'breadcrumb_settings_tags' => 'Тэги', + 'breadcrumb_add_significant_other' => 'Add significant other', + 'breadcrumb_edit_significant_other' => 'Edit significant other', + 'breadcrumb_add_note' => 'Добавить заметку', + 'breadcrumb_edit_note' => 'Редактировать заметку', + 'breadcrumb_api' => 'API', + 'breadcrumb_dav' => 'DAV Resources', + 'breadcrumb_edit_introductions' => 'How did you meet', + 'breadcrumb_settings_personalization' => 'Персонализация', + 'breadcrumb_settings_security' => 'Безопасность', + 'breadcrumb_settings_security_2fa' => 'Двухфакторная аутентификация', + 'breadcrumb_profile' => 'Профиль :name', + + 'gender_male' => 'Мужской', + 'gender_female' => 'Женский', + 'gender_none' => 'Неизвестно', + 'gender_no_gender' => 'Нет полов', + + 'error_title' => 'Ой, что-то пошло не так.', + 'error_unauthorized' => 'У вас нет прав для редактирования этого ресурса.', + 'error_user_account' => 'This user does not belong to the given account.', + 'error_save' => 'We had an error trying to save the data.', + 'error_try_again' => 'Произошла ошибка. Пожалуйста, попробуйте снова.', + 'error_id' => 'Error ID: :id', + 'error_unavailable' => 'Service unavailable', + 'error_maintenance' => 'Maintenance in progress. We’ll be right back.', + 'error_help' => 'We’ll be right back.', + 'error_twitter' => 'Follow our Twitter account to be alerted when it’s up again.', + 'error_no_term' => 'There is no policy for this instance yet.', + + 'default_save_success' => 'The data has been saved.', + + 'compliance_title' => 'Sorry for the interruption.', + 'compliance_desc' => 'We have changed our Terms of Use and Privacy Policy. By law we have to ask you to review them and accept them so you can continue to use your account.', + 'compliance_desc_end' => 'We don’t do anything nasty with your data or account and will never do.', + 'compliance_terms' => 'Accept new terms and privacy policy', + + // Relationship types + // Yes, each relationship type has 8 strings associated with it. + // This is because we need to indicate the name of the relationship type, + // and also the name of the opposite side of this relationship (father/son), + // and then, the feminine version of the string. Finally, in some sentences + // in the UI, we need to include the name of the person we add the relationship + // to. + 'relationship_type_group_love' => 'Любовные отношения', + 'relationship_type_group_family' => 'Семейные отношения', + 'relationship_type_group_friend' => 'Дружеские отношения', + 'relationship_type_group_work' => 'Рабочие отношения', + 'relationship_type_group_other' => 'Other kind of relationships', + + 'relationship_type_partner' => 'significant other', + 'relationship_type_partner_female' => 'significant other', + 'relationship_type_partner_male' => 'significant other', + 'relationship_type_partner_with_name' => ':name’s significant other', + 'relationship_type_partner_female_with_name' => ':name’s significant other', + 'relationship_type_partner_male_with_name' => ':name’s significant other', + + 'relationship_type_spouse' => 'spouse', + 'relationship_type_spouse_female' => 'wife', + 'relationship_type_spouse_male' => 'husband', + 'relationship_type_spouse_with_name' => ':name’s spouse', + 'relationship_type_spouse_female_with_name' => ':name’s wife', + 'relationship_type_spouse_male_with_name' => ':name’s husband', + + 'relationship_type_date' => 'дата', + 'relationship_type_date_female' => 'дата', + 'relationship_type_date_male' => 'date', + 'relationship_type_date_with_name' => ':name’s date', + 'relationship_type_date_female_with_name' => ':name’s date', + 'relationship_type_date_male_with_name' => ':name’s date', + + 'relationship_type_lover' => 'lover', + 'relationship_type_lover_female' => 'lover', + 'relationship_type_lover_male' => 'lover', + 'relationship_type_lover_with_name' => ':name’s lover', + 'relationship_type_lover_female_with_name' => ':name’s lover', + 'relationship_type_lover_male_with_name' => ':name’s lover', + + 'relationship_type_inlovewith' => 'in love with', + 'relationship_type_inlovewith_female' => 'in love with', + 'relationship_type_inlovewith_male' => 'in love with', + 'relationship_type_inlovewith_with_name' => 'someone :name is in love with', + 'relationship_type_inlovewith_female_with_name' => 'someone :name is in love with', + 'relationship_type_inlovewith_male_with_name' => 'someone :name is in love with', + + 'relationship_type_lovedby' => 'loved by', + 'relationship_type_lovedby_female' => 'loved by', + 'relationship_type_lovedby_male' => 'loved by', + 'relationship_type_lovedby_with_name' => ':name’s secret lover', + 'relationship_type_lovedby_female_with_name' => ':name’s secret lover', + 'relationship_type_lovedby_male_with_name' => ':name’s secret lover', + + 'relationship_type_ex' => 'ex-partner', + 'relationship_type_ex_female' => 'ex-girlfriend', + 'relationship_type_ex_male' => 'ex-boyfriend', + 'relationship_type_ex_with_name' => ':name’s ex-partner', + 'relationship_type_ex_female_with_name' => ':name’s ex-girlfriend', + 'relationship_type_ex_male_with_name' => ':name’s ex-boyfriend', + + 'relationship_type_parent' => 'parent', + 'relationship_type_parent_female' => 'мать', + 'relationship_type_parent_male' => 'father', + 'relationship_type_parent_with_name' => ':name’s parent', + 'relationship_type_parent_female_with_name' => ':name’s mother', + 'relationship_type_parent_male_with_name' => ':name’s father', + + 'relationship_type_child' => 'child', + 'relationship_type_child_female' => 'дочь', + 'relationship_type_child_male' => 'son', + 'relationship_type_child_with_name' => ':name’s child', + 'relationship_type_child_female_with_name' => ':name’s daughter', + 'relationship_type_child_male_with_name' => ':name’s son', + + 'relationship_type_stepparent' => 'step-parent', + 'relationship_type_stepparent_female' => 'мачеха', + 'relationship_type_stepparent_male' => 'stepfather', + 'relationship_type_stepparent_with_name' => ':name’s step-parent', + 'relationship_type_stepparent_female_with_name' => ':name’s stepmother', + 'relationship_type_stepparent_male_with_name' => ':name’s stepfather', + + 'relationship_type_stepchild' => 'stepchild', + 'relationship_type_stepchild_female' => 'stepdaughter', + 'relationship_type_stepchild_male' => 'stepson', + 'relationship_type_stepchild_with_name' => ':name’s stepchild', + 'relationship_type_stepchild_female_with_name' => ':name’s stepdaughter', + 'relationship_type_stepchild_male_with_name' => ':name’s stepson', + + 'relationship_type_sibling' => 'sibling', + 'relationship_type_sibling_female' => 'сестра', + 'relationship_type_sibling_male' => 'brother', + 'relationship_type_sibling_with_name' => ':name’s sibling', + 'relationship_type_sibling_female_with_name' => ':name’s sister', + 'relationship_type_sibling_male_with_name' => ':name’s brother', + + 'relationship_type_grandparent' => 'grandparent', + 'relationship_type_grandparent_female' => 'grandmother', + 'relationship_type_grandparent_male' => 'grandfather', + 'relationship_type_grandparent_with_name' => ':name’s grandparent', + 'relationship_type_grandparent_female_with_name' => ':name’s grandmother', + 'relationship_type_grandparent_male_with_name' => ':name’s grandfather', + + 'relationship_type_grandchild' => 'grandchild', + 'relationship_type_grandchild_female' => 'granddaughter', + 'relationship_type_grandchild_male' => 'grandson', + 'relationship_type_grandchild_with_name' => ':name’s grandchild', + 'relationship_type_grandchild_female_with_name' => ':name’s granddauther', + 'relationship_type_grandchild_male_with_name' => ':name’s grandson', + + 'relationship_type_uncle' => 'дядя', + 'relationship_type_uncle_female' => 'тётя', + 'relationship_type_uncle_male' => 'uncle', + 'relationship_type_uncle_with_name' => ':name’s uncle', + 'relationship_type_uncle_female_with_name' => ':name’s aunt', + 'relationship_type_uncle_male_with_name' => ':name’s uncle', + + 'relationship_type_nephew' => 'nephew', + 'relationship_type_nephew_female' => 'niece', + 'relationship_type_nephew_male' => 'nephew', + 'relationship_type_nephew_with_name' => ':name’s nephew', + 'relationship_type_nephew_female_with_name' => ':name’s niece', + 'relationship_type_nephew_male_with_name' => ':name’s nephew', + + 'relationship_type_cousin' => 'cousin', + 'relationship_type_cousin_female' => 'cousin', + 'relationship_type_cousin_male' => 'cousin', + 'relationship_type_cousin_with_name' => ':name’s cousin', + 'relationship_type_cousin_female_with_name' => ':name’s cousin', + 'relationship_type_cousin_male_with_name' => ':name’s cousin', + + 'relationship_type_godfather' => 'godparent', + 'relationship_type_godfather_female' => 'крёстная мать', + 'relationship_type_godfather_male' => 'godfather', + 'relationship_type_godfather_with_name' => ':name’s godparent', + 'relationship_type_godfather_female_with_name' => ':name’s godmother', + 'relationship_type_godfather_male_with_name' => ':name’s godfather', + + 'relationship_type_godson' => 'godchild', + 'relationship_type_godson_female' => 'goddaughter', + 'relationship_type_godson_male' => 'godson', + 'relationship_type_godson_with_name' => ':name’s godchild', + 'relationship_type_godson_female_with_name' => ':name’s goddaughter', + 'relationship_type_godson_male_with_name' => ':name’s godson', + + 'relationship_type_friend' => 'friend', + 'relationship_type_friend_female' => 'friend', + 'relationship_type_friend_male' => 'friend', + 'relationship_type_friend_with_name' => ':name’s friend', + 'relationship_type_friend_female_with_name' => ':name’s friend', + 'relationship_type_friend_male_with_name' => ':name’s friend', + + 'relationship_type_bestfriend' => 'best friend', + 'relationship_type_bestfriend_female' => 'best friend', + 'relationship_type_bestfriend_male' => 'best friend', + 'relationship_type_bestfriend_with_name' => ':name’s best friend', + 'relationship_type_bestfriend_female_with_name' => ':name’s best friend', + 'relationship_type_bestfriend_male_with_name' => ':name’s best friend', + + 'relationship_type_colleague' => 'коллега', + 'relationship_type_colleague_female' => 'коллега', + 'relationship_type_colleague_male' => 'colleague', + 'relationship_type_colleague_with_name' => ':name’s colleague', + 'relationship_type_colleague_female_with_name' => ':name’s colleague', + 'relationship_type_colleague_male_with_name' => ':name’s colleague', + + 'relationship_type_boss' => 'boss', + 'relationship_type_boss_female' => 'boss', + 'relationship_type_boss_male' => 'boss', + 'relationship_type_boss_with_name' => ':name’s boss', + 'relationship_type_boss_female_with_name' => ':name’s boss', + 'relationship_type_boss_male_with_name' => ':name’s boss', + + 'relationship_type_subordinate' => 'subordinate', + 'relationship_type_subordinate_female' => 'subordinate', + 'relationship_type_subordinate_male' => 'subordinate', + 'relationship_type_subordinate_with_name' => ':name’s subordinate', + 'relationship_type_subordinate_female_with_name' => ':name’s subordinate', + 'relationship_type_subordinate_male_with_name' => ':name’s subordinate', + + 'relationship_type_mentor' => 'mentor', + 'relationship_type_mentor_female' => 'mentor', + 'relationship_type_mentor_male' => 'mentor', + 'relationship_type_mentor_with_name' => ':name’s mentor', + 'relationship_type_mentor_female_with_name' => ':name’s mentor', + 'relationship_type_mentor_male_with_name' => ':name’s mentor', + + 'relationship_type_protege' => 'protégé', + 'relationship_type_protege_female' => 'protégé', + 'relationship_type_protege_male' => 'protégé', + 'relationship_type_protege_with_name' => ':name’s protégé', + 'relationship_type_protege_female_with_name' => ':name’s protégé', + 'relationship_type_protege_male_with_name' => ':name’s protégé', + + 'relationship_type_ex_husband' => 'ex-spouse', + 'relationship_type_ex_husband_female' => 'бывшая жена', + 'relationship_type_ex_husband_male' => 'ex-husband', + 'relationship_type_ex_husband_with_name' => ':name’s ex-spouse', + 'relationship_type_ex_husband_female_with_name' => ':name’s ex wife', + 'relationship_type_ex_husband_male_with_name' => ':name’s ex-husband', + + // emotions + 'emotion_primary_love' => 'Любовь', + 'emotion_primary_joy' => 'Радость', + 'emotion_primary_surprise' => 'Удивление', + 'emotion_primary_anger' => 'Гнев', + 'emotion_primary_sadness' => 'Грусть', + 'emotion_primary_fear' => 'Страх', + + 'emotion_secondary_affection' => 'Affection', + 'emotion_secondary_lust' => 'Lust', + 'emotion_secondary_longing' => 'Longing', + 'emotion_secondary_cheerfulness' => 'Cheerfulness', + 'emotion_secondary_zest' => 'Zest', + 'emotion_secondary_contentment' => 'Contentment', + 'emotion_secondary_pride' => 'Гордость', + 'emotion_secondary_optimism' => 'Оптимизм', + 'emotion_secondary_enthrallment' => 'Enthrallment', + 'emotion_secondary_relief' => 'Облегчение', + 'emotion_secondary_surprise' => 'Удивление', + 'emotion_secondary_irritation' => 'Раздражение', + 'emotion_secondary_exasperation' => 'Exasperation', + 'emotion_secondary_rage' => 'Ярость', + 'emotion_secondary_disgust' => 'Отвращение', + 'emotion_secondary_envy' => 'Зависть', + 'emotion_secondary_suffering' => 'Страдание', + 'emotion_secondary_sadness' => 'Грусть', + 'emotion_secondary_disappointment' => 'Разочарование', + 'emotion_secondary_shame' => 'Shame', + 'emotion_secondary_neglect' => 'Neglect', + 'emotion_secondary_sympathy' => 'Симпатия', + 'emotion_secondary_horror' => 'Ужас', + 'emotion_secondary_nervousness' => 'Nervousness', + + 'emotion_adoration' => 'Adoration', + 'emotion_affection' => 'Affection', + 'emotion_love' => 'Любовь', + 'emotion_fondness' => 'Fondness', + 'emotion_liking' => 'Liking', + 'emotion_attraction' => 'Attraction', + 'emotion_caring' => 'Caring', + 'emotion_tenderness' => 'Tenderness', + 'emotion_compassion' => 'Compassion', + 'emotion_sentimentality' => 'Sentimentality', + 'emotion_arousal' => 'Arousal', + 'emotion_desire' => 'Desire', + 'emotion_lust' => 'Lust', + 'emotion_passion' => 'Passion', + 'emotion_infatuation' => 'Infatuation', + 'emotion_longing' => 'Longing', + 'emotion_amusement' => 'Amusement', + 'emotion_bliss' => 'Bliss', + 'emotion_cheerfulness' => 'Cheerfulness', + 'emotion_gaiety' => 'Gaiety', + 'emotion_glee' => 'Glee', + 'emotion_jolliness' => 'Jolliness', + 'emotion_joviality' => 'Joviality', + 'emotion_joy' => 'Радость', + 'emotion_delight' => 'Delight', + 'emotion_enjoyment' => 'Enjoyment', + 'emotion_gladness' => 'Gladness', + 'emotion_happiness' => 'Happiness', + 'emotion_jubilation' => 'Jubilation', + 'emotion_elation' => 'Elation', + 'emotion_satisfaction' => 'Satisfaction', + 'emotion_ecstasy' => 'Ecstasy', + 'emotion_euphoria' => 'Euphoria', + 'emotion_enthusiasm' => 'Enthusiasm', + 'emotion_zeal' => 'Zeal', + 'emotion_zest' => 'Zest', + 'emotion_excitement' => 'Excitement', + 'emotion_thrill' => 'Thrill', + 'emotion_exhilaration' => 'Exhilaration', + 'emotion_contentment' => 'Contentment', + 'emotion_pleasure' => 'Pleasure', + 'emotion_pride' => 'Pride', + 'emotion_eagerness' => 'Eagerness', + 'emotion_hope' => 'Hope', + 'emotion_optimism' => 'Optimism', + 'emotion_enthrallment' => 'Enthrallment', + 'emotion_rapture' => 'Rapture', + 'emotion_relief' => 'Облегчение', + 'emotion_amazement' => 'Amazement', + 'emotion_surprise' => 'Surprise', + 'emotion_astonishment' => 'Astonishment', + 'emotion_aggravation' => 'Aggravation', + 'emotion_irritation' => 'Раздражение', + 'emotion_agitation' => 'Agitation', + 'emotion_annoyance' => 'Annoyance', + 'emotion_grouchiness' => 'Grouchiness', + 'emotion_grumpiness' => 'Grumpiness', + 'emotion_exasperation' => 'Exasperation', + 'emotion_frustration' => 'Frustration', + 'emotion_anger' => 'Anger', + 'emotion_rage' => 'Rage', + 'emotion_outrage' => 'Outrage', + 'emotion_fury' => 'Fury', + 'emotion_wrath' => 'Wrath', + 'emotion_hostility' => 'Hostility', + 'emotion_ferocity' => 'Ferocity', + 'emotion_bitterness' => 'Bitterness', + 'emotion_hate' => 'Hate', + 'emotion_loathing' => 'Loathing', + 'emotion_scorn' => 'Scorn', + 'emotion_spite' => 'Spite', + 'emotion_vengefulness' => 'Vengefulness', + 'emotion_dislike' => 'Dislike', + 'emotion_resentment' => 'Resentment', + 'emotion_disgust' => 'Disgust', + 'emotion_revulsion' => 'Revulsion', + 'emotion_contempt' => 'Contempt', + 'emotion_envy' => 'Envy', + 'emotion_jealousy' => 'Jealousy', + 'emotion_agony' => 'Agony', + 'emotion_suffering' => 'Suffering', + 'emotion_hurt' => 'Hurt', + 'emotion_anguish' => 'Anguish', + 'emotion_depression' => 'Depression', + 'emotion_despair' => 'Despair', + 'emotion_hopelessness' => 'Hopelessness', + 'emotion_gloom' => 'Gloom', + 'emotion_glumness' => 'Glumness', + 'emotion_sadness' => 'Sadness', + 'emotion_unhappiness' => 'Unhappiness', + 'emotion_grief' => 'Grief', + 'emotion_sorrow' => 'Sorrow', + 'emotion_woe' => 'Woe', + 'emotion_misery' => 'Misery', + 'emotion_melancholy' => 'Melancholy', + 'emotion_dismay' => 'Dismay', + 'emotion_disappointment' => 'Disappointment', + 'emotion_displeasure' => 'Displeasure', + 'emotion_guilt' => 'Guilt', + 'emotion_shame' => 'Shame', + 'emotion_regret' => 'Regret', + 'emotion_remorse' => 'Remorse', + 'emotion_alienation' => 'Alienation', + 'emotion_isolation' => 'Isolation', + 'emotion_neglect' => 'Neglect', + 'emotion_loneliness' => 'Loneliness', + 'emotion_rejection' => 'Rejection', + 'emotion_homesickness' => 'Homesickness', + 'emotion_defeat' => 'Defeat', + 'emotion_dejection' => 'Dejection', + 'emotion_insecurity' => 'Insecurity', + 'emotion_embarrassment' => 'Embarrassment', + 'emotion_humiliation' => 'Humiliation', + 'emotion_insult' => 'Insult', + 'emotion_pity' => 'Pity', + 'emotion_sympathy' => 'Sympathy', + 'emotion_alarm' => 'Alarm', + 'emotion_shock' => 'Shock', + 'emotion_fear' => 'Fear', + 'emotion_fright' => 'Fright', + 'emotion_horror' => 'Horror', + 'emotion_terror' => 'Terror', + 'emotion_panic' => 'Panic', + 'emotion_hysteria' => 'Hysteria', + 'emotion_mortification' => 'Mortification', + 'emotion_anxiety' => 'Anxiety', + 'emotion_nervousness' => 'Nervousness', + 'emotion_tenseness' => 'Tenseness', + 'emotion_uneasiness' => 'Uneasiness', + 'emotion_apprehension' => 'Apprehension', + 'emotion_worry' => 'Worry', + 'emotion_distress' => 'Distress', + 'emotion_dread' => 'Dread', + + // weather + 'weather_sunny' => 'Sunny', + 'weather_clear' => 'Clear', + 'weather_clear-day' => 'Clear', + 'weather_clear-night' => 'Clear night', + 'weather_light-drizzle' => 'Light drizzle', + 'weather_patchy-light-drizzle' => 'Patchy light drizzle', + 'weather_patchy-light-rain' => 'Patchy light rain', + 'weather_light-rain' => 'Light rain', + 'weather_moderate-rain-at-times' => 'Moderate rain at times', + 'weather_moderate-rain' => 'Moderate rain', + 'weather_patchy-rain-possible' => 'Patchy rain possible', + 'weather_heavy-rain-at-times' => 'Heavy rain at times', + 'weather_heavy-rain' => 'Heavy rain', + 'weather_light-freezing-rain' => 'Light freezing rain', + 'weather_moderate-or-heavy-freezing-rain' => 'Moderate or heavy freezing rain', + 'weather_light-sleet' => 'Light sleet', + 'weather_moderate-or-heavy-rain-shower' => 'Moderate or heavy rain shower', + 'weather_light-rain-shower' => 'Light rain shower', + 'weather_torrential-rain-shower' => 'Torrential rain shower', + 'weather_rain' => 'Rain', + 'weather_snow' => 'Snow', + 'weather_blowing-snow' => 'Blowing snow', + 'weather_patchy-light-snow' => 'Patchy light snow', + 'weather_light-snow' => 'Light snow', + 'weather_patchy-moderate-snow' => 'Patchy moderate snow', + 'weather_moderate-snow' => 'Moderate snow', + 'weather_patchy-heavy-snow' => 'Patchy heavy snow', + 'weather_heavy-snow' => 'Heavy snow', + 'weather_light-snow-showers' => 'Light snow showers', + 'weather_moderate-or-heavy-snow-showers' => 'Moderate or heavy snow showers', + 'weather_patchy-snow-possible' => 'Patchy snow possible', + 'weather_patchy-sleet-possible' => 'Patchy sleet possible', + 'weather_moderate-or-heavy-sleet' => 'Moderate or heavy sleet', + 'weather_light-sleet-showers' => 'Light sleet showers', + 'weather_moderate-or-heavy-sleet-showers' => 'Moderate or heavy sleet showers', + 'weather_sleet' => 'Sleet', + 'weather_wind' => 'Wind', + 'weather_fog' => 'Fog', + 'weather_freezing-fog' => 'Freezing fog', + 'weather_mist' => 'Mist', + 'weather_blizzard' => 'Blizzard', + 'weather_overcast' => 'Overcast', + 'weather_cloudy' => 'Cloudy', + 'weather_partly-cloudy-day' => 'Partly cloudy', + 'weather_partly-cloudy-night' => 'Partly cloudy', + 'weather_freezing-drizzle' => 'Freezing drizzle', + 'weather_heavy-freezing-drizzle' => 'Heavy freezing drizzle', + 'weather_patchy-freezing-drizzle-possible' => 'Patchy freezing drizzle possible', + 'weather_ice-pellets' => 'Ice pellets', + 'weather_light-showers-of-ice-pellets' => 'Light showers of ice pellets', + 'weather_moderate-or-heavy-showers-of-ice-pellets' => 'Moderate or heavy showers of ice pellets', + 'weather_thundery-outbreaks-possible' => 'Thundery outbreaks possible', + 'weather_patchy-light-rain-with-thunder' => 'Patchy light rain with thunder', + 'weather_moderate-or-heavy-rain-with-thunder' => 'Moderate or heavy rain with thunder', + 'weather_patchy-light-snow-with-thunder' => 'Patchy light snow with thunder', + 'weather_moderate-or-heavy-snow-with-thunder' => 'Moderate or heavy snow with thunder', + 'weather_current_temperature_celsius' => ':temperature °C', + 'weather_current_temperature_fahrenheit' => ':temperature °F', + 'weather_current_title' => 'Текущая погода', + + // dav + 'dav_contacts' => 'Контакты', + 'dav_contacts_description' => 'Контакты :name', + 'dav_birthdays' => 'Дни рождения', + 'dav_birthdays_description' => ':name’s contact’s birthdays', + 'dav_tasks' => 'Задачи', + 'dav_tasks_description' => 'Задачи :name', + + // contact list + 'contact_list_avatar' => 'Avatar', + 'contact_list_name' => 'Contact', + 'contact_list_description' => 'Description', + +]; diff --git a/resources/lang/ru/auth.php b/resources/lang/ru/auth.php new file mode 100644 index 0000000..6512bfe --- /dev/null +++ b/resources/lang/ru/auth.php @@ -0,0 +1,89 @@ + 'Имя пользователя и пароль не совпадают.', + 'throttle' => 'Слишком много попыток входа. Пожалуйста, попробуйте еще раз через :seconds секунд.', + 'not_authorized' => 'Вам не разрешено выполнять это действие.', + 'signup_disabled' => 'Регистрация сейчас выключена.', + 'signup_error' => 'Произошла ошибка при регистрации пользователя', + 'back_homepage' => 'Вернуться на главную страницу', + 'mfa_auth_otp' => 'Аутентифицироваться с двухфакторным устройством', + 'mfa_auth_webauthn' => 'Аутентифицироваться с ключом безопасности (WebAuthn)', + '2fa_title' => 'Двухфакторная аутентификация', + '2fa_wrong_validation' => 'Сбой двухфакторной аутентификации.', + '2fa_one_time_password' => 'Код двухфакторной аутентификации', + '2fa_recuperation_code' => 'Введите код двухфакторного восстановления', + '2fa_one_time_or_recuperation' => 'Введите код двухфакторной аутентификации или код восстановления', + '2fa_otp_help' => 'Откройте приложение для мобильной проверки подлинности и скопируйте код', + + 'login_to_account' => 'Войти в свою учетную запись', + 'login_with_recovery' => 'Войти с помощью кода восстановления', + 'login_again' => 'Пожалуйста, войдите снова в свою учетную запись', + 'email' => 'Адрес электронной почты', + 'password' => 'Пароль', + 'recovery' => 'Код восстановления', + 'login' => 'Вход', + 'button_remember' => 'Запомнить меня', + 'password_forget' => 'Забыли пароль?', + 'password_reset' => 'Сбросить пароль', + 'use_recovery' => 'Или вы можете использовать код восстановления', + 'signup_no_account' => 'Нет аккаунта?', + 'signup' => 'Регистрация', + 'create_account' => 'Создайте первую учетную запись, зарегистрировав', + 'change_language_title' => 'Изменить язык:', + 'change_language' => 'Изменить язык на :lang', + + 'password_reset_title' => 'Восстановить пароль', + 'password_reset_email' => 'E-Mail Адрес', + 'password_reset_send_link' => 'Отправить ссылку для сброса пароля', + 'password_reset_password' => 'Пароль', + 'password_reset_password_confirm' => 'Подтверждение пароля', + 'password_reset_action' => 'Восстановить пароль', + 'password_reset_email_content' => 'Нажмите, чтобы сбросить пароль:', + + 'register_title_welcome' => 'Добро пожаловать в ваш вновь установленный экземпляр Monica', + 'register_create_account' => 'Для использования Monica вам нужно создать аккаунт', + 'register_title_create' => 'Создайте свой аккаунт Monica', + 'register_login' => 'Войдите, если у вас уже есть аккаунт.', + 'register_email' => 'Введите действительный адрес электронной почты', + 'register_email_example' => 'you@home', + 'register_firstname' => 'Имя', + 'register_firstname_example' => 'например, Иван', + 'register_lastname' => 'Фамилия', + 'register_lastname_example' => 'например, Иванов', + 'register_password' => 'Пароль', + 'register_password_example' => 'Введите сложный пароль', + 'register_password_confirmation' => 'Подтверждение пароля', + 'register_action' => 'Регистрация', + 'register_policy' => 'Регистрация означает, что вы прочитали и согласны с Политикой конфиденциальности и Условиями использования.', + 'register_invitation_email' => 'В целях безопасности укажите, пожалуйста, электронное письмо с приглашенным вами человеком. Эта информация приведена в приглашённом письме.', + + 'confirmation_title' => 'Подтвердите ваш адрес электронной почты', + 'confirmation_fresh' => 'На ваш адрес электронной почты выслана ссылка для подтверждения.', + 'confirmation_check' => 'Прежде чем продолжить, пожалуйста, проверьте вашу электронную почту на наличие проверочной ссылки.', + 'confirmation_request_another' => 'Если вы не получили письмо , нажмите здесь, чтобы запросить другой.', + + 'confirmation_again' => 'Если вы хотите изменить свой адрес электронной почты, нажмите здесь.', + 'email_change_current_email' => 'Текущий адрес электронной почты:', + 'email_change_title' => 'Изменить адрес электронной почты', + 'email_change_new' => 'Новый адрес электронной почты', + 'email_changed' => 'Ваш адрес электронной почты изменен. Проверьте свой почтовый ящик, чтобы подтвердить его.', +]; diff --git a/resources/lang/ru/changelog.php b/resources/lang/ru/changelog.php new file mode 100644 index 0000000..184e33e --- /dev/null +++ b/resources/lang/ru/changelog.php @@ -0,0 +1,12 @@ + 'Изменения', + 'note' => 'Примечание: к сожалению, эта страница только на английском языке.', +]; diff --git a/resources/lang/ru/dashboard.php b/resources/lang/ru/dashboard.php new file mode 100644 index 0000000..271644a --- /dev/null +++ b/resources/lang/ru/dashboard.php @@ -0,0 +1,42 @@ + 'Добро пожаловать в ваш аккаунт!', + 'dashboard_blank_description' => 'Моника это место, чтобы организовать все взаимодействия, которые у вас есть с людьми, о которых вы заботитесь.', + 'dashboard_blank_cta' => 'Добавьте ваш первый контакт', + 'dashboard_blank_illustration' => 'Иллюстрация Freepik', + + 'notes_title' => 'У вас пока нет помеченных заметок.', + + 'tab_recent_calls' => 'Недавние вызовы', + 'tab_favorite_notes' => 'Избранные заметки', + 'tab_calls_blank' => 'Вы еще не внесли ни одного звонка.', + 'tab_debts' => 'Долги', + 'tab_debts_blank' => 'Вы еще не зарегистрировали ни одного долга.', + 'tab_tasks' => 'Задачи', + 'tab_tasks_blank' => 'У Вас еще нет задач.', + + 'tasks_add_task_placeholder' => 'Что это за задача?', + 'tasks_tab_your_contacts' => 'Задачи, связанные с вашими контактами', + 'tasks_tab_your_tasks' => 'Ваши задачи', + 'tasks_add_note' => 'Нажмите Enter, чтобы добавить задачу.', + 'task_add_cta' => 'Добавить задачу', + + 'debts_you_owe' => 'Вы должны', + + 'statistics_contacts' => 'Контакты', + 'statistics_activities' => 'Активности', + 'statistics_gifts' => 'Подарки', + + 'reminders_next_months' => 'События в ближайшие 3 месяца', + 'reminders_none' => 'Нет напоминаний в этом месяце.', + + 'product_changes' => 'Изменения', + 'product_view_details' => 'Детали', +]; diff --git a/resources/lang/ru/format.php b/resources/lang/ru/format.php new file mode 100644 index 0000000..99afe47 --- /dev/null +++ b/resources/lang/ru/format.php @@ -0,0 +1,36 @@ + 'd M Y H:i', + 'short_date_year' => 'd M Y', + 'short_date' => 'd M', + 'short_month' => 'M', + 'short_month_year' => 'M Y', + 'short_day' => 'D', + 'full_date_year' => 'd F Y', + 'full_month' => 'F', + 'full_month_year' => 'F Y', + 'full_hour' => 'H:i', + + 'short_text' => '{text}...', +]; diff --git a/resources/lang/ru/journal.php b/resources/lang/ru/journal.php new file mode 100644 index 0000000..0509c50 --- /dev/null +++ b/resources/lang/ru/journal.php @@ -0,0 +1,38 @@ + 'Как был ваш день? Вы можете оценить его один раз в день.', + 'journal_come_back' => 'Спасибо. Вернитесь завтра, чтобы снова оценить свой день.', + 'journal_description' => 'Примечание: в журнале перечислены как записи в журнале вручную, так и автоматические записи, такие как действия, выполняемые с вашими контактами. Хотя вы можете удалить записи журнала вручную, вам придется удалить их непосредственно на странице контактов.', + 'journal_add' => 'Добавить запись в журнал', + 'journal_edit' => 'Редактировать запись журнала', + 'journal_empty' => 'Пустой журнал', + 'journal_created_at' => 'Создано в {date}', + 'journal_created_automatically' => 'Создано автоматически', + 'journal_entry_type_journal' => 'Запись журнала', + 'journal_entry_type_activity' => 'Активность', + 'journal_entry_rate' => 'Вы оценили свой день.', + 'journal_add_comment' => 'Хотите добавить комментарий (необязательно)?', + 'journal_show_comment' => 'Показать комментарий', + 'entry_delete_success' => 'Запись была удалена.', + 'journal_add_title' => 'Заголовок (не обязательно)', + 'journal_add_date' => 'Дата', + 'journal_add_post' => 'Содержимое', + 'journal_add_cta' => 'Сохранить', + 'journal_blank_cta' => 'Добавить вашу первую запись в журнал', + 'journal_blank_description' => 'В журнал вы можете добавлять записи о событиях в вашей жизни, чтобы сохранить их.', + 'delete_confirmation' => 'Вы уверены что хотите удалить эту запись?', + 'apply_filter' => 'Apply filter', + 'start_date' => 'Start Date', + 'end_date' => 'End Date', + 'per_page' => 'Per Page', + 'sort_order' => 'Sort By Created At', + 'ascending' => 'Ascending', + 'descending' => 'Descending', +]; diff --git a/resources/lang/ru/logs.php b/resources/lang/ru/logs.php new file mode 100644 index 0000000..bd99eb7 --- /dev/null +++ b/resources/lang/ru/logs.php @@ -0,0 +1,29 @@ + 'Создан контакт.', + 'settings_log_contact_created_with_name' => 'Добавлено :name как контакт.', + + // contat description update + 'contact_log_contact_description_updated' => 'Обновлено описание.', + 'settings_log_contact_description_updated_with_name' => 'Описание :name обновлено.', + + // contact description clear + 'contact_log_contact_description_cleared' => 'Описание очищено.', + 'settings_log_contact_description_cleared_with_name' => 'Описание :name очищено.', + + // contact work information update + 'contact_log_contact_work_updated' => 'Обновленная информация о работе.', + 'settings_log_contact_work_updated_with_name' => 'Обновленная информация о работе :name.', + + // company created + 'settings_log_company_created' => 'Создана компания с именем :name.', +]; diff --git a/resources/lang/ru/mail.php b/resources/lang/ru/mail.php new file mode 100644 index 0000000..6659ff5 --- /dev/null +++ b/resources/lang/ru/mail.php @@ -0,0 +1,53 @@ + 'Напоминание для :contact', + 'greetings' => 'Привет :username', + 'want_reminded_of' => 'Вы хотели быть уведомлены о :reason', + 'for' => 'Для: :name', + 'comment' => 'Комментарий: :comment', + 'footer_contact_info' => 'Добавить, просмотреть, завершить и изменить информацию об этом контакте', + 'footer_contact_info2' => 'Смотрите профиль :name', + 'footer_contact_info2_link' => 'Смотрите профиль :name: :url', + + 'notification_subject_line' => 'У вас есть предстоящее событие', + 'notification_description' => 'В :count дней (на :date) произойдет следующее событие:', + + 'stay_in_touch_subject_line' => 'Оставайтесь на связи с :name', + 'stay_in_touch_subject_description' => 'Вы попросили напоминать о том, чтобы оставаться на связи с :name каждые :frequency день. Вы попросили напоминать о том, чтобы оставаться на связи с :name каждые :frequency дней.', + + 'notifications_whoops' => 'Упс!', + 'notifications_hello' => 'Привет!', + 'notifications_regards' => 'С уважением', + 'notifications_footer' => 'Если у вас возникли проблемы с нажатием на кнопку «:actionText», скопируйте и вставьте следующий URL-адрес в веб-браузер: [:actionURL](:actionURL)', + 'notifications_rights' => 'Все права защищены', + + 'confirmation_email_title' => 'Monica – проверка электронной почты', + 'confirmation_email_intro'=> 'Для подтверждения электронной почты нажмите на кнопку ниже', + 'confirmation_email_button' => 'Подтвердите email', + 'confirmation_email_bottom' => 'Если вы не регистрировали аккаунт, никаких дальнейших действий не требуется.', + + 'password_reset_title' => 'Monica – сброс пароля уведомления', + 'password_reset_intro' => 'Вы получили это письмо, потому что мы получили запрос на сброс пароля для вашего аккаунта.', + 'password_reset_button' => 'Сбросить пароль', + 'password_reset_expiration' => 'Эта ссылка для сброса пароля истекает через :count минут.', + 'password_reset_bottom' => 'Если вы не запрашивали сброс пароля, никаких дальнейших действий не требуется.', + + 'invitation_title' => 'Monica – Вас пригласил :name', + 'invitation_intro' => 'Вы были приглашены :name (:email) для использования Monica, отличного инструмента управления личными отношениями.', + 'invitation_link' => 'Чтобы принять приглашение, нажмите на ссылку ниже:', + 'invitation_button' => 'Принять приглашение', + 'invitation_expiration' => 'Срок действия этой ссылки истекает через :count дней.', + + 'export_title' => 'Your export is ready', + 'export_description' => 'You requested a data export on :date. It is now ready to download.', + 'export_download' => 'Download export', + +]; diff --git a/resources/lang/ru/pagination.php b/resources/lang/ru/pagination.php new file mode 100644 index 0000000..2ccb8fa --- /dev/null +++ b/resources/lang/ru/pagination.php @@ -0,0 +1,25 @@ + '❮ Назад', + 'next' => 'Вперёд ❯', + +]; diff --git a/resources/lang/ru/passwords.php b/resources/lang/ru/passwords.php new file mode 100644 index 0000000..0ad5bdb --- /dev/null +++ b/resources/lang/ru/passwords.php @@ -0,0 +1,30 @@ + 'Ваш пароль был сброшен!', + 'sent' => 'Ссылка на сброс пароля была отправлена.', + 'token' => 'Ошибочный код сброса пароля.', + 'user' => 'Ссылка на сброс пароля была отправлена.', + 'changed' => 'Пароль успешно изменен.', + 'invalid' => 'Введённый вами пароль неверный.', + 'throttled' => 'Пожалуйста, подождите перед повторной попыткой.', + +]; diff --git a/resources/lang/ru/people.php b/resources/lang/ru/people.php new file mode 100644 index 0000000..201e75c --- /dev/null +++ b/resources/lang/ru/people.php @@ -0,0 +1,539 @@ + 'Контакт не найден', + 'people_list_number_kids' => ':count ребёнок|:count детей', + 'people_list_last_updated' => 'Последнее обновление:', + 'people_list_number_reminders' => ':count напоминание|:count напоминаний', + 'people_list_blank_title' => 'Вы пока ни кого ещё не добавили', + 'people_list_blank_cta' => 'Добавить кого нибудь', + 'people_list_sort' => 'Сортировка', + 'people_list_stats' => ':count контакт|:count контактов', + 'people_list_firstnameAZ' => 'Сортировать по имени А → Я', + 'people_list_firstnameZA' => 'Сортировать по имени Я → А', + 'people_list_lastnameAZ' => 'Сортировать по фамилии А → Я', + 'people_list_lastnameZA' => 'Сортировать по фамилии Я → А', + 'people_list_lastactivitydateNewtoOld' => 'Сортировать по дате последней активности, от новых к старым', + 'people_list_lastactivitydateOldtoNew' => 'Сортировать по дате последней активности, от старых к новым', + 'people_list_filter_tag' => 'Показываются все контакты помеченные тэгом', + 'people_list_clear_filter' => 'Очистить фильтр', + 'people_list_contacts_per_tags' => ':count контакт|:count контактов', + 'people_list_show_dead' => 'Показать умерших людей (:count)', + 'people_list_hide_dead' => 'Скрыть умерших людей (:count)', + 'people_search' => 'Поиск контактов…', + 'people_search_no_results' => 'Ничего не найдено', + 'people_search_next' => 'Вперёд', + 'people_search_prev' => 'Предыдущий', + 'people_search_rows_per_page' => 'Строк на страницу', + 'people_search_of' => 'из', + 'people_search_page' => 'Страница', + 'people_search_all' => 'Все', + 'people_add_new' => 'Добавить', + 'people_list_account_usage' => 'Лимиты контактов: :current/:limit', + 'people_list_account_upgrade_title' => 'Перейдите на другой план чтобы получить больше возможностей.', + 'people_list_account_upgrade_cta' => 'Обновить сейчас', + 'people_list_untagged' => 'Просмотр контактов без тегов', + 'people_list_filter_untag' => 'Показаны все контакты без тегов', + 'archived_contact_readonly' => 'Архивированный контакт не может быть отредактирован, сначала разархивируйте его.', + + // people add + 'people_add_title' => 'Добавить человека', + 'people_add_missing' => 'Контакт не найден - добавьте новый сейчас', + 'people_add_firstname' => 'Имя', + 'people_add_middlename' => 'Отчество (не обязательно)', + 'people_add_lastname' => 'Фамилия (по желанию)', + 'people_add_email' => 'Электронная почта (по желанию)', + 'people_add_nickname' => 'Псевдоним (по желанию)', + 'people_add_cta' => 'Добавить', + 'people_save_and_add_another_cta' => 'Отправить и добавить кого-то еще', + 'people_add_success' => 'Контакт :name успешно создан', + 'people_add_gender' => 'Пол', + 'people_delete_success' => 'Контакт был удалён', + 'people_delete_message' => 'Удалить контакт', + 'people_delete_confirmation' => 'Вы уверены что хотите удалить этот контакт? Восстановление невозможно.', + 'people_add_birthday_reminder' => 'Поздравить :name с днём рождения', + 'people_add_birthday_reminder_deceased' => 'В этот день, :name будет отмечать свой день рождения', + 'people_add_import' => 'Вы хотите импортировать ваши контакты?', + 'people_edit_email_error' => 'В вашей учетной записи с таким адресом электронной почты уже есть контакт. Пожалуйста, выберите другой.', + 'people_export' => 'Экспортировать как vCard', + 'people_add_reminder_for_birthday' => 'Создать ежегодное напоминание о дне рождения', + + // show + 'section_contact_information' => 'Контактная информация', + 'section_personal_activities' => 'Активности', + 'section_personal_reminders' => 'Напоминания', + 'section_personal_tasks' => 'Задачи', + 'section_personal_gifts' => 'Подарки', + 'section_personal_notes' => 'Заметки', + + // archived contacts + 'list_link_to_active_contacts' => 'Вы просматриваете архивные контакты. Просмотреть список активных контактов.', + 'list_link_to_archived_contacts' => 'Список архивированных контактов', + + // Header + 'me' => 'Это вы', + 'edit_contact_information' => 'Редактировать контакты', + 'contact_archive' => 'Архивировать контакт', + 'contact_unarchive' => 'Разархивировать контакт', + 'contact_archive_help' => 'Архивированные контакты не будут отображаться в списке контактов, но будут появляться в результатах поиска.', + 'call_button' => 'Зафиксировать звонок', + 'set_favorite' => 'Избранные контакты размещаются в верхней части списка контактов', + + // Stay in touch + 'stay_in_touch' => 'Оставаться на связи', + 'stay_in_touch_frequency' => 'Будьте на связи каждый день|Оставайтесь на связи каждые {count} дней', + 'stay_in_touch_next_date' => 'Следующая дата: {date}', + 'stay_in_touch_invalid' => 'Частота должна быть числом больше 0.', + 'stay_in_touch_premium' => 'Вам нужно обновить свой аккаунт, чтобы использовать эту функцию', + 'stay_in_touch_modal_title' => 'Оставаться на связи', + 'stay_in_touch_modal_desc' => 'Мы можем напомнить вам по электронной почте связаться с {firstname} через одинаковый интервал.', + 'stay_in_touch_modal_label' => 'Присылать мне письма каждый… {count} день|Присылать мне письмо каждые… {count} дней', + + // Calls + 'modal_call_title' => 'Зафиксировать звонок', + 'modal_call_comment' => 'О чём вы разговаривали? (не обяз.)', + 'modal_call_exact_date' => 'Дата звонка', + 'modal_call_who_called' => 'Кто звонил?', + 'modal_call_emotion' => 'Вы хотите записать как вы чувствовали себя во время этого звонка? (опционально)', + 'calls_add_success' => 'Звонок сохранён.', + 'call_delete_confirmation' => 'Вы уверены что хотите удалить звонок?', + 'call_delete_success' => 'Звонок был удалён', + 'call_title' => 'Телефонные звонки', + 'call_empty_comment' => 'Нет деталей', + 'call_blank_title' => 'Отслеживать телефонные переговоры с {name}', + 'call_blank_desc' => 'Вы звонили {name}', + 'call_you_called' => 'Вы звонили', + 'call_he_called' => '{name} звонил(а)', + 'call_emotions' => 'Эмоции:', + + // Conversation + 'conversation_blank' => 'Записывать беседы с :name в соцсетях, SMS…', + 'conversation_delete_link' => 'Удалить разговор', + 'conversation_edit_title' => 'Изменить разговор', + 'conversation_edit_delete' => 'Вы уверены что хотите удалить этот разговор? Восстановление невозможно.', + 'conversation_add_success' => 'Разговор успешно добавлен.', + 'conversation_edit_success' => 'Разговор успешно обновлен.', + 'conversation_delete_success' => 'Разговор успешно удалён.', + 'conversation_add_title' => 'Записать беседу', + 'conversation_add_when' => 'Когда у вас был этот разговор?', + 'conversation_add_who_wrote' => 'От кого это сообщение?', + 'conversation_add_how' => 'Как вы общались?', + 'conversation_add_you' => 'Вы', + 'conversation_add_content' => 'Запишите о чем говорилось', + 'conversation_add_what_was_said' => 'Что вы сказали?', + 'conversation_add_another' => 'Добавить еще одно сообщение', + 'conversation_add_error' => 'Вы должны добавить хотя бы одно сообщение.', + 'conversation_list_table_messages' => 'Сообщения', + 'conversation_list_table_content' => 'Часть содержимого (последнее сообщение)', + 'conversation_list_title' => 'Разговоры', + 'conversation_list_cta' => 'Записать разговор', + + // age - birthday + 'birthdate_not_set' => 'Не указан день рождения', + 'age_approximate_in_years' => 'примерно :age лет', + 'age_exact_in_years' => ':age лет', + 'age_exact_birthdate' => 'день рожнения: :date', + + // Last called + 'last_called' => 'Последний звонок: :date', + 'last_talked_to' => 'Последний звонок: {date}', + 'last_called_empty' => 'Последний звонок: неизвестно', + 'last_activity_date' => 'Последняя активность вместе: :date', + 'last_activity_date_empty' => 'Последняя активность вместе: неизвестно', + + // additional information + 'information_edit_success' => 'Профиль был успешно обновлён', + 'information_edit_title' => 'Редактировать данные :name', + 'information_edit_max_size' => 'До :size Кб.', + 'information_edit_max_size2' => 'Макс. {size} Кб.', + 'information_edit_firstname' => 'Имя', + 'information_edit_lastname' => 'Фамилия (не обязательно)', + 'information_edit_description' => 'Описание (не обязательно)', + 'information_edit_description_help' => 'Используется в списке контактов для добавления контекста, в случае необходимости.', + 'information_edit_unknown' => 'Я не знаю возраст', + 'information_edit_probably' => 'Этот человек возможно…', + 'information_edit_not_year' => 'Я знаю день и месяц рождения товарища, но не знаю год…', + 'information_edit_exact' => 'Я знаю точный день рождения…', + 'information_edit_birthdate_label' => 'День рождения', + 'information_no_work_defined' => 'Рабочая информация не указана', + 'information_work_at' => 'работает в :company', + 'work_add_cta' => 'Обновите информацию о работе', + 'work_edit_success' => 'Информация о работе обновлена', + 'work_edit_title' => 'Обновление информации о работе: :name', + 'work_edit_job' => 'Должность (не обяз.)', + 'work_edit_company' => 'Компания (не обяз.)', + 'work_information' => 'Информация о работе', + + // food preferences + 'food_preferences_add_success' => 'Предпочтения в еде были сохранены', + 'food_preferences_edit_description' => 'Возможно у :firstname или кого-то из его(её) семьи есть аллергия. Или не любит какой-то определённый продукт. Запишите это и в следующий раз когда вы будете кушать вместе вы вспомните об этом', + 'food_preferences_edit_description_no_last_name' => 'Возможно у :firstname или кого-то из её семьи есть аллергия. Или не любит какой-то определённый продукт. Запишите это и в следующий раз когда вы будете кушать вместе вы вспомните об этом', + 'food_preferences_edit_title' => 'Укажите предпочтения в еде', + 'food_preferences_edit_cta' => 'Сохранить предпочтения в еде', + 'food_preferences_title' => 'Предпочтения в еде', + 'food_preferences_cta' => 'Добавить предпочтения в еде', + + // reminders + 'reminders_blank_title' => 'Есть ли что-то связанное с :name, о чём вы хотите получить напоминание?', + 'reminders_blank_add_activity' => 'Добавить напоминание', + 'reminders_add_title' => 'О чём, связанном с :name, вам напомнить?', + 'reminders_add_description' => 'Напомните мне…', + 'reminders_add_next_time' => 'Когда в следующий раз вы хотите получить напоминание?', + 'reminders_add_once' => 'Напомнить один раз', + 'reminders_add_recurrent' => 'Повторять напоминание с периодичностью: ', + 'reminders_add_starting_from' => 'начиная с даты указанной выше', + 'reminders_add_cta' => 'Добавить напоминание', + 'reminders_edit_update_cta' => 'Обновить напоминание', + 'reminders_add_error_custom_text' => 'Вы должны указать текст для этого напоминания', + 'reminders_create_success' => 'Напоминание было добавлено', + 'reminders_delete_success' => 'Напоминание было удалено', + 'reminders_update_success' => 'Напоминание успешно обновлено', + 'reminders_add_optional_comment' => 'Комментарий (не обязательно)', + + 'reminder_frequency_day' => 'каждый день|[2,4]раз в :number дня|[5,*]раз в :number дней', + 'reminder_frequency_week' => 'каждую :number неделю|каждые :number недели|каждые :number недель', + 'reminder_frequency_month' => 'каждый :number месяц|каждые :number месяца|каждые :number месяцев', + 'reminder_frequency_year' => 'каждый :number год|каждые :number года|каждые :number лет', + 'reminder_frequency_one_time' => 'в :date', + 'reminders_delete_confirmation' => 'Вы уверены что хотите удалить это напоминание?', + 'reminders_delete_cta' => 'Удалить', + 'reminders_next_expected_date' => 'в', + 'reminders_cta' => 'Добавить напоминание', + 'reminders_description' => 'Мы отправим email по каждому из приведенных ниже напоминаний. Напоминания отправляются каждое утро в день, когда произойдут события. Напоминания, автоматически добавленные для дней рождения, не могут быть удалены. Если вы хотите изменить эти даты, отредактируйте день рождения контакта.', + 'reminders_one_time' => 'один раз', + 'reminders_type_week' => 'неделя', + 'reminders_type_month' => 'месяц', + 'reminders_type_year' => 'год', + 'reminders_birthday' => 'Birthdate of :name', + 'reminders_free_plan_warning' => 'Вы находитесь на бесплатном тарифном плане. На этом тарифном плане электронные письма не отправляются. Чтобы получать напоминания по электронной почте, повысьте уровень своей учетной записи.', + + // relationships + 'relationship_form_add' => 'Добавить связь', + 'relationship_form_edit' => 'Редактировать существующие отношения', + 'relationship_form_is_with' => 'Этот человек…', + 'relationship_form_is_with_name' => ':name…', + 'relationship_form_add_choice' => 'С кем эти отношения?', + 'relationship_form_create_contact' => 'Добавить человека', + 'relationship_form_associate_contact' => 'Существующий контакт', + 'relationship_form_associate_dropdown' => 'Поиск и выбор существующего контакта из списка ниже', + 'relationship_form_associate_dropdown_placeholder' => 'Поиск и выбор существующего контакта', + 'relationship_form_also_create_contact' => 'Создать запись контакта для этого человека.', + 'relationship_form_add_description' => 'Это позволит вам относиться к этому человеку как к любому другому контакту.', + 'relationship_form_add_no_existing_contact' => 'У вас нет контактов, которые могут быть связаны с :name.', + 'relationship_delete_confirmation' => 'Вы уверены, что хотите удалить эти отношения? Восстановление невозможно.', + 'relationship_unlink_confirmation' => 'Вы уверены, что хотите удалить это отношение? Этот человек не будет удален – только отношения между ними.', + 'relationship_form_add_success' => 'Связь была успешно установлена.', + 'relationship_form_deletion_success' => 'Отношения были удалены.', + + // tasks + 'tasks_title' => 'Задачи', + 'tasks_blank_title' => 'У вас пока нет задач.', + 'tasks_form_title' => 'Заголовок', + 'tasks_form_description' => 'Описание (необязательно)', + 'tasks_add_task' => 'Добавить задачу', + 'tasks_delete_success' => 'Задача была усрешна удалена', + 'tasks_complete_success' => 'Статус задачи был изменён', + + // activities + 'activity_title' => 'Активности', + 'activity_type_category_simple_activities' => 'Простая деятельность', + 'activity_type_category_sport' => 'Спорт', + 'activity_type_category_food' => 'Еда', + 'activity_type_category_cultural_activities' => 'Культурная деятельность', + 'activity_type_just_hung_out' => 'просто повеселились', + 'activity_type_watched_movie_at_home' => 'смотрели кино дома', + 'activity_type_talked_at_home' => 'разговаривали дома', + 'activity_type_did_sport_activities_together' => 'занимались спортом', + 'activity_type_ate_at_his_place' => 'ели у них', + 'activity_type_went_bar' => 'отправились в бар', + 'activity_type_ate_at_home' => 'ели дома', + 'activity_type_picnicked' => 'устраивали пикник', + 'activity_type_ate_restaurant' => 'ели в ресторане', + 'activity_type_went_theater' => 'ходили в театр', + 'activity_type_went_concert' => 'ходили на концерт', + 'activity_type_went_play' => 'ходили играть', + 'activity_type_went_museum' => 'были в музее', + 'activities_add_activity' => 'Добавить активность', + 'activities_add_more_details' => 'Добавить подробности', + 'activities_add_emotions' => 'Добавить эмоции', + 'activities_add_category' => 'Укажите категорию', + 'activities_add_participants_cta' => 'Добавить участников', + 'activities_item_information' => ':Activity. Дата: :date', + 'activities_add_title' => 'Что вы делали с {name}?', + 'activities_summary' => 'Опишите что вы делали', + 'activities_add_pick_activity' => 'Хотели бы вы классифицировать эту деятельность? Это не обязательно, но она даст вам статистику позже (опционально)', + 'activities_add_date_occured' => 'Это происходило…', + 'activities_add_participants' => 'Кто, кроме {name}, участвовал в этой активности? (опционально)', + 'activities_add_emotions_title' => 'Вы хотите отметить свои ощущения во время этой активности? (опционально)', + 'activities_blank_title' => 'Следите за тем, что вы делали с {name} в прошлом, и о чём вы говорили', + 'activities_blank_add_activity' => 'Добавить активность', + 'activities_add_success' => 'Активность была добавлена', + 'activities_add_error' => 'Ошибка при добавлении активности', + 'activities_update_success' => 'Активность была обновлена', + 'activities_delete_success' => 'Активность была удалена', + 'activities_who_was_involved' => 'Кто был вовлечен?', + 'activities_activity' => 'Активная категория', + 'activities_view_activities_report' => 'Просмотр отчёта о действиях', + 'activities_profile_title' => 'Отчёт о действиях между :name и вами', + 'activities_profile_subtitle' => 'Вы вошли в систему :total_activity с :name в общей сложности и :activities_last_t12 месяцев за последние 12 месяцев. Вы вошли в систему :total_activities с :name в общей сложности и :activities_last_t12 месяцев за последние 12 месяцев.', + 'activities_profile_year_summary_activity_types' => 'Вот разбивка мероприятий, которые вы сделали вместе за :year', + 'activities_profile_year_summary' => 'Вот что вы два сделали за :year', + 'activities_profile_number_occurences' => ':value активность|:value активности', + 'activities_list_participants' => 'Участники ({total}):', + 'activities_list_emotions' => 'Испытанные эмоции:', + 'activities_list_date' => 'Произошло', + 'activities_list_category' => 'Категория:', + + // notes + 'notes_create_success' => 'Заметка была добавлена', + 'notes_update_success' => 'Заметка успешно сохранена', + 'notes_delete_success' => 'Заметка была удалена', + 'notes_add_cta' => 'Добавить заметку', + 'notes_favorite' => 'Добавить/удалить из избранного', + 'notes_delete_title' => 'Удалить заметку', + 'notes_delete_confirmation' => 'Вы уверены что хотите удалить эту заметку? Восстановление невозможно.', + + // gifts + 'gifts_title' => 'Подарки', + 'gifts_add_success' => 'Подарок был добавлен', + 'gifts_delete_success' => 'Подарок был удалён', + 'gifts_delete_confirmation' => 'Вы уверены что хотите удалить этот подарок?', + 'gifts_add_gift' => 'Добавить подарок', + 'gifts_link' => 'Ссылка', + 'gifts_for' => 'Для: {name}', + 'gifts_delete_cta' => 'Удалить', + 'gifts_add_title' => 'Управление подарками для :name', + 'gifts_add_gift_idea' => 'Идея подарка', + 'gifts_add_gift_already_offered' => 'Подарок уже предложен', + 'gifts_add_gift_received' => 'Получен подарок', + 'gifts_add_gift_title' => 'Что это за подарок?', + 'gifts_add_gift_name' => 'Название подарка', + 'gifts_add_link' => 'Ссылка на веб-страницу (не обязательно)', + 'gifts_add_value' => 'Стоимость (не обязательно)', + 'gifts_add_comment' => 'Комментарий (не обязательно)', + 'gifts_add_recipient' => 'Получатель (необязательно)', + 'gifts_add_recipient_field' => 'Получатель', + 'gifts_add_photo' => 'Фото (необязательно)', + 'gifts_add_photo_title' => 'Добавить фото этого подарка', + 'gifts_add_someone' => 'Этот подарок предназначен, в частности, для кого-то из семьи {name}', + 'gifts_delete_title' => 'Удалить подарок', + 'gifts_ideas' => 'Идеи подарка', + 'gifts_offered' => 'Предложенные подарки', + 'gifts_offered_as_an_idea' => 'Отметить как идею', + 'gifts_received' => 'Полученные подарки', + 'gifts_view_comment' => 'Просмотреть комментарий', + 'gifts_mark_offered' => 'Отметить как предложенный\'', + 'gifts_update_success' => 'Подарок успешно обновлен', + 'gifts_add_date' => 'Дата (необязательно)', + + // debts + 'debt_delete_confirmation' => 'Вы уверены что хотите удалить этот долг?', + 'debt_delete_success' => 'Долг был удалён', + 'debt_add_success' => 'Долг был добавлен', + 'debt_title' => 'Долги', + 'debt_add_cta' => 'Добавить долг', + 'debt_you_owe' => 'Вы должны :amount', + 'debt_they_owe' => ':name должен вам :amount', + 'debt_add_title' => 'Управление долгами', + 'debt_add_you_owe' => 'Вы должны :name', + 'debt_add_they_owe' => ':name должен вам', + 'debt_add_amount' => 'сумма ', + 'debt_add_reason' => 'причина долга (не обязательно)', + 'debt_add_add_cta' => 'Добавить долг', + 'debt_edit_update_cta' => 'Обновить задолженность', + 'debt_edit_success' => 'Долг успешно обновлен', + 'debts_blank_title' => 'Управление долговыми обязательствами :name или :name должны', + + // tags + 'tag_edit' => 'Редактировать метку', + 'tag_add' => 'Добавить метки', + 'tag_add_search' => 'Добавить или искать метки', + 'tag_no_tags' => 'Пока нет меток', + + // Introductions + 'introductions_sidebar_title' => 'Как вы встретились', + 'introductions_blank_cta' => 'Укажите, как вы встретились с :name', + 'introductions_title_edit' => 'Как вы познакомились с :name?', + 'introductions_additional_info' => 'Расскажите, как и где вы встретились', + 'introductions_edit_met_through' => 'Кто-то ознакомил вас с этим человеком?', + 'introductions_no_met_through' => 'Никто', + 'introductions_first_met_date' => 'Дата знакомства', + 'introductions_no_first_met_date' => 'Я не знаю дату, когда мы познакомились', + 'introductions_first_met_date_known' => 'Это дата, когда мы познакомились', + 'introductions_add_reminder' => 'Добавьте напоминание об юбилее', + 'introductions_update_success' => 'Вы успешно обновили информацию о том, как вы встретили этого человека', + 'introductions_met_through' => 'Знакомство через :name', + 'introductions_met_date' => 'Познакомились :date', + 'introductions_reminder_title' => 'Годовщина знакомства', + + // Deceased + 'deceased_reminder_title' => 'Годовщина смерти :name', + 'deceased_mark_person_deceased' => 'Пометить как умершего', + 'deceased_know_date' => 'Я знаю дату смерти этого человека', + 'deceased_add_reminder' => 'Добавить напоминание на эту дату', + 'deceased_label' => 'Умерший', + 'deceased_date_label' => 'Дата смерти', + 'deceased_label_with_date' => 'Дата смерти :date', + 'deceased_age' => 'Возраст смерти', + + // Contact information + 'contact_info_title' => 'Контактная информация', + 'contact_info_form_content' => 'Содержание', + 'contact_info_form_contact_type' => 'Тип контакта', + 'contact_info_form_personalize' => 'Персонализация', + 'contact_info_address' => 'Живёт в', + + // Addresses + 'contact_address_title' => 'Адреса', + 'contact_address_form_name' => 'Заголовок (необязательно)', + 'contact_address_form_street' => 'Улица (необязательно)', + 'contact_address_form_city' => 'Город (необязательно)', + 'contact_address_form_province' => 'Область (необязательно)', + 'contact_address_form_postal_code' => 'Почтовый индекс (необязательно)', + 'contact_address_form_country' => 'Страна (необязательно)', + 'contact_address_form_latitude' => 'Широта (только цифры) (необязательно)', + 'contact_address_form_longitude' => 'Долгота (только цифры) (необязательно)', + + // Pets + 'pets_kind' => 'Вид питомца', + 'pets_name' => 'Имя (необязательно)', + 'pets_create_success' => 'Питомец был успешно добавлен', + 'pets_update_success' => 'Домашнее животное было обновленно', + 'pets_delete_success' => 'Домашнее животное было удалено', + 'pets_title' => 'Питомцы', + 'pets_reptile' => 'Пресмыкающееся', + 'pets_bird' => 'Птица', + 'pets_cat' => 'Кошка', + 'pets_dog' => 'Собака', + 'pets_fish' => 'Рыбка', + 'pets_hamster' => 'Хомяк', + 'pets_horse' => 'Лошадь', + 'pets_rabbit' => 'Кролик', + 'pets_rat' => 'Крыса', + 'pets_small_animal' => 'Маленькое животное', + 'pets_other' => 'Другое', + + // life events + 'life_event_list_tab_life_events' => 'События жизни', + 'life_event_list_tab_other' => 'Заметки, напоминания, …', + 'life_event_list_title' => 'События жизни', + 'life_event_blank' => 'Регистрируйтесь, что происходит с жизнью {name} для вашей будущей ссылки.', + 'life_event_list_cta' => 'Добавить событие жизни', + 'life_event_create_category' => 'Все категории', + 'life_event_create_life_event' => 'Добавить событие жизни', + 'life_event_create_default_title' => 'Заголовок (необязательно)', + 'life_event_create_default_story' => 'История (необязательно)', + 'life_event_create_date' => 'Вам не обязательно указывать месяц или день – только год является обязательным.', + 'life_event_create_default_description' => 'Добавьте информацию о том, что вы знаете', + 'life_event_create_add_yearly_reminder' => 'Добавить ежегодное напоминание об этом событии', + 'life_event_create_success' => 'Событие жизни добавлено', + 'life_event_delete_title' => 'Удалить событие жизни', + 'life_event_delete_description' => 'Вы уверены, что хотите удалить это событие жизни? Удаление необратимо.', + 'life_event_delete_success' => 'Событие жизни удалено', + 'life_event_date_it_happened' => 'Дата, когда это произошло', + 'life_event_category_work_education' => 'Работа и образование', + 'life_event_category_family_relationships' => 'Семья и отношения', + 'life_event_category_home_living' => 'Дом и жизнь', + 'life_event_category_health_wellness' => 'Здоровье и самочувствие', + 'life_event_category_travel_experiences' => 'Путешествия и впечатления', + 'life_event_sentence_new_job' => 'Начато новое задание', + 'life_event_sentence_retirement' => 'Вышел/вышла на пенсию', + 'life_event_sentence_new_school' => 'Пошёл/пошла в школу', + 'life_event_sentence_study_abroad' => 'Учился за рубежом', + 'life_event_sentence_volunteer_work' => 'Начал/начала волонтёрскую работу', + 'life_event_sentence_published_book_or_paper' => 'Опубликовал/опубликовала работу', + 'life_event_sentence_military_service' => 'Начал/начала военную службу', + 'life_event_sentence_new_relationship' => 'Отношение начато', + 'life_event_sentence_engagement' => 'Обручился/обручилась', + 'life_event_sentence_marriage' => 'Женился/вышла замуж', + 'life_event_sentence_anniversary' => 'Годовщина', + 'life_event_sentence_expecting_a_baby' => 'Ждёт ребёнка', + 'life_event_sentence_new_child' => 'Был ребенком', + 'life_event_sentence_new_family_member' => 'Добавлен член семьи', + 'life_event_sentence_new_pet' => 'Завёл/завела питомца', + 'life_event_sentence_end_of_relationship' => 'Закончились отношения', + 'life_event_sentence_loss_of_a_loved_one' => 'Потерял любимого', + 'life_event_sentence_moved' => 'Переехал/переехала', + 'life_event_sentence_bought_a_home' => 'Купил/купила дом', + 'life_event_sentence_home_improvement' => 'Отремонтировал дом', + 'life_event_sentence_holidays' => 'Уехал в отпуск', + 'life_event_sentence_new_vehicle' => 'Приобрел новый автомобиль', + 'life_event_sentence_new_roommate' => 'Появился сосед по комнате', + 'life_event_sentence_overcame_an_illness' => 'Преодолел болезнь', + 'life_event_sentence_quit_a_habit' => 'Привычка закончилась', + 'life_event_sentence_new_eating_habits' => 'Начало новой привычки питания', + 'life_event_sentence_weight_loss' => 'Сбросил/сбросила вес', + 'life_event_sentence_wear_glass_or_contact' => 'Начал/начала носить очки или контактные линзы', + 'life_event_sentence_broken_bone' => 'Получил/получила перелом', + 'life_event_sentence_removed_braces' => 'Удалил/удалила брекеты', + 'life_event_sentence_surgery' => 'Перенёс/перенесла операцию', + 'life_event_sentence_dentist' => 'Посетил/посетила стоматолога', + 'life_event_sentence_new_sport' => 'Начал заниматься спортом', + 'life_event_sentence_new_hobby' => 'Начал заниматься хобби', + 'life_event_sentence_new_instrument' => 'Изучил новый инструмент', + 'life_event_sentence_new_language' => 'Изучить новый язык', + 'life_event_sentence_tattoo_or_piercing' => 'Сделал татуировку или пирсинг', + 'life_event_sentence_new_license' => 'Лицензия получена', + 'life_event_sentence_travel' => 'Путешествие', + 'life_event_sentence_achievement_or_award' => 'Получил/получила достижение или награду', + 'life_event_sentence_changed_beliefs' => 'Изменившиеся убеждения', + 'life_event_sentence_first_word' => 'Выступал впервые', + 'life_event_sentence_first_kiss' => 'Поцеловал в первый раз', + + // documents + 'document_list_title' => 'Документы', + 'document_list_cta' => 'Загрузить документ', + 'document_list_blank_desc' => 'Здесь вы можете хранить документы, связанные с этим человеком.', + 'document_upload_zone_cta' => 'Загрузить файл', + 'document_upload_zone_progress' => 'Загрузка документа…', + 'document_upload_zone_error' => 'Произошла ошибка при загрузке документа. Пожалуйста, попробуйте еще раз.', + + // Photos + 'photo_title' => 'Фото', + 'photo_list_title' => 'Похожие фото', + 'photo_list_cta' => 'Загрузить фото', + 'photo_list_blank_desc' => 'Вы можете хранить изображения об этом контакте. Загрузите сейчас!', + 'photo_upload_zone_cta' => 'Загрузить фото', + 'photo_current_profile_pic' => 'Текущее изображение профиля', + 'photo_make_profile_pic' => 'Установи изображение профиля', + 'photo_delete' => 'Удалить фото', + 'photo_next' => 'Следующее фото ❯', + 'photo_previous' => '❮ Предыдущее фото', + + // Avatars + 'avatar_change_title' => 'Изменить свой аватар', + 'avatar_question' => 'Какой аватар вы хотели бы использовать?', + 'avatar_default_avatar' => 'Аватар по умолчанию', + 'avatar_adorable_avatar' => 'Аватар от adorable.io', + 'avatar_gravatar' => 'Gravatar связанный с адресом электронной почты этого человека. Gravatar — глобальная система, позволяющая пользователям ассоциировать адреса электронной почты с фотографиями.', + 'avatar_current' => 'Оставить текущий аватар', + 'avatar_photo' => 'Загруженное фото', + 'avatar_crop_new_avatar_photo' => 'Обрезать новый аватар', + + // emotions + 'emotion_this_made_me_feel' => 'Это заставило вас чувствовать себя…', + + // logs + 'auditlogs_link' => 'История', + 'auditlogs_title' => 'Всё, что произошло с :name', + 'auditlogs_breadcrumb' => 'История', + 'auditlogs_author' => 'По :name в :date', + + // contact field label + 'contact_field_label_home' => 'Домашний номер телефона', + 'contact_field_label_work' => 'Рабочий номер телефона', + 'contact_field_label_cell' => 'Мобильный номер телефона', + 'contact_field_label_fax' => 'Номер факса', + 'contact_field_label_pager' => 'Номер пейджера', + 'contact_field_label_main' => 'Основной', + 'contact_field_label_other' => 'Другое', + 'contact_field_label_personal' => 'Личные', +]; diff --git a/resources/lang/ru/reminder.php b/resources/lang/ru/reminder.php new file mode 100644 index 0000000..e64bf4e --- /dev/null +++ b/resources/lang/ru/reminder.php @@ -0,0 +1,16 @@ + 'Поздравить с днём рождения', + 'type_phone_call' => 'Позвонить', + 'type_lunch' => 'Пообедать с', + 'type_hangout' => 'Тусоваться с', + 'type_email' => 'Адрес электронной почты', + 'type_birthday_kid' => 'Wish happy birthday to the child of', +]; diff --git a/resources/lang/ru/settings.php b/resources/lang/ru/settings.php new file mode 100644 index 0000000..7caafd6 --- /dev/null +++ b/resources/lang/ru/settings.php @@ -0,0 +1,557 @@ + 'Настройки аккаунта', + 'sidebar_personalization' => 'Персонализация', + 'sidebar_settings_storage' => 'Хранилище', + 'sidebar_settings_export' => 'Экспорт данных', + 'sidebar_settings_users' => 'Пользователи', + 'sidebar_settings_subscriptions' => 'Подписка', + 'sidebar_settings_import' => 'Импорт данных', + 'sidebar_settings_tags' => 'Управление тегами', + 'sidebar_settings_api' => 'API', + 'sidebar_settings_dav' => 'Ресурсы DAV', + 'sidebar_settings_security' => 'Безопасность', + 'sidebar_settings_auditlogs' => 'Журнал аудита', + + 'title_general' => 'Общая информация', + 'title_i18n' => 'Международные настройки', + 'title_layout' => 'Макет', + + 'me_title' => 'Я как контакт', + 'me_help' => 'Это контакт, который представляет вас в Monica', + 'me_select' => 'Выберите контакт', + 'me_no_contact' => 'Контакт еще не выбран.', + 'me_select_click' => 'Нажмите здесь, чтобы выбрать контакт.', + 'me_remove_contact' => 'Удалить связь', + 'me_choose' => 'Выберите себя', + 'me_choose_placeholder' => 'Выберите себя', + + 'export_title' => 'Экспортировать данные вашего аккаунта', + 'export_be_patient' => 'Click the button to start the export. It might take several minutes to process the export – please be patient and do not spam the button.', + 'export_title_sql' => 'Export to SQL', + 'export_sql_explanation' => 'Exporting your data in SQL format allows you to take your data and import it to your own Monica instance. This is only valuable if you do have your own server.', + 'export_sql_cta' => 'Export to SQL', + 'export_sql_link_instructions' => 'Note: read the instructions to learn more about importing this file to your instance.', + 'export_title_json' => 'Export to Json', + 'export_submitted' => 'Your export has been submitted, it will be available in a few moment…', + 'export_json_explanation' => 'Exporting your data in Json format for backup.', + 'export_json_beta' => 'Json export is in preview mode. Tell us what you think about it:', + 'export_json_cta' => 'Export to Json', + 'export_header_type' => 'Type', + 'export_header_timestamp' => 'Creation date', + 'export_header_status' => 'Status', + 'export_header_actions' => 'Actions', + 'export_last_title' => 'Last exports', + 'export_empty_title' => 'No exports yet', + 'export_type_json' => 'Json export', + 'export_type_sql' => 'SQL export', + 'export_status_todo' => 'Submitted', + 'export_status_doing' => 'Doing', + 'export_status_done' => 'Done', + 'export_status_failed' => 'Failed', + 'export_not_done' => 'Download impossible, this export is not done yet.', + + 'firstname' => 'Имя', + 'lastname' => 'Фамилия', + 'name_order' => 'Сортировка имени', + 'name_order_firstname_lastname' => '<Имя> <Фамилия> – Иван Иванов', + 'name_order_lastname_firstname' => '<Фамилия> <Имя> – Иванов Иван', + 'name_order_firstname_lastname_nickname' => '<Имя> <Фамилия> (<Псевдоним>) – Иван Иванов (Ivan228)', + 'name_order_firstname_nickname_lastname' => '<Имя> (<Псевдоним>) <Фамилия> - Иван (Ivan228) Иванов', + 'name_order_lastname_firstname_nickname' => '<Фамилия> <Имя> (<Псевдоним>) - Иванов Иван (Ivan228)', + 'name_order_lastname_nickname_firstname' => '<Фамилия> (<Псевдоним>) <Имя> - Иван (Ivan228) Иванов', + 'name_order_nickname_firstname_lastname' => '<Псевдоним> (<Имя> <Фамилия>) – Ivan228 (Иван Иванов)', + 'name_order_nickname_lastname_firstname' => '<Псевдоним> (<Фамилия> <Имя>) – Ivan228 (Иванов Иван)', + 'name_order_nickname' => '<Псевдоним> - Ivan228', + 'currency' => 'Валюта', + 'name' => 'Ваше имя: :name', + 'email' => 'Email', + 'email_placeholder' => 'Введите email', + 'email_help' => 'Это почта, используемая для входа в Monica и эту почту будут приходить напоминания.', + 'timezone' => 'Часовой пояс', + 'temperature_scale' => 'Температура', + 'temperature_scale_fahrenheit' => 'Fahrenheit (°F)', + 'temperature_scale_celsius' => 'Celsius (°C)', + 'layout' => 'Дизайн', + 'layout_small' => 'Максимум шириной в 1200 пикселей', + 'layout_big' => 'Шириной во весь экран', + 'save' => 'Обновить настройки', + 'delete_title' => 'Удалить аккаунт', + 'delete_desc' => 'Вы хотите удалить свою учетную запись? Удаление необратимо и все ваши данные будут удалены навсегда. Если у вас есть подписка, она будет немедленно отменена.', + 'delete_other_desc' => 'Your data in the main database will be deleted immediately. As described in our privacy policy, we carry out securely encrypted backups of the database every day. These backups are kept for 30 days after which they are completely deleted. We cannot delete specific data from the backups we hold any earlier than this. All of your data will be completely deleted no later than 31 days after your account’s deletion.', + 'reset_desc' => 'Вы хотите сбросить учетную запись? Это удалит все ваши контакты и все связанные с ними данные. Ваша учетная запись не будет удалена.', + 'reset_title' => 'Сброс аккаунта', + 'reset_cta' => 'Сбросить аккаунт', + 'reset_notice' => 'Вы уверены, что хотите сбросить свою учетную запись? Это окончательно и не может быть отменено.', + 'reset_success' => 'Ваша учетная запись успешно сброшена.', + 'delete_notice' => 'Are you sure you want to delete your account? This is permanent and cannot be undone. All of your data will be deleted and will not be recoverable.', + 'delete_cta' => 'Удалить аккаунт', + 'settings_success' => 'Настройки обновлены!', + 'locale' => 'Язык', + 'locale_help' => 'Хотите помочь с переводом Monica или добавить новый язык? Перейдите по этой ссылке для получения дополнительной информации.', + 'locale_ar' => 'Арабский', + 'locale_cs' => 'Чешский', + 'locale_de' => 'Немецкий', + 'locale_el' => 'Греческий', + 'locale_en' => 'Английский', + 'locale_en-GB' => 'Английский (Великобритания)', + 'locale_es' => 'Испанский', + 'locale_fr' => 'Французский', + 'locale_he' => 'Иврит', + 'locale_hr' => 'Хорватский', + 'locale_id' => 'Индонезийский', + 'locale_it' => 'Итальянский', + 'locale_ja' => 'Японский', + 'locale_nl' => 'Нидерландский', + 'locale_pt' => 'Португальский', + 'locale_pt-BR' => 'Бразильский португальский', + 'locale_ru' => 'Русский', + 'locale_sv' => 'Шведский', + 'locale_vi' => 'Vietnamese', + 'locale_zh' => 'Китайский упрощенный', + 'locale_zh-TW' => 'Китайский традиционный', + 'locale_tr' => 'Турецкий', + + 'security_title' => 'Безопасность', + 'security_help' => 'Изменение вопросов безопасности для вашей учетной записи.', + 'password_change' => 'Изменить пароль', + 'password_current' => 'Текущий пароль', + 'password_current_placeholder' => 'Введите ваш текущий пароль', + 'password_new1' => 'Новый пароль', + 'password_new1_placeholder' => 'Введите новый пароль', + 'password_new2' => 'Подтвердите новый пароль', + 'password_new2_placeholder' => 'Повторите новый пароль', + 'password_btn' => 'Изменить пароль', + '2fa_title' => 'Двухфакторная аутентификация', + '2fa_otp_title' => 'Мобильное приложение для двухфакторной аутентификации', + '2fa_enable_title' => 'Включить двухфакторную аутентификацию', + '2fa_enable_description' => 'Включите двухфакторную аутентификацию, чтобы повысить безопасность вашей учетной записи.', + '2fa_enable_otp' => 'Откройте ваше мобильное приложение двухфакторной аутентификации и отсканируйте следующий QR-код:', + '2fa_enable_otp_help' => 'Если ваше мобильное приложение двухфакторной аутентификации не поддерживает QR-коды, введите следующий код:', + '2fa_enable_otp_validate' => 'Пожалуйста, проверьте новое устройство, которое вы только что настроили:', + '2fa_enable_success' => 'Двухфакторная аутентификация активирована', + '2fa_enable_error' => 'Error when trying to activate Two Factor Authentication', + '2fa_enable_error_already_set' => 'Двухфакторная аутентификация уже включена', + '2fa_disable_title' => 'Отключить двухфакторную аутентификацию', + '2fa_disable_description' => 'Disable Two Factor Authentication for your account. Be careful, your account will be much less secure!', + '2fa_disable_success' => 'Двухфакторная аутентификация отключена', + '2fa_disable_error' => 'Ошибка при попытке отключить двухфакторную аутентификацию', + + 'webauthn_title' => 'Ключ безопасности — протокол WebAuthn', + 'webauthn_enable_description' => 'Добавить новый ключ безопасности', + 'webauthn_key_name_help' => 'Дайте вашему ключу имя.', + 'webauthn_key_name' => 'Имя ключа:', + 'webauthn_success' => 'Ваш ключ обнаружен и проверен.', + 'webauthn_last_use' => 'Последнее использование: {timestamp}', + 'webauthn_delete_confirmation' => 'Вы действительно хотите удалить этот ключ?', + 'webauthn_delete_success' => 'Ключ удалён', + 'webauthn_insertKey' => 'Вставьте ваш ключ безопасности.', + 'webauthn_buttonAdvise' => 'Если в вашем ключе безопасности есть кнопка, нажмите ее.', + 'webauthn_noButtonAdvise' => 'If it does not, remove it and insert it again.', + 'webauthn_not_supported' => 'Ваш браузер в настоящее время не поддерживает WebAuthn.', + 'webauthn_not_secured' => 'WebAuthn поддерживает только безопасные соединения. Загрузите эту страницу через https.', + 'webauthn_error_already_used' => 'Этот ключ уже зарегистрирован. Нет необходимости в его повторной регистрации.', + 'webauthn_error_not_allowed' => 'Операция истекла или не была разрешена.', + + 'recovery_title' => 'Коды восстановления', + 'recovery_show' => 'Получить коды восстановления', + 'recovery_copy_help' => 'Скопировать коды в буфер обмена', + 'recovery_help_intro' => 'Ваши коды восстановления:', + 'recovery_help_information' => 'Вы можете использовать каждый код восстановления один раз.', + 'recovery_clipboard' => 'Codes copied to the clipboard.', + 'recovery_generate' => 'Сгенерировать новые коды…', + 'recovery_generate_help' => 'Создание новых кодов аннулирует ранее сгенерированные коды.', + 'recovery_already_used_help' => 'This code has already been used.', + + 'users_list_title' => 'Пользователи с доступом к вашей учетной записи', + 'users_list_add_user' => 'Пригласить нового пользователя', + 'users_list_you' => 'Это вы', + 'users_list_invitations_title' => 'Приглашения, ожидающие ответа', + 'users_list_invitations_explanation' => 'Below are the people you’ve invited to join Monica as a collaborator.', + 'users_list_invitations_invited_by' => 'invited by :name', + 'users_list_invitations_sent_date' => 'sent on :date', + 'users_blank_title' => 'Вы единственный, кто имеет доступ к этому аккаунту.', + 'users_blank_add_title' => 'Хотите пригласить кого-нибудь?', + 'users_blank_description' => 'Этот человек будет иметь такой же доступ, как у вас, и сможет добавлять, редактировать или удалять контактную информацию.', + 'users_blank_cta' => 'Пригласить кого-нибудь', + 'users_add_title' => 'Invite a new user to your account by email', + 'users_add_description' => 'This person will have the same access as you do, including inviting or deleting other users, including you. Make sure you trust this person before giving them access.', + 'users_add_email_field' => 'Enter the email of the person you want to invite', + 'users_add_confirmation' => 'I confirm that I want to invite this user to my account. I understand that this person will have access to ALL of my data and see exactly what I see.', + 'users_add_cta' => 'Invite user by email', + 'users_accept_title' => 'Принять приглашение и создать аккаунт', + 'users_error_please_confirm' => 'Please confirm that you want to invite this before proceeding with the invitation', + 'users_error_email_already_taken' => 'Этот адрес уже используется. Пожалуйста, выберите другой', + 'users_error_already_invited' => 'Вы уже пригласили этого пользователя. Пожалуйста, выберите другой адрес электронной почты.', + 'users_error_email_not_similar' => 'This is not the email of the person who’ve invited you.', + 'users_invitation_deleted_confirmation_message' => 'Приглашение успешно удалено', + 'users_invitations_delete_confirmation' => 'Вы действительно хотите удалить это приглашение?', + 'users_list_delete_confirmation' => 'Вы точно хотите удалить этого пользователя из вашего аккаунта?', + 'users_invitation_need_subscription' => 'Adding more users requires a subscription.', + + 'subscriptions_account_current_plan' => 'Your current plan', + 'subscriptions_account_current_legacy' => 'Current plan, not selectable anymore:', + 'subscriptions_account_current_paid_plan' => 'You are on the :name plan. Thanks so much for being a subscriber.', + + 'subscriptions_account_next_billing_title' => 'Следующий платёж', + 'subscriptions_account_next_billing' => 'Your subscription will auto-renew on :date.', + 'subscriptions_account_bill_monthly' => 'We’ll bill you :price for another month.', + 'subscriptions_account_bill_annual' => 'We’ll bill you :price for another year.', + 'subscriptions_account_change' => 'Изменить план', + + 'subscriptions_account_cancel_title' => 'Cancel subscription', + 'subscriptions_account_cancel_action' => 'Отменить подписку', + 'subscriptions_account_cancel' => 'You can cancel your subscription at any time.', + 'subscriptions_account_free_plan' => 'Вы на бесплатном тарифном плане.', + 'subscriptions_account_free_plan_upgrade' => 'You can upgrade your account to the :name plan, which costs $:price per month. Here are the advantages:', + 'subscriptions_account_free_plan_benefits_users' => 'Неограниченное количество пользователей', + 'subscriptions_account_free_plan_benefits_reminders' => 'Напоминания по электронной почте', + 'subscriptions_account_free_plan_benefits_import_data_vcard' => 'Import your contacts with vCard', + 'subscriptions_account_free_plan_benefits_support' => 'Support the project in the long run, so we can introduce more great features.', + 'subscriptions_account_upgrade' => 'Upgrade your account', + 'subscriptions_account_upgrade_title' => 'Upgrade Monica today and have more meaningful relationships.', + 'subscriptions_account_upgrade_choice' => 'Pick a plan below and join over :customers persons who upgraded their Monica.', + 'subscriptions_account_update_title' => 'Update Monica subscription', + 'subscriptions_account_update_description' => 'You can change your subscription’s frequency here.', + 'subscriptions_account_update_information' => 'You will be billed immediately for the new amount. Your subscription will extend to the new period, depending on your choice.', + 'subscriptions_account_invoices' => 'Invoices', + 'subscriptions_account_invoices_download' => 'Download', + 'subscriptions_account_invoices_subscription' => 'Subscription from :startDate to :endDate', + 'subscriptions_account_payment' => 'Which payment option fits you best?', + 'subscriptions_account_confirm_payment' => 'Your payment is currently incomplete, please confirm your payment.', + 'subscriptions_downgrade_title' => 'Downgrade your account to the free plan', + 'subscriptions_downgrade_limitations' => 'The free plan has limitations. In order to be able to downgrade, you need to pass the checklist below:', + 'subscriptions_downgrade_rule_users' => 'You must have only 1 user in your account', + 'subscriptions_downgrade_rule_users_constraint' => 'You currently have 1 user in your account.|You currently have :count users in your account.', + 'subscriptions_downgrade_rule_invitations' => 'You must not have any pending invitations', + 'subscriptions_downgrade_rule_invitations_constraint' => 'You currently have 1 pending invitation.|You currently have :count pending invitations.', + 'subscriptions_downgrade_rule_contacts' => 'You must not have more than :number active contacts', + 'subscriptions_downgrade_rule_contacts_constraint' => 'You currently have 1 contact.|You currently have :count contacts.', + 'subscriptions_downgrade_rule_contacts_archive' => 'We can also archive all your contacts for you – that would clear this rule and let you proceed with your account’s downgrade process.', + 'subscriptions_downgrade_cta' => 'Downgrade', + 'subscriptions_downgrade_success' => 'You are back to the Free plan!', + 'subscriptions_downgrade_thanks' => 'Thanks so much for trying the paid plan. We keep adding new features on Monica all the time – so you might want to come back in the future to see if you might be interested in taking a subscription again.', + 'subscriptions_back' => 'Back to settings', + 'subscriptions_upgrade_title' => 'Upgrade your account', + 'subscriptions_upgrade_choose' => 'You picked the :plan plan.', + 'subscriptions_upgrade_infos' => 'We couldn’t be happier. Enter your payment info below.', + 'subscriptions_upgrade_name' => 'Имя держателя карты', + 'subscriptions_upgrade_zip' => 'Почтовый индекс', + 'subscriptions_upgrade_credit' => 'Кредитная или дебетовая карта', + 'subscriptions_upgrade_submit' => 'Pay {amount}', + 'subscriptions_upgrade_charge' => 'We’ll charge your card :price now. The next charge will be on :date. If you ever change your mind, you can cancel at any time, no questions asked.', + 'subscriptions_upgrade_charge_handled' => 'The payment is handled by Stripe. No card information touches our server.', + 'subscriptions_upgrade_success' => 'Thank you! You are now subscribed.', + 'subscriptions_upgrade_thanks' => 'Welcome to the community of people who try to make the world a better place.', + + 'subscriptions_payment_confirm_title' => 'Confirm your :amount payment', + 'subscriptions_payment_confirm_information' => 'Extra confirmation is needed to process your payment. Please confirm your payment by filling out your payment details below.', + 'subscriptions_payment_succeeded_title' => 'Платеж выполнен', + 'subscriptions_payment_succeeded' => 'Этот платеж уже был успешно подтвержден.', + 'subscriptions_payment_cancelled_title' => 'Платеж отменен', + 'subscriptions_payment_cancelled' => 'Этот платеж был отменен.', + 'subscriptions_payment_error_name' => 'Please provide your name.', + 'subscriptions_payment_success' => 'The payment was successful.', + + 'subscriptions_pdf_title' => 'Your :name monthly subscription', + 'subscriptions_plan_frequency_year' => ':amount / year', + 'subscriptions_plan_frequency_month' => ':amount / month', + 'subscriptions_plan_choose' => 'Choose this plan', + 'subscriptions_plan_year_title' => 'Pay annually', + 'subscriptions_plan_year_bonus' => 'Peace of mind for a whole year', + 'subscriptions_plan_month_title' => 'Pay monthly', + 'subscriptions_plan_month_bonus' => 'Можно отменить в любой момент', + 'subscriptions_plan_include1' => 'Included with your upgrade:', + 'subscriptions_plan_include2' => 'Unlimited number of contacts • Unlimited number of users • Reminders by email • Import with vCard • Personalization of the contact sheet', + 'subscriptions_plan_include3' => '100% of the profits go the development of this great open source project.', + 'subscriptions_help_title' => 'Additional details you may be curious about', + 'subscriptions_help_opensource_title' => 'Что такое проект с открытым исходным кодом?', + 'subscriptions_help_opensource_desc' => 'Monica is an open source project. This means it is built by a community who wants to build a great tool for the greater good. Being open source means the code is publicly available on GitHub, and everyone can inspect it, modify it or enhance it. All the money we raise is dedicated to building better features, paying for more powerful servers, and paying other costs. Thanks for your help. We couldn’t do it without you.', + 'subscriptions_help_limits_title' => 'Is there a limit to the number of contacts we can have on the free plan?', + 'subscriptions_help_limits_plan' => 'Yes. Free plans let you manage :number contacts.', + 'subscriptions_help_discounts_title' => 'Do you have discounts for non-profits and education?', + 'subscriptions_help_discounts_desc' => 'We do! Monica is free for students, and free for non-profits and charities. Just contact the support with a proof of your status and we’ll apply this special status in your account.', + 'subscriptions_help_change_title' => 'Что если я передумаю?', + 'subscriptions_help_change_desc' => 'Вы можете отменить это в любое время, без вопросов и самостоятельно – без необходимости обращаться в службу поддержки. Тем не менее, вам не будет осуществлен возврат средств за текущий период.', + + 'stripe_error_card' => 'Your card was declined. Decline message is: :message', + 'stripe_error_api_connection' => 'Network communication with Stripe failed. Try again later.', + 'stripe_error_rate_limit' => 'Too many requests with Stripe right now. Try again later.', + 'stripe_error_invalid_request' => 'Invalid parameters. Try again later.', + 'stripe_error_authentication' => 'Wrong authentication with Stripe', + + 'import_title' => 'Import contacts in your account', + 'import_cta' => 'Upload contacts', + 'import_stat' => 'You’ve imported :number files so far.', + 'import_result_stat' => 'Uploaded vCard with 1 contact (:total_imported imported, :total_skipped skipped)|Uploaded vCard with :total_contacts contacts (:total_imported imported, :total_skipped skipped)', + 'import_view_report' => 'View report', + 'import_in_progress' => 'The import is in progress. Reload the page in one minute.', + 'import_upload_title' => 'Import your contacts from a vCard file', + 'import_upload_rules_desc' => 'We do however have some rules:', + 'import_upload_rule_format' => 'We support .vcard and .vcf files.', + 'import_upload_rule_vcard' => 'We support the vCard 3.0 format, which is the default format for macOS’s Contacts.app and Google Contacts.', + 'import_upload_rule_instructions' => 'Export instructions for macOS Contacts.app and Google Contacts.', + 'import_upload_rule_multiple' => 'If your contacts have multiple email addresses or phone numbers, only the first entry will be saved.', + 'import_upload_rule_limit' => 'Files are limited to 10 MB.', + 'import_upload_rule_time' => 'It might take up to a minute to upload the contacts and process them. Please be patient.', + 'import_upload_rule_cant_revert' => 'Please make sure data is accurate before uploading, as you can’t undo the upload.', + 'import_upload_form_file' => 'Your .vcf or .vCard file:', + 'import_upload_behaviour' => 'Import behaviour:', + 'import_upload_behaviour_add' => 'Add new contacts and skip existing', + 'import_upload_behaviour_replace' => 'Replace existing contacts', + 'import_upload_behaviour_help' => 'Replacing will replace all data found in the vCard, but will keep existing contact fields.', + 'import_report_title' => 'Importing report', + 'import_report_date' => 'Date of the import', + 'import_report_type' => 'Type of import', + 'import_report_number_contacts' => 'Number of contacts in the file', + 'import_report_number_contacts_imported' => 'Number of imported contacts', + 'import_report_number_contacts_skipped' => 'Number of skipped contacts', + 'import_report_status_imported' => 'Imported', + 'import_report_status_skipped' => 'Skipped', + 'import_vcard_parse_error' => 'Error when parsing the vCard entry', + 'import_vcard_contact_exist' => 'Contact already exists', + 'import_vcard_contact_no_firstname' => 'No first name (mandatory)', + 'import_vcard_file_not_found' => 'File not found', + 'import_vcard_unknown_entry' => 'Unknown contact name', + 'import_vcard_file_no_entries' => 'File contains no entries', + 'import_blank_title' => 'You haven’t imported any contacts yet.', + 'import_blank_question' => 'Would you like to import contacts now?', + 'import_blank_description' => 'We can import vCard files that you can get from Google Contacts or your Contact manager.', + 'import_blank_cta' => 'Import vCard', + 'import_need_subscription' => 'Importing data requires a subscription.', + + 'tags_list_title' => 'Tags', + 'tags_list_description' => 'You can organize your contacts by setting up tags. Tags work like folders, but you can add more than one tag to a contact.', + 'tags_list_contact_number' => '1 contact|:count contacts', + 'tags_list_delete_success' => 'The tag has been successfully with success', + 'tags_list_edit_success' => 'The tag has been successfully updated', + 'tags_list_delete_confirmation' => 'Are you sure you want to delete the tag? No contacts will be deleted, only the tag.', + 'tags_blank_title' => 'Tags are a great way of categorizing your contacts.', + 'tags_blank_description' => 'Tags work like folders, but you can add more than one tag to a contact. Go to a contact and tag a friend, right below the name. Once a contact is tagged, come back here to manage all the tags in your account.', + + 'api_title' => 'Доступ к API', + 'api_description' => 'API может использоваться для работы с данными Monica из внешнего приложения, например – мобильного приложения.', + 'api_help' => 'To use the API, a token is mandatory. You can either create a personal access token (Bearer authentication), or authorize an OAuth client to create it for you. See API documentation.', + 'api_endpoint' => 'Конечная точка API для этого экземпляра Monica:', + + 'api_personal_access_tokens' => 'Personal access tokens', + 'api_pao_description' => 'Make sure you give this token to a source you trust – as they allow you to access all your data.', + 'api_token_title' => 'Personal Access Tokens', + 'api_token_create_new' => 'Create New Token', + 'api_token_not_created' => 'You have not created any personal access tokens.', + 'api_token_name' => 'Token name', + 'api_token_expire' => 'Expires at {date}', + 'api_token_delete' => 'Delete', + 'api_token_create' => 'Create Token', + 'api_token_scopes' => 'Scopes', + 'api_token_help' => 'Here is your new personal access token. This is the only time it will be shown so don’t lose it! You may now use this token to make API requests.', + + 'api_oauth_clients' => 'Ваши клиенты OAuth', + 'api_oauth_clients_desc' => 'This section lets you register your own OAuth clients.', + 'api_oauth_clients_desc2' => 'Use this client id to request a new token, and convert authorization codes to access tokens. See Laravel Passport documentation for more information.', + 'api_oauth_title' => 'Клиенты OAuth', + 'api_oauth_create_new' => 'Create New Client', + 'api_oauth_edit' => 'Edit Client', + 'api_oauth_not_created' => 'You have not created any OAuth clients.', + 'api_oauth_clientid' => 'Client ID', + 'api_oauth_name' => 'Name', + 'api_oauth_name_help' => 'Something your users will recognize and trust.', + 'api_oauth_secret' => 'Secret', + 'api_oauth_create' => 'Create Client', + 'api_oauth_redirecturl' => 'Redirect URL', + 'api_oauth_redirecturl_help' => 'Your application’s authorization callback URL.', + + 'api_authorized_clients' => 'List of authorized clients', + 'api_authorized_clients_desc' => 'This section lists all the clients you’ve authorized to access your application data. You can revoke this authorization at anytime.', + 'api_authorized_clients_title' => 'Authorized Applications', + 'api_authorized_clients_none' => 'There are no authorized clients yet.', + 'api_authorized_clients_name' => 'Name', + 'api_authorized_clients_scopes' => 'Scopes', + + 'personalization_tab_title' => 'Personalize your account', + + 'personalization_title' => 'Here you will find different settings to configure your account. These features are intended for “power users” who want maximum control over Monica.', + 'personalization_contact_field_type_title' => 'Contact field types', + 'personalization_contact_field_type_add' => 'Add new field type', + 'personalization_contact_field_type_description' => 'You can configure all the different types of contact fields that you can associate to all your contacts. For example, if a new social network appears in the future, you will be able to add this new way of communicating with your contacts right here.', + 'personalization_contact_field_type_table_name' => 'Name', + 'personalization_contact_field_type_table_protocol' => 'Protocol', + 'personalization_contact_field_type_table_actions' => 'Actions', + 'personalization_contact_field_type_modal_title' => 'Add a new contact field type', + 'personalization_contact_field_type_modal_edit_title' => 'Edit an existing contact field type', + 'personalization_contact_field_type_modal_delete_title' => 'Delete an existing contact field type', + 'personalization_contact_field_type_modal_delete_description' => 'Are you sure you want to delete this contact field type? Deleting this type of contact field will delete ALL the data with this type for all of your contacts.', + 'personalization_contact_field_type_modal_name' => 'Name', + 'personalization_contact_field_type_modal_protocol' => 'Protocol (optional)', + 'personalization_contact_field_type_modal_protocol_help' => 'Each new contact field type can be clickable. If a protocol is set, we will use it to trigger the action that is set.', + 'personalization_contact_field_type_modal_icon' => 'Icon (optional)', + 'personalization_contact_field_type_modal_icon_help' => 'You can associate an icon with this contact field type. You need to add a reference to a Font Awesome icon.', + 'personalization_contact_field_type_delete_success' => 'The contact field type has been successfully deleted.', + 'personalization_contact_field_type_add_success' => 'The contact field type has been successfully added.', + 'personalization_contact_field_type_edit_success' => 'The contact field type has been successfully updated.', + + 'personalization_genders_title' => 'Gender types', + 'personalization_genders_add' => 'Add new gender type', + 'personalization_genders_desc' => 'You can define as many genders as you need to. You need at least one gender type in your account.', + 'personalization_genders_modal_add' => 'Add gender type', + 'personalization_genders_modal_edit' => 'Update gender type', + 'personalization_genders_modal_name' => 'Name', + 'personalization_genders_modal_name_help' => 'The name used to display the gender on a contact page.', + 'personalization_genders_modal_sex' => 'Sex', + 'personalization_genders_modal_sex_help' => 'Used to define the relationships, and during the VCard import/export process.', + 'personalization_genders_modal_default' => 'Select the default gender for a new contact', + 'personalization_genders_modal_delete' => 'Delete gender type', + 'personalization_genders_modal_delete_desc' => 'Are you sure you want to delete the gender “{name}”?', + 'personalization_genders_modal_delete_question' => 'You currently have {count} contact with this gender. If you delete this gender, what gender should this contact have?|You currently have {count} contacts with this gender. If you delete this gender, what gender should these contacts have?', + 'personalization_genders_modal_delete_question_default' => 'This gender is the default one. If you delete this gender, which one will be the new default?', + 'personalization_genders_modal_error' => 'Please choose a gender from the list.', + 'personalization_genders_list_contact_number' => '{count} contact|{count} contacts', + 'personalization_genders_table_name' => 'Name', + 'personalization_genders_table_sex' => 'Sex', + 'personalization_genders_table_default' => 'Default', + 'personalization_genders_default' => 'Default gender', + 'personalization_genders_make_default' => 'Change default gender', + 'personalization_genders_select_default' => 'Select default gender', + 'personalization_genders_m' => 'Male', + 'personalization_genders_f' => 'Female', + 'personalization_genders_o' => 'Other', + 'personalization_genders_u' => 'Unknown', + 'personalization_genders_n' => 'None or not applicable', + + 'personalization_reminder_rule_save' => 'The change has been saved', + 'personalization_reminder_rule_title' => 'Reminder rules', + 'personalization_reminder_rule_line' => '{count} day before|{count} days before', + 'personalization_reminder_rule_desc' => 'For every reminder that you set, Monica can send you an email a number of days before the event happens. You can adjust these notification settings here. These notifications only apply to monthly and yearly reminders.', + + 'personalization_module_save' => 'The change has been saved', + 'personalization_module_title' => 'Features', + 'personalization_module_desc' => 'You may not need all of Monica’s features. Below you can toggle specific features that are used on a contact sheet. This change will affect ALL your contacts. Turning off a feature does not delete any data, it simply hides the feature.', + + 'personalisation_paid_upgrade' => 'This is a premium feature that requires a Paid subscription to be active. Upgrade your account by visiting Settings > Subscription.', + 'personalisation_paid_upgrade_vue' => 'This is a premium feature that requires a Paid subscription to be active. Upgrade your account by visiting Settings > Subscription.', + + 'reminder_time_to_send' => 'Time of the day reminders will be sent', + 'reminder_time_to_send_help' => 'Your next reminder is scheduled to be sent on {dateTime}.', + + 'personalization_activity_type_category_title' => 'Activity type categories', + 'personalization_activity_type_category_add' => 'Add a new activity type category', + 'personalization_activity_type_category_table_name' => 'Name', + 'personalization_activity_type_category_description' => 'An activity with one of your contacts can have a type and a category type. Your account comes with a set of predefined category types by default, but you can customize these here.', + 'personalization_activity_type_category_table_actions' => 'Actions', + 'personalization_activity_type_category_modal_add' => 'Add a new activity type category', + 'personalization_activity_type_category_modal_edit' => 'Edit an activity type category', + 'personalization_activity_type_category_modal_question' => 'What should we name this new category?', + 'personalization_activity_type_add_button' => 'Add a new activity type', + 'personalization_activity_type_modal_add' => 'Add a new activity type', + 'personalization_activity_type_modal_question' => 'What should we name this new activity type?', + 'personalization_activity_type_modal_edit' => 'Edit an activity type', + 'personalization_activity_type_category_modal_delete' => 'Delete an activity type category', + 'personalization_activity_type_category_modal_delete_desc' => 'Are you sure you want to delete this category? Deleting it will delete all associated activity types. Activities that belong to this category will not be affected by this deletion.', + 'personalization_activity_type_modal_delete' => 'Delete an activity type', + 'personalization_activity_type_modal_delete_desc' => 'Are you sure you want to delete this activity type? Activities that belong to this category will not be affected by this deletion.', + 'personalization_activity_type_modal_delete_error' => 'We can’t find this activity type.', + 'personalization_activity_type_category_modal_delete_error' => 'We can’t find this activity type category.', + + 'personalization_life_event_category_title' => 'Life event categories', + 'personalization_live_event_category_table_name' => 'Name', + 'personalization_life_event_category_description' => 'A life event can have a type and a category. Your account comes with a set of predefined categories and types by default, but you can customize life event types here.', + 'personalization_live_event_category_table_actions' => 'Actions', + 'personalization_life_event_type_add_button' => 'Add a new life event type', + 'personalization_life_event_type_modal_add' => 'Add a new life event type', + 'personalization_life_event_type_modal_question' => 'What should we name this new life event type?', + 'personalization_life_event_type_modal_edit' => 'Edit a life event type', + 'personalization_life_event_type_modal_delete' => 'Delete a life event type', + 'personalization_life_event_type_modal_delete_desc' => 'Are you sure you want to delete this life event type? Life events that belong to this type will be deleted by performing this action.', + 'personalization_life_event_type_modal_delete_error' => 'We can’t find this life event type.', + + 'personalization_life_event_category_work_education' => 'Work & education', + 'personalization_life_event_category_family_relationships' => 'Family & relationships', + 'personalization_life_event_category_home_living' => 'Home & living', + 'personalization_life_event_category_travel_experiences' => 'Travel & experiences', + 'personalization_life_event_category_health_wellness' => 'Health & wellness', + + 'personalization_life_event_type_new_job' => 'New job', + 'personalization_life_event_type_retirement' => 'Retirement', + 'personalization_life_event_type_new_school' => 'New school', + 'personalization_life_event_type_study_abroad' => 'Study abroad', + 'personalization_life_event_type_volunteer_work' => 'Volunteer work', + 'personalization_life_event_type_published_book_or_paper' => 'Published a book or paper', + 'personalization_life_event_type_military_service' => 'Military service', + 'personalization_life_event_type_first_met' => 'First met', + 'personalization_life_event_type_new_relationship' => 'New relationship', + 'personalization_life_event_type_engagement' => 'Engagement', + 'personalization_life_event_type_marriage' => 'Marriage', + 'personalization_life_event_type_anniversary' => 'Anniversary', + 'personalization_life_event_type_expecting_a_baby' => 'Expecting a baby', + 'personalization_life_event_type_new_child' => 'New child', + 'personalization_life_event_type_new_family_member' => 'New family member', + 'personalization_life_event_type_new_pet' => 'New pet', + 'personalization_life_event_type_end_of_relationship' => 'End of relationship', + 'personalization_life_event_type_loss_of_a_loved_one' => 'Loss of a loved one', + 'personalization_life_event_type_moved' => 'Moved', + 'personalization_life_event_type_bought_a_home' => 'Bought a home', + 'personalization_life_event_type_home_improvement' => 'Home improvement', + 'personalization_life_event_type_holidays' => 'Holidays', + 'personalization_life_event_type_new_vehicle' => 'New vehicle', + 'personalization_life_event_type_new_roommate' => 'New roommate', + 'personalization_life_event_type_overcame_an_illness' => 'Overcame an illness', + 'personalization_life_event_type_quit_a_habit' => 'Quit a habit', + 'personalization_life_event_type_new_eating_habits' => 'New eating habits', + 'personalization_life_event_type_weight_loss' => 'Weight loss', + 'personalization_life_event_type_wear_glass_or_contact' => 'Started wearing glasses or contacts', + 'personalization_life_event_type_broken_bone' => 'Broke a bone', + 'personalization_life_event_type_removed_braces' => 'Had braces removed', + 'personalization_life_event_type_surgery' => 'Had surgery', + 'personalization_life_event_type_dentist' => 'Had dental treatment', + 'personalization_life_event_type_new_sport' => 'Started playing a new sport', + 'personalization_life_event_type_new_hobby' => 'Took up a new hobby', + 'personalization_life_event_type_new_instrument' => 'Started learning a new instrument', + 'personalization_life_event_type_new_language' => 'Started learning a new language', + 'personalization_life_event_type_tattoo_or_piercing' => 'Tattoo or piercing', + 'personalization_life_event_type_new_license' => 'New license', + 'personalization_life_event_type_travel' => 'Travel', + 'personalization_life_event_type_achievement_or_award' => 'Achievement or award', + 'personalization_life_event_type_changed_beliefs' => 'Changed beliefs', + 'personalization_life_event_type_first_word' => 'First word', + 'personalization_life_event_type_first_kiss' => 'First kiss', + + 'storage_title' => 'Storage', + 'storage_account_info' => 'Your account limit is :accountLimit MB. Your current usage is :currentAccountSize MB (about :percentUsage%).', + 'storage_upgrade_notice' => 'Upgrade your account to be able to upload documents and photos.', + 'storage_description' => 'Here you can see all the documents and photos uploaded about your contacts.', + + 'dav_title' => 'WebDAV', + 'dav_description' => 'Here you can find all settings to use WebDAV resources for CardDAV and CalDAV exports.', + 'dav_copy_help' => 'Copy into your clipboard', + 'dav_clipboard_copied' => 'Value copied into your clipboard', + 'dav_url_base' => 'Base url for all CardDAV and CalDAV resources:', + 'dav_connect_help' => 'You can connect your contacts and/or calendars with this base url on you phone or computer.', + 'dav_connect_help2' => 'Use your login (email) and create an API token as the password to authenticate.', + 'dav_url_carddav' => 'CardDAV url for Contacts resource:', + 'dav_url_caldav_birthdays' => 'CalDAV url for Birthdays resources:', + 'dav_url_caldav_tasks' => 'CalDAV url for Tasks resources:', + 'dav_title_carddav' => 'CardDAV', + 'dav_title_caldav' => 'CalDAV', + 'dav_carddav_export' => 'Export all contacts in one file', + 'dav_caldav_birthdays_export' => 'Export all birthdays in one file', + 'dav_caldav_tasks_export' => 'Export all tasks in one file', + + 'archive_title' => 'Archive all of the contacts in your account', + 'archive_desc' => 'This will archive all of the contacts in your account.', + 'archive_cta' => 'Archive all of your contacts', + + 'logs_title' => 'Everything that has happened to this account', + 'logs_actor' => 'Actor', + 'logs_timestamp' => 'Timestamp', + 'logs_description' => 'Description', + 'logs_subject' => 'Subject', + 'logs_size' => 'Size (Kb)', + 'logs_object' => 'Object', +]; diff --git a/resources/lang/ru/validation.php b/resources/lang/ru/validation.php new file mode 100644 index 0000000..905824e --- /dev/null +++ b/resources/lang/ru/validation.php @@ -0,0 +1,166 @@ + 'Вы должны принять :attribute.', + 'active_url' => 'Поле :attribute содержит недействительный URL.', + 'after' => 'В поле :attribute должна быть дата после :date.', + 'after_or_equal' => 'В поле :attribute должна быть дата после или равняться :date.', + 'alpha' => 'Поле :attribute может содержать только буквы.', + 'alpha_dash' => 'Поле :attribute может содержать только буквы, цифры, дефис и нижнее подчеркивание.', + 'alpha_num' => 'Поле :attribute может содержать только буквы и цифры.', + 'array' => 'Поле :attribute должно быть массивом.', + 'before' => 'В поле :attribute должна быть дата до :date.', + 'before_or_equal' => 'В поле :attribute должна быть дата до или равняться :date.', + 'between' => [ + 'numeric' => 'Поле :attribute должно быть между :min и :max.', + 'file' => 'Размер файла в поле :attribute должен быть между :min и :max Килобайт(а).', + 'string' => 'Количество символов в поле :attribute должно быть между :min и :max.', + 'array' => 'Количество элементов в поле :attribute должно быть между :min и :max.', + ], + 'boolean' => 'Поле :attribute должно иметь значение логического типа.', + 'confirmed' => 'Поле :attribute не совпадает с подтверждением.', + 'date' => 'Поле :attribute не является датой.', + 'date_equals' => 'Поле :attribute должно быть датой равной :date.', + 'date_format' => 'Поле :attribute не соответствует формату :format.', + 'different' => 'Поля :attribute и :other должны различаться.', + 'digits' => 'Длина цифрового поля :attribute должна быть :digits.', + 'digits_between' => 'Длина цифрового поля :attribute должна быть между :min и :max.', + 'dimensions' => 'Поле :attribute имеет недопустимые размеры изображения.', + 'distinct' => 'Поле :attribute содержит повторяющееся значение.', + 'email' => 'Поле :attribute должно быть действительным электронным адресом.', + 'ends_with' => 'Поле :attribute должно заканчиваться одним из следующих значений: :values', + 'exists' => 'Выбранное значение для :attribute некорректно.', + 'file' => 'Поле :attribute должно быть файлом.', + 'filled' => 'Поле :attribute обязательно для заполнения.', + 'gt' => [ + 'numeric' => 'Поле :attribute должно быть больше :value.', + 'file' => 'Размер файла в поле :attribute должен быть больше :value Килобайт(а).', + 'string' => 'Количество символов в поле :attribute должно быть больше :value.', + 'array' => 'Количество элементов в поле :attribute должно быть больше :value.', + ], + 'gte' => [ + 'numeric' => 'Поле :attribute должно быть больше или равно :value.', + 'file' => 'Размер файла в поле :attribute должен быть больше или равен :value Килобайт(а).', + 'string' => 'Количество символов в поле :attribute должно быть больше или равно :value.', + 'array' => 'Количество элементов в поле :attribute должно быть больше или равно :value.', + ], + 'image' => 'Поле :attribute должно быть изображением.', + 'in' => 'Выбранное значение для :attribute ошибочно.', + 'in_array' => 'Поле :attribute не существует в :other.', + 'integer' => 'Поле :attribute должно быть целым числом.', + 'ip' => 'Поле :attribute должно быть действительным IP-адресом.', + 'ipv4' => 'Поле :attribute должно быть действительным IPv4-адресом.', + 'ipv6' => 'Поле :attribute должно быть действительным IPv6-адресом.', + 'json' => 'Поле :attribute должно быть JSON строкой.', + 'lt' => [ + 'numeric' => 'Поле :attribute должно быть меньше :value.', + 'file' => 'Размер файла в поле :attribute должен быть меньше :value Килобайт(а).', + 'string' => 'Количество символов в поле :attribute должно быть меньше :value.', + 'array' => 'Количество элементов в поле :attribute должно быть меньше :value.', + ], + 'lte' => [ + 'numeric' => 'Поле :attribute должно быть меньше или равно :value.', + 'file' => 'Размер файла в поле :attribute должен быть меньше или равен :value Килобайт(а).', + 'string' => 'Количество символов в поле :attribute должно быть меньше или равно :value.', + 'array' => 'Количество элементов в поле :attribute должно быть меньше или равно :value.', + ], + 'max' => [ + 'numeric' => 'Поле :attribute не может быть более :max.', + 'file' => 'Размер файла в поле :attribute не может быть более :max Килобайт(а).', + 'string' => 'Количество символов в поле :attribute не может превышать :max.', + 'array' => 'Количество элементов в поле :attribute не может превышать :max.', + ], + 'mimes' => 'Поле :attribute должно быть файлом одного из следующих типов: :values.', + 'mimetypes' => 'Поле :attribute должно быть файлом одного из следующих типов: :values.', + 'min' => [ + 'numeric' => 'Поле :attribute должно быть не менее :min.', + 'file' => 'Размер файла в поле :attribute должен быть не менее :min Килобайт(а).', + 'string' => 'Количество символов в поле :attribute должно быть не менее :min.', + 'array' => 'Количество элементов в поле :attribute должно быть не менее :min.', + ], + 'not_in' => 'Выбранное значение для :attribute ошибочно.', + 'not_regex' => 'Выбранный формат для :attribute ошибочный.', + 'numeric' => 'Поле :attribute должно быть числом.', + 'password' => 'Неверный пароль.', + 'present' => 'Поле :attribute должно присутствовать.', + 'regex' => 'Поле :attribute имеет ошибочный формат.', + 'required' => 'Поле :attribute обязательно для заполнения.', + 'required_if' => 'Поле :attribute обязательно для заполнения, когда :other равно :value.', + 'required_unless' => 'Поле :attribute обязательно для заполнения, когда :other не равно :values.', + 'required_with' => 'Поле :attribute обязательно для заполнения, когда :values указано.', + 'required_with_all' => 'Поле :attribute обязательно для заполнения, когда :values указано.', + 'required_without' => 'Поле :attribute обязательно для заполнения, когда :values не указано.', + 'required_without_all' => 'Поле :attribute обязательно для заполнения, когда ни одно из :values не указано.', + 'same' => 'Значение :attribute должно совпадать с :other.', + 'size' => [ + 'numeric' => 'Поле :attribute должно быть равным :size.', + 'file' => 'Размер файла в поле :attribute должен быть равен :size Килобайт(а).', + 'string' => 'Количество символов в поле :attribute должно быть равным :size.', + 'array' => 'Количество элементов в поле :attribute должно быть равным :size.', + ], + 'starts_with' => 'Поле :attribute должно начинаться из одного из следующих значений: :values', + 'string' => 'Поле :attribute должно быть строкой.', + 'timezone' => 'Поле :attribute должно быть действительным часовым поясом.', + 'unique' => 'Такое значение поля :attribute уже существует.', + 'uploaded' => 'Загрузка поля :attribute не удалась.', + 'url' => 'Поле :attribute имеет ошибочный формат.', + 'uuid' => 'Поле :attribute должно быть корректным UUID.', + + /* + |-------------------------------------------------------------------------- + | Custom Validation Language Lines + |-------------------------------------------------------------------------- + | + | Here you may specify custom validation messages for attributes using the + | convention "attribute.rule" to name the lines. This makes it quick to + | specify a specific custom language line for a given attribute rule. + | + */ + + 'custom' => [ + 'attribute-name' => [ + 'rule-name' => 'custom-message', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Custom Validation Attributes + |-------------------------------------------------------------------------- + | + | The following language lines are used to swap our attribute placeholder + | with something more reader friendly such as "E-Mail Address" instead + | of "email". This simply helps us make our message more expressive. + | + */ + + 'attributes' => [], + + 'vue' => [ + 'max' => [ + 'numeric' => '{field} не может быть больше, чем {max}.', + 'string' => 'Количество символов в {field} не может превышать {max}.', + ], + 'required' => '{field} обязательно.', + 'url' => '{field} не является действительным URL.', + ], + +]; diff --git a/resources/lang/sv.json b/resources/lang/sv.json new file mode 100644 index 0000000..ddea72e --- /dev/null +++ b/resources/lang/sv.json @@ -0,0 +1,7 @@ +{ + "The :attribute must contain at least one uppercase and one lowercase letter.": "The :attribute must contain at least one uppercase and one lowercase letter.", + "The :attribute must contain at least one letter.": "The :attribute must contain at least one letter.", + "The :attribute must contain at least one symbol.": "The :attribute must contain at least one symbol.", + "The :attribute must contain at least one number.": "The :attribute must contain at least one number.", + "The given :attribute has appeared in a data leak. Please choose a different :attribute.": "The given :attribute has appeared in a data leak. Please choose a different :attribute." +} diff --git a/resources/lang/sv/app.php b/resources/lang/sv/app.php new file mode 100644 index 0000000..3ec545b --- /dev/null +++ b/resources/lang/sv/app.php @@ -0,0 +1,571 @@ + 'Ja', + 'no' => 'Nej', + 'update' => 'Uppdatera', + 'save' => 'Spara', + 'add' => 'Lägg till', + 'cancel' => 'Avbryt', + 'confirm' => 'Bekräfta', + 'delete_confirm' => 'Är du säker?', + 'delete' => 'Radera', + 'edit' => 'Redigera', + 'upload' => 'Ladda upp', + 'download' => 'Hämta', + 'save_close' => 'Spara och stäng', + 'close' => 'Stäng', + 'copy' => 'Kopiera', + 'create' => 'Skapa', + 'remove' => 'Radera', + 'revoke' => 'Återkalla', + 'done' => 'Klar', + 'back' => 'Tillbaka', + 'verify' => 'Verifiera', + 'new' => 'ny', + 'unknown' => 'Jag vet inte', + 'load_more' => 'Ladda mer', + 'loading' => 'Laddar…', + 'with' => 'med', + 'today' => 'idag', + 'yesterday' => 'igår', + 'another_day' => 'en annan dag', + 'date' => 'Datum', + 'type' => 'Typ', + 'zoom' => 'Zoom', + 'upgrade' => 'Uppgradera för att låsa upp', + 'percent_uploaded' => '{percent}% uppladdat', + 'retry' => 'Försök igen', + 'filter' => 'Filtrera listan', + 'go_back' => 'Gå tillbaka', + 'file_selected' => 'En fil vald…|{count} filer valda…', + + 'application_title' => 'Monica – Personlig Relationsansvarig', + 'application_description' => 'Monica är ett verktyg för att hantera dina interaktioner med dina nära och kära, vänner och familj.', + 'application_og_title' => 'Skapa bättre relationer med dina nära och kära. Gratis online CRM för vänner och familj.', + + 'markdown_description' => 'Vill du formatera din text fint? Vi stöder Markdown för att lägga till fetstil, kursiva, listor och mycket mer.', + 'markdown_link' => 'Läs dokumentation', + + 'header_settings_link' => 'Inställningar', + 'header_logout_link' => 'Logga ut', + 'header_changelog_link' => 'Produkt ändringar', + + 'main_nav_cta' => 'Lägg till personer', + 'main_nav_dashboard' => 'Instrumentpanel', + 'main_nav_family' => 'Kontakter', + 'main_nav_journal' => 'Dagbok', + 'main_nav_activities' => 'Aktiviteter', + 'main_nav_tasks' => 'Uppgifter', + + 'footer_remarks' => 'Kommentarer?', + 'footer_send_email' => 'Skicka ett mail till oss', + 'footer_privacy' => 'Integritetspolicy', + 'footer_release' => 'Information om utgivningen', + 'footer_newsletter' => 'Nyhetsbrev', + 'footer_source_code' => 'Bidra', + 'footer_version' => 'Version: :version', + 'footer_new_version' => 'En ny version av Monica är tillgänglig', + + 'footer_modal_version_whats_new' => 'Vad är nytt', + 'footer_modal_version_release_away' => 'Du är 1 version bakom den senaste versionen. Du bör uppdatera din instans.|Du är :number releaser bakom den senaste versionen. Du bör uppdatera din instans.', + + 'breadcrumb_dashboard' => 'Instrumentpanel', + 'breadcrumb_list_contacts' => 'Lista över personer', + 'breadcrumb_archived_contacts' => 'Arkiverade kontakter', + 'breadcrumb_journal' => 'Dagbok', + 'breadcrumb_settings' => 'Inställningar', + 'breadcrumb_settings_export' => 'Exportera', + 'breadcrumb_settings_users' => 'Användare', + 'breadcrumb_settings_users_add' => 'Lägg till en användare', + 'breadcrumb_settings_subscriptions' => 'Prenumeration', + 'breadcrumb_settings_import' => 'Importera', + 'breadcrumb_settings_import_report' => 'Importera rapport', + 'breadcrumb_settings_import_upload' => 'Ladda upp', + 'breadcrumb_settings_tags' => 'Taggar', + 'breadcrumb_add_significant_other' => 'Lägg till partner/respektive', + 'breadcrumb_edit_significant_other' => 'Redigera partner/respektive', + 'breadcrumb_add_note' => 'Lägg till en anteckning', + 'breadcrumb_edit_note' => 'Redigera en anteckning', + 'breadcrumb_api' => 'API', + 'breadcrumb_dav' => 'DAV resurser', + 'breadcrumb_edit_introductions' => 'Hur träffades ni', + 'breadcrumb_settings_personalization' => 'Anpassa', + 'breadcrumb_settings_security' => 'Säkerhet', + 'breadcrumb_settings_security_2fa' => 'Tvåfaktorsautentisering', + 'breadcrumb_profile' => 'Profil för :name', + + 'gender_male' => 'Man', + 'gender_female' => 'Kvinna', + 'gender_none' => 'Vill inte säga', + 'gender_no_gender' => 'Inget kön', + + 'error_title' => 'Hoppsan! Något gick fel.', + 'error_unauthorized' => 'Du har inte rätt att redigera denna resurs.', + 'error_user_account' => 'Den här användaren tillhör inte det angivna kontot.', + 'error_save' => 'Ett fel uppstod när vi försökte spara data.', + 'error_try_again' => 'Något gick fel. Försök igen.', + 'error_id' => 'Fel ID: ID', + 'error_unavailable' => 'Tjänsten är inte tillgänglig', + 'error_maintenance' => 'Underhåll pågår, vi återkommer strax.', + 'error_help' => 'Vi kommer snart tillbaka.', + 'error_twitter' => 'Följ vårt Twitter-konto för att bli uppdaterad när sidan är uppe igen.', + 'error_no_term' => 'Det finns ingen policy för denna instans ännu.', + + 'default_save_success' => 'Datan har sparats.', + + 'compliance_title' => 'Ledsen för avbrottet.', + 'compliance_desc' => 'Vi har ändrat våra användarvillkor och sekretesspolicy. Enligt lag måste vi be dig granska dem och acceptera dem så att du kan fortsätta använda ditt konto.', + 'compliance_desc_end' => 'Vi gör inget otäckt med dina uppgifter eller ditt konto och vi kommer aldrig att göra det.', + 'compliance_terms' => 'Acceptera nya villkor och sekretesspolicy', + + // Relationship types + // Yes, each relationship type has 8 strings associated with it. + // This is because we need to indicate the name of the relationship type, + // and also the name of the opposite side of this relationship (father/son), + // and then, the feminine version of the string. Finally, in some sentences + // in the UI, we need to include the name of the person we add the relationship + // to. + 'relationship_type_group_love' => 'Kärleksrelationer', + 'relationship_type_group_family' => 'Familjerelationer', + 'relationship_type_group_friend' => 'Vänrelationer', + 'relationship_type_group_work' => 'Arbetsrelationer', + 'relationship_type_group_other' => 'Annan typ av relationer', + + 'relationship_type_partner' => 'partner', + 'relationship_type_partner_female' => 'partner', + 'relationship_type_partner_male' => 'partner', + 'relationship_type_partner_with_name' => '.name\'s partner', + 'relationship_type_partner_female_with_name' => ':name\'s partner', + 'relationship_type_partner_male_with_name' => ':name’s partner', + + 'relationship_type_spouse' => 'make', + 'relationship_type_spouse_female' => 'fru', + 'relationship_type_spouse_male' => 'make', + 'relationship_type_spouse_with_name' => ':name\'s make', + 'relationship_type_spouse_female_with_name' => ':name’s fru', + 'relationship_type_spouse_male_with_name' => ':name’s make', + + 'relationship_type_date' => 'dejt', + 'relationship_type_date_female' => 'dejt', + 'relationship_type_date_male' => 'date', + 'relationship_type_date_with_name' => ':name\'s dejt', + 'relationship_type_date_female_with_name' => ':name\'s dejt', + 'relationship_type_date_male_with_name' => ':name’s date', + + 'relationship_type_lover' => 'älskare', + 'relationship_type_lover_female' => 'älskare', + 'relationship_type_lover_male' => 'älskare', + 'relationship_type_lover_with_name' => ':name\'s älskare', + 'relationship_type_lover_female_with_name' => ':name\'s älskarinna', + 'relationship_type_lover_male_with_name' => ':name’s älskare', + + 'relationship_type_inlovewith' => 'kär i', + 'relationship_type_inlovewith_female' => 'kär i', + 'relationship_type_inlovewith_male' => 'kär i', + 'relationship_type_inlovewith_with_name' => 'någon :name är kär i', + 'relationship_type_inlovewith_female_with_name' => 'någon :name är kär i', + 'relationship_type_inlovewith_male_with_name' => 'någon :name är kär i', + + 'relationship_type_lovedby' => 'älskad av', + 'relationship_type_lovedby_female' => 'älskad av', + 'relationship_type_lovedby_male' => 'älskad av', + 'relationship_type_lovedby_with_name' => ':name\'s hemliga älskare', + 'relationship_type_lovedby_female_with_name' => ':name\'s hemliga älskarinna', + 'relationship_type_lovedby_male_with_name' => ':name\'s hemliga älskare', + + 'relationship_type_ex' => 'föredetta', + 'relationship_type_ex_female' => 'ex-flickvän', + 'relationship_type_ex_male' => 'föredetta pojkvän', + 'relationship_type_ex_with_name' => ':name’s föredetta partner', + 'relationship_type_ex_female_with_name' => ':name\'s ex-flickvän', + 'relationship_type_ex_male_with_name' => ':name’s föredetta pojkvän', + + 'relationship_type_parent' => 'förälder', + 'relationship_type_parent_female' => 'mor', + 'relationship_type_parent_male' => 'fader', + 'relationship_type_parent_with_name' => ':name’s förälder', + 'relationship_type_parent_female_with_name' => ':name\'s mamma', + 'relationship_type_parent_male_with_name' => ':name’s fader', + + 'relationship_type_child' => 'barn', + 'relationship_type_child_female' => 'dotter', + 'relationship_type_child_male' => 'son', + 'relationship_type_child_with_name' => ':name\'s barn', + 'relationship_type_child_female_with_name' => ':name\'s dotter', + 'relationship_type_child_male_with_name' => ':name\'s son', + + 'relationship_type_stepparent' => 'styvförälder', + 'relationship_type_stepparent_female' => 'styvmor', + 'relationship_type_stepparent_male' => 'styvfar', + 'relationship_type_stepparent_with_name' => ':name’s styvförälder', + 'relationship_type_stepparent_female_with_name' => ':name\'s styvmor', + 'relationship_type_stepparent_male_with_name' => ':name’s styvfar', + + 'relationship_type_stepchild' => 'styvbarn', + 'relationship_type_stepchild_female' => 'styvdotter', + 'relationship_type_stepchild_male' => 'styvson', + 'relationship_type_stepchild_with_name' => ':name’s styvbarn', + 'relationship_type_stepchild_female_with_name' => ':name\'s styvdotter', + 'relationship_type_stepchild_male_with_name' => ':name’s styvson', + + 'relationship_type_sibling' => 'syskon', + 'relationship_type_sibling_female' => 'syster', + 'relationship_type_sibling_male' => 'broder', + 'relationship_type_sibling_with_name' => ':name’s syskon', + 'relationship_type_sibling_female_with_name' => ':name\'s syster', + 'relationship_type_sibling_male_with_name' => ':name’s broder', + + 'relationship_type_grandparent' => 'mor- eller farförälder', + 'relationship_type_grandparent_female' => 'farmor/mormor', + 'relationship_type_grandparent_male' => 'farfar/morfar', + 'relationship_type_grandparent_with_name' => ':name’s far- eller morförälder', + 'relationship_type_grandparent_female_with_name' => ':name’s far- eller mormoder', + 'relationship_type_grandparent_male_with_name' => ':name’s far- eller morfar', + + 'relationship_type_grandchild' => 'barnbarn', + 'relationship_type_grandchild_female' => 'son-/dotterdotter', + 'relationship_type_grandchild_male' => 'son-/dotterson', + 'relationship_type_grandchild_with_name' => ':name’s barnbarn', + 'relationship_type_grandchild_female_with_name' => ':name’s son-/dotterdotter', + 'relationship_type_grandchild_male_with_name' => ':name’s son-/dotterson', + + 'relationship_type_uncle' => 'farbror', + 'relationship_type_uncle_female' => 'moster', + 'relationship_type_uncle_male' => 'far-/morbror', + 'relationship_type_uncle_with_name' => ':name\'s farbror', + 'relationship_type_uncle_female_with_name' => ':name\'s moster', + 'relationship_type_uncle_male_with_name' => ':name’s far-/morbror', + + 'relationship_type_nephew' => 'brorson', + 'relationship_type_nephew_female' => 'systerdotter', + 'relationship_type_nephew_male' => 'bror-/systerbarn', + 'relationship_type_nephew_with_name' => ':name\'s brorson', + 'relationship_type_nephew_female_with_name' => ':name\'s systerdotter', + 'relationship_type_nephew_male_with_name' => ':name’s bror-/systerbarn', + + 'relationship_type_cousin' => 'kusin', + 'relationship_type_cousin_female' => 'kusin', + 'relationship_type_cousin_male' => 'kusin', + 'relationship_type_cousin_with_name' => ':name\'s kusin', + 'relationship_type_cousin_female_with_name' => ':name\'s kusin', + 'relationship_type_cousin_male_with_name' => ':name\'s kusin', + + 'relationship_type_godfather' => 'gudförälder', + 'relationship_type_godfather_female' => 'gudmor', + 'relationship_type_godfather_male' => 'gudfar', + 'relationship_type_godfather_with_name' => ':name’s gudförälder', + 'relationship_type_godfather_female_with_name' => ':name\'s gudmamma', + 'relationship_type_godfather_male_with_name' => ':name’s gudfar', + + 'relationship_type_godson' => 'gudson/-dotter', + 'relationship_type_godson_female' => 'guddotter', + 'relationship_type_godson_male' => 'gudson', + 'relationship_type_godson_with_name' => ':name’s gudson/-dotter', + 'relationship_type_godson_female_with_name' => ':name\'s guddotter', + 'relationship_type_godson_male_with_name' => ':name’s gudson', + + 'relationship_type_friend' => 'vän', + 'relationship_type_friend_female' => 'vännina', + 'relationship_type_friend_male' => 'vän', + 'relationship_type_friend_with_name' => ':name\'s vännina', + 'relationship_type_friend_female_with_name' => ':name\'s vän', + 'relationship_type_friend_male_with_name' => ':name’s vän', + + 'relationship_type_bestfriend' => 'bästa vän', + 'relationship_type_bestfriend_female' => 'bästa vän', + 'relationship_type_bestfriend_male' => 'bästa vän', + 'relationship_type_bestfriend_with_name' => ':name\'s bästa vän', + 'relationship_type_bestfriend_female_with_name' => ':name\'s bästa vän', + 'relationship_type_bestfriend_male_with_name' => ':name\'s bästa vän', + + 'relationship_type_colleague' => 'kollega', + 'relationship_type_colleague_female' => 'kollega', + 'relationship_type_colleague_male' => 'kollega', + 'relationship_type_colleague_with_name' => ':name\'s kollega', + 'relationship_type_colleague_female_with_name' => ':name\'s kollega', + 'relationship_type_colleague_male_with_name' => ':name\'s kollega', + + 'relationship_type_boss' => 'chef', + 'relationship_type_boss_female' => 'chef', + 'relationship_type_boss_male' => 'chef', + 'relationship_type_boss_with_name' => ':name\'s chef', + 'relationship_type_boss_female_with_name' => ':name\'s chef', + 'relationship_type_boss_male_with_name' => ':name\'s chef', + + 'relationship_type_subordinate' => 'underordnad', + 'relationship_type_subordinate_female' => 'underordnad', + 'relationship_type_subordinate_male' => 'underordnad', + 'relationship_type_subordinate_with_name' => ':name\'s underordnade', + 'relationship_type_subordinate_female_with_name' => ':name\'s underordnade', + 'relationship_type_subordinate_male_with_name' => ':name\'s underordnade', + + 'relationship_type_mentor' => 'mentor', + 'relationship_type_mentor_female' => 'mentor', + 'relationship_type_mentor_male' => 'mentor', + 'relationship_type_mentor_with_name' => ':name\'s mentor', + 'relationship_type_mentor_female_with_name' => ':name\'s mentor', + 'relationship_type_mentor_male_with_name' => ':name\'s mentor', + + 'relationship_type_protege' => 'skyddsling', + 'relationship_type_protege_female' => 'skyddsling', + 'relationship_type_protege_male' => 'skyddsling', + 'relationship_type_protege_with_name' => ':name\'s skyddsling', + 'relationship_type_protege_female_with_name' => ':name\'s skyddsling', + 'relationship_type_protege_male_with_name' => ':name\'s skyddsling', + + 'relationship_type_ex_husband' => 'ex-make', + 'relationship_type_ex_husband_female' => 'ex-fru', + 'relationship_type_ex_husband_male' => 'ex-make', + 'relationship_type_ex_husband_with_name' => ':name’s ex-make', + 'relationship_type_ex_husband_female_with_name' => ':name\'s ex-fru', + 'relationship_type_ex_husband_male_with_name' => ':name’s ex-make', + + // emotions + 'emotion_primary_love' => 'Kärlek', + 'emotion_primary_joy' => 'Glädje', + 'emotion_primary_surprise' => 'Överraskning', + 'emotion_primary_anger' => 'Ilska', + 'emotion_primary_sadness' => 'Sorg', + 'emotion_primary_fear' => 'Rädsla', + + 'emotion_secondary_affection' => 'Tillgivenhet', + 'emotion_secondary_lust' => 'Lust', + 'emotion_secondary_longing' => 'Längtar', + 'emotion_secondary_cheerfulness' => 'Glädjande', + 'emotion_secondary_zest' => 'Iver', + 'emotion_secondary_contentment' => 'Belåtenhet', + 'emotion_secondary_pride' => 'Stolthet', + 'emotion_secondary_optimism' => 'Optimism', + 'emotion_secondary_enthrallment' => 'Fängslande', + 'emotion_secondary_relief' => 'Lättnad', + 'emotion_secondary_surprise' => 'Förvånad', + 'emotion_secondary_irritation' => 'Irriterad', + 'emotion_secondary_exasperation' => 'Irritation', + 'emotion_secondary_rage' => 'Raseri', + 'emotion_secondary_disgust' => 'Äcklad', + 'emotion_secondary_envy' => 'Avundsjuk', + 'emotion_secondary_suffering' => 'Lidande', + 'emotion_secondary_sadness' => 'Sorgsenhet', + 'emotion_secondary_disappointment' => 'Besvikelse', + 'emotion_secondary_shame' => 'Skam', + 'emotion_secondary_neglect' => 'Försumma', + 'emotion_secondary_sympathy' => 'Sympati', + 'emotion_secondary_horror' => 'Skräck', + 'emotion_secondary_nervousness' => 'Nervositet', + + 'emotion_adoration' => 'Dyrkan', + 'emotion_affection' => 'Tillgivenhet', + 'emotion_love' => 'Kärlek', + 'emotion_fondness' => 'Förkärlek', + 'emotion_liking' => 'Gillar', + 'emotion_attraction' => 'Attraktiv', + 'emotion_caring' => 'Omtänksam', + 'emotion_tenderness' => 'Ömhet', + 'emotion_compassion' => 'Medlidande', + 'emotion_sentimentality' => 'Sentimentalitet', + 'emotion_arousal' => 'Upphetsning', + 'emotion_desire' => 'Åtrå', + 'emotion_lust' => 'Lust', + 'emotion_passion' => 'Passion', + 'emotion_infatuation' => 'Förblindelse', + 'emotion_longing' => 'Längtan', + 'emotion_amusement' => 'Nöjen', + 'emotion_bliss' => 'Överlyckling', + 'emotion_cheerfulness' => 'Glädjande', + 'emotion_gaiety' => 'Munterhet', + 'emotion_glee' => 'Munterhet', + 'emotion_jolliness' => 'Överlycklig', + 'emotion_joviality' => 'Gladlynt', + 'emotion_joy' => 'Glädje', + 'emotion_delight' => 'Fröjd', + 'emotion_enjoyment' => 'Njutning', + 'emotion_gladness' => 'Glad', + 'emotion_happiness' => 'Lycka', + 'emotion_jubilation' => 'Jubel', + 'emotion_elation' => 'Förtjusning', + 'emotion_satisfaction' => 'Tillfredsställelse', + 'emotion_ecstasy' => 'Extas', + 'emotion_euphoria' => 'Eufori', + 'emotion_enthusiasm' => 'Entusiastisk', + 'emotion_zeal' => 'Iver', + 'emotion_zest' => 'Iver', + 'emotion_excitement' => 'Upphetsning', + 'emotion_thrill' => 'Spänning', + 'emotion_exhilaration' => 'Upprymdhet', + 'emotion_contentment' => 'Belåtenhet', + 'emotion_pleasure' => 'Njutning', + 'emotion_pride' => 'Stolt', + 'emotion_eagerness' => 'Ivrighet', + 'emotion_hope' => 'Hopp', + 'emotion_optimism' => 'Optimism', + 'emotion_enthrallment' => 'Fängslande', + 'emotion_rapture' => 'Hänföra', + 'emotion_relief' => 'Lättnad', + 'emotion_amazement' => 'Häpnadsväckande', + 'emotion_surprise' => 'Överraskning', + 'emotion_astonishment' => 'Häpnad', + 'emotion_aggravation' => 'Försvårande', + 'emotion_irritation' => 'Irriterad', + 'emotion_agitation' => 'Agitation', + 'emotion_annoyance' => 'Irritation', + 'emotion_grouchiness' => 'Gnällig', + 'emotion_grumpiness' => 'Grinig', + 'emotion_exasperation' => 'Ursinne', + 'emotion_frustration' => 'Frustration', + 'emotion_anger' => 'Ilska', + 'emotion_rage' => 'Raseri', + 'emotion_outrage' => 'Skandalös', + 'emotion_fury' => 'Ursinne', + 'emotion_wrath' => 'Vrede', + 'emotion_hostility' => 'Fiendskap', + 'emotion_ferocity' => 'Blodtörstig', + 'emotion_bitterness' => 'Bitterhet', + 'emotion_hate' => 'Hat', + 'emotion_loathing' => 'Äcklad', + 'emotion_scorn' => 'Förakt', + 'emotion_spite' => 'Illvilja', + 'emotion_vengefulness' => 'Hämndlysten', + 'emotion_dislike' => 'Ogilla', + 'emotion_resentment' => 'Förbittring', + 'emotion_disgust' => 'Äcklad', + 'emotion_revulsion' => 'Bakslag', + 'emotion_contempt' => 'Förakt', + 'emotion_envy' => 'Avundsjuk', + 'emotion_jealousy' => 'Avundsjuka', + 'emotion_agony' => 'Ångest', + 'emotion_suffering' => 'Lidande', + 'emotion_hurt' => 'Sårad', + 'emotion_anguish' => 'Plåga', + 'emotion_depression' => 'Depression', + 'emotion_despair' => 'Förtvivla', + 'emotion_hopelessness' => 'Hopplöshet', + 'emotion_gloom' => 'Dyster', + 'emotion_glumness' => 'Dysterhet', + 'emotion_sadness' => 'Sorg', + 'emotion_unhappiness' => 'Olycka', + 'emotion_grief' => 'Sorg', + 'emotion_sorrow' => 'Sorg', + 'emotion_woe' => 'Olycka/Elände', + 'emotion_misery' => 'Elände', + 'emotion_melancholy' => 'Melankoli', + 'emotion_dismay' => 'Förfäran', + 'emotion_disappointment' => 'Besvikelse', + 'emotion_displeasure' => 'Missnöje', + 'emotion_guilt' => 'Skuld', + 'emotion_shame' => 'Skam', + 'emotion_regret' => 'Ånger', + 'emotion_remorse' => 'Ångersfull', + 'emotion_alienation' => 'Utanförskap', + 'emotion_isolation' => 'Isolering', + 'emotion_neglect' => 'Försumma', + 'emotion_loneliness' => 'Ensamhet', + 'emotion_rejection' => 'Avvisad', + 'emotion_homesickness' => 'Hemlängtan', + 'emotion_defeat' => 'Nederlag', + 'emotion_dejection' => 'Nedstämdhet', + 'emotion_insecurity' => 'Osäkerhet', + 'emotion_embarrassment' => 'Generad', + 'emotion_humiliation' => 'Förödmjukelse', + 'emotion_insult' => 'Förolämpning', + 'emotion_pity' => 'Medlidande', + 'emotion_sympathy' => 'Sympati', + 'emotion_alarm' => 'Alarm', + 'emotion_shock' => 'Chock', + 'emotion_fear' => 'Rädsla', + 'emotion_fright' => 'Skräck', + 'emotion_horror' => 'Skräck', + 'emotion_terror' => 'Terror', + 'emotion_panic' => 'Panik', + 'emotion_hysteria' => 'Hysteri', + 'emotion_mortification' => 'Förtret', + 'emotion_anxiety' => 'Ångest', + 'emotion_nervousness' => 'Nervositet', + 'emotion_tenseness' => 'Spänd', + 'emotion_uneasiness' => 'Obehaglighet', + 'emotion_apprehension' => 'Oro', + 'emotion_worry' => 'Oroande', + 'emotion_distress' => 'I nöd', + 'emotion_dread' => 'Fruktan', + + // weather + 'weather_sunny' => 'Soligt', + 'weather_clear' => 'Klart', + 'weather_clear-day' => 'Klar', + 'weather_clear-night' => 'Klart (natt)', + 'weather_light-drizzle' => 'Lätt duggregn', + 'weather_patchy-light-drizzle' => 'Fläckvis lätt duggregn', + 'weather_patchy-light-rain' => 'Fläckvis lätt regn', + 'weather_light-rain' => 'Lätt regn', + 'weather_moderate-rain-at-times' => 'Måttligt regn ibland', + 'weather_moderate-rain' => 'Måttligt regn', + 'weather_patchy-rain-possible' => 'Möjligen fläckvisa skurar', + 'weather_heavy-rain-at-times' => 'Möjligen mycket regn', + 'weather_heavy-rain' => 'Spöregn', + 'weather_light-freezing-rain' => 'Lätt underkylt regn', + 'weather_moderate-or-heavy-freezing-rain' => 'Måttlig eller kraftigt underkylt regn', + 'weather_light-sleet' => 'Lätt snöblandat', + 'weather_moderate-or-heavy-rain-shower' => 'Måttliga eller kraftiga regnskurar', + 'weather_light-rain-shower' => 'Lätta regnskurar', + 'weather_torrential-rain-shower' => 'Skyfall', + 'weather_rain' => 'Regn', + 'weather_snow' => 'Snö', + 'weather_blowing-snow' => 'Snövindar', + 'weather_patchy-light-snow' => 'Delvis lätt snöfall', + 'weather_light-snow' => 'Lätt snö', + 'weather_patchy-moderate-snow' => 'Fläckvis måttlig snö', + 'weather_moderate-snow' => 'Måttligt snöfall', + 'weather_patchy-heavy-snow' => 'Fläckvis kraftigt snöfall', + 'weather_heavy-snow' => 'Kraftigt snöfall', + 'weather_light-snow-showers' => 'Lätta snöbyar', + 'weather_moderate-or-heavy-snow-showers' => 'Måttliga eller kraftiga snöbyar', + 'weather_patchy-snow-possible' => 'Fläckvist snöfall möjligt', + 'weather_patchy-sleet-possible' => 'Fläckvis snöblandat regn möjligt', + 'weather_moderate-or-heavy-sleet' => 'Måttligt eller kraftigt snöblandat regn', + 'weather_light-sleet-showers' => 'Lätt snöblandat regn', + 'weather_moderate-or-heavy-sleet-showers' => 'Måttligt eller tungt snöblandat regn', + 'weather_sleet' => 'Nöje', + 'weather_wind' => 'Vind', + 'weather_fog' => 'Dimma', + 'weather_freezing-fog' => 'Underkyld dimma', + 'weather_mist' => 'Dimma', + 'weather_blizzard' => 'Snöstorm', + 'weather_overcast' => 'Mulet', + 'weather_cloudy' => 'Molnigt', + 'weather_partly-cloudy-day' => 'Växlande molnighet', + 'weather_partly-cloudy-night' => 'Växlande molnighet', + 'weather_freezing-drizzle' => 'Underkylt duggregn', + 'weather_heavy-freezing-drizzle' => 'Kraftigt underkylt duggregn', + 'weather_patchy-freezing-drizzle-possible' => 'Fäckvis underkylt duggren möjligt', + 'weather_ice-pellets' => 'Hagel', + 'weather_light-showers-of-ice-pellets' => 'Lätta skurar av hagel', + 'weather_moderate-or-heavy-showers-of-ice-pellets' => 'Måttliga eller tunga skurar av hagel', + 'weather_thundery-outbreaks-possible' => 'Möjlighet till åska', + 'weather_patchy-light-rain-with-thunder' => 'Fläckvis lätt regn med åska', + 'weather_moderate-or-heavy-rain-with-thunder' => 'Måttliga eller tunga åskskurar', + 'weather_patchy-light-snow-with-thunder' => 'Fläckvis snö med åska', + 'weather_moderate-or-heavy-snow-with-thunder' => 'Måttlig eller tung snö med åskbyar', + 'weather_current_temperature_celsius' => ':temperatur °C', + 'weather_current_temperature_fahrenheit' => ':temperatur °F', + 'weather_current_title' => 'Aktuell väderikon', + + // dav + 'dav_contacts' => 'Kontakter', + 'dav_contacts_description' => ':name\'s kontakter', + 'dav_birthdays' => 'Födelsedagar', + 'dav_birthdays_description' => ':name\'s kontakter födelsedagar', + 'dav_tasks' => 'Uppgifter', + 'dav_tasks_description' => ':name\'s uppgifter', + + // contact list + 'contact_list_avatar' => 'Avatar', + 'contact_list_name' => 'Kontakt', + 'contact_list_description' => 'Beskrivning', + +]; diff --git a/resources/lang/sv/auth.php b/resources/lang/sv/auth.php new file mode 100644 index 0000000..d49f643 --- /dev/null +++ b/resources/lang/sv/auth.php @@ -0,0 +1,89 @@ + 'Uppgifterna stämmer inte överrens med våra register.', + 'throttle' => 'För många inloggningsförsök. Vänligen försök igen om :seconds sekunder.', + 'not_authorized' => 'Du har inte behörighet att utföra denna åtgärd', + 'signup_disabled' => 'Registreringen är för närvarande inaktiverad', + 'signup_error' => 'Ett fel inträffade vid försök att registrera användaren', + 'back_homepage' => 'Tillbaka till startsidan', + 'mfa_auth_otp' => 'Autentisera med din tvåfaktorsenhet', + 'mfa_auth_webauthn' => 'Autentisera med en säkerhetsnyckel (WebAuthn)', + '2fa_title' => 'Tvåfaktorsautentisering', + '2fa_wrong_validation' => 'Den tvåfaktorsautentiseringen har misslyckats.', + '2fa_one_time_password' => 'Tvåfaktorsautentiseringskod', + '2fa_recuperation_code' => 'Ange en tvåfaktorsåterställningskod', + '2fa_one_time_or_recuperation' => 'Ange en auktoriseringskod eller återställningskod', + '2fa_otp_help' => 'Öppna din tvåfaktorsautentisering mobilapp och kopiera koden', + + 'login_to_account' => 'Logga in på ditt konto', + 'login_with_recovery' => 'Logga in med en återställningskod', + 'login_again' => 'Logga in igen på ditt konto', + 'email' => 'E-post', + 'password' => 'Lösenord', + 'recovery' => 'Återställningskod', + 'login' => 'Logga in', + 'button_remember' => 'Kom ihåg mig', + 'password_forget' => 'Glömt Ditt lösenord?', + 'password_reset' => 'Återställ ditt lösenord', + 'use_recovery' => 'Eller så kan du använda en återställningskod', + 'signup_no_account' => 'Har du inget konto?', + 'signup' => 'Registrera dig', + 'create_account' => 'Skapa det första kontot genom att registrera dig', + 'change_language_title' => 'Växla språk:', + 'change_language' => 'Byt språk till', + + 'password_reset_title' => 'Återställ lösenord', + 'password_reset_email' => 'E-postadress', + 'password_reset_send_link' => 'Skicka lösenordsåterställningslänk', + 'password_reset_password' => 'Lösenord', + 'password_reset_password_confirm' => 'Bekräfta Lösenord', + 'password_reset_action' => 'Återställ lösenord', + 'password_reset_email_content' => 'Klicka här för att återställa ditt lösenord:', + + 'register_title_welcome' => 'Välkommen till din nyinstallerade Monica-instans', + 'register_create_account' => 'Du måste skapa ett konto för att använda Monica', + 'register_title_create' => 'Skapa ditt Monica-konto', + 'register_login' => 'Logga in om du redan har ett konto.', + 'register_email' => 'Ange en giltig e-postadress', + 'register_email_example' => 'du@hem', + 'register_firstname' => 'Förnamn', + 'register_firstname_example' => 'ex: John', + 'register_lastname' => 'Efternamn', + 'register_lastname_example' => 'ex. Svensson', + 'register_password' => 'Lösenord', + 'register_password_example' => 'Ange ett säkert lösenord', + 'register_password_confirmation' => 'Bekräftelse på lösenord', + 'register_action' => 'Registrera', + 'register_policy' => 'Att registrera dig innebär att du har läst och godkänner vår Integritetspolicy och Användarvillkor.', + 'register_invitation_email' => 'Av säkerhetsskäl ber vi dig att ange e-postmeddelandet för den person som har bjudit in dig till detta konto. Denna information finns i e-postmeddelandet med inbjudan.', + + 'confirmation_title' => 'Verifiera e-postadressen', + 'confirmation_fresh' => 'En ny verifieringslänk har skickats till din e-postadress.', + 'confirmation_check' => 'Innan du fortsätter, kontrollera din e-post efter en verifieringslänk.', + 'confirmation_request_another' => 'Om du inte fick e-postmeddelandet klicka här för att begära en annan.', + + 'confirmation_again' => 'Om du vill ändra din e-postadress kan du klicka här.', + 'email_change_current_email' => 'Nuvarande e-postadresser:', + 'email_change_title' => 'Ändra din e-postadress', + 'email_change_new' => 'Ny e-postadress', + 'email_changed' => 'Din e-postadress har ändrats. Kolla din brevlåda för att validera den.', +]; diff --git a/resources/lang/sv/changelog.php b/resources/lang/sv/changelog.php new file mode 100644 index 0000000..6d83a57 --- /dev/null +++ b/resources/lang/sv/changelog.php @@ -0,0 +1,12 @@ + 'Produkt ändringar', + 'note' => 'Obs: Tyvärr är denna sida endast på engelska.', +]; diff --git a/resources/lang/sv/dashboard.php b/resources/lang/sv/dashboard.php new file mode 100644 index 0000000..cbff331 --- /dev/null +++ b/resources/lang/sv/dashboard.php @@ -0,0 +1,42 @@ + 'Välkommen till ditt konto!', + 'dashboard_blank_description' => 'Monica är platsen för att organisera alla de interaktioner du har med de du bryr dig om.', + 'dashboard_blank_cta' => 'Lägg till din första kontakt', + 'dashboard_blank_illustration' => 'Illustration av Freepik', + + 'notes_title' => 'Du har inga stjärnmärkta anteckningar ännu.', + + 'tab_recent_calls' => 'Senaste samtal', + 'tab_favorite_notes' => 'Favoritanteckningar', + 'tab_calls_blank' => 'Du har inte loggat ett samtal än.', + 'tab_debts' => 'Skulder', + 'tab_debts_blank' => 'Du har inte loggat någon skuld ännu.', + 'tab_tasks' => 'Uppgifter', + 'tab_tasks_blank' => 'Du har inte någon uppgift ännu.', + + 'tasks_add_task_placeholder' => 'Vad handlar denna uppgift om?', + 'tasks_tab_your_contacts' => 'Uppgifter relaterade till dina kontakter', + 'tasks_tab_your_tasks' => 'Dina uppgifter', + 'tasks_add_note' => 'Tryck på Enter för att lägga till uppgiften.', + 'task_add_cta' => 'Lägg till en uppgift', + + 'debts_you_owe' => 'Du är skyldig', + + 'statistics_contacts' => 'Kontakter', + 'statistics_activities' => 'Aktiviteter', + 'statistics_gifts' => 'Gåvor', + + 'reminders_next_months' => 'Händelser under de kommande 3 månaderna', + 'reminders_none' => 'Ingen påminnelse för denna månad.', + + 'product_changes' => 'Produkt ändringar', + 'product_view_details' => 'Visa detaljer', +]; diff --git a/resources/lang/sv/format.php b/resources/lang/sv/format.php new file mode 100644 index 0000000..b1dc5c8 --- /dev/null +++ b/resources/lang/sv/format.php @@ -0,0 +1,36 @@ + 'd M, Y H:i', + 'short_date_year' => 'd M, Y', + 'short_date' => 'd M', + 'short_month' => 'M', + 'short_month_year' => 'M Y', + 'short_day' => 'D', + 'full_date_year' => 'F d, Y', + 'full_month' => 'F', + 'full_month_year' => 'F Y', + 'full_hour' => 'h.i A', + + 'short_text' => '{text}…', +]; diff --git a/resources/lang/sv/journal.php b/resources/lang/sv/journal.php new file mode 100644 index 0000000..40640ab --- /dev/null +++ b/resources/lang/sv/journal.php @@ -0,0 +1,38 @@ + 'Hur var din dag? Du kan betygsätta den en gång om dagen.', + 'journal_come_back' => 'Tack. Kom tillbaka imorgon för att betygsätta din dag igen.', + 'journal_description' => 'Obs: tidskriften listar både manuella journalposter och automatiska poster som Aktiviteter gjorda med dina kontakter. Medan du kan ta bort journalposter manuellt, måste du ta bort aktiviteten direkt på kontaktsidan.', + 'journal_add' => 'Lägg till en journalpost', + 'journal_edit' => 'Redigera en journalpost', + 'journal_empty' => 'Tom journal', + 'journal_created_at' => 'Created at {date}', + 'journal_created_automatically' => 'Skapad automatiskt', + 'journal_entry_type_journal' => 'Journalpost', + 'journal_entry_type_activity' => 'Aktivitet', + 'journal_entry_rate' => 'Du betygsatte din dag.', + 'journal_add_comment' => 'Vill du lägga till en kommentar (valfritt)?', + 'journal_show_comment' => 'Visa kommentar', + 'entry_delete_success' => 'Journalposten har tagits bort.', + 'journal_add_title' => 'Titel (valfritt)', + 'journal_add_date' => 'Datum', + 'journal_add_post' => 'Inlägg', + 'journal_add_cta' => 'Spara', + 'journal_blank_cta' => 'Lägg till din första journalpost', + 'journal_blank_description' => 'Tidskriften låter dig skriva händelser som hände dig, och kom ihåg dem.', + 'delete_confirmation' => 'Är du säker på att du vill ta bort denna journalpost?', + 'apply_filter' => 'Apply filter', + 'start_date' => 'Start Date', + 'end_date' => 'End Date', + 'per_page' => 'Per Page', + 'sort_order' => 'Sort By Created At', + 'ascending' => 'Ascending', + 'descending' => 'Descending', +]; diff --git a/resources/lang/sv/logs.php b/resources/lang/sv/logs.php new file mode 100644 index 0000000..b18bcb2 --- /dev/null +++ b/resources/lang/sv/logs.php @@ -0,0 +1,29 @@ + 'Skapade kontakten.', + 'settings_log_contact_created_with_name' => 'Lade till :name som en kontakt.', + + // contat description update + 'contact_log_contact_description_updated' => 'Uppdaterade beskrivningen.', + 'settings_log_contact_description_updated_with_name' => 'Uppdaterade beskrivningen av :name.', + + // contact description clear + 'contact_log_contact_description_cleared' => 'Rensade beskrivningen.', + 'settings_log_contact_description_cleared_with_name' => 'Rensade beskrivningen av :name.', + + // contact work information update + 'contact_log_contact_work_updated' => 'Uppdaterade jobb information.', + 'settings_log_contact_work_updated_with_name' => 'Uppdaterade jobb information för :name.', + + // company created + 'settings_log_company_created' => 'Skapade ett företag som heter :name.', +]; diff --git a/resources/lang/sv/mail.php b/resources/lang/sv/mail.php new file mode 100644 index 0000000..6f3953c --- /dev/null +++ b/resources/lang/sv/mail.php @@ -0,0 +1,53 @@ + 'Påminnelse om :contact', + 'greetings' => 'Hej :username', + 'want_reminded_of' => 'Du ville bli påmind om :reason', + 'for' => 'För: :name', + 'comment' => 'Kommentar: :comment', + 'footer_contact_info' => 'Lägg till, visa, komplettera och ändra information om denna kontakt:', + 'footer_contact_info2' => 'Se :name\'s profil', + 'footer_contact_info2_link' => 'Se :name’s profil: :url', + + 'notification_subject_line' => 'Du har en kommande händelse', + 'notification_description' => 'Om :count dagar (på :date) kommer följande händelse att hända:', + + 'stay_in_touch_subject_line' => 'Håll kontakten med :name', + 'stay_in_touch_subject_description' => 'Du bad om att bli påmind om att hålla kontakten med :name varje :frequency dag. Du bad om att bli påmind om att hålla kontakten med :name varje :frequency dagar.', + + 'notifications_whoops' => 'Hoppsan!', + 'notifications_hello' => 'Hej!', + 'notifications_regards' => 'Hälsningar', + 'notifications_footer' => 'Om du har problem med att klicka på knappen ":actionText", kopiera och klistra in URL:en nedan i din webbläsare: [:actionURL](:actionURL)', + 'notifications_rights' => 'Alla rättigheter reserverade', + + 'confirmation_email_title' => 'Monica – e-postverifiering', + 'confirmation_email_intro'=> 'För att validera din e-post klicka på knappen nedan', + 'confirmation_email_button' => 'Verifiera e-postadress', + 'confirmation_email_bottom' => 'Om du inte har skapat ett konto krävs inga ytterligare åtgärder.', + + 'password_reset_title' => 'Monica – Återställ lösenordsmeddelanden', + 'password_reset_intro' => 'Du får detta e-postmeddelande eftersom vi fick en begäran om återställning av lösenord för ditt konto.', + 'password_reset_button' => 'Återställ lösenord', + 'password_reset_expiration' => 'Denna länk för återställning av lösenord kommer att löpa ut om :count minuter.', + 'password_reset_bottom' => 'Om du inte har begärt en återställning av lösenordet krävs ingen ytterligare åtgärd.', + + 'invitation_title' => 'Monica – Du är inbjuden till :name', + 'invitation_intro' => 'Du har blivit inbjuden av :name (:email) för att använda Monica, ett trevligt Personligt Relationship Management-verktyg.', + 'invitation_link' => 'För att acceptera inbjudan, klicka på länken nedan:', + 'invitation_button' => 'Acceptera inbjudan', + 'invitation_expiration' => 'Denna länk löper ut om :count dagar.', + + 'export_title' => 'Your export is ready', + 'export_description' => 'You requested a data export on :date. It is now ready to download.', + 'export_download' => 'Download export', + +]; diff --git a/resources/lang/sv/pagination.php b/resources/lang/sv/pagination.php new file mode 100644 index 0000000..c47c09b --- /dev/null +++ b/resources/lang/sv/pagination.php @@ -0,0 +1,25 @@ + '< Föregående', + 'next' => 'Nästa ❯', + +]; diff --git a/resources/lang/sv/passwords.php b/resources/lang/sv/passwords.php new file mode 100644 index 0000000..a25492d --- /dev/null +++ b/resources/lang/sv/passwords.php @@ -0,0 +1,30 @@ + 'Ditt lösenord har återställts!', + 'sent' => 'Om e-postmeddelandet du angav finns i våra register, har du fått en länk för att återställa lösenordet.', + 'token' => 'Denna återställningstoken för lösenord är ogiltig.', + 'user' => 'Om e-postmeddelandet du angav finns i våra register, har du fått en länk för att återställa lösenordet.', + 'changed' => 'Lösenordet har ändrats.', + 'invalid' => 'Nuvarande lösenord du angav är inte korrekt.', + 'throttled' => 'Vänta innan du försöker igen.', + +]; diff --git a/resources/lang/sv/people.php b/resources/lang/sv/people.php new file mode 100644 index 0000000..e78ccd2 --- /dev/null +++ b/resources/lang/sv/people.php @@ -0,0 +1,539 @@ + 'Kontakt hittades ej', + 'people_list_number_kids' => ':count barn|:count barn', + 'people_list_last_updated' => 'Senast kontakt:', + 'people_list_number_reminders' => ':count påminnelse|:count påminnelser', + 'people_list_blank_title' => 'Du har ingen på ditt konto ännu', + 'people_list_blank_cta' => 'Lägg till en person', + 'people_list_sort' => 'Sortera', + 'people_list_stats' => ':count kontakt|:count kontakter', + 'people_list_firstnameAZ' => 'Sortera efter förnamn A → Ö', + 'people_list_firstnameZA' => 'Sortera efter förnamn Ö → A', + 'people_list_lastnameAZ' => 'Sortera efter efternamn A → Ö', + 'people_list_lastnameZA' => 'Sortera efter efternamn Ö → A', + 'people_list_lastactivitydateNewtoOld' => 'Sortera efter senaste aktivitetsdatum, nyaste till äldsta', + 'people_list_lastactivitydateOldtoNew' => 'Sortera efter senaste aktivitetsdatum, äldsta till nyaste', + 'people_list_filter_tag' => 'Visar alla kontakter taggade med', + 'people_list_clear_filter' => 'Töm filter', + 'people_list_contacts_per_tags' => ':{count} kontakt|:{count} kontakter', + 'people_list_show_dead' => 'Visa avlidna personer (:count)', + 'people_list_hide_dead' => 'Dölj avlidna personer (:count)', + 'people_search' => 'Sök bland dina kontakter…', + 'people_search_no_results' => 'Inga resultat hittades', + 'people_search_next' => 'Nästa', + 'people_search_prev' => 'Föregående', + 'people_search_rows_per_page' => 'Rader per sida', + 'people_search_of' => 'av', + 'people_search_page' => 'Sida', + 'people_search_all' => 'Alla', + 'people_add_new' => 'Lägg till ny person', + 'people_list_account_usage' => 'Din kontoanvändning: :current/:limit kontakter', + 'people_list_account_upgrade_title' => 'Uppgradera ditt konto för att låsa upp det till dess fulla potential.', + 'people_list_account_upgrade_cta' => 'Uppgradera nu', + 'people_list_untagged' => 'Visa ej taggade kontakter', + 'people_list_filter_untag' => 'Visar alla omarkerade kontakter', + 'archived_contact_readonly' => 'Arkiverade kontakter kan inte redigeras, vänligen avarkivera den först.', + + // people add + 'people_add_title' => 'Lägg till en ny person', + 'people_add_missing' => 'Ingen person hittades – lägg till en ny nu', + 'people_add_firstname' => 'Förnamn', + 'people_add_middlename' => 'Mellannamn (valfritt)', + 'people_add_lastname' => 'Efternamn (valfritt)', + 'people_add_email' => 'E-post (valfritt)', + 'people_add_nickname' => 'Smeknamn (valfritt)', + 'people_add_cta' => 'Lägg till', + 'people_save_and_add_another_cta' => 'Skicka in och lägg till någon annan', + 'people_add_success' => ':name har skapats', + 'people_add_gender' => 'Kön', + 'people_delete_success' => 'Kontakten har tagits bort', + 'people_delete_message' => 'Ta bort kontakt', + 'people_delete_confirmation' => 'Är du säker på att du vill ta bort :name\'s kontakt? Radering är omedelbar och permanent.', + 'people_add_birthday_reminder' => 'Önska födelsedag till :name', + 'people_add_birthday_reminder_deceased' => 'Detta datum skulle :name ha firat sin födelsedag', + 'people_add_import' => 'Vill du importera dina kontakter?', + 'people_edit_email_error' => 'Det finns redan en kontakt på ditt konto med denna e-postadress. Välj en annan.', + 'people_export' => 'Exportera som vCard', + 'people_add_reminder_for_birthday' => 'Skapa en årlig födelsedagspåminnelse', + + // show + 'section_contact_information' => 'Kontaktuppgifter', + 'section_personal_activities' => 'Aktiviteter', + 'section_personal_reminders' => 'Påminnelser', + 'section_personal_tasks' => 'Uppgifter', + 'section_personal_gifts' => 'Gåvor', + 'section_personal_notes' => 'Anteckningar', + + // archived contacts + 'list_link_to_active_contacts' => 'Du visar arkiverade kontakter. Se listan över aktiva kontakter istället.', + 'list_link_to_archived_contacts' => 'Lista över arkiverade kontakter', + + // Header + 'me' => 'Detta är du', + 'edit_contact_information' => 'Redigera kontaktinformation', + 'contact_archive' => 'Arkivera kontakt', + 'contact_unarchive' => 'Avarkivera kontakt', + 'contact_archive_help' => 'Arkiverade kontakter visas inte på kontaktlistan, men visas fortfarande i sökresultaten.', + 'call_button' => 'Logga ett samtal', + 'set_favorite' => 'Favoritkontakter placeras högst upp i kontaktlistan', + + // Stay in touch + 'stay_in_touch' => 'Håll kontakten', + 'stay_in_touch_frequency' => 'Håll kontakten varje dag|Håll kontakten var {count} dag', + 'stay_in_touch_next_date' => 'Nästa tillfälle: {date}', + 'stay_in_touch_invalid' => 'Frekvensen måste vara större än 0.', + 'stay_in_touch_premium' => 'Du måste uppgradera ditt konto för att använda denna funktion', + 'stay_in_touch_modal_title' => 'Håll kontakten', + 'stay_in_touch_modal_desc' => 'Vi kan påminna dig via e-post om att hålla kontakten med {firstname} med ett regelbundet intervall.', + 'stay_in_touch_modal_label' => 'Skicka ett mail var… {count} dag|Skicka ett mail varje… {count} dagar', + + // Calls + 'modal_call_title' => 'Registrera samtal', + 'modal_call_comment' => 'Vad pratade ni om? (valfritt)', + 'modal_call_exact_date' => 'Samtalet skedde den', + 'modal_call_who_called' => 'Vem ringde?', + 'modal_call_emotion' => 'Vill du logga hur du kände dig under detta samtal? (valfritt)', + 'calls_add_success' => 'Samtalet har sparats.', + 'call_delete_confirmation' => 'Är du säker på att du vill ta bort detta samtal?', + 'call_delete_success' => 'Samtalet har tagits bort', + 'call_title' => 'Telefonsamtal', + 'call_empty_comment' => 'Inga detaljer', + 'call_blank_title' => 'Håll koll på de telefonsamtal du gjort med {name}', + 'call_blank_desc' => 'Du ringde {name}', + 'call_you_called' => 'Du ringde', + 'call_he_called' => '{name} ringde', + 'call_emotions' => 'Känslor:', + + // Conversation + 'conversation_blank' => 'Registrera samtal du har med :name på sociala media, SMS…', + 'conversation_delete_link' => 'Ta bort konversationen', + 'conversation_edit_title' => 'Redigera konversation', + 'conversation_edit_delete' => 'Är du säker på att du vill ta bort denna konversation? Borttagning är permanent.', + 'conversation_add_success' => 'Samtalet har lagts till.', + 'conversation_edit_success' => 'Samtalet har uppdaterats.', + 'conversation_delete_success' => 'Konversationen har tagits bort.', + 'conversation_add_title' => 'Spela in en ny konversation', + 'conversation_add_when' => 'När hade du den här konversationen?', + 'conversation_add_who_wrote' => 'Vem har skickat detta meddelande?', + 'conversation_add_how' => 'Hur kommunicerade du?', + 'conversation_add_you' => 'Du', + 'conversation_add_content' => 'Skriv ner vad som sades', + 'conversation_add_what_was_said' => 'Vad sade du?', + 'conversation_add_another' => 'Lägg till ett annat meddelande', + 'conversation_add_error' => 'Du måste lägga till minst ett meddelande.', + 'conversation_list_table_messages' => 'Meddelanden', + 'conversation_list_table_content' => 'Partiellt innehåll (senaste meddelande)', + 'conversation_list_title' => 'Konversationer', + 'conversation_list_cta' => 'Logga konversation', + + // age - birthday + 'birthdate_not_set' => 'Födelsedatum är inte satt', + 'age_approximate_in_years' => 'runt :age år gammal', + 'age_exact_in_years' => ':age år gammal', + 'age_exact_birthdate' => 'född :date', + + // Last called + 'last_called' => 'Senast anropad: :date', + 'last_talked_to' => 'Senast uppringd: {date}', + 'last_called_empty' => 'Senast anropad: okänd', + 'last_activity_date' => 'Senaste aktiviteten tillsammans: :date', + 'last_activity_date_empty' => 'Senaste aktiviteten tillsammans: okänd', + + // additional information + 'information_edit_success' => 'Profilen har uppdaterats', + 'information_edit_title' => 'Redigera :name\'s personliga information', + 'information_edit_max_size' => 'Max :size Kb.', + 'information_edit_max_size2' => 'Max {size} Kb.', + 'information_edit_firstname' => 'Förnamn', + 'information_edit_lastname' => 'Efternamn (valfritt)', + 'information_edit_description' => 'Beskrivning (valfritt)', + 'information_edit_description_help' => 'Används på kontaktlistan för att vid behov lägga till lite kontext.', + 'information_edit_unknown' => 'Jag känner inte till personens ålder', + 'information_edit_probably' => 'Den här personen är förmodligen…', + 'information_edit_not_year' => 'Jag vet dag och månad för personens födelsedag men inte året…', + 'information_edit_exact' => 'Jag vet att den här personen fyller år…', + 'information_edit_birthdate_label' => 'Födelsedag', + 'information_no_work_defined' => 'Ingen arbetsinformation angiven', + 'information_work_at' => 'vid :company', + 'work_add_cta' => 'Uppdatera arbetsinformation', + 'work_edit_success' => 'Arbetsinformation uppdaterad', + 'work_edit_title' => 'Uppdatera :name\'s jobbinformation', + 'work_edit_job' => 'Jobbtitel (valfritt)', + 'work_edit_company' => 'Företag (valfritt)', + 'work_information' => 'Information om arbete', + + // food preferences + 'food_preferences_add_success' => 'Matinställningar har sparats', + 'food_preferences_edit_description' => 'Kanske :förnamn eller någon i familjen :family har en allergi. Eller inte gillar en viss flaska vin. Ange dem här så att du kommer ihåg det nästa gång du bjuder in dem till middag', + 'food_preferences_edit_description_no_last_name' => 'Kanske : förnamn har en allergi, eller inte gillar en viss flaska vin. Ange dem här så att du kommer ihåg det nästa gång du bjuder in dem till middag', + 'food_preferences_edit_title' => 'Ange matpreferenser', + 'food_preferences_edit_cta' => 'Spara matpreferenser', + 'food_preferences_title' => 'Inställningar för mat', + 'food_preferences_cta' => 'Lägg till matpreferenser', + + // reminders + 'reminders_blank_title' => 'Finns det något du vill bli påmind om :name?', + 'reminders_blank_add_activity' => 'Lägg till en påminnelse', + 'reminders_add_title' => 'Vad skulle du vilja bli påmind om :name?', + 'reminders_add_description' => 'Påminn mig om att…', + 'reminders_add_next_time' => 'När är nästa gång du vill bli påmind om detta?', + 'reminders_add_once' => 'Påminn mig om detta bara en gång', + 'reminders_add_recurrent' => 'Påminn mig om detta varje', + 'reminders_add_starting_from' => 'från och med det datum som anges ovan', + 'reminders_add_cta' => 'Lägg till påminnelse', + 'reminders_edit_update_cta' => 'Uppdatera påminnelse', + 'reminders_add_error_custom_text' => 'Du måste ange en text för denna påminnelse', + 'reminders_create_success' => 'Påminnelsen har lagts till', + 'reminders_delete_success' => 'Påminnelsen har tagits bort', + 'reminders_update_success' => 'Påminnelsen har uppdaterats', + 'reminders_add_optional_comment' => 'Valfri kommentar', + + 'reminder_frequency_day' => 'varje dag|var :number dag', + 'reminder_frequency_week' => 'varje vecka|var :number vecka', + 'reminder_frequency_month' => 'varje månad|var :number månad', + 'reminder_frequency_year' => 'varje år|vart :number år', + 'reminder_frequency_one_time' => 'den :date', + 'reminders_delete_confirmation' => 'Är du säker på att du vill ta bort denna påminnelse?', + 'reminders_delete_cta' => 'Radera', + 'reminders_next_expected_date' => 'på', + 'reminders_cta' => 'Lägg till en påminnelse', + 'reminders_description' => 'Vi kommer skicka ett mail för varje påminnelse nedan. Påminnelser skickas samma morgon som de inträffar. Påminnelser som skapats automatiskt för födelsedagar kan inte tas bort. Om du vill ändra dessa så behöver du ändra födelsedagen för kontakten.', + 'reminders_one_time' => 'En gång', + 'reminders_type_week' => 'vecka', + 'reminders_type_month' => 'månad', + 'reminders_type_year' => 'år', + 'reminders_birthday' => ':name\'s födelsedag', + 'reminders_free_plan_warning' => 'Du är på den fria planen. Inga e-postmeddelanden skickas på denna plan. För att få dina påminnelser via e-post, uppgradera ditt konto.', + + // relationships + 'relationship_form_add' => 'Lägg till ett nytt förhållande', + 'relationship_form_edit' => 'Redigera ett befintligt förhållande', + 'relationship_form_is_with' => 'Den här personen är…', + 'relationship_form_is_with_name' => ':name är…', + 'relationship_form_add_choice' => 'Vem är relationen med?', + 'relationship_form_create_contact' => 'Lägg till en ny person', + 'relationship_form_associate_contact' => 'En befintlig kontakt', + 'relationship_form_associate_dropdown' => 'Sök och välj en befintlig kontakt i menyn nedan', + 'relationship_form_associate_dropdown_placeholder' => 'Sök och välj en befintlig kontakt', + 'relationship_form_also_create_contact' => 'Skapa en kontaktpost för denna person.', + 'relationship_form_add_description' => 'Detta kommer att låta dig behandla denna person som alla andra kontakter.', + 'relationship_form_add_no_existing_contact' => 'Du har inga kontakter som kan vara relaterade till :name just nu.', + 'relationship_delete_confirmation' => 'Är du säker på att du vill ta bort detta förhållande? Borttagning är permanent.', + 'relationship_unlink_confirmation' => 'Är du säker på att du vill ta bort detta förhållande? Denna person kommer inte att tas bort – bara förhållandet mellan de två.', + 'relationship_form_add_success' => 'Relationen har fastställts framgångsrikt.', + 'relationship_form_deletion_success' => 'Relationen har tagits bort.', + + // tasks + 'tasks_title' => 'Uppgifter', + 'tasks_blank_title' => 'Du har inga uppgifter ännu.', + 'tasks_form_title' => 'Titel', + 'tasks_form_description' => 'Beskrivning (valfritt)', + 'tasks_add_task' => 'Lägg till en uppgift', + 'tasks_delete_success' => 'Uppgiften har tagits bort', + 'tasks_complete_success' => 'Uppgiften har ändrat status', + + // activities + 'activity_title' => 'Aktiviteter', + 'activity_type_category_simple_activities' => 'Enkla aktiviteter', + 'activity_type_category_sport' => 'Sport', + 'activity_type_category_food' => 'Mat', + 'activity_type_category_cultural_activities' => 'Kulturella aktiviteter', + 'activity_type_just_hung_out' => 'hängde precis ut', + 'activity_type_watched_movie_at_home' => 'såg en film hemma', + 'activity_type_talked_at_home' => 'precis pratat hemma', + 'activity_type_did_sport_activities_together' => 'spelade en sport tillsammans', + 'activity_type_ate_at_his_place' => 'åt hemma hos sig', + 'activity_type_went_bar' => 'gick till en bar', + 'activity_type_ate_at_home' => 'åt hemma', + 'activity_type_picnicked' => 'utflykt', + 'activity_type_ate_restaurant' => 'åt på en restaurang', + 'activity_type_went_theater' => 'gick till teatern', + 'activity_type_went_concert' => 'gick till en konsert', + 'activity_type_went_play' => 'gick till en pjäs', + 'activity_type_went_museum' => 'gick till museet', + 'activities_add_activity' => 'Lägg till aktivitet', + 'activities_add_more_details' => 'Lägg till fler detaljer', + 'activities_add_emotions' => 'Lägg till känslor', + 'activities_add_category' => 'Ange en kategori', + 'activities_add_participants_cta' => 'Lägg till deltagare', + 'activities_item_information' => ':Activity. Hänt :date', + 'activities_add_title' => 'Vad gjorde du med {name}?', + 'activities_summary' => 'Beskriv vad du gjorde', + 'activities_add_pick_activity' => 'Vill du kategorisera denna aktivitet? Du behöver inte, men det kommer att ge dig statistik senare (valfritt)', + 'activities_add_date_occured' => 'Aktiviteten hände på…', + 'activities_add_participants' => 'Vem deltog förutom {name} i denna aktivitet? (valfritt)', + 'activities_add_emotions_title' => 'Vill du logga hur du kände dig under detta samtal? (valfritt)', + 'activities_blank_title' => 'Håll koll på vad du har gjort med {name} tidigare och vad du har pratat om', + 'activities_blank_add_activity' => 'Lägg till en aktivitet', + 'activities_add_success' => 'Aktiviteten har lagts till', + 'activities_add_error' => 'Fel när aktiviteten lades till', + 'activities_update_success' => 'Aktiviteten har uppdaterats', + 'activities_delete_success' => 'Aktiviteten har tagits bort', + 'activities_who_was_involved' => 'Vem var inblandad?', + 'activities_activity' => 'Aktivitetskategori', + 'activities_view_activities_report' => 'Visa aktivitetsrapport', + 'activities_profile_title' => 'Aktiviteter rapport mellan :name och dig', + 'activities_profile_subtitle' => 'Du har loggat :total_activities aktivitet med :name totalt och :aktiviteter_last_tolve_months under de senaste 12 månaderna hittills.|Du har loggat :total_activities aktiviteter med :name totalt och :aktiviteter_last_tolve_months under de senaste 12 månaderna hittills.', + 'activities_profile_year_summary_activity_types' => 'Här är en uppdelning av den typ av aktiviteter du har gjort tillsammans :year', + 'activities_profile_year_summary' => 'Här är vad ni två har gjort :year', + 'activities_profile_number_occurences' => ':value aktivitet|:value aktiviteter', + 'activities_list_participants' => 'Deltagare ({total}):', + 'activities_list_emotions' => 'Känslor jag kände:', + 'activities_list_date' => 'Hänt på', + 'activities_list_category' => 'Kategori:', + + // notes + 'notes_create_success' => 'Anteckningen har skapats', + 'notes_update_success' => 'Anteckningen har sparats', + 'notes_delete_success' => 'Anteckningen har tagits bort', + 'notes_add_cta' => 'Lägg till anteckning', + 'notes_favorite' => 'Lägg till/ta bort från favoriter', + 'notes_delete_title' => 'Ta bort en anteckning', + 'notes_delete_confirmation' => 'Är du säker på att du vill ta bort denna anteckning? Radering är permanent', + + // gifts + 'gifts_title' => 'Gåvor', + 'gifts_add_success' => 'Gåvan har lagts till', + 'gifts_delete_success' => 'Gåvan har tagits bort', + 'gifts_delete_confirmation' => 'Är du säker på att du vill ta bort denna gåva?', + 'gifts_add_gift' => 'Lägg till en gåva', + 'gifts_link' => 'Länk', + 'gifts_for' => 'För: {name}', + 'gifts_delete_cta' => 'Radera', + 'gifts_add_title' => 'Gåvohantering för :name', + 'gifts_add_gift_idea' => 'Gåva idé', + 'gifts_add_gift_already_offered' => 'Erbjuden gåvan', + 'gifts_add_gift_received' => 'Gåva mottagen', + 'gifts_add_gift_title' => 'Vad är denna gåva?', + 'gifts_add_gift_name' => 'Gåvans namn', + 'gifts_add_link' => 'Länk till webbsidan (valfritt)', + 'gifts_add_value' => 'Värde (valfritt)', + 'gifts_add_comment' => 'Kommentar (valfritt)', + 'gifts_add_recipient' => 'Mottagare (valfritt)', + 'gifts_add_recipient_field' => 'Mottagare', + 'gifts_add_photo' => 'Foto (valfritt)', + 'gifts_add_photo_title' => 'Lägg till ett foto för denna gåva', + 'gifts_add_someone' => 'Denna gåva är till för någon i {name} familj i synnerhet', + 'gifts_delete_title' => 'Ta bort en gåva', + 'gifts_ideas' => 'Gåva idéer', + 'gifts_offered' => 'Erbjudna gåvor', + 'gifts_offered_as_an_idea' => 'Markera som en idé', + 'gifts_received' => 'Gåvor mottagna', + 'gifts_view_comment' => 'Visa kommentar', + 'gifts_mark_offered' => 'Markera som erbjuden', + 'gifts_update_success' => 'Gåvan har uppdaterats', + 'gifts_add_date' => 'Datum (valfritt)', + + // debts + 'debt_delete_confirmation' => 'Är du säker på att du vill ta bort denna skuld?', + 'debt_delete_success' => 'Skulden har tagits bort', + 'debt_add_success' => 'Skulden har lagts till', + 'debt_title' => 'Skulder', + 'debt_add_cta' => 'Lägg till skuld', + 'debt_you_owe' => 'Du är skyldig :amount', + 'debt_they_owe' => ':name är skyldig dig :amount', + 'debt_add_title' => 'Skuldhantering', + 'debt_add_you_owe' => 'Du är skyldig :name', + 'debt_add_they_owe' => ':name är skyldig dig', + 'debt_add_amount' => 'summan av', + 'debt_add_reason' => 'av följande skäl (valfritt)', + 'debt_add_add_cta' => 'Lägg till skuld', + 'debt_edit_update_cta' => 'Uppdatera skuld', + 'debt_edit_success' => 'Skulden har uppdaterats', + 'debts_blank_title' => 'Hantera skulder du är skyldig :name eller :name är du skyldig', + + // tags + 'tag_edit' => 'Redigera tagg', + 'tag_add' => 'Lägg till taggar', + 'tag_add_search' => 'Lägg till eller sök taggar', + 'tag_no_tags' => 'Inga taggar ännu', + + // Introductions + 'introductions_sidebar_title' => 'Hur du träffades', + 'introductions_blank_cta' => 'Ange hur du träffade :name', + 'introductions_title_edit' => 'Hur träffade du :name?', + 'introductions_additional_info' => 'Förklara hur och var du träffades', + 'introductions_edit_met_through' => 'Har någon introducerat dig till den här personen?', + 'introductions_no_met_through' => 'Ingen', + 'introductions_first_met_date' => 'Datum som ni träffades', + 'introductions_no_first_met_date' => 'Jag vet inte vilket datum vi träffades', + 'introductions_first_met_date_known' => 'Detta är det datum vi möttes', + 'introductions_add_reminder' => 'Lägg till en påminnelse för att fira detta möte på årsdagen denna händelse hände', + 'introductions_update_success' => 'Du har uppdaterat informationen om hur du träffade den här personen', + 'introductions_met_through' => 'Möttes genom :name', + 'introductions_met_date' => 'Träffades den :date', + 'introductions_reminder_title' => 'Årsdagen av dagen du träffade första gången', + + // Deceased + 'deceased_reminder_title' => 'Årsdag av bortgång av :name', + 'deceased_mark_person_deceased' => 'Markera person som avliden', + 'deceased_know_date' => 'Jag vet när personen avled', + 'deceased_add_reminder' => 'Lägg till en påminnelse för detta datum', + 'deceased_label' => 'Avlidna', + 'deceased_date_label' => 'Datum av bortgång', + 'deceased_label_with_date' => 'Datum bortgång :date', + 'deceased_age' => 'Ålder vid avldining', + + // Contact information + 'contact_info_title' => 'Kontaktinformation', + 'contact_info_form_content' => 'Innehåll', + 'contact_info_form_contact_type' => 'Typ av kontakt', + 'contact_info_form_personalize' => 'Anpassa', + 'contact_info_address' => 'Bor i', + + // Addresses + 'contact_address_title' => 'Adresser', + 'contact_address_form_name' => 'Etikett (valfritt)', + 'contact_address_form_street' => 'Gatuadress (valfri)', + 'contact_address_form_city' => 'Stad (valfritt)', + 'contact_address_form_province' => 'Län (valfritt)', + 'contact_address_form_postal_code' => 'Postnummer (valfritt)', + 'contact_address_form_country' => 'Land (valfritt)', + 'contact_address_form_latitude' => 'Latitud (endast siffror) (valfritt)', + 'contact_address_form_longitude' => 'Longitud (endast nummer) (valfritt)', + + // Pets + 'pets_kind' => 'Typ av husdjur', + 'pets_name' => 'Namn (frivilligt)', + 'pets_create_success' => 'Husdjuret har lagts till', + 'pets_update_success' => 'Husdjuret har uppdaterats', + 'pets_delete_success' => 'Husdjuret har tagits bort', + 'pets_title' => 'Husdjur', + 'pets_reptile' => 'Reptil', + 'pets_bird' => 'Fågel', + 'pets_cat' => 'Katt', + 'pets_dog' => 'Hund', + 'pets_fish' => 'Fisk', + 'pets_hamster' => 'Hamster', + 'pets_horse' => 'Häst', + 'pets_rabbit' => 'Kanin', + 'pets_rat' => 'Råtta', + 'pets_small_animal' => 'Små djur', + 'pets_other' => 'Övriga', + + // life events + 'life_event_list_tab_life_events' => 'Livshändelser', + 'life_event_list_tab_other' => 'Anteckningar, påminnelser, …', + 'life_event_list_title' => 'Livshändelser', + 'life_event_blank' => 'Logga vad som händer med livet för {name} för din framtida referens.', + 'life_event_list_cta' => 'Lägg till livshändelse', + 'life_event_create_category' => 'Alla kategorier', + 'life_event_create_life_event' => 'Lägg till livshändelse', + 'life_event_create_default_title' => 'Titel (valfritt)', + 'life_event_create_default_story' => 'Berättelse (valfritt)', + 'life_event_create_date' => 'Du behöver inte ange månad eller dag - bara årtal är obligatoriskt.', + 'life_event_create_default_description' => 'Lägg till information om vad du vet', + 'life_event_create_add_yearly_reminder' => 'Lägg till en årlig påminnelse för denna händelse', + 'life_event_create_success' => 'Livshändelsen har lagts till', + 'life_event_delete_title' => 'Ta bort en livshändelse', + 'life_event_delete_description' => 'Är du säker på att du vill ta bort denna livshändelse? Borttagning är permanent.', + 'life_event_delete_success' => 'Livshändelsen har tagits bort', + 'life_event_date_it_happened' => 'Datum det hände', + 'life_event_category_work_education' => 'Arbete & utbildning', + 'life_event_category_family_relationships' => 'Familj & relationer', + 'life_event_category_home_living' => 'Hem & Boende', + 'life_event_category_health_wellness' => 'Hälsa & välmående', + 'life_event_category_travel_experiences' => 'Resor & upplevelser', + 'life_event_sentence_new_job' => 'Började på ett nytt jobb', + 'life_event_sentence_retirement' => 'Pensionerad', + 'life_event_sentence_new_school' => 'Började på skola', + 'life_event_sentence_study_abroad' => 'Studerade utomlands', + 'life_event_sentence_volunteer_work' => 'Började volontärarbeta', + 'life_event_sentence_published_book_or_paper' => 'Publicerat ett papper', + 'life_event_sentence_military_service' => 'Värnplikt', + 'life_event_sentence_new_relationship' => 'Började ett förhållande', + 'life_event_sentence_engagement' => 'Förlovade mig', + 'life_event_sentence_marriage' => 'Gifte mig', + 'life_event_sentence_anniversary' => 'Årsdag', + 'life_event_sentence_expecting_a_baby' => 'Förväntar sig ett barn', + 'life_event_sentence_new_child' => 'Fick barn', + 'life_event_sentence_new_family_member' => 'Lade till en familjemedlem', + 'life_event_sentence_new_pet' => 'Fick ett husdjur', + 'life_event_sentence_end_of_relationship' => 'Avslutade ett förhållande', + 'life_event_sentence_loss_of_a_loved_one' => 'Bortgång av närstående', + 'life_event_sentence_moved' => 'Flyttade', + 'life_event_sentence_bought_a_home' => 'Köpte ett hem', + 'life_event_sentence_home_improvement' => 'Förbättrade och renoverade hemmet', + 'life_event_sentence_holidays' => 'Åkte på semester', + 'life_event_sentence_new_vehicle' => 'Fick ett nytt fordon', + 'life_event_sentence_new_roommate' => 'Fick en rumskompis', + 'life_event_sentence_overcame_an_illness' => 'Övervann en sjukdom', + 'life_event_sentence_quit_a_habit' => 'Avslutade en vana', + 'life_event_sentence_new_eating_habits' => 'Började nya matvanor', + 'life_event_sentence_weight_loss' => '"Gå ner i vikt"', + 'life_event_sentence_wear_glass_or_contact' => 'Började bära glasögon eller kontaktlinser', + 'life_event_sentence_broken_bone' => 'Bröt ett ben', + 'life_event_sentence_removed_braces' => 'Tog bort tandställning', + 'life_event_sentence_surgery' => 'Hade operation', + 'life_event_sentence_dentist' => 'Gick till tandläkaren', + 'life_event_sentence_new_sport' => 'Började med sport', + 'life_event_sentence_new_hobby' => 'Började med en hobby', + 'life_event_sentence_new_instrument' => 'Lärde mig ett nytt instrument', + 'life_event_sentence_new_language' => 'Lärde sig ett nytt språk', + 'life_event_sentence_tattoo_or_piercing' => 'Fick en tatuering eller piercing', + 'life_event_sentence_new_license' => 'Fick license', + 'life_event_sentence_travel' => 'Reste', + 'life_event_sentence_achievement_or_award' => 'Fick en prestation eller belöning', + 'life_event_sentence_changed_beliefs' => 'Ändrad trosuppfattning', + 'life_event_sentence_first_word' => 'Talade för första gången', + 'life_event_sentence_first_kiss' => 'Första kyss', + + // documents + 'document_list_title' => 'Dokument', + 'document_list_cta' => 'Ladda upp dokument', + 'document_list_blank_desc' => 'Här kan du lagra dokument relaterade till denna person.', + 'document_upload_zone_cta' => 'Ladda upp en fil', + 'document_upload_zone_progress' => 'Laddar upp dokumentet…', + 'document_upload_zone_error' => 'Det gick inte att ladda upp dokumentet. Försök igen nedan.', + + // Photos + 'photo_title' => 'Foton', + 'photo_list_title' => 'Relaterade foton', + 'photo_list_cta' => 'Ladda upp foto', + 'photo_list_blank_desc' => 'Du kan lagra bilder om denna kontakt. Ladda upp en nu!', + 'photo_upload_zone_cta' => 'Ladda upp ett foto', + 'photo_current_profile_pic' => 'Nuvarande profilbild', + 'photo_make_profile_pic' => 'Skapa profilbild', + 'photo_delete' => 'Ta bort foto', + 'photo_next' => 'Nästa foto ❯', + 'photo_previous' => '❮ Föregående foto', + + // Avatars + 'avatar_change_title' => 'Ändra din avatar', + 'avatar_question' => 'Vilken avatar vill du använda?', + 'avatar_default_avatar' => 'Standardavatar', + 'avatar_adorable_avatar' => 'Den beundransvärda avatar', + 'avatar_gravatar' => 'Gravatar som associeras med den här personens e-postadress. Gravatar är ett globalt system som låter användare associera e-postadresser med foton.', + 'avatar_current' => 'Behåll nuvarande avatar', + 'avatar_photo' => 'Från ett foto som du laddar upp', + 'avatar_crop_new_avatar_photo' => 'Beskär ny avatar foto', + + // emotions + 'emotion_this_made_me_feel' => 'Detta fick dig att känna…', + + // logs + 'auditlogs_link' => 'Historik', + 'auditlogs_title' => 'Allt som hände :name', + 'auditlogs_breadcrumb' => 'Historik', + 'auditlogs_author' => 'Av :name den :date', + + // contact field label + 'contact_field_label_home' => 'Hem', + 'contact_field_label_work' => 'Arbete', + 'contact_field_label_cell' => 'Mobil', + 'contact_field_label_fax' => 'Fax', + 'contact_field_label_pager' => 'Personsökare', + 'contact_field_label_main' => 'Primär', + 'contact_field_label_other' => 'Övriga', + 'contact_field_label_personal' => 'Personligt', +]; diff --git a/resources/lang/sv/reminder.php b/resources/lang/sv/reminder.php new file mode 100644 index 0000000..c2ffb0e --- /dev/null +++ b/resources/lang/sv/reminder.php @@ -0,0 +1,16 @@ + 'Önskar gärna födelsedag till', + 'type_phone_call' => 'Ring', + 'type_lunch' => 'Lunch med', + 'type_hangout' => 'Umgås med', + 'type_email' => 'E-post', + 'type_birthday_kid' => 'Wish happy birthday to the child of', +]; diff --git a/resources/lang/sv/settings.php b/resources/lang/sv/settings.php new file mode 100644 index 0000000..3f825b3 --- /dev/null +++ b/resources/lang/sv/settings.php @@ -0,0 +1,557 @@ + 'Kontoinställningar', + 'sidebar_personalization' => 'Personalisering', + 'sidebar_settings_storage' => 'Lagring', + 'sidebar_settings_export' => 'Exportera', + 'sidebar_settings_users' => 'Användare', + 'sidebar_settings_subscriptions' => 'Prenumeration', + 'sidebar_settings_import' => 'Importera data', + 'sidebar_settings_tags' => 'Tag management', + 'sidebar_settings_api' => 'API', + 'sidebar_settings_dav' => 'DAV resurser', + 'sidebar_settings_security' => 'Säkerhet', + 'sidebar_settings_auditlogs' => 'Granskningsloggar', + + 'title_general' => 'Allmän information', + 'title_i18n' => 'Internationella inställningar', + 'title_layout' => 'Layout', + + 'me_title' => 'Me as a contact', + 'me_help' => 'Detta är den kontakt som representerar du i Monica', + 'me_select' => 'Välj en kontakt', + 'me_no_contact' => 'Ingen kontakt vald ännu.', + 'me_select_click' => 'Klicka här för att välja en kontakt.', + 'me_remove_contact' => 'Ta bort associationen', + 'me_choose' => 'Välj själv', + 'me_choose_placeholder' => 'Välj själv', + + 'export_title' => 'Exportera dina kontouppgifter', + 'export_be_patient' => 'Click the button to start the export. It might take several minutes to process the export – please be patient and do not spam the button.', + 'export_title_sql' => 'Export to SQL', + 'export_sql_explanation' => 'Exporting your data in SQL format allows you to take your data and import it to your own Monica instance. This is only valuable if you do have your own server.', + 'export_sql_cta' => 'Export to SQL', + 'export_sql_link_instructions' => 'Note: read the instructions to learn more about importing this file to your instance.', + 'export_title_json' => 'Export to Json', + 'export_submitted' => 'Your export has been submitted, it will be available in a few moment…', + 'export_json_explanation' => 'Exporting your data in Json format for backup.', + 'export_json_beta' => 'Json export is in preview mode. Tell us what you think about it:', + 'export_json_cta' => 'Export to Json', + 'export_header_type' => 'Type', + 'export_header_timestamp' => 'Creation date', + 'export_header_status' => 'Status', + 'export_header_actions' => 'Actions', + 'export_last_title' => 'Last exports', + 'export_empty_title' => 'No exports yet', + 'export_type_json' => 'Json export', + 'export_type_sql' => 'SQL export', + 'export_status_todo' => 'Submitted', + 'export_status_doing' => 'Doing', + 'export_status_done' => 'Done', + 'export_status_failed' => 'Failed', + 'export_not_done' => 'Download impossible, this export is not done yet.', + + 'firstname' => 'Förnamn', + 'lastname' => 'Efternamn', + 'name_order' => 'Namn order', + 'name_order_firstname_lastname' => ' – John Doe', + 'name_order_lastname_firstname' => ' – Doe John', + 'name_order_firstname_lastname_nickname' => ' () – John Doe (Rambo)', + 'name_order_firstname_nickname_lastname' => ' () – John (Rambo) Doe', + 'name_order_lastname_firstname_nickname' => ' () – Doe John (Rambo)', + 'name_order_lastname_nickname_firstname' => ' () – Doe (Rambo) John', + 'name_order_nickname_firstname_lastname' => ' ( ) – Rambo (John Doe)', + 'name_order_nickname_lastname_firstname' => ' ( ) – Rambo (Doe John)', + 'name_order_nickname' => ' – Rambo', + 'currency' => 'Valuta', + 'name' => 'Ditt namn: :name', + 'email' => 'E-postadress', + 'email_placeholder' => 'Ange e-postadress', + 'email_help' => 'This is the email used to login, and this is where Monica will send your reminders.', + 'timezone' => 'Tidszon', + 'temperature_scale' => 'Temperaturområde', + 'temperature_scale_fahrenheit' => 'Fahrenheit (°F)', + 'temperature_scale_celsius' => 'Celsius (°C)', + 'layout' => 'Design', + 'layout_small' => 'Max 1200 pixlar brett', + 'layout_big' => 'Full bredd på webbläsaren', + 'save' => 'Uppdatera preferenser', + 'delete_title' => 'Radera ditt konto', + 'delete_desc' => 'Do you wish to delete your account? Deletion is permanent and all of your data will be erased permanently. If you have a subscription, it will be cancelled immediately.', + 'delete_other_desc' => 'Your data in the main database will be deleted immediately. As described in our privacy policy, we carry out securely encrypted backups of the database every day. These backups are kept for 30 days after which they are completely deleted. We cannot delete specific data from the backups we hold any earlier than this. All of your data will be completely deleted no later than 31 days after your account’s deletion.', + 'reset_desc' => 'Do you wish to reset your account? This will remove all your contacts, and all of the data associated with them. Your account will not be deleted.', + 'reset_title' => 'Återställ ditt konto', + 'reset_cta' => 'Återställ konto', + 'reset_notice' => 'Are you sure to reset your account? This is permanent and cannot be undone.', + 'reset_success' => 'Your account has been reset successfully.', + 'delete_notice' => 'Are you sure you want to delete your account? This is permanent and cannot be undone. All of your data will be deleted and will not be recoverable.', + 'delete_cta' => 'Ta bort konto', + 'settings_success' => 'Inställningar uppdaterades!', + 'locale' => 'Språk som används i appen', + 'locale_help' => 'Vill du hjälpa till att översätta Monica eller lägga till ett nytt språk? Följ denna länk för mer information.', + 'locale_ar' => 'Arabiska', + 'locale_cs' => 'Tjeckiska', + 'locale_de' => 'Tyska', + 'locale_el' => 'Greek', + 'locale_en' => 'Engelska', + 'locale_en-GB' => 'Engelska (Storbritannien)', + 'locale_es' => 'Spanska', + 'locale_fr' => 'Franska', + 'locale_he' => 'Hebreiska', + 'locale_hr' => 'Kroatiska', + 'locale_id' => 'Indonesian', + 'locale_it' => 'Italienska', + 'locale_ja' => 'Japanska', + 'locale_nl' => 'Nederländska', + 'locale_pt' => 'Portugisiska', + 'locale_pt-BR' => 'Portuguese, Brazil', + 'locale_ru' => 'Ryska', + 'locale_sv' => 'Swedish', + 'locale_vi' => 'Vietnamese', + 'locale_zh' => 'Kinesiska, förenklad', + 'locale_zh-TW' => 'Kinesiska Traditionell', + 'locale_tr' => 'Turkiska', + + 'security_title' => 'Säkerhet', + 'security_help' => 'Ändra säkerhetsfrågor för ditt konto.', + 'password_change' => 'Change your password', + 'password_current' => 'Nuvarande lösenord', + 'password_current_placeholder' => 'Ange ditt nuvarande lösenord', + 'password_new1' => 'Nytt lösenord', + 'password_new1_placeholder' => 'Enter your new password', + 'password_new2' => 'Confirm your new password', + 'password_new2_placeholder' => 'Retype your new password', + 'password_btn' => 'Ändra lösenord', + '2fa_title' => 'Tvåfaktorsautentisering', + '2fa_otp_title' => 'Mobilapplikation för tvåfaktorsautentisering', + '2fa_enable_title' => 'Aktivera tvåfaktorsautentisering', + '2fa_enable_description' => 'Enable Two Factor Authentication to increase the security of your account.', + '2fa_enable_otp' => 'Open up your Two Factor Authentication mobile app and scan the following QR barcode:', + '2fa_enable_otp_help' => 'If your Two Factor Authentication mobile app does not support QR barcodes, enter in the following code:', + '2fa_enable_otp_validate' => 'Please validate the new device you’ve just set up:', + '2fa_enable_success' => 'Tvåfaktorsautentisering aktiverat', + '2fa_enable_error' => 'Fel vid försök att aktivera tvåfaktorsautentisering', + '2fa_enable_error_already_set' => 'Tvåfaktorsautentisering är redan aktiverat', + '2fa_disable_title' => 'Inaktivera tvåfaktorsautentisering', + '2fa_disable_description' => 'Disable Two Factor Authentication for your account. Be careful, your account will be much less secure!', + '2fa_disable_success' => 'Tvåfaktorsautentisering inaktiverad', + '2fa_disable_error' => 'Fel vid försök att inaktivera tvåfaktorsautentisering', + + 'webauthn_title' => 'Säkerhetsnyckel — WebAuthn protokoll', + 'webauthn_enable_description' => 'Lägg till en ny säkerhetsnyckel', + 'webauthn_key_name_help' => 'Ge din nyckel ett namn.', + 'webauthn_key_name' => 'Nyckelnamn:', + 'webauthn_success' => 'Din nyckel är upptäckt och validerad.', + 'webauthn_last_use' => 'Senaste användning: {timestamp}', + 'webauthn_delete_confirmation' => 'Är du säker på att du vill ta bort denna nyckel?', + 'webauthn_delete_success' => 'Nyckel borttagen', + 'webauthn_insertKey' => 'Infoga din säkerhetsnyckel.', + 'webauthn_buttonAdvise' => 'Om din säkerhetsnyckel har en knapp trycker du på den.', + 'webauthn_noButtonAdvise' => 'Om det inte gör det, ta bort det och sätt in det igen.', + 'webauthn_not_supported' => 'Din webbläsare stöder för närvarande inte WebAuthn.', + 'webauthn_not_secured' => 'WebAuthn stöder endast säkra anslutningar. Ladda denna sida med https schema.', + 'webauthn_error_already_used' => 'Denna nyckel är redan registrerad. Det är inte nödvändigt att registrera den igen.', + 'webauthn_error_not_allowed' => 'Operationen misslyckad, antingen väntade du för länge eller så var det inte tillåtet.', + + 'recovery_title' => 'Återställningskoder', + 'recovery_show' => 'Hämta återställningskoder', + 'recovery_copy_help' => 'Kopiera till Urklipp', + 'recovery_help_intro' => 'Detta är dina återställningskoder:', + 'recovery_help_information' => 'Du kan använda varje återställningskod en gång.', + 'recovery_clipboard' => 'Codes copied to the clipboard.', + 'recovery_generate' => 'Generate new codes…', + 'recovery_generate_help' => 'Generating new codes will invalidate previously generated codes.', + 'recovery_already_used_help' => 'This code has already been used.', + + 'users_list_title' => 'Användare med åtkomst till ditt konto', + 'users_list_add_user' => 'Bjud in en ny användare', + 'users_list_you' => 'Det är du', + 'users_list_invitations_title' => 'Väntande inbjudningar', + 'users_list_invitations_explanation' => 'Nedan är de personer som du har bjudit in till Monica som samarbetspartner.', + 'users_list_invitations_invited_by' => 'inbjuden av :name', + 'users_list_invitations_sent_date' => 'skickad :date', + 'users_blank_title' => 'Du är den enda som har tillgång till detta konto.', + 'users_blank_add_title' => 'Vill du bjuda in någon annan?', + 'users_blank_description' => 'Denna person kommer att ha samma åtkomst som du har, och kommer att kunna lägga till, redigera eller ta bort kontaktuppgifter.', + 'users_blank_cta' => 'Bjud in någon', + 'users_add_title' => 'Invite a new user to your account by email', + 'users_add_description' => 'This person will have the same access as you do, including inviting or deleting other users, including you. Make sure you trust this person before giving them access.', + 'users_add_email_field' => 'Ange e-post för den person du vill bjuda in', + 'users_add_confirmation' => 'I confirm that I want to invite this user to my account. I understand that this person will have access to ALL of my data and see exactly what I see.', + 'users_add_cta' => 'Bjud in användare via e-post', + 'users_accept_title' => 'Acceptera inbjudan och skapa ett nytt konto', + 'users_error_please_confirm' => 'Bekräfta att du vill bjuda in denna användare innan du fortsätter med inbjudan', + 'users_error_email_already_taken' => 'Denna e-postadress är redan upptagen. Välj en annan', + 'users_error_already_invited' => 'Du har redan bjudit in den här användaren. Välj en annan e-postadress.', + 'users_error_email_not_similar' => 'Detta är inte e-postmeddelandet för den person som har bjudit in dig.', + 'users_invitation_deleted_confirmation_message' => 'Inbjudan har tagits bort', + 'users_invitations_delete_confirmation' => 'Är du säker du vill ta bort denna inbjudan?', + 'users_list_delete_confirmation' => 'Är du säker på att ta bort denna användare från ditt konto?', + 'users_invitation_need_subscription' => 'För att lägga till fler användare krävs en prenumeration.', + + 'subscriptions_account_current_plan' => 'Din nuvarande plan', + 'subscriptions_account_current_legacy' => 'Current plan, not selectable anymore:', + 'subscriptions_account_current_paid_plan' => 'Du är på :name plan. Tack så mycket för att vara en prenumerant.', + + 'subscriptions_account_next_billing_title' => 'Next bill', + 'subscriptions_account_next_billing' => 'Din prenumeration förnyas automatiskt den :date.', + 'subscriptions_account_bill_monthly' => 'We’ll bill you :price for another month.', + 'subscriptions_account_bill_annual' => 'We’ll bill you :price for another year.', + 'subscriptions_account_change' => 'Change plan', + + 'subscriptions_account_cancel_title' => 'Cancel subscription', + 'subscriptions_account_cancel_action' => 'Cancel subscription', + 'subscriptions_account_cancel' => 'Du kan avbryta prenumerationen när som helst.', + 'subscriptions_account_free_plan' => 'Du är på den fria planen.', + 'subscriptions_account_free_plan_upgrade' => 'Du kan uppgradera ditt konto till :name plan, vilket kostar $:price per månad. Här är fördelarna:', + 'subscriptions_account_free_plan_benefits_users' => 'Obegränsat antal användare', + 'subscriptions_account_free_plan_benefits_reminders' => 'Påminnelse via e-post', + 'subscriptions_account_free_plan_benefits_import_data_vcard' => 'Importera dina kontakter med vCard', + 'subscriptions_account_free_plan_benefits_support' => 'Support the project in the long run, so we can introduce more great features.', + 'subscriptions_account_upgrade' => 'Uppgradera ditt konto', + 'subscriptions_account_upgrade_title' => 'Uppgradera Monica idag och ha mer meningsfulla relationer.', + 'subscriptions_account_upgrade_choice' => 'Välj en plan nedan och gå över :customers personer som uppgraderat sin Monica.', + 'subscriptions_account_update_title' => 'Update Monica subscription', + 'subscriptions_account_update_description' => 'You can change your subscription’s frequency here.', + 'subscriptions_account_update_information' => 'You will be billed immediately for the new amount. Your subscription will extend to the new period, depending on your choice.', + 'subscriptions_account_invoices' => 'Fakturor', + 'subscriptions_account_invoices_download' => 'Hämta', + 'subscriptions_account_invoices_subscription' => 'Prenumeration från :startDate till :endDate', + 'subscriptions_account_payment' => 'Vilket betalningsalternativ passar dig bäst?', + 'subscriptions_account_confirm_payment' => 'Din betalning är för närvarande ofullständig, vänligen bekräfta din betalning.', + 'subscriptions_downgrade_title' => 'Nedgradera ditt konto till den kostnadsfria planen', + 'subscriptions_downgrade_limitations' => 'Den fria planen har begränsningar. För att kunna nedgradera måste du klara checklistan nedan:', + 'subscriptions_downgrade_rule_users' => 'Du måste ha endast 1 användare på ditt konto', + 'subscriptions_downgrade_rule_users_constraint' => 'Du har för närvarande 1 användare på ditt konto.|Du har :count användare på ditt konto.', + 'subscriptions_downgrade_rule_invitations' => 'You must not have any pending invitations', + 'subscriptions_downgrade_rule_invitations_constraint' => 'You currently have 1 pending invitation.|You currently have :count pending invitations.', + 'subscriptions_downgrade_rule_contacts' => 'Du får inte ha fler än :number aktiva kontakter', + 'subscriptions_downgrade_rule_contacts_constraint' => 'Du har för närvarande 1 kontakt.|Du har :count kontakter.', + 'subscriptions_downgrade_rule_contacts_archive' => 'We can also archive all your contacts for you – that would clear this rule and let you proceed with your account’s downgrade process.', + 'subscriptions_downgrade_cta' => 'Nedgradera', + 'subscriptions_downgrade_success' => 'Du är tillbaka till den fria planen!', + 'subscriptions_downgrade_thanks' => 'Thanks so much for trying the paid plan. We keep adding new features on Monica all the time – so you might want to come back in the future to see if you might be interested in taking a subscription again.', + 'subscriptions_back' => 'Tillbaka till inställningar', + 'subscriptions_upgrade_title' => 'Uppgradera ditt konto', + 'subscriptions_upgrade_choose' => 'Du valde :plan plan.', + 'subscriptions_upgrade_infos' => 'Vi kunde inte vara lyckligare. Ange din betalningsinformation nedan.', + 'subscriptions_upgrade_name' => 'Namn på kort', + 'subscriptions_upgrade_zip' => 'Postnummer', + 'subscriptions_upgrade_credit' => 'Kortuppgifter', + 'subscriptions_upgrade_submit' => 'Betala {amount}', + 'subscriptions_upgrade_charge' => 'We’ll charge your card :price now. The next charge will be on :date. If you ever change your mind, you can cancel at any time, no questions asked.', + 'subscriptions_upgrade_charge_handled' => 'Betalningen hanteras av Stripe. Ingen kortinformation berör vår server.', + 'subscriptions_upgrade_success' => 'Tack! Du prenumererar nu.', + 'subscriptions_upgrade_thanks' => 'Välkommen till gemenskapen av människor som försöker göra världen till en bättre plats.', + + 'subscriptions_payment_confirm_title' => 'Bekräfta din betalning med :amount', + 'subscriptions_payment_confirm_information' => 'Extra bekräftelse krävs för att behandla din betalning. Vänligen bekräfta din betalning genom att fylla i dina betalningsuppgifter nedan.', + 'subscriptions_payment_succeeded_title' => 'Betalning lyckad', + 'subscriptions_payment_succeeded' => 'Denna betalning har redan bekräftats.', + 'subscriptions_payment_cancelled_title' => 'Betalning avbruten', + 'subscriptions_payment_cancelled' => 'Denna betalning avbröts.', + 'subscriptions_payment_error_name' => 'Ange ditt namn.', + 'subscriptions_payment_success' => 'Betalningen var framgångsrik.', + + 'subscriptions_pdf_title' => 'Ditt :name månatliga abonnemang', + 'subscriptions_plan_frequency_year' => ':amount / year', + 'subscriptions_plan_frequency_month' => ':amount / month', + 'subscriptions_plan_choose' => 'Välj denna plan', + 'subscriptions_plan_year_title' => 'Betala årligen', + 'subscriptions_plan_year_bonus' => 'Du behöver inte oroa dig i ett helt år', + 'subscriptions_plan_month_title' => 'Betala månadsvis', + 'subscriptions_plan_month_bonus' => 'Ingen bindningstid', + 'subscriptions_plan_include1' => 'Inkluderat med din uppgradering:', + 'subscriptions_plan_include2' => 'Obegränsat antal kontakter • Obegränsat antal användare • Påminnelser via e-post • Importera med vCard • Anpassning av kontaktbladet', + 'subscriptions_plan_include3' => '100% av vinsten går utvecklingen av detta stora open source-projekt.', + 'subscriptions_help_title' => 'Ytterligare detaljer som du kan vara nyfiken på', + 'subscriptions_help_opensource_title' => 'Vad är ett projekt med öppen källkod?', + 'subscriptions_help_opensource_desc' => 'Monica is an open source project. This means it is built by a community who wants to build a great tool for the greater good. Being open source means the code is publicly available on GitHub, and everyone can inspect it, modify it or enhance it. All the money we raise is dedicated to building better features, paying for more powerful servers, and paying other costs. Thanks for your help. We couldn’t do it without you.', + 'subscriptions_help_limits_title' => 'Is there a limit to the number of contacts we can have on the free plan?', + 'subscriptions_help_limits_plan' => 'Ja. Fria planer låter dig hantera :number kontakter.', + 'subscriptions_help_discounts_title' => 'Har du rabatter för ideella organisationer och utbildning?', + 'subscriptions_help_discounts_desc' => 'Vi gör! Monica är gratis för studenter, och gratis för ideella organisationer och välgörenhetsorganisationer. Kontakta bara supporten med ett bevis på din status så tillämpar vi denna speciella status på ditt konto.', + 'subscriptions_help_change_title' => 'Vad händer om jag ändrar mig?', + 'subscriptions_help_change_desc' => 'You can cancel anytime, no questions asked, and all by yourself – no need to contact support. However, you will not be refunded for the current period.', + + 'stripe_error_card' => 'Ditt kort avvisades. Avvisa meddelande är: :message', + 'stripe_error_api_connection' => 'Nätverkskommunikation med Stripe misslyckades. Försök igen senare.', + 'stripe_error_rate_limit' => 'För många förfrågningar med Stripe just nu. Försök igen senare.', + 'stripe_error_invalid_request' => 'Ogiltiga parametrar. Försök igen senare.', + 'stripe_error_authentication' => 'Fel autentisering med Stripe', + + 'import_title' => 'Importera kontakter till ditt konto', + 'import_cta' => 'Ladda upp kontakter', + 'import_stat' => 'Du har importerat :number filer hittills.', + 'import_result_stat' => 'Uppladdad vCard med 1 kontakt (:total_import importerad, :total_skipped hoppas över)|Uppladdad vCard med :total_contacts kontakter (:total_imported importerad, :total_skipped hoppas över)', + 'import_view_report' => 'Visa rapport', + 'import_in_progress' => 'Importen pågår. Ladda om sidan om en minut.', + 'import_upload_title' => 'Importera dina kontakter från en vCard-fil', + 'import_upload_rules_desc' => 'Vi har dock vissa regler:', + 'import_upload_rule_format' => 'Vi stödjer .vcard och .vcf -filer.', + 'import_upload_rule_vcard' => 'We support the vCard 3.0 format, which is the default format for macOS’s Contacts.app and Google Contacts.', + 'import_upload_rule_instructions' => 'Export instructions for macOS Contacts.app and Google Contacts.', + 'import_upload_rule_multiple' => 'If your contacts have multiple email addresses or phone numbers, only the first entry will be saved.', + 'import_upload_rule_limit' => 'Files are limited to 10 MB.', + 'import_upload_rule_time' => 'It might take up to a minute to upload the contacts and process them. Please be patient.', + 'import_upload_rule_cant_revert' => 'Please make sure data is accurate before uploading, as you can’t undo the upload.', + 'import_upload_form_file' => 'Din .vcf eller .vCard -fil:', + 'import_upload_behaviour' => 'Import beteende:', + 'import_upload_behaviour_add' => 'Add new contacts and skip existing', + 'import_upload_behaviour_replace' => 'Ersätt befintliga kontakter', + 'import_upload_behaviour_help' => 'Replacing will replace all data found in the vCard, but will keep existing contact fields.', + 'import_report_title' => 'Importerar rapport', + 'import_report_date' => 'Datum för importen', + 'import_report_type' => 'Typ av import', + 'import_report_number_contacts' => 'Antal kontakter i filen', + 'import_report_number_contacts_imported' => 'Antal importerade kontakter', + 'import_report_number_contacts_skipped' => 'Antal överhoppade kontakter', + 'import_report_status_imported' => 'Importerad', + 'import_report_status_skipped' => 'Hoppat över', + 'import_vcard_parse_error' => 'Fel vid tolkning av vCard-post', + 'import_vcard_contact_exist' => 'Kontakten existerar redan', + 'import_vcard_contact_no_firstname' => 'No first name (mandatory)', + 'import_vcard_file_not_found' => 'Filen hittades inte', + 'import_vcard_unknown_entry' => 'Okänt kontaktnamn', + 'import_vcard_file_no_entries' => 'Filen innehåller inga poster', + 'import_blank_title' => 'Du har inte importerat några kontakter än.', + 'import_blank_question' => 'Vill du importera kontakter nu?', + 'import_blank_description' => 'Vi kan importera vCard-filer som du kan få från Google Kontakter eller din kontakthanterare.', + 'import_blank_cta' => 'Import vCard', + 'import_need_subscription' => 'För att importera data krävs ett abonnemang.', + + 'tags_list_title' => 'Taggar', + 'tags_list_description' => 'Du kan organisera dina kontakter genom att konfigurera taggar. Taggar fungerar som mappar, men du kan lägga till fler än en tagg till en kontakt. För att lägga till en ny tagg, lägg till den på själva kontakten.', + 'tags_list_contact_number' => '1 kontakt|:count kontakter', + 'tags_list_delete_success' => 'Taggen har tagits bort', + 'tags_list_edit_success' => 'The tag has been successfully updated', + 'tags_list_delete_confirmation' => 'Är du säker på att du vill ta bort taggen? Inga kontakter kommer att tas bort, bara taggen.', + 'tags_blank_title' => 'Taggar är ett bra sätt att kategorisera dina kontakter.', + 'tags_blank_description' => 'Tags work like folders, but you can add more than one tag to a contact. Go to a contact and tag a friend, right below the name. Once a contact is tagged, come back here to manage all the tags in your account.', + + 'api_title' => 'API åtkomst', + 'api_description' => 'API: et kan användas för att manipulera Monicas data från en extern applikation, som till exempel en mobilapp.', + 'api_help' => 'Att använda API:et är obligatoriskt. Du kan antingen skapa en personlig åtkomst-token (Bearer-autentisering) eller auktorisera en OAuth klient att skapa den åt dig. Se API-dokumentation.', + 'api_endpoint' => 'API slutpunkt för denna Monica instans är:', + + 'api_personal_access_tokens' => 'Personliga åtkomsttokens', + 'api_pao_description' => 'Se till att du ger denna token till en källa du litar på – eftersom de ger dig tillgång till alla dina data.', + 'api_token_title' => 'Personliga åtkomst-Tokens', + 'api_token_create_new' => 'Skapa ny token', + 'api_token_not_created' => 'Du har inte skapat några personliga åtkomsttokens.', + 'api_token_name' => 'Token namn', + 'api_token_expire' => 'Förfaller vid {date}', + 'api_token_delete' => 'Radera', + 'api_token_create' => 'Skapa token', + 'api_token_scopes' => 'Omfattningar', + 'api_token_help' => 'Här är din nya personliga tillgång token. Detta är den enda gången det kommer att visas så förlora inte det! Du kan nu använda denna token för att göra API-förfrågningar.', + + 'api_oauth_clients' => 'Dina OAuth klienter', + 'api_oauth_clients_desc' => 'Det här avsnittet låter dig registrera dina egna OAuth klienter.', + 'api_oauth_clients_desc2' => 'Använd detta klient-id för att begära en ny token, och konvertera behörighetskoder till åtkomsttokens. Se Laravel Passport dokumentation för mer information.', + 'api_oauth_title' => 'OAuth klienter', + 'api_oauth_create_new' => 'Skapa ny klient', + 'api_oauth_edit' => 'Redigera klient', + 'api_oauth_not_created' => 'Du har inte skapat några OAuth klienter.', + 'api_oauth_clientid' => 'Klient ID', + 'api_oauth_name' => 'Namn', + 'api_oauth_name_help' => 'Något som dina användare kommer att känna igen och lita på.', + 'api_oauth_secret' => 'Hemlighet', + 'api_oauth_create' => 'Skapa klient', + 'api_oauth_redirecturl' => 'Omdirigera URL', + 'api_oauth_redirecturl_help' => 'Din applikations auktorisering callback URL.', + + 'api_authorized_clients' => 'Lista över auktoriserade kunder', + 'api_authorized_clients_desc' => 'I det här avsnittet listas alla klienter som du har behörighet att komma åt dina applikationsdata. Du kan när som helst återkalla denna behörighet.', + 'api_authorized_clients_title' => 'Auktoriserade program', + 'api_authorized_clients_none' => 'There are no authorized clients yet.', + 'api_authorized_clients_name' => 'Namn', + 'api_authorized_clients_scopes' => 'Omfattningar', + + 'personalization_tab_title' => 'Anpassa ditt konto', + + 'personalization_title' => 'Here you will find different settings to configure your account. These features are intended for “power users” who want maximum control over Monica.', + 'personalization_contact_field_type_title' => 'Typ av kontaktfält', + 'personalization_contact_field_type_add' => 'Lägg till ny fälttyp', + 'personalization_contact_field_type_description' => 'You can configure all the different types of contact fields that you can associate to all your contacts. For example, if a new social network appears in the future, you will be able to add this new way of communicating with your contacts right here.', + 'personalization_contact_field_type_table_name' => 'Namn', + 'personalization_contact_field_type_table_protocol' => 'Protokoll', + 'personalization_contact_field_type_table_actions' => 'Åtgärder', + 'personalization_contact_field_type_modal_title' => 'Lägg till en ny typ av kontaktfält', + 'personalization_contact_field_type_modal_edit_title' => 'Redigera en befintlig typ av kontaktfält', + 'personalization_contact_field_type_modal_delete_title' => 'Ta bort en befintlig kontakttyp', + 'personalization_contact_field_type_modal_delete_description' => 'Are you sure you want to delete this contact field type? Deleting this type of contact field will delete ALL the data with this type for all of your contacts.', + 'personalization_contact_field_type_modal_name' => 'Namn', + 'personalization_contact_field_type_modal_protocol' => 'Protokoll (valfritt)', + 'personalization_contact_field_type_modal_protocol_help' => 'Varje ny kontakttyp kan klickas. Om ett protokoll är inställt, kommer vi att använda det för att utlösa åtgärden som är inställd.', + 'personalization_contact_field_type_modal_icon' => 'Ikon (valfritt)', + 'personalization_contact_field_type_modal_icon_help' => 'Du kan associera en ikon med den här kontaktfälttypen. Du måste lägga till en referens till en typsnitts-ikon.', + 'personalization_contact_field_type_delete_success' => 'The contact field type has been successfully deleted.', + 'personalization_contact_field_type_add_success' => 'Kontakttypen har lagts till.', + 'personalization_contact_field_type_edit_success' => 'Kontakttypen har uppdaterats.', + + 'personalization_genders_title' => 'Könsidentitet', + 'personalization_genders_add' => 'Lägg till ny könstyp', + 'personalization_genders_desc' => 'Du kan definiera så många kön som du behöver. Du behöver minst en könstyp på ditt konto.', + 'personalization_genders_modal_add' => 'Lägg till könstyp', + 'personalization_genders_modal_edit' => 'Uppdatera könstyp', + 'personalization_genders_modal_name' => 'Namn', + 'personalization_genders_modal_name_help' => 'Namnet som används för att visa kön på en kontaktsida.', + 'personalization_genders_modal_sex' => 'Kön', + 'personalization_genders_modal_sex_help' => 'Används för att definiera relationerna och under VCard-import/exportprocessen.', + 'personalization_genders_modal_default' => 'Välj standard kön för en ny kontakt', + 'personalization_genders_modal_delete' => 'Ta bort könstyp', + 'personalization_genders_modal_delete_desc' => 'Are you sure you want to delete the gender “{name}”?', + 'personalization_genders_modal_delete_question' => 'You currently have {count} contact with this gender. If you delete this gender, what gender should this contact have?|You currently have {count} contacts with this gender. If you delete this gender, what gender should these contacts have?', + 'personalization_genders_modal_delete_question_default' => 'This gender is the default one. If you delete this gender, which one will be the new default?', + 'personalization_genders_modal_error' => 'Please choose a gender from the list.', + 'personalization_genders_list_contact_number' => '{count} kontakt|{count} kontakter', + 'personalization_genders_table_name' => 'Namn', + 'personalization_genders_table_sex' => 'Kön', + 'personalization_genders_table_default' => 'Standard', + 'personalization_genders_default' => 'Förvalt kön', + 'personalization_genders_make_default' => 'Ändra standard kön', + 'personalization_genders_select_default' => 'Välj standard kön', + 'personalization_genders_m' => 'Man', + 'personalization_genders_f' => 'Kvinna', + 'personalization_genders_o' => 'Annat', + 'personalization_genders_u' => 'Okänd', + 'personalization_genders_n' => 'Inget eller ej tillämpligt', + + 'personalization_reminder_rule_save' => 'Ändringen har sparats', + 'personalization_reminder_rule_title' => 'Påminnelse regler', + 'personalization_reminder_rule_line' => '{count} dag före|{count} dagar före', + 'personalization_reminder_rule_desc' => 'For every reminder that you set, Monica can send you an email a number of days before the event happens. You can adjust these notification settings here. These notifications only apply to monthly and yearly reminders.', + + 'personalization_module_save' => 'Ändringen har sparats', + 'personalization_module_title' => 'Funktioner', + 'personalization_module_desc' => 'You may not need all of Monica’s features. Below you can toggle specific features that are used on a contact sheet. This change will affect ALL your contacts. Turning off a feature does not delete any data, it simply hides the feature.', + + 'personalisation_paid_upgrade' => 'Detta är en premiumfunktion som kräver att en betald prenumeration är aktiv. Uppgradera ditt konto genom att besöka Inställningar > Prenumeration.', + 'personalisation_paid_upgrade_vue' => 'Detta är en premiumfunktion som kräver att en betald prenumeration är aktiv. Uppgradera ditt konto genom att besöka Inställningar > Prenumeration.', + + 'reminder_time_to_send' => 'Time of the day reminders will be sent', + 'reminder_time_to_send_help' => 'Your next reminder is scheduled to be sent on {dateTime}.', + + 'personalization_activity_type_category_title' => 'Kategorier av aktivitetstyper', + 'personalization_activity_type_category_add' => 'Lägg till en ny kategori för aktivitetstyp', + 'personalization_activity_type_category_table_name' => 'Namn', + 'personalization_activity_type_category_description' => 'An activity with one of your contacts can have a type and a category type. Your account comes with a set of predefined category types by default, but you can customize these here.', + 'personalization_activity_type_category_table_actions' => 'Åtgärder', + 'personalization_activity_type_category_modal_add' => 'Lägg till en ny kategori för aktivitetstyp', + 'personalization_activity_type_category_modal_edit' => 'Redigera kategori för aktivitetstyp', + 'personalization_activity_type_category_modal_question' => 'What should we name this new category?', + 'personalization_activity_type_add_button' => 'Lägg till en ny aktivitetstyp', + 'personalization_activity_type_modal_add' => 'Lägg till en ny aktivitetstyp', + 'personalization_activity_type_modal_question' => 'What should we name this new activity type?', + 'personalization_activity_type_modal_edit' => 'Redigera en aktivitetstyp', + 'personalization_activity_type_category_modal_delete' => 'Ta bort en kategori av aktivitetstyper', + 'personalization_activity_type_category_modal_delete_desc' => 'Are you sure you want to delete this category? Deleting it will delete all associated activity types. Activities that belong to this category will not be affected by this deletion.', + 'personalization_activity_type_modal_delete' => 'Ta bort en aktivitetstyp', + 'personalization_activity_type_modal_delete_desc' => 'Är du säker på att du vill ta bort denna aktivitetstyp? Aktiviteter som tillhör denna kategori kommer inte att påverkas av denna borttagning.', + 'personalization_activity_type_modal_delete_error' => 'Vi kan inte hitta denna typ av aktivitet.', + 'personalization_activity_type_category_modal_delete_error' => 'Vi kan inte hitta denna kategori av aktivitetstyper.', + + 'personalization_life_event_category_title' => 'Livshändelse kategorier', + 'personalization_live_event_category_table_name' => 'Namn', + 'personalization_life_event_category_description' => 'A life event can have a type and a category. Your account comes with a set of predefined categories and types by default, but you can customize life event types here.', + 'personalization_live_event_category_table_actions' => 'Åtgärder', + 'personalization_life_event_type_add_button' => 'Lägg till en ny livshändelstyp', + 'personalization_life_event_type_modal_add' => 'Lägg till en ny livshändelstyp', + 'personalization_life_event_type_modal_question' => 'What should we name this new life event type?', + 'personalization_life_event_type_modal_edit' => 'Redigera en livshändelsetyp', + 'personalization_life_event_type_modal_delete' => 'Ta bort en livshändelstyp', + 'personalization_life_event_type_modal_delete_desc' => 'Är du säker på att du vill ta bort denna livshändelstyp? Livshändelser som tillhör denna typ kommer att raderas genom att utföra denna åtgärd.', + 'personalization_life_event_type_modal_delete_error' => 'Vi kan inte hitta denna typ av händelser i livet.', + + 'personalization_life_event_category_work_education' => 'Arbete & utbildning', + 'personalization_life_event_category_family_relationships' => 'Familj & relationer', + 'personalization_life_event_category_home_living' => 'Hem & Boende', + 'personalization_life_event_category_travel_experiences' => 'Resor & upplevelser', + 'personalization_life_event_category_health_wellness' => 'Hälsa och hälsa', + + 'personalization_life_event_type_new_job' => 'Nytt jobb', + 'personalization_life_event_type_retirement' => 'Pensionering', + 'personalization_life_event_type_new_school' => 'Ny skola', + 'personalization_life_event_type_study_abroad' => 'Studera utomlands', + 'personalization_life_event_type_volunteer_work' => 'Volontärarbete', + 'personalization_life_event_type_published_book_or_paper' => 'Publicerat en bok eller ett papper', + 'personalization_life_event_type_military_service' => 'Militär tjänst', + 'personalization_life_event_type_first_met' => 'Första möte', + 'personalization_life_event_type_new_relationship' => 'Ny relation', + 'personalization_life_event_type_engagement' => 'Förlovning', + 'personalization_life_event_type_marriage' => 'Äktenskap', + 'personalization_life_event_type_anniversary' => 'Årsdag', + 'personalization_life_event_type_expecting_a_baby' => 'Väntar barn', + 'personalization_life_event_type_new_child' => 'Nytt barn', + 'personalization_life_event_type_new_family_member' => 'Ny familjemedlem', + 'personalization_life_event_type_new_pet' => 'Nytt husdjur', + 'personalization_life_event_type_end_of_relationship' => 'Slut på relationen', + 'personalization_life_event_type_loss_of_a_loved_one' => 'Bortgång av närstående', + 'personalization_life_event_type_moved' => 'Flyttad', + 'personalization_life_event_type_bought_a_home' => 'Köpte hus', + 'personalization_life_event_type_home_improvement' => 'Förbättring av hemmet', + 'personalization_life_event_type_holidays' => 'Semester', + 'personalization_life_event_type_new_vehicle' => 'Nytt fordon', + 'personalization_life_event_type_new_roommate' => 'Ny rumskompis', + 'personalization_life_event_type_overcame_an_illness' => 'Övervann en sjukdom', + 'personalization_life_event_type_quit_a_habit' => 'Avsluta en vana', + 'personalization_life_event_type_new_eating_habits' => 'Nya matvanor', + 'personalization_life_event_type_weight_loss' => 'Viktminskning', + 'personalization_life_event_type_wear_glass_or_contact' => 'Started wearing glasses or contacts', + 'personalization_life_event_type_broken_bone' => 'Broke a bone', + 'personalization_life_event_type_removed_braces' => 'Had braces removed', + 'personalization_life_event_type_surgery' => 'Had surgery', + 'personalization_life_event_type_dentist' => 'Had dental treatment', + 'personalization_life_event_type_new_sport' => 'Started playing a new sport', + 'personalization_life_event_type_new_hobby' => 'Took up a new hobby', + 'personalization_life_event_type_new_instrument' => 'Started learning a new instrument', + 'personalization_life_event_type_new_language' => 'Started learning a new language', + 'personalization_life_event_type_tattoo_or_piercing' => 'Tatuering eller piercingar', + 'personalization_life_event_type_new_license' => 'Ny licens', + 'personalization_life_event_type_travel' => 'Resor', + 'personalization_life_event_type_achievement_or_award' => 'Prestation eller belöning', + 'personalization_life_event_type_changed_beliefs' => 'Ändrad trosuppfattning', + 'personalization_life_event_type_first_word' => 'Första ordet', + 'personalization_life_event_type_first_kiss' => 'Första kyssen', + + 'storage_title' => 'Lagringsutrymme', + 'storage_account_info' => 'Your account limit is :accountLimit MB. Your current usage is :currentAccountSize MB (about :percentUsage%).', + 'storage_upgrade_notice' => 'Uppgradera ditt konto för att kunna ladda upp dokument och foton.', + 'storage_description' => 'Här kan du se alla dokument och foton som laddats upp om dina kontakter.', + + 'dav_title' => 'WebDAV', + 'dav_description' => 'Här hittar du alla inställningar för att använda WebDAV resurser för CardDAV och CalDAV export.', + 'dav_copy_help' => 'Kopiera till Urklipp', + 'dav_clipboard_copied' => 'Värdet har kopierats till Urklipp', + 'dav_url_base' => 'Bas-url för alla CardDAV och CalDAV resurser:', + 'dav_connect_help' => 'Du kan ansluta dina kontakter och/eller kalendrar med denna bas-url på din telefon eller dator.', + 'dav_connect_help2' => 'Använd din inloggning (e-post) och skapa en API-token som lösenord för att autentisera.', + 'dav_url_carddav' => 'CardDAV url för kontakter källa:', + 'dav_url_caldav_birthdays' => 'CalDAV url för födelsedagsresurser:', + 'dav_url_caldav_tasks' => 'CalDAV url för uppgiftskällor:', + 'dav_title_carddav' => 'CardDAV', + 'dav_title_caldav' => 'CalDAV', + 'dav_carddav_export' => 'Exportera alla kontakter i en fil', + 'dav_caldav_birthdays_export' => 'Exportera alla födelsedagar i en fil', + 'dav_caldav_tasks_export' => 'Exportera alla uppgifter i en fil', + + 'archive_title' => 'Archive all of the contacts in your account', + 'archive_desc' => 'This will archive all of the contacts in your account.', + 'archive_cta' => 'Archive all of your contacts', + + 'logs_title' => 'Everything that has happened to this account', + 'logs_actor' => 'Actor', + 'logs_timestamp' => 'Timestamp', + 'logs_description' => 'Description', + 'logs_subject' => 'Subject', + 'logs_size' => 'Size (Kb)', + 'logs_object' => 'Object', +]; diff --git a/resources/lang/sv/validation.php b/resources/lang/sv/validation.php new file mode 100644 index 0000000..cfd4c13 --- /dev/null +++ b/resources/lang/sv/validation.php @@ -0,0 +1,166 @@ + ':attribute måste accepteras.', + 'active_url' => ':attribute är inte en giltig URL.', + 'after' => ':attribute måste vara ett datum efter :date.', + 'after_or_equal' => ':attribute måste vara ett datum efter eller lika med :date.', + 'alpha' => ':attribute får endast innehålla bokstäver.', + 'alpha_dash' => ':attribute får endast innehålla bokstäver, siffror, bindestreck och understreck.', + 'alpha_num' => ':attribute får endast innehålla bokstäver och siffror.', + 'array' => ':attribute måste vara en lista med värden.', + 'before' => ':attribute måste vara ett datum före :date.', + 'before_or_equal' => ':attribute måste vara ett datum före eller lika med :date.', + 'between' => [ + 'numeric' => ':attribute måste vara mellan :min och :max.', + 'file' => ':attribute måste vara mellan :min och :max kilobyte.', + 'string' => ':attribute måste vara mellan :min och :max tecken.', + 'array' => ':attribute måste vara mellan :min och :max föremål.', + ], + 'boolean' => ':attribute måste vara sant eller falskt.', + 'confirmed' => ':attribute bekräftelsen matchar inte.', + 'date' => ':attribute är inte ett giltigt datum.', + 'date_equals' => ':attribute måste vara ett datum efter :date.', + 'date_format' => ':attribute matchar inte formatet :format.', + 'different' => ':attribute och :other måste vara olika.', + 'digits' => ':attributet måste vara :digits siffror.', + 'digits_between' => ':attribute måste vara mellan :min och :max siffror.', + 'dimensions' => ':attribute har ogiltiga bilddimensioner.', + 'distinct' => 'Fältet :attribute har ett dubbelt värde.', + 'email' => ':attribute måste vara en giltig e-postadress.', + 'ends_with' => ':attribute måste sluta med något av följande: :values.', + 'exists' => 'Valt värde för :attribute är ogiltigt.', + 'file' => ':attribute måste vara en fil.', + 'filled' => ':attribute fältet måste ha ett värde.', + 'gt' => [ + 'numeric' => ':attribute måste vara större än :value.', + 'file' => ':attribute måste vara större än :value kilobytes.', + 'string' => ':attribute måste vara större än :value tecken.', + 'array' => ':attribute måste ha mer än :value objekt.', + ], + 'gte' => [ + 'numeric' => ':attribute måste vara större än eller lika :value.', + 'file' => ':attribute måste vara större än eller lika med :value kilobytes.', + 'string' => ':attribute måste vara större än eller lika med :value tecken.', + 'array' => ':attribute måste ha :value objekt eller mer.', + ], + 'image' => ':attribute måste vara en bild.', + 'in' => 'Valt värde för :attribute är ogiltigt.', + 'in_array' => ':attribute fältet existerar inte i :other.', + 'integer' => ':attribute måste vara ett heltal.', + 'ip' => ':attribute måste vara en giltig IP-adress.', + 'ipv4' => ':attribute måste vara en giltig IPv4-adress.', + 'ipv6' => ':attribute måste vara en giltig IPv6-adress.', + 'json' => ':attribute måste vara en giltig JSON-sträng.', + 'lt' => [ + 'numeric' => ':attribute måste vara mindre än :value.', + 'file' => ':attribute måste vara mindre än :value kilobytes.', + 'string' => ':attribute måste vara mindre än :value tecken.', + 'array' => ':attribute måste ha mindre än :value objekt.', + ], + 'lte' => [ + 'numeric' => ':attribute måste vara mindre än eller lika :value.', + 'file' => ':attribute måste vara mindre än eller lika med :value kilobytes.', + 'string' => ':attribute måste vara mindre än eller lika med :value tecken.', + 'array' => ':attribute får inte ha mer än :value objekt.', + ], + 'max' => [ + 'numeric' => ':attribute får inte vara större än :max.', + 'file' => ':attribute får inte vara större än :max kilobyte.', + 'string' => ':attribute får inte vara större än :max tecken.', + 'array' => ':attribute får inte ha mer än :max objekt.', + ], + 'mimes' => ':attribute måste vara en fil av typ: :values.', + 'mimetypes' => ':attribute måste vara en fil av typ: :values.', + 'min' => [ + 'numeric' => ':attribute måste vara minst :min.', + 'file' => ':attribute måste vara minst :min kilobyte.', + 'string' => ':attribute måste innehålla minst :min tecken.', + 'array' => ':attribute måste innehålla minst :min objekt.', + ], + 'not_in' => 'Det valda :attribute är ogiltigt.', + 'not_regex' => ':attribute format är ogiltigt.', + 'numeric' => ':attribute måste vara ett tal.', + 'password' => 'Lösenordet är felaktigt.', + 'present' => 'Fältet :attribute måste vara närvarande.', + 'regex' => ':attribute format är ogiltigt.', + 'required' => 'Fältet :attribute är obligatoriskt.', + 'required_if' => 'Fältet :attribute är obligatoriskt när :other är :value.', + 'required_unless' => ':attribute är obligatoriskt om inte :other finns i :values.', + 'required_with' => ':attribute fältet är obligatoriskt när :values är angivet.', + 'required_with_all' => 'Fältet :attribute är obligatoriskt när :values är presenterade.', + 'required_without' => 'Fältet :attribute är obligatoriskt när :values inte visas.', + 'required_without_all' => ':attribute är obligatirskt när ingen av :values finns.', + 'same' => ':attribute och :other måste matcha.', + 'size' => [ + 'numeric' => ':attribute måste vara :size.', + 'file' => ':attribute måste vara :size kilobyte.', + 'string' => ':attribute måste vara :size tecken.', + 'array' => ':attribute måste innehålla :size objekt.', + ], + 'starts_with' => ':attribute måste börja med något av följande: :values.', + 'string' => ':attribute måste vara en sträng.', + 'timezone' => ':attribute måste vara en giltig zon.', + 'unique' => ':attribute har redan tagits.', + 'uploaded' => ':attribute kunde inte laddas upp.', + 'url' => ':attribute format är ogiltigt.', + 'uuid' => ':attribute måste vara ett giltigt UUID.', + + /* + |-------------------------------------------------------------------------- + | Custom Validation Language Lines + |-------------------------------------------------------------------------- + | + | Here you may specify custom validation messages for attributes using the + | convention "attribute.rule" to name the lines. This makes it quick to + | specify a specific custom language line for a given attribute rule. + | + */ + + 'custom' => [ + 'attribute-name' => [ + 'rule-name' => 'custom-message', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Custom Validation Attributes + |-------------------------------------------------------------------------- + | + | The following language lines are used to swap our attribute placeholder + | with something more reader friendly such as "E-Mail Address" instead + | of "email". This simply helps us make our message more expressive. + | + */ + + 'attributes' => [], + + 'vue' => [ + 'max' => [ + 'numeric' => '{field} may not be greater than {max}.', + 'string' => '{field} may not be greater than {max} characters.', + ], + 'required' => '{field} is required.', + 'url' => '{field} is not a valid URL.', + ], + +]; diff --git a/resources/lang/tr.json b/resources/lang/tr.json new file mode 100644 index 0000000..d4c4865 --- /dev/null +++ b/resources/lang/tr.json @@ -0,0 +1,7 @@ +{ + "The :attribute must contain at least one uppercase and one lowercase letter.": ":attribute en az bir büyük harf ve bir küçük harf içermelidir.", + "The :attribute must contain at least one letter.": ":attribute en az bir harf içermelidir.", + "The :attribute must contain at least one symbol.": ":attribute en az bir sembol içermelidir.", + "The :attribute must contain at least one number.": ":attribute en az bir sayı içermelidir.", + "The given :attribute has appeared in a data leak. Please choose a different :attribute.": "Girilen :attribute bir veri sızıntısında ortaya çıktı. Lütfen farklı bir :attribute seçin." +} diff --git a/resources/lang/tr/app.php b/resources/lang/tr/app.php new file mode 100644 index 0000000..2a8399c --- /dev/null +++ b/resources/lang/tr/app.php @@ -0,0 +1,571 @@ + 'Evet', + 'no' => 'Hayır', + 'update' => 'Güncelle', + 'save' => 'Kaydet', + 'add' => 'Ekle', + 'cancel' => 'Vazgeç', + 'confirm' => 'Doğrula', + 'delete_confirm' => 'Emin misiniz?', + 'delete' => 'Sil', + 'edit' => 'Düzenle', + 'upload' => 'Yükle', + 'download' => 'İndir', + 'save_close' => 'Kaydet ve kapat', + 'close' => 'Kapat', + 'copy' => 'Kopyala', + 'create' => 'Oluştur', + 'remove' => 'Kaldır', + 'revoke' => 'Geri al', + 'done' => 'Bitti', + 'back' => 'Geri', + 'verify' => 'Doğrula', + 'new' => 'yeni', + 'unknown' => 'Bilmiyorum', + 'load_more' => 'Daha fazla', + 'loading' => 'Yükleniyor…', + 'with' => 'ile', + 'today' => 'bugün', + 'yesterday' => 'dün', + 'another_day' => 'başka bir gün', + 'date' => 'Tarih', + 'type' => 'Tip', + 'zoom' => 'Yakınlaştır', + 'upgrade' => 'Kilidi açmak için yükselt', + 'percent_uploaded' => '%{percent} yüklendi', + 'retry' => 'Tekrar dene', + 'filter' => 'Listeyi filtrele', + 'go_back' => 'Geri dön', + 'file_selected' => '{count} dosya seçildi…', + + 'application_title' => 'Monica - kişisel ilişki yöneticisi', + 'application_description' => 'Monica sevdiklerinizle, arkadaşlarınızla ve ailenizle etkileşimlerinizi yönetebileceğiniz bir araçtır.', + 'application_og_title' => 'Sevdiklerinizle daha iyi ilişkiler kurun. Arkadaşlarınız ve aileniz için ücretsiz Online CRM.', + + 'markdown_description' => 'Metninizi güzel bir şekilde biçimlendirmek mi istiyorsunuz? Kalın, italik, listeler ve daha fazlasını eklemek için Markdown desteğimiz bulunmaktadır.', + 'markdown_link' => 'Dokümantasyonu oku', + + 'header_settings_link' => 'Ayarlar', + 'header_logout_link' => 'Çıkış yap', + 'header_changelog_link' => 'Ürün değişiklikleri', + + 'main_nav_cta' => 'Kişi ekle', + 'main_nav_dashboard' => 'Kontrol paneli', + 'main_nav_family' => 'Kişiler', + 'main_nav_journal' => 'Günlük', + 'main_nav_activities' => 'Aktiviteler', + 'main_nav_tasks' => 'Görevler', + + 'footer_remarks' => 'Yorumlar?', + 'footer_send_email' => 'Bize e-posta gönderin', + 'footer_privacy' => 'Gizlilik politikası', + 'footer_release' => 'Sürüm notları', + 'footer_newsletter' => 'Haber Bülteni', + 'footer_source_code' => 'Katkıda bulun', + 'footer_version' => 'Sürüm: :version', + 'footer_new_version' => 'Monica\'nın yeni sürümü mevcut', + + 'footer_modal_version_whats_new' => 'Neler yeni', + 'footer_modal_version_release_away' => 'Mevcut olan son sürümden 1 sürüm aşağıdasın. Uygulamanı güncellemelisin.|Mevcut olan son sürümden :number sürüm aşağıdasın. Uygulamanı güncellemelisin.', + + 'breadcrumb_dashboard' => 'Başlangıç', + 'breadcrumb_list_contacts' => 'Kişi Listesi', + 'breadcrumb_archived_contacts' => 'Arşivli kişiler', + 'breadcrumb_journal' => 'Günlük', + 'breadcrumb_settings' => 'Ayarlar', + 'breadcrumb_settings_export' => 'Dışa aktar', + 'breadcrumb_settings_users' => 'Kullanıcılar', + 'breadcrumb_settings_users_add' => 'Bir kullanıcı ekle', + 'breadcrumb_settings_subscriptions' => 'Abonelik', + 'breadcrumb_settings_import' => 'İçe aktar', + 'breadcrumb_settings_import_report' => 'İçe aktarım raporu', + 'breadcrumb_settings_import_upload' => 'Yükle', + 'breadcrumb_settings_tags' => 'Etiketler', + 'breadcrumb_add_significant_other' => 'Sevgili ekle', + 'breadcrumb_edit_significant_other' => 'Sevgiliyi düzenle', + 'breadcrumb_add_note' => 'Bir not ekle', + 'breadcrumb_edit_note' => 'Bir notu düzenle', + 'breadcrumb_api' => 'API', + 'breadcrumb_dav' => 'DAV Kaynakları', + 'breadcrumb_edit_introductions' => 'Nasıl tanıştınız', + 'breadcrumb_settings_personalization' => 'Kişiselleştirme', + 'breadcrumb_settings_security' => 'Güvenlik', + 'breadcrumb_settings_security_2fa' => 'İki Aşamalı Doğrulama', + 'breadcrumb_profile' => ':name kişisinin Profili', + + 'gender_male' => 'Erkek', + 'gender_female' => 'Kadın', + 'gender_none' => 'Söylemek istemiyorum', + 'gender_no_gender' => 'Cinsiyetsiz', + + 'error_title' => 'Amanın! Bir şeyler ters gitti.', + 'error_unauthorized' => 'Bu kaynağı düzenlemeye yetkiniz yok.', + 'error_user_account' => 'Kullanıcı belirli bir role sahip değil.', + 'error_save' => 'Verileri kaydetmeye çalışırken bir hata oluştu.', + 'error_try_again' => 'Bir şeyler ters gitti. Lütfen tekrar deneyin.', + 'error_id' => 'Hata kimliği: :id', + 'error_unavailable' => 'Hizmet kullanılamıyor', + 'error_maintenance' => 'Bakım devam ediyor. Birazdan geri döneceğiz.', + 'error_help' => 'Kısa süre sonra geri döneceğiz.', + 'error_twitter' => 'Yeniden çalışır olduğunda haberdar olmak için Twitter hesabımızı takip edin.', + 'error_no_term' => 'Bu olay için henüz bir politika yok.', + + 'default_save_success' => 'Veri kaydedildi.', + + 'compliance_title' => 'Rahatsız ettiğimiz için üzgünüz.', + 'compliance_desc' => 'Kullanım Koşullarımızı ve Gizlilik Politikamızı değiştirdik. Yasalara göre, sizden hesabınızı kullanmaya devam edebilmeniz için onları gözden geçirmenizi ve kabul etmenizi istemek zorundayız.', + 'compliance_desc_end' => 'Verileriniz veya hesabınızla ilgili kötü bir şey yapmıyoruz ve asla yapmayacağız.', + 'compliance_terms' => 'Yeni şartları ve gizlilik politikasını kabul et', + + // Relationship types + // Yes, each relationship type has 8 strings associated with it. + // This is because we need to indicate the name of the relationship type, + // and also the name of the opposite side of this relationship (father/son), + // and then, the feminine version of the string. Finally, in some sentences + // in the UI, we need to include the name of the person we add the relationship + // to. + 'relationship_type_group_love' => 'Aşk İlişkileri', + 'relationship_type_group_family' => 'Aile İlişkileri', + 'relationship_type_group_friend' => 'Arkadaşlık İlişkileri', + 'relationship_type_group_work' => 'İş İlişkileri', + 'relationship_type_group_other' => 'Diğer tür ilişkiler', + + 'relationship_type_partner' => 'sevgili', + 'relationship_type_partner_female' => 'sevgili', + 'relationship_type_partner_male' => 'significant other', + 'relationship_type_partner_with_name' => ':name kişisinin sevgilisi', + 'relationship_type_partner_female_with_name' => ':name kişisinin sevgilisi', + 'relationship_type_partner_male_with_name' => ':name’s significant other', + + 'relationship_type_spouse' => 'eş', + 'relationship_type_spouse_female' => 'wife', + 'relationship_type_spouse_male' => 'husband', + 'relationship_type_spouse_with_name' => ':name kişisinin eşi', + 'relationship_type_spouse_female_with_name' => ':name’s wife', + 'relationship_type_spouse_male_with_name' => ':name’s husband', + + 'relationship_type_date' => 'flört', + 'relationship_type_date_female' => 'flört', + 'relationship_type_date_male' => 'date', + 'relationship_type_date_with_name' => ':name kişisinin flörtü', + 'relationship_type_date_female_with_name' => ':name kişisinin flörtü', + 'relationship_type_date_male_with_name' => ':name’s date', + + 'relationship_type_lover' => 'aşık', + 'relationship_type_lover_female' => 'aşık', + 'relationship_type_lover_male' => 'lover', + 'relationship_type_lover_with_name' => ':name kişisinin aşığı', + 'relationship_type_lover_female_with_name' => ':name kişisinin aşığı', + 'relationship_type_lover_male_with_name' => ':name’s lover', + + 'relationship_type_inlovewith' => 'aşık olduğu', + 'relationship_type_inlovewith_female' => 'aşık olduğu', + 'relationship_type_inlovewith_male' => 'in love with', + 'relationship_type_inlovewith_with_name' => ':name kişisinin aşık olduğu kişi', + 'relationship_type_inlovewith_female_with_name' => ':name kişisinin aşık olduğu kişi', + 'relationship_type_inlovewith_male_with_name' => 'someone :name is in love with', + + 'relationship_type_lovedby' => 'gizli aşık', + 'relationship_type_lovedby_female' => 'gizli aşık', + 'relationship_type_lovedby_male' => 'loved by', + 'relationship_type_lovedby_with_name' => ':name kişisinin gizli aşığı', + 'relationship_type_lovedby_female_with_name' => ':name kişisinin gizli aşığı', + 'relationship_type_lovedby_male_with_name' => ':name’s secret lover', + + 'relationship_type_ex' => 'ex-partner', + 'relationship_type_ex_female' => 'eski kız arkadaş', + 'relationship_type_ex_male' => 'ex-boyfriend', + 'relationship_type_ex_with_name' => ':name’s ex-partner', + 'relationship_type_ex_female_with_name' => ':name adlı kişinin eski kız arkadaşı', + 'relationship_type_ex_male_with_name' => ':name’s ex-boyfriend', + + 'relationship_type_parent' => 'parent', + 'relationship_type_parent_female' => 'anne', + 'relationship_type_parent_male' => 'father', + 'relationship_type_parent_with_name' => ':name’s parent', + 'relationship_type_parent_female_with_name' => ':name kişisinin annesi', + 'relationship_type_parent_male_with_name' => ':name’s father', + + 'relationship_type_child' => 'child', + 'relationship_type_child_female' => 'kız', + 'relationship_type_child_male' => 'son', + 'relationship_type_child_with_name' => ':name’s child', + 'relationship_type_child_female_with_name' => ':name kişisinin kızı', + 'relationship_type_child_male_with_name' => ':name’s son', + + 'relationship_type_stepparent' => 'step-parent', + 'relationship_type_stepparent_female' => 'üvey anne', + 'relationship_type_stepparent_male' => 'stepfather', + 'relationship_type_stepparent_with_name' => ':name’s step-parent', + 'relationship_type_stepparent_female_with_name' => ':name adlı kişinin üvey annesi', + 'relationship_type_stepparent_male_with_name' => ':name’s stepfather', + + 'relationship_type_stepchild' => 'stepchild', + 'relationship_type_stepchild_female' => 'üvey kız', + 'relationship_type_stepchild_male' => 'stepson', + 'relationship_type_stepchild_with_name' => ':name’s stepchild', + 'relationship_type_stepchild_female_with_name' => ':name adlı kişinin üvey kızı', + 'relationship_type_stepchild_male_with_name' => ':name’s stepson', + + 'relationship_type_sibling' => 'sibling', + 'relationship_type_sibling_female' => 'kız kardeş', + 'relationship_type_sibling_male' => 'brother', + 'relationship_type_sibling_with_name' => ':name’s sibling', + 'relationship_type_sibling_female_with_name' => ':name kişisinin kız kardeşi', + 'relationship_type_sibling_male_with_name' => ':name’s brother', + + 'relationship_type_grandparent' => 'grandparent', + 'relationship_type_grandparent_female' => 'grandmother', + 'relationship_type_grandparent_male' => 'grandfather', + 'relationship_type_grandparent_with_name' => ':name’s grandparent', + 'relationship_type_grandparent_female_with_name' => ':name’s grandmother', + 'relationship_type_grandparent_male_with_name' => ':name’s grandfather', + + 'relationship_type_grandchild' => 'grandchild', + 'relationship_type_grandchild_female' => 'granddaughter', + 'relationship_type_grandchild_male' => 'grandson', + 'relationship_type_grandchild_with_name' => ':name’s grandchild', + 'relationship_type_grandchild_female_with_name' => ':name’s granddauther', + 'relationship_type_grandchild_male_with_name' => ':name’s grandson', + + 'relationship_type_uncle' => 'amca/dayı', + 'relationship_type_uncle_female' => 'hala/teyze', + 'relationship_type_uncle_male' => 'uncle', + 'relationship_type_uncle_with_name' => ':name kişisinin amcası/dayısı', + 'relationship_type_uncle_female_with_name' => ':name kişisinin halası/teyzesi', + 'relationship_type_uncle_male_with_name' => ':name’s uncle', + + 'relationship_type_nephew' => 'yeğen', + 'relationship_type_nephew_female' => 'yeğen', + 'relationship_type_nephew_male' => 'nephew', + 'relationship_type_nephew_with_name' => ':name kişisinin yeğeni', + 'relationship_type_nephew_female_with_name' => ':name kişisinin yeğeni', + 'relationship_type_nephew_male_with_name' => ':name’s nephew', + + 'relationship_type_cousin' => 'kuzen', + 'relationship_type_cousin_female' => 'kuzen', + 'relationship_type_cousin_male' => 'cousin', + 'relationship_type_cousin_with_name' => ':name kişisinin kuzeni', + 'relationship_type_cousin_female_with_name' => ':name kişisinin kuzeni', + 'relationship_type_cousin_male_with_name' => ':name’s cousin', + + 'relationship_type_godfather' => 'godparent', + 'relationship_type_godfather_female' => 'vaftiz anası', + 'relationship_type_godfather_male' => 'godfather', + 'relationship_type_godfather_with_name' => ':name’s godparent', + 'relationship_type_godfather_female_with_name' => ':name kişisinin vaftiz anası', + 'relationship_type_godfather_male_with_name' => ':name’s godfather', + + 'relationship_type_godson' => 'godchild', + 'relationship_type_godson_female' => 'vaftiz kızı', + 'relationship_type_godson_male' => 'godson', + 'relationship_type_godson_with_name' => ':name’s godchild', + 'relationship_type_godson_female_with_name' => ':name kişisinin vaftiz kızı', + 'relationship_type_godson_male_with_name' => ':name’s godson', + + 'relationship_type_friend' => 'arkadaş', + 'relationship_type_friend_female' => 'arkadaş', + 'relationship_type_friend_male' => 'friend', + 'relationship_type_friend_with_name' => ':name kişisinin arkadaşı', + 'relationship_type_friend_female_with_name' => ':name kişisinin arkadaşı', + 'relationship_type_friend_male_with_name' => ':name’s friend', + + 'relationship_type_bestfriend' => 'en iyi arkadaş', + 'relationship_type_bestfriend_female' => 'en iyi arkadaş', + 'relationship_type_bestfriend_male' => 'best friend', + 'relationship_type_bestfriend_with_name' => ':name kişisinin en iyi arkadaşı', + 'relationship_type_bestfriend_female_with_name' => ':name kişisinin en iyi arkadaşı', + 'relationship_type_bestfriend_male_with_name' => ':name’s best friend', + + 'relationship_type_colleague' => 'iş arkadaşı', + 'relationship_type_colleague_female' => 'iş arkadaşı', + 'relationship_type_colleague_male' => 'colleague', + 'relationship_type_colleague_with_name' => ':name kişisinin iş arkadaşı', + 'relationship_type_colleague_female_with_name' => ':name kişisinin iş arkadaşı', + 'relationship_type_colleague_male_with_name' => ':name’s colleague', + + 'relationship_type_boss' => 'patron', + 'relationship_type_boss_female' => 'patron', + 'relationship_type_boss_male' => 'boss', + 'relationship_type_boss_with_name' => ':name kişisinin patronu', + 'relationship_type_boss_female_with_name' => ':name kişisinin patronu', + 'relationship_type_boss_male_with_name' => ':name’s boss', + + 'relationship_type_subordinate' => 'altında çalışan', + 'relationship_type_subordinate_female' => 'altında çalışan', + 'relationship_type_subordinate_male' => 'subordinate', + 'relationship_type_subordinate_with_name' => ':name kişisinin altında çalışan', + 'relationship_type_subordinate_female_with_name' => ':name kişisinin altında çalışan', + 'relationship_type_subordinate_male_with_name' => ':name’s subordinate', + + 'relationship_type_mentor' => 'danışman', + 'relationship_type_mentor_female' => 'danışman', + 'relationship_type_mentor_male' => 'mentor', + 'relationship_type_mentor_with_name' => ':name kişisinin danışmanı', + 'relationship_type_mentor_female_with_name' => ':name kişisinin danışmanı', + 'relationship_type_mentor_male_with_name' => ':name’s mentor', + + 'relationship_type_protege' => 'protégé', + 'relationship_type_protege_female' => 'protégé', + 'relationship_type_protege_male' => 'protégé', + 'relationship_type_protege_with_name' => ':name’s protégé', + 'relationship_type_protege_female_with_name' => ':name’s protégé', + 'relationship_type_protege_male_with_name' => ':name’s protégé', + + 'relationship_type_ex_husband' => 'ex-spouse', + 'relationship_type_ex_husband_female' => 'eski karı', + 'relationship_type_ex_husband_male' => 'ex-husband', + 'relationship_type_ex_husband_with_name' => ':name’s ex-spouse', + 'relationship_type_ex_husband_female_with_name' => ':name kişisinin eski karısı', + 'relationship_type_ex_husband_male_with_name' => ':name’s ex-husband', + + // emotions + 'emotion_primary_love' => 'Aşk', + 'emotion_primary_joy' => 'Sevinç', + 'emotion_primary_surprise' => 'Sürpriz', + 'emotion_primary_anger' => 'Öfke', + 'emotion_primary_sadness' => 'Üzüntü', + 'emotion_primary_fear' => 'Korku', + + 'emotion_secondary_affection' => 'İlgi', + 'emotion_secondary_lust' => 'Arzu', + 'emotion_secondary_longing' => 'Hasret', + 'emotion_secondary_cheerfulness' => 'Neşe', + 'emotion_secondary_zest' => 'Keyif', + 'emotion_secondary_contentment' => 'Hoşnutluk', + 'emotion_secondary_pride' => 'Onur', + 'emotion_secondary_optimism' => 'İyimserlik', + 'emotion_secondary_enthrallment' => 'Büyülenme', + 'emotion_secondary_relief' => 'Rahatlama', + 'emotion_secondary_surprise' => 'Sürpriz', + 'emotion_secondary_irritation' => 'Rahatsızlık', + 'emotion_secondary_exasperation' => 'Çileden çıkma', + 'emotion_secondary_rage' => 'Hiddet', + 'emotion_secondary_disgust' => 'İğrenti', + 'emotion_secondary_envy' => 'İmrenme', + 'emotion_secondary_suffering' => 'Acı çekme', + 'emotion_secondary_sadness' => 'Üzüntü', + 'emotion_secondary_disappointment' => 'Hayal Kırıklığı', + 'emotion_secondary_shame' => 'Utanma', + 'emotion_secondary_neglect' => 'İhmal', + 'emotion_secondary_sympathy' => 'Sempati', + 'emotion_secondary_horror' => 'Korku', + 'emotion_secondary_nervousness' => 'Tedirginlik', + + 'emotion_adoration' => 'Tapma', + 'emotion_affection' => 'İlgi', + 'emotion_love' => 'Aşk', + 'emotion_fondness' => 'Düşkünlük', + 'emotion_liking' => 'Beğeni', + 'emotion_attraction' => 'Cazibe', + 'emotion_caring' => 'Önemseme', + 'emotion_tenderness' => 'Hassasiyet', + 'emotion_compassion' => 'Şefkat', + 'emotion_sentimentality' => 'Duyarlılık', + 'emotion_arousal' => 'Uyarılma', + 'emotion_desire' => 'Arzu', + 'emotion_lust' => 'İhtiras', + 'emotion_passion' => 'Tutku', + 'emotion_infatuation' => 'Sevdalanma', + 'emotion_longing' => 'Hasret', + 'emotion_amusement' => 'Eğlence', + 'emotion_bliss' => 'Saadet', + 'emotion_cheerfulness' => 'Neşe', + 'emotion_gaiety' => 'Sevinç', + 'emotion_glee' => 'Keyif', + 'emotion_jolliness' => 'Neşe', + 'emotion_joviality' => 'Neşe', + 'emotion_joy' => 'Neşe', + 'emotion_delight' => 'Keyif', + 'emotion_enjoyment' => 'Hoşnutluk', + 'emotion_gladness' => 'Memnuniyet', + 'emotion_happiness' => 'Mutluluk', + 'emotion_jubilation' => 'Coşku', + 'emotion_elation' => 'Kıvanç', + 'emotion_satisfaction' => 'Tatmin olma', + 'emotion_ecstasy' => 'Zevk', + 'emotion_euphoria' => 'Öfori', + 'emotion_enthusiasm' => 'Heves', + 'emotion_zeal' => 'Gayret', + 'emotion_zest' => 'Keyif', + 'emotion_excitement' => 'Heyecan', + 'emotion_thrill' => 'Heyecan', + 'emotion_exhilaration' => 'Neşe', + 'emotion_contentment' => 'Hoşnutluk', + 'emotion_pleasure' => 'Zevk', + 'emotion_pride' => 'Onur', + 'emotion_eagerness' => 'Heves', + 'emotion_hope' => 'Umut', + 'emotion_optimism' => 'İyimserlik', + 'emotion_enthrallment' => 'Büyülenme', + 'emotion_rapture' => 'Kendinden geçme', + 'emotion_relief' => 'Rahatlama', + 'emotion_amazement' => 'Şaşkınlık', + 'emotion_surprise' => 'Sürpriz', + 'emotion_astonishment' => 'Hayret', + 'emotion_aggravation' => 'Çileden çıkma', + 'emotion_irritation' => 'Rahatsızlık', + 'emotion_agitation' => 'Kışkırtma', + 'emotion_annoyance' => 'Rahatsızlık', + 'emotion_grouchiness' => 'Huysuzluk', + 'emotion_grumpiness' => 'Somurtkanlık', + 'emotion_exasperation' => 'Çileden çıkma', + 'emotion_frustration' => 'Düş kırıklığı', + 'emotion_anger' => 'Öfke', + 'emotion_rage' => 'Hiddet', + 'emotion_outrage' => 'Hakaret', + 'emotion_fury' => 'Öfke', + 'emotion_wrath' => 'Gazap', + 'emotion_hostility' => 'Düşmanlık', + 'emotion_ferocity' => 'Vahşilik', + 'emotion_bitterness' => 'Keskinlik', + 'emotion_hate' => 'Nefret', + 'emotion_loathing' => 'İğrenme', + 'emotion_scorn' => 'Aşağılama', + 'emotion_spite' => 'Nispet', + 'emotion_vengefulness' => 'İntikamcılık', + 'emotion_dislike' => 'Beğenmeme', + 'emotion_resentment' => 'Gücenme', + 'emotion_disgust' => 'İğrenme', + 'emotion_revulsion' => 'Uzaklaşma', + 'emotion_contempt' => 'Küçümseme', + 'emotion_envy' => 'İmrenme', + 'emotion_jealousy' => 'Kıskançlık', + 'emotion_agony' => 'Acı çekme', + 'emotion_suffering' => 'Acı çekme', + 'emotion_hurt' => 'Acı', + 'emotion_anguish' => 'Izdırap', + 'emotion_depression' => 'Depresyon', + 'emotion_despair' => 'Ümitsizlik', + 'emotion_hopelessness' => 'Umutsuzluk', + 'emotion_gloom' => 'Kasvet', + 'emotion_glumness' => 'Asık suratlılık', + 'emotion_sadness' => 'Üzüntü', + 'emotion_unhappiness' => 'Mutsuzluk', + 'emotion_grief' => 'Keder', + 'emotion_sorrow' => 'Üzüntü', + 'emotion_woe' => 'Gam', + 'emotion_misery' => 'Sefalet', + 'emotion_melancholy' => 'Melankoli', + 'emotion_dismay' => 'Dehşet', + 'emotion_disappointment' => 'Hayal kırıklığı', + 'emotion_displeasure' => 'Hoşnutsuzluk', + 'emotion_guilt' => 'Suçluluk', + 'emotion_shame' => 'Utanma', + 'emotion_regret' => 'Pişman olma', + 'emotion_remorse' => 'Vicdan azabı', + 'emotion_alienation' => 'Yabancılaşma', + 'emotion_isolation' => 'İzolasyon', + 'emotion_neglect' => 'İhmal', + 'emotion_loneliness' => 'Yalnızlık', + 'emotion_rejection' => 'Reddedilme', + 'emotion_homesickness' => 'Sıla hasreti', + 'emotion_defeat' => 'Yenilgi', + 'emotion_dejection' => 'Keyifsizlik', + 'emotion_insecurity' => 'Güvensizlik', + 'emotion_embarrassment' => 'Mahçubiyet', + 'emotion_humiliation' => 'Aşağılanma', + 'emotion_insult' => 'Hakaret', + 'emotion_pity' => 'Acıma', + 'emotion_sympathy' => 'Sempati', + 'emotion_alarm' => 'Alarm', + 'emotion_shock' => 'Şok', + 'emotion_fear' => 'Korku', + 'emotion_fright' => 'Korku', + 'emotion_horror' => 'Korku', + 'emotion_terror' => 'Dehşet', + 'emotion_panic' => 'Panik', + 'emotion_hysteria' => 'Histeri', + 'emotion_mortification' => 'Küçük düşme', + 'emotion_anxiety' => 'Kaygı', + 'emotion_nervousness' => 'Tedirginlik', + 'emotion_tenseness' => 'Gerginlik', + 'emotion_uneasiness' => 'Huzursuzluk', + 'emotion_apprehension' => 'Kaygı', + 'emotion_worry' => 'Endişe', + 'emotion_distress' => 'Sıkıntı', + 'emotion_dread' => 'Korku', + + // weather + 'weather_sunny' => 'Sunny', + 'weather_clear' => 'Clear', + 'weather_clear-day' => 'Clear', + 'weather_clear-night' => 'Gece hava açık', + 'weather_light-drizzle' => 'Light drizzle', + 'weather_patchy-light-drizzle' => 'Patchy light drizzle', + 'weather_patchy-light-rain' => 'Patchy light rain', + 'weather_light-rain' => 'Light rain', + 'weather_moderate-rain-at-times' => 'Moderate rain at times', + 'weather_moderate-rain' => 'Moderate rain', + 'weather_patchy-rain-possible' => 'Patchy rain possible', + 'weather_heavy-rain-at-times' => 'Heavy rain at times', + 'weather_heavy-rain' => 'Heavy rain', + 'weather_light-freezing-rain' => 'Light freezing rain', + 'weather_moderate-or-heavy-freezing-rain' => 'Moderate or heavy freezing rain', + 'weather_light-sleet' => 'Light sleet', + 'weather_moderate-or-heavy-rain-shower' => 'Moderate or heavy rain shower', + 'weather_light-rain-shower' => 'Light rain shower', + 'weather_torrential-rain-shower' => 'Torrential rain shower', + 'weather_rain' => 'Yağmur', + 'weather_snow' => 'Kar', + 'weather_blowing-snow' => 'Blowing snow', + 'weather_patchy-light-snow' => 'Patchy light snow', + 'weather_light-snow' => 'Light snow', + 'weather_patchy-moderate-snow' => 'Patchy moderate snow', + 'weather_moderate-snow' => 'Moderate snow', + 'weather_patchy-heavy-snow' => 'Patchy heavy snow', + 'weather_heavy-snow' => 'Heavy snow', + 'weather_light-snow-showers' => 'Light snow showers', + 'weather_moderate-or-heavy-snow-showers' => 'Moderate or heavy snow showers', + 'weather_patchy-snow-possible' => 'Patchy snow possible', + 'weather_patchy-sleet-possible' => 'Patchy sleet possible', + 'weather_moderate-or-heavy-sleet' => 'Moderate or heavy sleet', + 'weather_light-sleet-showers' => 'Light sleet showers', + 'weather_moderate-or-heavy-sleet-showers' => 'Moderate or heavy sleet showers', + 'weather_sleet' => 'Karla karışık yağmur', + 'weather_wind' => 'Rüzgar', + 'weather_fog' => 'Sis', + 'weather_freezing-fog' => 'Freezing fog', + 'weather_mist' => 'Mist', + 'weather_blizzard' => 'Blizzard', + 'weather_overcast' => 'Overcast', + 'weather_cloudy' => 'Bulutlu', + 'weather_partly-cloudy-day' => 'Partly cloudy', + 'weather_partly-cloudy-night' => 'Partly cloudy', + 'weather_freezing-drizzle' => 'Freezing drizzle', + 'weather_heavy-freezing-drizzle' => 'Heavy freezing drizzle', + 'weather_patchy-freezing-drizzle-possible' => 'Patchy freezing drizzle possible', + 'weather_ice-pellets' => 'Ice pellets', + 'weather_light-showers-of-ice-pellets' => 'Light showers of ice pellets', + 'weather_moderate-or-heavy-showers-of-ice-pellets' => 'Moderate or heavy showers of ice pellets', + 'weather_thundery-outbreaks-possible' => 'Thundery outbreaks possible', + 'weather_patchy-light-rain-with-thunder' => 'Patchy light rain with thunder', + 'weather_moderate-or-heavy-rain-with-thunder' => 'Moderate or heavy rain with thunder', + 'weather_patchy-light-snow-with-thunder' => 'Patchy light snow with thunder', + 'weather_moderate-or-heavy-snow-with-thunder' => 'Moderate or heavy snow with thunder', + 'weather_current_temperature_celsius' => ':temperature °C', + 'weather_current_temperature_fahrenheit' => ':temperature °F', + 'weather_current_title' => 'Şu anki hava durumu', + + // dav + 'dav_contacts' => 'Kişiler', + 'dav_contacts_description' => ':name kişisinin kişileri', + 'dav_birthdays' => 'Doğum Günleri', + 'dav_birthdays_description' => ':name kişisinin kişilerinin doğum günleri', + 'dav_tasks' => 'Görevler', + 'dav_tasks_description' => ':name kişisinin görevleri', + + // contact list + 'contact_list_avatar' => 'Profil Resmi', + 'contact_list_name' => 'İletişim', + 'contact_list_description' => 'Açıklama', + +]; diff --git a/resources/lang/tr/auth.php b/resources/lang/tr/auth.php new file mode 100644 index 0000000..afbf46f --- /dev/null +++ b/resources/lang/tr/auth.php @@ -0,0 +1,89 @@ + 'Girilmiş olan kullanıcı verileri sistemdekiler ile eşleşmemektedir.', + 'throttle' => 'Çok fazla oturum açma girişiminde bulundunuz. Lütfen :seconds saniye içerisinde tekrar deneyiz.', + 'not_authorized' => 'Bu işlemi yürütme yetkiniz yok', + 'signup_disabled' => 'Kayıt şu anda devre dışı', + 'signup_error' => 'Kullanıcı kayıt ederken bir hata oluştu', + 'back_homepage' => 'Ana sayfaya dön', + 'mfa_auth_otp' => 'İki faktörlü cihazınızla kimlik doğrulaması', + 'mfa_auth_webauthn' => 'Bir güvenlik anahtarıyla (WebAuthn) kimlik doğrulaması', + '2fa_title' => 'İki Adımlı Doğrulama', + '2fa_wrong_validation' => 'İki adımlı doğrulaması başarısız oldu.', + '2fa_one_time_password' => 'İki adımlı doğrulama kodu', + '2fa_recuperation_code' => 'İki aşamalı doğrulama kodu ile girin', + '2fa_one_time_or_recuperation' => 'Enter a two factor authentication code or a recovery code', + '2fa_otp_help' => 'İki aşamalı kimlik doğrulama mobil uygulamanızı açın ve kodu kopyalayın', + + 'login_to_account' => 'Hesabınıza giriş yapın', + 'login_with_recovery' => 'Bir kurtarma kodu ile giriş yap', + 'login_again' => 'Lütfen hesabınıza tekrar giriş yapınız', + 'email' => 'E-posta', + 'password' => 'Şifre', + 'recovery' => 'Kurtarma kodu', + 'login' => 'Oturum Aç', + 'button_remember' => 'Beni Hatırla', + 'password_forget' => 'Şifremi unuttum', + 'password_reset' => 'Şifrenizi değiştirin', + 'use_recovery' => 'Veya bir kurtarma kodu kullanabilirsiniz', + 'signup_no_account' => 'Hesabınız yok mu?', + 'signup' => 'Kayıt ol', + 'create_account' => 'Kayıt olarak ilk hesabınızı oluşturun', + 'change_language_title' => 'Dili değiştir:', + 'change_language' => 'Dili :lang ile değiştir', + + 'password_reset_title' => 'Şifreyi Yenile', + 'password_reset_email' => 'E-posta Adresi', + 'password_reset_send_link' => 'Şifre sıfırlama bağlantısını gönder', + 'password_reset_password' => 'Şifre', + 'password_reset_password_confirm' => 'Şifreyi Doğrula', + 'password_reset_action' => 'Şifreyi Sıfırla', + 'password_reset_email_content' => 'Şifrenizi sıfırlamak için buraya tıklayın:', + + 'register_title_welcome' => 'Yeni yüklenen Monica örneğinize hoş geldiniz', + 'register_create_account' => 'Monica\'yı kullanmak için bir hesap oluşturmanız gerekir', + 'register_title_create' => 'Monica hesabınızı oluşturun', + 'register_login' => 'Zaten bir hesabınız varsa Giriş Yapın.', + 'register_email' => 'Geçerli bir e-posta adresi girin', + 'register_email_example' => 'mail@mail', + 'register_firstname' => 'Ad', + 'register_firstname_example' => 'örn: Mehmet', + 'register_lastname' => 'Soyad', + 'register_lastname_example' => 'örn: Ağa', + 'register_password' => 'Şifre', + 'register_password_example' => 'Güçlü bir şifre girin', + 'register_password_confirmation' => 'Şifre doğrulama', + 'register_action' => 'Kayıt Ol', + 'register_policy' => 'Kayıt olmak, Gizlilik Politikamızı ve Kullanım Koşullarımızı okuduğunuz ve kabul ettiğiniz anlamına gelmektedir.', + 'register_invitation_email' => 'Güvenlik nedeniyle, lütfen sizi bu hesaba katılmaya davet eden kişinin e-posta adresini belirtin. Bu bilgi davet e-postasında verilmektedir.', + + 'confirmation_title' => 'E-posta adresinizi doğrulayın', + 'confirmation_fresh' => 'E-posta adresinize yeni bir doğrulama linki gönderildi.', + 'confirmation_check' => 'Devam etmeden önce lütfen doğrulama linki için e-postanızı kontrol edin.', + 'confirmation_request_another' => 'E-postayı almadıysanız başka bir tane istemek için buraya tıklayın.', + + 'confirmation_again' => 'Eğer e-posta adresinizi değiştirmek istiyorsanız buraya tıklayabilirsiniz.', + 'email_change_current_email' => 'Geçerli e-posta adresi:', + 'email_change_title' => 'E-posta adresini değiştir', + 'email_change_new' => 'Yeni e-posta adresi', + 'email_changed' => 'E-posta adresiniz değiştirildi. Doğrulamak için posta kutunuzu kontrol edin.', +]; diff --git a/resources/lang/tr/changelog.php b/resources/lang/tr/changelog.php new file mode 100644 index 0000000..a228145 --- /dev/null +++ b/resources/lang/tr/changelog.php @@ -0,0 +1,12 @@ + 'Ürün değişiklikleri', + 'note' => 'Not: ne yazık ki, bu sayfa yalnızca İngilizce\'dir.', +]; diff --git a/resources/lang/tr/dashboard.php b/resources/lang/tr/dashboard.php new file mode 100644 index 0000000..28e74c1 --- /dev/null +++ b/resources/lang/tr/dashboard.php @@ -0,0 +1,42 @@ + 'Hesabınıza hoş geldiniz!', + 'dashboard_blank_description' => 'Monica, önemsediğiniz kişilerle olan tüm etkileşimlerinizi organize edebileceğiniz bir yerdir.', + 'dashboard_blank_cta' => 'İlk bağlantınızı ekleyin', + 'dashboard_blank_illustration' => 'Illustration by Freepik', + + 'notes_title' => 'Yıldız eklediğiniz herhangi bir not yok.', + + 'tab_recent_calls' => 'Son çağrılar', + 'tab_favorite_notes' => 'Favori notlar', + 'tab_calls_blank' => 'Herhangi bir çağrı kayıt etmediniz.', + 'tab_debts' => 'Borçlar', + 'tab_debts_blank' => 'Herhangi bir borç kaydı girmediniz.', + 'tab_tasks' => 'Görevler', + 'tab_tasks_blank' => 'Herhangi bir görev eklemediniz.', + + 'tasks_add_task_placeholder' => 'Bu görev ne ile ilgili?', + 'tasks_tab_your_contacts' => 'Kişilerinizle ilgili görevler', + 'tasks_tab_your_tasks' => 'Yapılacaklar', + 'tasks_add_note' => 'Görevi eklemek için Enter tuşuna basın.', + 'task_add_cta' => 'Görev ekle', + + 'debts_you_owe' => 'Borcunuz', + + 'statistics_contacts' => 'Bağlantılar', + 'statistics_activities' => 'Etkinlikler', + 'statistics_gifts' => 'Hediyeler', + + 'reminders_next_months' => 'Gelecek 3 aydaki etkinlikler', + 'reminders_none' => 'Bu ay için hatırlatma yok.', + + 'product_changes' => 'Ürün değişiklikleri', + 'product_view_details' => 'Detayları göster', +]; diff --git a/resources/lang/tr/format.php b/resources/lang/tr/format.php new file mode 100644 index 0000000..eb6c636 --- /dev/null +++ b/resources/lang/tr/format.php @@ -0,0 +1,36 @@ + 'd M Y H:i', + 'short_date_year' => 'd M Y', + 'short_date' => 'd M', + 'short_month' => 'M', + 'short_month_year' => 'M Y', + 'short_day' => 'D', + 'full_date_year' => 'd F Y', + 'full_month' => 'F', + 'full_month_year' => 'F Y', + 'full_hour' => 'h.i A', + + 'short_text' => '{text}…', +]; diff --git a/resources/lang/tr/journal.php b/resources/lang/tr/journal.php new file mode 100644 index 0000000..d005d37 --- /dev/null +++ b/resources/lang/tr/journal.php @@ -0,0 +1,38 @@ + 'Günün nasıl geçti? Günde bir defa derecelendirebilirsin.', + 'journal_come_back' => 'Teşekkürler. Gününü değerlendirmek için yarın tekrar gel.', + 'journal_description' => 'Not: günlük içerikleri hem sizin tarafınızdan hem de bağlantılarınızla olan etkinliklerden otomatik oluşturulur. Günlük girdilerini silebilirsiniz, etkinlikleri bağlantı sayfalarından silmeniz gerekir.', + 'journal_add' => 'Bir günlük girdisi ekle', + 'journal_edit' => 'Bir günlük girdisini düzenle', + 'journal_empty' => 'Boş günlük', + 'journal_created_at' => 'Created at {date}', + 'journal_created_automatically' => 'Otomatik olarak oluşturuldu', + 'journal_entry_type_journal' => 'Günlük girdisi', + 'journal_entry_type_activity' => 'Faaliyet', + 'journal_entry_rate' => 'Gününü değerlendirdin.', + 'journal_add_comment' => 'Yorum eklemek ister misiniz (isteğe bağlı)?', + 'journal_show_comment' => 'Yorumu göster', + 'entry_delete_success' => 'Günlük girdisi başarıyla silindi.', + 'journal_add_title' => 'Başlık (isteğe bağlı)', + 'journal_add_date' => 'Tarih', + 'journal_add_post' => 'Girdi', + 'journal_add_cta' => 'Kaydet', + 'journal_blank_cta' => 'İlk günlük içeriğini yaz', + 'journal_blank_description' => 'Günlük başından geçen olayları yazmanı ve onları tekrar hatırlamanı sağlar.', + 'delete_confirmation' => 'Bu girdiyi silmek istediğinizden emin misiniz?', + 'apply_filter' => 'Apply filter', + 'start_date' => 'Start Date', + 'end_date' => 'End Date', + 'per_page' => 'Per Page', + 'sort_order' => 'Sort By Created At', + 'ascending' => 'Ascending', + 'descending' => 'Descending', +]; diff --git a/resources/lang/tr/logs.php b/resources/lang/tr/logs.php new file mode 100644 index 0000000..77a7cb0 --- /dev/null +++ b/resources/lang/tr/logs.php @@ -0,0 +1,29 @@ + 'Yeni kişi oluşturuldu.', + 'settings_log_contact_created_with_name' => ':name bağlantı olarak eklendi.', + + // contat description update + 'contact_log_contact_description_updated' => 'Açıklama güncellendi.', + 'settings_log_contact_description_updated_with_name' => ':name açıklaması güncellendi.', + + // contact description clear + 'contact_log_contact_description_cleared' => 'Açıklama temizlendi.', + 'settings_log_contact_description_cleared_with_name' => ':name açıklaması temizlendi.', + + // contact work information update + 'contact_log_contact_work_updated' => 'İş bilgileri güncellendi.', + 'settings_log_contact_work_updated_with_name' => ':name iş bilgileri güncellendi.', + + // company created + 'settings_log_company_created' => ':name adında bir firma oluşturuldu.', +]; diff --git a/resources/lang/tr/mail.php b/resources/lang/tr/mail.php new file mode 100644 index 0000000..f706b42 --- /dev/null +++ b/resources/lang/tr/mail.php @@ -0,0 +1,53 @@ + ':contact için hatırlatıcı', + 'greetings' => 'Merhaba :username', + 'want_reminded_of' => ':reason için hatırlatma istediniz', + 'for' => ':name için', + 'comment' => 'Yorum: :comment', + 'footer_contact_info' => 'Bu kişiyle ilgili bilgileri ekleyin, görüntüleyin, tamamlayın ve değiştirin:', + 'footer_contact_info2' => ':name adlı kişinin profilini gör', + 'footer_contact_info2_link' => ':name adlı kişinin profilini gör: :url', + + 'notification_subject_line' => 'Yaklaşan etkinliğiniz var', + 'notification_description' => ':count gün içinde (:date tarihinde), şu olay meydana gelecek:', + + 'stay_in_touch_subject_line' => ':name ile iletişimde kal', + 'stay_in_touch_subject_description' => ':name ile her :frequency günde bir irtibatta kalmayı hatırlatılmak istediniz.', + + 'notifications_whoops' => 'Hoppala!', + 'notifications_hello' => 'Merhaba!', + 'notifications_regards' => 'Saygılarımızla', + 'notifications_footer' => '":actionText" butonuna basmakta sorun yaşıyorsanız, aşağıdaki URL\'yi kopyalayıp web tarayıcınıza yapıştırın: [:actionURL](:actionURL)', + 'notifications_rights' => 'Tüm Hakları Saklıdır', + + 'confirmation_email_title' => 'Monica – E-posta Adresi Onayı', + 'confirmation_email_intro'=> 'E-postanızı doğrulamak için aşağıdaki butona tıklayın', + 'confirmation_email_button' => 'E-posta adresini doğrula', + 'confirmation_email_bottom' => 'Eğer bir hesap oluşturmadıysanız, başka bir işlem yapmanıza gerek yoktur.', + + 'password_reset_title' => 'Monica – Parola Sıfırlama Bildirimi', + 'password_reset_intro' => 'Hesabınız için bir parola sıfırlama talebi aldığımız için bu e-postayı alıyorsunuz.', + 'password_reset_button' => 'Parola Sıfırla', + 'password_reset_expiration' => 'Bu parola sıfırlama linkinin geçerlilik süresi :count dakika içinde dolacaktır.', + 'password_reset_bottom' => 'Eğer parola sıfırlama talebinde bulunmadıysanız, başka bir işlem yapmanıza gerek yoktur.', + + 'invitation_title' => 'Monica - :name tarafından davet edildiniz', + 'invitation_intro' => ':name (:email) tarafından güzel bir Kişisel İlişki Yönetimi aracı olan Monica\'yı kullanmaya davet edildiniz.', + 'invitation_link' => 'Daveti kabul etmek için aşağıdaki linke tıklayın:', + 'invitation_button' => 'Daveti kabul et', + 'invitation_expiration' => 'Bu linkin süresi :count gün içinde dolacaktır.', + + 'export_title' => 'Dışa aktarım dosyanız hazır', + 'export_description' => ':date tarihinde verilerinizi indirmek için talep oluşturdunuz. Şimdi indirilmeye hazır.', + 'export_download' => 'Download export', + +]; diff --git a/resources/lang/tr/pagination.php b/resources/lang/tr/pagination.php new file mode 100644 index 0000000..8483f6c --- /dev/null +++ b/resources/lang/tr/pagination.php @@ -0,0 +1,25 @@ + '❮ Önceki', + 'next' => 'Sonraki ❯', + +]; diff --git a/resources/lang/tr/passwords.php b/resources/lang/tr/passwords.php new file mode 100644 index 0000000..9a038b4 --- /dev/null +++ b/resources/lang/tr/passwords.php @@ -0,0 +1,30 @@ + 'Şifreniz sıfırlandı!', + 'sent' => 'Eğer girdiğiniz e-posta adresi kayıtlarımızda varsa, şifre yenileme linki gönderilecektir.', + 'token' => 'Bu şifre sıfırlama güvenlik anahtarı geçersiz.', + 'user' => 'Eğer girdiğiniz e-posta adresi kayıtlarımızda varsa, şifre yenileme linki gönderilecektir.', + 'changed' => 'Parola başarıyla değiştirildi.', + 'invalid' => 'Girdiğiniz güncel şifreniz doğru değil.', + 'throttled' => 'Lütfen tekrar denemeden önce bekleyin.', + +]; diff --git a/resources/lang/tr/people.php b/resources/lang/tr/people.php new file mode 100644 index 0000000..51e6835 --- /dev/null +++ b/resources/lang/tr/people.php @@ -0,0 +1,539 @@ + 'Kişi bulunamadı', + 'people_list_number_kids' => ':count child|:count children', + 'people_list_last_updated' => 'Son görüşme:', + 'people_list_number_reminders' => ':count reminder|:count reminders', + 'people_list_blank_title' => 'Hesabınızda kayıtlı kişi yok', + 'people_list_blank_cta' => 'Birisini ekle', + 'people_list_sort' => 'Sırala', + 'people_list_stats' => ':count contact|:count contacts', + 'people_list_firstnameAZ' => 'İsimleri A → Z göre sırala', + 'people_list_firstnameZA' => 'İsimleri Z → A göre sırala', + 'people_list_lastnameAZ' => 'Soy isimleri A → Z göre sırala', + 'people_list_lastnameZA' => 'Soy isimleri Z → A göre sırala', + 'people_list_lastactivitydateNewtoOld' => 'Sort by last activity date, newest to oldest', + 'people_list_lastactivitydateOldtoNew' => 'Sort by last activity date, oldest to newest', + 'people_list_filter_tag' => 'Etiketlenen tüm bağlantılar listele', + 'people_list_clear_filter' => 'Filtreyi temizle', + 'people_list_contacts_per_tags' => ':count contact|:count contacts', + 'people_list_show_dead' => 'Ölmüş kişileri göster (:count)', + 'people_list_hide_dead' => 'Ölmüş kişileri gizle (:count)', + 'people_search' => 'Search your contacts…', + 'people_search_no_results' => 'Sonuç bulunamadı', + 'people_search_next' => 'Sonraki', + 'people_search_prev' => 'Previous', + 'people_search_rows_per_page' => 'Rows per page', + 'people_search_of' => '/', + 'people_search_page' => 'Sayfa', + 'people_search_all' => 'Hepsi', + 'people_add_new' => 'Yeni kişi ekle', + 'people_list_account_usage' => 'Hesap kullanım bilgileriniz: :current/:limit bağlantı', + 'people_list_account_upgrade_title' => 'Tüm özellikleri kullanmak için hesabınızı yükseltin.', + 'people_list_account_upgrade_cta' => 'Şimdi güncelle', + 'people_list_untagged' => 'Etiketlenmemiş bağlantıları göster', + 'people_list_filter_untag' => 'Etiketlenmemiş bağlantılar listeleniyor', + 'archived_contact_readonly' => 'Archived contact can’t be edited, please unarchive it first.', + + // people add + 'people_add_title' => 'Yeni kişi ekle', + 'people_add_missing' => 'No person found – add a new one now', + 'people_add_firstname' => 'Ad', + 'people_add_middlename' => 'Middle name (optional)', + 'people_add_lastname' => 'Last name (optional)', + 'people_add_email' => 'Email (optional)', + 'people_add_nickname' => 'Nickname (optional)', + 'people_add_cta' => 'Ekle', + 'people_save_and_add_another_cta' => 'Kaydet ve başka birini ekle', + 'people_add_success' => ':name başarıyla kayıt edildi', + 'people_add_gender' => 'Cinsiyet', + 'people_delete_success' => 'Bağlantı silindi', + 'people_delete_message' => 'Bağlantıyı Sil', + 'people_delete_confirmation' => 'Are you sure you want to delete :name’s contact? Deletion is immediate and permanent.', + 'people_add_birthday_reminder' => 'Mutlu yıllar diliyorum: isim', + 'people_add_birthday_reminder_deceased' => 'On this date, :name would have celebrated their birthday', + 'people_add_import' => 'Bağlantılarınızı aktarmak istiyor musunuz?', + 'people_edit_email_error' => 'Bu e-mail adresine sahip bir kişi listenize kayıtlı. Lütfen farklı bir adres giriniz.', + 'people_export' => 'Vcard Formatında Çıkar', + 'people_add_reminder_for_birthday' => 'Create an annual birthday reminder', + + // show + 'section_contact_information' => 'İletişim bilgileri', + 'section_personal_activities' => 'Aktiviteler', + 'section_personal_reminders' => 'Hatırlatıcılar', + 'section_personal_tasks' => 'Görevler', + 'section_personal_gifts' => 'Hediyeler', + 'section_personal_notes' => 'Notlar', + + // archived contacts + 'list_link_to_active_contacts' => 'Arşivlenmiş kişileri görüntülüyorsunuz. Bunun yerine aktif kişilerin listesine bakın.', + 'list_link_to_archived_contacts' => 'Arşivlenmiş kişilerin listesi', + + // Header + 'me' => 'Bu sensin', + 'edit_contact_information' => 'İletişim bilgilerini düzenle', + 'contact_archive' => 'Kişiyi arşivle', + 'contact_unarchive' => 'Kişiyi arşivden kaldır', + 'contact_archive_help' => 'Archived contacts are not be shown on the contact list, but still appear in search results.', + 'call_button' => 'Çağrıyı logla', + 'set_favorite' => 'Favori kişiler, kişi listesinin en üstüne yerleştirilir', + + // Stay in touch + 'stay_in_touch' => 'İrtibatta kal', + 'stay_in_touch_frequency' => 'Her {count} gün iletişimde kalın', + 'stay_in_touch_next_date' => 'Next due: {date}', + 'stay_in_touch_invalid' => 'Sıklık değeri, 0\'dan daha büyük bir sayı olmalıdır.', + 'stay_in_touch_premium' => 'Bu özelliği kullanabilmek için hesabınızı yükseltmeniz gerekir', + 'stay_in_touch_modal_title' => 'İrtibatta kal', + 'stay_in_touch_modal_desc' => 'Size {firstname} ile bağlantıda kalmanızı düzenli aralıklarla e-posta ile hatırlatabiliriz.', + 'stay_in_touch_modal_label' => 'Send me an email every… {count} day|Send me an email every… {count} days', + + // Calls + 'modal_call_title' => 'Çağrıyı logla', + 'modal_call_comment' => 'Ne hakkında konuştunuz? (isteğe bağlı)', + 'modal_call_exact_date' => 'Telefon görüşmesi gerçekleşti', + 'modal_call_who_called' => 'Kim aradı?', + 'modal_call_emotion' => 'Bu görüşme sırasında nasıl hissettiğinizi kaydetmek ister misiniz? (isteğe bağlı)', + 'calls_add_success' => 'Telefon görüşmesi kaydedildi.', + 'call_delete_confirmation' => 'Bu aramayı silmek istediğinize emin misiniz?', + 'call_delete_success' => 'Arama başarılı bir şekilde silindi', + 'call_title' => 'Telefon görüşmeleri', + 'call_empty_comment' => 'Ayrıntı yok', + 'call_blank_title' => '{name} ile yaptığınız telefon görüşmelerinin kaydını tutun', + 'call_blank_desc' => '{name} kişisini aradınız', + 'call_you_called' => 'Siz aradınız', + 'call_he_called' => '{name} aradı', + 'call_emotions' => 'Duygular:', + + // Conversation + 'conversation_blank' => 'Record conversations you have with :name on social media, SMS…', + 'conversation_delete_link' => 'Sohbeti sil', + 'conversation_edit_title' => 'Sohbeti düzenle', + 'conversation_edit_delete' => 'Bu sohbeti silmek istediğinizden emin misiniz? Silme işlemi geri alınamaz.', + 'conversation_add_success' => 'Sohbet başarıyla eklendi.', + 'conversation_edit_success' => 'Sohbet başarıyla güncellendi.', + 'conversation_delete_success' => 'Sohbet başarıyla silindi.', + 'conversation_add_title' => 'Yeni bir sohbet kaydet', + 'conversation_add_when' => 'Bu konuşmayı ne zaman yaptınız?', + 'conversation_add_who_wrote' => 'Who sent this message?', + 'conversation_add_how' => 'Nasıl iletişim kurdunuz?', + 'conversation_add_you' => 'Siz', + 'conversation_add_content' => 'Söyleneni yazın', + 'conversation_add_what_was_said' => 'Siz ne dediniz?', + 'conversation_add_another' => 'Başka bir mesaj ekle', + 'conversation_add_error' => 'En az bir mesaj eklemelisiniz.', + 'conversation_list_table_messages' => 'Mesajlar', + 'conversation_list_table_content' => 'Kısmi içerik (en son mesaj)', + 'conversation_list_title' => 'Sohbetler', + 'conversation_list_cta' => 'Konuşmayı günlüğe kaydet', + + // age - birthday + 'birthdate_not_set' => 'Birthday is not set', + 'age_approximate_in_years' => 'yaklaşık :age yaşında', + 'age_exact_in_years' => ':age yaşında', + 'age_exact_birthdate' => 'doğum tarihi :date', + + // Last called + 'last_called' => 'Son arama: :date', + 'last_talked_to' => 'Last called: {date}', + 'last_called_empty' => 'Son arama: bilinmiyor', + 'last_activity_date' => 'Birlikte son aktivite: :date', + 'last_activity_date_empty' => 'Birlikte son aktivite: bilinmiyor', + + // additional information + 'information_edit_success' => 'Profil başarıyla güncellendi', + 'information_edit_title' => ':name kişisinin kişisel bilgilerini düzenle', + 'information_edit_max_size' => 'En fazla :size Kb.', + 'information_edit_max_size2' => 'En fazla {size} Kb.', + 'information_edit_firstname' => 'Ad', + 'information_edit_lastname' => 'Last name (optional)', + 'information_edit_description' => 'Description (optional)', + 'information_edit_description_help' => 'Kişi listesine gerekli olduğunda biraz içerik eklemek için kullanılır.', + 'information_edit_unknown' => 'Bu kişinin yaşını bilmiyorum', + 'information_edit_probably' => 'This person is probably…', + 'information_edit_not_year' => 'I know the day and month of this person’s birthday, but not the year…', + 'information_edit_exact' => 'I know this person’s exact birthday…', + 'information_edit_birthdate_label' => 'Birthday', + 'information_no_work_defined' => 'Tanımlanmış iş bilgisi yok', + 'information_work_at' => ':company', + 'work_add_cta' => 'İş Bilgilerini Güncelle', + 'work_edit_success' => 'Work information updated', + 'work_edit_title' => 'Güncelle :name kişisinin iş bilgisi', + 'work_edit_job' => 'İş unvanı (isteğe bağlı)', + 'work_edit_company' => 'Şirket (isteğe bağlı)', + 'work_information' => 'İş bilgisi', + + // food preferences + 'food_preferences_add_success' => 'Yiyecek tercihleri kaydedildi', + 'food_preferences_edit_description' => 'Belki :firstname ya da :family\'nin ailesinden birinin bir alerjisi vardır. Veya belirli bir şişe şarabı sevmemektedir. Bunları burada belirtin, böylece onları bir dahaki sefere akşam yemeğine davet ettiğinizde hatırlayacaksınız', + 'food_preferences_edit_description_no_last_name' => 'Belki :firstname\'in bir alerjisi vardır. Veya belirli bir şişe şarabı sevmemektedir. Bunları burada belirtin, böylece onları bir dahaki sefere akşam yemeğine davet ettiğinizde hatırlayacaksınız', + 'food_preferences_edit_title' => 'Yiyecek tercihlerini belirtin', + 'food_preferences_edit_cta' => 'Yiyecek tercihlerini kaydet', + 'food_preferences_title' => 'Yiyecek tercihleri', + 'food_preferences_cta' => 'Yiyecek tercihleri ekle', + + // reminders + 'reminders_blank_title' => ':name ile ilgili hatırlatılmasını istediğiniz bir şey var mı?', + 'reminders_blank_add_activity' => 'Hatırlatıcı ekle', + 'reminders_add_title' => ':name hakkında neyin hatırlatılmasını istersiniz?', + 'reminders_add_description' => 'Please remind me to…', + 'reminders_add_next_time' => 'Bir dahaki sefere ne zaman bunun hatırlatılmasını istersiniz?', + 'reminders_add_once' => 'Bunu sadece bir kere hatırlat', + 'reminders_add_recurrent' => 'Bunu bana hatırlat', + 'reminders_add_starting_from' => 'yukarıda belirtilen tarihten başlayarak', + 'reminders_add_cta' => 'Hatırlatıcı ekle', + 'reminders_edit_update_cta' => 'Hatırlatıcıyı güncelle', + 'reminders_add_error_custom_text' => 'Bu hatırlatıcı için bir metin belirtmeniz gerekmektedir', + 'reminders_create_success' => 'Hatırlatıcı başarıyla eklendi', + 'reminders_delete_success' => 'Hatırlatıcı başarıyla silindi', + 'reminders_update_success' => 'Hatırlatıcı başarıyla güncellendi', + 'reminders_add_optional_comment' => 'İsteğe bağlı yorum', + + 'reminder_frequency_day' => 'her gün|[2,*]:number günde bir', + 'reminder_frequency_week' => 'her hafta|[2,*]:number haftada bir', + 'reminder_frequency_month' => 'her ay|[2,*]:number ayda bir', + 'reminder_frequency_year' => 'her yıl|[2,*]:number yılda bir', + 'reminder_frequency_one_time' => ':date tarihinde', + 'reminders_delete_confirmation' => 'Bu hatırlatıcıyı silmek ister misiniz?', + 'reminders_delete_cta' => 'Sil', + 'reminders_next_expected_date' => 'tarihinde', + 'reminders_cta' => 'Hatırlatıcı ekle', + 'reminders_description' => 'We will send an email for each one of the reminders below. Reminders are sent every morning the day events will happen. Reminders automatically added for birthdays can not be deleted. If you want to change those dates, edit the birthday of the contacts.', + 'reminders_one_time' => 'Bir kez', + 'reminders_type_week' => 'hafta', + 'reminders_type_month' => 'ay', + 'reminders_type_year' => 'yıl', + 'reminders_birthday' => ':name\'nin/nun Doğum Günü', + 'reminders_free_plan_warning' => 'Ücretsiz plana dahilsiniz. Bu planda e-posta gönderilmez. Hatırlatıcılarınızı e-posta ile almak için hesabınızı yükseltin.', + + // relationships + 'relationship_form_add' => 'Yeni bir ilişki ekle', + 'relationship_form_edit' => 'Mevcut bir ilişkiyi güncelle', + 'relationship_form_is_with' => 'This person is…', + 'relationship_form_is_with_name' => ':name is…', + 'relationship_form_add_choice' => 'İlişki kiminle?', + 'relationship_form_create_contact' => 'Yeni kişi ekle', + 'relationship_form_associate_contact' => 'Var olan bir kişi', + 'relationship_form_associate_dropdown' => 'Aşağıdaki açılır listeden mevcut bir kişiyi arayın ve seçin', + 'relationship_form_associate_dropdown_placeholder' => 'Mevcut bir kişiyi arayın ve seçin', + 'relationship_form_also_create_contact' => 'Bu kişi için bir Kişi girişi oluşturun.', + 'relationship_form_add_description' => 'Bu, bu kişiye diğer kişileriniz gibi davranmanıza izin verecektir.', + 'relationship_form_add_no_existing_contact' => 'Şu anda :name ile ilişkili olabilecek herhangi bir kişiniz yok.', + 'relationship_delete_confirmation' => 'Bu ilişkiyi silmek istediğinizden emin misiniz? Silme işlemi geri alınamaz.', + 'relationship_unlink_confirmation' => 'Bu ilişkiyi silmek istediğinizden emin misiniz? Bu kişi silinmeyecek - yalnızca ikisi arasındaki ilişki silinecek.', + 'relationship_form_add_success' => 'İlişki başarıyla kuruldu.', + 'relationship_form_deletion_success' => 'İlişki silindi.', + + // tasks + 'tasks_title' => 'Görevler', + 'tasks_blank_title' => 'Henüz bir göreviniz yok.', + 'tasks_form_title' => 'Başlık', + 'tasks_form_description' => 'Açıklama (isteğe bağlı)', + 'tasks_add_task' => 'Görev ekle', + 'tasks_delete_success' => 'Görev başarılı bir şekilde silindi', + 'tasks_complete_success' => 'Görev başarıyla durumu değiştirdi', + + // activities + 'activity_title' => 'Aktiviteler', + 'activity_type_category_simple_activities' => 'Basit aktiviteler', + 'activity_type_category_sport' => 'Spor', + 'activity_type_category_food' => 'Yemek', + 'activity_type_category_cultural_activities' => 'Kültürel aktiviteler', + 'activity_type_just_hung_out' => 'sadece takıldık', + 'activity_type_watched_movie_at_home' => 'evde film izledik', + 'activity_type_talked_at_home' => 'sadece evde konuştuk', + 'activity_type_did_sport_activities_together' => 'birlikte spor yapıldı', + 'activity_type_ate_at_his_place' => 'onların yerinde yemek yedik', + 'activity_type_went_bar' => 'bara gittik', + 'activity_type_ate_at_home' => 'evde yemek yedik', + 'activity_type_picnicked' => 'piknik yaptık', + 'activity_type_ate_restaurant' => 'restoranda yemek yedik', + 'activity_type_went_theater' => 'tiyatroya gittik', + 'activity_type_went_concert' => 'konsere gittik', + 'activity_type_went_play' => 'oyuna gittik', + 'activity_type_went_museum' => 'müzeye gittik', + 'activities_add_activity' => 'Aktivite ekle', + 'activities_add_more_details' => 'Daha fazla ayrıntı ekle', + 'activities_add_emotions' => 'Duygu ekle', + 'activities_add_category' => 'Bir kategori belirt', + 'activities_add_participants_cta' => 'Katılımcıları ekle', + 'activities_item_information' => ':Activity. :date tarihinde oldu', + 'activities_add_title' => '{name} ile ne yaptınız?', + 'activities_summary' => 'Ne yaptığınızı açıklayın', + 'activities_add_pick_activity' => 'Would you like to categorize this activity? You don’t have to, but it will give you statistics later on (optional)', + 'activities_add_date_occured' => 'The activity happened on…', + 'activities_add_participants' => 'Bu aktiviteye {name} dışında kim katıldı? (isteğe bağlı)', + 'activities_add_emotions_title' => 'Bu aktivite sırasında neler hissettiğinizi kaydetmek ister misiniz? (isteğe bağlı)', + 'activities_blank_title' => 'Geçmişte {name} ile ne yaptığınızın ve ne hakkında konuştuğunuzun kaydını tutun', + 'activities_blank_add_activity' => 'Bir aktivite ekle', + 'activities_add_success' => 'Aktivite başarıyla eklendi', + 'activities_add_error' => 'Aktivite eklenirken hata oluştu', + 'activities_update_success' => 'Aktivite başarıyla güncellendi', + 'activities_delete_success' => 'Aktivite başarıyla silindi', + 'activities_who_was_involved' => 'Kimler dahil oldu?', + 'activities_activity' => 'Aktivite Kategorisi', + 'activities_view_activities_report' => 'Aktiviteler raporunu görüntüle', + 'activities_profile_title' => ':name ile sizin aranızdaki aktiviteler raporu', + 'activities_profile_subtitle' => 'Şu ana kadar :name ile toplamda :total_activities, son 12 ayda :activities_last_twelve_months aktiviteyi kayıt altına aldınız.', + 'activities_profile_year_summary_activity_types' => 'İşte :year yılında birlikte gerçekleştirdiğiniz etkinlik türlerinin bir dökümü', + 'activities_profile_year_summary' => 'İşte :year yılında ikinizin yaptıkları', + 'activities_profile_number_occurences' => ':value aktivite', + 'activities_list_participants' => 'Participants ({total}):', + 'activities_list_emotions' => 'Hissedilen duygular:', + 'activities_list_date' => 'Gerçekleşti', + 'activities_list_category' => 'Kategori:', + + // notes + 'notes_create_success' => 'Not başarıyla oluşturuldu', + 'notes_update_success' => 'Not başarıyla kaydedildi', + 'notes_delete_success' => 'Not başarıyla silindi', + 'notes_add_cta' => 'Not ekle', + 'notes_favorite' => 'Favorilere ekle/kaldır', + 'notes_delete_title' => 'Not sil', + 'notes_delete_confirmation' => 'Bu notu silmek istediğinizden emin misiniz? Silme işlemi geri alınamaz', + + // gifts + 'gifts_title' => 'Hediyeler', + 'gifts_add_success' => 'Hediye başarıyla eklendi', + 'gifts_delete_success' => 'Hediye başarıyla silindi', + 'gifts_delete_confirmation' => 'Bu hediyeyi silmek istediğinizden emin misiniz?', + 'gifts_add_gift' => 'Hediye ekle', + 'gifts_link' => 'Link', + 'gifts_for' => '{name} için', + 'gifts_delete_cta' => 'Sil', + 'gifts_add_title' => ':name için hediye yönetimi', + 'gifts_add_gift_idea' => 'Hediye Fikri', + 'gifts_add_gift_already_offered' => 'Hediye önerildi', + 'gifts_add_gift_received' => 'Hediye alındı', + 'gifts_add_gift_title' => 'Bu hediye nedir?', + 'gifts_add_gift_name' => 'Hediye adı', + 'gifts_add_link' => 'Web sayfası linki (isteğe bağlı)', + 'gifts_add_value' => 'Değer (isteğe bağlı)', + 'gifts_add_comment' => 'Yorum (isteğe bağlı)', + 'gifts_add_recipient' => 'Alıcı (isteğe bağlı)', + 'gifts_add_recipient_field' => 'Alıcı', + 'gifts_add_photo' => 'Fotoğraf (isteğe bağlı)', + 'gifts_add_photo_title' => 'Bu hediye için bir fotoğraf ekle', + 'gifts_add_someone' => 'Bu hediye özellikle {name}\'nin ailesinden birisi için', + 'gifts_delete_title' => 'Hediye sil', + 'gifts_ideas' => 'Hediye fikirleri', + 'gifts_offered' => 'Önerilen hediyeler', + 'gifts_offered_as_an_idea' => 'Fikir olarak işaretle', + 'gifts_received' => 'Alınan hediyeler', + 'gifts_view_comment' => 'Yorumu görüntüle', + 'gifts_mark_offered' => 'Önerildi olarak işaretle', + 'gifts_update_success' => 'Hediye başarıyla güncellendi', + 'gifts_add_date' => 'Date (optional)', + + // debts + 'debt_delete_confirmation' => 'Bu borcu silmek istediğinizden emin misiniz?', + 'debt_delete_success' => 'Borç başarıyla silindi', + 'debt_add_success' => 'Borç başarıyla eklendi', + 'debt_title' => 'Borçlar', + 'debt_add_cta' => 'Borç ekle', + 'debt_you_owe' => ':amount borcunuz var', + 'debt_they_owe' => ':name\'in size :amount borcu var', + 'debt_add_title' => 'Borç yönetimi', + 'debt_add_you_owe' => ':name\'e borcunuz var', + 'debt_add_they_owe' => ':name\'in size borcu var', + 'debt_add_amount' => 'toplamı', + 'debt_add_reason' => 'şu sebepten dolayı (isteğe bağlı)', + 'debt_add_add_cta' => 'Borç ekle', + 'debt_edit_update_cta' => 'Borcu güncelle', + 'debt_edit_success' => 'Borç başarıyla güncellendi', + 'debts_blank_title' => ':name\'e olan borçlarınızı ya da :name\'in size olan borçlarını yönetin', + + // tags + 'tag_edit' => 'Etiketi düzenle', + 'tag_add' => 'Etiket ekle', + 'tag_add_search' => 'Etiket ekle veya ara', + 'tag_no_tags' => 'Henüz etiket yok', + + // Introductions + 'introductions_sidebar_title' => 'Nasıl tanıştınız', + 'introductions_blank_cta' => ':name ile nasıl tanıştığınızı belirtin', + 'introductions_title_edit' => ':name ile nasıl tanıştınız?', + 'introductions_additional_info' => 'Nasıl ve nerede tanıştığınızı açıklayın', + 'introductions_edit_met_through' => 'Birisi sizi bu kişiyle tanıştırdı mı?', + 'introductions_no_met_through' => 'Hiç kimse', + 'introductions_first_met_date' => 'Tanıştığınız tarih', + 'introductions_no_first_met_date' => 'Tanıştığımız tarihi bilmiyorum', + 'introductions_first_met_date_known' => 'Tanıştığımız tarih bu', + 'introductions_add_reminder' => 'Bu olayı yıl dönümünde kutlamak için bir hatırlatma ekleyin', + 'introductions_update_success' => 'Bu kişiyle nasıl tanıştığınıza ilişkin bilgileri başarıyla güncellediniz', + 'introductions_met_through' => ':name aracılığıyla tanıştık', + 'introductions_met_date' => ':date tarihinde tanıştık', + 'introductions_reminder_title' => 'İlk tanıştığınız günün yıl dönümü', + + // Deceased + 'deceased_reminder_title' => ':name\'in ölümünün yıl dönümü', + 'deceased_mark_person_deceased' => 'Mark this as deceased', + 'deceased_know_date' => 'I know the date that this person died', + 'deceased_add_reminder' => 'Bu tarih için bir hatırlatıcı ekleyin', + 'deceased_label' => 'Ölmüş', + 'deceased_date_label' => 'Ölüm tarihi', + 'deceased_label_with_date' => ':date tarihinde öldü', + 'deceased_age' => 'Ölüm yaşı', + + // Contact information + 'contact_info_title' => 'İletişim bilgileri', + 'contact_info_form_content' => 'İçerik', + 'contact_info_form_contact_type' => 'Kişi türü', + 'contact_info_form_personalize' => 'Kişiselleştir', + 'contact_info_address' => 'Yaşadığı yer', + + // Addresses + 'contact_address_title' => 'Adresler', + 'contact_address_form_name' => 'Etiket (isteğe bağlı)', + 'contact_address_form_street' => 'Sokak (isteğe bağlı)', + 'contact_address_form_city' => 'Şehir (isteğe bağlı)', + 'contact_address_form_province' => 'İl (isteğe bağlı)', + 'contact_address_form_postal_code' => 'Posta kodu (isteğe bağlı)', + 'contact_address_form_country' => 'Ülke (isteğe bağlı)', + 'contact_address_form_latitude' => 'Eylem (sadece sayı) (isteğe bağlı)', + 'contact_address_form_longitude' => 'Boylam (sadece sayı) (isteğe bağlı)', + + // Pets + 'pets_kind' => 'Evcil hayvan türü', + 'pets_name' => 'İsim (isteğe bağlı)', + 'pets_create_success' => 'Evcil hayvan başarıyla eklendi', + 'pets_update_success' => 'Evcil hayvan güncellendi', + 'pets_delete_success' => 'Evcil hayvan silindi', + 'pets_title' => 'Evcil hayvanlar', + 'pets_reptile' => 'Sürüngen', + 'pets_bird' => 'Kuş', + 'pets_cat' => 'Kedi', + 'pets_dog' => 'Köpek', + 'pets_fish' => 'Balık', + 'pets_hamster' => 'Hamster', + 'pets_horse' => 'At', + 'pets_rabbit' => 'Tavşan', + 'pets_rat' => 'Fare', + 'pets_small_animal' => 'Küçük hayvan', + 'pets_other' => 'Diğer', + + // life events + 'life_event_list_tab_life_events' => 'Yaşam olayları', + 'life_event_list_tab_other' => 'Notes, reminders, …', + 'life_event_list_title' => 'Yaşam olayları', + 'life_event_blank' => 'Gelecekteki referansınız için {name}\'in hayatına ne olacağını kaydedin.', + 'life_event_list_cta' => 'Yaşam olayı ekleyin', + 'life_event_create_category' => 'Tüm kategoriler', + 'life_event_create_life_event' => 'Yaşam olayı ekleyin', + 'life_event_create_default_title' => 'Başlık (isteğe bağlı)', + 'life_event_create_default_story' => 'Hikaye (isteğe bağlı)', + 'life_event_create_date' => 'You do not need to indicate a month or a day – only the year is mandatory.', + 'life_event_create_default_description' => 'Bildikleriniz hakkında bilgi ekleyin', + 'life_event_create_add_yearly_reminder' => 'Bu etkinlik için yıllık hatırlatıcı ekleyin', + 'life_event_create_success' => 'Yaşam olayı eklendi', + 'life_event_delete_title' => 'Bir yaşam olayını silin', + 'life_event_delete_description' => 'Bu yaşam olayını silmek istediğinizden emin misiniz? Silme işlemi geri alınamaz.', + 'life_event_delete_success' => 'Yaşam olayı silindi', + 'life_event_date_it_happened' => 'Meydana geldiği tarih', + 'life_event_category_work_education' => 'İş & eğitim', + 'life_event_category_family_relationships' => 'Aile & ilişkiler', + 'life_event_category_home_living' => 'Ev & yaşam', + 'life_event_category_health_wellness' => 'Sağlık & sıhhat', + 'life_event_category_travel_experiences' => 'Seyahat & deneyimler', + 'life_event_sentence_new_job' => 'Yeni bir iş başlatmak', + 'life_event_sentence_retirement' => 'Emekli olmak', + 'life_event_sentence_new_school' => 'Okula başlamak', + 'life_event_sentence_study_abroad' => 'Yurtdışında eğitim görmek', + 'life_event_sentence_volunteer_work' => 'Gönüllü çalışmaya başlamak', + 'life_event_sentence_published_book_or_paper' => 'Bir makale yayınlamak', + 'life_event_sentence_military_service' => 'Askerliğe başlamak', + 'life_event_sentence_new_relationship' => 'Bir ilişkiye başlamak', + 'life_event_sentence_engagement' => 'Nişanlanmak', + 'life_event_sentence_marriage' => 'Evlenmek', + 'life_event_sentence_anniversary' => 'Yıldönümü', + 'life_event_sentence_expecting_a_baby' => 'Bebek beklemek', + 'life_event_sentence_new_child' => 'Çocuk sahibi olmak', + 'life_event_sentence_new_family_member' => 'Bir aile üyesi eklemek', + 'life_event_sentence_new_pet' => 'Evcil hayvan almak', + 'life_event_sentence_end_of_relationship' => 'Bir ilişkiyi sonlandırmak', + 'life_event_sentence_loss_of_a_loved_one' => 'Sevdiği birini kaybetmek', + 'life_event_sentence_moved' => 'Taşınmak', + 'life_event_sentence_bought_a_home' => 'Ev satın almak', + 'life_event_sentence_home_improvement' => 'Evde iyileştirme yapmak', + 'life_event_sentence_holidays' => 'Tatile gitmek', + 'life_event_sentence_new_vehicle' => 'Yeni araç almak', + 'life_event_sentence_new_roommate' => 'Oda arkadaşı edinmek', + 'life_event_sentence_overcame_an_illness' => 'Bir hastalığı yenmek', + 'life_event_sentence_quit_a_habit' => 'Bir alışkanlığı bırakmak', + 'life_event_sentence_new_eating_habits' => 'Yeni yeme alışkanlıkları başlatmak', + 'life_event_sentence_weight_loss' => 'Kilo vermek', + 'life_event_sentence_wear_glass_or_contact' => 'Gözlük veya lens takmaya başlamak', + 'life_event_sentence_broken_bone' => 'Bir kemiği kırmak', + 'life_event_sentence_removed_braces' => 'Diş tellerini kaldırmak', + 'life_event_sentence_surgery' => 'El cerrahisi', + 'life_event_sentence_dentist' => 'Dişçiye gitmek', + 'life_event_sentence_new_sport' => 'Bir spora başlamak', + 'life_event_sentence_new_hobby' => 'Bir hobiye başlamak', + 'life_event_sentence_new_instrument' => 'Yeni bir çalgı aleti öğrenmek', + 'life_event_sentence_new_language' => 'Yeni bir dil öğrenmek', + 'life_event_sentence_tattoo_or_piercing' => 'Dövme veya piercing yaptırmak', + 'life_event_sentence_new_license' => 'Bir lisans sahibi olmak', + 'life_event_sentence_travel' => 'Seyahat etmek', + 'life_event_sentence_achievement_or_award' => 'Bir başarı veya ödül almak', + 'life_event_sentence_changed_beliefs' => 'İnançlarını değiştirmek', + 'life_event_sentence_first_word' => 'İlk defa konuşmak', + 'life_event_sentence_first_kiss' => 'İlk defa öpüşmek', + + // documents + 'document_list_title' => 'Belgeler', + 'document_list_cta' => 'Belge yükleyin', + 'document_list_blank_desc' => 'Burada, bu kişiyle ilgili belgeleri saklayabilirsiniz.', + 'document_upload_zone_cta' => 'Dosya Yükle', + 'document_upload_zone_progress' => 'Uploading the document…', + 'document_upload_zone_error' => 'Dosya karşıya yüklenirken bir hata oluştu. Lütfen aşağıdan tekrar deneyin.', + + // Photos + 'photo_title' => 'Fotoğraflar', + 'photo_list_title' => 'İlgili fotoğraflar', + 'photo_list_cta' => 'Fotoğraf yükle', + 'photo_list_blank_desc' => 'Bu kişiyle ilgili görüntüleri saklayabilirsiniz. Şimdi bir tane yükle!', + 'photo_upload_zone_cta' => 'Bir fotoğraf yükle', + 'photo_current_profile_pic' => 'Mevcut profil resmi', + 'photo_make_profile_pic' => 'Profil resmi yapın', + 'photo_delete' => 'Fotoğrafı sil', + 'photo_next' => 'Next photo ❯', + 'photo_previous' => '❮ Previous photo', + + // Avatars + 'avatar_change_title' => 'Avatarınızı değiştirin', + 'avatar_question' => 'Hangi avatarı kullanmak istersiniz?', + 'avatar_default_avatar' => 'Varsayılan avatar', + 'avatar_adorable_avatar' => 'Adorable avatarı', + 'avatar_gravatar' => 'Bu kişinin e-posta adresiyle ilişkili Gravatar.Gravatar, kullanıcıların e-posta adreslerini fotoğraflarla ilişkilendirmelerini sağlayan global bir sistemdir.', + 'avatar_current' => 'Mevcut avatarı kullanın', + 'avatar_photo' => 'Yüklediğiniz bir fotoğraftan', + 'avatar_crop_new_avatar_photo' => 'Yeni profil resmini kırp', + + // emotions + 'emotion_this_made_me_feel' => 'Bu size … hissettirdi', + + // logs + 'auditlogs_link' => 'Geçmiş', + 'auditlogs_title' => 'Bu hesaba ait tüm kayıtlar', + 'auditlogs_breadcrumb' => 'Geçmiş', + 'auditlogs_author' => ':date tarihinde :name adına göre', + + // contact field label + 'contact_field_label_home' => 'Ev', + 'contact_field_label_work' => 'Iş', + 'contact_field_label_cell' => 'Cep Telefonu', + 'contact_field_label_fax' => 'Faks', + 'contact_field_label_pager' => 'Çağrı Cihazı', + 'contact_field_label_main' => 'Ana', + 'contact_field_label_other' => 'Diğer', + 'contact_field_label_personal' => 'Kişisel', +]; diff --git a/resources/lang/tr/reminder.php b/resources/lang/tr/reminder.php new file mode 100644 index 0000000..44d4bb0 --- /dev/null +++ b/resources/lang/tr/reminder.php @@ -0,0 +1,16 @@ + 'Mutlu yıllar dile:', + 'type_phone_call' => 'Çağrı', + 'type_lunch' => 'Öğle yemeği', + 'type_hangout' => 'Birlikte takıl', + 'type_email' => 'E-posta', + 'type_birthday_kid' => 'Wish happy birthday to the child of', +]; diff --git a/resources/lang/tr/settings.php b/resources/lang/tr/settings.php new file mode 100644 index 0000000..fee1398 --- /dev/null +++ b/resources/lang/tr/settings.php @@ -0,0 +1,557 @@ + 'Hesap ayarları', + 'sidebar_personalization' => 'Kişiselleştirme', + 'sidebar_settings_storage' => 'Saklama alanı', + 'sidebar_settings_export' => 'Verileri dışa aktar', + 'sidebar_settings_users' => 'Kullanıcılar', + 'sidebar_settings_subscriptions' => 'Abonelik', + 'sidebar_settings_import' => 'Verileri içe aktar', + 'sidebar_settings_tags' => 'Etiket Yönetimi', + 'sidebar_settings_api' => 'API', + 'sidebar_settings_dav' => 'DAV Kaynakları', + 'sidebar_settings_security' => 'Güvenlik', + 'sidebar_settings_auditlogs' => 'İnceleme günlüğü', + + 'title_general' => 'Genel Bilgiler', + 'title_i18n' => 'Uluslararası ayarlar', + 'title_layout' => 'Görünüm', + + 'me_title' => 'Bir kişi olarak ben', + 'me_help' => 'Bu hesap Monica\'da sizi temsil eden kişidir', + 'me_select' => 'Bir kişi seçin', + 'me_no_contact' => 'Henüz bir kişi seçilmedi.', + 'me_select_click' => 'Bir kişi seçmek için buraya tıklayınız.', + 'me_remove_contact' => 'İlişkiyi Kaldır', + 'me_choose' => 'Kendinizi seçin', + 'me_choose_placeholder' => 'Kendinizi seçin', + + 'export_title' => 'Hesap bilgilerini dışarı aktar', + 'export_be_patient' => 'Click the button to start the export. It might take several minutes to process the export – please be patient and do not spam the button.', + 'export_title_sql' => 'Export to SQL', + 'export_sql_explanation' => 'Exporting your data in SQL format allows you to take your data and import it to your own Monica instance. This is only valuable if you do have your own server.', + 'export_sql_cta' => 'Export to SQL', + 'export_sql_link_instructions' => 'Note: read the instructions to learn more about importing this file to your instance.', + 'export_title_json' => 'Export to Json', + 'export_submitted' => 'Your export has been submitted, it will be available in a few moment…', + 'export_json_explanation' => 'Exporting your data in Json format for backup.', + 'export_json_beta' => 'Json export is in preview mode. Tell us what you think about it:', + 'export_json_cta' => 'Export to Json', + 'export_header_type' => 'Type', + 'export_header_timestamp' => 'Creation date', + 'export_header_status' => 'Status', + 'export_header_actions' => 'Actions', + 'export_last_title' => 'Last exports', + 'export_empty_title' => 'No exports yet', + 'export_type_json' => 'Json export', + 'export_type_sql' => 'SQL export', + 'export_status_todo' => 'Submitted', + 'export_status_doing' => 'Doing', + 'export_status_done' => 'Done', + 'export_status_failed' => 'Failed', + 'export_not_done' => 'Download impossible, this export is not done yet.', + + 'firstname' => 'Ad', + 'lastname' => 'Soyad', + 'name_order' => 'İsim gösterimi', + 'name_order_firstname_lastname' => ' – John Doe', + 'name_order_lastname_firstname' => ' – Doe John', + 'name_order_firstname_lastname_nickname' => ' () – John Doe (Rambo)', + 'name_order_firstname_nickname_lastname' => ' () – John (Rambo) Doe', + 'name_order_lastname_firstname_nickname' => ' () – Doe John (Rambo)', + 'name_order_lastname_nickname_firstname' => ' () – Doe (Rambo) John', + 'name_order_nickname_firstname_lastname' => ' ( ) – Rambo (John Doe)', + 'name_order_nickname_lastname_firstname' => ' ( ) – Rambo (Doe John)', + 'name_order_nickname' => ' – Rambo', + 'currency' => 'Para Birimi', + 'name' => 'Adınız: :name', + 'email' => 'E-posta adresi', + 'email_placeholder' => 'E-posta girin', + 'email_help' => 'This is the email used to login, and this is where Monica will send your reminders.', + 'timezone' => 'Zaman Dilimi', + 'temperature_scale' => 'Sıcaklık ölçeği', + 'temperature_scale_fahrenheit' => 'Fahrenheit (°F)', + 'temperature_scale_celsius' => 'Celsius (°C)', + 'layout' => 'Görünüm', + 'layout_small' => 'En fazla 1200 piksel genişliğinde', + 'layout_big' => 'Tarayıcının tam genişliği', + 'save' => 'Tercihleri güncelle', + 'delete_title' => 'Hesabınızı silin', + 'delete_desc' => 'Do you wish to delete your account? Deletion is permanent and all of your data will be erased permanently. If you have a subscription, it will be cancelled immediately.', + 'delete_other_desc' => 'Your data in the main database will be deleted immediately. As described in our privacy policy, we carry out securely encrypted backups of the database every day. These backups are kept for 30 days after which they are completely deleted. We cannot delete specific data from the backups we hold any earlier than this. All of your data will be completely deleted no later than 31 days after your account’s deletion.', + 'reset_desc' => 'Do you wish to reset your account? This will remove all your contacts, and all of the data associated with them. Your account will not be deleted.', + 'reset_title' => 'Hesabınızı sıfırlayın', + 'reset_cta' => 'Hesabı sıfırla', + 'reset_notice' => 'Are you sure to reset your account? This is permanent and cannot be undone.', + 'reset_success' => 'Your account has been reset successfully.', + 'delete_notice' => 'Are you sure you want to delete your account? This is permanent and cannot be undone. All of your data will be deleted and will not be recoverable.', + 'delete_cta' => 'Hesabı sil', + 'settings_success' => 'Tercihler güncellendi!', + 'locale' => 'Uygulamada kullanılacak dil', + 'locale_help' => 'Monica\'yı çevirmeye yardım etmek veya yeni bir dil eklemek ister misiniz? Lütfen daha fazla bilgi için bu linki takip edin.', + 'locale_ar' => 'Arapça', + 'locale_cs' => 'Çekçe', + 'locale_de' => 'Almanca', + 'locale_el' => 'Greek', + 'locale_en' => 'İngilizce', + 'locale_en-GB' => 'İngilizce (Birleşik Krallık)', + 'locale_es' => 'İspanyolca', + 'locale_fr' => 'Fransızca', + 'locale_he' => 'İbranice', + 'locale_hr' => 'Hırvatca', + 'locale_id' => 'Indonesian', + 'locale_it' => 'İtalyanca', + 'locale_ja' => 'Japonca', + 'locale_nl' => 'Flemenkçe', + 'locale_pt' => 'Portekizce', + 'locale_pt-BR' => 'Portuguese, Brazil', + 'locale_ru' => 'Rusça', + 'locale_sv' => 'İsveççe', + 'locale_vi' => 'Vietnamese', + 'locale_zh' => 'Çince (Basitleştirilmiş)', + 'locale_zh-TW' => 'Geleneksel Çince', + 'locale_tr' => 'Türkçe', + + 'security_title' => 'Güvenlik', + 'security_help' => 'Hesabınız için güvenlik unsurlarını değiştirin.', + 'password_change' => 'Change your password', + 'password_current' => 'Geçerli şifre', + 'password_current_placeholder' => 'Geçerli şifrenizi giriniz', + 'password_new1' => 'Yeni şifre', + 'password_new1_placeholder' => 'Enter your new password', + 'password_new2' => 'Confirm your new password', + 'password_new2_placeholder' => 'Retype your new password', + 'password_btn' => 'Şifreyi Değiştir', + '2fa_title' => 'İki Aşamalı Kimlik Doğrulaması', + '2fa_otp_title' => 'İki Aşamalı Kimlik Doğrulaması mobil uygulama', + '2fa_enable_title' => 'İki aşamalı kimlik doğrulamasını etkinleştir', + '2fa_enable_description' => 'Enable Two Factor Authentication to increase the security of your account.', + '2fa_enable_otp' => 'Open up your Two Factor Authentication mobile app and scan the following QR barcode:', + '2fa_enable_otp_help' => 'If your Two Factor Authentication mobile app does not support QR barcodes, enter in the following code:', + '2fa_enable_otp_validate' => 'Please validate the new device you’ve just set up:', + '2fa_enable_success' => 'İki aşamalı kimlik doğrulaması etkinleştirildi', + '2fa_enable_error' => 'İki Adımlı Kimlik Doğrulamayı etkinleştirmeye çalışırken hata oluştu', + '2fa_enable_error_already_set' => 'İki aşamalı kimlik doğrulaması zaten etkinleştirildi', + '2fa_disable_title' => 'İki Aşamalı Kimlik Doğrulamasını devre dışı bırak', + '2fa_disable_description' => 'Disable Two Factor Authentication for your account. Be careful, your account will be much less secure!', + '2fa_disable_success' => 'İki Adımlı Kimlik Doğrulaması devre dışı', + '2fa_disable_error' => 'İki Adımlı Kimlik Doğrulamayı devre dışı bırakmaya çalışırken hata oluştu', + + 'webauthn_title' => 'Güvenlik anahtarı — WebAuthn protokolü', + 'webauthn_enable_description' => 'Yeni bir güvenlik anahtarı ekleyin', + 'webauthn_key_name_help' => 'Anahtarınıza bir isim verin.', + 'webauthn_key_name' => 'Anahtar adı:', + 'webauthn_success' => 'Anahtarınız algılandı ve doğrulandı.', + 'webauthn_last_use' => 'Son kullanım: {timestamp}', + 'webauthn_delete_confirmation' => 'Bu anahtarı silmek istediğinizden emin misiniz?', + 'webauthn_delete_success' => 'Anahtar silindi', + 'webauthn_insertKey' => 'Güvenlik anahtarınızı girin.', + 'webauthn_buttonAdvise' => 'Güvenlik anahtarında bir düğme varsa, ona basın.', + 'webauthn_noButtonAdvise' => 'Eğer yoksa, çıkarın ve tekrar takın.', + 'webauthn_not_supported' => 'Tarayıcınız şu anda WebAuthn\'u desteklememektedir.', + 'webauthn_not_secured' => 'WebAuthn yalnızca güvenli bağlantıları desteklemektedir. Lütfen bu sayfayı https şeması ile yükleyin.', + 'webauthn_error_already_used' => 'Bu anahtar zaten kayıtlı. Tekrar kaydetmek gerekli değil.', + 'webauthn_error_not_allowed' => 'İşlem zaman aşımına uğradı veya izin verilmedi.', + + 'recovery_title' => 'Kurtarma kodları', + 'recovery_show' => 'Kurtarma kodlarını göster', + 'recovery_copy_help' => 'Kodları panonuza kopyalayın', + 'recovery_help_intro' => 'Bunlar sizin kurtarma kodlarınız:', + 'recovery_help_information' => 'Her kurtarma kodunu bir kez kullanabilirsiniz.', + 'recovery_clipboard' => 'Codes copied to the clipboard.', + 'recovery_generate' => 'Generate new codes…', + 'recovery_generate_help' => 'Generating new codes will invalidate previously generated codes.', + 'recovery_already_used_help' => 'This code has already been used.', + + 'users_list_title' => 'Hesabınıza erişim hakkı olan kullanıcılar', + 'users_list_add_user' => 'Yeni kullanıcı davet et', + 'users_list_you' => 'Sizin Listeniz', + 'users_list_invitations_title' => 'Bekleyen davetler', + 'users_list_invitations_explanation' => 'Birlikte çalışmak için Monica\'ya katılmaya davet ettiğiniz kişiler aşağıdadır.', + 'users_list_invitations_invited_by' => 'davet eden :name', + 'users_list_invitations_sent_date' => ':date tarihinde gönderildi', + 'users_blank_title' => 'Bu hesaba erişim sağlayan tek kişi sensin.', + 'users_blank_add_title' => 'Birini davet etmek ister misin?', + 'users_blank_description' => 'Bu kullanıcı senin sahil olduğun erişime sahip olacak, ve kişi bilgilerini ekleme, düzenleme veya silme yetkisine sahip olacak.', + 'users_blank_cta' => 'Birini davet et', + 'users_add_title' => 'Invite a new user to your account by email', + 'users_add_description' => 'This person will have the same access as you do, including inviting or deleting other users, including you. Make sure you trust this person before giving them access.', + 'users_add_email_field' => 'Davet etmek istediğiniz kişinin e-posta adresini girin', + 'users_add_confirmation' => 'I confirm that I want to invite this user to my account. I understand that this person will have access to ALL of my data and see exactly what I see.', + 'users_add_cta' => 'E-postayla kullanıcı davet et', + 'users_accept_title' => 'Daveti kabul et ve yeni bir hesap oluştur', + 'users_error_please_confirm' => 'Lütfen davet işlemine devam etmeden önce bu kullanıcıyı davet etmek istediğinizi onaylayın', + 'users_error_email_already_taken' => 'Bu e-posta adresi zaten alınmış. Lütfen başka bir tane seçin', + 'users_error_already_invited' => 'Bu kullanıcıyı zaten davet ettiniz. Lütfen başka bir e-posta adresi seçin.', + 'users_error_email_not_similar' => 'Bu, sizi davet eden kişinin e-posta adresi değil.', + 'users_invitation_deleted_confirmation_message' => 'Davet başarıyla silindi', + 'users_invitations_delete_confirmation' => 'Bu daveti silmek istediğinizden emin misiniz?', + 'users_list_delete_confirmation' => 'Bu kullanıcıyı hesabınızdan silmek istediğinizden emin misiniz?', + 'users_invitation_need_subscription' => 'Daha fazla kullanıcı eklemek için abonelik gerekmektedir.', + + 'subscriptions_account_current_plan' => 'Geçerli planınız', + 'subscriptions_account_current_legacy' => 'Current plan, not selectable anymore:', + 'subscriptions_account_current_paid_plan' => ':name planındasınız. Abone olduğunuz için çok teşekkürler.', + + 'subscriptions_account_next_billing_title' => 'Next bill', + 'subscriptions_account_next_billing' => 'Aboneliğiniz :date tarihinde otomatik olarak yenilenecektir.', + 'subscriptions_account_bill_monthly' => 'We’ll bill you :price for another month.', + 'subscriptions_account_bill_annual' => 'We’ll bill you :price for another year.', + 'subscriptions_account_change' => 'Change plan', + + 'subscriptions_account_cancel_title' => 'Cancel subscription', + 'subscriptions_account_cancel_action' => 'Cancel subscription', + 'subscriptions_account_cancel' => 'İstediğiniz zaman aboneliğinizi iptal edebilirsiniz.', + 'subscriptions_account_free_plan' => 'Ücretsiz plandasınız.', + 'subscriptions_account_free_plan_upgrade' => 'Hesabınızı :name planına yükseltebilirsiniz, aylık $:price mal olacaktır. Avantajları şunlardır:', + 'subscriptions_account_free_plan_benefits_users' => 'Sınırsız sayıda kullanıcı', + 'subscriptions_account_free_plan_benefits_reminders' => 'E-posta yoluyla hatırlatmalar', + 'subscriptions_account_free_plan_benefits_import_data_vcard' => 'Kişilerinizi vCard ile içe aktarın', + 'subscriptions_account_free_plan_benefits_support' => 'Support the project in the long run, so we can introduce more great features.', + 'subscriptions_account_upgrade' => 'Hesabınızı yükseltin', + 'subscriptions_account_upgrade_title' => 'Monica\'yı bugün yükseltin ve daha anlamlı ilişkilere sahip olun.', + 'subscriptions_account_upgrade_choice' => 'Aşağıdaki planlardan birini seçin ve Monica\'larını yükselten :customers üzerinde kişinin arasına katılın.', + 'subscriptions_account_update_title' => 'Update Monica subscription', + 'subscriptions_account_update_description' => 'You can change your subscription’s frequency here.', + 'subscriptions_account_update_information' => 'You will be billed immediately for the new amount. Your subscription will extend to the new period, depending on your choice.', + 'subscriptions_account_invoices' => 'Faturalar', + 'subscriptions_account_invoices_download' => 'İndir', + 'subscriptions_account_invoices_subscription' => ':startDate ile :endDate arasında abonelik', + 'subscriptions_account_payment' => 'Size en uygun ödeme seçeneği hangisi?', + 'subscriptions_account_confirm_payment' => 'Ödemeniz şu anda tamamlanmamış durumda, lütfen ödemenizi doğrulayın.', + 'subscriptions_downgrade_title' => 'Hesabınızı ücretsiz plana düşürün', + 'subscriptions_downgrade_limitations' => 'Ücretsiz planın limitleri vardır. Paketi düşürebilmek için aşağıdaki kontrol listesini onaylanman gerekiyor:', + 'subscriptions_downgrade_rule_users' => 'Hesabında sadece 1 kişi bulundurmalısın', + 'subscriptions_downgrade_rule_users_constraint' => 'Şu an hesabında 1 kişi var.|Şu an hesabında :count kişi var.', + 'subscriptions_downgrade_rule_invitations' => 'You must not have any pending invitations', + 'subscriptions_downgrade_rule_invitations_constraint' => 'You currently have 1 pending invitation.|You currently have :count pending invitations.', + 'subscriptions_downgrade_rule_contacts' => ':number adetten fazla aktif kişiye sahip olmamalısınız', + 'subscriptions_downgrade_rule_contacts_constraint' => 'Şu anda :count adet kişiniz bulunmaktadır.', + 'subscriptions_downgrade_rule_contacts_archive' => 'We can also archive all your contacts for you – that would clear this rule and let you proceed with your account’s downgrade process.', + 'subscriptions_downgrade_cta' => 'Düşür', + 'subscriptions_downgrade_success' => 'Ücretsiz plana geri döndün!', + 'subscriptions_downgrade_thanks' => 'Thanks so much for trying the paid plan. We keep adding new features on Monica all the time – so you might want to come back in the future to see if you might be interested in taking a subscription again.', + 'subscriptions_back' => 'Ayarlara geri dön', + 'subscriptions_upgrade_title' => 'Hesabınızı yükseltin', + 'subscriptions_upgrade_choose' => ':plan planını seçtin.', + 'subscriptions_upgrade_infos' => 'Daha mutlu olamazdık. Ödeme bilgilerinizi aşağıya girin.', + 'subscriptions_upgrade_name' => 'Kart üzerindeki isim', + 'subscriptions_upgrade_zip' => 'Posta Kodu', + 'subscriptions_upgrade_credit' => 'Kredi kartı veya banka kartı', + 'subscriptions_upgrade_submit' => '{amount} öde', + 'subscriptions_upgrade_charge' => 'We’ll charge your card :price now. The next charge will be on :date. If you ever change your mind, you can cancel at any time, no questions asked.', + 'subscriptions_upgrade_charge_handled' => 'Ödeme Stripe tarafından gerçekleştirilmektedir. Hiçbir kart bilgisi sunucumuza ulaşmamaktadır.', + 'subscriptions_upgrade_success' => 'Teşekkürler! Artık bir abonesin.', + 'subscriptions_upgrade_thanks' => 'Dünyayı daha iyi bir yer yapmaya çalışan insanların topluluğuna hoş geldiniz.', + + 'subscriptions_payment_confirm_title' => ':amount miktarındaki ödemenizi onaylayın', + 'subscriptions_payment_confirm_information' => 'Ödemenizi işleme koymak için ek onay gerekmektedir. Lütfen aşağıda ödeme ayrıntılarınızı doldurarak ödemenizi onaylayın.', + 'subscriptions_payment_succeeded_title' => 'Ödeme Başarılı', + 'subscriptions_payment_succeeded' => 'Bu ödeme zaten başarıyla onaylandı.', + 'subscriptions_payment_cancelled_title' => 'Ödeme İptal Edildi', + 'subscriptions_payment_cancelled' => 'Bu ödeme iptal edildi.', + 'subscriptions_payment_error_name' => 'Lütfen isminizi girin.', + 'subscriptions_payment_success' => 'Ödeme başarılı oldu.', + + 'subscriptions_pdf_title' => ':name aylık aboneliğiniz', + 'subscriptions_plan_frequency_year' => ':amount / year', + 'subscriptions_plan_frequency_month' => ':amount / month', + 'subscriptions_plan_choose' => 'Bu planı seç', + 'subscriptions_plan_year_title' => 'Yıllık ödeme', + 'subscriptions_plan_year_bonus' => 'Bir yıl boyunca gönül rahatlığı', + 'subscriptions_plan_month_title' => 'Aylık ödeme', + 'subscriptions_plan_month_bonus' => 'İstediğin zaman iptal et', + 'subscriptions_plan_include1' => 'Yükseltmen ile birlikte gelen ayrıcalıklar:', + 'subscriptions_plan_include2' => 'Sınırsız sayıda kişiler • Sınırsız sayıda kullanıcı • E-postayla bilgilendirme • vCard ile içe aktarım • Kişiler sayfasını kişiselleştirebilme', + 'subscriptions_plan_include3' => 'Kârların %100\'ü bu harika açık kaynaklı projenin geliştirilmesine gidiyor.', + 'subscriptions_help_title' => 'Merak edebileceğin bazı ek detaylar', + 'subscriptions_help_opensource_title' => 'Açık kaynaklı proje de nedir?', + 'subscriptions_help_opensource_desc' => 'Monica is an open source project. This means it is built by a community who wants to build a great tool for the greater good. Being open source means the code is publicly available on GitHub, and everyone can inspect it, modify it or enhance it. All the money we raise is dedicated to building better features, paying for more powerful servers, and paying other costs. Thanks for your help. We couldn’t do it without you.', + 'subscriptions_help_limits_title' => 'Is there a limit to the number of contacts we can have on the free plan?', + 'subscriptions_help_limits_plan' => 'Evet. Ücretsiz plan :number adet kişiyi yönetmenize izin vermektedir.', + 'subscriptions_help_discounts_title' => 'Kâr amacı gütmeyen kurumlara ve eğitim hizmetlerine indiriminiz var mı?', + 'subscriptions_help_discounts_desc' => 'Evet var! Monica öğrencilere ve kâr amacı gütmeyen kuruluşlara ücretsizdir. Sadece durumunuz ile ilgili bir kanıt ile birlikte destek ile iletişime geçin ve hesabınıza bu özel duruma uygun hale getirelim.', + 'subscriptions_help_change_title' => 'Ya fikrimi değiştirirsem?', + 'subscriptions_help_change_desc' => 'You can cancel anytime, no questions asked, and all by yourself – no need to contact support. However, you will not be refunded for the current period.', + + 'stripe_error_card' => 'Kartın reddedildi. Reddedilme mesajı: :message', + 'stripe_error_api_connection' => 'Stripe ile ağ iletişimi başarısız oldu. Daha sonra tekrar deneyin.', + 'stripe_error_rate_limit' => 'Şu anda Stripe için çok fazla istek var. Daha sonra tekrar deneyin.', + 'stripe_error_invalid_request' => 'Parametreler geçersiz. Daha sonra tekrar deneyin.', + 'stripe_error_authentication' => 'Stripe ile yanlış kimlik doğrulaması', + + 'import_title' => 'Hesabınızdaki kişileri içe aktarın', + 'import_cta' => 'Kişileri karşıya yükle', + 'import_stat' => 'Şimdiye kadar :number dosyayı içe aktardınız.', + 'import_result_stat' => 'vCard :total_contacts kişi ile karşıya yüklendi (:total_imported içe aktarıldı, :total_skipped atlandı)', + 'import_view_report' => 'Raporu görüntüle', + 'import_in_progress' => 'İçe aktarma devam ediyor. Sayfayı bir dakika içinde yeniden yükleyin.', + 'import_upload_title' => 'Kişilerinizi bir vCard dosyasından içe aktarın', + 'import_upload_rules_desc' => 'Ancak bazı kurallarımız var:', + 'import_upload_rule_format' => '.vcard ve .vcf dosyalarını destekliyoruz.', + 'import_upload_rule_vcard' => 'We support the vCard 3.0 format, which is the default format for macOS’s Contacts.app and Google Contacts.', + 'import_upload_rule_instructions' => 'Export instructions for macOS Contacts.app and Google Contacts.', + 'import_upload_rule_multiple' => 'If your contacts have multiple email addresses or phone numbers, only the first entry will be saved.', + 'import_upload_rule_limit' => 'Files are limited to 10 MB.', + 'import_upload_rule_time' => 'It might take up to a minute to upload the contacts and process them. Please be patient.', + 'import_upload_rule_cant_revert' => 'Please make sure data is accurate before uploading, as you can’t undo the upload.', + 'import_upload_form_file' => '.vcf veya .vCard dosyanız:', + 'import_upload_behaviour' => 'İçe aktarma davranışı:', + 'import_upload_behaviour_add' => 'Add new contacts and skip existing', + 'import_upload_behaviour_replace' => 'Mevcut kişileri değiştir', + 'import_upload_behaviour_help' => 'Replacing will replace all data found in the vCard, but will keep existing contact fields.', + 'import_report_title' => 'İçe aktarma raporu', + 'import_report_date' => 'İçe aktarma tarihi', + 'import_report_type' => 'İçe aktarma türü', + 'import_report_number_contacts' => 'Dosyadaki kişi sayısı', + 'import_report_number_contacts_imported' => 'İçe aktarılan kişi sayısı', + 'import_report_number_contacts_skipped' => 'Atlanan kişi sayısı', + 'import_report_status_imported' => 'İçe aktarıldı', + 'import_report_status_skipped' => 'Atlandı', + 'import_vcard_parse_error' => 'vCard girdisini ayrıştırırken hata', + 'import_vcard_contact_exist' => 'Kişi zaten mevcut', + 'import_vcard_contact_no_firstname' => 'İlk isim yok (zorunlu)', + 'import_vcard_file_not_found' => 'Dosya bulunamadı', + 'import_vcard_unknown_entry' => 'Bilinmeyen kişi adı', + 'import_vcard_file_no_entries' => 'Dosya herhangi bir girdi içermiyor', + 'import_blank_title' => 'Henüz herhangi bir kişiyi içe aktarmadınız.', + 'import_blank_question' => 'Şimdi kişileri içe aktarmak ister misiniz?', + 'import_blank_description' => 'Google Kişiler\'den veya Kişi yöneticinizden alabileceğiniz vCard dosyalarını içe aktarabiliriz.', + 'import_blank_cta' => 'vCard İçe Aktarım', + 'import_need_subscription' => 'Verileri içe aktarmak abonelik gerektirir.', + + 'tags_list_title' => 'Etiketler', + 'tags_list_description' => 'Etiketler ayarlayarak kişilerinizi düzenleyebilirsiniz. Etiketler klasörler gibi çalışır, ancak bir kişiye birden fazla etiket ekleyebilirsiniz. Yeni bir etiket eklemek için, kişinin üzerine ekleyin.', + 'tags_list_contact_number' => ':count bağlantı', + 'tags_list_delete_success' => 'Etiket başarıyla silindi', + 'tags_list_edit_success' => 'The tag has been successfully updated', + 'tags_list_delete_confirmation' => 'Etiketi silmek istediğinizden emin misiniz? Bağlantılar silinmeyecek, sadece etiket silinecektir.', + 'tags_blank_title' => 'Etiketler, kişilerinizi sınıflandırmanın harika bir yoludur.', + 'tags_blank_description' => 'Tags work like folders, but you can add more than one tag to a contact. Go to a contact and tag a friend, right below the name. Once a contact is tagged, come back here to manage all the tags in your account.', + + 'api_title' => 'API erişimi', + 'api_description' => 'API, Monica’nın verilerini harici bir uygulamadan, örneğin bir mobil uygulama gibi, yönetmek için kullanılabilir.', + 'api_help' => 'API\'yi kullanmak için, bir belirteç zorunludur. Bir kişisel erişim belirteci (Taşıyıcı kimlik doğrulaması) oluşturabilir veya sizin için oluşturması için bir OAuth istemcisine yetki verebilirsiniz. API dokümantasyonuna bakın.', + 'api_endpoint' => 'Bu Monica örneğinin API uç noktası:', + + 'api_personal_access_tokens' => 'Kişisel erişim belirteçleri', + 'api_pao_description' => 'Bu belirteci güvendiğiniz bir kaynağa verdiğinizden emin olun - çünkü tüm verilerinize erişmenize izin verir.', + 'api_token_title' => 'Kişisel Erişim Belirteçleri', + 'api_token_create_new' => 'Yeni Belirteç Oluştur', + 'api_token_not_created' => 'Herhangi bir kişisel erişim belirteci oluşturmadınız.', + 'api_token_name' => 'Belirteç adı', + 'api_token_expire' => '{date} tarihinde süresi doluyor', + 'api_token_delete' => 'Sil', + 'api_token_create' => 'Belirteç Oluştur', + 'api_token_scopes' => 'Kapsamlar', + 'api_token_help' => 'İşte yeni kişisel erişim belirteciniz. Bundan başka gösterilmeyecek, bu yüzden kaybetmeyin! Artık API isteğinde bulunmak için bu belirteci kullanabilirsiniz.', + + 'api_oauth_clients' => 'OAuth istemcileriniz', + 'api_oauth_clients_desc' => 'Bu bölüm kendi OAuth istemcilerinizi kaydetmenize izin verir.', + 'api_oauth_clients_desc2' => 'Yeni bir belirteç istemek için bu istemci kimliğini kullanın ve yetkilendirme kodlarını erişim belirteçlerine çevirin. Daha fazla bilgi için Laravel Passport dokümantasyonuna bakın.', + 'api_oauth_title' => 'OAuth İstemcileri', + 'api_oauth_create_new' => 'Yeni İstemci Oluştur', + 'api_oauth_edit' => 'İstemci Düzenle', + 'api_oauth_not_created' => 'Herhangi bir OAuth istemcisi oluşturmadınız.', + 'api_oauth_clientid' => 'İstemci Kimliği', + 'api_oauth_name' => 'Ad', + 'api_oauth_name_help' => 'Kullanıcılarınızın tanıyacağı ve güveneceği bir şey.', + 'api_oauth_secret' => 'Gizli', + 'api_oauth_create' => 'İstemci Oluştur', + 'api_oauth_redirecturl' => 'Yönlendirme URL\'si', + 'api_oauth_redirecturl_help' => 'Uygulamanızın yetkilendirme geri çağırma URL\'si.', + + 'api_authorized_clients' => 'Yetkilendirilmiş istemcilerin listesi', + 'api_authorized_clients_desc' => 'Bu bölüm, uygulama verilerinize erişmek için yetkilendirdiğiniz tüm istemcileri listeler. Bu yetkilendirmeyi istediğiniz zaman iptal edebilirsiniz.', + 'api_authorized_clients_title' => 'Yetkilendirilmiş Uygulamalar', + 'api_authorized_clients_none' => 'Henüz yetkilendirilmiş bir istemci yok.', + 'api_authorized_clients_name' => 'Ad', + 'api_authorized_clients_scopes' => 'Kapsamlar', + + 'personalization_tab_title' => 'Hesabınızı kişiselleştirin', + + 'personalization_title' => 'Here you will find different settings to configure your account. These features are intended for “power users” who want maximum control over Monica.', + 'personalization_contact_field_type_title' => 'Kişi alanı türleri', + 'personalization_contact_field_type_add' => 'Yeni alan türü ekle', + 'personalization_contact_field_type_description' => 'You can configure all the different types of contact fields that you can associate to all your contacts. For example, if a new social network appears in the future, you will be able to add this new way of communicating with your contacts right here.', + 'personalization_contact_field_type_table_name' => 'İsim', + 'personalization_contact_field_type_table_protocol' => 'Protokol', + 'personalization_contact_field_type_table_actions' => 'Eylemler', + 'personalization_contact_field_type_modal_title' => 'Yeni bir kişi alanı türü ekle', + 'personalization_contact_field_type_modal_edit_title' => 'Mevcut bir kişi alanı türünü düzenle', + 'personalization_contact_field_type_modal_delete_title' => 'Mevcut bir kişi alanı türünü sil', + 'personalization_contact_field_type_modal_delete_description' => 'Are you sure you want to delete this contact field type? Deleting this type of contact field will delete ALL the data with this type for all of your contacts.', + 'personalization_contact_field_type_modal_name' => 'İsim', + 'personalization_contact_field_type_modal_protocol' => 'Protokol (isteğe bağlı)', + 'personalization_contact_field_type_modal_protocol_help' => 'Her yeni kişi alanı türü tıklanabilir. Bir protokol ayarlanmışsa, ayarlanan eylemi tetiklemek için onu kullanırız.', + 'personalization_contact_field_type_modal_icon' => 'Simge (isteğe bağlı)', + 'personalization_contact_field_type_modal_icon_help' => 'Bir simgeyi bu kişi alanı türüyle ilişkilendirebilirsiniz. Bir Font Awesome simgesine bir referans eklemeniz gerekmektedir.', + 'personalization_contact_field_type_delete_success' => 'The contact field type has been successfully deleted.', + 'personalization_contact_field_type_add_success' => 'Kişi alanı türü başarıyla eklendi.', + 'personalization_contact_field_type_edit_success' => 'Kişi alanı türü başarıyla güncellendi.', + + 'personalization_genders_title' => 'Cinsiyet türleri', + 'personalization_genders_add' => 'Yeni cinsiyet türü ekle', + 'personalization_genders_desc' => 'İhtiyacınız olan sayıda cinsiyet tanımlayabilirsiniz. Hesabınızda en az bir cinsiyet türü olmalıdır.', + 'personalization_genders_modal_add' => 'Cinsiyet türü ekle', + 'personalization_genders_modal_edit' => 'Cinsiyet türünü düzenle', + 'personalization_genders_modal_name' => 'İsim', + 'personalization_genders_modal_name_help' => 'İletişim sayfasında gösterilecek olan cinsiyetin ismi.', + 'personalization_genders_modal_sex' => 'Cinsiyet', + 'personalization_genders_modal_sex_help' => 'İlişkileri tanımlamak için ve vCard içe/dışa aktarma işlemi sırasında kullanılır.', + 'personalization_genders_modal_default' => 'Yeni bağlantılar için genel cinsiyeti seçiniz', + 'personalization_genders_modal_delete' => 'Cinsiyet türünü sil', + 'personalization_genders_modal_delete_desc' => 'Are you sure you want to delete the gender “{name}”?', + 'personalization_genders_modal_delete_question' => 'You currently have {count} contact with this gender. If you delete this gender, what gender should this contact have?|You currently have {count} contacts with this gender. If you delete this gender, what gender should these contacts have?', + 'personalization_genders_modal_delete_question_default' => 'This gender is the default one. If you delete this gender, which one will be the new default?', + 'personalization_genders_modal_error' => 'Please choose a gender from the list.', + 'personalization_genders_list_contact_number' => '{count} kişi', + 'personalization_genders_table_name' => 'İsim', + 'personalization_genders_table_sex' => 'Cinsiyet', + 'personalization_genders_table_default' => 'Varsayılan', + 'personalization_genders_default' => 'Varsayılan cinsiyet', + 'personalization_genders_make_default' => 'Varsayılan cinsiyeti değiştir', + 'personalization_genders_select_default' => 'Varsayılan cinsiyeti seç', + 'personalization_genders_m' => 'Erkek', + 'personalization_genders_f' => 'Kadın', + 'personalization_genders_o' => 'Diğer', + 'personalization_genders_u' => 'Bilinmeyen', + 'personalization_genders_n' => 'Hiçbiri veya hiçbirine uygun değil', + + 'personalization_reminder_rule_save' => 'Değişiklik kaydedildi', + 'personalization_reminder_rule_title' => 'Hatırlatma kuralları', + 'personalization_reminder_rule_line' => '{count} gün önce', + 'personalization_reminder_rule_desc' => 'For every reminder that you set, Monica can send you an email a number of days before the event happens. You can adjust these notification settings here. These notifications only apply to monthly and yearly reminders.', + + 'personalization_module_save' => 'Değişiklik kaydedildi', + 'personalization_module_title' => 'Özellikler', + 'personalization_module_desc' => 'You may not need all of Monica’s features. Below you can toggle specific features that are used on a contact sheet. This change will affect ALL your contacts. Turning off a feature does not delete any data, it simply hides the feature.', + + 'personalisation_paid_upgrade' => 'Bu Ücretli bir abonelik gerektiren premium bir özelliktir. Ayarlar > Abonelik bölümünü ziyaret ederek hesabınızı yükseltin.', + 'personalisation_paid_upgrade_vue' => 'Bu Ücretli bir abonelik gerektiren premium bir özelliktir. Ayarlar > Abonelik bölümünü ziyaret ederek hesabınızı yükseltin.', + + 'reminder_time_to_send' => 'Time of the day reminders will be sent', + 'reminder_time_to_send_help' => 'Your next reminder is scheduled to be sent on {dateTime}.', + + 'personalization_activity_type_category_title' => 'Aktivite türü kategorileri', + 'personalization_activity_type_category_add' => 'Yeni bir aktivite türü kategorisi ekle', + 'personalization_activity_type_category_table_name' => 'Ad', + 'personalization_activity_type_category_description' => 'An activity with one of your contacts can have a type and a category type. Your account comes with a set of predefined category types by default, but you can customize these here.', + 'personalization_activity_type_category_table_actions' => 'Eylemler', + 'personalization_activity_type_category_modal_add' => 'Yeni bir etkinlik türü kategorisi ekle', + 'personalization_activity_type_category_modal_edit' => 'Bir etkinlik türü kategorisini düzenle', + 'personalization_activity_type_category_modal_question' => 'What should we name this new category?', + 'personalization_activity_type_add_button' => 'Yeni bir aktivite türü ekle', + 'personalization_activity_type_modal_add' => 'Yeni bir aktivite türü ekle', + 'personalization_activity_type_modal_question' => 'What should we name this new activity type?', + 'personalization_activity_type_modal_edit' => 'Bir aktivite türünü düzenle', + 'personalization_activity_type_category_modal_delete' => 'Bir aktivite türü kategorisini sil', + 'personalization_activity_type_category_modal_delete_desc' => 'Are you sure you want to delete this category? Deleting it will delete all associated activity types. Activities that belong to this category will not be affected by this deletion.', + 'personalization_activity_type_modal_delete' => 'Bir aktivite türünü sil', + 'personalization_activity_type_modal_delete_desc' => 'Bu aktivite türünü silmek istediğinize emin misiniz? Bu kategoriye ait aktiviteler bu silme işleminden etkilenmeyecektir.', + 'personalization_activity_type_modal_delete_error' => 'Bu aktivite türünü bulamıyoruz.', + 'personalization_activity_type_category_modal_delete_error' => 'Bu aktivite türü kategorisini bulamıyoruz.', + + 'personalization_life_event_category_title' => 'Life event categories', + 'personalization_live_event_category_table_name' => 'İsim', + 'personalization_life_event_category_description' => 'A life event can have a type and a category. Your account comes with a set of predefined categories and types by default, but you can customize life event types here.', + 'personalization_live_event_category_table_actions' => 'Eylemler', + 'personalization_life_event_type_add_button' => 'Add a new life event type', + 'personalization_life_event_type_modal_add' => 'Add a new life event type', + 'personalization_life_event_type_modal_question' => 'What should we name this new life event type?', + 'personalization_life_event_type_modal_edit' => 'Edit a life event type', + 'personalization_life_event_type_modal_delete' => 'Delete a life event type', + 'personalization_life_event_type_modal_delete_desc' => 'Are you sure you want to delete this life event type? Life events that belong to this type will be deleted by performing this action.', + 'personalization_life_event_type_modal_delete_error' => 'We can’t find this life event type.', + + 'personalization_life_event_category_work_education' => 'İş ve eğitim', + 'personalization_life_event_category_family_relationships' => 'Aile & ilişkiler', + 'personalization_life_event_category_home_living' => 'Ev & yaşam', + 'personalization_life_event_category_travel_experiences' => 'Seyahat & deneyimler', + 'personalization_life_event_category_health_wellness' => 'Sağlık & Fitness', + + 'personalization_life_event_type_new_job' => 'Yeni iş', + 'personalization_life_event_type_retirement' => 'Emeklilik', + 'personalization_life_event_type_new_school' => 'Yeni okul', + 'personalization_life_event_type_study_abroad' => 'Yurtdışında Eğitim', + 'personalization_life_event_type_volunteer_work' => 'Gönüllü çalışma', + 'personalization_life_event_type_published_book_or_paper' => 'Kitap ya da makale yayını', + 'personalization_life_event_type_military_service' => 'Askerlik hizmeti', + 'personalization_life_event_type_first_met' => 'İlk buluşma', + 'personalization_life_event_type_new_relationship' => 'Yeni ilişki', + 'personalization_life_event_type_engagement' => 'Nişanlanma', + 'personalization_life_event_type_marriage' => 'Evlilik', + 'personalization_life_event_type_anniversary' => 'Yıldönümü', + 'personalization_life_event_type_expecting_a_baby' => 'Bebek bekleme', + 'personalization_life_event_type_new_child' => 'Yeni çocuk', + 'personalization_life_event_type_new_family_member' => 'Yeni aile üyesi', + 'personalization_life_event_type_new_pet' => 'Yeni evcil hayvan', + 'personalization_life_event_type_end_of_relationship' => 'İlişkiyi sonlandırma', + 'personalization_life_event_type_loss_of_a_loved_one' => 'Sevdiğin birini kaybetme', + 'personalization_life_event_type_moved' => 'Taşındı', + 'personalization_life_event_type_bought_a_home' => 'Ev alma', + 'personalization_life_event_type_home_improvement' => 'Evi geliştirme', + 'personalization_life_event_type_holidays' => 'Tatiller', + 'personalization_life_event_type_new_vehicle' => 'Yeni araç', + 'personalization_life_event_type_new_roommate' => 'Yeni oda arkadaşı', + 'personalization_life_event_type_overcame_an_illness' => 'Bir hastalığı yen', + 'personalization_life_event_type_quit_a_habit' => 'Bir alışkanlığı bırak', + 'personalization_life_event_type_new_eating_habits' => 'Yeni yemek alışkanlıkları', + 'personalization_life_event_type_weight_loss' => 'Kilo verme', + 'personalization_life_event_type_wear_glass_or_contact' => 'Started wearing glasses or contacts', + 'personalization_life_event_type_broken_bone' => 'Broke a bone', + 'personalization_life_event_type_removed_braces' => 'Had braces removed', + 'personalization_life_event_type_surgery' => 'Had surgery', + 'personalization_life_event_type_dentist' => 'Had dental treatment', + 'personalization_life_event_type_new_sport' => 'Started playing a new sport', + 'personalization_life_event_type_new_hobby' => 'Took up a new hobby', + 'personalization_life_event_type_new_instrument' => 'Started learning a new instrument', + 'personalization_life_event_type_new_language' => 'Started learning a new language', + 'personalization_life_event_type_tattoo_or_piercing' => 'Dövme veya piercing', + 'personalization_life_event_type_new_license' => 'Yeni lisans', + 'personalization_life_event_type_travel' => 'Seyahat', + 'personalization_life_event_type_achievement_or_award' => 'Başarılar ya da Ödüller', + 'personalization_life_event_type_changed_beliefs' => 'Değişen inançlar', + 'personalization_life_event_type_first_word' => 'İlk kelime', + 'personalization_life_event_type_first_kiss' => 'İlk öpücük', + + 'storage_title' => 'Depolama', + 'storage_account_info' => 'Your account limit is :accountLimit MB. Your current usage is :currentAccountSize MB (about :percentUsage%).', + 'storage_upgrade_notice' => 'Belge ve fotoğraf yükleyebilmek için hesabınızı yükseltin.', + 'storage_description' => 'Burada, kişileriniz hakkında yüklenen tüm dokümanları ve fotoğrafları görebilirsiniz.', + + 'dav_title' => 'WebDAV', + 'dav_description' => 'Burada CardDAV ve CalDAV dışa aktarma işlemleri için WebDAV kaynaklarını kullanmak için tüm ayarları bulabilirsiniz.', + 'dav_copy_help' => 'Panoya kopyalayın', + 'dav_clipboard_copied' => 'Değer panonuza kopyalandı', + 'dav_url_base' => 'Tüm CardDAV ve CalDAV kaynakları için temel URL:', + 'dav_connect_help' => 'Telefonunuzda veya bilgisayarınızda kişilerinizi ve/veya takvimlerinizi, bu temel URL ile bağlayabilirsiniz.', + 'dav_connect_help2' => 'Use your login (email) and create an API token as the password to authenticate.', + 'dav_url_carddav' => 'Kişiler kaynağı için CardDAV URL\'si:', + 'dav_url_caldav_birthdays' => 'Doğum günleri kaynağı için CalDAV URL\'si:', + 'dav_url_caldav_tasks' => 'Görevler kaynağı için CalDAV URL\'si:', + 'dav_title_carddav' => 'CardDAV', + 'dav_title_caldav' => 'CalDAV', + 'dav_carddav_export' => 'Tüm kişileri tek bir dosyada dışa aktar', + 'dav_caldav_birthdays_export' => 'Tüm doğum günlerini tek bir dosyada dışa aktar', + 'dav_caldav_tasks_export' => 'Tüm görevleri tek bir dosyada dışa aktar', + + 'archive_title' => 'Archive all of the contacts in your account', + 'archive_desc' => 'This will archive all of the contacts in your account.', + 'archive_cta' => 'Archive all of your contacts', + + 'logs_title' => 'Everything that has happened to this account', + 'logs_actor' => 'Actor', + 'logs_timestamp' => 'Timestamp', + 'logs_description' => 'Description', + 'logs_subject' => 'Subject', + 'logs_size' => 'Size (Kb)', + 'logs_object' => 'Object', +]; diff --git a/resources/lang/tr/validation.php b/resources/lang/tr/validation.php new file mode 100644 index 0000000..97cd51e --- /dev/null +++ b/resources/lang/tr/validation.php @@ -0,0 +1,166 @@ + ':attribute kabul edilmelidir.', + 'active_url' => ':attribute geçerli bir URL değil.', + 'after' => ':attribtute , :date tarihinden sonra bir tarih olmalıdır.', + 'after_or_equal' => ':attribute tarihi :date tarihinden sonra veya tarihine eşit olmalıdır.', + 'alpha' => ':attribute sadece harflerden oluşmalıdır.', + 'alpha_dash' => ':attribute sadece harfler, rakamlar ve tirelerden oluşmalıdır.', + 'alpha_num' => ':attribute sadece harfler ve rakamlar içermelidir.', + 'array' => ':attribute dizi olmalıdır.', + 'before' => ':attribute şundan daha önceki bir tarih olmalıdır :date.', + 'before_or_equal' => ':attribute tarihi :date tarihinden önce veya tarihine eşit olmalıdır.', + 'between' => [ + 'numeric' => ':attribute :min - :max arasında olmalıdır.', + 'file' => ':attribute :min - :max arasındaki kilobayt değeri olmalıdır.', + 'string' => ':attribute :min - :max arasında karakterden oluşmalıdır.', + 'array' => ':attribute :min - :max arasında nesneye sahip olmalıdır.', + ], + 'boolean' => ':attribute sadece doğru veya yanlış olmalıdır.', + 'confirmed' => ':attribute tekrarı eşleşmiyor.', + 'date' => ':attribute geçerli bir tarih olmalıdır.', + 'date_equals' => ':attribute şuna eşit bir tarih olmalıdır: :date.', + 'date_format' => ':attribute :format biçimi ile eşleşmiyor.', + 'different' => ':attribute ile :other birbirinden farklı olmalıdır.', + 'digits' => ':attribute :digits rakam olmalıdır.', + 'digits_between' => ':attribute :min ile :max arasında rakam olmalıdır.', + 'dimensions' => ':attribute görsel ölçüleri geçersiz.', + 'distinct' => ':attribute alanı yinelenen bir değere sahip.', + 'email' => ':attribute biçimi geçersiz.', + 'ends_with' => ':attribute şunlardan biriyle bitmelidir: :values', + 'exists' => 'Seçili :attribute geçersiz.', + 'file' => ':attribute dosya olmalıdır.', + 'filled' => ':attribute alanının doldurulması zorunludur.', + 'gt' => [ + 'numeric' => ':attribute şu değerden büyük olmalıdır: :value.', + 'file' => ':attribute, :value kilobayttan fazla olmalıdır.', + 'string' => ':attribute, :value karakterden fazla olmalıdır.', + 'array' => ':attribute, :value öğeden fazla öğe içermelidir.', + ], + 'gte' => [ + 'numeric' => ':attribute, :value değerinden büyük veya bu değere eşit olmalıdır.', + 'file' => ':attribute, :value kilobayttan fazla veya bu değere eşit olmalıdır.', + 'string' => ':attribute, :value karakter veya daha fazla karakter içermelidir.', + 'array' => ':attribute, :value veya daha fazla öğe içermelidir.', + ], + 'image' => ':attribute alanı resim dosyası olmalıdır.', + 'in' => ':attribute değeri geçersiz.', + 'in_array' => ':attribute alanı :other içinde mevcut değil.', + 'integer' => ':attribute tamsayı olmalıdır.', + 'ip' => ':attribute geçerli bir IP adresi olmalıdır.', + 'ipv4' => ':attribute geçerli bir IPv4 adresi olmalıdır.', + 'ipv6' => ':attribute geçerli bir IPv6 adresi olmalıdır.', + 'json' => ':attribute geçerli bir JSON değişkeni olmalıdır.', + 'lt' => [ + 'numeric' => ':attribute şu değerden küçük olmalıdır: :value.', + 'file' => ':attribute, :value kilobayttan fazla olmalıdır.', + 'string' => ':attribute, :value karakterden az olmalıdır.', + 'array' => ':attribute, :value öğeden az öğe içermelidir.', + ], + 'lte' => [ + 'numeric' => ':attribute, :value değerinden düşük veya bu değere eşit olmalıdır.', + 'file' => ':attribute, :value kilobayttan az veya bu değere eşit olmalıdır.', + 'string' => ':attribute, :value karakterden az veya buna eşit olmalıdır.', + 'array' => ':attribute, :value öğeden fazla öğe içermemelidir.', + ], + 'max' => [ + 'numeric' => ':attribute değeri :max değerinden küçük olmalıdır.', + 'file' => ':attribute değeri :max kilobayt değerinden küçük olmalıdır.', + 'string' => ':attribute değeri :max karakter değerinden küçük olmalıdır.', + 'array' => ':attribute değeri :max adedinden az nesneye sahip olmalıdır.', + ], + 'mimes' => ':attribute dosya biçimi :values olmalıdır.', + 'mimetypes' => ':attribute dosya biçimi :values olmalıdır.', + 'min' => [ + 'numeric' => ':attribute değeri :min değerinden büyük olmalıdır.', + 'file' => ':attribute değeri :min kilobayt değerinden büyük olmalıdır.', + 'string' => ':attribute değeri :min karakter değerinden büyük olmalıdır.', + 'array' => ':attribute en az :min nesneye sahip olmalıdır.', + ], + 'not_in' => 'Seçili :attribute geçersiz.', + 'not_regex' => ':attribute biçimi geçersiz.', + 'numeric' => ':attribute sayı olmalıdır.', + 'password' => 'Parola hatalı.', + 'present' => ':attribute alanı mevcut olmalıdır.', + 'regex' => ':attribute biçimi geçersiz.', + 'required' => ':attribute alanı gereklidir.', + 'required_if' => ':attribute alanı, :other :value değerine sahip olduğunda zorunludur.', + 'required_unless' => ':attribute alanı, :other alanı :value değerlerinden birine sahip olmadığında zorunludur.', + 'required_with' => ':attribute alanı :values varken zorunludur.', + 'required_with_all' => ':attribute alanı herhangi bir :values değeri varken zorunludur.', + 'required_without' => ':attribute alanı :values yokken zorunludur.', + 'required_without_all' => ':attribute alanı :values değerlerinden herhangi biri yokken zorunludur.', + 'same' => ':attribute ile :other eşleşmelidir.', + 'size' => [ + 'numeric' => ':attribute :size olmalıdır.', + 'file' => ':attribute :size kilobyte olmalıdır.', + 'string' => ':attribute :size karakter olmalıdır.', + 'array' => ':attribute :size nesneye sahip olmalıdır.', + ], + 'starts_with' => ':attribute şunlardan biriyle başlamalıdır: :values', + 'string' => ':attribute dizge olmalıdır.', + 'timezone' => ':attribute geçerli bir saat dilimi olmalıdır.', + 'unique' => ':attribute daha önceden kayıt edilmiş.', + 'uploaded' => ':attribute yüklemesi başarısız.', + 'url' => ':attribute biçimi geçersiz.', + 'uuid' => ':attribute geçerli bir UUID olmalıdır.', + + /* + |-------------------------------------------------------------------------- + | Custom Validation Language Lines + |-------------------------------------------------------------------------- + | + | Here you may specify custom validation messages for attributes using the + | convention "attribute.rule" to name the lines. This makes it quick to + | specify a specific custom language line for a given attribute rule. + | + */ + + 'custom' => [ + 'attribute-name' => [ + 'rule-name' => 'custom-message', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Custom Validation Attributes + |-------------------------------------------------------------------------- + | + | The following language lines are used to swap our attribute placeholder + | with something more reader friendly such as "E-Mail Address" instead + | of "email". This simply helps us make our message more expressive. + | + */ + + 'attributes' => [], + + 'vue' => [ + 'max' => [ + 'numeric' => '{field} may not be greater than {max}.', + 'string' => '{field} may not be greater than {max} characters.', + ], + 'required' => '{field} is required.', + 'url' => '{field} is not a valid URL.', + ], + +]; diff --git a/resources/lang/uk.json b/resources/lang/uk.json new file mode 100644 index 0000000..66df15c --- /dev/null +++ b/resources/lang/uk.json @@ -0,0 +1,7 @@ +{ + "The :attribute must contain at least one uppercase and one lowercase letter.": "Поле :attribute повинне містити принаймні одну велику і одну малу літеру.", + "The :attribute must contain at least one letter.": "Поле :attribute повинне містити принаймні одну букву.", + "The :attribute must contain at least one symbol.": "Поле :attribute повинне містити принаймні один символ.", + "The :attribute must contain at least one number.": "Поле :attribute повинне містити принаймні одну цифру.", + "The given :attribute has appeared in a data leak. Please choose a different :attribute.": "Значення поля :attribute було частиною витоку даних. Будь ласка, вкажіть інше значення :attribute." +} diff --git a/resources/lang/uk/app.php b/resources/lang/uk/app.php new file mode 100644 index 0000000..e78c235 --- /dev/null +++ b/resources/lang/uk/app.php @@ -0,0 +1,571 @@ + 'Yes', + 'no' => 'No', + 'update' => 'Update', + 'save' => 'Save', + 'add' => 'Add', + 'cancel' => 'Cancel', + 'confirm' => 'Confirm', + 'delete_confirm' => 'Are you sure?', + 'delete' => 'Delete', + 'edit' => 'Edit', + 'upload' => 'Upload', + 'download' => 'Download', + 'save_close' => 'Save and close', + 'close' => 'Close', + 'copy' => 'Copy', + 'create' => 'Create', + 'remove' => 'Remove', + 'revoke' => 'Revoke', + 'done' => 'Done', + 'back' => 'Back', + 'verify' => 'Verify', + 'new' => 'new', + 'unknown' => 'I don’t know', + 'load_more' => 'Load more', + 'loading' => 'Loading…', + 'with' => 'with', + 'today' => 'today', + 'yesterday' => 'yesterday', + 'another_day' => 'another day', + 'date' => 'Date', + 'type' => 'Type', + 'zoom' => 'Zoom', + 'upgrade' => 'Upgrade to unlock', + 'percent_uploaded' => '{percent}% uploaded', + 'retry' => 'Retry', + 'filter' => 'Filter the list', + 'go_back' => 'Go back', + 'file_selected' => 'One file selected…|{count} files selected…', + + 'application_title' => 'Monica – personal relationship manager', + 'application_description' => 'Monica is a tool to manage your interactions with your loved ones, friends, and family.', + 'application_og_title' => 'Have better relations with your loved ones. Free online CRM for friends and family.', + + 'markdown_description' => 'Want to format your text nicely? We support Markdown to add bold, italic, lists, and more.', + 'markdown_link' => 'Read documentation', + + 'header_settings_link' => 'Settings', + 'header_logout_link' => 'Logout', + 'header_changelog_link' => 'Product changes', + + 'main_nav_cta' => 'Add people', + 'main_nav_dashboard' => 'Dashboard', + 'main_nav_family' => 'Contacts', + 'main_nav_journal' => 'Journal', + 'main_nav_activities' => 'Activities', + 'main_nav_tasks' => 'Tasks', + + 'footer_remarks' => 'Comments?', + 'footer_send_email' => 'Send us an email', + 'footer_privacy' => 'Privacy policy', + 'footer_release' => 'Release notes', + 'footer_newsletter' => 'Newsletter', + 'footer_source_code' => 'Contribute', + 'footer_version' => 'Version: :version', + 'footer_new_version' => 'A new version of Monica is available', + + 'footer_modal_version_whats_new' => 'What’s new', + 'footer_modal_version_release_away' => 'You are 1 release behind the latest version available. You should update your instance.|You are :number releases behind the latest version available. You should update your instance.', + + 'breadcrumb_dashboard' => 'Dashboard', + 'breadcrumb_list_contacts' => 'List of people', + 'breadcrumb_archived_contacts' => 'Archived contacts', + 'breadcrumb_journal' => 'Journal', + 'breadcrumb_settings' => 'Settings', + 'breadcrumb_settings_export' => 'Export', + 'breadcrumb_settings_users' => 'Users', + 'breadcrumb_settings_users_add' => 'Add a user', + 'breadcrumb_settings_subscriptions' => 'Subscription', + 'breadcrumb_settings_import' => 'Import', + 'breadcrumb_settings_import_report' => 'Import report', + 'breadcrumb_settings_import_upload' => 'Upload', + 'breadcrumb_settings_tags' => 'Tags', + 'breadcrumb_add_significant_other' => 'Add significant other', + 'breadcrumb_edit_significant_other' => 'Edit significant other', + 'breadcrumb_add_note' => 'Add a note', + 'breadcrumb_edit_note' => 'Edit a note', + 'breadcrumb_api' => 'API', + 'breadcrumb_dav' => 'DAV Resources', + 'breadcrumb_edit_introductions' => 'How did you meet', + 'breadcrumb_settings_personalization' => 'Personalization', + 'breadcrumb_settings_security' => 'Security', + 'breadcrumb_settings_security_2fa' => 'Two Factor Authentication', + 'breadcrumb_profile' => 'Profile of :name', + + 'gender_male' => 'Man', + 'gender_female' => 'Woman', + 'gender_none' => 'Rather not say', + 'gender_no_gender' => 'No gender', + + 'error_title' => 'Whoops! Something went wrong.', + 'error_unauthorized' => 'You don’t have the right to edit this resource.', + 'error_user_account' => 'This user does not belong to the given account.', + 'error_save' => 'We had an error trying to save the data.', + 'error_try_again' => 'Something went wrong. Please try again.', + 'error_id' => 'Error ID: :id', + 'error_unavailable' => 'Service unavailable', + 'error_maintenance' => 'Maintenance in progress. We’ll be right back.', + 'error_help' => 'We’ll be right back.', + 'error_twitter' => 'Follow our Twitter account to be alerted when it’s up again.', + 'error_no_term' => 'There is no policy for this instance yet.', + + 'default_save_success' => 'The data has been saved.', + + 'compliance_title' => 'Sorry for the interruption.', + 'compliance_desc' => 'We have changed our Terms of Use and Privacy Policy. By law we have to ask you to review them and accept them so you can continue to use your account.', + 'compliance_desc_end' => 'We don’t do anything nasty with your data or your account and we never will.', + 'compliance_terms' => 'Accept new terms and privacy policy', + + // Relationship types + // Yes, each relationship type has 8 strings associated with it. + // This is because we need to indicate the name of the relationship type, + // and also the name of the opposite side of this relationship (father/son), + // and then, the feminine version of the string. Finally, in some sentences + // in the UI, we need to include the name of the person we add the relationship + // to. + 'relationship_type_group_love' => 'Love relationships', + 'relationship_type_group_family' => 'Family relationships', + 'relationship_type_group_friend' => 'Friend relationships', + 'relationship_type_group_work' => 'Work relationships', + 'relationship_type_group_other' => 'Other kind of relationships', + + 'relationship_type_partner' => 'significant other', + 'relationship_type_partner_female' => 'significant other', + 'relationship_type_partner_male' => 'партнер', + 'relationship_type_partner_with_name' => ':name’s significant other', + 'relationship_type_partner_female_with_name' => ':name’s significant other', + 'relationship_type_partner_male_with_name' => ':name’s significant other', + + 'relationship_type_spouse' => 'spouse', + 'relationship_type_spouse_female' => 'wife', + 'relationship_type_spouse_male' => 'husband', + 'relationship_type_spouse_with_name' => ':name’s spouse', + 'relationship_type_spouse_female_with_name' => ':name’s wife', + 'relationship_type_spouse_male_with_name' => ':name’s husband', + + 'relationship_type_date' => 'date', + 'relationship_type_date_female' => 'date', + 'relationship_type_date_male' => 'хлопець', + 'relationship_type_date_with_name' => ':name’s date', + 'relationship_type_date_female_with_name' => ':name’s date', + 'relationship_type_date_male_with_name' => ':name’s date', + + 'relationship_type_lover' => 'lover', + 'relationship_type_lover_female' => 'lover', + 'relationship_type_lover_male' => 'lover', + 'relationship_type_lover_with_name' => ':name’s lover', + 'relationship_type_lover_female_with_name' => ':name’s lover', + 'relationship_type_lover_male_with_name' => ':name’s lover', + + 'relationship_type_inlovewith' => 'in love with', + 'relationship_type_inlovewith_female' => 'in love with', + 'relationship_type_inlovewith_male' => 'in love with', + 'relationship_type_inlovewith_with_name' => 'someone :name is in love with', + 'relationship_type_inlovewith_female_with_name' => 'someone :name is in love with', + 'relationship_type_inlovewith_male_with_name' => 'someone :name is in love with', + + 'relationship_type_lovedby' => 'loved by', + 'relationship_type_lovedby_female' => 'loved by', + 'relationship_type_lovedby_male' => 'loved by', + 'relationship_type_lovedby_with_name' => ':name’s secret lover', + 'relationship_type_lovedby_female_with_name' => ':name’s secret lover', + 'relationship_type_lovedby_male_with_name' => ':name’s secret lover', + + 'relationship_type_ex' => 'ex-partner', + 'relationship_type_ex_female' => 'ex-girlfriend', + 'relationship_type_ex_male' => 'ex-boyfriend', + 'relationship_type_ex_with_name' => ':name’s ex-partner', + 'relationship_type_ex_female_with_name' => ':name’s ex-girlfriend', + 'relationship_type_ex_male_with_name' => ':name’s ex-boyfriend', + + 'relationship_type_parent' => 'parent', + 'relationship_type_parent_female' => 'mother', + 'relationship_type_parent_male' => 'father', + 'relationship_type_parent_with_name' => ':name’s parent', + 'relationship_type_parent_female_with_name' => ':name’s mother', + 'relationship_type_parent_male_with_name' => ':name’s father', + + 'relationship_type_child' => 'child', + 'relationship_type_child_female' => 'daughter', + 'relationship_type_child_male' => 'son', + 'relationship_type_child_with_name' => ':name’s child', + 'relationship_type_child_female_with_name' => ':name’s daughter', + 'relationship_type_child_male_with_name' => ':name’s son', + + 'relationship_type_stepparent' => 'step-parent', + 'relationship_type_stepparent_female' => 'stepmother', + 'relationship_type_stepparent_male' => 'stepfather', + 'relationship_type_stepparent_with_name' => ':name’s step-parent', + 'relationship_type_stepparent_female_with_name' => ':name’s stepmother', + 'relationship_type_stepparent_male_with_name' => ':name’s stepfather', + + 'relationship_type_stepchild' => 'stepchild', + 'relationship_type_stepchild_female' => 'stepdaughter', + 'relationship_type_stepchild_male' => 'stepson', + 'relationship_type_stepchild_with_name' => ':name’s stepchild', + 'relationship_type_stepchild_female_with_name' => ':name’s stepdaughter', + 'relationship_type_stepchild_male_with_name' => ':name’s stepson', + + 'relationship_type_sibling' => 'sibling', + 'relationship_type_sibling_female' => 'sister', + 'relationship_type_sibling_male' => 'brother', + 'relationship_type_sibling_with_name' => ':name’s sibling', + 'relationship_type_sibling_female_with_name' => ':name’s sister', + 'relationship_type_sibling_male_with_name' => ':name’s brother', + + 'relationship_type_grandparent' => 'grandparent', + 'relationship_type_grandparent_female' => 'grandmother', + 'relationship_type_grandparent_male' => 'grandfather', + 'relationship_type_grandparent_with_name' => ':name’s grandparent', + 'relationship_type_grandparent_female_with_name' => ':name’s grandmother', + 'relationship_type_grandparent_male_with_name' => ':name’s grandfather', + + 'relationship_type_grandchild' => 'grandchild', + 'relationship_type_grandchild_female' => 'granddaughter', + 'relationship_type_grandchild_male' => 'grandson', + 'relationship_type_grandchild_with_name' => ':name’s grandchild', + 'relationship_type_grandchild_female_with_name' => ':name’s granddauther', + 'relationship_type_grandchild_male_with_name' => ':name’s grandson', + + 'relationship_type_uncle' => 'uncle', + 'relationship_type_uncle_female' => 'aunt', + 'relationship_type_uncle_male' => 'uncle', + 'relationship_type_uncle_with_name' => ':name’s uncle', + 'relationship_type_uncle_female_with_name' => ':name’s aunt', + 'relationship_type_uncle_male_with_name' => ':name’s uncle', + + 'relationship_type_nephew' => 'nephew', + 'relationship_type_nephew_female' => 'niece', + 'relationship_type_nephew_male' => 'nephew', + 'relationship_type_nephew_with_name' => ':name’s nephew', + 'relationship_type_nephew_female_with_name' => ':name’s niece', + 'relationship_type_nephew_male_with_name' => ':name’s nephew', + + 'relationship_type_cousin' => 'cousin', + 'relationship_type_cousin_female' => 'cousin', + 'relationship_type_cousin_male' => 'cousin', + 'relationship_type_cousin_with_name' => ':name’s cousin', + 'relationship_type_cousin_female_with_name' => ':name’s cousin', + 'relationship_type_cousin_male_with_name' => ':name’s cousin', + + 'relationship_type_godfather' => 'godparent', + 'relationship_type_godfather_female' => 'godmother', + 'relationship_type_godfather_male' => 'godfather', + 'relationship_type_godfather_with_name' => ':name’s godparent', + 'relationship_type_godfather_female_with_name' => ':name’s godmother', + 'relationship_type_godfather_male_with_name' => ':name’s godfather', + + 'relationship_type_godson' => 'godchild', + 'relationship_type_godson_female' => 'goddaughter', + 'relationship_type_godson_male' => 'godson', + 'relationship_type_godson_with_name' => ':name’s godchild', + 'relationship_type_godson_female_with_name' => ':name’s goddaughter', + 'relationship_type_godson_male_with_name' => ':name’s godson', + + 'relationship_type_friend' => 'friend', + 'relationship_type_friend_female' => 'friend', + 'relationship_type_friend_male' => 'friend', + 'relationship_type_friend_with_name' => ':name’s friend', + 'relationship_type_friend_female_with_name' => ':name’s friend', + 'relationship_type_friend_male_with_name' => ':name’s friend', + + 'relationship_type_bestfriend' => 'best friend', + 'relationship_type_bestfriend_female' => 'best friend', + 'relationship_type_bestfriend_male' => 'best friend', + 'relationship_type_bestfriend_with_name' => ':name’s best friend', + 'relationship_type_bestfriend_female_with_name' => ':name’s best friend', + 'relationship_type_bestfriend_male_with_name' => ':name’s best friend', + + 'relationship_type_colleague' => 'colleague', + 'relationship_type_colleague_female' => 'colleague', + 'relationship_type_colleague_male' => 'colleague', + 'relationship_type_colleague_with_name' => ':name’s colleague', + 'relationship_type_colleague_female_with_name' => ':name’s colleague', + 'relationship_type_colleague_male_with_name' => ':name’s colleague', + + 'relationship_type_boss' => 'boss', + 'relationship_type_boss_female' => 'boss', + 'relationship_type_boss_male' => 'boss', + 'relationship_type_boss_with_name' => ':name’s boss', + 'relationship_type_boss_female_with_name' => ':name’s boss', + 'relationship_type_boss_male_with_name' => ':name’s boss', + + 'relationship_type_subordinate' => 'subordinate', + 'relationship_type_subordinate_female' => 'subordinate', + 'relationship_type_subordinate_male' => 'subordinate', + 'relationship_type_subordinate_with_name' => ':name’s subordinate', + 'relationship_type_subordinate_female_with_name' => ':name’s subordinate', + 'relationship_type_subordinate_male_with_name' => ':name’s subordinate', + + 'relationship_type_mentor' => 'mentor', + 'relationship_type_mentor_female' => 'mentor', + 'relationship_type_mentor_male' => 'mentor', + 'relationship_type_mentor_with_name' => ':name’s mentor', + 'relationship_type_mentor_female_with_name' => ':name’s mentor', + 'relationship_type_mentor_male_with_name' => ':name’s mentor', + + 'relationship_type_protege' => 'protégé', + 'relationship_type_protege_female' => 'protégé', + 'relationship_type_protege_male' => 'protégé', + 'relationship_type_protege_with_name' => ':name’s protégé', + 'relationship_type_protege_female_with_name' => ':name’s protégé', + 'relationship_type_protege_male_with_name' => ':name’s protégé', + + 'relationship_type_ex_husband' => 'ex-spouse', + 'relationship_type_ex_husband_female' => 'ex-wife', + 'relationship_type_ex_husband_male' => 'ex-husband', + 'relationship_type_ex_husband_with_name' => ':name’s ex-spouse', + 'relationship_type_ex_husband_female_with_name' => ':name’s ex-wife', + 'relationship_type_ex_husband_male_with_name' => ':name’s ex-husband', + + // emotions + 'emotion_primary_love' => 'Love', + 'emotion_primary_joy' => 'Joy', + 'emotion_primary_surprise' => 'Surprise', + 'emotion_primary_anger' => 'Anger', + 'emotion_primary_sadness' => 'Sadness', + 'emotion_primary_fear' => 'Fear', + + 'emotion_secondary_affection' => 'Affection', + 'emotion_secondary_lust' => 'Lust', + 'emotion_secondary_longing' => 'Longing', + 'emotion_secondary_cheerfulness' => 'Cheerfulness', + 'emotion_secondary_zest' => 'Zest', + 'emotion_secondary_contentment' => 'Contentment', + 'emotion_secondary_pride' => 'Pride', + 'emotion_secondary_optimism' => 'Optimism', + 'emotion_secondary_enthrallment' => 'Enthrallment', + 'emotion_secondary_relief' => 'Relief', + 'emotion_secondary_surprise' => 'Surprise', + 'emotion_secondary_irritation' => 'Irritation', + 'emotion_secondary_exasperation' => 'Exasperation', + 'emotion_secondary_rage' => 'Rage', + 'emotion_secondary_disgust' => 'Disgust', + 'emotion_secondary_envy' => 'Envy', + 'emotion_secondary_suffering' => 'Suffering', + 'emotion_secondary_sadness' => 'Sadness', + 'emotion_secondary_disappointment' => 'Disappointment', + 'emotion_secondary_shame' => 'Shame', + 'emotion_secondary_neglect' => 'Neglect', + 'emotion_secondary_sympathy' => 'Sympathy', + 'emotion_secondary_horror' => 'Horror', + 'emotion_secondary_nervousness' => 'Nervousness', + + 'emotion_adoration' => 'Adoration', + 'emotion_affection' => 'Affection', + 'emotion_love' => 'Love', + 'emotion_fondness' => 'Fondness', + 'emotion_liking' => 'Liking', + 'emotion_attraction' => 'Attraction', + 'emotion_caring' => 'Caring', + 'emotion_tenderness' => 'Tenderness', + 'emotion_compassion' => 'Compassion', + 'emotion_sentimentality' => 'Sentimentality', + 'emotion_arousal' => 'Arousal', + 'emotion_desire' => 'Desire', + 'emotion_lust' => 'Lust', + 'emotion_passion' => 'Passion', + 'emotion_infatuation' => 'Infatuation', + 'emotion_longing' => 'Longing', + 'emotion_amusement' => 'Amusement', + 'emotion_bliss' => 'Bliss', + 'emotion_cheerfulness' => 'Cheerfulness', + 'emotion_gaiety' => 'Gaiety', + 'emotion_glee' => 'Glee', + 'emotion_jolliness' => 'Jolliness', + 'emotion_joviality' => 'Joviality', + 'emotion_joy' => 'Joy', + 'emotion_delight' => 'Delight', + 'emotion_enjoyment' => 'Enjoyment', + 'emotion_gladness' => 'Gladness', + 'emotion_happiness' => 'Happiness', + 'emotion_jubilation' => 'Jubilation', + 'emotion_elation' => 'Elation', + 'emotion_satisfaction' => 'Satisfaction', + 'emotion_ecstasy' => 'Ecstasy', + 'emotion_euphoria' => 'Euphoria', + 'emotion_enthusiasm' => 'Enthusiasm', + 'emotion_zeal' => 'Zeal', + 'emotion_zest' => 'Zest', + 'emotion_excitement' => 'Excitement', + 'emotion_thrill' => 'Thrill', + 'emotion_exhilaration' => 'Exhilaration', + 'emotion_contentment' => 'Contentment', + 'emotion_pleasure' => 'Pleasure', + 'emotion_pride' => 'Pride', + 'emotion_eagerness' => 'Eagerness', + 'emotion_hope' => 'Hope', + 'emotion_optimism' => 'Optimism', + 'emotion_enthrallment' => 'Enthrallment', + 'emotion_rapture' => 'Rapture', + 'emotion_relief' => 'Relief', + 'emotion_amazement' => 'Amazement', + 'emotion_surprise' => 'Surprise', + 'emotion_astonishment' => 'Astonishment', + 'emotion_aggravation' => 'Aggravation', + 'emotion_irritation' => 'Irritation', + 'emotion_agitation' => 'Agitation', + 'emotion_annoyance' => 'Annoyance', + 'emotion_grouchiness' => 'Grouchiness', + 'emotion_grumpiness' => 'Grumpiness', + 'emotion_exasperation' => 'Exasperation', + 'emotion_frustration' => 'Frustration', + 'emotion_anger' => 'Anger', + 'emotion_rage' => 'Rage', + 'emotion_outrage' => 'Outrage', + 'emotion_fury' => 'Fury', + 'emotion_wrath' => 'Wrath', + 'emotion_hostility' => 'Hostility', + 'emotion_ferocity' => 'Ferocity', + 'emotion_bitterness' => 'Bitterness', + 'emotion_hate' => 'Hate', + 'emotion_loathing' => 'Loathing', + 'emotion_scorn' => 'Scorn', + 'emotion_spite' => 'Spite', + 'emotion_vengefulness' => 'Vengefulness', + 'emotion_dislike' => 'Dislike', + 'emotion_resentment' => 'Resentment', + 'emotion_disgust' => 'Disgust', + 'emotion_revulsion' => 'Revulsion', + 'emotion_contempt' => 'Contempt', + 'emotion_envy' => 'Envy', + 'emotion_jealousy' => 'Jealousy', + 'emotion_agony' => 'Agony', + 'emotion_suffering' => 'Suffering', + 'emotion_hurt' => 'Hurt', + 'emotion_anguish' => 'Anguish', + 'emotion_depression' => 'Depression', + 'emotion_despair' => 'Despair', + 'emotion_hopelessness' => 'Hopelessness', + 'emotion_gloom' => 'Gloom', + 'emotion_glumness' => 'Glumness', + 'emotion_sadness' => 'Sadness', + 'emotion_unhappiness' => 'Unhappiness', + 'emotion_grief' => 'Grief', + 'emotion_sorrow' => 'Sorrow', + 'emotion_woe' => 'Woe', + 'emotion_misery' => 'Misery', + 'emotion_melancholy' => 'Melancholy', + 'emotion_dismay' => 'Dismay', + 'emotion_disappointment' => 'Disappointment', + 'emotion_displeasure' => 'Displeasure', + 'emotion_guilt' => 'Guilt', + 'emotion_shame' => 'Shame', + 'emotion_regret' => 'Regret', + 'emotion_remorse' => 'Remorse', + 'emotion_alienation' => 'Alienation', + 'emotion_isolation' => 'Isolation', + 'emotion_neglect' => 'Neglect', + 'emotion_loneliness' => 'Loneliness', + 'emotion_rejection' => 'Rejection', + 'emotion_homesickness' => 'Homesickness', + 'emotion_defeat' => 'Defeat', + 'emotion_dejection' => 'Dejection', + 'emotion_insecurity' => 'Insecurity', + 'emotion_embarrassment' => 'Embarrassment', + 'emotion_humiliation' => 'Humiliation', + 'emotion_insult' => 'Insult', + 'emotion_pity' => 'Pity', + 'emotion_sympathy' => 'Sympathy', + 'emotion_alarm' => 'Alarm', + 'emotion_shock' => 'Shock', + 'emotion_fear' => 'Fear', + 'emotion_fright' => 'Fright', + 'emotion_horror' => 'Horror', + 'emotion_terror' => 'Terror', + 'emotion_panic' => 'Panic', + 'emotion_hysteria' => 'Hysteria', + 'emotion_mortification' => 'Mortification', + 'emotion_anxiety' => 'Anxiety', + 'emotion_nervousness' => 'Nervousness', + 'emotion_tenseness' => 'Tenseness', + 'emotion_uneasiness' => 'Uneasiness', + 'emotion_apprehension' => 'Apprehension', + 'emotion_worry' => 'Worry', + 'emotion_distress' => 'Distress', + 'emotion_dread' => 'Dread', + + // weather + 'weather_sunny' => 'Sunny', + 'weather_clear' => 'Clear', + 'weather_clear-day' => 'Clear', + 'weather_clear-night' => 'Clear night', + 'weather_light-drizzle' => 'Light drizzle', + 'weather_patchy-light-drizzle' => 'Patchy light drizzle', + 'weather_patchy-light-rain' => 'Patchy light rain', + 'weather_light-rain' => 'Light rain', + 'weather_moderate-rain-at-times' => 'Moderate rain at times', + 'weather_moderate-rain' => 'Moderate rain', + 'weather_patchy-rain-possible' => 'Patchy rain possible', + 'weather_heavy-rain-at-times' => 'Heavy rain at times', + 'weather_heavy-rain' => 'Heavy rain', + 'weather_light-freezing-rain' => 'Light freezing rain', + 'weather_moderate-or-heavy-freezing-rain' => 'Moderate or heavy freezing rain', + 'weather_light-sleet' => 'Light sleet', + 'weather_moderate-or-heavy-rain-shower' => 'Moderate or heavy rain shower', + 'weather_light-rain-shower' => 'Light rain shower', + 'weather_torrential-rain-shower' => 'Torrential rain shower', + 'weather_rain' => 'Rain', + 'weather_snow' => 'Snow', + 'weather_blowing-snow' => 'Blowing snow', + 'weather_patchy-light-snow' => 'Patchy light snow', + 'weather_light-snow' => 'Light snow', + 'weather_patchy-moderate-snow' => 'Patchy moderate snow', + 'weather_moderate-snow' => 'Moderate snow', + 'weather_patchy-heavy-snow' => 'Patchy heavy snow', + 'weather_heavy-snow' => 'Heavy snow', + 'weather_light-snow-showers' => 'Light snow showers', + 'weather_moderate-or-heavy-snow-showers' => 'Moderate or heavy snow showers', + 'weather_patchy-snow-possible' => 'Patchy snow possible', + 'weather_patchy-sleet-possible' => 'Patchy sleet possible', + 'weather_moderate-or-heavy-sleet' => 'Moderate or heavy sleet', + 'weather_light-sleet-showers' => 'Light sleet showers', + 'weather_moderate-or-heavy-sleet-showers' => 'Moderate or heavy sleet showers', + 'weather_sleet' => 'Sleet', + 'weather_wind' => 'Wind', + 'weather_fog' => 'Fog', + 'weather_freezing-fog' => 'Freezing fog', + 'weather_mist' => 'Mist', + 'weather_blizzard' => 'Blizzard', + 'weather_overcast' => 'Overcast', + 'weather_cloudy' => 'Cloudy', + 'weather_partly-cloudy-day' => 'Partly cloudy', + 'weather_partly-cloudy-night' => 'Partly cloudy', + 'weather_freezing-drizzle' => 'Freezing drizzle', + 'weather_heavy-freezing-drizzle' => 'Heavy freezing drizzle', + 'weather_patchy-freezing-drizzle-possible' => 'Patchy freezing drizzle possible', + 'weather_ice-pellets' => 'Ice pellets', + 'weather_light-showers-of-ice-pellets' => 'Light showers of ice pellets', + 'weather_moderate-or-heavy-showers-of-ice-pellets' => 'Moderate or heavy showers of ice pellets', + 'weather_thundery-outbreaks-possible' => 'Thundery outbreaks possible', + 'weather_patchy-light-rain-with-thunder' => 'Patchy light rain with thunder', + 'weather_moderate-or-heavy-rain-with-thunder' => 'Moderate or heavy rain with thunder', + 'weather_patchy-light-snow-with-thunder' => 'Patchy light snow with thunder', + 'weather_moderate-or-heavy-snow-with-thunder' => 'Moderate or heavy snow with thunder', + 'weather_current_temperature_celsius' => ':temperature °C', + 'weather_current_temperature_fahrenheit' => ':temperature °F', + 'weather_current_title' => 'Current weather', + + // dav + 'dav_contacts' => 'Contacts', + 'dav_contacts_description' => ':name’s contacts', + 'dav_birthdays' => 'Birthdays', + 'dav_birthdays_description' => ':name’s contact’s birthdays', + 'dav_tasks' => 'Tasks', + 'dav_tasks_description' => ':name’s tasks', + + // contact list + 'contact_list_avatar' => 'Avatar', + 'contact_list_name' => 'Contact', + 'contact_list_description' => 'Description', + +]; diff --git a/resources/lang/uk/auth.php b/resources/lang/uk/auth.php new file mode 100644 index 0000000..73e0db0 --- /dev/null +++ b/resources/lang/uk/auth.php @@ -0,0 +1,89 @@ + 'These credentials do not match our records.', + 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.', + 'not_authorized' => 'You are not authorized to execute this action', + 'signup_disabled' => 'Registration is currently disabled', + 'signup_error' => 'An error occured trying to register the user', + 'back_homepage' => 'Back to homepage', + 'mfa_auth_otp' => 'Authenticate with your two factor device', + 'mfa_auth_webauthn' => 'Authenticate with a security key (WebAuthn)', + '2fa_title' => 'Two Factor Authentication', + '2fa_wrong_validation' => 'The two factor authentication has failed.', + '2fa_one_time_password' => 'Two factor authentication code', + '2fa_recuperation_code' => 'Enter a two factor recovery code', + '2fa_one_time_or_recuperation' => 'Enter a two factor authentication code or a recovery code', + '2fa_otp_help' => 'Open up your two factor authentication mobile app and copy the code', + + 'login_to_account' => 'Login to your account', + 'login_with_recovery' => 'Login with a recovery code', + 'login_again' => 'Please login again to your account', + 'email' => 'Email', + 'password' => 'Password', + 'recovery' => 'Recovery code', + 'login' => 'Login', + 'button_remember' => 'Remember Me', + 'password_forget' => 'Forget your password?', + 'password_reset' => 'Reset your password', + 'use_recovery' => 'Or you can use a recovery code', + 'signup_no_account' => 'Don’t have an account?', + 'signup' => 'Sign up', + 'create_account' => 'Create the first account by signing up', + 'change_language_title' => 'Change language:', + 'change_language' => 'Change language to :lang', + + 'password_reset_title' => 'Reset Password', + 'password_reset_email' => 'E-Mail Address', + 'password_reset_send_link' => 'Send Password Reset Link', + 'password_reset_password' => 'Password', + 'password_reset_password_confirm' => 'Confirm Password', + 'password_reset_action' => 'Reset Password', + 'password_reset_email_content' => 'Click here to reset your password:', + + 'register_title_welcome' => 'Welcome to your newly installed Monica instance', + 'register_create_account' => 'You need to create an account to use Monica', + 'register_title_create' => 'Create your Monica account', + 'register_login' => 'Log in if you already have an account.', + 'register_email' => 'Enter a valid email address', + 'register_email_example' => 'you@home', + 'register_firstname' => 'First name', + 'register_firstname_example' => 'eg. John', + 'register_lastname' => 'Last name', + 'register_lastname_example' => 'eg. Doe', + 'register_password' => 'Password', + 'register_password_example' => 'Enter a secure password', + 'register_password_confirmation' => 'Password confirmation', + 'register_action' => 'Register', + 'register_policy' => 'Signing up signifies you’ve read and agree to our Privacy Policy and Terms of use.', + 'register_invitation_email' => 'For security purposes, please indicate the email of the person who’ve invited you to join this account. This information is provided in the invitation email.', + + 'confirmation_title' => 'Verify Your Email Address', + 'confirmation_fresh' => 'A fresh verification link has been sent to your email address.', + 'confirmation_check' => 'Before proceeding, please check your email for a verification link.', + 'confirmation_request_another' => 'If you did not receive the email click here to request another.', + + 'confirmation_again' => 'If you want to change your email address you can click here.', + 'email_change_current_email' => 'Current email address:', + 'email_change_title' => 'Change your email address', + 'email_change_new' => 'New email address', + 'email_changed' => 'Your email address has been changed. Check your mailbox to validate it.', +]; diff --git a/resources/lang/uk/changelog.php b/resources/lang/uk/changelog.php new file mode 100644 index 0000000..981b018 --- /dev/null +++ b/resources/lang/uk/changelog.php @@ -0,0 +1,12 @@ + 'Product changes', + 'note' => 'Note: unfortunately, this page is only in English.', +]; diff --git a/resources/lang/uk/dashboard.php b/resources/lang/uk/dashboard.php new file mode 100644 index 0000000..5190352 --- /dev/null +++ b/resources/lang/uk/dashboard.php @@ -0,0 +1,42 @@ + 'Welcome to your account!', + 'dashboard_blank_description' => 'Monica is the place to organize all the interactions you have with the people you care about.', + 'dashboard_blank_cta' => 'Add your first contact', + 'dashboard_blank_illustration' => 'Illustration by Freepik', + + 'notes_title' => 'You don’t have any starred notes yet.', + + 'tab_recent_calls' => 'Recent calls', + 'tab_favorite_notes' => 'Favorite notes', + 'tab_calls_blank' => 'You haven’t logged any calls yet.', + 'tab_debts' => 'Debts', + 'tab_debts_blank' => 'You haven’t logged any debts yet.', + 'tab_tasks' => 'Tasks', + 'tab_tasks_blank' => 'You haven’t any tasks yet.', + + 'tasks_add_task_placeholder' => 'What is this task about?', + 'tasks_tab_your_contacts' => 'Tasks related to your contacts', + 'tasks_tab_your_tasks' => 'Your tasks', + 'tasks_add_note' => 'Press Enter to add the task.', + 'task_add_cta' => 'Add a task', + + 'debts_you_owe' => 'You owe', + + 'statistics_contacts' => 'Contacts', + 'statistics_activities' => 'Activities', + 'statistics_gifts' => 'Gifts', + + 'reminders_next_months' => 'Events in the next 3 months', + 'reminders_none' => 'No reminders for this month.', + + 'product_changes' => 'Product changes', + 'product_view_details' => 'View details', +]; diff --git a/resources/lang/uk/format.php b/resources/lang/uk/format.php new file mode 100644 index 0000000..a70a6ba --- /dev/null +++ b/resources/lang/uk/format.php @@ -0,0 +1,36 @@ + 'M d, Y H:i', + 'short_date_year' => 'M d, Y', + 'short_date' => 'M d', + 'short_month' => 'M', + 'short_month_year' => 'M Y', + 'short_day' => 'D', + 'full_date_year' => 'F d, Y', + 'full_month' => 'F', + 'full_month_year' => 'F Y', + 'full_hour' => 'h.i A', + + 'short_text' => '{text}…', +]; diff --git a/resources/lang/uk/journal.php b/resources/lang/uk/journal.php new file mode 100644 index 0000000..9b1f0be --- /dev/null +++ b/resources/lang/uk/journal.php @@ -0,0 +1,38 @@ + 'How was your day? You can rate it once a day.', + 'journal_come_back' => 'Thanks. Come back tomorrow to rate your day again.', + 'journal_description' => 'Note: the journal lists both manual journal entries, and automatic entries like Activities done with your contacts. While you can delete journal entries manually, you’ll have to delete the activity directly on the contact page.', + 'journal_add' => 'Add a journal entry', + 'journal_edit' => 'Edit a journal entry', + 'journal_empty' => 'Empty journal', + 'journal_created_at' => 'Created at {date}', + 'journal_created_automatically' => 'Created automatically', + 'journal_entry_type_journal' => 'Journal entry', + 'journal_entry_type_activity' => 'Activity', + 'journal_entry_rate' => 'You rated your day.', + 'journal_add_comment' => 'Care to add a comment (optional)?', + 'journal_show_comment' => 'Show comment', + 'entry_delete_success' => 'The journal entry has been successfully deleted.', + 'journal_add_title' => 'Title (optional)', + 'journal_add_date' => 'Date', + 'journal_add_post' => 'Entry', + 'journal_add_cta' => 'Save', + 'journal_blank_cta' => 'Add your first journal entry', + 'journal_blank_description' => 'The journal lets you write events that happened to you, and remember them.', + 'delete_confirmation' => 'Are you sure you want to delete this journal entry?', + 'apply_filter' => 'Apply filter', + 'start_date' => 'Start Date', + 'end_date' => 'End Date', + 'per_page' => 'Per Page', + 'sort_order' => 'Sort By Created At', + 'ascending' => 'Ascending', + 'descending' => 'Descending', +]; diff --git a/resources/lang/uk/logs.php b/resources/lang/uk/logs.php new file mode 100644 index 0000000..7b6654b --- /dev/null +++ b/resources/lang/uk/logs.php @@ -0,0 +1,29 @@ + 'Created the contact.', + 'settings_log_contact_created_with_name' => 'Added :name as a contact.', + + // contat description update + 'contact_log_contact_description_updated' => 'Updated the description.', + 'settings_log_contact_description_updated_with_name' => 'Updated the description of :name.', + + // contact description clear + 'contact_log_contact_description_cleared' => 'Cleared the description.', + 'settings_log_contact_description_cleared_with_name' => 'Cleared the description of :name.', + + // contact work information update + 'contact_log_contact_work_updated' => 'Updated work information.', + 'settings_log_contact_work_updated_with_name' => 'Updated work information of :name.', + + // company created + 'settings_log_company_created' => 'Created a company called :name.', +]; diff --git a/resources/lang/uk/mail.php b/resources/lang/uk/mail.php new file mode 100644 index 0000000..749f3d1 --- /dev/null +++ b/resources/lang/uk/mail.php @@ -0,0 +1,53 @@ + 'Reminder for :contact', + 'greetings' => 'Hi :username', + 'want_reminded_of' => 'You wanted to be reminded of :reason', + 'for' => 'For: :name', + 'comment' => 'Comment: :comment', + 'footer_contact_info' => 'Add, view, complete, and change information about this contact:', + 'footer_contact_info2' => 'See :name’s profile', + 'footer_contact_info2_link' => 'See :name’s profile: :url', + + 'notification_subject_line' => 'You have an upcoming event', + 'notification_description' => 'In :count days (on :date), the following event will happen:', + + 'stay_in_touch_subject_line' => 'Stay in touch with :name', + 'stay_in_touch_subject_description' => 'You asked to be reminded to stay in touch with :name every :frequency day.|You asked to be reminded to stay in touch with :name every :frequency days.', + + 'notifications_whoops' => 'Whoops!', + 'notifications_hello' => 'Hello!', + 'notifications_regards' => 'Regards', + 'notifications_footer' => 'If you’re having trouble clicking the ":actionText" button, copy and paste the URL below into your web browser: [:actionURL](:actionURL)', + 'notifications_rights' => 'All rights reserved', + + 'confirmation_email_title' => 'Monica – Email verification', + 'confirmation_email_intro'=> 'To validate your email click on the button below', + 'confirmation_email_button' => 'Verify email address', + 'confirmation_email_bottom' => 'If you did not create an account, no further action is required.', + + 'password_reset_title' => 'Monica – Reset Password Notification', + 'password_reset_intro' => 'You are receiving this email because we received a password reset request for your account.', + 'password_reset_button' => 'Reset Password', + 'password_reset_expiration' => 'This password reset link will expire in :count minutes.', + 'password_reset_bottom' => 'If you did not request a password reset, no further action is required.', + + 'invitation_title' => 'Monica – You are invited by :name', + 'invitation_intro' => 'You’ve been invited by :name (:email) to use Monica, a nice Personal Relationship Management tool.', + 'invitation_link' => 'To accept the invitation, click on the link below:', + 'invitation_button' => 'Accept invitation', + 'invitation_expiration' => 'This link will expire in :count days.', + + 'export_title' => 'Your export is ready', + 'export_description' => 'You requested a data export on :date. It is now ready to download.', + 'export_download' => 'Download export', + +]; diff --git a/resources/lang/uk/pagination.php b/resources/lang/uk/pagination.php new file mode 100644 index 0000000..d663041 --- /dev/null +++ b/resources/lang/uk/pagination.php @@ -0,0 +1,25 @@ + '❮ Previous', + 'next' => 'Next ❯', + +]; diff --git a/resources/lang/uk/passwords.php b/resources/lang/uk/passwords.php new file mode 100644 index 0000000..1487bb9 --- /dev/null +++ b/resources/lang/uk/passwords.php @@ -0,0 +1,30 @@ + 'Your password has been reset!', + 'sent' => 'If the email you entered exists in our records, you’ve been sent a password reset link.', + 'token' => 'This password reset token is invalid.', + 'user' => 'If the email you entered exists in our records, you’ve been sent a password reset link.', + 'changed' => 'Password changed successfully.', + 'invalid' => 'Current password you entered is not correct.', + 'throttled' => 'Please wait before retrying.', + +]; diff --git a/resources/lang/uk/people.php b/resources/lang/uk/people.php new file mode 100644 index 0000000..011e6e1 --- /dev/null +++ b/resources/lang/uk/people.php @@ -0,0 +1,539 @@ + 'Контакт не знайдено', + 'people_list_number_kids' => ':count дитина|:count дітей', + 'people_list_last_updated' => 'Остання консультація:', + 'people_list_number_reminders' => ':count нагадування|:count нагадувань', + 'people_list_blank_title' => 'У вас ще нікого немає', + 'people_list_blank_cta' => 'Додати когось', + 'people_list_sort' => 'Упорядкувати', + 'people_list_stats' => ':count контакт|:count контактів', + 'people_list_firstnameAZ' => 'Сортувати за іменем А → Я', + 'people_list_firstnameZA' => 'Сортувати за іменем Я → А', + 'people_list_lastnameAZ' => 'Сортувати за прізвищем А → Я', + 'people_list_lastnameZA' => 'Сортувати за прізвищем Я → А', + 'people_list_lastactivitydateNewtoOld' => 'Сортувати за датою останньої активності, від новіших до старіших', + 'people_list_lastactivitydateOldtoNew' => 'Сортувати за датою останньої активності, від старіших до новіших', + 'people_list_filter_tag' => 'Показано всі контакти, відмічені', + 'people_list_clear_filter' => 'Очистити фільтр', + 'people_list_contacts_per_tags' => ':count контакт|:count контактів', + 'people_list_show_dead' => 'Показати померлих (:count)', + 'people_list_hide_dead' => 'Приховати померлих людей (:count)', + 'people_search' => 'Знайти ваші контакти…', + 'people_search_no_results' => 'Результатів не знайдено', + 'people_search_next' => 'Далі', + 'people_search_prev' => 'Назад', + 'people_search_rows_per_page' => 'Рядків на сторінку', + 'people_search_of' => 'з', + 'people_search_page' => 'Сторінка', + 'people_search_all' => 'Усі', + 'people_add_new' => 'Додати нову особу', + 'people_list_account_usage' => 'Ваш обліковий запис використовує :current/:limit контактів', + 'people_list_account_upgrade_title' => 'Оновіть свій обліковий запис, щоб використвувати його на повну потужність.', + 'people_list_account_upgrade_cta' => 'Оновіть тарифний план', + 'people_list_untagged' => 'Переглянути контакти без міток', + 'people_list_filter_untag' => 'Показано всі контакти без міток', + 'archived_contact_readonly' => 'Архівні контакти не можна редагувати. Будь ласка, спершу розархівуйте.', + + // people add + 'people_add_title' => 'Додати нову особу', + 'people_add_missing' => 'Не знайдено жодної людини – додайте нову зараз', + 'people_add_firstname' => 'Ім\'я', + 'people_add_middlename' => 'По батькові (необовʼязково)', + 'people_add_lastname' => 'Прізвище (необовʼязково)', + 'people_add_email' => 'Електронна пошта (необовʼязково)', + 'people_add_nickname' => 'Прізвисько (необовʼязково)', + 'people_add_cta' => 'Додати', + 'people_save_and_add_another_cta' => 'Надіслати і додати когось іншого', + 'people_add_success' => ':name було успішно створено', + 'people_add_gender' => 'Стать', + 'people_delete_success' => 'Контакт видалено', + 'people_delete_message' => 'Видалити контакт', + 'people_delete_confirmation' => 'Ви впевнені, що хочете видалити :name? Видалення є миттєвим і незворотним.', + 'people_add_birthday_reminder' => 'Привітайте з днем народження :name', + 'people_add_birthday_reminder_deceased' => 'У цей день :name святкував би свій день народження', + 'people_add_import' => 'Ви хочете імпортувати ваші контакти?', + 'people_edit_email_error' => 'Обліковий запис з цією електронною адресою вже існує. Будь ласка, оберіть іншу.', + 'people_export' => 'Експортувати як vCard', + 'people_add_reminder_for_birthday' => 'Створити щорічне нагадування про день народження', + + // show + 'section_contact_information' => 'Контактна інформація', + 'section_personal_activities' => 'Активності', + 'section_personal_reminders' => 'Нагадування', + 'section_personal_tasks' => 'Завдання', + 'section_personal_gifts' => 'Подарунки', + 'section_personal_notes' => 'Нотатки', + + // archived contacts + 'list_link_to_active_contacts' => 'Ви переглядаєте архівовані контакти. Подивіться список активних контактів.', + 'list_link_to_archived_contacts' => 'Список архівованих контактів', + + // Header + 'me' => 'Це ви', + 'edit_contact_information' => 'Редагувати контактну інформацію', + 'contact_archive' => 'Архівувати контакт', + 'contact_unarchive' => 'Розархівувати контакт', + 'contact_archive_help' => 'Архівовані контакти не показуються в списку контактів, але показуються в результатах пошуку.', + 'call_button' => 'Записати дзвінок', + 'set_favorite' => 'Улюблені контакти показуються на початку списку контактів', + + // Stay in touch + 'stay_in_touch' => 'Підтримуйте звʼязок', + 'stay_in_touch_frequency' => 'Звʼязуйтеся кожного дня|Звʼязуйтеся кожні {count} днів', + 'stay_in_touch_next_date' => 'Наступна подія: {date}', + 'stay_in_touch_invalid' => 'Частота має більшою за 0.', + 'stay_in_touch_premium' => 'You need to upgrade your account to make use of this feature', + 'stay_in_touch_modal_title' => 'Підтримуйте звʼязок', + 'stay_in_touch_modal_desc' => 'Ми можемо періодично нагадувати вам електронною поштою про підтримування звʼязку з {firstname}.', + 'stay_in_touch_modal_label' => 'Відправляти мені електронного листа кожен… {count} день|Відправляти мені електронного листа кожні… {count} дні', + + // Calls + 'modal_call_title' => 'Записати дзвінок', + 'modal_call_comment' => 'Про що ви говорили? (необовʼязково)', + 'modal_call_exact_date' => 'Дзвінок відбувся', + 'modal_call_who_called' => 'Хто дзвонив?', + 'modal_call_emotion' => 'Хочете записати, як ви почувалися під час цього дзвінка? (необовʼязково)', + 'calls_add_success' => 'Дзвінок збережено.', + 'call_delete_confirmation' => 'Ви точно хочете видалити цей дзвінок?', + 'call_delete_success' => 'Дзвінок було успішно видалено', + 'call_title' => 'Дзвінки', + 'call_empty_comment' => 'Немає подробиць', + 'call_blank_title' => 'Записуйте спільні дзвінки з {name}', + 'call_blank_desc' => 'Ви дзвонили до {name}', + 'call_you_called' => 'Ви дзвонили', + 'call_he_called' => '{name} дзвонив', + 'call_emotions' => 'Емоції:', + + // Conversation + 'conversation_blank' => 'Записуйте розмови з :name у соціальних мережах, SMS…', + 'conversation_delete_link' => 'Видалити розмову', + 'conversation_edit_title' => 'Редагувати розмову', + 'conversation_edit_delete' => 'Ви точно хочете видалити цю розмову? Видалення є незворотним.', + 'conversation_add_success' => 'Розмову було успішно додано.', + 'conversation_edit_success' => 'Розмову було успішно змінено.', + 'conversation_delete_success' => 'Розмову було успішно видалено.', + 'conversation_add_title' => 'Записати нову розмову', + 'conversation_add_when' => 'Коли ця розмова відбулася?', + 'conversation_add_who_wrote' => 'Хто відправив це повідомлення?', + 'conversation_add_how' => 'Як ви спілкувалися?', + 'conversation_add_you' => 'Ви', + 'conversation_add_content' => 'Запишіть, що було сказано', + 'conversation_add_what_was_said' => 'Що ви сказали?', + 'conversation_add_another' => 'Додати ще повідомлення', + 'conversation_add_error' => 'Вам необхідно додати хоча б одне повідомлення.', + 'conversation_list_table_messages' => 'Повідомлення', + 'conversation_list_table_content' => 'Частковий вміст (останнє повідомлення)', + 'conversation_list_title' => 'Бесіди', + 'conversation_list_cta' => 'Журнал розмов', + + // age - birthday + 'birthdate_not_set' => 'День народження не вказано', + 'age_approximate_in_years' => 'близько :age років', + 'age_exact_in_years' => ':age років', + 'age_exact_birthdate' => 'народився :date', + + // Last called + 'last_called' => 'Останній виклик: :date', + 'last_talked_to' => 'Останній дзвінок: {date}', + 'last_called_empty' => 'Останній виклик: невідомо', + 'last_activity_date' => 'Остання активність разом: :date', + 'last_activity_date_empty' => 'Остання активність разом: невідомо', + + // additional information + 'information_edit_success' => 'Профіль було успішно оновлено', + 'information_edit_title' => 'Редагувати особисту інформацію :name', + 'information_edit_max_size' => 'Максимально :size кілобайт.', + 'information_edit_max_size2' => 'Максимально :size кілобайт.', + 'information_edit_firstname' => 'Ім\'я', + 'information_edit_lastname' => 'Прізвище (необовʼязково)', + 'information_edit_description' => 'Опис (необовʼязково)', + 'information_edit_description_help' => 'Використовується в списку контактів, щоб додати деякий контекст, якщо це необхідно.', + 'information_edit_unknown' => 'Я не знаю віку цієї людини', + 'information_edit_probably' => 'Ця людина, ймовірно,…', + 'information_edit_not_year' => 'Я знаю день і місяць народження цієї людини, але не рік…', + 'information_edit_exact' => 'Я знаю точний день народження цієї людини…', + 'information_edit_birthdate_label' => 'День народження', + 'information_no_work_defined' => 'Інформацію про роботу не визначено', + 'information_work_at' => 'у :company', + 'work_add_cta' => 'Оновити інформацію про роботу', + 'work_edit_success' => 'Інформація про роботу оновлена', + 'work_edit_title' => 'Оновити інформацію про роботу :name', + 'work_edit_job' => 'Посада (необов\'язково)', + 'work_edit_company' => 'Компанія (необов\'язково)', + 'work_information' => 'Інформація про роботу', + + // food preferences + 'food_preferences_add_success' => 'Вподобання у їжі збережено', + 'food_preferences_edit_description' => 'Можливо, :firstname або хтось у родині :family має алергію або не любить конкретну пляшку вина. Вкажіть їх тут, щоб ви пам\'ятали наступного разу, коли запросите їх на вечерю', + 'food_preferences_edit_description_no_last_name' => 'Можливо, :firstname або хтось у родині :family має алергію або не любить конкретну пляшку вина. Вкажіть їх тут, щоб ви пам\'ятали наступного разу, коли запросите їх на вечерю', + 'food_preferences_edit_title' => 'Вкажіть вподобання у їжі', + 'food_preferences_edit_cta' => 'Зберегти уподобання їжі', + 'food_preferences_title' => 'Вподобання їжі', + 'food_preferences_cta' => 'Додати уподобання їжі', + + // reminders + 'reminders_blank_title' => 'Хочете отримати нагадування про щось щодо :name?', + 'reminders_blank_add_activity' => 'Додати нагадування', + 'reminders_add_title' => 'Про що ви хочете отримати нагадування щодо :name?', + 'reminders_add_description' => 'Будь ласка, нагадайте мені…', + 'reminders_add_next_time' => 'Коли ви хочете отримати наступне нагадування про це?', + 'reminders_add_once' => 'Нагадати один раз', + 'reminders_add_recurrent' => 'Нагадувати кожні', + 'reminders_add_starting_from' => 'від вказаної вище дати', + 'reminders_add_cta' => 'Додати нагадування', + 'reminders_edit_update_cta' => 'Змінити нагадування', + 'reminders_add_error_custom_text' => 'Необхідно вказати текст цього нагадування', + 'reminders_create_success' => 'Нагадування було успішно додано', + 'reminders_delete_success' => 'Нагадування було успішно видалено', + 'reminders_update_success' => 'Нагадування було успішно змінено', + 'reminders_add_optional_comment' => 'Додатковий коментар', + + 'reminder_frequency_day' => 'щодня|кожні :number днів', + 'reminder_frequency_week' => 'щотижня|кожні :number тижнів', + 'reminder_frequency_month' => 'щомісяця|кожні :number місяців', + 'reminder_frequency_year' => 'щороку|кожні :number років', + 'reminder_frequency_one_time' => ':date', + 'reminders_delete_confirmation' => 'Ви точно хочете видалити це нагадування?', + 'reminders_delete_cta' => 'Видалити', + 'reminders_next_expected_date' => 'on', + 'reminders_cta' => 'Додати нагадування', + 'reminders_description' => 'Ми відправимо електронного листа з кожним з перелічених далі нагадуванням. Нагадування відправляються зранку у день події. Нагадування про дні народження додаються автоматично і не можуть бути видаленими. Якщо хочете змінити дати, відредагуйте день народження контактів.', + 'reminders_one_time' => 'Один раз', + 'reminders_type_week' => 'тиждень', + 'reminders_type_month' => 'місяць', + 'reminders_type_year' => 'рік', + 'reminders_birthday' => 'День народження :name', + 'reminders_free_plan_warning' => 'You are on the Free plan. No emails are sent on this plan. To receive your reminders by email, upgrade your account.', + + // relationships + 'relationship_form_add' => 'Додати новий звʼязок', + 'relationship_form_edit' => 'Редагувати звʼязок', + 'relationship_form_is_with' => 'Ця людина є…', + 'relationship_form_is_with_name' => ':name …', + 'relationship_form_add_choice' => 'Who is the relationship with?', + 'relationship_form_create_contact' => 'Додати нову особу', + 'relationship_form_associate_contact' => 'Існуючий контакт', + 'relationship_form_associate_dropdown' => 'Search and select an existing contact from the dropdown below', + 'relationship_form_associate_dropdown_placeholder' => 'Search and select an existing contact', + 'relationship_form_also_create_contact' => 'Create a Contact entry for this person.', + 'relationship_form_add_description' => 'This will let you treat this person like any other contact.', + 'relationship_form_add_no_existing_contact' => 'You don’t have any contacts who can be related to :name at the moment.', + 'relationship_delete_confirmation' => 'Are you sure you want to delete this relationship? Deletion is permanent.', + 'relationship_unlink_confirmation' => 'Are you sure you want to delete this relationship? This person will not be deleted – only the relationship between the two.', + 'relationship_form_add_success' => 'The relationship has been successfully set.', + 'relationship_form_deletion_success' => 'The relationship has been deleted.', + + // tasks + 'tasks_title' => 'Завдання', + 'tasks_blank_title' => 'У тебе немає ніяких завдань.', + 'tasks_form_title' => 'Title', + 'tasks_form_description' => 'Description (optional)', + 'tasks_add_task' => 'Додати завдання', + 'tasks_delete_success' => 'The task has been deleted successfully', + 'tasks_complete_success' => 'The task has changed status successfully', + + // activities + 'activity_title' => 'Activities', + 'activity_type_category_simple_activities' => 'Simple activities', + 'activity_type_category_sport' => 'Спорт', + 'activity_type_category_food' => 'Їжа', + 'activity_type_category_cultural_activities' => 'Cultural activities', + 'activity_type_just_hung_out' => 'just hung out', + 'activity_type_watched_movie_at_home' => 'подивилися фільм вдома', + 'activity_type_talked_at_home' => 'просто побалакали вдома', + 'activity_type_did_sport_activities_together' => 'пограли разом в спорт', + 'activity_type_ate_at_his_place' => 'поїли у нього/неї', + 'activity_type_went_bar' => 'сходили до бару', + 'activity_type_ate_at_home' => 'поїли вдома', + 'activity_type_picnicked' => 'picnicked', + 'activity_type_ate_restaurant' => 'ate at a restaurant', + 'activity_type_went_theater' => 'went to the theater', + 'activity_type_went_concert' => 'went to a concert', + 'activity_type_went_play' => 'went to a play', + 'activity_type_went_museum' => 'went to the museum', + 'activities_add_activity' => 'Add activity', + 'activities_add_more_details' => 'Add more details', + 'activities_add_emotions' => 'Add emotions', + 'activities_add_category' => 'Indicate a category', + 'activities_add_participants_cta' => 'Add participants', + 'activities_item_information' => ':Activity. Happened on :date', + 'activities_add_title' => 'What did you do with {name}?', + 'activities_summary' => 'Describe what you did', + 'activities_add_pick_activity' => 'Would you like to categorize this activity? You don’t have to, but it will give you statistics later on (optional)', + 'activities_add_date_occured' => 'The activity happened on…', + 'activities_add_participants' => 'Who, apart from {name}, participated in this activity? (optional)', + 'activities_add_emotions_title' => 'Do you want to log how you felt during this activity? (optional)', + 'activities_blank_title' => 'Keep track of what you’ve done with {name} in the past, and what you’ve talked about', + 'activities_blank_add_activity' => 'Add an activity', + 'activities_add_success' => 'The activity has been added successfully', + 'activities_add_error' => 'Error when adding the activity', + 'activities_update_success' => 'The activity has been updated successfully', + 'activities_delete_success' => 'The activity has been deleted successfully', + 'activities_who_was_involved' => 'Who was involved?', + 'activities_activity' => 'Activity Category', + 'activities_view_activities_report' => 'View activities report', + 'activities_profile_title' => 'Activities report between :name and you', + 'activities_profile_subtitle' => 'You’ve logged :total_activities activity with :name in total and :activities_last_twelve_months in the last 12 months so far.|You’ve logged :total_activities activities with :name in total and :activities_last_twelve_months in the last 12 months so far.', + 'activities_profile_year_summary_activity_types' => 'Here is a breakdown of the type of activities you’ve done together in :year', + 'activities_profile_year_summary' => 'Here is what you two have done in :year', + 'activities_profile_number_occurences' => ':value activity|:value activities', + 'activities_list_participants' => 'Participants ({total}):', + 'activities_list_emotions' => 'Emotions felt:', + 'activities_list_date' => 'Happened on', + 'activities_list_category' => 'Category:', + + // notes + 'notes_create_success' => 'The note has been created successfully', + 'notes_update_success' => 'The note has been saved successfully', + 'notes_delete_success' => 'The note has been deleted successfully', + 'notes_add_cta' => 'Add note', + 'notes_favorite' => 'Add/remove from favorites', + 'notes_delete_title' => 'Delete a note', + 'notes_delete_confirmation' => 'Are you sure you want to delete this note? Deletion is permanent', + + // gifts + 'gifts_title' => 'Gifts', + 'gifts_add_success' => 'The gift has been added successfully', + 'gifts_delete_success' => 'The gift has been deleted successfully', + 'gifts_delete_confirmation' => 'Are you sure you want to delete this gift?', + 'gifts_add_gift' => 'Add a gift', + 'gifts_link' => 'Link', + 'gifts_for' => 'For: {name}', + 'gifts_delete_cta' => 'Delete', + 'gifts_add_title' => 'Gift management for :name', + 'gifts_add_gift_idea' => 'Gift idea', + 'gifts_add_gift_already_offered' => 'Gift given', + 'gifts_add_gift_received' => 'Gift received', + 'gifts_add_gift_title' => 'What is this gift?', + 'gifts_add_gift_name' => 'Gift name', + 'gifts_add_link' => 'Link to the web page (optional)', + 'gifts_add_value' => 'Value (optional)', + 'gifts_add_comment' => 'Comment (optional)', + 'gifts_add_recipient' => 'Recipient (optional)', + 'gifts_add_recipient_field' => 'Recipient', + 'gifts_add_photo' => 'Photo (optional)', + 'gifts_add_photo_title' => 'Add a photo for this gift', + 'gifts_add_someone' => 'This gift is for someone in {name}’s family in particular', + 'gifts_delete_title' => 'Delete a gift', + 'gifts_ideas' => 'Gift ideas', + 'gifts_offered' => 'Gifts given', + 'gifts_offered_as_an_idea' => 'Mark as an idea', + 'gifts_received' => 'Gifts received', + 'gifts_view_comment' => 'View comment', + 'gifts_mark_offered' => 'Mark as given', + 'gifts_update_success' => 'The gift has been updated successfully', + 'gifts_add_date' => 'Date (optional)', + + // debts + 'debt_delete_confirmation' => 'Are you sure you want to delete this debt?', + 'debt_delete_success' => 'The debt has been deleted successfully', + 'debt_add_success' => 'The debt has been added successfully', + 'debt_title' => 'Debts', + 'debt_add_cta' => 'Add debt', + 'debt_you_owe' => 'You owe :amount', + 'debt_they_owe' => ':name owes you :amount', + 'debt_add_title' => 'Debt management', + 'debt_add_you_owe' => 'You owe :name', + 'debt_add_they_owe' => ':name owes you', + 'debt_add_amount' => 'the sum of', + 'debt_add_reason' => 'for the following reason (optional)', + 'debt_add_add_cta' => 'Add debt', + 'debt_edit_update_cta' => 'Update debt', + 'debt_edit_success' => 'The debt has been updated successfully', + 'debts_blank_title' => 'Manage debts you owe to :name or :name owes you', + + // tags + 'tag_edit' => 'Edit tag', + 'tag_add' => 'Add tags', + 'tag_add_search' => 'Add or search tags', + 'tag_no_tags' => 'No tags yet', + + // Introductions + 'introductions_sidebar_title' => 'How you met', + 'introductions_blank_cta' => 'Indicate how you met :name', + 'introductions_title_edit' => 'How did you meet :name?', + 'introductions_additional_info' => 'Explain how and where you met', + 'introductions_edit_met_through' => 'Has someone introduced you to this person?', + 'introductions_no_met_through' => 'No one', + 'introductions_first_met_date' => 'Date you met', + 'introductions_no_first_met_date' => 'I don’t know the date we met', + 'introductions_first_met_date_known' => 'This is the date we met', + 'introductions_add_reminder' => 'Add a reminder to celebrate this encounter on the anniversary this event happened', + 'introductions_update_success' => 'You’ve successfully updated the information about how you met this person', + 'introductions_met_through' => 'Met through :name', + 'introductions_met_date' => 'Met on :date', + 'introductions_reminder_title' => 'Anniversary of the day you first met', + + // Deceased + 'deceased_reminder_title' => 'Anniversary of the death of :name', + 'deceased_mark_person_deceased' => 'Mark this as deceased', + 'deceased_know_date' => 'I know the date that this person died', + 'deceased_add_reminder' => 'Add a reminder for this date', + 'deceased_label' => 'Deceased', + 'deceased_date_label' => 'Deceased date', + 'deceased_label_with_date' => 'Deceased on :date', + 'deceased_age' => 'Age at death', + + // Contact information + 'contact_info_title' => 'Contact information', + 'contact_info_form_content' => 'Content', + 'contact_info_form_contact_type' => 'Contact type', + 'contact_info_form_personalize' => 'Personalize', + 'contact_info_address' => 'Lives in', + + // Addresses + 'contact_address_title' => 'Addresses', + 'contact_address_form_name' => 'Label (optional)', + 'contact_address_form_street' => 'Street (optional)', + 'contact_address_form_city' => 'City (optional)', + 'contact_address_form_province' => 'Province (optional)', + 'contact_address_form_postal_code' => 'Postal code (optional)', + 'contact_address_form_country' => 'Country (optional)', + 'contact_address_form_latitude' => 'Latitude (numbers only) (optional)', + 'contact_address_form_longitude' => 'Longitude (numbers only) (optional)', + + // Pets + 'pets_kind' => 'Kind of pet', + 'pets_name' => 'Name (optional)', + 'pets_create_success' => 'The pet has been successfully added', + 'pets_update_success' => 'The pet has been updated', + 'pets_delete_success' => 'The pet has been deleted', + 'pets_title' => 'Pets', + 'pets_reptile' => 'Reptile', + 'pets_bird' => 'Bird', + 'pets_cat' => 'Cat', + 'pets_dog' => 'Dog', + 'pets_fish' => 'Fish', + 'pets_hamster' => 'Hamster', + 'pets_horse' => 'Horse', + 'pets_rabbit' => 'Rabbit', + 'pets_rat' => 'Rat', + 'pets_small_animal' => 'Small animal', + 'pets_other' => 'Other', + + // life events + 'life_event_list_tab_life_events' => 'Life events', + 'life_event_list_tab_other' => 'Notes, reminders, …', + 'life_event_list_title' => 'Life events', + 'life_event_blank' => 'Log what happens to the life of {name} for your future reference.', + 'life_event_list_cta' => 'Add life event', + 'life_event_create_category' => 'All categories', + 'life_event_create_life_event' => 'Add life event', + 'life_event_create_default_title' => 'Title (optional)', + 'life_event_create_default_story' => 'Story (optional)', + 'life_event_create_date' => 'You do not need to indicate a month or a day – only the year is mandatory.', + 'life_event_create_default_description' => 'Add information about what you know', + 'life_event_create_add_yearly_reminder' => 'Add a yearly reminder for this event', + 'life_event_create_success' => 'The life event has been added', + 'life_event_delete_title' => 'Delete a life event', + 'life_event_delete_description' => 'Are you sure you want to delete this life event? Deletion is permanent.', + 'life_event_delete_success' => 'The life event has been deleted', + 'life_event_date_it_happened' => 'Date it happened', + 'life_event_category_work_education' => 'Work & education', + 'life_event_category_family_relationships' => 'Family & relationships', + 'life_event_category_home_living' => 'Home & living', + 'life_event_category_health_wellness' => 'Health & wellness', + 'life_event_category_travel_experiences' => 'Travel & experiences', + 'life_event_sentence_new_job' => 'Started a new job', + 'life_event_sentence_retirement' => 'Retired', + 'life_event_sentence_new_school' => 'Started school', + 'life_event_sentence_study_abroad' => 'Studied abroad', + 'life_event_sentence_volunteer_work' => 'Started volunteering', + 'life_event_sentence_published_book_or_paper' => 'Published a paper', + 'life_event_sentence_military_service' => 'Started military service', + 'life_event_sentence_new_relationship' => 'Started a relationship', + 'life_event_sentence_engagement' => 'Got engaged', + 'life_event_sentence_marriage' => 'Got married', + 'life_event_sentence_anniversary' => 'Anniversary', + 'life_event_sentence_expecting_a_baby' => 'Expects a baby', + 'life_event_sentence_new_child' => 'Had a child', + 'life_event_sentence_new_family_member' => 'Added a family member', + 'life_event_sentence_new_pet' => 'Got a pet', + 'life_event_sentence_end_of_relationship' => 'Ended a relationship', + 'life_event_sentence_loss_of_a_loved_one' => 'Lost a loved one', + 'life_event_sentence_moved' => 'Moved', + 'life_event_sentence_bought_a_home' => 'Bought a home', + 'life_event_sentence_home_improvement' => 'Made a home improvement', + 'life_event_sentence_holidays' => 'Went on holidays', + 'life_event_sentence_new_vehicle' => 'Got a new vehicle', + 'life_event_sentence_new_roommate' => 'Got a roommate', + 'life_event_sentence_overcame_an_illness' => 'Overcame an illness', + 'life_event_sentence_quit_a_habit' => 'Quit a habit', + 'life_event_sentence_new_eating_habits' => 'Started new eating habits', + 'life_event_sentence_weight_loss' => 'Lost weight', + 'life_event_sentence_wear_glass_or_contact' => 'Started to wear glass or contact lenses', + 'life_event_sentence_broken_bone' => 'Broke a bone', + 'life_event_sentence_removed_braces' => 'Removed braces', + 'life_event_sentence_surgery' => 'Had surgery', + 'life_event_sentence_dentist' => 'Went to the dentist', + 'life_event_sentence_new_sport' => 'Started a sport', + 'life_event_sentence_new_hobby' => 'Started a hobby', + 'life_event_sentence_new_instrument' => 'Learned a new instrument', + 'life_event_sentence_new_language' => 'Learned a new language', + 'life_event_sentence_tattoo_or_piercing' => 'Got a tattoo or piercing', + 'life_event_sentence_new_license' => 'Got a license', + 'life_event_sentence_travel' => 'Traveled', + 'life_event_sentence_achievement_or_award' => 'Got an achievement or award', + 'life_event_sentence_changed_beliefs' => 'Changed beliefs', + 'life_event_sentence_first_word' => 'Spoke for the first time', + 'life_event_sentence_first_kiss' => 'Kissed for the first time', + + // documents + 'document_list_title' => 'Documents', + 'document_list_cta' => 'Upload document', + 'document_list_blank_desc' => 'Here you can store documents related to this person.', + 'document_upload_zone_cta' => 'Upload a file', + 'document_upload_zone_progress' => 'Uploading the document…', + 'document_upload_zone_error' => 'There was an error uploading the document. Please try again below.', + + // Photos + 'photo_title' => 'Photos', + 'photo_list_title' => 'Related photos', + 'photo_list_cta' => 'Upload photo', + 'photo_list_blank_desc' => 'You can store images about this contact. Upload one now!', + 'photo_upload_zone_cta' => 'Upload a photo', + 'photo_current_profile_pic' => 'Current profile picture', + 'photo_make_profile_pic' => 'Make profile picture', + 'photo_delete' => 'Delete photo', + 'photo_next' => 'Next photo ❯', + 'photo_previous' => '❮ Previous photo', + + // Avatars + 'avatar_change_title' => 'Change your avatar', + 'avatar_question' => 'Which avatar would you like to use?', + 'avatar_default_avatar' => 'The default avatar', + 'avatar_adorable_avatar' => 'The Adorable avatar', + 'avatar_gravatar' => 'The Gravatar associated with the email address of this person. Gravatar is a global system that lets users associate email addresses with photos.', + 'avatar_current' => 'Keep the current avatar', + 'avatar_photo' => 'From a photo that you upload', + 'avatar_crop_new_avatar_photo' => 'Crop new avatar photo', + + // emotions + 'emotion_this_made_me_feel' => 'This made you feel…', + + // logs + 'auditlogs_link' => 'History', + 'auditlogs_title' => 'Everything that happened to :name', + 'auditlogs_breadcrumb' => 'History', + 'auditlogs_author' => 'By :name on :date', + + // contact field label + 'contact_field_label_home' => 'Home', + 'contact_field_label_work' => 'Work', + 'contact_field_label_cell' => 'Mobile', + 'contact_field_label_fax' => 'Fax', + 'contact_field_label_pager' => 'Pager', + 'contact_field_label_main' => 'Main', + 'contact_field_label_other' => 'Other', + 'contact_field_label_personal' => 'Personal', +]; diff --git a/resources/lang/uk/reminder.php b/resources/lang/uk/reminder.php new file mode 100644 index 0000000..bcab17c --- /dev/null +++ b/resources/lang/uk/reminder.php @@ -0,0 +1,16 @@ + 'Wish happy birthday to', + 'type_phone_call' => 'Call', + 'type_lunch' => 'Lunch with', + 'type_hangout' => 'Hangout with', + 'type_email' => 'Email', + 'type_birthday_kid' => 'Wish happy birthday to the child of', +]; diff --git a/resources/lang/uk/settings.php b/resources/lang/uk/settings.php new file mode 100644 index 0000000..316cf93 --- /dev/null +++ b/resources/lang/uk/settings.php @@ -0,0 +1,557 @@ + 'Account settings', + 'sidebar_personalization' => 'Personalization', + 'sidebar_settings_storage' => 'Storage', + 'sidebar_settings_export' => 'Export data', + 'sidebar_settings_users' => 'Users', + 'sidebar_settings_subscriptions' => 'Subscription', + 'sidebar_settings_import' => 'Import data', + 'sidebar_settings_tags' => 'Tag management', + 'sidebar_settings_api' => 'API', + 'sidebar_settings_dav' => 'DAV Resources', + 'sidebar_settings_security' => 'Security', + 'sidebar_settings_auditlogs' => 'Audit logs', + + 'title_general' => 'General Information', + 'title_i18n' => 'International settings', + 'title_layout' => 'Layout', + + 'me_title' => 'Me as a contact', + 'me_help' => 'This is the contact that represents you in Monica', + 'me_select' => 'Select a contact', + 'me_no_contact' => 'No contact selected yet.', + 'me_select_click' => 'Click here to select a contact.', + 'me_remove_contact' => 'Remove the association', + 'me_choose' => 'Choose yourself', + 'me_choose_placeholder' => 'Choose yourself', + + 'export_title' => 'Export your account data', + 'export_be_patient' => 'Click the button to start the export. It might take several minutes to process the export – please be patient and do not spam the button.', + 'export_title_sql' => 'Export to SQL', + 'export_sql_explanation' => 'Exporting your data in SQL format allows you to take your data and import it to your own Monica instance. This is only valuable if you do have your own server.', + 'export_sql_cta' => 'Export to SQL', + 'export_sql_link_instructions' => 'Note: read the instructions to learn more about importing this file to your instance.', + 'export_title_json' => 'Export to Json', + 'export_submitted' => 'Your export has been submitted, it will be available in a few moment…', + 'export_json_explanation' => 'Exporting your data in Json format for backup.', + 'export_json_beta' => 'Json export is in preview mode. Tell us what you think about it:', + 'export_json_cta' => 'Export to Json', + 'export_header_type' => 'Type', + 'export_header_timestamp' => 'Creation date', + 'export_header_status' => 'Status', + 'export_header_actions' => 'Actions', + 'export_last_title' => 'Last exports', + 'export_empty_title' => 'No exports yet', + 'export_type_json' => 'Json export', + 'export_type_sql' => 'SQL export', + 'export_status_todo' => 'Submitted', + 'export_status_doing' => 'Doing', + 'export_status_done' => 'Done', + 'export_status_failed' => 'Failed', + 'export_not_done' => 'Download impossible, this export is not done yet.', + + 'firstname' => 'First name', + 'lastname' => 'Last name', + 'name_order' => 'Name order', + 'name_order_firstname_lastname' => ' – John Doe', + 'name_order_lastname_firstname' => ' – Doe John', + 'name_order_firstname_lastname_nickname' => ' () – John Doe (Rambo)', + 'name_order_firstname_nickname_lastname' => ' () – John (Rambo) Doe', + 'name_order_lastname_firstname_nickname' => ' () – Doe John (Rambo)', + 'name_order_lastname_nickname_firstname' => ' () – Doe (Rambo) John', + 'name_order_nickname_firstname_lastname' => ' ( ) – Rambo (John Doe)', + 'name_order_nickname_lastname_firstname' => ' ( ) – Rambo (Doe John)', + 'name_order_nickname' => ' – Rambo', + 'currency' => 'Currency', + 'name' => 'Your name: :name', + 'email' => 'Email address', + 'email_placeholder' => 'Enter email', + 'email_help' => 'This is the email used to login, and this is where Monica will send your reminders.', + 'timezone' => 'Timezone', + 'temperature_scale' => 'Temperature scale', + 'temperature_scale_fahrenheit' => 'Fahrenheit (°F)', + 'temperature_scale_celsius' => 'Celsius (°C)', + 'layout' => 'Layout', + 'layout_small' => 'Maximum 1200 pixels wide', + 'layout_big' => 'Full width of the browser', + 'save' => 'Update preferences', + 'delete_title' => 'Delete your account', + 'delete_desc' => 'Do you wish to delete your account? Deletion is permanent and all of your data will be erased permanently. If you have a subscription, it will be cancelled immediately.', + 'delete_other_desc' => 'Your data in the main database will be deleted immediately. As described in our privacy policy, we carry out securely encrypted backups of the database every day. These backups are kept for 30 days after which they are completely deleted. We cannot delete specific data from the backups we hold any earlier than this. All of your data will be completely deleted no later than 31 days after your account’s deletion.', + 'reset_desc' => 'Do you wish to reset your account? This will remove all your contacts, and all of the data associated with them. Your account will not be deleted.', + 'reset_title' => 'Reset your account', + 'reset_cta' => 'Reset account', + 'reset_notice' => 'Are you sure to reset your account? This is permanent and cannot be undone.', + 'reset_success' => 'Your account has been reset successfully.', + 'delete_notice' => 'Are you sure you want to delete your account? This is permanent and cannot be undone. All of your data will be deleted and will not be recoverable.', + 'delete_cta' => 'Delete account', + 'settings_success' => 'Preferences updated!', + 'locale' => 'Language used in the app', + 'locale_help' => 'Do you want to help translating Monica or add a new language? Please follow this link for more information.', + 'locale_ar' => 'Arabic', + 'locale_cs' => 'Czech', + 'locale_de' => 'German', + 'locale_el' => 'Greek', + 'locale_en' => 'English', + 'locale_en-GB' => 'English (United Kingdom)', + 'locale_es' => 'Spanish', + 'locale_fr' => 'French', + 'locale_he' => 'Hebrew', + 'locale_hr' => 'Croatian', + 'locale_id' => 'Indonesian', + 'locale_it' => 'Italian', + 'locale_ja' => 'Japanese', + 'locale_nl' => 'Dutch', + 'locale_pt' => 'Portuguese', + 'locale_pt-BR' => 'Portuguese, Brazil', + 'locale_ru' => 'Russian', + 'locale_sv' => 'Swedish', + 'locale_vi' => 'Vietnamese', + 'locale_zh' => 'Chinese Simplified', + 'locale_zh-TW' => 'Chinese Traditional', + 'locale_tr' => 'Turkish', + + 'security_title' => 'Security', + 'security_help' => 'Change security matters for your account.', + 'password_change' => 'Change your password', + 'password_current' => 'Current password', + 'password_current_placeholder' => 'Enter your current password', + 'password_new1' => 'New password', + 'password_new1_placeholder' => 'Enter your new password', + 'password_new2' => 'Confirm your new password', + 'password_new2_placeholder' => 'Retype your new password', + 'password_btn' => 'Change password', + '2fa_title' => 'Two Factor Authentication', + '2fa_otp_title' => 'Two Factor Authentication mobile application', + '2fa_enable_title' => 'Enable Two Factor Authentication', + '2fa_enable_description' => 'Enable Two Factor Authentication to increase the security of your account.', + '2fa_enable_otp' => 'Open up your Two Factor Authentication mobile app and scan the following QR barcode:', + '2fa_enable_otp_help' => 'If your Two Factor Authentication mobile app does not support QR barcodes, enter in the following code:', + '2fa_enable_otp_validate' => 'Please validate the new device you’ve just set up:', + '2fa_enable_success' => 'Two Factor Authentication activated', + '2fa_enable_error' => 'Error when trying to activate Two Factor Authentication', + '2fa_enable_error_already_set' => 'Two Factor Authentication is already activated', + '2fa_disable_title' => 'Disable Two Factor Authentication', + '2fa_disable_description' => 'Disable Two Factor Authentication for your account. Be careful, your account will be much less secure!', + '2fa_disable_success' => 'Two Factor Authentication disabled', + '2fa_disable_error' => 'Error when trying to disable Two Factor Authentication', + + 'webauthn_title' => 'Security key — WebAuthn protocol', + 'webauthn_enable_description' => 'Add a new security key', + 'webauthn_key_name_help' => 'Give your key a name.', + 'webauthn_key_name' => 'Key name:', + 'webauthn_success' => 'Your key is detected and validated.', + 'webauthn_last_use' => 'Last use: {timestamp}', + 'webauthn_delete_confirmation' => 'Are you sure you want to delete this key?', + 'webauthn_delete_success' => 'Key deleted', + 'webauthn_insertKey' => 'Insert your security key.', + 'webauthn_buttonAdvise' => 'If your security key has a button, press it.', + 'webauthn_noButtonAdvise' => 'If it does not, remove it and insert it again.', + 'webauthn_not_supported' => 'Your browser doesn’t currently support WebAuthn.', + 'webauthn_not_secured' => 'WebAuthn only supports secure connections. Please load this page with https scheme.', + 'webauthn_error_already_used' => 'This key is already registered. It’s not necessary to register it again.', + 'webauthn_error_not_allowed' => 'The operation either timed out or was not allowed.', + + 'recovery_title' => 'Recovery codes', + 'recovery_show' => 'Get recovery codes', + 'recovery_copy_help' => 'Copy codes in your clipboard', + 'recovery_help_intro' => 'These are your recovery codes:', + 'recovery_help_information' => 'You can use each recovery code once.', + 'recovery_clipboard' => 'Codes copied to the clipboard.', + 'recovery_generate' => 'Generate new codes…', + 'recovery_generate_help' => 'Generating new codes will invalidate previously generated codes.', + 'recovery_already_used_help' => 'This code has already been used.', + + 'users_list_title' => 'Users with access to your account', + 'users_list_add_user' => 'Invite a new user', + 'users_list_you' => 'That’s you', + 'users_list_invitations_title' => 'Pending invitations', + 'users_list_invitations_explanation' => 'Below are the people you’ve invited to join Monica as a collaborator.', + 'users_list_invitations_invited_by' => 'invited by :name', + 'users_list_invitations_sent_date' => 'sent on :date', + 'users_blank_title' => 'You are the only one who has access to this account.', + 'users_blank_add_title' => 'Would you like to invite someone else?', + 'users_blank_description' => 'This person will have the same access that you have, and will be able to add, edit or delete contact information.', + 'users_blank_cta' => 'Invite someone', + 'users_add_title' => 'Invite a new user to your account by email', + 'users_add_description' => 'This person will have the same access as you do, including inviting or deleting other users, including you. Make sure you trust this person before giving them access.', + 'users_add_email_field' => 'Enter the email of the person you want to invite', + 'users_add_confirmation' => 'I confirm that I want to invite this user to my account. I understand that this person will have access to ALL of my data and see exactly what I see.', + 'users_add_cta' => 'Invite user by email', + 'users_accept_title' => 'Accept invitation and create a new account', + 'users_error_please_confirm' => 'Please confirm that you want to invite this user before proceeding with the invitation', + 'users_error_email_already_taken' => 'This email is already taken. Please choose another one', + 'users_error_already_invited' => 'You already have invited this user. Please choose another email address.', + 'users_error_email_not_similar' => 'This is not the email of the person who’ve invited you.', + 'users_invitation_deleted_confirmation_message' => 'The invitation has been successfully deleted', + 'users_invitations_delete_confirmation' => 'Are you sure you want to delete this invitation?', + 'users_list_delete_confirmation' => 'Are you sure to delete this user from your account?', + 'users_invitation_need_subscription' => 'Adding more users requires a subscription.', + + 'subscriptions_account_current_plan' => 'Your current plan', + 'subscriptions_account_current_legacy' => 'Current plan, not selectable anymore:', + 'subscriptions_account_current_paid_plan' => 'You are on the :name plan. Thanks so much for being a subscriber.', + + 'subscriptions_account_next_billing_title' => 'Next bill', + 'subscriptions_account_next_billing' => 'Your subscription will auto-renew on :date.', + 'subscriptions_account_bill_monthly' => 'We’ll bill you :price for another month.', + 'subscriptions_account_bill_annual' => 'We’ll bill you :price for another year.', + 'subscriptions_account_change' => 'Change plan', + + 'subscriptions_account_cancel_title' => 'Cancel subscription', + 'subscriptions_account_cancel_action' => 'Cancel subscription', + 'subscriptions_account_cancel' => 'You can cancel subscription anytime.', + 'subscriptions_account_free_plan' => 'You are on the free plan.', + 'subscriptions_account_free_plan_upgrade' => 'You can upgrade your account to the :name plan, which costs $:price per month. Here are the advantages:', + 'subscriptions_account_free_plan_benefits_users' => 'Unlimited number of users', + 'subscriptions_account_free_plan_benefits_reminders' => 'Reminders by email', + 'subscriptions_account_free_plan_benefits_import_data_vcard' => 'Import your contacts with vCard', + 'subscriptions_account_free_plan_benefits_support' => 'Support the project in the long run, so we can introduce more great features.', + 'subscriptions_account_upgrade' => 'Upgrade your account', + 'subscriptions_account_upgrade_title' => 'Upgrade Monica today and have more meaningful relationships.', + 'subscriptions_account_upgrade_choice' => 'Pick a plan below and join over :customers persons who upgraded their Monica.', + 'subscriptions_account_update_title' => 'Update Monica subscription', + 'subscriptions_account_update_description' => 'You can change your subscription’s frequency here.', + 'subscriptions_account_update_information' => 'You will be billed immediately for the new amount. Your subscription will extend to the new period, depending on your choice.', + 'subscriptions_account_invoices' => 'Invoices', + 'subscriptions_account_invoices_download' => 'Download', + 'subscriptions_account_invoices_subscription' => 'Subscription from :startDate to :endDate', + 'subscriptions_account_payment' => 'Which payment option fits you best?', + 'subscriptions_account_confirm_payment' => 'Your payment is currently incomplete, please confirm your payment.', + 'subscriptions_downgrade_title' => 'Downgrade your account to the free plan', + 'subscriptions_downgrade_limitations' => 'The free plan has limitations. In order to be able to downgrade, you need to pass the checklist below:', + 'subscriptions_downgrade_rule_users' => 'You must have only 1 user in your account', + 'subscriptions_downgrade_rule_users_constraint' => 'You currently have 1 user in your account.|You currently have :count users in your account.', + 'subscriptions_downgrade_rule_invitations' => 'You must not have any pending invitations', + 'subscriptions_downgrade_rule_invitations_constraint' => 'You currently have 1 pending invitation.|You currently have :count pending invitations.', + 'subscriptions_downgrade_rule_contacts' => 'You must not have more than :number active contacts', + 'subscriptions_downgrade_rule_contacts_constraint' => 'You currently have 1 contact.|You currently have :count contacts.', + 'subscriptions_downgrade_rule_contacts_archive' => 'We can also archive all your contacts for you – that would clear this rule and let you proceed with your account’s downgrade process.', + 'subscriptions_downgrade_cta' => 'Downgrade', + 'subscriptions_downgrade_success' => 'You are back to the Free plan!', + 'subscriptions_downgrade_thanks' => 'Thanks so much for trying the paid plan. We keep adding new features on Monica all the time – so you might want to come back in the future to see if you might be interested in taking a subscription again.', + 'subscriptions_back' => 'Back to settings', + 'subscriptions_upgrade_title' => 'Upgrade your account', + 'subscriptions_upgrade_choose' => 'You picked the :plan plan.', + 'subscriptions_upgrade_infos' => 'We couldn’t be happier. Enter your payment info below.', + 'subscriptions_upgrade_name' => 'Name on card', + 'subscriptions_upgrade_zip' => 'ZIP or postal code', + 'subscriptions_upgrade_credit' => 'Credit or debit card', + 'subscriptions_upgrade_submit' => 'Pay {amount}', + 'subscriptions_upgrade_charge' => 'We’ll charge your card :price now. The next charge will be on :date. If you ever change your mind, you can cancel at any time, no questions asked.', + 'subscriptions_upgrade_charge_handled' => 'The payment is handled by Stripe. No card information touches our server.', + 'subscriptions_upgrade_success' => 'Thank you! You are now subscribed.', + 'subscriptions_upgrade_thanks' => 'Welcome to the community of people who try to make the world a better place.', + + 'subscriptions_payment_confirm_title' => 'Confirm your :amount payment', + 'subscriptions_payment_confirm_information' => 'Extra confirmation is needed to process your payment. Please confirm your payment by filling out your payment details below.', + 'subscriptions_payment_succeeded_title' => 'Payment Successful', + 'subscriptions_payment_succeeded' => 'This payment was already successfully confirmed.', + 'subscriptions_payment_cancelled_title' => 'Payment Cancelled', + 'subscriptions_payment_cancelled' => 'This payment was cancelled.', + 'subscriptions_payment_error_name' => 'Please provide your name.', + 'subscriptions_payment_success' => 'The payment was successful.', + + 'subscriptions_pdf_title' => 'Your :name monthly subscription', + 'subscriptions_plan_frequency_year' => ':amount / year', + 'subscriptions_plan_frequency_month' => ':amount / month', + 'subscriptions_plan_choose' => 'Choose this plan', + 'subscriptions_plan_year_title' => 'Pay annually', + 'subscriptions_plan_year_bonus' => 'Peace of mind for a whole year', + 'subscriptions_plan_month_title' => 'Pay monthly', + 'subscriptions_plan_month_bonus' => 'Cancel any time', + 'subscriptions_plan_include1' => 'Included with your upgrade:', + 'subscriptions_plan_include2' => 'Unlimited number of contacts • Unlimited number of users • Reminders by email • Import with vCard • Personalization of the contact sheet', + 'subscriptions_plan_include3' => '100% of the profits go the development of this great open source project.', + 'subscriptions_help_title' => 'Additional details you may be curious about', + 'subscriptions_help_opensource_title' => 'What is an open source project?', + 'subscriptions_help_opensource_desc' => 'Monica is an open source project. This means it is built by a community who wants to build a great tool for the greater good. Being open source means the code is publicly available on GitHub, and everyone can inspect it, modify it or enhance it. All the money we raise is dedicated to building better features, paying for more powerful servers, and paying other costs. Thanks for your help. We couldn’t do it without you.', + 'subscriptions_help_limits_title' => 'Is there a limit to the number of contacts we can have on the free plan?', + 'subscriptions_help_limits_plan' => 'Yes. Free plans let you manage :number contacts.', + 'subscriptions_help_discounts_title' => 'Do you have discounts for non-profits and education?', + 'subscriptions_help_discounts_desc' => 'We do! Monica is free for students, and free for non-profits and charities. Just contact the support with a proof of your status and we’ll apply this special status in your account.', + 'subscriptions_help_change_title' => 'What if I change my mind?', + 'subscriptions_help_change_desc' => 'You can cancel anytime, no questions asked, and all by yourself – no need to contact support. However, you will not be refunded for the current period.', + + 'stripe_error_card' => 'Your card was declined. Decline message is: :message', + 'stripe_error_api_connection' => 'Network communication with Stripe failed. Try again later.', + 'stripe_error_rate_limit' => 'Too many requests with Stripe right now. Try again later.', + 'stripe_error_invalid_request' => 'Invalid parameters. Try again later.', + 'stripe_error_authentication' => 'Wrong authentication with Stripe', + + 'import_title' => 'Import contacts in your account', + 'import_cta' => 'Upload contacts', + 'import_stat' => 'You’ve imported :number files so far.', + 'import_result_stat' => 'Uploaded vCard with 1 contact (:total_imported imported, :total_skipped skipped)|Uploaded vCard with :total_contacts contacts (:total_imported imported, :total_skipped skipped)', + 'import_view_report' => 'View report', + 'import_in_progress' => 'The import is in progress. Reload the page in one minute.', + 'import_upload_title' => 'Import your contacts from a vCard file', + 'import_upload_rules_desc' => 'We do however have some rules:', + 'import_upload_rule_format' => 'We support .vcard and .vcf files.', + 'import_upload_rule_vcard' => 'We support the vCard 3.0 format, which is the default format for macOS’s Contacts.app and Google Contacts.', + 'import_upload_rule_instructions' => 'Export instructions for macOS Contacts.app and Google Contacts.', + 'import_upload_rule_multiple' => 'If your contacts have multiple email addresses or phone numbers, only the first entry will be saved.', + 'import_upload_rule_limit' => 'Files are limited to 10 MB.', + 'import_upload_rule_time' => 'It might take up to a minute to upload the contacts and process them. Please be patient.', + 'import_upload_rule_cant_revert' => 'Please make sure data is accurate before uploading, as you can’t undo the upload.', + 'import_upload_form_file' => 'Your .vcf or .vCard file:', + 'import_upload_behaviour' => 'Import behaviour:', + 'import_upload_behaviour_add' => 'Add new contacts and skip existing', + 'import_upload_behaviour_replace' => 'Replace existing contacts', + 'import_upload_behaviour_help' => 'Replacing will replace all data found in the vCard, but will keep existing contact fields.', + 'import_report_title' => 'Importing report', + 'import_report_date' => 'Date of the import', + 'import_report_type' => 'Type of import', + 'import_report_number_contacts' => 'Number of contacts in the file', + 'import_report_number_contacts_imported' => 'Number of imported contacts', + 'import_report_number_contacts_skipped' => 'Number of skipped contacts', + 'import_report_status_imported' => 'Imported', + 'import_report_status_skipped' => 'Skipped', + 'import_vcard_parse_error' => 'Error when parsing the vCard entry', + 'import_vcard_contact_exist' => 'Contact already exists', + 'import_vcard_contact_no_firstname' => 'No first name (mandatory)', + 'import_vcard_file_not_found' => 'File not found', + 'import_vcard_unknown_entry' => 'Unknown contact name', + 'import_vcard_file_no_entries' => 'File contains no entries', + 'import_blank_title' => 'You haven’t imported any contacts yet.', + 'import_blank_question' => 'Would you like to import contacts now?', + 'import_blank_description' => 'We can import vCard files that you can get from Google Contacts or your Contact manager.', + 'import_blank_cta' => 'Import vCard', + 'import_need_subscription' => 'Importing data requires a subscription.', + + 'tags_list_title' => 'Tags', + 'tags_list_description' => 'You can organize your contacts by setting up tags. Tags work like folders, but you can add more than one tag to a contact. To add a new tag, add it on the contact itself.', + 'tags_list_contact_number' => '1 contact|:count contacts', + 'tags_list_delete_success' => 'The tag has been successfully deleted', + 'tags_list_edit_success' => 'The tag has been successfully updated', + 'tags_list_delete_confirmation' => 'Are you sure you want to delete the tag? No contacts will be deleted, only the tag.', + 'tags_blank_title' => 'Tags are a great way of categorizing your contacts.', + 'tags_blank_description' => 'Tags work like folders, but you can add more than one tag to a contact. Go to a contact and tag a friend, right below the name. Once a contact is tagged, come back here to manage all the tags in your account.', + + 'api_title' => 'API access', + 'api_description' => 'The API can be used to manipulate Monica’s data from an external application, like a mobile application for instance.', + 'api_help' => 'To use the API, a token is mandatory. You can either create a personal access token (Bearer authentication), or authorize an OAuth client to create it for you. See API documentation.', + 'api_endpoint' => 'The API endpoint for this Monica instance is:', + + 'api_personal_access_tokens' => 'Personal access tokens', + 'api_pao_description' => 'Make sure you give this token to a source you trust – as they allow you to access all your data.', + 'api_token_title' => 'Personal Access Tokens', + 'api_token_create_new' => 'Create New Token', + 'api_token_not_created' => 'You have not created any personal access tokens.', + 'api_token_name' => 'Token name', + 'api_token_expire' => 'Expires at {date}', + 'api_token_delete' => 'Delete', + 'api_token_create' => 'Create Token', + 'api_token_scopes' => 'Scopes', + 'api_token_help' => 'Here is your new personal access token. This is the only time it will be shown so don’t lose it! You may now use this token to make API requests.', + + 'api_oauth_clients' => 'Your OAuth clients', + 'api_oauth_clients_desc' => 'This section lets you register your own OAuth clients.', + 'api_oauth_clients_desc2' => 'Use this client id to request a new token, and convert authorization codes to access tokens. See Laravel Passport documentation for more information.', + 'api_oauth_title' => 'OAuth Clients', + 'api_oauth_create_new' => 'Create New Client', + 'api_oauth_edit' => 'Edit Client', + 'api_oauth_not_created' => 'You have not created any OAuth clients.', + 'api_oauth_clientid' => 'Client ID', + 'api_oauth_name' => 'Name', + 'api_oauth_name_help' => 'Something your users will recognize and trust.', + 'api_oauth_secret' => 'Secret', + 'api_oauth_create' => 'Create Client', + 'api_oauth_redirecturl' => 'Redirect URL', + 'api_oauth_redirecturl_help' => 'Your application’s authorization callback URL.', + + 'api_authorized_clients' => 'List of authorized clients', + 'api_authorized_clients_desc' => 'This section lists all the clients you’ve authorized to access your application data. You can revoke this authorization at anytime.', + 'api_authorized_clients_title' => 'Authorized Applications', + 'api_authorized_clients_none' => 'There are no authorized clients yet.', + 'api_authorized_clients_name' => 'Name', + 'api_authorized_clients_scopes' => 'Scopes', + + 'personalization_tab_title' => 'Personalize your account', + + 'personalization_title' => 'Here you will find different settings to configure your account. These features are intended for “power users” who want maximum control over Monica.', + 'personalization_contact_field_type_title' => 'Contact field types', + 'personalization_contact_field_type_add' => 'Add new field type', + 'personalization_contact_field_type_description' => 'You can configure all the different types of contact fields that you can associate to all your contacts. For example, if a new social network appears in the future, you will be able to add this new way of communicating with your contacts right here.', + 'personalization_contact_field_type_table_name' => 'Name', + 'personalization_contact_field_type_table_protocol' => 'Protocol', + 'personalization_contact_field_type_table_actions' => 'Actions', + 'personalization_contact_field_type_modal_title' => 'Add a new contact field type', + 'personalization_contact_field_type_modal_edit_title' => 'Edit an existing contact field type', + 'personalization_contact_field_type_modal_delete_title' => 'Delete an existing contact field type', + 'personalization_contact_field_type_modal_delete_description' => 'Are you sure you want to delete this contact field type? Deleting this type of contact field will delete ALL the data with this type for all of your contacts.', + 'personalization_contact_field_type_modal_name' => 'Name', + 'personalization_contact_field_type_modal_protocol' => 'Protocol (optional)', + 'personalization_contact_field_type_modal_protocol_help' => 'Each new contact field type can be clickable. If a protocol is set, we will use it to trigger the action that is set.', + 'personalization_contact_field_type_modal_icon' => 'Icon (optional)', + 'personalization_contact_field_type_modal_icon_help' => 'You can associate an icon with this contact field type. You need to add a reference to a Font Awesome icon.', + 'personalization_contact_field_type_delete_success' => 'The contact field type has been successfully deleted.', + 'personalization_contact_field_type_add_success' => 'The contact field type has been successfully added.', + 'personalization_contact_field_type_edit_success' => 'The contact field type has been successfully updated.', + + 'personalization_genders_title' => 'Gender types', + 'personalization_genders_add' => 'Add new gender type', + 'personalization_genders_desc' => 'You can define as many genders as you need to. You need at least one gender type in your account.', + 'personalization_genders_modal_add' => 'Add gender type', + 'personalization_genders_modal_edit' => 'Update gender type', + 'personalization_genders_modal_name' => 'Name', + 'personalization_genders_modal_name_help' => 'The name used to display the gender on a contact page.', + 'personalization_genders_modal_sex' => 'Sex', + 'personalization_genders_modal_sex_help' => 'Used to define the relationships, and during the VCard import/export process.', + 'personalization_genders_modal_default' => 'Select the default gender for a new contact', + 'personalization_genders_modal_delete' => 'Delete gender type', + 'personalization_genders_modal_delete_desc' => 'Are you sure you want to delete the gender “{name}”?', + 'personalization_genders_modal_delete_question' => 'You currently have {count} contact with this gender. If you delete this gender, what gender should this contact have?|You currently have {count} contacts with this gender. If you delete this gender, what gender should these contacts have?', + 'personalization_genders_modal_delete_question_default' => 'This gender is the default one. If you delete this gender, which one will be the new default?', + 'personalization_genders_modal_error' => 'Please choose a gender from the list.', + 'personalization_genders_list_contact_number' => '{count} contact|{count} contacts', + 'personalization_genders_table_name' => 'Name', + 'personalization_genders_table_sex' => 'Sex', + 'personalization_genders_table_default' => 'Default', + 'personalization_genders_default' => 'Default gender', + 'personalization_genders_make_default' => 'Change default gender', + 'personalization_genders_select_default' => 'Select default gender', + 'personalization_genders_m' => 'Male', + 'personalization_genders_f' => 'Female', + 'personalization_genders_o' => 'Other', + 'personalization_genders_u' => 'Unknown', + 'personalization_genders_n' => 'None or not applicable', + + 'personalization_reminder_rule_save' => 'The change has been saved', + 'personalization_reminder_rule_title' => 'Reminder rules', + 'personalization_reminder_rule_line' => '{count} day before|{count} days before', + 'personalization_reminder_rule_desc' => 'For every reminder that you set, Monica can send you an email a number of days before the event happens. You can adjust these notification settings here. These notifications only apply to monthly and yearly reminders.', + + 'personalization_module_save' => 'The change has been saved', + 'personalization_module_title' => 'Features', + 'personalization_module_desc' => 'You may not need all of Monica’s features. Below you can toggle specific features that are used on a contact sheet. This change will affect ALL your contacts. Turning off a feature does not delete any data, it simply hides the feature.', + + 'personalisation_paid_upgrade' => 'This is a premium feature that requires a Paid subscription to be active. Upgrade your account by visiting Settings > Subscription.', + 'personalisation_paid_upgrade_vue' => 'This is a premium feature that requires a Paid subscription to be active. Upgrade your account by visiting Settings > Subscription.', + + 'reminder_time_to_send' => 'Time of the day reminders will be sent', + 'reminder_time_to_send_help' => 'Your next reminder is scheduled to be sent on {dateTime}.', + + 'personalization_activity_type_category_title' => 'Activity type categories', + 'personalization_activity_type_category_add' => 'Add a new activity type category', + 'personalization_activity_type_category_table_name' => 'Name', + 'personalization_activity_type_category_description' => 'An activity with one of your contacts can have a type and a category type. Your account comes with a set of predefined category types by default, but you can customize these here.', + 'personalization_activity_type_category_table_actions' => 'Actions', + 'personalization_activity_type_category_modal_add' => 'Add a new activity type category', + 'personalization_activity_type_category_modal_edit' => 'Edit an activity type category', + 'personalization_activity_type_category_modal_question' => 'What should we name this new category?', + 'personalization_activity_type_add_button' => 'Add a new activity type', + 'personalization_activity_type_modal_add' => 'Add a new activity type', + 'personalization_activity_type_modal_question' => 'What should we name this new activity type?', + 'personalization_activity_type_modal_edit' => 'Edit an activity type', + 'personalization_activity_type_category_modal_delete' => 'Delete an activity type category', + 'personalization_activity_type_category_modal_delete_desc' => 'Are you sure you want to delete this category? Deleting it will delete all associated activity types. Activities that belong to this category will not be affected by this deletion.', + 'personalization_activity_type_modal_delete' => 'Delete an activity type', + 'personalization_activity_type_modal_delete_desc' => 'Are you sure you want to delete this activity type? Activities that belong to this category will not be affected by this deletion.', + 'personalization_activity_type_modal_delete_error' => 'We can’t find this activity type.', + 'personalization_activity_type_category_modal_delete_error' => 'We can’t find this activity type category.', + + 'personalization_life_event_category_title' => 'Life event categories', + 'personalization_live_event_category_table_name' => 'Name', + 'personalization_life_event_category_description' => 'A life event can have a type and a category. Your account comes with a set of predefined categories and types by default, but you can customize life event types here.', + 'personalization_live_event_category_table_actions' => 'Actions', + 'personalization_life_event_type_add_button' => 'Add a new life event type', + 'personalization_life_event_type_modal_add' => 'Add a new life event type', + 'personalization_life_event_type_modal_question' => 'What should we name this new life event type?', + 'personalization_life_event_type_modal_edit' => 'Edit a life event type', + 'personalization_life_event_type_modal_delete' => 'Delete a life event type', + 'personalization_life_event_type_modal_delete_desc' => 'Are you sure you want to delete this life event type? Life events that belong to this type will be deleted by performing this action.', + 'personalization_life_event_type_modal_delete_error' => 'We can’t find this life event type.', + + 'personalization_life_event_category_work_education' => 'Work & education', + 'personalization_life_event_category_family_relationships' => 'Family & relationships', + 'personalization_life_event_category_home_living' => 'Home & living', + 'personalization_life_event_category_travel_experiences' => 'Travel & experiences', + 'personalization_life_event_category_health_wellness' => 'Health & wellness', + + 'personalization_life_event_type_new_job' => 'New job', + 'personalization_life_event_type_retirement' => 'Retirement', + 'personalization_life_event_type_new_school' => 'New school', + 'personalization_life_event_type_study_abroad' => 'Study abroad', + 'personalization_life_event_type_volunteer_work' => 'Volunteer work', + 'personalization_life_event_type_published_book_or_paper' => 'Published a book or paper', + 'personalization_life_event_type_military_service' => 'Military service', + 'personalization_life_event_type_first_met' => 'First met', + 'personalization_life_event_type_new_relationship' => 'New relationship', + 'personalization_life_event_type_engagement' => 'Engagement', + 'personalization_life_event_type_marriage' => 'Marriage', + 'personalization_life_event_type_anniversary' => 'Anniversary', + 'personalization_life_event_type_expecting_a_baby' => 'Expecting a baby', + 'personalization_life_event_type_new_child' => 'New child', + 'personalization_life_event_type_new_family_member' => 'New family member', + 'personalization_life_event_type_new_pet' => 'New pet', + 'personalization_life_event_type_end_of_relationship' => 'End of relationship', + 'personalization_life_event_type_loss_of_a_loved_one' => 'Loss of a loved one', + 'personalization_life_event_type_moved' => 'Moved', + 'personalization_life_event_type_bought_a_home' => 'Bought a home', + 'personalization_life_event_type_home_improvement' => 'Home improvement', + 'personalization_life_event_type_holidays' => 'Holidays', + 'personalization_life_event_type_new_vehicle' => 'New vehicle', + 'personalization_life_event_type_new_roommate' => 'New roommate', + 'personalization_life_event_type_overcame_an_illness' => 'Overcame an illness', + 'personalization_life_event_type_quit_a_habit' => 'Quit a habit', + 'personalization_life_event_type_new_eating_habits' => 'New eating habits', + 'personalization_life_event_type_weight_loss' => 'Weight loss', + 'personalization_life_event_type_wear_glass_or_contact' => 'Started wearing glasses or contacts', + 'personalization_life_event_type_broken_bone' => 'Broke a bone', + 'personalization_life_event_type_removed_braces' => 'Had braces removed', + 'personalization_life_event_type_surgery' => 'Had surgery', + 'personalization_life_event_type_dentist' => 'Had dental treatment', + 'personalization_life_event_type_new_sport' => 'Started playing a new sport', + 'personalization_life_event_type_new_hobby' => 'Took up a new hobby', + 'personalization_life_event_type_new_instrument' => 'Started learning a new instrument', + 'personalization_life_event_type_new_language' => 'Started learning a new language', + 'personalization_life_event_type_tattoo_or_piercing' => 'Tattoo or piercing', + 'personalization_life_event_type_new_license' => 'New license', + 'personalization_life_event_type_travel' => 'Travel', + 'personalization_life_event_type_achievement_or_award' => 'Achievement or award', + 'personalization_life_event_type_changed_beliefs' => 'Changed beliefs', + 'personalization_life_event_type_first_word' => 'First word', + 'personalization_life_event_type_first_kiss' => 'First kiss', + + 'storage_title' => 'Storage', + 'storage_account_info' => 'Your account limit is :accountLimit MB. Your current usage is :currentAccountSize MB (about :percentUsage%).', + 'storage_upgrade_notice' => 'Upgrade your account to be able to upload documents and photos.', + 'storage_description' => 'Here you can see all the documents and photos uploaded about your contacts.', + + 'dav_title' => 'WebDAV', + 'dav_description' => 'Here you can find all settings to use WebDAV resources for CardDAV and CalDAV exports.', + 'dav_copy_help' => 'Copy into your clipboard', + 'dav_clipboard_copied' => 'Value copied into your clipboard', + 'dav_url_base' => 'Base url for all CardDAV and CalDAV resources:', + 'dav_connect_help' => 'You can connect your contacts and/or calendars with this base url on you phone or computer.', + 'dav_connect_help2' => 'Use your login (email) and create an API token as the password to authenticate.', + 'dav_url_carddav' => 'CardDAV url for Contacts resource:', + 'dav_url_caldav_birthdays' => 'CalDAV url for Birthdays resources:', + 'dav_url_caldav_tasks' => 'CalDAV url for Tasks resources:', + 'dav_title_carddav' => 'CardDAV', + 'dav_title_caldav' => 'CalDAV', + 'dav_carddav_export' => 'Export all contacts in one file', + 'dav_caldav_birthdays_export' => 'Export all birthdays in one file', + 'dav_caldav_tasks_export' => 'Export all tasks in one file', + + 'archive_title' => 'Archive all of the contacts in your account', + 'archive_desc' => 'This will archive all of the contacts in your account.', + 'archive_cta' => 'Archive all of your contacts', + + 'logs_title' => 'Everything that has happened to this account', + 'logs_actor' => 'Actor', + 'logs_timestamp' => 'Timestamp', + 'logs_description' => 'Description', + 'logs_subject' => 'Subject', + 'logs_size' => 'Size (Kb)', + 'logs_object' => 'Object', +]; diff --git a/resources/lang/uk/validation.php b/resources/lang/uk/validation.php new file mode 100644 index 0000000..0153365 --- /dev/null +++ b/resources/lang/uk/validation.php @@ -0,0 +1,166 @@ + 'The :attribute must be accepted.', + 'active_url' => 'The :attribute is not a valid URL.', + 'after' => 'The :attribute must be a date after :date.', + 'after_or_equal' => 'The :attribute must be a date after or equal to :date.', + 'alpha' => 'The :attribute may only contain letters.', + 'alpha_dash' => 'The :attribute may only contain letters, numbers, dashes and underscores.', + 'alpha_num' => 'The :attribute may only contain letters and numbers.', + 'array' => 'The :attribute must be an array.', + 'before' => 'The :attribute must be a date before :date.', + 'before_or_equal' => 'The :attribute must be a date before or equal to :date.', + 'between' => [ + 'numeric' => 'The :attribute must be between :min and :max.', + 'file' => 'The :attribute must be between :min and :max kilobytes.', + 'string' => 'The :attribute must be between :min and :max characters.', + 'array' => 'The :attribute must have between :min and :max items.', + ], + 'boolean' => 'The :attribute field must be true or false.', + 'confirmed' => 'The :attribute confirmation does not match.', + 'date' => 'The :attribute is not a valid date.', + 'date_equals' => 'The :attribute must be a date equal to :date.', + 'date_format' => 'The :attribute does not match the format :format.', + 'different' => 'The :attribute and :other must be different.', + 'digits' => 'The :attribute must be :digits digits.', + 'digits_between' => 'The :attribute must be between :min and :max digits.', + 'dimensions' => 'The :attribute has invalid image dimensions.', + 'distinct' => 'The :attribute field has a duplicate value.', + 'email' => 'The :attribute must be a valid email address.', + 'ends_with' => 'The :attribute must end with one of the following: :values.', + 'exists' => 'The selected :attribute is invalid.', + 'file' => 'The :attribute must be a file.', + 'filled' => 'The :attribute field must have a value.', + 'gt' => [ + 'numeric' => 'The :attribute must be greater than :value.', + 'file' => 'The :attribute must be greater than :value kilobytes.', + 'string' => 'The :attribute must be greater than :value characters.', + 'array' => 'The :attribute must have more than :value items.', + ], + 'gte' => [ + 'numeric' => 'The :attribute must be greater than or equal :value.', + 'file' => 'The :attribute must be greater than or equal :value kilobytes.', + 'string' => 'The :attribute must be greater than or equal :value characters.', + 'array' => 'The :attribute must have :value items or more.', + ], + 'image' => 'The :attribute must be an image.', + 'in' => 'The selected :attribute is invalid.', + 'in_array' => 'The :attribute field does not exist in :other.', + 'integer' => 'The :attribute must be an integer.', + 'ip' => 'The :attribute must be a valid IP address.', + 'ipv4' => 'The :attribute must be a valid IPv4 address.', + 'ipv6' => 'The :attribute must be a valid IPv6 address.', + 'json' => 'The :attribute must be a valid JSON string.', + 'lt' => [ + 'numeric' => 'The :attribute must be less than :value.', + 'file' => 'The :attribute must be less than :value kilobytes.', + 'string' => 'The :attribute must be less than :value characters.', + 'array' => 'The :attribute must have less than :value items.', + ], + 'lte' => [ + 'numeric' => 'The :attribute must be less than or equal :value.', + 'file' => 'The :attribute must be less than or equal :value kilobytes.', + 'string' => 'The :attribute must be less than or equal :value characters.', + 'array' => 'The :attribute must not have more than :value items.', + ], + 'max' => [ + 'numeric' => 'The :attribute may not be greater than :max.', + 'file' => 'The :attribute may not be greater than :max kilobytes.', + 'string' => 'The :attribute may not be greater than :max characters.', + 'array' => 'The :attribute may not have more than :max items.', + ], + 'mimes' => 'The :attribute must be a file of type: :values.', + 'mimetypes' => 'The :attribute must be a file of type: :values.', + 'min' => [ + 'numeric' => 'The :attribute must be at least :min.', + 'file' => 'The :attribute must be at least :min kilobytes.', + 'string' => 'The :attribute must be at least :min characters.', + 'array' => 'The :attribute must have at least :min items.', + ], + 'not_in' => 'The selected :attribute is invalid.', + 'not_regex' => 'The :attribute format is invalid.', + 'numeric' => 'The :attribute must be a number.', + 'password' => 'The password is incorrect.', + 'present' => 'The :attribute field must be present.', + 'regex' => 'The :attribute format is invalid.', + 'required' => 'The :attribute field is required.', + 'required_if' => 'The :attribute field is required when :other is :value.', + 'required_unless' => 'The :attribute field is required unless :other is in :values.', + 'required_with' => 'The :attribute field is required when :values is present.', + 'required_with_all' => 'The :attribute field is required when :values are present.', + 'required_without' => 'The :attribute field is required when :values is not present.', + 'required_without_all' => 'The :attribute field is required when none of :values are present.', + 'same' => 'The :attribute and :other must match.', + 'size' => [ + 'numeric' => 'The :attribute must be :size.', + 'file' => 'The :attribute must be :size kilobytes.', + 'string' => 'The :attribute must be :size characters.', + 'array' => 'The :attribute must contain :size items.', + ], + 'starts_with' => 'The :attribute must start with one of the following: :values.', + 'string' => 'The :attribute must be a string.', + 'timezone' => 'The :attribute must be a valid zone.', + 'unique' => 'The :attribute has already been taken.', + 'uploaded' => 'The :attribute failed to upload.', + 'url' => 'The :attribute format is invalid.', + 'uuid' => 'The :attribute must be a valid UUID.', + + /* + |-------------------------------------------------------------------------- + | Custom Validation Language Lines + |-------------------------------------------------------------------------- + | + | Here you may specify custom validation messages for attributes using the + | convention "attribute.rule" to name the lines. This makes it quick to + | specify a specific custom language line for a given attribute rule. + | + */ + + 'custom' => [ + 'attribute-name' => [ + 'rule-name' => 'custom-message', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Custom Validation Attributes + |-------------------------------------------------------------------------- + | + | The following language lines are used to swap our attribute placeholder + | with something more reader friendly such as "E-Mail Address" instead + | of "email". This simply helps us make our message more expressive. + | + */ + + 'attributes' => [], + + 'vue' => [ + 'max' => [ + 'numeric' => '{field} may not be greater than {max}.', + 'string' => '{field} may not be greater than {max} characters.', + ], + 'required' => '{field} is required.', + 'url' => '{field} is not a valid URL.', + ], + +]; diff --git a/resources/lang/vendor/confirmation/ar/confirmation.php b/resources/lang/vendor/confirmation/ar/confirmation.php new file mode 100644 index 0000000..94139db --- /dev/null +++ b/resources/lang/vendor/confirmation/ar/confirmation.php @@ -0,0 +1,16 @@ + 'شكراً لتسجيل إشتراكك! الرجاء التحقق من بريدك الإلكتروني لتأكيد بريدك.', + 'success' => 'لقد تم تأكيد حسابك بنجاح! يمكنك تسجيل الدخول الآن.', + 'again' => 'يجب أن تقوم بتأكيد بريدك الإلكتروني قبل أن يمكنك دخول الموقع. +
    إذا لم تتلقى رسالة التأكيد فتحقق من مجلد البريد الغير هام. +
    للحصول على رسألة تأكيد جديدة، الرجاء الضغط هنا.', + 'resend' => 'تم إرسال رسألة التأكيد. الرجاء التحقق من صندوق بريدك.' +]; diff --git a/resources/lang/vendor/confirmation/cs/confirmation.php b/resources/lang/vendor/confirmation/cs/confirmation.php new file mode 100644 index 0000000..b6f8d50 --- /dev/null +++ b/resources/lang/vendor/confirmation/cs/confirmation.php @@ -0,0 +1,16 @@ + 'Děkujeme za registraci! Prosím, zkontrolujte své e-maily a potvrďte svou e-mailovou adresu.', + 'success' => 'Úspěšně jste ověřili svůj účet! Nyní se můžete přihlásit.', + 'again' => 'Musíte ověřit svůj e-mail, než budete moci přistupovat na web. +
    Pokud jste neobdrželi potvrzovací e-mail, zkontrolujte složku s nevyžádanou poštou. +
    Chcete-li získat nový potvrzovací e-mail, klikněte zde.', + 'resend' => 'Potvrzovací zpráva byla odeslána. Zkontrolujte prosím svou e-mailovou schránku.' +]; diff --git a/resources/lang/vendor/confirmation/da/confirmation.php b/resources/lang/vendor/confirmation/da/confirmation.php new file mode 100644 index 0000000..05fd786 --- /dev/null +++ b/resources/lang/vendor/confirmation/da/confirmation.php @@ -0,0 +1,16 @@ + 'Tak for din tilmelding. Tjek venligst din e-mail for at bekræfte din konto.', + 'success' => 'Du har bekræftet din konto! Du kan nu logge ind.', + 'again' => 'Du skal bekræfte din e-mail adresse før du kan tilgå siden. +
    Hvis du ikke har modtaget bekræftelsesmailen, bør du tjekke din spam mappe. +
    For at modtage en ny bekræftelsesmail kan du klikke her.', + 'resend' => 'E-mail til bekræftelse er sendt. Kontrollér venligst din e-mail.' +]; diff --git a/resources/lang/vendor/confirmation/de/confirmation.php b/resources/lang/vendor/confirmation/de/confirmation.php new file mode 100644 index 0000000..4c55ab7 --- /dev/null +++ b/resources/lang/vendor/confirmation/de/confirmation.php @@ -0,0 +1,16 @@ + 'Vielen Dank für deine Anmeldung! Bitte überprüfe deine E-Mail-Postfach, um deine E-Mail-Adresse zu bestätigen.', + 'success' => 'Du hast dein Konto erfolgreich verifiziert. Du kannst dich ab sofort anmelden.', + 'again' => 'Du musst deine Email-Adresse verifzieren, bevor du auf die Webseite zugreifen kannst. +
    Wenn du die Bestätigungs-E-Mail nicht erhalten hast, überprüfe deinen Spam-Ordner. +
    Um eine neue Bestätigungs-E-Mail zu erhalten, klicke bitte hier.', + 'resend' => 'Wir haben dir einen Link zur Verifikation gesendet. Bitte überprüfe dein E-Mail-Postfach.' +]; diff --git a/resources/lang/vendor/confirmation/el/confirmation.php b/resources/lang/vendor/confirmation/el/confirmation.php new file mode 100644 index 0000000..35ee023 --- /dev/null +++ b/resources/lang/vendor/confirmation/el/confirmation.php @@ -0,0 +1,16 @@ + 'Ευχαριστούμε για την εγγραφή σας! Παρακαλούμε ελέγξτε τα email σας για να επιβεβαιώσετε την διεύθυνση email σας.', + 'success' => 'Έχετε επιβεβαιώσει το λογαριασμό σας! Τώρα μπορείτε να συνδεθείτε.', + 'again' => 'Πρέπει να επιβεβαιώσετε το email σας προτού σας επιτραπεί η είσοδος στον ιστότοπο. +
    Αν δεν έχετε λάβει το email επιβεβαίωσης ελέγξτε τον φάκελο spam. +
    Για να σας αποστείλουμε νέο email επιβεβαίωσης παρακαλώ πατήστε εδώ.', + 'resend' => 'Σας έχουμε αποστείλει ένα email επιβεβαίωσης. Παρακαλώ ελέγξτε το mailbox σας.' +]; diff --git a/resources/lang/vendor/confirmation/en-GB/confirmation.php b/resources/lang/vendor/confirmation/en-GB/confirmation.php new file mode 100644 index 0000000..6124e02 --- /dev/null +++ b/resources/lang/vendor/confirmation/en-GB/confirmation.php @@ -0,0 +1,16 @@ + 'Thanks for signing up! Please check your emails to confirm your email address.', + 'success' => 'You have successfully verified your account! You can now login.', + 'again' => 'You must verify your email before you can access the site. +
    If you have not received the confirmation email check your spam folder. +
    To get a new confirmation email please click here.', + 'resend' => 'A confirmation message has been sent. Please check your mailbox.' +]; diff --git a/resources/lang/vendor/confirmation/en/confirmation.php b/resources/lang/vendor/confirmation/en/confirmation.php new file mode 100644 index 0000000..6124e02 --- /dev/null +++ b/resources/lang/vendor/confirmation/en/confirmation.php @@ -0,0 +1,16 @@ + 'Thanks for signing up! Please check your emails to confirm your email address.', + 'success' => 'You have successfully verified your account! You can now login.', + 'again' => 'You must verify your email before you can access the site. +
    If you have not received the confirmation email check your spam folder. +
    To get a new confirmation email please click here.', + 'resend' => 'A confirmation message has been sent. Please check your mailbox.' +]; diff --git a/resources/lang/vendor/confirmation/es/confirmation.php b/resources/lang/vendor/confirmation/es/confirmation.php new file mode 100644 index 0000000..bed57d0 --- /dev/null +++ b/resources/lang/vendor/confirmation/es/confirmation.php @@ -0,0 +1,16 @@ + '¡Gracias por registrarte! Por favor, revisa tus correos para confirmar tu dirección de correo electrónico.', + 'success' => '¡Has verificado tu cuenta con éxito! Ya puedes iniciar sesión.', + 'again' => 'Debes verificar tu correo electrónico antes de acceder al sitio. +
    Si no has recibido correo de confirmación, revisa tu carpeta de spam. +
    Para obtener un nuevo correo de confirmación, haz clic aquí.', + 'resend' => 'Se ha enviado un mensaje de confirmación. Por favor, revisa tu bandeja de entrada.' +]; diff --git a/resources/lang/vendor/confirmation/fa/confirmation.php b/resources/lang/vendor/confirmation/fa/confirmation.php new file mode 100644 index 0000000..6dd122e --- /dev/null +++ b/resources/lang/vendor/confirmation/fa/confirmation.php @@ -0,0 +1,16 @@ + 'از ثبت نام شما متشکریم! لطفا ایمیل خود را بررسی کنید ، ما ایمیل فعال سازی برایتان ارسال کرده ایم .', + 'success' => 'شما با موفقیت حساب خود را تایید کردید! اکنون می توانید وارد شوید.', + 'again' => 'شما باید ابتدا ایمیل خود را تایید کنید. +
    اگر ایمیل در اینباکس شما نبود پوشه اسپم نیز سرچ کنید. +
    و یا برای دریافت ایمیل جدید اینجا کلیک کنید.', + 'resend' => 'ایمیل تایید برای شما ارسال شد، لطفآ ایمیل خود را بررسی نمایید.' +]; diff --git a/resources/lang/vendor/confirmation/fi/confirmation.php b/resources/lang/vendor/confirmation/fi/confirmation.php new file mode 100644 index 0000000..6124e02 --- /dev/null +++ b/resources/lang/vendor/confirmation/fi/confirmation.php @@ -0,0 +1,16 @@ + 'Thanks for signing up! Please check your emails to confirm your email address.', + 'success' => 'You have successfully verified your account! You can now login.', + 'again' => 'You must verify your email before you can access the site. +
    If you have not received the confirmation email check your spam folder. +
    To get a new confirmation email please click here.', + 'resend' => 'A confirmation message has been sent. Please check your mailbox.' +]; diff --git a/resources/lang/vendor/confirmation/fr/confirmation.php b/resources/lang/vendor/confirmation/fr/confirmation.php new file mode 100644 index 0000000..5fc495c --- /dev/null +++ b/resources/lang/vendor/confirmation/fr/confirmation.php @@ -0,0 +1,16 @@ + 'Merci pour votre inscription ! Merci de vérifier vos courriels pour confirmer votre adresse courriel.', + 'success' => 'Votre compte a été validé ! Vous pouvez maintenant vous connecter.', + 'again' => 'Vous devez vérifier votre adresse courriel avant de pouvoir accéder au site. +
    Si vous n’avez pas reçu le courriel de confirmation vérifiez votre dossier spam. +
    Pour obtenir un nouveau courriel de confirmation cliquez ici.', + 'resend' => 'Un message de confirmation a été envoyé. Merci de vérifier votre boîte aux lettres.' +]; diff --git a/resources/lang/vendor/confirmation/he/confirmation.php b/resources/lang/vendor/confirmation/he/confirmation.php new file mode 100644 index 0000000..772be61 --- /dev/null +++ b/resources/lang/vendor/confirmation/he/confirmation.php @@ -0,0 +1,16 @@ + 'תודה לך על ההרשמה! נא לבדוק את תיבת הדוא״ל שלך כדי לאמת את אותה.', + 'success' => 'החשבון שלך אומת בהצלחה! כעת יתאפשר לך להיכנס אליו.', + 'again' => 'עליך לאמת את כתובת הדוא״ל שלך לפני שתהיה לך אפשרות לגשת לאתר. +
    אם לא קיבלת הודעת אימות בדוא״ל מוטב לחפש בתיקיית דואר הזבל. +
    כדי לקבל דוא״ל אימות מחדש נא ללחוץ כאן.', + 'resend' => 'נשלחה הודעת אימות. נא לבדוק בתיבת הדוא״ל שלך.' +]; diff --git a/resources/lang/vendor/confirmation/hr/confirmation.php b/resources/lang/vendor/confirmation/hr/confirmation.php new file mode 100644 index 0000000..51b2392 --- /dev/null +++ b/resources/lang/vendor/confirmation/hr/confirmation.php @@ -0,0 +1,16 @@ + 'Hvala za prijavu! Molimo vas provjerite e-mail kako biste potvrdili vašu e-mail adresu.', + 'success' => 'Uspješno ste potvrdili vaš račun! Možete se ulogirati.', + 'again' => 'Potrebno je potvrditi e-mail prije pristupa ovoj webstranici. +
    ako niste primili e-mail provjetite spam folder. +
    ako želite poslati novi e-mail za verifikaciju kliknite ovdje.', + 'resend' => 'Poslan vam je e-mail za verifikaciju. Molimo provjerite Vaš e-mail sandučić.' +]; diff --git a/resources/lang/vendor/confirmation/id/confirmation.php b/resources/lang/vendor/confirmation/id/confirmation.php new file mode 100644 index 0000000..b4fbe5d --- /dev/null +++ b/resources/lang/vendor/confirmation/id/confirmation.php @@ -0,0 +1,16 @@ + 'Terima kasih telah mendaftar! Silakan periksa email Anda untuk mengkonfirmasi alamat email Anda.', + 'success' => 'Anda telah berhasil memverifikasi akun Anda! Sekarang Anda bisa masuk.', + 'again' => 'Anda harus memverifikasi email Anda sebelum Anda dapat mengakses situs. +
    Jika Anda belum menerima email konfirmasi, silahkan periksa folder spam Anda. +
    Untuk mendapatkan email konfirmasi baru, silahkan klik di sini.', + 'resend' => 'Sebuah pesan konfirmasi telah dikirimkan. Silakan periksa kotak surat Anda.' +]; diff --git a/resources/lang/vendor/confirmation/it/confirmation.php b/resources/lang/vendor/confirmation/it/confirmation.php new file mode 100644 index 0000000..6d12105 --- /dev/null +++ b/resources/lang/vendor/confirmation/it/confirmation.php @@ -0,0 +1,16 @@ + 'Grazie per esserti registrato! Per favore, controlla la tua casella mail per verificare il tuo indirizzo.', + 'success' => 'Indirizzo verificato con successo! Adesso puoi effettuare il login.', + 'again' => 'Devi verificare il tuo indirizzo mail prima di utilizzare il sito. +
    Se non hai ricevuto la mail, controlla la cartella spam. +
    Per ricevere una nuova mail di conferma clicca qui.', + 'resend' => 'Un messaggio di conferma è stato mandato, controlla la tua casella adesso.' +]; diff --git a/resources/lang/vendor/confirmation/ja/confirmation.php b/resources/lang/vendor/confirmation/ja/confirmation.php new file mode 100644 index 0000000..6124e02 --- /dev/null +++ b/resources/lang/vendor/confirmation/ja/confirmation.php @@ -0,0 +1,16 @@ + 'Thanks for signing up! Please check your emails to confirm your email address.', + 'success' => 'You have successfully verified your account! You can now login.', + 'again' => 'You must verify your email before you can access the site. +
    If you have not received the confirmation email check your spam folder. +
    To get a new confirmation email please click here.', + 'resend' => 'A confirmation message has been sent. Please check your mailbox.' +]; diff --git a/resources/lang/vendor/confirmation/nl/confirmation.php b/resources/lang/vendor/confirmation/nl/confirmation.php new file mode 100644 index 0000000..064587b --- /dev/null +++ b/resources/lang/vendor/confirmation/nl/confirmation.php @@ -0,0 +1,16 @@ + 'Bedankt voor het aanmelden! Controleer je e-mail om je e-mailadres bevestigen.', + 'success' => 'Je hebt je account met succes geverifieerd! Je kunt nu inloggen.', + 'again' => 'Voordat je kunt inloggen, moet je je e-mailadres verifiëren. +
    Als je geen bevestiging ontvangen hebt, controleer dan eerst je spam-map. +
    Klik hier om een nieuwe bevestigings-e-mail te ontvangen.', + 'resend' => 'We hebben een verificatielink gestuurd. Kijk in je inbox.' +]; diff --git a/resources/lang/vendor/confirmation/no/confirmation.php b/resources/lang/vendor/confirmation/no/confirmation.php new file mode 100644 index 0000000..600be86 --- /dev/null +++ b/resources/lang/vendor/confirmation/no/confirmation.php @@ -0,0 +1,16 @@ + 'Takk for at du registrerte deg! Vennligst sjekk e-posten din for å bekrefte e-postadressen din.', + 'success' => 'Du har bekreftet din konto! Du kan nå logge inn.', + 'again' => 'Du må bekrefte e-postadressen din før du får tilgang til nettstedet. +
    Hvis du ikke har mottatt bekreftelsen for e-posten, sjekk søppelpostmappen din. +
    For å få en ny bekreftelses-e-post, klikk her.', + 'resend' => 'En verifikasjons-epost har blitt sendt. Vennligst sjekk din e-post.' +]; diff --git a/resources/lang/vendor/confirmation/pt-BR/confirmation.php b/resources/lang/vendor/confirmation/pt-BR/confirmation.php new file mode 100644 index 0000000..859cfd1 --- /dev/null +++ b/resources/lang/vendor/confirmation/pt-BR/confirmation.php @@ -0,0 +1,16 @@ + 'Obrigado por se cadastrar! Por favor, verifique seus e-mails para confirmar seu endereço de e-mail.', + 'success' => 'Você verificou com sucesso sua conta! Agora você pode entrar.', + 'again' => 'Você deve confirmar seu e-mail antes de acessar o site. +
    Se você não recebeu o e-mail de confirmação, verifique sua caixa de spam. +
    Para receber um novo e-mail de confirmação, por favor clique aqui.', + 'resend' => 'Um e-mail de confirmação foi enviado. Por favor, verifique seu e-mail.' +]; diff --git a/resources/lang/vendor/confirmation/pt/confirmation.php b/resources/lang/vendor/confirmation/pt/confirmation.php new file mode 100644 index 0000000..6124e02 --- /dev/null +++ b/resources/lang/vendor/confirmation/pt/confirmation.php @@ -0,0 +1,16 @@ + 'Thanks for signing up! Please check your emails to confirm your email address.', + 'success' => 'You have successfully verified your account! You can now login.', + 'again' => 'You must verify your email before you can access the site. +
    If you have not received the confirmation email check your spam folder. +
    To get a new confirmation email please click here.', + 'resend' => 'A confirmation message has been sent. Please check your mailbox.' +]; diff --git a/resources/lang/vendor/confirmation/ru/confirmation.php b/resources/lang/vendor/confirmation/ru/confirmation.php new file mode 100644 index 0000000..309c44a --- /dev/null +++ b/resources/lang/vendor/confirmation/ru/confirmation.php @@ -0,0 +1,16 @@ + 'Спасибо за регистрацию! Чтобы подтвердить аккаунт, пожалуйста, проверьте ваш email.', + 'success' => 'Вы успешно подтвердили свой аккаунт. Теперь вы можете войти.', + 'again' => 'Вы должны подтвердить свой email, прежде чем получить доступ к сайту. +
    Если вы не получили письмо с подтверждением, проверьте папку "Спам". +
    Чтобы получить новое письмо с подтверждением, пожалуйста, нажмите здесь.', + 'resend' => 'Письмо с подтверждением было отправлено. Пожалуйста, проверьте вашу почту.' +]; diff --git a/resources/lang/vendor/confirmation/sv/confirmation.php b/resources/lang/vendor/confirmation/sv/confirmation.php new file mode 100644 index 0000000..01b69cd --- /dev/null +++ b/resources/lang/vendor/confirmation/sv/confirmation.php @@ -0,0 +1,16 @@ + 'Tack för att du registrerar dig! Kontrollera dina e-postmeddelanden för att bekräfta din e-postadress.', + 'success' => 'Du har verifierat ditt konto! Du kan nu logga in.', + 'again' => 'Du måste verifiera din e-post innan du kan komma åt webbplatsen. +
    Om du inte har fått bekräftelsemail kontrollera din skräppostmapp. +
    För att få ett nytt bekräftelsemail vänligen klicka här.', + 'resend' => 'Ett verifieringsmail har skickats. Vänligen kontrollera din e-post.' +]; diff --git a/resources/lang/vendor/confirmation/tr/confirmation.php b/resources/lang/vendor/confirmation/tr/confirmation.php new file mode 100644 index 0000000..0765a40 --- /dev/null +++ b/resources/lang/vendor/confirmation/tr/confirmation.php @@ -0,0 +1,16 @@ + 'Hesap oluşturduğunuz için teşekkür ederiz! Lütfen e-posta adresinizi doğrulatmak için e-postanızı kontrol ediniz.', + 'success' => 'Hesabınızı başarıyla doğruladınız. Şimdi giriş yapabilirsiniz.', + 'again' => 'Oturum açabilmek için e-posta adresinizi doğrulatmanız gerekmektedir. +
    Gelen kutunuzda doğrulama mailini göremiyorsanız Spam klasörünü de kontrol ediniz. +
    Doğrulama mailini tekrar almak için lütfen buraya tıklayınız.', + 'resend' => 'Doğrulama e-postası gönderildi. Lütfen e-postanızı kontrol edin.' +]; diff --git a/resources/lang/vendor/confirmation/uk/confirmation.php b/resources/lang/vendor/confirmation/uk/confirmation.php new file mode 100644 index 0000000..6f8147c --- /dev/null +++ b/resources/lang/vendor/confirmation/uk/confirmation.php @@ -0,0 +1,16 @@ + 'Дякуємо за реєстрацію! Перевірте вашу електронну пошту, щоб підтвердити свою адресу електронної пошти.', + 'success' => 'Ви успішно підтвердили свій акаунт! Тепер ви можете увійти.', + 'again' => 'Ви повинні підтвердити вашу електронну пошту, перш ніж ви зможете увійти на сайт. +
    Якщо ви не отримали електронного листа з підтвердженням, перевірте папку зі спамом. +
    Щоб отримати новий лист для підтвердження, будь ласка, натисніть тут.', + 'resend' => 'Підтверджувального листа було надіслано. Будь ласка, перевірте вашу поштову скриньку.' +]; diff --git a/resources/lang/vendor/confirmation/vi/confirmation.php b/resources/lang/vendor/confirmation/vi/confirmation.php new file mode 100644 index 0000000..97fe65b --- /dev/null +++ b/resources/lang/vendor/confirmation/vi/confirmation.php @@ -0,0 +1,16 @@ + 'Cảm ơn bạn đã đăng kí! Vui lòng kiểm tra email để xác thực địa chỉ email của bạn.', + 'success' => 'Xác thực tài khoản thành công. Bạn có thể đăng nhập ngay bây giờ.', + 'again' => 'Bạn phải xác thực email trước khi có thể truy cập website. +
    Nếu bạn không nhận được email xác thực, hãy kiểm tra trong hòm thư rác. +
    Để lấy email xác thực mới, hãy bấm vào đây.', + 'resend' => 'Tin nhắn xác thực đã được gửi. Hãy kiểm tra hộp thư đến của bạn.' +]; diff --git a/resources/lang/vendor/confirmation/zh-TW/confirmation.php b/resources/lang/vendor/confirmation/zh-TW/confirmation.php new file mode 100644 index 0000000..24d26ce --- /dev/null +++ b/resources/lang/vendor/confirmation/zh-TW/confirmation.php @@ -0,0 +1,16 @@ + '您已註冊完成,請至Email信箱點選確認以驗證信箱地址。', + 'success' => '帳戶驗證成功!請登入後開始使用', + 'again' => '您必須先確認信箱地址正確。 +
    若尚未收到驗證信,請檢查垃圾信件匣。 +
    重新發送驗證信。', + 'resend' => '信箱驗證信已寄出,請檢查您的收件匣。' +]; diff --git a/resources/lang/vendor/confirmation/zh/confirmation.php b/resources/lang/vendor/confirmation/zh/confirmation.php new file mode 100644 index 0000000..02a0813 --- /dev/null +++ b/resources/lang/vendor/confirmation/zh/confirmation.php @@ -0,0 +1,16 @@ + '感谢您的注册!请检查您的邮箱来验证您的邮件。', + 'success' => '您已成功验证邮件地址!现在您可以正常登录了。', + 'again' => '您需要验证邮件地址才能访问此网站。 +
    如果您没有收到电子邮件,您可以检查一下垃圾箱。 +
    点击此处来重新发送验证邮件', + 'resend' => '验证邮件已发送,请检查您的收件箱。' +]; diff --git a/resources/lang/vendor/webauthn/ar/errors.php b/resources/lang/vendor/webauthn/ar/errors.php new file mode 100644 index 0000000..ca01b52 --- /dev/null +++ b/resources/lang/vendor/webauthn/ar/errors.php @@ -0,0 +1,15 @@ + 'You need to log in before doing a Webauthn authentication', + 'auth_data_not_found' => 'Authentication data not found', + 'create_data_not_found' => 'Register data not found', + 'object_not_found' => 'Object not found', + +]; diff --git a/resources/lang/vendor/webauthn/cs/errors.php b/resources/lang/vendor/webauthn/cs/errors.php new file mode 100644 index 0000000..4988715 --- /dev/null +++ b/resources/lang/vendor/webauthn/cs/errors.php @@ -0,0 +1,15 @@ + 'Před autentizací Webauthn se musíte přihlásit', + 'auth_data_not_found' => 'Ověřovací údaje nebyly nalezeny', + 'create_data_not_found' => 'Register data not found', + 'object_not_found' => 'Objekt nenalezen', + +]; diff --git a/resources/lang/vendor/webauthn/da/errors.php b/resources/lang/vendor/webauthn/da/errors.php new file mode 100644 index 0000000..ca01b52 --- /dev/null +++ b/resources/lang/vendor/webauthn/da/errors.php @@ -0,0 +1,15 @@ + 'You need to log in before doing a Webauthn authentication', + 'auth_data_not_found' => 'Authentication data not found', + 'create_data_not_found' => 'Register data not found', + 'object_not_found' => 'Object not found', + +]; diff --git a/resources/lang/vendor/webauthn/de/errors.php b/resources/lang/vendor/webauthn/de/errors.php new file mode 100644 index 0000000..8c07bc8 --- /dev/null +++ b/resources/lang/vendor/webauthn/de/errors.php @@ -0,0 +1,15 @@ + 'Sie müssen sich vor einer WebAuthn-Authentifizierung anmelden', + 'auth_data_not_found' => 'Authentifizierungsdaten nicht gefunden', + 'create_data_not_found' => 'Registrierungsdaten nicht gefunden', + 'object_not_found' => 'Objekt nicht gefunden', + +]; diff --git a/resources/lang/vendor/webauthn/el/errors.php b/resources/lang/vendor/webauthn/el/errors.php new file mode 100644 index 0000000..2581f8d --- /dev/null +++ b/resources/lang/vendor/webauthn/el/errors.php @@ -0,0 +1,15 @@ + 'Θα πρέπει να εισέλθετε προτού να κάνετε μία αυθεντικοποίηση Webauth', + 'auth_data_not_found' => 'Δεν βρέθηκαν δεδομένα αυθεντικοποίησης', + 'create_data_not_found' => 'Δεν βρέθηκαν δεδομένα εγγραφής', + 'object_not_found' => 'Δεν βρέθηκε το αντικείμενο', + +]; diff --git a/resources/lang/vendor/webauthn/en-GB/errors.php b/resources/lang/vendor/webauthn/en-GB/errors.php new file mode 100644 index 0000000..ca01b52 --- /dev/null +++ b/resources/lang/vendor/webauthn/en-GB/errors.php @@ -0,0 +1,15 @@ + 'You need to log in before doing a Webauthn authentication', + 'auth_data_not_found' => 'Authentication data not found', + 'create_data_not_found' => 'Register data not found', + 'object_not_found' => 'Object not found', + +]; diff --git a/resources/lang/vendor/webauthn/en/errors.php b/resources/lang/vendor/webauthn/en/errors.php new file mode 100644 index 0000000..ca01b52 --- /dev/null +++ b/resources/lang/vendor/webauthn/en/errors.php @@ -0,0 +1,15 @@ + 'You need to log in before doing a Webauthn authentication', + 'auth_data_not_found' => 'Authentication data not found', + 'create_data_not_found' => 'Register data not found', + 'object_not_found' => 'Object not found', + +]; diff --git a/resources/lang/vendor/webauthn/es/errors.php b/resources/lang/vendor/webauthn/es/errors.php new file mode 100644 index 0000000..bd5d5de --- /dev/null +++ b/resources/lang/vendor/webauthn/es/errors.php @@ -0,0 +1,15 @@ + 'Necesitas iniciar sesión antes de realizar una autenticación Webauthn', + 'auth_data_not_found' => 'Datos de autenticación no encontrados', + 'create_data_not_found' => 'Datos de registro no encontrados', + 'object_not_found' => 'Objeto no encontrado', + +]; diff --git a/resources/lang/vendor/webauthn/fa/errors.php b/resources/lang/vendor/webauthn/fa/errors.php new file mode 100644 index 0000000..05c3538 --- /dev/null +++ b/resources/lang/vendor/webauthn/fa/errors.php @@ -0,0 +1,15 @@ + 'قبل از انجام احراز هویت Webauthn باید وارد سیستم شوید ', + 'auth_data_not_found' => 'داده های احراز هویت یافت نشد', + 'create_data_not_found' => 'دیتای ثبت نام یافت نشد', + 'object_not_found' => 'شی پیدا نشد ', + +]; diff --git a/resources/lang/vendor/webauthn/fi/errors.php b/resources/lang/vendor/webauthn/fi/errors.php new file mode 100644 index 0000000..a6e28e5 --- /dev/null +++ b/resources/lang/vendor/webauthn/fi/errors.php @@ -0,0 +1,15 @@ + 'Sinun täytyy kirjautua sisään ennen Webauthn todennusta', + 'auth_data_not_found' => 'Todennustietoja ei löytynyt', + 'create_data_not_found' => 'Rekisteröintitietoja ei löydy', + 'object_not_found' => 'Kohdetta ei löydy', + +]; diff --git a/resources/lang/vendor/webauthn/fr/errors.php b/resources/lang/vendor/webauthn/fr/errors.php new file mode 100644 index 0000000..a7fbb09 --- /dev/null +++ b/resources/lang/vendor/webauthn/fr/errors.php @@ -0,0 +1,15 @@ + 'Vous devez vous connecter avant de faire une authentification Webauthn', + 'auth_data_not_found' => 'Données d’authentification introuvables', + 'create_data_not_found' => 'Données d’enregistrement non trouvées', + 'object_not_found' => 'Objet introuvable', + +]; diff --git a/resources/lang/vendor/webauthn/he/errors.php b/resources/lang/vendor/webauthn/he/errors.php new file mode 100644 index 0000000..5bde6c4 --- /dev/null +++ b/resources/lang/vendor/webauthn/he/errors.php @@ -0,0 +1,15 @@ + 'עליך להיכנס בטרם ביצוע אימות עם Webauthn', + 'auth_data_not_found' => 'לא נמצאו נתוני אימות', + 'create_data_not_found' => 'נתוני ההרשמה לא נמצאו', + 'object_not_found' => 'הפריט לא נמצא', + +]; diff --git a/resources/lang/vendor/webauthn/hr/errors.php b/resources/lang/vendor/webauthn/hr/errors.php new file mode 100644 index 0000000..ca01b52 --- /dev/null +++ b/resources/lang/vendor/webauthn/hr/errors.php @@ -0,0 +1,15 @@ + 'You need to log in before doing a Webauthn authentication', + 'auth_data_not_found' => 'Authentication data not found', + 'create_data_not_found' => 'Register data not found', + 'object_not_found' => 'Object not found', + +]; diff --git a/resources/lang/vendor/webauthn/id/errors.php b/resources/lang/vendor/webauthn/id/errors.php new file mode 100644 index 0000000..324fdba --- /dev/null +++ b/resources/lang/vendor/webauthn/id/errors.php @@ -0,0 +1,15 @@ + 'Anda perlu masuk sebelum melakukan sebuah otentikasi WebAuthn', + 'auth_data_not_found' => 'Data otentikasi tidak ditemukan', + 'create_data_not_found' => 'Data Pendaftaran tidak ditemukan', + 'object_not_found' => 'Obyek tidak ditemukan', + +]; diff --git a/resources/lang/vendor/webauthn/it/errors.php b/resources/lang/vendor/webauthn/it/errors.php new file mode 100644 index 0000000..5265787 --- /dev/null +++ b/resources/lang/vendor/webauthn/it/errors.php @@ -0,0 +1,15 @@ + 'Devi effettuare l\'accesso prima di effettuare un\'autenticazione Webauthn', + 'auth_data_not_found' => 'Dati di autenticazione non trovati', + 'create_data_not_found' => 'Dati di registrazione non trovati', + 'object_not_found' => 'Oggetto non trovato', + +]; diff --git a/resources/lang/vendor/webauthn/ja/errors.php b/resources/lang/vendor/webauthn/ja/errors.php new file mode 100644 index 0000000..ca01b52 --- /dev/null +++ b/resources/lang/vendor/webauthn/ja/errors.php @@ -0,0 +1,15 @@ + 'You need to log in before doing a Webauthn authentication', + 'auth_data_not_found' => 'Authentication data not found', + 'create_data_not_found' => 'Register data not found', + 'object_not_found' => 'Object not found', + +]; diff --git a/resources/lang/vendor/webauthn/nl/errors.php b/resources/lang/vendor/webauthn/nl/errors.php new file mode 100644 index 0000000..3aae8b7 --- /dev/null +++ b/resources/lang/vendor/webauthn/nl/errors.php @@ -0,0 +1,15 @@ + 'U moet inloggen voordat u een Webauthn authenticatie kunt uitvoeren', + 'auth_data_not_found' => 'Authenticatiedata niet gevonden', + 'create_data_not_found' => 'Registratiedata niet gevonden', + 'object_not_found' => 'Voorwerp niet gevonden', + +]; diff --git a/resources/lang/vendor/webauthn/no/errors.php b/resources/lang/vendor/webauthn/no/errors.php new file mode 100644 index 0000000..bc3e638 --- /dev/null +++ b/resources/lang/vendor/webauthn/no/errors.php @@ -0,0 +1,15 @@ + 'Du må logge inn før du utfører en Webauthn-autentisering', + 'auth_data_not_found' => 'Autentiseringsdata ikke funnet', + 'create_data_not_found' => 'Registreringsdata ikke funnet', + 'object_not_found' => 'Objektet ble ikke funnet', + +]; diff --git a/resources/lang/vendor/webauthn/pt-BR/errors.php b/resources/lang/vendor/webauthn/pt-BR/errors.php new file mode 100644 index 0000000..44669a9 --- /dev/null +++ b/resources/lang/vendor/webauthn/pt-BR/errors.php @@ -0,0 +1,15 @@ + 'Você precisa fazer login antes de fazer uma autenticação de Webauthn', + 'auth_data_not_found' => 'Dados de autenticação não encontrados', + 'create_data_not_found' => 'Registro de dados não encontrado', + 'object_not_found' => 'Objeto não encontrado', + +]; diff --git a/resources/lang/vendor/webauthn/pt/errors.php b/resources/lang/vendor/webauthn/pt/errors.php new file mode 100644 index 0000000..ca01b52 --- /dev/null +++ b/resources/lang/vendor/webauthn/pt/errors.php @@ -0,0 +1,15 @@ + 'You need to log in before doing a Webauthn authentication', + 'auth_data_not_found' => 'Authentication data not found', + 'create_data_not_found' => 'Register data not found', + 'object_not_found' => 'Object not found', + +]; diff --git a/resources/lang/vendor/webauthn/ru/errors.php b/resources/lang/vendor/webauthn/ru/errors.php new file mode 100644 index 0000000..e7dfb1f --- /dev/null +++ b/resources/lang/vendor/webauthn/ru/errors.php @@ -0,0 +1,15 @@ + 'Вы должны войти в систему перед использованием Webauth аутентификации', + 'auth_data_not_found' => 'Данные аутентификации не найдены', + 'create_data_not_found' => 'Данные о регистрации не найдены', + 'object_not_found' => 'Объект не найден', + +]; diff --git a/resources/lang/vendor/webauthn/sv/errors.php b/resources/lang/vendor/webauthn/sv/errors.php new file mode 100644 index 0000000..c53c7ff --- /dev/null +++ b/resources/lang/vendor/webauthn/sv/errors.php @@ -0,0 +1,15 @@ + 'Du måste logga in innan du gör en Webauthn-autentisering', + 'auth_data_not_found' => 'Autentiseringsdata hittades inte', + 'create_data_not_found' => 'Registerdata hittades inte', + 'object_not_found' => 'Objekt hittades inte', + +]; diff --git a/resources/lang/vendor/webauthn/tr/errors.php b/resources/lang/vendor/webauthn/tr/errors.php new file mode 100644 index 0000000..2a202ca --- /dev/null +++ b/resources/lang/vendor/webauthn/tr/errors.php @@ -0,0 +1,15 @@ + 'Webauthn kimlik doğrulaması yapmadan önce giriş yapmanız gerekmektedir', + 'auth_data_not_found' => 'Kimlik doğrulama verileri bulunamadı', + 'create_data_not_found' => 'Kayıt verisi bulunamadı', + 'object_not_found' => 'Nesne bulunamadı', + +]; diff --git a/resources/lang/vendor/webauthn/uk/errors.php b/resources/lang/vendor/webauthn/uk/errors.php new file mode 100644 index 0000000..e4d2c6c --- /dev/null +++ b/resources/lang/vendor/webauthn/uk/errors.php @@ -0,0 +1,15 @@ + 'Вам необхідно увійти в систему перед виконанням аутентифікації Webauthn', + 'auth_data_not_found' => 'Дані аутентифікації не знайдено', + 'create_data_not_found' => 'Реєстраційні дані не знайдені', + 'object_not_found' => 'Об\'єкт не знайдено', + +]; diff --git a/resources/lang/vendor/webauthn/vi/errors.php b/resources/lang/vendor/webauthn/vi/errors.php new file mode 100644 index 0000000..0e09a21 --- /dev/null +++ b/resources/lang/vendor/webauthn/vi/errors.php @@ -0,0 +1,15 @@ + 'Bạn cần đăng nhập trước khi thực hiện xác thực Webauthn', + 'auth_data_not_found' => 'Không tìm thấy dữ liệu xác thực', + 'create_data_not_found' => 'Không tìm thấy dữ liệu đăng kí', + 'object_not_found' => 'Không tìm thấy đối tượng', + +]; diff --git a/resources/lang/vendor/webauthn/zh-TW/errors.php b/resources/lang/vendor/webauthn/zh-TW/errors.php new file mode 100644 index 0000000..ca01b52 --- /dev/null +++ b/resources/lang/vendor/webauthn/zh-TW/errors.php @@ -0,0 +1,15 @@ + 'You need to log in before doing a Webauthn authentication', + 'auth_data_not_found' => 'Authentication data not found', + 'create_data_not_found' => 'Register data not found', + 'object_not_found' => 'Object not found', + +]; diff --git a/resources/lang/vendor/webauthn/zh/errors.php b/resources/lang/vendor/webauthn/zh/errors.php new file mode 100644 index 0000000..0a96cf2 --- /dev/null +++ b/resources/lang/vendor/webauthn/zh/errors.php @@ -0,0 +1,15 @@ + ' Webauthn 认证之前您需要先登录', + 'auth_data_not_found' => '找不到身份验证数据', + 'create_data_not_found' => '未找到注册数据', + 'object_not_found' => '未找到对象', + +]; diff --git a/resources/lang/vi.json b/resources/lang/vi.json new file mode 100644 index 0000000..a4aa42c --- /dev/null +++ b/resources/lang/vi.json @@ -0,0 +1,7 @@ +{ + "The :attribute must contain at least one uppercase and one lowercase letter.": "Trường :attribute phải chứa ít nhất một chữ hoa và một chữ thường.", + "The :attribute must contain at least one letter.": "Trường :attribute phải chứa ít nhất một chữ cái.", + "The :attribute must contain at least one symbol.": "Trường :attribute must phải chứa ít nhất một ký hiệu.", + "The :attribute must contain at least one number.": "Trường :attribute phải chứa ít nhất một số.", + "The given :attribute has appeared in a data leak. Please choose a different :attribute.": ":attribute đã cho đã xuất hiện trong một vụ rò rỉ dữ liệu. Vui lòng chọn :attribute khác." +} diff --git a/resources/lang/vi/app.php b/resources/lang/vi/app.php new file mode 100644 index 0000000..7147e5f --- /dev/null +++ b/resources/lang/vi/app.php @@ -0,0 +1,571 @@ + 'Có', + 'no' => 'Không', + 'update' => 'Cập nhật', + 'save' => 'Lưu', + 'add' => 'Thêm', + 'cancel' => 'Hủy', + 'confirm' => 'Xác nhận', + 'delete_confirm' => 'Bạn chắc chứ?', + 'delete' => 'Xóa', + 'edit' => 'Sửa', + 'upload' => 'Tải lên', + 'download' => 'Tải xuống', + 'save_close' => 'Lưu và đóng', + 'close' => 'Đóng', + 'copy' => 'Sao chép', + 'create' => 'Tạo', + 'remove' => 'Xoá', + 'revoke' => 'Thu hồi', + 'done' => 'Xong', + 'back' => 'Quay lại', + 'verify' => 'Xác minh', + 'new' => 'new', + 'unknown' => 'Tôi không biết', + 'load_more' => 'Hiển thị thêm', + 'loading' => 'Đang tải…', + 'with' => 'với', + 'today' => 'hôm nay', + 'yesterday' => 'hôm qua', + 'another_day' => 'ngày khác', + 'date' => 'Ngày', + 'type' => 'Kiểu', + 'zoom' => 'Phóng to / Thu nhỏ', + 'upgrade' => 'Nâng cấp để mở khóa', + 'percent_uploaded' => 'Đã tải lên {percent}%', + 'retry' => 'Thử lại', + 'filter' => 'Lọc danh sách', + 'go_back' => 'Quay lại', + 'file_selected' => '{count} tệp được chọn…', + + 'application_title' => 'Monica – hệ thống quản trị quan hệ cá nhân', + 'application_description' => 'Monica là công cụ để quản lý tương tác của bạn với người yêu, bạn bè và gia đình.', + 'application_og_title' => 'Có một mối quan hệ tốt hơn với người bạn yêu thương. CRM miễn phí cho bạn và gia đình.', + + 'markdown_description' => 'Muốn định dạng chữ tốt hơn? Chúng tôi hỗ trợ Markdown với chữ đậm, nghiêng, danh sách và nhiều hơn thế nữa.', + 'markdown_link' => 'Đọc tài liệu hướng dẫn', + + 'header_settings_link' => 'Cài đặt', + 'header_logout_link' => 'Thoát', + 'header_changelog_link' => 'Cập nhật sản phẩm', + + 'main_nav_cta' => 'Thêm người', + 'main_nav_dashboard' => 'Trang tổng quan', + 'main_nav_family' => 'Danh bạ', + 'main_nav_journal' => 'Nhật ký', + 'main_nav_activities' => 'Hoạt động', + 'main_nav_tasks' => 'Tác vụ', + + 'footer_remarks' => 'Bình luận?', + 'footer_send_email' => 'Gửi email cho chúng tôi', + 'footer_privacy' => 'Chính sách quyền riêng tư', + 'footer_release' => 'Ghi chú phát hành', + 'footer_newsletter' => 'Bản tin', + 'footer_source_code' => 'Đóng góp', + 'footer_version' => 'Phiên bản: :version', + 'footer_new_version' => 'Đã có phiên bản mới của Monica', + + 'footer_modal_version_whats_new' => 'Có gì mới', + 'footer_modal_version_release_away' => 'Bạn có :number bản cập nhật. Bạn nên cập nhật hệ thống.', + + 'breadcrumb_dashboard' => 'Trang tổng quan', + 'breadcrumb_list_contacts' => 'Danh sách liên hệ', + 'breadcrumb_archived_contacts' => 'Liên hệ đã lưu trữ', + 'breadcrumb_journal' => 'Nhật ký', + 'breadcrumb_settings' => 'Cài đặt', + 'breadcrumb_settings_export' => 'Xuất', + 'breadcrumb_settings_users' => 'Người dùng', + 'breadcrumb_settings_users_add' => 'Thêm người dùng', + 'breadcrumb_settings_subscriptions' => 'Gói thuê bao', + 'breadcrumb_settings_import' => 'Nhập dữ liệu', + 'breadcrumb_settings_import_report' => 'Nhập báo cáo', + 'breadcrumb_settings_import_upload' => 'Tải lên', + 'breadcrumb_settings_tags' => 'Nhãn', + 'breadcrumb_add_significant_other' => 'Thêm điều trọng đại khác', + 'breadcrumb_edit_significant_other' => 'Sửa điều trọng đại khác', + 'breadcrumb_add_note' => 'Thêm ghi chú', + 'breadcrumb_edit_note' => 'Sửa ghi chú', + 'breadcrumb_api' => 'API', + 'breadcrumb_dav' => 'DAV Resources', + 'breadcrumb_edit_introductions' => 'Đã gặp nhau thế nào', + 'breadcrumb_settings_personalization' => 'Cá nhân hóa', + 'breadcrumb_settings_security' => 'Bảo mật', + 'breadcrumb_settings_security_2fa' => 'Xác thực 2 yếu tố', + 'breadcrumb_profile' => 'Hồ sơ của :name', + + 'gender_male' => 'Nam', + 'gender_female' => 'Nữ', + 'gender_none' => 'Không tiết lộ', + 'gender_no_gender' => 'Không xác định giới tính', + + 'error_title' => 'Rất tiếc! Đã xảy ra lỗi.', + 'error_unauthorized' => 'Bạn không có quyền chỉnh sửa tài nguyên này.', + 'error_user_account' => 'Người dùng này không thuộc về tài khoản nào.', + 'error_save' => 'Có lỗi khi lưu dữ liệu.', + 'error_try_again' => 'Có sự cố. Xin vui lòng thử lại.', + 'error_id' => 'ID lỗi: :id', + 'error_unavailable' => 'Dịch vụ không khả dụng', + 'error_maintenance' => 'Đang bảo trì. Chúng tôi sẽ trở lại ngay.', + 'error_help' => 'Chúng tôi sẽ trở lại ngay.', + 'error_twitter' => 'Follow tài khoản Twitter để nhận thông báo điều gì đang diễn ra.', + 'error_no_term' => 'Không có chính sách nào cho instance này.', + + 'default_save_success' => 'Lưu dữ liệu thành công.', + + 'compliance_title' => 'Xin lỗi vì sự gián đoạn.', + 'compliance_desc' => 'Chúng tôi đã thay đổi Điều khoản sử dụngChính sách bảo mật. Theo luật chúng tôi phải hỏi bạn xem xét và chấp nhận chúng, sau đó bạn có thể tiếp tục sử dụng tài khoản của bạn.', + 'compliance_desc_end' => 'Chúng tôi không làm bất cứ điều gì xấu với dữ liệu hoặc tài khoản của bạn và chúng tôi sẽ không bao giờ làm như vậy.', + 'compliance_terms' => 'Đồng ý điều khoản mới', + + // Relationship types + // Yes, each relationship type has 8 strings associated with it. + // This is because we need to indicate the name of the relationship type, + // and also the name of the opposite side of this relationship (father/son), + // and then, the feminine version of the string. Finally, in some sentences + // in the UI, we need to include the name of the person we add the relationship + // to. + 'relationship_type_group_love' => 'Mối quan hệ tình cảm', + 'relationship_type_group_family' => 'Mối quan hệ gia đình', + 'relationship_type_group_friend' => 'Mối quan hệ bạn bè', + 'relationship_type_group_work' => 'Mối quan hệ công việc', + 'relationship_type_group_other' => 'Các mối quan hệ khác', + + 'relationship_type_partner' => 'điều trọng đại khác', + 'relationship_type_partner_female' => 'điều trọng đại khác', + 'relationship_type_partner_male' => 'significant other', + 'relationship_type_partner_with_name' => 'người quan trọng với :name', + 'relationship_type_partner_female_with_name' => 'người quan trọng với :name', + 'relationship_type_partner_male_with_name' => ':name’s significant other', + + 'relationship_type_spouse' => 'vợ/chồng', + 'relationship_type_spouse_female' => 'wife', + 'relationship_type_spouse_male' => 'husband', + 'relationship_type_spouse_with_name' => 'chồng của :name', + 'relationship_type_spouse_female_with_name' => ':name’s wife', + 'relationship_type_spouse_male_with_name' => ':name’s husband', + + 'relationship_type_date' => 'hẹn hò', + 'relationship_type_date_female' => 'hẹn hò', + 'relationship_type_date_male' => 'date', + 'relationship_type_date_with_name' => 'hẹn hò với :name', + 'relationship_type_date_female_with_name' => 'hẹn hò với :name', + 'relationship_type_date_male_with_name' => ':name’s date', + + 'relationship_type_lover' => 'người yêu', + 'relationship_type_lover_female' => 'người yêu', + 'relationship_type_lover_male' => 'lover', + 'relationship_type_lover_with_name' => 'người yêu của :name', + 'relationship_type_lover_female_with_name' => 'người yêu của :name', + 'relationship_type_lover_male_with_name' => ':name’s lover', + + 'relationship_type_inlovewith' => 'có tình cảm với', + 'relationship_type_inlovewith_female' => 'có tình cảm với', + 'relationship_type_inlovewith_male' => 'in love with', + 'relationship_type_inlovewith_with_name' => 'ai đó :name có tình cảm', + 'relationship_type_inlovewith_female_with_name' => 'ai đó :name có tình cảm', + 'relationship_type_inlovewith_male_with_name' => 'someone :name is in love with', + + 'relationship_type_lovedby' => 'được thích bởi', + 'relationship_type_lovedby_female' => 'được thích bởi', + 'relationship_type_lovedby_male' => 'loved by', + 'relationship_type_lovedby_with_name' => 'người tình bí mật của :name', + 'relationship_type_lovedby_female_with_name' => 'người tình bí mật của :name', + 'relationship_type_lovedby_male_with_name' => ':name’s secret lover', + + 'relationship_type_ex' => 'ex-partner', + 'relationship_type_ex_female' => 'người yêu cũ', + 'relationship_type_ex_male' => 'ex-boyfriend', + 'relationship_type_ex_with_name' => ':name’s ex-partner', + 'relationship_type_ex_female_with_name' => 'người yêu cũ của :name', + 'relationship_type_ex_male_with_name' => ':name’s ex-boyfriend', + + 'relationship_type_parent' => 'parent', + 'relationship_type_parent_female' => 'mẹ', + 'relationship_type_parent_male' => 'father', + 'relationship_type_parent_with_name' => ':name’s parent', + 'relationship_type_parent_female_with_name' => 'mẹ của :name', + 'relationship_type_parent_male_with_name' => ':name’s father', + + 'relationship_type_child' => 'child', + 'relationship_type_child_female' => 'con gái', + 'relationship_type_child_male' => 'son', + 'relationship_type_child_with_name' => ':name’s child', + 'relationship_type_child_female_with_name' => 'con gái của :name', + 'relationship_type_child_male_with_name' => ':name’s son', + + 'relationship_type_stepparent' => 'step-parent', + 'relationship_type_stepparent_female' => 'mẹ kế', + 'relationship_type_stepparent_male' => 'stepfather', + 'relationship_type_stepparent_with_name' => ':name’s step-parent', + 'relationship_type_stepparent_female_with_name' => 'mẹ kế của :name', + 'relationship_type_stepparent_male_with_name' => ':name’s stepfather', + + 'relationship_type_stepchild' => 'stepchild', + 'relationship_type_stepchild_female' => 'con gái riêng', + 'relationship_type_stepchild_male' => 'stepson', + 'relationship_type_stepchild_with_name' => ':name’s stepchild', + 'relationship_type_stepchild_female_with_name' => 'con gái riêng của :name', + 'relationship_type_stepchild_male_with_name' => ':name’s stepson', + + 'relationship_type_sibling' => 'sibling', + 'relationship_type_sibling_female' => 'Chị em gái', + 'relationship_type_sibling_male' => 'brother', + 'relationship_type_sibling_with_name' => ':name’s sibling', + 'relationship_type_sibling_female_with_name' => 'chị gái của :name', + 'relationship_type_sibling_male_with_name' => ':name’s brother', + + 'relationship_type_grandparent' => 'grandparent', + 'relationship_type_grandparent_female' => 'grandmother', + 'relationship_type_grandparent_male' => 'grandfather', + 'relationship_type_grandparent_with_name' => ':name’s grandparent', + 'relationship_type_grandparent_female_with_name' => ':name’s grandmother', + 'relationship_type_grandparent_male_with_name' => ':name’s grandfather', + + 'relationship_type_grandchild' => 'grandchild', + 'relationship_type_grandchild_female' => 'granddaughter', + 'relationship_type_grandchild_male' => 'grandson', + 'relationship_type_grandchild_with_name' => ':name’s grandchild', + 'relationship_type_grandchild_female_with_name' => ':name’s granddauther', + 'relationship_type_grandchild_male_with_name' => ':name’s grandson', + + 'relationship_type_uncle' => 'chú/bác', + 'relationship_type_uncle_female' => 'cô/dì', + 'relationship_type_uncle_male' => 'uncle', + 'relationship_type_uncle_with_name' => 'chú/bác của :name', + 'relationship_type_uncle_female_with_name' => 'cô/dì của :name', + 'relationship_type_uncle_male_with_name' => ':name’s uncle', + + 'relationship_type_nephew' => 'cháu trai', + 'relationship_type_nephew_female' => 'cháu gái', + 'relationship_type_nephew_male' => 'nephew', + 'relationship_type_nephew_with_name' => 'cháu trai của :name', + 'relationship_type_nephew_female_with_name' => 'cháu gái của :name', + 'relationship_type_nephew_male_with_name' => ':name’s nephew', + + 'relationship_type_cousin' => 'anh em họ', + 'relationship_type_cousin_female' => 'anh em họ', + 'relationship_type_cousin_male' => 'cousin', + 'relationship_type_cousin_with_name' => 'anh em họ của :name', + 'relationship_type_cousin_female_with_name' => 'anh em họ của :name', + 'relationship_type_cousin_male_with_name' => ':name’s cousin', + + 'relationship_type_godfather' => 'godparent', + 'relationship_type_godfather_female' => 'mẹ đỡ đầu', + 'relationship_type_godfather_male' => 'godfather', + 'relationship_type_godfather_with_name' => ':name’s godparent', + 'relationship_type_godfather_female_with_name' => 'mẹ đỡ đầu của :name', + 'relationship_type_godfather_male_with_name' => ':name’s godfather', + + 'relationship_type_godson' => 'godchild', + 'relationship_type_godson_female' => 'con gái đỡ đầu', + 'relationship_type_godson_male' => 'godson', + 'relationship_type_godson_with_name' => ':name’s godchild', + 'relationship_type_godson_female_with_name' => 'con gái đỡ đầu của :name', + 'relationship_type_godson_male_with_name' => ':name’s godson', + + 'relationship_type_friend' => 'bạn bè', + 'relationship_type_friend_female' => 'bạn bè', + 'relationship_type_friend_male' => 'friend', + 'relationship_type_friend_with_name' => 'bạn của :name', + 'relationship_type_friend_female_with_name' => 'bạn của :name', + 'relationship_type_friend_male_with_name' => ':name’s friend', + + 'relationship_type_bestfriend' => 'bạn thân', + 'relationship_type_bestfriend_female' => 'bạn thân', + 'relationship_type_bestfriend_male' => 'best friend', + 'relationship_type_bestfriend_with_name' => 'bạn thân của :name', + 'relationship_type_bestfriend_female_with_name' => 'bạn thân của :name', + 'relationship_type_bestfriend_male_with_name' => ':name’s best friend', + + 'relationship_type_colleague' => 'đồng nghiệp', + 'relationship_type_colleague_female' => 'đồng nghiệp', + 'relationship_type_colleague_male' => 'colleague', + 'relationship_type_colleague_with_name' => 'dồng nghiệp của :name', + 'relationship_type_colleague_female_with_name' => 'dồng nghiệp của :name', + 'relationship_type_colleague_male_with_name' => ':name’s colleague', + + 'relationship_type_boss' => 'sếp', + 'relationship_type_boss_female' => 'sếp', + 'relationship_type_boss_male' => 'boss', + 'relationship_type_boss_with_name' => 'sếp của :name', + 'relationship_type_boss_female_with_name' => 'sếp của :name', + 'relationship_type_boss_male_with_name' => ':name’s boss', + + 'relationship_type_subordinate' => 'cấp dưới', + 'relationship_type_subordinate_female' => 'cấp dưới', + 'relationship_type_subordinate_male' => 'subordinate', + 'relationship_type_subordinate_with_name' => 'cấp dưới của :name', + 'relationship_type_subordinate_female_with_name' => 'cấp dưới của :name', + 'relationship_type_subordinate_male_with_name' => ':name’s subordinate', + + 'relationship_type_mentor' => 'người hướng dẫn', + 'relationship_type_mentor_female' => 'người hướng dẫn', + 'relationship_type_mentor_male' => 'mentor', + 'relationship_type_mentor_with_name' => 'người hướng dẫn của :name', + 'relationship_type_mentor_female_with_name' => 'người hướng dẫn của :name', + 'relationship_type_mentor_male_with_name' => ':name’s mentor', + + 'relationship_type_protege' => 'protégé', + 'relationship_type_protege_female' => 'protégé', + 'relationship_type_protege_male' => 'protégé', + 'relationship_type_protege_with_name' => ':name’s protégé', + 'relationship_type_protege_female_with_name' => ':name’s protégé', + 'relationship_type_protege_male_with_name' => ':name’s protégé', + + 'relationship_type_ex_husband' => 'ex-spouse', + 'relationship_type_ex_husband_female' => 'vợ cũ', + 'relationship_type_ex_husband_male' => 'ex-husband', + 'relationship_type_ex_husband_with_name' => ':name’s ex-spouse', + 'relationship_type_ex_husband_female_with_name' => 'vợ cũ của :name', + 'relationship_type_ex_husband_male_with_name' => ':name’s ex-husband', + + // emotions + 'emotion_primary_love' => 'Yêu', + 'emotion_primary_joy' => 'Vui sướng', + 'emotion_primary_surprise' => 'Ngạc nhiên', + 'emotion_primary_anger' => 'Tức giận', + 'emotion_primary_sadness' => 'Buồn bã', + 'emotion_primary_fear' => 'Sợ hãi', + + 'emotion_secondary_affection' => 'Cảm kích', + 'emotion_secondary_lust' => 'Ham muốn', + 'emotion_secondary_longing' => 'Ao ước', + 'emotion_secondary_cheerfulness' => 'Vui vẻ', + 'emotion_secondary_zest' => 'Hăng hái', + 'emotion_secondary_contentment' => 'Hài lòng', + 'emotion_secondary_pride' => 'Tự hào', + 'emotion_secondary_optimism' => 'Lạc quan', + 'emotion_secondary_enthrallment' => 'Say mê', + 'emotion_secondary_relief' => 'Cứu trợ', + 'emotion_secondary_surprise' => 'Ngạc nhiên', + 'emotion_secondary_irritation' => 'Kích thích', + 'emotion_secondary_exasperation' => 'Bực tức', + 'emotion_secondary_rage' => 'Thịnh nộ', + 'emotion_secondary_disgust' => 'Chán ghét', + 'emotion_secondary_envy' => 'Đố kị', + 'emotion_secondary_suffering' => 'Đau khổ', + 'emotion_secondary_sadness' => 'Buồn bã', + 'emotion_secondary_disappointment' => 'Thất vọng', + 'emotion_secondary_shame' => 'Xấu hổ', + 'emotion_secondary_neglect' => 'Bỏ bê', + 'emotion_secondary_sympathy' => 'Cảm thông', + 'emotion_secondary_horror' => 'Ghê rợn', + 'emotion_secondary_nervousness' => 'Lo lắng', + + 'emotion_adoration' => 'Yêu mến', + 'emotion_affection' => 'Cảm kích', + 'emotion_love' => 'Yêu', + 'emotion_fondness' => 'Thương mến', + 'emotion_liking' => 'Có thiện cảm', + 'emotion_attraction' => 'Thu hút', + 'emotion_caring' => 'Chăm sóc', + 'emotion_tenderness' => 'Nhạy cảm', + 'emotion_compassion' => 'Thương xót', + 'emotion_sentimentality' => 'Đa cảm', + 'emotion_arousal' => 'Thức tỉnh', + 'emotion_desire' => 'Khao khát', + 'emotion_lust' => 'Ham muốn', + 'emotion_passion' => 'Đam mê', + 'emotion_infatuation' => 'Mê đắm', + 'emotion_longing' => 'Ao ước', + 'emotion_amusement' => 'Thích thú', + 'emotion_bliss' => 'Hạnh phúc', + 'emotion_cheerfulness' => 'Hân hoan', + 'emotion_gaiety' => 'Tươi vui', + 'emotion_glee' => 'Hân hoan', + 'emotion_jolliness' => 'Vui vẻ', + 'emotion_joviality' => 'Tâm hồn vui vẻ', + 'emotion_joy' => 'Vui sướng', + 'emotion_delight' => 'Delight', + 'emotion_enjoyment' => 'Enjoyment', + 'emotion_gladness' => 'Gladness', + 'emotion_happiness' => 'Happiness', + 'emotion_jubilation' => 'Hân hoan', + 'emotion_elation' => 'Phấn khởi', + 'emotion_satisfaction' => 'Satisfaction', + 'emotion_ecstasy' => 'Ecstasy', + 'emotion_euphoria' => 'Euphoria', + 'emotion_enthusiasm' => 'Enthusiasm', + 'emotion_zeal' => 'Zeal', + 'emotion_zest' => 'Zest', + 'emotion_excitement' => 'Hứng thú', + 'emotion_thrill' => 'Thrill', + 'emotion_exhilaration' => 'Exhilaration', + 'emotion_contentment' => 'Contentment', + 'emotion_pleasure' => 'Pleasure', + 'emotion_pride' => 'Pride', + 'emotion_eagerness' => 'Eagerness', + 'emotion_hope' => 'Hy vọng', + 'emotion_optimism' => 'Optimism', + 'emotion_enthrallment' => 'Enthrallment', + 'emotion_rapture' => 'Rapture', + 'emotion_relief' => 'Relief', + 'emotion_amazement' => 'Amazement', + 'emotion_surprise' => 'Surprise', + 'emotion_astonishment' => 'Astonishment', + 'emotion_aggravation' => 'Aggravation', + 'emotion_irritation' => 'Irritation', + 'emotion_agitation' => 'Agitation', + 'emotion_annoyance' => 'Annoyance', + 'emotion_grouchiness' => 'Grouchiness', + 'emotion_grumpiness' => 'Grumpiness', + 'emotion_exasperation' => 'Exasperation', + 'emotion_frustration' => 'Frustration', + 'emotion_anger' => 'Anger', + 'emotion_rage' => 'Rage', + 'emotion_outrage' => 'Outrage', + 'emotion_fury' => 'Fury', + 'emotion_wrath' => 'Wrath', + 'emotion_hostility' => 'Hostility', + 'emotion_ferocity' => 'Ferocity', + 'emotion_bitterness' => 'Bitterness', + 'emotion_hate' => 'Hate', + 'emotion_loathing' => 'Loathing', + 'emotion_scorn' => 'Scorn', + 'emotion_spite' => 'Spite', + 'emotion_vengefulness' => 'Vengefulness', + 'emotion_dislike' => 'Không thích', + 'emotion_resentment' => 'Resentment', + 'emotion_disgust' => 'Disgust', + 'emotion_revulsion' => 'Revulsion', + 'emotion_contempt' => 'Contempt', + 'emotion_envy' => 'Envy', + 'emotion_jealousy' => 'Jealousy', + 'emotion_agony' => 'Agony', + 'emotion_suffering' => 'Suffering', + 'emotion_hurt' => 'Tổn thương', + 'emotion_anguish' => 'Anguish', + 'emotion_depression' => 'Depression', + 'emotion_despair' => 'Despair', + 'emotion_hopelessness' => 'Hopelessness', + 'emotion_gloom' => 'Gloom', + 'emotion_glumness' => 'Glumness', + 'emotion_sadness' => 'Sadness', + 'emotion_unhappiness' => 'Unhappiness', + 'emotion_grief' => 'Grief', + 'emotion_sorrow' => 'Sorrow', + 'emotion_woe' => 'Woe', + 'emotion_misery' => 'Misery', + 'emotion_melancholy' => 'Melancholy', + 'emotion_dismay' => 'Dismay', + 'emotion_disappointment' => 'Disappointment', + 'emotion_displeasure' => 'Displeasure', + 'emotion_guilt' => 'Guilt', + 'emotion_shame' => 'Shame', + 'emotion_regret' => 'Hối tiếc', + 'emotion_remorse' => 'Remorse', + 'emotion_alienation' => 'Alienation', + 'emotion_isolation' => 'Isolation', + 'emotion_neglect' => 'Neglect', + 'emotion_loneliness' => 'Loneliness', + 'emotion_rejection' => 'Rejection', + 'emotion_homesickness' => 'Homesickness', + 'emotion_defeat' => 'Defeat', + 'emotion_dejection' => 'Dejection', + 'emotion_insecurity' => 'Không an toàn', + 'emotion_embarrassment' => 'Embarrassment', + 'emotion_humiliation' => 'Humiliation', + 'emotion_insult' => 'Insult', + 'emotion_pity' => 'Pity', + 'emotion_sympathy' => 'Sympathy', + 'emotion_alarm' => 'Báo động', + 'emotion_shock' => 'Shock', + 'emotion_fear' => 'Sợ hãi', + 'emotion_fright' => 'Fright', + 'emotion_horror' => 'Ghê rợn', + 'emotion_terror' => 'Khủng bố', + 'emotion_panic' => 'Hoảng sợ', + 'emotion_hysteria' => 'Hysteria', + 'emotion_mortification' => 'Mortification', + 'emotion_anxiety' => 'Anxiety', + 'emotion_nervousness' => 'Lo lắng', + 'emotion_tenseness' => 'Tenseness', + 'emotion_uneasiness' => 'Uneasiness', + 'emotion_apprehension' => 'Apprehension', + 'emotion_worry' => 'Lo lắng', + 'emotion_distress' => 'Distress', + 'emotion_dread' => 'Dread', + + // weather + 'weather_sunny' => 'Có nắng', + 'weather_clear' => 'Trời quang, ít mây', + 'weather_clear-day' => 'Ngày đẹp trời', + 'weather_clear-night' => 'Đêm quang đãng', + 'weather_light-drizzle' => 'Mưa phùn', + 'weather_patchy-light-drizzle' => 'Patchy light drizzle', + 'weather_patchy-light-rain' => 'Patchy light rain', + 'weather_light-rain' => 'Light rain', + 'weather_moderate-rain-at-times' => 'Moderate rain at times', + 'weather_moderate-rain' => 'Moderate rain', + 'weather_patchy-rain-possible' => 'Patchy rain possible', + 'weather_heavy-rain-at-times' => 'Heavy rain at times', + 'weather_heavy-rain' => 'Heavy rain', + 'weather_light-freezing-rain' => 'Light freezing rain', + 'weather_moderate-or-heavy-freezing-rain' => 'Moderate or heavy freezing rain', + 'weather_light-sleet' => 'Light sleet', + 'weather_moderate-or-heavy-rain-shower' => 'Moderate or heavy rain shower', + 'weather_light-rain-shower' => 'Light rain shower', + 'weather_torrential-rain-shower' => 'Torrential rain shower', + 'weather_rain' => 'Mưa', + 'weather_snow' => 'Tuyết', + 'weather_blowing-snow' => 'Blowing snow', + 'weather_patchy-light-snow' => 'Patchy light snow', + 'weather_light-snow' => 'Light snow', + 'weather_patchy-moderate-snow' => 'Patchy moderate snow', + 'weather_moderate-snow' => 'Moderate snow', + 'weather_patchy-heavy-snow' => 'Patchy heavy snow', + 'weather_heavy-snow' => 'Heavy snow', + 'weather_light-snow-showers' => 'Light snow showers', + 'weather_moderate-or-heavy-snow-showers' => 'Moderate or heavy snow showers', + 'weather_patchy-snow-possible' => 'Patchy snow possible', + 'weather_patchy-sleet-possible' => 'Patchy sleet possible', + 'weather_moderate-or-heavy-sleet' => 'Moderate or heavy sleet', + 'weather_light-sleet-showers' => 'Light sleet showers', + 'weather_moderate-or-heavy-sleet-showers' => 'Moderate or heavy sleet showers', + 'weather_sleet' => 'Mưa tuyết', + 'weather_wind' => 'Gió', + 'weather_fog' => 'Sương mù', + 'weather_freezing-fog' => 'Freezing fog', + 'weather_mist' => 'Mist', + 'weather_blizzard' => 'Blizzard', + 'weather_overcast' => 'Overcast', + 'weather_cloudy' => 'Nhiều mây', + 'weather_partly-cloudy-day' => 'Partly cloudy', + 'weather_partly-cloudy-night' => 'Partly cloudy', + 'weather_freezing-drizzle' => 'Freezing drizzle', + 'weather_heavy-freezing-drizzle' => 'Heavy freezing drizzle', + 'weather_patchy-freezing-drizzle-possible' => 'Patchy freezing drizzle possible', + 'weather_ice-pellets' => 'Ice pellets', + 'weather_light-showers-of-ice-pellets' => 'Light showers of ice pellets', + 'weather_moderate-or-heavy-showers-of-ice-pellets' => 'Moderate or heavy showers of ice pellets', + 'weather_thundery-outbreaks-possible' => 'Thundery outbreaks possible', + 'weather_patchy-light-rain-with-thunder' => 'Patchy light rain with thunder', + 'weather_moderate-or-heavy-rain-with-thunder' => 'Moderate or heavy rain with thunder', + 'weather_patchy-light-snow-with-thunder' => 'Patchy light snow with thunder', + 'weather_moderate-or-heavy-snow-with-thunder' => 'Moderate or heavy snow with thunder', + 'weather_current_temperature_celsius' => ':temperature °C', + 'weather_current_temperature_fahrenheit' => ':temperature °F', + 'weather_current_title' => 'Thời tiết hiện tại', + + // dav + 'dav_contacts' => 'Danh bạ', + 'dav_contacts_description' => 'danh bạ của :name', + 'dav_birthdays' => 'Sinh nhật', + 'dav_birthdays_description' => ':name’s contact’s birthdays', + 'dav_tasks' => 'Nhiệm vụ', + 'dav_tasks_description' => 'nhiệm vụ của :name', + + // contact list + 'contact_list_avatar' => 'Ảnh đại diện', + 'contact_list_name' => 'Liên hệ', + 'contact_list_description' => 'Mô tả', + +]; diff --git a/resources/lang/vi/auth.php b/resources/lang/vi/auth.php new file mode 100644 index 0000000..6beb6f7 --- /dev/null +++ b/resources/lang/vi/auth.php @@ -0,0 +1,89 @@ + 'Thông tin đăng nhập không đúng.', + 'throttle' => 'Đăng nhập thất bại nhiều lần. Vui lòng thử lại sau :seconds.', + 'not_authorized' => 'Bạn không có quyền thực hiện hành động này', + 'signup_disabled' => 'Việc đăng kí đang bị tạm dừng', + 'signup_error' => 'Có lỗi xảy ra khi đăng kí tài khoản', + 'back_homepage' => 'Quay lại trang chủ', + 'mfa_auth_otp' => 'Xác thực với xác thực hai yếu tố', + 'mfa_auth_webauthn' => 'Xác thực với khóa bảo mật (WebAuthn)', + '2fa_title' => 'Xác minh 2 bước', + '2fa_wrong_validation' => 'Xác thực hai bước thất bại.', + '2fa_one_time_password' => 'Mã xác thực hai bước', + '2fa_recuperation_code' => 'Nhập mã khôi phục hai bước', + '2fa_one_time_or_recuperation' => 'Nhập mã xác thực hoặc khôi phục hai bước', + '2fa_otp_help' => 'Mở ứng dụng mã xác thực hai bước và copy mã', + + 'login_to_account' => 'Đăng nhập vào tài khoản của bạn', + 'login_with_recovery' => 'Đăng nhập với mã khôi phục', + 'login_again' => 'Hãy đăng nhập lại vào tài khoản của bạn', + 'email' => 'Email', + 'password' => 'Mật khẩu', + 'recovery' => 'Mã khôi phục', + 'login' => 'Đăng nhập', + 'button_remember' => 'Ghi nhớ đăng nhập', + 'password_forget' => 'Quên mật khẩu?', + 'password_reset' => 'Đặt lại mật khẩu', + 'use_recovery' => 'Hoặc bạn có thể dùng mã khôi phục', + 'signup_no_account' => 'Chưa có tài khoản?', + 'signup' => 'Đăng ký', + 'create_account' => 'Tạo tài khoản đầu tiên bằng cách đăng ký', + 'change_language_title' => 'Đổi ngôn ngữ:', + 'change_language' => 'Đổi ngôn ngữ thành :lang', + + 'password_reset_title' => 'Đặt lại mật khẩu', + 'password_reset_email' => 'Địa chỉ E-Mail', + 'password_reset_send_link' => 'Gửi liên kết đặt lại mật khẩu', + 'password_reset_password' => 'Mật khẩu', + 'password_reset_password_confirm' => 'Xác nhận mật khẩu', + 'password_reset_action' => 'Đặt lại mật khẩu', + 'password_reset_email_content' => 'Nhấn vào đây để đặt lại mật khẩu của bạn:', + + 'register_title_welcome' => 'Chào mừng bạn đến với hệ thống Monica mới cài đặt', + 'register_create_account' => 'Bạn cần tạo tài khoản để sử dụng Monica', + 'register_title_create' => 'Tạo tài khoản Monica của bạn', + 'register_login' => 'Đăng nhập nếu bạn đã có tài khoản.', + 'register_email' => 'Nhập địa chỉ email hợp lệ', + 'register_email_example' => 'you@home', + 'register_firstname' => 'Tên', + 'register_firstname_example' => 'ví dụ: Dũng', + 'register_lastname' => 'Họ', + 'register_lastname_example' => 'eg. Nguyễn', + 'register_password' => 'Mật khẩu', + 'register_password_example' => 'Nhập mật khẩu bảo mật', + 'register_password_confirmation' => 'Xác nhận mật khẩu', + 'register_action' => 'Đăng kí', + 'register_policy' => 'Đăng kí có nghĩa là bạn đã đọc và đồng ý với Điều khoản bảo mậtThỏa thuận sử dụng.', + 'register_invitation_email' => 'Vì mục đích bảo mật, vui lòng cho biết email của người đã mời bạn tham gia tài khoản này. Thông tin này được cung cấp trong email mời.', + + 'confirmation_title' => 'Kiểm tra lại địa chỉ e-mail', + 'confirmation_fresh' => 'Một liên kết xác nhận mới đã được gửi vào địa chỉ email của bạn.', + 'confirmation_check' => 'Trước khi tiếp tục, hãy kiểm tra email của bạn cho liên kết xác thực.', + 'confirmation_request_another' => 'Nếu bạn không nhận được email bấm vào đây để yêu cầu một email khác.', + + 'confirmation_again' => 'Nếu bạn muốn đổi địa chỉ email, bạn có thể bấm vào đây.', + 'email_change_current_email' => 'Địa chỉ email hiện tại:', + 'email_change_title' => 'Đổi địa chỉ email', + 'email_change_new' => 'Địa chỉ email mới', + 'email_changed' => 'Địa chỉ email của bạn đã được thay đổi. Hãy kiểm tra hòm thư để xác nhận lại.', +]; diff --git a/resources/lang/vi/changelog.php b/resources/lang/vi/changelog.php new file mode 100644 index 0000000..4c4f46b --- /dev/null +++ b/resources/lang/vi/changelog.php @@ -0,0 +1,12 @@ + 'Cập nhật sản phẩm', + 'note' => 'Ghi chú: rất tiếc, trang này chỉ có tiếng Anh.', +]; diff --git a/resources/lang/vi/dashboard.php b/resources/lang/vi/dashboard.php new file mode 100644 index 0000000..5c87dd4 --- /dev/null +++ b/resources/lang/vi/dashboard.php @@ -0,0 +1,42 @@ + 'Chào mừng đến với tài khoản của bạn!', + 'dashboard_blank_description' => 'Monica là nơi sắp xếp tất cả tương tác của bạn với người mà bạn quan tâm.', + 'dashboard_blank_cta' => 'Thêm liên hệ đầu tiên', + 'dashboard_blank_illustration' => 'Minh họa bởi Freepik', + + 'notes_title' => 'Bạn chưa có ghi chú gắn dấu sao nào.', + + 'tab_recent_calls' => 'Cuộc gọi gần đây', + 'tab_favorite_notes' => 'Ghi chú ưa thích', + 'tab_calls_blank' => 'Bạn chưa ghi nhật ký cuộc gọi nào.', + 'tab_debts' => 'Khoản nợ', + 'tab_debts_blank' => 'Bạn chưa ghi khoản nợ nào.', + 'tab_tasks' => 'Nhiệm vụ', + 'tab_tasks_blank' => 'Bạn chưa có nhiệm vụ nào.', + + 'tasks_add_task_placeholder' => 'Nhiệm vụ này làm gì?', + 'tasks_tab_your_contacts' => 'Nhiệm vụ liên quan đến liên hệ của bạn', + 'tasks_tab_your_tasks' => 'Nhiệm vụ của bạn', + 'tasks_add_note' => 'Nhấn Enter để thêm nhiệm vụ.', + 'task_add_cta' => 'Thêm nhiệm vụ', + + 'debts_you_owe' => 'Bạn nợ', + + 'statistics_contacts' => 'Danh bạ', + 'statistics_activities' => 'Hoạt động', + 'statistics_gifts' => 'Quà tặng', + + 'reminders_next_months' => 'Sự kiện trong 3 tháng tới', + 'reminders_none' => 'Không có nhắc nhở nào trong tháng này.', + + 'product_changes' => 'Cập nhật sản phẩm', + 'product_view_details' => 'Xem chi tiết', +]; diff --git a/resources/lang/vi/format.php b/resources/lang/vi/format.php new file mode 100644 index 0000000..8ae530d --- /dev/null +++ b/resources/lang/vi/format.php @@ -0,0 +1,36 @@ + 'd/m/Y H:i', + 'short_date_year' => 'd/m/Y', + 'short_date' => 'd M', + 'short_month' => 'M', + 'short_month_year' => 'M Y', + 'short_day' => 'D', + 'full_date_year' => 'd/m/Y', + 'full_month' => 'F', + 'full_month_year' => 'F Y', + 'full_hour' => 'h.i A', + + 'short_text' => '{text}…', +]; diff --git a/resources/lang/vi/journal.php b/resources/lang/vi/journal.php new file mode 100644 index 0000000..9b1960f --- /dev/null +++ b/resources/lang/vi/journal.php @@ -0,0 +1,38 @@ + 'Hôm nay của bạn thế nào? Bạn có thể đánh giá mỗi ngày một lần.', + 'journal_come_back' => 'Cảm ơn. Quay lại vào ngày mai để đánh giá lại một ngày của bạn.', + 'journal_description' => 'Ghi chú: danh sách nhật ký liệt kê cả mục nhật ký thủ công và mục tự động như Hoạt động được thực hiện với liên hệ của bạn. Mặc dù bạn có thể xóa nhật ký thủ công, bạn sẽ phải xóa hoạt động trực tiếp ở trang liên hệ.', + 'journal_add' => 'Thêm mục nhật ký', + 'journal_edit' => 'Sửa mục nhật ký', + 'journal_empty' => 'Không có nhật ký nào', + 'journal_created_at' => 'Tạo ngày {date}', + 'journal_created_automatically' => 'Được tạo tự động', + 'journal_entry_type_journal' => 'Mục nhật ký', + 'journal_entry_type_activity' => 'Hoạt động', + 'journal_entry_rate' => 'Bạn đã đánh giá ngày của bạn.', + 'journal_add_comment' => 'Bạn muốn thêm nhận xét (không bắt buộc)?', + 'journal_show_comment' => 'Hiển thị bình luận', + 'entry_delete_success' => 'Xóa nhật ký thành công.', + 'journal_add_title' => 'Tiêu đề (không bắt buộc)', + 'journal_add_date' => 'Ngày', + 'journal_add_post' => 'Mục', + 'journal_add_cta' => 'Lưu', + 'journal_blank_cta' => 'Thêm mục nhật ký đầu tiên của bạn', + 'journal_blank_description' => 'Nhật ký cho phép bạn viết sự kiện đã xảy ra với bạn, và ghi nhớ chúng.', + 'delete_confirmation' => 'Bạn chắc chắn muốn xóa mục nhật ký này?', + 'apply_filter' => 'Apply filter', + 'start_date' => 'Start Date', + 'end_date' => 'End Date', + 'per_page' => 'Per Page', + 'sort_order' => 'Sort By Created At', + 'ascending' => 'Ascending', + 'descending' => 'Descending', +]; diff --git a/resources/lang/vi/logs.php b/resources/lang/vi/logs.php new file mode 100644 index 0000000..cc14b90 --- /dev/null +++ b/resources/lang/vi/logs.php @@ -0,0 +1,29 @@ + 'Đã tạo liên hệ.', + 'settings_log_contact_created_with_name' => 'Đã thêm :name thành liên hệ.', + + // contat description update + 'contact_log_contact_description_updated' => 'Đã cập nhật mô tả.', + 'settings_log_contact_description_updated_with_name' => 'Đã cập nhật mô tả của :name.', + + // contact description clear + 'contact_log_contact_description_cleared' => 'Đã xóa mô tả.', + 'settings_log_contact_description_cleared_with_name' => 'Đã xóa mô tả của :name.', + + // contact work information update + 'contact_log_contact_work_updated' => 'Đã cập nhật thông tin công việc.', + 'settings_log_contact_work_updated_with_name' => 'Đã cập nhật thông tin công việc của :name.', + + // company created + 'settings_log_company_created' => 'Đã tạo công ty tên :name.', +]; diff --git a/resources/lang/vi/mail.php b/resources/lang/vi/mail.php new file mode 100644 index 0000000..08e86c8 --- /dev/null +++ b/resources/lang/vi/mail.php @@ -0,0 +1,53 @@ + 'Lời nhắc cho :contact', + 'greetings' => 'Chào :username', + 'want_reminded_of' => 'Bạn muốn được nhắc về :reason', + 'for' => 'Cho: :name', + 'comment' => 'Bình luận: :comment', + 'footer_contact_info' => 'Thêm, xem, hoàn thiện, và thay đổi thông tin về liên hệ này:', + 'footer_contact_info2' => 'Xem hồ sơ của :name', + 'footer_contact_info2_link' => 'Xem hồ sơ của :name :url', + + 'notification_subject_line' => 'Bạn có sự kiện sắp diễn ra', + 'notification_description' => 'Trong :count ngày (on :date), các sự kiện dưới đây sẽ diễn ra:', + + 'stay_in_touch_subject_line' => 'Giữ liên lạc với :name', + 'stay_in_touch_subject_description' => 'Bạn đã yêu cầu được nhắc giữ liên lạc với :name mỗi :frequency ngày.', + + 'notifications_whoops' => 'Whoops!', + 'notifications_hello' => 'Xin chào!', + 'notifications_regards' => 'Thân', + 'notifications_footer' => 'Nếu bạn đang có vấn đề trong việc bấm nút ":actionText", sao chép và dán địa chỉ URL dưới đây vào trình duyệt web: [:actionURL](:actionURL)', + 'notifications_rights' => 'Bản quyền đã được bảo hộ', + + 'confirmation_email_title' => 'Monica – Email xác thực', + 'confirmation_email_intro'=> 'Để xác thực email của bạn, hãy bấm vào nút bên dưới', + 'confirmation_email_button' => 'Xác thực địa chỉ email', + 'confirmation_email_bottom' => 'Nếu bạn không tạo tài khoản, không cần có bất cứ hành động nào khác.', + + 'password_reset_title' => 'Monica - Thông báo đặt lại mật khẩu', + 'password_reset_intro' => 'Bạn nhận được email này bởi vì chúng tôi nhận được một yêu cầu đặt lại mật khẩu cho tài khoản của bạn.', + 'password_reset_button' => 'Đặt lại mật khẩu', + 'password_reset_expiration' => 'Liên kết đặt lại mật khẩu này sẽ hết hạn trong :count phút.', + 'password_reset_bottom' => 'Nếu bạn không yêu cầu đặt lại mật khẩu, không cần có bất cứ hành động nào khác.', + + 'invitation_title' => 'Monica - Bạn đã được mời bởi :name', + 'invitation_intro' => 'Bạn đã được mời sử dụng Monica bởi :name (:email), một công cụ Quản lý mối quan hệ cá nhân tuyệt vời.', + 'invitation_link' => 'Để chấp nhận lời mời, bấm vào liên kết bên dưới:', + 'invitation_button' => 'Chấp nhận lời mời', + 'invitation_expiration' => 'Liên kết này sẽ hết hạn trong :count days.', + + 'export_title' => 'Your export is ready', + 'export_description' => 'You requested a data export on :date. It is now ready to download.', + 'export_download' => 'Download export', + +]; diff --git a/resources/lang/vi/pagination.php b/resources/lang/vi/pagination.php new file mode 100644 index 0000000..a31e492 --- /dev/null +++ b/resources/lang/vi/pagination.php @@ -0,0 +1,25 @@ + '❮ Trước', + 'next' => 'Kế tiếp ❯', + +]; diff --git a/resources/lang/vi/passwords.php b/resources/lang/vi/passwords.php new file mode 100644 index 0000000..507fa0e --- /dev/null +++ b/resources/lang/vi/passwords.php @@ -0,0 +1,30 @@ + 'Mật khẩu của bạn đã được đặt lại!', + 'sent' => 'Nếu bạn nhập email đã có trong hệ thống, bạn sẽ nhận được 1 liên kết đặt lại mật khẩu.', + 'token' => 'Token đặt lại mật khẩu không hợp lệ.', + 'user' => 'Nếu bạn nhập email đã có trong hệ thống, bạn sẽ nhận được 1 liên kết đặt lại mật khẩu.', + 'changed' => 'Thay đổi mật khẩu thành công.', + 'invalid' => 'Mật khẩu hiện tại không đúng.', + 'throttled' => 'Vui lòng chờ trước khi thử lại.', + +]; diff --git a/resources/lang/vi/people.php b/resources/lang/vi/people.php new file mode 100644 index 0000000..c47a30c --- /dev/null +++ b/resources/lang/vi/people.php @@ -0,0 +1,539 @@ + 'Không tìm thấy liên hệ', + 'people_list_number_kids' => ':count con', + 'people_list_last_updated' => 'Danh sách liên hệ mới cập nhật:', + 'people_list_number_reminders' => ':count lời nhắc', + 'people_list_blank_title' => 'Bạn không có ai trong tài khoản', + 'people_list_blank_cta' => 'Thêm ai đó', + 'people_list_sort' => 'Sắp xếp', + 'people_list_stats' => ':count liên hệ', + 'people_list_firstnameAZ' => 'Xếp theo tên A → Z', + 'people_list_firstnameZA' => 'Xếp theo tên Z → A', + 'people_list_lastnameAZ' => 'Xếp theo họ A → Z', + 'people_list_lastnameZA' => 'Xếp theo họ Z → A', + 'people_list_lastactivitydateNewtoOld' => 'Xếp theo hoạt động gần đây nhất', + 'people_list_lastactivitydateOldtoNew' => 'Xếp theo hoạt động lâu nhất', + 'people_list_filter_tag' => 'Hiện tất cả liên hệ được tag với', + 'people_list_clear_filter' => 'Xoá bộ lọc', + 'people_list_contacts_per_tags' => ':count liên hệ|:count liên hệ', + 'people_list_show_dead' => 'Hiện người đã qua đời (:count)', + 'people_list_hide_dead' => 'Ẩn người đã qua đời (:count)', + 'people_search' => 'Tìm kiếm liên hệ…', + 'people_search_no_results' => 'Không tìm thấy kết quả', + 'people_search_next' => 'Kế tiếp', + 'people_search_prev' => 'Trước', + 'people_search_rows_per_page' => 'Số dòng trên mỗi trang', + 'people_search_of' => 'của', + 'people_search_page' => 'Trang', + 'people_search_all' => 'Tất cả', + 'people_add_new' => 'Thêm người mới', + 'people_list_account_usage' => 'Tài khoản của bạn sử dụng: :current/:limit liên hệ', + 'people_list_account_upgrade_title' => 'Nâng cấp tài khoản của bạn để mở khoá tất cả tính năng.', + 'people_list_account_upgrade_cta' => 'Nâng cấp ngay', + 'people_list_untagged' => 'Xem liên hệ không được gắn thẻ', + 'people_list_filter_untag' => 'Hiện tất cả liên hệ không được gắn thẻ', + 'archived_contact_readonly' => 'Liên hệ đã lưu trữ không thể sửa, hãy bỏ lưu trữ trước.', + + // people add + 'people_add_title' => 'Thêm người mới', + 'people_add_missing' => 'Không có người nào - hãy thêm một người mới ngay bây giờ', + 'people_add_firstname' => 'Tên', + 'people_add_middlename' => 'Tên đệm (tùy chọn)', + 'people_add_lastname' => 'Họ (Tuỳ chọn)', + 'people_add_email' => 'Email (Tùy chọn)', + 'people_add_nickname' => 'Biệt danh (Tuỳ chọn)', + 'people_add_cta' => 'Thêm', + 'people_save_and_add_another_cta' => 'Gửi và thêm một người khác', + 'people_add_success' => ':name đã được tạo thành công', + 'people_add_gender' => 'Giới tính', + 'people_delete_success' => 'Liên hệ đã bị xoá', + 'people_delete_message' => 'Xóa liên hệ', + 'people_delete_confirmation' => 'Bạn chắc chắn muốn xóa liên hệ :name? Việc xóa sẽ không thể khôi phục.', + 'people_add_birthday_reminder' => 'Chúc mừng sinh nhật tới :name', + 'people_add_birthday_reminder_deceased' => ':name sẽ tổ chức sinh nhật vào ngày này', + 'people_add_import' => 'Bạn có muốn nhập danh bạ của bạn?', + 'people_edit_email_error' => 'Đã có liên hệ trong tài khoản của bạn sử dụng địa chỉ email này. Hãy chọn một cái khác.', + 'people_export' => 'Xuất dưới dạng vCard', + 'people_add_reminder_for_birthday' => 'Tạo lời nhắc sinh nhật hàng năm', + + // show + 'section_contact_information' => 'Thông tin liên hệ', + 'section_personal_activities' => 'Hoạt động', + 'section_personal_reminders' => 'Lời nhắc', + 'section_personal_tasks' => 'Nhiệm vụ', + 'section_personal_gifts' => 'Quà tặng', + 'section_personal_notes' => 'Ghi chú', + + // archived contacts + 'list_link_to_active_contacts' => 'Bạn đang xem liên hệ đã lưu trữ. Hãy xem danh sách liên hệ đang hoạt động.', + 'list_link_to_archived_contacts' => 'Danh sách các liên hệ đã lưu trữ', + + // Header + 'me' => 'Đây là bạn', + 'edit_contact_information' => 'Sửa thông tin liên hệ', + 'contact_archive' => 'Lưu trữ liên hệ', + 'contact_unarchive' => 'Bỏ lưu trữ liên hệ', + 'contact_archive_help' => 'Liên hệ đã lưu trữ sẽ không hiện ở danh sách liên hệ, nhưng vẫn hiển thị ở kết quả tìm kiếm.', + 'call_button' => 'Ghi nhật ký cuộc gọi', + 'set_favorite' => 'Liên hệ ưa thích sẽ được đặt ở đầu danh sách', + + // Stay in touch + 'stay_in_touch' => 'Giữ liên lạc', + 'stay_in_touch_frequency' => 'Giữ liên lạc mỗi ngày|Giữ liên lạc mỗi {count} ngày', + 'stay_in_touch_next_date' => 'Next due: {date}', + 'stay_in_touch_invalid' => 'Tần suất phải là số lớn hơn 0.', + 'stay_in_touch_premium' => 'Bạn cần nâng cấp tài khoản để sử dụng tính năng này', + 'stay_in_touch_modal_title' => 'Giữ liên lạc', + 'stay_in_touch_modal_desc' => 'Chúng tôi có thể nhắc nhở bạn bằng email để giữ liên lạc với {firstname} đều đặn.', + 'stay_in_touch_modal_label' => 'Gửi email cho tôi mỗi... {count} ngày', + + // Calls + 'modal_call_title' => 'Ghi nhật ký cuộc gọi', + 'modal_call_comment' => 'Bạn đã nói về chuyện gì? (tùy chọn)', + 'modal_call_exact_date' => 'Cuộc gọi xảy ra vào', + 'modal_call_who_called' => 'Ai đã gọi?', + 'modal_call_emotion' => 'Bạn có muốn ghi lại cảm giác của bạn trong cuộc gọi này không? (tùy chọn)', + 'calls_add_success' => 'Đã lưu cuộc gọi.', + 'call_delete_confirmation' => 'Bạn chắc chắn muốn xóa cuộc gọi này?', + 'call_delete_success' => 'Đã xóa cuộc gọi thành công', + 'call_title' => 'Cuộc gọi điện thoại', + 'call_empty_comment' => 'Không có chi tiết', + 'call_blank_title' => 'Giữ theo dõi các cuộc gọi bạn đã thực hiện với {name}', + 'call_blank_desc' => 'Bạn đã gọi {name}', + 'call_you_called' => 'Bạn đã gọi', + 'call_he_called' => '{name} đã gọi', + 'call_emotions' => 'Cảm xúc:', + + // Conversation + 'conversation_blank' => 'Ghi lại cuộc hội thoại giữa bạn và :name trên mạng xã hội, SMS…', + 'conversation_delete_link' => 'Xóa cuộc hội thoại', + 'conversation_edit_title' => 'Sửa cuộc hội thoại', + 'conversation_edit_delete' => 'Bạn chắc chắn muốn xóa vĩnh viễn cuộc hội thoại này?', + 'conversation_add_success' => 'Thêm hội thoại thành công.', + 'conversation_edit_success' => 'Cập nhật hội thoại thành công.', + 'conversation_delete_success' => 'Xóa hội thoại thành công.', + 'conversation_add_title' => 'Ghi cuộc hội thoại mới', + 'conversation_add_when' => 'Bạn có cuộc hội thoại này khi nào?', + 'conversation_add_who_wrote' => 'Ai đã gửi tin nhắn này?', + 'conversation_add_how' => 'Bạn giao tiếp như thế nào?', + 'conversation_add_you' => 'bạn', + 'conversation_add_content' => 'Ghi xuống những gì đã được nói', + 'conversation_add_what_was_said' => 'Bạn đã nói gì?', + 'conversation_add_another' => 'Thêm tin nhắn khác', + 'conversation_add_error' => 'Phải có ít nhất một tin nhắn.', + 'conversation_list_table_messages' => 'Tin nhắn', + 'conversation_list_table_content' => 'Một phần nội dung (tin nhắn mới nhất)', + 'conversation_list_title' => 'Cuộc trò chuyện', + 'conversation_list_cta' => 'Nhật ký trò chuyện', + + // age - birthday + 'birthdate_not_set' => 'Ngày sinh chưa được cài đặt', + 'age_approximate_in_years' => 'khoảng :age tuổi', + 'age_exact_in_years' => ':age tuổi', + 'age_exact_birthdate' => 'sinh ngày :date', + + // Last called + 'last_called' => 'Lần gọi gần đây nhất: :date', + 'last_talked_to' => 'Lần gọi gần đây nhất: {date}', + 'last_called_empty' => 'Lần gọi gần đây nhất: không rõ', + 'last_activity_date' => 'Lần hoạt động cùng nhau gần đây nhất: :date', + 'last_activity_date_empty' => 'Lần hoạt động cùng nhau gần đây nhất: không rõ', + + // additional information + 'information_edit_success' => 'Hồ sơ đã được cập nhật thành công', + 'information_edit_title' => 'Sửa thông tin cá nhân của :name', + 'information_edit_max_size' => 'Tối đa :size Kb.', + 'information_edit_max_size2' => 'Tối đa {size} Kb.', + 'information_edit_firstname' => 'Tên', + 'information_edit_lastname' => 'Họ (Tùy chọn)', + 'information_edit_description' => 'Mô tả (tùy chọn)', + 'information_edit_description_help' => 'Sử dụng trong danh sách liên hệ để thêm vài ngữ cảnh nếu cần thiết.', + 'information_edit_unknown' => 'Tôi không biết tuổi người này', + 'information_edit_probably' => 'Người này có lẽ là…', + 'information_edit_not_year' => 'Tôi biết ngày và tháng sinh của người này, nhưng không biết năm…', + 'information_edit_exact' => 'Tôi biết chính xác sinh nhật của người này…', + 'information_edit_birthdate_label' => 'Sinh nhật', + 'information_no_work_defined' => 'Không có thông tin công việc', + 'information_work_at' => 'ở :company', + 'work_add_cta' => 'Cập nhật thông tin công việc', + 'work_edit_success' => 'Đã cập nhật thông tin công việc', + 'work_edit_title' => 'Cập nhật thông tin công việc của :name', + 'work_edit_job' => 'Tên công việc (tuỳ chọn)', + 'work_edit_company' => 'Tên công ty (nếu có)', + 'work_information' => 'Chi tiết công việc', + + // food preferences + 'food_preferences_add_success' => 'Sở thích ăn uống đã được lưu', + 'food_preferences_edit_description' => 'Có thể :firstname hoặc ai đó trong gia đình :family bị dị ứng. Hoặc không thích một chai rượu cụ thể. Đánh dấu ở đây để bạn có thể nhớ trong lần tới bạn mời họ ăn tối', + 'food_preferences_edit_description_no_last_name' => 'Có thể :firstname bị dị ứng. Hoặc không thích một chai rượu cụ thể. Đánh dấu ở đây để bạn có thể nhớ trong lần tới bạn mời họ ăn tối', + 'food_preferences_edit_title' => 'Trình bày sở thích ăn uống', + 'food_preferences_edit_cta' => 'Lưu sở thích ăn uống', + 'food_preferences_title' => 'Sở thích ăn uống', + 'food_preferences_cta' => 'Thêm sở thích ăn uống', + + // reminders + 'reminders_blank_title' => 'Có điều gì bạn muốn được nhắc về :name?', + 'reminders_blank_add_activity' => 'Thêm lời nhắc', + 'reminders_add_title' => 'Bạn muốn được nhắc điều gì về :name?', + 'reminders_add_description' => 'Hãy nhắc tôi…', + 'reminders_add_next_time' => 'Lần tới là khi nào bạn muốn được nhắc về điều này?', + 'reminders_add_once' => 'Nhắc tôi về cái này một lần', + 'reminders_add_recurrent' => 'Nhắc tôi về cái này mỗi', + 'reminders_add_starting_from' => 'bắt đầu từ ngày chọn phía trên', + 'reminders_add_cta' => 'Thêm lời nhắc', + 'reminders_edit_update_cta' => 'Cập nhật lời nhắc', + 'reminders_add_error_custom_text' => 'Bạn cần chỉ ra một lời nhắn cho lời nhắc này', + 'reminders_create_success' => 'Thêm nhắc nhở thành công', + 'reminders_delete_success' => 'Xóa nhắc nhở thành công', + 'reminders_update_success' => 'Cập nhật nhắc nhở thành công', + 'reminders_add_optional_comment' => 'Bình luận không bắt buộc', + + 'reminder_frequency_day' => 'mỗi :number ngày', + 'reminder_frequency_week' => 'mỗi :number tuần', + 'reminder_frequency_month' => 'mỗi :number tháng', + 'reminder_frequency_year' => 'mỗi :number năm', + 'reminder_frequency_one_time' => 'vào ngày :date', + 'reminders_delete_confirmation' => 'Bạn chắc chắn muốn hủy lời nhắc này?', + 'reminders_delete_cta' => 'Xóa', + 'reminders_next_expected_date' => 'vào', + 'reminders_cta' => 'Thêm lời nhắc', + 'reminders_description' => 'Chúng tôi sẽ gửi email cho mỗi lời nhắc bên dưới. Nhắc nhở được gửi mỗi buổi sáng của ngày sự kiện sẽ xảy ra. Lời nhắc sinh nhật được tự động thêm vào sẽ không thể xóa. Nếu bạn muốn đổi ngày, hãy sửa sinh nhật trong liên hệ.', + 'reminders_one_time' => 'Một lần', + 'reminders_type_week' => 'tuần', + 'reminders_type_month' => 'tháng', + 'reminders_type_year' => 'năm', + 'reminders_birthday' => 'Sinh nhật của :name', + 'reminders_free_plan_warning' => 'Bạn đang ở gói miễn phí. Không email nào được gửi ở gói này. Để nhận lời nhắc của bạn qua email, hãy nâng cấp tài khoản.', + + // relationships + 'relationship_form_add' => 'Thêm mối quan hệ mới', + 'relationship_form_edit' => 'Sửa mối quan hệ hiện tại', + 'relationship_form_is_with' => 'Người này là…', + 'relationship_form_is_with_name' => ':name là…', + 'relationship_form_add_choice' => 'Ai là người có mối quan hệ với?', + 'relationship_form_create_contact' => 'Thêm người mới', + 'relationship_form_associate_contact' => 'Thêm vào một liên hệ có sẵn', + 'relationship_form_associate_dropdown' => 'Tìm và chọn liên hệ ở dropdown dưới đây', + 'relationship_form_associate_dropdown_placeholder' => 'Tìm và chọn liên hệ đã có', + 'relationship_form_also_create_contact' => 'Tạo mục liên hệ cho người này.', + 'relationship_form_add_description' => 'Điều này sẽ cho phép bạn đối xử người này giống với các liên hệ khác.', + 'relationship_form_add_no_existing_contact' => 'Bạn không có liên hệ nào có liên quan đến :name lúc này.', + 'relationship_delete_confirmation' => 'Bạn chắc chắn muốn xóa mối quan hệ này? Việc xóa sẽ không thể khôi phục.', + 'relationship_unlink_confirmation' => 'Bạn chắc chắn muốn xóa mối quan hệ này? Người này sẽ không bị xóa - chỉ có mối quan hệ giữa hai người bị xóa.', + 'relationship_form_add_success' => 'Thiết lập mối quan hệ thành công.', + 'relationship_form_deletion_success' => 'Đã xóa mối quan hệ.', + + // tasks + 'tasks_title' => 'Nhiệm vụ', + 'tasks_blank_title' => 'Bạn chưa có nhiệm vụ nào.', + 'tasks_form_title' => 'Tiêu đề', + 'tasks_form_description' => 'Mô tả (tùy chọn)', + 'tasks_add_task' => 'Thêm nhiệm vụ', + 'tasks_delete_success' => 'Đã xóa nhiệm vụ thành công', + 'tasks_complete_success' => 'Đã thay đổi trạng thái nhiệm vụ thành công', + + // activities + 'activity_title' => 'Hoạt động', + 'activity_type_category_simple_activities' => 'Hoạt động đơn giản', + 'activity_type_category_sport' => 'Thể thao', + 'activity_type_category_food' => 'Ẩm thực', + 'activity_type_category_cultural_activities' => 'Hoạt động văn hoá', + 'activity_type_just_hung_out' => 'vừa đi chơi', + 'activity_type_watched_movie_at_home' => 'xem phim ở nhà', + 'activity_type_talked_at_home' => 'nói chuyện ở nhà', + 'activity_type_did_sport_activities_together' => 'chơi thể thao cùng nhau', + 'activity_type_ate_at_his_place' => 'ăn ở chỗ của họ', + 'activity_type_went_bar' => 'đến bar', + 'activity_type_ate_at_home' => 'ăn ở nhà', + 'activity_type_picnicked' => 'picnic', + 'activity_type_ate_restaurant' => 'ăn ở nhà hàng', + 'activity_type_went_theater' => 'đến rạp chiếu phim', + 'activity_type_went_concert' => 'đến concert', + 'activity_type_went_play' => 'đi chơi', + 'activity_type_went_museum' => 'đến viện bảo tàng', + 'activities_add_activity' => 'Thêm hoạt động', + 'activities_add_more_details' => 'Thêm thông tin chi tiết hơn', + 'activities_add_emotions' => 'Thêm cảm xúc', + 'activities_add_category' => 'Cho biết một danh mục', + 'activities_add_participants_cta' => 'Thêm người tham gia', + 'activities_item_information' => ':Activity. Đã xảy ra vào :date', + 'activities_add_title' => 'Bạn đã làm gì với {name}?', + 'activities_summary' => 'Mô tả những gì bạn đã làm', + 'activities_add_pick_activity' => 'Bạn có muốn phân loại hoạt động này không? Bạn không nhất thiết phải làm, nhưng nó sẽ giúp thống kê sau này (không bắt buộc)', + 'activities_add_date_occured' => 'Hoạt động này xảy ra vào…', + 'activities_add_participants' => 'Ai đã tham gia hoạt động này ngoài {name}? (không bắt buộc)', + 'activities_add_emotions_title' => 'Bạn có muốn ghi lại cảm giác của mình trong hoạt động này không? (không bắt buộc)', + 'activities_blank_title' => 'Theo dõi những gì bạn đã làm với {name} trong quá khứ và những gì bạn đã nói', + 'activities_blank_add_activity' => 'Thêm hoạt động', + 'activities_add_success' => 'Thêm hoạt động thành công', + 'activities_add_error' => 'Có lỗi xảy ra khi thêm hoạt động', + 'activities_update_success' => 'Cập nhật hoạt động thành công', + 'activities_delete_success' => 'Xoá hoạt động thành công', + 'activities_who_was_involved' => 'Ai đã tham gia?', + 'activities_activity' => 'Danh mục hoạt động', + 'activities_view_activities_report' => 'Xem báo cáo hoạt động', + 'activities_profile_title' => 'Báo cáo hoạt động giữa :name và bạn', + 'activities_profile_subtitle' => 'Bạn đã ghi lại tất cả :total_activities hoạt động với :name và :activities_last_twelve_months hoạt động trong 12 tháng qua.', + 'activities_profile_year_summary_activity_types' => 'Dưới đây là bảng phân tích các loại hoạt động các bạn đã làm cùng nhau trong năm :year', + 'activities_profile_year_summary' => 'Dưới đây là những gì hai bạn đã làm trong năm :year', + 'activities_profile_number_occurences' => ':value hoạt động', + 'activities_list_participants' => 'Participants ({total}):', + 'activities_list_emotions' => 'Cảm thấy:', + 'activities_list_date' => 'Xảy ra vào', + 'activities_list_category' => 'Thể loại:', + + // notes + 'notes_create_success' => 'Tạo ghi chú thành công', + 'notes_update_success' => 'Lưu ghi chú thành công', + 'notes_delete_success' => 'Xoá ghi chú thành công', + 'notes_add_cta' => 'Thêm ghi chú', + 'notes_favorite' => 'Thêm/xoá khỏi mục yêu thích', + 'notes_delete_title' => 'Xoá ghi chú', + 'notes_delete_confirmation' => 'Bạn chắc chắn muốn xoá ghi chú này vĩnh viễn?', + + // gifts + 'gifts_title' => 'Quà tặng', + 'gifts_add_success' => 'Thêm quà thành công', + 'gifts_delete_success' => 'Xoá quà thành công', + 'gifts_delete_confirmation' => 'Bạn có chắc muốn xoá quà này?', + 'gifts_add_gift' => 'Thêm quà', + 'gifts_link' => 'Liên kết', + 'gifts_for' => 'Cho: {name}', + 'gifts_delete_cta' => 'Xóa', + 'gifts_add_title' => 'Quản lý quà cho :name', + 'gifts_add_gift_idea' => 'Ý tưởng quà tặng', + 'gifts_add_gift_already_offered' => 'Quà đã tặng', + 'gifts_add_gift_received' => 'Quà đã nhận', + 'gifts_add_gift_title' => 'Đây là quà gì?', + 'gifts_add_gift_name' => 'Tên quà', + 'gifts_add_link' => 'Đường dẫn đến trang web (tuỳ chọn)', + 'gifts_add_value' => 'Giá trị (tuỳ chọn)', + 'gifts_add_comment' => 'Nhận xét (tuỳ chọn)', + 'gifts_add_recipient' => 'Người nhận (tuỳ chọn)', + 'gifts_add_recipient_field' => 'Người nhận', + 'gifts_add_photo' => 'Ảnh (tuỳ chọn)', + 'gifts_add_photo_title' => 'Thêm ảnh cho món quà này', + 'gifts_add_someone' => 'Món quà này đặc biệt dành cho một người nào đó trong gia đình của {name}', + 'gifts_delete_title' => 'Xoá quà tặng', + 'gifts_ideas' => 'Ý tưởng quà tặng', + 'gifts_offered' => 'Quà đã tặng', + 'gifts_offered_as_an_idea' => 'Đánh dấu là một ý tưởng', + 'gifts_received' => 'Quà đã nhận', + 'gifts_view_comment' => 'Xem bình luận', + 'gifts_mark_offered' => 'Đánh dấu là đã tặng', + 'gifts_update_success' => 'Cập nhật quà tặng thành công', + 'gifts_add_date' => 'Ngày (không bắt buộc)', + + // debts + 'debt_delete_confirmation' => 'Bạn chắc chắn muốn xóa khoản nợ này?', + 'debt_delete_success' => 'Đã xóa khoản nợ thành công', + 'debt_add_success' => 'Đã thêm khoản nợ thành công', + 'debt_title' => 'Khoản nợ', + 'debt_add_cta' => 'Thêm khoản nợ', + 'debt_you_owe' => 'Bạn nợ :amount', + 'debt_they_owe' => ':name nợ bạn :amount', + 'debt_add_title' => 'Quản lý khoản nợ', + 'debt_add_you_owe' => 'Bạn nợ :name', + 'debt_add_they_owe' => ':name nợ bạn', + 'debt_add_amount' => 'lấy tổng của', + 'debt_add_reason' => 'vì lý do sau (không bắt buộc)', + 'debt_add_add_cta' => 'Thêm khoản nợ', + 'debt_edit_update_cta' => 'Cập nhật khoản nợ', + 'debt_edit_success' => 'Cập nhật khoản nợ thành công', + 'debts_blank_title' => 'Quản lý các khoản bạn nợ :name hoặc :name nợ bạn', + + // tags + 'tag_edit' => 'Sửa thẻ tag', + 'tag_add' => 'Thêm thẻ tag', + 'tag_add_search' => 'Thêm hoặc tìm thẻ tag', + 'tag_no_tags' => 'Không có thẻ tag nào', + + // Introductions + 'introductions_sidebar_title' => 'Bạn đã gặp nhau như thế nào', + 'introductions_blank_cta' => 'Trình bày bạn đã gặp :name như thế nào', + 'introductions_title_edit' => 'Bạn đã gặp :name thế nào?', + 'introductions_additional_info' => 'Diễn giải làm thế nào bạn gặp và ở đâu', + 'introductions_edit_met_through' => 'Có ai đó giới thiệu bạn cho người này?', + 'introductions_no_met_through' => 'Không có ai', + 'introductions_first_met_date' => 'Ngày bạn gặp', + 'introductions_no_first_met_date' => 'Tôi không biết ngày chúng tôi gặp', + 'introductions_first_met_date_known' => 'Đây là ngày chúng tôi gặp', + 'introductions_add_reminder' => 'Thêm lời nhắc để kỷ niệm cuộc gặp gỡ này vào ngày kỷ niệm sự kiện này đã xảy ra', + 'introductions_update_success' => 'Cập nhật thông tin làm thế nào gặp người này thành công', + 'introductions_met_through' => 'Đã gặp qua :name', + 'introductions_met_date' => 'Gặp vào :date', + 'introductions_reminder_title' => 'Kỷ niệm ngày đầu tiên gặp nhau', + + // Deceased + 'deceased_reminder_title' => 'Kỷ niệm ngày mất của :name', + 'deceased_mark_person_deceased' => 'Đánh dấu người này đã qua đời', + 'deceased_know_date' => 'Tôi biết ngày người này mất', + 'deceased_add_reminder' => 'Thêm nhắc nhở cho ngày này', + 'deceased_label' => 'Đã qua đời', + 'deceased_date_label' => 'Ngày mất', + 'deceased_label_with_date' => 'Đã mất vào :date', + 'deceased_age' => 'Tuổi thọ', + + // Contact information + 'contact_info_title' => 'Thông tin liên hệ', + 'contact_info_form_content' => 'Nội dung', + 'contact_info_form_contact_type' => 'Loại liên hệ', + 'contact_info_form_personalize' => 'Cá nhân hoá', + 'contact_info_address' => 'Sống tại', + + // Addresses + 'contact_address_title' => 'Địa chỉ', + 'contact_address_form_name' => 'Nhãn (không bắt buộc)', + 'contact_address_form_street' => 'Tên đường (không bắt buộc)', + 'contact_address_form_city' => 'Thành phố (không bắt buộc)', + 'contact_address_form_province' => 'Tỉnh (không bắt buộc)', + 'contact_address_form_postal_code' => 'Mã bưu điện (không bắt buộc)', + 'contact_address_form_country' => 'Quốc gia (không bắt buộc)', + 'contact_address_form_latitude' => 'Vĩ độ (chỉ số) (không bắt buộc)', + 'contact_address_form_longitude' => 'Kinh độ (chỉ số) (không bắt buộc)', + + // Pets + 'pets_kind' => 'Loại thú cưng', + 'pets_name' => 'Tên (tùy chọn)', + 'pets_create_success' => 'Thêm thú cưng thành công', + 'pets_update_success' => 'Cập nhật thú cưng thành công', + 'pets_delete_success' => 'Đã xóa thú cưng', + 'pets_title' => 'Thú cưng', + 'pets_reptile' => 'Bò sát', + 'pets_bird' => 'Chim', + 'pets_cat' => 'Mèo', + 'pets_dog' => 'Chó', + 'pets_fish' => 'Cá', + 'pets_hamster' => 'Chuột hamster', + 'pets_horse' => 'Ngựa', + 'pets_rabbit' => 'Thỏ', + 'pets_rat' => 'Chuột', + 'pets_small_animal' => 'Động vật nhỏ', + 'pets_other' => 'Khác', + + // life events + 'life_event_list_tab_life_events' => 'Sự kiện trong đời', + 'life_event_list_tab_other' => 'Ghi chú, nhắc nhở, …', + 'life_event_list_title' => 'Sự kiện trong đời', + 'life_event_blank' => 'Ghi lại những gì xảy ra trong đời của {name} để tham chiếu trong tương lai.', + 'life_event_list_cta' => 'Thêm sự kiện trong đời', + 'life_event_create_category' => 'Tất cả thể loại', + 'life_event_create_life_event' => 'Thêm sự kiện trong đời', + 'life_event_create_default_title' => 'Tiêu đề (tuỳ chọn)', + 'life_event_create_default_story' => 'Câu chuyện (tuỳ chọn)', + 'life_event_create_date' => 'Bạn không cần chỉ rõ ngày hoặc tháng - chỉ bắt buộc năm.', + 'life_event_create_default_description' => 'Thêm thông tin về những gì bạn biết', + 'life_event_create_add_yearly_reminder' => 'Thêm nhắc nhở hàng năm cho sự kiện này', + 'life_event_create_success' => 'Đã thêm sự kiện trong đời', + 'life_event_delete_title' => 'Xoá sự kiện trong đời', + 'life_event_delete_description' => 'Bạn chắc chắn muốn xoá vĩnh viễn sự kiện này?', + 'life_event_delete_success' => 'Đã xoá sự kiện', + 'life_event_date_it_happened' => 'Ngày xảy ra', + 'life_event_category_work_education' => 'Công việc & học vấn', + 'life_event_category_family_relationships' => 'Gia đình & các mối quan hệ', + 'life_event_category_home_living' => 'Nhà cửa & đời sống', + 'life_event_category_health_wellness' => 'Sức khỏe thể chất & tinh thần', + 'life_event_category_travel_experiences' => 'Du lịch và trải nghiệm', + 'life_event_sentence_new_job' => 'Đã bắt đầu công việc mới', + 'life_event_sentence_retirement' => 'Nghỉ hưu', + 'life_event_sentence_new_school' => 'Đã bắt đầu đi học', + 'life_event_sentence_study_abroad' => 'Du học', + 'life_event_sentence_volunteer_work' => 'Bắt đầu công việc tình nguyện', + 'life_event_sentence_published_book_or_paper' => 'Xuất bản một nghiên cứu', + 'life_event_sentence_military_service' => 'Đã bắt đầu nghĩa vụ quân sự', + 'life_event_sentence_new_relationship' => 'Đã bắt đầu mối quan hệ', + 'life_event_sentence_engagement' => 'Đã đính hôn', + 'life_event_sentence_marriage' => 'Đã kết hơn', + 'life_event_sentence_anniversary' => 'Kỉ niệm', + 'life_event_sentence_expecting_a_baby' => 'Muốn có con', + 'life_event_sentence_new_child' => 'Đã có con', + 'life_event_sentence_new_family_member' => 'Đã thêm thành viên gia đình', + 'life_event_sentence_new_pet' => 'Đã có thú cưng', + 'life_event_sentence_end_of_relationship' => 'Đã kết thúc mối quan hệ', + 'life_event_sentence_loss_of_a_loved_one' => 'Đã mất một người thân yêu', + 'life_event_sentence_moved' => 'Đã chuyển đi', + 'life_event_sentence_bought_a_home' => 'Đã mua nhà', + 'life_event_sentence_home_improvement' => 'Cải tạo nhà', + 'life_event_sentence_holidays' => 'Đã đi vào ngày lễ', + 'life_event_sentence_new_vehicle' => 'Đã có xe mới', + 'life_event_sentence_new_roommate' => 'Đã có bạn cùng phòng', + 'life_event_sentence_overcame_an_illness' => 'Đã vượt qua bệnh tật', + 'life_event_sentence_quit_a_habit' => 'Bỏ một thói quen', + 'life_event_sentence_new_eating_habits' => 'Đã bắt đầu một thói quen ăn uống mới', + 'life_event_sentence_weight_loss' => 'Giảm cân', + 'life_event_sentence_wear_glass_or_contact' => 'Bắt đầu đeo kinh hoặc kính áp tròng', + 'life_event_sentence_broken_bone' => 'Gãy xương', + 'life_event_sentence_removed_braces' => 'Đã bỏ niềng', + 'life_event_sentence_surgery' => 'Đã phẫu thuật', + 'life_event_sentence_dentist' => 'Đến nha sĩ', + 'life_event_sentence_new_sport' => 'Bắt đầu chơi thể thao', + 'life_event_sentence_new_hobby' => 'Bắt đầu một sở thích', + 'life_event_sentence_new_instrument' => 'Học một nhạc cụ mới', + 'life_event_sentence_new_language' => 'Học một ngôn ngữ mới', + 'life_event_sentence_tattoo_or_piercing' => 'Có một hình xăm hoặc xỏ lỗ', + 'life_event_sentence_new_license' => 'Có bằng lái', + 'life_event_sentence_travel' => 'Du lịch', + 'life_event_sentence_achievement_or_award' => 'Có giải thưởng hoặc thành tựu', + 'life_event_sentence_changed_beliefs' => 'Thay đổi niềm tin', + 'life_event_sentence_first_word' => 'Nói lần đầu tiên', + 'life_event_sentence_first_kiss' => 'Hôn lần đầu tiên', + + // documents + 'document_list_title' => 'Tài liệu', + 'document_list_cta' => 'Tải tài liệu lên', + 'document_list_blank_desc' => 'Bạn có thể lưu trữ tài liệu liên quan đến người này ở đây.', + 'document_upload_zone_cta' => 'Tải lên tệp tin', + 'document_upload_zone_progress' => 'Đang tải lên tài liệu…', + 'document_upload_zone_error' => 'Có lỗi xảy ra khi tải tài liệu lên. Hãy thử lại sau.', + + // Photos + 'photo_title' => 'Ảnh', + 'photo_list_title' => 'Ảnh liên quan', + 'photo_list_cta' => 'Tải ảnh lên', + 'photo_list_blank_desc' => 'Bạn có thể lưu ảnh về liên hệ này. Tải lên ngay!', + 'photo_upload_zone_cta' => 'Tải ảnh lên', + 'photo_current_profile_pic' => 'Ảnh đại diện hiện tại', + 'photo_make_profile_pic' => 'Tạo ảnh đại diện', + 'photo_delete' => 'Xóa ảnh', + 'photo_next' => 'Ảnh tiếp theo ❯', + 'photo_previous' => '❮ Ảnh trước', + + // Avatars + 'avatar_change_title' => 'Đổi ảnh đại diện', + 'avatar_question' => 'Bạn muốn dùng ảnh đại diện nào?', + 'avatar_default_avatar' => 'Ảnh đại diện mặc định', + 'avatar_adorable_avatar' => 'Ảnh đại diện Adorable', + 'avatar_gravatar' => 'Gravatar được gắn với địa chỉ email của người này. Gravatar là hệ thống toàn cầu giúp người dùng gắn email với ảnh đại diện.', + 'avatar_current' => 'Giữ hình đại diện hiện tại', + 'avatar_photo' => 'Từ ảnh bạn đã tải lên', + 'avatar_crop_new_avatar_photo' => 'Crop ảnh đại diện mới', + + // emotions + 'emotion_this_made_me_feel' => 'Điều này khiến bạn cảm thấy…', + + // logs + 'auditlogs_link' => 'Lịch sử', + 'auditlogs_title' => 'Mọi thứ đã xảy ra với :name', + 'auditlogs_breadcrumb' => 'Lịch sử', + 'auditlogs_author' => 'Bởi :name vào :date', + + // contact field label + 'contact_field_label_home' => 'Nhà', + 'contact_field_label_work' => 'Cơ quan', + 'contact_field_label_cell' => 'Di động', + 'contact_field_label_fax' => 'Fax', + 'contact_field_label_pager' => 'Máy nhắn tin', + 'contact_field_label_main' => 'Chính', + 'contact_field_label_other' => 'Khác', + 'contact_field_label_personal' => 'Cá nhân', +]; diff --git a/resources/lang/vi/reminder.php b/resources/lang/vi/reminder.php new file mode 100644 index 0000000..6328afa --- /dev/null +++ b/resources/lang/vi/reminder.php @@ -0,0 +1,16 @@ + 'Chúc mừng sinh nhật tới', + 'type_phone_call' => 'Gọi', + 'type_lunch' => 'Ăn trưa với', + 'type_hangout' => 'Đi chơi với', + 'type_email' => 'Email', + 'type_birthday_kid' => 'Wish happy birthday to the child of', +]; diff --git a/resources/lang/vi/settings.php b/resources/lang/vi/settings.php new file mode 100644 index 0000000..88cbe86 --- /dev/null +++ b/resources/lang/vi/settings.php @@ -0,0 +1,557 @@ + 'Thiết lập tài khoản', + 'sidebar_personalization' => 'Cá nhân hóa', + 'sidebar_settings_storage' => 'Dung lượng', + 'sidebar_settings_export' => 'Xuất dữ liệu', + 'sidebar_settings_users' => 'Người dùng', + 'sidebar_settings_subscriptions' => 'Đăng ký', + 'sidebar_settings_import' => 'Nhập dữ liệu', + 'sidebar_settings_tags' => 'Quản lý tag', + 'sidebar_settings_api' => 'API', + 'sidebar_settings_dav' => 'Tài nguyên DAV', + 'sidebar_settings_security' => 'Bảo mật', + 'sidebar_settings_auditlogs' => 'Nhật ký kiểm duyệt', + + 'title_general' => 'Thông tin chung', + 'title_i18n' => 'Thiết lập ngôn ngữ và định dạng', + 'title_layout' => 'Giao diện', + + 'me_title' => 'Thông tin liên hệ của tôi', + 'me_help' => 'Đây là liên hệ đại diện cho bạn trên Monica', + 'me_select' => 'Chọn 1 liên hệ', + 'me_no_contact' => 'Không có liên hệ nào được chọn.', + 'me_select_click' => 'Bấm vào đây để chọn 1 liên hệ.', + 'me_remove_contact' => 'Gỡ liên kết', + 'me_choose' => 'Chọn bản thân', + 'me_choose_placeholder' => 'Chọn bản thân', + + 'export_title' => 'Xuất dữ liệu tài khoản của bạn', + 'export_be_patient' => 'Click the button to start the export. It might take several minutes to process the export – please be patient and do not spam the button.', + 'export_title_sql' => 'Export to SQL', + 'export_sql_explanation' => 'Exporting your data in SQL format allows you to take your data and import it to your own Monica instance. This is only valuable if you do have your own server.', + 'export_sql_cta' => 'Export to SQL', + 'export_sql_link_instructions' => 'Note: read the instructions to learn more about importing this file to your instance.', + 'export_title_json' => 'Export to Json', + 'export_submitted' => 'Your export has been submitted, it will be available in a few moment…', + 'export_json_explanation' => 'Exporting your data in Json format for backup.', + 'export_json_beta' => 'Json export is in preview mode. Tell us what you think about it:', + 'export_json_cta' => 'Export to Json', + 'export_header_type' => 'Type', + 'export_header_timestamp' => 'Creation date', + 'export_header_status' => 'Status', + 'export_header_actions' => 'Actions', + 'export_last_title' => 'Last exports', + 'export_empty_title' => 'No exports yet', + 'export_type_json' => 'Json export', + 'export_type_sql' => 'SQL export', + 'export_status_todo' => 'Submitted', + 'export_status_doing' => 'Doing', + 'export_status_done' => 'Done', + 'export_status_failed' => 'Failed', + 'export_not_done' => 'Download impossible, this export is not done yet.', + + 'firstname' => 'Tên', + 'lastname' => 'Họ', + 'name_order' => 'Sắp xếp tên theo', + 'name_order_firstname_lastname' => ' – Tèo Nguyễn', + 'name_order_lastname_firstname' => ' – Nguyễn Tèo', + 'name_order_firstname_lastname_nickname' => ' () – Tèo Nguyễn (cu tèo)', + 'name_order_firstname_nickname_lastname' => ' () – Tèo (cu tèo) Nguyễn', + 'name_order_lastname_firstname_nickname' => ' () – Nguyễn Tèo (cu tèo)', + 'name_order_lastname_nickname_firstname' => ' () – Nguyễn (cu tèo) Tèo', + 'name_order_nickname_firstname_lastname' => ' ( ) – cu tèo (Tèo Nguyễn)', + 'name_order_nickname_lastname_firstname' => ' ( ) – cu tèo (Nguyễn Tèo)', + 'name_order_nickname' => ' – cu tèo', + 'currency' => 'Tiền tệ', + 'name' => 'Tên bạn: :name', + 'email' => 'Địa chỉ email', + 'email_placeholder' => 'Nhập email', + 'email_help' => 'Đây là email dùng để đăng nhập, và là nơi bạn sẽ nhận các thông báo nhắc nhở.', + 'timezone' => 'Múi giờ', + 'temperature_scale' => 'Thang đo nhiệt độ', + 'temperature_scale_fahrenheit' => 'Fahrenheit (°F)', + 'temperature_scale_celsius' => 'Celsius (°C)', + 'layout' => 'Giao diện', + 'layout_small' => 'Chiều rộng tối đa 1200 pixel', + 'layout_big' => 'Toàn bộ chiều rộng trình duyệt', + 'save' => 'Cập nhật tùy chọn', + 'delete_title' => 'Xóa tài khoản', + 'delete_desc' => 'Bạn có muốn xóa tài khoản của bạn? Việc xóa không thể khôi phục và tất cả dữ liệu của bạn sẽ biến mất vĩnh viễn. Nếu bạn có gói thuê bao, nó sẽ bị hủy ngay lập túc.', + 'delete_other_desc' => 'Your data in the main database will be deleted immediately. As described in our privacy policy, we carry out securely encrypted backups of the database every day. These backups are kept for 30 days after which they are completely deleted. We cannot delete specific data from the backups we hold any earlier than this. All of your data will be completely deleted no later than 31 days after your account’s deletion.', + 'reset_desc' => 'Bạn chắc chắn muốn cài đặt lại tài khoản? Tất cả liên hệ và dữ liệu liên quan sẽ bị xóa. Tài khoản của bạn sẽ không bị xóa.', + 'reset_title' => 'Cài đặt lại tài khoản của bạn', + 'reset_cta' => 'Cài đặt lại tài khoản', + 'reset_notice' => 'Bạn chắc chắn muốn cài đặt lại tài khoản? Hành động này không thể hoàn tác.', + 'reset_success' => 'Cài đặt lại tài khoản thành công.', + 'delete_notice' => 'Bạn chắc chắn muốn xóa tài khoản? Hành động này không thể hoàn tác. Tất cả dữ liệu của bạn sẽ bị xóa và không thể khôi phục.', + 'delete_cta' => 'Xoá tài khoản', + 'settings_success' => 'Đã cập nhật Tùy Chọn!', + 'locale' => 'Ngôn ngữ', + 'locale_help' => 'Bạn có muốn hỗ trợ dịch Monica hoặc thêm ngôn ngữ mới? Hãy theo dõi thêm thông tin ở liên kết này.', + 'locale_ar' => 'Tiếng Ả Rập', + 'locale_cs' => 'Tiếng Séc', + 'locale_de' => 'Tiếng Đức', + 'locale_el' => 'Tiếng Hy Lạp', + 'locale_en' => 'Tiếng Anh', + 'locale_en-GB' => 'Tiếng Anh (Vương Quốc Anh)', + 'locale_es' => 'Tiếng Tây Ban Nha', + 'locale_fr' => 'Tiếng Pháp', + 'locale_he' => 'Tiếng Do Thái', + 'locale_hr' => 'Tiếng Croatia', + 'locale_id' => 'Tiếng Indonesia', + 'locale_it' => 'Tiếng Ý', + 'locale_ja' => 'Tiếng Nhật', + 'locale_nl' => 'Tiếng Hà Lan', + 'locale_pt' => 'Tiếng Bồ Đào Nha', + 'locale_pt-BR' => 'Tiếng Bồ Đào Nha (Brazil)', + 'locale_ru' => 'Tiếng Nga', + 'locale_sv' => 'Tiếng Thụy Điển', + 'locale_vi' => 'tiếng Việt', + 'locale_zh' => 'Tiếng Trung Giản thể', + 'locale_zh-TW' => 'Tiếng Trung phồn thể', + 'locale_tr' => 'Tiếng Thổ Nhĩ Kỳ', + + 'security_title' => 'Bảo mật', + 'security_help' => 'Thay đổi các vấn đề bảo mật cho tài khoản của bạn.', + 'password_change' => 'Đổi mật khẩu', + 'password_current' => 'Mật khẩu hiện tại', + 'password_current_placeholder' => 'Nhập mật khẩu hiện tại', + 'password_new1' => 'Mật khẩu mới', + 'password_new1_placeholder' => 'Nhập mật khẩu mới', + 'password_new2' => 'Xác nhận mật khẩu mới', + 'password_new2_placeholder' => 'Nhập lại mật khẩu mới', + 'password_btn' => 'Đổi mật khẩu', + '2fa_title' => 'Xác minh 2 bước', + '2fa_otp_title' => 'Ứng dụng mobile xác thực 2 yếu tố', + '2fa_enable_title' => 'Bật xác thực 2 bước', + '2fa_enable_description' => 'Bật xác thực 2 yếu tố để nâng cao bảo mật cho tài khoản của bạn.', + '2fa_enable_otp' => 'Mở ứng dụng điện thoại xác thực 2 yếu tố và quét mã QR dưới đây:', + '2fa_enable_otp_help' => 'Nếu ứng dụng xác thực 2 yếu tố của bạn không hỗ trợ QR code, nhập mã dưới đây:', + '2fa_enable_otp_validate' => 'Hãy xác nhận thiết bị mới bạn vừa thiết lập:', + '2fa_enable_success' => 'Đã kích hoạt xác thực 2 yếu tố', + '2fa_enable_error' => 'Có lỗi khi thử kích hoạt xác thực 2 yếu tố', + '2fa_enable_error_already_set' => 'Xác thực 2 yếu tố đã được kích hoạt trước đó', + '2fa_disable_title' => 'Vô hiệu xác thực 2 yếu tố', + '2fa_disable_description' => 'Vô hiệu hóa xác thực 2 yếu tố cho tài khoản của bạn. Hãy cẩn thận, tài khoản của bạn sẽ kém bảo mật hơn!', + '2fa_disable_success' => 'Đã vô hiệu xác thực 2 yếu tố', + '2fa_disable_error' => 'Có lỗi khi thử kích hoạt xác thực 2 yếu tố', + + 'webauthn_title' => 'Khoá bảo mật — giao thức WebAuthn', + 'webauthn_enable_description' => 'Thêm khóa bảo mật mới', + 'webauthn_key_name_help' => 'Đặt tên cho khoá bảo mật.', + 'webauthn_key_name' => 'Tên khoá:', + 'webauthn_success' => 'Khóa của bạn đã được phát hiện và xác thực.', + 'webauthn_last_use' => 'Lần sử dụng gần đây: {timestamp}', + 'webauthn_delete_confirmation' => 'Bạn chắc chắn muốn xóa key này?', + 'webauthn_delete_success' => 'Đã xóa key', + 'webauthn_insertKey' => 'Lắp khóa bảo mật của bạn.', + 'webauthn_buttonAdvise' => 'Nếu khóa bảo mật có nút, hãy chạm vào.', + 'webauthn_noButtonAdvise' => 'Nếu nó không có, tháo ra và cắm lại.', + 'webauthn_not_supported' => 'Trình duyệt của bạn không hỗ trợ WebAuthn.', + 'webauthn_not_secured' => 'WebAuthn chỉ hỗ trợ kết nối bảo mật. Hãy tải lại trang với giao thức https.', + 'webauthn_error_already_used' => 'Khóa này đã được đăng kí. Không cần thiết đăng kí lại.', + 'webauthn_error_not_allowed' => 'Hoạt động đã hết thời gian chờ hoặc không được phép.', + + 'recovery_title' => 'Mã khôi phục', + 'recovery_show' => 'Tạo mã khôi phục', + 'recovery_copy_help' => 'Sao chép mã vào clipboard', + 'recovery_help_intro' => 'Đây là mã khôi phục của bạn:', + 'recovery_help_information' => 'Bạn có thể sử dụng mỗi mã khôi phục một lần.', + 'recovery_clipboard' => 'Đã sao chép mã vào bộ nhớ tạm.', + 'recovery_generate' => 'Tạo mã mới…', + 'recovery_generate_help' => 'Khởi tạo bộ mã mới sẽ vô hiệu các bộ mã đã khởi tạo trước đây.', + 'recovery_already_used_help' => 'Mã này đã được sử dụng.', + + 'users_list_title' => 'Người dùng với quyền truy cập tài khoản của bạn', + 'users_list_add_user' => 'Mời người dùng mới', + 'users_list_you' => 'Bạn', + 'users_list_invitations_title' => 'Lời mời đang chờ', + 'users_list_invitations_explanation' => 'Dưới đây là những người bạn đã mời tham gia Monica với tư cách cộng tác viên.', + 'users_list_invitations_invited_by' => 'đã được mời bởi :name', + 'users_list_invitations_sent_date' => 'gửi vào :date', + 'users_blank_title' => 'Bạn là người duy nhất truy cập vào tài khoản này.', + 'users_blank_add_title' => 'Bạn có muốn mời thêm ai khác?', + 'users_blank_description' => 'Người này sẽ có cùng quyền truy cập mà bạn có và có thể thêm, chỉnh sửa hoặc xóa thông tin liên hệ.', + 'users_blank_cta' => 'Mời một ai đó', + 'users_add_title' => 'Mời người dùng mới cho tài khoản của bạn bằng email', + 'users_add_description' => 'Người này sẽ có quyền truy cập giống như bạn, bao gồm cả việc mời hoặc xóa những người dùng khác, bao gồm cả bạn. Đảm bảo rằng bạn tin tưởng người này trước khi cấp cho họ quyền truy cập.', + 'users_add_email_field' => 'Nhập email của người bạn muốn mời', + 'users_add_confirmation' => 'Tôi xác nhận rằng tôi muốn mời người dùng này vào tài khoản của tôi. Tôi hiểu rằng người này sẽ được truy cập vào TẤT CẢ dữ liệu của tôi và xem được chính xác những gì tôi đang thấy.', + 'users_add_cta' => 'Mời bằng email', + 'users_accept_title' => 'Chấp nhận lời mời và tạo tài khoản mới', + 'users_error_please_confirm' => 'Hãy xác nhận rằng bạn muốn mời người dùng này trước khi tiếp tục với lời mời', + 'users_error_email_already_taken' => 'Email này đã được sử dụng. Vui lòng chọn email khác', + 'users_error_already_invited' => 'Bạn đã mời người dùng này trước đó. Hãy chọn địa chỉ email khác.', + 'users_error_email_not_similar' => 'Đây không phải email của người đã mời bạn.', + 'users_invitation_deleted_confirmation_message' => 'Đã xóa lời mời thành công', + 'users_invitations_delete_confirmation' => 'Bạn có chắc muốn xóa lời mời này?', + 'users_list_delete_confirmation' => 'Bạn chắc chắn muốn xóa người dùng này khỏi tài khoản của bạn?', + 'users_invitation_need_subscription' => 'Cần gói thuê bao để thêm người dùng mới.', + + 'subscriptions_account_current_plan' => 'Gói hiện tại', + 'subscriptions_account_current_legacy' => 'Current plan, not selectable anymore:', + 'subscriptions_account_current_paid_plan' => 'Bạn đang dùng gói :name. Cảm ơn bạn vì đã đăng kí thuê bao.', + + 'subscriptions_account_next_billing_title' => 'Next bill', + 'subscriptions_account_next_billing' => 'Thuê bao của bạn sẽ tự động thanh toán vào :date.', + 'subscriptions_account_bill_monthly' => 'We’ll bill you :price for another month.', + 'subscriptions_account_bill_annual' => 'We’ll bill you :price for another year.', + 'subscriptions_account_change' => 'Change plan', + + 'subscriptions_account_cancel_title' => 'Cancel subscription', + 'subscriptions_account_cancel_action' => 'Cancel subscription', + 'subscriptions_account_cancel' => 'You can cancel your subscription at any time.', + 'subscriptions_account_free_plan' => 'Bạn đang dùng gói miễn phí.', + 'subscriptions_account_free_plan_upgrade' => 'Bạn có thể nâng cấp tài khoản của bạn lên gói :name, chi phí $:price mỗi tháng. Đây là những lợi ích:', + 'subscriptions_account_free_plan_benefits_users' => 'Không giới hạn số lượng người dùng', + 'subscriptions_account_free_plan_benefits_reminders' => 'Nhắc nhở bằng email', + 'subscriptions_account_free_plan_benefits_import_data_vcard' => 'Nhập danh bạ của bạn với vCard', + 'subscriptions_account_free_plan_benefits_support' => 'Hỗ trợ dự án có thể vận hành lâu dài, khi đó chúng tôi có thể giới thiệu nhiều tính năng tuyệt vời hơn.', + 'subscriptions_account_upgrade' => 'Nâng cấp tài khoản của bạn', + 'subscriptions_account_upgrade_title' => 'Nâng cấp Monica hôm nay và có thêm nhiều mối quan hệ ý nghĩa.', + 'subscriptions_account_upgrade_choice' => 'Chọn gói dưới đây và gia nhập hội :customers đã nâng cấp Monica.', + 'subscriptions_account_update_title' => 'Update Monica subscription', + 'subscriptions_account_update_description' => 'You can change your subscription’s frequency here.', + 'subscriptions_account_update_information' => 'You will be billed immediately for the new amount. Your subscription will extend to the new period, depending on your choice.', + 'subscriptions_account_invoices' => 'Hoá đơn', + 'subscriptions_account_invoices_download' => 'Tải xuống', + 'subscriptions_account_invoices_subscription' => 'Gói thuê bao từ :startDate đến :endDate', + 'subscriptions_account_payment' => 'Tùy chọn thanh toán nào phù hợp với bạn?', + 'subscriptions_account_confirm_payment' => 'Việc thanh toán của bạn chưa hoàn thành, hãy xác nhận thanh toán của bạn.', + 'subscriptions_downgrade_title' => 'Hạ cấp tài khoản xuống gói miễn phí', + 'subscriptions_downgrade_limitations' => 'Gói miễn phí bị giới hạn. Để có thể hạ cấp, bạn phải thỏa mãn các điều kiện dưới đây:', + 'subscriptions_downgrade_rule_users' => 'Bạn chỉ được có 1 người dùng trong tài khoản của bạn', + 'subscriptions_downgrade_rule_users_constraint' => 'Bạn đang có 1 người dùng trong tài khoản.|Bạn đang có :count người dùng trong tài khoản.', + 'subscriptions_downgrade_rule_invitations' => 'Bạn không được phép có lời mời đang chờ nào', + 'subscriptions_downgrade_rule_invitations_constraint' => 'Hiện tại bạn đang có 1 lời mời đang chờ.|Hiện tại bạn đang có :count lời mời đang chờ.', + 'subscriptions_downgrade_rule_contacts' => 'Bạn không được có nhiều hơn :number liên hệ đang hoạt động', + 'subscriptions_downgrade_rule_contacts_constraint' => 'Hiện tại bạn đang có :count liên hệ.', + 'subscriptions_downgrade_rule_contacts_archive' => 'We can also archive all your contacts for you – that would clear this rule and let you proceed with your account’s downgrade process.', + 'subscriptions_downgrade_cta' => 'Hạ cấp', + 'subscriptions_downgrade_success' => 'Bạn đã trở lại gói miễn phí!', + 'subscriptions_downgrade_thanks' => 'Thanks so much for trying the paid plan. We keep adding new features on Monica all the time – so you might want to come back in the future to see if you might be interested in taking a subscription again.', + 'subscriptions_back' => 'Quay lại cài đặt', + 'subscriptions_upgrade_title' => 'Nâng cấp tài khoản của bạn', + 'subscriptions_upgrade_choose' => 'Bạn đã chọn gói :plan.', + 'subscriptions_upgrade_infos' => 'Chúng tôi không thể vui sướng hơn. Hãy nhập thông tin thanh toán dưới đây.', + 'subscriptions_upgrade_name' => 'Tên trên thẻ', + 'subscriptions_upgrade_zip' => 'Mã bưu chính', + 'subscriptions_upgrade_credit' => 'Thẻ tín dụng', + 'subscriptions_upgrade_submit' => 'Thanh toán {amount}', + 'subscriptions_upgrade_charge' => 'Chúng tôi sẽ tính phí :price vào thẻ của bạn bây giờ. Lần tính phí tiếp theo vào :date. Nếu bạn muốn thay đổi, bạn có thể hủy bất cứ lúc nào, không cần hỏi.', + 'subscriptions_upgrade_charge_handled' => 'Việc thanh toán được xử lý bởi Stripe. Chúng tôi không lưu trữ thông tin thẻ.', + 'subscriptions_upgrade_success' => 'Cảm ơn! Bây giờ bạn đã đăng kí thuê bao.', + 'subscriptions_upgrade_thanks' => 'Chào mừng bạn đến cộng đồng những người muốn làm thế giới tốt đẹp hơn.', + + 'subscriptions_payment_confirm_title' => 'Xác nhận thanh toán :amount', + 'subscriptions_payment_confirm_information' => 'Xác thục bổ sung là cần thiết để tiến hành thanh toán. Hãy xác thực thanh toán bằng cách điền thông tin thanh toán bên dưới.', + 'subscriptions_payment_succeeded_title' => 'Thanh toán thành công', + 'subscriptions_payment_succeeded' => 'Bạn đã xác thực thanh toán thành công.', + 'subscriptions_payment_cancelled_title' => 'Thanh toán bị hủy', + 'subscriptions_payment_cancelled' => 'Thanh toán đã bị hủy.', + 'subscriptions_payment_error_name' => 'Hãy nhập tên bạn.', + 'subscriptions_payment_success' => 'Thanh toán thành công.', + + 'subscriptions_pdf_title' => 'Gói thuê bao :name hàng tháng của bạn', + 'subscriptions_plan_frequency_year' => ':amount / year', + 'subscriptions_plan_frequency_month' => ':amount / month', + 'subscriptions_plan_choose' => 'Chọn gói', + 'subscriptions_plan_year_title' => 'Thanh toán hàng năm', + 'subscriptions_plan_year_bonus' => 'An tâm cả năm', + 'subscriptions_plan_month_title' => 'Thanh toán hàng tháng', + 'subscriptions_plan_month_bonus' => 'Hủy bất cứ lúc nào', + 'subscriptions_plan_include1' => 'Đi kèm với nâng cấp của bạn:', + 'subscriptions_plan_include2' => 'Không giới hạn số lượng liên hệ • Không giới hạn số lượng người dùng • Nhắc nhờ bằng email • Nhập với vCard • Cá nhân hóa bảng liên hệ', + 'subscriptions_plan_include3' => '100% lợi nhuận dành cho việc phát triển dự án mã nguồn mở này.', + 'subscriptions_help_title' => 'Các thông tin bổ sung mà bạn có thể hứng thú', + 'subscriptions_help_opensource_title' => 'Dự án mã nguồn mở là gì?', + 'subscriptions_help_opensource_desc' => 'Monica là dự án mã nguồn mở. Điều đó tức là nó được xây dựng bởi cộng đồng những người muốn xây dựng công cụ cho những điều tốt đẹp. Trở thành mã nguồn mở nghĩa là mã nguồn được công khai trên GitHub, và mọi người có thể xem, sửa và tối ưu nó. Tất cả số tiền chúng tôi quyên góp được dành để xây dựng các tính năng tốt hơn, trả tiền cho các máy chủ mạnh hơn và trả các chi phí khác. Cảm ơn bạn đã giúp đỡ. Chúng tôi không thể làm điều đó nếu không có bạn.', + 'subscriptions_help_limits_title' => 'Có giới hạn số lượng liên hệ trong gói miễn phí không?', + 'subscriptions_help_limits_plan' => 'Có. Gói miễn phí cho phép bạn quản lý :number liên hệ.', + 'subscriptions_help_discounts_title' => 'Bạn có mã giảm giá nào cho tổ chức phi lợi nhuận và giáo dục không?', + 'subscriptions_help_discounts_desc' => 'Chúng tôi có! Monica miễn phí cho sinh viên, tổ chức phi lợi nhuận và từ thiện. Hãy liên hệ đội hỗ trợ với bằng chứng về tình trạng của bạn và chúng tôi sẽ áp dụng trạng thái đặc biệt cho tài khoản của bạn.', + 'subscriptions_help_change_title' => 'Nếu tôi đổi ý thì sao?', + 'subscriptions_help_change_desc' => 'Bạn có thể hủy bất cứ lúc nào, không cần hỏi, mọi thứ được làm bởi chính bạn - không cần liên hệ hỗ trợ. Tuy nhiên, bạn sẽ không được hoàn lại tiền cho giai đoạn hiện tại.', + + 'stripe_error_card' => 'Thẻ của bạn bị từ chối. Tin nhắn từ chối là: :message', + 'stripe_error_api_connection' => 'Kết nối mạng với Stripe bị lỗi. Hãy thử lại.', + 'stripe_error_rate_limit' => 'Hiện tại đang có quá nhiều yêu cầu đến Stripe. Hãy thử lại sau.', + 'stripe_error_invalid_request' => 'Thông số không hợp lệ. Hãy thử lại sau.', + 'stripe_error_authentication' => 'Sai thông tin xác thực với Stripe', + + 'import_title' => 'Nhập danh bạ vào tài khoản của bạn', + 'import_cta' => 'Tải lên danh bạ', + 'import_stat' => 'Bạn đã nhập :number tệp từ trước đến giờ.', + 'import_result_stat' => 'Đã tải lên vCard với 1 liên hệ (đã nhập :total_imported, đã bỏ qua :total_skipped)', + 'import_view_report' => 'Xem báo cáo', + 'import_in_progress' => 'Đang xử lý việc nhập. Tải lại trang trong một phút.', + 'import_upload_title' => 'Nhập danh bạ của bạn với vCard', + 'import_upload_rules_desc' => 'Tuy nhiên, chúng tôi có một số quy tắc:', + 'import_upload_rule_format' => 'Chúng tôi hỗ trợ tệp .vcard.vcf.', + 'import_upload_rule_vcard' => 'Chúng tôi hỗ trợ định dạng vCard 3.0 format, là định dạng mặc định của macOS’s Contacts.app và Google Contacts.', + 'import_upload_rule_instructions' => 'Hướng dẫn xuất dữ liệu cho macOS Contacts.appGoogle Contacts.', + 'import_upload_rule_multiple' => 'Nếu liên hệ của bạn có nhiều địa chỉ email hoặc số điện thoại, chỉ có mục đầu tiên sẽ được lưu.', + 'import_upload_rule_limit' => 'Tệp giới hạn tới 10MB.', + 'import_upload_rule_time' => 'Có thể mất tới vài phút để tải lên danh bạ và xử lý chúng. Vui lòng đợi.', + 'import_upload_rule_cant_revert' => 'Hãy chắc chắn dữ liệu chính xác trước khi tải lên, bạn không thể hủy tiến trình tải lên.', + 'import_upload_form_file' => 'Tệp .vcf hoặc .vCard của bạn:', + 'import_upload_behaviour' => 'Import behaviour:', + 'import_upload_behaviour_add' => 'Thêm liên hệ mới và bỏ qua liên hệ đã tồn tại', + 'import_upload_behaviour_replace' => 'Thay thế liên hệ đã tồn tại', + 'import_upload_behaviour_help' => 'Việc thay thế sẽ thay thế toàn bộ dữ liệu tồn tại trong vCard, nhưng vẫn giữ các trường liên hệ đã tồn tại.', + 'import_report_title' => 'Báo cáo nhập', + 'import_report_date' => 'Ngày nhập', + 'import_report_type' => 'Kiểu nhập', + 'import_report_number_contacts' => 'Số lượng liên hệ trong tệp', + 'import_report_number_contacts_imported' => 'Số lượng danh bạ đã nhập', + 'import_report_number_contacts_skipped' => 'Số lượng danh bạ đã bỏ qua', + 'import_report_status_imported' => 'Đã nhập', + 'import_report_status_skipped' => 'Đã bỏ qua', + 'import_vcard_parse_error' => 'Lỗi khi phân tích mục vCard', + 'import_vcard_contact_exist' => 'Liên hệ đã tồn tại', + 'import_vcard_contact_no_firstname' => 'Không có tên (bắt buộc)', + 'import_vcard_file_not_found' => 'Tệp không tồn tại', + 'import_vcard_unknown_entry' => 'Tên liên hệ không xác định', + 'import_vcard_file_no_entries' => 'Tệp không chứa mục nào', + 'import_blank_title' => 'Bạn chưa nhập bất kì liên hệ nào.', + 'import_blank_question' => 'Bạn có muốn nhập liên hệ ngay bây giờ?', + 'import_blank_description' => 'Chúng tôi có thể nhập tệp vCard bạn lấy từ Google Contacts hoặc ứng dụng quản lý Danh bạ của bạn.', + 'import_blank_cta' => 'Nhập file vCard', + 'import_need_subscription' => 'Việc nhập dữ liệu yêu cầu gói thuê bao.', + + 'tags_list_title' => 'Nhãn', + 'tags_list_description' => 'Bạn có thể sắp xếp liên hệ bằng cách thiết lập nhãn. Nhãn hoạt động giống thư mục, nhưng bạn có thể thêm nhiều nhãn vào một liên hệ. Để thêm nhãn mới, thêm nó ở trang liên hệ.', + 'tags_list_contact_number' => ':count liên hệ', + 'tags_list_delete_success' => 'The tag has been successfully deleted', + 'tags_list_edit_success' => 'The tag has been successfully updated', + 'tags_list_delete_confirmation' => 'Are you sure you want to delete the tag? No contacts will be deleted, only the tag.', + 'tags_blank_title' => 'Tag là một cách tốt để phân loại liên hệ của bạn.', + 'tags_blank_description' => 'Tag hoạt động như một thư mục, nhưng bạn có thể thêm nhiều tag vào một liên hệ. Truy cập trang liên hệ và thêm tag bạn bè, ngay dưới phần tên. Khi liên hệ được gắn tag, quay trở lại đây và quản lý tất cả tag trong tài khoản của bạn.', + + 'api_title' => 'Khoá truy cập API', + 'api_description' => 'API có thể dùng để tương tác với dữ liệu của Monica từ ứng dụng bên ngoài, như app điện thoại.', + 'api_help' => 'Để sử dụng API, phải có một token. Bạn có thể tạo một token truy cập cá nhân (Bearer authentication), hoặc xác thực qua OAuth client để tạo nó cho bạn. Xem tài liệu API.', + 'api_endpoint' => 'Đường dẫn đích API cho Monica là:', + + 'api_personal_access_tokens' => 'Mã token truy cập cá nhân', + 'api_pao_description' => 'Chắc chắn rằng bạn chỉ cấp quyền truy cập token này cho nguồn mà bạn tin tưởng - vì chúng cho phép truy cập tất cả dữ liệu của bạn.', + 'api_token_title' => 'Mã truy cập cá nhân', + 'api_token_create_new' => 'Tạo token mới', + 'api_token_not_created' => 'Bạn chưa tạo bất kỳ mã truy cập cá nhân nào.', + 'api_token_name' => 'Tên token', + 'api_token_expire' => 'Hết hạn vào {date}', + 'api_token_delete' => 'Xóa', + 'api_token_create' => 'Tạo Token', + 'api_token_scopes' => 'Phạm vi', + 'api_token_help' => 'Here is your new personal access token. This is the only time it will be shown so don’t lose it! You may now use this token to make API requests.', + + 'api_oauth_clients' => 'OAuth client của bạn', + 'api_oauth_clients_desc' => 'Mục này cho phép bạn đăng kí OAuth client của bạn.', + 'api_oauth_clients_desc2' => 'Dùng client id để yêu cầu token mới, và chuyển đổi mã xác thực sang mã token truy cập. Xem tài liệu Laravel Passport để biết thêm thông tin.', + 'api_oauth_title' => 'OAuth Clients', + 'api_oauth_create_new' => 'Tạo mới Client', + 'api_oauth_edit' => 'Sửa Client', + 'api_oauth_not_created' => 'Bạn không có bất kì OAuth client nào.', + 'api_oauth_clientid' => 'Client ID', + 'api_oauth_name' => 'Tên', + 'api_oauth_name_help' => 'Một cái gì đó người dùng của bạn sẽ nhận ra và tin tưởng.', + 'api_oauth_secret' => 'Mã bí mật', + 'api_oauth_create' => 'Tạo Client', + 'api_oauth_redirecturl' => 'URL chuyển tiếp', + 'api_oauth_redirecturl_help' => 'URL gọi lại ủy quyền của ứng dụng của bạn.', + + 'api_authorized_clients' => 'Danh sách client được ủy quyền', + 'api_authorized_clients_desc' => 'Mục này liệt kê tất cả client bạn đã cấp quyền truy cập dữ liệu ứng dụng của bạn. Bạn có thể hủy quyền bất kì lúc nào.', + 'api_authorized_clients_title' => 'Ứng dụng được ủy quyền', + 'api_authorized_clients_none' => 'Không có client nào được cấp quyền.', + 'api_authorized_clients_name' => 'Tên', + 'api_authorized_clients_scopes' => 'Phạm vi', + + 'personalization_tab_title' => 'Cá nhân hóa tài khoản', + + 'personalization_title' => 'Here you will find different settings to configure your account. These features are intended for “power users” who want maximum control over Monica.', + 'personalization_contact_field_type_title' => 'Loại trường liên hệ', + 'personalization_contact_field_type_add' => 'Thêm loại trường mới', + 'personalization_contact_field_type_description' => 'You can configure all the different types of contact fields that you can associate to all your contacts. For example, if a new social network appears in the future, you will be able to add this new way of communicating with your contacts right here.', + 'personalization_contact_field_type_table_name' => 'Tên', + 'personalization_contact_field_type_table_protocol' => 'Giao thức', + 'personalization_contact_field_type_table_actions' => 'Hành động', + 'personalization_contact_field_type_modal_title' => 'Add a new contact field type', + 'personalization_contact_field_type_modal_edit_title' => 'Edit an existing contact field type', + 'personalization_contact_field_type_modal_delete_title' => 'Delete an existing contact field type', + 'personalization_contact_field_type_modal_delete_description' => 'Are you sure you want to delete this contact field type? Deleting this type of contact field will delete ALL the data with this type for all of your contacts.', + 'personalization_contact_field_type_modal_name' => 'Tên', + 'personalization_contact_field_type_modal_protocol' => 'Giao thức (tùy chọn)', + 'personalization_contact_field_type_modal_protocol_help' => 'Each new contact field type can be clickable. If a protocol is set, we will use it to trigger the action that is set.', + 'personalization_contact_field_type_modal_icon' => 'Icon (tùy chọn)', + 'personalization_contact_field_type_modal_icon_help' => 'Bạn có thể gắn icon với loại trường liên hệ này. Bạn cần thêm chỉ dẫn tới Font Awesome icon.', + 'personalization_contact_field_type_delete_success' => 'Xóa loại trường liên hệ thành công.', + 'personalization_contact_field_type_add_success' => 'Thêm loại trường liên hệ thành công.', + 'personalization_contact_field_type_edit_success' => 'Cập nhật loại trường liên hệ thành công.', + + 'personalization_genders_title' => 'Loại giới tính', + 'personalization_genders_add' => 'Thêm loại giới tính mới', + 'personalization_genders_desc' => 'Bạn có thể định nghĩa bao nhiêu giới tính cũng được. Bạn cần tối thiểu 1 loại giới tính trong tài khoản của bạn.', + 'personalization_genders_modal_add' => 'Thêm loại giới tính', + 'personalization_genders_modal_edit' => 'Cập nhật loại giới tính', + 'personalization_genders_modal_name' => 'Tên', + 'personalization_genders_modal_name_help' => 'Tên sẽ được dùng hiển thị giới tính trên trang liên hệ.', + 'personalization_genders_modal_sex' => 'Giới tính', + 'personalization_genders_modal_sex_help' => 'Sử dụng để định nghĩa mối quan hệ, và trong quá trình nhập/xuất vCard.', + 'personalization_genders_modal_default' => 'Chọn giới tính mặc định cho liên hệ mới', + 'personalization_genders_modal_delete' => 'Xóa loại giới tính', + 'personalization_genders_modal_delete_desc' => 'Bạn chắc chắn muốn xóa giới tính "{name}"?', + 'personalization_genders_modal_delete_question' => 'Hiện bạn đang có {count} liên hệ với giới tính này. Nếu bạn xóa giới tính này, giới tính nào các liên hệ sẽ sở hữu?', + 'personalization_genders_modal_delete_question_default' => 'Giới tính này đang được đặt là mặc định. Nếu bạn xóa giới tính này, cái nào sẽ được đặt làm mặc định?', + 'personalization_genders_modal_error' => 'Hãy chọn giới tính trong danh sách.', + 'personalization_genders_list_contact_number' => '{count} liên hệ', + 'personalization_genders_table_name' => 'Tên', + 'personalization_genders_table_sex' => 'Giới tính', + 'personalization_genders_table_default' => 'Mặc định', + 'personalization_genders_default' => 'Giới tính mặc định', + 'personalization_genders_make_default' => 'Đổi giới tính mặc định', + 'personalization_genders_select_default' => 'Chọn giới tính mặc định', + 'personalization_genders_m' => 'Nam', + 'personalization_genders_f' => 'Nữ', + 'personalization_genders_o' => 'Khác', + 'personalization_genders_u' => 'Không rõ', + 'personalization_genders_n' => 'Không có hoặc không áp dụng', + + 'personalization_reminder_rule_save' => 'Đã lưu thay đổi', + 'personalization_reminder_rule_title' => 'Quy tắc nhắc nhở', + 'personalization_reminder_rule_line' => 'trước {count} ngày', + 'personalization_reminder_rule_desc' => 'Với mỗi nhắc nhở bạn cài, Monica có thể gửi email cho bạn vào x ngày trước khi sự kiện xảy ra. Bạn có thể điều chỉnh cài đặt thông báo ở đây. Thông báo này chỉ áp dụng cho lời nhắc hàng tháng và hàng năm.', + + 'personalization_module_save' => 'Đã lưu thay đổi', + 'personalization_module_title' => 'Tính năng', + 'personalization_module_desc' => 'Có thể bạn không cần dùng tất cả tính năng của Monica. Phần dưới đây cho phép bạn bật/tắt tính năng sử dụng trong khung liên hệ. Thay đổi này sẽ ảnh hưởng đến TẤT CẢ liên hệ của bạn. Tắt tính năng không xóa dữ liệu, chỉ ẩn tính năng này đi.', + + 'personalisation_paid_upgrade' => 'Đây là tính năng nâng cao cần thuê bao trả phí để kích hoạt. Hãy nâng cấp tài khoản của bạn bằng cách truy cập Cài đặt > Gói thuê bao.', + 'personalisation_paid_upgrade_vue' => 'Đây là tính năng nâng cao cần thuê bao trả phí để kích hoạt. Hãy nâng cấp tài khoản của bạn bằng cách truy cập Cài đặt > Gói thuê bao.', + + 'reminder_time_to_send' => 'Thời gian lời nhắc sẽ được gửi', + 'reminder_time_to_send_help' => 'Lời nhắc tiếp theo của bạn được lên lịch gửi vào {dateTime}.', + + 'personalization_activity_type_category_title' => 'Danh mục loại hoạt động', + 'personalization_activity_type_category_add' => 'Thêm danh mục', + 'personalization_activity_type_category_table_name' => 'Tên', + 'personalization_activity_type_category_description' => 'An activity with one of your contacts can have a type and a category type. Your account comes with a set of predefined category types by default, but you can customize these here.', + 'personalization_activity_type_category_table_actions' => 'Hành động', + 'personalization_activity_type_category_modal_add' => 'Thêm danh mục', + 'personalization_activity_type_category_modal_edit' => 'Sửa danh mục', + 'personalization_activity_type_category_modal_question' => 'Chúng ta nên đặt tên cho danh mục này là gì?', + 'personalization_activity_type_add_button' => 'Thêm loại hoạt động mới', + 'personalization_activity_type_modal_add' => 'Thêm loại hoạt động mới', + 'personalization_activity_type_modal_question' => 'Chúng ta nên đặt tên cho hoạt động này là gì?', + 'personalization_activity_type_modal_edit' => 'Sửa loại hoạt động', + 'personalization_activity_type_category_modal_delete' => 'Xóa danh mục', + 'personalization_activity_type_category_modal_delete_desc' => 'Bạn chắc chắn muốn xóa danh mục này? Việc này sẽ xóa tất cả loại hoạt động bên trong. Hoạt động gắn với danh mục này không bị ảnh hưởng.', + 'personalization_activity_type_modal_delete' => 'Xóa loại hoạt động', + 'personalization_activity_type_modal_delete_desc' => 'Bạn chắc chắn muốn xóa loại hoạt động này? Các hoạt động gắn với danh mục này sẽ không bị ảnh hưởng.', + 'personalization_activity_type_modal_delete_error' => 'Chúng tôi không thể tìm thấy loại hoạt động này.', + 'personalization_activity_type_category_modal_delete_error' => 'Chúng tôi không thể tìm thấy danh mục này.', + + 'personalization_life_event_category_title' => 'Danh mục sự kiện trong đời', + 'personalization_live_event_category_table_name' => 'Tên', + 'personalization_life_event_category_description' => 'Sự kiện trong đời có thể có loại và danh mục. Tài khoản của bạn được cài đặt danh mục và loại mặc định, nhưng bạn có thể tùy biến ở đây.', + 'personalization_live_event_category_table_actions' => 'Hành động', + 'personalization_life_event_type_add_button' => 'Thêm loại sự kiện trong đời', + 'personalization_life_event_type_modal_add' => 'Thêm loại sự kiện trong đời', + 'personalization_life_event_type_modal_question' => 'Chúng ta nên đặt tên cho sự kiện này là gì?', + 'personalization_life_event_type_modal_edit' => 'Sửa kiểu sự kiện trong đời', + 'personalization_life_event_type_modal_delete' => 'Xoá kiểu sự kiện trong đời', + 'personalization_life_event_type_modal_delete_desc' => 'Bạn muốn xóa kiểu sự kiện trong đời này? Các dữ liệu sự kiện gắn liền với kiểu sự kiện này cũng sẽ bị xóa bỏ.', + 'personalization_life_event_type_modal_delete_error' => 'Chúng tôi không thể tìm được kiểu sự kiện trong đời này.', + + 'personalization_life_event_category_work_education' => 'Công việc & học vấn', + 'personalization_life_event_category_family_relationships' => 'Gia đình & các mối quan hệ', + 'personalization_life_event_category_home_living' => 'Nhà cửa & đời sống', + 'personalization_life_event_category_travel_experiences' => 'Du lịch & trải nghiệm', + 'personalization_life_event_category_health_wellness' => 'Sức khỏe thể chất & tinh thần', + + 'personalization_life_event_type_new_job' => 'Công việc mới', + 'personalization_life_event_type_retirement' => 'Nghỉ hưu', + 'personalization_life_event_type_new_school' => 'Trường học mới', + 'personalization_life_event_type_study_abroad' => 'Du học', + 'personalization_life_event_type_volunteer_work' => 'Công việc tình nguyện', + 'personalization_life_event_type_published_book_or_paper' => 'Xuất bản sách hoặc văn bản', + 'personalization_life_event_type_military_service' => 'Nghĩa vụ quân sự', + 'personalization_life_event_type_first_met' => 'Lần đầu gặp', + 'personalization_life_event_type_new_relationship' => 'Mối quan hệ mới', + 'personalization_life_event_type_engagement' => 'Đính hôn', + 'personalization_life_event_type_marriage' => 'Kết hôn', + 'personalization_life_event_type_anniversary' => 'Kỷ niệm', + 'personalization_life_event_type_expecting_a_baby' => 'Muốn có con', + 'personalization_life_event_type_new_child' => 'Mới có con', + 'personalization_life_event_type_new_family_member' => 'Thêm thành viên mới trong gia đình', + 'personalization_life_event_type_new_pet' => 'Thêm thú cưng', + 'personalization_life_event_type_end_of_relationship' => 'Kết thúc mối quan hệ', + 'personalization_life_event_type_loss_of_a_loved_one' => 'Mất một người thân yêu', + 'personalization_life_event_type_moved' => 'Đã chuyển đi', + 'personalization_life_event_type_bought_a_home' => 'Đã mua nhà', + 'personalization_life_event_type_home_improvement' => 'Sửa nhà', + 'personalization_life_event_type_holidays' => 'Kỳ nghỉ', + 'personalization_life_event_type_new_vehicle' => 'Mua xe mới', + 'personalization_life_event_type_new_roommate' => 'Bạn cùng phòng mới', + 'personalization_life_event_type_overcame_an_illness' => 'Đã vượt qua bệnh tật', + 'personalization_life_event_type_quit_a_habit' => 'Bỏ một thói quen', + 'personalization_life_event_type_new_eating_habits' => 'Thói quen ăn uống mới', + 'personalization_life_event_type_weight_loss' => 'Giảm cân', + 'personalization_life_event_type_wear_glass_or_contact' => 'Đã đeo kính hoặc kính áp tròng', + 'personalization_life_event_type_broken_bone' => 'Gãy xương', + 'personalization_life_event_type_removed_braces' => 'Gỡ niềng răng', + 'personalization_life_event_type_surgery' => 'Đã phẫu thuật', + 'personalization_life_event_type_dentist' => 'Đã điều trị nha khoa', + 'personalization_life_event_type_new_sport' => 'Bắt đầu chơi môn thể thao mới', + 'personalization_life_event_type_new_hobby' => 'Có sở thích mới', + 'personalization_life_event_type_new_instrument' => 'Bắt đầu học nhạc cụ mới', + 'personalization_life_event_type_new_language' => 'Bắt đầu học ngôn ngữ mới', + 'personalization_life_event_type_tattoo_or_piercing' => 'Xăm hình hoặc xỏ khuyên', + 'personalization_life_event_type_new_license' => 'Bằng lái mới', + 'personalization_life_event_type_travel' => 'Du lịch', + 'personalization_life_event_type_achievement_or_award' => 'Có giải thưởng hoặc thành tựu', + 'personalization_life_event_type_changed_beliefs' => 'Thay đổi đức tin', + 'personalization_life_event_type_first_word' => 'Câu nói đầu tiên', + 'personalization_life_event_type_first_kiss' => 'Nụ hôn đầu', + + 'storage_title' => 'Bộ nhớ', + 'storage_account_info' => 'Giới hạn tài khoản của bạn là :accountLimit MB. Bạn đã sử dụng :currentAccountSize MB (khoảng :percentUsage%).', + 'storage_upgrade_notice' => 'Nâng cấp tài khoản của bạn để có thể tải lên tài liệu và ảnh.', + 'storage_description' => 'Bạn có thể xem tất cả tài liệu và ảnh đã tải lên về liên hệ của bạn ở đây.', + + 'dav_title' => 'WebDAV', + 'dav_description' => 'Bạn có thể tìm thấy tất cả cài đặt sử dụng WebDAV để xuất CardDAV và CalDAV ở đây.', + 'dav_copy_help' => 'Sao chép vào clipboard', + 'dav_clipboard_copied' => 'Giá trị đã được copy vào clipboard', + 'dav_url_base' => 'URL chung cho tất cả tài nguyên CardDAV và CalDAV:', + 'dav_connect_help' => 'Bạn có thể kết nối danh bạ và/hoặc lịch với url này trên điện thoại hoặc máy tính.', + 'dav_connect_help2' => 'Sử dụng email đăng nhập và tạo khóa API để xác thực.', + 'dav_url_carddav' => 'CardDAV url cho danh sách Danh bạ:', + 'dav_url_caldav_birthdays' => 'CalDAV url cho danh sách Sinh nhật:', + 'dav_url_caldav_tasks' => 'CalDAV url cho danh sách Công việc:', + 'dav_title_carddav' => 'CardDAV', + 'dav_title_caldav' => 'CalDAV', + 'dav_carddav_export' => 'Xuất tất cả danh bạ thành một tệp', + 'dav_caldav_birthdays_export' => 'Xuất tất cả sinh nhật thành một tệp', + 'dav_caldav_tasks_export' => 'Xuất tất cả công việc thành một tệp', + + 'archive_title' => 'Lưu trữ tát cả danh bạ trong tài khoản của bạn', + 'archive_desc' => 'Hành động này sẽ lưu trữ tất cả liên hệ trong tài khoản của bạn.', + 'archive_cta' => 'Lưu trữ tất cả liên hệ', + + 'logs_title' => 'Mọi thứ đã xảy ra với tài khoản này', + 'logs_actor' => 'Người thao tác', + 'logs_timestamp' => 'Mốc thời gian', + 'logs_description' => 'Mô tả', + 'logs_subject' => 'Tiêu đề', + 'logs_size' => 'Kích thước (Kb)', + 'logs_object' => 'Đối tượng', +]; diff --git a/resources/lang/vi/validation.php b/resources/lang/vi/validation.php new file mode 100644 index 0000000..a3ad77c --- /dev/null +++ b/resources/lang/vi/validation.php @@ -0,0 +1,166 @@ + ':attribute phải được chấp nhận.', + 'active_url' => ':attribute không phải một URL hợp lệ.', + 'after' => ':attribute phải là một ngày sau :date.', + 'after_or_equal' => ':attribute phải là ngày :date hoặc sau đó.', + 'alpha' => ':attribute chỉ được chứa chữ cái.', + 'alpha_dash' => ':attribute chỉ được chứa chữ cái, chữ số, gạch nối và gạch dưới.', + 'alpha_num' => ':attribute chỉ có thể chứa các chữ cái và số.', + 'array' => ':attribute phải là một mảng.', + 'before' => ':attribute phải là một ngày trước :date.', + 'before_or_equal' => ':attribute phải là ngày :date hoặc trước đó.', + 'between' => [ + 'numeric' => ':attribute phải nằm giữa :min - :max.', + 'file' => ':attribute phải nằm trong khoảng :min đến :max KB.', + 'string' => ':attribute phải trong khoảng :min đến :max ký tự.', + 'array' => ':attribute phải nằm trong khoảng :min đến :max mục.', + ], + 'boolean' => ':attribute phải là true hoặc false.', + 'confirmed' => ':attribute xác nhận không đúng.', + 'date' => ':attribute không phải là ngày hợp lệ.', + 'date_equals' => ':attribute phải là ngày trùng với :date.', + 'date_format' => ':attribute không khớp với định dạng :format.', + 'different' => ':attribute và :other phải khác nhau.', + 'digits' => ':attribute phải có :digits số.', + 'digits_between' => ':attribute phải nằm giữa :min và :max chữ số.', + 'dimensions' => ':attribute có kích thước hình ảnh không hợp lệ.', + 'distinct' => 'Trường :attribute có giá trị trùng lặp.', + 'email' => ':attribute phải là địa chỉ email hợp lệ.', + 'ends_with' => ':attribute phải kết thúc bằng một trong các ký tự: :values.', + 'exists' => ':attribute được chọn không hợp lệ.', + 'file' => ':attribute phải là một tệp.', + 'filled' => 'Trường :attribute phải có giá trị.', + 'gt' => [ + 'numeric' => ':attribute phải lớn hơn :value.', + 'file' => ':attribute phải lớn hơn :value KB.', + 'string' => ':attribute phải có nhiều hơn :value ký tự.', + 'array' => ':attribute phải có nhiều hơn :value mục.', + ], + 'gte' => [ + 'numeric' => ':attribute phải lớn hơn hoặc bằng :value.', + 'file' => ':attribute phải lớn hơn hoặc bằng :value KB.', + 'string' => ':attribute phải có nhiều hơn hoặc bằng :value ký tự.', + 'array' => ':attribute phải có :value mục trở lên.', + ], + 'image' => ':attribute phải là một file ảnh.', + 'in' => ':attribute được chọn không hợp lệ.', + 'in_array' => 'Thuộc tính :attribute không tồn tại trong :other.', + 'integer' => ':attribute phải là một số nguyên.', + 'ip' => ':attribute phải là một địa chỉ IP hợp lệ.', + 'ipv4' => 'Thuộc tính: phải là địa chỉ IPv4 hợp lệ.', + 'ipv6' => ':attribute phải là địa chỉ IPv6 hợp lệ.', + 'json' => ':attribute phải là một chuỗi JSON hợp lệ.', + 'lt' => [ + 'numeric' => ':attribute phải nhỏ hơn :value.', + 'file' => ':attribute phải nhỏ hơn :value KB.', + 'string' => ':attribute phải ít hơn :value ký tự.', + 'array' => ':attribute phải có ít hơn :value mục.', + ], + 'lte' => [ + 'numeric' => ':attribute phải nhỏ hơn hoặc bằng :value.', + 'file' => ':attribute phải nhỏ hơn hoặc bằng :value KB.', + 'string' => ':attribute phải ít hơn hoặc bằng :value ký tự.', + 'array' => ':attribute không được có nhiều hơn :value mục.', + ], + 'max' => [ + 'numeric' => ':attribute có thể không lớn hơn :max.', + 'file' => ':attribute không được lớn hơn :max KB.', + 'string' => ':attribute không được nhiều hơn :max ký tự.', + 'array' => ':attribute không thể có nhiều hơn :max mục.', + ], + 'mimes' => ':attribute phải là một tập tin có phần mở rộng là: :values.', + 'mimetypes' => ':attribute phải là một tập tin có phần mở rộng là: :values.', + 'min' => [ + 'numeric' => ':attribute phải có ít nhất :min.', + 'file' => ':attribute tối thiểu phải nặng :min KB.', + 'string' => ':attribute phải có ít nhất :min ký tự.', + 'array' => ':attribute phải chọn ít nhất :min mục.', + ], + 'not_in' => ':attribute đã chọn không hợp lệ.', + 'not_regex' => 'Định dạng :attribute không hợp lệ.', + 'numeric' => ':attribute phải là số.', + 'password' => 'Mật khẩu không đúng.', + 'present' => 'Trường :attribute phải được cung cấp.', + 'regex' => 'Định dạng :attribute không hợp lệ.', + 'required' => 'Trường :attribute không được bỏ trống.', + 'required_if' => 'Trường :attribute là bắt buộc khi :other là :value.', + 'required_unless' => 'Trường :attribute không được bỏ trống trừ khi :other là :values.', + 'required_with' => 'Trường :attribute là bắt buộc khi :values có giá trị.', + 'required_with_all' => 'Trường :attribute là bắt buộc khi :values có giá trị.', + 'required_without' => 'Trường :attribute là bắt buộc khi :values không có giá trị.', + 'required_without_all' => 'Trường :attribute là bắt buộc khi không có :values nào có giá trị.', + 'same' => ':attribute và :other phải giống nhau.', + 'size' => [ + 'numeric' => ':attribute phải là :size.', + 'file' => ':attribute phải có cỡ :size KB.', + 'string' => ':attribute phải có :size ký tự.', + 'array' => ':attribute phải chứa :size mục.', + ], + 'starts_with' => ':attribute phải bắt đầu bằng một trong các ký tự: :values.', + 'string' => ':attribute phải là một chuỗi.', + 'timezone' => ':attribute phải là một khu vực hợp lệ.', + 'unique' => ':attribute đã được dùng.', + 'uploaded' => ':attribute bị lỗi trong quá trình tải lên.', + 'url' => 'Định dạng :attribute không hợp lệ.', + 'uuid' => ':attribute phải là UUID hợp lệ.', + + /* + |-------------------------------------------------------------------------- + | Custom Validation Language Lines + |-------------------------------------------------------------------------- + | + | Here you may specify custom validation messages for attributes using the + | convention "attribute.rule" to name the lines. This makes it quick to + | specify a specific custom language line for a given attribute rule. + | + */ + + 'custom' => [ + 'attribute-name' => [ + 'rule-name' => 'custom-message', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Custom Validation Attributes + |-------------------------------------------------------------------------- + | + | The following language lines are used to swap our attribute placeholder + | with something more reader friendly such as "E-Mail Address" instead + | of "email". This simply helps us make our message more expressive. + | + */ + + 'attributes' => [], + + 'vue' => [ + 'max' => [ + 'numeric' => '{field} không được lớn hơn {max}.', + 'string' => '{field} không được lớn hơn {max} kí tự.', + ], + 'required' => '{field} là bắt buộc.', + 'url' => '{field} không phải là URL hợp lệ.', + ], + +]; diff --git a/resources/lang/zh-TW.json b/resources/lang/zh-TW.json new file mode 100644 index 0000000..ddea72e --- /dev/null +++ b/resources/lang/zh-TW.json @@ -0,0 +1,7 @@ +{ + "The :attribute must contain at least one uppercase and one lowercase letter.": "The :attribute must contain at least one uppercase and one lowercase letter.", + "The :attribute must contain at least one letter.": "The :attribute must contain at least one letter.", + "The :attribute must contain at least one symbol.": "The :attribute must contain at least one symbol.", + "The :attribute must contain at least one number.": "The :attribute must contain at least one number.", + "The given :attribute has appeared in a data leak. Please choose a different :attribute.": "The given :attribute has appeared in a data leak. Please choose a different :attribute." +} diff --git a/resources/lang/zh-TW/app.php b/resources/lang/zh-TW/app.php new file mode 100644 index 0000000..604071e --- /dev/null +++ b/resources/lang/zh-TW/app.php @@ -0,0 +1,571 @@ + '是', + 'no' => '否', + 'update' => '更新', + 'save' => '儲存', + 'add' => '新增', + 'cancel' => '取消', + 'confirm' => '確認', + 'delete_confirm' => 'Are you sure?', + 'delete' => '刪除', + 'edit' => '編輯', + 'upload' => '上傳', + 'download' => '下載', + 'save_close' => '儲存並關閉', + 'close' => '關閉', + 'copy' => '複製', + 'create' => '建立', + 'remove' => '刪除', + 'revoke' => '撤銷', + 'done' => '完成', + 'back' => '返回', + 'verify' => '驗證', + 'new' => '新', + 'unknown' => '我不知道', + 'load_more' => '載入更多', + 'loading' => 'Loading…', + 'with' => '與', + 'today' => '今天', + 'yesterday' => '昨天', + 'another_day' => '某一天', + 'date' => '日期', + 'type' => '型別', + 'zoom' => '放大', + 'upgrade' => '升級解鎖', + 'percent_uploaded' => '已上傳 {percent}%', + 'retry' => '重試', + 'filter' => '過濾列表', + 'go_back' => '後退', + 'file_selected' => 'One file selected…|{count} files selected…', + + 'application_title' => 'Monica – 您的私人社交關係管家', + 'application_description' => 'Monica是用來收集並管理您與親朋好友之間的關係的得力助手。', + 'application_og_title' => 'Have better relations with your loved ones. Free online CRM for friends and family.', + + 'markdown_description' => '想用一種美觀的方式格式化文字嗎?我們以Markdown語法支援粗體、斜體、列表等樣式。', + 'markdown_link' => '閱讀文件', + + 'header_settings_link' => '設定', + 'header_logout_link' => '登出', + 'header_changelog_link' => '更新日誌', + + 'main_nav_cta' => '聯絡人', + 'main_nav_dashboard' => '儀表盤', + 'main_nav_family' => '聯絡人', + 'main_nav_journal' => '日記', + 'main_nav_activities' => '活動', + 'main_nav_tasks' => '任務', + + 'footer_remarks' => 'Comments?', + 'footer_send_email' => 'Send us an email', + 'footer_privacy' => '隱私條款', + 'footer_release' => '版本說明', + 'footer_newsletter' => '新聞簡報', + 'footer_source_code' => '捐助', + 'footer_version' => '版本::version', + 'footer_new_version' => 'A new version of Monica is available', + + 'footer_modal_version_whats_new' => '新增內容', + 'footer_modal_version_release_away' => '您有一個最新發布版本可更新。您應該更新例項. |您已經有:number個版本沒有更新,應該更新了。', + + 'breadcrumb_dashboard' => '儀表盤', + 'breadcrumb_list_contacts' => '聯絡人', + 'breadcrumb_archived_contacts' => '存檔的聯絡人', + 'breadcrumb_journal' => '日記', + 'breadcrumb_settings' => '設定', + 'breadcrumb_settings_export' => '匯出', + 'breadcrumb_settings_users' => '使用者', + 'breadcrumb_settings_users_add' => '新增使用者', + 'breadcrumb_settings_subscriptions' => '訂閱', + 'breadcrumb_settings_import' => '匯入', + 'breadcrumb_settings_import_report' => '匯入報表', + 'breadcrumb_settings_import_upload' => '上傳', + 'breadcrumb_settings_tags' => '標籤', + 'breadcrumb_add_significant_other' => '新增其他重要', + 'breadcrumb_edit_significant_other' => '編輯其他重要', + 'breadcrumb_add_note' => '添加註釋', + 'breadcrumb_edit_note' => '編輯註釋', + 'breadcrumb_api' => 'API', + 'breadcrumb_dav' => 'DAV 資源', + 'breadcrumb_edit_introductions' => '你是怎麼知道的', + 'breadcrumb_settings_personalization' => '個性化', + 'breadcrumb_settings_security' => '安全', + 'breadcrumb_settings_security_2fa' => '二次驗證', + 'breadcrumb_profile' => ':name的資料', + + 'gender_male' => '男', + 'gender_female' => '女', + 'gender_none' => '保密', + 'gender_no_gender' => '無性別', + + 'error_title' => '糟糕! 出錯了。', + 'error_unauthorized' => '你沒有許可權編輯此頁', + 'error_user_account' => '此使用者不屬於此帳號', + 'error_save' => '當儲存資料時出現了一個錯誤', + 'error_try_again' => '出了點問題,請再試一次。', + 'error_id' => '錯誤程式碼::id', + 'error_unavailable' => '服務不可用', + 'error_maintenance' => 'Maintenance in progress. We’ll be right back.', + 'error_help' => '待會見!', + 'error_twitter' => '關注我們的推特來得知網站的最新訊息!', + 'error_no_term' => '此例項尚無策略', + + 'default_save_success' => '資料已被儲存', + + 'compliance_title' => '抱歉,打擾您一下', + 'compliance_desc' => '我們更新了使用者協議 以及 隱私政策,您需要閱讀並同意才能繼續使用您的帳號。', + 'compliance_desc_end' => '我們會保護您的隱私安全', + 'compliance_terms' => '我已閱讀並同意', + + // Relationship types + // Yes, each relationship type has 8 strings associated with it. + // This is because we need to indicate the name of the relationship type, + // and also the name of the opposite side of this relationship (father/son), + // and then, the feminine version of the string. Finally, in some sentences + // in the UI, we need to include the name of the person we add the relationship + // to. + 'relationship_type_group_love' => '戀愛關係', + 'relationship_type_group_family' => '家庭關係', + 'relationship_type_group_friend' => '朋友關係', + 'relationship_type_group_work' => '工作關係', + 'relationship_type_group_other' => '其他關係', + + 'relationship_type_partner' => '搭檔', + 'relationship_type_partner_female' => '搭檔', + 'relationship_type_partner_male' => 'significant other', + 'relationship_type_partner_with_name' => ':name的搭檔', + 'relationship_type_partner_female_with_name' => ':name的搭檔', + 'relationship_type_partner_male_with_name' => ':name’s significant other', + + 'relationship_type_spouse' => '配偶', + 'relationship_type_spouse_female' => 'wife', + 'relationship_type_spouse_male' => 'husband', + 'relationship_type_spouse_with_name' => ':name的配偶', + 'relationship_type_spouse_female_with_name' => ':name’s wife', + 'relationship_type_spouse_male_with_name' => ':name’s husband', + + 'relationship_type_date' => '約會物件', + 'relationship_type_date_female' => '約會物件', + 'relationship_type_date_male' => 'date', + 'relationship_type_date_with_name' => ':name的約會物件', + 'relationship_type_date_female_with_name' => ':name的約會物件', + 'relationship_type_date_male_with_name' => ':name’s date', + + 'relationship_type_lover' => '情人', + 'relationship_type_lover_female' => '情人', + 'relationship_type_lover_male' => 'lover', + 'relationship_type_lover_with_name' => ':name的情人', + 'relationship_type_lover_female_with_name' => ':name的情人', + 'relationship_type_lover_male_with_name' => ':name’s lover', + + 'relationship_type_inlovewith' => '喜歡的人', + 'relationship_type_inlovewith_female' => '喜歡的人', + 'relationship_type_inlovewith_male' => 'in love with', + 'relationship_type_inlovewith_with_name' => ':name喜歡的人', + 'relationship_type_inlovewith_female_with_name' => ':name喜歡的人', + 'relationship_type_inlovewith_male_with_name' => 'someone :name is in love with', + + 'relationship_type_lovedby' => '追求者', + 'relationship_type_lovedby_female' => '追求者', + 'relationship_type_lovedby_male' => 'loved by', + 'relationship_type_lovedby_with_name' => ':name的追求者', + 'relationship_type_lovedby_female_with_name' => ':name的追求者', + 'relationship_type_lovedby_male_with_name' => ':name’s secret lover', + + 'relationship_type_ex' => 'ex-partner', + 'relationship_type_ex_female' => '前女友', + 'relationship_type_ex_male' => 'ex-boyfriend', + 'relationship_type_ex_with_name' => ':name’s ex-partner', + 'relationship_type_ex_female_with_name' => ':name的前女友', + 'relationship_type_ex_male_with_name' => ':name’s ex-boyfriend', + + 'relationship_type_parent' => 'parent', + 'relationship_type_parent_female' => '母親', + 'relationship_type_parent_male' => 'father', + 'relationship_type_parent_with_name' => ':name’s parent', + 'relationship_type_parent_female_with_name' => ':name的母親', + 'relationship_type_parent_male_with_name' => ':name’s father', + + 'relationship_type_child' => 'child', + 'relationship_type_child_female' => '女兒', + 'relationship_type_child_male' => 'son', + 'relationship_type_child_with_name' => ':name’s child', + 'relationship_type_child_female_with_name' => ':name的女人', + 'relationship_type_child_male_with_name' => ':name’s son', + + 'relationship_type_stepparent' => 'step-parent', + 'relationship_type_stepparent_female' => '繼母', + 'relationship_type_stepparent_male' => 'stepfather', + 'relationship_type_stepparent_with_name' => ':name’s step-parent', + 'relationship_type_stepparent_female_with_name' => ':name的繼母', + 'relationship_type_stepparent_male_with_name' => ':name’s stepfather', + + 'relationship_type_stepchild' => 'stepchild', + 'relationship_type_stepchild_female' => '繼女', + 'relationship_type_stepchild_male' => 'stepson', + 'relationship_type_stepchild_with_name' => ':name’s stepchild', + 'relationship_type_stepchild_female_with_name' => ':name的繼女', + 'relationship_type_stepchild_male_with_name' => ':name’s stepson', + + 'relationship_type_sibling' => 'sibling', + 'relationship_type_sibling_female' => '姐妹', + 'relationship_type_sibling_male' => 'brother', + 'relationship_type_sibling_with_name' => ':name’s sibling', + 'relationship_type_sibling_female_with_name' => ':name的姐妹', + 'relationship_type_sibling_male_with_name' => ':name’s brother', + + 'relationship_type_grandparent' => 'grandparent', + 'relationship_type_grandparent_female' => 'grandmother', + 'relationship_type_grandparent_male' => 'grandfather', + 'relationship_type_grandparent_with_name' => ':name’s grandparent', + 'relationship_type_grandparent_female_with_name' => ':name’s grandmother', + 'relationship_type_grandparent_male_with_name' => ':name’s grandfather', + + 'relationship_type_grandchild' => 'grandchild', + 'relationship_type_grandchild_female' => 'granddaughter', + 'relationship_type_grandchild_male' => 'grandson', + 'relationship_type_grandchild_with_name' => ':name’s grandchild', + 'relationship_type_grandchild_female_with_name' => ':name’s granddauther', + 'relationship_type_grandchild_male_with_name' => ':name’s grandson', + + 'relationship_type_uncle' => '叔叔', + 'relationship_type_uncle_female' => '阿姨', + 'relationship_type_uncle_male' => 'uncle', + 'relationship_type_uncle_with_name' => ':name的叔叔', + 'relationship_type_uncle_female_with_name' => ':name的阿姨', + 'relationship_type_uncle_male_with_name' => ':name’s uncle', + + 'relationship_type_nephew' => '外甥', + 'relationship_type_nephew_female' => '外甥女', + 'relationship_type_nephew_male' => 'nephew', + 'relationship_type_nephew_with_name' => ':name的外甥', + 'relationship_type_nephew_female_with_name' => ':name的外甥女', + 'relationship_type_nephew_male_with_name' => ':name’s nephew', + + 'relationship_type_cousin' => '堂兄弟', + 'relationship_type_cousin_female' => '堂姐妹', + 'relationship_type_cousin_male' => 'cousin', + 'relationship_type_cousin_with_name' => ':name的堂兄弟', + 'relationship_type_cousin_female_with_name' => ':name的堂姐妹', + 'relationship_type_cousin_male_with_name' => ':name’s cousin', + + 'relationship_type_godfather' => 'godparent', + 'relationship_type_godfather_female' => '神母', + 'relationship_type_godfather_male' => 'godfather', + 'relationship_type_godfather_with_name' => ':name’s godparent', + 'relationship_type_godfather_female_with_name' => ':name的神母', + 'relationship_type_godfather_male_with_name' => ':name’s godfather', + + 'relationship_type_godson' => 'godchild', + 'relationship_type_godson_female' => '義女', + 'relationship_type_godson_male' => 'godson', + 'relationship_type_godson_with_name' => ':name’s godchild', + 'relationship_type_godson_female_with_name' => ':name的義女', + 'relationship_type_godson_male_with_name' => ':name’s godson', + + 'relationship_type_friend' => '朋友', + 'relationship_type_friend_female' => '朋友', + 'relationship_type_friend_male' => 'friend', + 'relationship_type_friend_with_name' => ':name的朋友', + 'relationship_type_friend_female_with_name' => ':name的朋友', + 'relationship_type_friend_male_with_name' => ':name’s friend', + + 'relationship_type_bestfriend' => '基友', + 'relationship_type_bestfriend_female' => '閨密', + 'relationship_type_bestfriend_male' => 'best friend', + 'relationship_type_bestfriend_with_name' => ':name的基友', + 'relationship_type_bestfriend_female_with_name' => ':name的閨密', + 'relationship_type_bestfriend_male_with_name' => ':name’s best friend', + + 'relationship_type_colleague' => '同事', + 'relationship_type_colleague_female' => '同事', + 'relationship_type_colleague_male' => 'colleague', + 'relationship_type_colleague_with_name' => ':name的同事', + 'relationship_type_colleague_female_with_name' => ':name的同事', + 'relationship_type_colleague_male_with_name' => ':name’s colleague', + + 'relationship_type_boss' => '上司', + 'relationship_type_boss_female' => '上司', + 'relationship_type_boss_male' => 'boss', + 'relationship_type_boss_with_name' => ':name的上司', + 'relationship_type_boss_female_with_name' => ':name的上司', + 'relationship_type_boss_male_with_name' => ':name’s boss', + + 'relationship_type_subordinate' => '下屬', + 'relationship_type_subordinate_female' => '下屬', + 'relationship_type_subordinate_male' => 'subordinate', + 'relationship_type_subordinate_with_name' => ':name的下屬', + 'relationship_type_subordinate_female_with_name' => ':name的下屬', + 'relationship_type_subordinate_male_with_name' => ':name’s subordinate', + + 'relationship_type_mentor' => '老師', + 'relationship_type_mentor_female' => '老師', + 'relationship_type_mentor_male' => 'mentor', + 'relationship_type_mentor_with_name' => ':name的老師', + 'relationship_type_mentor_female_with_name' => ':name的老師', + 'relationship_type_mentor_male_with_name' => ':name’s mentor', + + 'relationship_type_protege' => 'protégé', + 'relationship_type_protege_female' => 'protégé', + 'relationship_type_protege_male' => 'protégé', + 'relationship_type_protege_with_name' => ':name’s protégé', + 'relationship_type_protege_female_with_name' => ':name’s protégé', + 'relationship_type_protege_male_with_name' => ':name’s protégé', + + 'relationship_type_ex_husband' => 'ex-spouse', + 'relationship_type_ex_husband_female' => '前妻', + 'relationship_type_ex_husband_male' => 'ex-husband', + 'relationship_type_ex_husband_with_name' => ':name’s ex-spouse', + 'relationship_type_ex_husband_female_with_name' => ':name的前妻', + 'relationship_type_ex_husband_male_with_name' => ':name’s ex-husband', + + // emotions + 'emotion_primary_love' => '喜愛', + 'emotion_primary_joy' => '開心', + 'emotion_primary_surprise' => '驚訝', + 'emotion_primary_anger' => '生氣', + 'emotion_primary_sadness' => '悲傷', + 'emotion_primary_fear' => '恐懼', + + 'emotion_secondary_affection' => '感情', + 'emotion_secondary_lust' => '慾望', + 'emotion_secondary_longing' => '渴望', + 'emotion_secondary_cheerfulness' => '興高采烈', + 'emotion_secondary_zest' => '熱情', + 'emotion_secondary_contentment' => '滿足', + 'emotion_secondary_pride' => '驕傲', + 'emotion_secondary_optimism' => '樂觀', + 'emotion_secondary_enthrallment' => '沉迷', + 'emotion_secondary_relief' => '如釋重負', + 'emotion_secondary_surprise' => '驚訝', + 'emotion_secondary_irritation' => '刺激', + 'emotion_secondary_exasperation' => '惱怒', + 'emotion_secondary_rage' => '狂怒', + 'emotion_secondary_disgust' => '厭惡', + 'emotion_secondary_envy' => '嫉妒', + 'emotion_secondary_suffering' => '痛苦', + 'emotion_secondary_sadness' => '悲傷', + 'emotion_secondary_disappointment' => '失望', + 'emotion_secondary_shame' => '恥辱', + 'emotion_secondary_neglect' => '忽視', + 'emotion_secondary_sympathy' => '同情', + 'emotion_secondary_horror' => '恐怖', + 'emotion_secondary_nervousness' => '緊張', + + 'emotion_adoration' => '崇拜', + 'emotion_affection' => '感情', + 'emotion_love' => '喜愛', + 'emotion_fondness' => '寵愛', + 'emotion_liking' => '喜歡', + 'emotion_attraction' => '吸引', + 'emotion_caring' => '關心', + 'emotion_tenderness' => '柔情', + 'emotion_compassion' => '同情', + 'emotion_sentimentality' => '多愁善感', + 'emotion_arousal' => '激勵', + 'emotion_desire' => '期望', + 'emotion_lust' => '慾望', + 'emotion_passion' => '熱情', + 'emotion_infatuation' => '迷戀', + 'emotion_longing' => '渴望', + 'emotion_amusement' => '娛樂', + 'emotion_bliss' => '欣喜若狂', + 'emotion_cheerfulness' => '興高采烈', + 'emotion_gaiety' => '歡樂', + 'emotion_glee' => '高興', + 'emotion_jolliness' => '喬利', + 'emotion_joviality' => '快樂', + 'emotion_joy' => '開心', + 'emotion_delight' => '喜悅', + 'emotion_enjoyment' => '享受', + 'emotion_gladness' => '喜悅', + 'emotion_happiness' => '快樂', + 'emotion_jubilation' => '喜慶', + 'emotion_elation' => '興高采烈', + 'emotion_satisfaction' => '稱心如意', + 'emotion_ecstasy' => '狂喜', + 'emotion_euphoria' => '過度興奮', + 'emotion_enthusiasm' => '熱情高漲', + 'emotion_zeal' => '狂熱', + 'emotion_zest' => '熱情', + 'emotion_excitement' => '興奮', + 'emotion_thrill' => '快感', + 'emotion_exhilaration' => '不亦樂乎', + 'emotion_contentment' => '滿足', + 'emotion_pleasure' => '快樂', + 'emotion_pride' => '驕傲', + 'emotion_eagerness' => '渴望', + 'emotion_hope' => '希望', + 'emotion_optimism' => '樂觀', + 'emotion_enthrallment' => '沉迷', + 'emotion_rapture' => '狂喜', + 'emotion_relief' => '如釋重負', + 'emotion_amazement' => '驚奇', + 'emotion_surprise' => '驚訝', + 'emotion_astonishment' => '驚訝', + 'emotion_aggravation' => '惡化', + 'emotion_irritation' => '刺激', + 'emotion_agitation' => '鼓動', + 'emotion_annoyance' => '煩惱', + 'emotion_grouchiness' => '發牢騷', + 'emotion_grumpiness' => '脾氣暴躁', + 'emotion_exasperation' => '惱怒', + 'emotion_frustration' => '受挫', + 'emotion_anger' => '生氣', + 'emotion_rage' => '狂怒', + 'emotion_outrage' => '憤怒', + 'emotion_fury' => '憤怒', + 'emotion_wrath' => '暴怒', + 'emotion_hostility' => '敵意', + 'emotion_ferocity' => '凶猛', + 'emotion_bitterness' => '辛酸', + 'emotion_hate' => '討厭', + 'emotion_loathing' => '嫌惡', + 'emotion_scorn' => '蔑視', + 'emotion_spite' => '怨恨', + 'emotion_vengefulness' => '報復', + 'emotion_dislike' => '不喜歡', + 'emotion_resentment' => '怨恨', + 'emotion_disgust' => '厭惡', + 'emotion_revulsion' => '反感', + 'emotion_contempt' => '輕蔑', + 'emotion_envy' => '嫉妒', + 'emotion_jealousy' => '嫉妒', + 'emotion_agony' => '痛苦', + 'emotion_suffering' => '痛苦', + 'emotion_hurt' => '傷心', + 'emotion_anguish' => '生不如死', + 'emotion_depression' => '憂鬱', + 'emotion_despair' => '絕望', + 'emotion_hopelessness' => '無可救藥', + 'emotion_gloom' => '沮喪', + 'emotion_glumness' => '陰沉', + 'emotion_sadness' => '悲傷', + 'emotion_unhappiness' => '不幸', + 'emotion_grief' => '悲痛', + 'emotion_sorrow' => '悲患', + 'emotion_woe' => '榮辱與共', + 'emotion_misery' => '痛苦', + 'emotion_melancholy' => '悲傷', + 'emotion_dismay' => '沮喪', + 'emotion_disappointment' => '失望', + 'emotion_displeasure' => '不滿', + 'emotion_guilt' => '內疚', + 'emotion_shame' => '恥辱', + 'emotion_regret' => '後悔', + 'emotion_remorse' => '悔恨', + 'emotion_alienation' => '異化', + 'emotion_isolation' => '分離', + 'emotion_neglect' => '忽視', + 'emotion_loneliness' => '孤獨', + 'emotion_rejection' => '拒絕', + 'emotion_homesickness' => '鄉愁', + 'emotion_defeat' => '失敗', + 'emotion_dejection' => '沮喪', + 'emotion_insecurity' => '緊張', + 'emotion_embarrassment' => '尷尬', + 'emotion_humiliation' => '屈辱', + 'emotion_insult' => '侮辱', + 'emotion_pity' => '可惜', + 'emotion_sympathy' => '同情', + 'emotion_alarm' => '警覺', + 'emotion_shock' => '震撼', + 'emotion_fear' => '恐懼', + 'emotion_fright' => '驚嚇', + 'emotion_horror' => '恐怖', + 'emotion_terror' => '恐怖', + 'emotion_panic' => '恐慌', + 'emotion_hysteria' => '歇斯底里', + 'emotion_mortification' => '屈辱', + 'emotion_anxiety' => '焦慮', + 'emotion_nervousness' => '緊張', + 'emotion_tenseness' => '神經緊繃', + 'emotion_uneasiness' => '不安', + 'emotion_apprehension' => '憂慮', + 'emotion_worry' => '擔心', + 'emotion_distress' => '苦惱', + 'emotion_dread' => '驚恐', + + // weather + 'weather_sunny' => 'Sunny', + 'weather_clear' => 'Clear', + 'weather_clear-day' => 'Clear', + 'weather_clear-night' => '晴朗的夜晚', + 'weather_light-drizzle' => 'Light drizzle', + 'weather_patchy-light-drizzle' => 'Patchy light drizzle', + 'weather_patchy-light-rain' => 'Patchy light rain', + 'weather_light-rain' => 'Light rain', + 'weather_moderate-rain-at-times' => 'Moderate rain at times', + 'weather_moderate-rain' => 'Moderate rain', + 'weather_patchy-rain-possible' => 'Patchy rain possible', + 'weather_heavy-rain-at-times' => 'Heavy rain at times', + 'weather_heavy-rain' => 'Heavy rain', + 'weather_light-freezing-rain' => 'Light freezing rain', + 'weather_moderate-or-heavy-freezing-rain' => 'Moderate or heavy freezing rain', + 'weather_light-sleet' => 'Light sleet', + 'weather_moderate-or-heavy-rain-shower' => 'Moderate or heavy rain shower', + 'weather_light-rain-shower' => 'Light rain shower', + 'weather_torrential-rain-shower' => 'Torrential rain shower', + 'weather_rain' => '雨', + 'weather_snow' => '雪', + 'weather_blowing-snow' => 'Blowing snow', + 'weather_patchy-light-snow' => 'Patchy light snow', + 'weather_light-snow' => 'Light snow', + 'weather_patchy-moderate-snow' => 'Patchy moderate snow', + 'weather_moderate-snow' => 'Moderate snow', + 'weather_patchy-heavy-snow' => 'Patchy heavy snow', + 'weather_heavy-snow' => 'Heavy snow', + 'weather_light-snow-showers' => 'Light snow showers', + 'weather_moderate-or-heavy-snow-showers' => 'Moderate or heavy snow showers', + 'weather_patchy-snow-possible' => 'Patchy snow possible', + 'weather_patchy-sleet-possible' => 'Patchy sleet possible', + 'weather_moderate-or-heavy-sleet' => 'Moderate or heavy sleet', + 'weather_light-sleet-showers' => 'Light sleet showers', + 'weather_moderate-or-heavy-sleet-showers' => 'Moderate or heavy sleet showers', + 'weather_sleet' => '雨夾雪', + 'weather_wind' => '風', + 'weather_fog' => '霧', + 'weather_freezing-fog' => 'Freezing fog', + 'weather_mist' => 'Mist', + 'weather_blizzard' => 'Blizzard', + 'weather_overcast' => 'Overcast', + 'weather_cloudy' => '多雲', + 'weather_partly-cloudy-day' => 'Partly cloudy', + 'weather_partly-cloudy-night' => 'Partly cloudy', + 'weather_freezing-drizzle' => 'Freezing drizzle', + 'weather_heavy-freezing-drizzle' => 'Heavy freezing drizzle', + 'weather_patchy-freezing-drizzle-possible' => 'Patchy freezing drizzle possible', + 'weather_ice-pellets' => 'Ice pellets', + 'weather_light-showers-of-ice-pellets' => 'Light showers of ice pellets', + 'weather_moderate-or-heavy-showers-of-ice-pellets' => 'Moderate or heavy showers of ice pellets', + 'weather_thundery-outbreaks-possible' => 'Thundery outbreaks possible', + 'weather_patchy-light-rain-with-thunder' => 'Patchy light rain with thunder', + 'weather_moderate-or-heavy-rain-with-thunder' => 'Moderate or heavy rain with thunder', + 'weather_patchy-light-snow-with-thunder' => 'Patchy light snow with thunder', + 'weather_moderate-or-heavy-snow-with-thunder' => 'Moderate or heavy snow with thunder', + 'weather_current_temperature_celsius' => ':temperature °C', + 'weather_current_temperature_fahrenheit' => ':temperature °F', + 'weather_current_title' => '當前天氣', + + // dav + 'dav_contacts' => '名片', + 'dav_contacts_description' => ':name的名片', + 'dav_birthdays' => '生日', + 'dav_birthdays_description' => ':name的名片生日', + 'dav_tasks' => '任務', + 'dav_tasks_description' => ':name的任務', + + // contact list + 'contact_list_avatar' => 'Avatar', + 'contact_list_name' => 'Contact', + 'contact_list_description' => 'Description', + +]; diff --git a/resources/lang/zh-TW/auth.php b/resources/lang/zh-TW/auth.php new file mode 100644 index 0000000..b0d4920 --- /dev/null +++ b/resources/lang/zh-TW/auth.php @@ -0,0 +1,89 @@ + '您輸入的資訊與我們的紀錄不匹配。', + 'throttle' => '登入失敗次數太多。請 :seconds 後再試。', + 'not_authorized' => '您無權執行此操作', + 'signup_disabled' => '註冊當前已停用', + 'signup_error' => 'An error occured trying to register the user', + 'back_homepage' => '回到主頁', + 'mfa_auth_otp' => '使用二次驗證裝置進行認證', + 'mfa_auth_webauthn' => '使用安全鑰匙驗證(WebAuthn)', + '2fa_title' => '二次驗證', + '2fa_wrong_validation' => '二次驗證失敗', + '2fa_one_time_password' => '驗證碼', + '2fa_recuperation_code' => '輸入二次驗證恢復碼', + '2fa_one_time_or_recuperation' => 'Enter a two factor authentication code or a recovery code', + '2fa_otp_help' => '開啟您的二次驗證APP並複製驗證碼', + + 'login_to_account' => '登入您的帳號', + 'login_with_recovery' => '使用恢復程式碼登入', + 'login_again' => '請再次登入您的帳號', + 'email' => '電子郵件', + 'password' => '密碼', + 'recovery' => '還原程式碼', + 'login' => '登入', + 'button_remember' => '記住我', + 'password_forget' => '忘記密碼?', + 'password_reset' => '重置密碼', + 'use_recovery' => '或者您可以使用 還原程式碼', + 'signup_no_account' => '沒有帳號?', + 'signup' => '註冊', + 'create_account' => '單擊此處 註冊', + 'change_language_title' => '更改語言:', + 'change_language' => '更改語言至::lang', + + 'password_reset_title' => '重置密碼', + 'password_reset_email' => '電子信箱', + 'password_reset_send_link' => '傳送重置連結', + 'password_reset_password' => '密碼', + 'password_reset_password_confirm' => '確認密碼', + 'password_reset_action' => '重置密碼', + 'password_reset_email_content' => '單擊此處來重置密碼:', + + 'register_title_welcome' => '歡迎註冊您的私人社交關係管家 - Monica', + 'register_create_account' => '您需要一個帳號來使用Monica', + 'register_title_create' => '建立您的Monica帳號', + 'register_login' => '已經有帳號了?點此登入', + 'register_email' => '請輸入一個有效的信箱', + 'register_email_example' => 'example@example.com', + 'register_firstname' => '名字', + 'register_firstname_example' => '例: 小明', + 'register_lastname' => '姓氏', + 'register_lastname_example' => '例: 王', + 'register_password' => '密碼', + 'register_password_example' => '鍵入密碼...', + 'register_password_confirmation' => '重複密碼', + 'register_action' => '註冊', + 'register_policy' => '我已閱讀並同意 隱私政策使用者協議', + 'register_invitation_email' => '為了安全,請您輸入邀請人的電子郵件地址。這可以在受邀郵件中找到', + + 'confirmation_title' => '驗證您的電子郵件地址', + 'confirmation_fresh' => '一條新的驗證連結已經發送到您的信箱', + 'confirmation_check' => '在您繼續之前,請檢查您的信箱以獲得驗證連結。', + 'confirmation_request_another' => '如果您沒有收到電子郵件 , 請點擊此處重新發送。', + + 'confirmation_again' => '如果要更改電子郵件地址, 可以 點擊此處。', + 'email_change_current_email' => '當前郵件地址:', + 'email_change_title' => '更換您的電子郵件', + 'email_change_new' => '新郵件地址:', + 'email_changed' => '您的電子郵件已更換,請檢查您的收件箱來驗證電子郵件地址。', +]; diff --git a/resources/lang/zh-TW/changelog.php b/resources/lang/zh-TW/changelog.php new file mode 100644 index 0000000..7a0b6d6 --- /dev/null +++ b/resources/lang/zh-TW/changelog.php @@ -0,0 +1,12 @@ + '更新日誌', + 'note' => '注:很抱歉,當前頁面只支援英文展示。', +]; diff --git a/resources/lang/zh-TW/dashboard.php b/resources/lang/zh-TW/dashboard.php new file mode 100644 index 0000000..8161aa6 --- /dev/null +++ b/resources/lang/zh-TW/dashboard.php @@ -0,0 +1,42 @@ + '歡迎登入帳號', + 'dashboard_blank_description' => 'Monica 是一個記錄你所有關心的人及與其互動資訊的地方', + 'dashboard_blank_cta' => '新增您的第一個聯絡人', + 'dashboard_blank_illustration' => '插畫: Freepik', + + 'notes_title' => '您還沒有任何便籤。', + + 'tab_recent_calls' => '最近通話', + 'tab_favorite_notes' => '收藏便籤', + 'tab_calls_blank' => '您還沒有電話撥打記錄。', + 'tab_debts' => '債務', + 'tab_debts_blank' => '您還沒有新增債務資訊。', + 'tab_tasks' => '任務', + 'tab_tasks_blank' => '你還沒有任何任務', + + 'tasks_add_task_placeholder' => '這個任務是關於什麼的?', + 'tasks_tab_your_contacts' => '與任務相關的聯絡人', + 'tasks_tab_your_tasks' => '您的任務', + 'tasks_add_note' => '按回車來新增任務', + 'task_add_cta' => '新增任務', + + 'debts_you_owe' => '待還金額', + + 'statistics_contacts' => '聯絡人', + 'statistics_activities' => '活動', + 'statistics_gifts' => '禮物', + + 'reminders_next_months' => '近三個月的活動', + 'reminders_none' => '本月尚無提醒事項.', + + 'product_changes' => '更新日誌', + 'product_view_details' => '檢視詳情', +]; diff --git a/resources/lang/zh-TW/format.php b/resources/lang/zh-TW/format.php new file mode 100644 index 0000000..5cc1a35 --- /dev/null +++ b/resources/lang/zh-TW/format.php @@ -0,0 +1,36 @@ + 'Y M d H:i', + 'short_date_year' => 'Y M d', + 'short_date' => 'M d', + 'short_month' => 'M', + 'short_month_year' => 'Y M', + 'short_day' => 'D', + 'full_date_year' => 'Y F d', + 'full_month' => 'F', + 'full_month_year' => 'Y F', + 'full_hour' => 'h.i A', + + 'short_text' => '{text}…', +]; diff --git a/resources/lang/zh-TW/journal.php b/resources/lang/zh-TW/journal.php new file mode 100644 index 0000000..e765797 --- /dev/null +++ b/resources/lang/zh-TW/journal.php @@ -0,0 +1,38 @@ + '今天過得怎麼樣?你可以每天給它一次評價。', + 'journal_come_back' => '謝謝. 明天再來給你的一天評價一下。', + 'journal_description' => '注意: 記錄裡列出了全部手動記錄的條目, 以及您與您的聯絡人進行的活動等自動條目。雖然可以手動刪除記錄條目, 但必須直接在 "聯絡人" 頁上進行刪除。', + 'journal_add' => '新增日記條目', + 'journal_edit' => '編輯日記條目', + 'journal_empty' => '暫無日記', + 'journal_created_at' => 'Created at {date}', + 'journal_created_automatically' => '自動建立', + 'journal_entry_type_journal' => '記錄條目', + 'journal_entry_type_activity' => '活動', + 'journal_entry_rate' => '評價你的一天。', + 'journal_add_comment' => '是否要添加註釋 (可選)?', + 'journal_show_comment' => '顯示評論', + 'entry_delete_success' => '記錄條目已成功刪除。', + 'journal_add_title' => '標題 (可選)', + 'journal_add_date' => '日期', + 'journal_add_post' => '內容', + 'journal_add_cta' => '儲存', + 'journal_blank_cta' => '新增您的第一個記錄條目', + 'journal_blank_description' => '記錄允許您編寫發生在您身上的事件, 並記住它們。', + 'delete_confirmation' => '您確定要刪除此條目嗎?', + 'apply_filter' => 'Apply filter', + 'start_date' => 'Start Date', + 'end_date' => 'End Date', + 'per_page' => 'Per Page', + 'sort_order' => 'Sort By Created At', + 'ascending' => 'Ascending', + 'descending' => 'Descending', +]; diff --git a/resources/lang/zh-TW/logs.php b/resources/lang/zh-TW/logs.php new file mode 100644 index 0000000..3b32c42 --- /dev/null +++ b/resources/lang/zh-TW/logs.php @@ -0,0 +1,29 @@ + '已建立聯絡人', + 'settings_log_contact_created_with_name' => '新增 :name 為聯絡人', + + // contat description update + 'contact_log_contact_description_updated' => '已更新描述', + 'settings_log_contact_description_updated_with_name' => '更新了 :name 的描述', + + // contact description clear + 'contact_log_contact_description_cleared' => '已清除描述', + 'settings_log_contact_description_cleared_with_name' => '已清除 :name 的描述', + + // contact work information update + 'contact_log_contact_work_updated' => '更新工作資訊.', + 'settings_log_contact_work_updated_with_name' => '更新了 :name 的工作資訊', + + // company created + 'settings_log_company_created' => '建立了一個名為 :name 的公司', +]; diff --git a/resources/lang/zh-TW/mail.php b/resources/lang/zh-TW/mail.php new file mode 100644 index 0000000..482d2ca --- /dev/null +++ b/resources/lang/zh-TW/mail.php @@ -0,0 +1,53 @@ + '提醒:contact', + 'greetings' => '您好:username', + 'want_reminded_of' => '您的提醒事項::reason', + 'for' => '為::name', + 'comment' => '備註::comment', + 'footer_contact_info' => '新增、檢視、完成和更改有關此聯絡人的資訊:', + 'footer_contact_info2' => '看看 :name的個人資料', + 'footer_contact_info2_link' => '看看:name的個人資料: :url', + + 'notification_subject_line' => '您有一個即將進行的活動', + 'notification_description' => '在:count天後(:date),將有以下事件發生:', + + 'stay_in_touch_subject_line' => '您的『常聯絡』提醒 :name', + 'stay_in_touch_subject_description' => '您的常聯絡提醒: 每 :frequency 天 與 :name 聯絡.', + + 'notifications_whoops' => '糟了!', + 'notifications_hello' => '您好!', + 'notifications_regards' => '此致', + 'notifications_footer' => '如果您無法點選 ":actionText" 按鈕, 複製以下連結至瀏覽器開啟: [:actionURL](:actionURL)', + 'notifications_rights' => '版權所有', + + 'confirmation_email_title' => 'Monica – Email 認證', + 'confirmation_email_intro'=> '請點選以下按鈕來完成Email認證', + 'confirmation_email_button' => 'Email 認證', + 'confirmation_email_bottom' => '如果不是您本人進行的建立帳戶操作,請忽略這封郵件。', + + 'password_reset_title' => 'Monica — 重置密碼通知', + 'password_reset_intro' => '您收到此郵件是因為我們收到了您的密碼重置請求', + 'password_reset_button' => '重置密碼', + 'password_reset_expiration' => '此密碼重置連結將在 :count 分鐘後過期', + 'password_reset_bottom' => '如果您沒有請求重置密碼,請忽略這封郵件。', + + 'invitation_title' => 'Monica — 您收到 :name 的邀請', + 'invitation_intro' => '您已被:name (:email)邀請使用 Monica, 個人社交關係管理工具。', + 'invitation_link' => '要接受邀請,請點選下面的連結:', + 'invitation_button' => '接受邀請', + 'invitation_expiration' => '此連結將在 :count 天後過期', + + 'export_title' => 'Your export is ready', + 'export_description' => 'You requested a data export on :date. It is now ready to download.', + 'export_download' => 'Download export', + +]; diff --git a/resources/lang/zh-TW/pagination.php b/resources/lang/zh-TW/pagination.php new file mode 100644 index 0000000..dab2273 --- /dev/null +++ b/resources/lang/zh-TW/pagination.php @@ -0,0 +1,25 @@ + '❮ 上一頁', + 'next' => '下一頁 ❯', + +]; diff --git a/resources/lang/zh-TW/passwords.php b/resources/lang/zh-TW/passwords.php new file mode 100644 index 0000000..67df047 --- /dev/null +++ b/resources/lang/zh-TW/passwords.php @@ -0,0 +1,30 @@ + '您的密碼已重置!', + 'sent' => '如果您輸入的電子郵件存在於我們的紀錄中, 密碼重置連結將被發送至改信箱。', + 'token' => '密碼重置祕鑰無效。', + 'user' => '如果您輸入的電子郵件存在於我們的紀錄中, 密碼重置連結將被發送至該信箱。', + 'changed' => '密碼修改成功', + 'invalid' => '您輸入的密碼不正確。', + 'throttled' => '請稍候再試', + +]; diff --git a/resources/lang/zh-TW/people.php b/resources/lang/zh-TW/people.php new file mode 100644 index 0000000..c36ab0a --- /dev/null +++ b/resources/lang/zh-TW/people.php @@ -0,0 +1,539 @@ + '聯絡人未找到', + 'people_list_number_kids' => ':count child|:count children', + 'people_list_last_updated' => '最近更新:', + 'people_list_number_reminders' => ':count reminder|:count reminders', + 'people_list_blank_title' => '您還沒有任何聯絡人', + 'people_list_blank_cta' => '新增某人', + 'people_list_sort' => '排序', + 'people_list_stats' => ':count contact|:count contacts', + 'people_list_firstnameAZ' => '以名字A → Z排序', + 'people_list_firstnameZA' => '以名字 Z → A排序', + 'people_list_lastnameAZ' => '以姓A → Z排序', + 'people_list_lastnameZA' => '以姓Z → A排序', + 'people_list_lastactivitydateNewtoOld' => '依最後活動日期,由近到遠排序', + 'people_list_lastactivitydateOldtoNew' => '依最後活動日期,由遠到近排序', + 'people_list_filter_tag' => '擁有以下標籤的聯絡人:', + 'people_list_clear_filter' => '清除篩選', + 'people_list_contacts_per_tags' => ':{count} 個聯絡人|{count} 個聯絡人', + 'people_list_show_dead' => '顯示已故人員 (:count)', + 'people_list_hide_dead' => '隱藏已故人員 (:count)', + 'people_search' => '搜尋聯絡人', + 'people_search_no_results' => '未找到任何結果', + 'people_search_next' => '下一頁', + 'people_search_prev' => '上一頁', + 'people_search_rows_per_page' => '每頁行數', + 'people_search_of' => '/', + 'people_search_page' => '頁', + 'people_search_all' => '所有', + 'people_add_new' => '新增新的聯絡人', + 'people_list_account_usage' => '您的賬戶已聯絡人使用情況是::current/:limit ', + 'people_list_account_upgrade_title' => '升級您的帳戶, 以開啟全部功能。', + 'people_list_account_upgrade_cta' => '立即升級', + 'people_list_untagged' => '檢視未加標籤的聯絡人', + 'people_list_filter_untag' => '所有未加標籤的聯絡人', + 'archived_contact_readonly' => 'Archived contact can’t be edited, please unarchive it first.', + + // people add + 'people_add_title' => '新增一位新的聯絡人', + 'people_add_missing' => 'No person found – add a new one now', + 'people_add_firstname' => '名字', + 'people_add_middlename' => '中間名 (選填)', + 'people_add_lastname' => '姓氏 (選填)', + 'people_add_email' => '電子信箱 (選填)', + 'people_add_nickname' => '暱稱 (選填)', + 'people_add_cta' => '新增', + 'people_save_and_add_another_cta' => '提交併新增其他人', + 'people_add_success' => ':name 已成功建立', + 'people_add_gender' => '性別', + 'people_delete_success' => '聯絡人已被刪除', + 'people_delete_message' => '刪除聯絡人', + 'people_delete_confirmation' => '是否確認永久刪除:name的聯絡人資訊?', + 'people_add_birthday_reminder' => '祝: name生日快樂', + 'people_add_birthday_reminder_deceased' => ':name會在這一天過生日', + 'people_add_import' => '是否要 匯入您的聯絡人?', + 'people_edit_email_error' => '您的聯絡人中已經有人使用此電子郵件,請更換一個', + 'people_export' => '匯出為 vCard', + 'people_add_reminder_for_birthday' => 'Create an annual birthday reminder', + + // show + 'section_contact_information' => '聯絡人資訊', + 'section_personal_activities' => '活動', + 'section_personal_reminders' => '提醒', + 'section_personal_tasks' => '任務', + 'section_personal_gifts' => '禮物', + 'section_personal_notes' => '便籤', + + // archived contacts + 'list_link_to_active_contacts' => '您正在檢視存檔的聯絡人, 單擊這裡 來檢視活動的聯絡人列表。', + 'list_link_to_archived_contacts' => '已存檔聯絡人列表', + + // Header + 'me' => '這是你', + 'edit_contact_information' => '編輯聯絡人資訊', + 'contact_archive' => '存檔聯絡人', + 'contact_unarchive' => '取消存檔', + 'contact_archive_help' => 'Archived contacts are not be shown on the contact list, but still appear in search results.', + 'call_button' => '記錄通話', + 'set_favorite' => '您收藏的聯絡人將在聯絡人列表置頂顯示。', + + // Stay in touch + 'stay_in_touch' => '常聯絡*', + 'stay_in_touch_frequency' => '常聯絡*提醒頻率:每天|常聯絡*提醒頻率:每 {count} 天', + 'stay_in_touch_next_date' => 'Next due: {date}', + 'stay_in_touch_invalid' => '頻率必須大於0。', + 'stay_in_touch_premium' => '您需要升級到高階賬戶來使用這個功能!', + 'stay_in_touch_modal_title' => '常聯絡*', + 'stay_in_touch_modal_desc' => '我們將會用郵件提醒您與{firstname}保持聯絡。', + 'stay_in_touch_modal_label' => '每… {count} 天寄Email提醒我|每… {count} 天寄Email提醒我', + + // Calls + 'modal_call_title' => '記錄通話', + 'modal_call_comment' => '你們說了什麼?(可選)', + 'modal_call_exact_date' => '通話日期', + 'modal_call_who_called' => '誰打來的?', + 'modal_call_emotion' => '您想記錄您在此通話中的感受嗎?(可選)', + 'calls_add_success' => '已儲存通話記錄。', + 'call_delete_confirmation' => '你確定要刪除此通話記錄嗎?', + 'call_delete_success' => '成功刪除通話記錄!', + 'call_title' => '通話記錄', + 'call_empty_comment' => '無詳細資訊', + 'call_blank_title' => '追蹤您與{name} 的通話記錄', + 'call_blank_desc' => '你打給{name}', + 'call_you_called' => '您撥出的', + 'call_he_called' => '{name} 撥出的', + 'call_emotions' => '情緒:', + + // Conversation + 'conversation_blank' => 'Record conversations you have with :name on social media, SMS…', + 'conversation_delete_link' => '刪除對話', + 'conversation_edit_title' => '編輯對話', + 'conversation_edit_delete' => '您是否要刪除這個對話?操作無法撤銷。', + 'conversation_add_success' => '對話成功新增', + 'conversation_edit_success' => '對話成功更新', + 'conversation_delete_success' => '對話成功刪除', + 'conversation_add_title' => '記錄一個新對話', + 'conversation_add_when' => '你們何時進行的對話?', + 'conversation_add_who_wrote' => 'Who sent this message?', + 'conversation_add_how' => '你們怎麼交流?', + 'conversation_add_you' => '您', + 'conversation_add_content' => '寫下你們說的話', + 'conversation_add_what_was_said' => '您說了什麼?', + 'conversation_add_another' => '新增另一條訊息', + 'conversation_add_error' => '您必須至少新增一條資訊', + 'conversation_list_table_messages' => '訊息', + 'conversation_list_table_content' => '部分內容(最新訊息)', + 'conversation_list_title' => '對話', + 'conversation_list_cta' => '記錄對話', + + // age - birthday + 'birthdate_not_set' => '未設定生日', + 'age_approximate_in_years' => '大概:age歲', + 'age_exact_in_years' => ':age歲', + 'age_exact_birthdate' => '出生:date', + + // Last called + 'last_called' => '最近通話: :date', + 'last_talked_to' => '最近通話:(date)', + 'last_called_empty' => '最近通話: 未知', + 'last_activity_date' => '最近一起的活動: :date', + 'last_activity_date_empty' => '最近一起的活動: 未知', + + // additional information + 'information_edit_success' => '紀錄更新成功', + 'information_edit_title' => '編輯 :name的個人資訊', + 'information_edit_max_size' => '最大值 :size Kb', + 'information_edit_max_size2' => '最大 {size} Kb', + 'information_edit_firstname' => '名字', + 'information_edit_lastname' => '姓氏 (選填)', + 'information_edit_description' => 'Description (optional)', + 'information_edit_description_help' => '用於在聯絡人列表中新增一些元素(如有必要)', + 'information_edit_unknown' => '我不知道具體年齡', + 'information_edit_probably' => '年齡大概...歲', + 'information_edit_not_year' => '只知道月和日,但不知道哪一年。', + 'information_edit_exact' => '我知道詳細的年月日', + 'information_edit_birthdate_label' => '生日', + 'information_no_work_defined' => '未定義工作資訊', + 'information_work_at' => '在 :company工作', + 'work_add_cta' => '更新工作資訊', + 'work_edit_success' => 'Work information updated', + 'work_edit_title' => '更新:name的工作資訊', + 'work_edit_job' => '職位名稱 (可選)', + 'work_edit_company' => '公司 (可選)', + 'work_information' => '工作資訊', + + // food preferences + 'food_preferences_add_success' => '食品偏好已被儲存', + 'food_preferences_edit_description' => '也許:firstname或:family的家庭有過敏,或者不喜歡一瓶特定的酒等。把這些資訊列在這裡,在下次和邀請他們吃飯時可以在這裡看到這些資訊。', + 'food_preferences_edit_description_no_last_name' => '也許:firstname有過敏情況,或者不喜歡一瓶特定的酒等。把這些資訊列在這裡,在下次和邀請他們吃飯時可以在這裡看到這些資訊。', + 'food_preferences_edit_title' => '註明食物偏好', + 'food_preferences_edit_cta' => '儲存食物偏好', + 'food_preferences_title' => '食物偏好', + 'food_preferences_cta' => '新增食物偏好', + + // reminders + 'reminders_blank_title' => '您有什麼關於:name的提醒嗎?', + 'reminders_blank_add_activity' => '新增提醒', + 'reminders_add_title' => '你需要關於:name的提醒嗎?', + 'reminders_add_description' => 'Please remind me to…', + 'reminders_add_next_time' => '您希望下一次關於這個的提醒的時間是?', + 'reminders_add_once' => '僅一次', + 'reminders_add_recurrent' => '每', + 'reminders_add_starting_from' => '提醒我', + 'reminders_add_cta' => '新增提醒', + 'reminders_edit_update_cta' => '更新提醒', + 'reminders_add_error_custom_text' => '您需要為此提醒指定文字', + 'reminders_create_success' => '已成功新增提醒', + 'reminders_delete_success' => '已成功刪除提醒', + 'reminders_update_success' => '已成功更新提醒', + 'reminders_add_optional_comment' => '可選備註', + + 'reminder_frequency_day' => '每:number天', + 'reminder_frequency_week' => ' 每:number星期', + 'reminder_frequency_month' => ' 每:number月', + 'reminder_frequency_year' => '每:number年', + 'reminder_frequency_one_time' => '在:date', + 'reminders_delete_confirmation' => '確實要刪除此提醒嗎?', + 'reminders_delete_cta' => '刪除', + 'reminders_next_expected_date' => '在', + 'reminders_cta' => '新增提醒', + 'reminders_description' => 'We will send an email for each one of the reminders below. Reminders are sent every morning the day events will happen. Reminders automatically added for birthdays can not be deleted. If you want to change those dates, edit the birthday of the contacts.', + 'reminders_one_time' => '一次性', + 'reminders_type_week' => '周', + 'reminders_type_month' => '月', + 'reminders_type_year' => '年', + 'reminders_birthday' => ':name的生日', + 'reminders_free_plan_warning' => '您當前使用的是免費版。若需要郵件提醒,請升級您的賬戶。', + + // relationships + 'relationship_form_add' => '新增一個新的關係', + 'relationship_form_edit' => '修改一個已有關係', + 'relationship_form_is_with' => '這個人是...', + 'relationship_form_is_with_name' => ':name 是', + 'relationship_form_add_choice' => '這是與誰的關係?', + 'relationship_form_create_contact' => '新增一個新的人', + 'relationship_form_associate_contact' => '匯入一位已存在的聯絡人', + 'relationship_form_associate_dropdown' => '請從下拉選單選擇一位聯絡人', + 'relationship_form_associate_dropdown_placeholder' => '搜尋並選擇一位現有聯絡人', + 'relationship_form_also_create_contact' => '將此人建立為您的聯絡人', + 'relationship_form_add_description' => '這會讓你像其他聯絡人一樣對待這個人。', + 'relationship_form_add_no_existing_contact' => '您暫時沒有能與 :name 連結的聯絡人', + 'relationship_delete_confirmation' => '您確定要將關係刪除嗎?本操作無法撤銷。', + 'relationship_unlink_confirmation' => '您確定要將關係刪除嗎?此操作不會從您的聯絡人列表將其刪除。', + 'relationship_form_add_success' => '關係設定完成', + 'relationship_form_deletion_success' => '此關係已刪除', + + // tasks + 'tasks_title' => '任務', + 'tasks_blank_title' => '您暫時還沒任務。', + 'tasks_form_title' => '標題', + 'tasks_form_description' => '描述 (可選)', + 'tasks_add_task' => '新增任務', + 'tasks_delete_success' => '成功刪除任務!', + 'tasks_complete_success' => '成功變更任務!', + + // activities + 'activity_title' => '活動', + 'activity_type_category_simple_activities' => '一般活動', + 'activity_type_category_sport' => '運動', + 'activity_type_category_food' => '食物', + 'activity_type_category_cultural_activities' => '文化', + 'activity_type_just_hung_out' => '約會', + 'activity_type_watched_movie_at_home' => '看電影', + 'activity_type_talked_at_home' => '談心', + 'activity_type_did_sport_activities_together' => '一起打球', + 'activity_type_ate_at_his_place' => '在對方家裡做客', + 'activity_type_went_bar' => '泡吧', + 'activity_type_ate_at_home' => '在家吃飯', + 'activity_type_picnicked' => '已選擇', + 'activity_type_ate_restaurant' => '在飯店吃', + 'activity_type_went_theater' => '看戲', + 'activity_type_went_concert' => '去音樂會', + 'activity_type_went_play' => '出去玩', + 'activity_type_went_museum' => '去博物館', + 'activities_add_activity' => '新增活動', + 'activities_add_more_details' => '新增更多詳情', + 'activities_add_emotions' => '新增情緒', + 'activities_add_category' => '指定類別', + 'activities_add_participants_cta' => '新增參與者', + 'activities_item_information' => ':Activity,發生於:date', + 'activities_add_title' => '您與 {name} 一起做了什麼?', + 'activities_summary' => '描述你做了什麼', + 'activities_add_pick_activity' => 'Would you like to categorize this activity? You don’t have to, but it will give you statistics later on (optional)', + 'activities_add_date_occured' => '這活動是在什麼時候?', + 'activities_add_participants' => '除了 {name} 之外,誰參與了這個活動?(可選)', + 'activities_add_emotions_title' => '您想記錄您在此通話中的感受嗎?(可選)', + 'activities_blank_title' => '記錄您與 {name} 之間的點滴', + 'activities_blank_add_activity' => '新增活動', + 'activities_add_success' => '已成功新增活動', + 'activities_add_error' => '新增活動時出現錯誤', + 'activities_update_success' => '活動已成功更新', + 'activities_delete_success' => '活動已成功刪除', + 'activities_who_was_involved' => '誰參與了?', + 'activities_activity' => '活動類別', + 'activities_view_activities_report' => '檢視活動報告', + 'activities_profile_title' => ':name 與您之間的活動報告', + 'activities_profile_subtitle' => '截至目前為止您與:name的活動記錄如下:近一年共 :activities_last_twelve_months次,總共 :total_activities次', + 'activities_profile_year_summary_activity_types' => ':year年活動型別彙總', + 'activities_profile_year_summary' => ':year年你們一起進行的活動', + 'activities_profile_number_occurences' => ':value 次活動', + 'activities_list_participants' => '與會人員 ({total})', + 'activities_list_emotions' => '我感覺:', + 'activities_list_date' => '發生於', + 'activities_list_category' => '分類:', + + // notes + 'notes_create_success' => '便籤已成功建立', + 'notes_update_success' => '便箋已成功儲存', + 'notes_delete_success' => '註釋已成功刪除', + 'notes_add_cta' => '新增註釋', + 'notes_favorite' => '新增/刪除喜愛標記', + 'notes_delete_title' => '刪除便籤', + 'notes_delete_confirmation' => '確實要刪除此便籤嗎?', + + // gifts + 'gifts_title' => '禮物往來', + 'gifts_add_success' => '已成功新增禮物', + 'gifts_delete_success' => '禮物已成功刪除', + 'gifts_delete_confirmation' => '是否確實要刪除此禮物?', + 'gifts_add_gift' => '新增禮物', + 'gifts_link' => '連結', + 'gifts_for' => '贈予:{name}', + 'gifts_delete_cta' => '刪除', + 'gifts_add_title' => '與:name的禮物來往', + 'gifts_add_gift_idea' => '禮品創意', + 'gifts_add_gift_already_offered' => '送出的禮物', + 'gifts_add_gift_received' => '收到的禮物', + 'gifts_add_gift_title' => '這是什麼禮物?', + 'gifts_add_gift_name' => '禮品名稱', + 'gifts_add_link' => '禮物連結 (可選)', + 'gifts_add_value' => '值 (可選)', + 'gifts_add_comment' => '備註 (可選)', + 'gifts_add_recipient' => '收件人(可選)', + 'gifts_add_recipient_field' => '收件人', + 'gifts_add_photo' => '相片(可選)', + 'gifts_add_photo_title' => '為此禮物新增一張照片', + 'gifts_add_someone' => '這份禮物特別是給{name}的家人', + 'gifts_delete_title' => '刪除禮物', + 'gifts_ideas' => '心願單', + 'gifts_offered' => '送出的禮物', + 'gifts_offered_as_an_idea' => '標記為心願單', + 'gifts_received' => '收到的禮物', + 'gifts_view_comment' => '檢視評論', + 'gifts_mark_offered' => '標記為提供', + 'gifts_update_success' => '禮物已成功更新', + 'gifts_add_date' => '日期 (選填)', + + // debts + 'debt_delete_confirmation' => '是否確實要刪除此債務?', + 'debt_delete_success' => '已成功刪除債務', + 'debt_add_success' => '已成功新增債務', + 'debt_title' => '債務', + 'debt_add_cta' => '增加債務', + 'debt_you_owe' => '您欠:amount', + 'debt_they_owe' => ':name欠您:amount', + 'debt_add_title' => '債務管理', + 'debt_add_you_owe' => ':name借給您', + 'debt_add_they_owe' => '您借給:name', + 'debt_add_amount' => '數額', + 'debt_add_reason' => '事由(可選)', + 'debt_add_add_cta' => '增加債務', + 'debt_edit_update_cta' => '更新債務', + 'debt_edit_success' => '債務已成功更新', + 'debts_blank_title' => '管理您與:name之間的債務關係', + + // tags + 'tag_edit' => '編輯標籤', + 'tag_add' => '新增標籤', + 'tag_add_search' => '新增或搜尋標籤', + 'tag_no_tags' => '還沒有標籤', + + // Introductions + 'introductions_sidebar_title' => '你們是如何認識的?', + 'introductions_blank_cta' => '您如何遇到的:name', + 'introductions_title_edit' => '你是怎麼認識:name的?', + 'introductions_additional_info' => '你在哪裡相遇', + 'introductions_edit_met_through' => '有人把你介紹給這個人嗎?', + 'introductions_no_met_through' => '沒有人', + 'introductions_first_met_date' => '第一次相見', + 'introductions_no_first_met_date' => '我不記得具體日期', + 'introductions_first_met_date_known' => '這是我們相遇的日子', + 'introductions_add_reminder' => '新增提醒以慶祝此事件發生的週年紀念', + 'introductions_update_success' => '你成功更新了關於你們相識的故事', + 'introductions_met_through' => '通過 :name遇到', + 'introductions_met_date' => '在:date遇到', + 'introductions_reminder_title' => '你第一次遇見的那一天的週年紀念日', + + // Deceased + 'deceased_reminder_title' => ':name的去世週年懷念', + 'deceased_mark_person_deceased' => '標記為已逝者', + 'deceased_know_date' => '我知道去世日期', + 'deceased_add_reminder' => '為此日期新增提醒', + 'deceased_label' => '逝者', + 'deceased_date_label' => '死亡日期', + 'deceased_label_with_date' => '在:date去世', + 'deceased_age' => '享年', + + // Contact information + 'contact_info_title' => '聯絡資訊', + 'contact_info_form_content' => '內容', + 'contact_info_form_contact_type' => '聯絡方式', + 'contact_info_form_personalize' => '個性化', + 'contact_info_address' => '生活在', + + // Addresses + 'contact_address_title' => '地址', + 'contact_address_form_name' => '標籤 (可選)', + 'contact_address_form_street' => '街 (可選)', + 'contact_address_form_city' => '城市 (可選)', + 'contact_address_form_province' => '省 (可選)', + 'contact_address_form_postal_code' => '郵政編碼 (可選)', + 'contact_address_form_country' => '國家 (可選)', + 'contact_address_form_latitude' => '緯度 (僅限數字) (可選)', + 'contact_address_form_longitude' => '經度 (僅限數字) (可選)', + + // Pets + 'pets_kind' => '寵物種類', + 'pets_name' => '名字 (可選)', + 'pets_create_success' => '已成功新增寵物', + 'pets_update_success' => '寵物已更新', + 'pets_delete_success' => '寵物已被刪除', + 'pets_title' => '寵物', + 'pets_reptile' => '爬行動物', + 'pets_bird' => '鳥', + 'pets_cat' => '貓', + 'pets_dog' => '狗', + 'pets_fish' => '魚', + 'pets_hamster' => '倉鼠', + 'pets_horse' => '馬', + 'pets_rabbit' => '兔子', + 'pets_rat' => '鼠', + 'pets_small_animal' => '小動物', + 'pets_other' => '其它', + + // life events + 'life_event_list_tab_life_events' => '生活事件', + 'life_event_list_tab_other' => 'Notes, reminders, …', + 'life_event_list_title' => '生活事件', + 'life_event_blank' => '記錄在{name} 身上發生的事情以供將來參考', + 'life_event_list_cta' => '新增生活事件', + 'life_event_create_category' => '全部類別', + 'life_event_create_life_event' => '新增生活事件', + 'life_event_create_default_title' => '標題 (可選)', + 'life_event_create_default_story' => '故事 (可選)', + 'life_event_create_date' => '不需要詳細到某一天,提供年份即可', + 'life_event_create_default_description' => '新增你知道的資訊', + 'life_event_create_add_yearly_reminder' => '為該事件新增年度提醒', + 'life_event_create_success' => '生活事件新增成功', + 'life_event_delete_title' => '刪除生活事件', + 'life_event_delete_description' => '確實要刪除此生活事件嗎?刪除是永久性的。', + 'life_event_delete_success' => '事件已刪除', + 'life_event_date_it_happened' => '發生日期', + 'life_event_category_work_education' => '工作 & 教育', + 'life_event_category_family_relationships' => 'Family & relationships', + 'life_event_category_home_living' => '居家生活', + 'life_event_category_health_wellness' => 'Health & wellness', + 'life_event_category_travel_experiences' => '旅行與經歷', + 'life_event_sentence_new_job' => '開始了新的工作', + 'life_event_sentence_retirement' => '退休', + 'life_event_sentence_new_school' => '開始上學', + 'life_event_sentence_study_abroad' => '出國留學', + 'life_event_sentence_volunteer_work' => '開始志願服務', + 'life_event_sentence_published_book_or_paper' => '發表了一篇論文', + 'life_event_sentence_military_service' => '開始服役', + 'life_event_sentence_new_relationship' => '開始一段關係', + 'life_event_sentence_engagement' => '訂婚了', + 'life_event_sentence_marriage' => '結婚', + 'life_event_sentence_anniversary' => '週年紀念日', + 'life_event_sentence_expecting_a_baby' => '想要孩子', + 'life_event_sentence_new_child' => '有個孩子', + 'life_event_sentence_new_family_member' => '新增了家庭成員', + 'life_event_sentence_new_pet' => '養了寵物', + 'life_event_sentence_end_of_relationship' => '結束了一段關係', + 'life_event_sentence_loss_of_a_loved_one' => '失去了心愛的人', + 'life_event_sentence_moved' => '搬家了', + 'life_event_sentence_bought_a_home' => '買了新房子', + 'life_event_sentence_home_improvement' => '裝修了', + 'life_event_sentence_holidays' => '去度假', + 'life_event_sentence_new_vehicle' => '買了輛新車', + 'life_event_sentence_new_roommate' => '有了新室友', + 'life_event_sentence_overcame_an_illness' => '熬過了疾病', + 'life_event_sentence_quit_a_habit' => '戒掉一個習慣', + 'life_event_sentence_new_eating_habits' => '開始新的飲食習慣', + 'life_event_sentence_weight_loss' => '減肥了', + 'life_event_sentence_wear_glass_or_contact' => '開始佩戴玻璃或隱形眼鏡', + 'life_event_sentence_broken_bone' => '折斷了骨頭', + 'life_event_sentence_removed_braces' => '去掉了牙齒矯正器', + 'life_event_sentence_surgery' => '做了手術', + 'life_event_sentence_dentist' => '去看牙醫了', + 'life_event_sentence_new_sport' => '開始運動', + 'life_event_sentence_new_hobby' => '有了新愛好', + 'life_event_sentence_new_instrument' => '學會了新樂器', + 'life_event_sentence_new_language' => '學了一門新的語言', + 'life_event_sentence_tattoo_or_piercing' => '紋身了或者打了耳洞', + 'life_event_sentence_new_license' => '獲得駕照', + 'life_event_sentence_travel' => '旅遊了', + 'life_event_sentence_achievement_or_award' => '獲得成就或獎項', + 'life_event_sentence_changed_beliefs' => '改變信仰', + 'life_event_sentence_first_word' => '第一次發言', + 'life_event_sentence_first_kiss' => '第一次接吻', + + // documents + 'document_list_title' => '檔案', + 'document_list_cta' => '上載檔案', + 'document_list_blank_desc' => '在這裡, 您可以儲存與此人相關的檔案。', + 'document_upload_zone_cta' => '上傳檔案', + 'document_upload_zone_progress' => '正在上傳檔案中...', + 'document_upload_zone_error' => '上傳檔案時出錯,請再試一次 !', + + // Photos + 'photo_title' => '照片', + 'photo_list_title' => '相關照片', + 'photo_list_cta' => '上傳照片', + 'photo_list_blank_desc' => '您可以儲存有關此聯絡人的影象。立即上傳一個!', + 'photo_upload_zone_cta' => '上傳照片', + 'photo_current_profile_pic' => '目前頭像', + 'photo_make_profile_pic' => '製作頭像', + 'photo_delete' => '刪除照片', + 'photo_next' => '下一張照片 ❯', + 'photo_previous' => '❮ 上一張照片', + + // Avatars + 'avatar_change_title' => '更換頭像', + 'avatar_question' => '您想使用哪個頭像?', + 'avatar_default_avatar' => '預設頭像', + 'avatar_adorable_avatar' => '喜愛頭像', + 'avatar_gravatar' => '此使用者的電子郵件地址 與 Gravatar 關聯 。 Gravatar 是全球通用的頭像服務。', + 'avatar_current' => '保持當前頭像', + 'avatar_photo' => '從您上傳的照片', + 'avatar_crop_new_avatar_photo' => '裁剪新頭像圖片', + + // emotions + 'emotion_this_made_me_feel' => '這讓你覺得...', + + // logs + 'auditlogs_link' => '歷史', + 'auditlogs_title' => ':name 發生的所有事件', + 'auditlogs_breadcrumb' => '歷史', + 'auditlogs_author' => ':name 於 :date ', + + // contact field label + 'contact_field_label_home' => '家庭', + 'contact_field_label_work' => '工作', + 'contact_field_label_cell' => '手機', + 'contact_field_label_fax' => '傳真', + 'contact_field_label_pager' => '呼叫器', + 'contact_field_label_main' => '主要', + 'contact_field_label_other' => '其它', + 'contact_field_label_personal' => '個人', +]; diff --git a/resources/lang/zh-TW/reminder.php b/resources/lang/zh-TW/reminder.php new file mode 100644 index 0000000..75ab417 --- /dev/null +++ b/resources/lang/zh-TW/reminder.php @@ -0,0 +1,16 @@ + '祝此人生日快樂', + 'type_phone_call' => '呼叫', + 'type_lunch' => '與此人共進午餐', + 'type_hangout' => '與此人約會', + 'type_email' => '電子郵件', + 'type_birthday_kid' => '祝此人的孩子生日快樂', +]; diff --git a/resources/lang/zh-TW/settings.php b/resources/lang/zh-TW/settings.php new file mode 100644 index 0000000..28acfc3 --- /dev/null +++ b/resources/lang/zh-TW/settings.php @@ -0,0 +1,557 @@ + '帳戶設定', + 'sidebar_personalization' => '個性化', + 'sidebar_settings_storage' => '儲存空間', + 'sidebar_settings_export' => '匯出資料', + 'sidebar_settings_users' => '使用者', + 'sidebar_settings_subscriptions' => '訂閱', + 'sidebar_settings_import' => '匯入資料', + 'sidebar_settings_tags' => '標籤管理', + 'sidebar_settings_api' => 'API', + 'sidebar_settings_dav' => 'Dav 資源', + 'sidebar_settings_security' => '安全', + 'sidebar_settings_auditlogs' => '追蹤日誌', + + 'title_general' => '基本資訊', + 'title_i18n' => '本地化', + 'title_layout' => '佈局', + + 'me_title' => 'Me as a contact', + 'me_help' => '這個聯絡人在Monica代表了 ', + 'me_select' => '選擇聯絡人', + 'me_no_contact' => '沒有選擇聯絡人', + 'me_select_click' => '單擊此處選擇一位聯絡人', + 'me_remove_contact' => '刪除關聯', + 'me_choose' => '選擇自己', + 'me_choose_placeholder' => '選擇自己', + + 'export_title' => '匯出帳戶資料', + 'export_be_patient' => 'Click the button to start the export. It might take several minutes to process the export – please be patient and do not spam the button.', + 'export_title_sql' => '匯出為 SQL 檔', + 'export_sql_explanation' => 'Exporting your data in SQL format allows you to take your data and import it to your own Monica instance. This is only valuable if you do have your own server.', + 'export_sql_cta' => '匯出為 SQL 檔', + 'export_sql_link_instructions' => 'Note: read the instructions to learn more about importing this file to your instance.', + 'export_title_json' => '匯出為 JSON 檔', + 'export_submitted' => 'Your export has been submitted, it will be available in a few moment…', + 'export_json_explanation' => 'Exporting your data in Json format for backup.', + 'export_json_beta' => 'Json export is in preview mode. Tell us what you think about it:', + 'export_json_cta' => '匯出為 JSON 檔', + 'export_header_type' => 'Type', + 'export_header_timestamp' => 'Creation date', + 'export_header_status' => 'Status', + 'export_header_actions' => 'Actions', + 'export_last_title' => 'Last exports', + 'export_empty_title' => 'No exports yet', + 'export_type_json' => 'Json export', + 'export_type_sql' => 'SQL export', + 'export_status_todo' => 'Submitted', + 'export_status_doing' => 'Doing', + 'export_status_done' => 'Done', + 'export_status_failed' => 'Failed', + 'export_not_done' => '正在匯出中,尚無法下載。', + + 'firstname' => '名', + 'lastname' => '姓氏', + 'name_order' => '名稱順序', + 'name_order_firstname_lastname' => '<名> <姓> - 小明 王', + 'name_order_lastname_firstname' => '<姓> <名> - 王 小明', + 'name_order_firstname_lastname_nickname' => '<名> <姓> (<暱稱>) - 小明 王 (小黑)', + 'name_order_firstname_nickname_lastname' => '<名> (<暱稱>) <姓> - 小明 (小黑) 王', + 'name_order_lastname_firstname_nickname' => '<姓> <名> (<暱稱>) - 王 小明 (小黑)', + 'name_order_lastname_nickname_firstname' => '<姓> (<暱稱>) <名> - 王 (小黑) 小明', + 'name_order_nickname_firstname_lastname' => '<暱稱> (<名> <姓>) - 小黑 (小明 王)', + 'name_order_nickname_lastname_firstname' => '<暱稱> (<姓> <名>) - 小黑 (王 小明)', + 'name_order_nickname' => '<暱稱> - 小黑', + 'currency' => '貨幣', + 'name' => '您的姓名: :name', + 'email' => '電子郵件地址', + 'email_placeholder' => '輸入電子郵箱', + 'email_help' => '這是用於登入的電子郵件, 同時也用來接收您的提醒。', + 'timezone' => '時區', + 'temperature_scale' => '溫度單位', + 'temperature_scale_fahrenheit' => '華氏 (°F)', + 'temperature_scale_celsius' => '攝氏 (°C)', + 'layout' => '佈局', + 'layout_small' => '最大1200畫素寬', + 'layout_big' => '瀏覽器的全寬度', + 'save' => '更新偏好', + 'delete_title' => '刪除您的帳戶', + 'delete_desc' => '是否刪除帳戶?您的資料將永遠刪除。若您是付費用戶,將立即退訂。', + 'delete_other_desc' => 'Your data in the main database will be deleted immediately. As described in our privacy policy, we carry out securely encrypted backups of the database every day. These backups are kept for 30 days after which they are completely deleted. We cannot delete specific data from the backups we hold any earlier than this. All of your data will be completely deleted no later than 31 days after your account’s deletion.', + 'reset_desc' => 'Do you wish to reset your account? This will remove all your contacts, and all of the data associated with them. Your account will not be deleted.', + 'reset_title' => '刪除您的帳戶', + 'reset_cta' => '重置帳戶', + 'reset_notice' => 'Are you sure to reset your account? This is permanent and cannot be undone.', + 'reset_success' => 'Your account has been reset successfully.', + 'delete_notice' => 'Are you sure you want to delete your account? This is permanent and cannot be undone. All of your data will be deleted and will not be recoverable.', + 'delete_cta' => '刪除帳戶', + 'settings_success' => '偏好設定已更新', + 'locale' => '應用程式中使用的語言', + 'locale_help' => '您想要幫助翻譯Monica或新增新語言嗎?請點選 瞭解更多資訊。', + 'locale_ar' => '阿拉伯文', + 'locale_cs' => '捷克文', + 'locale_de' => '德文', + 'locale_el' => '希臘語', + 'locale_en' => '英文', + 'locale_en-GB' => '英語 (英國)', + 'locale_es' => '西班牙文', + 'locale_fr' => '法文', + 'locale_he' => '希伯來文', + 'locale_hr' => '克羅埃西亞文', + 'locale_id' => '印尼語', + 'locale_it' => '義大利文', + 'locale_ja' => '日語', + 'locale_nl' => '荷蘭文', + 'locale_pt' => '葡萄牙文', + 'locale_pt-BR' => 'Portuguese, Brazil', + 'locale_ru' => '俄文', + 'locale_sv' => '瑞典語', + 'locale_vi' => '越南語', + 'locale_zh' => '簡體中文', + 'locale_zh-TW' => '繁體中文(台灣)', + 'locale_tr' => '土耳其文', + + 'security_title' => '安全', + 'security_help' => '更改您的帳戶的安全選項。', + 'password_change' => '變更您的密碼', + 'password_current' => '當前密碼', + 'password_current_placeholder' => '輸入當前密碼', + 'password_new1' => '新密碼', + 'password_new1_placeholder' => '請輸入新密碼', + 'password_new2' => '確認您的新密碼', + 'password_new2_placeholder' => '重新輸入新密碼', + 'password_btn' => '更改密碼', + '2fa_title' => '雙重驗證', + '2fa_otp_title' => '用於二次驗證的App', + '2fa_enable_title' => '啟用二次驗證', + '2fa_enable_description' => '啟用雙重身份驗證以提高帳戶的安全性。', + '2fa_enable_otp' => '開啟您的雙重認證移動應用程式, 並掃描以下 QR 條碼:', + '2fa_enable_otp_help' => '如果您的雙重認證移動應用程式不支援 QR 條碼, 請在下面的程式碼中輸入:', + '2fa_enable_otp_validate' => '請驗證您剛設定的新裝置:', + '2fa_enable_success' => '雙重認證已啟用', + '2fa_enable_error' => '嘗試啟用雙重身份驗證時出錯', + '2fa_enable_error_already_set' => '二次驗證已啟用', + '2fa_disable_title' => '關閉雙重身份驗證', + '2fa_disable_description' => '停用雙重素身份驗證。注意!您的帳戶將不再安全!', + '2fa_disable_success' => '雙重身份認證已禁用', + '2fa_disable_error' => '嘗試禁用雙重身份驗證時出錯', + + 'webauthn_title' => '安全鑰匙 - WebAuthn', + 'webauthn_enable_description' => '新增一個安全鑰匙', + 'webauthn_key_name_help' => '給你的鑰匙起個名字', + 'webauthn_key_name' => '鑰匙名稱:', + 'webauthn_success' => '您的鑰匙已被檢測到並驗證完畢。', + 'webauthn_last_use' => '最後使用: {timestamp}', + 'webauthn_delete_confirmation' => '確實要刪除這個鑰匙嗎?', + 'webauthn_delete_success' => '鑰匙已刪除', + 'webauthn_insertKey' => '插入您的安全鑰匙', + 'webauthn_buttonAdvise' => '如果您的安全鑰匙有按鈕,請按下它。', + 'webauthn_noButtonAdvise' => '如果沒有, 請將其拔出並再次插入。', + 'webauthn_not_supported' => '您的遊覽器並不支援WebAuthn', + 'webauthn_not_secured' => 'WebAuthn只支援SSL連線,請使用https開啟這個頁面', + 'webauthn_error_already_used' => '這個鑰匙已經註冊,您無需在註冊一次。', + 'webauthn_error_not_allowed' => '操作超時或不允許。', + + 'recovery_title' => '恢復程式碼', + 'recovery_show' => '獲取恢復程式碼', + 'recovery_copy_help' => '複製到您的剪貼簿', + 'recovery_help_intro' => '以下是您的恢復程式碼:', + 'recovery_help_information' => '您可以使用每個恢復程式碼一次。', + 'recovery_clipboard' => '已複製到剪貼簿.', + 'recovery_generate' => '正在產生中...', + 'recovery_generate_help' => '請注意,重新產生新程式碼將使以前的程式碼失效.', + 'recovery_already_used_help' => '此程式碼已被使用。', + + 'users_list_title' => '可以訪問您的帳戶的使用者', + 'users_list_add_user' => '邀請新使用者', + 'users_list_you' => '這是你', + 'users_list_invitations_title' => '待處理的邀請', + 'users_list_invitations_explanation' => '已邀請', + 'users_list_invitations_invited_by' => '被:name邀請', + 'users_list_invitations_sent_date' => '在:date傳送', + 'users_blank_title' => '您是唯一可以訪問此帳戶的人。', + 'users_blank_add_title' => '你想邀請別人嗎?', + 'users_blank_description' => '此人將具有您擁有的相同訪問許可權, 並且可以新增、編輯或刪除聯絡人資訊。', + 'users_blank_cta' => '邀請他人加入', + 'users_add_title' => '透過 Email 邀請新的用戶', + 'users_add_description' => '新的用戶權限將和您一樣,可以邀請或刪除其他用戶(包含您)。請再次確認您信任他/她後再授予權限。', + 'users_add_email_field' => '輸入您要邀請的人的電子郵件', + 'users_add_confirmation' => 'I confirm that I want to invite this user to my account. I understand that this person will have access to ALL of my data and see exactly what I see.', + 'users_add_cta' => '通過電子郵件邀請使用者', + 'users_accept_title' => '接受邀請並新建一個賬號', + 'users_error_please_confirm' => '請您先確認您要邀請此使用者', + 'users_error_email_already_taken' => '這個電子郵件已經存在,請另選一個!', + 'users_error_already_invited' => '您已經邀請了此使用者。請選擇其他電子郵件地址。', + 'users_error_email_not_similar' => '這不是邀請人的電子郵件。', + 'users_invitation_deleted_confirmation_message' => '已成功刪除邀請', + 'users_invitations_delete_confirmation' => '確實要刪除此邀請嗎?', + 'users_list_delete_confirmation' => '是否確實要從您的帳戶中刪除此使用者?', + 'users_invitation_need_subscription' => '您需要升級賬戶才能新增更多使用者', + + 'subscriptions_account_current_plan' => '您當前的訂閱', + 'subscriptions_account_current_legacy' => 'Current plan, not selectable anymore:', + 'subscriptions_account_current_paid_plan' => '您當前的訂閱是::name,感謝您的訂閱。', + + 'subscriptions_account_next_billing_title' => 'Next bill', + 'subscriptions_account_next_billing' => '您的訂閱將在 :date 自動續費', + 'subscriptions_account_bill_monthly' => 'We’ll bill you :price for another month.', + 'subscriptions_account_bill_annual' => 'We’ll bill you :price for another year.', + 'subscriptions_account_change' => 'Change plan', + + 'subscriptions_account_cancel_title' => 'Cancel subscription', + 'subscriptions_account_cancel_action' => 'Cancel subscription', + 'subscriptions_account_cancel' => '您可以隨時 取消訂閱。', + 'subscriptions_account_free_plan' => '您正在使用免費版', + 'subscriptions_account_free_plan_upgrade' => '您可以將您的帳戶升級為:name, 它的成本為每月$:price。您將享有以下特權:', + 'subscriptions_account_free_plan_benefits_users' => '不限數量的使用者', + 'subscriptions_account_free_plan_benefits_reminders' => '電子郵件提醒', + 'subscriptions_account_free_plan_benefits_import_data_vcard' => '從 vCard 檔案匯入聯絡人', + 'subscriptions_account_free_plan_benefits_support' => 'Support the project in the long run, so we can introduce more great features.', + 'subscriptions_account_upgrade' => '更新您的賬戶', + 'subscriptions_account_upgrade_title' => '立即升級您的Monica賬戶吧!', + 'subscriptions_account_upgrade_choice' => '在下方選擇一個訂閱(已有 :customers 訂閱了高階版)', + 'subscriptions_account_update_title' => 'Update Monica subscription', + 'subscriptions_account_update_description' => 'You can change your subscription’s frequency here.', + 'subscriptions_account_update_information' => 'You will be billed immediately for the new amount. Your subscription will extend to the new period, depending on your choice.', + 'subscriptions_account_invoices' => '發票', + 'subscriptions_account_invoices_download' => '下載', + 'subscriptions_account_invoices_subscription' => '訂閱週期::startDate 至 :endDate', + 'subscriptions_account_payment' => '哪個付費週期最適合您?', + 'subscriptions_account_confirm_payment' => '交易尚未完成,請您按此確認您的付款', + 'subscriptions_downgrade_title' => '將您的帳戶降級為免費版', + 'subscriptions_downgrade_limitations' => '免費版的功能有限制。如果您需要降級,請您確保完成以下檢查:', + 'subscriptions_downgrade_rule_users' => '您的帳戶中必須只有1個使用者', + 'subscriptions_downgrade_rule_users_constraint' => '您的帳戶中當前有 :count 個使用者。', + 'subscriptions_downgrade_rule_invitations' => 'You must not have any pending invitations', + 'subscriptions_downgrade_rule_invitations_constraint' => 'You currently have 1 pending invitation.|You currently have :count pending invitations.', + 'subscriptions_downgrade_rule_contacts' => '您不能超過 :number 的活躍聯絡人', + 'subscriptions_downgrade_rule_contacts_constraint' => '當前有 :count 位聯絡人。', + 'subscriptions_downgrade_rule_contacts_archive' => 'We can also archive all your contacts for you – that would clear this rule and let you proceed with your account’s downgrade process.', + 'subscriptions_downgrade_cta' => '降級', + 'subscriptions_downgrade_success' => '您已降級到免費版!', + 'subscriptions_downgrade_thanks' => 'Thanks so much for trying the paid plan. We keep adding new features on Monica all the time – so you might want to come back in the future to see if you might be interested in taking a subscription again.', + 'subscriptions_back' => '返回設定', + 'subscriptions_upgrade_title' => '升級您的帳戶', + 'subscriptions_upgrade_choose' => '您選擇了:plan', + 'subscriptions_upgrade_infos' => '請在下方輸入您的付款資訊:', + 'subscriptions_upgrade_name' => '持卡人姓名', + 'subscriptions_upgrade_zip' => '郵政編碼', + 'subscriptions_upgrade_credit' => '信用卡或借記卡', + 'subscriptions_upgrade_submit' => '支付{amount}', + 'subscriptions_upgrade_charge' => 'We’ll charge your card :price now. The next charge will be on :date. If you ever change your mind, you can cancel at any time, no questions asked.', + 'subscriptions_upgrade_charge_handled' => '支付服務由第三方支付平臺 Stripe 提供,我們無法接觸到您的個人資訊。', + 'subscriptions_upgrade_success' => '感謝您的訂閱!', + 'subscriptions_upgrade_thanks' => '歡迎來到讓世界變得更美好的社群。', + + 'subscriptions_payment_confirm_title' => '確認您的 :amount 付款', + 'subscriptions_payment_confirm_information' => '需要額外資訊來處理您的付款,請您補充下列付款資訊。', + 'subscriptions_payment_succeeded_title' => '支付成功', + 'subscriptions_payment_succeeded' => '此交易已經完成。', + 'subscriptions_payment_cancelled_title' => '付款已取消', + 'subscriptions_payment_cancelled' => '您的付款已被取消。', + 'subscriptions_payment_error_name' => '請提供您的姓名', + 'subscriptions_payment_success' => '您的付款已成功', + + 'subscriptions_pdf_title' => '您的:name每月訂閱', + 'subscriptions_plan_frequency_year' => ':amount / year', + 'subscriptions_plan_frequency_month' => ':amount / month', + 'subscriptions_plan_choose' => '選擇此計劃', + 'subscriptions_plan_year_title' => '按年度支付', + 'subscriptions_plan_year_bonus' => '一整年的安心', + 'subscriptions_plan_month_title' => '按月支付', + 'subscriptions_plan_month_bonus' => '隨時取消', + 'subscriptions_plan_include1' => '您將享有以下特權:', + 'subscriptions_plan_include2' => '無限新增聯絡人·無限的使用者數量·電子郵件提醒·匯入 vCard ·個性化的聯絡人資訊', + 'subscriptions_plan_include3' => '收入的100% 用於此專案的開發。', + 'subscriptions_help_title' => '您可能還關心', + 'subscriptions_help_opensource_title' => '什麼是開源專案?', + 'subscriptions_help_opensource_desc' => 'Monica is an open source project. This means it is built by a community who wants to build a great tool for the greater good. Being open source means the code is publicly available on GitHub, and everyone can inspect it, modify it or enhance it. All the money we raise is dedicated to building better features, paying for more powerful servers, and paying other costs. Thanks for your help. We couldn’t do it without you.', + 'subscriptions_help_limits_title' => 'Is there a limit to the number of contacts we can have on the free plan?', + 'subscriptions_help_limits_plan' => '是的。免費版您能擁有:number位聯絡人。', + 'subscriptions_help_discounts_title' => '你們對非盈利機構和學生有優惠嗎?', + 'subscriptions_help_discounts_desc' => '當然!Monica免費為學生,非盈利機構提供服務。您只需要提交一下材料給我們的 支援人員。', + 'subscriptions_help_change_title' => '如果我改變主意怎麼辦?', + 'subscriptions_help_change_desc' => 'You can cancel anytime, no questions asked, and all by yourself – no need to contact support. However, you will not be refunded for the current period.', + + 'stripe_error_card' => '您的卡被拒,原因是::message', + 'stripe_error_api_connection' => '與Stripe的通訊失敗,請稍候重試。', + 'stripe_error_rate_limit' => '與Stripe的通訊次數過多,請稍候再試。', + 'stripe_error_invalid_request' => '無效的引數,請稍後再試。', + 'stripe_error_authentication' => 'Stripe授權失敗', + + 'import_title' => '在您的帳戶中匯入聯絡人', + 'import_cta' => '上載聯絡人', + 'import_stat' => '您目前為止匯入了:number個檔案。', + 'import_result_stat' => '上傳了包含 :total_contacts 個聯絡人的 vCard (:total_imported imported, :total_skipped skipped)', + 'import_view_report' => '檢視報告', + 'import_in_progress' => '匯入正在進行中。在一分鐘內重新載入頁面。', + 'import_upload_title' => '從 vCard 檔案匯入聯絡人', + 'import_upload_rules_desc' => '但是, 我們有一些規則:', + 'import_upload_rule_format' => '我們支援 vcardvcf 檔案。', + 'import_upload_rule_vcard' => 'We support the vCard 3.0 format, which is the default format for macOS’s Contacts.app and Google Contacts.', + 'import_upload_rule_instructions' => 'Export instructions for macOS Contacts.app and Google Contacts.', + 'import_upload_rule_multiple' => 'If your contacts have multiple email addresses or phone numbers, only the first entry will be saved.', + 'import_upload_rule_limit' => 'Files are limited to 10 MB.', + 'import_upload_rule_time' => '上傳聯絡人有時需要幾分鐘的時間,請耐心等待。', + 'import_upload_rule_cant_revert' => '請確認您上傳的資料是正確的,一旦上傳就無法撤銷。', + 'import_upload_form_file' => '你的 .vcf. vCard 檔案:', + 'import_upload_behaviour' => '匯入偏好:', + 'import_upload_behaviour_add' => '新增新聯絡人,並跳過已存在的聯絡人', + 'import_upload_behaviour_replace' => '替換現有條目', + 'import_upload_behaviour_help' => 'Replacing will replace all data found in the vCard, but will keep existing contact fields.', + 'import_report_title' => '匯入報表', + 'import_report_date' => '匯入日期', + 'import_report_type' => '匯入型別', + 'import_report_number_contacts' => '檔案中的聯絡人數', + 'import_report_number_contacts_imported' => '匯入的聯絡人數量', + 'import_report_number_contacts_skipped' => '跳過的聯絡人數', + 'import_report_status_imported' => '匯入', + 'import_report_status_skipped' => '跳過', + 'import_vcard_parse_error' => '分析 vcard 項時出錯', + 'import_vcard_contact_exist' => '聯絡人已存在', + 'import_vcard_contact_no_firstname' => 'No first name (mandatory)', + 'import_vcard_file_not_found' => '檔案不存在', + 'import_vcard_unknown_entry' => '未知的聯絡人姓名', + 'import_vcard_file_no_entries' => '檔案不包含聯絡人', + 'import_blank_title' => '您暫無匯入的聯絡人。', + 'import_blank_question' => '是否立即匯入聯絡人?', + 'import_blank_description' => '我們可以從 Google Contacts 或您的Contact manager那裡匯入您的 vCard 檔案。', + 'import_blank_cta' => '匯入 vCard', + 'import_need_subscription' => '您需要訂閱才能匯入聯絡人', + + 'tags_list_title' => '標籤', + 'tags_list_description' => '您可以通過設定來標記聯絡人。標記的工作方式類似於資料夾, 但可以向聯絡人新增多個標記。若要新增新標記, 請在聯絡人中新增即可。', + 'tags_list_contact_number' => ':count 個聯絡人', + 'tags_list_delete_success' => '標籤已成功刪除', + 'tags_list_edit_success' => 'The tag has been successfully updated', + 'tags_list_delete_confirmation' => '確實要刪除該標籤嗎?不會刪除任何聯絡人, 只有標籤。', + 'tags_blank_title' => '標籤是對您的聯絡人進行分類的一種很好的方式。', + 'tags_blank_description' => 'Tags work like folders, but you can add more than one tag to a contact. Go to a contact and tag a friend, right below the name. Once a contact is tagged, come back here to manage all the tags in your account.', + + 'api_title' => 'API 訪問', + 'api_description' => 'API 可以用來從外部應用程式操縱Monica的資料, 例如移動應用程式。', + 'api_help' => '要使用 API,必須要有一個Token。 您可以建立個人訪問 Token,也可以授權OAuth 客戶端為您建立它。 檢視 API 文件獲取詳情', + 'api_endpoint' => '此 Monica 例項的 API 終端是:', + + 'api_personal_access_tokens' => '個人訪問令牌', + 'api_pao_description' => '請確保將此token授予您信任的源-因為它們允許您訪問所有資料。', + 'api_token_title' => '個人訪問 Token', + 'api_token_create_new' => '建立金鑰', + 'api_token_not_created' => '您沒有已建立的訪問金鑰', + 'api_token_name' => 'Token 名稱', + 'api_token_expire' => '過期於 {date}', + 'api_token_delete' => '刪除', + 'api_token_create' => '建立金鑰', + 'api_token_scopes' => '作用域', + 'api_token_help' => '這是您的個人訪問金鑰,我們只會展示一次,請妥善保管。您現在可以使用這個金鑰進行API請求', + + 'api_oauth_clients' => '您的 Oauth 客戶端', + 'api_oauth_clients_desc' => '您可以註冊自己的 OAuth 客戶端。', + 'api_oauth_clients_desc2' => '使用此客戶端ID請求一個新的Token,並將授權碼轉換為Token。請參閱 Laravel Passport文件 獲取更多資訊。', + 'api_oauth_title' => 'OAuth 客戶端', + 'api_oauth_create_new' => '建立新的客戶端', + 'api_oauth_edit' => '編輯客戶端', + 'api_oauth_not_created' => '您尚未建立Oauth客戶端', + 'api_oauth_clientid' => '客戶端 ID', + 'api_oauth_name' => '名稱', + 'api_oauth_name_help' => '安全碼', + 'api_oauth_secret' => '金鑰', + 'api_oauth_create' => '建立客戶端', + 'api_oauth_redirecturl' => '重定向URL', + 'api_oauth_redirecturl_help' => '應用程式的授權回撥 URL。', + + 'api_authorized_clients' => '授權客戶端列表', + 'api_authorized_clients_desc' => '本節列出了您授權訪問應用程式的所有客戶端,您可以隨時撤銷此授權。', + 'api_authorized_clients_title' => '已授權的應用', + 'api_authorized_clients_none' => 'There are no authorized clients yet.', + 'api_authorized_clients_name' => '名稱', + 'api_authorized_clients_scopes' => '作用域', + + 'personalization_tab_title' => '個性化您的帳戶', + + 'personalization_title' => 'Here you will find different settings to configure your account. These features are intended for “power users” who want maximum control over Monica.', + 'personalization_contact_field_type_title' => '聯絡人欄位型別', + 'personalization_contact_field_type_add' => '新增新欄位型別', + 'personalization_contact_field_type_description' => 'You can configure all the different types of contact fields that you can associate to all your contacts. For example, if a new social network appears in the future, you will be able to add this new way of communicating with your contacts right here.', + 'personalization_contact_field_type_table_name' => '名稱', + 'personalization_contact_field_type_table_protocol' => '協議', + 'personalization_contact_field_type_table_actions' => '行動', + 'personalization_contact_field_type_modal_title' => '新增新的聯絡人欄位型別', + 'personalization_contact_field_type_modal_edit_title' => '編輯現有聯絡人欄位型別', + 'personalization_contact_field_type_modal_delete_title' => '刪除現有聯絡人欄位型別', + 'personalization_contact_field_type_modal_delete_description' => 'Are you sure you want to delete this contact field type? Deleting this type of contact field will delete ALL the data with this type for all of your contacts.', + 'personalization_contact_field_type_modal_name' => '名稱', + 'personalization_contact_field_type_modal_protocol' => '協議 (可選)', + 'personalization_contact_field_type_modal_protocol_help' => '每個新的聯絡人欄位型別都可以選定。如果設定了協議, 我們將使用它來觸發設定的操作。', + 'personalization_contact_field_type_modal_icon' => '圖示 (可選)', + 'personalization_contact_field_type_modal_icon_help' => '您可以將圖示與此聯絡人欄位型別關聯。您需要新增對Font Awesome圖示的引用。', + 'personalization_contact_field_type_delete_success' => 'The contact field type has been successfully deleted.', + 'personalization_contact_field_type_add_success' => '已成功新增聯絡人欄位型別。', + 'personalization_contact_field_type_edit_success' => '聯絡人欄位型別已成功更新。', + + 'personalization_genders_title' => '性別型別', + 'personalization_genders_add' => '新增新的性別型別', + 'personalization_genders_desc' => '你可以根據需要定義儘可能多的性別。您的帳戶中至少需要一種性別型別。', + 'personalization_genders_modal_add' => '新增性別型別', + 'personalization_genders_modal_edit' => '更新性別型別', + 'personalization_genders_modal_name' => '名稱', + 'personalization_genders_modal_name_help' => '在聯絡人頁面顯示性別的名稱', + 'personalization_genders_modal_sex' => '性別', + 'personalization_genders_modal_sex_help' => '在匯入/匯出 VCard 時用於定義關係', + 'personalization_genders_modal_default' => '選擇新聯絡人的預設性別', + 'personalization_genders_modal_delete' => '刪除性別型別', + 'personalization_genders_modal_delete_desc' => 'Are you sure you want to delete the gender “{name}”?', + 'personalization_genders_modal_delete_question' => 'You currently have {count} contact with this gender. If you delete this gender, what gender should this contact have?|You currently have {count} contacts with this gender. If you delete this gender, what gender should these contacts have?', + 'personalization_genders_modal_delete_question_default' => 'This gender is the default one. If you delete this gender, which one will be the new default?', + 'personalization_genders_modal_error' => 'Please choose a gender from the list.', + 'personalization_genders_list_contact_number' => '{count} 個聯絡人|{count} 個聯絡人', + 'personalization_genders_table_name' => '名稱', + 'personalization_genders_table_sex' => '性別', + 'personalization_genders_table_default' => '預設', + 'personalization_genders_default' => '預設性別', + 'personalization_genders_make_default' => '更改預設性別', + 'personalization_genders_select_default' => '選擇預設性別', + 'personalization_genders_m' => '男性', + 'personalization_genders_f' => '女性', + 'personalization_genders_o' => '其他', + 'personalization_genders_u' => '未知', + 'personalization_genders_n' => '無或不適用', + + 'personalization_reminder_rule_save' => '更改已儲存', + 'personalization_reminder_rule_title' => '提醒規則', + 'personalization_reminder_rule_line' => '提前 {count} 天|提前 {count} 天', + 'personalization_reminder_rule_desc' => 'For every reminder that you set, Monica can send you an email a number of days before the event happens. You can adjust these notification settings here. These notifications only apply to monthly and yearly reminders.', + + 'personalization_module_save' => '更改已被儲存', + 'personalization_module_title' => '功能', + 'personalization_module_desc' => 'You may not need all of Monica’s features. Below you can toggle specific features that are used on a contact sheet. This change will affect ALL your contacts. Turning off a feature does not delete any data, it simply hides the feature.', + + 'personalisation_paid_upgrade' => '這是一個高階功能,需要付費訂閱才能啟用。通過訪問 設定 > 訂閱 來升級您的帳戶。', + 'personalisation_paid_upgrade_vue' => '這是一個高階功能,需要付費訂閱才能啟用。透過瀏覽 設定 > 訂閱 來升級您的帳戶。', + + 'reminder_time_to_send' => 'Time of the day reminders will be sent', + 'reminder_time_to_send_help' => 'Your next reminder is scheduled to be sent on {dateTime}.', + + 'personalization_activity_type_category_title' => '活動分類', + 'personalization_activity_type_category_add' => '增加一個活動分類', + 'personalization_activity_type_category_table_name' => '名稱', + 'personalization_activity_type_category_description' => 'An activity with one of your contacts can have a type and a category type. Your account comes with a set of predefined category types by default, but you can customize these here.', + 'personalization_activity_type_category_table_actions' => '行動', + 'personalization_activity_type_category_modal_add' => '增加活動分類', + 'personalization_activity_type_category_modal_edit' => '編輯活動分類', + 'personalization_activity_type_category_modal_question' => 'What should we name this new category?', + 'personalization_activity_type_add_button' => '增加一個活動', + 'personalization_activity_type_modal_add' => '增加一個活動', + 'personalization_activity_type_modal_question' => 'What should we name this new activity type?', + 'personalization_activity_type_modal_edit' => '編輯活動', + 'personalization_activity_type_category_modal_delete' => '刪除活動分類', + 'personalization_activity_type_category_modal_delete_desc' => 'Are you sure you want to delete this category? Deleting it will delete all associated activity types. Activities that belong to this category will not be affected by this deletion.', + 'personalization_activity_type_modal_delete' => '刪除活動', + 'personalization_activity_type_modal_delete_desc' => '您真的要刪除這個活動嗎?', + 'personalization_activity_type_modal_delete_error' => '我們無法找到這個活動', + 'personalization_activity_type_category_modal_delete_error' => '我們無法找到這個活動分類', + + 'personalization_life_event_category_title' => '生活事件分類', + 'personalization_live_event_category_table_name' => 'Name', + 'personalization_life_event_category_description' => 'A life event can have a type and a category. Your account comes with a set of predefined categories and types by default, but you can customize life event types here.', + 'personalization_live_event_category_table_actions' => 'Actions', + 'personalization_life_event_type_add_button' => 'Add a new life event type', + 'personalization_life_event_type_modal_add' => 'Add a new life event type', + 'personalization_life_event_type_modal_question' => 'What should we name this new life event type?', + 'personalization_life_event_type_modal_edit' => 'Edit a life event type', + 'personalization_life_event_type_modal_delete' => 'Delete a life event type', + 'personalization_life_event_type_modal_delete_desc' => 'Are you sure you want to delete this life event type? Life events that belong to this type will be deleted by performing this action.', + 'personalization_life_event_type_modal_delete_error' => 'We can’t find this life event type.', + + 'personalization_life_event_category_work_education' => '工作與教育', + 'personalization_life_event_category_family_relationships' => '家庭與戀愛', + 'personalization_life_event_category_home_living' => '家與生活', + 'personalization_life_event_category_travel_experiences' => '旅行與經歷', + 'personalization_life_event_category_health_wellness' => '健康與飲食', + + 'personalization_life_event_type_new_job' => '新工作', + 'personalization_life_event_type_retirement' => '退休', + 'personalization_life_event_type_new_school' => '新學校', + 'personalization_life_event_type_study_abroad' => '留學', + 'personalization_life_event_type_volunteer_work' => '志願者工作', + 'personalization_life_event_type_published_book_or_paper' => '出版一本書或一篇論文', + 'personalization_life_event_type_military_service' => '兵役', + 'personalization_life_event_type_first_met' => '第一次見面', + 'personalization_life_event_type_new_relationship' => '新關係', + 'personalization_life_event_type_engagement' => '訂婚', + 'personalization_life_event_type_marriage' => '婚姻', + 'personalization_life_event_type_anniversary' => '週年紀念日', + 'personalization_life_event_type_expecting_a_baby' => '想要孩子', + 'personalization_life_event_type_new_child' => '新的孩子', + 'personalization_life_event_type_new_family_member' => '新的家庭成員', + 'personalization_life_event_type_new_pet' => '新寵物', + 'personalization_life_event_type_end_of_relationship' => '結束了一段關係', + 'personalization_life_event_type_loss_of_a_loved_one' => '失去心愛的人', + 'personalization_life_event_type_moved' => '搬家了', + 'personalization_life_event_type_bought_a_home' => '買了新房子', + 'personalization_life_event_type_home_improvement' => '裝修', + 'personalization_life_event_type_holidays' => '假日', + 'personalization_life_event_type_new_vehicle' => '新車', + 'personalization_life_event_type_new_roommate' => '新室友', + 'personalization_life_event_type_overcame_an_illness' => '熬過了疾病', + 'personalization_life_event_type_quit_a_habit' => '戒掉一個習慣', + 'personalization_life_event_type_new_eating_habits' => '新的飲食習慣', + 'personalization_life_event_type_weight_loss' => '減肥', + 'personalization_life_event_type_wear_glass_or_contact' => '開始戴眼鏡或隱形眼鏡', + 'personalization_life_event_type_broken_bone' => '骨折', + 'personalization_life_event_type_removed_braces' => '摘掉牙套', + 'personalization_life_event_type_surgery' => '動過手術', + 'personalization_life_event_type_dentist' => '做過牙科治療', + 'personalization_life_event_type_new_sport' => '開始參與一項新的運動', + 'personalization_life_event_type_new_hobby' => '開始一項新的興趣', + 'personalization_life_event_type_new_instrument' => 'Started learning a new instrument', + 'personalization_life_event_type_new_language' => '開始學一門新的外語', + 'personalization_life_event_type_tattoo_or_piercing' => '紋身或耳洞', + 'personalization_life_event_type_new_license' => '新駕照', + 'personalization_life_event_type_travel' => '旅行', + 'personalization_life_event_type_achievement_or_award' => '成就或獎項', + 'personalization_life_event_type_changed_beliefs' => '改變信仰', + 'personalization_life_event_type_first_word' => '第一次發言', + 'personalization_life_event_type_first_kiss' => '初吻', + + 'storage_title' => '儲存空間', + 'storage_account_info' => '您的賬戶大小為: :accountLimit Mb / 您目前已使用: :currentAccountSize Mb (約 :percentUsage%).', + 'storage_upgrade_notice' => '升級您的帳戶, 以便上傳文件和照片。', + 'storage_description' => '在這裡, 您可以看到上傳的有關您的聯絡人的所有文件和照片。', + + 'dav_title' => 'WebDAV', + 'dav_description' => '在這裡, 您可以找到所有設定, 以便為 Carddav 和 CalDAV 匯出使用 webdav 資源。', + 'dav_copy_help' => '複製到您的剪貼簿', + 'dav_clipboard_copied' => '值已複製到剪貼簿', + 'dav_url_base' => '所有CardDAV和CalDAV資源的基本 url:', + 'dav_connect_help' => '您可以在手機或計算機上使用此基本 url 連線您的聯絡人和/或日曆。', + 'dav_connect_help2' => 'Use your login (email) and create an API token as the password to authenticate.', + 'dav_url_carddav' => '用於聯絡資源的 CardDAV', + 'dav_url_caldav_birthdays' => '用於生日資源的 caldav url:', + 'dav_url_caldav_tasks' => '用於任務資源的 caldav url:', + 'dav_title_carddav' => 'CardDAV', + 'dav_title_caldav' => 'CalDAV', + 'dav_carddav_export' => '匯出一個檔案中的所有聯絡人', + 'dav_caldav_birthdays_export' => '在一個檔案中匯出所有生日', + 'dav_caldav_tasks_export' => '匯出一個檔案中的所有任務', + + 'archive_title' => 'Archive all of the contacts in your account', + 'archive_desc' => 'This will archive all of the contacts in your account.', + 'archive_cta' => 'Archive all of your contacts', + + 'logs_title' => 'Everything that has happened to this account', + 'logs_actor' => 'Actor', + 'logs_timestamp' => 'Timestamp', + 'logs_description' => 'Description', + 'logs_subject' => 'Subject', + 'logs_size' => 'Size (Kb)', + 'logs_object' => 'Object', +]; diff --git a/resources/lang/zh-TW/validation.php b/resources/lang/zh-TW/validation.php new file mode 100644 index 0000000..32fe39d --- /dev/null +++ b/resources/lang/zh-TW/validation.php @@ -0,0 +1,166 @@ + '您必須同意 :attribute。', + 'active_url' => ':attribute 不是一個有效的 URL 網址', + 'after' => ':attribute 必須是一個在 :date 之後的日期。', + 'after_or_equal' => ':attribute 必須是一個在 :date 或之後的日期。', + 'alpha' => ':attribute 只能包含字母。', + 'alpha_dash' => ':attribute 只能由字母、數字、減號(-)和底線(_)組成。', + 'alpha_num' => ':attribute 只允許包含字母和數字', + 'array' => ':attribute 必須是個陣列。', + 'before' => ':attribute 必須在 :date 之前', + 'before_or_equal' => ':attribute 必須在 :date 或之前', + 'between' => [ + 'numeric' => ':attribute 必須在 :min 和 :max 之間。', + 'file' => ':attribute 必須在 :min KB 到 :max KB 之間。', + 'string' => ':attribute 必須在 :min 到 :max 字元之間', + 'array' => ':attribute 必須在 :min 到 :max 個數目之間', + ], + 'boolean' => ':attribute 欄位必須為 true 或 false。', + 'confirmed' => ':attribute 與確認專案不匹配', + 'date' => ':attribute 不是個有效日期', + 'date_equals' => ':attribute 必須要等於 :date。', + 'date_format' => ':attribute 不符合 :format 的格式', + 'different' => ':attribute 和 :other 不能相同。', + 'digits' => ':attribute 必須是 :digits 數字', + 'digits_between' => ':attribute 必須是 :min - :max 位數字。', + 'dimensions' => ':attribute 的圖片無效', + 'distinct' => ':屬性欄位具有重複值。', + 'email' => ':attribute 必須是一個有效的電子郵件地址。', + 'ends_with' => ':attribute 必須以 :values 為結尾。', + 'exists' => '選擇的 :attribute 無效', + 'file' => ':attribute 必須是個檔案', + 'filled' => ':attribute 欄位必須有一個值', + 'gt' => [ + 'numeric' => ':attribute 必須大於 :value。', + 'file' => ':attribute 必須大於 :value KB。', + 'string' => ':attribute 必須多於 :value 個字元。', + 'array' => ':attribute 必須多於 :value 個元素。', + ], + 'gte' => [ + 'numeric' => ':attribute 必須大於或等於 :value。', + 'file' => ':attribute 必須大於或等於 :value KB。', + 'string' => ':attribute 必須多於或等於 :value 個字元。', + 'array' => ':attribute 必須多於或等於 :value 個元素。', + ], + 'image' => ':attribute 必須是圖片。', + 'in' => '選擇的 :attribute 無效', + 'in_array' => ':attribute 不在 :other 中。', + 'integer' => ':attribute 必須是整數', + 'ip' => ':attribute 必須是一個有效的 IP 位址', + 'ipv4' => ':attribute 必須是一個有效的 IPv4 位址', + 'ipv6' => ':attribute 必須是一個有效的 IPv6 位址', + 'json' => ':屬性必須是有效的JSON字串。', + 'lt' => [ + 'numeric' => ':attribute 必須小於 :value。', + 'file' => ':attribute 必須小於 :value KB。', + 'string' => ':attribute 必須少於 :value 個字元。', + 'array' => ':attribute 必須少於 :value 個元素。', + ], + 'lte' => [ + 'numeric' => ':attribute 必須小於或等於 :value。', + 'file' => ':attribute 必須小於或等於 :value KB。', + 'string' => ':attribute 必須少於或等於 :value 個字元。', + 'array' => ':attribute 必須少於或等於 :value 個元素。', + ], + 'max' => [ + 'numeric' => ':attribute 不大於 :max', + 'file' => ':attribute 不大於 :max kb', + 'string' => ':attribute 不大於 :max 字元', + 'array' => ':attribute 的數量不能超過 :max 個。', + ], + 'mimes' => ':attribute 檔案類型必須是 :values。', + 'mimetypes' => ':attribute 檔案類型必須是 :values。', + 'min' => [ + 'numeric' => ':attribute 最少是 :min', + 'file' => ':attribute 最小是 :min 千位元組', + 'string' => ':attribute 最少為 :min 個字元', + 'array' => ':attribute 至少為 :min 個', + ], + 'not_in' => '選擇的 :attribute 無效', + 'not_regex' => ':attribute 格式無效', + 'numeric' => ':attribute 必須是數字。', + 'password' => '密碼錯誤', + 'present' => ':attribute 為必填項。', + 'regex' => ':attribute 格式不對', + 'required' => ':attribute 欄位必填', + 'required_if' => ':attribute 欄位在 :other 是 :value 時是必須的', + 'required_unless' => ':attribute 是必須的除非 :other 在 :values 中。', + 'required_with' => '當 :values 不存在時, :attribute 是必需的', + 'required_with_all' => '當 :values 存在時 :attribute 不能為空。', + 'required_without' => '當 :values 不存在時, :attribute 是必填的。', + 'required_without_all' => '當沒有任何 :values 存在時, :attribute 欄位為必填項。', + 'same' => ':attribute 和 :other 必需匹配', + 'size' => [ + 'numeric' => ':attribute 必需是 :size', + 'file' => ':attribute 必需是 :size kb', + 'string' => ':attribute 必須包含 :size 個字元。', + 'array' => ':attribute 必須包含 :size 個項。', + ], + 'starts_with' => ':attribute 必須以 :values 為開頭。', + 'string' => ':attribute 必須是一個字串。', + 'timezone' => ':attribute 必須是個有效的區域。', + 'unique' => ':attribute 已經被佔用', + 'uploaded' => ':attribute 上傳失敗.', + 'url' => ':attribute 格式不對', + 'uuid' => ':attribute 必須是有效的 UUID。', + + /* + |-------------------------------------------------------------------------- + | Custom Validation Language Lines + |-------------------------------------------------------------------------- + | + | Here you may specify custom validation messages for attributes using the + | convention "attribute.rule" to name the lines. This makes it quick to + | specify a specific custom language line for a given attribute rule. + | + */ + + 'custom' => [ + 'attribute-name' => [ + 'rule-name' => 'custom-message', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Custom Validation Attributes + |-------------------------------------------------------------------------- + | + | The following language lines are used to swap our attribute placeholder + | with something more reader friendly such as "E-Mail Address" instead + | of "email". This simply helps us make our message more expressive. + | + */ + + 'attributes' => [], + + 'vue' => [ + 'max' => [ + 'numeric' => '{field}不能大於{max}', + 'string' => '{field}不能超過{max}個字元', + ], + 'required' => '{field} 必填', + 'url' => '{field} 的網址不正確', + ], + +]; diff --git a/resources/lang/zh.json b/resources/lang/zh.json new file mode 100644 index 0000000..ccfd54a --- /dev/null +++ b/resources/lang/zh.json @@ -0,0 +1,7 @@ +{ + "The :attribute must contain at least one uppercase and one lowercase letter.": ":attribute 必须至少包含一个大写字母和一个小写字母。", + "The :attribute must contain at least one letter.": ":attribute 必须包含至少一个字母。", + "The :attribute must contain at least one symbol.": ":attribute 必须至少包含一个符号。", + "The :attribute must contain at least one number.": ":attribute 必须至少包含一个数字。", + "The given :attribute has appeared in a data leak. Please choose a different :attribute.": "The given :attribute has appeared in a data leak. Please choose a different :attribute." +} diff --git a/resources/lang/zh/app.php b/resources/lang/zh/app.php new file mode 100644 index 0000000..15fcfc2 --- /dev/null +++ b/resources/lang/zh/app.php @@ -0,0 +1,571 @@ + '是', + 'no' => '否', + 'update' => '更新', + 'save' => '保存', + 'add' => '添加', + 'cancel' => '取消', + 'confirm' => '确认', + 'delete_confirm' => '确定吗?', + 'delete' => '删除', + 'edit' => '编辑', + 'upload' => '上传', + 'download' => '下载', + 'save_close' => '保存并关闭', + 'close' => '关闭', + 'copy' => '复制', + 'create' => '创建', + 'remove' => '删除', + 'revoke' => '撤销', + 'done' => '完成', + 'back' => '返回', + 'verify' => '验证', + 'new' => '新', + 'unknown' => '我不知道', + 'load_more' => '载入更多', + 'loading' => '加载中...', + 'with' => '与', + 'today' => '今天', + 'yesterday' => '昨天', + 'another_day' => '某一天', + 'date' => '日期', + 'type' => '类型', + 'zoom' => '放大', + 'upgrade' => '升级解锁', + 'percent_uploaded' => '已上传 {percent}%', + 'retry' => '重试', + 'filter' => '过滤列表', + 'go_back' => '后退', + 'file_selected' => '选择了 1 个文件...| 选择了 {count} 个文件...', + + 'application_title' => 'Monica – 您的私人社交关系管家', + 'application_description' => 'Monica是用来收集并管理您与亲朋好友之间的关系的得力助手。', + 'application_og_title' => '促进你们之间的感情。一个免费开源的面向亲朋好友的 CRM 工具', + + 'markdown_description' => '想用一种美观的方式格式化文本吗?我们以Markdown语法支持粗体、斜体、列表等样式。', + 'markdown_link' => '阅读文档', + + 'header_settings_link' => '设置', + 'header_logout_link' => '注销', + 'header_changelog_link' => '更新日志', + + 'main_nav_cta' => '联系人', + 'main_nav_dashboard' => '仪表盘', + 'main_nav_family' => '联系人', + 'main_nav_journal' => '日记', + 'main_nav_activities' => '活动', + 'main_nav_tasks' => '任务', + + 'footer_remarks' => '想发送反馈?', + 'footer_send_email' => '给我们发邮件', + 'footer_privacy' => '隐私条款', + 'footer_release' => '版本说明', + 'footer_newsletter' => '新闻简报', + 'footer_source_code' => '捐助', + 'footer_version' => '版本::version', + 'footer_new_version' => '有新版本的 Monica 可用', + + 'footer_modal_version_whats_new' => '新增内容', + 'footer_modal_version_release_away' => '您有一个最新发布版本可更新。您应该更新实例. |您已经有:number个版本没有更新,应该更新了。', + + 'breadcrumb_dashboard' => '仪表盘', + 'breadcrumb_list_contacts' => '联系人', + 'breadcrumb_archived_contacts' => '存档的联系人', + 'breadcrumb_journal' => '日记', + 'breadcrumb_settings' => '设置', + 'breadcrumb_settings_export' => '导出', + 'breadcrumb_settings_users' => '用户', + 'breadcrumb_settings_users_add' => '添加用户', + 'breadcrumb_settings_subscriptions' => '订阅', + 'breadcrumb_settings_import' => '导入', + 'breadcrumb_settings_import_report' => '导入报表', + 'breadcrumb_settings_import_upload' => '上传', + 'breadcrumb_settings_tags' => '标签', + 'breadcrumb_add_significant_other' => '添加其他重要', + 'breadcrumb_edit_significant_other' => '编辑其他重要', + 'breadcrumb_add_note' => '添加注释', + 'breadcrumb_edit_note' => '编辑注释', + 'breadcrumb_api' => 'API', + 'breadcrumb_dav' => 'DAV 资源', + 'breadcrumb_edit_introductions' => '你是怎么知道的', + 'breadcrumb_settings_personalization' => '个性化', + 'breadcrumb_settings_security' => '安全', + 'breadcrumb_settings_security_2fa' => '二次验证', + 'breadcrumb_profile' => ':name的资料', + + 'gender_male' => '男', + 'gender_female' => '女', + 'gender_none' => '保密', + 'gender_no_gender' => '无性别', + + 'error_title' => '糟糕! 出错了。', + 'error_unauthorized' => '你没有权限编辑此页', + 'error_user_account' => '此用户不属于此账号', + 'error_save' => '当储存数据时出现了一个错误', + 'error_try_again' => '出了点问题,请再试一次。', + 'error_id' => '错误代码::id', + 'error_unavailable' => '服务不可用', + 'error_maintenance' => '网站维护中,待会见。', + 'error_help' => '待会见!', + 'error_twitter' => '关注我们的推特来得知网站的最新消息!', + 'error_no_term' => '此实例尚无策略', + + 'default_save_success' => '数据已被保存', + + 'compliance_title' => '抱歉,打扰您一下', + 'compliance_desc' => '我们更新了用户协议 以及 隐私政策,您需要阅读并同意才能继续使用您的账号。', + 'compliance_desc_end' => '我们会保护您的隐私安全', + 'compliance_terms' => '我已阅读并同意', + + // Relationship types + // Yes, each relationship type has 8 strings associated with it. + // This is because we need to indicate the name of the relationship type, + // and also the name of the opposite side of this relationship (father/son), + // and then, the feminine version of the string. Finally, in some sentences + // in the UI, we need to include the name of the person we add the relationship + // to. + 'relationship_type_group_love' => '恋爱关系', + 'relationship_type_group_family' => '家庭关系', + 'relationship_type_group_friend' => '朋友关系', + 'relationship_type_group_work' => '工作关系', + 'relationship_type_group_other' => '其他关系', + + 'relationship_type_partner' => '搭档', + 'relationship_type_partner_female' => '搭档', + 'relationship_type_partner_male' => '爱人', + 'relationship_type_partner_with_name' => ':name的情侣', + 'relationship_type_partner_female_with_name' => ':name的搭档', + 'relationship_type_partner_male_with_name' => ':name的爱人', + + 'relationship_type_spouse' => '配偶', + 'relationship_type_spouse_female' => '妻子', + 'relationship_type_spouse_male' => '丈夫', + 'relationship_type_spouse_with_name' => ':name的配偶', + 'relationship_type_spouse_female_with_name' => ':name的妻子', + 'relationship_type_spouse_male_with_name' => ':name的丈夫', + + 'relationship_type_date' => '约会对象', + 'relationship_type_date_female' => '约会对象', + 'relationship_type_date_male' => '约会对象', + 'relationship_type_date_with_name' => ':name的约会对象', + 'relationship_type_date_female_with_name' => ':name的约会对象', + 'relationship_type_date_male_with_name' => ':name的约会对象', + + 'relationship_type_lover' => '情人', + 'relationship_type_lover_female' => '情人', + 'relationship_type_lover_male' => '情人', + 'relationship_type_lover_with_name' => ':name的情人', + 'relationship_type_lover_female_with_name' => ':name的情人', + 'relationship_type_lover_male_with_name' => ':name的情人', + + 'relationship_type_inlovewith' => '喜欢的人', + 'relationship_type_inlovewith_female' => '喜欢的人', + 'relationship_type_inlovewith_male' => '喜欢的人', + 'relationship_type_inlovewith_with_name' => ':name喜欢的人', + 'relationship_type_inlovewith_female_with_name' => ':name喜欢的人', + 'relationship_type_inlovewith_male_with_name' => ':name喜欢的人', + + 'relationship_type_lovedby' => '追求者', + 'relationship_type_lovedby_female' => '追求者', + 'relationship_type_lovedby_male' => '追求者', + 'relationship_type_lovedby_with_name' => ':name的追求者', + 'relationship_type_lovedby_female_with_name' => ':name的追求者', + 'relationship_type_lovedby_male_with_name' => ':name暗恋的人', + + 'relationship_type_ex' => '前伴侣', + 'relationship_type_ex_female' => '前女友', + 'relationship_type_ex_male' => '前男友', + 'relationship_type_ex_with_name' => ':name的前伴侣', + 'relationship_type_ex_female_with_name' => ':name的前女友', + 'relationship_type_ex_male_with_name' => ':name的前男友', + + 'relationship_type_parent' => '父母', + 'relationship_type_parent_female' => '母亲', + 'relationship_type_parent_male' => '父亲', + 'relationship_type_parent_with_name' => ':name的父母', + 'relationship_type_parent_female_with_name' => ':name的母亲', + 'relationship_type_parent_male_with_name' => ':name的父亲', + + 'relationship_type_child' => '子女', + 'relationship_type_child_female' => '女儿', + 'relationship_type_child_male' => '儿子', + 'relationship_type_child_with_name' => ':name的子女', + 'relationship_type_child_female_with_name' => ':name的女人', + 'relationship_type_child_male_with_name' => ':name的儿子', + + 'relationship_type_stepparent' => '继父/继母', + 'relationship_type_stepparent_female' => '继母', + 'relationship_type_stepparent_male' => '继父', + 'relationship_type_stepparent_with_name' => ':name的继父母', + 'relationship_type_stepparent_female_with_name' => ':name的继母', + 'relationship_type_stepparent_male_with_name' => ':name的继父', + + 'relationship_type_stepchild' => '继子女', + 'relationship_type_stepchild_female' => '继女', + 'relationship_type_stepchild_male' => '继子', + 'relationship_type_stepchild_with_name' => ':name的继子女', + 'relationship_type_stepchild_female_with_name' => ':name的继女', + 'relationship_type_stepchild_male_with_name' => ':name的继子', + + 'relationship_type_sibling' => '兄弟姐妹', + 'relationship_type_sibling_female' => '姐妹', + 'relationship_type_sibling_male' => '兄弟', + 'relationship_type_sibling_with_name' => ':name的兄弟姐妹', + 'relationship_type_sibling_female_with_name' => ':name的姐妹', + 'relationship_type_sibling_male_with_name' => ':name的兄弟', + + 'relationship_type_grandparent' => '祖父母', + 'relationship_type_grandparent_female' => '祖母', + 'relationship_type_grandparent_male' => '祖父', + 'relationship_type_grandparent_with_name' => ':name的祖父母', + 'relationship_type_grandparent_female_with_name' => ':name的祖母', + 'relationship_type_grandparent_male_with_name' => ':name的祖父', + + 'relationship_type_grandchild' => '(外)孙子女', + 'relationship_type_grandchild_female' => '(外)孙女', + 'relationship_type_grandchild_male' => '(外)孙子', + 'relationship_type_grandchild_with_name' => ':name的(外)孙子女', + 'relationship_type_grandchild_female_with_name' => ':name的(外)孙女', + 'relationship_type_grandchild_male_with_name' => ':name的(外)孙子', + + 'relationship_type_uncle' => '叔叔', + 'relationship_type_uncle_female' => '阿姨', + 'relationship_type_uncle_male' => '叔叔', + 'relationship_type_uncle_with_name' => ':name的叔叔', + 'relationship_type_uncle_female_with_name' => ':name的阿姨', + 'relationship_type_uncle_male_with_name' => ':name的叔叔', + + 'relationship_type_nephew' => '外甥', + 'relationship_type_nephew_female' => '外甥女', + 'relationship_type_nephew_male' => '外甥', + 'relationship_type_nephew_with_name' => ':name的外甥', + 'relationship_type_nephew_female_with_name' => ':name的外甥女', + 'relationship_type_nephew_male_with_name' => ':name的外甥', + + 'relationship_type_cousin' => '堂兄弟', + 'relationship_type_cousin_female' => '堂姐妹', + 'relationship_type_cousin_male' => '堂兄弟', + 'relationship_type_cousin_with_name' => ':name的堂兄弟', + 'relationship_type_cousin_female_with_name' => ':name的堂姐妹', + 'relationship_type_cousin_male_with_name' => ':name的堂兄弟', + + 'relationship_type_godfather' => '义父母', + 'relationship_type_godfather_female' => '神母', + 'relationship_type_godfather_male' => '义父', + 'relationship_type_godfather_with_name' => ':name的义父', + 'relationship_type_godfather_female_with_name' => ':name的神母', + 'relationship_type_godfather_male_with_name' => ':name的义父', + + 'relationship_type_godson' => '义子', + 'relationship_type_godson_female' => '义女', + 'relationship_type_godson_male' => '义子', + 'relationship_type_godson_with_name' => ':name的义子', + 'relationship_type_godson_female_with_name' => ':name的义女', + 'relationship_type_godson_male_with_name' => ':name的义子', + + 'relationship_type_friend' => '朋友', + 'relationship_type_friend_female' => '朋友', + 'relationship_type_friend_male' => '朋友', + 'relationship_type_friend_with_name' => ':name的朋友', + 'relationship_type_friend_female_with_name' => ':name的朋友', + 'relationship_type_friend_male_with_name' => ':name的朋友', + + 'relationship_type_bestfriend' => '基友', + 'relationship_type_bestfriend_female' => '闺密', + 'relationship_type_bestfriend_male' => '好友', + 'relationship_type_bestfriend_with_name' => ':name的基友', + 'relationship_type_bestfriend_female_with_name' => ':name的闺密', + 'relationship_type_bestfriend_male_with_name' => ':name的好友', + + 'relationship_type_colleague' => '同事', + 'relationship_type_colleague_female' => '同事', + 'relationship_type_colleague_male' => '同事', + 'relationship_type_colleague_with_name' => ':name的同事', + 'relationship_type_colleague_female_with_name' => ':name的同事', + 'relationship_type_colleague_male_with_name' => ':name的同事', + + 'relationship_type_boss' => '上司', + 'relationship_type_boss_female' => '上司', + 'relationship_type_boss_male' => '上司', + 'relationship_type_boss_with_name' => ':name的上司', + 'relationship_type_boss_female_with_name' => ':name的上司', + 'relationship_type_boss_male_with_name' => ':name的上司', + + 'relationship_type_subordinate' => '下属', + 'relationship_type_subordinate_female' => '下属', + 'relationship_type_subordinate_male' => '下属', + 'relationship_type_subordinate_with_name' => ':name的下属', + 'relationship_type_subordinate_female_with_name' => ':name的下属', + 'relationship_type_subordinate_male_with_name' => ':name的下属', + + 'relationship_type_mentor' => '老师', + 'relationship_type_mentor_female' => '老师', + 'relationship_type_mentor_male' => '老师', + 'relationship_type_mentor_with_name' => ':name的老师', + 'relationship_type_mentor_female_with_name' => ':name的老师', + 'relationship_type_mentor_male_with_name' => ':name的老师', + + 'relationship_type_protege' => '门徒', + 'relationship_type_protege_female' => 'protégé', + 'relationship_type_protege_male' => 'protégé', + 'relationship_type_protege_with_name' => ':name’s protégé', + 'relationship_type_protege_female_with_name' => ':name’s protégé', + 'relationship_type_protege_male_with_name' => ':name’s protégé', + + 'relationship_type_ex_husband' => '前夫', + 'relationship_type_ex_husband_female' => '前妻', + 'relationship_type_ex_husband_male' => '前夫', + 'relationship_type_ex_husband_with_name' => ':name的前配偶', + 'relationship_type_ex_husband_female_with_name' => ':name的前妻', + 'relationship_type_ex_husband_male_with_name' => ':name的前夫', + + // emotions + 'emotion_primary_love' => '喜爱', + 'emotion_primary_joy' => '开心', + 'emotion_primary_surprise' => '惊讶', + 'emotion_primary_anger' => '生气', + 'emotion_primary_sadness' => '悲伤', + 'emotion_primary_fear' => '恐惧', + + 'emotion_secondary_affection' => '感情', + 'emotion_secondary_lust' => '欲望', + 'emotion_secondary_longing' => '渴望', + 'emotion_secondary_cheerfulness' => '兴高采烈', + 'emotion_secondary_zest' => '热情', + 'emotion_secondary_contentment' => '满足', + 'emotion_secondary_pride' => '骄傲', + 'emotion_secondary_optimism' => '乐观', + 'emotion_secondary_enthrallment' => '沉迷', + 'emotion_secondary_relief' => '如释重负', + 'emotion_secondary_surprise' => '惊讶', + 'emotion_secondary_irritation' => '刺激', + 'emotion_secondary_exasperation' => '恼怒', + 'emotion_secondary_rage' => '狂怒', + 'emotion_secondary_disgust' => '厌恶', + 'emotion_secondary_envy' => '嫉妒', + 'emotion_secondary_suffering' => '痛苦', + 'emotion_secondary_sadness' => '悲伤', + 'emotion_secondary_disappointment' => '失望', + 'emotion_secondary_shame' => '耻辱', + 'emotion_secondary_neglect' => '忽视', + 'emotion_secondary_sympathy' => '同情', + 'emotion_secondary_horror' => '恐怖', + 'emotion_secondary_nervousness' => '紧张', + + 'emotion_adoration' => '崇拜', + 'emotion_affection' => '感情', + 'emotion_love' => '喜爱', + 'emotion_fondness' => '宠爱', + 'emotion_liking' => '喜欢', + 'emotion_attraction' => '吸引', + 'emotion_caring' => '关心', + 'emotion_tenderness' => '柔情', + 'emotion_compassion' => '同情', + 'emotion_sentimentality' => '多愁善感', + 'emotion_arousal' => '激励', + 'emotion_desire' => '期望', + 'emotion_lust' => '欲望', + 'emotion_passion' => '热情', + 'emotion_infatuation' => '迷恋', + 'emotion_longing' => '渴望', + 'emotion_amusement' => '娱乐', + 'emotion_bliss' => '欣喜若狂', + 'emotion_cheerfulness' => '兴高采烈', + 'emotion_gaiety' => '欢乐', + 'emotion_glee' => '高兴', + 'emotion_jolliness' => '乔利', + 'emotion_joviality' => '快乐', + 'emotion_joy' => '开心', + 'emotion_delight' => '喜悦', + 'emotion_enjoyment' => '享受', + 'emotion_gladness' => '喜悦', + 'emotion_happiness' => '快乐', + 'emotion_jubilation' => '喜庆', + 'emotion_elation' => '兴高采烈', + 'emotion_satisfaction' => '称心如意', + 'emotion_ecstasy' => '狂喜', + 'emotion_euphoria' => '过度兴奋', + 'emotion_enthusiasm' => '热情高涨', + 'emotion_zeal' => '狂热', + 'emotion_zest' => '热情', + 'emotion_excitement' => '兴奋', + 'emotion_thrill' => '快感', + 'emotion_exhilaration' => '不亦乐乎', + 'emotion_contentment' => '满足', + 'emotion_pleasure' => '快乐', + 'emotion_pride' => '骄傲', + 'emotion_eagerness' => '渴望', + 'emotion_hope' => '希望', + 'emotion_optimism' => '乐观', + 'emotion_enthrallment' => '沉迷', + 'emotion_rapture' => '狂喜', + 'emotion_relief' => '如释重负', + 'emotion_amazement' => '惊奇', + 'emotion_surprise' => '惊讶', + 'emotion_astonishment' => '惊讶', + 'emotion_aggravation' => '恶化', + 'emotion_irritation' => '刺激', + 'emotion_agitation' => '鼓动', + 'emotion_annoyance' => '烦恼', + 'emotion_grouchiness' => '发牢骚', + 'emotion_grumpiness' => '脾气暴躁', + 'emotion_exasperation' => '恼怒', + 'emotion_frustration' => '受挫', + 'emotion_anger' => '生气', + 'emotion_rage' => '狂怒', + 'emotion_outrage' => '愤怒', + 'emotion_fury' => '愤怒', + 'emotion_wrath' => '暴怒', + 'emotion_hostility' => '敌意', + 'emotion_ferocity' => '凶猛', + 'emotion_bitterness' => '辛酸', + 'emotion_hate' => '讨厌', + 'emotion_loathing' => '嫌恶', + 'emotion_scorn' => '蔑视', + 'emotion_spite' => '怨恨', + 'emotion_vengefulness' => '报复', + 'emotion_dislike' => '不喜欢', + 'emotion_resentment' => '怨恨', + 'emotion_disgust' => '厌恶', + 'emotion_revulsion' => '反感', + 'emotion_contempt' => '轻蔑', + 'emotion_envy' => '嫉妒', + 'emotion_jealousy' => '嫉妒', + 'emotion_agony' => '痛苦', + 'emotion_suffering' => '痛苦', + 'emotion_hurt' => '伤心', + 'emotion_anguish' => '生不如死', + 'emotion_depression' => '忧郁', + 'emotion_despair' => '绝望', + 'emotion_hopelessness' => '无可救药', + 'emotion_gloom' => '沮丧', + 'emotion_glumness' => '阴沉', + 'emotion_sadness' => '悲伤', + 'emotion_unhappiness' => '不幸', + 'emotion_grief' => '悲痛', + 'emotion_sorrow' => '悲患', + 'emotion_woe' => '荣辱与共', + 'emotion_misery' => '痛苦', + 'emotion_melancholy' => '悲伤', + 'emotion_dismay' => '沮丧', + 'emotion_disappointment' => '失望', + 'emotion_displeasure' => '不满', + 'emotion_guilt' => '内疚', + 'emotion_shame' => '耻辱', + 'emotion_regret' => '后悔', + 'emotion_remorse' => '悔恨', + 'emotion_alienation' => '异化', + 'emotion_isolation' => '分离', + 'emotion_neglect' => '忽视', + 'emotion_loneliness' => '孤独', + 'emotion_rejection' => '拒绝', + 'emotion_homesickness' => '乡愁', + 'emotion_defeat' => '失败', + 'emotion_dejection' => '沮丧', + 'emotion_insecurity' => '紧张', + 'emotion_embarrassment' => '尴尬', + 'emotion_humiliation' => '屈辱', + 'emotion_insult' => '侮辱', + 'emotion_pity' => '可惜', + 'emotion_sympathy' => '同情', + 'emotion_alarm' => '警觉', + 'emotion_shock' => '震撼', + 'emotion_fear' => '恐惧', + 'emotion_fright' => '惊吓', + 'emotion_horror' => '恐怖', + 'emotion_terror' => '恐怖', + 'emotion_panic' => '恐慌', + 'emotion_hysteria' => '歇斯底里', + 'emotion_mortification' => '屈辱', + 'emotion_anxiety' => '焦虑', + 'emotion_nervousness' => '紧张', + 'emotion_tenseness' => '神经紧绷', + 'emotion_uneasiness' => '不安', + 'emotion_apprehension' => '忧虑', + 'emotion_worry' => '担心', + 'emotion_distress' => '苦恼', + 'emotion_dread' => '惊恐', + + // weather + 'weather_sunny' => '晴天', + 'weather_clear' => '万里无云', + 'weather_clear-day' => '晴朗', + 'weather_clear-night' => '晴朗的夜晚', + 'weather_light-drizzle' => '小雨', + 'weather_patchy-light-drizzle' => '局部小雨', + 'weather_patchy-light-rain' => '局部下雨', + 'weather_light-rain' => '小雨', + 'weather_moderate-rain-at-times' => '有时中雨', + 'weather_moderate-rain' => '中雨', + 'weather_patchy-rain-possible' => '可能有局部降雨', + 'weather_heavy-rain-at-times' => '有时大雨', + 'weather_heavy-rain' => '大雨', + 'weather_light-freezing-rain' => '小冻雨', + 'weather_moderate-or-heavy-freezing-rain' => '中度或重度冻雨', + 'weather_light-sleet' => '小雨夹雪', + 'weather_moderate-or-heavy-rain-shower' => '中到大雨,阵雨', + 'weather_light-rain-shower' => '小雨,阵雨', + 'weather_torrential-rain-shower' => '暴雨,阵雨', + 'weather_rain' => '雨', + 'weather_snow' => '雪', + 'weather_blowing-snow' => '高吹雪', + 'weather_patchy-light-snow' => '局部小雪', + 'weather_light-snow' => '小雪', + 'weather_patchy-moderate-snow' => '局部中雪', + 'weather_moderate-snow' => '中雪', + 'weather_patchy-heavy-snow' => '局部大雪', + 'weather_heavy-snow' => '大雪', + 'weather_light-snow-showers' => '小阵雪', + 'weather_moderate-or-heavy-snow-showers' => '中到大阵雪', + 'weather_patchy-snow-possible' => '可能有局部降雪', + 'weather_patchy-sleet-possible' => '可能有局部雨夹雪', + 'weather_moderate-or-heavy-sleet' => '中到大雨夹雪', + 'weather_light-sleet-showers' => '小阵雨夹雪', + 'weather_moderate-or-heavy-sleet-showers' => '中到大阵雨夹雪', + 'weather_sleet' => '雨夹雪', + 'weather_wind' => '风', + 'weather_fog' => '雾', + 'weather_freezing-fog' => '冻雾', + 'weather_mist' => '雾', + 'weather_blizzard' => '暴风雪', + 'weather_overcast' => '阴天', + 'weather_cloudy' => '多云', + 'weather_partly-cloudy-day' => '局部多云', + 'weather_partly-cloudy-night' => '局部多云', + 'weather_freezing-drizzle' => '冻毛毛雨', + 'weather_heavy-freezing-drizzle' => '冷冻大雨', + 'weather_patchy-freezing-drizzle-possible' => '可能有局部冻毛毛雨', + 'weather_ice-pellets' => '冰雹', + 'weather_light-showers-of-ice-pellets' => '阵雨加冰雹', + 'weather_moderate-or-heavy-showers-of-ice-pellets' => '中等或重度的冰雹阵雨', + 'weather_thundery-outbreaks-possible' => '雷雨可能', + 'weather_patchy-light-rain-with-thunder' => 'Patchy light rain with thunder', + 'weather_moderate-or-heavy-rain-with-thunder' => 'Moderate or heavy rain with thunder', + 'weather_patchy-light-snow-with-thunder' => 'Patchy light snow with thunder', + 'weather_moderate-or-heavy-snow-with-thunder' => 'Moderate or heavy snow with thunder', + 'weather_current_temperature_celsius' => ':temperature °C', + 'weather_current_temperature_fahrenheit' => ':temperature °F', + 'weather_current_title' => '当前天气', + + // dav + 'dav_contacts' => '名片', + 'dav_contacts_description' => ':name的名片', + 'dav_birthdays' => '生日', + 'dav_birthdays_description' => ':name的名片生日', + 'dav_tasks' => '任务', + 'dav_tasks_description' => ':name的任务', + + // contact list + 'contact_list_avatar' => '头像', + 'contact_list_name' => '联系人', + 'contact_list_description' => '描述', + +]; diff --git a/resources/lang/zh/auth.php b/resources/lang/zh/auth.php new file mode 100644 index 0000000..e3db4d5 --- /dev/null +++ b/resources/lang/zh/auth.php @@ -0,0 +1,89 @@ + '您输入的信息与我们的记录不匹配。', + 'throttle' => '登录失败次数太多。请 :seconds 后再试。', + 'not_authorized' => '您无权执行此操作', + 'signup_disabled' => '注册当前已停用', + 'signup_error' => '尝试注册用户时出错', + 'back_homepage' => '回到主页', + 'mfa_auth_otp' => '使用二次验证设备进行认证', + 'mfa_auth_webauthn' => '使用安全钥匙验证(WebAuthn)', + '2fa_title' => '二次验证', + '2fa_wrong_validation' => '二次验证失败', + '2fa_one_time_password' => '验证码', + '2fa_recuperation_code' => '输入二次验证恢复码', + '2fa_one_time_or_recuperation' => '输入两步验证代码或恢复代码', + '2fa_otp_help' => '打开您的二次验证APP并复制验证码', + + 'login_to_account' => '登录您的账号', + 'login_with_recovery' => '使用恢复代码登录', + 'login_again' => '请再次登录您的账号', + 'email' => '电子邮箱', + 'password' => '密码', + 'recovery' => '恢复代码', + 'login' => '登录', + 'button_remember' => '记住我', + 'password_forget' => '忘记密码?', + 'password_reset' => '重置密码', + 'use_recovery' => '或者您可以使用 恢复代码', + 'signup_no_account' => '没有账号?', + 'signup' => '注册', + 'create_account' => '单击此处 注册', + 'change_language_title' => '更改语言:', + 'change_language' => '更改语言至::lang', + + 'password_reset_title' => '重置密码', + 'password_reset_email' => '电子邮箱', + 'password_reset_send_link' => '发送重置链接', + 'password_reset_password' => '密码', + 'password_reset_password_confirm' => '确认密码', + 'password_reset_action' => '重置密码', + 'password_reset_email_content' => '单击此处来重置密码:', + + 'register_title_welcome' => '欢迎注册您的私人社交关系管家 - Monica', + 'register_create_account' => '您需要一个账号来使用Monica', + 'register_title_create' => '创建您的Monica账号', + 'register_login' => '已经有账号了?点此登录', + 'register_email' => '请输入一个有效的邮箱', + 'register_email_example' => 'example@example.com', + 'register_firstname' => '名字', + 'register_firstname_example' => '例:小明', + 'register_lastname' => '姓氏', + 'register_lastname_example' => '例:王', + 'register_password' => '密码', + 'register_password_example' => '键入密码...', + 'register_password_confirmation' => '重复密码', + 'register_action' => '注册', + 'register_policy' => '我已阅读并同意 隐私政策用户协议', + 'register_invitation_email' => '为了安全,请您输入邀请人的电子邮件地址。这可以在受邀邮件中找到', + + 'confirmation_title' => '验证您的电子邮件地址', + 'confirmation_fresh' => '一条新的验证链接已经发送到您的邮箱', + 'confirmation_check' => '在您继续之前,请检查您的邮箱以获得验证链接。', + 'confirmation_request_another' => '如果您没有收到电子邮件 , 请单击此处重新发送。', + + 'confirmation_again' => '如果要更改电子邮件地址, 可以 单击此处。', + 'email_change_current_email' => '当前邮件地址:', + 'email_change_title' => '更换您的电子邮箱', + 'email_change_new' => '新邮箱地址:', + 'email_changed' => '您的电子邮箱已更换,请检查您的收件箱来验证电子邮件地址。', +]; diff --git a/resources/lang/zh/changelog.php b/resources/lang/zh/changelog.php new file mode 100644 index 0000000..bca4ded --- /dev/null +++ b/resources/lang/zh/changelog.php @@ -0,0 +1,12 @@ + '更新日志', + 'note' => '注:很抱歉,当前页面只支持英文展示。', +]; diff --git a/resources/lang/zh/dashboard.php b/resources/lang/zh/dashboard.php new file mode 100644 index 0000000..4a133f3 --- /dev/null +++ b/resources/lang/zh/dashboard.php @@ -0,0 +1,42 @@ + '欢迎登录账号', + 'dashboard_blank_description' => 'Monica是一个记录你所有关心的人及与其交互信息的地方', + 'dashboard_blank_cta' => '添加您的第一个联系人', + 'dashboard_blank_illustration' => '插画: Freepik', + + 'notes_title' => '您还没有任何便签。', + + 'tab_recent_calls' => '最近通话', + 'tab_favorite_notes' => '收藏便签', + 'tab_calls_blank' => '您还没有电话拨打记录。', + 'tab_debts' => '债务', + 'tab_debts_blank' => '您还没有添加债务信息。', + 'tab_tasks' => '任务', + 'tab_tasks_blank' => '你还没有任何任务', + + 'tasks_add_task_placeholder' => '这个任务是关于什么的?', + 'tasks_tab_your_contacts' => '与任务相关的联系人', + 'tasks_tab_your_tasks' => '您的任务', + 'tasks_add_note' => '按回车来添加任务', + 'task_add_cta' => '添加任务', + + 'debts_you_owe' => '待还金额', + + 'statistics_contacts' => '联系人', + 'statistics_activities' => '活动', + 'statistics_gifts' => '礼物', + + 'reminders_next_months' => '近三个月的活动', + 'reminders_none' => '本月尚无提醒事项.', + + 'product_changes' => '更新日志', + 'product_view_details' => '查看详情', +]; diff --git a/resources/lang/zh/format.php b/resources/lang/zh/format.php new file mode 100644 index 0000000..5cc1a35 --- /dev/null +++ b/resources/lang/zh/format.php @@ -0,0 +1,36 @@ + 'Y M d H:i', + 'short_date_year' => 'Y M d', + 'short_date' => 'M d', + 'short_month' => 'M', + 'short_month_year' => 'Y M', + 'short_day' => 'D', + 'full_date_year' => 'Y F d', + 'full_month' => 'F', + 'full_month_year' => 'Y F', + 'full_hour' => 'h.i A', + + 'short_text' => '{text}…', +]; diff --git a/resources/lang/zh/journal.php b/resources/lang/zh/journal.php new file mode 100644 index 0000000..30750a6 --- /dev/null +++ b/resources/lang/zh/journal.php @@ -0,0 +1,38 @@ + '今天过得怎么样?你可以每天给它一次评价。', + 'journal_come_back' => '谢谢. 明天再来给你的一天评价一下。', + 'journal_description' => '注意: 记录里列出了全部手动记录的条目, 以及您与您的联系人进行的活动等自动条目。虽然可以手动删除记录条目, 但必须直接在 "联系人" 页上进行删除。', + 'journal_add' => '添加日记条目', + 'journal_edit' => '编辑日记条目', + 'journal_empty' => '暂无日记', + 'journal_created_at' => '创建于 {date}', + 'journal_created_automatically' => '自动创建', + 'journal_entry_type_journal' => '记录条目', + 'journal_entry_type_activity' => '活动', + 'journal_entry_rate' => '评价你的一天。', + 'journal_add_comment' => '是否要添加注释 (可选)?', + 'journal_show_comment' => '显示评论', + 'entry_delete_success' => '记录条目已成功删除。', + 'journal_add_title' => '标题 (可选)', + 'journal_add_date' => '日期', + 'journal_add_post' => '内容', + 'journal_add_cta' => '保存', + 'journal_blank_cta' => '添加您的第一个记录条目', + 'journal_blank_description' => '记录允许您编写发生在您身上的事件, 并记住它们。', + 'delete_confirmation' => '您确定要删除此条目吗?', + 'apply_filter' => 'Apply filter', + 'start_date' => 'Start Date', + 'end_date' => 'End Date', + 'per_page' => 'Per Page', + 'sort_order' => 'Sort By Created At', + 'ascending' => 'Ascending', + 'descending' => 'Descending', +]; diff --git a/resources/lang/zh/logs.php b/resources/lang/zh/logs.php new file mode 100644 index 0000000..31fb133 --- /dev/null +++ b/resources/lang/zh/logs.php @@ -0,0 +1,29 @@ + '已创建联系人', + 'settings_log_contact_created_with_name' => '添加 :name 为联系人', + + // contat description update + 'contact_log_contact_description_updated' => '已更新描述', + 'settings_log_contact_description_updated_with_name' => '更新了 :name 的描述', + + // contact description clear + 'contact_log_contact_description_cleared' => '已清除描述', + 'settings_log_contact_description_cleared_with_name' => '已清除 :name 的描述', + + // contact work information update + 'contact_log_contact_work_updated' => '更新工作信息.', + 'settings_log_contact_work_updated_with_name' => '更新了 :name 的工作信息', + + // company created + 'settings_log_company_created' => '创建了一个名为 :name 的公司', +]; diff --git a/resources/lang/zh/mail.php b/resources/lang/zh/mail.php new file mode 100644 index 0000000..b38d41b --- /dev/null +++ b/resources/lang/zh/mail.php @@ -0,0 +1,53 @@ + '提醒:contact', + 'greetings' => '您好:username', + 'want_reminded_of' => '您的提醒事项::reason', + 'for' => '为::name', + 'comment' => '备注::comment', + 'footer_contact_info' => '添加、查看、完成和更改有关此联系人的信息:', + 'footer_contact_info2' => '看看 :name的个人资料', + 'footer_contact_info2_link' => '看看:name的个人资料: :url', + + 'notification_subject_line' => '您有一个即将进行的活动', + 'notification_description' => '在:count天后(:date),将有以下事件发生:', + + 'stay_in_touch_subject_line' => '您的『常联系』提醒 :name', + 'stay_in_touch_subject_description' => '您的常联系提醒: 每 :frequency 天 与 :name 联系.', + + 'notifications_whoops' => '糟了!', + 'notifications_hello' => '您好!', + 'notifications_regards' => '此致', + 'notifications_footer' => '如果您无法点击 ":actionText" 按钮, 复制以下链接至浏览器打开: [:actionURL](:actionURL)', + 'notifications_rights' => '版权所有', + + 'confirmation_email_title' => 'Monica – Email 认证', + 'confirmation_email_intro'=> '请点击以下按钮来完成Email认证', + 'confirmation_email_button' => 'Email 认证', + 'confirmation_email_bottom' => '如果不是您本人进行的创建帐户操作,请忽略这封邮件。', + + 'password_reset_title' => 'Monica — 重置密码通知', + 'password_reset_intro' => '您收到此邮件是因为我们收到了您的密码重置请求', + 'password_reset_button' => '重置密码', + 'password_reset_expiration' => '此密码重置链接将在 :count 分钟后过期', + 'password_reset_bottom' => '如果您没有请求重置密码,请忽略这封邮件。', + + 'invitation_title' => 'Monica — 您收到 :name 的邀请', + 'invitation_intro' => '您已被:name (:email)邀请使用 Monica, 个人社交关系管理工具。', + 'invitation_link' => '要接受邀请,请点击下面的链接:', + 'invitation_button' => '接受邀请', + 'invitation_expiration' => '此链接将在 :count 天后过期', + + 'export_title' => '您的导出已就绪', + 'export_description' => '您请求的导出内容 :date. 已经可以下载了.', + 'export_download' => '下载导出内容', + +]; diff --git a/resources/lang/zh/pagination.php b/resources/lang/zh/pagination.php new file mode 100644 index 0000000..2331f27 --- /dev/null +++ b/resources/lang/zh/pagination.php @@ -0,0 +1,25 @@ + '❮ 上一页', + 'next' => '下一页 ❯', + +]; diff --git a/resources/lang/zh/passwords.php b/resources/lang/zh/passwords.php new file mode 100644 index 0000000..8b73b97 --- /dev/null +++ b/resources/lang/zh/passwords.php @@ -0,0 +1,30 @@ + '您的密码已重置!', + 'sent' => '如果您输入的电子邮件存在于我们的记录中, 密码重置链接将被发送至改邮箱。', + 'token' => '密码重置秘钥无效。', + 'user' => '如果您输入的电子邮件存在于我们的记录中, 密码重置链接将被发送至该邮箱。', + 'changed' => '密码修改成功', + 'invalid' => '您输入的密码不正确。', + 'throttled' => '请稍候再试', + +]; diff --git a/resources/lang/zh/people.php b/resources/lang/zh/people.php new file mode 100644 index 0000000..9e27469 --- /dev/null +++ b/resources/lang/zh/people.php @@ -0,0 +1,539 @@ + '联系人未找到', + 'people_list_number_kids' => ':count 个孩子', + 'people_list_last_updated' => '最近更新:', + 'people_list_number_reminders' => ':count 个提醒', + 'people_list_blank_title' => '您还没有任何联系人', + 'people_list_blank_cta' => '添加某人', + 'people_list_sort' => '排序', + 'people_list_stats' => ':count 个联系人', + 'people_list_firstnameAZ' => '以名字A → Z排序', + 'people_list_firstnameZA' => '以名字 Z → A排序', + 'people_list_lastnameAZ' => '以姓A → Z排序', + 'people_list_lastnameZA' => '以姓Z → A排序', + 'people_list_lastactivitydateNewtoOld' => '以最后活动日期从近到远排序', + 'people_list_lastactivitydateOldtoNew' => '以最后活动日期从远到近排序', + 'people_list_filter_tag' => '拥有以下标签的联系人:', + 'people_list_clear_filter' => '清除筛选', + 'people_list_contacts_per_tags' => ':count 个联系人', + 'people_list_show_dead' => '显示已故人员 (:count)', + 'people_list_hide_dead' => '隐藏已故人员 (:count)', + 'people_search' => '搜索联系人', + 'people_search_no_results' => '未找到任何结果', + 'people_search_next' => '下一页', + 'people_search_prev' => '上一页', + 'people_search_rows_per_page' => '每页行数', + 'people_search_of' => '/', + 'people_search_page' => '页', + 'people_search_all' => '所有', + 'people_add_new' => '添加新的联系人', + 'people_list_account_usage' => '您的账户已联系人使用情况是::current/:limit ', + 'people_list_account_upgrade_title' => '升级您的帐户, 以打开全部功能。', + 'people_list_account_upgrade_cta' => '立即升级', + 'people_list_untagged' => '查看未加标签的联系人', + 'people_list_filter_untag' => '所有未加标签的联系人', + 'archived_contact_readonly' => '无法编辑已归档的联系人,请先解除归档。', + + // people add + 'people_add_title' => '添加一位新的联系人', + 'people_add_missing' => '列表为空——现在添加一个新联系人', + 'people_add_firstname' => '名字', + 'people_add_middlename' => '中间名(可选)', + 'people_add_lastname' => '姓氏(选填)', + 'people_add_email' => '邮箱(选填)', + 'people_add_nickname' => '昵称(选填)', + 'people_add_cta' => '添加', + 'people_save_and_add_another_cta' => '提交并添加其他人', + 'people_add_success' => ':name 已成功创建', + 'people_add_gender' => '性别', + 'people_delete_success' => '联系人已被删除', + 'people_delete_message' => '删除联系人', + 'people_delete_confirmation' => '确定要删除 :name 联系人吗?删除将立即生效且无法恢复。', + 'people_add_birthday_reminder' => '祝 :name 生日快乐', + 'people_add_birthday_reminder_deceased' => '在这天,:name 会庆祝他们的生日', + 'people_add_import' => '是否要 导入您的联系人?', + 'people_edit_email_error' => '您的联系人中已经有人使用此电子邮件,请更换一个', + 'people_export' => '导出为 vCard', + 'people_add_reminder_for_birthday' => '创建年度生日提醒', + + // show + 'section_contact_information' => '联系人信息', + 'section_personal_activities' => '活动', + 'section_personal_reminders' => '提醒', + 'section_personal_tasks' => '任务', + 'section_personal_gifts' => '礼物', + 'section_personal_notes' => '便签', + + // archived contacts + 'list_link_to_active_contacts' => '您正在查看存档的联系人, 单击这里 来查看活动的联系人列表。', + 'list_link_to_archived_contacts' => '已存档联系人列表', + + // Header + 'me' => '这是你', + 'edit_contact_information' => '编辑联系人信息', + 'contact_archive' => '存档联系人', + 'contact_unarchive' => '取消存档', + 'contact_archive_help' => '已存档的联系人不会显示在联系人列表中,但仍会出现在搜索结果中。', + 'call_button' => '记录通话', + 'set_favorite' => '您收藏的联系人将在联系人列表置顶显示。', + + // Stay in touch + 'stay_in_touch' => '常联系*', + 'stay_in_touch_frequency' => '常联系*提醒频率:每天|常联系*提醒频率:每 {count} 天', + 'stay_in_touch_next_date' => '下次到期日: {date}', + 'stay_in_touch_invalid' => '频率必须大于0。', + 'stay_in_touch_premium' => '您需要升级到高级账户来使用这个功能!', + 'stay_in_touch_modal_title' => '常联系*', + 'stay_in_touch_modal_desc' => '我们将会用邮件提醒您与{firstname}保持联系。', + 'stay_in_touch_modal_label' => '每… {count} 天给我发一封电子邮件|每… {count} 天给我发一封电子邮件', + + // Calls + 'modal_call_title' => '记录通话', + 'modal_call_comment' => '你们说了什么?(可选)', + 'modal_call_exact_date' => '通话日期', + 'modal_call_who_called' => '谁打来的?', + 'modal_call_emotion' => '您想记录您在此通话中的感受吗?(可选)', + 'calls_add_success' => '已保存通话记录。', + 'call_delete_confirmation' => '你确定要删除此通话记录吗?', + 'call_delete_success' => '成功删除通话记录!', + 'call_title' => '通话记录', + 'call_empty_comment' => '无详细信息', + 'call_blank_title' => '追踪您与{name} 的通话记录', + 'call_blank_desc' => '你打给{name}', + 'call_you_called' => '您拨出的', + 'call_he_called' => '{name} 拨出的', + 'call_emotions' => '情绪:', + + // Conversation + 'conversation_blank' => '记录你和 :name 在社交媒体或 SMS 等平台的对话…', + 'conversation_delete_link' => '删除对话', + 'conversation_edit_title' => '编辑对话', + 'conversation_edit_delete' => '您是否要删除这个对话?操作无法撤销。', + 'conversation_add_success' => '对话成功添加', + 'conversation_edit_success' => '对话成功更新', + 'conversation_delete_success' => '对话成功删除', + 'conversation_add_title' => '记录一个新对话', + 'conversation_add_when' => '你们何时进行的对话?', + 'conversation_add_who_wrote' => '谁发的这条消息?', + 'conversation_add_how' => '你们怎么交流?', + 'conversation_add_you' => '您', + 'conversation_add_content' => '写下你们说的话', + 'conversation_add_what_was_said' => '您说了什么?', + 'conversation_add_another' => '添加另一条消息', + 'conversation_add_error' => '您必须至少添加一条信息', + 'conversation_list_table_messages' => '消息', + 'conversation_list_table_content' => '部分内容(最新消息)', + 'conversation_list_title' => '对话', + 'conversation_list_cta' => '记录对话', + + // age - birthday + 'birthdate_not_set' => '未设置生日', + 'age_approximate_in_years' => '大概:age岁', + 'age_exact_in_years' => ':age岁', + 'age_exact_birthdate' => '出生:date', + + // Last called + 'last_called' => '最近通话: :date', + 'last_talked_to' => '最近通话:{date}', + 'last_called_empty' => '最近通话: 未知', + 'last_activity_date' => '最近一起的活动: :date', + 'last_activity_date_empty' => '最近一起的活动: 未知', + + // additional information + 'information_edit_success' => '记录更新成功', + 'information_edit_title' => '编辑 :name的个人信息', + 'information_edit_max_size' => '最大值 :size Kb', + 'information_edit_max_size2' => '最大 {size} Kb', + 'information_edit_firstname' => '名字', + 'information_edit_lastname' => '姓(可选)', + 'information_edit_description' => '描述 (可选)', + 'information_edit_description_help' => '用于在联系人列表中添加一些元素(如有必要)', + 'information_edit_unknown' => '我不知道具体年龄', + 'information_edit_probably' => '此人可能是…', + 'information_edit_not_year' => '我知道这个人生日的月日,但不知是哪一年…', + 'information_edit_exact' => '我知道这个人确切的生日…', + 'information_edit_birthdate_label' => '生日', + 'information_no_work_defined' => '未定义工作信息', + 'information_work_at' => '在 :company工作', + 'work_add_cta' => '更新工作信息', + 'work_edit_success' => '工作信息已更新', + 'work_edit_title' => '更新:name的工作信息', + 'work_edit_job' => '职位名称 (可选)', + 'work_edit_company' => '公司 (可选)', + 'work_information' => '工作信息', + + // food preferences + 'food_preferences_add_success' => '食品偏好已被保存', + 'food_preferences_edit_description' => '也许:firstname或:family的家庭有过敏,或者不喜欢一瓶特定的酒等。把这些信息列在这里,在下次和邀请他们吃饭时可以在这里看到这些信息。', + 'food_preferences_edit_description_no_last_name' => '也许:firstname有过敏情况,或者不喜欢一瓶特定的酒等。把这些信息列在这里,在下次和邀请他们吃饭时可以在这里看到这些信息。', + 'food_preferences_edit_title' => '注明食物偏好', + 'food_preferences_edit_cta' => '保存食物偏好', + 'food_preferences_title' => '食物偏好', + 'food_preferences_cta' => '添加食物偏好', + + // reminders + 'reminders_blank_title' => '您有什么关于:name的提醒吗?', + 'reminders_blank_add_activity' => '添加提醒', + 'reminders_add_title' => '你需要关于:name的提醒吗?', + 'reminders_add_description' => '请提醒我…', + 'reminders_add_next_time' => '您希望下一次关于这个的提醒的时间是?', + 'reminders_add_once' => '仅一次', + 'reminders_add_recurrent' => '每', + 'reminders_add_starting_from' => '提醒我', + 'reminders_add_cta' => '添加提醒', + 'reminders_edit_update_cta' => '更新提醒', + 'reminders_add_error_custom_text' => '您需要为此提醒指定文本', + 'reminders_create_success' => '已成功添加提醒', + 'reminders_delete_success' => '已成功删除提醒', + 'reminders_update_success' => '已成功更新提醒', + 'reminders_add_optional_comment' => '可选备注', + + 'reminder_frequency_day' => '每:number天', + 'reminder_frequency_week' => ' 每:number星期', + 'reminder_frequency_month' => ' 每:number月', + 'reminder_frequency_year' => '每:number年', + 'reminder_frequency_one_time' => '在:date', + 'reminders_delete_confirmation' => '确实要删除此提醒吗?', + 'reminders_delete_cta' => '删除', + 'reminders_next_expected_date' => '在', + 'reminders_cta' => '添加提醒', + 'reminders_description' => '我们将为下面每个提醒发送一封电子邮件。提醒将于事件发生的当天早晨发送。自动添加的生日提醒无法删除。如果您想要更改生日提醒的日期,请编辑联系人的生日。', + 'reminders_one_time' => '一次性', + 'reminders_type_week' => '周', + 'reminders_type_month' => '月', + 'reminders_type_year' => '年', + 'reminders_birthday' => ':name的生日', + 'reminders_free_plan_warning' => '您当前使用的是免费版。若需要邮件提醒,请升级您的账户。', + + // relationships + 'relationship_form_add' => '添加一个新的关系', + 'relationship_form_edit' => '修改一个已有关系', + 'relationship_form_is_with' => '这个人是…', + 'relationship_form_is_with_name' => ':name 是...', + 'relationship_form_add_choice' => '这是与谁的关系?', + 'relationship_form_create_contact' => '添加一个新的人', + 'relationship_form_associate_contact' => '导入一位已存在的联系人', + 'relationship_form_associate_dropdown' => '请从下拉菜单选择一位联系人', + 'relationship_form_associate_dropdown_placeholder' => '搜索并选择一位现有联系人', + 'relationship_form_also_create_contact' => '将此人创建为您的联系人', + 'relationship_form_add_description' => '这会让你像其他联系人一样对待这个人。', + 'relationship_form_add_no_existing_contact' => '您暂时没有能与 :name 链接的联系人', + 'relationship_delete_confirmation' => '您确定要将关系删除吗?本操作无法撤销。', + 'relationship_unlink_confirmation' => '您确定要将关系删除吗?此操作不会从您的联系人列表将其删除。', + 'relationship_form_add_success' => '关系设置完成', + 'relationship_form_deletion_success' => '此关系已删除', + + // tasks + 'tasks_title' => '任务', + 'tasks_blank_title' => '您暂时还没任务。', + 'tasks_form_title' => '标题', + 'tasks_form_description' => '描述 (可选)', + 'tasks_add_task' => '添加任务', + 'tasks_delete_success' => '成功删除任务!', + 'tasks_complete_success' => '成功变更任务!', + + // activities + 'activity_title' => '活动', + 'activity_type_category_simple_activities' => '一般活动', + 'activity_type_category_sport' => '运动', + 'activity_type_category_food' => '食物', + 'activity_type_category_cultural_activities' => '文化', + 'activity_type_just_hung_out' => '约会', + 'activity_type_watched_movie_at_home' => '在家看电影', + 'activity_type_talked_at_home' => '谈心', + 'activity_type_did_sport_activities_together' => '一起打球', + 'activity_type_ate_at_his_place' => '在对方家里做客', + 'activity_type_went_bar' => '泡吧', + 'activity_type_ate_at_home' => '在家吃饭', + 'activity_type_picnicked' => '已选择', + 'activity_type_ate_restaurant' => '在饭店吃', + 'activity_type_went_theater' => '看戏', + 'activity_type_went_concert' => '去音乐会', + 'activity_type_went_play' => '出去玩', + 'activity_type_went_museum' => '去博物馆', + 'activities_add_activity' => '添加活动', + 'activities_add_more_details' => '添加更多详情', + 'activities_add_emotions' => '添加情绪', + 'activities_add_category' => '指定类别', + 'activities_add_participants_cta' => '添加参与者', + 'activities_item_information' => ':Activity,发生于:date', + 'activities_add_title' => '您与 {name} 一起做了什么?', + 'activities_summary' => '描述你做了什么', + 'activities_add_pick_activity' => '是否对此活动进行分类?这不是必须的,但日后会为您提供统计数据 (可选)', + 'activities_add_date_occured' => '活动发生于…', + 'activities_add_participants' => '除了 {name} 之外,谁参与了这个活动?(可选)', + 'activities_add_emotions_title' => '您想记录您在此通话中的感受吗?(可选)', + 'activities_blank_title' => '记录您与 {name} 之间的点滴', + 'activities_blank_add_activity' => '添加活动', + 'activities_add_success' => '已成功添加活动', + 'activities_add_error' => '添加活动时出现错误', + 'activities_update_success' => '活动已成功更新', + 'activities_delete_success' => '活动已成功删除', + 'activities_who_was_involved' => '谁参与了?', + 'activities_activity' => '活动类别', + 'activities_view_activities_report' => '查看活动报告', + 'activities_profile_title' => ':name 与您之间的活动报告', + 'activities_profile_subtitle' => '截至目前为止您与:name的活动记录如下:近一年共 :activities_last_twelve_months次,总共 :total_activities次', + 'activities_profile_year_summary_activity_types' => ':year年活动类型汇总', + 'activities_profile_year_summary' => ':year年你们一起进行的活动', + 'activities_profile_number_occurences' => ':value 次活动', + 'activities_list_participants' => '参与者 ({total}):', + 'activities_list_emotions' => '我感觉:', + 'activities_list_date' => '发生于', + 'activities_list_category' => '分类:', + + // notes + 'notes_create_success' => '便签已成功创建', + 'notes_update_success' => '便笺已成功保存', + 'notes_delete_success' => '注释已成功删除', + 'notes_add_cta' => '添加注释', + 'notes_favorite' => '添加/删除喜爱标记', + 'notes_delete_title' => '删除便签', + 'notes_delete_confirmation' => '确实要删除此便签吗?', + + // gifts + 'gifts_title' => '礼物往来', + 'gifts_add_success' => '已成功添加礼物', + 'gifts_delete_success' => '礼物已成功删除', + 'gifts_delete_confirmation' => '是否确实要删除此礼物?', + 'gifts_add_gift' => '添加礼物', + 'gifts_link' => '链接', + 'gifts_for' => '赠予:{name}', + 'gifts_delete_cta' => '删除', + 'gifts_add_title' => '与:name的礼物来往', + 'gifts_add_gift_idea' => '礼品创意', + 'gifts_add_gift_already_offered' => '送出的礼物', + 'gifts_add_gift_received' => '收到的礼物', + 'gifts_add_gift_title' => '这是什么礼物?', + 'gifts_add_gift_name' => '礼品名称', + 'gifts_add_link' => '礼物链接 (可选)', + 'gifts_add_value' => '值 (可选)', + 'gifts_add_comment' => '备注 (可选)', + 'gifts_add_recipient' => '收件人(可选)', + 'gifts_add_recipient_field' => '收件人', + 'gifts_add_photo' => '相片(可选)', + 'gifts_add_photo_title' => '为此礼物添加一张照片', + 'gifts_add_someone' => '这份礼物特别是给{name}的家人', + 'gifts_delete_title' => '删除礼物', + 'gifts_ideas' => '心愿单', + 'gifts_offered' => '送出的礼物', + 'gifts_offered_as_an_idea' => '标记为心愿单', + 'gifts_received' => '收到的礼物', + 'gifts_view_comment' => '查看评论', + 'gifts_mark_offered' => '标记为提供', + 'gifts_update_success' => '礼物已成功更新', + 'gifts_add_date' => '日期 (可选)', + + // debts + 'debt_delete_confirmation' => '是否确实要删除此债务?', + 'debt_delete_success' => '已成功删除债务', + 'debt_add_success' => '已成功添加债务', + 'debt_title' => '债务', + 'debt_add_cta' => '增加债务', + 'debt_you_owe' => '您欠:amount', + 'debt_they_owe' => ':name欠您:amount', + 'debt_add_title' => '债务管理', + 'debt_add_you_owe' => ':name借给您', + 'debt_add_they_owe' => '您借给:name', + 'debt_add_amount' => '数额', + 'debt_add_reason' => '事由(可选)', + 'debt_add_add_cta' => '增加债务', + 'debt_edit_update_cta' => '更新债务', + 'debt_edit_success' => '债务已成功更新', + 'debts_blank_title' => '管理您与:name之间的债务关系', + + // tags + 'tag_edit' => '编辑标签', + 'tag_add' => '添加标签', + 'tag_add_search' => '添加或搜索标签', + 'tag_no_tags' => '还没有标签', + + // Introductions + 'introductions_sidebar_title' => '你们是如何认识的?', + 'introductions_blank_cta' => '您如何遇到的:name', + 'introductions_title_edit' => '你是怎么认识:name的?', + 'introductions_additional_info' => '你在哪里相遇', + 'introductions_edit_met_through' => '有人把你介绍给这个人吗?', + 'introductions_no_met_through' => '没有人', + 'introductions_first_met_date' => '第一次相见', + 'introductions_no_first_met_date' => '我不记得具体日期', + 'introductions_first_met_date_known' => '这是我们相遇的日子', + 'introductions_add_reminder' => '添加提醒以庆祝此事件发生的周年纪念', + 'introductions_update_success' => '你成功更新了关于你们相识的故事', + 'introductions_met_through' => '通过 :name遇到', + 'introductions_met_date' => '在:date遇到', + 'introductions_reminder_title' => '你第一次遇见的那一天的周年纪念日', + + // Deceased + 'deceased_reminder_title' => ':name的去世周年怀念', + 'deceased_mark_person_deceased' => '将此人标记为已逝者', + 'deceased_know_date' => '我知道此人的去世日期', + 'deceased_add_reminder' => '为此日期添加提醒', + 'deceased_label' => '逝者', + 'deceased_date_label' => '死亡日期', + 'deceased_label_with_date' => '在:date去世', + 'deceased_age' => '享年', + + // Contact information + 'contact_info_title' => '联系信息', + 'contact_info_form_content' => '内容', + 'contact_info_form_contact_type' => '联系方式', + 'contact_info_form_personalize' => '个性化', + 'contact_info_address' => '生活在', + + // Addresses + 'contact_address_title' => '地址', + 'contact_address_form_name' => '标签 (可选)', + 'contact_address_form_street' => '街 (可选)', + 'contact_address_form_city' => '城市 (可选)', + 'contact_address_form_province' => '省 (可选)', + 'contact_address_form_postal_code' => '邮政编码 (可选)', + 'contact_address_form_country' => '国家 (可选)', + 'contact_address_form_latitude' => '纬度 (仅限数字) (可选)', + 'contact_address_form_longitude' => '经度 (仅限数字) (可选)', + + // Pets + 'pets_kind' => '宠物种类', + 'pets_name' => '名字 (可选)', + 'pets_create_success' => '已成功添加宠物', + 'pets_update_success' => '宠物已更新', + 'pets_delete_success' => '宠物已被删除', + 'pets_title' => '宠物', + 'pets_reptile' => '爬行动物', + 'pets_bird' => '鸟', + 'pets_cat' => '猫', + 'pets_dog' => '狗', + 'pets_fish' => '鱼', + 'pets_hamster' => '仓鼠', + 'pets_horse' => '马', + 'pets_rabbit' => '兔子', + 'pets_rat' => '鼠', + 'pets_small_animal' => '小动物', + 'pets_other' => '其它', + + // life events + 'life_event_list_tab_life_events' => '生活事件', + 'life_event_list_tab_other' => '便签,提醒...', + 'life_event_list_title' => '生活事件', + 'life_event_blank' => '记录在{name} 身上发生的事情以供将来参考', + 'life_event_list_cta' => '添加生活事件', + 'life_event_create_category' => '全部类别', + 'life_event_create_life_event' => '添加生活事件', + 'life_event_create_default_title' => '标题 (可选)', + 'life_event_create_default_story' => '故事 (可选)', + 'life_event_create_date' => '只需要提供年份即可 — 日与月不是必填项。', + 'life_event_create_default_description' => '添加你知道的信息', + 'life_event_create_add_yearly_reminder' => '为该事件添加年度提醒', + 'life_event_create_success' => '生活事件添加成功', + 'life_event_delete_title' => '删除生活事件', + 'life_event_delete_description' => '确实要删除此生活事件吗?删除是永久性的。', + 'life_event_delete_success' => '事件已删除', + 'life_event_date_it_happened' => '发生日期', + 'life_event_category_work_education' => '工作与教育', + 'life_event_category_family_relationships' => '家庭与恋爱', + 'life_event_category_home_living' => '生活日常', + 'life_event_category_health_wellness' => '健身', + 'life_event_category_travel_experiences' => '旅行与经历', + 'life_event_sentence_new_job' => '开始了新的工作', + 'life_event_sentence_retirement' => '退休', + 'life_event_sentence_new_school' => '开始上学', + 'life_event_sentence_study_abroad' => '出国留学', + 'life_event_sentence_volunteer_work' => '开始志愿服务', + 'life_event_sentence_published_book_or_paper' => '发表了一篇论文', + 'life_event_sentence_military_service' => '开始服役', + 'life_event_sentence_new_relationship' => '开始一段关系', + 'life_event_sentence_engagement' => '订婚了', + 'life_event_sentence_marriage' => '结婚', + 'life_event_sentence_anniversary' => '周年纪念日', + 'life_event_sentence_expecting_a_baby' => '想要孩子', + 'life_event_sentence_new_child' => '有个孩子', + 'life_event_sentence_new_family_member' => '新增了家庭成员', + 'life_event_sentence_new_pet' => '养了宠物', + 'life_event_sentence_end_of_relationship' => '结束了一段关系', + 'life_event_sentence_loss_of_a_loved_one' => '失去了心爱的人', + 'life_event_sentence_moved' => '搬家了', + 'life_event_sentence_bought_a_home' => '买了新房子', + 'life_event_sentence_home_improvement' => '装修了', + 'life_event_sentence_holidays' => '去度假', + 'life_event_sentence_new_vehicle' => '买了辆新车', + 'life_event_sentence_new_roommate' => '有了新室友', + 'life_event_sentence_overcame_an_illness' => '熬过了疾病', + 'life_event_sentence_quit_a_habit' => '戒掉一个习惯', + 'life_event_sentence_new_eating_habits' => '开始新的饮食习惯', + 'life_event_sentence_weight_loss' => '减肥了', + 'life_event_sentence_wear_glass_or_contact' => '开始佩戴玻璃或隐形眼镜', + 'life_event_sentence_broken_bone' => '折断了骨头', + 'life_event_sentence_removed_braces' => '去掉了牙齿矫正器', + 'life_event_sentence_surgery' => '做了手术', + 'life_event_sentence_dentist' => '去看牙医了', + 'life_event_sentence_new_sport' => '开始运动', + 'life_event_sentence_new_hobby' => '有了新爱好', + 'life_event_sentence_new_instrument' => '学会了新乐器', + 'life_event_sentence_new_language' => '学了一门新的语言', + 'life_event_sentence_tattoo_or_piercing' => '纹身了或者打了耳洞', + 'life_event_sentence_new_license' => '获得驾照', + 'life_event_sentence_travel' => '旅游了', + 'life_event_sentence_achievement_or_award' => '获得成就或奖项', + 'life_event_sentence_changed_beliefs' => '改变信仰', + 'life_event_sentence_first_word' => '第一次发言', + 'life_event_sentence_first_kiss' => '第一次接吻', + + // documents + 'document_list_title' => '文档', + 'document_list_cta' => '上载文档', + 'document_list_blank_desc' => '在这里, 您可以存储与此人相关的文档。', + 'document_upload_zone_cta' => '上传文件', + 'document_upload_zone_progress' => '正在上传文档...', + 'document_upload_zone_error' => '上传文件时出错,请再试一次 !', + + // Photos + 'photo_title' => '照片', + 'photo_list_title' => '相关照片', + 'photo_list_cta' => '上传照片', + 'photo_list_blank_desc' => '您可以存储有关此联系人的图像。立即上传一个!', + 'photo_upload_zone_cta' => '上传照片', + 'photo_current_profile_pic' => '目前头像', + 'photo_make_profile_pic' => '设为头像', + 'photo_delete' => '删除照片', + 'photo_next' => '下一张照片 ❯', + 'photo_previous' => '❮ 上一张照片', + + // Avatars + 'avatar_change_title' => '更换头像', + 'avatar_question' => '您想使用哪个头像?', + 'avatar_default_avatar' => '默认头像', + 'avatar_adorable_avatar' => '喜爱头像', + 'avatar_gravatar' => '此用户的电子邮件地址 与Gravatar关联 。 Gravatar 是全球通用的头像服务。', + 'avatar_current' => '保持当前头像', + 'avatar_photo' => '从您上传的照片', + 'avatar_crop_new_avatar_photo' => '裁剪新头像照片', + + // emotions + 'emotion_this_made_me_feel' => '这让你觉得...', + + // logs + 'auditlogs_link' => '历史', + 'auditlogs_title' => ':name 发生的所有事件', + 'auditlogs_breadcrumb' => '历史', + 'auditlogs_author' => ':name 于 :date ', + + // contact field label + 'contact_field_label_home' => '家庭', + 'contact_field_label_work' => '工作', + 'contact_field_label_cell' => '手机', + 'contact_field_label_fax' => '传真', + 'contact_field_label_pager' => '寻呼机', + 'contact_field_label_main' => '主要', + 'contact_field_label_other' => '其它', + 'contact_field_label_personal' => '个人', +]; diff --git a/resources/lang/zh/reminder.php b/resources/lang/zh/reminder.php new file mode 100644 index 0000000..fe89ea1 --- /dev/null +++ b/resources/lang/zh/reminder.php @@ -0,0 +1,16 @@ + '祝此人生日快乐', + 'type_phone_call' => '呼叫', + 'type_lunch' => '与此人共进午餐', + 'type_hangout' => '与此人约会', + 'type_email' => 'Email', + 'type_birthday_kid' => 'Wish happy birthday to the child of', +]; diff --git a/resources/lang/zh/settings.php b/resources/lang/zh/settings.php new file mode 100644 index 0000000..149e4bc --- /dev/null +++ b/resources/lang/zh/settings.php @@ -0,0 +1,557 @@ + '帐户设置', + 'sidebar_personalization' => '个性化', + 'sidebar_settings_storage' => '存储空间', + 'sidebar_settings_export' => '导出数据', + 'sidebar_settings_users' => '用户', + 'sidebar_settings_subscriptions' => '订阅', + 'sidebar_settings_import' => '导入数据', + 'sidebar_settings_tags' => '管理标签', + 'sidebar_settings_api' => 'API', + 'sidebar_settings_dav' => 'Dav 资源', + 'sidebar_settings_security' => '安全', + 'sidebar_settings_auditlogs' => '追踪日志', + + 'title_general' => '基本信息', + 'title_i18n' => '本地化', + 'title_layout' => '布局', + + 'me_title' => '代表自己的联系人', + 'me_help' => '这个联系人在Monica代表了 ', + 'me_select' => '选择联系人', + 'me_no_contact' => '没有选择联系人', + 'me_select_click' => '单击此处选择一位联系人', + 'me_remove_contact' => '删除关联', + 'me_choose' => '选择自己', + 'me_choose_placeholder' => '选择自己', + + 'export_title' => '导出帐户数据', + 'export_be_patient' => 'Click the button to start the export. It might take several minutes to process the export – please be patient and do not spam the button.', + 'export_title_sql' => '导出未SQL文件', + 'export_sql_explanation' => 'Exporting your data in SQL format allows you to take your data and import it to your own Monica instance. This is only valuable if you do have your own server.', + 'export_sql_cta' => 'Export to SQL', + 'export_sql_link_instructions' => 'Note: read the instructions to learn more about importing this file to your instance.', + 'export_title_json' => 'Export to Json', + 'export_submitted' => 'Your export has been submitted, it will be available in a few moment…', + 'export_json_explanation' => 'Exporting your data in Json format for backup.', + 'export_json_beta' => 'Json export is in preview mode. Tell us what you think about it:', + 'export_json_cta' => 'Export to Json', + 'export_header_type' => 'Type', + 'export_header_timestamp' => 'Creation date', + 'export_header_status' => 'Status', + 'export_header_actions' => 'Actions', + 'export_last_title' => 'Last exports', + 'export_empty_title' => 'No exports yet', + 'export_type_json' => 'Json export', + 'export_type_sql' => 'SQL export', + 'export_status_todo' => 'Submitted', + 'export_status_doing' => 'Doing', + 'export_status_done' => 'Done', + 'export_status_failed' => 'Failed', + 'export_not_done' => 'Download impossible, this export is not done yet.', + + 'firstname' => '名', + 'lastname' => '姓氏', + 'name_order' => '名称顺序', + 'name_order_firstname_lastname' => ' – John Doe', + 'name_order_lastname_firstname' => ' – Doe John', + 'name_order_firstname_lastname_nickname' => ' () – John Doe (Rambo)', + 'name_order_firstname_nickname_lastname' => ' () – John (Rambo) Doe', + 'name_order_lastname_firstname_nickname' => ' () – Doe John (Rambo)', + 'name_order_lastname_nickname_firstname' => ' () – Doe (Rambo) John', + 'name_order_nickname_firstname_lastname' => ' ( ) – Rambo (John Doe)', + 'name_order_nickname_lastname_firstname' => ' ( ) – Rambo (Doe John)', + 'name_order_nickname' => ' – Rambo', + 'currency' => '货币', + 'name' => '您的姓名: :name', + 'email' => '电子邮件地址', + 'email_placeholder' => '输入电子邮箱', + 'email_help' => '这是用于登录的电子邮件, 同时也用来接收您的提醒。', + 'timezone' => '时区', + 'temperature_scale' => '温度单位', + 'temperature_scale_fahrenheit' => 'Fahrenheit (°F)', + 'temperature_scale_celsius' => 'Celsius (°C)', + 'layout' => '布局', + 'layout_small' => '最大1200像素宽', + 'layout_big' => '浏览器的全宽度', + 'save' => '更新偏好', + 'delete_title' => '删除您的帐户', + 'delete_desc' => '想要删除帐户吗?删除是永久性的,你的所有数据都将被永久删除。 如果你有订阅,它将被立即取消。', + 'delete_other_desc' => 'Your data in the main database will be deleted immediately. As described in our privacy policy, we carry out securely encrypted backups of the database every day. These backups are kept for 30 days after which they are completely deleted. We cannot delete specific data from the backups we hold any earlier than this. All of your data will be completely deleted no later than 31 days after your account’s deletion.', + 'reset_desc' => '想重置帐户吗?这将删除所有联系人以及与之关联的所有数据。但帐户本身不会被删除。', + 'reset_title' => '删除您的帐户', + 'reset_cta' => '重置帐户', + 'reset_notice' => '你确定要重置账户吗?此操作是永久的且不可撤销。', + 'reset_success' => '你的账户已成功重置。', + 'delete_notice' => '你确定要删除账户吗?此操作是永久的且不可撤销。你的所有数据将被删除且无法恢复。', + 'delete_cta' => '删除帐户', + 'settings_success' => '偏好设置已更新', + 'locale' => '应用程序中使用的语言', + 'locale_help' => '您想要帮助翻译Monica或添加新语言吗?请点击 了解更多信息。', + 'locale_ar' => '阿拉伯文', + 'locale_cs' => '捷克文', + 'locale_de' => '德文', + 'locale_el' => '希腊文', + 'locale_en' => '英文', + 'locale_en-GB' => '英语 (英国)', + 'locale_es' => '西班牙文', + 'locale_fr' => '法文', + 'locale_he' => '希伯来文', + 'locale_hr' => '克罗地亚文', + 'locale_id' => '印度尼西亚文', + 'locale_it' => '意大利文', + 'locale_ja' => '日文', + 'locale_nl' => '荷兰文', + 'locale_pt' => '葡萄牙文', + 'locale_pt-BR' => '葡萄牙文(巴西)', + 'locale_ru' => '俄文', + 'locale_sv' => '瑞典文', + 'locale_vi' => '越南语', + 'locale_zh' => '简体中文', + 'locale_zh-TW' => '繁体中文', + 'locale_tr' => '土耳其文', + + 'security_title' => '安全', + 'security_help' => '更改您的帐户的安全选项。', + 'password_change' => '修改密码', + 'password_current' => '当前密码', + 'password_current_placeholder' => '输入当前密码', + 'password_new1' => '新密码', + 'password_new1_placeholder' => '请输入新密码', + 'password_new2' => '确认新密码', + 'password_new2_placeholder' => '请再次输入新密码', + 'password_btn' => '更改密码', + '2fa_title' => '双重验证', + '2fa_otp_title' => '用于二次验证的App', + '2fa_enable_title' => '启用二次验证', + '2fa_enable_description' => 'Enable Two Factor Authentication to increase the security of your account.', + '2fa_enable_otp' => 'Open up your Two Factor Authentication mobile app and scan the following QR barcode:', + '2fa_enable_otp_help' => 'If your Two Factor Authentication mobile app does not support QR barcodes, enter in the following code:', + '2fa_enable_otp_validate' => 'Please validate the new device you’ve just set up:', + '2fa_enable_success' => '双重认证已激活', + '2fa_enable_error' => '尝试激活双重身份验证时出错', + '2fa_enable_error_already_set' => '二次验证已激活', + '2fa_disable_title' => '关闭双重身份验证', + '2fa_disable_description' => 'Disable Two Factor Authentication for your account. Be careful, your account will be much less secure!', + '2fa_disable_success' => '双重身份认证已禁用', + '2fa_disable_error' => '尝试禁用双重身份验证时出错', + + 'webauthn_title' => '安全钥匙 - WebAuthn', + 'webauthn_enable_description' => '添加一个安全钥匙', + 'webauthn_key_name_help' => '给你的钥匙起个名字', + 'webauthn_key_name' => '钥匙名称:', + 'webauthn_success' => '您的钥匙已被检测到并验证完毕。', + 'webauthn_last_use' => '最后使用: {timestamp}', + 'webauthn_delete_confirmation' => '确实要删除这个钥匙吗?', + 'webauthn_delete_success' => '钥匙已删除', + 'webauthn_insertKey' => '插入您的安全钥匙', + 'webauthn_buttonAdvise' => '如果您的安全钥匙有按钮,请按下它。', + 'webauthn_noButtonAdvise' => '如果没有, 请将其拔出并再次插入。', + 'webauthn_not_supported' => '您的游览器并不支持WebAuthn', + 'webauthn_not_secured' => 'WebAuthn只支持SSL连接,请使用https打开这个页面', + 'webauthn_error_already_used' => '这个钥匙已经注册,您无需在注册一次。', + 'webauthn_error_not_allowed' => '操作超时或不允许。', + + 'recovery_title' => '恢复代码', + 'recovery_show' => '获取恢复代码', + 'recovery_copy_help' => '复制到您的剪贴板', + 'recovery_help_intro' => '以下是您的恢复代码:', + 'recovery_help_information' => '您可以使用每个恢复代码一次。', + 'recovery_clipboard' => 'Codes copied to the clipboard.', + 'recovery_generate' => 'Generate new codes…', + 'recovery_generate_help' => 'Generating new codes will invalidate previously generated codes.', + 'recovery_already_used_help' => 'This code has already been used.', + + 'users_list_title' => '可以访问您的帐户的用户', + 'users_list_add_user' => '邀请新用户', + 'users_list_you' => '这是你', + 'users_list_invitations_title' => '待处理的邀请', + 'users_list_invitations_explanation' => '已邀请', + 'users_list_invitations_invited_by' => '被:name邀请', + 'users_list_invitations_sent_date' => '在:date发送', + 'users_blank_title' => '您是唯一可以访问此帐户的人。', + 'users_blank_add_title' => '你想邀请别人吗?', + 'users_blank_description' => '此人将具有您拥有的相同访问权限, 并且可以添加、编辑或删除联系人信息。', + 'users_blank_cta' => '邀请他人加入', + 'users_add_title' => 'Invite a new user to your account by email', + 'users_add_description' => 'This person will have the same access as you do, including inviting or deleting other users, including you. Make sure you trust this person before giving them access.', + 'users_add_email_field' => '输入您要邀请的人的电子邮件', + 'users_add_confirmation' => 'I confirm that I want to invite this user to my account. I understand that this person will have access to ALL of my data and see exactly what I see.', + 'users_add_cta' => '通过电子邮件邀请用户', + 'users_accept_title' => '接受邀请并新建一个账号', + 'users_error_please_confirm' => '请您先确认您要邀请此用户', + 'users_error_email_already_taken' => '这个电子邮件已经存在,请另选一个!', + 'users_error_already_invited' => '您已经邀请了此用户。请选择其他电子邮件地址。', + 'users_error_email_not_similar' => '这不是邀请人的电子邮件。', + 'users_invitation_deleted_confirmation_message' => '已成功删除邀请', + 'users_invitations_delete_confirmation' => '确实要删除此邀请吗?', + 'users_list_delete_confirmation' => '是否确实要从您的帐户中删除此用户?', + 'users_invitation_need_subscription' => '您需要升级账户才能添加更多用户', + + 'subscriptions_account_current_plan' => '您当前的订阅', + 'subscriptions_account_current_legacy' => 'Current plan, not selectable anymore:', + 'subscriptions_account_current_paid_plan' => '您当前的订阅是::name,感谢您的订阅。', + + 'subscriptions_account_next_billing_title' => 'Next bill', + 'subscriptions_account_next_billing' => '您的订阅将在 :date 自动续费', + 'subscriptions_account_bill_monthly' => 'We’ll bill you :price for another month.', + 'subscriptions_account_bill_annual' => 'We’ll bill you :price for another year.', + 'subscriptions_account_change' => 'Change plan', + + 'subscriptions_account_cancel_title' => 'Cancel subscription', + 'subscriptions_account_cancel_action' => 'Cancel subscription', + 'subscriptions_account_cancel' => 'You can cancel your subscription at any time.', + 'subscriptions_account_free_plan' => '您正在使用免费版', + 'subscriptions_account_free_plan_upgrade' => '您可以将您的帐户升级为:name, 它的成本为每月$:price。您将享有以下特权:', + 'subscriptions_account_free_plan_benefits_users' => '不限数量的用户', + 'subscriptions_account_free_plan_benefits_reminders' => '电子邮件提醒', + 'subscriptions_account_free_plan_benefits_import_data_vcard' => '从 vCard 文件导入联系人', + 'subscriptions_account_free_plan_benefits_support' => 'Support the project in the long run, so we can introduce more great features.', + 'subscriptions_account_upgrade' => '更新您的账户', + 'subscriptions_account_upgrade_title' => '立即升级您的Monica账户吧!', + 'subscriptions_account_upgrade_choice' => '在下方选择一个订阅(已有 :customers 订阅了高级版)', + 'subscriptions_account_update_title' => 'Update Monica subscription', + 'subscriptions_account_update_description' => 'You can change your subscription’s frequency here.', + 'subscriptions_account_update_information' => 'You will be billed immediately for the new amount. Your subscription will extend to the new period, depending on your choice.', + 'subscriptions_account_invoices' => '发票', + 'subscriptions_account_invoices_download' => '下载', + 'subscriptions_account_invoices_subscription' => '订阅周期::startDate 至 :endDate', + 'subscriptions_account_payment' => '哪个付费周期最适合您?', + 'subscriptions_account_confirm_payment' => '交易尚未完成,请您按此确认您的付款', + 'subscriptions_downgrade_title' => '将您的帐户降级为免费版', + 'subscriptions_downgrade_limitations' => '免费版的功能有限制。如果您需要降级,请您确保完成以下检查:', + 'subscriptions_downgrade_rule_users' => '您的帐户中必须只有1个用户', + 'subscriptions_downgrade_rule_users_constraint' => '您的帐户中当前有 :count 个用户。', + 'subscriptions_downgrade_rule_invitations' => 'You must not have any pending invitations', + 'subscriptions_downgrade_rule_invitations_constraint' => 'You currently have 1 pending invitation.|You currently have :count pending invitations.', + 'subscriptions_downgrade_rule_contacts' => '您不能超过 :number 的活跃联系人', + 'subscriptions_downgrade_rule_contacts_constraint' => '当前有 :count 位联系人。', + 'subscriptions_downgrade_rule_contacts_archive' => 'We can also archive all your contacts for you – that would clear this rule and let you proceed with your account’s downgrade process.', + 'subscriptions_downgrade_cta' => '降级', + 'subscriptions_downgrade_success' => '您已降级到免费版!', + 'subscriptions_downgrade_thanks' => 'Thanks so much for trying the paid plan. We keep adding new features on Monica all the time – so you might want to come back in the future to see if you might be interested in taking a subscription again.', + 'subscriptions_back' => '返回设置', + 'subscriptions_upgrade_title' => '升级您的帐户', + 'subscriptions_upgrade_choose' => '您选择了:plan', + 'subscriptions_upgrade_infos' => '请在下方输入您的付款信息:', + 'subscriptions_upgrade_name' => '持卡人姓名', + 'subscriptions_upgrade_zip' => '邮政编码', + 'subscriptions_upgrade_credit' => '信用卡或借记卡', + 'subscriptions_upgrade_submit' => '支付{amount}', + 'subscriptions_upgrade_charge' => 'We’ll charge your card :price now. The next charge will be on :date. If you ever change your mind, you can cancel at any time, no questions asked.', + 'subscriptions_upgrade_charge_handled' => '支付服务由第三方支付平台 Stripe 提供,我们无法接触到您的个人信息。', + 'subscriptions_upgrade_success' => '感谢您的订阅!', + 'subscriptions_upgrade_thanks' => '欢迎来到让世界变得更美好的社区。', + + 'subscriptions_payment_confirm_title' => '确认您的 :amount 付款', + 'subscriptions_payment_confirm_information' => '需要额外信息来处理您的付款,请您补充下列付款信息。', + 'subscriptions_payment_succeeded_title' => '支付成功', + 'subscriptions_payment_succeeded' => '此交易已经完成。', + 'subscriptions_payment_cancelled_title' => '付款已取消', + 'subscriptions_payment_cancelled' => '您的付款已被取消。', + 'subscriptions_payment_error_name' => '请提供您的姓名', + 'subscriptions_payment_success' => '您的付款已成功', + + 'subscriptions_pdf_title' => '您的:name每月订阅', + 'subscriptions_plan_frequency_year' => ':amount / year', + 'subscriptions_plan_frequency_month' => ':amount / month', + 'subscriptions_plan_choose' => '选择此计划', + 'subscriptions_plan_year_title' => '按年度支付', + 'subscriptions_plan_year_bonus' => '一整年的安心', + 'subscriptions_plan_month_title' => '按月支付', + 'subscriptions_plan_month_bonus' => '随时取消', + 'subscriptions_plan_include1' => '您将享有以下特权:', + 'subscriptions_plan_include2' => '无限添加联系人·无限的用户数量·电子邮件提醒·导入 vCard ·个性化的联系人信息', + 'subscriptions_plan_include3' => '收入的100% 用于此项目的开发。', + 'subscriptions_help_title' => '您可能还关心', + 'subscriptions_help_opensource_title' => '什么是开源项目?', + 'subscriptions_help_opensource_desc' => 'Monica is an open source project. This means it is built by a community who wants to build a great tool for the greater good. Being open source means the code is publicly available on GitHub, and everyone can inspect it, modify it or enhance it. All the money we raise is dedicated to building better features, paying for more powerful servers, and paying other costs. Thanks for your help. We couldn’t do it without you.', + 'subscriptions_help_limits_title' => 'Is there a limit to the number of contacts we can have on the free plan?', + 'subscriptions_help_limits_plan' => '是的。免费版您能拥有:number位联系人。', + 'subscriptions_help_discounts_title' => '你们对非盈利机构和学生有优惠吗?', + 'subscriptions_help_discounts_desc' => '当然!Monica免费为学生,非盈利机构提供服务。您只需要提交一下材料给我们的 支持人员。', + 'subscriptions_help_change_title' => '如果我改变主意怎么办?', + 'subscriptions_help_change_desc' => 'You can cancel anytime, no questions asked, and all by yourself – no need to contact support. However, you will not be refunded for the current period.', + + 'stripe_error_card' => '您的卡被拒,原因是::message', + 'stripe_error_api_connection' => '与Stripe的通信失败,请稍候重试。', + 'stripe_error_rate_limit' => '与Stripe的通信次数过多,请稍候再试。', + 'stripe_error_invalid_request' => '无效的参数,请稍后再试。', + 'stripe_error_authentication' => 'Stripe授权失败', + + 'import_title' => '在您的帐户中导入联系人', + 'import_cta' => '上载联系人', + 'import_stat' => '您目前为止导入了:number个文件。', + 'import_result_stat' => '上传了包含 :total_contacts 个联系人的 vCard (:total_imported imported, :total_skipped skipped)', + 'import_view_report' => '查看报告', + 'import_in_progress' => '导入正在进行中。在一分钟内重新加载页面。', + 'import_upload_title' => '从 vCard 文件导入联系人', + 'import_upload_rules_desc' => '但是, 我们有一些规则:', + 'import_upload_rule_format' => '我们支持 vcardvcf 文件。', + 'import_upload_rule_vcard' => 'We support the vCard 3.0 format, which is the default format for macOS’s Contacts.app and Google Contacts.', + 'import_upload_rule_instructions' => 'Export instructions for macOS Contacts.app and Google Contacts.', + 'import_upload_rule_multiple' => 'If your contacts have multiple email addresses or phone numbers, only the first entry will be saved.', + 'import_upload_rule_limit' => 'Files are limited to 10 MB.', + 'import_upload_rule_time' => 'It might take up to a minute to upload the contacts and process them. Please be patient.', + 'import_upload_rule_cant_revert' => 'Please make sure data is accurate before uploading, as you can’t undo the upload.', + 'import_upload_form_file' => '你的 .vcf. vCard 文件:', + 'import_upload_behaviour' => '导入偏好:', + 'import_upload_behaviour_add' => 'Add new contacts and skip existing', + 'import_upload_behaviour_replace' => '替换现有条目', + 'import_upload_behaviour_help' => 'Replacing will replace all data found in the vCard, but will keep existing contact fields.', + 'import_report_title' => '导入报表', + 'import_report_date' => '导入日期', + 'import_report_type' => '导入类型', + 'import_report_number_contacts' => '文件中的联系人数', + 'import_report_number_contacts_imported' => '导入的联系人数量', + 'import_report_number_contacts_skipped' => '跳过的联系人数', + 'import_report_status_imported' => '导入', + 'import_report_status_skipped' => '跳过', + 'import_vcard_parse_error' => '分析 vcard 项时出错', + 'import_vcard_contact_exist' => '联系人已存在', + 'import_vcard_contact_no_firstname' => 'No first name (mandatory)', + 'import_vcard_file_not_found' => '文件不存在', + 'import_vcard_unknown_entry' => '未知的联系人姓名', + 'import_vcard_file_no_entries' => '文件不包含联系人', + 'import_blank_title' => '您暂无导入的联系人。', + 'import_blank_question' => '是否立即导入联系人?', + 'import_blank_description' => '我们可以从 Google Contacts 或您的Contact manager那里导入您的 vCard 文件。', + 'import_blank_cta' => '导入 vCard', + 'import_need_subscription' => '您需要订阅才能导入联系人', + + 'tags_list_title' => '标签', + 'tags_list_description' => '您可以通过设置来标记联系人。标记的工作方式类似于文件夹, 但可以向联系人添加多个标记。若要添加新标记, 请在联系人中添加即可。', + 'tags_list_contact_number' => ':count 个联系人', + 'tags_list_delete_success' => '标签已成功删除', + 'tags_list_edit_success' => 'The tag has been successfully updated', + 'tags_list_delete_confirmation' => '确实要删除该标签吗?不会删除任何联系人, 只有标签。', + 'tags_blank_title' => '标签是对您的联系人进行分类的一种很好的方式。', + 'tags_blank_description' => '标签的工作方式类似于文件夹,但可以向联系人添加多个标签。你可以转到联系人页面,并在名字的下方添加标签,之后可以返回此处管理帐户中的所有标签。', + + 'api_title' => 'API 访问', + 'api_description' => 'API 可以用来从外部应用程序操纵Monica的数据, 例如移动应用程序。', + 'api_help' => '要使用 API,必须要有一个Token。 您可以创建个人访问 Token,也可以授权OAuth 客户端为您创建它。 查看 API 文档获取详情', + 'api_endpoint' => '此 Monica 实例的 API 终端是:', + + 'api_personal_access_tokens' => '个人访问令牌', + 'api_pao_description' => '请确保将此token授予您信任的源-因为它们允许您访问所有数据。', + 'api_token_title' => '个人访问 Token', + 'api_token_create_new' => '创建密钥', + 'api_token_not_created' => '您没有已创建的访问密钥', + 'api_token_name' => 'Token 名称', + 'api_token_expire' => '过期于 {date}', + 'api_token_delete' => '删除', + 'api_token_create' => '创建密钥', + 'api_token_scopes' => '作用域', + 'api_token_help' => '这是您的个人访问密钥,我们只会展示一次,请妥善保管。您现在可以使用这个密钥进行API请求', + + 'api_oauth_clients' => '您的 Oauth 客户端', + 'api_oauth_clients_desc' => '您可以注册自己的 OAuth 客户端。', + 'api_oauth_clients_desc2' => '使用此客户端ID请求一个新的Token,并将授权码转换为Token。请参阅 Laravel Passport文档 获取更多信息。', + 'api_oauth_title' => 'OAuth 客户端', + 'api_oauth_create_new' => '创建新的客户端', + 'api_oauth_edit' => '编辑客户端', + 'api_oauth_not_created' => '您尚未创建Oauth客户端', + 'api_oauth_clientid' => '客户端 ID', + 'api_oauth_name' => '名称', + 'api_oauth_name_help' => '安全码', + 'api_oauth_secret' => '密钥', + 'api_oauth_create' => '创建客户端', + 'api_oauth_redirecturl' => '重定向URL', + 'api_oauth_redirecturl_help' => '应用程序的授权回调 URL。', + + 'api_authorized_clients' => '授权客户端列表', + 'api_authorized_clients_desc' => '本节列出了您授权访问应用程序的所有客户端,您可以随时撤销此授权。', + 'api_authorized_clients_title' => '已授权的应用', + 'api_authorized_clients_none' => '尚无授权客户端。', + 'api_authorized_clients_name' => '名称', + 'api_authorized_clients_scopes' => '作用域', + + 'personalization_tab_title' => '个性化您的帐户', + + 'personalization_title' => '你可以在这里配置你的账户。注意这些配置主要针对想要最大化控制 Monica 的高级用户。', + 'personalization_contact_field_type_title' => '联系人字段类型', + 'personalization_contact_field_type_add' => '添加新字段类型', + 'personalization_contact_field_type_description' => '你可以在此处配置多种联系人字段,这些字段可以关联到所有联系人。如果在为了出现了新的社交网络,你可以最终这里添加这种新的联系方式。', + 'personalization_contact_field_type_table_name' => '名称', + 'personalization_contact_field_type_table_protocol' => '协议', + 'personalization_contact_field_type_table_actions' => '行动', + 'personalization_contact_field_type_modal_title' => '添加新的联系人字段类型', + 'personalization_contact_field_type_modal_edit_title' => '编辑现有联系人字段类型', + 'personalization_contact_field_type_modal_delete_title' => '删除现有联系人字段类型', + 'personalization_contact_field_type_modal_delete_description' => 'Are you sure you want to delete this contact field type? Deleting this type of contact field will delete ALL the data with this type for all of your contacts.', + 'personalization_contact_field_type_modal_name' => '名称', + 'personalization_contact_field_type_modal_protocol' => '协议 (可选)', + 'personalization_contact_field_type_modal_protocol_help' => '每个新的联系人字段类型都可以选定。如果设置了协议, 我们将使用它来触发设置的操作。', + 'personalization_contact_field_type_modal_icon' => '图标 (可选)', + 'personalization_contact_field_type_modal_icon_help' => '您可以将图标与此联系人字段类型关联。您需要添加对Font Awesome图标的引用。', + 'personalization_contact_field_type_delete_success' => 'The contact field type has been successfully deleted.', + 'personalization_contact_field_type_add_success' => '已成功添加联系人字段类型。', + 'personalization_contact_field_type_edit_success' => '联系人字段类型已成功更新。', + + 'personalization_genders_title' => '性别类型', + 'personalization_genders_add' => '添加新的性别类型', + 'personalization_genders_desc' => '你可以根据需要定义尽可能多的性别。您的帐户中至少需要一种性别类型。', + 'personalization_genders_modal_add' => '添加性别类型', + 'personalization_genders_modal_edit' => '更新性别类型', + 'personalization_genders_modal_name' => '名称', + 'personalization_genders_modal_name_help' => '在联系人页面显示性别的名称', + 'personalization_genders_modal_sex' => '性别', + 'personalization_genders_modal_sex_help' => '在导入/导出 VCard 时用于定义关系', + 'personalization_genders_modal_default' => '选择新联系人的默认性别', + 'personalization_genders_modal_delete' => '删除性别类型', + 'personalization_genders_modal_delete_desc' => '你确定要删除性别“{name}”吗?', + 'personalization_genders_modal_delete_question' => '在这个性别下有 {count} 个联系人。如果你删除了这个性别,要为这些联系人分配什么性别呢?|在这个性别下有 {count} 个联系人。如果你删除了这个性别,要为这些联系人分配什么性别呢?', + 'personalization_genders_modal_delete_question_default' => 'This gender is the default one. If you delete this gender, which one will be the new default?', + 'personalization_genders_modal_error' => 'Please choose a gender from the list.', + 'personalization_genders_list_contact_number' => '{count} 个联系人|{count} 个联系人', + 'personalization_genders_table_name' => '名称', + 'personalization_genders_table_sex' => '性别', + 'personalization_genders_table_default' => '默认', + 'personalization_genders_default' => '默认性别', + 'personalization_genders_make_default' => '更改默认性别', + 'personalization_genders_select_default' => '选择默认性别', + 'personalization_genders_m' => '男性', + 'personalization_genders_f' => '女性', + 'personalization_genders_o' => '其他', + 'personalization_genders_u' => '未知', + 'personalization_genders_n' => '无或不适用', + + 'personalization_reminder_rule_save' => '更改已保存', + 'personalization_reminder_rule_title' => '提醒规则', + 'personalization_reminder_rule_line' => '提前 {count} 天|提前 {count} 天', + 'personalization_reminder_rule_desc' => '对于每个提醒,Monica 可以在事件发生的前几天向你发送电子邮件。你可以在此处配置这些通知。请注意,,这些通知只适用于月度和年度提醒。', + + 'personalization_module_save' => '更改已被保存', + 'personalization_module_title' => '功能', + 'personalization_module_desc' => '你不必启用 Monica 的所有功能。你可以在下方切换联系人表上启用的功能。 此更改将影响你的所有联系人。 关闭功能不会删除任何数据,只会隐藏该功能。', + + 'personalisation_paid_upgrade' => '这是一个高级功能,需要付费订阅才能激活。通过访问 设置 > 订阅 来升级您的帐户。', + 'personalisation_paid_upgrade_vue' => '这是一个高级功能,需要付费订阅才能激活。通过访问 设置 > 订阅 来升级您的帐户。', + + 'reminder_time_to_send' => '发送提醒的时间', + 'reminder_time_to_send_help' => '下一次提醒将于 {dateTime} 发送。', + + 'personalization_activity_type_category_title' => '活动分类', + 'personalization_activity_type_category_add' => '增加一个活动分类', + 'personalization_activity_type_category_table_name' => '名称', + 'personalization_activity_type_category_description' => '你与联系人的活动可能有不同类型。我们为你添加了一些默认活动及分类,你可以在此处修改它们。', + 'personalization_activity_type_category_table_actions' => '行动', + 'personalization_activity_type_category_modal_add' => '增加活动分类', + 'personalization_activity_type_category_modal_edit' => '编辑活动分类', + 'personalization_activity_type_category_modal_question' => '你想如何命名这个新的分类?', + 'personalization_activity_type_add_button' => '增加一个活动', + 'personalization_activity_type_modal_add' => '增加一个活动', + 'personalization_activity_type_modal_question' => '你想如何命名这个新的活动?', + 'personalization_activity_type_modal_edit' => '编辑活动', + 'personalization_activity_type_category_modal_delete' => '删除活动分类', + 'personalization_activity_type_category_modal_delete_desc' => '你确定要删除这个分类吗?删除分类也会删除该分类下的所有活动类型,但删除分类不会删除联系人中记录的属于此分类的活动。', + 'personalization_activity_type_modal_delete' => '删除活动', + 'personalization_activity_type_modal_delete_desc' => '您真的要删除这个活动吗?', + 'personalization_activity_type_modal_delete_error' => '我们无法找到这个活动', + 'personalization_activity_type_category_modal_delete_error' => '我们无法找到这个活动分类', + + 'personalization_life_event_category_title' => '生活事件分类', + 'personalization_live_event_category_table_name' => 'Name', + 'personalization_life_event_category_description' => '生命事件可能有不同分类。 你的帐户默认包含一组预定义的分类,你可以在这里自定义生命事件分类。', + 'personalization_live_event_category_table_actions' => 'Actions', + 'personalization_life_event_type_add_button' => 'Add a new life event type', + 'personalization_life_event_type_modal_add' => 'Add a new life event type', + 'personalization_life_event_type_modal_question' => 'What should we name this new life event type?', + 'personalization_life_event_type_modal_edit' => 'Edit a life event type', + 'personalization_life_event_type_modal_delete' => 'Delete a life event type', + 'personalization_life_event_type_modal_delete_desc' => 'Are you sure you want to delete this life event type? Life events that belong to this type will be deleted by performing this action.', + 'personalization_life_event_type_modal_delete_error' => 'We can’t find this life event type.', + + 'personalization_life_event_category_work_education' => '工作与教育', + 'personalization_life_event_category_family_relationships' => '家庭与恋爱', + 'personalization_life_event_category_home_living' => '家与生活', + 'personalization_life_event_category_travel_experiences' => '旅行与经历', + 'personalization_life_event_category_health_wellness' => '健康与饮食', + + 'personalization_life_event_type_new_job' => '新工作', + 'personalization_life_event_type_retirement' => '退休', + 'personalization_life_event_type_new_school' => '新学校', + 'personalization_life_event_type_study_abroad' => '留学', + 'personalization_life_event_type_volunteer_work' => '志愿者工作', + 'personalization_life_event_type_published_book_or_paper' => '出版一本书或一篇论文', + 'personalization_life_event_type_military_service' => '兵役', + 'personalization_life_event_type_first_met' => '第一次见面', + 'personalization_life_event_type_new_relationship' => '新关系', + 'personalization_life_event_type_engagement' => '订婚', + 'personalization_life_event_type_marriage' => '婚姻', + 'personalization_life_event_type_anniversary' => '周年纪念日', + 'personalization_life_event_type_expecting_a_baby' => '想要孩子', + 'personalization_life_event_type_new_child' => '新的孩子', + 'personalization_life_event_type_new_family_member' => '新的家庭成员', + 'personalization_life_event_type_new_pet' => '新宠物', + 'personalization_life_event_type_end_of_relationship' => '结束了一段关系', + 'personalization_life_event_type_loss_of_a_loved_one' => '失去心爱的人', + 'personalization_life_event_type_moved' => '搬家了', + 'personalization_life_event_type_bought_a_home' => '买了新房子', + 'personalization_life_event_type_home_improvement' => '装修', + 'personalization_life_event_type_holidays' => '假日', + 'personalization_life_event_type_new_vehicle' => '新车', + 'personalization_life_event_type_new_roommate' => '新室友', + 'personalization_life_event_type_overcame_an_illness' => '熬过了疾病', + 'personalization_life_event_type_quit_a_habit' => '戒掉一个习惯', + 'personalization_life_event_type_new_eating_habits' => '新的饮食习惯', + 'personalization_life_event_type_weight_loss' => '减肥', + 'personalization_life_event_type_wear_glass_or_contact' => 'Started wearing glasses or contacts', + 'personalization_life_event_type_broken_bone' => 'Broke a bone', + 'personalization_life_event_type_removed_braces' => 'Had braces removed', + 'personalization_life_event_type_surgery' => 'Had surgery', + 'personalization_life_event_type_dentist' => 'Had dental treatment', + 'personalization_life_event_type_new_sport' => 'Started playing a new sport', + 'personalization_life_event_type_new_hobby' => 'Took up a new hobby', + 'personalization_life_event_type_new_instrument' => 'Started learning a new instrument', + 'personalization_life_event_type_new_language' => 'Started learning a new language', + 'personalization_life_event_type_tattoo_or_piercing' => '纹身或耳洞', + 'personalization_life_event_type_new_license' => '新驾照', + 'personalization_life_event_type_travel' => '旅行', + 'personalization_life_event_type_achievement_or_award' => '成就或奖项', + 'personalization_life_event_type_changed_beliefs' => '改变信仰', + 'personalization_life_event_type_first_word' => '第一次发言', + 'personalization_life_event_type_first_kiss' => '初吻', + + 'storage_title' => '存储空间', + 'storage_account_info' => '你的账户容量为 :accountLimit MB.。你已经使用了 :currentAccountSize MB(约 :percentUsage%)。', + 'storage_upgrade_notice' => '升级您的帐户, 以便上传文档和照片。', + 'storage_description' => '在这里, 您可以看到上传的有关您的联系人的所有文档和照片。', + + 'dav_title' => 'WebDAV', + 'dav_description' => '在这里, 您可以找到所有设置, 以便为 Carddav 和 CalDAV 导出使用 webdav 资源。', + 'dav_copy_help' => '复制到您的剪贴板', + 'dav_clipboard_copied' => '值已复制到剪贴板', + 'dav_url_base' => '所有CardDAV和CalDAV资源的基本 url:', + 'dav_connect_help' => '您可以在手机或计算机上使用此基本 url 连接您的联系人和/或日历。', + 'dav_connect_help2' => 'Use your login (email) and create an API token as the password to authenticate.', + 'dav_url_carddav' => '用于联系资源的CardDAV', + 'dav_url_caldav_birthdays' => '用于生日资源的 caldav url:', + 'dav_url_caldav_tasks' => '用于任务资源的 caldav url:', + 'dav_title_carddav' => 'CardDAV', + 'dav_title_caldav' => 'CalDAV', + 'dav_carddav_export' => '导出一个文件中的所有联系人', + 'dav_caldav_birthdays_export' => '在一个文件中导出所有生日', + 'dav_caldav_tasks_export' => '导出一个文件中的所有任务', + + 'archive_title' => 'Archive all of the contacts in your account', + 'archive_desc' => 'This will archive all of the contacts in your account.', + 'archive_cta' => 'Archive all of your contacts', + + 'logs_title' => 'Everything that has happened to this account', + 'logs_actor' => 'Actor', + 'logs_timestamp' => 'Timestamp', + 'logs_description' => 'Description', + 'logs_subject' => 'Subject', + 'logs_size' => 'Size (Kb)', + 'logs_object' => 'Object', +]; diff --git a/resources/lang/zh/validation.php b/resources/lang/zh/validation.php new file mode 100644 index 0000000..a67f636 --- /dev/null +++ b/resources/lang/zh/validation.php @@ -0,0 +1,166 @@ + '您必须同意 :attribute。', + 'active_url' => ':attribute 不是一个有效的URL网址', + 'after' => ':attribute 必须是一个在 :date 之后的日期。', + 'after_or_equal' => ':attribute 必须是一个在 :date 或之后的日期。', + 'alpha' => ':attribute 只能包含字母。', + 'alpha_dash' => ':attribute 只能由字母、数字、短划线(-)和下划线(_)组成。', + 'alpha_num' => ':attribute 只允许包含字母和数字', + 'array' => ':attribute 必须是个数组。', + 'before' => ':attribute 必须在 :date 之前', + 'before_or_equal' => ':attribute 必须在 :date 或之前', + 'between' => [ + 'numeric' => ':attribute 必须在 :min 和 :max 之间。', + 'file' => ':attribute 必须在 :min 千字节到 :max 千字节之间。', + 'string' => ':attribute 必须在 :min 到 :max 字符之间', + 'array' => ':attribute 必须在 :min 到 :max 个数目之间', + ], + 'boolean' => ':attribute 字段必须为 true 或 false。', + 'confirmed' => ':attribute 与确认项目不匹配', + 'date' => ':attribute 不是个有效日期', + 'date_equals' => ':attribute 必须要等于 :date。', + 'date_format' => ':attribute 不符合 :format 的格式', + 'different' => ':attribute 和 :other 不能相同。', + 'digits' => ':attribute 必须是 :digits 数字', + 'digits_between' => ':attribute 必须是 :min - :max 位数字。', + 'dimensions' => ':attribute 的图片无效', + 'distinct' => ':属性字段具有重复值。', + 'email' => ':attribute 必须是一个有效的电子邮件地址。', + 'ends_with' => ':attribute 必须以 :values 为结尾。', + 'exists' => '选择的 :attribute 无效', + 'file' => ':attribute 必须是个文件', + 'filled' => ':attribute 字段必须有一个值', + 'gt' => [ + 'numeric' => ':attribute 必须大于 :value。', + 'file' => ':attribute 必须大于 :value KB。', + 'string' => ':attribute 必须多于 :value 个字符。', + 'array' => ':attribute 必须多于 :value 个元素。', + ], + 'gte' => [ + 'numeric' => ':attribute 必须大于或等于 :value。', + 'file' => ':attribute 必须大于或等于 :value KB。', + 'string' => ':attribute 必须多于或等于 :value 个字符。', + 'array' => ':attribute 必须多于或等于 :value 个元素。', + ], + 'image' => ':attribute 必须是图片。', + 'in' => '选择的 :attribute 无效', + 'in_array' => ':attribute 不在 :other 中。', + 'integer' => ':attribute 必须是整数', + 'ip' => ':attribute 必须是一个有效的 IP 地址', + 'ipv4' => ':attribute 必须是一个有效的 IPv4 地址', + 'ipv6' => ':attribute 必须是一个有效的 IPv6 地址', + 'json' => ':属性必须是有效的JSON字符串。', + 'lt' => [ + 'numeric' => ':attribute 必须小于 :value。', + 'file' => ':attribute 必须小于 :value KB。', + 'string' => ':attribute 必须少于 :value 个字符。', + 'array' => ':attribute 必须少于 :value 个元素。', + ], + 'lte' => [ + 'numeric' => ':attribute 必须小于或等于 :value。', + 'file' => ':attribute 必须小于或等于 :value KB。', + 'string' => ':attribute 必须少于或等于 :value 个字符。', + 'array' => ':attribute 必须少于或等于 :value 个元素。', + ], + 'max' => [ + 'numeric' => ':attribute 不大于 :max', + 'file' => ':attribute 不大于 :max kb', + 'string' => ':attribute 不大于 :max 字符', + 'array' => ':attribute 的数量不能超过 :max 个。', + ], + 'mimes' => ':attribute 文件类型必须是 :values。', + 'mimetypes' => ':attribute 文件类型必须是 :values。', + 'min' => [ + 'numeric' => ':attribute 最少是 :min', + 'file' => ':attribute 最小是 :min 千字节', + 'string' => ':attribute 最少为 :min个字符', + 'array' => ':attribute 至少为 :min 个', + ], + 'not_in' => '选择的 :attribute 无效', + 'not_regex' => ':attribute 格式无效', + 'numeric' => ':attribute 必须是数字。', + 'password' => '密码错误', + 'present' => ':attribute 为必填项。', + 'regex' => ':attribute 格式不对', + 'required' => ':attribute 字段必填', + 'required_if' => ':attribute 字段在 :other 是 :value 时是必须的', + 'required_unless' => ':attribute 是必须的除非 :other 在 :values 中。', + 'required_with' => '当 :values 不存在时, :attribute 是必需的', + 'required_with_all' => '当 :values 存在时 :attribute 不能为空。', + 'required_without' => '当 :values 不存在时, :attribute 是必填的。', + 'required_without_all' => '当没有任何 :values 存在时, :attribute 字段为必填项。', + 'same' => ':attribute 和 :other 必需匹配', + 'size' => [ + 'numeric' => ':attribute 必需是 :size', + 'file' => ':attribute 必需是 :size kb', + 'string' => ':attribute 必须包含 :size 个字符。', + 'array' => ':attribute 必须包含 :size 个项。', + ], + 'starts_with' => ':attribute 必须以 :values 为开头。', + 'string' => ':attribute 必须是一个字符串。', + 'timezone' => ':attribute 必须是个有效的区域。', + 'unique' => ':attribute 已经被占用', + 'uploaded' => ':attribute上传失败.', + 'url' => ':attribute 格式不对', + 'uuid' => ':attribute 必须是有效的 UUID。', + + /* + |-------------------------------------------------------------------------- + | Custom Validation Language Lines + |-------------------------------------------------------------------------- + | + | Here you may specify custom validation messages for attributes using the + | convention "attribute.rule" to name the lines. This makes it quick to + | specify a specific custom language line for a given attribute rule. + | + */ + + 'custom' => [ + 'attribute-name' => [ + 'rule-name' => '自定义消息', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Custom Validation Attributes + |-------------------------------------------------------------------------- + | + | The following language lines are used to swap our attribute placeholder + | with something more reader friendly such as "E-Mail Address" instead + | of "email". This simply helps us make our message more expressive. + | + */ + + 'attributes' => [], + + 'vue' => [ + 'max' => [ + 'numeric' => '{field} 不能大于 {max}', + 'string' => '{field} 不能超过 {max} 个字符', + ], + 'required' => '{field} 必填', + 'url' => '{field} 不是一个有效的URL地址', + ], + +]; diff --git a/resources/sass/_custom_bootstrap.scss b/resources/sass/_custom_bootstrap.scss new file mode 100644 index 0000000..57b4cb2 --- /dev/null +++ b/resources/sass/_custom_bootstrap.scss @@ -0,0 +1,48 @@ +// Bootstrap + +html { + font-size: 14px; +} + +// Includes all the imports from the standard Bootstrap project + +@import "~bootstrap/scss/bootstrap-reboot"; +@import "~bootstrap/scss/bootstrap-grid"; + +// @import "~bootstrap/scss/_functions"; +// @import "~bootstrap/scss/_variables"; +// @import "~bootstrap/scss/_mixins"; +// @import "~bootstrap/scss/_root"; +// @import "~bootstrap/scss/_reboot"; +// @import "~bootstrap/scss/_type"; +// @import "~bootstrap/scss/_images"; +// @import "~bootstrap/scss/_code"; +// @import "~bootstrap/scss/_grid"; +@import "~bootstrap/scss/_tables"; +@import "~bootstrap/scss/_forms"; +// @import "~bootstrap/scss/_buttons"; +// @import "~bootstrap/scss/_transitions"; +@import "~bootstrap/scss/_dropdown"; +// @import "~bootstrap/scss/_button-group"; +// @import "~bootstrap/scss/_input-group"; +// @import "~bootstrap/scss/_custom-forms"; +@import "~bootstrap/scss/_nav"; +// @import "~bootstrap/scss/_navbar"; +@import "~bootstrap/scss/_card"; +// @import "~bootstrap/scss/_breadcrumb"; +@import "~bootstrap/scss/_pagination"; +// @import "~bootstrap/scss/_badge"; +// @import "~bootstrap/scss/_jumbotron"; +@import "~bootstrap/scss/_alert"; +// @import "~bootstrap/scss/_progress"; +// @import "~bootstrap/scss/_media"; +// @import "~bootstrap/scss/_list-group"; +@import "~bootstrap/scss/_close"; +// @import "~bootstrap/scss/_toasts"; +@import "~bootstrap/scss/_modal"; +// @import "~bootstrap/scss/_tooltip"; +// @import "~bootstrap/scss/_popover"; +// @import "~bootstrap/scss/_carousel"; +// @import "~bootstrap/scss/_spinners"; +// @import "~bootstrap/scss/_utilities"; +@import "~bootstrap/scss/_print"; diff --git a/resources/sass/_datatable.min.scss b/resources/sass/_datatable.min.scss new file mode 100644 index 0000000..427f9ec --- /dev/null +++ b/resources/sass/_datatable.min.scss @@ -0,0 +1,225 @@ +table.dataTable { + clear: both; + margin-top: 6px !important; + margin-bottom: 6px !important; + max-width: none !important; + border-collapse: separate !important +} + +table.dataTable td, +table.dataTable th { + -webkit-box-sizing: content-box; + box-sizing: content-box +} + +table.dataTable td.dataTables_empty, +table.dataTable th.dataTables_empty { + text-align: center +} + +table.dataTable.nowrap th, +table.dataTable.nowrap td { + white-space: nowrap +} + +div.dataTables_wrapper div.dataTables_length label { + font-weight: normal; + text-align: left; + white-space: nowrap +} + +div.dataTables_wrapper div.dataTables_length select { + width: 75px; + display: inline-block +} + +div.dataTables_wrapper div.dataTables_filter { + text-align: right +} + +div.dataTables_wrapper div.dataTables_filter label { + font-weight: normal; + white-space: nowrap; + text-align: left +} + +div.dataTables_wrapper div.dataTables_filter input { + margin-left: 0.5em; + display: inline-block; + width: auto +} + +div.dataTables_wrapper div.dataTables_info { + padding-top: 0.85em; + white-space: nowrap +} + +div.dataTables_wrapper div.dataTables_paginate { + margin: 0; + white-space: nowrap; + text-align: right +} + +div.dataTables_wrapper div.dataTables_paginate ul.pagination { + margin: 2px 0; + white-space: nowrap +} + +div.dataTables_wrapper div.dataTables_processing { + position: absolute; + top: 50%; + left: 50%; + width: 200px; + margin-left: -100px; + margin-top: -26px; + text-align: center; + padding: 1em 0 +} + +table.dataTable thead>tr>th.sorting_asc, +table.dataTable thead>tr>th.sorting_desc, +table.dataTable thead>tr>th.sorting, +table.dataTable thead>tr>td.sorting_asc, +table.dataTable thead>tr>td.sorting_desc, +table.dataTable thead>tr>td.sorting { + padding-right: 30px +} + +table.dataTable thead>tr>th:active, +table.dataTable thead>tr>td:active { + outline: none +} + +table.dataTable thead .sorting, +table.dataTable thead .sorting_asc, +table.dataTable thead .sorting_desc, +table.dataTable thead .sorting_asc_disabled, +table.dataTable thead .sorting_desc_disabled { + cursor: pointer; + position: relative +} + +table.dataTable thead .sorting:before, +table.dataTable thead .sorting:after, +table.dataTable thead .sorting_asc:before, +table.dataTable thead .sorting_asc:after, +table.dataTable thead .sorting_desc:before, +table.dataTable thead .sorting_desc:after, +table.dataTable thead .sorting_asc_disabled:before, +table.dataTable thead .sorting_asc_disabled:after, +table.dataTable thead .sorting_desc_disabled:before, +table.dataTable thead .sorting_desc_disabled:after { + position: absolute; + bottom: 0.9em; + display: block; + opacity: 0.3 +} + +table.dataTable thead .sorting:before, +table.dataTable thead .sorting_asc:before, +table.dataTable thead .sorting_desc:before, +table.dataTable thead .sorting_asc_disabled:before, +table.dataTable thead .sorting_desc_disabled:before { + right: 23px; + content: "\2191"; + top: 2px; +} + +table.dataTable thead .sorting:after, +table.dataTable thead .sorting_asc:after, +table.dataTable thead .sorting_desc:after, +table.dataTable thead .sorting_asc_disabled:after, +table.dataTable thead .sorting_desc_disabled:after { + right: 10px; + content: "\2193"; + top: 2px; +} + +table.dataTable thead .sorting_asc:before, +table.dataTable thead .sorting_desc:after { + opacity: 1 +} + +table.dataTable thead .sorting_asc_disabled:before, +table.dataTable thead .sorting_desc_disabled:after { + opacity: 0 +} + +div.dataTables_scrollHead table.dataTable { + margin-bottom: 0 !important +} + +div.dataTables_scrollBody table { + border-top: none; + margin-top: 0 !important; + margin-bottom: 0 !important +} + +div.dataTables_scrollBody table thead .sorting:after, +div.dataTables_scrollBody table thead .sorting_asc:after, +div.dataTables_scrollBody table thead .sorting_desc:after { + display: none +} + +div.dataTables_scrollBody table tbody tr:first-child th, +div.dataTables_scrollBody table tbody tr:first-child td { + border-top: none +} + +div.dataTables_scrollFoot table { + margin-top: 0 !important; + border-top: none +} + +@media screen and (max-width: 767px) { + div.dataTables_wrapper div.dataTables_length, + div.dataTables_wrapper div.dataTables_filter, + div.dataTables_wrapper div.dataTables_info, + div.dataTables_wrapper div.dataTables_paginate { + text-align: center + } +} + +table.dataTable.table-condensed>thead>tr>th { + padding-right: 20px +} + +table.dataTable.table-condensed .sorting:after, +table.dataTable.table-condensed .sorting_asc:after, +table.dataTable.table-condensed .sorting_desc:after { + top: 6px; + right: 6px +} + +table.table-bordered.dataTable th, +table.table-bordered.dataTable td { + border-left-width: 0 +} + +table.table-bordered.dataTable th:last-child, +table.table-bordered.dataTable th:last-child, +table.table-bordered.dataTable td:last-child, +table.table-bordered.dataTable td:last-child { + border-right-width: 0 +} + +table.table-bordered.dataTable tbody th, +table.table-bordered.dataTable tbody td { + border-bottom-width: 0 +} + +div.dataTables_scrollHead table.table-bordered { + border-bottom-width: 0 +} + +div.table-responsive>div.dataTables_wrapper>div.row { + margin: 0 +} + +div.table-responsive>div.dataTables_wrapper>div.row>div[class^="col-"]:first-child { + padding-left: 0 +} + +div.table-responsive>div.dataTables_wrapper>div.row>div[class^="col-"]:last-child { + padding-right: 0 +} diff --git a/resources/sass/_variables.scss b/resources/sass/_variables.scss new file mode 100644 index 0000000..d8b9de9 --- /dev/null +++ b/resources/sass/_variables.scss @@ -0,0 +1,19 @@ +// Colors +// $blue: +// $indigo: +// $purple: +// $pink: +// $red: +// $orange: +// $yellow: +// $green: +// $teal: +// $cyan: + +// Body +// $body-bg: + +// Typography +$font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; +$font-size-base: 1rem; +$line-height-base: 1.5; diff --git a/resources/sass/app-ltr.scss b/resources/sass/app-ltr.scss new file mode 100644 index 0000000..0e70b0b --- /dev/null +++ b/resources/sass/app-ltr.scss @@ -0,0 +1,675 @@ +$htmldir: ltr !default; + +// Variables +@import "variables"; + +// Bootstrap +@import "custom_bootstrap"; + +@import "~hint.css/hint.min"; + +@import "~tachyons/css/tachyons.min"; + +// For the datatables +@import "_datatable.min"; + +// Icon fonts +@import "~font-awesome/scss/font-awesome"; + +// Tooltip +@import "~vue-directive-tooltip/src/css/index.scss"; + +// Radio button +@import "~pretty-checkbox/src/pretty-checkbox"; + +// datepicker +@import "~@hokify/vuejs-datepicker/dist/vuejs-datepicker.css"; + +// Tables +@import "~vue-good-table/dist/vue-good-table.css"; + +@import "buttons"; +@import "header"; +@import "people"; +@import "journal"; +@import "marketing"; +@import "settings"; +@import "modal"; +@import "changelog"; + +// Custom colors + +// Extending Tachyions +$bg-hover-monica: #d7e3ec; + +.bg-gray-monica { + background-color: #f2f4f8; +} + +.bg-blue-monica { + background-color: #325776; +} + +.b--gray-monica { + border-color: #d0d0d0; +} + +.bg-hover-monica { + &:hover { + background-color: $bg-hover-monica; + } +} + +.bg-pale-red { + background-color: #f4cecd; +} + +.box-shadow { + background: #ffffff; + border: 1px solid #d0d0d0; + box-shadow: 1px -1px 4px #d0d0d0; + border-radius: 11px; +} + +.w-5 { + width: 5%; +} + +.w-95 { + width: 95%; +} + +.w-12 { + width: calc(100% / 12); +} + +.form-error-message { + border-top: 1px solid #d9534f; + background-color: #f4cecd; + box-shadow: inset 0 3px 0 0 #d9534f, inset 0 0 0 0 transparent, 0 0 0 1px #eeeeee, 0 1px 3px 0 #d0d0d0; +} + +.form-information-message { + border-top: 1px solid #0366d5; + background-color: #d1ecfa; + box-shadow: inset 0 3px 0 0 #228b22, inset 0 0 0 0 transparent, 0 0 0 1px #eeeeee, 0 1px 3px 0 #d0d0d0; + + svg { + width: 20px; + color: #ffffff; + } +} + +// Utilities +.border-bottom { + border-bottom: 1px solid $border-color; +} + +.border-top { + border-top: 1px solid $border-color; +} + +.border-right { + border-right: 1px solid $border-color; +} + +.border-left { + border-left: 1px solid $border-color; +} + +.padding-left-none { + padding-left: 0; +} + +.boxed { + background: #ffffff; + border: 1px solid $border-color; + border-radius: 3px; + box-shadow: 0 1px 3px 0 #eeeeee; +} + +.box-padding { + padding: 15px; +} + +.badge { + display: inline-block; + padding: 4px 5px; + font-size: 75%; + font-weight: 700; + line-height: 1; + color: #ffffff; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: .25rem; +} + +.badge-success { + background-color: #32cd32; +} + +.badge-danger { + background-color: #d9534f; +} + +kbd { + padding:0.1em 0.6em; + border:1px solid #d0d0d0; + font-size:11px; + font-family:Arial,Helvetica,sans-serif; + background-color:#fafafa; + color:#313436; + box-shadow:0 1px 0px #999999,0 0 0 2px #ffffff inset; + border-radius:3px; + display:inline-block; + margin:0 0.1em; + text-shadow:0 1px 0 #ffffff; + line-height:1.4; + white-space:nowrap; +} + +.life-event { + .life-event-add-row { + &:hover { + background-color: $bg-hover-monica; + } + } + + .life-event-add-arrow { + right: 10px; + top: 12px; + } + + .life-event-add-icon { + background-color: #d7e3ec; + border-radius: 50%; + padding: 20px; + width: 65px; + + img { + width: 70px; + } + } +} + +.chart-activities { + display: table; + table-layout: fixed; + width: 100%; + max-width: 700px; + height: 200px; + background-image: linear-gradient(to bottom, #d0d0d0 2%, transparent 2%); + background-size: 100% 50px; + background-position: left top; + + li { + position: relative; + display: table-cell; + vertical-align: bottom; + height: 200px; + } + + span { + margin: 0 1em; + display: block; + background: #d1ecfa; + animation: draw 1s ease-in-out; + + &:before{ + position: absolute; + left: 0; + right: 0; + top: 100%; + padding: 5px 1em 0; + display: block; + text-align: center; + content: attr(title); + word-wrap: break-word; + } + } +} + +@keyframes draw{ + 0%{height:0;} +} + +// Generic styles +body { + color: #4a4a4a; + @if $htmldir == rtl { + text-align: right; + } +} + +a { + color: #0366d5; + padding: 1px; + text-decoration: underline; + + &:hover { + background-color: #0366d5; + color: #ffffff; + text-decoration: none; + } + + &.action-link { + color: #999999; + font-size: 11px; + text-decoration: underline; + margin-right: 5px; + + @if $htmldir == rtl { + float: right; + } + + &:hover { + background-color: #313436; + color: #eeeeee; + } + } +} + +a[hreflang]:after { + content: " (" attr(hreflang) ")"; +} + +ul { + list-style-type: none; + margin: 0; + padding: 0; + + &.horizontal { + li { + display: inline; + } + } +} + +.pretty { + white-space: inherit; + &.form-check-input { + @if $htmldir == ltr { + margin-left: 0; + } @else { + margin-right: 0; + } + } + .state label{ + text-indent: 0; + @if $htmldir == ltr { + padding-left: 2rem; + } @else { + padding-right: 2rem; + } + line-height: 1.4em; + &:after, &:before { + top: 0; + } + } +} + +.markdown { + ul { + list-style-type: disc; + margin-left: 15px; + padding-left: 0; + margin-top: 10px; + margin-bottom: 10px; + } +} + +.hidden { + display: none; +} + +input:disabled { + background-color: #999999; +} + +.pagination-box { + margin-top: 30px; + text-align: center; +} + +.alert-success { + margin: 20px 0; +} + +.central-form { + margin-top: 40px; + + h2 { + font-weight: 400; + margin-bottom: 20px; + text-align: center; + } + + .offset-sm-3-right { + margin-right: 25%; + } + + .form-check-inline { + margin-right: 10px; + } + + .form-group > label:not(:first-child) { + margin-top: 10px; + } + + input[type="radio"] { + margin-right: 5px; + } + + .dates { + .form-inline { + display: inline; + + input[type="number"] { + margin: 0 10px; + width: 52px; + } + + input[type="date"] { + margin-left: 20px; + margin-top: 10px; + } + } + } + + .form-group:not(:last-child) { + border-bottom: 1px solid #eeeeee; + padding-bottom: 20px; + } + + .nav { + margin-top: 40px; + + .nav-link { + text-decoration: none; + } + } + + .tab-content { + border-right: 1px solid #d0d0d0; + border-left: 1px solid #d0d0d0; + border-bottom: 1px solid #d0d0d0; + padding: 15px; + } +} + +.profile-page-avatar { + left: 48%; + top: -60px; + + img, div { + border-radius: 50%; + } + + @media (max-width: 480px) { + left: 40%; + } +} + +.avatar-photo { + img { + border-radius: 3px; + } +} + +.breadcrumb { + background-color: #fafafa; + + ul { + font-size: 12px; + padding: 30px 0 24px; + + li:not(:last-child):after { + content: '>'; + @if $htmldir == ltr { + margin-left: 5px; + margin-right: 1px; + } @else { + margin-right: 5px; + margin-left: 1px; + } + } + } +} + +.table { + border-collapse: collapse; + display: table; + width: 100%; + + .table-row { + border-left: 1px solid #d0d0d0; + border-right: 1px solid #d0d0d0; + border-top: 1px solid #d0d0d0; + display: table-row; + + &:first-child { + .table-cell:first-child { + border-top-left-radius: 3px; + } + + .table-cell:last-child { + border-top-right-radius: 3px; + } + } + + &:last-child { + border-bottom: 1px solid #d0d0d0; + } + + &:hover { + background-color: #fafafa; + } + } + + .table-cell { + display: table-cell; + padding: 8px 10px; + } + + .table-header { + color: #4a4a4a; + background-color: #f2f4f8; + font-size: 1.1rem; + font-weight: bolder; + } + + .audit-log-cell { + font-size: 0.9rem; + } +} + +.profile-selected { + .profile-selected-left { + border-left: 1px solid #d0d0d0; + border-top: 1px solid #d0d0d0; + border-bottom: 1px solid #d0d0d0; + border-top-left-radius: 3px; + border-bottom-left-radius: 3px; + } + + .profile-selected-right { + border-right: 1px solid #d0d0d0; + border-top: 1px solid #d0d0d0; + border-bottom: 1px solid #d0d0d0; + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; + } +} + +.full-page-modal { + background: #ffffff; + border: 1px solid #eeeeee; + box-shadow: 2px 0px 6px #999999; + border-radius: 12px; + + .full-page-modal-year-selector { + &:hover, &.selected { + background-color: #fafafa; + } + } +} + +.full-page-modal-header { + background: #ffffff; + border-top-left-radius: 12px; + border-top-right-radius: 12px; +} + +.column-list { + column-count: 3; + column-gap: 20px; +} + +.dt-row.hover { + &:hover { + background-color: #d7e3ec; + } +} + +.profile-edit-contact-button { + right: 20px; + top: 20px; +} + +.nowrap-link { + white-space: nowrap; +} + +// datasets +table.vgt-table { + font-size: 14px; + + td.vgt-table-date { + padding-left: 10px; + vertical-align: middle; + width: 110px; + } + + td.vgt-table-action .action-btn { + border: 1px solid transparent; + display: inline; + padding: 0px 8px 4px; + + &:hover { + box-shadow: 1px 0px 1px #d0d0d0, -1px 1px 1px #d0d0d0, 0px 1px 4px #d0d0d0; + border-radius: 3px; + } + } + + th { + padding-left: 10px; + font-size: 13px; + } + + td { + padding-left: 10px; + font-size: 14px; + } +} + +.vgt-wrap__footer { + padding: 3px 10px!important; + background: linear-gradient(#ffffff,#fafafa)!important; +} + +footer { + .badge-success { + font-size: 12px; + font-weight: 400; + } + + .show-version { + text-align: left; + + h2 { + font-size: 16px; + } + + .note { + margin-bottom: 20px; + + ul { + list-style-type: disc; + } + + li { + display: block; + font-size: 15px; + text-align: left; + } + } + } +} + +@media (max-width: 480px) { + .sidebar-box { + border: 1px solid $border-color; + border-radius: 3px; + + .sidebar-heading { + background-color: #fafafa; + margin-top: 0; + padding: 5px; + } + + .sidebar-blank { + background-color: #ffffff; + border: 0; + } + + li { + padding: 5px; + } + } + + .column-list { + column-count: 1; + column-gap: 20px; + } + + .chart-activities { + span{ + margin: 0 4px; + } + } +} + +// Fix svg alignment +svg { + vertical-align: baseline !important; +} + +// Input error handle +.form-group-error { + animation-name: shake; + animation-fill-mode: forwards; + animation-duration: .6s; + animation-timing-function: ease-in-out; + z-index: 99; +} + +.form-group-error .error { + border-color: #d9534f !important; + color:#f4cecd; +} + +// Shake animation +@keyframes shake { + 0%, 100% { + transform: translateX(0); + } + + 15%, 45%, 75% { + transform: translateX(0.375rem); + } + + 30%, 60%, 90% { + transform: translateX(-0.375rem); + } +} diff --git a/resources/sass/app-rtl.scss b/resources/sass/app-rtl.scss new file mode 100644 index 0000000..eb5b478 --- /dev/null +++ b/resources/sass/app-rtl.scss @@ -0,0 +1,3 @@ +$htmldir: rtl; + +@import "app-ltr"; diff --git a/resources/sass/buttons.scss b/resources/sass/buttons.scss new file mode 100644 index 0000000..8abcb6d --- /dev/null +++ b/resources/sass/buttons.scss @@ -0,0 +1,112 @@ +.btn { + color: #4a4a4a; + background-color: #d7e3ec; + background-image: -webkit-linear-gradient(270deg, #fafafa 0%, #d7e3ec 90%); + background-image: linear-gradient(-180deg, #fafafa 0%, #d7e3ec 90%); + position: relative; + display: inline-block; + padding: 6px 12px; + font-size: 14px; + font-weight: 600; + line-height: 20px; + white-space: nowrap; + vertical-align: middle; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + background-repeat: repeat-x; + background-position: -1px -1px; + background-size: 110% 110%; + border: 1px solid #d0d0d0; + border-radius: 0.25em; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + text-decoration: none; + + &:hover, &:focus { + text-decoration: none; + background-color: #d0d0d0; + background-image: -webkit-linear-gradient(270deg, #d7e3ec 0%, #d0d0d0 90%); + background-image: linear-gradient(-180deg, #d7e3ec 0%, #d0d0d0 90%); + background-position: 0 -0.5em; + background-repeat: repeat-x; + border-color: #999999; + color: #0366d5; + } + + &:active { + background-color: #d0d0d0; + background-image: none; + border-color: #999999; + box-shadow: inset 0 0.15em 0.3em #eeeeee; + } + + &:disabled { + background-image: linear-gradient(-180deg, #d0d0d0 0%, #d0d0d0 90%); + } + + &:focus { + outline: none; + text-decoration: none; + } +} + +.btn-primary { + color: #ffffff; + background-color: #228b22; + background-image: -webkit-linear-gradient(270deg, #32cd32 0%, #228b22 90%); + background-image: linear-gradient(-180deg, #32cd32 0%, #228b22 90%); + + &:hover, &:focus { + background-color: #228b22; + background-image: -webkit-linear-gradient(270deg, #32cd32 0%, #228b22 90%); + background-image: linear-gradient(-180deg, #32cd32 0%, #228b22 90%); + background-position: 0 -0.5em; + border-color: #228b22; + color: #ffd700; + } +} + +.btn-danger { + color: #b22222; + + &:hover, &:focus { + background-color: #b22222; + background-image: linear-gradient(#d9534f, #b22222); + border-color: #b22222; + color: #ffffff; + } +} + +.btn-warning { + color: #daa520; + + &:hover, &:focus { + background-color: #daa520; + background-image: linear-gradient(#fffacd, #daa520); + border-color: #daa520; + color: #ffffff; + } +} + +.btn-add { + position: relative; + top: 3px; + border: 1px solid #4a4a4a; + border-radius: 50%; + width: 15px; + margin-right: 3px; +} + +.small-btn { + background: #ffffff; + color: #4a4a4a; + opacity: 0.8; + box-shadow: 1px 0px 1px #d0d0d0, -1px 1px 1px #d0d0d0, 0px 1px 4px #d0d0d0; + border-radius: 11px; + font-weight: 500; + text-decoration: none; +} diff --git a/resources/sass/changelog.scss b/resources/sass/changelog.scss new file mode 100644 index 0000000..d745608 --- /dev/null +++ b/resources/sass/changelog.scss @@ -0,0 +1,7 @@ +.changelog { + img { + max-width: 100%; + border: 1px solid #d0d0d0; + padding: 3px; + } +} diff --git a/resources/sass/header.scss b/resources/sass/header.scss new file mode 100644 index 0000000..bf950dc --- /dev/null +++ b/resources/sass/header.scss @@ -0,0 +1,240 @@ +.header-logo { + text-decoration: none; + + &:hover { + text-decoration: none; + background-color: transparent; + } +} + +.header-search { + padding: 0; + position: relative; + margin: auto 0; +} + +.header-search-form { + position: relative; + + span { + color: #d0d0d0; + font-size: 12px; + left: 10px; + position: absolute; + top: 10px; + } + + input { + border: 0; + color: #ffffff; + padding-left: 29px; + } +} + +.header-nav { + @if $htmldir == ltr { + text-align: right; + } @else { + text-align: left; + } + + .header-nav-item { + display: inline; + margin-right: 10px; + + @if $htmldir == ltr { + &:last-child { + margin-right: 0; + } + } @else { + &:last-child { + margin-right: 10px; + } + &:first-child { + margin-right: 0; + } + } + } +} + +.header-nav-item-link { + color: #ffffff; + font-weight: 300; + padding: 3px 11px; + text-decoration: none; + + &:hover { + background-color: #497193; + border-radius: 3px; + color: #ffffff; + padding: 3px 11px; + text-decoration: none; + } + + svg { + position: relative; + top: 3px; + } +} + +.header-search-input { + background: #497193; + border-color: #497193; + color: #ffffff; + + &::placeholder { + opacity: 0.5; + color: #f2f4f8; + } + &:-ms-input-placeholder { + opacity: 0.5; + color: #f2f4f8; + } + + &:focus { + background: #f2f4f8; + color: #313436; + } +} + +.header-search-results { + position: absolute; + width: 100%; + z-index: 10; +} + +.header-search-result { + position: relative; + background: #ffffff; + box-shadow: 0 3px 3px 0 #999999; + border-bottom: 1px solid #eeeeee; + + a { + color: inherit; + text-decoration: none; + vertical-align: middle; + background: transparent; + + span { + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + + z-index: 1; + } + } + + a:hover { + background: inherit; + color: inherit; + } + + .avatar { + border-radius: 3px; + display: inline-block; + height: 36px; + margin: 10px; + width: 36px; + } + + .avatar-initials { + text-align: center; + padding-top: 6px; + font-size: 15px; + color: #ffffff; + } + + &:last-child { + border-bottom: initial; + } +} + +.header-search-result:hover { + background: #fafafa; +} + +.pulse { + -webkit-animation: pulse 3s linear infinite; + -moz-animation: pulse 3s linear infinite; + -ms-animation: pulse 3s linear infinite; + animation: pulse 3s linear infinite; + + &:before { + position: absolute; + content: ''; + background-color:#32cd32; + border-radius:50%; + width: 9px; + height: 9px; + pointer-events: none; + top: 16px; + left: 17px; + z-index: 10000; + } +} + +@keyframes pulse { + 0% { + -webkit-transform: scale(1.1); + -moz-transform: scale(1.1); + -o-transform: scale(1.1); + -ms-transform: scale(1.1); + transform: scale(1.1); + } + 50% { + -webkit-transform: scale(0.8); + -moz-transform: scale(0.8); + -o-transform: scale(0.8); + -ms-transform: scale(0.8); + transform: scale(0.8); + } + 100% { + -webkit-transform: scale(1); + -moz-transform: scale(1); + -o-transform: scale(1); + -ms-transform: scale(1); + transform: scale(1); + } +} + +@media (max-width: 767px) { + header { + .mobile-menu { + border: 1px solid #325776; + margin-bottom: 20px; + + li { + border-bottom: 1px solid #497193; + margin-bottom: 0; + padding: 4px 0; + + a { + text-decoration: none; + } + + &:last-child { + border-bottom: 0; + } + + &.cta { + border: 0; + + a { + width: 100%; + } + } + } + } + } + + .header-search { + padding: 0; + margin: 20px 0; + + ul { + padding-right: 26px; + } + } +} diff --git a/resources/sass/journal.scss b/resources/sass/journal.scss new file mode 100644 index 0000000..edbde9c --- /dev/null +++ b/resources/sass/journal.scss @@ -0,0 +1,26 @@ +.journal-calendar-text { + top: 19px; + line-height: 16px; + width: 62px; +} + +.journal-calendar-box { + width: 62px; + @if $htmldir == ltr { + margin-right: 11px; + } @else { + margin-left: 11px; + } +} + +.journal-calendar-content { + width: calc(100% - 73px); +} + +.journal-line { + transition: all .2s; + + &:hover { + border-color: #0366d5; + } +} diff --git a/resources/sass/marketing.scss b/resources/sass/marketing.scss new file mode 100644 index 0000000..40a5032 --- /dev/null +++ b/resources/sass/marketing.scss @@ -0,0 +1,317 @@ +.marketing { + + &.homepage { + + .top-page { + background-color: #4a4a4a; + border-bottom: 1px solid #d0d0d0; + color: #ffffff; + padding-top: 40px; + text-align: center; + + .navigation { + position: absolute; + right: 20px; + top: 20px; + + a { + border: 1px solid #ffffff; + border-radius: 6px; + color: #ffffff; + padding: 10px; + text-decoration: none; + } + } + + h1 { + font-size: 32px; + font-weight: 300; + margin-bottom: 40px; + } + + p { + font-size: 18px; + font-weight: 300; + margin: 0 auto; + max-width: 550px; + + &.cta { + margin-bottom: 50px; + margin-top: 70px; + + a { + font-size: 20px; + font-weight: 300; + padding: 20px 50px; + } + } + } + + .logo { + margin-bottom: 20px; + } + } + + .before-sections { + text-align: center; + + h3 { + font-size: 25px; + font-weight: 300; + margin-bottom: 40px; + margin-top: 80px; + } + } + + .section-homepage { + border-bottom: 1px solid #d0d0d0; + padding: 60px 0; + + .visual { + text-align: center; + } + + h2 { + font-size: 18px; + font-weight: 300; + margin-bottom: 25px; + } + + &.dates { + h2 { + margin-top: 40px; + } + } + + &.activities { + h2 { + margin-top: 130px; + } + } + + &.features { + h3 { + font-size: 18px; + font-weight: 300; + margin-bottom: 40px; + text-align: center; + } + + ul { + li { + font-size: 16px; + margin: 10px auto; + max-width: 60%; + + i { + color: #228b22; + } + } + } + } + + &.try { + text-align: center; + + p { + margin-bottom: 50px; + margin-top: 70px; + + a { + font-size: 20px; + font-weight: 300; + padding: 20px 50px; + } + } + } + } + + .why { + background-color: #4a4a4a; + color: #ffffff; + padding-bottom: 50px; + + h3 { + font-size: 20px; + font-weight: 300; + margin-bottom: 30px; + padding-top: 50px; + text-align: center; + } + + p { + font-size: 16px; + font-weight: 300; + margin: 10px auto 20px; + max-width: 550px; + } + } + } + + .footer-marketing { + margin-bottom: 40px; + padding-top: 40px; + text-align: center; + + a { + margin-right: 10px; + } + } + + &.register { + background-color: #fafafa; + padding-top: 90px; + padding-bottom: 40px; + + .offset-md-3-right { + margin-right: 25%; + } + + .signup-box { + background-color: #ffffff; + border: 1px solid #d0d0d0; + border-radius: 5px; + padding: 50px 20px 20px; + + h1 { + font-weight: 700; + text-align: center; + } + + h2, h3 { + font-weight: 300; + text-align: center; + } + + h2 { + margin-top: 20px; + margin-bottom: 20px; + } + + h3 { + font-size: 15px; + margin-bottom: 30px; + } + + .form-inline { + label { + display: block; + } + } + + button { + margin-top: 10px; + width: 100%; + } + + a.action { + margin-top: 10px; + width: 100%; + text-align: center; + } + + .help { + font-size: 13px; + text-align: center; + } + + .checkbox { + display: none; + } + + .links { + margin-top: 20px; + + li { + font-size: 14px; + margin-bottom: 5px; + } + } + } + } + + .subpages { + .header { + background-color: #4a4a4a; + text-align: center; + } + } + +} + +.releases, .privacy, .statistics { + max-width: 750px; + margin-left: auto; + margin-right: auto; + padding: 20px 30px 100px 30px; + margin-top: 50px; + background-color: #ffffff; + box-shadow: 0px 8px 20px #d0d0d0; + + h2 { + text-align: center; + } + + h3 { + font-size: 15px; + margin-top: 30px; + } +} + +.releases { + ul { + list-style-type: disc; + margin-left: 20px; + } +} + +@media (max-width: 480px) { + .marketing { + &.homepage { + img { + max-width: 100%; + } + + .before-sections h3 { + margin-bottom: 0; + } + + .section-homepage { + &.people { + .visual { + margin-top: 40px; + } + } + + &.activities { + h2 { + margin-top: 0; + } + + .visual { + margin-top: 40px; + } + } + + &.features { + ul li { + max-width: 100%; + } + } + + &.try { + padding: 30px 0; + } + } + } + + &.register { + .signup-box { + .logo { + left: 39%; + top: -47px; + } + } + } + } +} diff --git a/resources/sass/modal.scss b/resources/sass/modal.scss new file mode 100644 index 0000000..742745a --- /dev/null +++ b/resources/sass/modal.scss @@ -0,0 +1,97 @@ +.modal { + h5 { + font-size: 20px; + font-weight: 500; + } + + label { + padding-left: 0; + } + + .close { + position: absolute; + @if $htmldir == ltr { + right: 19px; + } @else { + left: 19px; + } + top: 14px; + font-size: 30px; + } + + &.log-call { + .date-it-happened { + margin-top: 20px; + } + + .exact-date { + display: none; + margin-top: 20px; + + input { + display: inline; + } + } + } +} + +.modal-mask { + position: fixed; + z-index: 9998; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: #4a4a4a; + display: table; + transition: opacity .3s ease; +} + +.modal-wrapper { + display: table-cell; + vertical-align: middle; +} + +.modal-container { + max-width: 500px; + max-height: 80%; + margin: 0px auto; + padding: 20px 30px; + background-color: #ffffff; + border-radius: 2px; + box-shadow: 0 2px 8px #999999; + transition: all .3s ease; + font-family: Helvetica, Arial, sans-serif; + text-align: center; + + img { + max-height: 400px; + } +} + +/* + * The following styles are auto-applied to elements with + * transition="modal" when their visibility is toggled + * by Vue.js. + * + * You can easily play with the modal transition by editing + * these styles. + */ + +.modal-enter { + opacity: 0; +} + +.modal-leave-active { + opacity: 0; +} + +.modal-enter .modal-container, +.modal-leave-active .modal-container { + -webkit-transform: scale(1.1); + transform: scale(1.1); +} + +.sweet-modal-overlay { + -webkit-perspective: none !important; +} diff --git a/resources/sass/people.scss b/resources/sass/people.scss new file mode 100644 index 0000000..3e3bc6f --- /dev/null +++ b/resources/sass/people.scss @@ -0,0 +1,618 @@ +.avatars { + a { + text-decoration: none; + + &:hover { + background-color: transparent; + } + } +} + +.people-list { + .breadcrumb { + border-bottom: 1px solid #eeeeee; + } + + .main-content { + margin-top: 20px; + } + + .sidebar { + .sidebar-cta { + margin-bottom: 20px; + padding: 15px; + text-align: center; + width: 100%; + } + + li { + margin-bottom: 7px; + @if $htmldir == ltr { + padding-left: 15px; + } @else { + padding-right: 15px; + } + position: relative; + + &.selected::before { + color: #999999; + content: '>'; + left: 0; + position: absolute; + } + + .number-contacts-per-tag, + .number-contacts-without-tag { + @if $htmldir == ltr { + float: right; + } @else { + float: left; + } + } + } + .number-contacts-without-tag{ + font-size: 1rem; + } + } + + .list { + border: 1px solid #eeeeee; + border-radius: 3px; + } + + .clear-filter { + border: 1px solid #eeeeee; + position: relative; + padding: 6px; + border-radius: 3px; + + a { + position: absolute; + @if $htmldir == ltr { + right: 10px; + } @else { + left: 10px; + } + } + } + + .people-list-item { + border-bottom: 1px solid #eeeeee; + padding: 10px; + + &:hover { + background-color: #fafafa; + } + + &.sorting { + background-color: #fafafa; + position: relative; + padding: 10px; + + .options { + display: inline; + position: absolute; + @if $htmldir == ltr { + right: 10px; + } @else { + left: 10px; + } + + .dropdown-btn { + &:after { + content: '\f0d7'; + font-family: FontAwesome; + margin-left: 5px; + } + } + + .dropdown-item { + padding: 3px 20px 3px 10px; + + &:before { + content: '\f00c'; + font-family: FontAwesome; + margin-right: 5px; + color: #ffffff; + } + + &:hover { + background-color: #0366d5; + color: #ffffff; + } + + &.selected { + &:before { + color: #999999; + } + } + } + } + } + + a { + color: #313436; + text-decoration: none; + display: block; + + &:hover { + background-color: transparent; + color: #313436; + } + } + + .people-list-item-information { + color: #999999; + font-size: 12px; + font-style: italic; + position: relative; + text-align: right; + top: 16px; + @if $htmldir == ltr { + float: right; + } @else { + float: left; + } + } + } +} + +.blank-people-state { + margin-top: 30px; + text-align: center; + + h3 { + font-weight: 400; + margin-bottom: 30px; + } + + .cta-blank { + margin-bottom: 30px; + } + + .illustration-blank { + p { + margin-top: 30px; + } + + img { + display: block; + margin: 0 auto 20px; + } + } +} + +.avatar-header { + top: 40px; + margin-top: -30px; + + .image-header::after { + box-shadow: inset 1px 1px 3px 0 #d0d0d0; + border-radius: 7px; + content: ''; + display: block; + height: 100%; + position: absolute; + top: 0; + width: 100%; + } + + .hide-child:hover { + .child { + background: #4a4a4a; + } + + a:hover { + background: transparent; + text-decoration: underline; + } + } +} + +.people-show { + .main-content { + background-color: #ffffff; + padding-bottom: 20px; + padding-top: 40px; + + .section-title { + position: relative; + + h3 { + border-bottom: 1px solid #d0d0d0; + font-size: 18px; + font-weight: 400; + margin-bottom: 20px; + padding-bottom: 10px; + @if $htmldir == ltr { + padding-left: 23px; + } @else { + padding-right: 23px; + } + padding-top: 10px; + position: relative; + } + + .icon-section { + position: absolute; + top: 14px; + width: 17px; + } + } + + .sidebar { + .sidebar-cta { + a { + margin-bottom: 20px; + width: 100%; + } + } + } + } + + .profile { + .sidebar-box { + background-color: #f2f4f8; + border: 1px solid #eeeeee; + border-radius: 3px; + color: #313436; + margin-bottom: 25px; + padding: 10px; + position: relative; + font-size: 0.875rem; + + &.edit { + background-color: #fffceb; + border-color: #ffd700; + } + } + + .sidebar-box-title { + margin-bottom: 4px; + position: relative; + + h3 { + font-size: 12px; + font-weight: 500; + text-transform: uppercase; + } + + a { + position: absolute; + right: 7px; + } + + img { + left: -3px; + position: relative; + width: 20px; + + &.people-information { + top: -4px; + } + } + } + + .sidebar-box-paragraph:not(:last-child) { + margin-bottom: 9px; + } + + .people-list { + li { + margin-bottom: 4px; + } + } + + .people-information, + .work, + .introductions { + li { + color: #999999; + font-size: 12px; + margin-bottom: 10px; + + &:last-child { + margin-bottom: 0; + } + + i { + text-align: center; + width: 17px; + } + } + } + + .section { + margin-bottom: 35px; + + &.kids, + &.food-preferences { + .section-heading img { + position: relative; + top: -3px; + } + } + + .inline-action { + display: inline; + margin-left: 10px; + + a { + margin-right: 5px; + } + } + + .section-heading { + border-bottom: 1px solid #eeeeee; + padding-bottom: 4px; + margin-bottom: 10px; + + img { + width: 25px; + } + } + + .section-action { + display: inline; + float: right; + } + + .section-blank { + background-color: #fafafa; + border: 1px solid #eeeeee; + border-radius: 3px; + padding: 15px; + text-align: center; + + h3 { + font-weight: 400; + font-size: 14px; + } + } + } + } + + .gifts { + .gift-recipient { + font-size: 15px; + + &:not(:first-child) { + margin-top: 25px; + } + } + + .offered { + background-color: #32cd32; + border-radius: 10rem; + display: inline-block; + font-size: 75%; + font-weight: 400; + line-height: 1; + color: #ffffff; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + padding: 2px 0; + padding-right: .6em; + padding-left: .6em; + } + + .gift-list-item { + border-top: 1px solid #eeeeee; + padding: 5px 0; + + &:last-child { + border-bottom: 0; + } + } + + .gift-list-item-url { + display: inline; + font-size: 12px; + margin-left: 10px; + padding: 5px 0 0; + } + + .gift-list-item-information { + display: inline; + margin-left: 10px; + } + + .gift-list-item-date, + .gift-list-item-actions { + color: #999999; + display: inline; + font-size: 12px; + + a { + color: #999999; + font-size: 11px; + margin-right: 5px; + text-decoration: underline; + } + + li { + display: inline; + } + } + + .gift-list-item-actions { + margin-left: 5px; + } + + .for { + font-style: italic; + margin-left: 10px; + } + } + + .activities, + .reminders, + .tasks, + .debts, + .gifts { + .date { + color: #999999; + font-size: 12px; + margin-right: 10px; + width: 100px; + } + + .frequency-type, + .value { + background-color: #d7e3ec; + border: 1px solid #eeeeee; + border-radius: 3px; + display: inline; + font-size: 12px; + padding: 0 6px; + } + + .list-actions { + position: relative; + text-align: center; + width: 90px; + + a:not(:last-child) { + @if $htmldir == ltr { + margin-right: 5px; + } @else { + margin-left: 5px; + } + } + + a.edit { + position: relative; + top: 1px; + } + } + + .empty { + font-style: italic; + } + } + + .reminders { + .frequency-type { + white-space: nowrap; + } + + input[type='date'] { + margin-bottom: 20px; + width: 170px; + } + + .form-check { + input[type='number'] { + display: inline; + width: 50px; + } + } + } + + .debts { + .debts-list { + .debt-nature { + width: 220px; + } + } + } +} + +.create-people { + .import { + margin-bottom: 30px; + text-align: center; + } +} + +@media (max-width: 480px) { + .people-list { + margin-top: 20px; + + .people-list-mobile { + border-bottom: 1px solid $border-color; + + li { + padding: 6px 0; + } + } + + .people-list-item { + .people-list-item-information { + display: none; + } + } + } + + .people-show { + .main-content { + &.modal { + margin-top: 0; + } + + &.dashboard { + .sidebar-box { + margin-bottom: 15px; + } + + .sidebar-cta { + margin-top: 15px; + } + + .people-information-actions { + margin-bottom: 20px; + } + } + + &.activities { + .cta-mobile { + margin-bottom: 20px; + + a { + width: 100%; + } + } + + .activities-list { + .activity-item-date { + top: -4px; + } + } + } + } + } + + .create-people { + width: 100%; + + .btn { + width: 100%; + } + } + + .list-add-item { + margin-left: 0; + } + + .inline-form { + .task-add-title { + width: 100%; + } + + textarea { + width: 100%; + } + } + + .box-links { + margin-bottom: 10px; + position: relative; + right: 0; + top: 0; + + li { + margin-left: 0; + } + } +} diff --git a/resources/sass/settings.scss b/resources/sass/settings.scss new file mode 100644 index 0000000..5320cbf --- /dev/null +++ b/resources/sass/settings.scss @@ -0,0 +1,299 @@ +.settings { + .breadcrumb { + margin-bottom: 20px; + } + + .sidebar-menu { + ul { + border: 1px solid $border-color; + border-radius: 3px; + } + + li { + padding: 10px; + + &:not(:last-child) { + border-bottom: 1px solid $border-color; + } + + &.selected { + background-color: #fafafa; + + i { + color: green; + } + } + + a { + width: 100%; + } + + i { + @if $htmldir == ltr { + margin-right: 5px; + } @else { + margin-left: 5px; + } + color: #999999; + } + } + } + + .settings-reset, + .settings-delete, + .settings-group { + border: 1px solid; + padding: 10px; + margin-top: 40px; + + h2 { + font-weight: normal; + font-size: 16px; + } + + h3 { + font-weight: normal; + font-size: 14px; + } + } + + .settings-delete { + border-color: #d9534f; + border-radius: 3px; + } + + .settings-reset { + border-color: #daa520; + border-radius: 3px; + } + + .settings-group { + border-color: #999999; + border-radius: 3px; + } + + .warning-zone { + margin-bottom: 30px; + margin-top: 30px; + padding: 10px 10px 5px 15px; + border: 1px solid #daa520; + border-radius: 3px; + background-color: #fffacd; + } + + .users-list { + h3.with-actions { + padding-bottom: 13px; + + a { + float: right; + } + } + + .table-cell.actions { + @if $htmldir == ltr { + text-align: right; + } @else { + text-align: left; + } + } + } + + .tags-list { + h3.with-actions { + padding-bottom: 13px; + } + + .table-cell.actions { + @if $htmldir == ltr { + text-align: right; + } @else { + text-align: left; + } + } + } + + .blank-screen { + text-align: center; + + img { + margin-bottom: 30px; + margin-top: 30px; + } + + h2 { + font-weight: normal; + margin-bottom: 10px; + } + + h3 { + margin-top: 0; + border-bottom: 0; + } + + p { + margin: 0 auto; + width: 400px; + + &.cta { + margin-top: 40px; + margin-bottom: 10px; + } + } + + .requires-subscription { + margin-top: 20px; + font-size: 13px; + color: #999999; + } + } + + .subscriptions { + .upgrade-benefits { + margin-bottom: 20px; + + li { + margin-left: 20px; + list-style-type: disc; + } + } + + #label-card-element { + margin-bottom: 15px; + } + + .downgrade { + ul { + background-color: #fafafa; + border: 1px solid $border-color; + border-radius: 6px; + margin-bottom: 20px; + padding: 25px; + } + + li { + padding-bottom: 15px; + + &:not(:last-child) { + border-bottom: 1px solid $border-color; + } + + &:not(:first-child) { + margin-top: 10px; + } + + &.success { + .rule-title { + text-decoration: line-through; + } + + .icon:after { + font-family: FontAwesome; + font-size: 17px; + content: "\f058"; + top: 10px; + position: relative;; + } + } + + &.fail { + .icon:after { + font-family: FontAwesome; + font-size: 17px; + color: #d9534f; + content: "\f057"; + top: 10px; + position: relative;; + } + } + + .rule-title { + font-size: 18px; + padding-left: 5px; + } + + .rule-to-succeed { + font-size: 13px; + display: block; + padding-left: 27px; + } + } + } + } + + .report { + .report-summary { + background-color: #fafafa; + border: 1px solid $border-color; + border-radius: 3px; + margin-bottom: 30px; + + li { + padding: 5px 10px; + + &:not(:last-child) { + border-bottom: 1px solid $border-color; + } + + span { + font-weight: 600; + } + } + } + + .status { + text-align: center; + width: 95px; + } + + .reason { + font-style: italic; + } + } + + &.import { + .success { + color: #32cd32; + } + + .failure { + color: #d9534f; + } + + .warning { + color: #daa520; + } + + .date { + font-size: 13px; + margin-left: 10px; + } + + h3.with-actions { + padding-bottom: 13px; + + a { + float: right; + } + } + } + + &.upload { + .warning-zone { + padding: 20px 15px; + + ul { + @if $htmldir == ltr { + margin-left: 20px; + } @else { + margin-right: 20px; + } + list-style-type: disc; + } + } + } + + .reminder-info { + text-decoration: underline dotted; + } +} diff --git a/resources/sass/stripe.scss b/resources/sass/stripe.scss new file mode 100644 index 0000000..73c2a54 --- /dev/null +++ b/resources/sass/stripe.scss @@ -0,0 +1,22 @@ +.StripeElement { + background-color: #ffffff; + height: 45px; + width: 100%; + padding: 13px 12px; + border-radius: 4px; + border: 1px solid #999999; + -webkit-transition: box-shadow 150ms ease; + transition: box-shadow 150ms ease; +} + +.StripeElement--focus { + box-shadow: 0 1px 3px 0 #d0d0d0; +} + +.StripeElement--invalid { + border-color: #d9534f; +} + +.StripeElement--webkit-autofill { + background-color: #fffacd !important; +} diff --git a/resources/views/auth/emailchange1.blade.php b/resources/views/auth/emailchange1.blade.php new file mode 100644 index 0000000..700270d --- /dev/null +++ b/resources/views/auth/emailchange1.blade.php @@ -0,0 +1,49 @@ +@extends('marketing.skeleton') + +@section('content') + +
    +
    +
    + + +
    +
    +
    + +@endsection diff --git a/resources/views/auth/emailchange2.blade.php b/resources/views/auth/emailchange2.blade.php new file mode 100644 index 0000000..60fd548 --- /dev/null +++ b/resources/views/auth/emailchange2.blade.php @@ -0,0 +1,50 @@ +@extends('marketing.skeleton') + +@section('content') + +
    +
    +
    + +
    + +
    +
    + + + +@endsection diff --git a/resources/views/auth/emails/password.blade.php b/resources/views/auth/emails/password.blade.php new file mode 100644 index 0000000..6ab41db --- /dev/null +++ b/resources/views/auth/emails/password.blade.php @@ -0,0 +1 @@ +{{ trans('auth.password_reset_email_content') }} {{ $link }} diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php new file mode 100644 index 0000000..63c6312 --- /dev/null +++ b/resources/views/auth/login.blade.php @@ -0,0 +1,82 @@ +@extends('marketing.skeleton') + +@section('content') + +
    +
    +
    + + +
    +
    +
    + +@endsection diff --git a/resources/views/auth/oauthlogin.blade.php b/resources/views/auth/oauthlogin.blade.php new file mode 100644 index 0000000..bc20659 --- /dev/null +++ b/resources/views/auth/oauthlogin.blade.php @@ -0,0 +1,61 @@ +@extends('marketing.skeleton') + +@section('content') + +
    +
    +
    + + +
    +
    +
    + +@endsection diff --git a/resources/views/auth/passwords/email.blade.php b/resources/views/auth/passwords/email.blade.php new file mode 100644 index 0000000..e019fb3 --- /dev/null +++ b/resources/views/auth/passwords/email.blade.php @@ -0,0 +1,39 @@ +@extends('marketing.skeleton') + +@section('content') + +
    +
    +
    + + +
    +
    +
    + +@endsection diff --git a/resources/views/auth/passwords/reset.blade.php b/resources/views/auth/passwords/reset.blade.php new file mode 100644 index 0000000..22d9b11 --- /dev/null +++ b/resources/views/auth/passwords/reset.blade.php @@ -0,0 +1,49 @@ +@extends('marketing.skeleton') + +@section('content') + +
    +
    +
    + + +
    +
    + +
    + +@endsection diff --git a/resources/views/auth/recovery/login.blade.php b/resources/views/auth/recovery/login.blade.php new file mode 100644 index 0000000..dfe19e9 --- /dev/null +++ b/resources/views/auth/recovery/login.blade.php @@ -0,0 +1,49 @@ +@extends('marketing.skeleton') + +@section('content') + +
    +
    +
    + + +
    +
    +
    + +@endsection diff --git a/resources/views/auth/register.blade.php b/resources/views/auth/register.blade.php new file mode 100644 index 0000000..2f8a73c --- /dev/null +++ b/resources/views/auth/register.blade.php @@ -0,0 +1,109 @@ +@extends('marketing.skeleton') + +@section('content') + +
    +
    +
    +
    +
      +
    • + + + + + + + + {{ trans('auth.change_language_title') }} +
    • + @foreach($locales as $locale) +
    • + @if (App::isLocale($locale['lang'])) + {{ $locale['lang'] }} + @else + + {{ $locale['lang'] }} + + @endif +
    • + @endforeach +
    +
    + + + +
    +
    +
    + +@endsection diff --git a/resources/views/auth/validate2fa.blade.php b/resources/views/auth/validate2fa.blade.php new file mode 100644 index 0000000..6d51d18 --- /dev/null +++ b/resources/views/auth/validate2fa.blade.php @@ -0,0 +1,34 @@ +@extends('marketing.auth') + +@section('content') +
    +
    + +
    +
    + +
    + +
    +@endsection diff --git a/resources/views/auth/validatewebauthn.blade.php b/resources/views/auth/validatewebauthn.blade.php new file mode 100644 index 0000000..8748d3d --- /dev/null +++ b/resources/views/auth/validatewebauthn.blade.php @@ -0,0 +1,48 @@ +@extends('marketing.auth') + +@section('content') +
    +
    + +
    +
    + +
    +
    + +
    +
    + +@endsection diff --git a/resources/views/auth/verify.blade.php b/resources/views/auth/verify.blade.php new file mode 100644 index 0000000..d9c3ef2 --- /dev/null +++ b/resources/views/auth/verify.blade.php @@ -0,0 +1,42 @@ +@extends('marketing.skeleton') + +@section('content') + +
    +
    +
    + +
    +
    +
    + +@endsection diff --git a/resources/views/changelog/index.blade.php b/resources/views/changelog/index.blade.php new file mode 100644 index 0000000..91abb7c --- /dev/null +++ b/resources/views/changelog/index.blade.php @@ -0,0 +1,29 @@ +@extends('layouts.skeleton') + +@section('content') +
    + +
    +
    +

    {{ trans('changelog.title') }}

    + + @if (\App::getLocale() != 'en') +

    {{ trans('changelog.note') }}

    + @endif +
    + + @foreach ($changelogs as $changelog) +
    +
    + {{ $changelog['date'] }} +
    +
    +

    {{ $changelog['title'] }}

    + {!! (new \Parsedown())->text($changelog['description']) !!} +
    +
    + @endforeach +
    + +
    +@endsection diff --git a/resources/views/compliance/index.blade.php b/resources/views/compliance/index.blade.php new file mode 100644 index 0000000..e6b4291 --- /dev/null +++ b/resources/views/compliance/index.blade.php @@ -0,0 +1,17 @@ +@extends('layouts.skeleton') + +@section('content') +
    + +
    +

    {{ trans('app.compliance_title') }}

    +

    {!! trans('app.compliance_desc', ['url' => 'https://monicahq.com/privacy', 'urlterm' => 'https://monicahq.com/terms', 'hreflang' => 'en', ]) !!}

    +

    {{ trans('app.compliance_desc_end') }}

    + +
    + @csrf + +
    +
    +
    +@endsection diff --git a/resources/views/components/sidebar.blade.php b/resources/views/components/sidebar.blade.php new file mode 100644 index 0000000..cf85ba7 --- /dev/null +++ b/resources/views/components/sidebar.blade.php @@ -0,0 +1,11 @@ +@if (Route::currentRouteName() == $route) +
  15. + + {{ trans($title) }} +
  16. +@else +
  17. + + {{ trans($title) }} +
  18. +@endif diff --git a/resources/views/dashboard/_monthReminder.blade.php b/resources/views/dashboard/_monthReminder.blade.php new file mode 100644 index 0000000..49afb12 --- /dev/null +++ b/resources/views/dashboard/_monthReminder.blade.php @@ -0,0 +1,30 @@ +@foreach($reminderOutboxesList as $month => $reminderOutboxes) +

    {{ \App\Helpers\DateHelper::getMonthAndYear($month) }}

    +
      + @if(count($reminderOutboxes) > 0) + @foreach($reminderOutboxes as $reminderOutbox) + @if (!is_object($reminderOutbox->reminder)) + @continue; + @endif +
    • + {{ \App\Helpers\DateHelper::getShortDateWithoutYear($reminderOutbox->planned_date) }} + + @if ($reminderOutbox->reminder->contact->is_partial) + + @php($relatedRealContact = $reminderOutbox->reminder->contact->getRelatedRealContact()) + {{ $relatedRealContact->getIncompleteName() }} + + @else + + {{ $reminderOutbox->reminder->contact->getIncompleteName() }} + + @endif + + {{ $reminderOutbox->reminder->title }} +
    • + @endforeach + @else +

      {{ trans('dashboard.reminders_none') }}

      + @endif +
    +@endforeach diff --git a/resources/views/dashboard/blank.blade.php b/resources/views/dashboard/blank.blade.php new file mode 100644 index 0000000..56d9519 --- /dev/null +++ b/resources/views/dashboard/blank.blade.php @@ -0,0 +1,14 @@ +@extends('layouts.skeleton') + +@section('content') + +
    +
    + +
    +

    {{ trans('dashboard.dashboard_blank_title') }}

    +

    {{ trans('dashboard.dashboard_blank_description') }}

    +

    {{ trans('dashboard.dashboard_blank_cta') }}

    +
    + +@endsection diff --git a/resources/views/dashboard/blank_notes.blade.php b/resources/views/dashboard/blank_notes.blade.php new file mode 100644 index 0000000..36103e1 --- /dev/null +++ b/resources/views/dashboard/blank_notes.blade.php @@ -0,0 +1,23 @@ +
    + + + + + + + + + + + + + + + +

    + {{ trans('dashboard.notes_title') }} +

    +

    + {{ trans('dashboard.notes_description') }} +

    +
    diff --git a/resources/views/dashboard/events/_activity.blade.php b/resources/views/dashboard/events/_activity.blade.php new file mode 100644 index 0000000..f8844d1 --- /dev/null +++ b/resources/views/dashboard/events/_activity.blade.php @@ -0,0 +1,9 @@ +
    + +
    + + diff --git a/resources/views/dashboard/events/_call.blade.php b/resources/views/dashboard/events/_call.blade.php new file mode 100644 index 0000000..6b1ce09 --- /dev/null +++ b/resources/views/dashboard/events/_call.blade.php @@ -0,0 +1,9 @@ +
    + +
    + + diff --git a/resources/views/dashboard/events/_contact.blade.php b/resources/views/dashboard/events/_contact.blade.php new file mode 100644 index 0000000..2c70857 --- /dev/null +++ b/resources/views/dashboard/events/_contact.blade.php @@ -0,0 +1,9 @@ +
    + +
    + +
    + {{ $event['contact_complete_name'] }}: + + {{ trans('dashboard.event_'.$event['nature_of_operation'].'_'.$event['object_type']) }} +
    diff --git a/resources/views/dashboard/events/_debt.blade.php b/resources/views/dashboard/events/_debt.blade.php new file mode 100644 index 0000000..30fd636 --- /dev/null +++ b/resources/views/dashboard/events/_debt.blade.php @@ -0,0 +1,9 @@ +
    + +
    + +
    + {{ $event['contact_complete_name'] }}: + + {{ trans('dashboard.event_'.$event['nature_of_operation'].'_'.$event['object_type']) }} +
    diff --git a/resources/views/dashboard/events/_gift.blade.php b/resources/views/dashboard/events/_gift.blade.php new file mode 100644 index 0000000..f5a65a1 --- /dev/null +++ b/resources/views/dashboard/events/_gift.blade.php @@ -0,0 +1,9 @@ +
    + +
    + +
    + {{ $event['contact_complete_name'] }}: + + {{ trans('dashboard.event_'.$event['nature_of_operation'].'_'.$event['object_type']) }} +
    diff --git a/resources/views/dashboard/events/_note.blade.php b/resources/views/dashboard/events/_note.blade.php new file mode 100644 index 0000000..0614ddb --- /dev/null +++ b/resources/views/dashboard/events/_note.blade.php @@ -0,0 +1,9 @@ +
    + +
    + + diff --git a/resources/views/dashboard/events/_reminder.blade.php b/resources/views/dashboard/events/_reminder.blade.php new file mode 100644 index 0000000..c63cdff --- /dev/null +++ b/resources/views/dashboard/events/_reminder.blade.php @@ -0,0 +1,9 @@ +
    + +
    + +
    + {{ $event['contact_complete_name'] }}: + + {{ trans('dashboard.event_'.$event['nature_of_operation'].'_'.$event['object_type']) }} +
    diff --git a/resources/views/dashboard/events/_task.blade.php b/resources/views/dashboard/events/_task.blade.php new file mode 100644 index 0000000..78f2a6c --- /dev/null +++ b/resources/views/dashboard/events/_task.blade.php @@ -0,0 +1,9 @@ +
    + +
    + +
    + {{ $event['contact_complete_name'] }}: + + {{ trans('dashboard.event_'.$event['nature_of_operation'].'_'.$event['object_type']) }} +
    diff --git a/resources/views/dashboard/index.blade.php b/resources/views/dashboard/index.blade.php new file mode 100644 index 0000000..9c6a3c3 --- /dev/null +++ b/resources/views/dashboard/index.blade.php @@ -0,0 +1,93 @@ +@extends('layouts.skeleton') + +@section('content') +
    + +
    +
    +
    +
    + {{ trans('people.people_list_last_updated') }} +
    +
    +
    + {{ trans('people.people_list_last_updated') }} +
    + @foreach($lastUpdatedContacts as $contact) +
    + +
    + @endforeach +
    +
    + +
    +
    + + {{-- Main section --}} +
    +
    +
    +
    +
    +

    + 📅 {{ trans('dashboard.reminders_next_months') }} +

    +
    +
    + @include('dashboard._monthReminder', ['reminderOutboxesList' => $reminderOutboxes]) +
    +
    +
    +
    +
    +
    +

    + ☀️ {{ trans('dashboard.product_changes') }} + + {{ trans('dashboard.product_view_details') }} + +

    +
    +
    +
      + @foreach ($changelogs as $changelog) +
    • + {{ $changelog['date'] }} + {{ $changelog['title'] }} +
    • + @endforeach +
    +
    +
    + + + +
    +
    +
      +
    • + {{ $number_of_contacts }} + {{ trans('dashboard.statistics_contacts') }} +
    • +
    • + {{ $number_of_activities }} + {{ trans('dashboard.statistics_activities') }} +
    • +
    • + {{ $number_of_gifts }} + {{ trans('dashboard.statistics_gifts') }} +
    • +
    +
    +
    +
    +
    +
    + +
    +@endsection diff --git a/resources/views/errors/402.blade.php b/resources/views/errors/402.blade.php new file mode 100644 index 0000000..48ac4d0 --- /dev/null +++ b/resources/views/errors/402.blade.php @@ -0,0 +1,27 @@ +@extends('marketing.skeleton') + +@section('content') + + +
    +
    +
    + +
    +

    @lang('auth.not_authorized')

    + + @lang('settings.personalisation_paid_upgrade', ['url' => route('settings.subscriptions.index')]) + + @if(isset($exception) && $exception->getMessage()) +

    {{ $exception->getMessage() }}

    + @endif + +

    {{ trans('auth.back_homepage') }}

    +
    + +
    +
    +
    + + +@endsection diff --git a/resources/views/errors/403.blade.php b/resources/views/errors/403.blade.php new file mode 100644 index 0000000..e10c679 --- /dev/null +++ b/resources/views/errors/403.blade.php @@ -0,0 +1,25 @@ +@extends('marketing.skeleton') + +@section('content') + + +
    +
    +
    + +
    +

    @lang('auth.not_authorized')

    + + @if(isset($exception) && $exception->getMessage()) +

    {{ $exception->getMessage() }}

    + @endif + +

    {{ trans('auth.back_homepage') }}

    +
    + +
    +
    +
    + + +@endsection diff --git a/resources/views/errors/500.blade.php b/resources/views/errors/500.blade.php new file mode 100644 index 0000000..24f0210 --- /dev/null +++ b/resources/views/errors/500.blade.php @@ -0,0 +1,33 @@ +@extends('errors::layout') + +@section('title', trans('app.error_title')) + +@section('message', trans('app.error_title')) + +@section('content') + @if(isset($exception) && $exception->getMessage()) +

    {{ $exception->getMessage() }}

    + @endif + + @if(Auth::check() && app()->bound('sentry') && config('monica.sentry_support') && ! empty(app('sentry')->getLastEventID())) +
    @lang('app.error_id', ['id' => app('sentry')->getLastEventID()])
    + + + + @endif + +

    {{ trans('auth.back_homepage') }}

    +@endsection diff --git a/resources/views/errors/503.blade.php b/resources/views/errors/503.blade.php new file mode 100644 index 0000000..4529f0f --- /dev/null +++ b/resources/views/errors/503.blade.php @@ -0,0 +1,12 @@ +@extends('errors::layout') + +@section('title', trans('app.error_unavailable')) + +@section('message', trans('app.error_maintenance')) + +@section('content') +

    + @lang('app.error_help') + @lang('app.error_twitter', ['twitter' => config('monica.twitter_account')]) +

    +@endsection diff --git a/resources/views/errors/layout.blade.php b/resources/views/errors/layout.blade.php new file mode 100644 index 0000000..b95351f --- /dev/null +++ b/resources/views/errors/layout.blade.php @@ -0,0 +1,56 @@ + + + + + @yield('title') + + + + +
    +
    +
    + @yield('message') +
    +
    + @yield('content') +
    +
    +
    + + diff --git a/resources/views/journal/add.blade.php b/resources/views/journal/add.blade.php new file mode 100644 index 0000000..b084621 --- /dev/null +++ b/resources/views/journal/add.blade.php @@ -0,0 +1,63 @@ +@extends('layouts.skeleton') + +@section('content') +
    + + {{-- Breadcrumb --}} +
    +
    + +
    +
    + + +
    +
    +
    +
    +
    + @csrf + + @include('partials.errors') + +

    {{ trans('journal.journal_add') }}

    + + {{-- Optional title --}} +
    + + +
    + +
    + + +
    + +
    + + +

    {{ trans('app.markdown_description')}} {{ trans('app.markdown_link') }}

    +
    + +
    + + {{ trans('app.cancel') }} +
    +
    +
    +
    +
    +
    + +
    +@endsection diff --git a/resources/views/journal/edit.blade.php b/resources/views/journal/edit.blade.php new file mode 100644 index 0000000..37969b4 --- /dev/null +++ b/resources/views/journal/edit.blade.php @@ -0,0 +1,68 @@ +@extends('layouts.skeleton') + +@section('content') +
    + + {{-- Breadcrumb --}} +
    +
    + +
    +
    + + +
    +
    +
    +
    +
    + @method('PUT') + @csrf + + @include('partials.errors') + +

    {{ trans('journal.journal_edit') }}

    + + {{-- Optional title --}} +
    + + +
    + +
    + + + +
    + +
    + + +

    {{ trans('app.markdown_description')}} {{ trans('app.markdown_link') }}

    +
    + +
    + + {{ trans('app.cancel') }} +
    +
    +
    +
    +
    +
    + +
    +@endsection \ No newline at end of file diff --git a/resources/views/journal/index.blade.php b/resources/views/journal/index.blade.php new file mode 100644 index 0000000..1bbace8 --- /dev/null +++ b/resources/views/journal/index.blade.php @@ -0,0 +1,27 @@ +@extends('layouts.skeleton') + +@section('content') + +
    + + {{-- Breadcrumb --}} +
    +
    + +
    +
    + + {{-- Main section --}} +
    + +
    + +
    +@endsection diff --git a/resources/views/layouts/skeleton.blade.php b/resources/views/layouts/skeleton.blade.php new file mode 100644 index 0000000..028015e --- /dev/null +++ b/resources/views/layouts/skeleton.blade.php @@ -0,0 +1,72 @@ + + + + + + + + + + + + + @yield('title', trans('app.application_title')) + + + + + {{-- Required only for the Upgrade account page --}} + @if (Route::currentRouteName() == 'settings.subscriptions.upgrade' || Route::currentRouteName() == 'settings.subscriptions.confirm') + + @endif + + + + + + + + + + + + + + +
    + @if (Route::currentRouteName() != 'settings.subscriptions.confirm') + @include('partials.header') + @include('partials.subscription') + @endif + @yield('content') +
    + + @if (Route::currentRouteName() != 'settings.subscriptions.confirm') + @include('partials.footer') + @endif + + {{-- THE JS FILE OF THE APP --}} + @push('scripts') + + + @endpush + + {{-- Load everywhere except on the Upgrade account page --}} + @if (Route::currentRouteName() != 'settings.subscriptions.upgrade' && Route::currentRouteName() != 'settings.subscriptions.confirm') + @push('scripts') + + @endpush + @endif + + @stack('scripts') + + + diff --git a/resources/views/marketing/auth.blade.php b/resources/views/marketing/auth.blade.php new file mode 100644 index 0000000..b9c101f --- /dev/null +++ b/resources/views/marketing/auth.blade.php @@ -0,0 +1,35 @@ + + + + + + + + @yield('title', trans('app.application_title')) + + + + + + + + user()->account_id }} class="marketing register bg-gray-monica"> + +
    + @yield('content') +
    + + {{-- THE JS FILE OF THE APP --}} + + + + + @stack('scripts') + + + diff --git a/resources/views/marketing/skeleton.blade.php b/resources/views/marketing/skeleton.blade.php new file mode 100644 index 0000000..f984cd9 --- /dev/null +++ b/resources/views/marketing/skeleton.blade.php @@ -0,0 +1,19 @@ + + + + + + + + {{ trans('app.application_title') }} + + + + + + + + + @yield('content') + + diff --git a/resources/views/partials/auth/validate2fa.blade.php b/resources/views/partials/auth/validate2fa.blade.php new file mode 100644 index 0000000..934dde5 --- /dev/null +++ b/resources/views/partials/auth/validate2fa.blade.php @@ -0,0 +1,39 @@ +@if ($errors->has('totp')) + + {{ $errors->first('totp') }} + +@endif +
    + + + {{ trans('auth.2fa_otp_help') }} +
    + +{{-- TODO +
    + + +
    +--}} + +{{-- TODO +
    + {{ trans('auth.2fa_recuperation_code') }} +
    +--}} + +
    +
    +
    + +
    +
    + +
    diff --git a/resources/views/partials/check.blade.php b/resources/views/partials/check.blade.php new file mode 100644 index 0000000..8f08dce --- /dev/null +++ b/resources/views/partials/check.blade.php @@ -0,0 +1,32 @@ +{{-- Version check --}} + +@if (config('monica.check_version')) + + @if (($version = config('monica.app_version')) !== '' && version_compare($instance->latest_version, $version) > 0) +
  19. + {{ trans('app.footer_new_version') }} +
  20. + @endif + + + + +@endif diff --git a/resources/views/partials/components/currency-select.blade.php b/resources/views/partials/components/currency-select.blade.php new file mode 100644 index 0000000..83f0c07 --- /dev/null +++ b/resources/views/partials/components/currency-select.blade.php @@ -0,0 +1,9 @@ + diff --git a/resources/views/partials/components/date-select.blade.php b/resources/views/partials/components/date-select.blade.php new file mode 100644 index 0000000..3db9caa --- /dev/null +++ b/resources/views/partials/components/date-select.blade.php @@ -0,0 +1,52 @@ +{{-- Data comes from DateSelectViewComposer --}} + +
    + + + + + + +
    diff --git a/resources/views/partials/components/people-upgrade-sidebar.blade.php b/resources/views/partials/components/people-upgrade-sidebar.blade.php new file mode 100644 index 0000000..c8fafb2 --- /dev/null +++ b/resources/views/partials/components/people-upgrade-sidebar.blade.php @@ -0,0 +1,11 @@ +@if ($accountHasLimitations) +
    + +
    +

    {{ trans('people.people_list_account_upgrade_title') }}

    + +
    +
    +@endif diff --git a/resources/views/partials/errors.blade.php b/resources/views/partials/errors.blade.php new file mode 100644 index 0000000..8ed720f --- /dev/null +++ b/resources/views/partials/errors.blade.php @@ -0,0 +1,11 @@ +@if (isset($errors)) + @if (count($errors) > 0) +
    +
      + @foreach ($errors->all() as $error) +
    • {{ $error }}
    • + @endforeach +
    +
    + @endif +@endif \ No newline at end of file diff --git a/resources/views/partials/footer.blade.php b/resources/views/partials/footer.blade.php new file mode 100644 index 0000000..3677b38 --- /dev/null +++ b/resources/views/partials/footer.blade.php @@ -0,0 +1,29 @@ + diff --git a/resources/views/partials/header.blade.php b/resources/views/partials/header.blade.php new file mode 100644 index 0000000..9e0bc12 --- /dev/null +++ b/resources/views/partials/header.blade.php @@ -0,0 +1,46 @@ + diff --git a/resources/views/partials/icons/header_birthday.blade.php b/resources/views/partials/icons/header_birthday.blade.php new file mode 100644 index 0000000..1854cdf --- /dev/null +++ b/resources/views/partials/icons/header_birthday.blade.php @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/resources/views/partials/icons/header_call.blade.php b/resources/views/partials/icons/header_call.blade.php new file mode 100644 index 0000000..35bd726 --- /dev/null +++ b/resources/views/partials/icons/header_call.blade.php @@ -0,0 +1,6 @@ + + + + + + diff --git a/resources/views/partials/icons/header_description.blade.php b/resources/views/partials/icons/header_description.blade.php new file mode 100644 index 0000000..c3743d0 --- /dev/null +++ b/resources/views/partials/icons/header_description.blade.php @@ -0,0 +1,5 @@ + + + + + diff --git a/resources/views/partials/icons/header_people.blade.php b/resources/views/partials/icons/header_people.blade.php new file mode 100644 index 0000000..a5c05ad --- /dev/null +++ b/resources/views/partials/icons/header_people.blade.php @@ -0,0 +1,3 @@ + + + diff --git a/resources/views/partials/icons/header_stayintouch.blade.php b/resources/views/partials/icons/header_stayintouch.blade.php new file mode 100644 index 0000000..bd063fb --- /dev/null +++ b/resources/views/partials/icons/header_stayintouch.blade.php @@ -0,0 +1,4 @@ + + + + diff --git a/resources/views/partials/icons/homepage_your_tasks.blade.php b/resources/views/partials/icons/homepage_your_tasks.blade.php new file mode 100644 index 0000000..d7f5441 --- /dev/null +++ b/resources/views/partials/icons/homepage_your_tasks.blade.php @@ -0,0 +1,167 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/views/partials/notification.blade.php b/resources/views/partials/notification.blade.php new file mode 100644 index 0000000..5daf8da --- /dev/null +++ b/resources/views/partials/notification.blade.php @@ -0,0 +1,7 @@ +@if(session('success')) + +
    + {{ session('success') }} +
    + +@endif diff --git a/resources/views/partials/subscription.blade.php b/resources/views/partials/subscription.blade.php new file mode 100644 index 0000000..e30097a --- /dev/null +++ b/resources/views/partials/subscription.blade.php @@ -0,0 +1,16 @@ +@if (($subscription = auth()->user()->account->getSubscribedPlan()) && $subscription->hasIncompletePayment()) + +
    + {!! trans('settings.subscriptions_account_confirm_payment', ['url' => route('settings.subscriptions.confirm', $subscription->latestPayment() ? $subscription->latestPayment()->id : '')]) !!} +
    + +@if (! app()->environment('production')) +

    + + {{-- No translation needed --}} + Force payment success (test). + +

    +@endif + +@endif diff --git a/resources/views/people/_header.blade.php b/resources/views/people/_header.blade.php new file mode 100644 index 0000000..aa12e5c --- /dev/null +++ b/resources/views/people/_header.blade.php @@ -0,0 +1,132 @@ +
    + + @if ($contact->isMe()) +
    + {{ trans('people.me') }} +
    + @endif + +
    + {{-- AVATAR --}} +
    +
    +
    + {{ $contact->initials }} + +
    + +
    +
    +
    + +
    + +

    + {{ $contact->name }} + + @if ($contact->job) + {{ $contact->job }} + @if ($contact->company) + ({{ $contact->company }}) + @endif + + @endif +

    + +
      + + {{-- AGE --}} +
    • + @if ($contact->birthdate && !($contact->is_dead)) + @if ($contact->getBirthdayState() !== 'unknown') + @include('partials.icons.header_birthday') + @if($contact->getBirthdayState() === 'approximate') + {{ trans('people.age_approximate_in_years', ['age' => $contact->birthdate->getAge()]) }} + @elseif($contact->getBirthdayState() === 'almost') + {{$contact->birthdate->toShortString()}} + @else + {{$contact->birthdate->toShortString()}} ({{ $contact->birthdate->getAge() }}) + @endif + @endif + @elseif ($contact->is_dead) + @if (! is_null($contact->deceasedDate)) + {{ trans('people.deceased_label_with_date', ['date' => $contact->deceasedDate->toShortString()]) }} + @if ($contact->deceasedDate->is_year_unknown == 0 && $contact->getBirthdayState() !== 'almost') + ({{ trans('people.deceased_age') }} {{ $contact->getAgeAtDeath() }}) + @endif + @else + {{ trans('people.deceased_label') }} + @endif + @endif +
    • + + {{-- LAST ACTIVITY --}} + @if (! $contact->isMe()) +
    • + @include('partials.icons.header_people') + @if (is_null($contact->getLastActivityDate())) + {{ trans('people.last_activity_date_empty') }} + @else + {{ trans('people.last_activity_date', ['date' => \App\Helpers\DateHelper::getShortDate($contact->getLastActivityDate())]) }} + @endif +
    • + @endif + + {{-- LAST CALLED --}} + @if (! $contact->isMe()) +
    • + @include('partials.icons.header_call') + + +
    • + @endif + + {{-- DESCRIPTION --}} + @if ($contact->description) +
    • + @include('partials.icons.header_description') + {{ $contact->description }} +
    • + @endif + + {{-- STAY IN TOUCH --}} + @if(!$contact->is_dead && ! $contact->isMe()) +
    • + @include('partials.icons.header_stayintouch') + +
    • + @endif +
    + + + + +
    +
    + +
    +
    + @include ('partials.errors') + @include ('partials.notification') +
    +
    diff --git a/resources/views/people/activities/index.blade.php b/resources/views/people/activities/index.blade.php new file mode 100644 index 0000000..e97f145 --- /dev/null +++ b/resources/views/people/activities/index.blade.php @@ -0,0 +1,7 @@ +
    +
    + + + +
    +
    diff --git a/resources/views/people/activities/year.blade.php b/resources/views/people/activities/year.blade.php new file mode 100644 index 0000000..f704e3a --- /dev/null +++ b/resources/views/people/activities/year.blade.php @@ -0,0 +1,130 @@ +@extends('layouts.skeleton') + +@section('title', $contact->name ) + +@section('content') +{{-- Breadcrumb --}} +
    +
    +
    + +
    +
    +
    + +{{-- Main section --}} +
    +
    +

    {{ trans('people.activities_profile_title', ['name' => $contact->first_name]) }}

    +

    🚀 {{ trans_choice('people.activities_profile_subtitle', $totalActivities, ['total_activities' => $totalActivities, 'activities_last_twelve_months' => $activitiesLastTwelveMonths, 'name' => $contact->first_name]) }}

    +
    + + {{-- Left sidebar --}} +
    +
    + +
    +
    + + {{-- Right Content --}} +
    +
    +

    🤲 {{ trans('people.activities_profile_year_summary_activity_types', ['year' => $year]) }}

    +
      + @foreach($uniqueActivityTypes as $activityType) +
    • + {{ $activityType['occurences'] }} + {{ $activityType['object']->name }} +
    • + @endforeach +
    + +

    {{ trans('people.activities_profile_year_summary', ['year' => $year]) }}

    + + {{-- Bar chart --}} +
    +
      + @foreach ($activitiesPerMonthForYear->sortBy('month') as $activityMonth) +
    • + +
    • + @endforeach +
    +
    + + {{-- Details about each month --}} + @foreach ($activitiesPerMonthForYear as $activityMonth) + @if ($activityMonth['occurences'] != 0) + +

    + + + + {{ \App\Helpers\DateHelper::getFullMonthAndDate(\Carbon\Carbon::create($year, $activityMonth['month'])) }} {{ trans_choice('people.activities_profile_number_occurences', $activityMonth['occurences'], ['value' => $activityMonth['occurences']]) }} +

    + + {{-- Activities list --}} + @foreach ($activityMonth['activities'] as $activity) +
    +
    +
    +
      +
    • + + + +
    • +
    • + {{ \App\Helpers\DateHelper::getShortDate($activity->happened_at) }} +
    • + @if (!is_null($activity->type)) +
    • + + + +
    • +
    • + {{ $activity->type->name }} +
    • + @endif +
    +
    +

    {{ $activity->summary }}

    +

    {{ $activity->description }}

    +
    +
    + @endforeach + + @endif + @endforeach + +
    +
    +
    +
    +
    + +@endsection \ No newline at end of file diff --git a/resources/views/people/auditlogs/index.blade.php b/resources/views/people/auditlogs/index.blade.php new file mode 100644 index 0000000..be61af1 --- /dev/null +++ b/resources/views/people/auditlogs/index.blade.php @@ -0,0 +1,65 @@ +@extends('layouts.skeleton') + +@section('title', $contact->name ) + +@section('content') +{{-- Breadcrumb --}} +
    +
    +
    + +
    +
    +
    + +{{-- Main section --}} +
    +
    +

    {{ trans('people.auditlogs_title', ['name' => $contact->first_name]) }}

    +
    + + {{-- Left sidebar --}} +
    +
    +
      +
    • +
      + {{ trans('settings.logs_actor') }} +
      +
      + {{ trans('settings.logs_timestamp') }} +
      +
      + {{ trans('settings.logs_description') }} +
      +
    • + @foreach ($logsCollection as $log) +
    • +
      + {{ $log['author_name'] }} +
      +
      + {{ \App\Helpers\DateHelper::getShortDateWithTime($log['audited_at']) }} +
      +
      + {{ $log['description'] }} +
      +
    • + @endforeach +
    + +
    + {{ $logsPagination->links() }} +
    + +
    +
    +
    +
    +
    + +@endsection diff --git a/resources/views/people/avatar/edit.blade.php b/resources/views/people/avatar/edit.blade.php new file mode 100644 index 0000000..23669dc --- /dev/null +++ b/resources/views/people/avatar/edit.blade.php @@ -0,0 +1,44 @@ +@extends('layouts.skeleton') + +@section('content') +
    + + {{-- Breadcrumb --}} +
    +

    < {{ $contact->name }}

    +

    {{ trans('people.avatar_change_title') }}

    +
    + +
    +
    + @csrf + + @include('partials.errors') + + {{-- Adorable --}} + + + + {{-- Form actions --}} +
    +
    + +
    + +
    +
    +
    +
    +
    + +@endsection diff --git a/resources/views/people/blank.blade.php b/resources/views/people/blank.blade.php new file mode 100644 index 0000000..238b8cb --- /dev/null +++ b/resources/views/people/blank.blade.php @@ -0,0 +1,40 @@ +
    +
    +
    +
    + @if (! is_null($tags)) +

    + {{ trans('people.people_list_filter_tag') }} + @foreach ($tags as $tag) + + {{ $tag->name }} + + @endforeach + {{ trans('people.people_list_clear_filter') }} +

    + @endif + @if ($tagLess) +

    + {{ trans('people.people_list_filter_untag') }} + {{ trans('people.people_list_clear_filter') }} +

    + @endif + +
    + Image +
    +

    {{ trans('people.people_list_blank_title') }}

    + + + +
    + @if ($hasArchived) + @lang('people.list_link_to_archived_contacts') + @endif +
    +
    +
    +
    +
    diff --git a/resources/views/people/calls/index.blade.php b/resources/views/people/calls/index.blade.php new file mode 100644 index 0000000..244bd44 --- /dev/null +++ b/resources/views/people/calls/index.blade.php @@ -0,0 +1,5 @@ +
    +
    + +
    +
    diff --git a/resources/views/people/conversations/edit.blade.php b/resources/views/people/conversations/edit.blade.php new file mode 100644 index 0000000..6eb3e7f --- /dev/null +++ b/resources/views/people/conversations/edit.blade.php @@ -0,0 +1,93 @@ +@extends('layouts.skeleton') + +@section('content') + +
    + + {{-- Breadcrumb --}} +
    +

    < {{ $contact->name }}

    +
    +

    {{ trans('people.conversation_edit_title') }}

    +
    + @method('DELETE') + @csrf + + {{ trans('people.conversation_delete_link') }} + +
    +
    +
    + +
    + + @if (session('status')) +
    + {{ session('status') }} +
    + @endif + + @include('partials.errors') + +
    + @method('PUT') + @csrf + + {{-- When did it take place --}} +
    +

    {{ trans('people.conversation_add_when') }}

    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    + + {{-- What tool did you use --}} +
    + + +
    + + {{-- Conversation --}} + + + {{-- Form actions --}} +
    +
    + +
    + +
    +
    +
    + +
    +
    +
    + +@endsection diff --git a/resources/views/people/conversations/index.blade.php b/resources/views/people/conversations/index.blade.php new file mode 100644 index 0000000..e82e44b --- /dev/null +++ b/resources/views/people/conversations/index.blade.php @@ -0,0 +1,28 @@ +
    +

    + 🗣 {{ trans('people.conversation_list_title') }} + + + {{ trans('people.conversation_list_cta') }} + +

    +
    + +@if ($contact->conversations->count() > 0) + +
    +
    + +
    +
    + +@else + +
    +
    +

    {{ trans('people.conversation_blank', ['name' => $contact->first_name]) }}

    + {{ trans('people.conversation_list_cta') }} +
    +
    + +@endif diff --git a/resources/views/people/conversations/new.blade.php b/resources/views/people/conversations/new.blade.php new file mode 100644 index 0000000..90fde2d --- /dev/null +++ b/resources/views/people/conversations/new.blade.php @@ -0,0 +1,83 @@ +@extends('layouts.skeleton') + +@section('content') + +
    + + {{-- Breadcrumb --}} +
    +

    < {{ $contact->name }}

    +
    +

    {{ trans('people.conversation_add_title') }}

    +
    +
    + +
    + + @if (session('status')) +
    + {{ session('status') }} +
    + @endif + + @include('partials.errors') + +
    + @csrf + + {{-- When did it take place --}} +
    +

    {{ trans('people.conversation_add_when') }}

    +
    +
    + + +
    +
    + + +
    +
    + + +
    + + +
    +
    +
    +
    + + {{-- What tool did you use --}} +
    + + +
    + + {{-- Conversation --}} + + + {{-- Form actions --}} +
    +
    + +
    + +
    +
    +
    + +
    +
    +
    + +@endsection diff --git a/resources/views/people/create.blade.php b/resources/views/people/create.blade.php new file mode 100644 index 0000000..6e21979 --- /dev/null +++ b/resources/views/people/create.blade.php @@ -0,0 +1,153 @@ +@extends('layouts.skeleton') + +@section('content') + +
    +
    + @if ($isContactMissing) +

    {{ trans('people.people_add_missing') }}

    + @else +

    {{ trans('people.people_add_title') }}

    + @endif + + @if (! $accountHasLimitations) +

    {!! trans('people.people_add_import', ['url' => route('settings.import')]) !!}

    + @endif +
    + +
    + + @if (session('status')) +
    + {{ session('status') }} +
    + @endif + + @include('partials.errors') + +
    + @csrf + +
    + {{-- This check is for the cultures that are used to say the last name first --}} + @if ($formNameOrder == 'firstname') + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + + @else + +
    + + +
    + +
    + + +
    + +
    + + +
    + +
    + + +
    + + @endif +
    + +
    + + +
    + +
    + + +
    + + {{-- Form actions --}} +
    +
    + +
    + + +
    +
    +
    + +
    +
    +
    + +@endsection diff --git a/resources/views/people/debt/add.blade.php b/resources/views/people/debt/add.blade.php new file mode 100644 index 0000000..795b813 --- /dev/null +++ b/resources/views/people/debt/add.blade.php @@ -0,0 +1,46 @@ +@extends('layouts.skeleton') + +@section('content') +
    + + {{-- Breadcrumb --}} + + + + @include('people._header') + + +
    +
    +
    +
    + @include('people.debt.form', [ + 'method' => 'POST', + 'action' => route('people.debts.store', $contact), + 'update_or_add' =>'add' + ]) +
    +
    +
    +
    + +
    +@endsection diff --git a/resources/views/people/debt/edit.blade.php b/resources/views/people/debt/edit.blade.php new file mode 100644 index 0000000..b4f94ff --- /dev/null +++ b/resources/views/people/debt/edit.blade.php @@ -0,0 +1,46 @@ +@extends('layouts.skeleton') + +@section('content') +
    + + {{-- Breadcrumb --}} + + + + @include('people._header') + + +
    +
    +
    +
    + @include('people.debt.form', [ + 'method' => 'PUT', + 'action' => route('people.debts.update', [$contact, $debt]), + 'update_or_add' =>'edit' + ]) +
    +
    +
    +
    + +
    +@endsection diff --git a/resources/views/people/debt/form.blade.php b/resources/views/people/debt/form.blade.php new file mode 100644 index 0000000..fbf807a --- /dev/null +++ b/resources/views/people/debt/form.blade.php @@ -0,0 +1,44 @@ +
    + @method($method) + @csrf + + @include('partials.errors') + +

    {{ trans('people.debt_add_title') }}

    + + {{-- Debt direction --}} +
    + + + +
    + + {{-- Amount --}} +
    + + +
    + + {{-- Reason --}} +
    + + +
    + +
    + + {{ trans('app.cancel') }} +
    +
    diff --git a/resources/views/people/debt/index.blade.php b/resources/views/people/debt/index.blade.php new file mode 100644 index 0000000..35acbfa --- /dev/null +++ b/resources/views/people/debt/index.blade.php @@ -0,0 +1,86 @@ +
    + +

    + {{ trans('people.debt_title') }} + + + {{ trans('people.debt_add_cta') }} + +

    +
    + +@if (!$contact->hasDebt()) + +
    +
    +

    {{ trans('people.debts_blank_title', ['name' => $contact->first_name]) }}

    + {{ trans('people.debt_add_cta') }} +
    +
    + +@else + +
    + +
      + @foreach($contact->debts as $debt) +
    • +
      + {{ \App\Helpers\DateHelper::getShortDate($debt->created_at) }} +
      +
      + @if ($debt->in_debt == 'yes') + {{ trans('people.debt_you_owe', [ + 'amount' => $debt->displayValue + ]) }} + @else + {{ trans('people.debt_they_owe', [ + 'name' => $contact->first_name, + 'amount' => $debt->displayValue + ]) }} + @endif +
      +
      + @if (! is_null($debt->reason)) + {{ $debt->reason }} + @endif +
      +
      + + + +
      + @method('DELETE') + @csrf + + + +
      +
      + +
    • + @endforeach +
    • +
      +
      + + @if ($contact->isOwedMoney()) + {{ trans('people.debt_they_owe', [ + 'name' => $contact->first_name, + 'amount' => App\Helpers\MoneyHelper::format($contact->totalOutstandingDebtAmount(), Auth::user()->currency) + ]) }} + @else + {{ trans('people.debt_you_owe', [ + 'amount' => App\Helpers\MoneyHelper::format(-$contact->totalOutstandingDebtAmount(), Auth::user()->currency) + ]) }} + @endif + +
      +
      +
      +
    • +
    + +
    + +@endif diff --git a/resources/views/people/documents/index.blade.php b/resources/views/people/documents/index.blade.php new file mode 100644 index 0000000..06d11e5 --- /dev/null +++ b/resources/views/people/documents/index.blade.php @@ -0,0 +1,22 @@ +
    +
    + + @if (config('monica.requires_subscription') && $accountHasLimitations) + +
    +

    + 📄 {{ trans('people.document_list_title') }} +

    + +
    +

    {{ trans('settings.storage_upgrade_notice') }}

    +
    +
    + + @else + + + + @endif +
    +
    diff --git a/resources/views/people/edit.blade.php b/resources/views/people/edit.blade.php new file mode 100644 index 0000000..e46b0c6 --- /dev/null +++ b/resources/views/people/edit.blade.php @@ -0,0 +1,165 @@ +@extends('layouts.skeleton') + +@section('content') +
    + + {{-- Breadcrumb --}} +
    +

    < {{ $contact->name }}

    +

    {{ trans('people.information_edit_title', ['name' => $contact->first_name]) }}

    + + @if (! $accountHasLimitations) +

    {!! trans('people.people_add_import', ['url' => 'settings/import']) !!}

    + @endif +
    + +
    +
    + @method('PUT') + @csrf + + @include('partials.errors') + + {{-- Name --}} +
    + {{-- This check is for the cultures that are used to say the last name first --}} +
    + @if ($formNameOrder == 'firstname') + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + @else + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + @endif +
    +
    + + {{-- Gender --}} +
    +
    + + +
    +
    + + {{-- Description --}} +
    +
    + + + {{ trans('people.information_edit_description_help') }} +
    +
    + + {{-- Birthdate --}} + + + {{-- Is the contact deceased? --}} + + + + {{-- Form actions --}} +
    +
    + +
    + +
    +
    +
    +
    +
    + +@endsection diff --git a/resources/views/people/food-preferences/edit.blade.php b/resources/views/people/food-preferences/edit.blade.php new file mode 100644 index 0000000..8ce7fa1 --- /dev/null +++ b/resources/views/people/food-preferences/edit.blade.php @@ -0,0 +1,64 @@ +@extends('layouts.skeleton') + +@section('content') +
    + + {{-- Breadcrumb --}} + + + + @include('people._header') + + +
    +
    +
    +
    +
    + @csrf + +

    {{ trans('people.food_preferences_edit_title') }}

    + + @include('partials.errors') + +

    + @if (is_null($contact->last_name)) + {{ trans('people.food_preferences_edit_description_no_last_name', ['firstname' => $contact->first_name]) }}

    + @else + {{ trans('people.food_preferences_edit_description', ['firstname' => $contact->first_name, 'family' => $contact->last_name]) }}

    + @endif + +
    + +
    + +
    + + {{ trans('app.cancel') }} +
    +
    +
    +
    +
    +
    + +
    +@endsection diff --git a/resources/views/people/food-preferences/index.blade.php b/resources/views/people/food-preferences/index.blade.php new file mode 100644 index 0000000..2ead41c --- /dev/null +++ b/resources/views/people/food-preferences/index.blade.php @@ -0,0 +1,18 @@ + diff --git a/resources/views/people/gifts/index.blade.php b/resources/views/people/gifts/index.blade.php new file mode 100644 index 0000000..e8dcc64 --- /dev/null +++ b/resources/views/people/gifts/index.blade.php @@ -0,0 +1,15 @@ +
    + + +
    diff --git a/resources/views/people/index.blade.php b/resources/views/people/index.blade.php new file mode 100644 index 0000000..5eb0e0d --- /dev/null +++ b/resources/views/people/index.blade.php @@ -0,0 +1,169 @@ +@extends('layouts.skeleton') + +@section('content') +
    + @csrf + + {{-- Breadcrumb --}} + + + +
    + + @if ($contactsCount == 0) + + @include('people.blank') + + @else + +
    +
    + + @if ($hasArchived and !$active) +
    +
    + {!! trans('people.list_link_to_active_contacts', ['url' => route('people.index')]) !!} +
    +
    + @endif + +
    + + @if (! is_null($tags)) +

    + {{ trans('people.people_list_filter_tag') }} + @foreach ($tags as $tag) + + {{ $tag->name }} + + @endforeach + {{ trans('people.people_list_clear_filter') }} +

    + @endif + + @if ($tagLess) +

    + {{ trans('people.people_list_filter_untag') }} + {{ trans('people.people_list_clear_filter') }} +

    + @endif + + +
    + + + +
    +
    + + @endif + +
    + +
    +@endsection diff --git a/resources/views/people/introductions/edit.blade.php b/resources/views/people/introductions/edit.blade.php new file mode 100644 index 0000000..44cd680 --- /dev/null +++ b/resources/views/people/introductions/edit.blade.php @@ -0,0 +1,116 @@ +@extends('layouts.skeleton') + +@section('content') +
    + + {{-- Breadcrumb --}} + + + +
    +
    +
    +
    +

    {{ trans('people.introductions_title_edit', ['name' => $contact->first_name]) }}

    + +
    + @csrf + + @include('partials.errors') + + {{-- How did they meet --}} +
    + + +
    + +
    + + +
    + + +
    + + {{-- You don't know the date you've met --}} +
    + +
    + + {{-- You know the date you've met --}} +
    + +
    +
    + +
    + + {{ trans('people.introductions_add_reminder') }} + +
    + +
    + + {{ trans('app.cancel') }} +
    +
    + +
    + +
    +
    +
    +
    + + +@endsection diff --git a/resources/views/people/introductions/index.blade.php b/resources/views/people/introductions/index.blade.php new file mode 100644 index 0000000..8c78694 --- /dev/null +++ b/resources/views/people/introductions/index.blade.php @@ -0,0 +1,41 @@ + diff --git a/resources/views/people/life-events/blank.blade.php b/resources/views/people/life-events/blank.blade.php new file mode 100644 index 0000000..f3ad110 --- /dev/null +++ b/resources/views/people/life-events/blank.blade.php @@ -0,0 +1,427 @@ +
    +
    +

    Life events

    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +

    Life Events let you add experiences from the different parts of Leo's life and record them for your future reference.

    + +
    +
    diff --git a/resources/views/people/life-events/index.blade.php b/resources/views/people/life-events/index.blade.php new file mode 100644 index 0000000..bd9f6eb --- /dev/null +++ b/resources/views/people/life-events/index.blade.php @@ -0,0 +1,11 @@ +
    +
    + + +
    +
    diff --git a/resources/views/people/photos/index.blade.php b/resources/views/people/photos/index.blade.php new file mode 100644 index 0000000..d11210f --- /dev/null +++ b/resources/views/people/photos/index.blade.php @@ -0,0 +1,12 @@ +
    +
    + + +
    +
    diff --git a/resources/views/people/profile.blade.php b/resources/views/people/profile.blade.php new file mode 100644 index 0000000..63249c6 --- /dev/null +++ b/resources/views/people/profile.blade.php @@ -0,0 +1,183 @@ +@extends('layouts.skeleton') + +@section('title', $contact->name ) + +@section('content') +
    + @csrf + + {{-- Breadcrumb --}} + + + {{-- Page header --}} + @include('people._header') + + {{-- Page content --}} +
    + +
    + +
    +
    + + @if (! is_null($weather) && $weather->summary) +
    +
    +
    +

    {{ trans('app.weather_current_title') }}

    +
    +
    + +

    + {{ $weather->emoji }} {{ $weather->summary }} / {{ trans('app.weather_current_temperature_'.auth()->user()->temperature_scale, ['temperature' => $weather->temperature(auth()->user()->temperature_scale)]) }} +

    +
    + @endif + + @include('people.relationship.index') + + @include('people.sidebar') + + +
    + +
    + +
    +
    + @if (! $contact->isMe()) + + @if (auth()->user()->profile_new_life_event_badge_seen == false) + {{ trans('app.new') }} + @endif + {{ trans('people.life_event_list_tab_life_events') }} ({{ $contact->lifeEvents()->count() }}) + + @endif + {{ trans('people.life_event_list_tab_other') }} + Photos +
    +
    + + @if (! $contact->isMe()) +
    +
    + @include('people.life-events.index') +
    +
    + @endif + +
    + @if ($modules->contains('key', 'notes')) +
    +
    + hashID() }}> +
    +
    + @endif + + @if ($modules->contains('key', 'conversations') && ! $contact->isMe()) +
    + @include('people.conversations.index') +
    + @endif + + @if ($modules->contains('key', 'phone_calls') && ! $contact->isMe()) +
    + @include('people.calls.index') +
    + @endif + + @if ($modules->contains('key', 'activities') && ! $contact->isMe()) +
    + @include('people.activities.index') +
    + @endif + + @if ($modules->contains('key', 'reminders')) +
    + @include('people.reminders.index') +
    + @endif + + @if ($modules->contains('key', 'tasks')) +
    + @include('people.tasks.index') +
    + @endif + + @if ($modules->contains('key', 'gifts') && ! $contact->isMe()) +
    + @include('people.gifts.index') +
    + @endif + + @if ($modules->contains('key', 'debts') && ! $contact->isMe()) +
    + @include('people.debt.index') +
    + @endif + + @if ($modules->contains('key', 'documents')) +
    + @include('people.documents.index') +
    + @endif + +
    + +
    +
    + @include('people.photos.index') +
    +
    +
    +
    + +
    + +
    +
    + +@endsection diff --git a/resources/views/people/relationship/_relationship.blade.php b/resources/views/people/relationship/_relationship.blade.php new file mode 100644 index 0000000..deb7cb5 --- /dev/null +++ b/resources/views/people/relationship/_relationship.blade.php @@ -0,0 +1,45 @@ +@foreach($relationships->groupByItemsProperty('relationshipTypeLocalized') as $type => $relationshipType) + + + + @foreach ($relationshipType as $relationship) + @if (! $relationship->ofContact) + @continue + @endif + +
    + @endforeach + +@endforeach diff --git a/resources/views/people/relationship/edit.blade.php b/resources/views/people/relationship/edit.blade.php new file mode 100644 index 0000000..b00fc8a --- /dev/null +++ b/resources/views/people/relationship/edit.blade.php @@ -0,0 +1,158 @@ +@extends('layouts.skeleton') + +@section('content') + +
    + + {{-- Breadcrumb --}} +
    +

    < {{ $contact->name }}

    +
    +

    {{ trans('people.relationship_form_edit') }}

    +
    +
    + +
    + + @if (session('status')) +
    + {{ session('status') }} +
    + @endif + + @include('partials.errors') + +
    + @method('PUT') + @csrf + + + @if ($partner->is_partial) + {{-- Name --}} +
    + {{-- This check is for the cultures that are used to say the last name first --}} +
    + @if ($formNameOrder == 'firstname') + +
    +
    + + +
    +
    + + +
    +
    + + @else + +
    +
    + + +
    +
    + + +
    +
    + + @endif +
    +
    + + {{-- Gender --}} +
    + + +
    + + {{-- Birthdate --}} + + +
    + {{-- Real or partial contact (false in this case) --}} + + + + {{ trans('people.relationship_form_add_description') }} + + +
    + @endif + + {{-- Nature of relationship --}} +
    + + +
    + + {{-- Form actions --}} +
    +
    + +
    + +
    +
    +
    + +
    +
    +
    + +@endsection diff --git a/resources/views/people/relationship/index.blade.php b/resources/views/people/relationship/index.blade.php new file mode 100644 index 0000000..77a9e45 --- /dev/null +++ b/resources/views/people/relationship/index.blade.php @@ -0,0 +1,43 @@ +@if ($modules->contains('key', 'love_relationships')) + +@endif + +@if ($modules->contains('key', 'family_relationships')) + +@endif + +@if ($modules->contains('key', 'other_relationships')) + +@endif diff --git a/resources/views/people/relationship/new.blade.php b/resources/views/people/relationship/new.blade.php new file mode 100644 index 0000000..5afe389 --- /dev/null +++ b/resources/views/people/relationship/new.blade.php @@ -0,0 +1,187 @@ +@extends('layouts.skeleton') + +@section('content') + +
    + + {{-- Breadcrumb --}} +
    +

    < {{ $contact->name }}

    +
    +

    {{ trans('people.relationship_form_add') }}

    +
    +
    + +
    + + @if (session('status')) +
    + {{ session('status') }} +
    + @endif + + @include('partials.errors') + +
    + @csrf + + {{-- New contact / link existing --}} +
    +

    {{ trans('people.relationship_form_add_choice') }}

    +
    +
    + + +
    +
    + + +
    +
    +
    + +
    + {{-- Name --}} +
    + {{-- This check is for the cultures that are used to say the last name first --}} +
    + @if ($formNameOrder == 'firstname') + +
    +
    + + +
    +
    + + +
    +
    + + @else + +
    +
    + + +
    +
    + + +
    +
    + + @endif +
    +
    + + {{-- Gender --}} +
    + + +
    + + {{-- Birthdate --}} + + +
    + {{-- Real or partial contact (default true) --}} + + + + {{ trans('people.relationship_form_add_description') }} + + +
    +
    + +
    +
    + @if ($existingContacts->count() == 0) +
    + +

    {{ trans('people.relationship_form_add_no_existing_contact', ['name' => $contact->first_name]) }}

    +
    + @else + + + @endif +
    +
    + + {{-- Nature of relationship --}} +
    + + +
    + + {{-- Form actions --}} +
    +
    + +
    + @if ($existingContacts->count() == 0) + + + @else + + @endif +
    +
    +
    + +
    +
    +
    + +@endsection diff --git a/resources/views/people/reminders/add.blade.php b/resources/views/people/reminders/add.blade.php new file mode 100644 index 0000000..953eef2 --- /dev/null +++ b/resources/views/people/reminders/add.blade.php @@ -0,0 +1,46 @@ +@extends('layouts.skeleton') + +@section('content') +
    + + {{-- Breadcrumb --}} + + + + @include('people._header') + + +
    +
    +
    +
    + @include('people.reminders.form', [ + 'method' => 'POST', + 'action' => route('people.reminders.store', $contact), + 'update_or_add' =>'add' + ]) +
    +
    +
    +
    + +
    +@endsection diff --git a/resources/views/people/reminders/edit.blade.php b/resources/views/people/reminders/edit.blade.php new file mode 100644 index 0000000..ae3f4dc --- /dev/null +++ b/resources/views/people/reminders/edit.blade.php @@ -0,0 +1,46 @@ +@extends('layouts.skeleton') + +@section('content') +
    + + {{-- Breadcrumb --}} + + + + @include('people._header') + + +
    +
    +
    +
    + @include('people.reminders.form', [ + 'method' => 'PUT', + 'action' => route('people.reminders.update', [$contact, $reminder]), + 'update_or_add' =>'edit' + ]) +
    +
    +
    +
    + +
    +@endsection diff --git a/resources/views/people/reminders/form.blade.php b/resources/views/people/reminders/form.blade.php new file mode 100644 index 0000000..b0816cc --- /dev/null +++ b/resources/views/people/reminders/form.blade.php @@ -0,0 +1,89 @@ +
    + @method($method) + @csrf + +

    {{ trans('people.reminders_add_title', ['name' => $contact->first_name]) }}

    + + @include('partials.errors') + +

    {{ trans('people.reminders_add_description') }}

    + + {{-- Nature of reminder --}} +
    +
    + +
    +
    + + {{-- Date --}} +
    + + + initial_date)) + value="{{ old('initial_date') ?? now(\App\Helpers\DateHelper::getTimezone())->toDateString() }}" + @else + value="{{ old('initial_date') ?? $reminder->initial_date->toDateString() }}" + @endif + min="{{ now(\App\Helpers\DateHelper::getTimezone())->toDateString() }}" + max="{{ now(\App\Helpers\DateHelper::getTimezone())->addYears(10)->toDateString() }}" + > + +
    + + {{-- One time reminder --}} +
    + +
    + + {{-- Recurring reminder --}} +
    + +
    +
    +
    + +
    + + +
    + +
    + + {{ trans('app.cancel') }} +
    +
    diff --git a/resources/views/people/reminders/index.blade.php b/resources/views/people/reminders/index.blade.php new file mode 100644 index 0000000..4dfdbe1 --- /dev/null +++ b/resources/views/people/reminders/index.blade.php @@ -0,0 +1,79 @@ +
    + +

    + {{ trans('people.section_personal_reminders') }} + + + {{ trans('people.reminders_cta') }} + +

    +
    + + +@if ($reminders->count() === 0) + +
    +
    +

    {{ trans('people.reminders_blank_title', ['name' => $contact->first_name]) }}

    + {{ trans('people.reminders_blank_add_activity') }} +
    +
    + +@else + +
    + + @if (! $accountHasLimitations) +

    {{ trans('people.reminders_description') }}

    + @else +

    {{ trans('people.reminders_free_plan_warning') }}

    + @endif + +
      + @foreach($reminders as $reminder) +
    • + +
      + {{ $reminder->next_expected_date_human_readable }} +
      + +
      + @if ($reminder->frequency_type != 'one_time') + {{ trans_choice('people.reminder_frequency_'.$reminder->frequency_type, $reminder->frequency_number, ['number' => $reminder->frequency_number]) }} + @else + {{ trans('people.reminders_one_time') }} + @endif +
      + +
      + {{ $reminder->title }} +
      + +
      + @if (!is_null($reminder->description)) + {{ $reminder->description }} + @endif +
      + +
      + {{-- Only display this if the reminder can be deleted - ie if it's not a reminder added automatically for birthdates --}} + @if ($reminder->delible || ! $reminder->isBirthdayReminder()) + + + + @endif +
      + @method('DELETE') + @csrf + + + +
      +
      + +
    • + @endforeach +
    +
    + +@endif diff --git a/resources/views/people/sidebar.blade.php b/resources/views/people/sidebar.blade.php new file mode 100644 index 0000000..ad611e1 --- /dev/null +++ b/resources/views/people/sidebar.blade.php @@ -0,0 +1,29 @@ +{{-- Pets --}} +@if ($modules->contains('key', 'pets')) + +@endif + +{{-- Contact information --}} +@if ($modules->contains('key', 'contact_information')) + +@endif + +{{-- Address --}} +@if ($modules->contains('key', 'addresses')) + +@endif + +{{-- Introductions --}} +@if ($modules->contains('key', 'how_you_met') && ! $contact->isMe()) +@include('people.introductions.index') +@endif + +{{-- Work --}} +@if ($modules->contains('key', 'work_information')) +@include('people.work.index') +@endif + +{{-- Food preferences --}} +@if ($modules->contains('key', 'food_preferences') && ! $contact->isMe()) +@include('people.food-preferences.index') +@endif diff --git a/resources/views/people/tasks/index.blade.php b/resources/views/people/tasks/index.blade.php new file mode 100644 index 0000000..78423ee --- /dev/null +++ b/resources/views/people/tasks/index.blade.php @@ -0,0 +1,3 @@ +
    + hashID() }} :contact-id={{ $contact->id }}> +
    diff --git a/resources/views/people/work/edit.blade.php b/resources/views/people/work/edit.blade.php new file mode 100644 index 0000000..0d0ea53 --- /dev/null +++ b/resources/views/people/work/edit.blade.php @@ -0,0 +1,62 @@ +@extends('layouts.skeleton') + +@section('content') +
    + + {{-- Breadcrumb --}} + + + +
    +
    +
    +
    +
    + @csrf + + @include('partials.errors') + +

    {{ trans('people.work_edit_title', ['name' => $contact->first_name]) }}

    + + {{-- Job --}} +
    + + +
    + + {{-- Company --}} +
    + + +
    + +
    + + {{ trans('app.cancel') }} +
    +
    +
    +
    +
    +
    + +
    +@endsection diff --git a/resources/views/people/work/index.blade.php b/resources/views/people/work/index.blade.php new file mode 100644 index 0000000..3f1d074 --- /dev/null +++ b/resources/views/people/work/index.blade.php @@ -0,0 +1,35 @@ + diff --git a/resources/views/settings/_sidebar.blade.php b/resources/views/settings/_sidebar.blade.php new file mode 100644 index 0000000..5d0b0b0 --- /dev/null +++ b/resources/views/settings/_sidebar.blade.php @@ -0,0 +1,81 @@ + diff --git a/resources/views/settings/api/index.blade.php b/resources/views/settings/api/index.blade.php new file mode 100644 index 0000000..07003cf --- /dev/null +++ b/resources/views/settings/api/index.blade.php @@ -0,0 +1,63 @@ +@extends('layouts.skeleton') + +@section('content') + +
    + + {{-- Breadcrumb --}} + + +
    +
    + + @include('settings._sidebar') + +
    + +
    +

    {{ trans('settings.api_title') }}

    +

    {{ trans('settings.api_description') }}

    +

    {!! trans('settings.api_help', ['url' => config('api.help')]) !!}

    +

    + {{ trans('settings.api_endpoint') }} + +

    +
    + +
    +
    + +
    +
    + +
    +
    + + +
    +
    + +
    +
    +
    +
    + +@endsection diff --git a/resources/views/settings/auditlog/index.blade.php b/resources/views/settings/auditlog/index.blade.php new file mode 100644 index 0000000..3227ca8 --- /dev/null +++ b/resources/views/settings/auditlog/index.blade.php @@ -0,0 +1,88 @@ +@extends('layouts.skeleton') + +@section('content') + +
    + + {{-- Breadcrumb --}} + + +
    +
    + + @include('settings._sidebar') + +
    + +
    +
    +

    + {{ trans('settings.logs_title') }} +

    +
      +
    • +
      + {{ trans('settings.logs_actor') }} +
      +
      + {{ trans('settings.logs_timestamp') }} +
      +
      + {{ trans('settings.logs_description') }} +
      +
      + {{ trans('settings.logs_subject') }} +
      +
    • + @foreach ($logsCollection as $log) +
    • +
      + {{ $log['author_name'] }} +
      +
      + {{ \App\Helpers\DateHelper::getShortDateWithTime($log['audited_at']) }} +
      +
      + {{ $log['description'] }} +
      +
      + @if($log['link']) + {{ $log['object'] }} + @else + {{ $log['object'] }} + @endif +
      +
    • + @endforeach +
    + +
    + {{ $logsPagination->links() }} +
    + +
    +
    +
    +
    +
    +
    + +@endsection diff --git a/resources/views/settings/dav/index.blade.php b/resources/views/settings/dav/index.blade.php new file mode 100644 index 0000000..62e544b --- /dev/null +++ b/resources/views/settings/dav/index.blade.php @@ -0,0 +1,51 @@ +@extends('layouts.skeleton') + +@section('content') + +
    + + {{-- Breadcrumb --}} + + +
    +
    + + @include('settings._sidebar') + +
    +
    +
    + + + +
    +
    +
    +
    +
    +
    + +@endsection diff --git a/resources/views/settings/export.blade.php b/resources/views/settings/export.blade.php new file mode 100644 index 0000000..14aa39c --- /dev/null +++ b/resources/views/settings/export.blade.php @@ -0,0 +1,123 @@ +@extends('layouts.skeleton') + +@section('content') + +
    + + {{-- Breadcrumb --}} + + +
    +
    + + @include('settings._sidebar') + +
    + +
    +
    + + @include ('partials.errors') + + @if (session('status')) +
    + {{ session('status') }} +
    + @endif + +

    {{ trans('settings.export_title') }}

    +

    {{ trans('settings.export_title_sql') }}

    +

    {{ trans('settings.export_sql_explanation') }}

    +

    {{ trans('settings.export_be_patient') }}

    +
    + @csrf +

    + +

    +
    +

    {!! trans('settings.export_sql_link_instructions', ['url' => 'https://github.com/monicahq/monica/blob/main/docs/installation/update.md#importing-sql-from-the-exporter-feature']) !!}

    + +

    {{ trans('settings.export_title_json') }}

    +

    {{ trans('settings.export_json_explanation') }}

    +
    + {{ trans('settings.export_json_beta') }} + https://github.com/monicahq/monica/discussions/5824 +
    +
    + @csrf +

    +
    + +

    {{ trans('settings.export_last_title') }}

    + @if ($exports->count() === 0) + {{ trans('settings.export_empty_title') }} + @else +
      +
    • +
      + {{ trans('settings.export_header_type') }} +
      +
      + {{ trans('settings.export_header_timestamp') }} +
      +
      + {{ trans('settings.export_header_status') }} +
      +
      + {{ trans('settings.export_header_actions') }} +
      +
    • + @foreach ($exports as $export) +
    • +
      + {{ trans("settings.export_type_{$export['type']}") }} +
      +
      + {{ \App\Helpers\DateHelper::getShortDateWithTime($export['created_at']) }} +
      +
      + {{ trans("settings.export_status_{$export['status']}") }} +
      +
      + @if ($export['status'] === \App\Models\Account\ExportJob::EXPORT_DONE) +
      + @csrf + + {{ trans('app.download') }} + +
      + + @endif +
      +
    • + @endforeach +
    + @endif + +
    +
    + +
    +
    +
    +
    + +@endsection diff --git a/resources/views/settings/imports/blank.blade.php b/resources/views/settings/imports/blank.blade.php new file mode 100644 index 0000000..47dc809 --- /dev/null +++ b/resources/views/settings/imports/blank.blade.php @@ -0,0 +1,59 @@ +@extends('layouts.skeleton') + +@section('content') + +
    + + {{-- Breadcrumb --}} + + +
    +
    + + @include('settings._sidebar') + +
    + +
    +
    + + + +

    {{ trans('settings.import_blank_title') }}

    + +

    {{ trans('settings.import_blank_question') }}

    + +

    {{ trans('settings.import_blank_description') }}

    + +

    {{ trans('settings.import_blank_cta') }}

    + + @if (config('monica.requires_subscription') && $accountHasLimitations) +

    {{ trans('settings.import_need_subscription') }}

    + @endif +
    +
    +
    + +
    +
    +
    + +@endsection diff --git a/resources/views/settings/imports/index.blade.php b/resources/views/settings/imports/index.blade.php new file mode 100644 index 0000000..6d743ae --- /dev/null +++ b/resources/views/settings/imports/index.blade.php @@ -0,0 +1,83 @@ +@extends('layouts.skeleton') + +@section('content') + +
    + + {{-- Breadcrumb --}} + + +
    +
    + + @include('settings._sidebar') + +
    + +

    + {{ trans('settings.import_title') }} + {{ trans('settings.import_cta') }} +

    + +

    {{ trans('settings.import_stat', ['number' => auth()->user()->account->importjobs->count()]) }}

    + +
      + @foreach (auth()->user()->account->importjobs as $importJob) +
    • +
      + @if (! is_null($importJob->ended_at)) + @if ($importJob->failed) + + @elseif ($importJob->contacts_found != $importJob->contacts_imported) + + @else + + @endif + @else + + @endif + {{ \App\Helpers\DateHelper::getShortDateWithTime($importJob->created_at) }} +
      +
      + @if (is_null($importJob->ended_at)) + {{ trans('settings.import_in_progress') }} + @endif + @if($importJob->failed_reason) + {{ $importJob->failed_reason }} + @elseif (! is_null($importJob->ended_at)) + {{ trans_choice('settings.import_result_stat', $importJob->contacts_found, ['total_contacts' => $importJob->contacts_found, 'total_imported' => $importJob->contacts_imported, 'total_skipped' => $importJob->contacts_skipped]) }} + @endif +
      +
      + @if (! is_null($importJob->ended_at)) + {{ trans('settings.import_view_report') }} + @endif +
      +
    • + @endforeach +
    + +
    +
    +
    +
    + +@endsection diff --git a/resources/views/settings/imports/report.blade.php b/resources/views/settings/imports/report.blade.php new file mode 100644 index 0000000..6e9c70b --- /dev/null +++ b/resources/views/settings/imports/report.blade.php @@ -0,0 +1,80 @@ +@extends('layouts.skeleton') + +@section('content') +
    + + {{-- Breadcrumb --}} + + + +
    +
    +
    +
    + +

    {{ trans('settings.import_report_title') }}

    + +
      +
    • {{ trans('settings.import_report_date') }}: {{ \App\Helpers\DateHelper::getShortDate($importJob->created_at) }}
    • +
    • {{ trans('settings.import_report_type') }}: {{ $importJob->type }}
    • +
    • {{ trans('settings.import_report_number_contacts') }}: {{ $importJob->contacts_found }}
    • +
    • {{ trans('settings.import_report_number_contacts_imported') }}: {{ $importJob->contacts_imported }}
    • +
    • {{ trans('settings.import_report_number_contacts_skipped') }}: {{ $importJob->contacts_skipped }}
    • +
    + +
      + + @foreach ($importJob->importJobReports as $importJobReport) +
    • +
      + @if ($importJobReport->skipped == 0) + {{ trans('settings.import_report_status_imported') }} + @else + {{ trans('settings.import_report_status_skipped') }} + @endif +
      +
      + {{ $importJobReport->contact_information }} +
      +
      + @if (! is_null($importJobReport->skip_reason)) + {{-- + settings.import_vcard_contact_exist + settings.import_vcard_contact_no_firstname + --}} + {{ trans('settings.'.$importJobReport->skip_reason) }} + @endif +
      +
    • + @endforeach + +
    + +
    +
    +
    +
    + +
    +@endsection diff --git a/resources/views/settings/imports/upload.blade.php b/resources/views/settings/imports/upload.blade.php new file mode 100644 index 0000000..4447b25 --- /dev/null +++ b/resources/views/settings/imports/upload.blade.php @@ -0,0 +1,89 @@ +@extends('layouts.skeleton') + +@section('content') + +
    + + {{-- Breadcrumb --}} + + +
    +
    +
    +
    + +
    +
    +

    {{ trans('settings.import_upload_title') }}

    + +
    +

    {{ trans('settings.import_upload_rules_desc') }}

    +
      +
    • {!! trans('settings.import_upload_rule_format') !!}
    • +
    • {{ trans('settings.import_upload_rule_vcard') }}
    • +
    • {!! trans('settings.import_upload_rule_instructions', [ + 'url1' => 'http://osxdaily.com/2015/07/14/export-contacts-mac-os-x/', + 'url2' => 'http://www.akruto.com/backup-phone-contacts-calendar/how-to-export-google-contacts-to-csv-or-vcard/' + ]) !!}
    • +
    • {{ trans('settings.import_upload_rule_multiple') }}
    • +
    • {{ trans('settings.import_upload_rule_limit') }}
    • +
    • {{ trans('settings.import_upload_rule_time') }}
    • +
    • {{ trans('settings.import_upload_rule_cant_revert') }}
    • +
    +
    + + @include('partials.errors') + +
    + @csrf + +
    + + + {{ trans('people.information_edit_max_size', ['size' => config('monica.max_upload_size')]) }} +
    + +
    + + + {{ trans('settings.import_upload_behaviour_help') }} +
    + +
    + + {{ trans('app.cancel') }} +
    +
    +
    +
    +
    +
    +
    +
    +
    + +@endsection diff --git a/resources/views/settings/index.blade.php b/resources/views/settings/index.blade.php new file mode 100644 index 0000000..daaf8c4 --- /dev/null +++ b/resources/views/settings/index.blade.php @@ -0,0 +1,172 @@ +@extends('layouts.skeleton') + +@section('content') + +
    + + {{-- Breadcrumb --}} + + +
    +
    + + @include('settings._sidebar') + +
    +
    +
    + + @include('partials.errors') + + @if (session('status')) +
    + {{ session('status') }} +
    + @endif + +
    + @csrf + + {{-- id --}} + + +

    @lang('settings.title_general')

    +
    + {{-- names --}} +
    + + +
    + +
    + + +
    + + {{-- email address --}} +
    + + + {{ trans('settings.email_help') }} +
    + +
    + + + @lang('settings.me_help') +
    +
    + +

    @lang('settings.title_i18n')

    +
    + {{-- Locale --}} +
    + + + {!! trans('settings.locale_help', ['url' => 'https://github.com/monicahq/monica/blob/main/docs/contribute/translate.md']) !!} +
    + + {{-- currency for user --}} +
    + + @include('partials.components.currency-select', ['selectionID' => auth()->user()->currency_id ]) +
    + + {{-- Temperature scale --}} +
    + + +
    + + {{-- Reminder --}} +
    + + +
    +
    + +

    @lang('settings.title_layout')

    +
    + {{-- Way of displaying names --}} +
    + + +
    + + {{-- Layout --}} +
    + + +
    +
    + + +
    +
    +
    + +
    + @csrf + +

    {{ trans('settings.reset_title') }}

    +

    {{ trans('settings.reset_desc') }}

    + +
    + +
    + @csrf + +

    {{ trans('settings.delete_title') }}

    +

    {{ trans('settings.delete_desc') }}

    +

    {{ trans('settings.delete_other_desc') }}

    + +
    + +
    +
    +
    +
    + +@endsection diff --git a/resources/views/settings/personalization/index.blade.php b/resources/views/settings/personalization/index.blade.php new file mode 100644 index 0000000..8749767 --- /dev/null +++ b/resources/views/settings/personalization/index.blade.php @@ -0,0 +1,81 @@ +@extends('layouts.skeleton') + +@section('content') + +
    + + {{-- Breadcrumb --}} + + +
    +
    + + @include('settings._sidebar') + +
    + +
    +

    {{ trans('settings.personalization_tab_title') }}

    +

    {{ trans('settings.personalization_title') }}

    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    + +@endsection diff --git a/resources/views/settings/security/index.blade.php b/resources/views/settings/security/index.blade.php new file mode 100644 index 0000000..032befb --- /dev/null +++ b/resources/views/settings/security/index.blade.php @@ -0,0 +1,101 @@ +@extends('layouts.skeleton') + +@section('content') + +
    + + {{-- Breadcrumb --}} + + +
    +
    + + @include('settings._sidebar') + +
    + +
    +
    + @include('partials.errors') + + @if (session('status')) +
    + {{ session('status') }} +
    + @endif + +

    {{ trans('settings.security_title') }}

    +

    {{ trans('settings.security_help') }}

    + +
    + @csrf + +

    {{ trans('settings.password_change') }}

    + +
    + + +
    +
    + + +
    +
    + + +
    + + +
    + + @if (config('google2fa.enabled')===true) +
    +

    {{ trans('settings.2fa_title') }}

    + + + + @if (config('google2fa.enabled')===true) + + + @endif + + @if (config('webauthn.enable')===true) + + + @endif + +
    + + @endif +
    +
    +
    +
    +
    +
    + +@endsection diff --git a/resources/views/settings/storage/index.blade.php b/resources/views/settings/storage/index.blade.php new file mode 100644 index 0000000..acfc4c2 --- /dev/null +++ b/resources/views/settings/storage/index.blade.php @@ -0,0 +1,89 @@ +@extends('layouts.skeleton') + +@section('content') + +
    + + {{-- Breadcrumb --}} + + +
    +
    + + @include('settings._sidebar') + +
    + +
    +
    + +

    {{ trans('settings.storage_title') }}

    + +

    {{ trans('settings.storage_account_info', ['accountLimit' => $accountLimit, 'currentAccountSize' => $currentAccountSize, 'percentUsage' => $percentUsage,]) }}

    + +

    {{ trans('settings.storage_description') }}

    + +
      +
    • +
      + {{ trans('settings.logs_timestamp') }} +
      +
      + {{ trans('settings.logs_object') }} +
      +
      + {{ trans('settings.logs_size') }} +
      +
      + {{ trans('settings.logs_subject') }} +
      +
    • + @foreach($elements as $element) +
    • +
      + {{ \App\Helpers\DateHelper::getShortDateWithTime($element->created_at) }} +
      +
      + {{ $element->original_filename }} +
      +
      + {{ round($element->filesize / 1000) }} +
      +
      + @if ($element->contact()) + @if ($element instanceof \App\Models\Contact\Document) + {{ $element->contact->name }} + @else + {{ $element->contact()->name }} + @endif + @endif +
      +
    • + @endforeach +
    +
    +
    +
    +
    +
    +
    + +@endsection diff --git a/resources/views/settings/subscriptions/account.blade.php b/resources/views/settings/subscriptions/account.blade.php new file mode 100644 index 0000000..ecf9e36 --- /dev/null +++ b/resources/views/settings/subscriptions/account.blade.php @@ -0,0 +1,123 @@ +@extends('layouts.skeleton') + +@section('content') + +
    + + {{-- Breadcrumb --}} + + +
    +
    + + @include('settings._sidebar') + +
    + +
    +
    + +

    {{ trans('settings.subscriptions_account_current_plan') }}

    + +

    {{ trans('settings.subscriptions_account_current_paid_plan', ['name' => $planInformation['name']]) }}

    + + @include('partials.subscription') + +
    +
    +
    +
    + {{ trans('settings.subscriptions_account_next_billing_title') }} +
    +
    +
    +
    + {!! trans('settings.subscriptions_account_next_billing', ['date' => $planInformation['nextBillingDate']]) !!} +
    +
    + {!! trans('settings.subscriptions_account_bill_' . $planInformation['type'], ['price' => $planInformation['friendlyPrice']]) !!} +
    +
    + +
    + +
    +
    +
    + {{ trans('settings.subscriptions_account_cancel_title') }} +
    +
    +
    +
    + {{ trans('settings.subscriptions_account_cancel') }} +
    +
    + +
    +
    + + + {{-- Only display invoices if the subscription exists or existed --}} + @if ($hasInvoices) +
    +

    {{ trans('settings.subscriptions_account_invoices') }}

    + +
    + @endif + +
    +
    + +
    +
    +
    +
    + +@endsection diff --git a/resources/views/settings/subscriptions/archive.blade.php b/resources/views/settings/subscriptions/archive.blade.php new file mode 100644 index 0000000..2f664bb --- /dev/null +++ b/resources/views/settings/subscriptions/archive.blade.php @@ -0,0 +1,53 @@ +@extends('layouts.skeleton') + +@section('content') + +
    + + {{-- Breadcrumb --}} + + +
    +
    +
    +
    + @include('partials.errors') + +
    +
    +

    {{ trans('settings.archive_title') }}

    + +

    {{ trans('settings.archive_desc') }}

    + +
    + @csrf + +

    + +
    + +
    +
    +
    +
    +
    + + @endsection diff --git a/resources/views/settings/subscriptions/blank.blade.php b/resources/views/settings/subscriptions/blank.blade.php new file mode 100644 index 0000000..266b52b --- /dev/null +++ b/resources/views/settings/subscriptions/blank.blade.php @@ -0,0 +1,114 @@ +@extends('layouts.skeleton') + +@section('content') + +
    + + {{-- Breadcrumb --}} + + +
    +
    +
    +
    + +

    {{ trans('settings.subscriptions_account_upgrade_title') }}

    +

    {{ trans('settings.subscriptions_account_upgrade_choice', ['customers' => $numberOfCustomers]) }}

    + +
    +
    + +

    {{ trans('settings.subscriptions_account_payment') }}

    +
    +
    +
    + +

    {{ trans('settings.subscriptions_plan_year_title') }}

    +

    + {{ trans('settings.subscriptions_plan_choose') }} +

    +

    + {{ trans('settings.subscriptions_plan_frequency_year', ['amount' => \App\Helpers\InstanceHelper::getPlanInformationFromConfig('annual')['friendlyPrice']]) }} +

    +
      +
    • + + + + + + + + + + {{ trans('settings.subscriptions_plan_year_bonus') }} +
    • +
    +
    +
    +
    +
    +

    {{ trans('settings.subscriptions_plan_month_title') }}

    +

    + {{ trans('settings.subscriptions_plan_choose') }} +

    +

    + {{ trans('settings.subscriptions_plan_frequency_month', ['amount' => \App\Helpers\InstanceHelper::getPlanInformationFromConfig('monthly')['friendlyPrice']]) }} +

    +
      +
    • + + + + + + + + + + {{ trans('settings.subscriptions_plan_month_bonus') }} +
    • +
    +
    +
    +
    +

    {{ trans('settings.subscriptions_plan_include1') }}

    +

    {{ trans('settings.subscriptions_plan_include2') }}

    +

    {{ trans('settings.subscriptions_plan_include3') }}

    +
    +
    + +

    {{ trans('settings.subscriptions_help_title') }}

    +

    {{ trans('settings.subscriptions_help_opensource_title') }}

    +

    {{ trans('settings.subscriptions_help_opensource_desc') }}

    + +

    {{ trans('settings.subscriptions_help_limits_title') }}

    +

    {{ trans('settings.subscriptions_help_limits_plan', ['number' => config('monica.number_of_allowed_contacts_free_account')]) }}

    + +

    {{ trans('settings.subscriptions_help_change_title') }}

    +

    {{ trans('settings.subscriptions_help_change_desc') }}

    +
    +
    +
    +
    +
    + +@endsection diff --git a/resources/views/settings/subscriptions/confirm.blade.php b/resources/views/settings/subscriptions/confirm.blade.php new file mode 100644 index 0000000..4e2b2b6 --- /dev/null +++ b/resources/views/settings/subscriptions/confirm.blade.php @@ -0,0 +1,36 @@ +@extends('layouts.skeleton') + +@section('content') + +
    + +
    +
    + @if (! $payment->isSucceeded() && ! $payment->isCancelled()) +

    {{ trans('settings.subscriptions_payment_confirm_title', ['amount' => $payment->amount()]) }}

    +

    {{ trans('settings.subscriptions_payment_confirm_information') }}

    + @endif + + @include('partials.errors') + + +
    +
    +
    + +@endsection + +@push('scripts') + + +@endpush diff --git a/resources/views/settings/subscriptions/downgrade-checklist.blade.php b/resources/views/settings/subscriptions/downgrade-checklist.blade.php new file mode 100644 index 0000000..2e828aa --- /dev/null +++ b/resources/views/settings/subscriptions/downgrade-checklist.blade.php @@ -0,0 +1,80 @@ +@extends('layouts.skeleton') + +@section('content') + +
    + + {{-- Breadcrumb --}} + + +
    +
    +
    +
    + @include('partials.errors') + +

    {{ trans('settings.subscriptions_downgrade_title') }}

    + +

    {{ trans('settings.subscriptions_downgrade_limitations') }}

    + +
      + +
    • + + {{ trans('settings.subscriptions_downgrade_rule_users') }} + {!! trans_choice('settings.subscriptions_downgrade_rule_users_constraint', $numberOfUsers, ['url' => route('settings.users.index'), 'count' => $numberOfUsers]) !!} +
    • + +
    • + + {{ trans('settings.subscriptions_downgrade_rule_invitations') }} + {!! trans_choice('settings.subscriptions_downgrade_rule_invitations_constraint', $numberOfPendingInvitations, ['url' => route('settings.users.index'), 'count' => $numberOfPendingInvitations]) !!} +
    • + +
    • + + {{ trans('settings.subscriptions_downgrade_rule_contacts', ['number' => config('monica.number_of_allowed_contacts_free_account')]) }} + {!! trans_choice('settings.subscriptions_downgrade_rule_contacts_constraint', $numberOfActiveContacts, ['url' => '/people', 'count' => $numberOfActiveContacts]) !!} + @if ($hasReachedContactLimit) + {!! trans('settings.subscriptions_downgrade_rule_contacts_archive', ['url' => route('settings.subscriptions.archive')]) !!} + @endif +
    • + +
    + +
    + @csrf + + @if ($canDowngrade) +

    + @else +

    + @endif + +
    + +
    +
    +
    +
    +
    + +@endsection diff --git a/resources/views/settings/subscriptions/downgrade-success.blade.php b/resources/views/settings/subscriptions/downgrade-success.blade.php new file mode 100644 index 0000000..06781b9 --- /dev/null +++ b/resources/views/settings/subscriptions/downgrade-success.blade.php @@ -0,0 +1,70 @@ +@extends('layouts.skeleton') + +@section('content') + +
    + + {{-- Breadcrumb --}} + + +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +

    {{ trans('settings.subscriptions_downgrade_success') }}

    +

    {{ trans('settings.subscriptions_downgrade_thanks') }}

    +

    {{trans('settings.subscriptions_back') }}

    +
    +
    +
    + +@endsection diff --git a/resources/views/settings/subscriptions/success.blade.php b/resources/views/settings/subscriptions/success.blade.php new file mode 100644 index 0000000..b356741 --- /dev/null +++ b/resources/views/settings/subscriptions/success.blade.php @@ -0,0 +1,67 @@ +@extends('layouts.skeleton') + +@section('content') + +
    + + {{-- Breadcrumb --}} + + +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + +

    {{ trans('settings.subscriptions_upgrade_success') }}

    +

    {{ trans('settings.subscriptions_upgrade_thanks') }}

    +

    {{ trans('settings.subscriptions_back') }}

    +
    +
    +
    + +@endsection diff --git a/resources/views/settings/subscriptions/update.blade.php b/resources/views/settings/subscriptions/update.blade.php new file mode 100644 index 0000000..594cd6d --- /dev/null +++ b/resources/views/settings/subscriptions/update.blade.php @@ -0,0 +1,73 @@ +@extends('layouts.skeleton') + +@section('content') + +
    + + {{-- Breadcrumb --}} + + +
    +
    +
    +
    + +

    {{ trans('settings.subscriptions_account_update_title') }}

    + +

    {{ trans('settings.subscriptions_account_update_description') }}

    + + @if ($legacyPlan) +
    + + +
    + @endif + +
    + @csrf + + @foreach ($plans as $plan) +
    + + +
    + @endforeach + +

    {{ trans('settings.subscriptions_account_update_information') }}

    + +
    + + {{ trans('app.cancel') }} +
    +
    + +
    +
    +
    +
    +
    + +@endsection diff --git a/resources/views/settings/subscriptions/upgrade.blade.php b/resources/views/settings/subscriptions/upgrade.blade.php new file mode 100644 index 0000000..7d75752 --- /dev/null +++ b/resources/views/settings/subscriptions/upgrade.blade.php @@ -0,0 +1,55 @@ +@extends('layouts.skeleton') + +@section('content') + +
    + + {{-- Breadcrumb --}} + + +
    +
    +

    {{ trans('settings.subscriptions_upgrade_choose', ['plan' => $planInformation['type']]) }}

    +

    {{ trans('settings.subscriptions_upgrade_infos') }}

    + + @include('partials.errors') + + +

    {{ trans('settings.subscriptions_upgrade_charge', ['price' => $planInformation['friendlyPrice'], 'date' => $nextTheoriticalBillingDate]) }}

    +

    {!! trans('settings.subscriptions_upgrade_charge_handled', ['url' => 'https://stripe.com']) !!}

    +
    +
    +
    + +@endsection + +@push('scripts') + + +@endpush diff --git a/resources/views/settings/tags.blade.php b/resources/views/settings/tags.blade.php new file mode 100644 index 0000000..c73d1eb --- /dev/null +++ b/resources/views/settings/tags.blade.php @@ -0,0 +1,119 @@ +@extends('layouts.skeleton') + +@section('content') + +
    + + {{-- Breadcrumb --}} + + +
    +
    + + @include('settings._sidebar') + +
    +
    +
    + @if (auth()->user()->account->tags->count() == 0) + +
    + + + +

    {{ trans('settings.tags_blank_title') }}

    + +

    {{ trans('settings.tags_blank_description') }}

    + +
    + + @else + +

    + {{ trans('settings.tags_list_title') }} +

    + +

    {{ trans('settings.tags_list_description') }}

    + + @if (session('success')) +
    + {{ session('success') }} +
    + @endif + +
      + @foreach (auth()->user()->account->tags as $tag) +
    • +
      + +
      + {{ $tag->name }} + ({{ trans_choice('settings.tags_list_contact_number', $tag->contacts()->count(), ['count' => $tag->contacts()->count()]) }}) + +
      +
      +
      + + + + +
      + @method('DELETE') + @csrf + + + +
      +
      + +
    • + @endforeach +
    + + @endif +
    +
    +
    +
    +
    +
    + +@endsection + diff --git a/resources/views/settings/users/accept.blade.php b/resources/views/settings/users/accept.blade.php new file mode 100644 index 0000000..7373da0 --- /dev/null +++ b/resources/views/settings/users/accept.blade.php @@ -0,0 +1,75 @@ +@extends('marketing.skeleton') + +@section('content') + +
    +
    +
    + + +
    +
    +
    + +@endsection diff --git a/resources/views/settings/users/add.blade.php b/resources/views/settings/users/add.blade.php new file mode 100644 index 0000000..e25a0d9 --- /dev/null +++ b/resources/views/settings/users/add.blade.php @@ -0,0 +1,80 @@ +@extends('layouts.skeleton') + +@section('content') +
    + + {{-- Breadcrumb --}} + + + +
    +
    +
    +
    +
    +
    +
    + @csrf + +

    {{ trans('settings.users_add_title') }}

    + +

    {{ trans('settings.users_add_description') }}

    + + @include('partials.errors') + + {{-- Email --}} +
    +
    + + +
    +
    + + {{-- Explicit confirmation --}} +
    + + {{ trans('settings.users_add_confirmation') }} + +
    + +
    + + {{ trans('app.cancel') }} +
    +
    +
    +
    +
    +
    +
    +
    + +
    +@endsection diff --git a/resources/views/settings/users/blank.blade.php b/resources/views/settings/users/blank.blade.php new file mode 100644 index 0000000..986b91f --- /dev/null +++ b/resources/views/settings/users/blank.blade.php @@ -0,0 +1,58 @@ +@extends('layouts.skeleton') + +@section('content') + +
    + + {{-- Breadcrumb --}} + + +
    +
    + + @include('settings._sidebar') + +
    + +
    +
    + + + +

    {{ trans('settings.users_blank_title') }}

    + +

    {{ trans('settings.users_blank_add_title') }}

    + +

    {{ trans('settings.users_blank_description') }}

    + +

    {{ trans('settings.users_blank_cta') }}

    + + @if (config('monica.requires_subscription') && $accountHasLimitations) +

    {{ trans('settings.users_invitation_need_subscription') }}

    + @endif +
    +
    +
    +
    +
    +
    + +@endsection diff --git a/resources/views/settings/users/index.blade.php b/resources/views/settings/users/index.blade.php new file mode 100644 index 0000000..6ee9ec9 --- /dev/null +++ b/resources/views/settings/users/index.blade.php @@ -0,0 +1,101 @@ +@extends('layouts.skeleton') + +@section('content') + +
    + + {{-- Breadcrumb --}} + + +
    +
    + + @include('settings._sidebar') + +
    + +
    +
    +

    + {{ trans('settings.users_list_title') }} + {{ trans('settings.users_list_add_user') }} +

    +
      + @foreach ($users as $user) +
    • +
      + {{ $user->name }} ({{ $user->email }}) +
      +
      + @if ($user->id == auth()->user()->id) + {{ trans('settings.users_list_you') }} + @else +
      + @method('DELETE') + @csrf + + + +
      + @endif +
      +
    • + @endforeach +
    + + @if (auth()->user()->account->invitations()->count() != 0) +

    {{ trans('settings.users_list_invitations_title') }}

    + +

    {{ trans('settings.users_list_invitations_explanation') }}

    + +
      + @foreach (auth()->user()->account->invitations as $invitation) +
    • +
      + {{ $invitation->email }} +
      +
      + {{ trans('settings.users_list_invitations_invited_by', ['name' => $invitation->invitedBy->name]) }} +
      +
      + {{ trans('settings.users_list_invitations_sent_date', ['date' => \App\Helpers\DateHelper::getShortDate($invitation->created_at)]) }} +
      +
      +
      + @method('DELETE') + @csrf + + + +
      +
      +
    • + @endforeach +
    + @endif +
    +
    +
    +
    +
    +
    + +@endsection diff --git a/resources/views/vendor/.gitkeep b/resources/views/vendor/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/resources/views/vendor/.gitkeep @@ -0,0 +1 @@ + diff --git a/resources/views/vendor/mail/html/button.blade.php b/resources/views/vendor/mail/html/button.blade.php new file mode 100644 index 0000000..e74fe55 --- /dev/null +++ b/resources/views/vendor/mail/html/button.blade.php @@ -0,0 +1,19 @@ + + + + + diff --git a/resources/views/vendor/mail/html/footer.blade.php b/resources/views/vendor/mail/html/footer.blade.php new file mode 100644 index 0000000..3ff41f8 --- /dev/null +++ b/resources/views/vendor/mail/html/footer.blade.php @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/resources/views/vendor/mail/html/header.blade.php b/resources/views/vendor/mail/html/header.blade.php new file mode 100644 index 0000000..fa1875c --- /dev/null +++ b/resources/views/vendor/mail/html/header.blade.php @@ -0,0 +1,11 @@ + + + +@if (trim($slot) === 'Laravel') + +@else +{{ $slot }} +@endif + + + diff --git a/resources/views/vendor/mail/html/layout.blade.php b/resources/views/vendor/mail/html/layout.blade.php new file mode 100644 index 0000000..02a54e2 --- /dev/null +++ b/resources/views/vendor/mail/html/layout.blade.php @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + diff --git a/resources/views/vendor/mail/html/message.blade.php b/resources/views/vendor/mail/html/message.blade.php new file mode 100644 index 0000000..7b774e9 --- /dev/null +++ b/resources/views/vendor/mail/html/message.blade.php @@ -0,0 +1,27 @@ +@component('mail::layout') +{{-- Header --}} +@slot('header') +@component('mail::header', ['url' => Str::of(config('app.url'))->ltrim('/')]) +{{ config('app.display_name') }} +@endcomponent +@endslot + +{{-- Body --}} +{{ $slot }} + +{{-- Subcopy --}} +@isset($subcopy) +@slot('subcopy') +@component('mail::subcopy') +{{ $subcopy }} +@endcomponent +@endslot +@endisset + +{{-- Footer --}} +@slot('footer') +@component('mail::footer') +© {{ date('Y') }} {{ config('app.display_name') }}. @lang('All rights reserved.') +@endcomponent +@endslot +@endcomponent diff --git a/resources/views/vendor/mail/html/panel.blade.php b/resources/views/vendor/mail/html/panel.blade.php new file mode 100644 index 0000000..2975a60 --- /dev/null +++ b/resources/views/vendor/mail/html/panel.blade.php @@ -0,0 +1,14 @@ + + + + + + diff --git a/resources/views/vendor/mail/html/subcopy.blade.php b/resources/views/vendor/mail/html/subcopy.blade.php new file mode 100644 index 0000000..790ce6c --- /dev/null +++ b/resources/views/vendor/mail/html/subcopy.blade.php @@ -0,0 +1,7 @@ + + + + + diff --git a/resources/views/vendor/mail/html/table.blade.php b/resources/views/vendor/mail/html/table.blade.php new file mode 100644 index 0000000..a5f3348 --- /dev/null +++ b/resources/views/vendor/mail/html/table.blade.php @@ -0,0 +1,3 @@ +
    +{{ Illuminate\Mail\Markdown::parse($slot) }} +
    diff --git a/resources/views/vendor/mail/html/themes/default.css b/resources/views/vendor/mail/html/themes/default.css new file mode 100644 index 0000000..aee84b4 --- /dev/null +++ b/resources/views/vendor/mail/html/themes/default.css @@ -0,0 +1,283 @@ +/* Base */ + +body, +body *:not(html):not(style):not(br):not(tr):not(code) { + box-sizing: border-box; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, + 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; + position: relative; +} + +body { + -webkit-text-size-adjust: none; + background-color: #ffffff; + color: #718096; + height: 100%; + line-height: 1.4; + margin: 0; + padding: 0; + width: 100% !important; +} + +p, +ul, +ol, +blockquote { + line-height: 1.4; + text-align: left; +} + +a { + color: #3869d4; +} + +a img { + border: none; +} + +/* Typography */ + +h1 { + color: #3d4852; + font-size: 18px; + font-weight: bold; + margin-top: 0; + text-align: left; +} + +h2 { + font-size: 16px; + font-weight: bold; + margin-top: 0; + text-align: left; +} + +h3 { + font-size: 14px; + font-weight: bold; + margin-top: 0; + text-align: left; +} + +p { + font-size: 16px; + line-height: 1.5em; + margin-top: 0; + text-align: left; +} + +p.sub { + font-size: 12px; +} + +img { + max-width: 100%; +} + +/* Layout */ + +.wrapper { + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 100%; + background-color: #edf2f7; + margin: 0; + padding: 0; + width: 100%; +} + +.content { + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 100%; + margin: 0; + padding: 0; + width: 100%; +} + +/* Header */ + +.header { + padding: 25px 0; + text-align: center; +} + +.header a { + color: #3d4852; + font-size: 19px; + font-weight: bold; + text-decoration: none; +} + +/* Logo */ + +.logo { + height: 75px; + width: 75px; +} + +/* Body */ + +.body { + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 100%; + background-color: #edf2f7; + border-bottom: 1px solid #edf2f7; + border-top: 1px solid #edf2f7; + margin: 0; + padding: 0; + width: 100%; +} + +.inner-body { + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 570px; + background-color: #ffffff; + border-color: #e8e5ef; + border-radius: 2px; + border-width: 1px; + box-shadow: 0 2px 0 rgba(0, 0, 150, 0.025), 2px 4px 0 rgba(0, 0, 150, 0.015); + margin: 0 auto; + padding: 0; + width: 570px; +} + +/* Subcopy */ + +.subcopy { + border-top: 1px solid #e8e5ef; + margin-top: 25px; + padding-top: 25px; +} + +.subcopy p { + font-size: 14px; +} + +/* Footer */ + +.footer { + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 570px; + margin: 0 auto; + padding: 0; + text-align: center; + width: 570px; +} + +.footer p { + color: #b0adc5; + font-size: 12px; + text-align: center; +} + +.footer a { + color: #b0adc5; + text-decoration: underline; +} + +/* Tables */ + +.table table { + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 100%; + margin: 30px auto; + width: 100%; +} + +.table th { + border-bottom: 1px solid #edeff2; + margin: 0; + padding-bottom: 8px; +} + +.table td { + color: #74787e; + font-size: 15px; + line-height: 18px; + margin: 0; + padding: 10px 0; +} + +.content-cell { + max-width: 100vw; + padding: 32px; +} + +/* Buttons */ + +.action { + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 100%; + margin: 30px auto; + padding: 0; + text-align: center; + width: 100%; +} + +.button { + -webkit-text-size-adjust: none; + border-radius: 4px; + color: #fff; + display: inline-block; + overflow: hidden; + text-decoration: none; +} + +.button-blue, +.button-primary { + background-color: #2d3748; + border-bottom: 8px solid #2d3748; + border-left: 18px solid #2d3748; + border-right: 18px solid #2d3748; + border-top: 8px solid #2d3748; +} + +.button-green, +.button-success { + background-color: #48bb78; + border-bottom: 8px solid #48bb78; + border-left: 18px solid #48bb78; + border-right: 18px solid #48bb78; + border-top: 8px solid #48bb78; +} + +.button-red, +.button-error { + background-color: #e53e3e; + border-bottom: 8px solid #e53e3e; + border-left: 18px solid #e53e3e; + border-right: 18px solid #e53e3e; + border-top: 8px solid #e53e3e; +} + +/* Panels */ + +.panel { + border-left: #2d3748 solid 4px; + margin: 21px 0; +} + +.panel-content { + background-color: #edf2f7; + color: #718096; + padding: 16px; +} + +.panel-content p { + color: #718096; +} + +.panel-item { + padding: 0; +} + +.panel-item p:last-of-type { + margin-bottom: 0; + padding-bottom: 0; +} diff --git a/resources/views/vendor/mail/text/button.blade.php b/resources/views/vendor/mail/text/button.blade.php new file mode 100644 index 0000000..97444eb --- /dev/null +++ b/resources/views/vendor/mail/text/button.blade.php @@ -0,0 +1 @@ +{{ $slot }}: {{ $url }} diff --git a/resources/views/vendor/mail/text/footer.blade.php b/resources/views/vendor/mail/text/footer.blade.php new file mode 100644 index 0000000..3338f62 --- /dev/null +++ b/resources/views/vendor/mail/text/footer.blade.php @@ -0,0 +1 @@ +{{ $slot }} diff --git a/resources/views/vendor/mail/text/header.blade.php b/resources/views/vendor/mail/text/header.blade.php new file mode 100644 index 0000000..aaa3e57 --- /dev/null +++ b/resources/views/vendor/mail/text/header.blade.php @@ -0,0 +1 @@ +[{{ $slot }}]({{ $url }}) diff --git a/resources/views/vendor/mail/text/layout.blade.php b/resources/views/vendor/mail/text/layout.blade.php new file mode 100644 index 0000000..9378baa --- /dev/null +++ b/resources/views/vendor/mail/text/layout.blade.php @@ -0,0 +1,9 @@ +{!! strip_tags($header) !!} + +{!! strip_tags($slot) !!} +@isset($subcopy) + +{!! strip_tags($subcopy) !!} +@endisset + +{!! strip_tags($footer) !!} diff --git a/resources/views/vendor/mail/text/message.blade.php b/resources/views/vendor/mail/text/message.blade.php new file mode 100644 index 0000000..09fae40 --- /dev/null +++ b/resources/views/vendor/mail/text/message.blade.php @@ -0,0 +1,27 @@ +@component('mail::layout') + {{-- Header --}} + @slot('header') + @component('mail::header', ['url' => Str::of(config('app.url'))->ltrim('/')]) + {{ config('app.display_name') }} + @endcomponent + @endslot + + {{-- Body --}} + {{ $slot }} + + {{-- Subcopy --}} + @isset($subcopy) + @slot('subcopy') + @component('mail::subcopy') + {{ $subcopy }} + @endcomponent + @endslot + @endisset + + {{-- Footer --}} + @slot('footer') + @component('mail::footer') + © {{ date('Y') }} {{ config('app.display_name') }}. @lang('All rights reserved.') + @endcomponent + @endslot +@endcomponent diff --git a/resources/views/vendor/mail/text/panel.blade.php b/resources/views/vendor/mail/text/panel.blade.php new file mode 100644 index 0000000..3338f62 --- /dev/null +++ b/resources/views/vendor/mail/text/panel.blade.php @@ -0,0 +1 @@ +{{ $slot }} diff --git a/resources/views/vendor/mail/text/subcopy.blade.php b/resources/views/vendor/mail/text/subcopy.blade.php new file mode 100644 index 0000000..3338f62 --- /dev/null +++ b/resources/views/vendor/mail/text/subcopy.blade.php @@ -0,0 +1 @@ +{{ $slot }} diff --git a/resources/views/vendor/mail/text/table.blade.php b/resources/views/vendor/mail/text/table.blade.php new file mode 100644 index 0000000..3338f62 --- /dev/null +++ b/resources/views/vendor/mail/text/table.blade.php @@ -0,0 +1 @@ +{{ $slot }} diff --git a/resources/views/vendor/notifications/email.blade.php b/resources/views/vendor/notifications/email.blade.php new file mode 100644 index 0000000..929cd26 --- /dev/null +++ b/resources/views/vendor/notifications/email.blade.php @@ -0,0 +1,62 @@ +@component('mail::message') +{{-- Greeting --}} +@if (! empty($greeting)) +# {{ $greeting }} +@else +@if ($level == 'error') +# @lang('mail.notifications_whoops') +@else +# @lang('mail.notifications_hello') +@endif +@endif + +{{-- Intro Lines --}} +@foreach ($introLines as $line) +{{ $line }} + +@endforeach + +{{-- Action Button --}} +@isset($actionText) + +@component('mail::button', ['url' => $actionUrl, 'color' => $color]) +{{ $actionText }} +@endcomponent +@endisset + +{{-- Outro Lines --}} +@foreach ($outroLines as $line) +{{ $line }} + +@endforeach + +{{-- Salutation --}} +@if (! empty($salutation)) +{{ $salutation }} +@else +@lang('mail.notifications_regards'),
    {{ config('app.display_name') }} +@endif + +{{-- Subcopy --}} +@isset($actionText) +@component('mail::subcopy') +@lang('mail.notifications_footer', + [ + 'actionText' => $actionText, + 'actionURL' => $actionUrl + ] +) +@endcomponent +@endisset +@endcomponent diff --git a/resources/views/vendor/pagination/default.blade.php b/resources/views/vendor/pagination/default.blade.php new file mode 100644 index 0000000..5a6a593 --- /dev/null +++ b/resources/views/vendor/pagination/default.blade.php @@ -0,0 +1,36 @@ +@if ($paginator->hasPages()) +
      + {{-- Previous Page Link --}} + @if ($paginator->onFirstPage()) +
    • «
    • + @else +
    • + @endif + + {{-- Pagination Elements --}} + @foreach ($elements as $element) + {{-- "Three Dots" Separator --}} + @if (is_string($element)) +
    • {{ $element }}
    • + @endif + + {{-- Array Of Links --}} + @if (is_array($element)) + @foreach ($element as $page => $url) + @if ($page == $paginator->currentPage()) +
    • {{ $page }}
    • + @else +
    • {{ $page }}
    • + @endif + @endforeach + @endif + @endforeach + + {{-- Next Page Link --}} + @if ($paginator->hasMorePages()) +
    • + @else +
    • »
    • + @endif +
    +@endif diff --git a/resources/views/vendor/passport/authorize.blade.php b/resources/views/vendor/passport/authorize.blade.php new file mode 100644 index 0000000..c71d6ac --- /dev/null +++ b/resources/views/vendor/passport/authorize.blade.php @@ -0,0 +1,58 @@ +@extends('marketing.skeleton') + +@section('content') + +
    +
    +
    + + +
    +
    +
    + +@endsection + diff --git a/routes/api.php b/routes/api.php new file mode 100644 index 0000000..adb30a6 --- /dev/null +++ b/routes/api.php @@ -0,0 +1,157 @@ + ['index']])->name('index', 'api.statistics'); + +Route::resource('compliance', 'Settings\\ApiComplianceController', ['only' => ['index', 'show']]); + +Route::resource('currencies', 'Settings\\ApiCurrencyController', ['only' => ['index', 'show']])->name('index', 'api.currencies'); + +Route::group(['middleware' => ['auth:api']], function () { + Route::get('/', 'ApiController@success')->name('api'); + Route::name('api.')->group(function () { + // Me + Route::get('/me', 'Account\\ApiUserController@show'); + Route::get('/me/compliance', 'Account\\ApiUserController@getSignedPolicies'); + Route::get('/me/compliance/{id}', 'Account\\ApiUserController@get'); + Route::post('/me/compliance', 'Account\\ApiUserController@set'); + + // Contacts + Route::apiResource('contacts', 'ApiContactController') + ->names(['index' => 'contacts', 'show' => 'contact']); + Route::post('/me/contact', 'ApiMeController@store'); + Route::delete('/me/contact', 'ApiMeController@destroy'); + + // Contacts properties + Route::put('/contacts/{contact}/work', 'ApiContactController@updateWork'); + Route::put('/contacts/{contact}/food', 'ApiContactController@updateFoodPreferences'); + Route::put('/contacts/{contact}/introduction', 'ApiContactController@updateIntroduction'); + + // Genders + Route::apiResource('genders', 'Account\\ApiGenderController'); + + // Relationships + Route::apiResource('relationships', 'ApiRelationshipController', ['except' => ['index']]) + ->names(['show' => 'relationship']); + Route::get('/contacts/{contact}/relationships', 'ApiRelationshipController@index') + ->name('relationships'); + + // Sets tags + Route::post('/contacts/{contact}/setTags', 'ApiContactTagController@setTags'); + Route::post('/contacts/{contact}/unsetTags', 'ApiContactTagController@unsetTags'); + Route::post('/contacts/{contact}/unsetTag', 'ApiContactTagController@unsetTag'); + + // Places + Route::apiResource('places', 'Account\\ApiPlaceController'); + + // Addresses + Route::apiResource('addresses', 'Contact\\ApiAddressController') + ->names(['index' => 'addresses', 'show' => 'address']); + Route::get('/contacts/{contact}/addresses', 'Contact\\ApiAddressController@addresses'); + + // Contact Fields + Route::apiResource('contactfields', 'ApiContactFieldController', ['except' => ['index']]); + Route::get('/contacts/{contact}/contactfields', 'ApiContactFieldController@contactFields'); + + // Pets + Route::apiResource('pets', 'ApiPetController'); + Route::get('/contacts/{contact}/pets', 'ApiPetController@pets'); + + // Tags + Route::apiResource('tags', 'ApiTagController'); + Route::get('/tags/{tag}/contacts', 'ApiTagController@contacts'); + + // Companies + Route::apiResource('companies', 'Account\\ApiCompanyController'); + + // Occupations + Route::apiResource('occupations', 'Contact\\ApiOccupationController'); + + // Notes + Route::apiResource('notes', 'ApiNoteController') + ->names(['index' => 'notes', 'show' => 'note']); + Route::get('/contacts/{contact}/notes', 'ApiNoteController@notes'); + + // Calls + Route::apiResource('calls', 'Contact\\ApiCallController') + ->names(['index' => 'calls', 'show' => 'call']); + Route::get('/contacts/{contact}/calls', 'Contact\\ApiCallController@calls'); + + // Conversations & messages + Route::apiResource('conversations', 'Contact\\ApiConversationController') + ->names(['index' => 'conversations', 'show' => 'conversation']); + Route::apiResource('conversations/{conversation}/messages', 'Contact\\ApiMessageController', ['except' => ['index', 'show']]); + Route::get('/contacts/{contact}/conversations', 'Contact\\ApiConversationController@conversations'); + + // Activities + Route::apiResource('activities', 'ApiActivitiesController') + ->names(['index' => 'activities', 'show' => 'activity']); + Route::get('/contacts/{contact}/activities', 'ApiActivitiesController@activities'); + + // Reminders + Route::get('reminders/upcoming/{month}', 'ApiReminderController@upcoming'); + Route::apiResource('reminders', 'ApiReminderController') + ->names(['index' => 'reminders']); + Route::get('/contacts/{contact}/reminders', 'ApiReminderController@reminders'); + + // Tasks + Route::apiResource('tasks', 'ApiTaskController'); + Route::get('/contacts/{contact}/tasks', 'ApiTaskController@tasks'); + + // Gifts + Route::apiResource('gifts', 'ApiGiftController'); + Route::get('/contacts/{contact}/gifts', 'ApiGiftController@gifts'); + Route::put('/gifts/{gift}/photo/{photo}', 'ApiGiftController@associate'); + + // Debts + Route::apiResource('debts', 'ApiDebtController'); + Route::get('/contacts/{contact}/debts', 'ApiDebtController@debts'); + + // Journal + Route::apiResource('journal', 'ApiJournalController') + ->names(['index' => 'journal', 'show' => 'entry']); + + // Activity Types + Route::apiResource('activitytypes', 'Account\\Activity\\ApiActivityTypeController'); + + // Activity Type Categories + Route::apiResource('activitytypecategories', 'Account\\Activity\\ApiActivityTypeCategoryController'); + + // Relationship Type Groups + Route::apiResource('relationshiptypegroups', 'ApiRelationshipTypeGroupController', ['only' => ['index', 'show']]); + + // Relationship Types + Route::apiResource('relationshiptypes', 'ApiRelationshipTypeController', ['only' => ['index', 'show']]); + + // Life events + Route::apiResource('lifeevents', 'Contact\\ApiLifeEventController'); + + // Documents + Route::apiResource('documents', 'Contact\\ApiDocumentController', ['except' => ['update']]) + ->names(['index' => 'documents', 'show' => 'document']); + Route::get('/contacts/{contact}/documents', 'Contact\\ApiDocumentController@contact'); + + // Photos + Route::apiResource('photos', 'Contact\\ApiPhotoController', ['except' => ['update']]) + ->names(['index' => 'photos', 'show' => 'photo']); + Route::get('/contacts/{contact}/photos', 'Contact\\ApiPhotoController@contact'); + + // Avatars + Route::put('/contacts/{contact}/avatar', 'Contact\\ApiAvatarController@update'); + + // Contact logs + Route::get('/contacts/{contact}/logs', 'Contact\\ApiAuditLogController@index'); + + /* + * SETTINGS + */ + Route::apiResource('contactfieldtypes', 'Settings\\ApiContactFieldTypeController'); + Route::apiResource('logs', 'Settings\\ApiAuditLogController'); + + /* + * MISC + */ + Route::get('/countries', 'Misc\\ApiCountryController@index')->name('countries'); + }); +}); diff --git a/routes/console.php b/routes/console.php new file mode 100644 index 0000000..71e3f65 --- /dev/null +++ b/routes/console.php @@ -0,0 +1,33 @@ +getStats(); + foreach ($hostStats as $host => $stats) { + $command->line('Host: '.$host); + foreach ($stats as $key => $value) { + $command->line($key.': '.$value); + } + $command->line(''); + } + } +})->purpose('Display memcached statistics'); +// @codeCoverageIgnoreEnd diff --git a/routes/oauth.php b/routes/oauth.php new file mode 100644 index 0000000..7b37855 --- /dev/null +++ b/routes/oauth.php @@ -0,0 +1,17 @@ +group(function () { + Route::get('/login', 'Auth\\OAuthController@index'); + Route::post('/login', 'Auth\\OAuthController@login')->name('oauth.login'); + + Route::middleware(['auth', 'mfa'])->group(function () { + Route::post('/verified', 'Auth\\OAuthController@verify')->name('oauth.verify'); + Route::get('/verified', 'Auth\\OAuthController@verify'); + }); + + Route::middleware(['auth', '2fa'])->group(function () { + Route::post('/validate2fa', '\\App\\Http\\Controllers\\Auth\\Validate2faController@index')->name('oauth.validate2fa'); + }); +}); diff --git a/routes/special.php b/routes/special.php new file mode 100644 index 0000000..936ff2b --- /dev/null +++ b/routes/special.php @@ -0,0 +1,14 @@ +group(function () { + Route::post('/settings/emailchange1', 'Auth\EmailChangeController@login'); +}); + +Route::middleware(['auth', '2fa', 'throttle:5,1'])->group(function () { + Route::get('/settings/emailchange2', 'Auth\EmailChangeController@index'); + Route::post('/settings/emailchange2', 'Auth\EmailChangeController@save'); +}); diff --git a/routes/web.php b/routes/web.php new file mode 100644 index 0000000..487727a --- /dev/null +++ b/routes/web.php @@ -0,0 +1,319 @@ +name('loginRedirect'); + +Auth::routes(['verify' => true]); + +// Redirect .well-known urls (https://en.wikipedia.org/wiki/List_of_/.well-known/_services_offered_by_webservers) +Route::permanentRedirect('/.well-known/carddav', '/dav/'); +Route::permanentRedirect('/.well-known/caldav', '/dav/'); +Route::permanentRedirect('/.well-known/security.txt', '/security.txt'); + +Route::get('/invitations/accept/{key}', 'Auth\InvitationController@show')->name('invitations.accept'); +Route::post('/invitations/accept/{key}', 'Auth\InvitationController@store')->name('invitations.send'); + +Route::middleware(['auth'])->group(function () { + Route::get('/logout', 'Auth\LoginController@logout'); + Route::get('/auth/login-recovery', 'Auth\RecoveryLoginController@get')->name('recovery.login'); + Route::post('/auth/login-recovery', 'Auth\RecoveryLoginController@store'); +}); + +Route::middleware(['auth', '2fa'])->group(function () { + Route::post('/validate2fa', 'Auth\Validate2faController@index')->name('validate2fa'); +}); + +Route::middleware(['auth', 'verified', 'mfa'])->group(function () { + Route::name('dashboard.')->group(function () { + Route::get('/dashboard', 'DashboardController@index')->name('index'); + Route::get('/dashboard/calls', 'DashboardController@calls'); + Route::get('/dashboard/notes', 'DashboardController@notes'); + Route::get('/dashboard/debts', 'DashboardController@debts'); + Route::post('/dashboard/setTab', 'DashboardController@setTab'); + }); + + Route::get('/store/{file}', 'StorageController@show')->where('file', '.*')->name('storage'); + + Route::get('/compliance', 'ComplianceController@index')->name('compliance'); + Route::post('/compliance/sign', 'ComplianceController@store'); + Route::get('/changelog', 'ChangelogController@index')->name('changelog.index'); + + Route::get('/emotions', 'EmotionController@primaries'); + Route::get('/emotions/primaries/{emotion}/secondaries', 'EmotionController@secondaries'); + Route::get('/emotions/primaries/{emotion}/secondaries/{secondaryEmotion}/emotions', 'EmotionController@emotions'); + + Route::post('/me/contact', 'MeController@store'); + Route::delete('/me/contact', 'MeController@destroy'); + + Route::name('people.')->group(function () { + Route::get('/people/notfound', 'ContactsController@missing')->name('missing'); + Route::get('/people/archived', 'ContactsController@archived')->name('archived'); + + // Dashboard + Route::get('/people', 'ContactsController@index')->name('index'); + Route::get('/people/add', 'ContactsController@create')->name('create'); + Route::get('/people/list', 'ContactsController@list')->name('list'); + Route::post('/people', 'ContactsController@store')->name('store'); + Route::get('/people/{contact}', 'ContactsController@show')->name('show'); + Route::get('/people/{contact}/edit', 'ContactsController@edit')->name('edit'); + Route::put('/people/{contact}', 'ContactsController@update')->name('update'); + Route::delete('/people/{contact}', 'ContactsController@destroy')->name('destroy'); + + // Avatar + Route::get('/people/{contact}/avatar', 'Contacts\\AvatarController@edit')->name('avatar.edit'); + Route::post('/people/{contact}/avatar', 'Contacts\\AvatarController@update')->name('avatar.update'); + Route::post('/people/{contact}/makeProfilePicture/{photo}', 'Contacts\\AvatarController@photo')->name('avatar.photo'); + + // Life events + Route::name('lifeevent.')->group(function () { + Route::get('/people/{contact}/lifeevents', 'Contacts\\LifeEventsController@index')->name('index'); + Route::get('/lifeevents/categories', 'Contacts\\LifeEventsController@categories')->name('categories'); + Route::get('/lifeevents/categories/{lifeEventCategory}/types', 'Contacts\\LifeEventsController@types')->name('types'); + Route::post('/people/{contact}/lifeevents', 'Contacts\\LifeEventsController@store')->name('store'); + Route::delete('/lifeevents/{lifeEvent}', 'Contacts\\LifeEventsController@destroy')->name('destroy'); + }); + + // Contact information + Route::get('/people/{contact}/contactfield', 'Contacts\\ContactFieldsController@getContactFields'); + Route::post('/people/{contact}/contactfield', 'Contacts\\ContactFieldsController@storeContactField'); + Route::put('/people/{contact}/contactfield/{contactField}', 'Contacts\\ContactFieldsController@editContactField'); + Route::delete('/people/{contact}/contactfield/{contactField}', 'Contacts\\ContactFieldsController@destroyContactField'); + Route::get('/people/{contact}/contactfieldtypes', 'Contacts\\ContactFieldsController@getContactFieldTypes'); + + // Export as vCard + Route::get('/people/{contact}/vcard', 'ContactsController@vcard')->name('vcard'); + + // Addresses + Route::get('/countries', 'Contacts\\AddressesController@getCountries'); + Route::get('/people/{contact}/addresses', 'Contacts\\AddressesController@index'); + Route::post('/people/{contact}/addresses', 'Contacts\\AddressesController@store'); + Route::put('/people/{contact}/addresses/{address}', 'Contacts\\AddressesController@edit'); + Route::delete('/people/{contact}/addresses/{address}', 'Contacts\\AddressesController@destroy'); + + // Work information + Route::name('work.')->group(function () { + Route::get('/people/{contact}/work/edit', 'ContactsController@editWork')->name('edit'); + Route::post('/people/{contact}/work/update', 'ContactsController@updateWork')->name('update'); + }); + + // Introductions + Route::name('introductions.')->group(function () { + Route::get('/people/{contact}/introductions/edit', 'Contacts\\IntroductionsController@edit')->name('edit'); + Route::post('/people/{contact}/introductions/update', 'Contacts\\IntroductionsController@update')->name('update'); + }); + + // Tags + Route::name('tags.')->group(function () { + Route::get('/tags', 'Contacts\\TagsController@index')->name('index'); + Route::get('/people/{contact}/tags', 'Contacts\\TagsController@get')->name('get'); + Route::post('/people/{contact}/tags/update', 'Contacts\\TagsController@update')->name('update'); + }); + + // Notes + Route::resource('people/{contact}/notes', 'Contacts\\NotesController')->only([ + 'index', 'store', 'update', 'destroy', + ]); + Route::post('/people/{contact}/notes/{note}/toggle', 'Contacts\\NotesController@toggle'); + + // Food preferences + Route::name('food.')->group(function () { + Route::get('/people/{contact}/food', 'ContactsController@editFoodPreferences')->name('index'); + Route::post('/people/{contact}/food/save', 'ContactsController@updateFoodPreferences')->name('update'); + }); + + // Relationships + Route::resource('people/{contact}/relationships', 'Contacts\\RelationshipsController')->only([ + 'create', 'store', 'edit', 'update', 'destroy', + ]); + + // Pets + Route::resource('people/{contact}/pets', 'Contacts\\PetsController')->only([ + 'index', 'store', 'update', 'destroy', + ]); + Route::get('/petcategories', 'Contacts\\PetsController@getPetCategories'); + + // Reminders + Route::resource('people/{contact}/reminders', 'Contacts\\RemindersController')->except(['index', 'show']); + + // Tasks + Route::get('people/{contact}/tasks', 'Contacts\\TasksController@index')->name('tasks.get'); + Route::resource('tasks', 'TasksController')->only([ + 'index', 'store', 'update', 'destroy', + ]); + + // Gifts + Route::resource('people/{contact}/gifts', 'Contacts\\GiftController')->only([ + 'index', 'show', 'store', 'update', 'destroy', + ]); + Route::put('people/{contact}/gifts/{gift}/photo/{photo}', 'Contacts\\GiftController@associate'); + + // Debt + Route::resource('people/{contact}/debts', 'Contacts\\DebtController')->except(['index', 'show']); + + // Phone calls + Route::get('people/{contact}/calls/last', [CallsController::class, 'lastCalled']); + Route::resource('people/{contact}/calls', 'Contacts\\CallsController')->except(['show']); + + // Conversations + Route::resource('people/{contact}/conversations', 'Contacts\\ConversationsController')->except(['show']); + + // Documents + Route::resource('people/{contact}/documents', 'Contacts\\DocumentsController')->only(['index', 'store', 'destroy']); + + // Photos + Route::resource('people/{contact}/photos', 'Contacts\\PhotosController')->only(['index', 'store', 'destroy']); + + // Search + Route::post('/people/search', 'ContactsController@search')->name('search'); + + // Stay in touch information + Route::post('/people/{contact}/stayintouch', 'ContactsController@stayInTouch'); + + // Set favorite + Route::post('/people/{contact}/favorite', 'ContactsController@favorite'); + + // Archive/Unarchive + Route::put('/people/{contact}/archive', 'ContactsController@archive'); + + // Activities + Route::get('/activityCategories', 'Contacts\\ActivitiesController@categories')->name('activities.categories'); + Route::resource('people/{contact}/activities', 'Contacts\\ActivitiesController')->only(['index']); + Route::get('/people/{contact}/activities/contacts', 'Contacts\\ActivitiesController@contacts')->name('activities.contacts'); + Route::get('/people/{contact}/activities/summary', 'Contacts\\ActivitiesController@summary')->name('activities.summary'); + Route::get('/people/{contact}/activities/{year}', 'Contacts\\ActivitiesController@year')->name('activities.year'); + Route::resource('activities', 'Contacts\\ActivitiesController')->only(['store', 'update', 'destroy']); + + // Audit logs + Route::get('/people/{contact}/auditlogs', 'Contacts\\ContactAuditLogController@index')->name('auditlogs'); + }); + + Route::name('journal.')->group(function () { + Route::get('/journal', 'JournalController@index')->name('index'); + Route::get('/journal/entries', 'JournalController@list')->name('list'); + Route::get('/journal/entries/{journalEntry}', 'JournalController@get'); + Route::get('/journal/hasRated', 'JournalController@hasRated'); + Route::post('/journal/day', 'JournalController@storeDay'); + Route::delete('/journal/day/{day}', 'JournalController@trashDay'); + Route::put('/journal/day/{day}/update', 'JournalController@updateDay'); + + Route::get('/journal/add', 'JournalController@create')->name('create'); + Route::post('/journal/create', 'JournalController@save')->name('save'); + Route::get('/journal/entries/{entry}/edit', 'JournalController@edit')->name('edit'); + Route::put('/journal/entries/{entry}', 'JournalController@update')->name('update'); + Route::delete('/journal/{entry}', 'JournalController@deleteEntry'); + }); + + Route::name('settings.')->group(function () { + Route::get('/settings', 'SettingsController@index')->name('index'); + Route::post('/settings/delete', 'SettingsController@delete')->name('delete'); + Route::post('/settings/reset', 'SettingsController@reset')->name('reset'); + Route::post('/settings/save', 'SettingsController@save')->name('save'); + + Route::name('personalization.')->group(function () { + Route::get('/settings/personalization', 'Settings\\PersonalizationController@index')->name('index'); + Route::get('/settings/personalization/contactfieldtypes', 'Settings\\PersonalizationController@getContactFieldTypes'); + Route::post('/settings/personalization/contactfieldtypes', 'Settings\\PersonalizationController@storeContactFieldType'); + Route::put('/settings/personalization/contactfieldtypes/{contactFieldType}', 'Settings\\PersonalizationController@editContactFieldType'); + Route::delete('/settings/personalization/contactfieldtypes/{contactFieldType}', 'Settings\\PersonalizationController@destroyContactFieldType'); + + Route::apiResource('settings/personalization/genders', 'Settings\\GendersController'); + Route::delete('/settings/personalization/genders/{gender}/replaceby/{genderToReplaceWith}', 'Settings\\GendersController@destroyAndReplaceGender'); + Route::get('/settings/personalization/genderTypes', 'Settings\\GendersController@types'); + Route::put('/settings/personalization/genders/default/{gender}', 'Settings\\GendersController@updateDefault'); + + Route::get('/settings/personalization/reminderrules', 'Settings\\ReminderRulesController@index'); + Route::post('/settings/personalization/reminderrules/{reminderRule}', 'Settings\\ReminderRulesController@toggle'); + + Route::get('/settings/personalization/modules', 'Settings\\ModulesController@index'); + Route::post('/settings/personalization/modules/{module}', 'Settings\\ModulesController@toggle'); + + Route::apiResource('settings/personalization/activitytypecategories', 'Account\\Activity\\ActivityTypeCategoriesController'); + Route::apiResource('settings/personalization/activitytypes', 'Account\\Activity\\ActivityTypesController', ['except' => ['index']]); + + Route::get('settings/personalization/lifeeventcategories', 'Account\\LifeEvent\\LifeEventCategoriesController@index'); + Route::apiResource('settings/personalization/lifeeventtypes', 'Account\\LifeEvent\\LifeEventTypesController', ['except' => ['index']]); + }); + + Route::get('/settings/export', 'Settings\\ExportController@index')->name('export.index'); + Route::post('/settings/exportToSql', 'Settings\\ExportController@storeSQL')->name('export.store.sql'); + Route::post('/settings/exportToJson', 'Settings\\ExportController@storeJson')->name('export.store.json'); + Route::post('/settings/export/{uuid}', 'Settings\\ExportController@download')->name('export.download'); + + Route::get('/settings/import', 'SettingsController@import')->name('import'); + Route::get('/settings/import/report/{importjobid}', 'SettingsController@report')->name('report'); + Route::get('/settings/import/upload', 'SettingsController@upload')->name('upload'); + Route::post('/settings/import/storeImport', 'SettingsController@storeImport')->name('storeImport'); + + Route::name('users.')->group(function () { + Route::get('/settings/users', 'SettingsController@users')->name('index'); + Route::get('/settings/users/create', 'SettingsController@addUser')->name('create'); + Route::post('/settings/users', 'SettingsController@inviteUser')->name('store'); + Route::delete('/settings/users/{user}', 'SettingsController@deleteAdditionalUser')->name('destroy'); + Route::delete('/settings/users/invitations/{invitation}', 'SettingsController@destroyInvitation')->name('invitation.delete'); + }); + + Route::name('storage.')->group(function () { + Route::get('/settings/storage', 'Settings\\StorageController@index')->name('index'); + }); + + Route::name('subscriptions.')->group(function () { + Route::get('/settings/subscriptions', 'Settings\\SubscriptionsController@index')->name('index'); + Route::get('/settings/subscriptions/upgrade', 'Settings\\SubscriptionsController@upgrade')->name('upgrade'); + Route::get('/settings/subscriptions/upgrade/success', 'Settings\\SubscriptionsController@upgradeSuccess')->name('upgrade.success'); + Route::get('/settings/subscriptions/update', 'Settings\\SubscriptionsController@update')->name('update'); + Route::post('/settings/subscriptions/update', 'Settings\\SubscriptionsController@processUpdate'); + Route::get('/settings/subscriptions/confirmPayment/{id}', 'Settings\\SubscriptionsController@confirmPayment')->name('confirm'); + Route::post('/settings/subscriptions/processPayment', 'Settings\\SubscriptionsController@processPayment')->name('payment'); + Route::get('/settings/subscriptions/invoice/{invoice}', 'Settings\\SubscriptionsController@downloadInvoice')->name('invoice'); + Route::get('/settings/subscriptions/downgrade', 'Settings\\SubscriptionsController@downgrade')->name('downgrade'); + Route::post('/settings/subscriptions/downgrade', 'Settings\\SubscriptionsController@processDowngrade'); + Route::get('/settings/subscriptions/archive', 'Settings\\SubscriptionsController@archive')->name('archive'); + Route::post('/settings/subscriptions/archive', 'Settings\\SubscriptionsController@processArchive'); + Route::get('/settings/subscriptions/downgrade/success', 'Settings\\SubscriptionsController@downgradeSuccess')->name('downgrade.success'); + if (! App::environment('production')) { + Route::get('/settings/subscriptions/forceCompletePaymentOnTesting', 'Settings\\SubscriptionsController@forceCompletePaymentOnTesting')->name('forceCompletePaymentOnTesting'); + } + }); + + Route::get('/settings/auditlogs', 'Settings\\AuditLogController@index')->name('auditlog.index'); + + Route::name('tags.')->group(function () { + Route::get('/settings/tags', 'SettingsController@tags')->name('index'); + Route::put('/settings/tags/{tag}', 'SettingsController@editTag')->name('update'); + Route::delete('/settings/tags/{tag}', 'SettingsController@deleteTag')->name('delete'); + }); + + Route::get('/settings/api', 'SettingsController@api')->name('api'); + Route::get('/settings/dav', 'SettingsController@dav')->name('dav'); + + Route::post('/settings/updateDefaultProfileView', 'SettingsController@updateDefaultProfileView'); + + // Security + Route::name('security.')->group(function () { + Route::get('/settings/security', 'SettingsController@security')->name('index'); + Route::post('/settings/security/passwordChange', 'Auth\\PasswordChangeController@passwordChange')->name('passwordChange'); + Route::get('/settings/security/2fa-enable', 'Settings\\MultiFAController@enableTwoFactor')->name('2fa-enable'); + Route::post('/settings/security/2fa-enable', 'Settings\\MultiFAController@validateTwoFactor'); + Route::get('/settings/security/2fa-disable', 'Settings\\MultiFAController@disableTwoFactor')->name('2fa-disable'); + Route::post('/settings/security/2fa-disable', 'Settings\\MultiFAController@deactivateTwoFactor'); + + Route::post('/settings/security/generate-recovery-codes', 'Settings\\RecoveryCodesController@store'); + Route::post('/settings/security/recovery-codes', 'Settings\\RecoveryCodesController@index'); + }); + }); +}); diff --git a/scripts/.htaccess_production b/scripts/.htaccess_production new file mode 100644 index 0000000..324b818 --- /dev/null +++ b/scripts/.htaccess_production @@ -0,0 +1,54 @@ + + + Options -MultiViews -Indexes + + + RewriteEngine On + + # Redirect to https + RewriteCond %{HTTP:X-Forwarded-Proto} !=https + RewriteRule .* https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301,N] + + + # Activate HSTS + Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload;" + + Header always set Referrer-Policy "no-referrer" + Header always set X-Content-Type-Options "nosniff" + Header always set X-Download-Options "noopen" + Header always set X-Frame-Options "SAMEORIGIN" + Header always set X-Permitted-Cross-Domain-Policies "none" + Header always set X-Robots-Tag "none" + Header always set X-XSS-Protection "1; mode=block" + + # Assets expire after 1 month + + Header set Cache-Control "public, max-age=2628000" + + + + # Handle Authorization Header + RewriteCond %{HTTP:Authorization} . + RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] + + # Redirect .well-known urls (https://en.wikipedia.org/wiki/List_of_/.well-known/_services_offered_by_webservers) + RewriteCond %{REQUEST_URI} .well-known/carddav + RewriteRule ^ /dav/ [L,R=301,N] + + RewriteCond %{REQUEST_URI} .well-known/caldav + RewriteRule ^ /dav/ [L,R=301,N] + + RewriteCond %{REQUEST_URI} .well-known/security.txt + RewriteRule ^ /security.txt [L,R=301,N] + + # Redirect Trailing Slashes If Not A Folder... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !dav/* + RewriteCond %{REQUEST_URI} (.+)/$ + RewriteRule ^ %1 [L,R=301] + + # Handle Front Controller... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^ index.php [L] + diff --git a/scripts/ci/.env.mysql b/scripts/ci/.env.mysql new file mode 100644 index 0000000..377603a --- /dev/null +++ b/scripts/ci/.env.mysql @@ -0,0 +1,28 @@ +# ENV FILE FOR THE GITHUB ACTION RUNNING MYSQL +APP_ENV=testing +APP_KEY=base64:NTrXToqFZJlv48dgPc+kNpc3SBt333TfDnF1mDShsBg= +APP_DEBUG=true +APP_URL=http://localhost:8000 + +DB_CONNECTION=mysql +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_DATABASE=monica +DB_USERNAME=root +DB_PASSWORD=root +DB_USE_UTF8MB4=true + +BROADCAST_DRIVER=log +LOG_CHANNEL=testing + +# Values from phpunit.xml +DEBUGBAR_ENABLED=false +CACHE_DRIVER=array +CHECK_VERSION=false +QUEUE_CONNECTION=sync +SESSION_DRIVER=file +MAIL_MAILER=array + +MAIL_FROM_ADDRESS= +MAIL_FROM_NAME= +APP_EMAIL_NEW_USERS_NOTIFICATION= diff --git a/scripts/ci/.env.postgres b/scripts/ci/.env.postgres new file mode 100644 index 0000000..83b0e11 --- /dev/null +++ b/scripts/ci/.env.postgres @@ -0,0 +1,17 @@ +APP_ENV=testing +APP_KEY=base64:NTrXToqFZJlv48dgPc+kNpc3SBt333TfDnF1mDShsBg= +APP_URL=http://localhost:8000 + +DB_CONNECTION=pgsqltesting +DB_TEST_HOST=127.0.0.1 +DB_TEST_DATABASE=monica +DB_TEST_USERNAME=postgres +DB_TEST_PASSWORD= + +CACHE_DRIVER=array +SESSION_DRIVER=file +QUEUE_CONNECTION=sync +LOG_CHANNEL=testing + +MFA_ENABLED=true +DAV_ENABLED=true diff --git a/scripts/ci/package.sh b/scripts/ci/package.sh new file mode 100644 index 0000000..1918418 --- /dev/null +++ b/scripts/ci/package.sh @@ -0,0 +1,101 @@ +#!/bin/bash + +set -eo pipefail + +SELF_PATH=$(cd -P -- "$(dirname -- "$0")" && /bin/pwd -P) +source $SELF_PATH/../realpath.sh +ROOT=$(realpath $SELF_PATH/../..) + +version=$1 +if [ -z "$version" ]; then + echo "Version parameter is mandatory" >&2 + exit 1 +fi + +commit=$2 +if [ -z "$commit" ]; then + commit=$(git --git-dir $ROOT/.git log --pretty="%H" -n1 HEAD) + release=$(git --git-dir $ROOT/.git log --pretty="%h" -n1 HEAD) +fi + +set -v + +echo -n "$version" | tee $ROOT/config/.version + +echo -n $commit | tee $ROOT/config/.commit + +echo -n ${release:-$version} | tee $ROOT/config/.release + + +# BUILD +composer install --no-progress --no-interaction --prefer-dist --optimize-autoloader --no-dev --working-dir=$ROOT +yarn --cwd $ROOT run inst +yarn --cwd $ROOT run production + + +# PACKAGE +package=monica-$version +mkdir -p $package/database +ln -s $ROOT/.env.example $package/ +ln -s $ROOT/app.json $package/ +ln -s $ROOT/artisan $package/ +ln -s $ROOT/CHANGELOG.md $package/ +ln -s $ROOT/CONTRIBUTING.md $package/ +ln -s $ROOT/CONTRIBUTORS $package/ +ln -s $ROOT/composer.json $package/ +ln -s $ROOT/composer.lock $package/ +ln -s $ROOT/LICENSE.md $package/ +ln -s $ROOT/nginx_app.conf $package/ +ln -s $ROOT/package.json $package/ +ln -s $ROOT/Procfile $package/ +ln -s $ROOT/README.md $package/ +ln -s $ROOT/server.php $package/ +ln -s $ROOT/webpack.mix.js $package/ +ln -s $ROOT/yarn.lock $package/ +ln -s $ROOT/app $package/ +ln -s $ROOT/bootstrap $package/ +ln -s $ROOT/config $package/ +ln -s $ROOT/docs $package/ +ln -s $ROOT/public $package/ +ln -s $ROOT/resources $package/ +ln -s $ROOT/routes $package/ +ln -s $ROOT/vendor $package/ + +ln -s $ROOT/database/factories $package/database/ +ln -s $ROOT/database/migrations $package/database/ +ln -s $ROOT/database/seeds $package/database/ + +mkdir -p $package/storage/app/public +mkdir -p $package/storage/logs +mkdir -p $package/storage/framework/cache +mkdir -p $package/storage/framework/views +mkdir -p $package/storage/framework/sessions + +tar chfj $package.tar.bz2 --exclude .gitignore --exclude .gitkeep $package +sha512sum "$package.tar.bz2" > "$package.tar.bz2.sha512" + +echo "package=$package.tar.bz2" >> $GITHUB_OUTPUT + + +# ASSETS +assets=monica-assets-$version +mkdir -p $assets/public +ln -s $ROOT/public/mix-manifest.json $assets/public/ +ln -s $ROOT/public/js $assets/public/ +ln -s $ROOT/public/css $assets/public/ +ln -s $ROOT/public/fonts $assets/public/ + +tar chfj $assets.tar.bz2 --exclude .gitignore --exclude .gitkeep $assets +sha512sum "$assets.tar.bz2" > "$assets.tar.bz2.sha512" + +echo "assets=$assets.tar.bz2" >> $GITHUB_OUTPUT + + +# SIGN +if [ -n "${GPG_FINGERPRINT:-}" -a -n "${GPG_PASSPHRASE:-}" ]; then + for f in {$package,$assets}.tar.bz2{,.sha512}; do + echo "Signing '$f'..." + echo "$GPG_PASSPHRASE" | gpg --batch --yes --passphrase-fd 0 --pinentry-mode=loopback --local-user $GPG_FINGERPRINT --sign --armor --detach-sig --output "$f.asc" "$f" + echo -e "\nSigned with key fingerprint $GPG_FINGERPRINT" >> "$f.asc" + done +fi diff --git a/scripts/ci/phpunitpostgres.xml b/scripts/ci/phpunitpostgres.xml new file mode 100644 index 0000000..524a18f --- /dev/null +++ b/scripts/ci/phpunitpostgres.xml @@ -0,0 +1,55 @@ + + + + + ./tests/Unit/Models + + + + ./tests/Unit/Helpers + + + + ./tests/Feature + + + + ./tests/Api + + + + ./tests/Commands + + + + + ./app + + ./app/Http/routes.php + + + + + + + + + + + + + + + + + diff --git a/scripts/ci/update-assets.sh b/scripts/ci/update-assets.sh new file mode 100644 index 0000000..71495c2 --- /dev/null +++ b/scripts/ci/update-assets.sh @@ -0,0 +1,106 @@ +#!/bin/bash + +if [ "$CIRCLECI" == "true" ]; then + if [[ ! -z $CIRCLE_PULL_REQUEST ]] ; then + CIRCLE_PR_NUMBER="${CIRCLE_PR_NUMBER:-${CIRCLE_PULL_REQUEST##*/}}" + REPO=$CIRCLE_PULL_REQUEST + REPO=${REPO##https://github.com/} + REPO=${REPO%%/pull/$CIRCLE_PR_NUMBER} + else + REPO=$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME + fi + BRANCH=${CIRCLE_BRANCH:-$CIRCLE_TAG} + PR_NUMBER=${CIRCLE_PR_NUMBER:-false} +elif [ "$TRAVIS" == "true" ]; then + REPO=$TRAVIS_REPO_SLUG + BRANCH=${TRAVIS_PULL_REQUEST_BRANCH:-$TRAVIS_BRANCH} + PR_NUMBER=$TRAVIS_PULL_REQUEST +elif [ "$TF_BUILD" == "True" ]; then + REPO=$BUILD_REPOSITORY_NAME + BRANCH=${SYSTEM_PULLREQUEST_SOURCEBRANCH:-$BUILD_SOURCEBRANCHNAME} + PR_NUMBER=${SYSTEM_PULLREQUEST_PULLREQUESTNUMBER:-false} +elif [[ -n $BUILD_NUMBER ]]; then + echo "CHANGE_ID=$CHANGE_ID" + echo "CHANGE_URL=$CHANGE_URL" + REPO=${CHANGE_URL##https://github.com/} + if [[ ! -z $CHANGE_ID ]] ; then + REPO=${REPO%%/pull/$CHANGE_ID} + fi + PR_NUMBER=${CHANGE_ID:-false} + BRANCH=$BRANCH_NAME +fi + +REPOSITORY_OWNER=monicahq/monica + +set -euo pipefail + +# Update assets +echo -e "\033[1;32m# Build assets ...\033[0;37m" +echo -e "\033[1;36myarn run production\033[0;37m" +yarn run production +echo "" + +# Check if there is zero update needed +status=$(git status --porcelain) +if [ "$status" = "" ]; then + echo "Nothing to push, already up to date." + exit 0; +fi +echo "Waiting modifications:" +echo $status + +# Add files +git add public/mix-manifest.json +git add public/js/* +git add public/css/* +git add public/fonts/* + +# Commit +if [ -z "${ASSETS_USERNAME:-}" ]; then + #No username + echo -e "\033[0;31mMonica asset are not up to date.\033[0;37m" + echo "Please update the Monica assets yourself by running:" + echo " ~ yarn run production" + exit 2 +fi +git config user.email $ASSETS_EMAIL +git config user.name $ASSETS_USERNAME +git commit -m "chore(assets): Update assets" + +# Push +if [ "$BRANCH" == "main" ] && [ "$PR_NUMBER" == "false" ]; then + echo -e "\033[0;31mmain branch is not up to date, but we can't update it directly...\033[0;37m" + exit 0 + +elif [ -n "${ASSETS_GITHUB_TOKEN:-}" ]; then + REPOS_VALUES=($(curl -H "Authorization: token $ASSETS_GITHUB_TOKEN" -sSL https://api.github.com/repos/$REPO/pulls/$PR_NUMBER | jq -r -c ".head.repo.full_name, .head.ref")) + + PULL_REQUEST_BRANCH= + PULL_REQUEST_REPOSITORY=${REPOS_VALUES[0]} + PULL_REQUEST_HEADBRANCH=${REPOS_VALUES[1]} + + if [ -z "${PULL_REQUEST_REPOSITORY:-}" ] || [ "$PULL_REQUEST_REPOSITORY" == "null" ]; then + echo -e "\033[0;31mError with github api call\033[0;37m" + exit 1 + elif [ "$PULL_REQUEST_REPOSITORY" == "$REPOSITORY_OWNER" ]; then + PULL_REQUEST_BRANCH=$PULL_REQUEST_HEADBRANCH + else + echo -e "\033[0;31mMonica asset are not up to date.\033[0;37m" + echo "We can't commit in $PULL_REQUEST_REPOSITORY to update them directly." + echo "Please update the Monica assets yourself by running:" + echo " ~ yarn run production" + exit 2 + fi + + echo "Pushing files to $PULL_REQUEST_BRANCH branch ..." + remote=monica-origin + remoteurl="https://$ASSETS_USERNAME:$ASSETS_GITHUB_TOKEN@github.com/$REPO" + git remote remove $remote || true + git remote add -f $remote $remoteurl || true + git push $remote HEAD:$PULL_REQUEST_BRANCH + + # Exit with error to stop the current build + echo "...pushed files successfully." + echo "Exit with error to stop the current build." + exit -1 +fi diff --git a/scripts/ci/upload-release-asset.sh b/scripts/ci/upload-release-asset.sh new file mode 100644 index 0000000..cf1bb4b --- /dev/null +++ b/scripts/ci/upload-release-asset.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +file=$1 +release=$2 + +curl -H "Authorization: token $GITHUB_TOKEN" \ + -H "Content-Type: application/octet-stream" \ + -X POST \ + --data-binary @"$file" \ + "https://uploads.github.com/repos/monicahq/monica/releases/$release/assets?name=$(basename $file)" diff --git a/scripts/database.test.sql b/scripts/database.test.sql new file mode 100644 index 0000000..38eee76 --- /dev/null +++ b/scripts/database.test.sql @@ -0,0 +1,1874 @@ + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!50503 SET NAMES utf8mb4 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `accounts` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `has_access_to_paid_version_for_free` tinyint(1) NOT NULL DEFAULT '0', + `api_key` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `number_of_invitations_sent` int DEFAULT NULL, + `default_time_reminder_is_sent` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '12:00', + `default_gender_id` int unsigned DEFAULT NULL, + `stripe_id` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `card_brand` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `card_last_four` varchar(4) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `trial_ends_at` timestamp NULL DEFAULT NULL, + `legacy_free_plan_unlimited_contacts` tinyint(1) NOT NULL DEFAULT '0', + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `accounts_default_gender_id_foreign` (`default_gender_id`), + KEY `accounts_stripe_id_index` (`stripe_id`), + CONSTRAINT `accounts_default_gender_id_foreign` FOREIGN KEY (`default_gender_id`) REFERENCES `genders` (`id`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `accounts` DISABLE KEYS */; +/*!40000 ALTER TABLE `accounts` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `activities` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `account_id` int unsigned NOT NULL, + `activity_type_id` int unsigned DEFAULT NULL, + `summary` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `description` longtext COLLATE utf8mb4_unicode_ci, + `happened_at` date NOT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `activities_account_id_foreign` (`account_id`), + KEY `activities_activity_type_id_foreign` (`activity_type_id`), + CONSTRAINT `activities_account_id_foreign` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE, + CONSTRAINT `activities_activity_type_id_foreign` FOREIGN KEY (`activity_type_id`) REFERENCES `activity_types` (`id`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `activities` DISABLE KEYS */; +/*!40000 ALTER TABLE `activities` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `activity_contact` ( + `activity_id` int unsigned NOT NULL, + `contact_id` int unsigned NOT NULL, + `account_id` int unsigned NOT NULL, + KEY `activity_contact_activity_id_foreign` (`activity_id`), + KEY `activity_contact_contact_id_foreign` (`contact_id`), + KEY `activity_contact_account_id_foreign` (`account_id`), + CONSTRAINT `activity_contact_account_id_foreign` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE, + CONSTRAINT `activity_contact_activity_id_foreign` FOREIGN KEY (`activity_id`) REFERENCES `activities` (`id`) ON DELETE CASCADE, + CONSTRAINT `activity_contact_contact_id_foreign` FOREIGN KEY (`contact_id`) REFERENCES `contacts` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `activity_contact` DISABLE KEYS */; +/*!40000 ALTER TABLE `activity_contact` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `activity_statistics` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `account_id` int unsigned NOT NULL, + `contact_id` int unsigned NOT NULL, + `year` int NOT NULL, + `count` int NOT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `activity_statistics_account_id_foreign` (`account_id`), + KEY `activity_statistics_contact_id_foreign` (`contact_id`), + CONSTRAINT `activity_statistics_account_id_foreign` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE, + CONSTRAINT `activity_statistics_contact_id_foreign` FOREIGN KEY (`contact_id`) REFERENCES `contacts` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `activity_statistics` DISABLE KEYS */; +/*!40000 ALTER TABLE `activity_statistics` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `activity_type_categories` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `account_id` int unsigned NOT NULL, + `name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `translation_key` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `activity_type_categories_account_id_foreign` (`account_id`), + CONSTRAINT `activity_type_categories_account_id_foreign` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `activity_type_categories` DISABLE KEYS */; +/*!40000 ALTER TABLE `activity_type_categories` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `activity_types` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `account_id` int unsigned NOT NULL, + `activity_type_category_id` int unsigned NOT NULL, + `name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `translation_key` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `location_type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `activity_types_account_id_foreign` (`account_id`), + KEY `activity_types_activity_type_category_id_foreign` (`activity_type_category_id`), + CONSTRAINT `activity_types_account_id_foreign` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE, + CONSTRAINT `activity_types_activity_type_category_id_foreign` FOREIGN KEY (`activity_type_category_id`) REFERENCES `activity_type_categories` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `activity_types` DISABLE KEYS */; +/*!40000 ALTER TABLE `activity_types` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `address_contact_field_label` ( + `contact_field_label_id` bigint unsigned NOT NULL, + `address_id` int unsigned NOT NULL, + `account_id` int unsigned NOT NULL, + KEY `address_contact_field_label_index` (`contact_field_label_id`,`address_id`,`account_id`), + KEY `address_contact_field_label_address_id_foreign` (`address_id`), + KEY `address_contact_field_label_account_id_foreign` (`account_id`), + CONSTRAINT `address_contact_field_label_account_id_foreign` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE, + CONSTRAINT `address_contact_field_label_address_id_foreign` FOREIGN KEY (`address_id`) REFERENCES `addresses` (`id`) ON DELETE CASCADE, + CONSTRAINT `address_contact_field_label_contact_field_label_id_foreign` FOREIGN KEY (`contact_field_label_id`) REFERENCES `contact_field_labels` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `address_contact_field_label` DISABLE KEYS */; +/*!40000 ALTER TABLE `address_contact_field_label` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `addresses` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `account_id` int unsigned NOT NULL, + `place_id` int unsigned DEFAULT NULL, + `contact_id` int unsigned NOT NULL, + `name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `addresses_account_id_foreign` (`account_id`), + KEY `addresses_contact_id_foreign` (`contact_id`), + KEY `addresses_place_id_foreign` (`place_id`), + CONSTRAINT `addresses_account_id_foreign` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE, + CONSTRAINT `addresses_contact_id_foreign` FOREIGN KEY (`contact_id`) REFERENCES `contacts` (`id`) ON DELETE CASCADE, + CONSTRAINT `addresses_place_id_foreign` FOREIGN KEY (`place_id`) REFERENCES `places` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `addresses` DISABLE KEYS */; +/*!40000 ALTER TABLE `addresses` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `api_usage` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `url` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `method` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `client_ip` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `api_usage` DISABLE KEYS */; +/*!40000 ALTER TABLE `api_usage` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `audit_logs` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `account_id` int unsigned NOT NULL, + `author_id` int unsigned DEFAULT NULL, + `about_contact_id` int unsigned DEFAULT NULL, + `author_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `action` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `objects` text COLLATE utf8mb4_unicode_ci NOT NULL, + `audited_at` datetime NOT NULL, + `should_appear_on_dashboard` tinyint(1) NOT NULL DEFAULT '0', + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `audit_logs_account_id_foreign` (`account_id`), + KEY `audit_logs_author_id_foreign` (`author_id`), + KEY `audit_logs_about_contact_id_foreign` (`about_contact_id`), + CONSTRAINT `audit_logs_about_contact_id_foreign` FOREIGN KEY (`about_contact_id`) REFERENCES `contacts` (`id`) ON DELETE SET NULL, + CONSTRAINT `audit_logs_account_id_foreign` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE, + CONSTRAINT `audit_logs_author_id_foreign` FOREIGN KEY (`author_id`) REFERENCES `users` (`id`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `audit_logs` DISABLE KEYS */; +/*!40000 ALTER TABLE `audit_logs` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `cache` ( + `key` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `value` text COLLATE utf8mb4_unicode_ci NOT NULL, + `expiration` int NOT NULL, + UNIQUE KEY `cache_key_unique` (`key`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `cache` DISABLE KEYS */; +/*!40000 ALTER TABLE `cache` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `calls` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `account_id` int unsigned NOT NULL, + `contact_id` int unsigned NOT NULL, + `called_at` datetime NOT NULL, + `content` mediumtext COLLATE utf8mb4_unicode_ci, + `contact_called` tinyint(1) NOT NULL DEFAULT '0', + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `calls_account_id_foreign` (`account_id`), + KEY `calls_contact_id_foreign` (`contact_id`), + CONSTRAINT `calls_account_id_foreign` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE, + CONSTRAINT `calls_contact_id_foreign` FOREIGN KEY (`contact_id`) REFERENCES `contacts` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `calls` DISABLE KEYS */; +/*!40000 ALTER TABLE `calls` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `companies` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `account_id` int unsigned NOT NULL, + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `website` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `number_of_employees` int DEFAULT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `companies_account_id_foreign` (`account_id`), + CONSTRAINT `companies_account_id_foreign` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `companies` DISABLE KEYS */; +/*!40000 ALTER TABLE `companies` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `contact_field_contact_field_label` ( + `contact_field_label_id` bigint unsigned NOT NULL, + `contact_field_id` int unsigned NOT NULL, + `account_id` int unsigned NOT NULL, + KEY `contact_field_contact_field_label_index` (`contact_field_label_id`,`contact_field_id`,`account_id`), + KEY `contact_field_contact_field_label_contact_field_id_foreign` (`contact_field_id`), + KEY `contact_field_contact_field_label_account_id_foreign` (`account_id`), + CONSTRAINT `contact_field_contact_field_label_account_id_foreign` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE, + CONSTRAINT `contact_field_contact_field_label_contact_field_id_foreign` FOREIGN KEY (`contact_field_id`) REFERENCES `contact_fields` (`id`) ON DELETE CASCADE, + CONSTRAINT `contact_field_contact_field_label_contact_field_label_id_foreign` FOREIGN KEY (`contact_field_label_id`) REFERENCES `contact_field_labels` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `contact_field_contact_field_label` DISABLE KEYS */; +/*!40000 ALTER TABLE `contact_field_contact_field_label` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `contact_field_labels` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `account_id` int unsigned NOT NULL, + `label_i18n` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `label` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `contact_field_labels_label_i18n_account_id_index` (`label_i18n`,`account_id`), + KEY `contact_field_labels_label_account_id_index` (`label`,`account_id`), + KEY `contact_field_labels_account_id_foreign` (`account_id`), + CONSTRAINT `contact_field_labels_account_id_foreign` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `contact_field_labels` DISABLE KEYS */; +/*!40000 ALTER TABLE `contact_field_labels` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `contact_field_types` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `account_id` int unsigned NOT NULL, + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `fontawesome_icon` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `protocol` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `delible` tinyint(1) NOT NULL DEFAULT '1', + `type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `contact_field_types_account_id_foreign` (`account_id`), + CONSTRAINT `contact_field_types_account_id_foreign` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `contact_field_types` DISABLE KEYS */; +/*!40000 ALTER TABLE `contact_field_types` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `contact_fields` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `account_id` int unsigned NOT NULL, + `contact_id` int unsigned NOT NULL, + `contact_field_type_id` int unsigned NOT NULL, + `data` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `contact_fields_account_id_foreign` (`account_id`), + KEY `contact_fields_contact_id_foreign` (`contact_id`), + KEY `contact_fields_contact_field_type_id_foreign` (`contact_field_type_id`), + CONSTRAINT `contact_fields_account_id_foreign` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE, + CONSTRAINT `contact_fields_contact_field_type_id_foreign` FOREIGN KEY (`contact_field_type_id`) REFERENCES `contact_field_types` (`id`) ON DELETE CASCADE, + CONSTRAINT `contact_fields_contact_id_foreign` FOREIGN KEY (`contact_id`) REFERENCES `contacts` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `contact_fields` DISABLE KEYS */; +/*!40000 ALTER TABLE `contact_fields` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `contact_photo` ( + `contact_id` int unsigned NOT NULL, + `photo_id` int unsigned NOT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + KEY `contact_photo_photo_id_foreign` (`photo_id`), + KEY `contact_photo_contact_id_foreign` (`contact_id`), + CONSTRAINT `contact_photo_contact_id_foreign` FOREIGN KEY (`contact_id`) REFERENCES `contacts` (`id`) ON DELETE CASCADE, + CONSTRAINT `contact_photo_photo_id_foreign` FOREIGN KEY (`photo_id`) REFERENCES `photos` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `contact_photo` DISABLE KEYS */; +/*!40000 ALTER TABLE `contact_photo` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `contact_tag` ( + `contact_id` int unsigned NOT NULL, + `tag_id` int unsigned NOT NULL, + `account_id` int unsigned NOT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + KEY `contact_tag_account_id_foreign` (`account_id`), + KEY `contact_tag_contact_id_foreign` (`contact_id`), + KEY `contact_tag_tag_id_foreign` (`tag_id`), + CONSTRAINT `contact_tag_account_id_foreign` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE, + CONSTRAINT `contact_tag_contact_id_foreign` FOREIGN KEY (`contact_id`) REFERENCES `contacts` (`id`) ON DELETE CASCADE, + CONSTRAINT `contact_tag_tag_id_foreign` FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `contact_tag` DISABLE KEYS */; +/*!40000 ALTER TABLE `contact_tag` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `contacts` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `account_id` int unsigned NOT NULL, + `first_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `middle_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `last_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `nickname` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `gender_id` int DEFAULT NULL, + `description` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `uuid` char(36) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `is_starred` tinyint(1) NOT NULL DEFAULT '0', + `is_partial` tinyint(1) NOT NULL DEFAULT '0', + `is_active` tinyint(1) NOT NULL DEFAULT '1', + `is_dead` tinyint(1) NOT NULL DEFAULT '0', + `deceased_special_date_id` int unsigned DEFAULT NULL, + `deceased_reminder_id` int unsigned DEFAULT NULL, + `last_talked_to` date DEFAULT NULL, + `stay_in_touch_frequency` int DEFAULT NULL, + `stay_in_touch_trigger_date` datetime DEFAULT NULL, + `birthday_special_date_id` int unsigned DEFAULT NULL, + `birthday_reminder_id` int unsigned DEFAULT NULL, + `first_met_through_contact_id` int DEFAULT NULL, + `first_met_special_date_id` int unsigned DEFAULT NULL, + `first_met_reminder_id` int unsigned DEFAULT NULL, + `first_met_where` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `first_met_additional_info` longtext COLLATE utf8mb4_unicode_ci, + `job` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `company` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `food_preferences` longtext COLLATE utf8mb4_unicode_ci, + `avatar_source` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'default', + `avatar_gravatar_url` varchar(250) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `avatar_adorable_uuid` char(36) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `avatar_adorable_url` varchar(250) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `avatar_default_url` varchar(250) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `avatar_photo_id` int unsigned DEFAULT NULL, + `has_avatar` tinyint(1) NOT NULL DEFAULT '0', + `avatar_external_url` varchar(400) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `avatar_file_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `avatar_location` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'local', + `gravatar_url` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `last_consulted_at` timestamp NULL DEFAULT NULL, + `number_of_views` int NOT NULL DEFAULT '0', + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + `default_avatar_color` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `has_avatar_bool` tinyint(1) NOT NULL DEFAULT '0', + PRIMARY KEY (`id`), + KEY `contacts_birthday_reminder_id_foreign` (`birthday_reminder_id`), + KEY `contacts_first_met_reminder_id_foreign` (`first_met_reminder_id`), + KEY `contacts_deceased_reminder_id_foreign` (`deceased_reminder_id`), + KEY `contacts_birthday_special_date_id_foreign` (`birthday_special_date_id`), + KEY `contacts_first_met_special_date_id_foreign` (`first_met_special_date_id`), + KEY `contacts_deceased_special_date_id_foreign` (`deceased_special_date_id`), + KEY `contacts_account_id_uuid_index` (`account_id`,`uuid`), + KEY `contacts_avatar_photo_id_foreign` (`avatar_photo_id`), + CONSTRAINT `contacts_account_id_foreign` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE, + CONSTRAINT `contacts_avatar_photo_id_foreign` FOREIGN KEY (`avatar_photo_id`) REFERENCES `photos` (`id`) ON DELETE SET NULL, + CONSTRAINT `contacts_birthday_reminder_id_foreign` FOREIGN KEY (`birthday_reminder_id`) REFERENCES `reminders` (`id`) ON DELETE SET NULL, + CONSTRAINT `contacts_birthday_special_date_id_foreign` FOREIGN KEY (`birthday_special_date_id`) REFERENCES `special_dates` (`id`) ON DELETE SET NULL, + CONSTRAINT `contacts_deceased_reminder_id_foreign` FOREIGN KEY (`deceased_reminder_id`) REFERENCES `reminders` (`id`) ON DELETE SET NULL, + CONSTRAINT `contacts_deceased_special_date_id_foreign` FOREIGN KEY (`deceased_special_date_id`) REFERENCES `special_dates` (`id`) ON DELETE SET NULL, + CONSTRAINT `contacts_first_met_reminder_id_foreign` FOREIGN KEY (`first_met_reminder_id`) REFERENCES `reminders` (`id`) ON DELETE SET NULL, + CONSTRAINT `contacts_first_met_special_date_id_foreign` FOREIGN KEY (`first_met_special_date_id`) REFERENCES `special_dates` (`id`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `contacts` DISABLE KEYS */; +/*!40000 ALTER TABLE `contacts` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `conversations` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `account_id` int unsigned NOT NULL, + `contact_id` int unsigned NOT NULL, + `contact_field_type_id` int unsigned NOT NULL, + `happened_at` datetime NOT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `conversations_account_id_foreign` (`account_id`), + KEY `conversations_contact_id_foreign` (`contact_id`), + KEY `conversations_contact_field_type_id_foreign` (`contact_field_type_id`), + CONSTRAINT `conversations_account_id_foreign` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE, + CONSTRAINT `conversations_contact_field_type_id_foreign` FOREIGN KEY (`contact_field_type_id`) REFERENCES `contact_field_types` (`id`) ON DELETE CASCADE, + CONSTRAINT `conversations_contact_id_foreign` FOREIGN KEY (`contact_id`) REFERENCES `contacts` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `conversations` DISABLE KEYS */; +/*!40000 ALTER TABLE `conversations` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `crons` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `command` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `last_run` timestamp NOT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `crons_command_unique` (`command`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `crons` DISABLE KEYS */; +/*!40000 ALTER TABLE `crons` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `currencies` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `iso` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `symbol` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=154 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `currencies` DISABLE KEYS */; +INSERT INTO `currencies` VALUES (1,'CAD','Canadian Dollar','$',NULL,NULL),(2,'USD','US Dollar','$',NULL,NULL),(3,'GBP','British Pound','£',NULL,NULL),(4,'EUR','Euro','€',NULL,NULL),(5,'RUB','Russian Ruble','₽',NULL,NULL),(6,'ZAR','South African Rand','R ',NULL,NULL),(7,'DKK','Danish krone','kr.',NULL,NULL),(8,'INR','Indian rupee','₹',NULL,NULL),(9,'BRL','Brazilian Real','R$',NULL,NULL),(10,'CHF','Swiss CHF','CHF',NULL,NULL),(11,'AED','Emirati Dirham','.د.ب',NULL,NULL),(12,'AFN','Afghan Afghani','؋',NULL,NULL),(13,'ALL','Albanian lek','lek',NULL,NULL),(14,'AMD','Armenian dram','',NULL,NULL),(15,'ANG','Dutch Guilder','ƒ',NULL,NULL),(16,'AOA','Angolan Kwanza','Kz',NULL,NULL),(17,'ARS','Argentine peso','$',NULL,NULL),(18,'AUD','Australian Dollar','$',NULL,NULL),(19,'AWG','Arubin florin','ƒ',NULL,NULL),(20,'AZN','Azerbaijani manat','ман',NULL,NULL),(21,'BAM','Bosnian Convertible Marka','KM',NULL,NULL),(22,'BBD','Barbadian dollar','$',NULL,NULL),(23,'BDT','Bangladeshi Taka','Tk',NULL,NULL),(24,'BGN','Bulgarian lev','лв',NULL,NULL),(25,'BHD','Bahraini Dinar','.د.ب or BD',NULL,NULL),(26,'BIF','Burundian Franc','',NULL,NULL),(27,'BMD','Bermudian dollar','$',NULL,NULL),(28,'BND','Bruneian Dollar','$',NULL,NULL),(29,'BOB','Bolivian Boliviano','$b',NULL,NULL),(30,'BSD','Bahamian dollar','B$',NULL,NULL),(31,'BTN','Bhutanese Ngultrum','Nu.',NULL,NULL),(32,'BWP','Botswana Pula','P',NULL,NULL),(33,'BYR','Belarusian ruble','р',NULL,NULL),(34,'BZD','Belize dollar','BZ$',NULL,NULL),(35,'CLP','Chilean Peso','$',NULL,NULL),(36,'CNY','Yuan or chinese renminbi','¥',NULL,NULL),(37,'COP','Colombian peso','$',NULL,NULL),(38,'CRC','Costa Rican colón','₡',NULL,NULL),(39,'CUC','Cuban convertible peso','$',NULL,NULL),(40,'CUP','Cuban peso','₱',NULL,NULL),(41,'CVE','Cape Verdean Escudo','$',NULL,NULL),(42,'CZK','Czech koruna','Kč',NULL,NULL),(43,'DJF','Djiboutian Franc','fdj',NULL,NULL),(44,'DOP','Dominican peso','$',NULL,NULL),(45,'DZD','Algerian Dinar','جد',NULL,NULL),(46,'EGP','Egyptian Pound','£ ',NULL,NULL),(47,'ERN','Eritrean nakfa','ናቕፋ',NULL,NULL),(48,'ETB','Ethiopian Birr','Br',NULL,NULL),(49,'FJD','Fijian dollar','$',NULL,NULL),(50,'FKP','Falkland Island Pound','£',NULL,NULL),(51,'GEL','Georgian lari','ლ',NULL,NULL),(52,'GHS','Ghanaian Cedi','GH¢',NULL,NULL),(53,'GIP','Gibraltar pound','£',NULL,NULL),(54,'GMD','Gambian dalasi','',NULL,NULL),(55,'GNF','Guinean Franc','',NULL,NULL),(56,'GTQ','Guatemalan Quetzal','Q',NULL,NULL),(57,'GYD','Guyanese dollar','$',NULL,NULL),(58,'HKD','Hong Kong dollar','HK$',NULL,NULL),(59,'HNL','Honduran lempira','L',NULL,NULL),(60,'HRK','Croatian kuna','kn',NULL,NULL),(61,'HTG','Haitian gourde','G',NULL,NULL),(62,'HUF','Hungarian forint','Ft',NULL,NULL),(63,'IDR','Indonesian Rupiah','Rp',NULL,NULL),(64,'ILS','Israeli Shekel','₪',NULL,NULL),(65,'IQD','Iraqi Dinar','ع.د',NULL,NULL),(66,'IRR','Iranian Rial','',NULL,NULL),(67,'ISK','Icelandic Krona','kr',NULL,NULL),(68,'JMD','Jamaican dollar','J$',NULL,NULL),(69,'JOD','Jordanian Dinar','',NULL,NULL),(70,'JPY','Japanese yen','¥',NULL,NULL),(71,'KES','Kenyan Shilling','KSh',NULL,NULL),(72,'KGS','Kyrgyzstani som','лв',NULL,NULL),(73,'KHR','Cambodian Riel','៛',NULL,NULL),(74,'KMF','Comoran Franc','',NULL,NULL),(75,'KPW','North Korean won','₩',NULL,NULL),(76,'KRW','South Korean won','₩',NULL,NULL),(77,'KWD','Kuwaiti Dinar','ك',NULL,NULL),(78,'KYD','Caymanian Dollar','$',NULL,NULL),(79,'KZT','Kazakhstani tenge','₸',NULL,NULL),(80,'LAK','Lao or Laotian Kip','₭',NULL,NULL),(81,'LBP','Lebanese Pound','ل.ل',NULL,NULL),(82,'LKR','Sri Lankan Rupee','Rs',NULL,NULL),(83,'LRD','Liberian Dollar','$',NULL,NULL),(84,'LSL','Lesotho loti','L or M',NULL,NULL),(85,'LTL','Lithuanian litas','Lt',NULL,NULL),(86,'LYD','Libyan Dinar',' د.ل',NULL,NULL),(87,'MAD','Moroccan Dirham','م.د.',NULL,NULL),(88,'MDL','Moldovan Leu','L',NULL,NULL),(89,'MGA','Malagasy Ariary','Ar',NULL,NULL),(90,'MKD','Macedonian Denar','ден',NULL,NULL),(91,'MMK','Burmese Kyat','K',NULL,NULL),(92,'MNT','Mongolian Tughrik','₮',NULL,NULL),(93,'MOP','Macau Pataca','MOP$',NULL,NULL),(94,'MRO','Mauritanian Ouguiya','UM',NULL,NULL),(95,'MUR','Mauritian rupee','Rs',NULL,NULL),(96,'MVR','Maldivian Rufiyaa','rf',NULL,NULL),(97,'MWK','Malawian Kwacha','MK',NULL,NULL),(98,'MXN','Mexico Peso','$',NULL,NULL),(99,'MYR','Malaysian Ringgit','RM',NULL,NULL),(100,'MZN','Mozambican Metical','MT',NULL,NULL),(101,'NAD','Namibian Dollar','$',NULL,NULL),(102,'NGN','Nigerian Naira','₦',NULL,NULL),(103,'NIO','Nicaraguan córdoba','C$',NULL,NULL),(104,'NOK','Norwegian krone','kr',NULL,NULL),(105,'NPR','Nepalese Rupee','Rs',NULL,NULL),(106,'NZD','New Zealand Dollar','$',NULL,NULL),(107,'OMR','Omani Rial','ع.ر.',NULL,NULL),(108,'PAB','Balboa panamérn','B/',NULL,NULL),(109,'PEN','Peruvian nuevo sol','S/',NULL,NULL),(110,'PGK','Papua New Guinean Kina','K',NULL,NULL),(111,'PHP','Philippine Peso','₱',NULL,NULL),(112,'PKR','Pakistani Rupee','Rs',NULL,NULL),(113,'PLN','Polish złoty','zł',NULL,NULL),(114,'PYG','Paraguayan guarani','₲',NULL,NULL),(115,'QAR','Qatari Riyal','ق.ر ',NULL,NULL),(116,'RON','Romanian leu','lei',NULL,NULL),(117,'RSD','Serbian Dinar','РСД',NULL,NULL),(118,'RWF','Rwandan franc','FRw, RF, R₣',NULL,NULL),(119,'SAR','Saudi Arabian Riyal','ر.س',NULL,NULL),(120,'SBD','Solomon Islander Dollar','SI$',NULL,NULL),(121,'SCR','Seychellois Rupee','Rs',NULL,NULL),(122,'SDG','Sudanese Pound','',NULL,NULL),(123,'SEK','Swedish krona','kr',NULL,NULL),(124,'SGD','Singapore Dollar','$',NULL,NULL),(125,'SLL','Sierra Leonean Leone','Le',NULL,NULL),(126,'SOS','Somali Shilling','S',NULL,NULL),(127,'SRD','Surinamese dollar','$',NULL,NULL),(128,'SSP','South Sudanese pound','£',NULL,NULL),(129,'SYP','Syrian Pound','£',NULL,NULL),(130,'SZL','Swazi Lilangeni','L or E',NULL,NULL),(131,'THB','Thai Baht','฿',NULL,NULL),(132,'TJS','Tajikistani somoni','',NULL,NULL),(133,'TMT','Turkmenistan manat','T',NULL,NULL),(134,'TND','Tunisian Dinar','',NULL,NULL),(135,'TOP','Tongan Pa\'anga','T$',NULL,NULL),(136,'TRY','Turkish Lira','',NULL,NULL),(137,'TTD','Trinidadian dollar','TT$',NULL,NULL),(138,'TWD','Taiwan New Dollar','NT$',NULL,NULL),(139,'TZS','Tanzanian Shilling','Sh',NULL,NULL),(140,'UAH','Ukrainian Hryvnia','₴',NULL,NULL),(141,'UGX','Ugandan Shilling','USh',NULL,NULL),(142,'UYU','Uruguayan peso','$U',NULL,NULL),(143,'UZS','Uzbekistani som','лв',NULL,NULL),(144,'VEF','Venezuelan bolivar','Bs',NULL,NULL),(145,'VND','Vietnamese Dong','₫',NULL,NULL),(146,'VUV','Ni-Vanuatu Vatu','VT',NULL,NULL),(147,'WST','Samoan Tālā','$',NULL,NULL),(148,'XCD','East Caribbean dollar','EC$',NULL,NULL),(149,'XOF','CFA Franc','',NULL,NULL),(150,'XPF','CFP Franc','',NULL,NULL),(151,'YER','Yemeni Rial','',NULL,NULL),(152,'ZMW','Zambian Kwacha','ZMK',NULL,NULL),(153,'ZWD','Zimbabwean Dollar','Z$',NULL,NULL); +/*!40000 ALTER TABLE `currencies` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `days` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `account_id` int unsigned NOT NULL, + `date` date NOT NULL, + `rate` int NOT NULL, + `comment` mediumtext COLLATE utf8mb4_unicode_ci, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `days_account_id_foreign` (`account_id`), + CONSTRAINT `days_account_id_foreign` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `days` DISABLE KEYS */; +/*!40000 ALTER TABLE `days` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `debts` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `account_id` int unsigned NOT NULL, + `contact_id` int unsigned NOT NULL, + `in_debt` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'no', + `status` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'inprogress', + `amount` int NOT NULL, + `currency_id` int unsigned DEFAULT NULL, + `reason` longtext COLLATE utf8mb4_unicode_ci, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `debts_account_id_foreign` (`account_id`), + KEY `debts_contact_id_foreign` (`contact_id`), + KEY `debts_currency_id_foreign` (`currency_id`), + CONSTRAINT `debts_account_id_foreign` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE, + CONSTRAINT `debts_contact_id_foreign` FOREIGN KEY (`contact_id`) REFERENCES `contacts` (`id`) ON DELETE CASCADE, + CONSTRAINT `debts_currency_id_foreign` FOREIGN KEY (`currency_id`) REFERENCES `currencies` (`id`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `debts` DISABLE KEYS */; +/*!40000 ALTER TABLE `debts` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `default_activity_type_categories` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `translation_key` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `default_activity_type_categories` DISABLE KEYS */; +INSERT INTO `default_activity_type_categories` VALUES (1,'simple_activities','2020-05-21 19:15:16','2020-05-21 19:15:16'),(2,'sport','2020-05-21 19:15:16','2020-05-21 19:15:16'),(3,'food','2020-05-21 19:15:16','2020-05-21 19:15:16'),(4,'cultural_activities','2020-05-21 19:15:16','2020-05-21 19:15:16'); +/*!40000 ALTER TABLE `default_activity_type_categories` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `default_activity_types` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `default_activity_type_category_id` int NOT NULL, + `translation_key` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `location_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `default_activity_types` DISABLE KEYS */; +INSERT INTO `default_activity_types` VALUES (1,1,'just_hung_out','outside','2020-05-21 19:15:16','2020-05-21 19:15:16'),(2,1,'watched_movie_at_home','my_place','2020-05-21 19:15:16','2020-05-21 19:15:16'),(3,1,'talked_at_home','my_place','2020-05-21 19:15:16','2020-05-21 19:15:16'),(4,2,'did_sport_activities_together','outside','2020-05-21 19:15:16','2020-05-21 19:15:16'),(5,3,'ate_at_his_place','his_place','2020-05-21 19:15:16','2020-05-21 19:15:16'),(6,3,'went_bar','outside','2020-05-21 19:15:16','2020-05-21 19:15:16'),(7,3,'ate_at_home','my_place','2020-05-21 19:15:16','2020-05-21 19:15:16'),(8,3,'picnicked','outside','2020-05-21 19:15:16','2020-05-21 19:15:16'),(9,3,'ate_restaurant','outside','2020-05-21 19:15:16','2020-05-21 19:15:16'),(10,4,'went_theater','outside','2020-05-21 19:15:16','2020-05-21 19:15:16'),(11,4,'went_concert','outside','2020-05-21 19:15:16','2020-05-21 19:15:16'),(12,4,'went_play','outside','2020-05-21 19:15:16','2020-05-21 19:15:16'),(13,4,'went_museum','outside','2020-05-21 19:15:16','2020-05-21 19:15:16'); +/*!40000 ALTER TABLE `default_activity_types` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `default_contact_field_types` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `fontawesome_icon` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `protocol` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `migrated` tinyint(1) NOT NULL DEFAULT '0', + `delible` tinyint(1) NOT NULL DEFAULT '1', + `type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `default_contact_field_types` DISABLE KEYS */; +INSERT INTO `default_contact_field_types` VALUES (1,'Email','fa fa-envelope-open-o','mailto:',1,0,'email',NULL,NULL),(2,'Phone','fa fa-volume-control-phone','tel:',1,0,'phone',NULL,NULL),(3,'Facebook','fa fa-facebook-official','https://facebook.com/',1,1,NULL,NULL,NULL),(4,'Twitter','fa fa-twitter-square',NULL,1,1,NULL,NULL,NULL),(5,'Whatsapp','fa fa-whatsapp','https://wa.me/',1,1,NULL,NULL,NULL),(6,'Telegram','fa fa-telegram','telegram:',1,1,NULL,NULL,NULL),(7,'LinkedIn','fa fa-linkedin-square',NULL,1,1,NULL,NULL,NULL); +/*!40000 ALTER TABLE `default_contact_field_types` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `default_contact_modules` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `key` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `translation_key` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `delible` tinyint(1) NOT NULL DEFAULT '0', + `active` tinyint(1) NOT NULL DEFAULT '1', + `migrated` tinyint(1) NOT NULL DEFAULT '0', + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=19 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `default_contact_modules` DISABLE KEYS */; +INSERT INTO `default_contact_modules` VALUES (1,'love_relationships','app.relationship_type_group_love',0,1,1,NULL,NULL),(2,'family_relationships','app.relationship_type_group_family',0,1,1,NULL,NULL),(3,'other_relationships','app.relationship_type_group_other',0,1,1,NULL,NULL),(4,'pets','people.pets_title',0,1,1,NULL,NULL),(5,'contact_information','people.section_contact_information',0,1,1,NULL,NULL),(6,'addresses','people.contact_address_title',0,1,1,NULL,NULL),(7,'how_you_met','people.introductions_sidebar_title',0,1,1,NULL,NULL),(8,'work_information','people.work_information',0,1,1,NULL,NULL),(9,'food_preferences','people.food_preferences_title',0,1,1,NULL,NULL),(10,'notes','people.section_personal_notes',0,1,1,NULL,NULL),(11,'phone_calls','people.call_title',0,1,1,NULL,NULL),(12,'activities','people.activity_title',0,1,1,NULL,NULL),(13,'reminders','people.section_personal_reminders',0,1,1,NULL,NULL),(14,'tasks','people.section_personal_tasks',0,1,1,NULL,NULL),(15,'gifts','people.gifts_title',0,1,1,NULL,NULL),(16,'debts','people.debt_title',0,1,1,NULL,NULL),(17,'conversations','people.conversation_list_title',0,1,1,NULL,NULL),(18,'documents','people.document_list_title',0,1,1,NULL,NULL); +/*!40000 ALTER TABLE `default_contact_modules` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `default_life_event_categories` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `translation_key` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `migrated` tinyint(1) NOT NULL DEFAULT '0', + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `default_life_event_categories` DISABLE KEYS */; +INSERT INTO `default_life_event_categories` VALUES (1,'work_education',0,'2020-05-21 19:15:22','2020-05-21 19:15:22'),(2,'family_relationships',0,'2020-05-21 19:15:22','2020-05-21 19:15:22'),(3,'home_living',0,'2020-05-21 19:15:22','2020-05-21 19:15:22'),(4,'health_wellness',0,'2020-05-21 19:15:22','2020-05-21 19:15:22'),(5,'travel_experiences',0,'2020-05-21 19:15:22','2020-05-21 19:15:22'); +/*!40000 ALTER TABLE `default_life_event_categories` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `default_life_event_types` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `default_life_event_category_id` int unsigned NOT NULL, + `translation_key` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `specific_information_structure` text COLLATE utf8mb4_unicode_ci, + `migrated` tinyint(1) NOT NULL DEFAULT '0', + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `default_life_event_types_default_life_event_category_id_foreign` (`default_life_event_category_id`), + CONSTRAINT `default_life_event_types_default_life_event_category_id_foreign` FOREIGN KEY (`default_life_event_category_id`) REFERENCES `default_life_event_categories` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=44 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `default_life_event_types` DISABLE KEYS */; +INSERT INTO `default_life_event_types` VALUES (1,1,'new_job','{\"employer\": {\"type\": \"string\", \"value\": \"\"}, \"job_title\": {\"type\": \"string\", \"value\": \"\"}}',0,'2020-05-21 19:15:22','2020-05-21 19:15:22'),(2,1,'retirement','{\"profession\": {\"type\": \"string\", \"value\": \"\"}}',0,'2020-05-21 19:15:22','2020-05-21 19:15:22'),(3,1,'new_school','{\"degree\": {\"type\": \"string\", \"value\": \"\"}, \"end_date\": {\"type\": \"date\", \"value\": \"\"}, \"end_date_reminder_id\": {\"type\": \"integer\", \"value\": \"\"}, \"school_name\": {\"type\": \"string\", \"value\": \"\"}, \"studying\": {\"type\": \"string\", \"value\": \"\"}}',0,'2020-05-21 19:15:22','2020-05-21 19:15:22'),(4,1,'study_abroad','{\"degree\": {\"type\": \"string\", \"value\": \"\"}, \"end_date\": {\"type\": \"date\", \"value\": \"\"}, \"end_date_reminder_id\": {\"type\": \"integer\", \"value\": \"\"}, \"school_name\": {\"type\": \"string\", \"value\": \"\"}, \"studying\": {\"type\": \"string\", \"value\": \"\"}}',0,'2020-05-21 19:15:22','2020-05-21 19:15:22'),(5,1,'volunteer_work','{\"organization\": {\"type\": \"string\", \"value\": \"\"}}',0,'2020-05-21 19:15:22','2020-05-21 19:15:22'),(6,1,'published_book_or_paper','{\"full_citation\": {\"type\": \"string\", \"value\": \"\"}, \"url\": {\"type\": \"string\", \"value\": \"\"}, \"citation\": {\"type\": \"string\", \"value\": \"\"}}',0,'2020-05-21 19:15:22','2020-05-21 19:15:22'),(7,1,'military_service','{\"end_date\": {\"type\": \"date\", \"value\": \"\"}, \"end_date_reminder_id\": {\"type\": \"integer\", \"value\": \"\"}, \"branch\": {\"type\": \"string\", \"value\": \"\"}, \"division\": {\"type\": \"string\", \"value\": \"\"}, \"country\": {\"type\": \"string\", \"value\": \"\"}}',0,'2020-05-21 19:15:22','2020-05-21 19:15:22'),(8,2,'new_relationship',NULL,0,'2020-05-21 19:15:22','2020-05-21 19:15:22'),(9,2,'engagement','{\"with_contact_id\": {\"type\": \"integer\", \"value\": \"\"}}',0,'2020-05-21 19:15:22','2020-05-21 19:15:22'),(10,2,'marriage','{\"with_contact_id\": {\"type\": \"integer\", \"value\": \"\"}}',0,'2020-05-21 19:15:22','2020-05-21 19:15:22'),(11,2,'anniversary',NULL,0,'2020-05-21 19:15:22','2020-05-21 19:15:22'),(12,2,'expecting_a_baby','{\"contact_id\": {\"type\": \"integer\", \"value\": \"\"}, \"expected_date\": {\"type\": \"date\", \"value\": \"\"}, \"expected_date_reminder_id\": {\"type\": \"integer\", \"value\": \"\"}, \"expected_gender\": {\"type\": \"string\", \"value\": \"\"}}',0,'2020-05-21 19:15:22','2020-05-21 19:15:22'),(13,2,'new_child',NULL,0,'2020-05-21 19:15:22','2020-05-21 19:15:22'),(14,2,'new_family_member',NULL,0,'2020-05-21 19:15:22','2020-05-21 19:15:22'),(15,2,'new_pet',NULL,0,'2020-05-21 19:15:22','2020-05-21 19:15:22'),(16,2,'end_of_relationship','{\"breakup_reason\": {\"type\": \"string\", \"value\": \"\"}, \"who_broke_up_contact_id\": {\"type\": \"integer\", \"value\": \"\"}}',0,'2020-05-21 19:15:22','2020-05-21 19:15:22'),(17,2,'loss_of_a_loved_one',NULL,0,'2020-05-21 19:15:22','2020-05-21 19:15:22'),(18,3,'moved','{\"where_to\": {\"type\": \"string\", \"value\": \"\"}}',0,'2020-05-21 19:15:22','2020-05-21 19:15:22'),(19,3,'bought_a_home','{\"address\": {\"type\": \"string\", \"value\": \"\"}, \"estimated_value\": {\"type\": \"number\", \"value\": \"\"}}',0,'2020-05-21 19:15:22','2020-05-21 19:15:22'),(20,3,'home_improvement',NULL,0,'2020-05-21 19:15:22','2020-05-21 19:15:22'),(21,3,'holidays','{\"where\": {\"type\": \"string\", \"value\": \"\"}, \"duration_in_days\": {\"type\": \"integer\", \"value\": \"\"}}',0,'2020-05-21 19:15:22','2020-05-21 19:15:22'),(22,3,'new_vehicle','{\"type\": {\"type\": \"string\", \"value\": \"\"}, \"model\": {\"type\": \"string\", \"value\": \"\"}, \"model_year\": {\"type\": \"string\", \"value\": \"\"}}',0,'2020-05-21 19:15:22','2020-05-21 19:15:22'),(23,3,'new_roommate','{\"contact_id\": {\"type\": \"string\", \"value\": \"\"}}',0,'2020-05-21 19:15:22','2020-05-21 19:15:22'),(24,4,'overcame_an_illness',NULL,0,'2020-05-21 19:15:22','2020-05-21 19:15:22'),(25,4,'quit_a_habit',NULL,0,'2020-05-21 19:15:22','2020-05-21 19:15:22'),(26,4,'new_eating_habits',NULL,0,'2020-05-21 19:15:22','2020-05-21 19:15:22'),(27,4,'weight_loss','{\"amount\": {\"type\": \"string\", \"value\": \"\"}, \"unit\": {\"type\": \"string\", \"value\": \"\"}}',0,'2020-05-21 19:15:22','2020-05-21 19:15:22'),(28,4,'wear_glass_or_contact',NULL,0,'2020-05-21 19:15:22','2020-05-21 19:15:22'),(29,4,'broken_bone',NULL,0,'2020-05-21 19:15:22','2020-05-21 19:15:22'),(30,4,'removed_braces',NULL,0,'2020-05-21 19:15:22','2020-05-21 19:15:22'),(31,4,'surgery','{\"nature\": {\"type\": \"string\", \"value\": \"\"}, \"number_days_in_hospital\": {\"type\": \"integer\", \"value\": \"\"}, \"number_days_in_hospital\": {\"type\": \"integer\", \"value\": \"\"}, \"expected_date_out_of_hospital_reminder_id\": {\"type\": \"integer\", \"value\": \"\"}}',0,'2020-05-21 19:15:22','2020-05-21 19:15:22'),(32,4,'dentist',NULL,0,'2020-05-21 19:15:22','2020-05-21 19:15:22'),(33,5,'new_sport',NULL,0,'2020-05-21 19:15:22','2020-05-21 19:15:22'),(34,5,'new_hobby',NULL,0,'2020-05-21 19:15:23','2020-05-21 19:15:23'),(35,5,'new_instrument',NULL,0,'2020-05-21 19:15:23','2020-05-21 19:15:23'),(36,5,'new_language',NULL,0,'2020-05-21 19:15:23','2020-05-21 19:15:23'),(37,5,'tattoo_or_piercing',NULL,0,'2020-05-21 19:15:23','2020-05-21 19:15:23'),(38,5,'new_license',NULL,0,'2020-05-21 19:15:23','2020-05-21 19:15:23'),(39,5,'travel','{\"visited_place\": {\"type\": \"string\", \"value\": \"\"}, \"duration_in_days\": {\"type\": \"integer\", \"value\": \"\"}}',0,'2020-05-21 19:15:23','2020-05-21 19:15:23'),(40,5,'achievement_or_award',NULL,0,'2020-05-21 19:15:23','2020-05-21 19:15:23'),(41,5,'changed_beliefs',NULL,0,'2020-05-21 19:15:23','2020-05-21 19:15:23'),(42,5,'first_word',NULL,0,'2020-05-21 19:15:23','2020-05-21 19:15:23'),(43,5,'first_kiss',NULL,0,'2020-05-21 19:15:23','2020-05-21 19:15:23'); +/*!40000 ALTER TABLE `default_life_event_types` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `default_relationship_type_groups` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `delible` tinyint(1) NOT NULL DEFAULT '0', + `migrated` tinyint(1) NOT NULL DEFAULT '0', + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `default_relationship_type_groups` DISABLE KEYS */; +INSERT INTO `default_relationship_type_groups` VALUES (1,'love',0,1,NULL,NULL),(2,'family',0,1,NULL,NULL),(3,'friend',0,1,NULL,NULL),(4,'work',0,1,NULL,NULL); +/*!40000 ALTER TABLE `default_relationship_type_groups` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `default_relationship_types` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_reverse_relationship` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `relationship_type_group_id` int NOT NULL, + `delible` tinyint(1) NOT NULL DEFAULT '0', + `migrated` tinyint(1) NOT NULL DEFAULT '0', + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `default_relationship_types_migrated_index` (`migrated`) +) ENGINE=InnoDB AUTO_INCREMENT=28 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `default_relationship_types` DISABLE KEYS */; +INSERT INTO `default_relationship_types` VALUES (1,'partner','partner',1,0,1,NULL,NULL),(2,'spouse','spouse',1,0,1,NULL,NULL),(3,'date','date',1,0,1,NULL,NULL),(4,'lover','lover',1,0,1,NULL,NULL),(5,'inlovewith','lovedby',1,0,1,NULL,NULL),(6,'lovedby','inlovewith',1,0,1,NULL,NULL),(7,'ex','ex',1,0,1,NULL,NULL),(8,'parent','child',2,0,1,NULL,NULL),(9,'child','parent',2,0,1,NULL,NULL),(10,'sibling','sibling',2,0,1,NULL,NULL),(11,'grandparent','grandchild',2,0,1,NULL,NULL),(12,'grandchild','grandparent',2,0,1,NULL,NULL),(13,'uncle','nephew',2,0,1,NULL,NULL),(14,'nephew','uncle',2,0,1,NULL,NULL),(15,'cousin','cousin',2,0,1,NULL,NULL),(16,'godfather','godson',2,0,1,NULL,NULL),(17,'godson','godfather',2,0,1,NULL,NULL),(18,'friend','friend',3,0,1,NULL,NULL),(19,'bestfriend','bestfriend',3,0,1,NULL,NULL),(20,'colleague','colleague',4,0,1,NULL,NULL),(21,'boss','subordinate',4,0,1,NULL,NULL),(22,'subordinate','boss',4,0,1,NULL,NULL),(23,'mentor','protege',4,0,1,NULL,NULL),(24,'protege','mentor',4,0,1,NULL,NULL),(25,'ex_husband','ex_husband',1,0,1,NULL,NULL),(26,'stepparent','stepchild',2,0,1,NULL,NULL),(27,'stepchild','stepparent',2,0,1,NULL,NULL); +/*!40000 ALTER TABLE `default_relationship_types` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `documents` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `account_id` int unsigned NOT NULL, + `contact_id` int unsigned NOT NULL, + `original_filename` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `new_filename` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `filesize` int DEFAULT NULL, + `type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `mime_type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `number_of_downloads` int NOT NULL DEFAULT '0', + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `documents_account_id_foreign` (`account_id`), + KEY `documents_contact_id_foreign` (`contact_id`), + CONSTRAINT `documents_account_id_foreign` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE, + CONSTRAINT `documents_contact_id_foreign` FOREIGN KEY (`contact_id`) REFERENCES `contacts` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `documents` DISABLE KEYS */; +/*!40000 ALTER TABLE `documents` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `emotion_activity` ( + `account_id` int unsigned NOT NULL, + `activity_id` int unsigned NOT NULL, + `emotion_id` int unsigned NOT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + KEY `emotion_activity_account_id_foreign` (`account_id`), + KEY `emotion_activity_activity_id_foreign` (`activity_id`), + KEY `emotion_activity_emotion_id_foreign` (`emotion_id`), + CONSTRAINT `emotion_activity_account_id_foreign` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE, + CONSTRAINT `emotion_activity_activity_id_foreign` FOREIGN KEY (`activity_id`) REFERENCES `activities` (`id`) ON DELETE CASCADE, + CONSTRAINT `emotion_activity_emotion_id_foreign` FOREIGN KEY (`emotion_id`) REFERENCES `emotions` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `emotion_activity` DISABLE KEYS */; +/*!40000 ALTER TABLE `emotion_activity` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `emotion_call` ( + `account_id` int unsigned NOT NULL, + `call_id` int unsigned NOT NULL, + `emotion_id` int unsigned NOT NULL, + `contact_id` int unsigned NOT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + KEY `emotion_call_account_id_foreign` (`account_id`), + KEY `emotion_call_call_id_foreign` (`call_id`), + KEY `emotion_call_emotion_id_foreign` (`emotion_id`), + KEY `emotion_call_contact_id_foreign` (`contact_id`), + CONSTRAINT `emotion_call_account_id_foreign` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE, + CONSTRAINT `emotion_call_call_id_foreign` FOREIGN KEY (`call_id`) REFERENCES `calls` (`id`) ON DELETE CASCADE, + CONSTRAINT `emotion_call_contact_id_foreign` FOREIGN KEY (`contact_id`) REFERENCES `contacts` (`id`) ON DELETE CASCADE, + CONSTRAINT `emotion_call_emotion_id_foreign` FOREIGN KEY (`emotion_id`) REFERENCES `emotions` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `emotion_call` DISABLE KEYS */; +/*!40000 ALTER TABLE `emotion_call` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `emotions` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `emotion_primary_id` int unsigned NOT NULL, + `emotion_secondary_id` int unsigned NOT NULL, + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `emotions_emotion_primary_id_foreign` (`emotion_primary_id`), + KEY `emotions_emotion_secondary_id_foreign` (`emotion_secondary_id`), + CONSTRAINT `emotions_emotion_primary_id_foreign` FOREIGN KEY (`emotion_primary_id`) REFERENCES `emotions_primary` (`id`) ON DELETE CASCADE, + CONSTRAINT `emotions_emotion_secondary_id_foreign` FOREIGN KEY (`emotion_secondary_id`) REFERENCES `emotions_secondary` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=134 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `emotions` DISABLE KEYS */; +INSERT INTO `emotions` VALUES (1,1,1,'adoration',NULL,NULL),(2,1,1,'affection',NULL,NULL),(3,1,1,'love',NULL,NULL),(4,1,1,'fondness',NULL,NULL),(5,1,1,'liking',NULL,NULL),(6,1,1,'attraction',NULL,NULL),(7,1,1,'caring',NULL,NULL),(8,1,1,'tenderness',NULL,NULL),(9,1,1,'compassion',NULL,NULL),(10,1,1,'sentimentality',NULL,NULL),(11,1,2,'arousal',NULL,NULL),(12,1,2,'desire',NULL,NULL),(13,1,2,'lust',NULL,NULL),(14,1,2,'passion',NULL,NULL),(15,1,2,'infatuation',NULL,NULL),(16,1,3,'longing',NULL,NULL),(17,2,4,'amusement',NULL,NULL),(18,2,4,'bliss',NULL,NULL),(19,2,4,'cheerfulness',NULL,NULL),(20,2,4,'gaiety',NULL,NULL),(21,2,4,'glee',NULL,NULL),(22,2,4,'jolliness',NULL,NULL),(23,2,4,'joviality',NULL,NULL),(24,2,4,'joy',NULL,NULL),(25,2,4,'delight',NULL,NULL),(26,2,4,'enjoyment',NULL,NULL),(27,2,4,'gladness',NULL,NULL),(28,2,4,'happiness',NULL,NULL),(29,2,4,'jubilation',NULL,NULL),(30,2,4,'elation',NULL,NULL),(31,2,4,'satisfaction',NULL,NULL),(32,2,4,'ecstasy',NULL,NULL),(33,2,4,'euphoria',NULL,NULL),(34,2,5,'enthusiasm',NULL,NULL),(35,2,5,'zeal',NULL,NULL),(36,2,5,'zest',NULL,NULL),(37,2,5,'excitement',NULL,NULL),(38,2,5,'thrill',NULL,NULL),(39,2,5,'exhilaration',NULL,NULL),(40,2,6,'contentment',NULL,NULL),(41,2,6,'pleasure',NULL,NULL),(42,2,7,'pride',NULL,NULL),(43,2,7,'pleasure',NULL,NULL),(44,2,8,'eagerness',NULL,NULL),(45,2,8,'hope',NULL,NULL),(46,2,9,'enthrallment',NULL,NULL),(47,2,9,'rapture',NULL,NULL),(48,2,10,'relief',NULL,NULL),(49,3,11,'amazement',NULL,NULL),(50,3,11,'surprise',NULL,NULL),(51,3,11,'astonishment',NULL,NULL),(52,4,12,'aggravation',NULL,NULL),(53,4,12,'irritation',NULL,NULL),(54,4,12,'agitation',NULL,NULL),(55,4,12,'annoyance',NULL,NULL),(56,4,12,'grouchiness',NULL,NULL),(57,4,12,'grumpiness',NULL,NULL),(58,4,13,'exasperation',NULL,NULL),(59,4,13,'frustration',NULL,NULL),(60,4,14,'anger',NULL,NULL),(61,4,14,'rage',NULL,NULL),(62,4,14,'outrage',NULL,NULL),(63,4,14,'fury',NULL,NULL),(64,4,14,'wrath',NULL,NULL),(65,4,14,'hostility',NULL,NULL),(66,4,14,'ferocity',NULL,NULL),(67,4,14,'bitterness',NULL,NULL),(68,4,14,'hate',NULL,NULL),(69,4,14,'loathing',NULL,NULL),(70,4,14,'scorn',NULL,NULL),(71,4,14,'spite',NULL,NULL),(72,4,14,'vengefulness',NULL,NULL),(73,4,14,'dislike',NULL,NULL),(74,4,14,'resentment',NULL,NULL),(75,4,15,'disgust',NULL,NULL),(76,4,15,'revulsion',NULL,NULL),(77,4,15,'contempt',NULL,NULL),(78,4,16,'envy',NULL,NULL),(79,4,16,'jealousy',NULL,NULL),(80,5,17,'agony',NULL,NULL),(81,5,17,'suffering',NULL,NULL),(82,5,17,'hurt',NULL,NULL),(83,5,17,'anguish',NULL,NULL),(84,5,18,'depression',NULL,NULL),(85,5,18,'despair',NULL,NULL),(86,5,18,'hopelessness',NULL,NULL),(87,5,18,'gloom',NULL,NULL),(88,5,18,'glumness',NULL,NULL),(89,5,18,'sadness',NULL,NULL),(90,5,18,'unhappiness',NULL,NULL),(91,5,18,'grief',NULL,NULL),(92,5,18,'sorrow',NULL,NULL),(93,5,18,'woe',NULL,NULL),(94,5,18,'misery',NULL,NULL),(95,5,18,'melancholy',NULL,NULL),(96,5,19,'dismay',NULL,NULL),(97,5,19,'disappointment',NULL,NULL),(98,5,19,'displeasure',NULL,NULL),(99,5,20,'guilt',NULL,NULL),(100,5,20,'shame',NULL,NULL),(101,5,20,'regret',NULL,NULL),(102,5,20,'remorse',NULL,NULL),(103,5,21,'alienation',NULL,NULL),(104,5,21,'isolation',NULL,NULL),(105,5,21,'neglect',NULL,NULL),(106,5,21,'loneliness',NULL,NULL),(107,5,21,'rejection',NULL,NULL),(108,5,21,'homesickness',NULL,NULL),(109,5,21,'defeat',NULL,NULL),(110,5,21,'dejection',NULL,NULL),(111,5,21,'insecurity',NULL,NULL),(112,5,21,'embarrassment',NULL,NULL),(113,5,21,'humiliation',NULL,NULL),(114,5,21,'insult',NULL,NULL),(115,5,22,'pity',NULL,NULL),(116,5,22,'sympathy',NULL,NULL),(117,6,23,'alarm',NULL,NULL),(118,6,23,'shock',NULL,NULL),(119,6,23,'fear',NULL,NULL),(120,6,23,'fright',NULL,NULL),(121,6,23,'horror',NULL,NULL),(122,6,23,'terror',NULL,NULL),(123,6,23,'panic',NULL,NULL),(124,6,23,'hysteria',NULL,NULL),(125,6,23,'mortification',NULL,NULL),(126,6,24,'anxiety',NULL,NULL),(127,6,24,'nervousness',NULL,NULL),(128,6,24,'tenseness',NULL,NULL),(129,6,24,'uneasiness',NULL,NULL),(130,6,24,'apprehension',NULL,NULL),(131,6,24,'worry',NULL,NULL),(132,6,24,'distress',NULL,NULL),(133,6,24,'dread',NULL,NULL); +/*!40000 ALTER TABLE `emotions` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `emotions_primary` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `emotions_primary` DISABLE KEYS */; +INSERT INTO `emotions_primary` VALUES (1,'love',NULL,NULL),(2,'joy',NULL,NULL),(3,'surprise',NULL,NULL),(4,'anger',NULL,NULL),(5,'sadness',NULL,NULL),(6,'fear',NULL,NULL); +/*!40000 ALTER TABLE `emotions_primary` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `emotions_secondary` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `emotion_primary_id` int unsigned NOT NULL, + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `emotions_secondary_emotion_primary_id_foreign` (`emotion_primary_id`), + CONSTRAINT `emotions_secondary_emotion_primary_id_foreign` FOREIGN KEY (`emotion_primary_id`) REFERENCES `emotions_primary` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB AUTO_INCREMENT=25 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `emotions_secondary` DISABLE KEYS */; +INSERT INTO `emotions_secondary` VALUES (1,1,'affection',NULL,NULL),(2,1,'lust',NULL,NULL),(3,1,'longing',NULL,NULL),(4,2,'cheerfulness',NULL,NULL),(5,2,'zest',NULL,NULL),(6,2,'contentment',NULL,NULL),(7,2,'pride',NULL,NULL),(8,2,'optimism',NULL,NULL),(9,2,'enthrallment',NULL,NULL),(10,2,'relief',NULL,NULL),(11,3,'surprise',NULL,NULL),(12,4,'irritation',NULL,NULL),(13,4,'exasperation',NULL,NULL),(14,4,'rage',NULL,NULL),(15,4,'disgust',NULL,NULL),(16,4,'envy',NULL,NULL),(17,5,'suffering',NULL,NULL),(18,5,'sadness',NULL,NULL),(19,5,'disappointment',NULL,NULL),(20,5,'shame',NULL,NULL),(21,5,'neglect',NULL,NULL),(22,5,'sympathy',NULL,NULL),(23,6,'horror',NULL,NULL),(24,6,'nervousness',NULL,NULL); +/*!40000 ALTER TABLE `emotions_secondary` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `entries` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `account_id` int unsigned NOT NULL, + `title` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `post` longtext COLLATE utf8mb4_unicode_ci NOT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `entries_account_id_foreign` (`account_id`), + CONSTRAINT `entries_account_id_foreign` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `entries` DISABLE KEYS */; +/*!40000 ALTER TABLE `entries` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `failed_jobs` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `connection` text COLLATE utf8mb4_unicode_ci NOT NULL, + `queue` text COLLATE utf8mb4_unicode_ci NOT NULL, + `payload` longtext COLLATE utf8mb4_unicode_ci NOT NULL, + `exception` longtext COLLATE utf8mb4_unicode_ci NOT NULL, + `failed_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `failed_jobs` DISABLE KEYS */; +/*!40000 ALTER TABLE `failed_jobs` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `genders` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `account_id` int unsigned NOT NULL, + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `type` char(1) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `genders_account_id_foreign` (`account_id`), + CONSTRAINT `genders_account_id_foreign` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `genders` DISABLE KEYS */; +/*!40000 ALTER TABLE `genders` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `gift_photo` ( + `photo_id` int unsigned NOT NULL, + `gift_id` int unsigned NOT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`photo_id`,`gift_id`), + KEY `gift_photo_gift_id_foreign` (`gift_id`), + CONSTRAINT `gift_photo_gift_id_foreign` FOREIGN KEY (`gift_id`) REFERENCES `gifts` (`id`) ON DELETE CASCADE, + CONSTRAINT `gift_photo_photo_id_foreign` FOREIGN KEY (`photo_id`) REFERENCES `photos` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `gift_photo` DISABLE KEYS */; +/*!40000 ALTER TABLE `gift_photo` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `gifts` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `account_id` int unsigned NOT NULL, + `contact_id` int unsigned NOT NULL, + `is_for` int unsigned DEFAULT NULL, + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `comment` longtext COLLATE utf8mb4_unicode_ci, + `url` longtext COLLATE utf8mb4_unicode_ci, + `amount` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `currency_id` int unsigned DEFAULT NULL, + `status` varchar(8) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'idea', + `date` datetime DEFAULT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `gifts_account_id_foreign` (`account_id`), + KEY `gifts_contact_id_foreign` (`contact_id`), + KEY `gifts_is_for_foreign` (`is_for`), + KEY `gifts_currency_id_foreign` (`currency_id`), + CONSTRAINT `gifts_account_id_foreign` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE, + CONSTRAINT `gifts_contact_id_foreign` FOREIGN KEY (`contact_id`) REFERENCES `contacts` (`id`) ON DELETE CASCADE, + CONSTRAINT `gifts_currency_id_foreign` FOREIGN KEY (`currency_id`) REFERENCES `currencies` (`id`) ON DELETE SET NULL, + CONSTRAINT `gifts_is_for_foreign` FOREIGN KEY (`is_for`) REFERENCES `contacts` (`id`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `gifts` DISABLE KEYS */; +/*!40000 ALTER TABLE `gifts` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `import_job_reports` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `account_id` int unsigned NOT NULL, + `user_id` int unsigned NOT NULL, + `import_job_id` int unsigned NOT NULL, + `contact_information` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL, + `skipped` tinyint(1) NOT NULL, + `skip_reason` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `import_job_reports_account_id_foreign` (`account_id`), + KEY `import_job_reports_user_id_foreign` (`user_id`), + KEY `import_job_reports_import_job_id_foreign` (`import_job_id`), + CONSTRAINT `import_job_reports_account_id_foreign` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE, + CONSTRAINT `import_job_reports_import_job_id_foreign` FOREIGN KEY (`import_job_id`) REFERENCES `import_jobs` (`id`) ON DELETE CASCADE, + CONSTRAINT `import_job_reports_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `import_job_reports` DISABLE KEYS */; +/*!40000 ALTER TABLE `import_job_reports` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `import_jobs` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `account_id` int unsigned NOT NULL, + `user_id` int unsigned NOT NULL, + `type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'vcard', + `contacts_found` int DEFAULT NULL, + `contacts_skipped` int DEFAULT NULL, + `contacts_imported` int DEFAULT NULL, + `filename` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `started_at` date DEFAULT NULL, + `ended_at` date DEFAULT NULL, + `failed` tinyint(1) NOT NULL DEFAULT '0', + `failed_reason` mediumtext COLLATE utf8mb4_unicode_ci, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `import_jobs_account_id_foreign` (`account_id`), + KEY `import_jobs_user_id_foreign` (`user_id`), + CONSTRAINT `import_jobs_account_id_foreign` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE, + CONSTRAINT `import_jobs_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `import_jobs` DISABLE KEYS */; +/*!40000 ALTER TABLE `import_jobs` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `instances` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `uuid` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `current_version` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `latest_version` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `latest_release_notes` mediumtext COLLATE utf8mb4_unicode_ci, + `number_of_versions_since_current_version` int DEFAULT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `instances` DISABLE KEYS */; +INSERT INTO `instances` VALUES (1,'5ec6ef58a9e01','2.17.0','2.17.0',NULL,NULL,'2020-05-21 19:15:04','2020-05-21 19:15:04'); +/*!40000 ALTER TABLE `instances` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `invitations` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `account_id` int unsigned NOT NULL, + `invited_by_user_id` int unsigned NOT NULL, + `email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `invitation_key` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `invitations_account_id_foreign` (`account_id`), + KEY `invitations_invited_by_user_id_foreign` (`invited_by_user_id`), + CONSTRAINT `invitations_account_id_foreign` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE, + CONSTRAINT `invitations_invited_by_user_id_foreign` FOREIGN KEY (`invited_by_user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `invitations` DISABLE KEYS */; +/*!40000 ALTER TABLE `invitations` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `jobs` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `queue` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `payload` longtext COLLATE utf8mb4_unicode_ci NOT NULL, + `attempts` tinyint unsigned NOT NULL, + `reserved_at` int unsigned DEFAULT NULL, + `available_at` int unsigned NOT NULL, + `created_at` int unsigned NOT NULL, + PRIMARY KEY (`id`), + KEY `jobs_queue_reserved_at_index` (`queue`,`reserved_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `jobs` DISABLE KEYS */; +/*!40000 ALTER TABLE `jobs` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `journal_entries` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `account_id` int unsigned NOT NULL, + `date` datetime NOT NULL, + `journalable_id` int NOT NULL, + `journalable_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `journal_entries_account_id_foreign` (`account_id`), + CONSTRAINT `journal_entries_account_id_foreign` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `journal_entries` DISABLE KEYS */; +/*!40000 ALTER TABLE `journal_entries` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `life_event_categories` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `account_id` int unsigned NOT NULL, + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `default_life_event_category_key` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `core_monica_data` tinyint(1) NOT NULL DEFAULT '0', + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `life_event_categories_account_id_foreign` (`account_id`), + CONSTRAINT `life_event_categories_account_id_foreign` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `life_event_categories` DISABLE KEYS */; +/*!40000 ALTER TABLE `life_event_categories` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `life_event_types` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `account_id` int unsigned NOT NULL, + `life_event_category_id` int unsigned NOT NULL, + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `default_life_event_type_key` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `core_monica_data` tinyint(1) NOT NULL DEFAULT '0', + `specific_information_structure` text COLLATE utf8mb4_unicode_ci, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `life_event_types_account_id_foreign` (`account_id`), + KEY `life_event_types_life_event_category_id_foreign` (`life_event_category_id`), + CONSTRAINT `life_event_types_account_id_foreign` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE, + CONSTRAINT `life_event_types_life_event_category_id_foreign` FOREIGN KEY (`life_event_category_id`) REFERENCES `life_event_categories` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `life_event_types` DISABLE KEYS */; +/*!40000 ALTER TABLE `life_event_types` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `life_events` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `account_id` int unsigned NOT NULL, + `contact_id` int unsigned NOT NULL, + `life_event_type_id` int unsigned NOT NULL, + `reminder_id` int unsigned DEFAULT NULL, + `name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `note` mediumtext COLLATE utf8mb4_unicode_ci, + `happened_at` datetime NOT NULL, + `happened_at_month_unknown` tinyint(1) NOT NULL DEFAULT '0', + `happened_at_day_unknown` tinyint(1) NOT NULL DEFAULT '0', + `specific_information` text COLLATE utf8mb4_unicode_ci, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `life_events_account_id_foreign` (`account_id`), + KEY `life_events_contact_id_foreign` (`contact_id`), + KEY `life_events_life_event_type_id_foreign` (`life_event_type_id`), + KEY `life_events_reminder_id_foreign` (`reminder_id`), + CONSTRAINT `life_events_account_id_foreign` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE, + CONSTRAINT `life_events_contact_id_foreign` FOREIGN KEY (`contact_id`) REFERENCES `contacts` (`id`) ON DELETE CASCADE, + CONSTRAINT `life_events_life_event_type_id_foreign` FOREIGN KEY (`life_event_type_id`) REFERENCES `life_event_types` (`id`) ON DELETE CASCADE, + CONSTRAINT `life_events_reminder_id_foreign` FOREIGN KEY (`reminder_id`) REFERENCES `reminders` (`id`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `life_events` DISABLE KEYS */; +/*!40000 ALTER TABLE `life_events` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `messages` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `account_id` int unsigned NOT NULL, + `contact_id` int unsigned NOT NULL, + `conversation_id` int unsigned NOT NULL, + `content` longtext COLLATE utf8mb4_unicode_ci NOT NULL, + `written_at` datetime NOT NULL, + `written_by_me` tinyint(1) NOT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `messages_conversation_id_foreign` (`conversation_id`), + KEY `messages_account_id_foreign` (`account_id`), + KEY `messages_contact_id_foreign` (`contact_id`), + CONSTRAINT `messages_account_id_foreign` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE, + CONSTRAINT `messages_contact_id_foreign` FOREIGN KEY (`contact_id`) REFERENCES `contacts` (`id`) ON DELETE CASCADE, + CONSTRAINT `messages_conversation_id_foreign` FOREIGN KEY (`conversation_id`) REFERENCES `conversations` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `messages` DISABLE KEYS */; +/*!40000 ALTER TABLE `messages` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `metadata_love_relationships` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `account_id` int unsigned NOT NULL, + `relationship_id` int unsigned NOT NULL, + `is_active` tinyint(1) NOT NULL, + `notes` mediumtext COLLATE utf8mb4_unicode_ci, + `meet_date` datetime DEFAULT NULL, + `official_date` datetime DEFAULT NULL, + `breakup_date` datetime DEFAULT NULL, + `breakup_reason` mediumtext COLLATE utf8mb4_unicode_ci, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `metadata_love_relationships_account_id_foreign` (`account_id`), + KEY `metadata_love_relationships_relationship_id_foreign` (`relationship_id`), + CONSTRAINT `metadata_love_relationships_account_id_foreign` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE, + CONSTRAINT `metadata_love_relationships_relationship_id_foreign` FOREIGN KEY (`relationship_id`) REFERENCES `relationships` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `metadata_love_relationships` DISABLE KEYS */; +/*!40000 ALTER TABLE `metadata_love_relationships` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `migrations` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `migration` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `batch` int NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=263 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `migrations` DISABLE KEYS */; +INSERT INTO `migrations` VALUES (1,'2014_10_12_000000_create_users_table',1),(2,'2014_10_12_100000_create_password_resets_table',1),(3,'2016_06_01_000001_create_oauth_auth_codes_table',1),(4,'2016_06_01_000002_create_oauth_access_tokens_table',1),(5,'2016_06_01_000003_create_oauth_refresh_tokens_table',1),(6,'2016_06_01_000004_create_oauth_clients_table',1),(7,'2016_06_01_000005_create_oauth_personal_access_clients_table',1),(8,'2016_06_07_234741_create_account_table',1),(9,'2016_06_08_003006_add_account_info_table',1),(10,'2016_06_08_005413_create_contacts_table',1),(11,'2016_06_25_224219_create_reminder_type_table',1),(12,'2016_06_28_191025_create_tasks_table',1),(13,'2016_06_30_185050_create_notes_table',1),(14,'2016_07_25_133835_add_width_field',1),(15,'2016_08_28_122938_create_kids_table',1),(16,'2016_08_28_215159_create_relations_table',1),(17,'2016_09_03_202027_add_reminder_id_to_contacts',1),(18,'2016_09_05_134937_add_last_talked_to_field',1),(19,'2016_09_05_135927_add_people_id_to_contacts',1),(20,'2016_09_05_145111_add_name_info_to_peoples',1),(21,'2016_09_06_213550_create_activity_type_table',1),(22,'2016_09_10_164406_create_jobs_table',1),(23,'2016_09_10_170122_create_notifications_table',1),(24,'2016_09_12_014120_create_failed_jobs_table',1),(25,'2016_09_30_014720_add_kid_to_reminder',1),(26,'2016_10_15_024156_add_deleted_at_to_users',1),(27,'2016_10_19_155139_create_cache_table',1),(28,'2016_10_19_155800_create_sessions_table',1),(29,'2016_10_21_022941_add_statistics_table',1),(30,'2016_10_24_013543_add_journal_setting_to_users',1),(31,'2016_10_24_014257_create_journal_tables',1),(32,'2016_10_28_002518_add_metric_to_settings',1),(33,'2016_11_01_014353_create_activities_table',1),(34,'2016_11_01_015957_add_icon_column',1),(35,'2016_11_03_150307_add_activity_location_to_activities',1),(36,'2016_11_09_013049_add_events_table',1),(37,'2016_12_08_011555_remove_type_from_notes',1),(38,'2016_12_13_133945_add_gifts_table',1),(39,'2016_12_28_150831_change_title_column',1),(40,'2017_01_14_200815_add_facebook_columns_to_users_table',1),(41,'2017_01_15_045025_add_colors_to_users',1),(42,'2017_01_22_142645_add_fields_to_contacts',1),(43,'2017_01_23_043831_change_people_to_contact_for_kids',1),(44,'2017_01_26_013524_change_people_to_significantother',1),(45,'2017_01_26_022852_change_notes_to_contact',1),(46,'2017_01_26_034553_add_notes_count_to_contact',1),(47,'2017_01_27_024356_change_people_in_events',1),(48,'2017_01_28_180156_remove_deleted_at_from_significant_others',1),(49,'2017_01_28_184901_remove_deleted_at_from_kids',1),(50,'2017_01_28_193913_remove_deleted_at_from_notes',1),(51,'2017_01_28_222114_remove_viewed_at_from_contacts',1),(52,'2017_01_29_175146_remove_delete_at_from_activities',1),(53,'2017_01_29_175629_add_number_activities_to_contacts',1),(54,'2017_01_31_025849_add_activity_statistics_table',1),(55,'2017_02_02_232450_add_confirmation',1),(56,'2017_02_04_225618_change_reminders_table',1),(57,'2017_02_05_035925_add_gifts_metrics_to_contacts',1),(58,'2017_02_05_041740_change_gifts_table',1),(59,'2017_02_05_042122_change_people_to_contact_for_gifts',1),(60,'2017_02_07_041607_change_tasks_table',1),(61,'2017_02_07_051355_add_number_tasks_to_contact',1),(62,'2017_02_08_002251_change_number_tasks_contact',1),(63,'2017_02_08_025358_add_sort_preferences_to_users',1),(64,'2017_02_10_195613_remove_notifications_table',1),(65,'2017_02_10_214714_remove_people_table',1),(66,'2017_02_10_215405_remove_entities_table',1),(67,'2017_02_10_215705_remove_deleted_at_from_contact',1),(68,'2017_02_10_224355_calculate_statistics',1),(69,'2017_02_11_154900_add_avatars_to_contacts',1),(70,'2017_02_12_134220_create_entries_table',1),(71,'2017_05_03_155254_move_significant_other_data',1),(72,'2017_05_04_164723_remove_contact_encryption',1),(73,'2017_05_04_185921_add_title_to_activities',1),(74,'2017_05_04_193252_alter_activity_nullable',1),(75,'2017_05_08_164514_remove_encryption_tasks',1),(76,'2017_05_30_002239_remove_predefined_reminders',1),(77,'2017_05_30_023116_create_money_table',1),(78,'2017_06_07_173437_add_multiple_genders_choices',1),(79,'2017_06_10_152945_add_social_networks_to_contacts',1),(80,'2017_06_10_155349_create_currencies_data',1),(81,'2017_06_11_025227_remove_encryption_journal',1),(82,'2017_06_11_110735_change_unique_constraint_for_contacts',1),(83,'2017_06_13_035059_remove_gifts_encryption',1),(84,'2017_06_13_195740_add_company_to_contacts',1),(85,'2017_06_14_131803_remove_bern_timezone',1),(86,'2017_06_14_132911_add_zar_currency_to_currencies_table',1),(87,'2017_06_16_215256_add_about_who_to_reminders',1),(88,'2017_06_17_010900_fix_contacts_table',1),(89,'2017_06_17_153814_refactor_user_table',1),(90,'2017_06_19_105842_add_stripe_fields_to_users',1),(91,'2017_06_20_121345_add_invitations_statistics',1),(92,'2017_06_22_210813_add_name_order_to_users',1),(93,'2017_06_27_134704_create_import_table',1),(94,'2017_06_29_211725_add_import_job_to_statistics',1),(95,'2017_06_29_230523_add_gravatar_url_to_users',1),(96,'2017_07_02_155736_create_tags_table',1),(97,'2017_07_04_132743_add_tags_to_statistics',1),(98,'2017_07_09_164312_update_bad_translation_key',1),(99,'2017_07_12_014244_create_calls_table',1),(100,'2017_07_17_005012_drop_reminders_count_from_contacts',1),(101,'2017_07_18_215312_add_danish_kroner_to_currencies_table',1),(102,'2017_07_18_215758_add_indian_rupee_to_currencies_table',1),(103,'2017_07_19_094503_add_brazilian_real_to_currencies',1),(104,'2017_07_22_153209_create_instance_table',1),(105,'2017_07_26_220021_change_contacts_table',1),(106,'2017_08_02_152838_change_string_to_boolean_for_reminders',1),(107,'2017_08_06_085629_change_events_data',1),(108,'2017_08_06_153253_move_kids_to_contacts',1),(109,'2017_08_16_041431_add_contact_avatar_location',1),(110,'2017_08_21_224835_remove_paid_limitations_for_current_users',1),(111,'2017_09_10_125918_remove_unusued_counters',1),(112,'2017_09_13_095923_add_tracking_table',1),(113,'2017_09_13_191714_add_partial_notion',1),(114,'2017_10_14_083556_change_gift_column_structure',1),(115,'2017_10_17_170803_change_gift_structure',1),(116,'2017_10_19_134816_create_activity_contact_table',1),(117,'2017_10_19_135215_move_activities_to_pivot_table',1),(118,'2017_10_25_102923_remove_contact_id_activities_table',1),(119,'2017_11_01_122541_add_met_through_to_contacts',1),(120,'2017_11_02_202601_add_is_dead_to_contacts',1),(121,'2017_11_10_174654_create_contact_fields_table',1),(122,'2017_11_10_181043_migrate_contacts_information',1),(123,'2017_11_10_202620_move_addresses_from_contact_to_addresses',1),(124,'2017_11_10_204035_delete_contact_fields_from_contacts',1),(125,'2017_11_20_115635_change-amount-to-double-on-debts',1),(126,'2017_11_27_083043_add_more_statistics',1),(127,'2017_11_27_134403_add_new_avatar_to_contacts',1),(128,'2017_11_27_202857_change_tasks_table_structure',1),(129,'2017_12_01_113748_update_notes',1),(130,'2017_12_04_164831_create_ages_table',1),(131,'2017_12_04_165421_move_ages_data',1),(132,'2017_12_10_181535_remove_important_dates_table',1),(133,'2017_12_10_205328_add_account_id_to_activities',1),(134,'2017_12_10_214545_add_last_consulted_at_to_contacts',1),(135,'2017_12_13_115857_create_day_table',1),(136,'2017_12_21_163616_update_journal_entries_with_existing_activities',1),(137,'2017_12_21_170327_add_google2fa_secret_to_users',1),(138,'2017_12_24_115641_create_pets_table',1),(139,'2017_12_31_114224_add_dashboard_tab_to_users',1),(140,'2018_01_15_105858_create_additional_reminders_table',1),(141,'2018_01_16_203358_add_gift_received',1),(142,'2018_01_16_212320_rename_gift_columns',1),(143,'2018_01_17_230820_add_gift_tab_view_to_users',1),(144,'2018_01_27_014146_add_custom_gender',1),(145,'2018_02_25_202752_change_locale_in_db',1),(146,'2018_02_28_223747_update_notification_table',1),(147,'2018_03_03_204440_create_relationship_type_table',1),(148,'2018_03_18_085815_populate_default_relationship_type_tables',1),(149,'2018_03_18_090209_populate_relationship_type_tables_with_default_values',1),(150,'2018_03_18_090345_migrate_current_relationship_table_to_new_relationship_structure',1),(151,'2018_03_24_083258_migrate_offsprings',1),(152,'2018_04_04_220850_create_default_modules_table',1),(153,'2018_04_04_222608_create_account_modules_table',1),(154,'2018_04_10_205655_fix_production_error',1),(155,'2018_04_10_222515_migrate-modules',1),(156,'2018_04_13_131008_fix-contacts-data',1),(157,'2018_04_13_205231_create_changes_table',1),(158,'2018_04_14_081052_fix_wrong_gender',1),(159,'2018_04_19_190239_stay_in_touch',1),(160,'2018_05_06_061227_external_countries',1),(161,'2018_05_06_194710_delete_reminder_sent_table',1),(162,'2018_05_07_070458_create_terms_table',1),(163,'2018_05_13_110706_add_ex_wife_husband_relationship',1),(164,'2018_05_16_143631_add_nickname_to_contacts',1),(165,'2018_05_16_214222_add_timestamps_to_currencies',1),(166,'2018_05_20_121028_accept_terms',1),(167,'2018_05_20_225034_change_name_order_user-_preferencies',1),(168,'2018_05_24_160546_fix-inconsistant-reminder-time',1),(169,'2018_06_10_191450_add_love_metadata_relationshisp',1),(170,'2018_06_10_221746_migrate_entries_objects',1),(171,'2018_06_11_184017_change_default_user_table',1),(172,'2018_06_13_000100_create_u2f_key_table',1),(173,'2018_06_14_212502_change_default_name_order_user_table',1),(174,'2018_07_03_204220_create_default_activity_type_groups_table',1),(175,'2018_07_08_104306_update-timestamps-timezone',1),(176,'2018_07_26_104306_create-conversations',1),(177,'2018_08_06_145046_add_starred_to_contacts',1),(178,'2018_08_09_18000_fix-empty-reminder-time',1),(179,'2018_08_18_180426_add_legacy_free_plan',1),(180,'2018_08_29_124804_add_conversations_to_statistics',1),(181,'2018_08_29_222051_add_conversations_to_modules',1),(182,'2018_08_31_020908_create_life_events_table',1),(183,'2018_09_02_150531_contact_archiving',1),(184,'2018_09_05_025008_add_default_profile_view',1),(185,'2018_09_05_213507_mark_modules_migrated',1),(186,'2018_09_13_135926_add_description_field_to_contacts',1),(187,'2018_09_18_142844_remove_events',1),(188,'2018_09_23_024528_add_documents_table',1),(189,'2018_09_29_114125_add_reminder_to_life_events',1),(190,'2018_10_01_211757_add_number_of_views',1),(191,'2018_10_04_181116_life_event_vehicle',1),(192,'2018_10_07_120133_fix_json_column',1),(193,'2018_10_16_000703_add_documents_to_module_table',1),(194,'2018_10_19_081816_life_event_tattoo',1),(195,'2018_10_27_230346_fix_non_english_tab_slugs',1),(196,'2018_10_28_165814_email_verified',1),(197,'2018_11_11_145035_remove_changelogs_table',1),(198,'2018_11_15_172333_make_contact_id_nullable_in_tasks',1),(199,'2018_11_18_021908_create_images_table',1),(200,'2018_11_21_212932_add_contacts_uuid',1),(201,'2018_11_25_020818_add_contact_photo_table',1),(202,'2018_11_30_154729_recovery_codes',1),(203,'2018_12_08_233140_add_who_called_to_calls',1),(204,'2018_12_09_023232_add_emotions_table',1),(205,'2018_12_09_145956_create_emotion_call_table',1),(206,'2018_12_16_195440_add_gps_coordinates_to_addressess',1),(207,'2018_12_19_002819_create_places_table',1),(208,'2018_12_19_003444_move_addresses_data',1),(209,'2018_12_21_235418_add_weather_table',1),(210,'2018_12_22_021123_add_weather_preferences_to_users',1),(211,'2018_12_22_200413_add_reminder_initial_date_to_reminders',1),(212,'2018_12_24_164256_add_companies_table',1),(213,'2018_12_24_220019_add_occupations_table',1),(214,'2018_12_25_001736_add_linkedin_to_default_contact_field_type',1),(215,'2018_12_25_012011_move_linkedin_data_to_contact_field_type',1),(216,'2018_12_29_091017_default_temperature_scale',1),(217,'2018_12_29_135516_sync_token',1),(218,'2019_01_05_152329_add_reminder_ids_to_contacts',1),(219,'2019_01_05_152405_migrate_previous_remiders',1),(220,'2019_01_05_152456_drop_special_date_id_from_reminders',1),(221,'2019_01_05_152526_schedule_new_reminders',1),(222,'2019_01_05_202557_add_foreign_keys_to_reminder',1),(223,'2019_01_05_202748_add_foreign_key_to_reminder_rule',1),(224,'2019_01_05_202938_add_foreign_key_to_contacts',1),(225,'2019_01_05_203201_add_foreign_key_for_reminder_in_life-events_table',1),(226,'2019_01_06_135133_update_u2f_key_table',1),(227,'2019_01_06_150143_add_inactive_flag_to_reminders',1),(228,'2019_01_06_190036_u2f_key_name',1),(229,'2019_01_11_142944_add_foreign_keys_to_activities',1),(230,'2019_01_11_183717_change_activities_date_type',1),(231,'2019_01_17_093812_add_admin_user',1),(232,'2019_01_18_142032_add_dav_uuid',1),(233,'2019_01_22_034555_create_emotion_activity_table',1),(234,'2019_01_24_221539_change_activity_model_location',1),(235,'2019_01_31_223600_add_swiss_chf_to_currencies_table',1),(236,'2019_02_08_234959_remove_users_without_account',1),(237,'2019_02_09_200203_add_gender_type',1),(238,'2019_02_17_112452_add_default_gender',1),(239,'2019_02_20_205744_allow_gender_null',1),(240,'2019_02_24_223855_remove_relation_type_name',1),(241,'2019_03_27_103012_set_default_profile_links',1),(242,'2019_03_29_163611_add_webauthn',1),(243,'2019_05_05_194746_add_cron_schedule',1),(244,'2019_05_15_205533_rename_preferences',1),(245,'2019_05_26_000000_add_relationship_table_indexes',1),(246,'2019_05_27_000000_populate_relationship_type_tables_with_stepparent_values',1),(247,'2019_08_12_213308_change_avatars_structure',1),(248,'2019_08_12_222938_create_avatars_for_existing_contacts',1),(249,'2019_08_13_160332_add_me_contact_on_user',1),(250,'2019_08_14_091427_update_stripe_columns',1),(251,'2019_09_04_075311_fix_tattoo_or_piercing_translation',1),(252,'2019_12_17_024553_add_foreign_keys',1),(253,'2019_12_21_100315_change_gift_status',1),(254,'2019_12_21_194559_add_photo_gift',1),(255,'2019_12_27_23533_rename_picnicked',1),(256,'2020_02_03_015403_create_audit_log_table',1),(257,'2020_02_18_211620_add_contact_field_label',1),(258,'2020_03_22_132429_rename_birthday_reminder_title_deceased',1),(259,'2020_04_24_185810_remove_duplicate_currency',1),(260,'2020_04_24_205810_currencies_table_seed',1),(261,'2020_04_24_212138_update_amount_format',1),(262,'2020_05_08_072433_google2fa_column_size',1); +/*!40000 ALTER TABLE `migrations` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `modules` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `account_id` int unsigned NOT NULL, + `key` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `translation_key` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `active` tinyint(1) NOT NULL DEFAULT '1', + `delible` tinyint(1) NOT NULL DEFAULT '0', + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `modules_account_id_foreign` (`account_id`), + CONSTRAINT `modules_account_id_foreign` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `modules` DISABLE KEYS */; +/*!40000 ALTER TABLE `modules` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `notes` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `account_id` int unsigned NOT NULL, + `contact_id` int unsigned NOT NULL, + `body` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL, + `is_favorited` tinyint(1) NOT NULL DEFAULT '0', + `favorited_at` date DEFAULT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `notes_account_id_foreign` (`account_id`), + KEY `notes_contact_id_foreign` (`contact_id`), + CONSTRAINT `notes_account_id_foreign` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE, + CONSTRAINT `notes_contact_id_foreign` FOREIGN KEY (`contact_id`) REFERENCES `contacts` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `notes` DISABLE KEYS */; +/*!40000 ALTER TABLE `notes` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `oauth_access_tokens` ( + `id` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, + `user_id` bigint unsigned DEFAULT NULL, + `client_id` bigint unsigned NOT NULL, + `name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `scopes` text COLLATE utf8mb4_unicode_ci, + `revoked` tinyint(1) NOT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + `expires_at` datetime DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `oauth_access_tokens_user_id_index` (`user_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `oauth_access_tokens` DISABLE KEYS */; +/*!40000 ALTER TABLE `oauth_access_tokens` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `oauth_auth_codes` ( + `id` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, + `user_id` bigint unsigned NOT NULL, + `client_id` bigint unsigned NOT NULL, + `scopes` text COLLATE utf8mb4_unicode_ci, + `revoked` tinyint(1) NOT NULL, + `expires_at` datetime DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `oauth_auth_codes_user_id_index` (`user_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `oauth_auth_codes` DISABLE KEYS */; +/*!40000 ALTER TABLE `oauth_auth_codes` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `oauth_clients` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `user_id` bigint unsigned DEFAULT NULL, + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `secret` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `redirect` text COLLATE utf8mb4_unicode_ci NOT NULL, + `personal_access_client` tinyint(1) NOT NULL, + `password_client` tinyint(1) NOT NULL, + `revoked` tinyint(1) NOT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `oauth_clients_user_id_index` (`user_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `oauth_clients` DISABLE KEYS */; +/*!40000 ALTER TABLE `oauth_clients` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `oauth_personal_access_clients` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `client_id` bigint unsigned NOT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `oauth_personal_access_clients` DISABLE KEYS */; +/*!40000 ALTER TABLE `oauth_personal_access_clients` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `oauth_refresh_tokens` ( + `id` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, + `access_token_id` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL, + `revoked` tinyint(1) NOT NULL, + `expires_at` datetime DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `oauth_refresh_tokens` DISABLE KEYS */; +/*!40000 ALTER TABLE `oauth_refresh_tokens` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `occupations` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `account_id` int unsigned NOT NULL, + `contact_id` int unsigned NOT NULL, + `company_id` int unsigned NOT NULL, + `title` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `description` varchar(1000) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `salary` int DEFAULT NULL, + `salary_unit` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `currently_works_here` tinyint(1) DEFAULT '0', + `start_date` date DEFAULT NULL, + `end_date` date DEFAULT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `occupations_account_id_foreign` (`account_id`), + KEY `occupations_contact_id_foreign` (`contact_id`), + KEY `occupations_company_id_foreign` (`company_id`), + CONSTRAINT `occupations_account_id_foreign` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE, + CONSTRAINT `occupations_company_id_foreign` FOREIGN KEY (`company_id`) REFERENCES `companies` (`id`) ON DELETE CASCADE, + CONSTRAINT `occupations_contact_id_foreign` FOREIGN KEY (`contact_id`) REFERENCES `contacts` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `occupations` DISABLE KEYS */; +/*!40000 ALTER TABLE `occupations` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `password_resets` ( + `email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `token` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `created_at` timestamp NOT NULL, + KEY `password_resets_email_index` (`email`), + KEY `password_resets_token_index` (`token`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `password_resets` DISABLE KEYS */; +/*!40000 ALTER TABLE `password_resets` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `pet_categories` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `is_common` tinyint(1) NOT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `pet_categories` DISABLE KEYS */; +INSERT INTO `pet_categories` VALUES (1,'reptile',0,NULL,NULL),(2,'bird',0,NULL,NULL),(3,'cat',1,NULL,NULL),(4,'dog',1,NULL,NULL),(5,'fish',1,NULL,NULL),(6,'hamster',0,NULL,NULL),(7,'horse',0,NULL,NULL),(8,'rabbit',0,NULL,NULL),(9,'rat',0,NULL,NULL),(10,'small_animal',0,NULL,NULL),(11,'other',0,NULL,NULL); +/*!40000 ALTER TABLE `pet_categories` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `pets` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `account_id` int unsigned NOT NULL, + `contact_id` int unsigned NOT NULL, + `pet_category_id` int unsigned NOT NULL, + `name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `pets_account_id_foreign` (`account_id`), + KEY `pets_contact_id_foreign` (`contact_id`), + KEY `pets_pet_category_id_foreign` (`pet_category_id`), + CONSTRAINT `pets_account_id_foreign` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE, + CONSTRAINT `pets_contact_id_foreign` FOREIGN KEY (`contact_id`) REFERENCES `contacts` (`id`) ON DELETE CASCADE, + CONSTRAINT `pets_pet_category_id_foreign` FOREIGN KEY (`pet_category_id`) REFERENCES `pet_categories` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `pets` DISABLE KEYS */; +/*!40000 ALTER TABLE `pets` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `photos` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `account_id` int unsigned NOT NULL, + `original_filename` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `new_filename` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `filesize` int DEFAULT NULL, + `mime_type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `photos_account_id_foreign` (`account_id`), + CONSTRAINT `photos_account_id_foreign` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `photos` DISABLE KEYS */; +/*!40000 ALTER TABLE `photos` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `places` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `account_id` int unsigned NOT NULL, + `street` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `city` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `province` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `postal_code` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `country` char(3) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `latitude` double DEFAULT NULL, + `longitude` double DEFAULT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `places_account_id_foreign` (`account_id`), + CONSTRAINT `places_account_id_foreign` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `places` DISABLE KEYS */; +/*!40000 ALTER TABLE `places` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `recovery_codes` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `account_id` int unsigned NOT NULL, + `user_id` int unsigned NOT NULL, + `recovery` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `used` tinyint(1) NOT NULL DEFAULT '0', + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `recovery_codes_account_id_foreign` (`account_id`), + KEY `recovery_codes_user_id_foreign` (`user_id`), + CONSTRAINT `recovery_codes_account_id_foreign` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE, + CONSTRAINT `recovery_codes_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `recovery_codes` DISABLE KEYS */; +/*!40000 ALTER TABLE `recovery_codes` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `relationship_type_groups` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `account_id` int unsigned NOT NULL, + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `delible` tinyint(1) NOT NULL DEFAULT '0', + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `relationship_type_groups_account_id_name_index` (`account_id`,`name`), + CONSTRAINT `relationship_type_groups_account_id_foreign` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `relationship_type_groups` DISABLE KEYS */; +/*!40000 ALTER TABLE `relationship_type_groups` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `relationship_types` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `account_id` int unsigned NOT NULL, + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_reverse_relationship` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `relationship_type_group_id` int unsigned NOT NULL, + `delible` tinyint(1) NOT NULL DEFAULT '0', + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `relationship_types_account_id_foreign` (`account_id`), + KEY `relationship_types_relationship_type_group_id_foreign` (`relationship_type_group_id`), + CONSTRAINT `relationship_types_account_id_foreign` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE, + CONSTRAINT `relationship_types_relationship_type_group_id_foreign` FOREIGN KEY (`relationship_type_group_id`) REFERENCES `relationship_type_groups` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `relationship_types` DISABLE KEYS */; +/*!40000 ALTER TABLE `relationship_types` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `relationships` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `account_id` int unsigned NOT NULL, + `relationship_type_id` int unsigned NOT NULL, + `contact_is` int unsigned NOT NULL, + `of_contact` int unsigned NOT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `relationships_account_id_foreign` (`account_id`), + KEY `relationships_relationship_type_id_foreign` (`relationship_type_id`), + KEY `relationships_contact_is_foreign` (`contact_is`), + KEY `relationships_of_contact_foreign` (`of_contact`), + CONSTRAINT `relationships_account_id_foreign` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE, + CONSTRAINT `relationships_contact_is_foreign` FOREIGN KEY (`contact_is`) REFERENCES `contacts` (`id`) ON DELETE CASCADE, + CONSTRAINT `relationships_of_contact_foreign` FOREIGN KEY (`of_contact`) REFERENCES `contacts` (`id`) ON DELETE CASCADE, + CONSTRAINT `relationships_relationship_type_id_foreign` FOREIGN KEY (`relationship_type_id`) REFERENCES `relationship_types` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `relationships` DISABLE KEYS */; +/*!40000 ALTER TABLE `relationships` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `reminder_outbox` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `account_id` int unsigned NOT NULL, + `reminder_id` int unsigned NOT NULL, + `user_id` int unsigned NOT NULL, + `planned_date` date NOT NULL, + `nature` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'reminder', + `notification_number_days_before` int DEFAULT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `reminder_outbox_account_id_foreign` (`account_id`), + KEY `reminder_outbox_reminder_id_foreign` (`reminder_id`), + KEY `reminder_outbox_user_id_foreign` (`user_id`), + CONSTRAINT `reminder_outbox_account_id_foreign` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE, + CONSTRAINT `reminder_outbox_reminder_id_foreign` FOREIGN KEY (`reminder_id`) REFERENCES `reminders` (`id`) ON DELETE CASCADE, + CONSTRAINT `reminder_outbox_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `reminder_outbox` DISABLE KEYS */; +/*!40000 ALTER TABLE `reminder_outbox` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `reminder_rules` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `account_id` int unsigned NOT NULL, + `number_of_days_before` int NOT NULL, + `active` tinyint(1) NOT NULL DEFAULT '1', + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `reminder_rules_account_id_foreign` (`account_id`), + CONSTRAINT `reminder_rules_account_id_foreign` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `reminder_rules` DISABLE KEYS */; +/*!40000 ALTER TABLE `reminder_rules` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `reminder_sent` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `account_id` int unsigned NOT NULL, + `reminder_id` int unsigned DEFAULT NULL, + `user_id` int unsigned NOT NULL, + `planned_date` date NOT NULL, + `sent_date` datetime NOT NULL, + `nature` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'reminder', + `frequency_type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `frequency_number` int DEFAULT NULL, + `html_content` longtext COLLATE utf8mb4_unicode_ci, + `text_content` longtext COLLATE utf8mb4_unicode_ci, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `reminder_sent_account_id_foreign` (`account_id`), + KEY `reminder_sent_reminder_id_foreign` (`reminder_id`), + KEY `reminder_sent_user_id_foreign` (`user_id`), + CONSTRAINT `reminder_sent_account_id_foreign` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE, + CONSTRAINT `reminder_sent_reminder_id_foreign` FOREIGN KEY (`reminder_id`) REFERENCES `reminders` (`id`) ON DELETE SET NULL, + CONSTRAINT `reminder_sent_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `reminder_sent` DISABLE KEYS */; +/*!40000 ALTER TABLE `reminder_sent` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `reminders` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `account_id` int unsigned NOT NULL, + `contact_id` int unsigned NOT NULL, + `initial_date` date NOT NULL, + `title` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci, + `description` longtext COLLATE utf8mb4_unicode_ci, + `frequency_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `frequency_number` int DEFAULT NULL, + `delible` tinyint(1) NOT NULL DEFAULT '1', + `inactive` tinyint(1) NOT NULL DEFAULT '0', + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `reminders_account_id_foreign` (`account_id`), + KEY `reminders_contact_id_foreign` (`contact_id`), + CONSTRAINT `reminders_account_id_foreign` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE, + CONSTRAINT `reminders_contact_id_foreign` FOREIGN KEY (`contact_id`) REFERENCES `contacts` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `reminders` DISABLE KEYS */; +/*!40000 ALTER TABLE `reminders` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `sessions` ( + `id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `user_id` int DEFAULT NULL, + `ip_address` varchar(45) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `user_agent` text COLLATE utf8mb4_unicode_ci, + `payload` text COLLATE utf8mb4_unicode_ci NOT NULL, + `last_activity` int NOT NULL, + UNIQUE KEY `sessions_id_unique` (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `sessions` DISABLE KEYS */; +/*!40000 ALTER TABLE `sessions` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `special_dates` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `account_id` int unsigned NOT NULL, + `contact_id` int unsigned NOT NULL, + `uuid` char(36) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `is_age_based` tinyint(1) NOT NULL DEFAULT '0', + `is_year_unknown` tinyint(1) NOT NULL DEFAULT '0', + `date` date NOT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `special_dates_account_id_uuid_index` (`account_id`,`uuid`), + KEY `special_dates_contact_id_foreign` (`contact_id`), + CONSTRAINT `special_dates_account_id_foreign` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE, + CONSTRAINT `special_dates_contact_id_foreign` FOREIGN KEY (`contact_id`) REFERENCES `contacts` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `special_dates` DISABLE KEYS */; +/*!40000 ALTER TABLE `special_dates` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `statistics` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `number_of_users` int NOT NULL, + `number_of_contacts` int NOT NULL, + `number_of_notes` int NOT NULL, + `number_of_oauth_access_tokens` int NOT NULL, + `number_of_oauth_clients` int NOT NULL, + `number_of_offsprings` int NOT NULL, + `number_of_progenitors` int NOT NULL, + `number_of_relationships` int NOT NULL, + `number_of_subscriptions` int NOT NULL, + `number_of_reminders` int NOT NULL, + `number_of_tasks` int NOT NULL, + `number_of_kids` int NOT NULL, + `number_of_activities` int NOT NULL, + `number_of_addresses` int NOT NULL, + `number_of_api_calls` int NOT NULL, + `number_of_calls` int NOT NULL, + `number_of_contact_fields` int NOT NULL, + `number_of_contact_field_types` int NOT NULL, + `number_of_debts` int NOT NULL, + `number_of_entries` int NOT NULL, + `number_of_gifts` int NOT NULL, + `number_of_invitations_sent` int DEFAULT NULL, + `number_of_accounts_with_more_than_one_user` int DEFAULT NULL, + `number_of_tags` int DEFAULT NULL, + `number_of_import_jobs` int DEFAULT NULL, + `number_of_conversations` int DEFAULT NULL, + `number_of_messages` int DEFAULT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `statistics` DISABLE KEYS */; +/*!40000 ALTER TABLE `statistics` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `subscriptions` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `account_id` int NOT NULL, + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `stripe_id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `stripe_status` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `stripe_plan` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `quantity` int NOT NULL, + `trial_ends_at` timestamp NULL DEFAULT NULL, + `ends_at` timestamp NULL DEFAULT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `subscriptions_account_id_stripe_status_index` (`account_id`,`stripe_status`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `subscriptions` DISABLE KEYS */; +/*!40000 ALTER TABLE `subscriptions` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `synctoken` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `account_id` int unsigned NOT NULL, + `user_id` int unsigned NOT NULL, + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'contacts', + `timestamp` timestamp NOT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `synctoken_user_id_foreign` (`user_id`), + KEY `synctoken_account_id_user_id_name_index` (`account_id`,`user_id`,`name`), + CONSTRAINT `synctoken_account_id_foreign` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE, + CONSTRAINT `synctoken_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `synctoken` DISABLE KEYS */; +/*!40000 ALTER TABLE `synctoken` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `tags` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `account_id` int unsigned NOT NULL, + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `name_slug` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `description` mediumtext COLLATE utf8mb4_unicode_ci, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `tags_account_id_foreign` (`account_id`), + CONSTRAINT `tags_account_id_foreign` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `tags` DISABLE KEYS */; +/*!40000 ALTER TABLE `tags` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `tasks` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `account_id` int unsigned NOT NULL, + `contact_id` int unsigned DEFAULT NULL, + `uuid` char(36) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `title` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `description` longtext COLLATE utf8mb4_unicode_ci, + `completed` tinyint(1) NOT NULL DEFAULT '0', + `completed_at` datetime DEFAULT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `tasks_account_id_uuid_index` (`account_id`,`uuid`), + KEY `tasks_contact_id_foreign` (`contact_id`), + CONSTRAINT `tasks_account_id_foreign` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE, + CONSTRAINT `tasks_contact_id_foreign` FOREIGN KEY (`contact_id`) REFERENCES `contacts` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `tasks` DISABLE KEYS */; +/*!40000 ALTER TABLE `tasks` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `term_user` ( + `account_id` int unsigned NOT NULL, + `user_id` int unsigned NOT NULL, + `term_id` int unsigned NOT NULL, + `ip_address` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + KEY `term_user_account_id_foreign` (`account_id`), + KEY `term_user_user_id_foreign` (`user_id`), + KEY `term_user_term_id_foreign` (`term_id`), + CONSTRAINT `term_user_account_id_foreign` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE, + CONSTRAINT `term_user_term_id_foreign` FOREIGN KEY (`term_id`) REFERENCES `terms` (`id`) ON DELETE CASCADE, + CONSTRAINT `term_user_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `term_user` DISABLE KEYS */; +/*!40000 ALTER TABLE `term_user` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `terms` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `term_version` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `term_content` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL, + `privacy_version` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `privacy_content` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `terms` DISABLE KEYS */; +INSERT INTO `terms` VALUES (1,'2','\nScope of service\nMonica supports the following browsers:\n\nInternet Explorer (11+)\nFirefox (50+)\nChrome (latest)\nSafari (latest)\nI do not guarantee that the site will work with other browsers, but it’s very likely that it will just work.\n\nRights\nYou don’t have to provide your real name when you register to an account. You do however need a valid email address if you want to upgrade your account to the paid version, or receive reminders by email.\n\nYou have the right to close your account at any time.\n\nYou have the right to export your data at any time, in the SQL format.\n\nYour data will not be intentionally shown to other users or shared with third parties.\n\nYour personal data will not be shared with anyone without your consent.\n\nYour data is backed up every hour.\n\nIf the site ceases operation, you will receive an opportunity to export all your data before the site dies.\n\nAny new features that affect privacy will be strictly opt-in.\n\nResponsibilities\nYou will not use the site to store illegal information or data under the Canadian law (or any law).\n\nYou have to be at least 18+ to create an account and use the site.\n\nYou must not abuse the site by knowingly posting malicious code that could harm you or the other users.\n\nYou must only use the site to do things that are widely accepted as morally good.\n\nYou may not make automated requests to the site.\n\nYou may not abuse the invitation system.\n\nYou are responsible for keeping your account secure.\n\nI reserve the right to close accounts that abuse the system (thousands of contacts with hundred of thousands of reminders for instance) or use it in an unreasonable manner.\n\nOther important legal stuff\nThough I want to provide a great service, there are certain things about the service I cannot promise. For example, the services and software are provided “as-is”, at your own risk, without express or implied warranty or condition of any kind. I also disclaim any warranties of merchantability, fitness for a particular purpose or non-infringement. Monica will have no responsibility for any harm to your computer system, loss or corruption of data, or other harm that results from your access to or use of the Services or Software.\n\nThese Terms can change at any time, but I’ll never be a dick about it. Running this site is a dream come true to me, and I hope I’ll be able to run it as long as I can.\n ','2','\nMonica is an open source project. The hosted version has a premium plan that let us collect money so we can pay for the servers and additional servers, but the main goal is not to make money (otherwise we wouldn’t have opened source it).\n\nMonica comes in two flavors: you can either use our hosted version, or download it and run it yourself. In the latter case, we do not track anything at all. We don’t know that you’ve even downloaded the product. Do whatever you want with it (but respect your local laws).\n\nWhen you create your account on our hosted version, you are giving the site information about yourself that we collect. This includes your name, your email address and your password, that is encrypted before being stored. We do not store any other personal information.\n\nWhen you login to the service, we are using cookies to remember your login credentials. This is the only use we do with the cookies.\n\nMonica runs on Linode and we are the only ones, apart from Linode’s employees, who have access to those servers.\n\nWe do hourly backups of the database.\n\nYour password is encrypted with bcrypt, a password hashing algorithm that is highly secure. You can also activate two factor authentication on your account if you need an extra layer of security. Apart from those encryptions mechanism, your data is not encrypted in the database. If someone gets access to the database, they will be able to read your data. We do our best to make sure that this will never happen, but it can happen.\n\nIf a data breach happens, we will contact the users who are affected to warn them about the breach.\n\nTransactional emails are dserved through Postmark.\n\nWe use an open source tool called Sentry to track errors that happen in production. Their service records the errors, but they don’t have access to any information apart the account ID, which lets me debug what’s going on.\n\nThe site does not currently and will never show ads. It also does not, and don’t intend to, sell data to a third party, with or without your consent. We are just against this. Fuck ads.\n\nWe do no use any tracking third parties, like Google Analytics or Intercom, that track user behaviours or data, neither on the marketing site or the hosted version. We are deeply against their principles as they would use those data to profile you, which we are totally against.\n\nAll the data you put on Monica belongs to you. We do not have any rights on it. Please don’t put illegal stuff on it, otherwise we’d be in trouble.\n\nAll the information about the contacts you put on Monica are private to you. We do not cross link information between accounts or use one information in an account to populate another account (unlike Facebook for instance).\n\nWe use Stripe to collect payments made to access the paid version. We do not store credit card information or anything concerning the transactions themselves on our servers. However, as per the open source library we use to process the payments (Laravel Cashier), we store the last 4 digits of the credit card, the brand name (VISA or MasterCard). As a user, you are identified on Stripe by a random number that they generate and use.\n\nRegarding the payments, you can downgrade to the free plan whenever you like. When you do, Stripe is automatically updated and we have no way to charge you again, even if we would like to. The less we deal with payment information, the happier we are.\n\nYou can export your data at any time. You can also use the API to export all your data if you know how to do it. You can also request that we process this ourselves and send it to you. Your data will be exported in the SQL format.\n\nWhen you close your account, we immediately destroy all your personal information and don’t keep any backup. While you have control over this, we can delete an account for you if you ask us.\n\nIn certain situations, we may be required to disclose peronal data in response to lawful requests by public authorities, including to met national security or law enforcements requirements. We just hope that this never happens.\n\nIf you violate the terms of use we will terminate your account and notify you about it. However if you follow the \"don’t be a dick\" policy, nothing should ever happen to you and we’ll all be happy.\n\nMonica uses only open-source projects that are mainly hosted on Github.\n\nWe will update this privacy policy as soon as we introduce new information practices. If we do, we will send an email to the email address specified in your account. We will never be a dick about it and will never, ever, introduce something in what we do that will affect your right to the absolute privacy.','2018-04-11 22:00:00',NULL); +/*!40000 ALTER TABLE `terms` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `u2f_key` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'key', + `user_id` int unsigned NOT NULL, + `keyHandle` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `publicKey` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `certificate` text COLLATE utf8mb4_unicode_ci NOT NULL, + `counter` int NOT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `u2f_key_publickey_unique` (`publicKey`), + KEY `u2f_key_user_id_foreign` (`user_id`), + CONSTRAINT `u2f_key_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `u2f_key` DISABLE KEYS */; +/*!40000 ALTER TABLE `u2f_key` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `users` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `first_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `last_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `me_contact_id` int unsigned DEFAULT NULL, + `admin` tinyint(1) NOT NULL DEFAULT '0', + `email_verified_at` timestamp NULL DEFAULT NULL, + `password` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `remember_token` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `google2fa_secret` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `account_id` int unsigned NOT NULL, + `timezone` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `currency_id` int unsigned DEFAULT '2', + `locale` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'en', + `metric` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'fahrenheit', + `fluid_container` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'false', + `contacts_sort_order` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'firstnameAZ', + `name_order` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'firstname_lastname_nickname', + `invited_by_user_id` int unsigned DEFAULT NULL, + `dashboard_active_tab` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'calls', + `gifts_active_tab` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'ideas', + `profile_active_tab` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'notes', + `profile_new_life_event_badge_seen` tinyint(1) NOT NULL DEFAULT '0', + `temperature_scale` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT 'celsius', + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `users_email_unique` (`email`), + KEY `users_me_contact_id_foreign` (`me_contact_id`), + KEY `users_account_id_foreign` (`account_id`), + KEY `users_currency_id_foreign` (`currency_id`), + KEY `users_invited_by_user_id_foreign` (`invited_by_user_id`), + CONSTRAINT `users_account_id_foreign` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE, + CONSTRAINT `users_currency_id_foreign` FOREIGN KEY (`currency_id`) REFERENCES `currencies` (`id`) ON DELETE SET NULL, + CONSTRAINT `users_invited_by_user_id_foreign` FOREIGN KEY (`invited_by_user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL, + CONSTRAINT `users_me_contact_id_foreign` FOREIGN KEY (`me_contact_id`) REFERENCES `contacts` (`id`) ON DELETE SET NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `users` DISABLE KEYS */; +/*!40000 ALTER TABLE `users` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `weather` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `account_id` int unsigned NOT NULL, + `place_id` int unsigned NOT NULL, + `weather_json` varchar(2000) COLLATE utf8mb4_unicode_ci NOT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `weather_account_id_foreign` (`account_id`), + KEY `weather_place_id_foreign` (`place_id`), + CONSTRAINT `weather_account_id_foreign` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`) ON DELETE CASCADE, + CONSTRAINT `weather_place_id_foreign` FOREIGN KEY (`place_id`) REFERENCES `places` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `weather` DISABLE KEYS */; +/*!40000 ALTER TABLE `weather` ENABLE KEYS */; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `webauthn_keys` ( + `id` int unsigned NOT NULL AUTO_INCREMENT, + `user_id` int unsigned NOT NULL, + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'key', + `credentialId` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `transports` text COLLATE utf8mb4_unicode_ci NOT NULL, + `attestationType` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `trustPath` text COLLATE utf8mb4_unicode_ci NOT NULL, + `aaguid` text COLLATE utf8mb4_unicode_ci NOT NULL, + `credentialPublicKey` text COLLATE utf8mb4_unicode_ci NOT NULL, + `counter` int NOT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `webauthn_keys_user_id_foreign` (`user_id`), + KEY `webauthn_keys_credentialid_index` (`credentialId`), + CONSTRAINT `webauthn_keys_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +/*!40101 SET character_set_client = @saved_cs_client */; + +/*!40000 ALTER TABLE `webauthn_keys` DISABLE KEYS */; +/*!40000 ALTER TABLE `webauthn_keys` ENABLE KEYS */; +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; + diff --git a/scripts/docker/Dockerfile b/scripts/docker/Dockerfile new file mode 100644 index 0000000..7991cda --- /dev/null +++ b/scripts/docker/Dockerfile @@ -0,0 +1,181 @@ +### +### ~ Monica dev Dockerfile +### +### This file is used for dev purpose. +### The standard monica image definition will be found here: https://github.com/monicahq/docker +### This file is based off of the `apache` variant in the above mentioned repo +### + +FROM php:8.1-apache + +# opencontainers annotations https://github.com/opencontainers/image-spec/blob/master/annotations.md +LABEL org.opencontainers.image.authors="Alexis Saettler " \ + org.opencontainers.image.title="MonicaHQ, the Personal Relationship Manager" \ + org.opencontainers.image.description="This is MonicaHQ, your personal memory! MonicaHQ is like a CRM but for the friends, family, and acquaintances around you." \ + org.opencontainers.image.url="https://monicahq.com" \ + org.opencontainers.image.vendor="Monica" + +# entrypoint.sh dependencies +RUN set -ex; \ + \ + apt-get update; \ + apt-get install -y --no-install-recommends \ + bash \ + busybox-static \ + ; \ + rm -rf /var/lib/apt/lists/* + +# Install required PHP extensions +RUN set -ex; \ + \ + savedAptMark="$(apt-mark showmanual)"; \ + \ + apt-get update; \ + apt-get install -y --no-install-recommends \ + libicu-dev \ + zlib1g-dev \ + libzip-dev \ + libpng-dev \ + libxml2-dev \ + libfreetype6-dev \ + libjpeg62-turbo-dev \ + libgmp-dev \ + libmemcached-dev \ + libmagickwand-dev \ + libwebp-dev \ + ; \ + \ + debMultiarch="$(dpkg-architecture --query DEB_BUILD_MULTIARCH)"; \ + if [ ! -e /usr/include/gmp.h ]; then ln -s /usr/include/$debMultiarch/gmp.h /usr/include/gmp.h; fi;\ + docker-php-ext-configure intl; \ + docker-php-ext-configure gd --with-jpeg --with-freetype --with-webp; \ + docker-php-ext-configure gmp; \ + docker-php-ext-install -j$(nproc) \ + intl \ + zip \ + bcmath \ + gd \ + gmp \ + pdo_mysql \ + mysqli \ + soap \ + ; \ + \ +# pecl will claim success even if one install fails, so we need to perform each install separately + pecl install APCu; \ + pecl install memcached; \ + pecl install redis; \ + \ + docker-php-ext-enable \ + apcu \ + memcached \ + redis \ + ; \ + \ +# reset apt-mark's "manual" list so that "purge --auto-remove" will remove all build dependencies + apt-mark auto '.*' > /dev/null; \ + apt-mark manual $savedAptMark; \ + ldd "$(php -r 'echo ini_get("extension_dir");')"/*.so \ + | awk '/=>/ { print $3 }' \ + | sort -u \ + | xargs -r dpkg-query -S \ + | cut -d: -f1 \ + | sort -u \ + | xargs -rt apt-mark manual; \ + \ + apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \ + rm -rf /var/lib/apt/lists/* + +# Set crontab for schedules +RUN set -ex; \ + \ + mkdir -p /var/spool/cron/crontabs; \ + rm -f /var/spool/cron/crontabs/root; \ + echo '*/5 * * * * php /var/www/html/artisan schedule:run -v' > /var/spool/cron/crontabs/www-data + +# Opcache +ENV PHP_OPCACHE_VALIDATE_TIMESTAMPS="0" \ + PHP_OPCACHE_MAX_ACCELERATED_FILES="20000" \ + PHP_OPCACHE_MEMORY_CONSUMPTION="192" \ + PHP_OPCACHE_MAX_WASTED_PERCENTAGE="10" +RUN set -ex; \ + \ + docker-php-ext-enable opcache; \ + { \ + echo '[opcache]'; \ + echo 'opcache.enable=1'; \ + echo 'opcache.revalidate_freq=0'; \ + echo 'opcache.validate_timestamps=${PHP_OPCACHE_VALIDATE_TIMESTAMPS}'; \ + echo 'opcache.max_accelerated_files=${PHP_OPCACHE_MAX_ACCELERATED_FILES}'; \ + echo 'opcache.memory_consumption=${PHP_OPCACHE_MEMORY_CONSUMPTION}'; \ + echo 'opcache.max_wasted_percentage=${PHP_OPCACHE_MAX_WASTED_PERCENTAGE}'; \ + echo 'opcache.interned_strings_buffer=16'; \ + echo 'opcache.fast_shutdown=1'; \ + } > $PHP_INI_DIR/conf.d/opcache-recommended.ini; \ + \ + echo 'apc.enable_cli=1' >> $PHP_INI_DIR/conf.d/docker-php-ext-apcu.ini; \ + \ + echo 'memory_limit=512M' > $PHP_INI_DIR/conf.d/memory-limit.ini + +RUN set -ex; \ + \ + a2enmod headers rewrite remoteip; \ + { \ + echo RemoteIPHeader X-Real-IP; \ + echo RemoteIPTrustedProxy 10.0.0.0/8; \ + echo RemoteIPTrustedProxy 172.16.0.0/12; \ + echo RemoteIPTrustedProxy 192.168.0.0/16; \ + } > $APACHE_CONFDIR/conf-available/remoteip.conf; \ + a2enconf remoteip + +RUN set -ex; \ + APACHE_DOCUMENT_ROOT=/var/www/html/public; \ + sed -ri -e "s!/var/www/html!${APACHE_DOCUMENT_ROOT}!g" $APACHE_CONFDIR/sites-available/*.conf; \ + sed -ri -e "s!/var/www/!${APACHE_DOCUMENT_ROOT}!g" $APACHE_CONFDIR/apache2.conf $APACHE_CONFDIR/conf-available/*.conf + +WORKDIR /var/www/html + + +# Copy the local (outside Docker) source into the working directory, +# copy system files into their proper homes, and set file ownership +# correctly +COPY --chown=www-data:www-data . ./ + +RUN set -ex; \ + \ + mkdir -p bootstrap/cache; \ + mkdir -p storage; \ + chown -R www-data:www-data bootstrap/cache storage; \ + chmod -R g+w bootstrap/cache storage +COPY --chown=www-data:www-data .env.example .env + +# Composer installation +COPY scripts/docker/install-composer.sh /usr/local/sbin/ +RUN install-composer.sh + +# Install composer dependencies +RUN set -ex; \ + \ + mkdir -p storage/framework/views; \ + composer install --no-interaction --no-progress --no-dev; \ + composer clear-cache; \ + rm -rf .composer + +# Install node dependencies +RUN set -ex; \ + \ + curl -fsSL https://deb.nodesource.com/setup_18.x | bash -; \ + apt-get install -y nodejs; \ + npm install -g yarn; \ + yarn run inst; \ + yarn run dev; \ + \ + rm -rf /var/lib/apt/lists/* + +COPY scripts/docker/entrypoint.sh \ + scripts/docker/cron.sh \ + scripts/docker/queue.sh \ + /usr/local/bin/ + +ENTRYPOINT ["entrypoint.sh"] +CMD ["apache2-foreground"] diff --git a/scripts/docker/build.sh b/scripts/docker/build.sh new file mode 100644 index 0000000..f7c27a7 --- /dev/null +++ b/scripts/docker/build.sh @@ -0,0 +1,50 @@ +#!/bin/bash + +set -eo pipefail + +SELF_PATH=$(cd -P -- "$(dirname -- "$0")" && /bin/pwd -P) +source $SELF_PATH/../realpath.sh +ROOT=$(realpath $SELF_PATH/../..) + +version=$(git --git-dir $ROOT/.git describe --abbrev=0 --tags | sed 's/^v//') + +commit=$1 +if [ "$commit" == "--skip-build" ]; then + shift + tag=$commit + commit=$1 +fi + +if [ -z "$commit" ]; then + commit=$(git --git-dir $ROOT/.git log --pretty="%H" -n1 HEAD) + release=$(git --git-dir $ROOT/.git log --pretty="%h" -n1 HEAD) +else + shift + release=$(git --git-dir $ROOT/.git describe --abbrev=0 --tags --exact-match $commit 2>/dev/null || git --git-dir $ROOT/.git log --pretty="%h" -n1 $commit) +fi + +if [ -z "$tag" ]; then + tag=${1:-monica-dev} +fi + +echo Version +echo -n "$version" | tee config/.version + +echo -e "\nCommit" +echo -n "$commit" | tee config/.commit + +echo -e "\nRelease" +echo -n "$release" | tee config/.release + +echo -e "\n" + +# BUILD +composer install --no-progress --no-interaction --prefer-dist --optimize-autoloader --no-dev --working-dir=$ROOT +yarn --cwd $ROOT run inst +yarn --cwd $ROOT run production + +# DOCKER BUILD +if [ "$tag" != "--skip-build" ]; then + docker build -t $tag -f $SELF_PATH/Dockerfile $ROOT + rm -f config/.{version,commit,release} +fi diff --git a/scripts/docker/cron.sh b/scripts/docker/cron.sh new file mode 100644 index 0000000..f38e3a0 --- /dev/null +++ b/scripts/docker/cron.sh @@ -0,0 +1,4 @@ +#!/bin/sh +set -eu + +exec busybox crond -f -l 0 -L /proc/1/fd/1 diff --git a/scripts/docker/entrypoint.sh b/scripts/docker/entrypoint.sh new file mode 100644 index 0000000..41a3f92 --- /dev/null +++ b/scripts/docker/entrypoint.sh @@ -0,0 +1,63 @@ +#!/bin/bash + +set -Eeo pipefail + +# wait for the database to start +waitfordb() { + HOST=${DB_HOST:-mysql} + PORT=${DB_PORT:-3306} + echo "Connecting to ${HOST}:${PORT}" + + attempts=0 + max_attempts=30 + while [ $attempts -lt $max_attempts ]; do + busybox nc -w 1 "${HOST}:${PORT}" && break + echo "Waiting for ${HOST}:${PORT}..." + sleep 1 + let "attempts=attempts+1" + done + + if [ $attempts -eq $max_attempts ]; then + echo "Unable to contact your database at ${HOST}:${PORT}" + exit 1 + fi + + echo "Waiting for database to settle..." + sleep 3 +} + +if expr "$1" : "apache" 1>/dev/null || [ "$1" = "php-fpm" ]; then + + MONICADIR=/var/www/html + ARTISAN="php ${MONICADIR}/artisan" + + # Ensure storage directories are present + STORAGE=${MONICADIR}/storage + mkdir -p ${STORAGE}/logs + mkdir -p ${STORAGE}/app/public + mkdir -p ${STORAGE}/framework/views + mkdir -p ${STORAGE}/framework/cache + mkdir -p ${STORAGE}/framework/sessions + chown -R www-data:www-data ${STORAGE} + chmod -R g+rw ${STORAGE} + + if [ -z "${APP_KEY:-}" -o "$APP_KEY" = "ChangeMeBy32KeyLengthOrGenerated" ]; then + ${ARTISAN} key:generate --no-interaction + else + echo "APP_KEY already set" + fi + + # Run migrations + waitfordb + ${ARTISAN} monica:update --force -vv + + if [ ! -f "${STORAGE}/oauth-public.key" -o ! -f "${STORAGE}/oauth-private.key" ]; then + echo "Passport keys creation ..." + ${ARTISAN} passport:keys + ${ARTISAN} passport:client --personal --no-interaction + echo "! Please be careful to backup $MONICADIR/storage/oauth-public.key and $MONICADIR/storage/oauth-private.key files !" + fi + +fi + +exec "$@" diff --git a/scripts/docker/install-composer.sh b/scripts/docker/install-composer.sh new file mode 100644 index 0000000..a3a17de --- /dev/null +++ b/scripts/docker/install-composer.sh @@ -0,0 +1,23 @@ +#!/bin/sh + +set -v + +SETUP=composer-setup.php +cd /tmp + +EXPECTED_SIGNATURE=$(curl -sS https://composer.github.io/installer.sig) +curl -sS -o $SETUP https://getcomposer.org/installer +ACTUAL_SIGNATURE=$(openssl sha384 $SETUP | cut -d' ' -f2) + +if [ "$EXPECTED_SIGNATURE" != "$ACTUAL_SIGNATURE" ] +then + >&2 echo 'ERROR: Invalid installer signature' + rm $SETUP + exit 1 +fi + +php $SETUP --quiet --install-dir=/usr/local/bin --filename=composer +RESULT=$? +rm $SETUP + +exit $RESULT diff --git a/scripts/docker/queue.sh b/scripts/docker/queue.sh new file mode 100644 index 0000000..d2150c5 --- /dev/null +++ b/scripts/docker/queue.sh @@ -0,0 +1,4 @@ +#!/bin/sh +set -eu + +exec php /var/www/html/artisan queue:work --sleep=10 --timeout=0 --tries=3 --queue=default,migration >/proc/1/fd/1 2>/proc/1/fd/2 diff --git a/scripts/realpath.sh b/scripts/realpath.sh new file mode 100644 index 0000000..edb0936 --- /dev/null +++ b/scripts/realpath.sh @@ -0,0 +1,16 @@ +realpath () +{ + f=$@; + if [ -z "$f" ]; then + f=$(pwd) + fi + if [ -d "$f" ]; then + base=""; + dir="$f"; + else + base="/$(basename "$f")"; + dir=$(dirname "$f"); + fi; + dir=$(cd "$dir" && /bin/pwd -P); + echo "$dir$base" +} diff --git a/scripts/tests/install-chrome.sh b/scripts/tests/install-chrome.sh new file mode 100644 index 0000000..a720aa6 --- /dev/null +++ b/scripts/tests/install-chrome.sh @@ -0,0 +1,7 @@ +#!/bin/bash +sudo apt-get update +sudo apt-get install lsb-release +curl -L -o google-chrome.deb https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb +sudo dpkg -i google-chrome.deb +sudo sed -i 's|HERE/chrome"|HERE/chrome" --disable-setuid-sandbox --no-sandbox|g' /opt/google/chrome/google-chrome +rm google-chrome.deb diff --git a/scripts/tests/server-cc.php b/scripts/tests/server-cc.php new file mode 100644 index 0000000..a3074a8 --- /dev/null +++ b/scripts/tests/server-cc.php @@ -0,0 +1,36 @@ +/dev/null || echo $APP_KEY) +if [[ -z ${APP_KEY:-} || "$APP_KEY" == "ChangeMeBy32KeyLengthOrGenerated" ]]; then + ${ARTISAN} key:generate --no-interaction +else + echo "APP_KEY already set" +fi + +# Run migrations +${ARTISAN} monica:update --force -v + +echo -e "\n\n\033[1;32mDone! You can access Monica by visiting \033[4;96mhttp://localhost:8080\033[0;40m\033[1;32m from your host machine\033[0;40m" + +SCRIPT + +Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| + config.vm.box = "monicahq/monicahq" + config.vm.hostname = "monica" + config.vm.define "monica" + + config.vm.network "forwarded_port", guest: 80, host: 8080 + + config.vm.provision "shell", inline: $script, keep_color: true +end \ No newline at end of file diff --git a/scripts/vagrant/build/.gitignore b/scripts/vagrant/build/.gitignore new file mode 100644 index 0000000..3cfa355 --- /dev/null +++ b/scripts/vagrant/build/.gitignore @@ -0,0 +1,3 @@ +*.box +*.log +.vagrant/ diff --git a/scripts/vagrant/build/Makefile b/scripts/vagrant/build/Makefile new file mode 100644 index 0000000..e53a74f --- /dev/null +++ b/scripts/vagrant/build/Makefile @@ -0,0 +1,22 @@ +GIT_TAG := $(shell git describe --abbrev=0 --tags) +VERSION := $(subst v,,$(GIT_TAG)) + +build: base + rm -rf .vagrant + GIT_TAG=$(GIT_TAG) vagrant up monicahq-stable + +base: + vagrant box list | grep -q "ubuntu/bionic64" || vagrant box add ubuntu/bionic64 + vagrant box update --box ubuntu/bionic64 + +package: build + rm -f monicahq-stable.box + vagrant box remove monicahq/monicahq --box-version 0 || true + vagrant package monicahq-stable --output ./monicahq-stable.box --vagrantfile ../Vagrantfile + vagrant box add monicahq/monicahq ./monicahq-stable.box + +upload: + mv monicahq-stable.box monicahq-$(GIT_TAG).box + ./vagrant-upload.sh + +.PHONY: build base package diff --git a/scripts/vagrant/build/Vagrantfile b/scripts/vagrant/build/Vagrantfile new file mode 100644 index 0000000..fe50982 --- /dev/null +++ b/scripts/vagrant/build/Vagrantfile @@ -0,0 +1,26 @@ +# -*- mode: ruby -*- +# vi: set ft=ruby : + +VAGRANTFILE_API_VERSION ||= "2" + +Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| + + config.vm.synced_folder ".", "/vagrant" + + # Create a box with the latest version available + config.vm.define "monicahq-latest", primary: true do |monicahq| + monicahq.vm.box = "ubuntu/bionic64" + monicahq.vm.hostname = "monica" + monicahq.vm.boot_timeout = 180 + monicahq.vm.provision "shell", path: "install-monica.sh", keep_color: true + end + + # Create a box with the specific version tag + config.vm.define "monicahq-stable" do |monicahq| + monicahq.vm.box = "ubuntu/bionic64" + monicahq.vm.hostname = "monica" + monicahq.vm.boot_timeout = 180 + monicahq.vm.provision "shell", path: "install-monica.sh", keep_color: true, env: {"GIT_TAG" => "#{ENV['GIT_TAG']}"} + end + +end diff --git a/scripts/vagrant/build/install-monica.sh b/scripts/vagrant/build/install-monica.sh new file mode 100644 index 0000000..6d11c0c --- /dev/null +++ b/scripts/vagrant/build/install-monica.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash + +set -euo pipefail + +MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD:-changeme} +MYSQL_DB_DATABASE=${MYSQL_DB_DATABASE:-monica} +MYSQL_DB_USERNAME=${MYSQL_DB_USERNAME:-monica} +MYSQL_DB_PASSWORD=${MYSQL_DB_PASSWORD:-changeme} +DESTDIR=/var/www/html/monica + +function update_setting() { + file=$1 + name=$2 + value=$3 + if $(grep -q "$name" $file); then + sed -i "s/\($name\).*/\1=$value/" $file; + else + echo -e "\n$name=$value" | tee -a $file; + fi +} + +export DEBIAN_FRONTEND=noninteractive + +apt-get update >/dev/null + +echo -e "\033[1;32m########################\033[0;40m" +echo -e "\033[1;32mInstalling Monica ${GIT_TAG:-}\033[0;40m" +echo -e "\033[1;32m########################\033[0;40m" + +echo -e "\n\033[4;32mInstalling apache\033[0;40m" +apt-get install -y apache2 >/dev/null + +echo "ServerName vagrant" >> /etc/apache2/apache2.conf # suppress apache warning + +echo -e "\n\033[4;32mInstalling MySQL with default root password\033[0;40m" +debconf-set-selections <<< "mysql-server mysql-server/root_password password $MYSQL_ROOT_PASSWORD" +debconf-set-selections <<< "mysql-server mysql-server/root_password_again password $MYSQL_ROOT_PASSWORD" +apt-get install -y mysql-server mysql-client >/dev/null + +echo -e "\n\033[4;32mInstalling PHP 8.1\033[0;40m" +apt-get install -y curl gnupg2 apt-transport-https apt-transport-https lsb-release ca-certificates >/dev/null +add-apt-repository -y ppa:ondrej/php >/dev/null +apt-get update >/dev/null +apt-get install -y php8.1 >/dev/null + +echo -e "\n\033[4;32mInstalling git\033[0;40m" +apt-get install -y git >/dev/null + +echo -e "\n\033[4;32mInstalling composer\033[0;40m" +apt-get install -y curl php8.1-cli >/dev/null +curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer >/dev/null + +echo -e "\n\033[4;32mInstalling packages for Monica\033[0;40m" +apt-get install -y php8.1-bcmath php8.1-curl php8.1-common php8.1-fpm \ + php8.1-gd php8.1-gmp php8.1-intl php8.1-json php8.1-mbstring php8.1-mysql \ + php8.1-opcache php8.1-redis php8.1-xml php8.1-zip >/dev/null + +echo -e "\n\033[4;32mInstalling node.js\033[0;40m" +curl -fsSL https://deb.nodesource.com/setup_18.x | bash - >/dev/null +apt-get install -y nodejs >/dev/null + +echo -e "\n\033[4;32mInstalling yarn\033[0;40m" +npm install --global yarn >/dev/null + +echo -e "\n\033[4;32mGetting database ready\033[0;40m" +mysql -uroot -p$MYSQL_ROOT_PASSWORD -e "CREATE DATABASE $MYSQL_DB_DATABASE; +CREATE USER '$MYSQL_DB_USERNAME'@'localhost' IDENTIFIED BY '$MYSQL_DB_PASSWORD'; +GRANT ALL ON $MYSQL_DB_DATABASE.* TO '$MYSQL_DB_USERNAME'@'localhost'; +FLUSH PRIVILEGES;" + +echo -e "\n\033[4;32mInstalling Monica\033[0;40m" +git clone https://github.com/monicahq/monica.git $DESTDIR +cd $DESTDIR +if [ -n "${GIT_TAG:-}" ]; then + git checkout tags/$GIT_TAG +fi +composer install --no-interaction --no-dev --no-progress >/dev/null +composer clear-cache + +echo -e "\n\033[4;32mBuild assets\033[0;40m" +yarn install +yarn run production + +echo -e "\n\033[4;32mConfiguring Monica\033[0;40m" +cp .env.example .env +update_setting .env DB_DATABASE "$MYSQL_DB_DATABASE" +update_setting .env DB_USERNAME "$MYSQL_DB_USERNAME" +update_setting .env DB_PASSWORD "$MYSQL_DB_PASSWORD" +update_setting .env APP_DISABLE_SIGNUP "false" +chown -R www-data:www-data . + +echo -e "\n\033[4;32mConfiguring cron script\033[0;40m" +{ crontab -l -u www-data; echo "* * * * * /usr/bin/php $DESTDIR/artisan schedule:run"; } | crontab -u www-data - || true + +echo -e "\n\033[4;32mConfiguring apache\033[0;40m" +a2enmod rewrite +sed -i "s/\(DocumentRoot\).*/\1 ${DESTDIR//\//\\\/}\/public/" /etc/apache2/sites-enabled/000-default.conf +sed -i "s/\/var\/www\//${DESTDIR//\//\\\/}\/public\//" /etc/apache2/apache2.conf +sed -i "//,/<\/Directory>/ s/AllowOverride None/AllowOverride All/" /etc/apache2/apache2.conf +systemctl restart apache2 + +echo -e "\n\033[4;32mSystem update\033[0;40m" +apt-get -y upgrade +apt-get -y autoremove +apt-get -y clean diff --git a/scripts/vagrant/build/vagrant-upload.sh b/scripts/vagrant/build/vagrant-upload.sh new file mode 100644 index 0000000..d5980ff --- /dev/null +++ b/scripts/vagrant/build/vagrant-upload.sh @@ -0,0 +1,75 @@ +#!/bin/bash + +set -euvo pipefail + +GIT_TAG=$(git describe --abbrev=0 --tags) +VERSION=${GIT_TAG##v} +API=https://app.vagrantup.com/api/v1 + +# Create a new version +response=$( +curl -sSL --header "Content-Type: application/json" --header "Authorization: Bearer $VAGRANT_CLOUD_TOKEN" \ + "$API/box/monicahq/monicahq/versions" \ + --data "{ \ + \"version\": { \ + \"version\": \"$VERSION\", \ + \"description\": \"https://github.com/monicahq/monica/releases/tag/$GIT_TAG\" \ + } \ + }" +) +if [ $(echo "$response" | jq .success) == "false" ]; then + if [ $(echo "$response" | jq .errors[0]) != "Version has already been taken" ]; then + echo "$response" | jq .errors[0] + exit 1 + fi +fi + +# Create provider +response=$( +curl -sSL --header "Content-Type: application/json" --header "Authorization: Bearer $VAGRANT_CLOUD_TOKEN" \ + "$API/box/monicahq/monicahq/version/$VERSION/providers" \ + --data "{ \ + \"provider\": { \ + \"name\": \"virtualbox\" \ + } \ + }" +) +if [ $(echo "$response" | jq .success) == "false" ]; then + if [ $(echo "$response" | jq .errors[0]) != "Metadata provider must be unique for version" ]; then + echo "$response" | jq .errors[0] + exit 1 + fi +fi + +# Upload the box +response=$( +curl -sSL --header "Authorization: Bearer $VAGRANT_CLOUD_TOKEN" \ + "$API/box/monicahq/monicahq/version/$VERSION/provider/virtualbox/upload" +) +if [ $(echo "$response" | jq .success) == "false" ]; then + echo "$response" | jq .errors[0] + exit 1 +fi +upload_path=$(echo "$response" | jq .upload_path) +upload_path="${upload_path%\"}" +upload_path="${upload_path#\"}" + +curl -fL --progress-bar \ + --request PUT \ + "$upload_path" \ + --upload-file "monicahq-$GIT_TAG.box" +retval=$? +if [ $? -ne 0 ]; then + exit 1 +fi + +# Publish the version +response=$( +curl -sSL --header "Authorization: Bearer $VAGRANT_CLOUD_TOKEN" \ + --request PUT \ + "$API/box/monicahq/monicahq/version/$VERSION/release" +) +if [ $(echo "$response" | jq .success) == "false" ]; then + echo "$response" | jq .errors[0] + exit 1 +fi diff --git a/scripts/vagrant/install-vagrant.sh b/scripts/vagrant/install-vagrant.sh new file mode 100644 index 0000000..e79e2dd --- /dev/null +++ b/scripts/vagrant/install-vagrant.sh @@ -0,0 +1,44 @@ +#!/bin/bash +set -euo pipefail + +# set version of vagrant to use : +vagrantversion=2.2.15 + +mkdir -p $HOME/vagrant +pushd $HOME/vagrant > /dev/null + +if [ ! -d "vagrant-$vagrantversion" ]; then + mkdir -p "vagrant-$vagrantversion" + pushd "vagrant-$vagrantversion" > /dev/null + + curl -Os https://releases.hashicorp.com/vagrant/${vagrantversion}/vagrant_${vagrantversion}_x86_64.deb + + curl -Os https://releases.hashicorp.com/vagrant/${vagrantversion}/vagrant_${vagrantversion}_SHA256SUMS + curl -Os https://releases.hashicorp.com/vagrant/${vagrantversion}/vagrant_${vagrantversion}_SHA256SUMS.sig + gpg --keyserver keys.gnupg.net --recv-keys 72D7468F + + verif=0 + if gpg --quiet --verify vagrant_${vagrantversion}_SHA256SUMS.sig vagrant_${vagrantversion}_SHA256SUMS 2>/dev/null; then + if grep vagrant_${vagrantversion}_x86_64.deb vagrant_${vagrantversion}_SHA256SUMS | shasum -a 256 -c - 2>/dev/null >/dev/null; then + verif=1 + else + echo ERROR: checksum don\'t match + fi + else + echo ERROR: signature don\'t match + fi + + rm -f vagrant_${vagrantversion}_SHA256SUMS* + + popd > /dev/null + + if [ "$verif" = 0 ]; then + rm -rf vagrant-$vagrantversion + fi +fi + +if [ -f vagrant-$vagrantversion/vagrant_${vagrantversion}_x86_64.deb ]; then + sudo dpkg -i vagrant-$vagrantversion/vagrant_${vagrantversion}_x86_64.deb +fi + +popd > /dev/null diff --git a/server.js b/server.js deleted file mode 100644 index 9fddcbc..0000000 --- a/server.js +++ /dev/null @@ -1,541 +0,0 @@ -const express = require('express'); -const path = require('path'); -const sqlite3 = require('sqlite3').verbose(); - -const app = express(); -const PORT = process.env.PORT || 8085; -const DB_PATH = process.env.DATABASE_PATH || path.join(__dirname, 'crm.db'); - -// Middleware -app.use(express.json()); -app.use(express.static(path.join(__dirname, 'public'))); - -// Connect to SQLite Database -const db = new sqlite3.Database(DB_PATH, (err) => { - if (err) { - console.error('Error connecting to SQLite database:', err.message); - } else { - console.log('Connected to SQLite database at:', DB_PATH); - initializeDatabase(); - } -}); - -// Initialize Database Tables -function initializeDatabase() { - db.serialize(() => { - // Friends Table - db.run(` - CREATE TABLE IF NOT EXISTS friends ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - birthday TEXT, - contact TEXT, - address TEXT, - relationship_status TEXT, - family TEXT, - honor INTEGER DEFAULT 0, - life_situation TEXT, - job TEXT, - hobbies TEXT, - milestones TEXT, - food_preferences TEXT, - random_notes TEXT, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - updated_at DATETIME DEFAULT CURRENT_TIMESTAMP - ) - `); - - // Meetings Table - db.run(` - CREATE TABLE IF NOT EXISTS meetings ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - friend_id INTEGER NOT NULL, - date TEXT NOT NULL, - activity TEXT, - mood TEXT, - details TEXT, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY(friend_id) REFERENCES friends(id) ON DELETE CASCADE - ) - `); - - // Topics Table - db.run(` - CREATE TABLE IF NOT EXISTS topics ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - friend_id INTEGER NOT NULL, - topic TEXT NOT NULL, - completed INTEGER DEFAULT 0, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY(friend_id) REFERENCES friends(id) ON DELETE CASCADE - ) - `); - - // Seed Initial Data (Aaron Lingel) if database is empty - db.get("SELECT COUNT(*) as count FROM friends", (err, row) => { - if (err) return console.error('Error checking friends count:', err.message); - - if (row.count === 0) { - console.log('Seeding initial data...'); - const stmt = db.prepare(` - INSERT INTO friends ( - name, birthday, contact, address, relationship_status, family, honor, life_situation, job, hobbies, milestones, food_preferences, random_notes - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `); - - stmt.run( - "Aaron Lingel", - "1999-08-13", - "+49 172 1821612", - "Am schlaggraben 18\n71272 renningen", - "FOREVER ALONE (aber hat mich)", - "Mama Lingel und Papa Africano", - 32, - "Auf der Suche nach neuen Abenteuern", - "Studium / Arbeit", - "Ehrenbruder sein, D&D", - "Hat ein stabiles Freundschaftsprofil bekommen", - "Gutes Essen", - "Bester Kumpel" - ); - stmt.finalize(); - - // Seed an initial meeting log for Aaron - db.get("SELECT id FROM friends WHERE name = 'Aaron Lingel'", (err, row) => { - if (row) { - db.run(` - INSERT INTO meetings (friend_id, date, activity, mood, details) - VALUES (?, ?, ?, ?, ?) - `, [row.id, "2026-05-20", "Gemütliches Kaltgetränk gezischt", "Strahlt vor Freude", "Lustige Gespräche über Gott und die Welt geführt. Aaron ist hochmotiviert für neue Projekte."]); - - db.run(` - INSERT INTO topics (friend_id, topic, completed) - VALUES (?, ?, ?) - `, [row.id, "Nächstes D&D Abenteuer planen", 0]); - - db.run(` - INSERT INTO topics (friend_id, topic, completed) - VALUES (?, ?, ?) - `, [row.id, "Seinen Ehren-Counter im CRM feiern", 0]); - } - }); - } - }); - }); -} - -// API ENDPOINTS - -// 1. Get all friends (with their last meeting date) -app.get('/api/friends', (req, res) => { - const query = ` - SELECT f.*, - MAX(m.date) as last_meeting_date, - (SELECT COUNT(*) FROM topics t WHERE t.friend_id = f.id AND t.completed = 0) as pending_topics_count - FROM friends f - LEFT JOIN meetings m ON f.id = m.friend_id - GROUP BY f.id - ORDER BY f.name ASC - `; - db.all(query, [], (err, rows) => { - if (err) { - return res.status(500).json({ error: err.message }); - } - res.json(rows); - }); -}); - -// 2. Get single friend details (including meetings & topics) -app.get('/api/friends/:id', (req, res) => { - const friendId = req.params.id; - - db.get("SELECT * FROM friends WHERE id = ?", [friendId], (err, friend) => { - if (err) return res.status(500).json({ error: err.message }); - if (!friend) return res.status(404).json({ error: 'Friend not found' }); - - db.all("SELECT * FROM meetings WHERE friend_id = ? ORDER BY date DESC", [friendId], (err, meetings) => { - if (err) return res.status(500).json({ error: err.message }); - - db.all("SELECT * FROM topics WHERE friend_id = ? ORDER BY completed ASC, created_at DESC", [friendId], (err, topics) => { - if (err) return res.status(500).json({ error: err.message }); - - res.json({ - ...friend, - meetings: meetings || [], - topics: topics || [] - }); - }); - }); - }); -}); - -// 3. Create a new friend -app.post('/api/friends', (req, res) => { - const { - name, birthday, contact, address, relationship_status, family, honor, - life_situation, job, hobbies, milestones, food_preferences, random_notes - } = req.body; - - if (!name) { - return res.status(400).json({ error: 'Name is required' }); - } - - const query = ` - INSERT INTO friends ( - name, birthday, contact, address, relationship_status, family, honor, - life_situation, job, hobbies, milestones, food_preferences, random_notes - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `; - - const params = [ - name, birthday || '', contact || '', address || '', relationship_status || '', - family || '', honor || 0, life_situation || '', job || '', hobbies || '', - milestones || '', food_preferences || '', random_notes || '' - ]; - - db.run(query, params, function(err) { - if (err) { - return res.status(500).json({ error: err.message }); - } - res.status(201).json({ id: this.lastID, message: 'Friend created successfully' }); - }); -}); - -// 4. Update friend details -app.put('/api/friends/:id', (req, res) => { - const friendId = req.params.id; - const { - name, birthday, contact, address, relationship_status, family, honor, - life_situation, job, hobbies, milestones, food_preferences, random_notes - } = req.body; - - if (!name) { - return res.status(400).json({ error: 'Name is required' }); - } - - const query = ` - UPDATE friends SET - name = ?, birthday = ?, contact = ?, address = ?, relationship_status = ?, - family = ?, honor = ?, life_situation = ?, job = ?, hobbies = ?, - milestones = ?, food_preferences = ?, random_notes = ?, updated_at = CURRENT_TIMESTAMP - WHERE id = ? - `; - - const params = [ - name, birthday, contact, address, relationship_status, family, honor, - life_situation, job, hobbies, milestones, food_preferences, random_notes, - friendId - ]; - - db.run(query, params, function(err) { - if (err) { - return res.status(500).json({ error: err.message }); - } - if (this.changes === 0) { - return res.status(404).json({ error: 'Friend not found' }); - } - res.json({ message: 'Friend updated successfully' }); - }); -}); - -// 5. Delete a friend -app.delete('/api/friends/:id', (req, res) => { - const friendId = req.params.id; - db.run("DELETE FROM friends WHERE id = ?", [friendId], function(err) { - if (err) { - return res.status(500).json({ error: err.message }); - } - res.json({ message: 'Friend deleted successfully' }); - }); -}); - -// 6. Update friend's honor score (+/-) -app.post('/api/friends/:id/honor', (req, res) => { - const friendId = req.params.id; - const { change } = req.body; // should be +1 or -1 - - if (change !== 1 && change !== -1) { - return res.status(400).json({ error: 'Invalid honor change value. Must be 1 or -1.' }); - } - - db.run("UPDATE friends SET honor = honor + ? WHERE id = ?", [change, friendId], function(err) { - if (err) { - return res.status(500).json({ error: err.message }); - } - if (this.changes === 0) { - return res.status(404).json({ error: 'Friend not found' }); - } - - // Retrieve the updated honor value to send back - db.get("SELECT honor FROM friends WHERE id = ?", [friendId], (err, row) => { - if (err) return res.status(500).json({ error: err.message }); - res.json({ honor: row.honor, message: 'Honor updated successfully' }); - }); - }); -}); - -// 6.5. Get all meetings (global list, sorted by date descending) -app.get('/api/meetings', (req, res) => { - const query = ` - SELECT m.*, f.name as friend_name - FROM meetings m - JOIN friends f ON m.friend_id = f.id - ORDER BY m.date DESC - `; - db.all(query, [], (err, rows) => { - if (err) { - return res.status(500).json({ error: err.message }); - } - res.json(rows); - }); -}); - -// 7. Log a new meeting -app.post('/api/meetings', (req, res) => { - const { friend_id, date, activity, mood, details } = req.body; - - if (!friend_id || !date) { - return res.status(400).json({ error: 'Friend ID and Date are required' }); - } - - const query = ` - INSERT INTO meetings (friend_id, date, activity, mood, details) - VALUES (?, ?, ?, ?, ?) - `; - - db.run(query, [friend_id, date, activity || '', mood || '', details || ''], function(err) { - if (err) { - return res.status(500).json({ error: err.message }); - } - res.status(201).json({ id: this.lastID, message: 'Meeting logged successfully' }); - }); -}); - -// 8. Delete a meeting -app.delete('/api/meetings/:id', (req, res) => { - const meetingId = req.params.id; - db.run("DELETE FROM meetings WHERE id = ?", [meetingId], function(err) { - if (err) { - return res.status(500).json({ error: err.message }); - } - res.json({ message: 'Meeting deleted successfully' }); - }); -}); - -// 9. Add a topic to discuss -app.post('/api/topics', (req, res) => { - const { friend_id, topic } = req.body; - - if (!friend_id || !topic) { - return res.status(400).json({ error: 'Friend ID and Topic description are required' }); - } - - db.run("INSERT INTO topics (friend_id, topic) VALUES (?, ?)", [friend_id, topic], function(err) { - if (err) { - return res.status(500).json({ error: err.message }); - } - res.status(201).json({ id: this.lastID, message: 'Topic added successfully' }); - }); -}); - -// 10. Toggle topic completion status -app.put('/api/topics/:id', (req, res) => { - const topicId = req.params.id; - const { completed } = req.body; // 0 or 1 - - db.run("UPDATE topics SET completed = ? WHERE id = ?", [completed ? 1 : 0, topicId], function(err) { - if (err) { - return res.status(500).json({ error: err.message }); - } - res.json({ message: 'Topic updated successfully' }); - }); -}); - -// 11. Delete a topic -app.delete('/api/topics/:id', (req, res) => { - const topicId = req.params.id; - db.run("DELETE FROM topics WHERE id = ?", [topicId], function(err) { - if (err) { - return res.status(500).json({ error: err.message }); - } - res.json({ message: 'Topic deleted successfully' }); - }); -}); - -// 12. Smart Import from Obsidian Markdown Content -app.post('/api/import-obsidian', (req, res) => { - const { filename, content } = req.body; - - if (!content) { - return res.status(400).json({ error: 'Markdown content is required' }); - } - - try { - // 1. Extract name from filename or title - let name = filename ? filename.replace(/\.md$/, '').trim() : ''; - - // 2. Parse YAML frontmatter if exists - const yamlRegex = /^---([\s\S]*?)---/; - const yamlMatch = content.match(yamlRegex); - let frontmatter = {}; - let markdownBody = content; - - if (yamlMatch) { - markdownBody = content.replace(yamlMatch[0], ''); - const yamlContent = yamlMatch[1]; - yamlContent.split('\n').forEach(line => { - const parts = line.split(':'); - if (parts.length >= 2) { - const key = parts[0].trim(); - const val = parts.slice(1).join(':').trim(); - frontmatter[key] = val; - } - }); - } - - // 3. Simple parser for markdown sections - // E.g. we find sections like ## Allgemeines, ## Aktuelle Lebenssituation, etc. - const sections = {}; - const headingRegex = /^##\s+(.+)$/gm; - let match; - const headings = []; - - // Get all ## headings and their indexes - while ((match = headingRegex.exec(markdownBody)) !== null) { - headings.push({ - title: match[1].trim().toLowerCase(), - index: match.index, - fullHeading: match[0] - }); - } - - for (let i = 0; i < headings.length; i++) { - const start = headings[i].index + headings[i].fullHeading.length; - const end = (i + 1 < headings.length) ? headings[i + 1].index : markdownBody.length; - const sectionText = markdownBody.slice(start, end).trim(); - sections[headings[i].title] = sectionText; - } - - // Parse 'allgemeines' list items - const generalText = sections['allgemeines'] || ''; - let birthday = ''; - let contact = ''; - let address = ''; - let relationship_status = ''; - let family = ''; - let honor = 0; - - generalText.split('\n').forEach(line => { - const cleaned = line.replace(/^-\s+\*\*/, '').replace(/^-/, '').trim(); - - if (cleaned.startsWith('Name:')) { - const parsedName = cleaned.replace('Name:', '').trim(); - if (parsedName) name = parsedName; - } else if (cleaned.startsWith('Geburtstag:')) { - let bday = cleaned.replace('Geburtstag:', '').trim(); - // Convert DD.MM.YYYY to YYYY-MM-DD - const dateMatch = bday.match(/(\d{2})\.(\d{2})\.(\d{4})/); - if (dateMatch) { - birthday = `${dateMatch[3]}-${dateMatch[2]}-${dateMatch[1]}`; - } else { - birthday = bday; - } - } else if (cleaned.startsWith('Kontakt:')) { - contact = cleaned.replace('Kontakt:', '').trim(); - } else if (cleaned.startsWith('Wohnort:')) { - address = cleaned.replace('Wohnort:', '').trim(); - } else if (cleaned.startsWith('Beziehungsstatus:')) { - relationship_status = cleaned.replace('Beziehungsstatus:', '').trim(); - } else if (cleaned.startsWith('Familie:')) { - family = cleaned.replace('Familie:', '').trim(); - } else if (cleaned.startsWith('Ehre:')) { - const honorStr = cleaned.replace('Ehre:', '').trim(); - const parsedHonor = parseInt(honorStr.replace('+', ''), 10); - if (!isNaN(parsedHonor)) honor = parsedHonor; - } - }); - - if (!name) { - name = 'Unbekannter Freund'; - } - - // Parse other sections - const life_situation = sections['aktuelle lebenssituation'] || ''; - - // Split job, hobbies, milestones - let job = ''; - let hobbies = ''; - life_situation.split('\n').forEach(line => { - const cleaned = line.replace(/^-\s+\*\*/, '').replace(/^-/, '').trim(); - if (cleaned.startsWith('Arbeit/Studium:')) { - job = cleaned.replace('Arbeit/Studium:', '').trim(); - } else if (cleaned.startsWith('Hobbys/Interessen:')) { - hobbies = cleaned.replace('Hobbys/Interessen:', '').trim(); - } - }); - - const milestonesText = sections['persönliche meilensteine'] || ''; - const milestones = milestonesText.split('\n') - .map(line => line.replace(/^-/, '').trim()) - .filter(line => line.length > 0) - .join('\n'); - - const randomText = sections['random infos'] || ''; - let food_preferences = ''; - randomText.split('\n').forEach(line => { - const cleaned = line.replace(/^-\s+\*\*/, '').replace(/^-/, '').trim(); - if (cleaned.startsWith('Lieblingsessen/-getränk:')) { - food_preferences = cleaned.replace('Lieblingsessen/-getränk:', '').trim(); - } - }); - - const random_notes = sections['random infos'] || ''; - - // Insert parsed friend into database - db.run(` - INSERT INTO friends ( - name, birthday, contact, address, relationship_status, family, honor, - life_situation, job, hobbies, milestones, food_preferences, random_notes - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `, [ - name, birthday, contact, address, relationship_status, family, honor, - life_situation, job, hobbies, milestones, food_preferences, random_notes - ], function(err) { - if (err) { - return res.status(500).json({ error: err.message }); - } - - const newFriendId = this.lastID; - - // Parse meetings and insert if any exist - // In the Aaron Lingel.md file, meetings are dataview queries, but let's see if we can parse some meetings - // Usually manual notes have a section like ## Treffen/Unterhaltungen - const meetingsText = sections['treffen/unterhaltungen'] || ''; - if (meetingsText && !meetingsText.includes('Letztes Treffen am: ')) { - // Simple manual meeting parse if it has content - let date = new Date().toISOString().split('T')[0]; - let activity = 'Treffen'; - let details = meetingsText; - - db.run(` - INSERT INTO meetings (friend_id, date, activity, mood, details) - VALUES (?, ?, ?, ?, ?) - `, [newFriendId, date, activity, 'Zufrieden', details]); - } - - res.status(201).json({ id: newFriendId, name, message: 'Obsidian file successfully imported!' }); - }); - } catch (err) { - res.status(500).json({ error: 'Failed to parse Obsidian file: ' + err.message }); - } -}); - -// Default static serving: fallback to index.html for SPA router -app.get('*', (req, res) => { - res.sendFile(path.join(__dirname, 'public', 'index.html')); -}); - -// Start Server -app.listen(PORT, () => { - console.log(`MischCRM server is running on http://localhost:${PORT}`); -}); diff --git a/server.php b/server.php new file mode 100644 index 0000000..1b4655b --- /dev/null +++ b/server.php @@ -0,0 +1,19 @@ + + */ +$uri = urldecode( + parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) +); + +// This file allows us to emulate Apache's "mod_rewrite" functionality from the +// built-in PHP web server. This provides a convenient way to test a Laravel +// application without having installed a "real" web server software here. +if ($uri !== '/' && file_exists(__DIR__.'/public'.$uri)) { + return false; +} + +require_once __DIR__.'/public/index.php'; diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..53cf751 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,21 @@ +# must be unique in a given SonarQube instance +sonar.projectKey=monica +sonar.projectName=monica +sonar.organization=monicahq + +# Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows. +# This property is optional if sonar.modules is set. +sonar.sources=app,bootstrap,config,database,public,resources,routes +sonar.exclusions=bootstrap/cache/*,public/vendor/**,resources/lang/** +sonar.tests=tests +sonar.coverage.exclusions=routes/*.php,config/**/*.php,bootstrap/**,resources/**/*.php,database/**/*.php,public/*.php,resources/**/*.vue,resources/**/*.js +sonar.cpd.exclusions=routes/*.php,config/*.php,bootstrap/**,resources/**/*.php,database/**/*.php + +# Encoding of the source code. Default is default system encoding +sonar.sourceEncoding=UTF-8 + +# Links for sonarcloud.io page +sonar.links.homepage=https://monicahq.com +sonar.links.ci=https://github.com/monicahq/monica/actions +sonar.links.scm=https://github.com/monicahq/monica +sonar.links.issue=https://github.com/monicahq/monica/issues diff --git a/storage/app/.gitignore b/storage/app/.gitignore new file mode 100644 index 0000000..8f4803c --- /dev/null +++ b/storage/app/.gitignore @@ -0,0 +1,3 @@ +* +!public/ +!.gitignore diff --git a/storage/app/public/.gitignore b/storage/app/public/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/app/public/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/storage/debugbar/.gitignore b/storage/debugbar/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/storage/debugbar/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/storage/framework/.gitignore b/storage/framework/.gitignore new file mode 100644 index 0000000..b02b700 --- /dev/null +++ b/storage/framework/.gitignore @@ -0,0 +1,8 @@ +config.php +routes.php +schedule-* +compiled.php +services.json +events.scanned.php +routes.scanned.php +down diff --git a/storage/framework/cache/.gitignore b/storage/framework/cache/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/framework/cache/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/storage/framework/sessions/.gitignore b/storage/framework/sessions/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/framework/sessions/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/storage/framework/testing/.gitignore b/storage/framework/testing/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/framework/testing/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/storage/framework/views/.gitignore b/storage/framework/views/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/framework/views/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/storage/logs/.gitignore b/storage/logs/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/logs/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/template-definition.yaml b/template-definition.yaml new file mode 100644 index 0000000..3b4e86e --- /dev/null +++ b/template-definition.yaml @@ -0,0 +1,58 @@ +# Platform.sh Project Initialization Template +# +# This file defines settings and workflow modifications that allow a git +# repository to be deployed to Platform.sh and its white-label partners. A +# project template can be a fully functioning ready-made application or a +# quick-start point for custom development work. +# +# It contains elements that affect the behaviour upon the initialisation of +# a new project (for example minimal plan sizes) as well as elements that +# allow Platform.sh to present it in a user interface (such as the description +# of the project, tags, an icon etc.). + +# The schema is versioned so that we can establish code paths differently in the future if we need to change this. +version: 20201127 + +# Templates are a small amount of information supporting a template URL. +# Each template is selectable at the project-creation step. +info: + # Unique machine name, prefaced by a vendor or organization identifier. + # The vendor should be the lowercase name of your company, organization, or project, and the project name + # the lowercase name of the template. This may be the same as the vendor in a single-product case. + id: monicahq/monica + # The human-readable name of the template. This is how the template will be named in the user interface. + name: Monica Personal Relationship Manager + # Human-readable descriptive text for the template. Supports limited HTML. + # This field should be 1-3 sentences describing how the project is setup, assuming the reader already knows what + # the application is. + description: | + Monica is an open-source web application to organize the interactions with your loved ones. We call it a PRM, or Personal Relationship Management. Think of it as a CRM (a popular tool used by sales teams in the corporate world) for your friends or family. + # A list of tags associated with the template. These should be highly generic terms like "CMS", "Framework", and + # the language in which the application is written. + tags: + - PHP + - CRM + + # An image URI (either base64-encoded or a URL) representing the template. Base64-encoded SVG strongly preferred. + image: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB3aWR0aD0iMTQwIiBoZWlnaHQ9IjE0MCIgdmVyc2lvbj0iMS4xIiB2aWV3Qm94PSIwIDAgNTAwIDUwMCI+PHRpdGxlPkFydGJvYXJkIDMuMTwvdGl0bGU+PGRlc2M+Q3JlYXRlZCB1c2luZyBGaWdtYTwvZGVzYz48ZyBpZD0iQ2FudmFzIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMjk4NCA2OCkiPjxjbGlwUGF0aCBpZD0iY2xpcC0wIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiPjxwYXRoIGZpbGw9IiNGRkYiIGQ9Ik0gMjk4NCAtNjhMIDM0ODQgLTY4TCAzNDg0IDQzMkwgMjk4NCA0MzJMIDI5ODQgLTY4WiIvPjwvY2xpcFBhdGg+PGcgaWQ9IkFydGJvYXJkIDMuMSIgY2xpcC1wYXRoPSJ1cmwoI2NsaXAtMCkiPjxnIGlkPSJHcm91cCAyIj48ZyBpZD0iT3ZhbCI+PHVzZSBmaWxsPSIjMkMyQjI5IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgzMDAyLjUxIC0yNi40Nzg4KSIgeGxpbms6aHJlZj0iI3BhdGgwX2ZpbGwiLz48L2c+PGcgaWQ9Ik92YWwiPjx1c2UgZmlsbD0iI0ZGRiIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMzA0Ny41NiAxNy4zNDU2KSIgeGxpbms6aHJlZj0iI3BhdGgxX2ZpbGwiLz48L2c+PGcgaWQ9Ik92YWwgMiI+PHVzZSBmaWxsPSIjMkMyQjI5IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgyOTkwIC0zOSkiIHhsaW5rOmhyZWY9IiNwYXRoMl9maWxsIi8+PC9nPjxnIGlkPSJPdmFsIDIiPjx1c2UgZmlsbD0iIzJDMkIyOSIgdHJhbnNmb3JtPSJtYXRyaXgoLTEgMCAwIDEgMzQ3OCAtMzkpIiB4bGluazpocmVmPSIjcGF0aDJfZmlsbCIvPjwvZz48ZyBpZD0iR3JvdXAiPjxnIGlkPSJPdmFsIDQiPjx1c2UgZmlsbD0iIzJCMkEyOCIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMzI2NS4zMiAxMjIuNjQ3KSIgeGxpbms6aHJlZj0iI3BhdGgzX2ZpbGwiLz48L2c+PGcgaWQ9Ik92YWwgMyI+PHVzZSBmaWxsPSIjRkZGIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgzMjkyLjYzIDE1NC43NjgpIiB4bGluazpocmVmPSIjcGF0aDRfZmlsbCIvPjwvZz48L2c+PGcgaWQ9Ikdyb3VwIj48ZyBpZD0iT3ZhbCA0Ij48dXNlIGZpbGw9IiMyQjJBMjgiIHRyYW5zZm9ybT0ibWF0cml4KC0xIDAgMCAxIDMyMDMuOTMgMTIyLjY0NykiIHhsaW5rOmhyZWY9IiNwYXRoM19maWxsIi8+PC9nPjxnIGlkPSJPdmFsIDMiPjx1c2UgZmlsbD0iI0ZGRiIgdHJhbnNmb3JtPSJtYXRyaXgoLTEgMCAwIDEgMzE3Ni42MiAxNTQuNzY4KSIgeGxpbms6aHJlZj0iI3BhdGg0X2ZpbGwiLz48L2c+PC9nPjxnIGlkPSJPdmFsIDUiPjx1c2UgZmlsbD0iIzJDMkIyOSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMzIwMC4yMiAyNTYuNTAxKSIgeGxpbms6aHJlZj0iI3BhdGg1X2ZpbGwiLz48L2c+PC9nPjwvZz48L2c+PGRlZnM+PHBhdGggaWQ9InBhdGgwX2ZpbGwiIGZpbGwtcnVsZT0iZXZlbm9kZCIgZD0iTSAyMzUuNDI1IDQyOS40NzlDIDM0OS42NCA0MjkuNDc5IDQ3Ni4zNjkgMzU2LjAyMiA0NjAuMzQ1IDIxNC43MzlDIDQ0NC4zMjEgNzMuNDU2NyAzNDkuNjQgMCAyMzUuNDI1IDBDIDEyMS4yMSAwIDI3LjI3MDUgNjQuMzk0OSAyLjc3MDgyIDIxNC43MzlDIC0yMS43Mjg5IDM2NS4wODQgMTIxLjIxIDQyOS40NzkgMjM1LjQyNSA0MjkuNDc5WiIvPjxwYXRoIGlkPSJwYXRoMV9maWxsIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0gMTg4Ljg1IDM0NC4zMzRDIDI4MC40NyAzNDQuMzM0IDM4Mi4xMjggMjg1LjQ0IDM2OS4yNzQgMTcyLjE2N0MgMzU2LjQyIDU4Ljg5MzkgMjgwLjQ3IDAgMTg4Ljg1IDBDIDk3LjIzMTIgMCAyMS44NzU1IDUxLjYyODYgMi4yMjI2NiAxNzIuMTY3QyAtMTcuNDMwMiAyOTIuNzA2IDk3LjIzMTIgMzQ0LjMzNCAxODguODUgMzQ0LjMzNFoiLz48cGF0aCBpZD0icGF0aDJfZmlsbCIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNIDY1LjU4NjUgMTY1LjI4QyA3OC4wMDE0IDE2NS4zMjIgODMuMzk0NSAxMjAuOSAxMDUuNTY1IDEwMC42NzVDIDEyNS45MTkgODIuMTA4MyAxNzIuNjc3IDc1Ljc0MiAxNzIuNjc3IDU0LjI3NjJDIDE3Mi42NzcgOS40MjcyOSAxMjQuMDU1IDAgODEuMTE5OSAwQyAzOC4xODUzIDAgMCA0NS40OTAzIDAgOTAuMzM5MkMgMCAxMzUuMTg4IDQxLjc3NDcgMTY1LjIwMSA2NS41ODY1IDE2NS4yOFoiLz48cGF0aCBpZD0icGF0aDNfZmlsbCIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNIDY5LjYgMTU0LjU3MkMgMTAyLjA5NCAxNDYuODIzIDExNS42NDMgMTMzLjE1NyAxMTUuNjQzIDg4Ljk5N0MgMTE1LjY0MyA0NC44MzY4IDk0Ljk0NCAwIDU5LjMwNDkgMEMgMjMuNjY1NyAwIDAgMzEuODg2MyAwIDc2LjA0NjVDIDAgMTIwLjIwNyAzNy4xMDYgMTYyLjMyIDY5LjYgMTU0LjU3MloiLz48cGF0aCBpZD0icGF0aDRfZmlsbCIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNIDMwLjM4NjMgNjYuNDI4N0MgMzkuMjI4NiA2Ni40Mjg3IDQxLjg1ODYgNjQuMTcxNSA0NS44NTU1IDU5LjEwMzdDIDUwLjMzNzQgNTMuNDIxIDUzLjY3OTcgNDYuODI5NCA1Mi41ODY4IDMzLjIxNDRDIDUwLjc2MDggMTAuNDY1IDQxLjE3MzYgMCAyMi40MTYyIDBDIDMuNjU4NyAwIC0zLjc4MzY1ZS0xNiAxNC40MjI4IDAgMzMuMjE0NEMgLTMuNzgzNjVlLTE2IDUyLjAwNTkgMTEuNjI4OCA2Ni40Mjg3IDMwLjM4NjMgNjYuNDI4N1oiLz48cGF0aCBpZD0icGF0aDVfZmlsbCIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNIDM1LjI5MDYgNTAuMDg1QyA1Mi45NDg3IDUwLjA4NSA3MC4wNzE4IDI0LjA5NTYgNzAuMDcxOCAxMi45NDk5QyA3MC4wNzE4IDEuODA0MTYgNTIuNjkzOSAwIDM1LjAzNTkgMEMgMTcuMzc3OSAwIDAgMS44MDQxNiAwIDEyLjk0OTlDIDAgMjQuMDk1NiAxNy42MzI2IDUwLjA4NSAzNS4yOTA2IDUwLjA4NVoiLz48L2RlZnM+PC9zdmc+ + # Additional notes displayed in the template's detail view. + # Each note object is displayed as a small section heading with content below. Supports limited HTML. + # The most important is a section that lists the "Apps and Services" (container images) that the project uses. + # These are the name/version of the Platforms.h containers, not including the Nginx router. + notes: + - heading: "Apps & Services" + content: "PHP 8.1
    MariaDB 10.4
    Redis 5.0" + + +# This key describes the initialization call made to the main environment at +# project creation time. This is part of the full v2 UI operation mode, which +# places project schema/options selection early in the creation process, rather +# than later as it exits now. To allow this schema to be backwards-compatible, +# this key also gets mapped to the appropriate location in project.settings so +# that the current UI can have its own workflow overridden as well. +initialize: + repository: git@github.com:monicahq/monica.git@main + config: null + files: [] + profile: PHP + diff --git a/tests/Api/Account/Activity/ApiActivityTypeCategoryControllerTest.php b/tests/Api/Account/Activity/ApiActivityTypeCategoryControllerTest.php new file mode 100644 index 0000000..43f0888 --- /dev/null +++ b/tests/Api/Account/Activity/ApiActivityTypeCategoryControllerTest.php @@ -0,0 +1,183 @@ + [ + 'id', + ], + 'created_at', + 'updated_at', + ]; + + /** @test */ + public function it_gets_a_list_of_activity_type_categories() + { + $user = $this->signin(); + + factory(ActivityTypeCategory::class, 10)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('GET', '/api/activitytypecategories'); + + $response->assertJsonStructure([ + 'data' => [ + '*' => $this->jsonStructureActivityTypeCategory, + ], + ]); + } + + /** @test */ + public function it_applies_limit_parameter() + { + $user = $this->signin(); + + factory(ActivityTypeCategory::class, 10)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('GET', '/api/activitytypecategories?limit=1'); + + $response->assertJsonFragment([ + 'total' => 10, + 'current_page' => 1, + 'per_page' => 1, + 'last_page' => 10, + ]); + + $response = $this->json('GET', '/api/activitytypecategories?limit=2'); + + $response->assertJsonFragment([ + 'total' => 10, + 'current_page' => 1, + 'per_page' => 2, + 'last_page' => 5, + ]); + } + + /** @test */ + public function it_stores_a_activity_type_category() + { + $user = $this->signin(); + + $response = $this->json('POST', '/api/activitytypecategories', [ + 'name' => 'Movies', + ]); + + $response->assertStatus(200); + + $this->assertDatabaseHas('activity_type_categories', [ + 'name' => 'Movies', + ]); + + $response->assertJsonStructure([ + 'data' => $this->jsonStructureActivityTypeCategory, + ]); + } + + /** @test */ + public function it_updates_a_activity_type_category() + { + $user = $this->signin(); + + $activityTypeCategory = factory(ActivityTypeCategory::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('PUT', '/api/activitytypecategories/'.$activityTypeCategory->id, [ + 'name' => 'Movies', + ]); + + $response->assertStatus(200); + + $this->assertDatabaseHas('activity_type_categories', [ + 'name' => 'Movies', + ]); + + $response->assertJsonStructure([ + 'data' => $this->jsonStructureActivityTypeCategory, + ]); + } + + /** @test */ + public function it_doesnt_update_if_custom_field_not_found() + { + $user = $this->signin(); + + $response = $this->json('PUT', '/api/activitytypecategories/2349273984279348', [ + 'name' => 'Movies', + ]); + + $response->assertStatus(422); + + $this->expectDataError($response, [ + 'The selected activity type category id is invalid.', + ]); + } + + /** @test */ + public function it_deletes_a_activity_type_category() + { + $user = $this->signin(); + + $activityTypeCategory = factory(ActivityTypeCategory::class)->create([ + 'account_id' => $user->account_id, + 'name' => 'France', + ]); + + $response = $this->delete('/api/activitytypecategories/'.$activityTypeCategory->id); + + $response->assertStatus(200); + + $this->assertDatabaseMissing('activity_type_categories', [ + 'id' => $activityTypeCategory->id, + ]); + + $response->assertJsonFragment([ + 'deleted' => true, + 'id' => $activityTypeCategory->id, + ]); + } + + /** @test */ + public function it_doesnt_delete_the_custom_field_if_not_found() + { + $user = $this->signin(); + + $response = $this->delete('/api/activitytypecategories/2349273984279348'); + + $response->assertStatus(422); + + $this->expectDataError($response, [ + 'The selected activity type category id is invalid.', + ]); + } + + /** @test */ + public function it_gets_a_single_activity_type_category() + { + $user = $this->signin(); + + $activityTypeCategory = factory(ActivityTypeCategory::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('GET', '/api/activitytypecategories/'.$activityTypeCategory->id); + + $response->assertJsonStructure([ + 'data' => $this->jsonStructureActivityTypeCategory, + ]); + } +} diff --git a/tests/Api/Account/Activity/ApiActivityTypeControllerTest.php b/tests/Api/Account/Activity/ApiActivityTypeControllerTest.php new file mode 100644 index 0000000..645369f --- /dev/null +++ b/tests/Api/Account/Activity/ApiActivityTypeControllerTest.php @@ -0,0 +1,228 @@ + [ + 'id', + 'object', + 'name', + 'account' => [ + 'id', + ], + 'created_at', + 'updated_at', + ], + 'account' => [ + 'id', + ], + 'created_at', + 'updated_at', + ]; + + /** @test */ + public function it_gets_a_list_of_activity_types() + { + $user = $this->signin(); + + factory(ActivityType::class, 10)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('GET', '/api/activitytypes'); + + $response->assertJsonStructure([ + 'data' => [ + '*' => $this->jsonStructureActivityType, + ], + ]); + } + + /** @test */ + public function it_applies_limit_parameter() + { + $user = $this->signin(); + + $activityTypes = factory(ActivityType::class, 10)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('GET', '/api/activitytypes?limit=1'); + + $response->assertJsonFragment([ + 'total' => 10, + 'current_page' => 1, + 'per_page' => 1, + 'last_page' => 10, + ]); + + $response = $this->json('GET', '/api/activitytypes?limit=2'); + + $response->assertJsonFragment([ + 'total' => 10, + 'current_page' => 1, + 'per_page' => 2, + 'last_page' => 5, + ]); + } + + /** @test */ + public function it_stores_an_activity_type() + { + $user = $this->signin(); + + $activityTypeCategory = factory(ActivityTypeCategory::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('POST', '/api/activitytypes', [ + 'name' => 'Movies', + 'activity_type_category_id' => $activityTypeCategory->id, + ]); + + $response->assertStatus(200); + + $this->assertDatabaseHas('activity_types', [ + 'name' => 'Movies', + 'activity_type_category_id' => $activityTypeCategory->id, + ]); + + $response->assertJsonStructure([ + 'data' => $this->jsonStructureActivityType, + ]); + } + + /** @test */ + public function it_doesnt_store_an_activity_type_if_query_not_valid() + { + $user = $this->signin(); + + $response = $this->json('POST', '/api/activitytypes'); + + $this->expectDataError($response, [ + 'The activity type category id field is required.', + ]); + } + + /** @test */ + public function it_updates_an_activity_type() + { + $user = $this->signin(); + + $activityTypeCategory = factory(ActivityTypeCategory::class)->create([ + 'account_id' => $user->account_id, + ]); + + $activityType = factory(ActivityType::class)->create([ + 'account_id' => $user->account_id, + 'activity_type_category_id' => $activityTypeCategory->id, + ]); + + $response = $this->json('PUT', '/api/activitytypes/'.$activityType->id, [ + 'name' => 'Movies', + 'activity_type_category_id' => $activityTypeCategory->id, + ]); + + $response->assertStatus(200); + + $this->assertDatabaseHas('activity_types', [ + 'name' => 'Movies', + ]); + + $response->assertJsonStructure([ + 'data' => $this->jsonStructureActivityType, + ]); + } + + /** @test */ + public function it_doesnt_update_if_activity_type_not_found() + { + $user = $this->signin(); + + $response = $this->json('PUT', '/api/activitytypes/2349273984279348', [ + 'name' => 'Movies', + ]); + + $this->expectDataError($response, [ + 'The activity type category id field is required.', + ]); + } + + /** @test */ + public function it_deletes_an_activity_type() + { + $user = $this->signin(); + + $activityType = factory(ActivityType::class)->create([ + 'account_id' => $user->account_id, + ]); + $activities = factory(Activity::class, 10)->create([ + 'account_id' => $user->account_id, + 'activity_type_id' => $activityType->id, + ]); + + $response = $this->delete('/api/activitytypes/'.$activityType->id); + + $response->assertStatus(200); + + $this->assertDatabaseMissing('activity_types', [ + 'id' => $activityType->id, + ]); + + $this->assertDatabaseMissing('activities', [ + 'activity_type_id' => $activityType->id, + ]); + + $response->assertJsonFragment([ + 'deleted' => true, + 'id' => $activityType->id, + ]); + } + + /** @test */ + public function it_doesnt_delete_the_activity_type_if_not_found() + { + $user = $this->signin(); + + $response = $this->delete('/api/activitytypes/2349273984279348'); + + $this->expectDataError($response, [ + 'The selected activity type id is invalid.', + ]); + } + + /** @test */ + public function it_gets_a_single_activity_type() + { + $user = $this->signin(); + + $activityTypeCategory = factory(ActivityTypeCategory::class)->create([ + 'account_id' => $user->account_id, + ]); + + $activityType = factory(ActivityType::class)->create([ + 'account_id' => $user->account_id, + 'activity_type_category_id' => $activityTypeCategory->id, + ]); + + $response = $this->json('GET', '/api/activitytypes/'.$activityType->id); + + $response->assertJsonStructure([ + 'data' => $this->jsonStructureActivityType, + ]); + } +} diff --git a/tests/Api/Account/ApiCompanyControllerTest.php b/tests/Api/Account/ApiCompanyControllerTest.php new file mode 100644 index 0000000..15a25d0 --- /dev/null +++ b/tests/Api/Account/ApiCompanyControllerTest.php @@ -0,0 +1,216 @@ + [ + 'id', + ], + 'created_at', + 'updated_at', + ]; + + /** @test */ + public function it_gets_a_list_of_companies() + { + $user = $this->signin(); + + factory(Company::class, 3)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('GET', '/api/companies'); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => ['*' => $this->jsonCompany], + ]); + } + + /** @test */ + public function it_applies_the_limit_parameter_in_search() + { + $user = $this->signin(); + + factory(Company::class, 10)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('GET', '/api/companies?limit=1'); + + $response->assertJsonFragment([ + 'total' => 10, + 'current_page' => 1, + 'per_page' => 1, + 'last_page' => 10, + ]); + + $response = $this->json('GET', '/api/companies?limit=2'); + + $response->assertJsonFragment([ + 'total' => 10, + 'current_page' => 1, + 'per_page' => 2, + 'last_page' => 5, + ]); + } + + /** @test */ + public function it_gets_one_company() + { + $user = $this->signin(); + + $company = factory(Company::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('get', '/api/companies/'.$company->id); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => $this->jsonCompany, + ]); + $response->assertJsonFragment([ + 'object' => 'company', + 'id' => $company->id, + ]); + } + + /** @test */ + public function it_cant_get_a_call_with_unexistent_id() + { + $user = $this->signin(); + + $response = $this->json('get', '/api/companies/0'); + + $this->expectNotFound($response); + } + + /** @test */ + public function it_creates_a_company() + { + $user = $this->signin(); + + $response = $this->json('post', '/api/companies', [ + 'name' => 'Central Perk', + ]); + + $response->assertStatus(201); + $response->assertJsonStructure([ + 'data' => $this->jsonCompany, + ]); + + $companyId = $response->json('data.id'); + + $response->assertJsonFragment([ + 'object' => 'company', + 'id' => $companyId, + ]); + + $this->assertDatabaseHas('companies', [ + 'account_id' => $user->account_id, + 'id' => $companyId, + 'name' => 'Central Perk', + 'number_of_employees' => null, + ]); + } + + /** @test */ + public function it_updates_a_company() + { + $user = $this->signin(); + + $company = factory(Company::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('put', '/api/companies/'.$company->id, [ + 'name' => 'Central Perk Central', + 'number_of_employees' => 30, + ]); + + $response->assertStatus(200); + + $response->assertJsonStructure([ + 'data' => $this->jsonCompany, + ]); + + $companyId = $response->json('data.id'); + + $this->assertEquals($company->id, $companyId); + + $response->assertJsonFragment([ + 'object' => 'company', + 'id' => $companyId, + ]); + + $this->assertDatabaseHas('companies', [ + 'account_id' => $user->account_id, + 'id' => $companyId, + 'name' => 'Central Perk Central', + 'number_of_employees' => 30, + ]); + } + + /** @test */ + public function it_cant_update_a_company_if_account_is_not_linked_to_company() + { + $user = $this->signin(); + + $account = factory(Account::class)->create([]); + $company = factory(Company::class)->create([ + 'account_id' => $account->id, + ]); + + $response = $this->json('put', '/api/companies/'.$company->id, [ + 'name' => 'Central Perk', + ]); + + $this->expectNotFound($response); + } + + /** @test */ + public function it_deletes_a_company() + { + $user = $this->signin(); + + $company = factory(Company::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('delete', '/api/companies/'.$company->id); + + $response->assertStatus(200); + + $this->assertdatabasemissing('companies', [ + 'account_id' => $user->account_id, + 'id' => $company->id, + ]); + } + + /** @test */ + public function it_cant_delete_a_company_if_company_doesnt_exist() + { + $user = $this->signin(); + + $response = $this->json('delete', '/api/companies/0'); + + $this->expectDataError($response, [ + 'The selected company id is invalid.', + ]); + } +} diff --git a/tests/Api/Account/ApiGenderControllerTest.php b/tests/Api/Account/ApiGenderControllerTest.php new file mode 100644 index 0000000..8debdd8 --- /dev/null +++ b/tests/Api/Account/ApiGenderControllerTest.php @@ -0,0 +1,236 @@ + [ + 'id', + ], + 'created_at', + 'updated_at', + ]; + + /** @test */ + public function it_gets_a_list_of_genders() + { + $user = $this->signin(); + + factory(Gender::class, 3)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('GET', '/api/genders'); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => ['*' => $this->jsonGender], + ]); + } + + /** @test */ + public function it_applies_the_limit_parameter_in_search() + { + $user = $this->signin(); + + factory(Gender::class, 10)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('GET', '/api/genders?limit=1'); + + $response->assertJsonFragment([ + 'total' => 10, + 'current_page' => 1, + 'per_page' => 1, + 'last_page' => 10, + ]); + + $response = $this->json('GET', '/api/genders?limit=2'); + + $response->assertJsonFragment([ + 'total' => 10, + 'current_page' => 1, + 'per_page' => 2, + 'last_page' => 5, + ]); + } + + /** @test */ + public function it_gets_one_gender() + { + $user = $this->signin(); + + $gender = factory(Gender::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('get', '/api/genders/'.$gender->id); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => $this->jsonGender, + ]); + $response->assertJsonFragment([ + 'object' => 'gender', + 'id' => $gender->id, + ]); + } + + /** @test */ + public function it_cant_get_a_gender_with_unexistent_id() + { + $user = $this->signin(); + + $response = $this->json('get', '/api/genders/0'); + + $this->expectNotFound($response); + } + + /** @test */ + public function it_creates_a_gender() + { + $user = $this->signin(); + + $response = $this->json('POST', '/api/genders', [ + 'name' => 'man', + 'type' => 'M', + ]); + + $response->assertStatus(201); + $response->assertJsonStructure([ + 'data' => $this->jsonGender, + ]); + + $genderId = $response->json('data.id'); + + $response->assertJsonFragment([ + 'object' => 'gender', + 'id' => $genderId, + ]); + + $this->assertDatabasehas('genders', [ + 'account_id' => $user->account_id, + 'id' => $genderId, + 'name' => 'man', + 'type' => 'M', + ]); + } + + /** @test */ + public function it_updates_a_gender() + { + $user = $this->signin(); + + $gender = factory(Gender::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('put', '/api/genders/'.$gender->id, [ + 'name' => 'man', + 'type' => 'M', + ]); + + $response->assertStatus(200); + + $response->assertJsonStructure([ + 'data' => $this->jsonGender, + ]); + + $genderId = $response->json('data.id'); + + $this->assertEquals($gender->id, $genderId); + + $response->assertJsonFragment([ + 'object' => 'gender', + 'id' => $genderId, + ]); + + $this->assertDatabaseHas('genders', [ + 'account_id' => $user->account_id, + 'id' => $genderId, + 'name' => 'man', + 'type' => 'M', + ]); + } + + /** @test */ + public function it_cant_update_a_gender_if_account_is_not_linked_to_gender() + { + $user = $this->signin(); + + $account = factory(Account::class)->create([]); + $gender = factory(Gender::class)->create([ + 'account_id' => $account->id, + ]); + + $response = $this->json('put', '/api/genders/'.$gender->id, [ + 'name' => 'man', + 'type' => 'M', + ]); + + $this->expectNotFound($response); + } + + /** @test */ + public function it_cant_update_a_gender_if_account_is_not_linked_to_gender2() + { + $user = $this->signin(); + + $account = factory(Account::class)->create([]); + $gender = factory(Gender::class)->create([ + 'account_id' => $account->id, + ]); + + $response = $this->json('put', '/api/genders/'.$gender->id, [ + 'account_id' => $account->id, + 'name' => 'man', + 'type' => 'M', + ]); + + $this->expectNotFound($response); + } + + /** @test */ + public function it_deletes_a_gender() + { + $user = $this->signin(); + + $gender = factory(Gender::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('delete', '/api/genders/'.$gender->id); + + $response->assertStatus(200); + + $this->assertDatabaseMissing('genders', [ + 'account_id' => $user->account_id, + 'id' => $gender->id, + ]); + } + + /** @test */ + public function it_cant_delete_a_gender_if_gender_doesnt_exist() + { + $user = $this->signin(); + + $response = $this->json('delete', '/api/genders/0'); + + $this->expectDataError($response, [ + 'The selected gender id is invalid.', + ]); + } +} diff --git a/tests/Api/Account/ApiPlaceControllerTest.php b/tests/Api/Account/ApiPlaceControllerTest.php new file mode 100644 index 0000000..78bae23 --- /dev/null +++ b/tests/Api/Account/ApiPlaceControllerTest.php @@ -0,0 +1,210 @@ + [ + 'id', + ], + 'created_at', + 'updated_at', + ]; + + public function test_it_gets_a_list_of_places() + { + $user = $this->signin(); + + factory(Place::class, 3)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('GET', '/api/places'); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => ['*' => $this->jsonPlace], + ]); + } + + public function test_it_applies_the_limit_parameter_in_search() + { + $user = $this->signin(); + + factory(Place::class, 10)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('GET', '/api/places?limit=1'); + + $response->assertJsonFragment([ + 'total' => 10, + 'current_page' => 1, + 'per_page' => 1, + 'last_page' => 10, + ]); + + $response = $this->json('GET', '/api/places?limit=2'); + + $response->assertJsonFragment([ + 'total' => 10, + 'current_page' => 1, + 'per_page' => 2, + 'last_page' => 5, + ]); + } + + public function test_it_gets_one_place() + { + $user = $this->signin(); + + $place = factory(Place::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('get', '/api/places/'.$place->id); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => $this->jsonPlace, + ]); + $response->assertJsonFragment([ + 'object' => 'place', + 'id' => $place->id, + ]); + } + + public function test_it_cant_get_a_call_with_unexistent_id() + { + $user = $this->signin(); + + $response = $this->json('get', '/api/places/0'); + + $this->expectNotFound($response); + } + + public function test_it_create_a_place() + { + $user = $this->signin(); + + $response = $this->json('post', '/api/places', [ + 'city' => 'New York', + ]); + + $response->assertStatus(201); + $response->assertJsonStructure([ + 'data' => $this->jsonPlace, + ]); + + $placeId = $response->json('data.id'); + + $response->assertJsonFragment([ + 'object' => 'place', + 'id' => $placeId, + ]); + + $this->assertDatabaseHas('places', [ + 'account_id' => $user->account_id, + 'id' => $placeId, + 'city' => 'New York', + 'latitude' => null, + ]); + } + + public function test_it_updates_a_place() + { + $user = $this->signin(); + + $place = factory(Place::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('put', '/api/places/'.$place->id, [ + 'city' => 'New York', + ]); + + $response->assertStatus(200); + + $response->assertJsonStructure([ + 'data' => $this->jsonPlace, + ]); + + $placeId = $response->json('data.id'); + + $this->assertEquals($place->id, $placeId); + + $response->assertJsonFragment([ + 'object' => 'place', + 'id' => $placeId, + ]); + + $this->assertDatabaseHas('places', [ + 'account_id' => $user->account_id, + 'id' => $placeId, + 'city' => 'New York', + 'latitude' => null, + ]); + } + + public function test_it_cant_update_a_place_if_account_is_not_linked_to_place() + { + $user = $this->signin(); + + $account = factory(Account::class)->create([]); + $place = factory(Place::class)->create([ + 'account_id' => $account->id, + ]); + + $response = $this->json('put', '/api/places/'.$place->id, [ + 'city' => 'New York', + ]); + + $this->expectNotFound($response); + } + + public function test_it_deletes_a_place() + { + $user = $this->signin(); + + $place = factory(Place::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('delete', '/api/places/'.$place->id); + + $response->assertStatus(200); + + $this->assertdatabasemissing('places', [ + 'account_id' => $user->account_id, + 'id' => $place->id, + ]); + } + + public function test_it_cant_delete_a_place_if_place_doesnt_exist() + { + $user = $this->signin(); + + $response = $this->json('delete', '/api/places/0'); + + $this->expectDataError($response, [ + 'The selected place id is invalid.', + ]); + } +} diff --git a/tests/Api/Account/ApiUserControllerTest.php b/tests/Api/Account/ApiUserControllerTest.php new file mode 100644 index 0000000..f69000e --- /dev/null +++ b/tests/Api/Account/ApiUserControllerTest.php @@ -0,0 +1,157 @@ + [ + 'id', + ], + 'created_at', + 'updated_at', + ]; + + /** @test */ + public function it_gets_the_authenticated_user() + { + $user = $this->signIn(); + + $response = $this->get('/api/me'); + + $response->assertStatus(200); + + $response->assertJsonStructure([ + 'data' => $this->jsonStructureUser, + ]); + + $response->assertJsonFragment([ + 'first_name' => $user->first_name, + 'object' => 'user', + ]); + } + + /** @test */ + public function it_tells_if_the_user_has_signed_a_given_policy() + { + $user = $this->signIn(); + + $term = factory(Term::class)->create([]); + $user->terms()->syncWithoutDetaching([$term->id => ['account_id' => $user->account_id]]); + + $response = $this->get('/api/me/compliance/'.$term->id); + + $response->assertJsonFragment([ + 'signed' => true, + 'ip_address' => null, + ]); + + $response->assertJsonStructure([ + 'data' => [ + 'signed', + 'signed_date', + 'ip_address', + 'user', + 'term', + ], + ]); + } + + /** @test */ + public function it_returns_method_not_found_if_no_policy_is_found() + { + $user = $this->signIn(); + + $response = $this->get('/api/me/compliance/32455212'); + + $response->assertStatus(404); + + $response->assertJsonFragment([ + 'message' => 'The resource has not been found', + 'error_code' => 31, + ]); + } + + /** @test */ + public function it_gets_all_the_compliances_signed_by_user() + { + $user = $this->signIn(); + $term = factory(Term::class)->create([]); + $user->terms()->syncWithoutDetaching([$term->id => ['account_id' => $user->account_id]]); + + $term2 = factory(Term::class)->create([]); + $user->terms()->syncWithoutDetaching([$term2->id => ['account_id' => $user->account_id]]); + + $response = $this->get('/api/me/compliance'); + + $response->assertStatus(200); + + $response->assertJsonCount(2, 'data'); + } + + /** @test */ + public function it_gets_no_compliances_signed_by_user() + { + $user = $this->signIn(); + + $response = $this->get('/api/me/compliance'); + + $response->assertStatus(404); + } + + /** @test */ + public function it_tries_to_sign_lapolicy() + { + $user = $this->signIn(); + + $response = $this->post('/api/me/compliance'); + + $this->expectDataError($response, [ + 'The ip address field is required.', + ]); + } + + /** @test */ + public function it_signs_lapolicy() + { + $user = $this->signIn(); + $term = factory(Term::class)->create([]); + $user->terms()->syncWithoutDetaching([$term->id => ['account_id' => $user->account_id]]); + + $term2 = factory(Term::class)->create([]); + $user->terms()->syncWithoutDetaching([$term2->id => ['account_id' => $user->account_id]]); + + $params = [ + 'ip_address' => '128.3.1.2', + ]; + + $response = $this->json('POST', '/api/me/compliance', $params); + + $response->assertStatus(200); + + $response->assertJsonStructure([ + 'data' => [ + 'signed', + 'signed_date', + 'ip_address', + 'user', + 'term', + ], + ]); + } +} diff --git a/tests/Api/ApiActivitiesTest.php b/tests/Api/ApiActivitiesTest.php new file mode 100644 index 0000000..a0fdcab --- /dev/null +++ b/tests/Api/ApiActivitiesTest.php @@ -0,0 +1,595 @@ + [ + 'id', + 'object', + 'name', + 'location_type', + 'activity_type_category' => [ + 'id', + 'object', + 'name', + 'account' => [ + 'id', + ], + 'created_at', + 'updated_at', + ], + 'account'=> [ + 'id', + ], + 'created_at', + 'updated_at', + ], + 'attendees' => [ + 'total', + 'contacts' => [ + '*' => [ + 'id', + 'object', + 'first_name', + 'last_name', + 'complete_name', + ], + ], + ], + 'emotions' => [ + '*' => [ + 'id', + 'object', + 'name', + ], + ], + 'account' => [ + 'id', + ], + 'created_at', + 'updated_at', + ]; + + protected $jsonActivityNoCategory = [ + 'id', + 'object', + 'summary', + 'description', + 'happened_at', + 'attendees' => [ + 'total', + 'contacts' => [ + '*' => [ + 'id', + 'object', + 'first_name', + 'last_name', + 'complete_name', + ], + ], + ], + 'emotions' => [ + '*' => [ + 'id', + 'object', + 'name', + ], + ], + 'account' => [ + 'id', + ], + 'created_at', + 'updated_at', + ]; + + /** @test */ + public function activities_get_all() + { + $user = $this->signin(); + $activity1 = factory(Activity::class)->create([ + 'account_id' => $user->account_id, + ]); + $activity2 = factory(Activity::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('GET', '/api/activities'); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => ['*' => $this->jsonActivity], + ]); + $response->assertJsonFragment([ + 'object' => 'activity', + 'id' => $activity1->id, + ]); + $response->assertJsonFragment([ + 'object' => 'activity', + 'id' => $activity2->id, + ]); + } + + /** @test */ + public function activities_get_contact_all() + { + $user = $this->signin(); + $contact1 = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $activity1 = factory(Activity::class)->create([ + 'account_id' => $user->account_id, + ]); + $activity1->contacts()->attach($contact1, ['account_id' => $user->account_id]); + + $contact2 = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $activity2 = factory(Activity::class)->create([ + 'account_id' => $user->account_id, + ]); + $activity2->contacts()->attach($contact2, ['account_id' => $user->account_id]); + + $response = $this->json('GET', '/api/contacts/'.$contact1->id.'/activities'); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => ['*' => $this->jsonActivity], + ]); + $response->assertJsonFragment([ + 'object' => 'activity', + 'id' => $activity1->id, + ]); + $response->assertJsonMissingExact([ + 'object' => 'activity', + 'id' => $activity2->id, + ]); + } + + /** @test */ + public function activities_get_contact_all_error() + { + $this->signin(); + + $response = $this->json('GET', '/api/contacts/0/activities'); + + $this->expectNotFound($response); + } + + /** @test */ + public function activities_get_contact_all_error_wrong_account() + { + $this->signin(); + $contact = factory(Contact::class)->create(); + + $response = $this->json('GET', '/api/contacts/'.$contact->id.'/activities'); + + $this->expectNotFound($response); + } + + /** @test */ + public function activities_get_one() + { + $user = $this->signin(); + $activity1 = factory(Activity::class)->create([ + 'account_id' => $user->account_id, + ]); + $activity2 = factory(Activity::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('GET', '/api/activities/'.$activity1->id); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => $this->jsonActivity, + ]); + $response->assertJsonFragment([ + 'object' => 'activity', + 'id' => $activity1->id, + ]); + $response->assertJsonMissingExact([ + 'object' => 'activity', + 'id' => $activity2->id, + ]); + } + + /** @test */ + public function activities_get_one_error() + { + $this->signin(); + + $response = $this->json('GET', '/api/activities/0'); + + $this->expectNotFound($response); + } + + /** @test */ + public function activities_get_one_error_wrong_account() + { + $this->signin(); + $activity = factory(Activity::class)->create(); + + $response = $this->json('GET', '/api/activities/'.$activity->id); + + $this->expectNotFound($response); + } + + /** @test */ + public function activities_create() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $activityType = factory(ActivityType::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('POST', '/api/activities', [ + 'contacts' => [$contact->id], + 'description' => 'the description', + 'summary' => 'the activity', + 'happened_at' => '2018-05-01', + 'activity_type_id' => $activityType->id, + ]); + + $response->assertStatus(201); + $response->assertJsonStructure([ + 'data' => $this->jsonActivity, + ]); + $activity_id = $response->json('data.id'); + $response->assertJsonFragment([ + 'object' => 'activity', + 'id' => $activity_id, + ]); + + $this->assertGreaterThan(0, $activity_id); + $this->assertDatabaseHas('activities', [ + 'account_id' => $user->account_id, + 'id' => $activity_id, + 'summary' => 'the activity', + 'description' => 'the description', + 'happened_at' => '2018-05-01', + ]); + $this->assertDatabaseHas('activity_contact', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'activity_id' => $activity_id, + ]); + } + + /** @test */ + public function activities_create_error_wrong_parameter() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('POST', '/api/activities', [ + 'contact_id' => [$contact->id], + ]); + + $this->expectDataError($response, [ + 'The summary field is required.', + 'The happened at field is required.', + ]); + } + + /** @test */ + public function activities_create_error_bad_account() + { + $this->signin(); + + $contact = factory(Contact::class)->create(); + + $response = $this->json('POST', '/api/activities', [ + 'contacts' => [$contact->id], + 'description' => 'the description', + 'summary' => 'the activity', + 'happened_at' => '2018-05-01', + ]); + + $this->expectNotFound($response); + } + + /** @test */ + public function activities_create_error_bad_account2() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $activityType = factory(ActivityType::class)->create(); + + $response = $this->json('POST', '/api/activities', [ + 'contacts' => [$contact->id], + 'description' => 'the description', + 'summary' => 'the activity', + 'happened_at' => '2018-05-01', + 'activity_type_id' => $activityType->id, + ]); + + $this->expectNotFound($response); + } + + /** @test */ + public function activities_update() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $activity = factory(Activity::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('PUT', '/api/activities/'.$activity->id, [ + 'contacts' => [$contact->id], + 'description' => 'the description', + 'summary' => 'the activity', + 'happened_at' => '2018-05-01', + ]); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => $this->jsonActivityNoCategory, + ]); + $activity_id = $response->json('data.id'); + $this->assertEquals($activity->id, $activity_id); + $response->assertJsonFragment([ + 'object' => 'activity', + 'id' => $activity_id, + ]); + + $this->assertGreaterThan(0, $activity_id); + $this->assertDatabaseHas('activities', [ + 'account_id' => $user->account_id, + 'id' => $activity_id, + 'summary' => 'the activity', + 'happened_at' => '2018-05-01', + ]); + $this->assertDatabaseHas('activity_contact', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'activity_id' => $activity_id, + ]); + } + + /** @test */ + public function activities_update_category() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $activity = factory(Activity::class)->create([ + 'account_id' => $user->account_id, + ]); + $activityType = factory(ActivityType::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('PUT', '/api/activities/'.$activity->id, [ + 'contacts' => [$contact->id], + 'description' => 'the description', + 'summary' => 'the activity', + 'happened_at' => '2018-05-01', + 'activity_type_id' => $activityType->id, + ]); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => $this->jsonActivity, + ]); + $activity_id = $response->json('data.id'); + $this->assertEquals($activity->id, $activity_id); + $response->assertJsonFragment([ + 'object' => 'activity', + 'id' => $activity_id, + ]); + + $activity_type_id = $response->json('data.activity_type.id'); + $this->assertEquals($activityType->id, $activity_type_id); + $response->assertJsonFragment([ + 'object' => 'activityType', + 'id' => $activity_type_id, + ]); + + $this->assertGreaterThan(0, $activity_id); + $this->assertDatabaseHas('activities', [ + 'account_id' => $user->account_id, + 'id' => $activity_id, + 'summary' => 'the activity', + 'happened_at' => '2018-05-01', + 'activity_type_id' => $activityType->id, + ]); + $this->assertDatabaseHas('activity_contact', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'activity_id' => $activity_id, + ]); + } + + /** @test */ + public function activities_update_existing() + { + $user = $this->signin(); + $activity = factory(Activity::class)->create([ + 'account_id' => $user->account_id, + ]); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $contact->activities()->attach($activity, [ + 'account_id' => $user->account_id, + ]); + $contact2 = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $contact2->activities()->attach($activity, [ + 'account_id' => $user->account_id, + ]); + $this->assertDatabaseHas('activity_contact', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'activity_id' => $activity->id, + ]); + $this->assertDatabaseHas('activity_contact', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact2->id, + 'activity_id' => $activity->id, + ]); + + $response = $this->json('PUT', '/api/activities/'.$activity->id, [ + 'contacts' => [$contact->id], + 'description' => 'the description', + 'summary' => 'the activity', + 'happened_at' => '2018-05-01', + ]); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => $this->jsonActivityNoCategory, + ]); + $activity_id = $response->json('data.id'); + $this->assertEquals($activity->id, $activity_id); + $response->assertJsonFragment([ + 'object' => 'activity', + 'id' => $activity_id, + ]); + + $this->assertGreaterThan(0, $activity_id); + $this->assertDatabaseHas('activities', [ + 'account_id' => $user->account_id, + 'id' => $activity_id, + 'summary' => 'the activity', + 'happened_at' => '2018-05-01', + ]); + $this->assertDatabaseHas('activity_contact', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'activity_id' => $activity_id, + ]); + $this->assertDatabaseMissing('activity_contact', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact2->id, + 'activity_id' => $activity_id, + ]); + } + + /** @test */ + public function activities_update_error_wrong_parameter() + { + $user = $this->signin(); + + $response = $this->json('PUT', '/api/activities/0', [ + 'description' => 'the description', + 'summary' => 'the activity', + 'happened_at' => '2018-05-01', + ]); + + $this->expectDataError($response, [ + 'The selected activity id is invalid.', + ]); + } + + /** @test */ + public function activities_update_error_wrong_account_for_activity() + { + $user = $this->signin(); + + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $activity = factory(Activity::class)->create(); + + $response = $this->json('PUT', '/api/activities/'.$activity->id, [ + 'contacts' => [$contact->id], + 'description' => 'the description', + 'summary' => 'the activity', + 'happened_at' => '2018-05-01', + ]); + + $this->expectNotFound($response); + } + + /** @test */ + public function activities_update_error_wrong_account_for_contacts() + { + $user = $this->signin(); + + $contact = factory(Contact::class)->create(); + $activity = factory(Activity::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('PUT', '/api/activities/'.$activity->id, [ + 'contacts' => [$contact->id], + 'description' => 'the description', + 'summary' => 'the activity', + 'happened_at' => '2018-05-01', + ]); + + $this->expectNotFound($response); + } + + /** @test */ + public function activities_delete() + { + $user = $this->signin(); + $activity = factory(Activity::class)->create([ + 'account_id' => $user->account_id, + ]); + $this->assertDatabaseHas('activities', [ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('DELETE', '/api/activities/'.$activity->id); + + $response->assertStatus(200); + $this->assertDatabaseMissing('activities', [ + 'account_id' => $user->account_id, + 'id' => $activity->id, + ]); + } + + /** @test */ + public function activities_delete_error() + { + $this->signin(); + + $response = $this->json('DELETE', '/api/activities/0'); + + $this->expectDataError($response, [ + 'The selected activity id is invalid.', + ]); + } + + /** @test */ + public function activities_delete_with_wrong_account() + { + $this->signin(); + $activity = factory(Activity::class)->create(); + + $response = $this->json('DELETE', '/api/activities/'.$activity->id); + + $this->expectNotFound($response); + } +} diff --git a/tests/Api/ApiControllerTest.php b/tests/Api/ApiControllerTest.php new file mode 100644 index 0000000..6b5b4b7 --- /dev/null +++ b/tests/Api/ApiControllerTest.php @@ -0,0 +1,203 @@ +assertEquals( + 200, + $apiController->getHTTPStatusCode() + ); + + $apiController->setHTTPStatusCode(300); + + $this->assertEquals( + 300, + $apiController->getHTTPStatusCode() + ); + } + + /** @test */ + public function get_error_code_returns_the_error_code() + { + $apiController = new ApiController; + + $this->assertNull( + $apiController->getErrorCode() + ); + + $apiController->setErrorCode(30); + + $this->assertEquals( + 30, + $apiController->getErrorCode() + ); + } + + /** @test */ + public function get_with_parameter_returns_the_parameter() + { + $apiController = new ApiController; + + $this->assertNull( + $apiController->getWithParameter() + ); + + $apiController->setWithParameter('test'); + + $this->assertEquals( + 'test', + $apiController->getWithParameter() + ); + } + + /** @test */ + public function get_limit_per_page_code_returns_the_limit_per_page() + { + $apiController = new ApiController; + + $this->assertEquals( + 0, + $apiController->getLimitPerPage() + ); + + $apiController->setLimitPerPage(30); + + $this->assertEquals( + 30, + $apiController->getLimitPerPage() + ); + } + + /** @test */ + public function it_gets_the_sort_criteria() + { + $apiController = new ApiController; + + $this->assertEquals( + 'created_at', + $apiController->getSortCriteria() + ); + + $apiController->setSortCriteria('created_at'); + + $this->assertEquals( + 'created_at', + $apiController->getSortCriteria() + ); + } + + /** @test */ + public function it_only_accepts_some_sorting_parameters() + { + $apiController = new ApiController; + + $apiController->setSortCriteria('created_at'); + + $this->assertEquals( + 'created_at', + $apiController->getSortCriteria() + ); + + $apiController->setSortCriteria('anything'); + + $this->assertEquals( + '', + $apiController->getSortCriteria() + ); + } + + /** @test */ + public function calling_api_with_insane_limit_number_raises_an_error() + { + $user = $this->signin(); + + $insaneLimit = config('api.max_limit_per_page') * 100; + + $response = $this->json('GET', "/api/contacts?limit={$insaneLimit}"); + + $response->assertStatus(400); + + $response->assertJsonFragment([ + 'message' => 'The limit parameter is too big', + 'error_code' => 30, + ]); + } + + /** @test */ + public function calling_api_with_a_wrong_sort_parameter_raises_an_error() + { + $user = $this->signin(); + + $criteria = 'anything'; + + $response = $this->json('GET', "/api/contacts?sort={$criteria}"); + + $response->assertStatus(400); + + $response->assertJsonFragment([ + 'message' => 'The sorting criteria is invalid', + 'error_code' => 39, + ]); + } + + /** @test */ + public function it_sets_the_order_by_parameters() + { + $apiController = new ApiController; + + $apiController->setSortCriteria('created_at'); + + $this->assertEquals( + 'created_at', + $apiController->getSortCriteria() + ); + + $this->assertEquals( + 'asc', + $apiController->getSortDirection() + ); + + $apiController->setSortCriteria('-created_at'); + + $this->assertEquals( + 'created_at', + $apiController->getSortCriteria() + ); + + $this->assertEquals( + 'desc', + $apiController->getSortDirection() + ); + } + + /** @test */ + public function root_api() + { + $user = $this->signin(); + + $response = $this->json('GET', '/api'); + + $response->assertStatus(200); + + $response->assertJsonFragment([ + 'success' => [ + 'message' => 'Welcome to Monica', + ], + ]); + $response->assertJsonFragment([ + 'contacts_url' => route('api.contacts'), + ]); + } +} diff --git a/tests/Api/ApiCountriesTest.php b/tests/Api/ApiCountriesTest.php new file mode 100644 index 0000000..f018054 --- /dev/null +++ b/tests/Api/ApiCountriesTest.php @@ -0,0 +1,62 @@ +signin(); + + $response = $this->json('GET', '/api/countries'); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => ['*' => $this->jsonCountries], + ]); + $response->assertJsonFragment([ + 'de' => [ + 'id' => 'DE', + 'iso' => 'DE', + 'name' => 'Germany', + 'object' => 'country', + ], + ]); + } + + /** @test */ + public function it_gets_a_specific_country_in_a_specific_locale() + { + $user = $this->signin(); + $user->locale = 'fr'; + $user->save(); + + $response = $this->json('GET', '/api/countries'); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => ['*' => $this->jsonCountries], + ]); + $response->assertJsonFragment([ + 'de' => [ + 'id' => 'DE', + 'iso' => 'DE', + 'name' => 'Allemagne', + 'object' => 'country', + ], + ]); + } +} diff --git a/tests/Api/ApiDebtsTest.php b/tests/Api/ApiDebtsTest.php new file mode 100644 index 0000000..b0adfb9 --- /dev/null +++ b/tests/Api/ApiDebtsTest.php @@ -0,0 +1,377 @@ + [ + 'id', + ], + 'contact' => [ + 'id', + ], + 'created_at', + 'updated_at', + ]; + + /** @test */ + public function it_gets_all_the_debts() + { + $user = $this->signin(); + $contact1 = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $debt1 = factory(Debt::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact1->id, + ]); + $contact2 = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $debt2 = factory(Debt::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact2->id, + ]); + + $response = $this->json('GET', '/api/debts'); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => ['*' => $this->jsonDebt], + ]); + $response->assertJsonFragment([ + 'object' => 'debt', + 'id' => $debt1->id, + ]); + $response->assertJsonFragment([ + 'object' => 'debt', + 'id' => $debt2->id, + ]); + } + + /** @test */ + public function it_gets_all_the_debts_for_a_given_contact() + { + $user = $this->signin(); + $contact1 = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $debt1 = factory(Debt::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact1->id, + ]); + $contact2 = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $debt2 = factory(Debt::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact2->id, + ]); + + $response = $this->json('GET', '/api/contacts/'.$contact1->id.'/debts'); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => ['*' => $this->jsonDebt], + ]); + $response->assertJsonFragment([ + 'object' => 'debt', + 'id' => $debt1->id, + ]); + $response->assertJsonMissingExact([ + 'object' => 'debt', + 'id' => $debt2->id, + ]); + } + + /** @test */ + public function it_cant_get_debts_from_an_invalid_contact() + { + $user = $this->signin(); + + $response = $this->json('GET', '/api/contacts/0/debts'); + + $this->expectNotFound($response); + } + + /** @test */ + public function it_gets_one_debt() + { + $user = $this->signin(); + $contact1 = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $debt1 = factory(Debt::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact1->id, + ]); + $debt2 = factory(Debt::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact1->id, + ]); + + $response = $this->json('GET', '/api/debts/'.$debt1->id); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => $this->jsonDebt, + ]); + $response->assertJsonFragment([ + 'object' => 'debt', + 'id' => $debt1->id, + ]); + $response->assertJsonMissingExact([ + 'object' => 'debt', + 'id' => $debt2->id, + ]); + } + + /** @test */ + public function it_cant_get_a_debt_with_an_invalid_id() + { + $user = $this->signin(); + + $response = $this->json('GET', '/api/debts/0'); + + $this->expectNotFound($response); + } + + /** @test */ + public function it_creates_a_debt() + { + $user = $this->signin(); + $user->locale = 'fr'; + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $currency = factory(Currency::class)->create([ + 'iso' => 'USD', + 'symbol' => '$', + ]); + $user->currency()->associate($currency); + $user->save(); + + $response = $this->json('POST', '/api/debts', [ + 'contact_id' => $contact->id, + 'in_debt' => 'yes', + 'status' => 'inprogress', + 'amount' => 42, + 'reason' => 'that\'s why', + ]); + + $response->assertStatus(201); + $response->assertJsonStructure([ + 'data' => $this->jsonDebt, + ]); + $debt_id = $response->json('data.id'); + $response->assertJsonFragment([ + 'object' => 'debt', + 'id' => $debt_id, + 'in_debt' => 'yes', + 'status' => 'inprogress', + 'amount' => '42.00', + 'value' => '42,00', + 'amount_with_currency' => '42,00'.chr(0xA0).'$US', + 'reason' => 'that\'s why', + ]); + + $this->assertGreaterThan(0, $debt_id); + $this->assertDatabaseHas('debts', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'id' => $debt_id, + 'in_debt' => 'yes', + 'status' => 'inprogress', + 'amount' => 4200, + 'reason' => 'that\'s why', + ]); + } + + /** @test */ + public function it_cant_create_a_debt_if_fields_are_missing() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('POST', '/api/debts', [ + 'contact_id' => $contact->id, + ]); + + $this->expectDataError($response, [ + 'The in debt field is required.', + 'The status field is required.', + 'The amount field is required.', + ]); + } + + /** @test */ + public function it_cant_create_a_debt_with_a_bad_account() + { + $user = $this->signin(); + + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + + $response = $this->json('POST', '/api/debts', [ + 'contact_id' => $contact->id, + 'in_debt' => 'yes', + 'status' => 'inprogress', + 'amount' => 42, + 'reason' => 'that\'s why', + ]); + + $this->expectNotFound($response); + } + + /** @test */ + public function it_updates_a_debt() + { + $user = $this->signin(); + $user->locale = 'fr'; + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $debt = factory(Debt::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + ]); + + $response = $this->json('PUT', '/api/debts/'.$debt->id, [ + 'contact_id' => $contact->id, + 'in_debt' => 'yes', + 'status' => 'completed', + 'amount' => 142.01, + 'reason' => 'voilà', + ]); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => $this->jsonDebt, + ]); + $debt_id = $response->json('data.id'); + $this->assertEquals($debt->id, $debt_id); + $response->assertJsonFragment([ + 'object' => 'debt', + 'id' => $debt_id, + 'in_debt' => 'yes', + 'status' => 'completed', + 'amount' => '142.01', + 'value' => '142,01', + 'amount_with_currency' => '142,01'.chr(0xA0).'$US', + 'reason' => 'voilà', + ]); + + $this->assertGreaterThan(0, $debt_id); + $this->assertDatabaseHas('debts', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'id' => $debt_id, + 'in_debt' => 'yes', + 'status' => 'completed', + 'amount' => 14201, + 'reason' => 'voilà', + ]); + } + + /** @test */ + public function it_cant_update_a_debt_with_missing_parameters() + { + $user = $this->signin(); + $debt = factory(Debt::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('PUT', '/api/debts/'.$debt->id, [ + 'contact_id' => $debt->contact_id, + ]); + + $this->expectDataError($response, [ + 'The in debt field is required.', + 'The status field is required.', + 'The amount field is required.', + ]); + } + + /** @test */ + public function it_cant_update_a_debt_with_a_wrong_account() + { + $user = $this->signin(); + + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + $debt = factory(Debt::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + ]); + + $response = $this->json('PUT', '/api/debts/'.$debt->id, [ + 'contact_id' => $contact->id, + 'in_debt' => 'yes', + 'status' => 'completed', + 'amount' => 142, + 'reason' => 'voilà', + ]); + + $this->expectNotFound($response); + } + + /** @test */ + public function it_deletes_a_debt() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $debt = factory(Debt::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + ]); + $this->assertDatabaseHas('debts', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'id' => $debt->id, + ]); + + $response = $this->json('DELETE', '/api/debts/'.$debt->id); + + $response->assertStatus(200); + $this->assertDatabaseMissing('debts', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'id' => $debt->id, + ]); + } + + /** @test */ + public function it_cant_delete_a_debt_with_an_invalid_id() + { + $user = $this->signin(); + + $response = $this->json('DELETE', '/api/debts/0'); + + $this->expectNotFound($response); + } +} diff --git a/tests/Api/ApiGiftsTest.php b/tests/Api/ApiGiftsTest.php new file mode 100644 index 0000000..bb046f7 --- /dev/null +++ b/tests/Api/ApiGiftsTest.php @@ -0,0 +1,467 @@ + [ + 'id', + ], + 'contact' => [ + 'id', + ], + 'created_at', + 'updated_at', + ]; + + /** @test */ + public function it_gets_all_the_gifts() + { + $user = $this->signin(); + $contact1 = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $gift1 = factory(Gift::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact1->id, + ]); + $contact2 = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $gift2 = factory(Gift::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact2->id, + ]); + + $response = $this->json('GET', '/api/gifts'); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => ['*' => $this->jsonGift], + ]); + $response->assertJsonFragment([ + 'object' => 'gift', + 'id' => $gift1->id, + ]); + $response->assertJsonFragment([ + 'object' => 'gift', + 'id' => $gift2->id, + ]); + } + + /** @test */ + public function it_gets_all_the_gifts_of_a_contact() + { + $user = $this->signin(); + $contact1 = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $gift1 = factory(Gift::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact1->id, + ]); + $contact2 = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $gift2 = factory(Gift::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact2->id, + ]); + + $response = $this->json('GET', '/api/contacts/'.$contact1->id.'/gifts'); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => ['*' => $this->jsonGift], + ]); + $response->assertJsonFragment([ + 'object' => 'gift', + 'id' => $gift1->id, + ]); + $response->assertJsonMissingExact([ + 'object' => 'gift', + 'id' => $gift2->id, + ]); + } + + /** @test */ + public function it_cant_get_all_the_gifts_of_an_invalid_contact() + { + $user = $this->signin(); + + $response = $this->json('GET', '/api/contacts/0/gifts'); + + $this->expectNotFound($response); + } + + /** @test */ + public function it_gets_one_gift() + { + $user = $this->signin(); + $contact1 = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $gift1 = factory(Gift::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact1->id, + ]); + $gift2 = factory(Gift::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact1->id, + ]); + + $response = $this->json('GET', '/api/gifts/'.$gift1->id); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => $this->jsonGift, + ]); + $response->assertJsonFragment([ + 'object' => 'gift', + 'id' => $gift1->id, + ]); + $response->assertJsonMissingExact([ + 'object' => 'gift', + 'id' => $gift2->id, + ]); + } + + /** @test */ + public function it_cant_get_a_gift_with_an_invalid_id() + { + $user = $this->signin(); + + $response = $this->json('GET', '/api/gifts/0'); + + $this->expectNotFound($response); + } + + /** @test */ + public function it_create_a_gift() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('POST', '/api/gifts', [ + 'contact_id' => $contact->id, + 'status' => 'idea', + 'name' => 'the gift', + ]); + + $response->assertStatus(201); + $response->assertJsonStructure([ + 'data' => $this->jsonGift, + ]); + $gift_id = $response->json('data.id'); + $response->assertJsonFragment([ + 'object' => 'gift', + 'id' => $gift_id, + ]); + + $this->assertGreaterThan(0, $gift_id); + $this->assertDatabaseHas('gifts', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'id' => $gift_id, + 'name' => 'the gift', + ]); + } + + /** @test */ + public function gifts_create_is_for() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $contact2 = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('POST', '/api/gifts', [ + 'contact_id' => $contact->id, + 'name' => 'the gift', + 'status' => 'idea', + 'recipient_id' => $contact2->id, + ]); + + $response->assertStatus(201); + $response->assertJsonStructure([ + 'data' => $this->jsonGift, + ]); + $gift_id = $response->json('data.id'); + $response->assertJsonFragment([ + 'object' => 'gift', + 'id' => $gift_id, + ]); + + $this->assertGreaterThan(0, $gift_id); + $this->assertDatabaseHas('gifts', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'id' => $gift_id, + 'name' => 'the gift', + 'is_for' => $contact2->id, + ]); + } + + /** @test */ + public function gifts_create_is_for_bad_account() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $account = factory(Account::class)->create(); + $contact2 = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + + $response = $this->json('POST', '/api/gifts', [ + 'contact_id' => $contact->id, + 'name' => 'the gift', + 'status' => 'idea', + 'recipient_id' => $contact2->id, + ]); + + $this->expectNotFound($response); + } + + /** @test */ + public function gifts_create_error() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('POST', '/api/gifts', [ + 'contact_id' => $contact->id, + 'status' => 'idea', + ]); + + $this->expectDataError($response, [ + 'The name field is required.', + ]); + } + + /** @test */ + public function gifts_create_error_bad_account() + { + $user = $this->signin(); + + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + + $response = $this->json('POST', '/api/gifts', [ + 'contact_id' => $contact->id, + 'name' => 'the gift', + 'status' => 'idea', + ]); + + $this->expectNotFound($response); + } + + /** @test */ + public function gifts_update() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $gift = factory(Gift::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + ]); + + $response = $this->json('PUT', '/api/gifts/'.$gift->id, [ + 'contact_id' => $contact->id, + 'name' => 'the gift', + 'status' => 'idea', + 'comment' => 'one comment', + ]); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => $this->jsonGift, + ]); + $gift_id = $response->json('data.id'); + $this->assertEquals($gift->id, $gift_id); + $response->assertJsonFragment([ + 'object' => 'gift', + 'id' => $gift_id, + ]); + + $this->assertGreaterThan(0, $gift_id); + $this->assertDatabaseHas('gifts', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'id' => $gift_id, + 'name' => 'the gift', + 'comment' => 'one comment', + ]); + } + + /** @test */ + public function gifts_update_is_for() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $contact2 = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $gift = factory(Gift::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + ]); + + $response = $this->json('PUT', '/api/gifts/'.$gift->id, [ + 'contact_id' => $contact->id, + 'name' => 'the gift', + 'status' => 'idea', + 'comment' => 'one comment', + 'recipient_id' => $contact2->id, + ]); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => $this->jsonGift, + ]); + $gift_id = $response->json('data.id'); + $this->assertEquals($gift->id, $gift_id); + $response->assertJsonFragment([ + 'object' => 'gift', + 'id' => $gift_id, + ]); + + $this->assertGreaterThan(0, $gift_id); + $this->assertDatabaseHas('gifts', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'id' => $gift_id, + 'name' => 'the gift', + 'comment' => 'one comment', + 'is_for' => $contact2->id, + ]); + } + + /** @test */ + public function gifts_update_error() + { + $user = $this->signin(); + $gift = factory(Gift::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('PUT', '/api/gifts/'.$gift->id, [ + 'contact_id' => $gift->contact_id, + 'status' => 'idea', + ]); + + $this->expectDataError($response, [ + 'The name field is required.', + ]); + } + + /** @test */ + public function gifts_update_error_bad_account() + { + $user = $this->signin(); + + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + $gift = factory(Gift::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + ]); + + $response = $this->json('PUT', '/api/gifts/'.$gift->id, [ + 'contact_id' => $contact->id, + 'name' => 'the gift', + 'status' => 'idea', + 'comment' => 'one comment', + ]); + + $this->expectNotFound($response); + } + + /** @test */ + public function gifts_delete() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $gift = factory(Gift::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + ]); + $this->assertDatabaseHas('gifts', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'id' => $gift->id, + ]); + + $response = $this->json('DELETE', '/api/gifts/'.$gift->id); + + $response->assertStatus(200); + $response->assertJson([ + 'deleted' => true, + 'id' => $gift->id, + ]); + + $this->assertDatabaseMissing('gifts', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'id' => $gift->id, + ]); + } + + /** @test */ + public function gifts_delete_error() + { + $user = $this->signin(); + + $response = $this->json('DELETE', '/api/gifts/0'); + + $response->assertStatus(422); + } + + /** @test */ + public function gifts_delete_wrong_account() + { + $user = $this->signin(); + $gift = factory(Gift::class)->create(); + + $response = $this->json('DELETE', '/api/gifts/'.$gift->id); + + $this->expectNotFound($response); + } +} diff --git a/tests/Api/ApiJournalTest.php b/tests/Api/ApiJournalTest.php new file mode 100644 index 0000000..8a0b43a --- /dev/null +++ b/tests/Api/ApiJournalTest.php @@ -0,0 +1,215 @@ + [ + 'id', + ], + 'created_at', + 'updated_at', + ]; + + /** @test */ + public function it_gets_all_the_journal_entries() + { + $user = $this->signin(); + $firstEntry = factory(Entry::class)->create([ + 'account_id' => $user->account_id, + ]); + $secondEntry = factory(Entry::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('GET', '/api/journal'); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => ['*' => $this->jsonJournal], + ]); + $response->assertJsonFragment([ + 'object' => 'entry', + 'id' => $firstEntry->id, + ]); + $response->assertJsonFragment([ + 'object' => 'entry', + 'id' => $secondEntry->id, + ]); + } + + /** @test */ + public function it_gets_one_journal_entry() + { + $user = $this->signin(); + $firstEntry = factory(Entry::class)->create([ + 'account_id' => $user->account_id, + ]); + $secondEntry = factory(Entry::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('GET', '/api/journal/'.$firstEntry->id); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => $this->jsonJournal, + ]); + $response->assertJsonFragment([ + 'object' => 'entry', + 'id' => $firstEntry->id, + ]); + $response->assertJsonMissingExact([ + 'object' => 'entry', + 'id' => $secondEntry->id, + ]); + } + + /** @test */ + public function it_cant_get_a_journal_entry_with_an_invalid_id() + { + $user = $this->signin(); + + $response = $this->json('GET', '/api/journal/0'); + + $this->expectNotFound($response); + } + + /** @test */ + public function it_creates_a_journal_entry() + { + $user = $this->signin(); + + $response = $this->json('POST', '/api/journal', [ + 'title' => 'my title', + 'post' => 'content post', + ]); + + $response->assertStatus(201); + $response->assertJsonStructure([ + 'data' => $this->jsonJournal, + ]); + $entryId = $response->json('data.id'); + $response->assertJsonFragment([ + 'object' => 'entry', + 'id' => $entryId, + 'title' => 'my title', + 'post' => 'content post', + ]); + + $this->assertGreaterThan(0, $entryId); + $this->assertDatabaseHas('entries', [ + 'account_id' => $user->account_id, + 'id' => $entryId, + 'title' => 'my title', + 'post' => 'content post', + ]); + } + + /** @test */ + public function it_cant_create_a_journal_entry_with_missing_parameters() + { + $user = $this->signin(); + + $response = $this->json('POST', '/api/journal', []); + + $this->expectDataError($response, [ + 'The title field is required.', + 'The post field is required.', + ]); + } + + /** @test */ + public function it_updates_a_journal_entry() + { + $user = $this->signin(); + $entry = factory(Entry::class)->create([ + 'account_id' => $user->account_id, + 'title' => 'xxx', + ]); + + $response = $this->json('PUT', '/api/journal/'.$entry->id, [ + 'title' => 'my title', + 'post' => 'content post', + ]); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => $this->jsonJournal, + ]); + $entryId = $response->json('data.id'); + $this->assertEquals($entry->id, $entryId); + $response->assertJsonFragment([ + 'object' => 'entry', + 'id' => $entryId, + 'title' => 'my title', + 'post' => 'content post', + ]); + + $this->assertGreaterThan(0, $entryId); + $this->assertDatabaseHas('entries', [ + 'account_id' => $user->account_id, + 'id' => $entryId, + 'title' => 'my title', + 'post' => 'content post', + ]); + } + + /** @test */ + public function it_cant_update_a_journal_entry_with_missing_parameters() + { + $user = $this->signin(); + $entry = factory(Entry::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('PUT', '/api/journal/'.$entry->id, []); + + $this->expectDataError($response, [ + 'The title field is required.', + 'The post field is required.', + ]); + } + + /** @test */ + public function it_deletes_a_journal_entry() + { + $user = $this->signin(); + $entry = factory(Entry::class)->create([ + 'account_id' => $user->account_id, + ]); + $this->assertDatabaseHas('entries', [ + 'account_id' => $user->account_id, + 'id' => $entry->id, + ]); + + $response = $this->json('DELETE', '/api/journal/'.$entry->id); + + $response->assertStatus(200); + $this->assertDatabaseMissing('entries', [ + 'account_id' => $user->account_id, + 'id' => $entry->id, + ]); + } + + /** @test */ + public function it_cant_delete_a_journal_entry_with_an_invalid_id() + { + $user = $this->signin(); + + $response = $this->json('DELETE', '/api/journal/0'); + + $this->expectNotFound($response); + } +} diff --git a/tests/Api/ApiNotesTest.php b/tests/Api/ApiNotesTest.php new file mode 100644 index 0000000..690292e --- /dev/null +++ b/tests/Api/ApiNotesTest.php @@ -0,0 +1,428 @@ + [ + 'id', + ], + 'contact' => [ + 'id', + ], + 'created_at', + 'updated_at', + ]; + + /** @test */ + public function it_gets_all_the_notes() + { + $user = $this->signin(); + $contact1 = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $note1 = factory(Note::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact1->id, + ]); + $contact2 = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $note2 = factory(Note::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact2->id, + ]); + + $response = $this->json('GET', '/api/notes'); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => ['*' => $this->jsonNote], + ]); + $response->assertJsonFragment([ + 'object' => 'note', + 'id' => $note1->id, + ]); + $response->assertJsonFragment([ + 'object' => 'note', + 'id' => $note2->id, + ]); + } + + /** @test */ + public function it_gets_all_the_notes_of_a_given_contact() + { + $user = $this->signin(); + $contact1 = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $note1 = factory(Note::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact1->id, + ]); + $contact2 = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $note2 = factory(Note::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact2->id, + ]); + + $response = $this->json('GET', '/api/contacts/'.$contact1->id.'/notes'); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => ['*' => $this->jsonNote], + ]); + $response->assertJsonFragment([ + 'object' => 'note', + 'id' => $note1->id, + ]); + $response->assertJsonMissingExact([ + 'object' => 'note', + 'id' => $note2->id, + ]); + } + + /** @test */ + public function it_cant_get_notes_from_a_contact_with_invalid_id() + { + $user = $this->signin(); + + $response = $this->json('GET', '/api/contacts/0/notes'); + + $this->expectNotFound($response); + } + + /** @test */ + public function it_gets_one_note() + { + $user = $this->signin(); + $contact1 = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $note1 = factory(Note::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact1->id, + ]); + $note2 = factory(Note::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact1->id, + ]); + + $response = $this->json('GET', '/api/notes/'.$note1->id); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => $this->jsonNote, + ]); + $response->assertJsonFragment([ + 'object' => 'note', + 'id' => $note1->id, + ]); + $response->assertJsonMissingExact([ + 'object' => 'note', + 'id' => $note2->id, + ]); + } + + /** @test */ + public function it_gets_a_note_with_an_invalid_id() + { + $user = $this->signin(); + + $response = $this->json('GET', '/api/notes/0'); + + $this->expectNotFound($response); + } + + /** @test */ + public function it_creates_a_note() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('POST', '/api/notes', [ + 'contact_id' => $contact->id, + 'body' => 'the body of the note', + 'is_favorited' => false, + ]); + + $response->assertStatus(201); + $response->assertJsonStructure([ + 'data' => $this->jsonNote, + ]); + $note_id = $response->json('data.id'); + $response->assertJsonFragment([ + 'object' => 'note', + 'id' => $note_id, + 'body' => 'the body of the note', + 'is_favorited' => false, + ]); + + $this->assertGreaterThan(0, $note_id); + $this->assertDatabaseHas('notes', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'id' => $note_id, + 'body' => 'the body of the note', + 'is_favorited' => false, + ]); + } + + /** @test */ + public function it_creates_a_note_and_marks_as_favorite() + { + Carbon::setTestNow(Carbon::create(2018, 1, 1, 7, 0, 0)); + + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('POST', '/api/notes', [ + 'contact_id' => $contact->id, + 'body' => 'the body of the note', + 'is_favorited' => true, + ]); + + $response->assertStatus(201); + $response->assertJsonStructure([ + 'data' => $this->jsonNote, + ]); + $note_id = $response->json('data.id'); + $response->assertJsonFragment([ + 'object' => 'note', + 'id' => $note_id, + 'body' => 'the body of the note', + 'is_favorited' => true, + 'favorited_at' => '2018-01-01T07:00:00Z', + ]); + + $this->assertGreaterThan(0, $note_id); + $this->assertDatabaseHas('notes', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'id' => $note_id, + 'body' => 'the body of the note', + 'is_favorited' => true, + 'favorited_at' => '2018-01-01', + ]); + } + + /** @test */ + public function it_cant_create_a_note_with_missing_parameters() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('POST', '/api/notes', [ + 'contact_id' => $contact->id, + ]); + + $this->expectDataError($response, [ + 'The body field is required.', + ]); + } + + /** @test */ + public function it_cant_create_a_note_with_an_invalid_account() + { + $user = $this->signin(); + + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + + $response = $this->json('POST', '/api/notes', [ + 'contact_id' => $contact->id, + 'body' => 'the body of the note', + 'is_favorited' => false, + ]); + + $this->expectNotFound($response); + } + + /** @test */ + public function it_updates_a_note() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $note = factory(Note::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + ]); + + $response = $this->json('PUT', '/api/notes/'.$note->id, [ + 'contact_id' => $contact->id, + 'body' => 'the body of the note', + 'is_favorited' => false, + ]); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => $this->jsonNote, + ]); + $note_id = $response->json('data.id'); + $this->assertEquals($note->id, $note_id); + $response->assertJsonFragment([ + 'object' => 'note', + 'id' => $note_id, + 'body' => 'the body of the note', + 'is_favorited' => false, + ]); + + $this->assertGreaterThan(0, $note_id); + $this->assertDatabaseHas('notes', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'id' => $note_id, + 'body' => 'the body of the note', + 'is_favorited' => false, + ]); + } + + /** @test */ + public function it_updates_a_note_and_marks_it_as_favorite() + { + Carbon::setTestNow(Carbon::create(2018, 1, 1, 7, 0, 0)); + + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $note = factory(Note::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + ]); + + $response = $this->json('PUT', '/api/notes/'.$note->id, [ + 'contact_id' => $contact->id, + 'body' => 'the body of the note', + 'is_favorited' => true, + ]); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => $this->jsonNote, + ]); + $note_id = $response->json('data.id'); + $this->assertEquals($note->id, $note_id); + $response->assertJsonFragment([ + 'object' => 'note', + 'id' => $note_id, + 'body' => 'the body of the note', + 'is_favorited' => true, + 'favorited_at' => '2018-01-01T07:00:00Z', + ]); + + $this->assertGreaterThan(0, $note_id); + $this->assertDatabaseHas('notes', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'id' => $note_id, + 'body' => 'the body of the note', + 'is_favorited' => true, + 'favorited_at' => '2018-01-01', + ]); + } + + /** @test */ + public function it_cant_update_a_note_with_missing_parameters() + { + $user = $this->signin(); + $note = factory(Note::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('PUT', '/api/notes/'.$note->id, [ + 'contact_id' => $note->contact_id, + ]); + + $this->expectDataError($response, [ + 'The body field is required.', + ]); + } + + /** @test */ + public function it_cant_update_a_note_with_an_invalid_account() + { + $user = $this->signin(); + + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + $note = factory(Note::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + ]); + + $response = $this->json('PUT', '/api/notes/'.$note->id, [ + 'contact_id' => $contact->id, + 'body' => 'the body of the note', + 'is_favorited' => false, + ]); + + $this->expectNotFound($response); + } + + /** @test */ + public function it_deletes_a_note() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $note = factory(Note::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + ]); + $this->assertDatabaseHas('notes', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'id' => $note->id, + ]); + + $response = $this->json('DELETE', '/api/notes/'.$note->id); + + $response->assertStatus(200); + $this->assertDatabaseMissing('notes', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'id' => $note->id, + ]); + } + + /** @test */ + public function it_cant_delete_a_note_with_an_invalid_id() + { + $user = $this->signin(); + + $response = $this->json('DELETE', '/api/notes/0'); + + $this->expectNotFound($response); + } +} diff --git a/tests/Api/ApiPetsTest.php b/tests/Api/ApiPetsTest.php new file mode 100644 index 0000000..48f7781 --- /dev/null +++ b/tests/Api/ApiPetsTest.php @@ -0,0 +1,347 @@ + [ + 'id', + 'object', + 'name', + 'is_common', + ], + 'account' => [ + 'id', + ], + 'contact' => [ + 'id', + ], + 'created_at', + 'updated_at', + ]; + + /** @test */ + public function pets_get_all() + { + $user = $this->signin(); + $contact1 = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $pet1 = factory(Pet::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact1->id, + ]); + $contact2 = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $pet2 = factory(Pet::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact2->id, + ]); + + $response = $this->json('GET', '/api/pets'); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => ['*' => $this->jsonPet], + ]); + $response->assertJsonFragment([ + 'object' => 'pet', + 'id' => $pet1->id, + ]); + $response->assertJsonFragment([ + 'object' => 'pet', + 'id' => $pet2->id, + ]); + } + + /** @test */ + public function pets_get_contact_all() + { + $user = $this->signin(); + $contact1 = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $pet1 = factory(Pet::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact1->id, + ]); + $contact2 = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $pet2 = factory(Pet::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact2->id, + ]); + + $response = $this->json('GET', '/api/contacts/'.$contact1->id.'/pets'); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => ['*' => $this->jsonPet], + ]); + $response->assertJsonFragment([ + 'object' => 'pet', + 'id' => $pet1->id, + ]); + $response->assertJsonMissingExact([ + 'object' => 'pet', + 'id' => $pet2->id, + ]); + } + + /** @test */ + public function pets_get_contact_all_error() + { + $user = $this->signin(); + + $response = $this->json('GET', '/api/contacts/0/pets'); + + $this->expectNotFound($response); + } + + /** @test */ + public function pets_get_one() + { + $user = $this->signin(); + $contact1 = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $pet1 = factory(Pet::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact1->id, + ]); + $pet2 = factory(Pet::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact1->id, + ]); + + $response = $this->json('GET', '/api/pets/'.$pet1->id); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => $this->jsonPet, + ]); + $response->assertJsonFragment([ + 'object' => 'pet', + 'id' => $pet1->id, + ]); + $response->assertJsonMissingExact([ + 'object' => 'pet', + 'id' => $pet2->id, + ]); + } + + /** @test */ + public function pets_get_one_error() + { + $user = $this->signin(); + + $response = $this->json('GET', '/api/pets/0'); + + $this->expectNotFound($response); + } + + /** @test */ + public function pets_create() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $pet_category = factory(PetCategory::class)->create(); + + $response = $this->json('POST', '/api/pets', [ + 'contact_id' => $contact->id, + 'pet_category_id' => $pet_category->id, + 'name' => 'the name', + ]); + + $response->assertStatus(201); + $response->assertJsonStructure([ + 'data' => $this->jsonPet, + ]); + $pet_id = $response->json('data.id'); + $response->assertJsonFragment([ + 'object' => 'pet', + 'id' => $pet_id, + 'name' => 'the name', + ]); + + $this->assertGreaterThan(0, $pet_id); + $this->assertDatabaseHas('pets', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'pet_category_id' => $pet_category->id, + 'id' => $pet_id, + 'name' => 'the name', + ]); + } + + /** @test */ + public function pets_create_error() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('POST', '/api/pets', [ + 'contact_id' => $contact->id, + ]); + + $this->expectDataError($response, [ + 'The pet category id field is required.', + ]); + } + + /** @test */ + public function pets_create_error_bad_account() + { + $user = $this->signin(); + + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + $pet_category = factory(PetCategory::class)->create(); + + $response = $this->json('POST', '/api/pets', [ + 'contact_id' => $contact->id, + 'pet_category_id' => $pet_category->id, + ]); + + $this->expectNotFound($response); + } + + /** @test */ + public function pets_update() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $pet = factory(Pet::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + ]); + $pet_category = factory(PetCategory::class)->create(); + + $response = $this->json('PUT', '/api/pets/'.$pet->id, [ + 'contact_id' => $contact->id, + 'pet_category_id' => $pet_category->id, + 'name' => 'the name', + ]); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => $this->jsonPet, + ]); + $pet_id = $response->json('data.id'); + $this->assertEquals($pet->id, $pet_id); + $response->assertJsonFragment([ + 'object' => 'pet', + 'id' => $pet_id, + 'name' => 'the name', + ]); + + $this->assertGreaterThan(0, $pet_id); + $this->assertDatabaseHas('pets', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'pet_category_id' => $pet_category->id, + 'id' => $pet_id, + 'name' => 'the name', + ]); + } + + /** @test */ + public function pets_update_error() + { + $user = $this->signin(); + $pet = factory(Pet::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('PUT', '/api/pets/'.$pet->id, [ + 'contact_id' => $pet->contact_id, + ]); + + $this->expectDataError($response, [ + 'The pet category id field is required.', + ]); + } + + /** @test */ + public function pets_update_error_bad_account() + { + $user = $this->signin(); + + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + $pet = factory(Pet::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + ]); + $pet_category = factory(PetCategory::class)->create(); + + $response = $this->json('PUT', '/api/pets/'.$pet->id, [ + 'contact_id' => $contact->id, + 'pet_category_id' => $pet_category->id, + ]); + + $this->expectNotFound($response); + } + + /** @test */ + public function pets_delete() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $pet = factory(Pet::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + ]); + $this->assertDatabaseHas('pets', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'id' => $pet->id, + ]); + + $response = $this->json('DELETE', '/api/pets/'.$pet->id); + + $response->assertStatus(200); + $this->assertDatabaseMissing('pets', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'id' => $pet->id, + ]); + } + + /** @test */ + public function pets_delete_error() + { + $user = $this->signin(); + + $response = $this->json('DELETE', '/api/pets/0'); + + $this->expectNotFound($response); + } +} diff --git a/tests/Api/ApiRelationshipControllerTest.php b/tests/Api/ApiRelationshipControllerTest.php new file mode 100644 index 0000000..aec676f --- /dev/null +++ b/tests/Api/ApiRelationshipControllerTest.php @@ -0,0 +1,389 @@ +signin(); + $contactA = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $contactB = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $relationshipType = factory(RelationshipType::class)->create([ + 'account_id' => $user->account_id, + ]); + + // make sure contact_is is an integer + $response = $this->json('POST', '/api/relationships', [ + 'contact_is' => 'a', + 'relationship_type_id' => $relationshipType->id, + 'of_contact' => $contactB->id, + ]); + + $this->expectDataError($response, ['The contact is must be an integer.']); + + // make sure relationship type id is an integer + $response = $this->json('POST', '/api/relationships', [ + 'contact_is' => $contactA->id, + 'relationship_type_id' => 'a', + 'of_contact' => $contactB->id, + ]); + + $this->expectDataError($response, ['The relationship type id must be an integer.']); + + // make sure of_contact is an integer + $response = $this->json('POST', '/api/relationships', [ + 'contact_is' => $contactA->id, + 'relationship_type_id' => $relationshipType->id, + 'of_contact' => 'a', + ]); + + $this->expectDataError($response, ['The of contact must be an integer.']); + } + + /** @test */ + public function it_fails_if_relationship_type_id_is_invalid() + { + $user = $this->signin(); + $contactA = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $contactB = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $relationshipType = factory(RelationshipType::class)->create(); + + $response = $this->json('POST', '/api/relationships', [ + 'contact_is' => $contactA->id, + 'relationship_type_id' => $relationshipType->id, + 'of_contact' => $contactB->id, + ]); + + $this->expectNotFound($response); + } + + /** @test */ + public function it_fails_if_contact_is_id_is_invalid() + { + $user = $this->signin(); + $contactA = factory(Contact::class)->create(); + $contactB = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $relationshipType = factory(RelationshipType::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('POST', '/api/relationships', [ + 'contact_is' => $contactA->id, + 'relationship_type_id' => $relationshipType->id, + 'of_contact' => $contactB->id, + ]); + + $this->expectNotFound($response); + } + + /** @test */ + public function it_fails_if_of_contact_id_is_invalid() + { + $user = $this->signin(); + $contactA = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $contactB = factory(Contact::class)->create(); + $relationshipType = factory(RelationshipType::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('POST', '/api/relationships', [ + 'contact_is' => $contactA->id, + 'relationship_type_id' => $relationshipType->id, + 'of_contact' => $contactB->id, + ]); + + $this->expectNotFound($response); + } + + /** @test */ + public function it_creates_a_new_resource() + { + $user = $this->signin(); + $contactA = factory(Contact::class)->create(['account_id' => $user->account_id]); + $contactB = factory(Contact::class)->create(['account_id' => $user->account_id]); + $relationshipType = factory(RelationshipType::class)->create([ + 'account_id' => $user->account_id, + 'name' => 'uncle', + 'name_reverse_relationship' => 'nephew', + ]); + + $relationshipTypeB = factory(RelationshipType::class)->create([ + 'account_id' => $user->account_id, + 'name' => 'nephew', + 'name_reverse_relationship' => 'uncle', + ]); + + $response = $this->json('POST', '/api/relationships', [ + 'contact_is' => $contactA->id, + 'relationship_type_id' => $relationshipType->id, + 'of_contact' => $contactB->id, + ]); + + $response->assertStatus(201); + + $this->assertDatabaseHas('relationships', [ + 'account_id' => auth()->user()->account_id, + 'contact_is' => $contactA->id, + 'of_contact' => $contactB->id, + 'relationship_type_id' => $relationshipType->id, + ]); + + $this->assertDatabaseHas('relationships', [ + 'account_id' => auth()->user()->account_id, + 'contact_is' => $contactB->id, + 'of_contact' => $contactA->id, + 'relationship_type_id' => auth()->user()->account->getRelationshipTypeByType($relationshipType->name_reverse_relationship)->id, + ]); + } + + /** @test */ + public function it_displays_a_relationship() + { + $user = $this->signin(); + $contactA = factory(Contact::class)->create(['account_id' => $user->account_id]); + $contactB = factory(Contact::class)->create(['account_id' => $user->account_id]); + $relationshipType = factory(RelationshipType::class)->create([ + 'account_id' => $user->account_id, + 'name' => 'uncle', + 'name_reverse_relationship' => 'nephew', + ]); + + $relationshipTypeB = factory(RelationshipType::class)->create([ + 'account_id' => $user->account_id, + 'name' => 'nephew', + 'name_reverse_relationship' => 'uncle', + ]); + + $relationship = factory(Relationship::class)->create([ + 'account_id' => $user->account_id, + 'relationship_type_id' => $relationshipType->id, + 'contact_is' => $contactA->id, + 'of_contact' => $contactB->id, + ]); + + $response = $this->json('GET', '/api/relationships/'.$relationship->id); + + $response->assertStatus(200) + ->assertJsonFragment([ + 'id' => $relationship->id, + 'object' => 'relationship', + ]); + } + + /** @test */ + public function it_deletes_a_relationship() + { + $user = $this->signin(); + $contactA = factory(Contact::class)->create(['account_id' => $user->account_id]); + $contactB = factory(Contact::class)->create(['account_id' => $user->account_id]); + $relationshipType = factory(RelationshipType::class)->create([ + 'account_id' => $user->account_id, + 'name' => 'uncle', + 'name_reverse_relationship' => 'nephew', + ]); + + $relationshipTypeB = factory(RelationshipType::class)->create([ + 'account_id' => $user->account_id, + 'name' => 'nephew', + 'name_reverse_relationship' => 'uncle', + ]); + + $relationship = factory(Relationship::class)->create([ + 'account_id' => $user->account_id, + 'relationship_type_id' => $relationshipType->id, + 'contact_is' => $contactA->id, + 'of_contact' => $contactB->id, + ]); + + $relationshipB = factory(Relationship::class)->create([ + 'account_id' => $user->account_id, + 'relationship_type_id' => $relationshipTypeB->id, + 'contact_is' => $contactB->id, + 'of_contact' => $contactA->id, + ]); + + $response = $this->json('DELETE', '/api/relationships/'.$relationship->id); + + $response->assertStatus(200) + ->assertJson([ + 'deleted' => true, + 'id' => $relationship->id, + ]); + + $this->assertDatabaseMissing('relationships', [ + 'id' => $relationship->id, + ]); + + $this->assertDatabaseMissing('relationships', [ + 'id' => $relationshipB->id, + ]); + } + + /** @test */ + public function it_rejects_the_delete_api_call_if_parameters_are_not_right() + { + $user = $this->signin(); + + // make sure relationship id is valid + $response = $this->json('DELETE', '/api/relationships/0'); + $this->expectDataError($response, ['The selected relationship id is invalid.']); + + // make sure relationship id is an integer + $response = $this->json('DELETE', '/api/relationships/x'); + $this->expectDataError($response, ['The relationship id must be an integer.']); + + // make sure relationship id is with the right account + $relationship = factory(Relationship::class)->create(); + $response = $this->json('DELETE', '/api/relationships/'.$relationship->id); + $this->expectNotFound($response); + } + + /** @test */ + public function it_rejects_the_update_api_call_if_parameters_are_not_right() + { + $user = $this->signin(); + $relationship = factory(Relationship::class)->create([ + 'account_id' => $user->account_id, + ]); + $relationshipType = factory(RelationshipType::class)->create(); + + // make sure relationship type id is an integer + $response = $this->json('PUT', '/api/relationships/'.$relationship->id, [ + 'relationship_type_id' => $relationshipType->id, + ]); + + $this->expectNotFound($response); + } + + /** @test */ + public function it_rejects_the_update_api_call_if_parameters_are_not_right2() + { + $user = $this->signin(); + $relationship = factory(Relationship::class)->create([ + 'account_id' => $user->account_id, + ]); + + // make sure relationship type id is an integer + $response = $this->json('PUT', '/api/relationships/'.$relationship->id, [ + 'relationship_type_id' => 'a', + ]); + + $this->expectDataError($response, ['The relationship type id must be an integer.']); + } + + /** @test */ + public function it_fails_the_update_if_relationship_type_id_is_invalid() + { + $user = $this->signin(); + $relationship = factory(Relationship::class)->create([ + 'account_id' => $user->account_id, + ]); + $relationshipType = factory(RelationshipType::class)->create(); + + $response = $this->json('PUT', '/api/relationships/'.$relationship->id, [ + 'relationship_type_id' => $relationshipType->id, + ]); + + $this->expectNotFound($response); + } + + /** @test */ + public function it_updates_a_relationship() + { + $user = $this->signin(); + $contactA = factory(Contact::class)->create(['account_id' => $user->account_id]); + $contactB = factory(Contact::class)->create(['account_id' => $user->account_id]); + $relationshipType = factory(RelationshipType::class)->create([ + 'account_id' => $user->account_id, + 'name' => 'uncle', + 'name_reverse_relationship' => 'nephew', + ]); + + $relationshipTypeC = factory(RelationshipType::class)->create([ + 'account_id' => $user->account_id, + 'name' => 'fuckfriend', + 'name_reverse_relationship' => 'funnysituation', + ]); + + $relationship = factory(Relationship::class)->create([ + 'account_id' => $user->account_id, + 'relationship_type_id' => $relationshipType->id, + 'contact_is' => $contactA->id, + 'of_contact' => $contactB->id, + ]); + + $response = $this->json('PUT', '/api/relationships/'.$relationship->id, [ + 'relationship_type_id' => $relationshipTypeC->id, + ]); + + $response->assertStatus(200) + ->assertJsonFragment([ + 'id' => $relationshipTypeC->id, + 'name' => 'fuckfriend', + ]); + + $this->assertDatabaseHas('relationships', [ + 'id' => $relationship->id, + 'relationship_type_id' => $relationshipTypeC->id, + ]); + } + + /** @test */ + public function it_displays_all_relationships_of_a_contact() + { + $user = $this->signin(); + $contactA = factory(Contact::class)->create(['account_id' => $user->account_id]); + $contactB = factory(Contact::class)->create(['account_id' => $user->account_id]); + $relationshipType = factory(RelationshipType::class)->create([ + 'account_id' => $user->account_id, + 'name' => 'uncle', + 'name_reverse_relationship' => 'nephew', + ]); + + $relationshipTypeB = factory(RelationshipType::class)->create([ + 'account_id' => $user->account_id, + 'name' => 'nephew', + 'name_reverse_relationship' => 'uncle', + ]); + + $relationship = factory(Relationship::class, 3)->create([ + 'account_id' => $user->account_id, + 'relationship_type_id' => $relationshipType->id, + 'contact_is' => $contactA->id, + 'of_contact' => $contactB->id, + ]); + + $response = $this->json('GET', '/api/contacts/'.$contactA->id.'/relationships'); + + $response->assertStatus(200); + + $decodedJson = $response->decodeResponseJson(); + + $this->assertCount( + 3, + $decodedJson['data'] + ); + } +} diff --git a/tests/Api/ApiRelationshipTypeControllerTest.php b/tests/Api/ApiRelationshipTypeControllerTest.php new file mode 100644 index 0000000..4447abb --- /dev/null +++ b/tests/Api/ApiRelationshipTypeControllerTest.php @@ -0,0 +1,100 @@ +signin(); + + factory(RelationshipType::class, 10)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('GET', '/api/relationshiptypes'); + + $response->assertStatus(200); + $decodedJson = $response->decodeResponseJson(); + + $this->assertCount( + 10, + $decodedJson['data'] + ); + } + + /** @test */ + public function it_gets_the_list_of_relationship_types() + { + $user = $this->signin(); + + $relationshipTypeGroup = factory(RelationshipTypeGroup::class)->create([ + 'account_id' => $user->account_id, + ]); + + factory(RelationshipType::class)->create([ + 'account_id' => $user->account_id, + 'name' => 'father', + 'name_reverse_relationship' => 'son', + 'relationship_type_group_id' => $relationshipTypeGroup->id, + 'delible' => 0, + ]); + $relationshipType2 = factory(RelationshipType::class)->create([ + 'account_id' => $user->account_id, + 'name' => 'son', + 'name_reverse_relationship' => 'father', + 'relationship_type_group_id' => $relationshipTypeGroup->id, + 'delible' => 0, + ]); + + $response = $this->json('GET', '/api/relationshiptypes'); + + $response->assertStatus(200); + + $response->assertJsonFragment([ + 'id' => $relationshipType2->id, + 'object' => 'relationshiptype', + 'name' => 'son', + 'delible' => false, + ]); + } + + /** @test */ + public function it_gets_a_specific_relationship_type_group() + { + $user = $this->signin(); + + $relationshipTypeGroup = factory(RelationshipTypeGroup::class)->create([ + 'account_id' => $user->account_id, + ]); + + $relationshipType = factory(RelationshipType::class)->create([ + 'account_id' => $user->account_id, + 'name' => 'father', + 'name_reverse_relationship' => 'son', + 'relationship_type_group_id' => $relationshipTypeGroup->id, + 'delible' => 0, + ]); + + $response = $this->json('GET', '/api/relationshiptypes/'.$relationshipType->id); + + $response->assertStatus(200); + + $response->assertJsonFragment([ + 'id' => $relationshipType->id, + 'object' => 'relationshiptype', + 'name' => 'father', + 'name_reverse_relationship' => 'son', + 'relationship_type_group_id' => $relationshipTypeGroup->id, + 'delible' => false, + ]); + } +} diff --git a/tests/Api/ApiRelationshipTypeGroupControllerTest.php b/tests/Api/ApiRelationshipTypeGroupControllerTest.php new file mode 100644 index 0000000..ffe3d1f --- /dev/null +++ b/tests/Api/ApiRelationshipTypeGroupControllerTest.php @@ -0,0 +1,83 @@ +signin(); + + factory(RelationshipTypeGroup::class, 10)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('GET', '/api/relationshiptypegroups'); + + $response->assertStatus(200); + $decodedJson = $response->decodeResponseJson(); + + $this->assertCount( + 10, + $decodedJson['data'] + ); + } + + /** @test */ + public function it_gets_the_list_of_relationship_type_groups() + { + $user = $this->signin(); + + factory(RelationshipTypeGroup::class)->create([ + 'account_id' => $user->account_id, + 'name' => 'love', + 'delible' => 0, + ]); + $relationshipTypeGroup2 = factory(RelationshipTypeGroup::class)->create([ + 'account_id' => $user->account_id, + 'name' => 'hate', + 'delible' => 0, + ]); + + $response = $this->json('GET', '/api/relationshiptypegroups'); + + $response->assertStatus(200); + + $response->assertJsonFragment([ + 'id' => $relationshipTypeGroup2->id, + 'object' => 'relationshiptypegroup', + 'name' => 'hate', + 'delible' => false, + ]); + } + + /** @test */ + public function it_gets_a_specific_relationship_type_group() + { + $user = $this->signin(); + + $relationshipTypeGroup = factory(RelationshipTypeGroup::class)->create([ + 'account_id' => $user->account_id, + 'name' => 'love', + 'delible' => 0, + ]); + + $response = $this->json('GET', '/api/relationshiptypegroups/'.$relationshipTypeGroup->id); + + $response->assertStatus(200); + + $response->assertJsonFragment([ + 'id' => $relationshipTypeGroup->id, + 'object' => 'relationshiptypegroup', + 'name' => 'love', + 'delible' => false, + ]); + } +} diff --git a/tests/Api/ApiReminderControllerTest.php b/tests/Api/ApiReminderControllerTest.php new file mode 100644 index 0000000..d87253e --- /dev/null +++ b/tests/Api/ApiReminderControllerTest.php @@ -0,0 +1,409 @@ + [ + 'id', + ], + 'contact' => [ + 'id', + ], + 'created_at', + 'updated_at', + ]; + + /** @test */ + public function it_gets_all_reminders() + { + $user = $this->signin(); + $contact1 = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $reminder1 = factory(Reminder::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact1->id, + ]); + $contact2 = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $reminder2 = factory(Reminder::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact2->id, + 'delible' => false, + ]); + + $response = $this->json('GET', '/api/reminders'); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => ['*' => $this->jsonReminder], + ]); + $response->assertJsonFragment([ + 'object' => 'reminder', + 'id' => $reminder1->id, + 'delible' => true, + ]); + $response->assertJsonFragment([ + 'object' => 'reminder', + 'id' => $reminder2->id, + 'delible' => false, + ]); + } + + /** @test */ + public function it_gets_all_the_reminders_of_a_contact() + { + $user = $this->signin(); + $contact1 = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $reminder1 = factory(Reminder::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact1->id, + ]); + $contact2 = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $reminder2 = factory(Reminder::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact2->id, + ]); + + $response = $this->json('GET', '/api/contacts/'.$contact1->id.'/reminders'); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => ['*' => $this->jsonReminder], + ]); + $response->assertJsonFragment([ + 'object' => 'reminder', + 'id' => $reminder1->id, + ]); + $response->assertJsonMissingExact([ + 'object' => 'reminder', + 'id' => $reminder2->id, + ]); + } + + /** @test */ + public function it_cant_get_a_reminder_of_a_contact_with_an_invalid_id() + { + $user = $this->signin(); + + $response = $this->json('GET', '/api/contacts/0/reminders'); + + $this->expectNotFound($response); + } + + /** @test */ + public function it_gets_one_reminder() + { + $user = $this->signin(); + $contact1 = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $reminder1 = factory(Reminder::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact1->id, + ]); + $reminder2 = factory(Reminder::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact1->id, + ]); + + $response = $this->json('GET', '/api/reminders/'.$reminder1->id); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => $this->jsonReminder, + ]); + $response->assertJsonFragment([ + 'object' => 'reminder', + 'id' => $reminder1->id, + ]); + $response->assertJsonMissingExact([ + 'object' => 'reminder', + 'id' => $reminder2->id, + ]); + } + + /** @test */ + public function it_cant_get_a_reminder_with_an_invalid_id() + { + $user = $this->signin(); + + $response = $this->json('GET', '/api/reminders/0'); + + $this->expectNotFound($response); + } + + /** @test */ + public function it_creates_a_reminder() + { + Carbon::setTestNow(Carbon::create(2018, 1, 1, 7, 0, 0)); + + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('POST', '/api/reminders', [ + 'contact_id' => $contact->id, + 'title' => 'the title', + 'initial_date' => '2018-05-01', + 'frequency_type' => 'one_time', + 'frequency_number' => 1, + 'description' => 'the description', + ]); + + $response->assertStatus(201); + $response->assertJsonStructure([ + 'data' => $this->jsonReminder, + ]); + $reminderId = $response->json('data.id'); + $response->assertJsonFragment([ + 'object' => 'reminder', + 'id' => $reminderId, + ]); + + $this->assertGreaterThan(0, $reminderId); + $this->assertDatabaseHas('reminders', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'id' => $reminderId, + 'title' => 'the title', + 'initial_date' => '2018-05-01', + 'frequency_type' => 'one_time', + 'description' => 'the description', + ]); + } + + /** @test */ + public function create_reminders_gets_an_error_if_fields_are_missing() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('POST', '/api/reminders', [ + 'contact_id' => $contact->id, + ]); + + $this->expectDataError($response, [ + 'The initial date field is required.', + 'The frequency type field is required.', + 'The frequency number field is required.', + 'The title field is required.', + ]); + } + + /** @test */ + public function reminders_create_error_bad_account() + { + Carbon::setTestNow(Carbon::create(2018, 1, 1, 7, 0, 0)); + + $user = $this->signin(); + + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + + $response = $this->json('POST', '/api/reminders', [ + 'contact_id' => $contact->id, + 'title' => 'the title', + 'initial_date' => '2018-05-01', + 'frequency_type' => 'one_time', + 'frequency_number' => 1, + 'description' => 'the description', + ]); + + $this->expectNotFound($response); + } + + /** @test */ + public function it_updates_a_reminder() + { + Carbon::setTestNow(Carbon::create(2018, 1, 1, 7, 0, 0)); + + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $reminder = factory(Reminder::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + ]); + + $response = $this->json('PUT', '/api/reminders/'.$reminder->id, [ + 'contact_id' => $contact->id, + 'title' => 'the title', + 'initial_date' => '2018-05-01', + 'frequency_type' => 'one_time', + 'description' => 'the description', + ]); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => $this->jsonReminder, + ]); + $reminder_id = $response->json('data.id'); + $this->assertEquals($reminder->id, $reminder_id); + $response->assertJsonFragment([ + 'object' => 'reminder', + 'id' => $reminder_id, + 'title' => 'the title', + 'initial_date' => '2018-05-01T00:00:00Z', + 'frequency_type' => 'one_time', + 'description' => 'the description', + ]); + + $this->assertGreaterThan(0, $reminder_id); + $this->assertDatabaseHas('reminders', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'id' => $reminder_id, + 'title' => 'the title', + 'initial_date' => '2018-05-01 00:00:00', + 'frequency_type' => 'one_time', + 'description' => 'the description', + ]); + } + + /** @test */ + public function updating_reminder_generates_an_error() + { + $user = $this->signin(); + $reminder = factory(Reminder::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('PUT', '/api/reminders/'.$reminder->id, [ + 'contact_id' => $reminder->contact_id, + ]); + + $this->expectDataError($response, [ + 'The initial date field is required.', + 'The frequency type field is required.', + 'The title field is required.', + ]); + } + + /** @test */ + public function reminders_update_error_bad_account() + { + Carbon::setTestNow(Carbon::create(2018, 1, 1, 7, 0, 0)); + + $user = $this->signin(); + + $contact = factory(Contact::class)->create([]); + $reminder = factory(Reminder::class)->create([ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + ]); + + $response = $this->json('PUT', '/api/reminders/'.$reminder->id, [ + 'contact_id' => $contact->id, + 'title' => 'the title', + 'initial_date' => '2018-05-01', + 'frequency_type' => 'one_time', + 'description' => 'the description', + ]); + + $this->expectNotFound($response); + } + + /** @test */ + public function it_deletes_a_reminder() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $reminder = factory(Reminder::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + ]); + + $response = $this->json('DELETE', '/api/reminders/'.$reminder->id); + + $response->assertStatus(200); + $this->assertDatabaseMissing('reminders', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'id' => $reminder->id, + ]); + } + + /** @test */ + public function reminders_delete_error() + { + $user = $this->signin(); + + $response = $this->json('DELETE', '/api/reminders/0'); + + $this->expectDataError($response, [ + 'The selected reminder id is invalid.', + ]); + } + + /** @test */ + public function it_gets_all_upcoming_reminders() + { + $user = $this->signin(); + + Carbon::setTestNow(Carbon::create(2017, 1, 1)); + + // add 2 reminders for the month of March + $reminder1 = factory(Reminder::class)->create([ + 'account_id' => $user->account_id, + 'initial_date' => '2017-03-03 00:00:00', + ]); + $reminder1->schedule($user); + + $reminder2 = factory(Reminder::class)->create([ + 'account_id' => $user->account_id, + 'initial_date' => '2017-03-03 00:00:00', + 'delible' => false, + ]); + $reminder2->schedule($user); + + $response = $this->json('GET', '/api/reminders/upcoming/2'); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => ['*' => $this->jsonReminder], + ]); + $response->assertJsonFragment([ + 'object' => 'reminder', + 'reminder_id' => $reminder1->id, + 'delible' => true, + ]); + $response->assertJsonFragment([ + 'object' => 'reminder', + 'reminder_id' => $reminder2->id, + 'delible' => false, + ]); + } +} diff --git a/tests/Api/ApiStatisticsControllerTest.php b/tests/Api/ApiStatisticsControllerTest.php new file mode 100644 index 0000000..fa87214 --- /dev/null +++ b/tests/Api/ApiStatisticsControllerTest.php @@ -0,0 +1,48 @@ + true]); + + $user = $this->signin(); + + $response = $this->json('GET', '/api/statistics'); + + $response->assertStatus(200); + + $response->assertJsonStructure([ + $this->jsonStructure, + ]); + } + + /** @test */ + public function it_returns_an_error_if_public_statistics_are_not_available() + { + config(['monica.allow_statistics_through_public_api_access' => false]); + + $user = $this->signin(); + + $response = $this->json('GET', '/api/statistics'); + + $this->expectNotFound($response); + } +} diff --git a/tests/Api/ApiTagControllerTest.php b/tests/Api/ApiTagControllerTest.php new file mode 100644 index 0000000..c36afc3 --- /dev/null +++ b/tests/Api/ApiTagControllerTest.php @@ -0,0 +1,406 @@ + [ + 'id', + ], + 'created_at', + 'updated_at', + ]; + + protected $jsonStructureContactWithContactFields = [ + 'id', + 'object', + 'hash_id', + 'first_name', + 'last_name', + 'gender', + 'gender_type', + 'is_starred', + 'is_partial', + 'is_dead', + 'last_called', + 'last_activity_together', + 'stay_in_touch_frequency', + 'stay_in_touch_trigger_date', + 'information' => [ + 'relationships' => [ + 'love' => [ + 'total', + 'contacts', + ], + 'family' => [ + 'total', + 'contacts', + ], + 'friend' => [ + 'total', + 'contacts', + ], + 'work' => [ + 'total', + 'contacts', + ], + ], + 'dates' => [ + 'birthdate' => [ + 'is_age_based', + 'is_year_unknown', + 'date', + ], + 'deceased_date' => [ + 'is_age_based', + 'is_year_unknown', + 'date', + ], + ], + 'career' => [ + 'job', + 'company', + ], + 'avatar' => [ + 'url', + 'source', + 'default_avatar_color', + ], + 'food_preferences', + 'how_you_met' => [ + 'general_information', + 'first_met_date' => [ + 'is_age_based', + 'is_year_unknown', + 'date', + ], + 'first_met_through_contact', + ], + ], + 'addresses' => [], + 'tags' => [], + 'statistics' => [ + 'number_of_calls', + 'number_of_notes', + 'number_of_activities', + 'number_of_reminders', + 'number_of_tasks', + 'number_of_gifts', + 'number_of_debts', + ], + 'contactFields' => [ + '*' => [ + 'id', + 'object', + 'content', + 'contact_field_type' => [ + 'id', + 'object', + 'name', + 'fontawesome_icon', + 'protocol', + 'delible', + 'type', + 'account' => [ + 'id', + ], + 'created_at', + 'updated_at', + ], + 'account' => [ + 'id', + ], + 'contact' => [], + 'created_at', + 'updated_at', + ], + ], + 'notes' => [], + 'account' => [ + 'id', + ], + 'created_at', + 'updated_at', + ]; + + /** @test */ + public function it_get_all_tags() + { + $user = $this->signin(); + $tag1 = factory(Tag::class)->create([ + 'account_id' => $user->account_id, + ]); + $tag2 = factory(Tag::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('GET', '/api/tags'); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => [ + '*' => $this->jsonTag, + ], + ]); + + $response->assertJsonFragment([ + 'object' => 'tag', + 'id' => $tag1->id, + ]); + + $response->assertJsonFragment([ + 'object' => 'tag', + 'id' => $tag2->id, + ]); + } + + /** @test */ + public function it_gets_a_specific_tag() + { + $user = $this->signin(); + $tag = factory(Tag::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('GET', '/api/tags/'.$tag->id); + + $response->assertStatus(200); + + $response->assertJsonStructure([ + 'data' => $this->jsonTag, + ]); + + $response->assertJsonFragment([ + 'object' => 'tag', + 'id' => $tag->id, + ]); + } + + /** @test */ + public function it_triggers_error_if_tag_unknown() + { + $user = $this->signin(); + + $response = $this->json('GET', '/api/tags/0'); + + $this->expectNotFound($response); + } + + /** @test */ + public function it_creates_a_tag() + { + $user = $this->signin(); + + $response = $this->json('POST', '/api/tags', [ + 'name' => 'the tag', + ]); + + $response->assertStatus(201); + $response->assertJsonStructure([ + 'data' => $this->jsonTag, + ]); + $tag_id = $response->json('data.id'); + $response->assertJsonFragment([ + 'object' => 'tag', + 'id' => $tag_id, + 'name' => 'the tag', + ]); + + $this->assertGreaterThan(0, $tag_id); + $this->assertDatabaseHas('tags', [ + 'account_id' => $user->account_id, + 'id' => $tag_id, + 'name' => 'the tag', + ]); + } + + /** @test */ + public function it_updates_a_tag() + { + $user = $this->signin(); + $tag = factory(Tag::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('PUT', '/api/tags/'.$tag->id, [ + 'name' => 'the tag', + ]); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => $this->jsonTag, + ]); + + $tag_id = $response->json('data.id'); + $this->assertEquals($tag->id, $tag_id); + + $response->assertJsonFragment([ + 'object' => 'tag', + 'id' => $tag_id, + 'name' => 'the tag', + ]); + + $this->assertGreaterThan(0, $tag_id); + $this->assertDatabaseHas('tags', [ + 'account_id' => $user->account_id, + 'id' => $tag_id, + 'name' => 'the tag', + ]); + } + + /** @test */ + public function it_deletes_a_tag() + { + $user = $this->signin(); + $tag = factory(Tag::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('DELETE', '/api/tags/'.$tag->id); + + $response->assertStatus(200); + + $this->assertDatabaseMissing('contact_tag', [ + 'account_id' => $user->account_id, + 'tag_id' => $tag->id, + ]); + $this->assertDatabaseMissing('tags', [ + 'account_id' => $user->account_id, + 'id' => $tag->id, + ]); + } + + /** @test */ + public function it_deletes_a_tag_associated() + { + $user = $this->signin(); + $tag = factory(Tag::class)->create([ + 'account_id' => $user->account_id, + ]); + $contact1 = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $contact2 = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('POST', "/api/contacts/{$contact1->id}/setTags", ['tags' => [$tag->name]]); + $response = $this->json('POST', "/api/contacts/{$contact2->id}/setTags", ['tags' => [$tag->name]]); + + $this->assertDatabaseHas('contact_tag', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact1->id, + 'tag_id' => $tag->id, + ]); + $this->assertDatabaseHas('contact_tag', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact2->id, + 'tag_id' => $tag->id, + ]); + + $response = $this->json('DELETE', '/api/tags/'.$tag->id); + + $response->assertStatus(200); + + $this->assertDatabaseMissing('contact_tag', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact1->id, + 'tag_id' => $tag->id, + ]); + $this->assertDatabaseMissing('contact_tag', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact2->id, + 'tag_id' => $tag->id, + ]); + $this->assertDatabaseMissing('tags', [ + 'account_id' => $user->account_id, + 'id' => $tag->id, + ]); + } + + /** @test */ + public function it_gets_all_the_contacts_for_a_given_tag() + { + $user = $this->signin(); + + $tag = factory(Tag::class)->create([ + 'account_id' => $user->account_id, + ]); + factory(Contact::class, 10)->create([ + 'account_id' => $user->account_id, + ]); + for ($i = 0; $i < 3; $i++) { + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $contact->tags()->sync([ + $tag->id => [ + 'account_id' => $user->account_id, + ], + ]); + } + + $response = $this->json('GET', '/api/tags/'.$tag->id.'/contacts'); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => ['*' => $this->jsonStructureContactWithContactFields], + ]); + + $this->assertCount( + 3, + $response->decodeResponseJson()['data'] + ); + } + + /** @test */ + public function it_gets_all_the_contacts_for_a_given_tag_and_applies_pagination() + { + $user = $this->signin(); + + $tag = factory(Tag::class)->create([ + 'account_id' => $user->account_id, + ]); + factory(Contact::class, 10)->create([ + 'account_id' => $user->account_id, + ]); + for ($i = 0; $i < 3; $i++) { + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $contact->tags()->sync([ + $tag->id => [ + 'account_id' => $user->account_id, + ], + ]); + } + + $response = $this->json('GET', '/api/tags/'.$tag->id.'/contacts?limit=1'); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => ['*' => $this->jsonStructureContactWithContactFields], + ]); + + $response->assertJsonFragment([ + 'total' => 3, + 'current_page' => 1, + 'per_page' => 1, + 'last_page' => 3, + ]); + } +} diff --git a/tests/Api/ApiTaskControllerTest.php b/tests/Api/ApiTaskControllerTest.php new file mode 100644 index 0000000..e7ae922 --- /dev/null +++ b/tests/Api/ApiTaskControllerTest.php @@ -0,0 +1,369 @@ + [ + 'id', + ], + 'contact' => [ + 'id', + ], + 'created_at', + 'updated_at', + ]; + + /** @test */ + public function it_gets_all_the_tasks() + { + $user = $this->signin(); + $contact1 = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $task1 = factory(Task::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact1->id, + ]); + $contact2 = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $task2 = factory(Task::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact2->id, + ]); + + $response = $this->json('GET', '/api/tasks'); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => ['*' => $this->jsonTask], + ]); + $response->assertJsonFragment([ + 'object' => 'task', + 'id' => $task1->id, + ]); + $response->assertJsonFragment([ + 'object' => 'task', + 'id' => $task2->id, + ]); + } + + /** @test */ + public function it_gets_all_the_tasks_of_a_contact() + { + $user = $this->signin(); + $contact1 = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $task1 = factory(Task::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact1->id, + ]); + $contact2 = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $task2 = factory(Task::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact2->id, + ]); + + $response = $this->json('GET', '/api/contacts/'.$contact1->id.'/tasks'); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => ['*' => $this->jsonTask], + ]); + $response->assertJsonFragment([ + 'object' => 'task', + 'id' => $task1->id, + ]); + $response->assertJsonMissingExact([ + 'object' => 'task', + 'id' => $task2->id, + ]); + } + + /** @test */ + public function it_cant_get_the_tasks_of_a_contact_with_an_invalid_id() + { + $user = $this->signin(); + + $response = $this->json('GET', '/api/contacts/0/tasks'); + + $this->expectNotFound($response); + } + + /** @test */ + public function it_gets_a_specific_task() + { + $user = $this->signin(); + $contact1 = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $task1 = factory(Task::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact1->id, + ]); + $task2 = factory(Task::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact1->id, + ]); + + $response = $this->json('GET', '/api/tasks/'.$task1->id); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => $this->jsonTask, + ]); + $response->assertJsonFragment([ + 'object' => 'task', + 'id' => $task1->id, + ]); + $response->assertJsonMissingExact([ + 'object' => 'task', + 'id' => $task2->id, + ]); + } + + /** @test */ + public function it_cant_get_a_task_with_an_invalid_id() + { + $user = $this->signin(); + + $response = $this->json('GET', '/api/tasks/0'); + + $this->expectNotFound($response); + } + + /** @test */ + public function it_create_a_task_associated_to_a_contact() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('POST', '/api/tasks', [ + 'contact_id' => $contact->id, + 'title' => 'the task', + 'description' => 'description', + 'completed' => false, + ]); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => $this->jsonTask, + ]); + $taskId = $response->json('data.id'); + $response->assertJsonFragment([ + 'object' => 'task', + 'id' => $taskId, + ]); + + $this->assertGreaterThan(0, $taskId); + $this->assertDatabaseHas('tasks', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'id' => $taskId, + 'title' => 'the task', + 'completed' => false, + ]); + } + + /** @test */ + public function it_create_a_task_not_associated_to_a_contact() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('POST', '/api/tasks', [ + 'contact_id' => $contact->id, + 'title' => 'the task', + 'description' => 'description', + 'completed' => false, + ]); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => $this->jsonTask, + ]); + $taskId = $response->json('data.id'); + $response->assertJsonFragment([ + 'object' => 'task', + 'id' => $taskId, + ]); + + $this->assertGreaterThan(0, $taskId); + $this->assertDatabaseHas('tasks', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'id' => $taskId, + 'title' => 'the task', + 'completed' => false, + ]); + } + + /** @test */ + public function creating_a_task_triggers_invalid_parameter_error() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('POST', '/api/tasks', [ + 'contact_id' => $contact->id, + ]); + + $this->expectDataError($response, [ + 'The title field is required.', + ]); + } + + /** @test */ + public function creating_a_task_with_a_wrong_account_id_triggers_an_error() + { + $user = $this->signin(); + + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + + $response = $this->json('POST', '/api/tasks', [ + 'contact_id' => $contact->id, + 'title' => 'the task', + 'completed' => false, + ]); + + $this->expectNotFound($response); + } + + /** @test */ + public function it_updates_a_task() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $task = factory(Task::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + ]); + + $response = $this->json('PUT', '/api/tasks/'.$task->id, [ + 'contact_id' => $contact->id, + 'title' => 'the task', + 'completed' => false, + ]); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => $this->jsonTask, + ]); + $taskId = $response->json('data.id'); + $this->assertEquals($task->id, $taskId); + $response->assertJsonFragment([ + 'object' => 'task', + 'id' => $taskId, + ]); + + $this->assertGreaterThan(0, $taskId); + $this->assertDatabaseHas('tasks', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'id' => $taskId, + 'title' => 'the task', + 'completed' => false, + ]); + } + + /** @test */ + public function updating_a_task_with_missing_parameters_triggers_an_error() + { + $user = $this->signin(); + $task = factory(Task::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('PUT', '/api/tasks/'.$task->id, [ + 'contact_id' => $task->contact_id, + ]); + + $this->expectDataError($response, [ + 'The title field is required.', + 'The completed field is required.', + ]); + } + + /** @test */ + public function updating_a_task_with_wrong_account_triggers_an_error() + { + $user = $this->signin(); + + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + $task = factory(Task::class)->create([]); + + $response = $this->json('PUT', '/api/tasks/'.$task->id, [ + 'contact_id' => $contact->id, + 'title' => 'the task', + 'completed' => false, + ]); + + $this->expectNotFound($response); + } + + /** @test */ + public function it_deletes_a_task() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $task = factory(Task::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + ]); + + $response = $this->json('DELETE', '/api/tasks/'.$task->id); + + $response->assertStatus(200); + + $this->assertDatabaseMissing('tasks', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'id' => $task->id, + ]); + } + + /** @test */ + public function it_cant_delete_a_task_if_wrong_task_id() + { + $user = $this->signin(); + + $response = $this->json('DELETE', '/api/tasks/0'); + + $this->expectNotFound($response); + } +} diff --git a/tests/Api/Authentication/ApiAuthenticateTest.php b/tests/Api/Authentication/ApiAuthenticateTest.php new file mode 100644 index 0000000..ffe6966 --- /dev/null +++ b/tests/Api/Authentication/ApiAuthenticateTest.php @@ -0,0 +1,19 @@ +json('GET', '/api/contacts'); + + $response->assertStatus(401); + $response->assertJsonFragment([ + 'message' => 'Unauthenticated.', + ]); + } +} diff --git a/tests/Api/Contact/ApiAdressesControllerTest.php b/tests/Api/Contact/ApiAdressesControllerTest.php new file mode 100644 index 0000000..20eef99 --- /dev/null +++ b/tests/Api/Contact/ApiAdressesControllerTest.php @@ -0,0 +1,340 @@ + [ + 'id', + ], + 'contact' => [ + 'id', + ], + 'created_at', + 'updated_at', + ]; + + /** @test */ + public function it_gets_a_list_of_addresses() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create(['account_id' => $user->account_id]); + $address = factory(Address::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + ]); + + $response = $this->json('GET', '/api/addresses'); + + $response->assertStatus(200); + + $response->assertJsonStructure([ + 'data' => ['*' => $this->jsonAddress], + ]); + + $response->assertJsonFragment([ + 'object' => 'address', + 'id' => $address->id, + ]); + + $response->assertJsonFragment([ + 'total' => 1, + 'current_page' => 1, + ]); + } + + /** @test */ + public function it_applies_the_limit_parameter_in_search() + { + $user = $this->signin(); + + $contact = factory(Contact::class)->create(['account_id' => $user->account_id]); + factory(Address::class, 20)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + ]); + + $response = $this->json('GET', '/api/addresses?limit=1'); + + $response->assertStatus(200); + + $response->assertJsonFragment([ + 'total' => 20, + 'current_page' => 1, + 'per_page' => 1, + 'last_page' => 20, + ]); + + $response = $this->json('GET', '/api/addresses?limit=2'); + + $response->assertStatus(200); + + $response->assertJsonFragment([ + 'total' => 20, + 'current_page' => 1, + 'per_page' => 2, + 'last_page' => 10, + ]); + } + + /** @test */ + public function it_gets_addresses_for_a_specific_contact() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create(['account_id' => $user->account_id]); + $address = factory(Address::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + ]); + + $response = $this->json('GET', '/api/contacts/'.$contact->id.'/addresses'); + + $response->assertStatus(200); + + $response->assertJsonFragment([ + 'object' => 'address', + 'id' => $address->id, + 'name' => $address->name, + ]); + } + + /** @test */ + public function calling_addresses_gets_an_error_if_contact_doesnt_exist() + { + $user = $this->signin(); + + $response = $this->json('GET', '/api/contacts/0/addresses'); + + $this->expectNotFound($response); + } + + /** @test */ + public function it_gets_a_specific_address() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create(['account_id' => $user->account_id]); + $address = factory(Address::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + ]); + + $response = $this->json('GET', '/api/addresses/'.$address->id); + + $response->assertStatus(200); + + $response->assertJsonStructure([ + 'data' => $this->jsonAddress, + ]); + + $response->assertJsonFragment([ + 'object' => 'address', + 'id' => $address->id, + 'name' => $address->name, + 'street' => $address->place->street, + ]); + } + + /** @test */ + public function it_creates_an_address() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create(['account_id' => $user->account_id]); + + $response = $this->json('POST', '/api/addresses', [ + 'contact_id' => $contact->id, + 'name' => 'address name', + 'street' => 'street', + 'postal_code' => '12345', + 'country' => 'FR', + ]); + + $response->assertStatus(201); + + $this->assertDatabaseHas('addresses', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'name' => 'address name', + ]); + + $response->assertJsonFragment([ + 'object' => 'address', + 'name' => 'address name', + 'country' => [ + 'object' => 'country', + 'id' => 'FR', + 'name' => 'France', + 'iso' => 'FR', + ], + 'street' => 'street', + 'postal_code' => '12345', + ]); + + $addressId = $response->json('data.id'); + $this->assertGreaterThan(0, $addressId); + } + + /** @test */ + public function create_addresses_gets_an_error_if_fields_are_missing() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create(['account_id' => $user->account_id]); + + $response = $this->json('POST', '/api/addresses', [ + ]); + + $this->expectDataError($response, [ + 'The contact id field is required.', + ]); + } + + /** @test */ + public function create_addresses_gets_an_error_if_contact_is_not_linked_to_user() + { + $user = $this->signin(); + + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + + $response = $this->json('POST', '/api/addresses', [ + 'contact_id' => $contact->id, + 'name' => 'address name', + 'street' => 'street', + 'postal_code' => '12345', + 'country' => 'FR', + ]); + + $this->expectNotFound($response); + } + + /** @test */ + public function it_updates_an_address() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create(['account_id' => $user->account_id]); + $address = factory(Address::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'name' => 'address name', + ]); + + $response = $this->json('PUT', '/api/addresses/'.$address->id, [ + 'contact_id' => $contact->id, + 'name' => 'address name up', + 'country' => 'US', + ]); + + $response->assertStatus(200); + + $response->assertJsonFragment([ + 'object' => 'address', + 'id' => $address->id, + 'name' => 'address name up', + 'country' => [ + 'object' => 'country', + 'id' => 'US', + 'name' => 'United States', + 'iso' => 'US', + ], + 'postal_code' => $address->place->postal_code, + ]); + + $this->assertDatabaseHas('addresses', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'place_id' => $address->place->id, + 'id' => $address->id, + 'name' => 'address name up', + ]); + } + + /** @test */ + public function updating_address_generates_an_error() + { + $user = $this->signin(); + $address = factory(Address::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('PUT', '/api/addresses/'.$address->id, []); + + $this->expectDataError($response, [ + 'The contact id field is required.', + ]); + } + + /** @test */ + public function it_cant_update_an_address_if_account_is_not_linked_to_address() + { + $user = $this->signin(); + + $contact = factory(Contact::class)->create([]); + $address = factory(Address::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('PUT', '/api/addresses/'.$address->id, [ + 'contact_id' => $contact->id, + ]); + + $this->expectNotFound($response); + } + + /** @test */ + public function it_deletes_an_address() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create(['account_id' => $user->account_id]); + $address = factory(Address::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + ]); + + $response = $this->json('DELETE', '/api/addresses/'.$address->id); + + $response->assertStatus(200); + + $response->assertJsonFragment([ + 'id' => $address->id, + 'deleted' => true, + ]); + + $this->assertDatabaseMissing('addresses', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'id' => $address->id, + ]); + } + + /** @test */ + public function address_delete_error() + { + $user = $this->signin(); + + $response = $this->json('DELETE', '/api/addresses/0'); + + $this->expectDataError($response, [ + 'The selected address id is invalid.', + ]); + } +} diff --git a/tests/Api/Contact/ApiAuditLogControllerTest.php b/tests/Api/Contact/ApiAuditLogControllerTest.php new file mode 100644 index 0000000..116044c --- /dev/null +++ b/tests/Api/Contact/ApiAuditLogControllerTest.php @@ -0,0 +1,59 @@ + [ + 'name', + ], + 'action', + 'objects', + 'audited_at', + 'created_at', + 'updated_at', + ]; + + /** @test */ + public function it_gets_a_list_of_audit_logs() + { + $user = $this->signin(); + + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + 'first_name' => 'roger', + ]); + + factory(AuditLog::class, 10)->create([ + 'account_id' => $user->account_id, + 'about_contact_id' => $contact->id, + ]); + + $response = $this->json('GET', '/api/contacts/'.$contact->id.'/logs'); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => ['*' => $this->jsonStructureAuditLog], + ]); + + $this->assertCount( + 10, + $response->decodeResponseJson()['data'] + ); + + $response->assertJsonFragment([ + 'total' => 10, + 'current_page' => 1, + ]); + } +} diff --git a/tests/Api/Contact/ApiAvatarControllerTest.php b/tests/Api/Contact/ApiAvatarControllerTest.php new file mode 100644 index 0000000..6ee5102 --- /dev/null +++ b/tests/Api/Contact/ApiAvatarControllerTest.php @@ -0,0 +1,180 @@ + [ + 'id', + ], + 'information' => [ + 'avatar' => [ + 'url', + 'source', + 'default_avatar_color', + ], + ], + 'created_at', + 'updated_at', + ]; + + /** @test */ + public function it_updates_the_photo_avatar() + { + Storage::fake(); + + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('POST', '/api/photos', [ + 'contact_id' => $contact->id, + 'photo' => UploadedFile::fake()->image('test.jpg'), + ]); + + $response->assertStatus(201); + + $this->assertDatabaseHas('photos', [ + 'account_id' => $user->account_id, + 'original_filename' => 'test.jpg', + ]); + + $photo = $contact->photos->first(); + + Storage::disk('public')->assertExists($photo->new_filename); + + $response = $this->json('PUT', '/api/contacts/'.$contact->id.'/avatar', [ + 'photo' => UploadedFile::fake()->image('test.jpg'), + 'source' => 'photo', + 'photo_id' => $photo->id, + ]); + + $response->assertStatus(200); + + $response->assertJsonStructure([ + '*' => $this->jsonDatas, + ]); + + $this->assertDatabaseHas('contacts', [ + 'id' => $contact->id, + 'avatar_source' => 'photo', + 'avatar_photo_id' => $photo->id, + ]); + } + + /** @test */ + public function it_updates_the_gravatar_avatar() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + 'avatar_gravatar_url' => 'a gravatar url', + ]); + + $response = $this->json('PUT', '/api/contacts/'.$contact->id.'/avatar', [ + 'source' => 'gravatar', + ]); + + $response->assertStatus(200); + + $response->assertJsonStructure([ + '*' => $this->jsonDatas, + ]); + + $this->assertDatabaseHas('contacts', [ + 'id' => $contact->id, + 'avatar_source' => 'gravatar', + ]); + } + + /** @test */ + public function it_updates_the_adorable_avatar() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('PUT', '/api/contacts/'.$contact->id.'/avatar', [ + 'source' => 'adorable', + ]); + + $response->assertStatus(200); + + $response->assertJsonStructure([ + '*' => $this->jsonDatas, + ]); + + $this->assertDatabaseHas('contacts', [ + 'id' => $contact->id, + 'avatar_source' => 'adorable', + ]); + } + + /** @test */ + public function it_updates_the_default_avatar() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('PUT', '/api/contacts/'.$contact->id.'/avatar', [ + 'source' => 'default', + ]); + + $response->assertStatus(200); + + $response->assertJsonStructure([ + '*' => $this->jsonDatas, + ]); + + $this->assertDatabaseHas('contacts', [ + 'id' => $contact->id, + 'avatar_source' => 'default', + ]); + } + + /** @test */ + public function avatar_update_gets_an_error_if_fields_are_missing() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('PUT', '/api/contacts/'.$contact->id.'/avatar', [ + 'source' => 'blabla', + ]); + + $this->expectDataError($response, [ + 'The selected source is invalid.', + ]); + } + + /** @test */ + public function avatar_update_gets_an_error_if_contact_is_not_linked_to_user() + { + $user = $this->signin(); + + $contact = factory(Contact::class)->create(); + + $response = $this->json('PUT', '/api/contacts/'.$contact->id.'/avatar', [ + 'source' => 'default', + ]); + + $this->expectNotFound($response); + } +} diff --git a/tests/Api/Contact/ApiCallControllerTest.php b/tests/Api/Contact/ApiCallControllerTest.php new file mode 100644 index 0000000..a2ad2ae --- /dev/null +++ b/tests/Api/Contact/ApiCallControllerTest.php @@ -0,0 +1,352 @@ + [ + 'id', + ], + 'contact' => [ + 'id', + ], + 'created_at', + 'updated_at', + ]; + + /** @test */ + public function it_gets_a_list_of_calls() + { + $user = $this->signin(); + $contact1 = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $call1 = factory(Call::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact1->id, + ]); + $contact2 = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $call2 = factory(Call::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact2->id, + ]); + + $response = $this->json('GET', '/api/calls'); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => ['*' => $this->jsonCall], + ]); + $response->assertJsonFragment([ + 'object' => 'call', + 'id' => $call1->id, + ]); + $response->assertJsonFragment([ + 'object' => 'call', + 'id' => $call2->id, + ]); + } + + /** @test */ + public function it_gets_the_calls_of_a_contact() + { + $user = $this->signin(); + $contact1 = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $call1 = factory(Call::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact1->id, + ]); + $contact2 = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $call2 = factory(Call::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact2->id, + ]); + + $response = $this->json('GET', '/api/contacts/'.$contact1->id.'/calls'); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => ['*' => $this->jsonCall], + ]); + $response->assertJsonFragment([ + 'object' => 'call', + 'id' => $call1->id, + ]); + $response->assertJsonMissingExact([ + 'object' => 'call', + 'id' => $call2->id, + ]); + } + + /** @test */ + public function calling_calls_get_error() + { + $user = $this->signin(); + + $response = $this->json('GET', '/api/contacts/0/calls'); + + $this->expectNotFound($response); + } + + /** @test */ + public function it_gets_one_call() + { + $user = $this->signin(); + $contact1 = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $call1 = factory(Call::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact1->id, + ]); + $call2 = factory(Call::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact1->id, + ]); + + $response = $this->json('GET', '/api/calls/'.$call1->id); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => $this->jsonCall, + ]); + $response->assertJsonFragment([ + 'object' => 'call', + 'id' => $call1->id, + ]); + $response->assertJsonMissingExact([ + 'object' => 'call', + 'id' => $call2->id, + ]); + } + + /** @test */ + public function calling_one_call_gets_an_error() + { + $user = $this->signin(); + + $response = $this->json('GET', '/api/calls/0'); + + $this->expectNotFound($response); + } + + /** @test */ + public function it_creates_a_call() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('POST', '/api/calls', [ + 'contact_id' => $contact->id, + 'content' => 'the call', + 'called_at' => '2018-05-01', + ]); + + $response->assertStatus(201); + $response->assertJsonStructure([ + 'data' => $this->jsonCall, + ]); + $callId = $response->json('data.id'); + $response->assertJsonFragment([ + 'object' => 'call', + 'id' => $callId, + ]); + + $this->assertGreaterThan(0, $callId); + $this->assertDatabaseHas('calls', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'id' => $callId, + 'content' => 'the call', + 'called_at' => '2018-05-01', + ]); + } + + /** @test */ + public function create_calls_gets_an_error_if_fields_are_missing() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('POST', '/api/calls', [ + 'contact_id' => $contact->id, + ]); + + $this->expectDataError($response, [ + 'The called at field is required.', + ]); + } + + /** @test */ + public function it_cant_create_a_call_if_account_is_wrong() + { + $user = $this->signin(); + + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + + $response = $this->json('POST', '/api/calls', [ + 'contact_id' => $contact->id, + 'content' => 'the call', + 'called_at' => '2018-05-01', + ]); + + $this->expectNotFound($response); + } + + /** @test */ + public function it_updates_a_call() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $call = factory(Call::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + ]); + + $response = $this->json('PUT', '/api/calls/'.$call->id, [ + 'contact_id' => $contact->id, + 'content' => 'the call', + 'called_at' => '2018-05-01', + ]); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => $this->jsonCall, + ]); + $callId = $response->json('data.id'); + $this->assertEquals($call->id, $callId); + $response->assertJsonFragment([ + 'object' => 'call', + 'id' => $callId, + ]); + + $this->assertGreaterThan(0, $callId); + $this->assertDatabaseHas('calls', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'id' => $callId, + 'content' => 'the call', + 'called_at' => '2018-05-01', + ]); + } + + /** @test */ + public function updating_call_generates_an_error() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $call = factory(Call::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + ]); + + $response = $this->json('PUT', '/api/calls/'.$call->id, [ + 'contact_id' => $call->contact_id, + ]); + + $this->expectDataError($response, [ + 'The called at field is required.', + ]); + } + + /** @test */ + public function it_cant_update_a_call_if_account_is_not_linked_to_call() + { + $user = $this->signin(); + + $contact = factory(Contact::class)->create([]); + $call = factory(Call::class)->create([ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + ]); + + $response = $this->json('PUT', '/api/calls/'.$call->id, [ + 'content' => 'the call', + 'called_at' => '2018-05-01', + ]); + + $this->expectNotFound($response); + } + + /** @test */ + public function it_deletes_a_call() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $call = factory(Call::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + ]); + $this->assertDatabaseHas('calls', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'id' => $call->id, + ]); + + $response = $this->json('DELETE', '/api/calls/'.$call->id); + + $response->assertStatus(200); + $this->assertDatabaseMissing('calls', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'id' => $call->id, + ]); + } + + /** @test */ + public function it_cant_delete_a_call_if_call_doesnt_exist() + { + $user = $this->signin(); + + $response = $this->json('DELETE', '/api/calls/0'); + + $this->expectNotFound($response); + } + + /** @test */ + public function it_cant_delete_a_call_if_account_is_not_linked() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([]); + $call = factory(Call::class)->create([ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + ]); + + $response = $this->json('DELETE', '/api/calls/'.$call->id); + + $this->expectNotFound($response); + } +} diff --git a/tests/Api/Contact/ApiContactControllerTest.php b/tests/Api/Contact/ApiContactControllerTest.php new file mode 100644 index 0000000..0a083c7 --- /dev/null +++ b/tests/Api/Contact/ApiContactControllerTest.php @@ -0,0 +1,1694 @@ + [ + 'relationships' => [ + 'love' => [ + 'total', + 'contacts', + ], + 'family' => [ + 'total', + 'contacts', + ], + 'friend' => [ + 'total', + 'contacts', + ], + 'work' => [ + 'total', + 'contacts', + ], + ], + 'dates' => [ + 'birthdate' => [ + 'is_age_based', + 'is_year_unknown', + 'date', + ], + 'deceased_date' => [ + 'is_age_based', + 'is_year_unknown', + 'date', + ], + ], + 'career', + 'avatar', + 'food_preferences', + 'how_you_met', + ], + 'addresses', + 'tags', + 'statistics', + 'account' => [ + 'id', + ], + 'created_at', + 'updated_at', + ]; + + protected $jsonStructureContactWithContactFields = [ + 'id', + 'object', + 'hash_id', + 'first_name', + 'last_name', + 'gender', + 'gender_type', + 'is_starred', + 'is_partial', + 'is_dead', + 'last_called', + 'last_activity_together', + 'stay_in_touch_frequency', + 'stay_in_touch_trigger_date', + 'information' => [ + 'relationships' => [ + 'love' => [ + 'total', + 'contacts', + ], + 'family' => [ + 'total', + 'contacts', + ], + 'friend' => [ + 'total', + 'contacts', + ], + 'work' => [ + 'total', + 'contacts', + ], + ], + 'dates' => [ + 'birthdate' => [ + 'is_age_based', + 'is_year_unknown', + 'date', + ], + 'deceased_date' => [ + 'is_age_based', + 'is_year_unknown', + 'date', + ], + ], + 'career' => [ + 'job', + 'company', + ], + 'avatar' => [ + 'url', + 'source', + 'default_avatar_color', + ], + 'food_preferences', + 'how_you_met' => [ + 'general_information', + 'first_met_date' => [ + 'is_age_based', + 'is_year_unknown', + 'date', + ], + 'first_met_through_contact', + ], + ], + 'addresses' => [], + 'tags' => [], + 'statistics' => [ + 'number_of_calls', + 'number_of_notes', + 'number_of_activities', + 'number_of_reminders', + 'number_of_tasks', + 'number_of_gifts', + 'number_of_debts', + ], + 'contactFields' => [ + '*' => [ + 'id', + 'object', + 'content', + 'contact_field_type' => [ + 'id', + 'object', + 'name', + 'fontawesome_icon', + 'protocol', + 'delible', + 'type', + 'account' => [ + 'id', + ], + 'created_at', + 'updated_at', + ], + 'account' => [ + 'id', + ], + 'contact' => [], + 'created_at', + 'updated_at', + ], + ], + 'notes' => [], + 'account' => [ + 'id', + ], + 'created_at', + 'updated_at', + ]; + + protected $jsonStructureContactShort = [ + 'id', + 'object', + 'hash_id', + 'first_name', + 'last_name', + 'nickname', + 'gender', + 'gender_type', + 'is_partial', + 'is_dead', + 'information' => [ + 'dates' => [ + 'birthdate' => [ + 'is_age_based', + 'is_year_unknown', + 'date', + ], + 'deceased_date' => [ + 'is_age_based', + 'is_year_unknown', + 'date', + ], + ], + ], + 'account' => [ + 'id', + ], + 'created_at', + 'updated_at', + ]; + + /** @test */ + public function it_gets_a_list_of_contacts() + { + $user = $this->signin(); + + $contact = factory(Contact::class, 10)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('GET', '/api/contacts'); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => ['*' => $this->jsonStructureContact], + ]); + + $this->assertCount( + 10, + $response->decodeResponseJson()['data'] + ); + } + + /** @test */ + public function it_gets_a_list_of_contacts_without_gender() + { + $user = $this->signin(); + + $contact = factory(Contact::class, 10)->state('no_gender')->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('GET', '/api/contacts'); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => ['*' => $this->jsonStructureContact], + ]); + + $this->assertCount( + 10, + $response->decodeResponseJson()['data'] + ); + } + + /** @test */ + public function it_contains_pagination_when_fetching_contacts() + { + $user = $this->signin(); + + $contact = factory(Contact::class, 10)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('GET', '/api/contacts'); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => ['*' => $this->jsonStructureContact], + ]); + + $response->assertJsonFragment([ + 'total' => 10, + 'current_page' => 1, + ]); + } + + /** @test */ + public function it_applies_the_limit_parameter_in_search() + { + $user = $this->signin(); + + $contact = factory(Contact::class, 10)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('GET', '/api/contacts?limit=1'); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => ['*' => $this->jsonStructureContact], + ]); + $response->assertJsonFragment([ + 'total' => 10, + 'current_page' => 1, + 'per_page' => 1, + 'last_page' => 10, + ]); + + $response = $this->json('GET', '/api/contacts?limit=2'); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => ['*' => $this->jsonStructureContact], + ]); + $response->assertJsonFragment([ + 'total' => 10, + 'current_page' => 1, + 'per_page' => 2, + 'last_page' => 5, + ]); + } + + /** @test */ + public function it_is_possible_to_search_contacts_with_query() + { + $user = $this->signin(); + + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + 'first_name' => 'roger', + ]); + + // create 10 other contacts named Bob (to avoid random conflicts if we took a random name) + $contact = factory(Contact::class, 10)->create([ + 'account_id' => $user->account_id, + 'first_name' => 'bob', + ]); + + $response = $this->json('GET', '/api/contacts?query=ro'); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => ['*' => $this->jsonStructureContact], + ]); + + $response->assertJsonFragment([ + 'first_name' => 'roger', + 'total' => 1, + 'query' => 'ro', + ]); + } + + /** @test */ + public function it_is_possible_to_search_contacts_and_limit_query() + { + $user = $this->signin(); + + $contact = factory(Contact::class, 2)->create([ + 'account_id' => $user->account_id, + 'first_name' => 'roger', + ]); + + // create 10 other contacts named Bob (to avoid random conflicts if we took a random name) + $contact = factory(Contact::class, 10)->create([ + 'account_id' => $user->account_id, + 'first_name' => 'bob', + ]); + + $response = $this->json('GET', '/api/contacts?query=ro&limit=1'); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => ['*' => $this->jsonStructureContact], + ]); + + $response->assertJsonFragment([ + 'first_name' => 'roger', + 'total' => 2, + 'query' => 'ro', + 'per_page' => 1, + 'current_page' => 1, + ]); + } + + /** @test */ + public function it_is_possible_to_search_contacts_and_limit_query_and_paginate() + { + $user = $this->signin(); + + $contact = factory(Contact::class, 2)->create([ + 'account_id' => $user->account_id, + 'first_name' => 'roger', + ]); + + // create 10 other contacts named Bob (to avoid random conflicts if we took a random name) + $contact = factory(Contact::class, 10)->create([ + 'account_id' => $user->account_id, + 'first_name' => 'bob', + ]); + + $response = $this->json('GET', '/api/contacts?query=ro&limit=1&page=2'); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => ['*' => $this->jsonStructureContact], + ]); + + $response->assertJsonFragment([ + 'first_name' => 'roger', + 'total' => 2, + 'query' => 'ro', + 'per_page' => 1, + 'current_page' => 2, + ]); + } + + /** @test */ + public function it_gets_a_contact() + { + $user = $this->signin(); + + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + 'first_name' => 'roger', + ]); + + $response = $this->json('GET', '/api/contacts/'.$contact->id); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => $this->jsonStructureContact, + ]); + + $response->assertJsonFragment([ + 'first_name' => 'roger', + 'object' => 'contact', + ]); + } + + /** @test */ + public function getting_a_contact_matches_a_specific_json_structure() + { + $user = $this->signin(); + + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + 'first_name' => 'roger', + ]); + + $response = $this->json('GET', '/api/contacts/'.$contact->id); + + $response->assertStatus(200); + + $response->assertJsonStructure([ + 'data' => $this->jsonStructureContact, + ]); + } + + /** @test */ + public function getting_a_partial_contact_matches_a_specific_json_structure() + { + $user = $this->signin(); + + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + 'first_name' => 'roger', + 'is_partial' => true, + ]); + + $response = $this->json('GET', '/api/contacts/'.$contact->id); + + $response->assertStatus(200); + + $response->assertJsonStructure([ + 'data' => $this->jsonStructureContactShort, + ]); + } + + /** @test */ + public function getting_a_contact_with_the_parameter_with_matches_a_specific_json_structure() + { + $user = $this->signin(); + + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + 'first_name' => 'roger', + ]); + + $field = factory(ContactFieldType::class)->create([ + 'account_id' => $user->account_id, + ]); + + $contactField = factory(ContactField::class)->create([ + 'contact_id' => $contact->id, + 'account_id' => $user->account_id, + 'contact_field_type_id' => $field->id, + ]); + + $response = $this->json('GET', '/api/contacts?with=contactfields'); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => ['*' => $this->jsonStructureContactWithContactFields], + ]); + + $response->assertJsonFragment([ + 'id' => $contactField->id, + 'object' => 'contactfield', + 'account' => [ + 'id' => $user->account_id, + ], + ]); + } + + /** @test */ + public function it_gets_list_of_contacts_with_parameter_and_limit_and_page() + { + $user = $this->signin(); + + $initialContact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + 'created_at' => now()->addDays(-1), + ]); + + $field = factory(ContactFieldType::class)->create([ + 'account_id' => $user->account_id, + ]); + + $contactField = factory(ContactField::class)->create([ + 'contact_id' => $initialContact->id, + 'account_id' => $user->account_id, + 'contact_field_type_id' => $field->id, + ]); + + $counter = 1; + while ($counter < 12) { + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $field = factory(ContactFieldType::class)->create([ + 'account_id' => $user->account_id, + ]); + + factory(ContactField::class)->create([ + 'contact_id' => $contact->id, + 'account_id' => $user->account_id, + 'contact_field_type_id' => $field->id, + ]); + + $counter++; + } + + $response = $this->json('GET', '/api/contacts?with=contactfields&page=1&limit=10'); + + $response->assertStatus(200); + + $response->assertJsonStructure([ + 'data' => ['*' => $this->jsonStructureContactWithContactFields], + ]); + + $response->assertJsonFragment([ + 'id' => $contactField->id, + 'object' => 'contactfield', + 'account' => [ + 'id' => $user->account_id, + ], + ]); + } + + /** @test */ + public function it_prevents_a_contact_query_injection() + { + $firstuser = $this->signin(); + $firstcontact = factory(Contact::class)->create([ + 'account_id' => $firstuser->account_id, + 'first_name' => 'Bad', + ]); + + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $response = $this->json('GET', "/api/contacts?with=contactfields&page=1&limit=100&query=1')%20or%20('%'='"); + + $response->assertStatus(200); + // Ensure that firstcontact from other account is not get (SQL injection) + $response->assertJsonMissing([ + 'id' => $firstcontact->id, + 'first_name' => 'Bad', + 'account' => [ + 'id' => $firstuser->account_id, + ], + ]); + } + + /** @test */ + public function it_gets_a_contact_with_the_contact_fields() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + 'first_name' => 'roger', + ]); + $field = factory(ContactFieldType::class)->create([ + 'account_id' => $user->account_id, + ]); + $contactField = factory(ContactField::class)->create([ + 'contact_id' => $contact->id, + 'account_id' => $user->account_id, + 'contact_field_type_id' => $field->id, + ]); + + $response = $this->json('GET', '/api/contacts/'.$contact->id.'?with=contactfields'); + + $response->assertOk(); + + $response->assertJsonStructure([ + 'data' => $this->jsonStructureContactWithContactFields, + ]); + + $response->assertJsonFragment([ + 'birthdate' => [ + 'date' => null, + 'is_age_based' => null, + 'is_year_unknown' => null, + ], + ]); + $response->assertJsonFragment([ + 'id' => $contactField->id, + 'object' => 'contactfield', + 'content' => 'john@doe.com', + ]); + $response->assertJsonFragment([ + 'id' => $field->id, + 'object' => 'contactfieldtype', + 'name' => 'Email', + ]); + } + + /** @test */ + public function contact_field_query_all_account() + { + $firstuser = $this->signin(); + $firstcontact = factory(Contact::class)->create([ + 'account_id' => $firstuser->account_id, + 'first_name' => 'Bad', + ]); + $firstfield = factory(ContactFieldType::class)->create([ + 'account_id' => $firstuser->account_id, + ]); + $contactField = factory(ContactField::class)->create([ + 'contact_id' => $firstcontact->id, + 'account_id' => $firstuser->account_id, + 'contact_field_type_id' => $firstfield->id, + ]); + + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $field = factory(ContactFieldType::class)->create([ + 'account_id' => $user->account_id, + ]); + $contactField = factory(ContactField::class)->create([ + 'contact_id' => $contact->id, + 'account_id' => $user->account_id, + 'contact_field_type_id' => $field->id, + ]); + + $response = $this->json('GET', '/api/contacts?with=contactfields&page=1&limit=100&query=email:john@doe'); + + $response->assertStatus(200); + // Assure that firstcontact from other account is not get (wrong filter on account id) + $response->assertJsonMissing([ + 'id' => $firstcontact->id, + 'first_name' => 'Bad', + 'account' => [ + 'id' => $firstuser->account_id, + ], + ]); + } + + /** @test */ + public function contact_query_internationalphone() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $field = factory(ContactFieldType::class)->create([ + 'account_id' => $user->account_id, + 'name' => 'Phone', + 'protocol' => 'tel:', + 'type' => 'phone', + ]); + $contactField = factory(ContactField::class)->create([ + 'contact_id' => $contact->id, + 'account_id' => $user->account_id, + 'contact_field_type_id' => $field->id, + 'data' => '+447007007007', + ]); + + $response = $this->json('GET', '/api/contacts?query=Phone:%2B447007007007'); + + $response->assertStatus(200); + $response->assertJsonFragment([ + 'id' => $contact->id, + 'first_name' => 'John', + 'last_name' => 'Doe', + 'account' => [ + 'id' => $user->account_id, + ], + ]); + } + + /** @test */ + public function it_creates_a_contact() + { + $user = $this->signin(); + $gender = factory(Gender::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('POST', '/api/contacts/', [ + 'first_name' => 'John', + 'middle_name' => 'Freaking', + 'last_name' => 'Doe', + 'nickname' => 'Titi', + 'gender_id' => $gender->id, + 'description' => 'A great guy', + 'is_partial' => false, + 'is_birthdate_known' => false, + 'is_deceased' => false, + 'is_deceased_date_known' => false, + ]); + + $response->assertStatus(201); + + $response->assertJsonStructure([ + 'data' => $this->jsonStructureContact, + ]); + $contact_id = $response->json('data.id'); + $response->assertJsonFragment([ + 'object' => 'contact', + 'id' => $contact_id, + ]); + + $this->assertGreaterThan(0, $contact_id); + + $this->assertDatabaseHas('contacts', [ + 'account_id' => $user->account_id, + 'id' => $contact_id, + 'first_name' => 'John', + 'last_name' => 'Doe', + 'gender_id' => $gender->id, + 'is_starred' => false, + 'is_partial' => false, + 'is_dead' => false, + ]); + } + + /** @test */ + public function creating_contact_is_not_possible_if_parameters_are_missing() + { + $user = $this->signin(); + + $response = $this->json('POST', '/api/contacts/', [ + 'first_name' => 'John', + 'last_name' => 'Doe', + ]); + + $this->expectDataError($response, [ + 'The is birthdate known field is required.', + 'The is deceased field is required.', + 'The is deceased date known field is required.', + ]); + } + + /** @test */ + public function it_creates_a_birthdate() + { + $user = $this->signin(); + $gender = factory(Gender::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('POST', '/api/contacts/', [ + 'first_name' => 'John', + 'middle_name' => 'Freaking', + 'last_name' => 'Doe', + 'nickname' => 'Titi', + 'gender_id' => $gender->id, + 'description' => 'A great guy', + 'is_partial' => false, + 'is_birthdate_known' => true, + 'birthdate_day' => 10, + 'birthdate_month' => 10, + 'birthdate_year' => 1980, + 'birthdate_is_age_based' => false, + 'birthdate_add_reminder' => true, + 'is_deceased' => false, + 'is_deceased_date_known' => false, + ]); + + $response->assertStatus(201); + + $response->assertJsonStructure([ + 'data' => $this->jsonStructureContact, + ]); + $contact_id = $response->json('data.id'); + $response->assertJsonFragment([ + 'object' => 'contact', + 'id' => $contact_id, + ]); + + $response->assertJsonFragment([ + 'birthdate' => [ + 'date' => '1980-10-10T00:00:00Z', + 'is_age_based' => false, + 'is_year_unknown' => false, + ], + ]); + + $this->assertDatabaseHas('special_dates', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact_id, + 'id' => Contact::find($contact_id)->birthday_special_date_id, + 'is_age_based' => false, + 'is_year_unknown' => false, + 'date' => '1980-10-10', + ]); + } + + /** @test */ + public function contact_create_birthdate_year_unknown() + { + Carbon::setTestNow(Carbon::create(2018, 1, 1, 7, 0, 0)); + $user = $this->signin(); + $gender = factory(Gender::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('POST', '/api/contacts/', [ + 'first_name' => 'John', + 'middle_name' => 'Freaking', + 'last_name' => 'Doe', + 'nickname' => 'Titi', + 'gender_id' => $gender->id, + 'description' => 'A great guy', + 'is_partial' => false, + 'is_birthdate_known' => true, + 'birthdate_day' => 10, + 'birthdate_month' => 10, + 'birthdate_year' => 0, + 'birthdate_is_age_based' => false, + 'birthdate_add_reminder' => true, + 'is_deceased' => false, + 'is_deceased_date_known' => false, + ]); + + $response->assertStatus(201); + + $response->assertJsonStructure([ + 'data' => $this->jsonStructureContact, + ]); + $contact_id = $response->json('data.id'); + $response->assertJsonFragment([ + 'object' => 'contact', + 'id' => $contact_id, + ]); + + $response->assertJsonFragment([ + 'birthdate' => [ + 'date' => '2018-10-10T00:00:00Z', + 'is_age_based' => false, + 'is_year_unknown' => true, + ], + ]); + + $this->assertDatabaseHas('special_dates', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact_id, + 'id' => Contact::find($contact_id)->birthday_special_date_id, + 'is_age_based' => false, + 'is_year_unknown' => true, + 'date' => '2018-10-10', + ]); + } + + /** @test */ + public function contact_create_birthdate_age_based() + { + Carbon::setTestNow(Carbon::create(2018, 1, 1, 7, 0, 0)); + $user = $this->signin(); + $gender = factory(Gender::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('POST', '/api/contacts/', [ + 'first_name' => 'John', + 'middle_name' => 'Freaking', + 'last_name' => 'Doe', + 'nickname' => 'Titi', + 'gender_id' => $gender->id, + 'description' => 'A great guy', + 'is_partial' => false, + 'is_birthdate_known' => true, + 'birthdate_day' => 10, + 'birthdate_month' => 10, + 'birthdate_year' => 0, + 'birthdate_is_age_based' => true, + 'birthdate_age' => 30, + 'birthdate_add_reminder' => true, + 'is_deceased' => false, + 'is_deceased_date_known' => false, + ]); + + $response->assertStatus(201); + + $response->assertJsonStructure([ + 'data' => $this->jsonStructureContact, + ]); + $contact_id = $response->json('data.id'); + $response->assertJsonFragment([ + 'object' => 'contact', + 'id' => $contact_id, + ]); + + $response->assertJsonFragment([ + 'birthdate' => [ + 'date' => '1988-01-01T00:00:00Z', + 'is_age_based' => true, + 'is_year_unknown' => false, + ], + ]); + + $this->assertDatabaseHas('special_dates', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact_id, + 'id' => Contact::find($contact_id)->birthday_special_date_id, + 'is_age_based' => true, + 'is_year_unknown' => false, + 'date' => '1988-01-01', + ]); + } + + /** @test */ + public function contact_create_deceased_date() + { + Carbon::setTestNow(Carbon::create(2018, 1, 1, 7, 0, 0)); + $user = $this->signin(); + $gender = factory(Gender::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('POST', '/api/contacts/', [ + 'first_name' => 'John', + 'middle_name' => 'Freaking', + 'last_name' => 'Doe', + 'nickname' => 'Titi', + 'gender_id' => $gender->id, + 'description' => 'A great guy', + 'is_partial' => false, + 'is_birthdate_known' => false, + 'is_deceased' => true, + 'is_deceased_date_known' => true, + 'deceased_date_day' => 10, + 'deceased_date_month' => 10, + 'deceased_date_year' => 1900, + 'deceased_date_add_reminder' => false, + ]); + + $response->assertStatus(201); + + $response->assertJsonStructure([ + 'data' => $this->jsonStructureContact, + ]); + $contact_id = $response->json('data.id'); + $response->assertJsonFragment([ + 'object' => 'contact', + 'id' => $contact_id, + ]); + + $response->assertJsonFragment([ + 'deceased_date' => [ + 'date' => '1900-10-10T00:00:00Z', + 'is_age_based' => false, + 'is_year_unknown' => false, + ], + ]); + + $this->assertDatabaseHas('special_dates', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact_id, + 'id' => Contact::find($contact_id)->deceased_special_date_id, + 'is_age_based' => false, + 'is_year_unknown' => false, + 'date' => '1900-10-10', + ]); + } + + /** @test */ + public function contact_create_deceased_date_year_unknown() + { + Carbon::setTestNow(Carbon::create(2018, 1, 1, 7, 0, 0)); + $user = $this->signin(); + $gender = factory(Gender::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('POST', '/api/contacts/', [ + 'first_name' => 'John', + 'middle_name' => 'Freaking', + 'last_name' => 'Doe', + 'nickname' => 'Titi', + 'gender_id' => $gender->id, + 'description' => 'A great guy', + 'is_partial' => false, + 'is_birthdate_known' => false, + 'is_deceased' => true, + 'is_deceased_date_known' => true, + 'deceased_date_day' => 10, + 'deceased_date_month' => 10, + 'deceased_date_year' => 0, + 'deceased_date_add_reminder' => false, + ]); + + $response->assertStatus(201); + + $response->assertJsonStructure([ + 'data' => $this->jsonStructureContact, + ]); + $contact_id = $response->json('data.id'); + $response->assertJsonFragment([ + 'object' => 'contact', + 'id' => $contact_id, + ]); + + $response->assertJsonFragment([ + 'deceased_date' => [ + 'date' => '2018-10-10T00:00:00Z', + 'is_age_based' => false, + 'is_year_unknown' => true, + ], + ]); + + $this->assertDatabaseHas('special_dates', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact_id, + 'id' => Contact::find($contact_id)->deceased_special_date_id, + 'is_age_based' => false, + 'is_year_unknown' => true, + 'date' => '2018-10-10', + ]); + } + + /** @test */ + public function it_updates_a_contact() + { + Carbon::setTestNow(Carbon::create(2018, 1, 1, 7, 0, 0)); + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('PUT', '/api/contacts/'.$contact->id, [ + 'first_name' => 'John', + 'middle_name' => 'Freaking', + 'last_name' => 'Doe', + 'nickname' => 'Titi', + 'gender_id' => $contact->gender_id, + 'description' => 'A great guy', + 'is_partial' => false, + 'is_birthdate_known' => true, + 'birthdate_day' => 01, + 'birthdate_month' => 01, + 'birthdate_year' => 1900, + 'birthdate_is_age_based' => false, + 'birthdate_age' => 0, + 'birthdate_add_reminder' => true, + 'is_deceased' => false, + 'is_deceased_date_known' => false, + ]); + + $response->assertStatus(200); + + $response->assertJsonStructure([ + 'data' => $this->jsonStructureContact, + ]); + $contact_id = $response->json('data.id'); + $response->assertJsonFragment([ + 'object' => 'contact', + 'id' => $contact_id, + 'first_name' => 'John', + ]); + + $response->assertJsonFragment([ + 'birthdate' => [ + 'date' => '1900-01-01T00:00:00Z', + 'is_age_based' => false, + 'is_year_unknown' => false, + ], + ]); + + $this->assertDatabaseHas('special_dates', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact_id, + 'id' => Contact::find($contact_id)->birthday_special_date_id, + 'is_age_based' => false, + 'is_year_unknown' => false, + 'date' => '1900-01-01', + ]); + } + + /** @test */ + public function contact_update_bad_account() + { + $user = $this->signin(); + $gender = factory(Gender::class)->create([ + 'account_id' => $user->account_id, + ]); + + $contact = factory(Contact::class)->create(); + + $response = $this->json('PUT', '/api/contacts/'.$contact->id, [ + 'first_name' => 'John', + 'middle_name' => 'Freaking', + 'last_name' => 'Doe', + 'nickname' => 'Titi', + 'gender_id' => $contact->gender_id, + 'description' => 'A great guy', + 'is_partial' => false, + 'is_birthdate_known' => true, + 'birthdate_day' => 01, + 'birthdate_month' => 01, + 'birthdate_year' => 1900, + 'birthdate_is_age_based' => false, + 'birthdate_age' => 0, + 'birthdate_add_reminder' => true, + 'is_deceased' => false, + 'is_deceased_date_known' => false, + ]); + + $this->expectNotFound($response); + } + + /** @test */ + public function it_cant_update_the_contact_if_parameters_are_missing() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('PUT', '/api/contacts/'.$contact->id, [ + 'first_name' => 'Jane', + 'last_name' => 'Doe', + ]); + + $this->expectDataError($response, [ + 'The is birthdate known field is required.', + 'The is deceased date known field is required.', + ]); + } + + /** @test */ + public function contact_update_birthdate() + { + Carbon::setTestNow(Carbon::create(2018, 1, 1, 7, 0, 0)); + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $gender = factory(Gender::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('PUT', '/api/contacts/'.$contact->id, [ + 'first_name' => 'John', + 'middle_name' => 'Freaking', + 'last_name' => 'Doe', + 'nickname' => 'Titi', + 'gender_id' => $gender->id, + 'description' => 'A great guy', + 'is_partial' => false, + 'is_birthdate_known' => true, + 'birthdate_day' => 10, + 'birthdate_month' => 10, + 'birthdate_year' => 1980, + 'birthdate_is_age_based' => false, + 'birthdate_add_reminder' => true, + 'is_deceased' => false, + 'is_deceased_date_known' => false, + ]); + + $response->assertStatus(200); + + $response->assertJsonFragment([ + 'birthdate' => [ + 'date' => '1980-10-10T00:00:00Z', + 'is_age_based' => false, + 'is_year_unknown' => false, + ], + ]); + + $this->assertDatabaseHas('special_dates', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'id' => Contact::find($contact->id)->birthday_special_date_id, + 'is_age_based' => false, + 'is_year_unknown' => false, + 'date' => '1980-10-10', + ]); + } + + /** @test */ + public function contact_update_birthdate_year_unknown() + { + Carbon::setTestNow(Carbon::create(2018, 1, 1, 7, 0, 0)); + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $gender = factory(Gender::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('PUT', '/api/contacts/'.$contact->id, [ + 'first_name' => 'John', + 'middle_name' => 'Freaking', + 'last_name' => 'Doe', + 'nickname' => 'Titi', + 'gender_id' => $gender->id, + 'description' => 'A great guy', + 'is_partial' => false, + 'is_birthdate_known' => true, + 'birthdate_day' => 10, + 'birthdate_month' => 10, + 'birthdate_year' => 0, + 'birthdate_is_age_based' => false, + 'birthdate_add_reminder' => true, + 'is_deceased' => false, + 'is_deceased_date_known' => false, + ]); + + $response->assertStatus(200); + + $response->assertJsonFragment([ + 'birthdate' => [ + 'date' => '2018-10-10T00:00:00Z', + 'is_age_based' => false, + 'is_year_unknown' => true, + ], + ]); + + $this->assertDatabaseHas('special_dates', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'id' => Contact::find($contact->id)->birthday_special_date_id, + 'is_age_based' => false, + 'is_year_unknown' => true, + 'date' => '2018-10-10', + ]); + } + + /** @test */ + public function contact_update_birthdate_age_based() + { + Carbon::setTestNow(Carbon::create(2018, 1, 1, 7, 0, 0)); + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $gender = factory(Gender::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('PUT', '/api/contacts/'.$contact->id, [ + 'first_name' => 'John', + 'middle_name' => 'Freaking', + 'last_name' => 'Doe', + 'nickname' => 'Titi', + 'gender_id' => $gender->id, + 'description' => 'A great guy', + 'is_partial' => false, + 'is_birthdate_known' => true, + 'birthdate_day' => 10, + 'birthdate_month' => 10, + 'birthdate_year' => 0, + 'birthdate_is_age_based' => true, + 'birthdate_age' => 30, + 'birthdate_add_reminder' => true, + 'is_deceased' => false, + 'is_deceased_date_known' => false, + ]); + + $response->assertStatus(200); + + $response->assertJsonFragment([ + 'birthdate' => [ + 'date' => '1988-01-01T00:00:00Z', + 'is_age_based' => true, + 'is_year_unknown' => false, + ], + ]); + + $this->assertDatabaseHas('special_dates', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'id' => Contact::find($contact->id)->birthday_special_date_id, + 'is_age_based' => true, + 'is_year_unknown' => false, + 'date' => '1988-01-01', + ]); + } + + /** @test */ + public function contact_update_deceased_date() + { + Carbon::setTestNow(Carbon::create(2018, 1, 1, 7, 0, 0)); + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $gender = factory(Gender::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('PUT', '/api/contacts/'.$contact->id, [ + 'first_name' => 'John', + 'middle_name' => 'Freaking', + 'last_name' => 'Doe', + 'nickname' => 'Titi', + 'gender_id' => $gender->id, + 'description' => 'A great guy', + 'is_partial' => false, + 'is_birthdate_known' => false, + 'is_deceased' => true, + 'is_deceased_date_known' => true, + 'deceased_date_day' => 10, + 'deceased_date_month' => 10, + 'deceased_date_year' => 1910, + 'deceased_date_add_reminder' => false, + ]); + + $response->assertStatus(200); + + $response->assertJsonFragment([ + 'deceased_date' => [ + 'date' => '1910-10-10T00:00:00Z', + 'is_age_based' => false, + 'is_year_unknown' => false, + ], + ]); + + $this->assertDatabaseHas('special_dates', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'id' => Contact::find($contact->id)->deceased_special_date_id, + 'is_age_based' => false, + 'is_year_unknown' => false, + 'date' => '1910-10-10', + ]); + } + + /** @test */ + public function it_deletes_a_contact() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('DELETE', '/api/contacts/'.$contact->id); + + $response->assertStatus(200); + $this->assertDatabaseMissing('contacts', [ + 'account_id' => $user->account_id, + 'id' => $contact->id, + 'deleted_at' => null, + ]); + } + + /** @test */ + public function it_gets_me_contact() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $user->me_contact_id = $contact->id; + $user->save(); + + $response = $this->json('GET', '/api/contacts/'.$contact->id); + + $response->assertOk(); + + $response->assertJsonStructure([ + 'data' => $this->jsonStructureContactShort, + ]); + + $response->assertJsonFragment([ + 'id' => $contact->id, + 'is_me' => true, + ]); + } + + /** @test */ + public function it_sets_career() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('PUT', '/api/contacts/'.$contact->id.'/work', [ + 'job' => 'Astronaut', + 'company' => 'NASA', + ]); + + $response->assertStatus(200); + $response->assertJsonFragment([ + 'career' => [ + 'job' => 'Astronaut', + 'company' => 'NASA', + ], + ]); + + $this->assertDatabaseHas('contacts', [ + 'account_id' => $user->account_id, + 'id' => $contact->id, + 'job' => 'Astronaut', + 'company' => 'NASA', + ]); + + $response = $this->json('PUT', '/api/contacts/'.$contact->id.'/work', [ + 'job' => 'Mom', + 'company' => null, + ]); + + $response->assertStatus(200); + $response->assertJsonFragment([ + 'career' => [ + 'job' => 'Mom', + 'company' => null, + ], + ]); + + $this->assertDatabaseHas('contacts', [ + 'account_id' => $user->account_id, + 'id' => $contact->id, + 'job' => 'Mom', + 'company' => null, + ]); + } + + /** @test */ + public function it_get_an_error_when_set_career_with_wrong_params() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create(); + + $response = $this->json('PUT', '/api/contacts/'.$contact->id.'/work', [ + 'job' => 'xx', + 'company' => 'xx', + ]); + $this->expectNotFound($response); + } + + /** @test */ + public function it_get_an_error_when_set_career_on_partial_contact() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + 'is_partial' => true, + ]); + + $response = $this->json('PUT', '/api/contacts/'.$contact->id.'/work', [ + ]); + $this->expectDataError($response, [ + 'The contact can\'t be a partial contact', + ]); + } + + /** @test */ + public function it_sets_food_preferences() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('PUT', '/api/contacts/'.$contact->id.'/food', [ + 'food_preferences' => 'Pas de laitages, le lait c\'est le mal', + ]); + + $response->assertStatus(200); + $response->assertJsonFragment([ + 'food_preferences' => 'Pas de laitages, le lait c\'est le mal', + ]); + + $this->assertDatabaseHas('contacts', [ + 'account_id' => $user->account_id, + 'id' => $contact->id, + 'food_preferences' => 'Pas de laitages, le lait c\'est le mal', + ]); + + $response = $this->json('PUT', '/api/contacts/'.$contact->id.'/food', [ + 'food_preferences' => null, + ]); + + $response->assertStatus(200); + $response->assertJsonFragment([ + 'food_preferences' => null, + ]); + + $this->assertDatabaseHas('contacts', [ + 'account_id' => $user->account_id, + 'id' => $contact->id, + 'food_preferences' => null, + ]); + } + + /** @test */ + public function it_get_an_error_when_set_food_preferences_with_wrong_params() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create(); + + $response = $this->json('PUT', '/api/contacts/'.$contact->id.'/food'); + $this->expectNotFound($response); + } + + /** @test */ + public function it_get_an_error_when_set_food_preferences_on_partial_contact() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + 'is_partial' => true, + ]); + + $response = $this->json('PUT', '/api/contacts/'.$contact->id.'/food', [ + ]); + $this->expectDataError($response, [ + 'The contact can\'t be a partial contact', + ]); + } + + /** @test */ + public function it_sets_first_met() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('PUT', '/api/contacts/'.$contact->id.'/introduction', [ + 'is_date_known' => true, + 'year' => 2006, + 'month' => 1, + 'day' => 2, + ]); + + $response->assertStatus(200); + + $response->assertJsonFragment([ + 'first_met_date' => [ + 'date' => '2006-01-02T00:00:00Z', + 'is_age_based' => false, + 'is_year_unknown' => false, + ], + ]); + + $this->assertDatabaseHas('special_dates', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'id' => Contact::find($contact->id)->first_met_special_date_id, + 'is_age_based' => false, + 'is_year_unknown' => false, + 'date' => '2006-01-02', + ]); + } + + /** @test */ + public function it_sets_first_met_age() + { + Carbon::setTestNow(Carbon::create(2019, 12, 1, 7, 0, 0)); + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('PUT', '/api/contacts/'.$contact->id.'/introduction', [ + 'is_date_known' => true, + 'is_age_based' => true, + 'age' => 13, + ]); + + $response->assertStatus(200); + + $response->assertJsonFragment([ + 'first_met_date' => [ + 'date' => '2006-01-01T00:00:00Z', + 'is_age_based' => true, + 'is_year_unknown' => false, + ], + ]); + + $this->assertDatabaseHas('special_dates', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'id' => Contact::find($contact->id)->first_met_special_date_id, + 'is_age_based' => true, + 'is_year_unknown' => false, + 'date' => '2006-01-01', + ]); + } + + /** @test */ + public function it_sets_first_met_reminder() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('PUT', '/api/contacts/'.$contact->id.'/introduction', [ + 'is_date_known' => true, + 'year' => 2006, + 'month' => 1, + 'day' => 2, + 'add_reminder' => true, + ]); + + $response->assertStatus(200); + + $response->assertJsonFragment([ + 'first_met_date' => [ + 'date' => '2006-01-02T00:00:00Z', + 'is_age_based' => false, + 'is_year_unknown' => false, + ], + ]); + + $this->assertDatabaseHas('reminders', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'id' => Contact::find($contact->id)->first_met_reminder_id, + ]); + } + + /** @test */ + public function it_get_an_error_when_set_first_met_with_wrong_params() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create(); + + $response = $this->json('PUT', '/api/contacts/'.$contact->id.'/introduction', [ + 'is_date_known' => true, + 'year' => 2006, + 'month' => 1, + 'day' => 2, + 'add_reminder' => false, + ]); + $this->expectNotFound($response); + } + + /** @test */ + public function it_get_an_error_when_set_first_met_on_partial_contact() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + 'is_partial' => true, + ]); + + $response = $this->json('PUT', '/api/contacts/'.$contact->id.'/introduction', [ + 'is_date_known' => true, + 'year' => 2006, + 'month' => 1, + 'day' => 2, + 'add_reminder' => false, + ]); + $this->expectDataError($response, [ + 'The contact can\'t be a partial contact', + ]); + } +} diff --git a/tests/Api/Contact/ApiContactTagControllerTest.php b/tests/Api/Contact/ApiContactTagControllerTest.php new file mode 100644 index 0000000..75287df --- /dev/null +++ b/tests/Api/Contact/ApiContactTagControllerTest.php @@ -0,0 +1,289 @@ +signin(); + + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('POST', "/api/contacts/{$contact->id}/setTags"); + + $this->expectDataError($response, ['The tags field is required.']); + } + + /** @test */ + public function it_associates_tags_to_a_contact() + { + $user = $this->signin(); + + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('POST', "/api/contacts/{$contact->id}/setTags", [ + 'tags' => ['very-specific-tag-name', 'very-specific-tag-name-2'], + ]); + + $response->assertStatus(200); + $tagId1 = $response->json('data.tags.0.id'); + $tagId2 = $response->json('data.tags.1.id'); + + $response->assertJsonFragment([ + 'object' => 'tag', + 'id' => $tagId1, + 'name' => 'very-specific-tag-name', + ]); + + $response->assertJsonFragment([ + 'object' => 'tag', + 'id' => $tagId2, + 'name' => 'very-specific-tag-name-2', + ]); + + $this->assertDatabaseHas('contact_tag', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'tag_id' => $tagId1, + ]); + + $this->assertDatabaseHas('contact_tag', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'tag_id' => $tagId2, + ]); + } + + /** @test */ + public function tags_ignore_empty_tags() + { + $user = $this->signin(); + + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('POST', "/api/contacts/{$contact->id}/setTags", [ + 'tags' => [ + 'very-specific-tag-name', + null, + 'very-specific-tag-name-2', + ], + ]); + + $response->assertStatus(200); + $tagId1 = $response->json('data.tags.0.id'); + $tagId2 = $response->json('data.tags.1.id'); + + $response->assertJsonFragment([ + 'object' => 'tag', + 'id' => $tagId1, + 'name' => 'very-specific-tag-name', + ]); + + $response->assertJsonFragment([ + 'object' => 'tag', + 'id' => $tagId2, + 'name' => 'very-specific-tag-name-2', + ]); + + $this->assertDatabaseHas('contact_tag', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'tag_id' => $tagId1, + ]); + + $this->assertDatabaseHas('contact_tag', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'tag_id' => $tagId2, + ]); + } + + /** @test */ + public function a_list_of_tags_are_required_to_remove_a_tag_from_a_contact() + { + $user = $this->signin(); + + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('POST', "/api/contacts/{$contact->id}/unsetTag"); + + $this->expectDataError($response, ['The tags field is required.']); + } + + /** @test */ + public function it_removes_one_tag_from_a_contact() + { + $user = $this->signin(); + + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $tag = factory(Tag::class)->create([ + 'account_id' => $user->account_id, + 'name' => 'friend', + ]); + $tag2 = factory(Tag::class)->create([ + 'account_id' => $user->account_id, + 'name' => 'family', + ]); + + $contact->tags()->syncWithoutDetaching([ + $tag->id => [ + 'account_id' => $contact->account_id, + ], + $tag2->id => [ + 'account_id' => $contact->account_id, + ], + ]); + + $response = $this->json('POST', "/api/contacts/{$contact->id}/unsetTag", [ + 'tags' => [$tag->id], + ]); + + $response->assertStatus(200); + + $response->assertJsonFragment([ + 'id' => $contact->id, + 'name' => $tag2->name, + ]); + + $response->assertJsonMissing([ + 'name' => $tag->name, + ]); + + $this->assertDatabaseHas('contact_tag', [ + 'contact_id' => $contact->id, + 'tag_id' => $tag2->id, + 'account_id' => $user->account_id, + ]); + $this->assertDatabaseMissing('contact_tag', [ + 'contact_id' => $contact->id, + 'tag_id' => $tag->id, + 'account_id' => $user->account_id, + ]); + } + + /** @test */ + public function it_removes_multiple_tags_from_a_contact() + { + $user = $this->signin(); + + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $tag = factory(Tag::class)->create([ + 'account_id' => $user->account_id, + 'name' => 'friend', + ]); + $tag2 = factory(Tag::class)->create([ + 'account_id' => $user->account_id, + 'name' => 'family', + ]); + $tag3 = factory(Tag::class)->create([ + 'account_id' => $user->account_id, + 'name' => 'work', + ]); + + $contact->tags()->syncWithoutDetaching([ + $tag->id => [ + 'account_id' => $contact->account_id, + ], + $tag2->id => [ + 'account_id' => $contact->account_id, + ], + $tag3->id => [ + 'account_id' => $contact->account_id, + ], + ]); + + $response = $this->json('POST', "/api/contacts/{$contact->id}/unsetTag", [ + 'tags' => [$tag->id, $tag2->id], + ]); + + $response->assertStatus(200); + + $response->assertJsonFragment([ + 'id' => $contact->id, + 'name' => $tag3->name, + ]); + + $response->assertJsonMissing([ + 'name' => $tag2->name, + ]); + $this->assertDatabaseMissing('contact_tag', [ + 'contact_id' => $contact->id, + 'account_id' => $user->account_id, + 'tag_id' => $tag->id, + ]); + $this->assertDatabaseMissing('contact_tag', [ + 'contact_id' => $contact->id, + 'account_id' => $user->account_id, + 'tag_id' => $tag2->id, + ]); + $this->assertDatabaseHas('contact_tag', [ + 'contact_id' => $contact->id, + 'account_id' => $user->account_id, + 'tag_id' => $tag3->id, + ]); + } + + /** @test */ + public function it_removes_all_tags_from_a_contact() + { + $user = $this->signin(); + + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $tag = factory(Tag::class)->create([ + 'account_id' => $user->account_id, + 'name' => 'friend', + ]); + $tag2 = factory(Tag::class)->create([ + 'account_id' => $user->account_id, + 'name' => 'family', + ]); + + $contact->tags()->syncWithoutDetaching([ + $tag->id => [ + 'account_id' => $contact->account_id, + ], + $tag2->id => [ + 'account_id' => $contact->account_id, + ], + ]); + + $response = $this->json('POST', "/api/contacts/{$contact->id}/unsetTags"); + + $response->assertStatus(200); + + $response->assertJsonMissing([ + 'name' => $tag2->name, + ]); + + $this->assertDatabaseMissing('contact_tag', [ + 'contact_id' => $contact->id, + 'account_id' => $user->account_id, + ]); + } +} diff --git a/tests/Api/Contact/ApiConversationControllerTest.php b/tests/Api/Contact/ApiConversationControllerTest.php new file mode 100644 index 0000000..1d66c95 --- /dev/null +++ b/tests/Api/Contact/ApiConversationControllerTest.php @@ -0,0 +1,208 @@ + [ + 'id', + ], + 'account' => [ + 'id', + ], + 'contact' => [ + 'id', + ], + 'created_at', + 'updated_at', + ]; + + private function createConversation(User $user): Conversation + { + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $contactFieldType = factory(ContactFieldType::class)->create([ + 'account_id' => $user->account_id, + ]); + + $conversation = factory(Conversation::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'contact_field_type_id' => $contactFieldType->id, + 'happened_at' => now(), + ]); + + return $conversation; + } + + /** @test */ + public function it_gets_a_list_of_conversations() + { + $user = $this->signin(); + + for ($i = 0; $i < 10; $i++) { + $this->createConversation($user); + } + + $response = $this->json('GET', '/api/conversations'); + + $response->assertStatus(200); + + $this->assertCount( + 10, + $response->decodeResponseJson()['data'] + ); + + $response->assertJsonFragment([ + 'total' => 10, + 'current_page' => 1, + ]); + + $response->assertJsonStructure([ + 'data' => [ + '*' => $this->jsonConversations, + ], + ]); + } + + /** @test */ + public function it_applies_the_limit_parameter_in_search() + { + $user = $this->signin(); + + for ($i = 0; $i < 10; $i++) { + $this->createConversation($user); + } + + $response = $this->json('GET', '/api/conversations?limit=1'); + + $response->assertJsonFragment([ + 'total' => 10, + 'current_page' => 1, + 'per_page' => 1, + 'last_page' => 10, + ]); + + $response = $this->json('GET', '/api/conversations?limit=2'); + + $response->assertJsonFragment([ + 'total' => 10, + 'current_page' => 1, + 'per_page' => 2, + 'last_page' => 5, + ]); + } + + /** @test */ + public function it_gets_a_conversation() + { + $user = $this->signin(); + + $conversation = $this->createConversation($user); + + $response = $this->json('GET', '/api/conversations/'.$conversation->id); + + $response->assertStatus(200); + + $response->assertJsonStructure([ + '*' => $this->jsonConversations, + ]); + } + + /** @test */ + public function it_gets_a_conversation_for_a_specific_contact() + { + $user = $this->signin(); + + $conversation = $this->createConversation($user); + + $response = $this->json('GET', '/api/contacts/'.$conversation['contact_id'].'/conversations'); + + $response->assertStatus(200); + + $response->assertJsonStructure([ + 'data' => [ + '*' => $this->jsonConversations, + ], + ]); + } + + /** @test */ + public function it_creates_a_conversation() + { + $user = $this->signin(); + + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $contactFieldType = factory(ContactFieldType::class)->create([ + 'account_id' => $contact->account_id, + ]); + + $response = $this->json('POST', '/api/conversations', [ + 'contact_id' => $contact->id, + 'happened_at' => '1989-02-02', + 'contact_field_type_id' => $contactFieldType->id, + ]); + + $response->assertStatus(201); + + $response->assertJsonStructure([ + 'data' => $this->jsonConversations, + ]); + } + + /** @test */ + public function it_updates_a_conversation() + { + $user = $this->signin(); + + $conversation = $this->createConversation($user); + $contactFieldType = factory(ContactFieldType::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('PUT', '/api/conversations/'.$conversation->id, [ + 'happened_at' => '1989-02-02', + 'contact_field_type_id' => $contactFieldType->id, + ]); + + $response->assertStatus(200); + + $response->assertJsonStructure([ + 'data' => $this->jsonConversations, + ]); + } + + /** @test */ + public function it_destroys_a_conversation() + { + $user = $this->signin(); + + $conversation = $this->createConversation($user); + + $response = $this->delete('/api/conversations/'.$conversation->id); + + $response->assertStatus(200); + + $response->assertJsonFragment([ + 'deleted' => true, + 'id' => $conversation->id, + ]); + } +} diff --git a/tests/Api/Contact/ApiDocumentControllerTest.php b/tests/Api/Contact/ApiDocumentControllerTest.php new file mode 100644 index 0000000..b116cb6 --- /dev/null +++ b/tests/Api/Contact/ApiDocumentControllerTest.php @@ -0,0 +1,256 @@ + [ + 'id', + ], + 'contact' => [ + 'id', + ], + 'created_at', + 'updated_at', + ]; + + private function createDocument(User $user): Document + { + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $document = factory(Document::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + ]); + + return $document; + } + + /** @test */ + public function it_gets_a_list_of_documents() + { + $user = $this->signin(); + + for ($i = 0; $i < 10; $i++) { + $this->createDocument($user); + } + + $response = $this->json('GET', '/api/documents'); + + $response->assertStatus(200); + + $this->assertCount( + 10, + $response->decodeResponseJson()['data'] + ); + + $response->assertJsonFragment([ + 'total' => 10, + 'current_page' => 1, + ]); + + $response->assertJsonStructure([ + 'data' => [ + '*' => $this->jsonDocuments, + ], + ]); + } + + /** @test */ + public function it_applies_the_limit_parameter_in_search() + { + $user = $this->signin(); + + for ($i = 0; $i < 10; $i++) { + $this->createDocument($user); + } + + $response = $this->json('GET', '/api/documents?limit=1'); + + $response->assertJsonFragment([ + 'total' => 10, + 'current_page' => 1, + 'per_page' => 1, + 'last_page' => 10, + ]); + + $response = $this->json('GET', '/api/documents?limit=2'); + + $response->assertJsonFragment([ + 'total' => 10, + 'current_page' => 1, + 'per_page' => 2, + 'last_page' => 5, + ]); + } + + /** @test */ + public function it_gets_a_document() + { + $user = $this->signin(); + + $document = $this->createDocument($user); + + $response = $this->json('GET', '/api/documents/'.$document->id); + + $response->assertStatus(200); + + $response->assertJsonStructure([ + '*' => $this->jsonDocuments, + ]); + } + + /** @test */ + public function document_show_gets_an_error_if_document_is_not_linked_to_account() + { + $user = $this->signin(); + + $contact = factory(Contact::class)->create(); + $document = factory(Document::class)->create([ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + ]); + + $response = $this->json('GET', '/api/documents/'.$document->id); + + $this->expectNotFound($response); + } + + /** @test */ + public function it_gets_a_document_for_a_specific_contact() + { + $user = $this->signin(); + + $document = $this->createDocument($user); + + $response = $this->json('GET', '/api/contacts/'.$document['contact_id'].'/documents'); + + $response->assertStatus(200); + + $response->assertJsonStructure([ + 'data' => [ + '*' => $this->jsonDocuments, + ], + ]); + + $response->assertJsonFragment([ + 'total' => 1, + 'current_page' => 1, + 'per_page' => 15, + 'last_page' => 1, + ]); + } + + /** @test */ + public function it_store_a_document_for_a_specific_contact() + { + Storage::fake(); + + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('POST', '/api/documents', [ + 'contact_id' => $contact->id, + 'document' => UploadedFile::fake()->image('test.pdf'), + ]); + + $response->assertStatus(201); + + $response->assertJsonStructure([ + 'data' => $this->jsonDocuments, + ]); + + $this->assertDatabaseHas('documents', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'original_filename' => 'test.pdf', + ]); + + Storage::disk('public')->assertExists($response->json('data.new_filename')); + } + + /** @test */ + public function document_store_gets_an_error_if_fields_are_missing() + { + $user = $this->signin(); + + $response = $this->json('POST', '/api/documents', [ + ]); + + $this->expectDataError($response, [ + 'The contact id field is required.', + ]); + } + + /** @test */ + public function document_store_gets_an_error_if_contact_is_not_linked_to_user() + { + $user = $this->signin(); + + $contact = factory(Contact::class)->create(); + + $response = $this->json('POST', '/api/documents', [ + 'contact_id' => $contact->id, + 'document' => UploadedFile::fake()->image('test.pdf'), + ]); + + $this->expectNotFound($response); + } + + /** @test */ + public function it_destroy_a_document() + { + $user = $this->signin(); + + $document = $this->createDocument($user); + + $response = $this->json('DELETE', '/api/documents/'.$document->id); + + $response->assertStatus(200); + + $response->assertJsonFragment([ + 'deleted' => true, + 'id' => $document->id, + ]); + } + + /** @test */ + public function document_destroy_gets_an_error_if_document_is_not_linked_to_user() + { + $user = $this->signin(); + + $contact = factory(Contact::class)->create(); + $document = factory(Document::class)->create([ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + ]); + + $response = $this->json('DELETE', '/api/documents/'.$document->id); + + $this->expectNotFound($response); + } +} diff --git a/tests/Api/Contact/ApiLifeEventControllerTest.php b/tests/Api/Contact/ApiLifeEventControllerTest.php new file mode 100644 index 0000000..dd24f33 --- /dev/null +++ b/tests/Api/Contact/ApiLifeEventControllerTest.php @@ -0,0 +1,340 @@ + [ + 'id', + ], + 'account' => [ + 'id', + ], + 'contact' => [ + 'id', + ], + 'created_at', + 'updated_at', + ]; + + private function createLifeEvent(User $user): LifeEvent + { + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $lifeEventType = factory(LifeEventType::class)->create([ + 'account_id' => $user->account_id, + ]); + + $lifeEvent = factory(LifeEvent::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'life_event_type_id' => $lifeEventType->id, + 'happened_at' => now(), + 'name' => 'This is a text', + 'note' => 'This is a text', + ]); + + return $lifeEvent; + } + + /** @test */ + public function it_gets_a_list_of_life_events() + { + $user = $this->signin(); + + for ($i = 0; $i < 10; $i++) { + $this->createLifeEvent($user); + } + + $response = $this->json('GET', '/api/lifeevents'); + + $response->assertStatus(200); + + $this->assertCount( + 10, + $response->decodeResponseJson()['data'] + ); + + $response->assertJsonFragment([ + 'total' => 10, + 'current_page' => 1, + ]); + + $response->assertJsonStructure([ + 'data' => [ + '*' => $this->jsonLifeEvents, + ], + ]); + } + + /** @test */ + public function it_applies_the_limit_parameter_in_search() + { + $user = $this->signin(); + + for ($i = 0; $i < 10; $i++) { + $this->createLifeEvent($user); + } + + $response = $this->json('GET', '/api/lifeevents?limit=1'); + + $response->assertJsonFragment([ + 'total' => 10, + 'current_page' => 1, + 'per_page' => 1, + 'last_page' => 10, + ]); + + $response = $this->json('GET', '/api/lifeevents?limit=2'); + + $response->assertJsonFragment([ + 'total' => 10, + 'current_page' => 1, + 'per_page' => 2, + 'last_page' => 5, + ]); + } + + /** @test */ + public function it_gets_a_life_event() + { + $user = $this->signin(); + + $lifeEvent = $this->createLifeEvent($user); + + $response = $this->json('GET', '/api/lifeevents/'.$lifeEvent->id); + + $response->assertStatus(200); + + $response->assertJsonStructure([ + '*' => $this->jsonLifeEvents, + ]); + } + + /** @test */ + public function getting_a_life_event_doesnt_work_if_life_event_doesnt_exist() + { + $user = $this->signin(); + + $response = $this->json('GET', '/api/lifeevents/329029093809'); + + $this->expectNotFound($response); + } + + /** @test */ + public function it_creates_a_life_event() + { + $user = $this->signin(); + + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $lifeEventType = factory(LifeEventType::class)->create([ + 'account_id' => $contact->account_id, + ]); + + $response = $this->json('POST', '/api/lifeevents', [ + 'contact_id' => $contact->id, + 'life_event_type_id' => $lifeEventType->id, + 'happened_at' => '1989-02-02', + 'name' => 'This is a text', + 'note' => 'This is a text', + 'has_reminder' => false, + 'happened_at_month_unknown' => false, + 'happened_at_day_unknown' => false, + ]); + + $response->assertStatus(200); + + $response->assertJsonStructure([ + 'data' => $this->jsonLifeEvents, + ]); + } + + /** @test */ + public function creating_a_life_event_doesnt_work_if_ids_are_not_found() + { + $user = $this->signin(); + + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('POST', '/api/lifeevents', [ + 'contact_id' => $contact->id, + 'life_event_type_id' => 0, + 'happened_at' => '1989-02-02', + 'name' => 'This is a text', + 'note' => 'This is a text', + 'has_reminder' => false, + 'happened_at_month_unknown' => false, + 'happened_at_day_unknown' => false, + ]); + + $this->expectNotFound($response); + + $lifeEventType = factory(LifeEventType::class)->create([ + 'account_id' => $contact->account_id, + ]); + + $response = $this->json('POST', '/api/lifeevents', [ + 'contact_id' => 0, + 'life_event_type_id' => $lifeEventType->id, + 'happened_at' => '1989-02-02', + 'name' => 'This is a text', + 'note' => 'This is a text', + 'has_reminder' => false, + 'happened_at_month_unknown' => false, + 'happened_at_day_unknown' => false, + ]); + + $this->expectNotFound($response); + } + + /** @test */ + public function creating_a_life_event_doesnt_work_if_parameters_are_not_right() + { + $user = $this->signin(); + + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $lifeEventType = factory(LifeEventType::class)->create([ + 'account_id' => $contact->account_id, + ]); + + $response = $this->json('POST', '/api/lifeevents', [ + 'contact_id' => $contact->id, + 'life_event_type_id' => $lifeEventType->id, + 'name' => 'This is a text', + 'note' => 'This is a text', + ]); + + $this->expectDataError($response, [ + 'The happened at field is required.', + 'The has reminder field is required.', + 'The happened at month unknown field is required.', + 'The happened at day unknown field is required.', + ]); + } + + /** @test */ + public function it_updates_a_life_event() + { + $user = $this->signin(); + + $lifeEvent = $this->createLifeEvent($user); + $lifeEventType = factory(LifeEventType::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('PUT', '/api/lifeevents/'.$lifeEvent->id, [ + 'happened_at' => '1989-02-02', + 'life_event_type_id' => $lifeEventType->id, + 'name' => 'This is a text', + 'note' => 'This is a text', + ]); + + $response->assertStatus(200); + + $response->assertJsonStructure([ + 'data' => $this->jsonLifeEvents, + ]); + } + + /** @test */ + public function updating_a_life_event_doesnt_work_if_ids_are_not_found() + { + $user = $this->signin(); + + $lifeEvent = $this->createLifeEvent($user); + $lifeEventType = factory(LifeEventType::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('PUT', '/api/lifeevents/23929390', [ + 'happened_at' => '1989-02-02', + 'life_event_type_id' => $lifeEventType->id, + 'name' => 'This is a text', + 'note' => 'This is a text', + ]); + + $this->expectNotFound($response); + + $response = $this->json('PUT', '/api/lifeevents/'.$lifeEvent->id, [ + 'happened_at' => '1989-02-02', + 'life_event_type_id' => 3283028, + 'name' => 'This is a text', + 'note' => 'This is a text', + ]); + + $this->expectNotFound($response); + } + + /** @test */ + public function updating_a_life_event_doesnt_work_if_parameters_are_not_right() + { + $user = $this->signin(); + + $lifeEvent = $this->createLifeEvent($user); + $lifeEventType = factory(LifeEventType::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('PUT', '/api/lifeevents/'.$lifeEvent->id, [ + 'life_event_type_id' => $lifeEventType->id, + 'name' => 'This is a text', + 'note' => 'This is a text', + ]); + + $this->expectDataError($response, [ + 'The happened at field is required.', + ]); + } + + /** @test */ + public function it_destroys_a_life_event() + { + $user = $this->signin(); + + $lifeEvent = $this->createLifeEvent($user); + + $response = $this->delete('/api/lifeevents/'.$lifeEvent->id); + + $response->assertStatus(200); + + $response->assertJsonFragment([ + 'deleted' => true, + 'id' => $lifeEvent->id, + ]); + } + + /** @test */ + public function deleting_a_life_event_doesnt_work_if_ids_are_not_found() + { + $user = $this->signin(); + + $lifeEvent = $this->createLifeEvent($user); + + $response = $this->delete('/api/lifeevents/39230990'); + + $this->expectNotFound($response); + } +} diff --git a/tests/Api/Contact/ApiMeControllerTest.php b/tests/Api/Contact/ApiMeControllerTest.php new file mode 100644 index 0000000..6a72560 --- /dev/null +++ b/tests/Api/Contact/ApiMeControllerTest.php @@ -0,0 +1,73 @@ +signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('POST', '/api/me/contact', ['contact_id' => $contact->id]); + + $response->assertStatus(200); + + $this->assertDatabaseHas('users', [ + 'account_id' => $user->account_id, + 'me_contact_id' => $contact->id, + ]); + } + + /** @test */ + public function it_throws_an_error_if_wrong_account_on_sets_me_contact() + { + $this->signin(); + $contact = factory(Contact::class)->create(); + + $response = $this->json('POST', '/api/me/contact', ['contact_id' => $contact->id]); + + $this->expectNotFound($response); + } + + /** @test */ + public function it_throws_an_error_if_account_not_exists_on_sets_me_contact() + { + $this->signin(); + + $response = $this->json('POST', '/api/me/contact', ['contact_id' => 0]); + + $this->expectDataError($response, [ + 'The selected contact id is invalid.', + ]); + } + + /** @test */ + public function it_removes_me_contact() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $user->me_contact_id = $contact->id; + $user->save(); + + $response = $this->json('DELETE', '/api/me/contact'); + + $response->assertStatus(200); + + $this->assertDatabaseHas('users', [ + 'account_id' => $user->account_id, + 'me_contact_id' => null, + ]); + } +} diff --git a/tests/Api/Contact/ApiMessageControllerTest.php b/tests/Api/Contact/ApiMessageControllerTest.php new file mode 100644 index 0000000..f0d4e8b --- /dev/null +++ b/tests/Api/Contact/ApiMessageControllerTest.php @@ -0,0 +1,127 @@ + [ + [ + 'id', + ], + ], + 'contact_field_type' => [ + 'id', + ], + 'account' => [ + 'id', + ], + 'contact' => [ + 'id', + ], + 'created_at', + 'updated_at', + ]; + + private function createConversation(User $user): Conversation + { + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $contactFieldType = factory(ContactFieldType::class)->create([ + 'account_id' => $user->account_id, + ]); + + $conversation = factory(Conversation::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'contact_field_type_id' => $contactFieldType->id, + 'happened_at' => now(), + ]); + + return $conversation; + } + + private function addMessage(Conversation $conversation): Message + { + $message = factory(Message::class)->create([ + 'account_id' => $conversation->account_id, + 'contact_id' => $conversation->contact_id, + 'conversation_id' => $conversation->id, + ]); + + return $message; + } + + /** @test */ + public function it_adds_a_message_to_a_conversation() + { + $user = $this->signin(); + + $conversation = $this->createConversation($user); + + $response = $this->json('POST', '/api/conversations/'.$conversation->id.'/messages', [ + 'written_at' => '1998-02-02', + 'written_by_me' => true, + 'content' => 'lorem ipsum', + ]); + + $response->assertStatus(200); + + $response->assertJsonStructure([ + 'data' => $this->jsonConversations, + ]); + } + + /** @test */ + public function it_updates_a_message() + { + $user = $this->signin(); + + $conversation = $this->createConversation($user); + $message = $this->addMessage($conversation); + + $response = $this->json('PUT', '/api/conversations/'.$conversation->id.'/messages/'.$message->id, [ + 'written_at' => '1989-02-02', + 'written_by_me' => true, + 'content' => 'lorem ipsum', + ]); + + $response->assertStatus(200); + + $response->assertJsonStructure([ + 'data' => $this->jsonConversations, + ]); + } + + /** @test */ + public function it_destroys_a_message() + { + $user = $this->signin(); + + $conversation = $this->createConversation($user); + $message = $this->addMessage($conversation); + + $response = $this->delete('/api/conversations/'.$conversation->id.'/messages/'.$message->id); + + $response->assertStatus(200); + + $response->assertJsonFragment([ + 'deleted' => true, + 'id' => $message->id, + ]); + } +} diff --git a/tests/Api/Contact/ApiOccupationControllerTest.php b/tests/Api/Contact/ApiOccupationControllerTest.php new file mode 100644 index 0000000..6d16691 --- /dev/null +++ b/tests/Api/Contact/ApiOccupationControllerTest.php @@ -0,0 +1,228 @@ + [ + 'id', + ], + 'account' => [ + 'id', + ], + 'created_at', + 'updated_at', + ]; + + public function test_it_gets_a_list_of_occupations() + { + $user = $this->signin(); + + factory(Occupation::class, 3)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('GET', '/api/occupations'); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => ['*' => $this->jsonOccupation], + ]); + } + + public function test_it_applies_the_limit_parameter_in_search() + { + $user = $this->signin(); + + factory(Occupation::class, 10)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('GET', '/api/occupations?limit=1'); + + $response->assertJsonFragment([ + 'total' => 10, + 'current_page' => 1, + 'per_page' => 1, + 'last_page' => 10, + ]); + + $response = $this->json('GET', '/api/occupations?limit=2'); + + $response->assertJsonFragment([ + 'total' => 10, + 'current_page' => 1, + 'per_page' => 2, + 'last_page' => 5, + ]); + } + + public function test_it_gets_one_occupation() + { + $user = $this->signin(); + + $occupation = factory(Occupation::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('get', '/api/occupations/'.$occupation->id); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => $this->jsonOccupation, + ]); + $response->assertJsonFragment([ + 'object' => 'occupation', + 'id' => $occupation->id, + ]); + } + + public function test_it_cant_get_a_occupation_with_unexistent_id() + { + $user = $this->signin(); + + $response = $this->json('get', '/api/occupations/0'); + + $this->expectNotFound($response); + } + + public function test_it_creates_a_occupation() + { + $user = $this->signin(); + + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $company = factory(Company::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('post', '/api/occupations', [ + 'contact_id' => $contact->id, + 'company_id' => $company->id, + 'title' => 'Waiter', + ]); + + $response->assertStatus(201); + $response->assertJsonStructure([ + 'data' => $this->jsonOccupation, + ]); + + $occupationId = $response->json('data.id'); + + $response->assertJsonFragment([ + 'object' => 'occupation', + 'id' => $occupationId, + ]); + + $this->assertDatabaseHas('occupations', [ + 'account_id' => $user->account_id, + 'id' => $occupationId, + 'title' => 'Waiter', + ]); + } + + public function test_it_updates_a_occupation() + { + $user = $this->signin(); + $occupation = factory(Occupation::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('put', '/api/occupations/'.$occupation->id, [ + 'contact_id' => $occupation->contact_id, + 'company_id' => $occupation->company_id, + 'title' => 'Commissaire', + 'salary' => null, + ]); + + $response->assertStatus(200); + + $response->assertJsonStructure([ + 'data' => $this->jsonOccupation, + ]); + + $occupationId = $response->json('data.id'); + + $this->assertEquals($occupation->id, $occupationId); + + $response->assertJsonFragment([ + 'object' => 'occupation', + 'id' => $occupationId, + ]); + + $this->assertDatabaseHas('occupations', [ + 'account_id' => $user->account_id, + 'id' => $occupationId, + 'title' => 'Commissaire', + 'salary' => null, + ]); + } + + public function test_it_cant_update_a_occupation_if_account_is_not_linked_to_occupation() + { + $user = $this->signin(); + + $account = factory(Account::class)->create([]); + $occupation = factory(Occupation::class)->create([ + 'account_id' => $account->id, + ]); + + $response = $this->json('put', '/api/occupations/'.$occupation->id, [ + 'contact_id' => $occupation->contact_id, + 'company_id' => $occupation->company_id, + 'title' => 'Commissaire', + 'salary' => null, + ]); + + $this->expectNotFound($response); + } + + public function test_it_deletes_a_occupation() + { + $user = $this->signin(); + + $occupation = factory(Occupation::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('delete', '/api/occupations/'.$occupation->id); + + $response->assertStatus(200); + + $this->assertdatabasemissing('occupations', [ + 'account_id' => $user->account_id, + 'id' => $occupation->id, + ]); + } + + public function test_it_cant_delete_a_occupation_if_occupation_doesnt_exist() + { + $user = $this->signin(); + + $response = $this->json('delete', '/api/occupations/0'); + + $this->expectDataError($response, [ + 'The selected occupation id is invalid.', + ]); + } +} diff --git a/tests/Api/Contact/ApiPhotoControllerTest.php b/tests/Api/Contact/ApiPhotoControllerTest.php new file mode 100644 index 0000000..0133f4e --- /dev/null +++ b/tests/Api/Contact/ApiPhotoControllerTest.php @@ -0,0 +1,294 @@ + [ + 'id', + ], + 'contact' => [ + 'id', + ], + 'created_at', + 'updated_at', + ]; + + private function createPhoto(User $user): Photo + { + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $photo = factory(Photo::class)->create([ + 'account_id' => $user->account_id, + ]); + UploadedFile::fake()->image('file.jpg')->storeAs('', 'file.jpg'); + + $contact->photos()->syncWithoutDetaching([$photo->id]); + + return $photo; + } + + /** @test */ + public function it_gets_a_list_of_photos() + { + $user = $this->signin(); + + for ($i = 0; $i < 10; $i++) { + $this->createPhoto($user); + } + + $response = $this->json('GET', '/api/photos'); + + $response->assertStatus(200); + + $this->assertCount( + 10, + $response->decodeResponseJson()['data'] + ); + + $response->assertJsonFragment([ + 'total' => 10, + 'current_page' => 1, + ]); + + $response->assertJsonStructure([ + 'data' => [ + '*' => $this->jsonDatas, + ], + ]); + } + + /** @test */ + public function it_applies_the_limit_parameter_in_search() + { + $user = $this->signin(); + + for ($i = 0; $i < 10; $i++) { + $this->createPhoto($user); + } + + $response = $this->json('GET', '/api/photos?limit=1'); + + $response->assertJsonFragment([ + 'total' => 10, + 'current_page' => 1, + 'per_page' => 1, + 'last_page' => 10, + ]); + + $response = $this->json('GET', '/api/photos?limit=2'); + + $response->assertJsonFragment([ + 'total' => 10, + 'current_page' => 1, + 'per_page' => 2, + 'last_page' => 5, + ]); + } + + /** @test */ + public function it_gets_a_photo() + { + $user = $this->signin(); + + $photo = $this->createPhoto($user); + + $response = $this->json('GET', '/api/photos/'.$photo->id); + + $response->assertStatus(200); + + $response->assertJsonStructure([ + '*' => $this->jsonDatas, + ]); + } + + /** @test */ + public function photo_show_gets_an_error_if_photo_is_not_linked_to_account() + { + $user = $this->signin(); + + $contact = factory(Contact::class)->create(); + $photo = factory(Photo::class)->create([ + 'account_id' => $contact->account_id, + ]); + + $contact->photos()->syncWithoutDetaching([$photo->id]); + + $response = $this->json('GET', '/api/photos/'.$photo->id); + + $this->expectNotFound($response); + } + + /** @test */ + public function it_gets_a_photo_for_a_specific_contact() + { + $user = $this->signin(); + + $photo = $this->createPhoto($user); + + $response = $this->json('GET', '/api/contacts/'.$photo->contact()->id.'/photos'); + + $response->assertStatus(200); + + $response->assertJsonStructure([ + 'data' => [ + '*' => $this->jsonDatas, + ], + ]); + + $response->assertJsonFragment([ + 'total' => 1, + 'current_page' => 1, + 'per_page' => 15, + 'last_page' => 1, + ]); + } + + /** @test */ + public function it_store_a_photo_for_a_specific_contact() + { + Storage::fake(); + + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('POST', '/api/photos', [ + 'contact_id' => $contact->id, + 'photo' => UploadedFile::fake()->image('test.jpg'), + ]); + + $response->assertStatus(201); + + $response->assertJsonStructure([ + 'data' => $this->jsonDatas, + ]); + + $this->assertDatabaseHas('photos', [ + 'account_id' => $user->account_id, + 'original_filename' => 'test.jpg', + ]); + + Storage::disk('public')->assertExists($response->json('data.new_filename')); + } + + /** @test */ + public function photo_store_gets_an_error_if_fields_are_missing() + { + $user = $this->signin(); + + $response = $this->json('POST', '/api/photos', [ + ]); + + $this->expectDataError($response, [ + 'The contact id field is required.', + ]); + } + + /** @test */ + public function photo_store_gets_an_error_if_contact_is_not_linked_to_user() + { + $user = $this->signin(); + + $contact = factory(Contact::class)->create(); + + $response = $this->json('POST', '/api/photos', [ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'photo' => UploadedFile::fake()->image('test.jpg'), + ]); + + $this->expectNotFound($response); + } + + /** @test */ + public function it_destroy_a_photo() + { + $user = $this->signin(); + + $photo = $this->createPhoto($user); + + $response = $this->json('DELETE', '/api/photos/'.$photo->id); + + $response->assertStatus(200); + + $response->assertJsonFragment([ + 'deleted' => true, + 'id' => $photo->id, + ]); + + $this->assertDatabaseMissing('photos', [ + 'id' => $photo->id, + 'account_id' => $user->account_id, + ]); + } + + /** @test */ + public function photo_destroy_gets_an_error_if_photo_is_not_linked_to_account() + { + $user = $this->signin(); + + $contact = factory(Contact::class)->create(); + $photo = factory(Photo::class)->create([ + 'account_id' => $contact->account_id, + ]); + + $contact->photos()->syncWithoutDetaching([$photo->id]); + + $response = $this->json('DELETE', '/api/photos/'.$photo->id); + + $this->expectNotFound($response); + } + + /** @test */ + public function it_store_and_destroy_a_photo() + { + Storage::fake(); + + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('POST', '/api/photos/', [ + 'contact_id' => $contact->id, + 'photo' => UploadedFile::fake()->image('test.jpg'), + ]); + + $photo = $contact->photos->first(); + + Storage::disk('public')->assertExists($photo->new_filename); + + $response = $this->json('DELETE', '/api/photos/'.$photo->id); + + $response->assertStatus(200); + + $response->assertJsonFragment([ + 'deleted' => true, + 'id' => $photo->id, + ]); + + Storage::disk('public')->assertMissing($photo->new_filename); + } +} diff --git a/tests/Api/ContactField/ApiContactFieldControllerTest.php b/tests/Api/ContactField/ApiContactFieldControllerTest.php new file mode 100644 index 0000000..22c69b5 --- /dev/null +++ b/tests/Api/ContactField/ApiContactFieldControllerTest.php @@ -0,0 +1,311 @@ + [ + 'id', + ], + 'contact', + 'created_at', + 'updated_at', + ]; + + /** @test */ + public function contact_fields_get_contact_all() + { + $user = $this->signin(); + $contact1 = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $contactField1 = factory(ContactField::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact1->id, + ]); + $contact2 = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $contactField2 = factory(ContactField::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact2->id, + ]); + + $response = $this->json('GET', '/api/contacts/'.$contact1->id.'/contactfields'); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => ['*' => $this->jsonContactField], + ]); + $response->assertJsonFragment([ + 'object' => 'contactfield', + 'id' => $contactField1->id, + ]); + $response->assertJsonMissingExact([ + 'object' => 'contactfield', + 'id' => $contactField2->id, + ]); + } + + /** @test */ + public function contact_fields_get_contact_all_error() + { + $user = $this->signin(); + + $response = $this->json('GET', '/api/contacts/0/contactfields'); + + $this->expectNotFound($response); + } + + /** @test */ + public function contact_fields_get_one() + { + $user = $this->signin(); + $contact1 = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $contactField1 = factory(ContactField::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact1->id, + ]); + $contactField2 = factory(ContactField::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact1->id, + ]); + + $response = $this->json('GET', '/api/contactfields/'.$contactField1->id); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => $this->jsonContactField, + ]); + $response->assertJsonFragment([ + 'object' => 'contactfield', + 'id' => $contactField1->id, + ]); + $response->assertJsonMissingExact([ + 'object' => 'contactfield', + 'id' => $contactField2->id, + ]); + } + + /** @test */ + public function contact_fields_get_one_error() + { + $user = $this->signin(); + + $response = $this->json('GET', '/api/contactfields/0'); + + $this->expectNotFound($response); + } + + /** @test */ + public function contact_fields_create() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $field = factory(ContactFieldType::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('POST', '/api/contactfields', [ + 'contact_id' => $contact->id, + 'contact_field_type_id' => $field->id, + 'data' => 'ok', + ]); + + $response->assertStatus(201); + $response->assertJsonStructure([ + 'data' => $this->jsonContactField, + ]); + $contactField_id = $response->json('data.id'); + $response->assertJsonFragment([ + 'object' => 'contactfield', + 'id' => $contactField_id, + ]); + + $this->assertGreaterThan(0, $contactField_id); + $this->assertDatabaseHas('contact_fields', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'id' => $contactField_id, + 'contact_field_type_id' => $field->id, + 'data' => 'ok', + ]); + } + + /** @test */ + public function contact_fields_create_error() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('POST', '/api/contactfields', [ + 'contact_id' => $contact->id, + ]); + + $this->expectDataError($response, [ + 'The contact field type id field is required.', + 'The data field is required.', + ]); + } + + /** @test */ + public function contact_fields_create_error_bad_account() + { + $user = $this->signin(); + + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + $field = factory(ContactFieldType::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('POST', '/api/contactfields', [ + 'contact_id' => $contact->id, + 'contact_field_type_id' => $field->id, + 'data' => 'ok', + ]); + + $this->expectNotFound($response); + } + + /** @test */ + public function contact_fields_update() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $contactField = factory(ContactField::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + ]); + + $response = $this->json('PUT', '/api/contactfields/'.$contactField->id, [ + 'contact_id' => $contact->id, + 'contact_field_type_id' => $contactField->contact_field_type_id, + 'data' => 'ok', + ]); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => $this->jsonContactField, + ]); + $contactField_id = $response->json('data.id'); + $this->assertEquals($contactField->id, $contactField_id); + $response->assertJsonFragment([ + 'object' => 'contactfield', + 'id' => $contactField_id, + ]); + + $this->assertGreaterThan(0, $contactField_id); + $this->assertDatabaseHas('contact_fields', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'id' => $contactField_id, + 'contact_field_type_id' => $contactField->contact_field_type_id, + 'data' => 'ok', + ]); + } + + /** @test */ + public function contact_fields_update_error() + { + $user = $this->signin(); + $contactField = factory(ContactField::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('PUT', '/api/contactfields/'.$contactField->id, [ + 'contact_id' => $contactField->contact_id, + ]); + + $this->expectDataError($response, [ + 'The contact field type id field is required.', + 'The data field is required.', + ]); + } + + /** @test */ + public function contact_fields_update_error_bad_account() + { + $user = $this->signin(); + + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + $contactField = factory(ContactField::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + ]); + + $response = $this->json('PUT', '/api/contactfields/'.$contactField->id, [ + 'contact_id' => $contact->id, + 'contact_field_type_id' => $contactField->contact_field_type_id, + 'data' => 'ok', + ]); + + $this->expectNotFound($response); + } + + /** @test */ + public function contact_fields_delete() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $contactField = factory(ContactField::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + ]); + $this->assertDatabaseHas('contact_fields', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'id' => $contactField->id, + ]); + + $response = $this->json('DELETE', '/api/contactfields/'.$contactField->id); + + $response->assertStatus(200); + $this->assertDatabaseMissing('contact_fields', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'id' => $contactField->id, + ]); + } + + /** @test */ + public function contact_fields_delete_error() + { + $user = $this->signin(); + + $response = $this->json('DELETE', '/api/contactfields/0'); + + $this->expectDataError($response, [ + 'The selected contact field id is invalid.', + ]); + } +} diff --git a/tests/Api/ContactField/ApiContactFieldTypeControllerTest.php b/tests/Api/ContactField/ApiContactFieldTypeControllerTest.php new file mode 100644 index 0000000..90db168 --- /dev/null +++ b/tests/Api/ContactField/ApiContactFieldTypeControllerTest.php @@ -0,0 +1,223 @@ + [ + 'id', + ], + 'created_at', + 'updated_at', + ]; + + /** @test */ + public function contact_field_type_get_one() + { + $user = $this->signin(); + $contactFieldType1 = factory(ContactFieldType::class)->create([ + 'account_id' => $user->account_id, + ]); + $contactFieldType2 = factory(ContactFieldType::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('GET', '/api/contactfieldtypes/'.$contactFieldType1->id); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => $this->jsonContactFieldType, + ]); + $response->assertJsonFragment([ + 'object' => 'contactfieldtype', + 'id' => $contactFieldType1->id, + ]); + $response->assertJsonMissingExact([ + 'object' => 'contactfieldtype', + 'id' => $contactFieldType2->id, + ]); + } + + /** @test */ + public function contact_field_type_get_one_error() + { + $user = $this->signin(); + + $response = $this->json('GET', '/api/contactfieldtypes/0'); + + $this->expectNotFound($response); + } + + /** @test */ + public function contact_field_type_create() + { + $user = $this->signin(); + + $response = $this->json('POST', '/api/contactfieldtypes', [ + 'name' => 'Email', + 'protocol' => 'mailto:', + 'type' => 'email', + ]); + + $response->assertStatus(201); + $response->assertJsonStructure([ + 'data' => $this->jsonContactFieldType, + ]); + $contactFieldTypeId = $response->json('data.id'); + $response->assertJsonFragment([ + 'object' => 'contactfieldtype', + 'id' => $contactFieldTypeId, + ]); + + $this->assertGreaterThan(0, $contactFieldTypeId); + $this->assertDatabaseHas('contact_field_types', [ + 'account_id' => $user->account_id, + 'id' => $contactFieldTypeId, + 'name' => 'Email', + 'protocol' => 'mailto:', + 'type' => 'email', + ]); + } + + /** @test */ + public function contact_field_type_create_error() + { + $user = $this->signin(); + + $response = $this->json('POST', '/api/contactfieldtypes', [ + ]); + + $this->expectDataError($response, [ + 'The name field is required.', + ]); + } + + /** @test */ + public function contact_field_type_update() + { + $user = $this->signin(); + $contactFieldType = factory(ContactFieldType::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('PUT', '/api/contactfieldtypes/'.$contactFieldType->id, [ + 'name' => 'Email2', + 'protocol' => 'mailto:', + 'type' => 'email', + ]); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => $this->jsonContactFieldType, + ]); + $contactFieldTypeId = $response->json('data.id'); + $this->assertEquals($contactFieldType->id, $contactFieldTypeId); + $response->assertJsonFragment([ + 'object' => 'contactfieldtype', + 'id' => $contactFieldTypeId, + ]); + + $this->assertGreaterThan(0, $contactFieldTypeId); + $this->assertDatabaseHas('contact_field_types', [ + 'account_id' => $user->account_id, + 'id' => $contactFieldType->id, + 'name' => 'Email2', + 'protocol' => 'mailto:', + 'type' => 'email', + ]); + } + + /** @test */ + public function contact_field_type_update_error() + { + $user = $this->signin(); + $contactFieldType = factory(ContactFieldType::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('PUT', '/api/contactfieldtypes/'.$contactFieldType->id, []); + + $this->expectDataError($response, [ + 'The name field is required.', + ]); + } + + /** @test */ + public function contact_field_type_update_error_bad_account() + { + $user = $this->signin(); + + $account = factory(Account::class)->create(); + $contactFieldType = factory(ContactFieldType::class)->create([ + 'account_id' => $account->id, + ]); + + $response = $this->json('PUT', '/api/contactfieldtypes/'.$contactFieldType->id, [ + 'name' => 'Email2', + 'protocol' => 'mailto:', + 'type' => 'email', + ]); + + $this->expectNotFound($response); + } + + /** @test */ + public function contact_field_type_delete() + { + $user = $this->signin(); + $contactFieldType = factory(ContactFieldType::class)->create([ + 'account_id' => $user->account_id, + ]); + $this->assertDatabaseHas('contact_field_types', [ + 'account_id' => $user->account_id, + 'id' => $contactFieldType->id, + ]); + + $response = $this->json('DELETE', '/api/contactfieldtypes/'.$contactFieldType->id); + + $response->assertStatus(200); + $this->assertDatabaseMissing('contact_field_types', [ + 'account_id' => $user->account_id, + 'id' => $contactFieldType->id, + ]); + } + + /** @test */ + public function contact_field_type_delete_error() + { + $user = $this->signin(); + + $response = $this->json('DELETE', '/api/contactfieldtypes/0'); + + $this->expectNotFound($response); + } + + /** @test */ + public function contact_field_type_delete_bad_account() + { + $user = $this->signin(); + + $account = factory(Account::class)->create(); + $contactFieldType = factory(ContactFieldType::class)->create([ + 'account_id' => $account->id, + ]); + + $response = $this->json('DELETE', '/api/contactfieldtypes/'.$contactFieldType->id); + + $this->expectNotFound($response); + } +} diff --git a/tests/Api/DAV/CalDAVBirthdaysTest.php b/tests/Api/DAV/CalDAVBirthdaysTest.php new file mode 100644 index 0000000..b2d7047 --- /dev/null +++ b/tests/Api/DAV/CalDAVBirthdaysTest.php @@ -0,0 +1,294 @@ +signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $specialDate = $contact->setSpecialDate('birthdate', 1983, 03, 04); + + $response = $this->call('PROPFIND', "/dav/calendars/{$user->email}/birthdays"); + + $response->assertStatus(207); + $response->assertHeader('X-Sabre-Version'); + + $response->assertSee("/dav/calendars/{$user->email}/birthdays/", false); + $specialDate->refresh(); + $response->assertSee("/dav/calendars/{$user->email}/birthdays/{$specialDate->uuid}.ics", false); + } + + public function test_caldav_birthdays_propfind_with_props() + { + $user = $this->signin(); + + $response = $this->call('PROPFIND', "/dav/calendars/{$user->email}/birthdays/", [], [], [], + [ + 'HTTP_DEPTH' => 0, + ], + ' + + + + ' + ); + + $response->assertStatus(207); + $response->assertHeader('X-Sabre-Version'); + + $response->assertSee(''. + ''. + "/dav/calendars/{$user->email}/birthdays/". + ''. + ''. + 'Birthdays'. + ''. + 'HTTP/1.1 200 OK'. + ''. + ''. + 'signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $specialDate = $contact->setSpecialDate('birthdate', 1983, 03, 04); + $specialDate->uuid = Str::uuid(); + $specialDate->save(); + + $response = $this->call('PROPFIND', "/dav/calendars/{$user->email}/birthdays/{$specialDate->uuid}.ics"); + + $response->assertStatus(207); + $response->assertHeader('X-Sabre-Version'); + + $response->assertSee("/dav/calendars/{$user->email}/birthdays/{$specialDate->uuid}.ics", false); + } + + public function test_caldav_birthdays_getctag() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $specialDate = $contact->setSpecialDate('birthdate', 1983, 03, 04); + $specialDate->uuid = Str::uuid(); + $specialDate->save(); + + $response = $this->call('PROPFIND', "/dav/calendars/{$user->email}/", [], [], [], + [ + 'HTTP_DEPTH' => '1', + 'content-type' => 'application/xml; charset=utf-8', + ], + " + + + + + + " + ); + + $response->assertStatus(207); + $response->assertHeader('X-Sabre-Version'); + + $tokens = SyncToken::where([ + 'account_id' => $user->account_id, + 'user_id' => $user->id, + 'name' => 'birthdays', + ])->orderBy('created_at')->get(); + + $this->assertGreaterThan(0, $tokens->count()); + $token = $tokens->last(); + + $response->assertSee('', false); + $response->assertSee(''. + "/dav/calendars/{$user->email}/birthdays/". + ''. + ''. + "http://sabre.io/ns/sync/{$token->id}". + "http://sabre.io/ns/sync/{$token->id}". + "http://sabre.io/ns/sync/{$token->id}". + ''. + 'HTTP/1.1 200 OK'. + ''. + '', false); + } + + public function test_caldav_birthdays_getctag_birthday() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $specialDate = $contact->setSpecialDate('birthdate', 1983, 03, 04); + $specialDate->uuid = Str::uuid(); + $specialDate->save(); + + $response = $this->call('PROPFIND', "/dav/calendars/{$user->email}/birthdays/", [], [], [], + [ + 'HTTP_DEPTH' => '0', + 'content-type' => 'application/xml; charset=utf-8', + ], + " + + + + + + " + ); + + $response->assertStatus(207); + $response->assertHeader('X-Sabre-Version'); + + $tokens = SyncToken::where([ + 'account_id' => $user->account_id, + 'user_id' => $user->id, + 'name' => 'birthdays', + ])->orderBy('created_at')->get(); + + $this->assertGreaterThan(0, $tokens->count()); + $token = $tokens->last(); + + $response->assertSee('', false); + $response->assertSee(''. + "/dav/calendars/{$user->email}/birthdays/". + ''. + ''. + "http://sabre.io/ns/sync/{$token->id}". + "http://sabre.io/ns/sync/{$token->id}". + "http://sabre.io/ns/sync/{$token->id}". + ''. + 'HTTP/1.1 200 OK'. + ''. + '', false); + } + + public function test_caldav_birthdays_sync_collection_with_token() + { + Carbon::setTestNow(Carbon::create(2019, 1, 1, 9, 0, 0)); + + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $specialDate = $contact->setSpecialDate('birthdate', 1983, 03, 04); + $specialDate->uuid = Str::uuid(); + $specialDate->save(); + + Carbon::setTestNow(Carbon::create(2019, 1, 1, 8, 0, 0)); + $token = factory(SyncToken::class)->create([ + 'account_id' => $user->account_id, + 'user_id' => $user->id, + 'name' => 'birthdays', + 'timestamp' => now(), + ]); + + $response = $this->call('REPORT', "/dav/calendars/{$user->email}/birthdays/", [], [], [], + [ + 'content-type' => 'application/xml; charset=utf-8', + ], + " + http://sabre.io/ns/sync/{$token->id} + 1 + + + + " + ); + + $response->assertStatus(207); + + $token = SyncToken::where([ + 'account_id' => $user->account_id, + 'user_id' => $user->id, + 'name' => 'birthdays', + ]) + ->orderBy('created_at') + ->get() + ->last(); + $response->assertSee(" + + /dav/calendars/{$user->email}/birthdays/{$specialDate->uuid}.ics + + + "{$this->getEtag($specialDate)}" + + HTTP/1.1 200 OK + + + http://sabre.io/ns/sync/{$token->id} +", false); + } + + public function test_caldav_birthdays_sync_collection_init() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $specialDate = $contact->setSpecialDate('birthdate', 1983, 03, 04); + $specialDate->uuid = Str::uuid(); + $specialDate->save(); + + $response = $this->call('REPORT', "/dav/calendars/{$user->email}/birthdays/", [], [], [], + [ + 'content-type' => 'application/xml; charset=utf-8', + ], + " + + 1 + + + + " + ); + + $response->assertStatus(207); + $response->assertHeader('X-Sabre-Version'); + + $tokens = SyncToken::where([ + 'account_id' => $user->account_id, + 'user_id' => $user->id, + 'name' => 'birthdays', + ])->orderBy('created_at')->get(); + + $this->assertGreaterThan(0, $tokens->count()); + $token = $tokens->last(); + + $response->assertSee(" + + /dav/calendars/{$user->email}/birthdays/{$specialDate->uuid}.ics + + + "{$this->getEtag($specialDate)}" + + HTTP/1.1 200 OK + + + http://sabre.io/ns/sync/{$token->id} +", false); + } +} diff --git a/tests/Api/DAV/CalDAVTasksTest.php b/tests/Api/DAV/CalDAVTasksTest.php new file mode 100644 index 0000000..5eef9ed --- /dev/null +++ b/tests/Api/DAV/CalDAVTasksTest.php @@ -0,0 +1,303 @@ +signin(); + $task = factory(Task::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => null, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + $response = $this->call('PROPFIND', "/dav/calendars/{$user->email}/tasks"); + + $response->assertStatus(207); + $response->assertHeader('X-Sabre-Version'); + + $response->assertSee("/dav/calendars/{$user->email}/tasks/", false); + $response->assertSee("/dav/calendars/{$user->email}/tasks/{$task->uuid}.ics", false); + } + + public function test_caldav_tasks_propfind_with_props() + { + $user = $this->signin(); + + $response = $this->call('PROPFIND', "/dav/calendars/{$user->email}/tasks/", [], [], [], + [ + 'HTTP_DEPTH' => 0, + ], + ' + + + + ' + ); + + $response->assertStatus(207); + $response->assertHeader('X-Sabre-Version'); + + $response->assertSee(''. + ''. + "/dav/calendars/{$user->email}/tasks/". + ''. + ''. + 'Tasks'. + ''. + 'HTTP/1.1 200 OK'. + ''. + ''. + 'signin(); + $task = factory(Task::class)->create([ + 'account_id' => $user->account_id, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + $response = $this->call('PROPFIND', "/dav/calendars/{$user->email}/tasks/{$task->uuid}.ics"); + + $response->assertStatus(207); + $response->assertHeader('X-Sabre-Version'); + + $response->assertSee("/dav/calendars/{$user->email}/tasks/{$task->uuid}.ics", false); + } + + public function test_caldav_tasks_getctag() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $task = factory(Task::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + ]); + + $response = $this->call('PROPFIND', "/dav/calendars/{$user->email}/", [], [], [], + [ + 'HTTP_DEPTH' => '1', + 'content-type' => 'application/xml; charset=utf-8', + ], + " + + + + + + " + ); + + $response->assertStatus(207); + $response->assertHeader('X-Sabre-Version'); + + $tokens = SyncToken::where([ + 'account_id' => $user->account_id, + 'user_id' => $user->id, + 'name' => 'tasks', + ])->orderBy('created_at')->get(); + + $this->assertGreaterThan(0, $tokens->count()); + $token = $tokens->last(); + + $response->assertSee('', false); + $response->assertSee(''. + "/dav/calendars/{$user->email}/tasks/". + ''. + ''. + "http://sabre.io/ns/sync/{$token->id}". + "http://sabre.io/ns/sync/{$token->id}". + "http://sabre.io/ns/sync/{$token->id}". + ''. + 'HTTP/1.1 200 OK'. + ''. + '', false); + } + + public function test_caldav_tasks_getctag_task() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $task = factory(Task::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + $response = $this->call('PROPFIND', "/dav/calendars/{$user->email}/tasks/", [], [], [], + [ + 'HTTP_DEPTH' => '0', + 'content-type' => 'application/xml; charset=utf-8', + ], + " + + + + + + " + ); + + $response->assertStatus(207); + $response->assertHeader('X-Sabre-Version'); + + $tokens = SyncToken::where([ + 'account_id' => $user->account_id, + 'user_id' => $user->id, + 'name' => 'tasks', + ])->orderBy('created_at')->get(); + + $this->assertGreaterThan(0, $tokens->count()); + $token = $tokens->last(); + + $response->assertSee('', false); + $response->assertSee(''. + "/dav/calendars/{$user->email}/tasks/". + ''. + ''. + "http://sabre.io/ns/sync/{$token->id}". + "http://sabre.io/ns/sync/{$token->id}". + "http://sabre.io/ns/sync/{$token->id}". + ''. + 'HTTP/1.1 200 OK'. + ''. + '', false); + } + + public function test_caldav_tasks_sync_collection_with_token() + { + Carbon::setTestNow(Carbon::create(2019, 1, 1, 9, 0, 0)); + + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $task = factory(Task::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + Carbon::setTestNow(Carbon::create(2019, 1, 1, 8, 0, 0)); + $token = factory(SyncToken::class)->create([ + 'account_id' => $user->account_id, + 'user_id' => $user->id, + 'name' => 'tasks', + 'timestamp' => now(), + ]); + + $response = $this->call('REPORT', "/dav/calendars/{$user->email}/tasks/", [], [], [], + [ + 'content-type' => 'application/xml; charset=utf-8', + ], + " + http://sabre.io/ns/sync/{$token->id} + 1 + + + + " + ); + $response->assertStatus(207); + + $token = SyncToken::where([ + 'account_id' => $user->account_id, + 'user_id' => $user->id, + 'name' => 'tasks', + ]) + ->orderBy('created_at') + ->get() + ->last(); + $response->assertSee(" + + /dav/calendars/{$user->email}/tasks/{$task->uuid}.ics + + + "{$this->getEtag($task)}" + + HTTP/1.1 200 OK + + + http://sabre.io/ns/sync/{$token->id} +", false); + } + + public function test_caldav_tasks_sync_collection_init() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $task = factory(Task::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + $response = $this->call('REPORT', "/dav/calendars/{$user->email}/tasks/", [], [], [], + [ + 'content-type' => 'application/xml; charset=utf-8', + ], + " + + 1 + + + + " + ); + + $response->assertStatus(207); + $response->assertHeader('X-Sabre-Version'); + + $tokens = SyncToken::where([ + 'account_id' => $user->account_id, + 'user_id' => $user->id, + 'name' => 'tasks', + ])->orderBy('created_at')->get(); + + $this->assertGreaterThan(0, $tokens->count()); + $token = $tokens->last(); + + $response->assertSee(" + + /dav/calendars/{$user->email}/tasks/{$task->uuid}.ics + + + "{$this->getEtag($task)}" + + HTTP/1.1 200 OK + + + http://sabre.io/ns/sync/{$token->id} +", false); + } +} diff --git a/tests/Api/DAV/CardDAVTest.php b/tests/Api/DAV/CardDAVTest.php new file mode 100644 index 0000000..c46f4f5 --- /dev/null +++ b/tests/Api/DAV/CardDAVTest.php @@ -0,0 +1,451 @@ +signin(); + + $response = $this->call('PROPFIND', '/dav/addressbooks'); + + $response->assertStatus(207); + $response->assertHeader('X-Sabre-Version'); + + $response->assertSee('/dav/addressbooks/', false); + $response->assertSee("/dav/addressbooks/{$user->email}/", false); + } + + /** + * @group dav + */ + public function test_carddav_propfind_addressbooks_user() + { + $user = $this->signin(); + + $response = $this->call('PROPFIND', "/dav/addressbooks/{$user->email}"); + + $response->assertStatus(207); + $response->assertHeader('X-Sabre-Version'); + + $response->assertSee("/dav/addressbooks/{$user->email}/", false); + $response->assertSee("/dav/addressbooks/{$user->email}/contacts/", false); + } + + /** + * @group dav + */ + public function test_carddav_propfind_contacts() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->call('PROPFIND', "/dav/addressbooks/{$user->email}/contacts"); + + $response->assertStatus(207); + $response->assertHeader('X-Sabre-Version'); + + $response->assertSee("/dav/addressbooks/{$user->email}/contacts/", false); + $contactId = urlencode($contact->uuid); + $response->assertSee("/dav/addressbooks/{$user->email}/contacts/{$contactId}.vcf", false); + } + + public function test_carddav_propfind_contacts_with_props() + { + $user = $this->signin(); + + $response = $this->call('PROPFIND', "/dav/addressbooks/{$user->email}/contacts/", [], [], [], + [ + 'HTTP_DEPTH' => 0, + ], + ' + + + + ' + ); + + $response->assertStatus(207); + $response->assertHeader('X-Sabre-Version'); + + $response->assertSee(''. + ''. + "/dav/addressbooks/{$user->email}/contacts/". + ''. + ''. + 'Contacts'. + ''. + 'HTTP/1.1 200 OK'. + ''. + ''. + 'signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->call('PROPFIND', "/dav/addressbooks/{$user->email}/contacts/{$contact->uuid}.vcf"); + + $response->assertStatus(207); + $response->assertHeader('X-Sabre-Version'); + + $response->assertSee("/dav/addressbooks/{$user->email}/contacts/{$contact->uuid}.vcf", false); + } + + /** + * @group dav + */ + public function test_carddav_propfind_one_contact_without_extension() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->call('PROPFIND', "/dav/addressbooks/{$user->email}/contacts/{$contact->uuid}"); + + $response->assertStatus(207); + $response->assertHeader('X-Sabre-Version'); + + $response->assertSee("/dav/addressbooks/{$user->email}/contacts/{$contact->uuid}", false); + } + + public function test_carddav_getctag() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->call('PROPFIND', "/dav/addressbooks/{$user->email}/", [], [], [], + [ + 'HTTP_DEPTH' => '1', + 'content-type' => 'application/xml; charset=utf-8', + ], + " + + + + + " + ); + + $response->assertStatus(207); + $response->assertHeader('X-Sabre-Version'); + + $tokens = SyncToken::where([ + 'account_id' => $user->account_id, + 'user_id' => $user->id, + 'name' => 'contacts', + ])->orderBy('created_at')->get(); + + $this->assertGreaterThan(0, $tokens->count()); + $token = $tokens->last(); + + $response->assertSee('', false); + $response->assertSee(''. + "/dav/addressbooks/{$user->email}/contacts/". + ''. + ''. + "http://sabre.io/ns/sync/{$token->id}". + "http://sabre.io/ns/sync/{$token->id}". + ''. + 'HTTP/1.1 200 OK'. + ''. + '', false); + } + + public function test_carddav_get_me_card() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $user->me_contact_id = $contact->id; + $user->save(); + + $response = $this->call('PROPFIND', "/dav/addressbooks/{$user->email}", [], [], [], + [ + 'HTTP_DEPTH' => '1', + 'content-type' => 'application/xml; charset=utf-8', + ], + " + + + + " + ); + + $response->assertStatus(207); + $response->assertHeader('X-Sabre-Version'); + + $response->assertSee(''. + "/dav/addressbooks/{$user->email}/contacts/". + ''. + ''. + "/dav/addressbooks/{$user->email}/contacts/{$contact->uuid}.vcf". + ''. + 'HTTP/1.1 200 OK'. + ''. + '', false); + } + + public function test_carddav_set_me_card() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->call('PROPPATCH', "/dav/addressbooks/{$user->email}/contacts", [], [], [], + [ + 'content-type' => 'application/xml; charset=utf-8', + ], + " + + + + /dav/addressbooks/{$user->email}/contacts/{$contact->uuid}.vcf + + + + " + ); + + $response->assertSee('', false); + $response->assertSee(''. + "/dav/addressbooks/{$user->email}/contacts". + ''. + ''. + ''. + ''. + 'HTTP/1.1 200 OK'. + ''. + '', false); + + $this->assertDatabaseHas('users', [ + 'id' => $user->id, + 'me_contact_id' => $contact->id, + ]); + } + + public function test_carddav_getctag_contacts() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->call('PROPFIND', "/dav/addressbooks/{$user->email}/contacts/", [], [], [], + [ + 'HTTP_DEPTH' => '0', + 'content-type' => 'application/xml; charset=utf-8', + ], + " + + + + + " + ); + + $response->assertStatus(207); + $response->assertHeader('X-Sabre-Version'); + + $tokens = SyncToken::where([ + 'account_id' => $user->account_id, + 'user_id' => $user->id, + 'name' => 'contacts', + ])->orderBy('created_at')->get(); + + $this->assertGreaterThan(0, $tokens->count()); + $token = $tokens->last(); + + $response->assertSee('', false); + $response->assertSee(''. + "/dav/addressbooks/{$user->email}/contacts/". + ''. + ''. + "http://sabre.io/ns/sync/{$token->id}". + "http://sabre.io/ns/sync/{$token->id}". + ''. + 'HTTP/1.1 200 OK'. + ''. + '', false); + } + + public function test_carddav_sync_collection_with_token() + { + Carbon::setTestNow(Carbon::create(2019, 1, 1, 9, 0, 0)); + + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + Carbon::setTestNow(Carbon::create(2018, 1, 1, 8, 0, 0)); + $token = factory(SyncToken::class)->create([ + 'account_id' => $user->account_id, + 'user_id' => $user->id, + 'name' => 'contacts', + 'timestamp' => now(), + ]); + + $response = $this->call('REPORT', "/dav/addressbooks/{$user->email}/contacts/", [], [], [], + [ + 'content-type' => 'application/xml; charset=utf-8', + ], + " + http://sabre.io/ns/sync/{$token->id} + 1 + + + + " + ); + + $response->assertStatus(207); + + $token = SyncToken::where([ + 'account_id' => $user->account_id, + 'user_id' => $user->id, + 'name' => 'contacts', + ]) + ->orderBy('created_at') + ->get() + ->last(); + + $response->assertSee(" + + /dav/addressbooks/{$user->email}/contacts/{$contact->uuid}.vcf + + + "{$this->getEtag($contact)}" + + HTTP/1.1 200 OK + + + http://sabre.io/ns/sync/{$token->id} +", false); + } + + public function test_carddav_sync_collection_init() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->call('REPORT', "/dav/addressbooks/{$user->email}/contacts/", [], [], [], + [ + 'content-type' => 'application/xml; charset=utf-8', + ], + " + + 1 + + + + " + ); + + $response->assertStatus(207); + $response->assertHeader('X-Sabre-Version'); + + $tokens = SyncToken::where([ + 'account_id' => $user->account_id, + 'user_id' => $user->id, + 'name' => 'contacts', + ])->orderBy('created_at')->get(); + + $this->assertGreaterThan(0, $tokens->count()); + $token = $tokens->last(); + + $response->assertSee(" + + /dav/addressbooks/{$user->email}/contacts/{$contact->uuid}.vcf + + + "{$this->getEtag($contact)}" + + HTTP/1.1 200 OK + + + http://sabre.io/ns/sync/{$token->id} +", false); + } + + public function test_carddav_sync_collection_deleted_contact() + { + Carbon::setTestNow(Carbon::create(2019, 1, 1, 9, 0, 0)); + + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + 'deleted_at' => Carbon::create(2019, 3, 1, 9, 0, 0), + ]); + + Carbon::setTestNow(Carbon::create(2019, 2, 1, 9, 0, 0)); + $token = factory(SyncToken::class)->create([ + 'account_id' => $user->account_id, + 'user_id' => $user->id, + 'name' => 'contacts', + 'timestamp' => now(), + ]); + + Carbon::setTestNow(Carbon::create(2019, 4, 1, 9, 0, 0)); + + $response = $this->call('REPORT', "/dav/addressbooks/{$user->email}/contacts/", [], [], [], + [ + 'content-type' => 'application/xml; charset=utf-8', + ], + " + http://sabre.io/ns/sync/{$token->id} + 1 + + + + " + ); + + $response->assertStatus(207); + + $token = SyncToken::where([ + 'account_id' => $user->account_id, + 'user_id' => $user->id, + 'name' => 'contacts', + ]) + ->orderBy('created_at') + ->get() + ->last(); + + $response->assertSee(" + + /dav/addressbooks/{$user->email}/contacts/{$contact->uuid}.vcf + HTTP/1.1 404 Not Found + + http://sabre.io/ns/sync/{$token->id} +", false); + } +} diff --git a/tests/Api/DAV/CardEtag.php b/tests/Api/DAV/CardEtag.php new file mode 100644 index 0000000..5803855 --- /dev/null +++ b/tests/Api/DAV/CardEtag.php @@ -0,0 +1,176 @@ +getCard($obj, true); + } elseif ($obj instanceof SpecialDate) { + $data = $this->getCal($obj, true); + } elseif ($obj instanceof Task) { + $data = $this->getVTodo($obj, true); + } + + $etag = sha1($data); + if ($quotes) { + $etag = '"'.$etag.'"'; + } + + return $etag; + } + + protected function getCard(Contact $contact, bool $realFormat = false): string + { + $contact = $contact->refresh(); + $url = route('people.show', $contact); + $sabreversion = \Sabre\VObject\Version::VERSION; + $timestamp = $contact->updated_at->format('Ymd\THis\Z'); + + $data = "BEGIN:VCARD +VERSION:4.0 +PRODID:-//Sabre//Sabre VObject {$sabreversion}//EN +UID:{$contact->uuid} +SOURCE:{$url} +FN:{$contact->name} +N:{$contact->last_name};{$contact->first_name};{$contact->middle_name};; +"; + + if ($contact->gender) { + $data .= "GENDER:{$contact->gender->type}"; + $data .= "\n"; + } + + $picture = $contact->getAvatarURL(); + if (! empty($picture)) { + $data .= "PHOTO;VALUE=URI:{$picture}\n"; + } + + foreach ($contact->addresses as $address) { + $data .= 'ADR:;;'; + $data .= $address->place->street.';'; + $data .= $address->place->city.';'; + $data .= $address->place->province.';'; + $data .= $address->place->postal_code.';'; + $data .= $address->place->country; + $data .= "\n"; + } + foreach ($contact->contactFields as $contactField) { + $type = ''; + if ($contactField->labels->count() > 0) { + $type = ';TYPE='.$contactField->labels->map(function ($label) { + return $label->label_i18n ?: $label->label; + })->join(','); + } + switch ($contactField->contactFieldType->type) { + case ContactFieldType::PHONE: + $data .= "TEL$type:{$contactField->data}\n"; + break; + case ContactFieldType::EMAIL: + $data .= "EMAIL$type:{$contactField->data}\n"; + break; + default: + break; + } + } + $data .= "REV:{$timestamp}\n"; + $tags = $contact->getTagsAsString(); + if (! empty($tags)) { + $data .= "CATEGORIES:{$tags}\n"; + } + $data .= "END:VCARD\n"; + + if ($realFormat) { + $data = mb_ereg_replace("\n", "\r\n", $data); + } + + return $data; + } + + protected function getCal(SpecialDate $specialDate, bool $realFormat = false): string + { + $contact = $specialDate->contact; + $url = route('people.show', $contact); + $description = "See {$contact->name}’s profile: {$url}"; + $description1 = mb_substr($description, 0, 61); + $description2 = mb_substr($description, 61); + + $sabreversion = \Sabre\VObject\Version::VERSION; + $timestamp = $specialDate->created_at->format('Ymd\THis\Z'); + + $start = $specialDate->date->format('Ymd'); + $end = $specialDate->date->addDays(1)->format('Ymd'); + + $data = "BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sabre//Sabre VObject {$sabreversion}//EN +CALSCALE:GREGORIAN +BEGIN:VTIMEZONE +TZID:UTC +END:VTIMEZONE +BEGIN:VEVENT +UID:{$specialDate->uuid} +DTSTART;VALUE=DATE:{$start} +DTEND;VALUE=DATE:{$end} +RRULE:FREQ=YEARLY +DTSTAMP:{$timestamp} +CREATED:{$timestamp} +SUMMARY:Birthday of {$contact->name} +ATTACH:{$url} +DESCRIPTION:{$description1} + {$description2} +END:VEVENT +END:VCALENDAR +"; + + if ($realFormat) { + $data = mb_ereg_replace("\n", "\r\n", $data); + } + + return $data; + } + + protected function getVTodo(Task $task, bool $realFormat = false): string + { + $sabreversion = \Sabre\VObject\Version::VERSION; + $timestamp = $task->created_at->format('Ymd\THis\Z'); + $contact = $task->contact; + + $data = "BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Sabre//Sabre VObject {$sabreversion}//EN +CALSCALE:GREGORIAN +BEGIN:VTIMEZONE +TZID:UTC +END:VTIMEZONE +BEGIN:VTODO +UID:{$task->uuid} +SUMMARY:{$task->title} +DTSTAMP:{$timestamp} +CREATED:{$timestamp} +DESCRIPTION:{$task->description} +"; + if ($contact) { + $url = route('people.show', $contact); + $data .= "ATTACH:{$url} +"; + } + $data .= 'END:VTODO +END:VCALENDAR +'; + + if ($realFormat) { + $data = mb_ereg_replace("\n", "\r\n", $data); + } + + return $data; + } +} diff --git a/tests/Api/DAV/DAVServerTest.php b/tests/Api/DAV/DAVServerTest.php new file mode 100644 index 0000000..45cd51f --- /dev/null +++ b/tests/Api/DAV/DAVServerTest.php @@ -0,0 +1,218 @@ +signin(); + + $response = $this->call('PROPFIND', '/dav'); + + $response->assertStatus(207); + $response->assertHeader('X-Sabre-Version'); + + $response->assertSee('/dav/', false); + $response->assertSee('/dav/principals/', false); + $response->assertSee('/dav/addressbooks/', false); + $response->assertSee('/dav/calendars/', false); + } + + /** + * @group dav + */ + public function test_dav_propfind_principals() + { + $user = $this->signin(); + + $response = $this->call('PROPFIND', '/dav/principals'); + + $response->assertStatus(207); + $response->assertHeader('X-Sabre-Version'); + + $response->assertSee('/dav/principals/', false); + $response->assertSee("/dav/principals/{$user->email}/", false); + } + + /** + * @group dav + */ + public function test_dav_propfind_principals_user() + { + $user = $this->signin(); + + $response = $this->call('PROPFIND', "/dav/principals/{$user->email}"); + + $response->assertStatus(207); + $response->assertHeader('X-Sabre-Version'); + + $response->assertSee("/dav/principals/{$user->email}/", false); + } + + /** + * @group dav + */ + public function test_dav_ensure_browser_plugin_not_enabled() + { + $user = $this->signin(); + + $response = $this->call('GET', '/dav'); + + $response->assertStatus(302); + $response->assertHeader('X-Sabre-Version'); + $response->assertHeader('Location', route('settings.dav')); + } + + /** + * @group dav + */ + public function test_carddav_propfind_groupmemberset() + { + $user = $this->signin(); + + $response = $this->call('PROPFIND', "/dav/principals/{$user->email}/", [], [], [], + [ + 'content-type' => 'application/xml; charset=utf-8', + ], + ' + + + + + ' + ); + + $response->assertStatus(207); + $response->assertHeader('X-Sabre-Version'); + + $response->assertSee(''. + ''. + "/dav/principals/{$user->email}/". + ''. + ''. + ''. + "/dav/addressbooks/{$user->email}/". + ''. + ''. + "/dav/principals/{$user->email}/". + ''. + ''. + 'HTTP/1.1 200 OK'. + ''. + ''. + '', false); + } + + /** + * @group dav + */ + public function test_carddav_report_propertysearch() + { + $user = $this->signin(); + + $response = $this->call('REPORT', '/dav/principals/', [], [], [], + [ + 'HTTP_DEPTH' => '0', + 'content-type' => 'application/xml; charset=utf-8', + ], + " + + {$user->name} + + + + + + + + " + ); + + $response->assertStatus(207); + $response->assertHeader('X-Sabre-Version'); + + $response->assertSee(''. + ''. + "/dav/principals/{$user->email}/". + ''. + ''. + "{$user->name}". + ''. + 'HTTP/1.1 200 OK'. + ''. + ''. + '', false); + } + + /** + * @group dav + */ + public function test_caldav_propfind() + { + $user = $this->signin(); + + $response = $this->call('PROPFIND', '/dav/calendars'); + + $response->assertStatus(207); + $response->assertHeader('X-Sabre-Version'); + + $response->assertSee('/dav/calendars/', false); + $response->assertSee("/dav/calendars/{$user->email}/", false); + } + + /** + * @group dav + */ + public function test_caldav_propfind_calendars_user() + { + $user = $this->signin(); + + $response = $this->call('PROPFIND', "/dav/calendars/{$user->email}"); + + $response->assertStatus(207); + $response->assertHeader('X-Sabre-Version'); + + $response->assertSee("/dav/calendars/{$user->email}/", false); + $response->assertSee("/dav/calendars/{$user->email}/birthdays/", false); + $response->assertSee("/dav/calendars/{$user->email}/tasks/", false); + } + + /** + * @group dav + */ + public function test_dav_limit_users_unauthorized() + { + $user = $this->signin(); + + config(['laravelsabre.users' => 'unauthorized']); + + $response = $this->call('PROPFIND', '/dav'); + + $response->assertStatus(403); + } + + /** + * @group dav + */ + public function test_dav_limit_users_authorized() + { + $user = $this->signin(); + + config(['laravelsabre.users' => $user->email]); + + $response = $this->call('PROPFIND', '/dav'); + + $response->assertStatus(207); + } +} diff --git a/tests/Api/DAV/VCardContactTest.php b/tests/Api/DAV/VCardContactTest.php new file mode 100644 index 0000000..dc20ee2 --- /dev/null +++ b/tests/Api/DAV/VCardContactTest.php @@ -0,0 +1,494 @@ +signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->get("/dav/addressbooks/{$user->email}/contacts/{$contact->uuid}.vcf", [ + 'HTTP_ACCEPT' => 'text/vcard; version=4.0', + ]); + + $response->assertStatus(200); + $response->assertHeader('X-Sabre-Version'); + + $this->assertVObjectEqualsVObject($this->getCard($contact, true), $response->getContent()); + } + + /** + * @group dav + */ + public function test_carddav_put_one_contact() + { + $user = $this->signin(); + + $response = $this->call('PUT', "/dav/addressbooks/{$user->email}/contacts/single_vcard_stub.vcf", [], [], [], + ['content-type' => 'application/xml; charset=utf-8'], + "BEGIN:VCARD\nVERSION:4.0\nFN:John Doe\nN:Doe;John;;;\nEND:VCARD" + ); + + $response->assertStatus(201); + $response->assertHeader('X-Sabre-Version'); + $response->assertHeaderMissing('ETag'); + + $this->assertDatabaseHas('contacts', [ + 'account_id' => $user->account_id, + 'first_name' => 'John', + 'last_name' => 'Doe', + ]); + } + + /** + * @group dav + */ + public function test_carddav_put_one_contact_with_photo() + { + Storage::fake(); + + $user = $this->signin(); + + $image = Image::canvas(1, 1, '#fff')->encode('data-url'); + + $response = $this->call('PUT', "/dav/addressbooks/{$user->email}/contacts/single_vcard_stub.vcf", [], [], [], + ['content-type' => 'application/xml; charset=utf-8'], + "BEGIN:VCARD\nVERSION:4.0\nFN:John Doe\nN:Doe;John;;;\nPHOTO:$image\nEND:VCARD" + ); + + $response->assertStatus(201); + $response->assertHeader('X-Sabre-Version'); + $response->assertHeaderMissing('ETag'); + + $this->assertDatabaseHas('contacts', [ + 'account_id' => $user->account_id, + 'first_name' => 'John', + 'last_name' => 'Doe', + ]); + $this->assertDatabaseHas('photos', [ + 'account_id' => $user->account_id, + ]); + + $photo = Photo::where(['account_id' => $user->account_id])->first(); + + Storage::disk('public')->assertExists($photo->new_filename); + } + + /** + * @group dav + */ + public function test_carddav_put_one_contact_with_photo_already_set() + { + $user = $this->signin(); + $photo = factory(Photo::class)->create([ + 'account_id' => $user->account_id, + ]); + UploadedFile::fake()->image('file.jpg')->storeAs('', 'file.jpg'); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + 'avatar_source' => 'photo', + 'avatar_photo_id' => $photo->id, + 'uuid' => Str::uuid()->toString(), + ]); + + $image = Image::canvas(1, 1, '#fff')->encode('data-url'); + + $response = $this->call('PUT', "/dav/addressbooks/{$user->email}/contacts/{$contact->uuid}.vcf", [], [], [], + ['content-type' => 'application/xml; charset=utf-8'], + "BEGIN:VCARD\nVERSION:4.0\nFN:John Doe\nN:Doe;John;;;\nPHOTO:$image\nEND:VCARD" + ); + + $response->assertStatus(204); + $response->assertHeader('X-Sabre-Version'); + $response->assertHeaderMissing('ETag'); + + $this->assertDatabaseHas('contacts', [ + 'account_id' => $user->account_id, + 'first_name' => 'John', + 'last_name' => 'Doe', + 'avatar_photo_id' => $photo->id, + ]); + } + + /** + * @group dav + */ + public function test_carddav_put_one_contact_with_photo_and_attributes() + { + Storage::fake(); + + $user = $this->signin(); + + $image = base64_encode(Image::canvas(1, 1, '#fff')->encode('jpg')); + + $response = $this->call('PUT', "/dav/addressbooks/{$user->email}/contacts/single_vcard_stub.vcf", [], [], [], + ['content-type' => 'application/xml; charset=utf-8'], + "BEGIN:VCARD\nVERSION:3.0\nFN:John Doe\nN:Doe;John;;;\nPHOTO;ENCODING=B;TYPE=JPEG:$image\nEND:VCARD" + ); + + $response->assertStatus(201); + $response->assertHeader('X-Sabre-Version'); + $response->assertHeaderMissing('ETag'); + + $this->assertDatabaseHas('contacts', [ + 'account_id' => $user->account_id, + 'first_name' => 'John', + 'last_name' => 'Doe', + ]); + $this->assertDatabaseHas('photos', [ + 'account_id' => $user->account_id, + ]); + + $photo = Photo::where(['account_id' => $user->account_id])->first(); + + Storage::disk('public')->assertExists($photo->new_filename); + } + + /** + * @group dav + */ + public function test_carddav_update_existing_contact() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->call('PUT', "/dav/addressbooks/{$user->email}/contacts/{$contact->uuid}.vcf", [], [], [], + ['content-type' => 'application/xml; charset=utf-8'], + "BEGIN:VCARD\nVERSION:4.0\nFN:John Doex\nN:Doex;John;;;\nEND:VCARD" + ); + + $response->assertStatus(204); + $response->assertHeader('X-Sabre-Version'); + $response->assertHeaderMissing('ETag'); + + $this->assertDatabaseHas('contacts', [ + 'account_id' => $user->account_id, + 'first_name' => 'John', + 'last_name' => 'Doex', + ]); + } + + /** + * @group dav + */ + public function test_carddav_update_existing_contact_if_modified() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $filename = urlencode($contact->uuid.'.vcf'); + + $response = $this->call('PUT', "/dav/addressbooks/{$user->email}/contacts/{$filename}", [], [], [], + [ + 'HTTP_If-Modified-Since' => $contact->updated_at->addDays(-1)->toRfc7231String(), + 'content-type' => 'application/xml; charset=utf-8', + ], + "BEGIN:VCARD\nVERSION:4.0\nFN:John Doex\nN:Doex;John;;;\nEND:VCARD" + ); + + $response->assertStatus(204); + $response->assertHeader('X-Sabre-Version'); + $response->assertHeaderMissing('ETag'); + + $this->assertDatabaseHas('contacts', [ + 'account_id' => $user->account_id, + 'first_name' => 'John', + 'last_name' => 'Doex', + ]); + } + + /** + * @group dav + */ + public function test_carddav_update_existing_contact_if_modified_not_modified() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $filename = urlencode($contact->uuid.'.vcf'); + + $response = $this->call('PUT', "/dav/addressbooks/{$user->email}/contacts/{$filename}", [], [], [], + [ + 'HTTP_If-Modified-Since' => $contact->updated_at->addDays(1)->toRfc7231String(), + 'content-type' => 'application/xml; charset=utf-8', + ], + "BEGIN:VCARD\nVERSION:4.0\nFN:John Doex\nN:Doex;John;;;\nEND:VCARD" + ); + + // Not modified + $response->assertStatus(304); + + $response->assertHeader('X-Sabre-Version'); + + // see http://tools.ietf.org/html/rfc2616#section-10.3.5 + $response->assertHeaderMissing('Last-Modified'); + + $response->assertHeaderMissing('ETag'); + } + + /** + * @group dav + */ + public function test_carddav_update_existing_contact_if_unmodified() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $filename = urlencode($contact->uuid.'.vcf'); + + $response = $this->call('PUT', "/dav/addressbooks/{$user->email}/contacts/{$filename}", [], [], [], + [ + 'HTTP_If-Unmodified-Since' => $contact->updated_at->addDays(1)->toRfc7231String(), + 'content-type' => 'application/xml; charset=utf-8', + ], + "BEGIN:VCARD\nVERSION:4.0\nFN:John Doex\nN:Doex;John;;;\nEND:VCARD" + ); + + $response->assertStatus(204); + $response->assertHeader('X-Sabre-Version'); + $response->assertHeaderMissing('ETag'); + + $this->assertDatabaseHas('contacts', [ + 'account_id' => $user->account_id, + 'first_name' => 'John', + 'last_name' => 'Doex', + ]); + } + + /** + * @group dav + */ + public function test_carddav_update_existing_contact_if_unmodified_error() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $filename = urlencode($contact->uuid.'.vcf'); + + $response = $this->call('PUT', "/dav/addressbooks/{$user->email}/contacts/{$filename}", [], [], [], + [ + 'HTTP_If-Unmodified-Since' => $contact->updated_at->addDays(-1)->toRfc7231String(), + 'content-type' => 'application/xml; charset=utf-8', + ], + "BEGIN:VCARD\nVERSION:4.0\nFN:John Doex\nN:Doex;John;;;\nEND:VCARD" + ); + + // PRECONDITION FAILED + $response->assertStatus(412); + + $response->assertHeader('X-Sabre-Version'); + + $sabreversion = \Sabre\DAV\Version::VERSION; + $response->assertSee(" + {$sabreversion} + Sabre\DAV\Exception\PreconditionFailed + An If-Unmodified-Since header was specified, but the entity has been changed since the specified date.", false); + } + + /** + * @group dav + */ + public function test_carddav_update_existing_contact_no_modify() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $filename = urlencode($contact->uuid.'.vcf'); + + $response = $this->get("/dav/addressbooks/{$user->email}/contacts/{$filename}"); + $data = $response->getContent(); + + $response = $this->call('PUT', "/dav/addressbooks/{$user->email}/contacts/{$filename}", [], [], [], + ['content-type' => 'application/xml; charset=utf-8'], + $data + ); + + $response->assertStatus(204); + $response->assertHeader('X-Sabre-Version'); + //$response->assertHeader('ETag'); // etag no more sent + } + + public function test_carddav_contacts_report_version4() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->call('REPORT', "/dav/addressbooks/{$user->email}/contacts/", [], [], [], + [ + 'HTTP_DEPTH' => '1', + 'content-type' => 'application/xml; charset=utf-8', + ], + ' + + + + + ' + ); + + $response->assertStatus(207); + $response->assertHeader('X-Sabre-Version'); + + $vcard = mb_ereg_replace("\n", " \n", $this->getCard($contact)); + + $response->assertSee(''. + ''. + "/dav/addressbooks/{$user->email}/contacts/{$contact->uuid}.vcf". + ''. + ''. + ""{$this->getEtag($contact)}"". + "{$vcard}". + ''. + 'HTTP/1.1 200 OK'. + ''. + ''. + '', false); + } + + public function test_carddav_contacts_report_version3() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->call('REPORT', "/dav/addressbooks/{$user->email}/contacts/", [], [], [], + [ + 'HTTP_DEPTH' => '1', + 'content-type' => 'application/xml; charset=utf-8', + ], + ' + + + + + ' + ); + + $response->assertStatus(207); + $response->assertHeader('X-Sabre-Version'); + + $vcard = mb_ereg_replace('VERSION:4.0', 'VERSION:3.0', $this->getCard($contact)); + $vcard = mb_ereg_replace("\n", " \n", $vcard); + + $response->assertSee(''. + ''. + "/dav/addressbooks/{$user->email}/contacts/{$contact->uuid}.vcf". + ''. + ''. + ""{$this->getEtag($contact)}"". + "{$vcard}". + ''. + 'HTTP/1.1 200 OK'. + ''. + ''. + '', false); + } + + public function test_carddav_contacts_report_multiget() + { + $user = $this->signin(); + $contact1 = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $contact2 = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->call('REPORT', "/dav/addressbooks/{$user->email}/contacts/", [], [], [], + [ + 'HTTP_DEPTH' => '1', + ], + " + + + + + /dav/addressbooks/{$user->email}/contacts/{$contact1->uuid}.vcf + /dav/addressbooks/{$user->email}/contacts/{$contact2->uuid}.vcf + " + ); + + $response->assertStatus(207); + $response->assertHeader('X-Sabre-Version'); + + $vcard1 = mb_ereg_replace("\n", " \n", $this->getCard($contact1)); + $vcard2 = mb_ereg_replace("\n", " \n", $this->getCard($contact2)); + + $response->assertSee(''. + ''. + "/dav/addressbooks/{$user->email}/contacts/{$contact1->uuid}.vcf". + ''. + ''. + ""{$this->getEtag($contact1)}"". + "{$vcard1}". + ''. + 'HTTP/1.1 200 OK'. + ''. + '', false); + $response->assertSee( + ''. + "/dav/addressbooks/{$user->email}/contacts/{$contact2->uuid}.vcf". + ''. + ''. + ""{$this->getEtag($contact2)}"". + "{$vcard2}". + ''. + 'HTTP/1.1 200 OK'. + ''. + ''. + '', false); + } + + /** + * @group dav + * @test + */ + public function carddav_delete_one_contact() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->call('DELETE', "/dav/addressbooks/{$user->email}/contacts/{$contact->uuid}.vcf"); + + $response->assertStatus(204); + $response->assertHeader('X-Sabre-Version'); + $response->assertHeaderMissing('ETag'); + + $this->assertDatabaseMissing('contacts', [ + 'account_id' => $user->account_id, + 'id' => $contact->id, + 'deleted_at' => null, + ]); + } +} diff --git a/tests/Api/DAV/VEventBirthdayTest.php b/tests/Api/DAV/VEventBirthdayTest.php new file mode 100644 index 0000000..588b283 --- /dev/null +++ b/tests/Api/DAV/VEventBirthdayTest.php @@ -0,0 +1,138 @@ +signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $specialDate = $contact->setSpecialDate('birthdate', 1983, 03, 04); + $specialDate->uuid = Str::uuid(); + $specialDate->save(); + + $response = $this->get("/dav/calendars/{$user->email}/birthdays/{$specialDate->uuid}.ics"); + + $response->assertStatus(200); + $response->assertHeader('X-Sabre-Version'); + + $this->assertVObjectEqualsVObject($this->getCal($specialDate, true), $response->getContent() ?: $response->streamedContent()); + } + + public function test_caldav_birthdays_report() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $specialDate = $contact->setSpecialDate('birthdate', 1983, 03, 04); + $specialDate->uuid = Str::uuid(); + $specialDate->save(); + + $response = $this->call('REPORT', "/dav/calendars/{$user->email}/birthdays/", [], [], [], + [ + 'HTTP_DEPTH' => '1', + 'content-type' => 'application/xml; charset=utf-8', + ], + ' + + + + + + + + ' + ); + + $response->assertStatus(207); + $response->assertHeader('X-Sabre-Version'); + + $response->assertSee(''. + ''. + "/dav/calendars/{$user->email}/birthdays/{$specialDate->uuid}.ics". + ''. + ''. + ""{$this->getEtag($specialDate)}"". + "{$this->getCal($specialDate)}". + ''. + 'HTTP/1.1 200 OK'. + ''. + ''. + '', false); + } + + public function test_caldav_birthdays_report_multiget() + { + $user = $this->signin(); + $contact1 = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $specialDate1 = $contact1->setSpecialDate('birthdate', 1983, 03, 04); + $specialDate1->uuid = Str::uuid(); + $specialDate1->save(); + + $contact2 = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + 'firstname' => 'Jane', + ]); + $specialDate2 = $contact2->setSpecialDate('birthdate', 1980, 05, 01); + $specialDate2->uuid = Str::uuid(); + $specialDate2->save(); + + $response = $this->call('REPORT', "/dav/calendars/{$user->email}/birthdays/", [], [], [], + [ + 'HTTP_DEPTH' => '1', + ], + " + + + + + /dav/calendars/{$user->email}/birthdays/{$specialDate1->uuid}.ics + /dav/calendars/{$user->email}/birthdays/{$specialDate2->uuid}.ics + " + ); + + $response->assertStatus(207); + $response->assertHeader('X-Sabre-Version'); + + $response->assertSee(''. + ''. + "/dav/calendars/{$user->email}/birthdays/{$specialDate1->uuid}.ics". + ''. + ''. + ""{$this->getEtag($specialDate1)}"". + "{$this->getCal($specialDate1)}". + ''. + 'HTTP/1.1 200 OK'. + ''. + '', false); + $response->assertSee( + ''. + "/dav/calendars/{$user->email}/birthdays/{$specialDate2->uuid}.ics". + ''. + ''. + ""{$this->getEtag($specialDate2)}"". + "{$this->getCal($specialDate2)}". + ''. + 'HTTP/1.1 200 OK'. + ''. + ''. + '', false); + } +} diff --git a/tests/Api/DAV/VTodoTaskTest.php b/tests/Api/DAV/VTodoTaskTest.php new file mode 100644 index 0000000..339b83e --- /dev/null +++ b/tests/Api/DAV/VTodoTaskTest.php @@ -0,0 +1,244 @@ +signin(); + $task = factory(Task::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => null, + 'created_at' => now(), + 'updated_at' => now(), + ]); + + $response = $this->get("/dav/calendars/{$user->email}/tasks/{$task->uuid}.ics"); + + $response->assertStatus(200); + $response->assertHeader('X-Sabre-Version'); + + $this->assertVObjectEqualsVObject($this->getVTodo($task, true), $response->getContent() ?: $response->streamedContent()); + } + + /** + * @group dav + */ + public function test_caldav_put_one_task() + { + $user = $this->signin(); + + $uuid = Str::uuid(); + + $response = $this->call('PUT', "/dav/calendars/{$user->email}/tasks/{$uuid->toString()}.ics", [], [], [], + ['content-type' => 'application/xml; charset=utf-8'], + "BEGIN:VCALENDAR +BEGIN:VTODO +UID:{$uuid->toString()} +SUMMARY:title +DESCRIPTION:description +END:VTODO +END:VCALENDAR +" + ); + + $response->assertStatus(201); + $response->assertHeader('X-Sabre-Version'); + $response->assertHeaderMissing('ETag'); + + $this->assertDatabaseHas('tasks', [ + 'account_id' => $user->account_id, + 'contact_id' => null, + 'uuid' => $uuid, + 'title' => 'title', + 'description' => 'description', + ]); + } + + /** + * @group dav + */ + public function test_caldav_update_existing_task() + { + $user = $this->signin(); + $task = factory(Task::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => null, + ]); + + $response = $this->call('PUT', "/dav/calendars/{$user->email}/tasks/{$task->uuid}.ics", [], [], [], + ['content-type' => 'application/xml; charset=utf-8'], + "BEGIN:VCALENDAR +BEGIN:VTODO +UID:{$task->uuid} +SUMMARY:new title +DESCRIPTION:new description +END:VTODO +END:VCALENDAR +" + ); + + $response->assertStatus(204); + $response->assertHeader('X-Sabre-Version'); + $response->assertHeaderMissing('ETag'); + + $this->assertDatabaseHas('tasks', [ + 'account_id' => $user->account_id, + 'uuid' => $task->uuid, + 'title' => 'new title', + 'description' => 'new description', + ]); + } + + /** + * @group dav + */ + public function test_caldav_update_task_complete() + { + $user = $this->signin(); + $task = factory(Task::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => null, + ]); + + $response = $this->call('PUT', "/dav/calendars/{$user->email}/tasks/{$task->uuid}.ics", [], [], [], + ['content-type' => 'application/xml; charset=utf-8'], + "BEGIN:VCALENDAR +BEGIN:VTODO +UID:{$task->uuid} +SUMMARY:{$task->title} +DESCRIPTION:{$task->description} +STATUS:COMPLETED +COMPLETED:20190121T182800Z +END:VTODO +END:VCALENDAR +" + ); + + $response->assertStatus(204); + $response->assertHeader('X-Sabre-Version'); + $response->assertHeaderMissing('ETag'); + + $this->assertDatabaseHas('tasks', [ + 'account_id' => $user->account_id, + 'uuid' => $task->uuid, + 'completed' => true, + 'completed_at' => Carbon::create(2019, 01, 21, 18, 28, 00), + ]); + } + + public function test_caldav_tasks_report() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $task = factory(Task::class)->create([ + 'account_id' => $user->account_id, + 'created_at' => now(), + ]); + + $response = $this->call('REPORT', "/dav/calendars/{$user->email}/tasks/", [], [], [], + [ + 'HTTP_DEPTH' => '1', + 'content-type' => 'application/xml; charset=utf-8', + ], + ' + + + + + + + + ' + ); + + $response->assertStatus(207); + $response->assertHeader('X-Sabre-Version'); + + $peopleurl = route('people.show', $contact); + $sabreversion = \Sabre\VObject\Version::VERSION; + + $response->assertSee(''. + ''. + "/dav/calendars/{$user->email}/tasks/{$task->uuid}.ics". + ''. + ''. + ""{$this->getEtag($task)}"". + "{$this->getVTodo($task)}". + ''. + 'HTTP/1.1 200 OK'. + ''. + ''. + '', false); + } + + public function test_caldav_tasks_report_multiget() + { + $user = $this->signin(); + $task1 = factory(Task::class)->create([ + 'account_id' => $user->account_id, + 'created_at' => now(), + ]); + $task2 = factory(Task::class)->create([ + 'account_id' => $user->account_id, + 'created_at' => now(), + ]); + + $response = $this->call('REPORT', "/dav/calendars/{$user->email}/tasks/", [], [], [], + [ + 'HTTP_DEPTH' => '1', + ], + " + + + + + /dav/calendars/{$user->email}/tasks/{$task1->uuid}.ics + /dav/calendars/{$user->email}/tasks/{$task2->uuid}.ics + " + ); + + $response->assertStatus(207); + $response->assertHeader('X-Sabre-Version'); + + $response->assertSee(''. + ''. + "/dav/calendars/{$user->email}/tasks/{$task1->uuid}.ics". + ''. + ''. + ""{$this->getEtag($task1)}"". + "{$this->getVTodo($task1)}". + ''. + 'HTTP/1.1 200 OK'. + ''. + '', false); + $response->assertSee( + ''. + "/dav/calendars/{$user->email}/tasks/{$task2->uuid}.ics". + ''. + ''. + ""{$this->getEtag($task2)}"". + "{$this->getVTodo($task2)}". + ''. + 'HTTP/1.1 200 OK'. + ''. + ''. + '', false); + } +} diff --git a/tests/Api/Settings/ApiAuditLogControllerTest.php b/tests/Api/Settings/ApiAuditLogControllerTest.php new file mode 100644 index 0000000..d0d0cee --- /dev/null +++ b/tests/Api/Settings/ApiAuditLogControllerTest.php @@ -0,0 +1,75 @@ + [ + 'name', + ], + 'action', + 'objects', + 'audited_at', + 'created_at', + 'updated_at', + ]; + + /** @test */ + public function it_gets_a_list_of_audit_logs() + { + $user = $this->signin(); + + factory(AuditLog::class, 10)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('GET', '/api/logs'); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => ['*' => $this->jsonStructureAuditLog], + ]); + + $this->assertCount( + 10, + $response->decodeResponseJson()['data'] + ); + + $response->assertJsonFragment([ + 'total' => 10, + 'current_page' => 1, + ]); + } + + /** @test */ + public function it_is_possible_to_get_audit_logs_and_limit_query_and_paginate() + { + $user = $this->signin(); + + factory(AuditLog::class, 10)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('GET', '/api/logs?limit=1&page=2'); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => ['*' => $this->jsonStructureAuditLog], + ]); + + $response->assertJsonFragment([ + 'total' => 10, + 'per_page' => 1, + 'current_page' => 2, + ]); + } +} diff --git a/tests/Api/Settings/ApiComplianceControllerTest.php b/tests/Api/Settings/ApiComplianceControllerTest.php new file mode 100644 index 0000000..cca8ecc --- /dev/null +++ b/tests/Api/Settings/ApiComplianceControllerTest.php @@ -0,0 +1,91 @@ +create([ + 'term_version' => rand(1, 100), + 'term_content' => 'dummy data', + 'privacy_version' => rand(1, 100), + 'privacy_content' => 'dummy data', + ]); + + $response = $this->json('GET', '/api/compliance/'); + + $response->assertStatus(200); + + $this->assertCount( + Term::get()->count(), + $response->decodeResponseJson()['data'] + ); + + $response->assertJsonFragment([ + 'total' => Term::get()->count(), + 'current_page' => 1, + ]); + + $response->assertJsonStructure([ + 'data' => [ + '*' => $this->jsonStructureCompliance, + ], + ]); + } + + /** @test */ + public function it_gets_a_single_term() + { + $term = factory(Term::class)->create([ + 'term_version' => rand(1, 100), + 'term_content' => 'dummy data', + 'privacy_version' => rand(1, 100), + 'privacy_content' => 'dummy data', + ]); + + $response = $this->json('GET', '/api/compliance/'.$term->id); + + $response->assertStatus(200); + + $response->assertJsonFragment([ + 'id' => $term->id, + 'object' => 'term', + ]); + + $response->assertJsonStructure([ + 'data' => $this->jsonStructureCompliance, + ]); + } + + /** @test */ + public function it_doesnt_get_a_single_term() + { + $response = $this->json('GET', '/api/compliance/3'); + + $response->assertStatus(404); + + $response->assertJsonFragment([ + 'message' => 'The resource has not been found', + 'error_code' => 31, + ]); + } +} diff --git a/tests/Api/Settings/ApiCurrencyControllerTest.php b/tests/Api/Settings/ApiCurrencyControllerTest.php new file mode 100644 index 0000000..1558bad --- /dev/null +++ b/tests/Api/Settings/ApiCurrencyControllerTest.php @@ -0,0 +1,70 @@ +json('GET', '/api/currencies/'); + + $response->assertStatus(200); + + $this->assertCount( + 15, + $response->decodeResponseJson()['data'] + ); + + $response->assertJsonFragment([ + 'total' => 153, + 'current_page' => 1, + ]); + + $response->assertJsonStructure([ + 'data' => ['*' => $this->jsonStructureCurrency], + ]); + } + + /** @test */ + public function it_gets_one_currency() + { + $currency = factory(Currency::class)->create([]); + + $response = $this->json('GET', '/api/currencies/'.$currency->id); + + $response->assertStatus(200); + + $response->assertJsonFragment([ + 'id' => $currency->id, + 'object' => 'currency', + ]); + + $response->assertJsonStructure([ + 'data' => $this->jsonStructureCurrency, + ]); + } + + /** @test */ + public function it_gets_a_currency_that_is_invalid() + { + $response = $this->json('GET', '/api/currencies/0'); + + $this->expectNotFound($response); + } +} diff --git a/tests/ApiTestCase.php b/tests/ApiTestCase.php new file mode 100644 index 0000000..1233d91 --- /dev/null +++ b/tests/ApiTestCase.php @@ -0,0 +1,14 @@ +getActualConnection() != 'testing') { + $this->markTestSkipped("Set DB_CONNECTION on 'testing' to run this test."); + } + } + + protected $jsonStructureOAuthLogin = [ + 'access_token', + 'expires_in', + ]; + + private const OAUTH_LOGIN_URL = 'http://localhost:8001/oauth/login'; + + public function test_oauth_login() + { + $repository = new ClientRepository(); + $client = null; + try { + $client = $repository->createPasswordGrantClient( + null, config('app.name'), config('app.url') + ); + + $this->setEnvironmentValue([ + 'PASSPORT_PASSWORD_GRANT_CLIENT_ID' => $client->id, + 'PASSPORT_PASSWORD_GRANT_CLIENT_SECRET' => $client->secret, + ]); + + $userPassword = 'password'; + $user = factory(User::class)->create([ + 'password' => bcrypt($userPassword), + ]); + + $response = $this->postClient(self::OAUTH_LOGIN_URL, [ + 'email' => $user->email, + 'password' => $userPassword, + ]); + + $response->assertStatus(200); + + $response->assertJsonStructure($this->jsonStructureOAuthLogin); + } finally { + if ($client) { + $repository->delete($client); + } + if ($user) { + $user->account->delete(); + } + } + } + + public function test_oauth_login_bad_password() + { + $repository = new ClientRepository(); + $client = null; + try { + $client = $repository->createPasswordGrantClient( + null, config('app.name'), config('app.url') + ); + + $this->setEnvironmentValue([ + 'PASSPORT_PASSWORD_GRANT_CLIENT_ID' => $client->id, + 'PASSPORT_PASSWORD_GRANT_CLIENT_SECRET' => $client->secret, + ]); + + $userPassword = 'password'; + $user = factory(User::class)->create([ + 'password' => bcrypt($userPassword), + ]); + + $response = $this->postClient(self::OAUTH_LOGIN_URL, [ + 'email' => $user->email, + 'password' => 'wrongPassword', + ]); + + $this->expectNotAuthorized($response); + } finally { + if ($client) { + $repository->delete($client); + } + if ($user) { + $user->account->delete(); + } + } + } + + public function test_oauth_login_wrong_mail() + { + $response = $this->postClient(self::OAUTH_LOGIN_URL, [ + 'email' => 'badmail', + 'password' => 'xx', + ]); + + $response->assertStatus(422); + $this->expectDataError($response, ['The email must be a valid email address.']); + } + + public function test_oauth_login_wrong_password() + { + $response = $this->postClient(self::OAUTH_LOGIN_URL, [ + 'email' => 'mail@mail.com', + 'password' => 'xx', + ]); + + $this->expectNotAuthorized($response); + } + + public function test_oauth_login_2fa() + { + $repository = new ClientRepository(); + $client = null; + try { + $client = $repository->createPasswordGrantClient( + null, config('app.name'), config('app.url') + ); + + $this->setEnvironmentValue([ + 'PASSPORT_PASSWORD_GRANT_CLIENT_ID' => $client->id, + 'PASSPORT_PASSWORD_GRANT_CLIENT_SECRET' => $client->secret, + ]); + + $userPassword = 'password'; + $user = factory(User::class)->create([ + 'password' => bcrypt($userPassword), + 'google2fa_secret' => 'UFKZDTYO64WDEZPPQEO4HF3PC5UUTFLE', + ]); + + $response = $this->postClient(self::OAUTH_LOGIN_URL, [ + 'email' => $user->email, + 'password' => $userPassword, + ]); + + $response->assertStatus(200); + + $response->assertSee('Two Factor Authentication'); + } finally { + if ($client) { + $repository->delete($client); + } + if ($user) { + $user->account->delete(); + } + } + } + + private function getActualConnection() + { + $handle = fopen('.env', 'r'); + if (! $handle) { + return; + } + + $value = null; + while (($line = fgets($handle)) !== false) { + if (preg_match('/DB_CONNECTION=(.{1,})/', $line, $matches)) { + $value = $matches[1]; + break; + } + } + + fclose($handle); + + return $value; + } + + private function setEnvironmentValue(array $values) + { + $envFile = app()->environmentFilePath(); + $str = file_get_contents($envFile); + + if (count($values) > 0) { + foreach ($values as $envKey => $envValue) { + $str .= "\n"; // In case the searched variable is in the last line without \n + $keyPosition = strpos($str, "{$envKey}="); + $endOfLinePosition = strpos($str, "\n", $keyPosition); + $oldLine = substr($str, $keyPosition, $endOfLinePosition - $keyPosition); + + // If key does not exist, add it + if (! $keyPosition || ! $endOfLinePosition || ! $oldLine) { + $str .= "{$envKey}={$envValue}\n"; + } else { + $str = str_replace($oldLine, "{$envKey}={$envValue}", $str); + } + } + } + + $str = substr($str, 0, -1); + + return file_put_contents($envFile, $str); + } + + /** + * @param string $path + * @param array $param + * @return TestResponse + */ + protected function postClient($path, $param) + { + try { + $http = new Client([ + 'timeout' => 30, + ]); + $response = $http->post($path, [ + 'form_params' => $param, + ]); + } catch (\GuzzleHttp\Exception\RequestException $e) { + $response = $e->getResponse(); + } + + $factory = new HttpFoundationFactory(); + $response = $factory->createResponse($response); + + return TestResponse::fromBaseResponse($response); + } +} diff --git a/tests/Browser/ExampleTest.php b/tests/Browser/ExampleTest.php new file mode 100644 index 0000000..1eb3a17 --- /dev/null +++ b/tests/Browser/ExampleTest.php @@ -0,0 +1,21 @@ +browse(function ($browser) { + $browser->visit('/') + ->assertSee('Login'); + }); + } +} diff --git a/tests/Browser/Feature/UploadVCardTest.php b/tests/Browser/Feature/UploadVCardTest.php new file mode 100644 index 0000000..fa291b8 --- /dev/null +++ b/tests/Browser/Feature/UploadVCardTest.php @@ -0,0 +1,83 @@ +browse(function ($browser) { + $browser->login() + ->visit('/people/add') + ->assertSee('import your contacts'); + + $browser->clickLink('import your contacts') + ->assertSee('You haven’t imported any contacts yet'); + }); + } + + /** + * Make sure that the Import button leads to the Import screen, and that + * the cancel button leads to the Blank import screen. + * + * @return void + */ + public function test_import_button_leads_to_import_screen() + { + $this->browse(function ($browser) { + $browser->login() + ->visit('/settings/import') + ->clickLink('Import vCard') + ->assertPathIs('/settings/import/upload') + ->clickLink('Cancel') + ->assertPathIs('/settings/import'); + }); + } + + /** + * Upload a single contact from a valid vcard file. + * + * @return void + */ + public function test_user_can_import_contacts_from_a_vcf_card() + { + $this->browse(function ($browser) { + $browser->login() + ->visit('/settings/import') + ->clickLink('Import vCard') + ->attach('vcard', base_path('tests/stubs/single_vcard_stub.vcard')) + ->on(new ImportVCardUpload) + ->scrollTo('upload') + ->press('Upload') + ->assertSee('1 imported'); + }); + } + + /** + * Upload a contact from a broken vCard and see that it triggers an error. + * + * @return void + */ + public function test_user_see_error_when_importing_broken_vcard() + { + $this->browse(function ($browser) { + $browser->login() + ->visit('/settings/import') + ->clickLink('Import vCard') + ->attach('vcard', base_path('tests/stubs/broken_vcard_stub.vcard')) + ->on(new ImportVCardUpload) + ->scrollTo('upload') + ->press('Upload') + ->assertSee('The vcard must be a file of type: vcf, vcard.'); + }); + } +} diff --git a/tests/Browser/Pages/DashboardValidate2fa.php b/tests/Browser/Pages/DashboardValidate2fa.php new file mode 100644 index 0000000..c5fff29 --- /dev/null +++ b/tests/Browser/Pages/DashboardValidate2fa.php @@ -0,0 +1,45 @@ +assertPathIs($this->url()); + } + + /** + * Get the element shortcuts for the page. + * + * @return array + */ + public function elements() + { + return [ + 'verify' => "button[name='verify']", + 'otp' => '#one_time_password', + ]; + } +} diff --git a/tests/Browser/Pages/HomePage.php b/tests/Browser/Pages/HomePage.php new file mode 100644 index 0000000..7c6f447 --- /dev/null +++ b/tests/Browser/Pages/HomePage.php @@ -0,0 +1,43 @@ + '#selector', + ]; + } +} diff --git a/tests/Browser/Pages/ImportVCardUpload.php b/tests/Browser/Pages/ImportVCardUpload.php new file mode 100644 index 0000000..1ee2c77 --- /dev/null +++ b/tests/Browser/Pages/ImportVCardUpload.php @@ -0,0 +1,44 @@ +assertPathIs($this->url()); + } + + /** + * Get the element shortcuts for the page. + * + * @return array + */ + public function elements() + { + return [ + 'upload' => '#upload', + ]; + } +} diff --git a/tests/Browser/Pages/Page.php b/tests/Browser/Pages/Page.php new file mode 100644 index 0000000..2218c52 --- /dev/null +++ b/tests/Browser/Pages/Page.php @@ -0,0 +1,21 @@ + "a[@href='link']", + 'alert' => '.alert', + ]; + } +} diff --git a/tests/Browser/Pages/Settings/SettingsPersonnalization.php b/tests/Browser/Pages/Settings/SettingsPersonnalization.php new file mode 100644 index 0000000..72371e0 --- /dev/null +++ b/tests/Browser/Pages/Settings/SettingsPersonnalization.php @@ -0,0 +1,45 @@ +assertPathIs($this->url()); + } + + /** + * Get the element shortcuts for the page. + * + * @return array + */ + public function elements() + { + return [ + '@reminder-rule-label' => '.reminder-rule-7 > span', + ]; + } +} diff --git a/tests/Browser/Pages/SettingsDAV.php b/tests/Browser/Pages/SettingsDAV.php new file mode 100644 index 0000000..2d7e108 --- /dev/null +++ b/tests/Browser/Pages/SettingsDAV.php @@ -0,0 +1,44 @@ +assertPathIs($this->url()); + } + + /** + * Get the element shortcuts for the page. + * + * @return array + */ + public function elements() + { + return [ + 'dav_url_base' => '#dav_url_base', + ]; + } +} diff --git a/tests/Browser/Pages/SettingsSecurity.php b/tests/Browser/Pages/SettingsSecurity.php new file mode 100644 index 0000000..8da6f9d --- /dev/null +++ b/tests/Browser/Pages/SettingsSecurity.php @@ -0,0 +1,54 @@ +assertPathIs($this->url()); + } + + /** + * Get the element shortcuts for the page. + * + * @return array + */ + public function elements() + { + return [ + 'two_factor_link' => "a:contains('Enable Two Factor Authentication')", + 'barcode' => '#barcode', + 'secretkey' => '#secretkey', + 'buttonVerify' => "button[name='verify']", + 'enableVerify' => '#verify1', + 'disableVerify' => '#verify2', + 'otpenable' => '#one_time_password1', + 'otpdisable' => '#one_time_password2', + 'enableModal' => '#enableModal', + 'disableModal' => '#disableModal', + 'registerModal' => '#registerModal', + ]; + } +} diff --git a/tests/Browser/Pages/SettingsSecurity2faDisable.php b/tests/Browser/Pages/SettingsSecurity2faDisable.php new file mode 100644 index 0000000..6c88423 --- /dev/null +++ b/tests/Browser/Pages/SettingsSecurity2faDisable.php @@ -0,0 +1,45 @@ +assertPathIs($this->url()); + } + + /** + * Get the element shortcuts for the page. + * + * @return array + */ + public function elements() + { + return [ + 'verify' => "button[name='verify']", + 'otp' => '#one_time_password', + ]; + } +} diff --git a/tests/Browser/Pages/SettingsSecurity2faEnable.php b/tests/Browser/Pages/SettingsSecurity2faEnable.php new file mode 100644 index 0000000..60c149e --- /dev/null +++ b/tests/Browser/Pages/SettingsSecurity2faEnable.php @@ -0,0 +1,47 @@ +assertPathIs($this->url()); + } + + /** + * Get the element shortcuts for the page. + * + * @return array + */ + public function elements() + { + return [ + 'barcode' => '#barcode', + 'secretkey' => '#secretkey', + 'verify' => "button[name='verify']", + 'otp' => '#one_time_password', + ]; + } +} diff --git a/tests/Browser/Pages/Validate2fa.php b/tests/Browser/Pages/Validate2fa.php new file mode 100644 index 0000000..dca4a22 --- /dev/null +++ b/tests/Browser/Pages/Validate2fa.php @@ -0,0 +1,44 @@ +assertPathIs($this->url()); + } + + /** + * Get the element shortcuts for the page. + * + * @return array + */ + public function elements() + { + return [ + '@element' => '#selector', + ]; + } +} diff --git a/tests/Browser/Settings/DAVControllerTest.php b/tests/Browser/Settings/DAVControllerTest.php new file mode 100644 index 0000000..acdd4f6 --- /dev/null +++ b/tests/Browser/Settings/DAVControllerTest.php @@ -0,0 +1,23 @@ +browse(function (Browser $browser) { + $browser->login() + ->visit(new SettingsDAV) + ->assertVisible('dav_url_base') + ->assertSourceHas(config('app.url').'/dav'); + }); + } +} diff --git a/tests/Browser/Settings/MultiFAControllerTest.php b/tests/Browser/Settings/MultiFAControllerTest.php new file mode 100644 index 0000000..9a81925 --- /dev/null +++ b/tests/Browser/Settings/MultiFAControllerTest.php @@ -0,0 +1,342 @@ +browse(function (Browser $browser) { + $browser->login() + ->visit(new SettingsSecurity) + ->assertSeeLink('Enable Two Factor Authentication'); + }); + } + + /** + * Test if the user has WebAuthn Enable Link in Security Page. + * + * @group multifa + */ + public function testHasSettingsWebAuthnEnableLink() + { + $this->browse(function (Browser $browser) { + $browser->login() + ->visit(new SettingsSecurity) + ->assertSeeLink('Add a new security key'); + }); + } + + /** + * Test the barcode generated in 2fa Enable Page. + * + * @group multifa + */ + public function testHas2faEnableBarCode() + { + $this->markTestIncomplete('Ignore 2fa tests for now.'); + + $this->browse(function (Browser $browser) { + $browser->login() + ->visit(new SettingsSecurity) + ->scrollTo('two_factor_link') + ->clickLink('Enable Two Factor Authentication') + ->waitFor('enableModal') + ->assertVisible('barcode') + ->assertVisible('secretkey'); + }); + } + + /** + * Test the barcode generated in 2fa Enable Page. + * + * @group multifa + * @group multifabarcode + */ + public function testBarCodeContent() + { + $this->markTestIncomplete('Ignore 2fa tests for now.'); + + $this->browse(function (Browser $browser) { + $browser = + $browser->login() + ->visit(new SettingsSecurity) + ->scrollTo('two_factor_link') + ->clickLink('Enable Two Factor Authentication') + ->waitFor('enableModal'); + + // \Facebook\WebDriver\Remote\RemoteWebElement + $barcode = $browser->element('barcode'); + $imgsrc = $barcode->getAttribute('src'); + + $key = $this->unparseBarcode($imgsrc); + $this->assertEquals(32, strlen($key)); + + $this->assertEquals($browser->text('secretkey'), $key); + }); + } + + private function unparseBarcode($imgsrc) + { + $this->assertStringStartsWith('data:image/png', $imgsrc); + + $imgcode = str_replace('data:image/png;base64,', '', $imgsrc); + + $qrcode = new QrReader(base64_decode($imgcode), QrReader::SOURCE_TYPE_BLOB); + $text = $qrcode->text(); + $this->assertStringStartsWith('otpauth://totp/', $text); + + // unparse $text + // See PragmaRX\Google2FA\Support\QRCode getQRCodeUrl + // example : + //otpauth://totp/monicalocal.test:admin%40admin.com?secret=H25L7JLI7I57KYE7U53BIIOUELWXMRE6&issuer=monicalocal.test + + $ret = preg_match('@^otpauth://totp/([^:]+):([^?]+)\?secret=([^&]+)&issuer=(.+)@i', $text, $matches); + $this->assertEquals(1, $ret, 'otp content does not match format'); + $this->assertCount(5, $matches); + + return $matches[3]; + } + + /** + * Test the 2fa Enable Page with wrong code. + * + * @group multifa + */ + public function testEnable2faWrongCode() + { + $this->markTestIncomplete('Ignore 2fa tests for now.'); + + $this->browse(function (Browser $browser) { + $browser = + $browser->login() + ->visit(new SettingsSecurity) + ->scrollTo('two_factor_link') + ->clickLink('Enable Two Factor Authentication') + ->waitFor('enableModal') + ->type('otpenable', '000000') + ->scrollTo('enableVerify') + ->press('enableVerify') + ->waitUntilMissing('enableModal'); + + $this->assertTrue($this->hasNotification($browser)); + $notification = $this->getNotification($browser); + $this->assertStringContainsString('error', $notification->getAttribute('class')); + $this->assertStringContainsString('Two Factor Authentication', $notification->getText()); + }); + } + + /** + * Test the 2fa Enable Page. + * + * @group multifa + */ + public function testEnable2fa() + { + $this->markTestIncomplete('Ignore 2fa tests for now.'); + + $this->browse(function (Browser $browser) { + $browser = + $browser->login() + ->visit(new SettingsSecurity) + ->scrollTo('two_factor_link') + ->clickLink('Enable Two Factor Authentication') + ->waitFor('enableModal'); + + $this->enable2fa($browser); + }); + } + + private function enable2fa(Browser $browser) + { + $secretkey = $browser->waitFor('enableModal') + ->text('secretkey'); + + $google2fa = new \PragmaRX\Google2FA\Google2FA(); + $one_time_password = $google2fa->getCurrentOtp($secretkey); + $browser->type('otpenable', $one_time_password); + + $browser = $browser->scrollTo('enableVerify') + ->press('enableVerify') + ->waitUntilMissing('enableModal'); + + $this->assertTrue($this->hasNotification($browser)); + $notification = $this->getNotification($browser); + $this->assertStringContainsString('success', $notification->getAttribute('class')); + $this->assertStringContainsString('Two Factor Authentication', $notification->getText()); + + // TODO: test if user has 2fa enabled actually + // TODO: test if session token auth is right + + $browser->assertSeeLink('Disable Two Factor Authentication'); + + return $secretkey; + } + + /** + * Test the 2fa Enable Page. + * + * @group multifa + */ + public function testEnable2faLoginWrongCode() + { + $this->markTestIncomplete('Ignore 2fa tests for now.'); + + $user = call_user_func(Browser::$userResolver); + + $this->browse(function (Browser $browser) use ($user) { + $browser = + $browser->loginAs($user) + ->visit(new SettingsSecurity) + ->scrollTo('two_factor_link') + ->clickLink('Enable Two Factor Authentication') + ->waitFor('enableModal'); + + $secretkey = $this->enable2fa($browser); + + $browser = + $browser->clickLink('Logout') + ->loginAs($user) + ->visit(new DashboardValidate2fa) + ->assertVisible('otp') + ->type('otp', '000000') + ->press('verify'); + + $this->assertTrue($this->hasDivAlert($browser)); + $notification = $this->getDivAlert($browser); + $this->assertStringContainsString('alert-danger', $notification->getAttribute('class')); + $this->assertStringContainsString('The two factor authentication has failed.', $notification->getText()); + }); + } + + /** + * Test the 2fa Enable Page. + * + * @group multifa + */ + public function testEnable2faLogin() + { + $this->markTestIncomplete('Ignore 2fa tests for now.'); + + $user = call_user_func(Browser::$userResolver); + + $this->browse(function (Browser $browser) use ($user) { + $browser = + $browser->loginAs($user) + ->visit(new SettingsSecurity) + ->scrollTo('two_factor_link') + ->clickLink('Enable Two Factor Authentication') + ->waitFor('enableModal'); + + $secretkey = $this->enable2fa($browser); + $google2fa = new \PragmaRX\Google2FA\Google2FA(); + $one_time_password = $google2fa->getCurrentOtp($secretkey); + + $browser = + $browser->clickLink('Logout') + ->loginAs($user) + ->visit(new DashboardValidate2fa) + ->assertVisible('otp') + ->type('otp', $one_time_password) + ->press('verify'); + + $this->assertFalse($this->hasDivAlert($browser)); + $browser->assertPathIs('/dashboard'); + }); + } + + /** + * Test 2fa Enable Page and Disable Page. + * + * @group multifa + */ + public function testEnable2faDisable2fa() + { + $this->markTestIncomplete('Ignore 2fa tests for now.'); + + $this->browse(function (Browser $browser) { + $browser = + $browser->login() + ->visit(new SettingsSecurity) + ->scrollTo('two_factor_link') + ->clickLink('Enable Two Factor Authentication') + ->waitFor('enableModal'); + + $secretkey = $this->enable2fa($browser); + $google2fa = new \PragmaRX\Google2FA\Google2FA(); + $one_time_password = $google2fa->getCurrentOtp($secretkey); + + $browser = + $browser->clickLink('Disable Two Factor Authentication') + ->waitFor('disableModal') + ->assertVisible('otpdisable') + ->type('otpdisable', $one_time_password) + ->scrollTo('disableVerify') + ->press('disableVerify') + ->waitUntilMissing('enableModal'); + + $this->assertTrue($this->hasNotification($browser)); + $notification = $this->getNotification($browser); + $this->assertStringContainsString('success', $notification->getAttribute('class')); + $this->assertStringContainsString('Two Factor Authentication', $notification->getText()); + }); + } + + /** + * Test 2fa Enable Page and Disable Page. + * + * @group multifa + */ + public function testEnable2faDisable2faWrongCode() + { + $this->markTestIncomplete('Ignore 2fa tests for now.'); + + $this->browse(function (Browser $browser) { + $browser = + $browser->login() + ->visit(new SettingsSecurity) + ->scrollTo('two_factor_link') + ->clickLink('Enable Two Factor Authentication') + ->waitFor('enableModal'); + + $this->enable2fa($browser); + + $browser = + $browser->clickLink('Disable Two Factor Authentication') + ->waitFor('disableModal') + ->assertVisible('otpdisable') + ->type('otpdisable', '000000') + ->scrollTo('disableVerify') + ->press('disableVerify') + ->waitUntilMissing('disableModal'); + + $this->assertTrue($this->hasNotification($browser)); + + $res = $browser->elements('.notification'); + $notification = $res[1]; + + $this->assertStringContainsString('error', $notification->getAttribute('class')); + $this->assertStringContainsString('Two Factor Authentication', $notification->getText()); + }); + } +} diff --git a/tests/Browser/console/.gitignore b/tests/Browser/console/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/tests/Browser/console/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/tests/Browser/screenshots/.gitignore b/tests/Browser/screenshots/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/tests/Browser/screenshots/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/tests/Commands/OneTime/MoveAvatarsToPhotosDirectoryTest.php b/tests/Commands/OneTime/MoveAvatarsToPhotosDirectoryTest.php new file mode 100644 index 0000000..130ba1c --- /dev/null +++ b/tests/Commands/OneTime/MoveAvatarsToPhotosDirectoryTest.php @@ -0,0 +1,92 @@ +create(); + + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + return [$user, $contact]; + } + + /** @test */ + public function it_move_avatars_to_photo_directory() + { + [$user, $contact] = $this->fetchUser(); + + Storage::fake('public'); + + Storage::disk('public')->put('avatars/avatar.jpg', 'content'); + Storage::disk('public')->put('avatars/avatar_110.jpg', 'content'); + Storage::disk('public')->put('avatars/avatar_174.jpg', 'content'); + + $contact->avatar_file_name = 'avatars/avatar.jpg'; + $contact->avatar_location = 'public'; + $contact->has_avatar = true; + $contact->save(); + + Storage::disk('public')->assertExists('avatars/avatar.jpg'); + + $this->artisan('monica:moveavatarstophotosdirectory')->run(); + + Storage::disk('public')->assertMissing('avatars/avatar.jpg'); + Storage::disk('public')->assertMissing('avatars/avatar_110.jpg'); + Storage::disk('public')->assertMissing('avatars/avatar_174.jpg'); + + $contact->refresh(); + $photo = Photo::find($contact->avatar_photo_id); + + $this->assertDatabaseHas('contacts', [ + 'id' => $contact->id, + 'avatar_source' => 'photo', + ]); + $this->assertStringContainsString('photos/', $photo->new_filename); + + Storage::disk('public')->assertExists($photo->new_filename); + } + + /** @test */ + public function it_handles_missing_avatar() + { + [$user, $contact] = $this->fetchUser(); + + Storage::fake('public'); + + $contact->avatar_file_name = 'avatars/avatar.jpg'; + $contact->avatar_location = 'public'; + $contact->has_avatar = true; + $contact->save(); + + $this->artisan('monica:moveavatarstophotosdirectory')->run(); + + Storage::disk('public')->assertMissing('avatars/avatar.jpg'); + + $this->assertDatabaseHas('contacts', [ + 'id' => $contact->id, + 'avatar_source' => 'default', + 'avatar_file_name' => 'avatars/avatar.jpg', + 'avatar_location' => 'public', + ]); + } +} diff --git a/tests/Commands/Other/CleanCommandTest.php b/tests/Commands/Other/CleanCommandTest.php new file mode 100644 index 0000000..0452144 --- /dev/null +++ b/tests/Commands/Other/CleanCommandTest.php @@ -0,0 +1,102 @@ +create(); + $user = factory(User::class)->create([ + 'account_id' => $account->id, + ]); + + SyncToken::create([ + 'account_id' => $account->id, + 'user_id' => $user->id, + 'name' => 'contacts', + 'timestamp' => now(), + ]); + + $this->artisan('monica:clean')->run(); + + $this->assertDatabaseHas('synctoken', [ + 'account_id' => $account->id, + 'user_id' => $user->id, + 'name' => 'contacts', + ]); + } + + /** @test */ + public function clean_command_left_all_token() + { + $account = factory(Account::class)->create(); + $user = factory(User::class)->create([ + 'account_id' => $account->id, + ]); + + $s1 = SyncToken::create([ + 'account_id' => $account->id, + 'user_id' => $user->id, + 'name' => 'contacts', + 'timestamp' => now(), + ]); + $s2 = SyncToken::create([ + 'account_id' => $account->id, + 'user_id' => $user->id, + 'name' => 'contacts', + 'timestamp' => now()->addDays(-10), + ]); + + $command = $this->artisan('monica:clean'); + $command->expectsOutput("Delete token {$s2->id} - User {$user->id} - Type contacts - timestamp {$s2->timestamp}"); + $command->run(); + + $this->assertDatabaseHas('synctoken', [ + 'id' => $s1->id, + ]); + $this->assertDatabaseMissing('synctoken', [ + 'id' => $s2->id, + ]); + } + + /** @test */ + public function clean_command_dryrun() + { + $account = factory(Account::class)->create(); + $user = factory(User::class)->create([ + 'account_id' => $account->id, + ]); + + $s1 = SyncToken::create([ + 'account_id' => $account->id, + 'user_id' => $user->id, + 'name' => 'contacts', + 'timestamp' => now(), + ]); + $s2 = SyncToken::create([ + 'account_id' => $account->id, + 'user_id' => $user->id, + 'name' => 'contacts', + 'timestamp' => now()->addDays(-10), + ]); + + $this->artisan('monica:clean', ['--dry-run' => true])->run(); + + $this->assertDatabaseHas('synctoken', [ + 'id' => $s1->id, + ]); + $this->assertDatabaseHas('synctoken', [ + 'id' => $s2->id, + ]); + } +} diff --git a/tests/Commands/Other/CreateAccountTest.php b/tests/Commands/Other/CreateAccountTest.php new file mode 100644 index 0000000..d16f6bf --- /dev/null +++ b/tests/Commands/Other/CreateAccountTest.php @@ -0,0 +1,60 @@ +artisan('account:create', ['--email' => 'user1@example.com', '--password' => 'astrongpassword']) + ->run(); + + $user = User::where('email', '=', $email)->first(); + $this->assertNotEmpty($user); + } + + /** @test */ + public function it_creates_account_with_specified_name() + { + $email = 'user1@example.com'; + $firstname = 'firstname'; + $lastname = 'lastname'; + $this->artisan('account:create', [ + '--email' => $email, + '--password' => 'astrongpassword', + '--firstname' => $firstname, + '--lastname' => $lastname, + ])->run(); + + $user = User::where('email', '=', $email)->first(); + $this->assertNotEmpty($user); + } + + /** @test */ + public function it_fails_creation_without_email() + { + $this->artisan('account:create', ['--password' => 'astrongpassword']) + ->expectsOutput(CreateAccount::ERROR_MISSING_EMAIL) + ->doesntExpectOutput(CreateAccount::ERROR_MISSING_PASSWORD) + ->run(); + } + + /** @test */ + public function it_fails_creation_without_password() + { + $email = 'user1@example.com'; + $this->artisan('account:create', ['--email' => $email]) + ->expectsOutput(CreateAccount::ERROR_MISSING_PASSWORD) + ->doesntExpectOutput(CreateAccount::ERROR_MISSING_EMAIL) + ->run(); + } +} diff --git a/tests/Commands/Other/CreateAddressBookSubscriptionTest.php b/tests/Commands/Other/CreateAddressBookSubscriptionTest.php new file mode 100644 index 0000000..307f7f6 --- /dev/null +++ b/tests/Commands/Other/CreateAddressBookSubscriptionTest.php @@ -0,0 +1,43 @@ +create(); + + $this->mock(CreateAddressBookSubscription::class, function (MockInterface $mock) use ($user) { + $mock->shouldReceive('execute') + ->once() + ->withArgs(function ($data) use ($user) { + $this->assertEquals([ + 'account_id' => $user->account_id, + 'user_id' => $user->id, + 'base_uri' => 'https://test', + 'username' => 'login', + 'password' => 'password', + ], $data); + + return true; + }); + }); + + $this->artisan('monica:newaddressbooksubscription', [ + '--email' => $user->email, + '--url' => 'https://test', + '--login' => 'login', + '--password' => 'password', + ])->run(); + } +} diff --git a/tests/Commands/Other/ImportCSVTest.php b/tests/Commands/Other/ImportCSVTest.php new file mode 100644 index 0000000..4d7dd57 --- /dev/null +++ b/tests/Commands/Other/ImportCSVTest.php @@ -0,0 +1,87 @@ +getUser(); + $path = base_path('tests/stubs/single_contact_stub.csv'); + + $totalContacts = Contact::where('account_id', $user->account_id)->count(); + + $this->artisan('import:csv', [ + 'user' => $user->email, + 'file' => $path, + ]) + ->assertSuccessful() + ->run(); + + $this->assertDatabaseHas('contacts', [ + 'first_name' => 'Bono', + 'last_name' => 'Hewson', + ]); + + $this->assertDatabaseHas('contact_fields', [ + 'data' => 'bono@example.com', + ]); + + // Allows checking if birthday was correctly set + $this->assertDatabaseHas('special_dates', [ + 'date' => '1960-05-10', + ]); + + // Asserts that only 3 new contacts were created + $this->assertEquals( + $totalContacts + 1, + Contact::where('account_id', $user->account_id)->count() + ); + } + + /** @test */ + public function csv_import_validates_user() + { + $path = base_path('tests/stubs/single_contact_stub.csv'); + + $this->artisan('import:csv', [ + 'user' => 'test@test.com', + 'file' => $path, + ]) + ->assertFailed() + ->expectsOutput('You need to provide a valid User ID or email address!') + ->run(); + } + + /** @test */ + public function csv_import_validates_file() + { + $user = $this->getUser(); + + $this->artisan('import:csv', [ + 'user' => $user->email, + 'file' => 'xxx', + ]) + ->assertFailed() + ->expectsOutput('You need to provide a valid file path.') + ->run(); + } + + private function getUser() + { + $account = Account::createDefault('John', 'Doe', 'johndoe@example.com', 'secret', null, 'en'); + + return $account->users()->first(); + } +} diff --git a/tests/Commands/Other/ImportVCardsTest.php b/tests/Commands/Other/ImportVCardsTest.php new file mode 100644 index 0000000..2c5056f --- /dev/null +++ b/tests/Commands/Other/ImportVCardsTest.php @@ -0,0 +1,104 @@ +artisan('import:vcard', ['--user' => 'notfound@example.com', '--path' => $path, '--no-interaction' => true]) + ->assertFailed() + ->expectsOutput('No user with that email.') + ->run(); + } + + /** @test */ + public function it_validates_file() + { + $user = $this->getUser(); + + $this->artisan('import:vcard', ['--user' => $user->email, '--path' => 'not_found', '--no-interaction' => true]) + ->assertFailed() + ->expectsOutput('The provided vcard file was not found or is not valid!') + ->run(); + } + + /** @test */ + public function it_imports_contacts() + { + Storage::fake('public'); + + $user = $this->getUser(); + $path = base_path('tests/stubs/vcard_stub.vcf'); + + $totalContacts = Contact::where('account_id', $user->account_id)->count(); + + $this->artisan('import:vcard', ['--user' => $user->email, '--path' => $path, '--no-interaction' => true]) + ->assertSuccessful() + ->run(); + + $this->assertDatabaseHas('contacts', [ + 'first_name' => 'John', + 'last_name' => 'Doe', + ]); + + $this->assertDatabaseHas('contact_fields', [ + 'data' => 'john.doe@example.com', + ]); + + // Allows checking if birthday was correctly set + $this->assertDatabaseHas('special_dates', [ + 'date' => '1960-05-10', + ]); + + // Allows checking nickname fallback + $this->assertDatabaseHas('contacts', [ + 'first_name' => 'Johnny', + ]); + + $this->assertDatabaseHas('contacts', [ + 'company' => 'U2', + 'job' => 'Lead vocalist', + ]); + + // Allows checking addresses are correctly saved + $this->assertDatabaseHas('places', [ + 'street' => '17 Shakespeare Ave.', + 'postal_code' => 'SO17 2HB', + 'city' => 'Southampton', + 'country' => 'GB', + ]); + + $this->assertDatabaseHas('contact_fields', [ + 'data' => 'bono@example.com', + ]); + + $this->assertDatabaseHas('contact_fields', [ + 'data' => '+1 202-555-0191', + ]); + + // Asserts that only 3 new contacts were created + $this->assertEquals( + $totalContacts + 3, + Contact::where('account_id', $user->account_id)->count() + ); + } + + private function getUser() + { + $account = Account::createDefault('John', 'Doe', 'johndoe@example.com', 'secret', null, 'en'); + + return $account->users()->first(); + } +} diff --git a/tests/Commands/Other/UpdateCommandTest.php b/tests/Commands/Other/UpdateCommandTest.php new file mode 100644 index 0000000..cfc0b0a --- /dev/null +++ b/tests/Commands/Other/UpdateCommandTest.php @@ -0,0 +1,59 @@ +artisan('monica:update')->run(); + + $this->assertCount(9, $fake->buffer); + $this->assertCommandContains($fake->buffer[0], 'Maintenance mode: on', 'php artisan down'); + $this->assertCommandContains($fake->buffer[1], 'Resetting application cache', 'php artisan cache:clear'); + $this->assertCommandContains($fake->buffer[2], 'Clear config cache', 'php artisan config:clear'); + $this->assertCommandContains($fake->buffer[3], 'Clear route cache', 'php artisan route:clear'); + $this->assertCommandContains($fake->buffer[4], 'Clear view cache', 'php artisan view:clear'); + $this->assertCommandContains($fake->buffer[5], 'Performing migrations', 'php artisan migrate'); + $this->assertCommandContains($fake->buffer[6], 'Check for encryption keys', 'php artisan monica:passport'); + $this->assertCommandContains($fake->buffer[7], 'Ping for new version', 'php artisan monica:ping'); + $this->assertCommandContains($fake->buffer[8], 'Maintenance mode: off', 'php artisan up'); + } + + /** @test */ + public function update_command_composer() + { + /** @var \Tests\Helpers\CommandCallerFake */ + $fake = Command::fake(); + + $this->artisan('monica:update', ['--composer-install' => true])->run(); + + $this->assertCount(10, $fake->buffer); + $this->assertCommandContains($fake->buffer[0], 'Maintenance mode: on', 'php artisan down'); + $this->assertCommandContains($fake->buffer[1], 'Resetting application cache', 'php artisan cache:clear'); + $this->assertCommandContains($fake->buffer[2], 'Clear config cache', 'php artisan config:clear'); + $this->assertCommandContains($fake->buffer[3], 'Clear route cache', 'php artisan route:clear'); + $this->assertCommandContains($fake->buffer[4], 'Clear view cache', 'php artisan view:clear'); + $this->assertCommandContains($fake->buffer[5], 'Updating composer dependencies', 'composer install'); + $this->assertCommandContains($fake->buffer[6], 'Performing migrations', 'php artisan migrate'); + $this->assertCommandContains($fake->buffer[7], 'Check for encryption keys', 'php artisan monica:passport'); + $this->assertCommandContains($fake->buffer[8], 'Ping for new version', 'php artisan monica:ping'); + $this->assertCommandContains($fake->buffer[9], 'Maintenance mode: off', 'php artisan up'); + } + + private function assertCommandContains($array, $message, $command) + { + $this->assertStringContainsString($message, $array['message']); + $this->assertStringContainsString($command, $array['command']); + } +} diff --git a/tests/Commands/Scheduling/CalculateStatisticsTest.php b/tests/Commands/Scheduling/CalculateStatisticsTest.php new file mode 100644 index 0000000..6db686b --- /dev/null +++ b/tests/Commands/Scheduling/CalculateStatisticsTest.php @@ -0,0 +1,26 @@ +artisan('monica:calculatestatistics')->run(); + } catch (QueryException $e) { + $runsWell = false; + } + + $this->assertTrue($runsWell); + } +} diff --git a/tests/Commands/Scheduling/CronEventTest.php b/tests/Commands/Scheduling/CronEventTest.php new file mode 100644 index 0000000..f0d0d6a --- /dev/null +++ b/tests/Commands/Scheduling/CronEventTest.php @@ -0,0 +1,119 @@ +create(); + + $event = CronEvent::command($cron->command); + + $this->assertEquals($event->cron()->id, $cron->id); + } + + /** @test */ + public function now_not_due() + { + $cron = factory(Cron::class)->create(); + $event = new CronEvent($cron); + + $this->assertFalse($event->isDue()); + } + + /** @test */ + public function next_minute_is_due() + { + Carbon::setTestNow(Carbon::create(2019, 5, 1, 7, 0, 0)); + + $cron = factory(Cron::class)->create(); + $event = new CronEvent($cron); + + $this->assertFalse($event->isDue()); + + Carbon::setTestNow(Carbon::create(2019, 5, 1, 7, 1, 0)); + + $this->assertTrue($event->isDue()); + + $this->assertDatabaseHas('crons', [ + 'command' => $cron->command, + 'last_run' => '2019-05-01 07:01:00', + ]); + } + + /** @test */ + public function hourly_cron() + { + Carbon::setTestNow(Carbon::create(2019, 5, 1, 7, 0, 0)); + + $cron = factory(Cron::class)->create(); + $event = new CronEvent($cron); + $event->hourly(); + + $this->assertFalse($event->isDue()); + + Carbon::setTestNow(Carbon::create(2019, 5, 1, 8, 22, 0)); + + $this->assertTrue($event->isDue()); + + $this->assertDatabaseHas('crons', [ + 'command' => $cron->command, + 'last_run' => '2019-05-01 08:22:00', + ]); + + Carbon::setTestNow(Carbon::create(2019, 5, 1, 8, 59, 0)); + + $this->assertFalse($event->isDue()); + + Carbon::setTestNow(Carbon::create(2019, 5, 1, 9, 01, 0)); + + $this->assertTrue($event->isDue()); + + $this->assertDatabaseHas('crons', [ + 'command' => $cron->command, + 'last_run' => '2019-05-01 09:01:00', + ]); + } + + /** @test */ + public function daily_cron() + { + Carbon::setTestNow(Carbon::create(2019, 5, 1, 7, 0, 0)); + + $cron = factory(Cron::class)->create(); + $event = new CronEvent($cron); + $event->daily(); + + Carbon::setTestNow(Carbon::create(2019, 5, 2, 8, 10, 0)); + + $this->assertTrue($event->isDue()); + + $this->assertDatabaseHas('crons', [ + 'command' => $cron->command, + 'last_run' => '2019-05-02 08:10:00', + ]); + + Carbon::setTestNow(Carbon::create(2019, 5, 2, 10, 0, 0)); + + $this->assertFalse($event->isDue()); + + Carbon::setTestNow(Carbon::create(2019, 5, 3, 0, 0, 0)); + + $this->assertTrue($event->isDue()); + + $this->assertDatabaseHas('crons', [ + 'command' => $cron->command, + 'last_run' => '2019-05-03 00:00:00', + ]); + } +} diff --git a/tests/Commands/Scheduling/DavClientsUpdateTest.php b/tests/Commands/Scheduling/DavClientsUpdateTest.php new file mode 100644 index 0000000..fe37add --- /dev/null +++ b/tests/Commands/Scheduling/DavClientsUpdateTest.php @@ -0,0 +1,28 @@ +create(); + + $this->artisan('monica:davclients')->run(); + + Queue::assertPushed(SynchronizeAddressBooks::class, function ($job) use ($subscription) { + return $job->subscription->id === $subscription->id; + }); + } +} diff --git a/tests/Commands/Scheduling/PingVersionServerTest.php b/tests/Commands/Scheduling/PingVersionServerTest.php new file mode 100644 index 0000000..26f2b54 --- /dev/null +++ b/tests/Commands/Scheduling/PingVersionServerTest.php @@ -0,0 +1,101 @@ + 'https://version.test/ping']); + config(['monica.app_version' => '2.9.0']); + config(['monica.check_version' => true]); + + Instance::all()->each(function ($instance) { + $instance->delete(); + }); + $instance = factory(Instance::class)->create(); + + $ret = [ + 'new_version' => true, + 'latest_version' => '3.1.0', + 'number_of_versions_since_user_version' => 2, + 'notes' => 'notes', + ]; + + Http::fake([ + 'https://version.test/*' => Http::response($ret, 200), + ]); + + $this->artisan('monica:ping')->run(); + + $instance->refresh(); + + $this->assertEquals('3.1.0', $instance->latest_version); + $this->assertEquals('notes', $instance->latest_release_notes); + $this->assertEquals(2, $instance->number_of_versions_since_current_version); + } + + /** @test */ + public function it_clear_instance() + { + config(['monica.weekly_ping_server_url' => 'https://version.test/ping']); + config(['monica.app_version' => '3.1.0']); + + Instance::all()->each(function ($instance) { + $instance->delete(); + }); + $instance = factory(Instance::class)->create([ + 'latest_version' => '3.1.0', + ]); + + $ret = [ + 'new_version' => false, + 'latest_version' => '2.9.0', + 'number_of_versions_since_user_version' => 0, + 'notes' => '', + ]; + + Http::fake([ + 'https://version.test/*' => Http::response($ret, 200), + ]); + + $this->artisan('monica:ping')->run(); + + $instance->refresh(); + + $this->assertEquals('3.1.0', $instance->latest_version); + $this->assertNull($instance->latest_release_notes); + $this->assertNull($instance->number_of_versions_since_current_version); + } + + /** + * If an instance sets `version_check` env variable to false, the command + * should exit with 0. + * + * @return void + */ + public function test_check_version_set_to_false_disables_the_check() + { + config(['monica.weekly_ping_server_url' => 'https://version.test/ping']); + config(['monica.app_version' => '2.9.0']); + config(['monica.check_version' => false]); + + $fake = Http::fake([ + 'https://version.test/*' => Http::response([], 500), + ]); + + $this->artisan('monica:ping') + ->assertSuccessful() + ->run(); + + $fake->assertNothingSent(); + } +} diff --git a/tests/Commands/Scheduling/SendRemindersTest.php b/tests/Commands/Scheduling/SendRemindersTest.php new file mode 100644 index 0000000..c296140 --- /dev/null +++ b/tests/Commands/Scheduling/SendRemindersTest.php @@ -0,0 +1,78 @@ +create([ + 'default_time_reminder_is_sent' => '07:00', + ]); + $contact = factory(Contact::class)->create(['account_id' => $account->id]); + $user = factory(User::class)->create(['account_id' => $account->id]); + $reminder = factory(Reminder::class)->create([ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + 'initial_date' => '2017-01-01', + ]); + factory(ReminderOutbox::class)->create([ + 'account_id' => $account->id, + 'reminder_id' => $reminder->id, + 'user_id' => $user->id, + 'planned_date' => '2017-01-01', + ]); + + $this->artisan('send:reminders')->run(); + Bus::assertDispatched(NotifyUserAboutReminder::class); + } + + /** @test */ + public function it_doesnt_schedule_a_notification_if_it_is_not_the_right_time() + { + Bus::fake(); + + Carbon::setTestNow(Carbon::create(2017, 1, 1, 7, 0, 0)); + + $account = factory(Account::class)->create([ + 'default_time_reminder_is_sent' => '08:00', + ]); + + $contact = factory(Contact::class)->create(['account_id' => $account->id]); + $user = factory(User::class)->create([ + 'account_id' => $account->id, + ]); + $reminder = factory(Reminder::class)->create([ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + 'initial_date' => '2017-01-01', + ]); + $reminderOutbox = factory(ReminderOutbox::class)->create([ + 'account_id' => $account->id, + 'reminder_id' => $reminder->id, + 'user_id' => $user->id, + 'planned_date' => '2017-01-01', + ]); + + $this->artisan('send:reminders')->run(); + Bus::assertNotDispatched(NotifyUserAboutReminder::class); + } +} diff --git a/tests/Commands/Scheduling/SendStayInTouchTest.php b/tests/Commands/Scheduling/SendStayInTouchTest.php new file mode 100644 index 0000000..36b2261 --- /dev/null +++ b/tests/Commands/Scheduling/SendStayInTouchTest.php @@ -0,0 +1,54 @@ +create([]); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + 'stay_in_touch_trigger_date' => '2017-01-01 07:00:00', + 'stay_in_touch_frequency' => 30, + ]); + + $this->artisan('send:stay_in_touch')->run(); + + Bus::assertDispatched(ScheduleStayInTouch::class); + } + + /** @test */ + public function it_doesnt_schedule_stay_in_touch_jobs_if_no_date_is_found() + { + Bus::fake(); + + Carbon::setTestNow(Carbon::create(2017, 1, 1, 7, 0, 0)); + + $account = factory(Account::class)->create([]); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + 'stay_in_touch_trigger_date' => '2017-03-01 07:00:00', + 'stay_in_touch_frequency' => 30, + ]); + + $this->artisan('send:stay_in_touch')->run(); + + Bus::assertNotDispatched(ScheduleStayInTouch::class); + } +} diff --git a/tests/Commands/Tests/PassportCommandTest.php b/tests/Commands/Tests/PassportCommandTest.php new file mode 100644 index 0000000..ae1c0a0 --- /dev/null +++ b/tests/Commands/Tests/PassportCommandTest.php @@ -0,0 +1,71 @@ +markTestSkipped('Run "php artisan key:generate" before executing these tests.'); + } + + foreach (PersonalAccessClient::all() as $client) { + $client->delete(); + } + } + + /** @test */ + public function passport_command_create() + { + /** @var \Tests\Helpers\CommandCallerFake */ + $fake = Command::fake(); + + $this->artisan('monica:passport')->run(); + + $this->assertCount(1, $fake->buffer, $fake->buffer->implode(',')); + $this->assertCommandContains($fake->buffer[0], '✓ Creating personal access client', 'php artisan passport:client'); + } + + /** @test */ + public function passport_command_already_created() + { + /** @var \Tests\Helpers\CommandCallerFake */ + $fake = Command::fake(); + + PersonalAccessClient::create(); + + $this->artisan('monica:passport')->run(); + + $this->assertCount(0, $fake->buffer, $fake->buffer->implode(',')); + } + + /** @test */ + public function passport_command_env_config() + { + /** @var \Tests\Helpers\CommandCallerFake */ + $fake = Command::fake(); + + config(['passport.private_key' => '-', 'passport.public_key' => '-']); + + $this->artisan('monica:passport')->run(); + + $this->assertCount(1, $fake->buffer, $fake->buffer->implode(',')); + $this->assertCommandContains($fake->buffer[0], '✓ Creating personal access client', 'php artisan passport:client'); + } + + private function assertCommandContains($array, $message, $command) + { + $this->assertStringContainsString($message, $array['message']); + $this->assertStringContainsString($command, $array['command']); + } +} diff --git a/tests/Commands/Tests/SendTestEmailTest.php b/tests/Commands/Tests/SendTestEmailTest.php new file mode 100644 index 0000000..c502c26 --- /dev/null +++ b/tests/Commands/Tests/SendTestEmailTest.php @@ -0,0 +1,55 @@ +artisan('monica:test-email', ['--email' => $exampleEmail]) + ->expectsOutput("Invalid email address: \"$exampleEmail\".") + ->assertFailed() + ->run(); + } + + /** @test */ + public function command_prompts_for_email() + { + $exampleEmail = 'no.at.symbol'; + + $this->artisan('monica:test-email') + ->expectsQuestion('What email address should I send the test email to?', $exampleEmail) + ->expectsOutput("Invalid email address: \"$exampleEmail\".") + ->assertFailed() + ->run(); + } + + /** + * @test + */ + public function command_attempts_to_send_email() + { + $exampleEmail = 'test@example.org'; + + Mail::shouldReceive('raw') + ->once() + ->withArgs(function ($message, $closure) use ($exampleEmail) { + $this->assertEquals( + "Hi $exampleEmail, you requested a test email from Monica.", + $message + ); + + return true; + }); + + $this->artisan('monica:test-email', ['--email' => $exampleEmail]) + ->assertSuccessful() + ->run(); + } +} diff --git a/tests/Commands/Tests/SetupFrontEndTestUserTest.php b/tests/Commands/Tests/SetupFrontEndTestUserTest.php new file mode 100644 index 0000000..7d75515 --- /dev/null +++ b/tests/Commands/Tests/SetupFrontEndTestUserTest.php @@ -0,0 +1,25 @@ +artisan('setup:frontendtestuser')->run(); + + $this->assertEquals($accountCount + 1, Account::count()); + $this->assertEquals($userCount + 1, User::count()); + } +} diff --git a/tests/DuskTestCase.php b/tests/DuskTestCase.php new file mode 100644 index 0000000..0a55f8c --- /dev/null +++ b/tests/DuskTestCase.php @@ -0,0 +1,125 @@ +addArguments(collect([ + '--window-size=1920,1080', + ])->unless($this->hasHeadlessDisabled(), function ($items) { + return $items->merge([ + '--disable-gpu', + '--headless', + ]); + })->all()); + + return RemoteWebDriver::create( + $_ENV['DUSK_DRIVER_URL'] ?? 'http://localhost:9515', + DesiredCapabilities::chrome()->setCapability( + ChromeOptions::CAPABILITY, $options + ) + ); + } + + /** + * Determine whether the Dusk command has disabled headless mode. + * + * @return bool + */ + protected function hasHeadlessDisabled() + { + return isset($_SERVER['DUSK_HEADLESS_DISABLED']) || + isset($_ENV['DUSK_HEADLESS_DISABLED']); + } + + /** + * Return the default user to authenticate. + * + * @return \App\Models\User\User + */ + protected function user() + { + $user = factory(User::class)->create(); + $user->account->populateDefaultFields(); + $user->account->update(['has_access_to_paid_version_for_free' => true]); + + app(AcceptPolicy::class)->execute([ + 'account_id' => $user->account->id, + 'user_id' => $user->id, + 'ip_address' => null, + ]); + + return $user; + } + + public function hasDivAlert(Browser $browser) + { + $res = $browser->elements('alert'); + + return count($res) > 0; + } + + public function hasNotification(Browser $browser) + { + $res = $browser->elements('.notifications'); + + return count($res) > 0; + } + + public function getDivAlert(Browser $browser) + { + $res = $browser->elements('alert'); + if (count($res) > 0) { + return $res[0]; + } + } + + public function getNotification($browser) + { + $res = $browser->elements('.notification'); + if (count($res) > 0) { + return $res[0]; + } + } +} diff --git a/tests/Feature/AccountSubscriptionTest.php b/tests/Feature/AccountSubscriptionTest.php new file mode 100644 index 0000000..dff5e59 --- /dev/null +++ b/tests/Feature/AccountSubscriptionTest.php @@ -0,0 +1,304 @@ +markTestSkipped('Set STRIPE_SECRET to run this test.'); + } else { + config([ + 'services.stripe.secret' => env('STRIPE_SECRET'), + 'monica.requires_subscription' => true, + 'monica.paid_plan_monthly_friendly_name' => 'Monthly', + 'monica.paid_plan_monthly_id' => 'monthly', + 'monica.paid_plan_monthly_price' => 100, + 'monica.paid_plan_annual_friendly_name' => 'Annual', + 'monica.paid_plan_annual_id' => 'annual', + 'monica.paid_plan_annual_price' => 500, + ]); + } + } + + public static function setUpBeforeClass(): void + { + if (empty(env('STRIPE_SECRET'))) { + return; + } + + Stripe::setApiVersion('2019-03-14'); + Stripe::setApiKey(env('STRIPE_SECRET')); + + static::$productId = static::$stripePrefix.'product-'.Str::random(10); + static::$monthlyPlanId = static::$stripePrefix.'monthly-'.Str::random(10); + static::$annualPlanId = static::$stripePrefix.'annual-'.Str::random(10); + + Product::create([ + 'id' => static::$productId, + 'name' => 'Monica Test Product', + 'type' => 'service', + ]); + + Plan::create([ + 'id' => static::$monthlyPlanId, + 'nickname' => 'Monthly', + 'currency' => 'USD', + 'interval' => 'month', + 'billing_scheme' => 'per_unit', + 'amount' => 100, + 'product' => static::$productId, + ]); + Plan::create([ + 'id' => static::$annualPlanId, + 'nickname' => 'Annual', + 'currency' => 'USD', + 'interval' => 'year', + 'billing_scheme' => 'per_unit', + 'amount' => 500, + 'product' => static::$productId, + ]); + } + + public static function tearDownAfterClass(): void + { + parent::tearDownAfterClass(); + + if (static::$monthlyPlanId) { + static::deleteStripeResource(new Plan(static::$monthlyPlanId)); + static::$monthlyPlanId = null; + } + if (static::$annualPlanId) { + static::deleteStripeResource(new Plan(static::$annualPlanId)); + static::$annualPlanId = null; + } + if (static::$productId) { + static::deleteStripeResource(new Product(static::$productId)); + static::$productId = null; + } + } + + protected static function deleteStripeResource($resource) + { + try { + if (method_exists($resource, 'delete')) { + $resource->delete(); + } + } catch (\Stripe\Exception\ApiErrorException $e) { + // + } + } + + public function test_it_throw_an_error_on_subscribe() + { + $user = $this->signin(); + $user->email = 'test_it_throw_an_error_on_subscribe@monica-test.com'; + $user->save(); + + $this->expectException(\App\Exceptions\StripeException::class); + $user->account->subscribe('xxx', 'annual'); + } + + public function test_it_sees_the_plan_names() + { + $user = $this->signin(); + + $response = $this->get('/settings/subscriptions'); + + $response->assertSee('Pick a plan below and join over 0 persons who upgraded their Monica.'); + } + + public function test_it_get_the_plan_name() + { + $user = $this->signin(); + + factory(Subscription::class)->create([ + 'account_id' => $user->account_id, + 'name' => 'Annual', + 'stripe_price' => 'annual', + 'stripe_id' => 'test', + 'quantity' => 1, + ]); + + $this->assertEquals('Annual', $user->account->getSubscribedPlanName()); + } + + public function test_it_throw_an_error_on_cancel() + { + $user = $this->signin(); + + factory(Subscription::class)->create([ + 'account_id' => $user->account_id, + 'name' => 'Annual', + 'stripe_price' => 'annual', + 'stripe_id' => 'test', + 'quantity' => 1, + ]); + + $this->expectException(\App\Exceptions\StripeException::class); + $user->account->subscriptionCancel(); + } + + public function test_it_get_subscription_page() + { + $user = $this->signin(); + + factory(Subscription::class)->create([ + 'account_id' => $user->account_id, + 'name' => 'Annual', + 'stripe_price' => 'annual', + 'stripe_id' => 'sub_X', + 'quantity' => 1, + ]); + + $response = $this->get('/settings/subscriptions'); + + $response->assertSee('You are on the Annual plan. Thanks so much for being a subscriber.'); + } + + public function test_it_get_upgrade_page() + { + $user = $this->signin(); + + $response = $this->get('/settings/subscriptions/upgrade?plan=annual'); + + $response->assertSee('You picked the annual plan.'); + } + + public function test_it_subscribe() + { + $user = $this->signin(); + $user->email = 'test_it_subscribe@monica-test.com'; + $user->save(); + + $response = $this->post('/settings/subscriptions/processPayment', [ + 'payment_method' => 'pm_card_visa', + 'plan' => 'annual', + ]); + + $response->assertRedirect('/settings/subscriptions/upgrade/success'); + } + + // public function test_it_subscribe_with_2nd_auth() + // { + // $user = $this->signin(); + // $user->email = 'test_it_subscribe_with_2nd_auth@monica-test.com'; + // $user->save(); + + // $response = $this->followingRedirects()->post('/settings/subscriptions/processPayment', [ + // 'payment_method' => 'pm_card_threeDSecure2Required', + // 'plan' => 'annual', + // ]); + + // $response->assertSee('Extra confirmation is needed to process your payment.'); + // } + + public function test_it_subscribe_with_error() + { + $user = $this->signin(); + $user->email = 'test_it_subscribe_with_error@monica-test.com'; + $user->save(); + + $response = $this->post('/settings/subscriptions/processPayment', [ + 'payment_method' => 'error', + 'plan' => 'annual', + ], [ + 'HTTP_REFERER' => 'back', + ]); + + $response->assertRedirect('/back'); + } + + public function test_it_does_not_subscribe() + { + $user = $this->signin(); + $user->email = 'test_it_does_not_subscribe@monica-test.com'; + $user->save(); + + try { + $user->account->subscribe('pm_card_chargeDeclined', 'annual'); + } catch (\App\Exceptions\StripeException $e) { + $this->assertEquals('Your card was declined. Decline message is: Your card was declined.', $e->getMessage()); + + return; + } + $this->fail(); + } + + public function test_it_get_blank_page_on_update_if_not_subscribed() + { + $this->signin(); + + $response = $this->get('/settings/subscriptions/update'); + + $response->assertSee('Upgrade Monica today and have more meaningful relationships.'); + } + + public function test_it_get_subscription_update() + { + $user = $this->signin(); + $user->email = 'test_it_subscribe@monica-test.com'; + $user->save(); + + $response = $this->post('/settings/subscriptions/processPayment', [ + 'payment_method' => 'pm_card_visa', + 'plan' => 'annual', + ]); + + $response = $this->get('/settings/subscriptions/update'); + + $response->assertSee('Monthly – $1.00'); + $response->assertSee('Annual – $5.00'); + } + + public function test_it_process_subscription_update() + { + $user = $this->signin(); + $user->email = 'test_it_subscribe@monica-test.com'; + $user->save(); + + $response = $this->post('/settings/subscriptions/processPayment', [ + 'payment_method' => 'pm_card_visa', + 'plan' => 'monthly', + ]); + + $response = $this->followingRedirects()->post('/settings/subscriptions/update', [ + 'frequency' => 'annual', + ]); + + $response->assertSee('You are on the Annual plan.'); + } +} diff --git a/tests/Feature/ActivityTest.php b/tests/Feature/ActivityTest.php new file mode 100644 index 0000000..b3179c5 --- /dev/null +++ b/tests/Feature/ActivityTest.php @@ -0,0 +1,574 @@ + [ + 'total', + 'contacts', + ], + 'emotions', + 'account' => [ + 'id', + ], + 'created_at', + 'updated_at', + ]; + + protected $jsonStructureContacts = [ + 'id', + 'name', + ]; + + protected $jsonActivity = [ + 'id', + 'object', + 'summary', + 'description', + 'happened_at', + 'activity_type' => [ + 'id', + 'object', + 'name', + 'location_type', + 'activity_type_category' => [ + 'id', + 'object', + 'name', + 'account' => [ + 'id', + ], + 'created_at', + 'updated_at', + ], + 'account'=> [ + 'id', + ], + 'created_at', + 'updated_at', + ], + 'attendees' => [ + 'total', + 'contacts' => [ + '*' => [ + 'id', + 'object', + 'first_name', + 'last_name', + 'complete_name', + ], + ], + ], + 'emotions' => [ + '*' => [ + 'id', + 'object', + 'name', + ], + ], + 'account' => [ + 'id', + ], + 'created_at', + 'updated_at', + ]; + + protected $jsonActivityNoCategory = [ + 'id', + 'object', + 'summary', + 'description', + 'happened_at', + 'attendees' => [ + 'total', + 'contacts' => [ + '*' => [ + 'id', + 'object', + 'first_name', + 'last_name', + 'complete_name', + ], + ], + ], + 'emotions' => [ + '*' => [ + 'id', + 'object', + 'name', + ], + ], + 'account' => [ + 'id', + ], + 'created_at', + 'updated_at', + ]; + + private function createActivityAndAttachToContact(User $user, Contact $contact) + { + $activity = factory(Activity::class)->create([ + 'account_id' => $user->account_id, + ]); + $activity->contacts()->syncWithoutDetaching([$contact->id => [ + 'account_id' => $activity->account_id, + ]]); + } + + public function test_it_gets_the_list_of_activities() + { + $user = $this->signin(); + + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $this->createActivityAndAttachToContact($user, $contact); + $this->createActivityAndAttachToContact($user, $contact); + $this->createActivityAndAttachToContact($user, $contact); + + $response = $this->json('GET', '/people/'.$contact->hashID().'/activities'); + + $response->assertStatus(200); + + $response->assertJsonStructure([ + 'data' => [ + '*' => $this->jsonStructure, + ], + ]); + + $this->assertCount( + 3, + $response->decodeResponseJson()['data'] + ); + } + + public function test_it_gets_the_list_of_contacts_to_associate_with_the_activity() + { + $user = $this->signin(); + + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + // also create of other contacts in the account + factory(Contact::class, 3)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('GET', '/people/'.$contact->hashID().'/activities/contacts/'); + + $response->assertStatus(200); + + $response->assertJsonStructure([ + '*' => $this->jsonStructureContacts, + ]); + + $this->assertCount( + 3, + $response->decodeResponseJson() + ); + } + + /** @test */ + public function activities_create() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $activityType = factory(ActivityType::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('POST', '/activities', [ + 'contacts' => [$contact->id], + 'description' => 'the description', + 'summary' => 'the activity', + 'happened_at' => '2018-05-01', + 'activity_type_id' => $activityType->id, + ]); + + $response->assertStatus(201); + $response->assertJsonStructure([ + 'data' => $this->jsonActivity, + ]); + $activity_id = $response->json('data.id'); + $response->assertJsonFragment([ + 'object' => 'activity', + 'id' => $activity_id, + ]); + + $this->assertGreaterThan(0, $activity_id); + $this->assertDatabaseHas('activities', [ + 'account_id' => $user->account_id, + 'id' => $activity_id, + 'summary' => 'the activity', + 'description' => 'the description', + 'happened_at' => '2018-05-01', + ]); + $this->assertDatabaseHas('activity_contact', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'activity_id' => $activity_id, + ]); + } + + /** @test */ + public function activities_create_error_wrong_parameter() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('POST', '/activities', [ + 'contact_id' => [$contact->id], + ]); + + $response->assertStatus(422); + $response->assertJson([ + 'errors' => [ + 'summary' => ['The summary field is required.'], + 'happened_at' => ['The happened at field is required.'], + 'contacts' => ['The contacts field is required.'], + ], + ]); + } + + /** @test */ + public function activities_create_error_bad_account() + { + $this->signin(); + + $contact = factory(Contact::class)->create(); + + $response = $this->json('POST', '/activities', [ + 'contacts' => [$contact->id], + 'description' => 'the description', + 'summary' => 'the activity', + 'happened_at' => '2018-05-01', + ]); + + $response->assertStatus(404); + $response->assertJson([ + 'message' => "No query results for model [App\\Models\\Contact\\Contact] {$contact->id}", + ]); + } + + /** @test */ + public function activities_create_error_bad_account2() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $activityType = factory(ActivityType::class)->create(); + + $response = $this->json('POST', '/activities', [ + 'contacts' => [$contact->id], + 'description' => 'the description', + 'summary' => 'the activity', + 'happened_at' => '2018-05-01', + 'activity_type_id' => $activityType->id, + ]); + + $response->assertStatus(404); + $response->assertJson([ + 'message' => "No query results for model [App\\Models\\Account\\ActivityType] {$activityType->id}", + ]); + } + + /** @test */ + public function activities_update() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $activity = factory(Activity::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('PUT', '/activities/'.$activity->id, [ + 'contacts' => [$contact->id], + 'description' => 'the description', + 'summary' => 'the activity', + 'happened_at' => '2018-05-01', + ]); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => $this->jsonActivityNoCategory, + ]); + $activity_id = $response->json('data.id'); + $this->assertEquals($activity->id, $activity_id); + $response->assertJsonFragment([ + 'object' => 'activity', + 'id' => $activity_id, + ]); + + $this->assertGreaterThan(0, $activity_id); + $this->assertDatabaseHas('activities', [ + 'account_id' => $user->account_id, + 'id' => $activity_id, + 'summary' => 'the activity', + 'happened_at' => '2018-05-01', + ]); + $this->assertDatabaseHas('activity_contact', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'activity_id' => $activity_id, + ]); + } + + /** @test */ + public function activities_update_category() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $activity = factory(Activity::class)->create([ + 'account_id' => $user->account_id, + ]); + $activityType = factory(ActivityType::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('PUT', '/activities/'.$activity->id, [ + 'contacts' => [$contact->id], + 'description' => 'the description', + 'summary' => 'the activity', + 'happened_at' => '2018-05-01', + 'activity_type_id' => $activityType->id, + ]); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => $this->jsonActivity, + ]); + $activity_id = $response->json('data.id'); + $this->assertEquals($activity->id, $activity_id); + $response->assertJsonFragment([ + 'object' => 'activity', + 'id' => $activity_id, + ]); + + $activity_type_id = $response->json('data.activity_type.id'); + $this->assertEquals($activityType->id, $activity_type_id); + $response->assertJsonFragment([ + 'object' => 'activityType', + 'id' => $activity_type_id, + ]); + + $this->assertGreaterThan(0, $activity_id); + $this->assertDatabaseHas('activities', [ + 'account_id' => $user->account_id, + 'id' => $activity_id, + 'summary' => 'the activity', + 'happened_at' => '2018-05-01', + 'activity_type_id' => $activityType->id, + ]); + $this->assertDatabaseHas('activity_contact', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'activity_id' => $activity_id, + ]); + } + + /** @test */ + public function activities_update_existing() + { + $user = $this->signin(); + $activity = factory(Activity::class)->create([ + 'account_id' => $user->account_id, + ]); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $contact->activities()->attach($activity, [ + 'account_id' => $user->account_id, + ]); + $contact2 = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $contact2->activities()->attach($activity, [ + 'account_id' => $user->account_id, + ]); + $this->assertDatabaseHas('activity_contact', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'activity_id' => $activity->id, + ]); + $this->assertDatabaseHas('activity_contact', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact2->id, + 'activity_id' => $activity->id, + ]); + + $response = $this->json('PUT', '/activities/'.$activity->id, [ + 'contacts' => [$contact->id], + 'description' => 'the description', + 'summary' => 'the activity', + 'happened_at' => '2018-05-01', + ]); + + $response->assertStatus(200); + $response->assertJsonStructure([ + 'data' => $this->jsonActivityNoCategory, + ]); + $activity_id = $response->json('data.id'); + $this->assertEquals($activity->id, $activity_id); + $response->assertJsonFragment([ + 'object' => 'activity', + 'id' => $activity_id, + ]); + + $this->assertGreaterThan(0, $activity_id); + $this->assertDatabaseHas('activities', [ + 'account_id' => $user->account_id, + 'id' => $activity_id, + 'summary' => 'the activity', + 'happened_at' => '2018-05-01', + ]); + $this->assertDatabaseHas('activity_contact', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'activity_id' => $activity_id, + ]); + $this->assertDatabaseMissing('activity_contact', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact2->id, + 'activity_id' => $activity_id, + ]); + } + + /** @test */ + public function activities_update_error_wrong_parameter() + { + $user = $this->signin(); + + $response = $this->json('PUT', '/activities/0', [ + 'description' => 'the description', + 'summary' => 'the activity', + 'happened_at' => '2018-05-01', + ]); + + $response->assertStatus(404); + $response->assertJson([ + 'message' => 'No query results for model [App\\Models\\Account\\Activity] 0', + ]); + } + + /** @test */ + public function activities_update_error_wrong_account_for_activity() + { + $user = $this->signin(); + + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $activity = factory(Activity::class)->create(); + + $response = $this->json('PUT', '/activities/'.$activity->id, [ + 'contacts' => [$contact->id], + 'description' => 'the description', + 'summary' => 'the activity', + 'happened_at' => '2018-05-01', + ]); + + $response->assertStatus(404); + $response->assertJson([ + 'message' => "No query results for model [App\\Models\\Account\\Activity] {$activity->id}", + ]); + } + + /** @test */ + public function activities_update_error_wrong_account_for_contacts() + { + $user = $this->signin(); + + $contact = factory(Contact::class)->create(); + $activity = factory(Activity::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('PUT', '/activities/'.$activity->id, [ + 'contacts' => [$contact->id], + 'description' => 'the description', + 'summary' => 'the activity', + 'happened_at' => '2018-05-01', + ]); + + $response->assertStatus(404); + $response->assertJson([ + 'message' => "No query results for model [App\\Models\\Contact\\Contact] {$contact->id}", + ]); + } + + /** @test */ + public function activities_delete() + { + $user = $this->signin(); + $activity = factory(Activity::class)->create([ + 'account_id' => $user->account_id, + ]); + $this->assertDatabaseHas('activities', [ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('DELETE', '/activities/'.$activity->id); + + $response->assertStatus(200); + $this->assertDatabaseMissing('activities', [ + 'account_id' => $user->account_id, + 'id' => $activity->id, + ]); + } + + /** @test */ + public function activities_delete_error() + { + $this->signin(); + + $response = $this->json('DELETE', '/activities/0'); + + $response->assertStatus(404); + $response->assertJson([ + 'message' => 'No query results for model [App\\Models\\Account\\Activity] 0', + ]); + } + + /** @test */ + public function activities_delete_with_wrong_account() + { + $this->signin(); + $activity = factory(Activity::class)->create(); + + $response = $this->json('DELETE', '/activities/'.$activity->id); + + $response->assertStatus(404); + $response->assertJson([ + 'message' => "No query results for model [App\\Models\\Account\\Activity] {$activity->id}", + ]); + } +} diff --git a/tests/Feature/ActivityTypeCategoriesTest.php b/tests/Feature/ActivityTypeCategoriesTest.php new file mode 100644 index 0000000..8c37894 --- /dev/null +++ b/tests/Feature/ActivityTypeCategoriesTest.php @@ -0,0 +1,102 @@ +signin(); + + $activityTypeCategories = factory(ActivityTypeCategory::class, 10)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('GET', '/settings/personalization/activitytypecategories'); + + $response->assertJsonStructure([ + '*' => $this->jsonStructureActivityTypeCategory, + ]); + } + + public function test_it_stores_a_activity_type_category() + { + $user = $this->signin(); + + $response = $this->json('POST', '/settings/personalization/activitytypecategories', [ + 'name' => 'Movies', + ]); + + $response->assertStatus(200); + + $this->assertDatabaseHas('activity_type_categories', [ + 'name' => 'Movies', + ]); + } + + public function test_it_updates_a_activity_type_category() + { + $user = $this->signin(); + + $activityTypeCategory = factory(ActivityTypeCategory::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('PUT', '/settings/personalization/activitytypecategories/'.$activityTypeCategory->id, [ + 'name' => 'Movies', + ]); + + $response->assertStatus(200); + + $this->assertDatabaseHas('activity_type_categories', [ + 'id' => $activityTypeCategory->id, + 'name' => 'Movies', + ]); + } + + public function test_activity_type_category_update_bad_account() + { + $user = $this->signin(); + + $activityTypeCategory = factory(ActivityTypeCategory::class)->create(); + + $response = $this->json('PUT', '/settings/personalization/activitytypecategories/'.$activityTypeCategory->id, [ + 'name' => 'Movies', + ]); + + $response->assertStatus(404); + + $this->assertDatabaseMissing('activity_type_categories', [ + 'id' => $activityTypeCategory->id, + 'name' => 'Movies', + ]); + } + + public function test_it_deletes_a_activity_type_category() + { + $user = $this->signin(); + + $activityTypeCategory = factory(ActivityTypeCategory::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('DELETE', '/settings/personalization/activitytypecategories/'.$activityTypeCategory->id); + + $response->assertStatus(200); + + $this->assertDatabaseMissing('activity_type_categories', [ + 'name' => 'Movies', + ]); + } +} diff --git a/tests/Feature/ActivityTypesTest.php b/tests/Feature/ActivityTypesTest.php new file mode 100644 index 0000000..ade5d55 --- /dev/null +++ b/tests/Feature/ActivityTypesTest.php @@ -0,0 +1,60 @@ +signin(); + + $activityTypeCategory = factory(ActivityTypeCategory::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('POST', '/settings/personalization/activitytypes', [ + 'name' => 'Movies', + 'activity_type_category_id' => $activityTypeCategory->id, + ]); + + $response->assertStatus(200); + + $this->assertDatabaseHas('activity_types', [ + 'name' => 'Movies', + 'activity_type_category_id' => $activityTypeCategory->id, + ]); + } + + public function test_it_updates_a_activity_type() + { + $user = $this->signin(); + + $activityType = factory(ActivityType::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('PUT', '/settings/personalization/activitytypes/'.$activityType->id, [ + 'name' => 'Movies', + 'activity_type_category_id' => $activityType->activity_type_category_id, + ]); + + $response->assertStatus(200); + + $this->assertDatabaseHas('activity_types', [ + 'name' => 'Movies', + ]); + } +} diff --git a/tests/Feature/AddressTest.php b/tests/Feature/AddressTest.php new file mode 100644 index 0000000..6297b0e --- /dev/null +++ b/tests/Feature/AddressTest.php @@ -0,0 +1,133 @@ +signIn(); + + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + return [$user, $contact]; + } + + public function test_users_can_get_countries() + { + $user = $this->signIn(); + + $response = $this->get('/countries'); + + $response->assertStatus(200); + + $countries = CountriesHelper::getAll(); + + $response->assertSee($countries->first()['country']); + } + + public function test_users_can_get_addresses() + { + [$user, $contact] = $this->fetchUser(); + + $address = factory(Address::class)->create([ + 'contact_id' => $contact->id, + 'account_id' => $user->account_id, + 'name' => 'test', + ]); + + $response = $this->get('/people/'.$contact->hashID().'/addresses'); + + $response->assertStatus(200); + + $response->assertSee('test'); + } + + public function test_users_can_add_addresses() + { + [$user, $contact] = $this->fetchUser(); + + $params = [ + 'name' => 'test', + ]; + + $response = $this->post('/people/'.$contact->hashID().'/addresses', $params); + + $response->assertStatus(201); + + $params['account_id'] = $user->account_id; + $params['contact_id'] = $contact->id; + $params['name'] = 'test'; + + $this->assertDatabaseHas('addresses', $params); + + $response = $this->get('/people/'.$contact->hashID().'/addresses'); + + $response->assertStatus(200); + + $response->assertSee('test'); + } + + public function test_users_can_edit_addresses() + { + [$user, $contact] = $this->fetchUser(); + + $params = [ + 'name' => 'test2', + ]; + + $address = factory(Address::class)->create([ + 'contact_id' => $contact->id, + 'account_id' => $user->account_id, + ]); + + $response = $this->put('/people/'.$contact->hashID().'/addresses/'.$address->id, $params); + + $response->assertStatus(200); + + $params['account_id'] = $user->account_id; + $params['contact_id'] = $contact->id; + $params['name'] = 'test2'; + + $this->assertDatabaseHas('addresses', $params); + + $response = $this->get('/people/'.$contact->hashID().'/addresses'); + + $response->assertStatus(200); + + $response->assertSee('test2'); + } + + public function test_users_can_delete_addresses() + { + [$user, $contact] = $this->fetchUser(); + + $address = factory(Address::class)->create([ + 'contact_id' => $contact->id, + 'account_id' => $user->account_id, + ]); + + $response = $this->delete('/people/'.$contact->hashID().'/addresses/'.$address->id); + $response->assertStatus(200); + + $params = ['id' => $address->id]; + + $this->assertDatabaseMissing('addresses', $params); + } +} diff --git a/tests/Feature/Authentication/AuthenticateTest.php b/tests/Feature/Authentication/AuthenticateTest.php new file mode 100644 index 0000000..decb69e --- /dev/null +++ b/tests/Feature/Authentication/AuthenticateTest.php @@ -0,0 +1,16 @@ +get('/people'); + + $response->assertStatus(302); + $response->assertRedirect('/'); + } +} diff --git a/tests/Feature/AvatarTest.php b/tests/Feature/AvatarTest.php new file mode 100644 index 0000000..830b485 --- /dev/null +++ b/tests/Feature/AvatarTest.php @@ -0,0 +1,137 @@ +signIn(); + + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + return [$user, $contact]; + } + + public function test_user_can_add_an_avatar_as_photo() + { + [$user, $contact] = $this->fetchUser(); + + Storage::fake('public'); + $file = UploadedFile::fake()->image('avatar.jpg'); + + $params = [ + 'avatar' => 'upload', + 'photo' => $file, + ]; + + $response = $this->post('/people/'.$contact->hashID().'/avatar', $params); + + $response->assertStatus(302); + + // Assert the photo has been added for the correct user. + $this->assertDatabaseHas('photos', [ + 'account_id' => $user->account_id, + 'original_filename' => 'avatar.jpg', + 'new_filename' => 'photos/'.$file->hashName(), + ]); + $this->assertDatabaseHas('contacts', [ + 'id' => $contact->id, + 'account_id' => $user->account_id, + 'avatar_source' => 'photo', + ]); + $this->assertDatabaseHas('contact_photo', [ + 'contact_id' => $contact->id, + ]); + + Storage::disk('public')->assertExists('photos/'.$file->hashName()); + } + + public function test_user_can_add_an_avatar_as_adorable() + { + [$user, $contact] = $this->fetchUser(); + + $params = [ + 'avatar' => 'adorable', + ]; + + $response = $this->post('/people/'.$contact->hashID().'/avatar', $params); + + $response->assertStatus(302); + + $this->assertDatabaseHas('contacts', [ + 'id' => $contact->id, + 'account_id' => $user->account_id, + 'avatar_source' => 'adorable', + ]); + } + + public function test_user_can_associate_an_avatar_as_photo() + { + [$user, $contact] = $this->fetchUser(); + + Storage::fake('public'); + $file = UploadedFile::fake()->image('avatar.jpg'); + + $params = [ + 'avatar' => 'upload', + 'photo' => $file, + ]; + + $response = $this->post('/people/'.$contact->hashID().'/avatar', $params); + + $response->assertStatus(302); + + $contact->refresh(); + $photo = $contact->photos->first(); + + $contact2 = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->post('/people/'.$contact2->hashID().'/makeProfilePicture/'.$photo->id); + + // Assert the photo has been added for the correct user. + $this->assertDatabaseHas('photos', [ + 'id' => $photo->id, + 'account_id' => $user->account_id, + 'original_filename' => 'avatar.jpg', + 'new_filename' => 'photos/'.$file->hashName(), + ]); + $this->assertDatabaseHas('contacts', [ + 'id' => $contact->id, + 'account_id' => $user->account_id, + 'avatar_source' => 'photo', + ]); + $this->assertDatabaseHas('contacts', [ + 'id' => $contact2->id, + 'account_id' => $user->account_id, + 'avatar_source' => 'photo', + ]); + $this->assertDatabaseHas('contact_photo', [ + 'contact_id' => $contact->id, + 'photo_id' => $photo->id, + ]); + $this->assertDatabaseHas('contact_photo', [ + 'contact_id' => $contact2->id, + 'photo_id' => $photo->id, + ]); + } +} diff --git a/tests/Feature/CallsTest.php b/tests/Feature/CallsTest.php new file mode 100644 index 0000000..762e70c --- /dev/null +++ b/tests/Feature/CallsTest.php @@ -0,0 +1,138 @@ +signin(); + + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + factory(Call::class, 10)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + ]); + + $response = $this->json('GET', '/people/'.$contact->hashID().'/calls'); + + $response->assertStatus(200); + + $response->assertJsonStructure([ + 'data' => [ + '*' => $this->jsonStructure, + ], + ]); + + $this->assertCount( + 10, + $response->decodeResponseJson()['data'] + ); + } + + /** @test */ + public function it_gets_last_talked_to() + { + $user = $this->signin(); + + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $referenceDate = now(); + + app(CreateCall::class)->execute([ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'called_at' => $referenceDate->format('Y-m-d'), + ]); + + $response = $this->json('GET', "/people/{$contact->hashId()}/calls/last"); + + $response->assertStatus(200); + + $response->assertJsonStructure([ + 'last_talked_to', + ]); + + $this->assertEquals($response->json('last_talked_to'), DateHelper::getShortDate($referenceDate)); + } + + /** @test */ + public function it_gets_a_empty_last_talked_to() + { + $user = $this->signin(); + + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('GET', "/people/{$contact->hashId()}/calls/last"); + + $response->assertStatus(200); + + $response->assertJsonStructure([ + 'last_talked_to', + ]); + + $this->assertNull($response->json('last_talked_to')); + } + + public function test_dashboard_calls() + { + $user = $this->signin(); + + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + 'first_name' => 'Éric', + 'last_name' => 'Çezt', + ]); + + factory(Call::class, 10)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + ]); + + $response = $this->json('GET', '/dashboard/calls'); + + $response->assertStatus(200); + + $response->assertJsonStructure([ + '*' => $this->jsonDashboardStructure, + ]); + + $this->assertCount( + 10, + $response->decodeResponseJson() + ); + } +} diff --git a/tests/Feature/ContactFieldTest.php b/tests/Feature/ContactFieldTest.php new file mode 100644 index 0000000..fdd5e58 --- /dev/null +++ b/tests/Feature/ContactFieldTest.php @@ -0,0 +1,159 @@ +signIn(); + + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + return [$user, $contact]; + } + + public function test_user_can_get_contact_fields() + { + [$user, $contact] = $this->fetchUser(); + + $field = factory(ContactFieldType::class)->create([ + 'account_id' => $user->account_id, + ]); + + $contactField = factory(ContactField::class)->create([ + 'contact_id' => $contact->id, + 'account_id' => $user->account_id, + 'contact_field_type_id' => $field->id, + ]); + + $response = $this->get('/people/'.$contact->hashID().'/contactfield'); + + $response->assertStatus(200); + + $response->assertSee($contactField->data); + } + + public function test_user_can_get_contact_field_types() + { + [$user, $contact] = $this->fetchUser(); + + $field = factory(ContactFieldType::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->get('/people/'.$contact->hashID().'/contactfieldtypes'); + + $response->assertStatus(200); + + $response->assertSee($field->name); + } + + public function test_users_can_add_contact_field() + { + [$user, $contact] = $this->fetchUser(); + + $field = factory(ContactFieldType::class)->create([ + 'account_id' => $user->account_id, + 'name' => 'Test Name', + 'type' => 'test', + ]); + + $params = [ + 'contact_field_type_id' => $field->id, + 'data' => 'test_data', + ]; + + $response = $this->post('/people/'.$contact->hashID().'/contactfield', $params); + + $response->assertStatus(201); + + $params['account_id'] = $user->account_id; + $params['contact_id'] = $contact->id; + $params['data'] = 'test_data'; + + $this->assertDatabaseHas('contact_fields', $params); + + $response = $this->get('/people/'.$contact->hashID().'/contactfield'); + + $response->assertStatus(200); + + $response->assertSee('test_data'); + } + + public function test_users_can_edit_contact_field() + { + [$user, $contact] = $this->fetchUser(); + + $params = ['data' => 'test_data']; + + $field = factory(ContactFieldType::class)->create([ + 'account_id' => $user->account_id, + 'name' => 'Test Name', + 'type' => 'test', + ]); + + $contactField = factory(ContactField::class)->create([ + 'contact_id' => $contact->id, + 'account_id' => $user->account_id, + 'contact_field_type_id' => $field->id, + ]); + + $params['id'] = $contactField->id; + $params['contact_field_type_id'] = $field->id; + + $response = $this->put('/people/'.$contact->hashID().'/contactfield/'.$contactField->id, $params); + + $response->assertStatus(200); + + $params['account_id'] = $user->account_id; + $params['contact_id'] = $contact->id; + $params['data'] = 'test_data'; + + $this->assertDatabaseHas('contact_fields', $params); + + $response = $this->get('/people/'.$contact->hashID().'/contactfield'); + + $response->assertStatus(200); + + $response->assertSee('test_data'); + } + + public function test_users_can_delete_addresses() + { + [$user, $contact] = $this->fetchUser(); + + $field = factory(ContactFieldType::class)->create([ + 'account_id' => $user->account_id, + ]); + + $contactField = factory(ContactField::class)->create([ + 'contact_id' => $contact->id, + 'account_id' => $user->account_id, + 'contact_field_type_id' => $field->id, + ]); + + $response = $this->delete('/people/'.$contact->hashID().'/contactfield/'.$contactField->id); + $response->assertStatus(200); + + $params = ['id' => $contactField->id]; + + $this->assertDatabaseMissing('contact_fields', $params); + } +} diff --git a/tests/Feature/ContactTest.php b/tests/Feature/ContactTest.php new file mode 100644 index 0000000..c2787d4 --- /dev/null +++ b/tests/Feature/ContactTest.php @@ -0,0 +1,705 @@ +signIn(); + + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + return [$user, $contact]; + } + + public function test_user_can_query_search_contacts() + { + $user = $this->signIn(); + + factory(Contact::class, 10)->state('named')->create([ + 'account_id' => $user->account_id, + ]); + $randomContact = Contact::where('account_id', $user->account_id) + ->inRandomOrder() + ->first(); + + $keyword = $randomContact->first_name.' '.$randomContact->last_name; + + $records = Contact::search($keyword, $user->account_id, 'id')->get(); + + $this->assertGreaterThanOrEqual(1, count($records)); + } + + public function test_user_can_query_search_no_result() + { + $user = $this->signIn(); + + $contacts = factory(Contact::class, 10)->state('named')->create([ + 'account_id' => $user->account_id, + ]); + + $keyword = 'no_result_with_this_keyword'; + + $records = Contact::search($keyword, $user->account_id, 'id')->get(); + + $this->assertEquals(0, count($records)); + } + + public function test_user_can_search_one_contact_firstname() + { + $user = $this->signIn(); + + factory(Contact::class, 10)->state('named')->create([ + 'account_id' => $user->account_id, + ]); + $randomContact = Contact::where('account_id', $user->account_id) + ->inRandomOrder() + ->first(); + + $response = $this->post('/people/search', [ + 'needle' => $randomContact->first_name, + ]); + + $response->assertSuccessful(); + $response->assertJsonFragment([ + 'id' => $randomContact->id, + 'complete_name' => $randomContact->first_name.' '.$randomContact->last_name, + ]); + } + + public function test_user_can_search_one_contact_lastname() + { + $user = $this->signIn(); + + factory(Contact::class, 10)->state('named')->create([ + 'account_id' => $user->account_id, + ]); + $randomContact = Contact::where('account_id', $user->account_id) + ->inRandomOrder() + ->first(); + + $response = $this->post('/people/search', [ + 'needle' => $randomContact->last_name, + ]); + + $response->assertSuccessful(); + $response->assertJsonFragment([ + 'id' => $randomContact->id, + 'complete_name' => $randomContact->first_name.' '.$randomContact->last_name, + ]); + } + + public function test_user_can_search_one_contact_firstname_lastname() + { + $user = $this->signIn(); + + factory(Contact::class, 10)->state('named')->create([ + 'account_id' => $user->account_id, + ]); + $randomContact = Contact::where('account_id', $user->account_id) + ->inRandomOrder() + ->first(); + + $response = $this->post('/people/search', [ + 'needle' => $randomContact->first_name.' '.$randomContact->last_name, + ]); + + $response->assertSuccessful(); + $response->assertJsonFragment([ + 'id' => $randomContact->id, + 'complete_name' => $randomContact->first_name.' '.$randomContact->last_name, + ]); + } + + public function test_user_can_search_one_contact_lastname_firstname() + { + $user = $this->signIn(); + + factory(Contact::class, 10)->state('named')->create([ + 'account_id' => $user->account_id, + ]); + $randomContact = Contact::where('account_id', $user->account_id) + ->inRandomOrder() + ->first(); + + $response = $this->post('/people/search', [ + 'needle' => $randomContact->last_name.' '.$randomContact->first_name, + ]); + + $response->assertSuccessful(); + $response->assertJsonFragment([ + 'id' => $randomContact->id, + 'complete_name' => $randomContact->first_name.' '.$randomContact->last_name, + ]); + } + + public function test_user_can_search_one_contact_no_result() + { + $user = $this->signIn(); + + factory(Contact::class, 10)->state('named')->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->post('/people/search', [ + 'needle' => 'no_result_with_this needle', + ]); + + $response->assertSuccessful(); + $response->assertJsonFragment([ + 'noResults' => 'No results found', + ]); + } + + public function test_user_can_list_one_contact_firstname() + { + $user = $this->signIn(); + + factory(Contact::class, 10)->state('named')->create([ + 'account_id' => $user->account_id, + ]); + $randomContact = Contact::where('account_id', $user->account_id) + ->inRandomOrder() + ->first(); + + $response = $this->get('/people/list?search='.$randomContact->first_name); + + $response->assertSuccessful(); + $response->assertJsonFragment([ + 'id' => $randomContact->id, + 'complete_name' => $randomContact->first_name.' '.$randomContact->last_name, + ]); + } + + public function test_user_can_list_contacts_with_tags() + { + $user = $this->signIn(); + + factory(Contact::class, 10)->state('named')->create([ + 'account_id' => $user->account_id, + ]); + $contact = Contact::where('account_id', $user->account_id) + ->inRandomOrder() + ->first(); + + $tag = factory(Tag::class)->create([ + 'account_id' => $user->account_id, + 'name' => 'c++', + ]); + $contact->tags()->sync([ + $tag->id => [ + 'account_id' => $user->account_id, + ], + ]); + + $response = $this->get('/people/list?tags[]='.urlencode($tag->name)); + + $response->assertSuccessful(); + $response->assertJsonFragment([ + 'id' => $contact->id, + 'complete_name' => $contact->first_name.' '.$contact->last_name, + ]); + $response->assertJsonCount(1, 'contacts'); + } + + public function test_user_can_show_contacts_with_tags() + { + $user = $this->signIn(); + + factory(Contact::class, 10)->state('named')->create([ + 'account_id' => $user->account_id, + ]); + $contact = Contact::where('account_id', $user->account_id) + ->inRandomOrder() + ->first(); + + $tag = factory(Tag::class)->create([ + 'account_id' => $user->account_id, + 'name' => 'c++', + ]); + $contact->tags()->sync([ + $tag->id => [ + 'account_id' => $user->account_id, + ], + ]); + + $response = $this->get('/people?tags[]='.urlencode($tag->name)); + + $response->assertSuccessful(); + $response->assertSee('1 contact'); + } + + public function test_user_can_see_contacts() + { + [$user, $contact] = $this->fetchUser(); + $response = $this->get('/people'); + $response->assertSee('1 contact'); + } + + private function setUpContacts() + { + $user = $this->signIn(); + + $contacts = factory(Contact::class, 10)->create([ + 'account_id' => $user->account_id, + ]); + + foreach ($contacts as $contact) { + factory(Activity::class)->create([ + 'account_id' => $contact->account_id, + ]); + } + } + + public function test_user_can_see_contacts_sorted_by_lastactivitydateNewtoOld() + { + $this->setUpContacts(); + + $response = $this->get('/people/list?sort=lastactivitydateNewtoOld'); + + $response->assertJsonFragment([ + 'totalRecords' => 10, + ]); + } + + public function test_user_can_see_contacts_sorted_by_lastactivitydateOldtoNew() + { + $this->setUpContacts(); + + $response = $this->get('/people/list?sort=lastactivitydateOldtoNew'); + + $response->assertJsonFragment([ + 'totalRecords' => 10, + ]); + } + + public function test_user_can_be_reminded_about_an_event_once() + { + [$user, $contact] = $this->fetchUser(); + + $reminder = [ + 'title' => $this->faker->sentence('5'), + 'initial_date' => DateHelper::getDate(DateHelper::parseDateTime($this->faker->dateTimeBetween('now', '+2 years'))), + 'frequency_type' => 'one_time', + 'description' => $this->faker->sentence(), + ]; + + $this->post( + route('people.reminders.store', $contact), + $reminder + ); + + $this->assertDatabaseHas( + 'reminders', + array_merge($reminder, [ + 'frequency_type' => 'one_time', + 'contact_id' => $contact->id, + 'account_id' => $user->account_id, + ]) + ); + } + + public function test_user_can_add_a_task_to_a_contact() + { + [$user, $contact] = $this->fetchUser(); + + $task = [ + 'title' => $this->faker->sentence(), + 'description' => $this->faker->sentence(3), + 'completed' => 0, + 'contact_id' => $contact->id, + ]; + + $this->post( + '/tasks', + $task + ); + + $this->assertDatabaseHas( + 'tasks', + $task + [ + 'contact_id' => $contact->id, + 'account_id' => $user->account_id, + ] + ); + } + + public function test_user_can_be_in_debt_to_a_contact() + { + [$user, $contact] = $this->fetchUser(); + + $debt = [ + 'in_debt' => 'yes', + 'amount' => $this->faker->numberBetween(1, 5000), + 'reason' => $this->faker->sentence(), + ]; + + $response = $this->post( + route('people.debts.store', $contact), + $debt + ); + $response->assertStatus(302); + + $debt['amount'] = $debt['amount'] * 100; + $this->assertDatabaseHas('debts', + $debt + [ + 'contact_id' => $contact->id, + 'account_id' => $user->account_id, + ]); + } + + public function test_user_can_be_owed_debt_by_a_contact() + { + [$user, $contact] = $this->fetchUser(); + + $debt = [ + 'in_debt' => 'no', + 'amount' => $this->faker->numberBetween(1, 5000), + 'reason' => $this->faker->sentence(), + ]; + + $response = $this->post( + route('people.debts.store', $contact), + $debt + ); + $response->assertStatus(302); + + $debt['amount'] = $debt['amount'] * 100; + $this->assertDatabaseHas('debts', + $debt + [ + 'contact_id' => $contact->id, + 'account_id' => $user->account_id, + ]); + } + + public function test_a_contact_edit_food_preferences() + { + [$user, $contact] = $this->fetchUser(); + + $response = $this->get('/people/'.$contact->hashID().'/food'); + + $response->assertStatus(200); + $response->assertSee('Indicate food preferences'); + } + + public function test_a_contact_can_have_food_preferences() + { + [$user, $contact] = $this->fetchUser(); + + $food = ['food' => $this->faker->sentence()]; + + $this->post('/people/'.$contact->hashID().'/food/save', $food); + + $this->assertDatabaseHas('contacts', [ + 'id' => $contact->id, + 'food_preferences' => $food['food'], + ]); + } + + public function test_a_contact_edit_work() + { + [$user, $contact] = $this->fetchUser(); + + $response = $this->get('/people/'.$contact->hashID().'/work/edit'); + + $response->assertStatus(200); + $response->assertSee("Update {$contact->first_name}’s job information"); + } + + public function test_a_contact_can_update_work() + { + [$user, $contact] = $this->fetchUser(); + + $input = [ + 'job' => $this->faker->sentence(), + 'company' => $this->faker->sentence(), + ]; + + $response = $this->post('/people/'.$contact->hashID().'/work/update', $input); + $response->assertStatus(302); + + $this->assertDatabaseHas('contacts', [ + 'id' => $contact->id, + 'job' => $input['job'], + 'company' => $input['company'], + ]); + } + + public function test_a_contact_can_have_its_last_name_removed() + { + [$user, $contact] = $this->fetchUser(); + + $data = [ + 'firstname' => $contact->first_name, + 'lastname' => '', + 'gender' => $contact->gender_id, + 'birthdate' => 'unknown', + ]; + + $this->put('/people/'.$contact->hashID(), $data); + + $data['id'] = $contact->id; + $this->assertDatabaseHas('contacts', [ + 'id' => $contact->id, + 'last_name' => null, + ]); + } + + public function test_user_cant_add_new_contacts_if_limit_reached() + { + [$user, $contact] = $this->fetchUser(); + + $contacts = factory(Contact::class, 3)->create([ + 'account_id' => $user->account_id, + ]); + + config(['monica.number_of_allowed_contacts_free_account' => 1]); + config(['monica.requires_subscription' => true]); + + $response = $this->get('/people/add'); + + $response->assertRedirect('/settings/subscriptions'); + } + + public function test_user_can_add_new_contacts_when_instance_requires_no_subscription() + { + [$user, $contact] = $this->fetchUser(); + + $contacts = factory(Contact::class, 3)->create([ + 'account_id' => $user->account_id, + ]); + + config(['monica.number_of_allowed_contacts_free_account' => 1]); + config(['monica.requires_subscription' => false]); + + $response = $this->get('/people/add'); + + $response->assertStatus(200); + } + + public function test_viewing_a_user_increments_the_number_of_views() + { + [$user, $contact] = $this->fetchUser(); + + $this->assertDatabaseHas('contacts', [ + 'number_of_views' => 0, + ]); + + $this->get('/people/'.$contact->hashID()); + $this->get('/people/'.$contact->hashID()); + + $this->assertDatabaseHas('contacts', [ + 'number_of_views' => 2, + ]); + } + + public function test_vcard_download() + { + [$user, $contact] = $this->fetchUser(); + + $response = $this->get('/people/'.$contact->hashID().'/vcard'); + + $response->assertOk(); + $response->assertHeader('Content-type', 'text/x-vcard; charset=UTF-8'); + $response->assertSee('FN:John Doe'); + $response->assertSee('N:Doe;John;;;'); + } + + public function test_edit_contact_has_specialdeceased() + { + [$user, $contact] = $this->fetchUser(); + + $response = $this->get('/people/'.$contact->hashID().'/edit'); + + $response->assertSee(' + ', false); + } + + public function test_edit_contact_with_specialdeceased() + { + [$user, $contact] = $this->fetchUser(); + + $reminder = factory(Reminder::class)->create([ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + ]); + + $contact->is_dead = true; + $contact->deceased_reminder_id = $reminder->id; + $contact->save(); + + $response = $this->get('/people/'.$contact->hashID().'/edit'); + + $response->assertSee(' + ', false); + } + + public function test_edit_contact_put_deceased() + { + [$user, $contact] = $this->fetchUser(); + + $data = [ + 'firstname' => $contact->first_name, + 'lastname' => $contact->last_name, + 'gender' => $contact->gender_id, + 'birthdate' => 'unknown', + 'is_deceased' => 'true', + 'is_deceased_date_known' => 'true', + 'deceased_date' => '2012-06-22', + ]; + + $this->put('/people/'.$contact->hashID(), $data); + + $data['id'] = $contact->id; + $this->assertDatabaseHas('contacts', [ + 'id' => $contact->id, + 'is_dead' => true, + ]); + + $contact->refresh(); + $this->assertDatabaseHas('special_dates', [ + 'id' => $contact->deceased_special_date_id, + 'date' => '2012-06-22', + ]); + } + + public function test_edit_contact_put_deceased_dont_stay_in_touch() + { + [$user, $contact] = $this->fetchUser(); + + $data = [ + 'firstname' => $contact->first_name, + 'lastname' => $contact->last_name, + 'gender' => $contact->gender_id, + 'birthdate' => 'unknown', + 'is_deceased' => 'true', + 'is_deceased_date_known' => 'true', + 'deceased_date' => '2012-06-22', + 'stay_in_touch_frequency' => 11, + 'stay_in_touch_trigger_date' => '2012-06-22', + ]; + + $this->put('/people/'.$contact->hashID(), $data); + + $contact->updateStayInTouchFrequency(0); + $contact->setStayInTouchTriggerDate(0); + + $data['id'] = $contact->id; + $this->assertDatabaseHas('contacts', [ + 'id' => $contact->id, + 'is_dead' => true, + 'stay_in_touch_frequency' => null, + 'stay_in_touch_trigger_date' => null, + ]); + } + + public function test_edit_contact_put_deceased_with_reminder() + { + [$user, $contact] = $this->fetchUser(); + + $data = [ + 'firstname' => $contact->first_name, + 'lastname' => $contact->last_name, + 'gender' => $contact->gender_id, + 'birthdate' => 'unknown', + 'is_deceased' => 'true', + 'is_deceased_date_known' => 'true', + 'deceased_date' => '2012-06-22', + 'add_reminder_deceased' => 'true', + ]; + + $this->put('/people/'.$contact->hashID(), $data); + + $this->assertDatabaseHas('contacts', [ + 'id' => $contact->id, + 'is_dead' => true, + ]); + + $contact->refresh(); + $this->assertDatabaseHas('special_dates', [ + 'id' => $contact->deceased_special_date_id, + 'date' => '2012-06-22', + ]); + $this->assertDatabaseHas('reminders', [ + 'id' => $contact->deceased_reminder_id, + 'contact_id' => $contact->id, + 'initial_date' => '2012-06-22', + ]); + } + + public function test_it_create_a_contact() + { + $user = $this->signIn(); + + $gender = factory(Gender::class)->create([ + 'account_id' => $user->account_id, + ]); + + $data = [ + 'first_name' => 'John', + 'last_name' => 'Doe', + 'middle_name' => 'Mike', + 'gender' => $gender->id, + ]; + + $response = $this->post('/people', $data); + + $response->assertStatus(302); + + $this->assertDatabaseHas('contacts', [ + 'first_name' => 'John', + 'last_name' => 'Doe', + 'middle_name' => 'Mike', + 'gender_id' => $gender->id, + ]); + } + + /** @test */ + public function it_gets_the_value() + { + $user = $this->signin(); + $currency = factory(Currency::class)->create([ + 'iso' => 'USD', + 'symbol' => '$', + ]); + $user->currency()->associate($currency); + $user->save(); + + $gift = factory(Gift::class)->make(); + $gift->amount = '100'; + + $this->assertEquals('100.00', $gift->amount); + $this->assertEquals('$100.00', $gift->displayValue); + } +} diff --git a/tests/Feature/ContactsControllerTest.php b/tests/Feature/ContactsControllerTest.php new file mode 100644 index 0000000..78a3e70 --- /dev/null +++ b/tests/Feature/ContactsControllerTest.php @@ -0,0 +1,57 @@ + true]); + $user = $this->signin(); + + $contact = factory(Contact::class)->state('archived')->create([ + 'account_id' => $user->account_id, + ]); + + factory(Contact::class, 10)->create([ + 'account_id' => $user->account_id, + ]); + + $this->assertTrue(AccountHelper::hasReachedContactLimit($user->account)); + $this->assertTrue(AccountHelper::hasLimitations($user->account)); + + $response = $this->put("/people/{$contact->hashID()}/archive"); + + $response->assertStatus(402); + } + + /** @test */ + public function it_stays_in_touch() + { + $user = $this->signin(); + + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->post("/people/{$contact->hashID()}/stayintouch", [ + 'frequency' => 5, + 'state' => 1, + ]); + + $response->assertStatus(200); + + $this->assertDatabaseHas('contacts', [ + 'id' => $contact->id, + 'stay_in_touch_frequency' => 5, + ]); + } +} diff --git a/tests/Feature/ConversationTest.php b/tests/Feature/ConversationTest.php new file mode 100644 index 0000000..63e5e6a --- /dev/null +++ b/tests/Feature/ConversationTest.php @@ -0,0 +1,168 @@ +signIn(); + + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + return [$user, $contact]; + } + + public function test_user_can_add_a_conversation() + { + [$user, $contact] = $this->fetchUser(); + $contactFieldType = factory(ContactFieldType::class)->create([ + 'account_id' => $user->account_id, + ]); + + $params = [ + 'conversationDateRadio' => 'another', + 'conversationDate' => '2019-08-12', + 'contactFieldTypeId' => $contactFieldType->id, + 'messages' => '1', + 'who_wrote_1' => 'me', + 'content_1' => 'test', + ]; + + $response = $this->post('/people/'.$contact->hashID().'/conversations', $params); + + $response->assertStatus(302); + + $this->assertDatabaseHas('conversations', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'contact_field_type_id' => $contactFieldType->id, + ]); + $this->assertDatabaseHas('messages', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'content' => 'test', + 'written_by_me' => true, + 'written_at' => '2019-08-12', + ]); + } + + public function test_user_cannot_add_a_conversation_without_message() + { + [$user, $contact] = $this->fetchUser(); + $contactFieldType = factory(ContactFieldType::class)->create([ + 'account_id' => $user->account_id, + ]); + + $params = [ + 'conversationDateRadio' => 'today', + 'contactFieldTypeId' => $contactFieldType->id, + ]; + + $response = $this->post('/people/'.$contact->hashID().'/conversations', $params, [ + 'HTTP_REFERER' => 'back', + ]); + + $response->assertStatus(302); + + $response->assertRedirect('back'); + $response->assertSessionHasErrors(['messages' => 'You must add at least one message.']); + } + + public function test_user_can_update_a_conversation() + { + [$user, $contact] = $this->fetchUser(); + $contactFieldType = factory(ContactFieldType::class)->create([ + 'account_id' => $user->account_id, + ]); + $conversation = factory(Conversation::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'contact_field_type_id' => $contactFieldType->id, + ]); + $message = factory(Message::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'content' => 'test', + 'written_by_me' => true, + 'written_at' => '2019-08-12', + ]); + + $params = [ + 'conversationDateRadio' => 'another', + 'conversationDate' => '2019-08-01', + 'contactFieldTypeId' => $contactFieldType->id, + 'messages' => '1', + 'who_wrote_1' => 'me', + 'content_1' => 'bla bla', + ]; + + $response = $this->put('/people/'.$contact->hashID().'/conversations/'.$conversation->hashID(), $params); + + $response->assertStatus(302); + + $this->assertDatabaseHas('conversations', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'contact_field_type_id' => $contactFieldType->id, + ]); + $this->assertDatabaseHas('messages', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'content' => 'bla bla', + 'written_by_me' => true, + 'written_at' => '2019-08-01', + ]); + } + + public function test_user_cannot_update_a_conversation_without_message() + { + [$user, $contact] = $this->fetchUser(); + $contactFieldType = factory(ContactFieldType::class)->create([ + 'account_id' => $user->account_id, + ]); + $conversation = factory(Conversation::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'contact_field_type_id' => $contactFieldType->id, + ]); + $message = factory(Message::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'content' => 'test', + 'written_by_me' => true, + 'written_at' => '2019-08-12', + ]); + + $params = [ + 'conversationDateRadio' => 'today', + 'contactFieldTypeId' => $contactFieldType->id, + ]; + + $response = $this->put('/people/'.$contact->hashID().'/conversations/'.$conversation->hashID(), $params, [ + 'HTTP_REFERER' => 'back', + ]); + + $response->assertStatus(302); + + $response->assertRedirect('back'); + $response->assertSessionHasErrors(['messages' => 'You must add at least one message.']); + } +} diff --git a/tests/Feature/DocumentsTest.php b/tests/Feature/DocumentsTest.php new file mode 100644 index 0000000..f89b903 --- /dev/null +++ b/tests/Feature/DocumentsTest.php @@ -0,0 +1,55 @@ +signin(); + + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + factory(Document::class, 10)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + ]); + + $response = $this->json('GET', '/people/'.$contact->hashID().'/documents'); + + $response->assertStatus(200); + + $response->assertJsonStructure([ + 'data' => [ + '*' => $this->jsonStructure, + ], + ]); + + $this->assertCount( + 10, + $response->decodeResponseJson()['data'] + ); + } +} diff --git a/tests/Feature/ExportAccountTest.php b/tests/Feature/ExportAccountTest.php new file mode 100644 index 0000000..d229003 --- /dev/null +++ b/tests/Feature/ExportAccountTest.php @@ -0,0 +1,81 @@ + 'database']); + + $user = $this->signin(); + + $response = $this->json('POST', '/settings/exportToJson'); + + $response->assertStatus(302); + + $this->assertDatabaseHas('export_jobs', [ + 'account_id' => $user->account_id, + 'user_id' => $user->id, + 'type' => ExportJob::JSON, + 'status' => ExportJob::EXPORT_TODO, + ]); + } + + /** @test */ + public function it_create_export_job_sql() + { + config(['queue.default' => 'database']); + + $user = $this->signin(); + + $response = $this->json('POST', '/settings/exportToSql'); + + $response->assertStatus(302); + + $this->assertDatabaseHas('export_jobs', [ + 'account_id' => $user->account_id, + 'user_id' => $user->id, + 'type' => ExportJob::SQL, + 'status' => ExportJob::EXPORT_TODO, + ]); + } + + /** @test */ + public function it_delete_old_export() + { + config(['queue.default' => 'database']); + + $user = $this->signin(); + + Carbon::setTestNow(Carbon::create(2022, 1, 1, 0, 0, 0)); + $exportJob = ExportJob::factory()->create([ + 'account_id' => $user->account_id, + 'user_id' => $user->id, + 'status' => ExportJob::EXPORT_DONE, + ]); + + Carbon::setTestNow(Carbon::create(2022, 1, 2, 0, 0, 0)); + ExportJob::factory()->count(4)->create([ + 'account_id' => $user->account_id, + 'user_id' => $user->id, + 'status' => ExportJob::EXPORT_DONE, + ]); + + $response = $this->json('POST', '/settings/exportToJson'); + + $response->assertStatus(302); + + $this->assertDatabaseMissing('export_jobs', [ + 'id' => $exportJob->id, + ]); + } +} diff --git a/tests/Feature/InstanceTest.php b/tests/Feature/InstanceTest.php new file mode 100644 index 0000000..5c39d69 --- /dev/null +++ b/tests/Feature/InstanceTest.php @@ -0,0 +1,48 @@ + false]); + factory(Account::class)->create(); + + $response = $this->get('/'); + + $response->assertSee( + 'Sign up' + ); + } + + /** + * If an instance sets `disable_signup` env variable to true, it should hide + * the signup button on the Sign in page. + * Also, trying to reach `/register` should lead to a 403 page. + * + * @return void + */ + public function test_disable_signup_set_to_true_hides_signup_button_and_register_page() + { + config(['monica.disable_signup' => true]); + factory(Account::class)->create(); + + $response = $this->get('/'); + $response->assertDontSee( + 'Sign up' + ); + } +} diff --git a/tests/Feature/IntroductionsTest.php b/tests/Feature/IntroductionsTest.php new file mode 100644 index 0000000..4eef20f --- /dev/null +++ b/tests/Feature/IntroductionsTest.php @@ -0,0 +1,56 @@ +signin(); + + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->get("/people/{$contact->hashID()}/introductions/edit"); + + $response->assertStatus(200); + $response->assertSee("How did you meet {$contact->first_name}"); + } + + public function test_it_update_introductions() + { + $user = $this->signin(); + + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->post("/people/{$contact->hashID()}/introductions/update", [ + 'first_met_additional_info' => 'info', + 'is_first_met_date_known' => 'known', + 'first_met_year' => 2006, + 'first_met_month' => 1, + 'first_met_day' => 2, + 'addReminder' => 'on', + ]); + + $response->assertStatus(302); + $response->assertRedirect("/people/{$contact->hashID()}"); + + $this->assertDatabaseHas('special_dates', [ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + 'id' => Contact::find($contact->id)->first_met_special_date_id, + 'is_age_based' => false, + 'is_year_unknown' => false, + 'date' => '2006-01-02', + ]); + } +} diff --git a/tests/Feature/InvitationTest.php b/tests/Feature/InvitationTest.php new file mode 100644 index 0000000..afe50bc --- /dev/null +++ b/tests/Feature/InvitationTest.php @@ -0,0 +1,61 @@ +create(); + + $invitation = factory(Invitation::class)->create([ + 'account_id' => $account->id, + 'email' => 'test@test.com', + ]); + + $response = $this->get('/invitations/accept/'.$invitation->invitation_key); + + $response->assertStatus(200); + $response->assertSee('test@test.com'); + } + + public function test_it_can_respond_to_invitation() + { + NotificationFacade::fake(); + + $account = factory(Account::class)->create(); + + $invitation = factory(Invitation::class)->create([ + 'account_id' => $account->id, + 'email' => 'test@test.com', + ]); + + $response = $this->post('/invitations/accept/'.$invitation->invitation_key, [ + 'email' => 'test@test007.com', + 'first_name' => 'john', + 'last_name' => 'doe', + 'password' => 'admin0', + 'password_confirmation' => 'admin0', + 'policy' => 'true', + 'email_security' => $invitation->invitedBy->email, + ]); + + $response->assertStatus(302); + $response->assertRedirect('/dashboard'); + + $this->assertDataBaseHas('users', [ + 'email' => 'test@test007.com', + 'first_name' => 'john', + 'last_name' => 'doe', + 'invited_by_user_id' => $invitation->invitedBy->id, + ]); + } +} diff --git a/tests/Feature/JournalEntryTest.php b/tests/Feature/JournalEntryTest.php new file mode 100644 index 0000000..fa27ebf --- /dev/null +++ b/tests/Feature/JournalEntryTest.php @@ -0,0 +1,93 @@ +signIn(); + + $params = [ + 'entry' => 'Good day', + 'date' => '2018-01-01', + ]; + + $response = $this->post('/journal/create', $params); + + $response->assertStatus(302); + + $this->assertDatabaseHas('journal_entries', [ + 'account_id' => $user->account_id, + 'date' => '2018-01-01 00:00:00', + 'journalable_type' => 'App\Models\Journal\Entry', + ]); + $this->assertDatabaseHas('entries', [ + 'account_id' => $user->account_id, + 'post' => 'Good day', + ]); + } + + public function test_user_can_edit_a_journal_entry() + { + $user = $this->signIn(); + + $entry = factory(Entry::class)->create([ + 'account_id' => $user->account_id, + 'title' => 'This is the title', + 'post' => 'this is a post', + ]); + $entry->date = '2017-01-01'; + $journalEntry = JournalEntry::add($entry); + + $params = [ + 'entry' => 'Good day', + 'date' => '2018-01-01', + ]; + + $response = $this->put('/journal/entries/'.$entry->id, $params); + + $response->assertStatus(302); + + $this->assertDatabaseHas('journal_entries', [ + 'account_id' => $user->account_id, + 'date' => '2018-01-01 00:00:00', + 'journalable_id' => $entry->id, + 'journalable_type' => 'App\Models\Journal\Entry', + ]); + $this->assertDatabaseHas('entries', [ + 'account_id' => $user->account_id, + 'post' => 'Good day', + ]); + } + + public function test_user_can_delete_a_journal_entry() + { + $user = $this->signIn(); + + $entry = factory(Entry::class)->create([ + 'account_id' => $user->account_id, + 'title' => 'This is the title', + 'post' => 'this is a post', + ]); + $entry->date = '2017-01-01'; + $journalEntry = JournalEntry::add($entry); + + $response = $this->delete('/journal/'.$entry->id); + $response->assertSuccessful(); + + $this->assertDatabaseMissing('entries', [ + 'id' => $entry->id, + ]); + $this->assertDatabaseMissing('journal_entries', [ + 'id' => $journalEntry->id, + ]); + } +} diff --git a/tests/Feature/MeTest.php b/tests/Feature/MeTest.php new file mode 100644 index 0000000..7eeb018 --- /dev/null +++ b/tests/Feature/MeTest.php @@ -0,0 +1,87 @@ +signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->json('POST', '/me/contact', [ + 'contact_id' => $contact->id, + ]); + + $response->assertStatus(200); + $response->assertJson([ + 'true', + ]); + + $this->assertDatabaseHas('users', [ + 'id' => $user->id, + 'me_contact_id' => $contact->id, + ]); + } + + /** @test */ + public function it_stores_error_wrong_parameter() + { + $this->signin(); + + $response = $this->json('POST', '/me/contact', []); + + $response->assertStatus(422); + $response->assertJson([ + 'errors' => [ + 'contact_id' => ['The contact id field is required.'], + ], + ]); + } + + /** @test */ + public function it_stores_error_bad_account() + { + $this->signin(); + + $contact = factory(Contact::class)->create(); + + $response = $this->json('POST', '/me/contact', [ + 'contact_id' => $contact->id, + ]); + + $response->assertStatus(404); + $response->assertJson([ + 'message' => "No query results for model [App\\Models\\Contact\\Contact] {$contact->id}", + ]); + } + + /** @test */ + public function it_deletes_me() + { + $user = $this->signin(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $user->me_contact_id = $contact->id; + $user->save(); + + $response = $this->json('DELETE', '/me/contact'); + + $response->assertStatus(200); + + $this->assertDatabaseHas('users', [ + 'id' => $user->id, + 'me_contact_id' => null, + ]); + } +} diff --git a/tests/Feature/NoteTest.php b/tests/Feature/NoteTest.php new file mode 100644 index 0000000..9955c4a --- /dev/null +++ b/tests/Feature/NoteTest.php @@ -0,0 +1,92 @@ +signIn(); + + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + return [$user, $contact]; + } + + public function test_user_can_add_a_note() + { + [$user, $contact] = $this->fetchUser(); + + $noteBody = 'This is a note that I would like to see'; + + $params = [ + 'body' => $noteBody, + 'is_favorited' => 0, + ]; + + $response = $this->post('/people/'.$contact->hashID().'/notes', $params); + + // Assert the note has been added for the correct user. + $this->assertDatabaseHas('notes', [ + 'body' => $noteBody, + ]); + } + + public function test_user_can_edit_a_note() + { + [$user, $contact] = $this->fetchUser(); + + $note = factory(Note::class)->create([ + 'contact_id' => $contact->id, + 'account_id' => $user->account_id, + 'body' => 'this is a test', + 'is_favorited' => 1, + ]); + + // now edit the note + $params = [ + 'body' => 'this is another test', + 'is_favorited' => 0, + ]; + + $this->put('/people/'.$contact->hashID().'/notes/'.$note->id, $params); + + // Assert the note has been added for the correct user. + $this->assertDatabaseHas('notes', [ + 'body' => 'this is another test', + ]); + } + + public function test_user_can_delete_a_note() + { + [$user, $contact] = $this->fetchUser(); + + $note = factory(Note::class)->create([ + 'contact_id' => $contact->id, + 'account_id' => $user->account_id, + 'body' => 'this is a test', + ]); + + $response = $this->delete('/people/'.$contact->hashID().'/notes/'.$note->id); + + $params = []; + $params['id'] = $note->id; + + $this->assertDatabaseMissing('notes', $params); + } +} diff --git a/tests/Feature/PasswordChangeTest.php b/tests/Feature/PasswordChangeTest.php new file mode 100644 index 0000000..5baad40 --- /dev/null +++ b/tests/Feature/PasswordChangeTest.php @@ -0,0 +1,90 @@ +signIn(); + + $user->password = $password = bcrypt('password'); + $user->save(); + + $response = $this->followingRedirects()->post('/settings/security/passwordChange', [ + 'password_current' => 'password', + 'password' => 'newPassword', + 'password_confirmation' => 'newPassword', + ]); + + $response->assertStatus(200); + + $response->assertSee('Password changed successfully.'); + + $user->refresh(); + $this->assertNotEquals($password, $user->password); + } + + public function test_current_password_checked() + { + $user = $this->signIn(); + + $user->password = bcrypt('password'); + $user->save(); + + $response = $this->followingRedirects()->post('/settings/security/passwordChange', [ + 'password_current' => 'xpassword', + 'password' => 'newPassword', + 'password_confirmation' => 'newPassword', + ]); + + $response->assertStatus(200); + + $response->assertSee('Current password you entered is not correct.'); + } + + public function test_new_password_policy_check() + { + $user = $this->signIn(); + + $user->password = bcrypt('password'); + $user->save(); + + $response = $this->followingRedirects()->post('/settings/security/passwordChange', [ + 'password_current' => 'password', + 'password' => 'admin', + 'password_confirmation' => 'admin', + ], [ + 'HTTP_REFERER' => '/settings/security', + ]); + + $response->assertStatus(200); + + $response->assertSee('The password must be at least 6 characters.'); + } + + public function test_new_password_validation_check() + { + $user = $this->signIn(); + + $user->password = bcrypt('password'); + $user->save(); + + $response = $this->followingRedirects()->post('/settings/security/passwordChange', [ + 'password_current' => 'password', + 'password' => 'admin0', + 'password_confirmation' => 'admin1', + ], [ + 'HTTP_REFERER' => '/settings/security', + ]); + + $response->assertStatus(200); + + $response->assertSee('The password confirmation does not match.'); + } +} diff --git a/tests/Feature/PhotosTest.php b/tests/Feature/PhotosTest.php new file mode 100644 index 0000000..121e87d --- /dev/null +++ b/tests/Feature/PhotosTest.php @@ -0,0 +1,136 @@ +signIn(); + + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + return [$user, $contact]; + } + + public function test_user_can_add_a_photo() + { + [$user, $contact] = $this->fetchUser(); + + Storage::fake('public'); + $file = UploadedFile::fake()->image('avatar.jpg'); + + $params = [ + 'photo' => $file, + ]; + + $response = $this->post('/people/'.$contact->hashID().'/photos', $params); + + $response->assertStatus(201); + + $response->assertJsonStructure([ + 'data' => $this->jsonStructure, + ]); + + // Assert the photo has been added for the correct user. + $this->assertDatabaseHas('photos', [ + 'account_id' => $user->account_id, + 'original_filename' => 'avatar.jpg', + 'new_filename' => 'photos/'.$file->hashName(), + ]); + $this->assertDatabaseHas('contact_photo', [ + 'contact_id' => $contact->id, + 'photo_id' => $response->json('data.id'), + ]); + + Storage::disk('public')->assertExists('photos/'.$file->hashName()); + } + + public function test_user_can_delete_a_photo() + { + [$user, $contact] = $this->fetchUser(); + + Storage::fake('public'); + $file = UploadedFile::fake()->image('avatar.jpg'); + + $params = [ + 'photo' => $file, + ]; + + $response1 = $this->post('/people/'.$contact->hashID().'/photos', $params); + + $response2 = $this->delete('/people/'.$contact->hashID().'/photos/'.$response1->json('data.id')); + + $response2->assertStatus(200); + + $this->assertDatabaseMissing('photos', [ + 'account_id' => $user->account_id, + 'original_filename' => 'avatar.jpg', + 'new_filename' => 'photos/'.$file->hashName(), + ]); + $this->assertDatabaseMissing('contact_photo', [ + 'contact_id' => $contact->id, + 'photo_id' => $response1->json('data.id'), + ]); + } + + public function test_user_can_delete_a_photo_even_if_it_s_already_deleted() + { + [$user, $contact] = $this->fetchUser(); + + Storage::fake('public'); + $file = UploadedFile::fake()->image('avatar.jpg'); + + $params = [ + 'photo' => $file, + ]; + + $response1 = $this->post('/people/'.$contact->hashID().'/photos', $params); + + Storage::delete($response1->json('data.new_filename')); + + $response2 = $this->delete('/people/'.$contact->hashID().'/photos/'.$response1->json('data.id')); + + $response2->assertStatus(200); + + $this->assertDatabaseMissing('photos', [ + 'account_id' => $user->account_id, + 'original_filename' => 'avatar.jpg', + 'new_filename' => 'photos/'.$file->hashName(), + ]); + $this->assertDatabaseMissing('contact_photo', [ + 'contact_id' => $contact->id, + 'photo_id' => $response1->json('data.id'), + ]); + } +} diff --git a/tests/Feature/RegisterTest.php b/tests/Feature/RegisterTest.php new file mode 100644 index 0000000..84be797 --- /dev/null +++ b/tests/Feature/RegisterTest.php @@ -0,0 +1,92 @@ + false]); + + Mail::fake(); + + $params = [ + 'email' => 'john.mike@doe.com', + 'first_name' => 'john', + 'last_name' => 'doe', + 'password' => 'admin0', + 'password_confirmation' => 'admin0', + 'policy' => 'true', + 'lang' => 'en', + ]; + + $response = $this->post('/register', $params); + + $response->assertStatus(302); + $response->assertRedirect('/dashboard'); + + $this->assertDatabaseHas('users', [ + 'email' => 'john.mike@doe.com', + ]); + } + + public function test_user_cannot_register_twice() + { + config(['monica.disable_signup' => false]); + + Mail::fake(); + + $user = factory(User::class)->create(); + + $params = [ + 'email' => $user->email, + 'first_name' => 'john', + 'last_name' => 'doe', + 'password' => 'admin0', + 'password_confirmation' => 'admin0', + 'policy' => 'true', + 'lang' => 'en', + ]; + + $response = $this->post('/register', $params, [ + 'HTTP_REFERER' => '/register', + ]); + + $response->assertStatus(302); + $response->assertRedirect('/register'); + } + + public function test_it_dispatches_an_email() + { + config(['monica.disable_signup' => false]); + + $route = Notification::route('mail', 'test@test.com'); + Notification::fake(); + + config(['monica.email_new_user_notification' => 'test@test.com']); + + $user = factory(User::class)->create(); + + SendNewUserAlert::dispatch($user); + + Notification::assertSentTo($route, NewUserAlert::class); + + $notifications = Notification::sent($route, NewUserAlert::class); + $message = $notifications[0]->toMail(); + + $this->assertStringContainsString('New registration', $message->subject); + $this->assertStringContainsString($user->first_name, implode('', $message->introLines)); + $this->assertStringContainsString($user->last_name, implode('', $message->introLines)); + $this->assertStringContainsString($user->email, implode('', $message->introLines)); + } +} diff --git a/tests/Feature/RelationshipTest.php b/tests/Feature/RelationshipTest.php new file mode 100644 index 0000000..02d0307 --- /dev/null +++ b/tests/Feature/RelationshipTest.php @@ -0,0 +1,332 @@ +signIn(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->get('/people/'.$contact->hashID().'/relationships/create'); + + $response->assertStatus(200); + + $response->assertSee('This person is…'); + } + + public function test_user_can_add_a_relationship() + { + $user = $this->signIn(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $partner = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $relationshipType = factory(RelationshipType::class)->create([ + 'account_id' => $user->account_id, + ]); + + $params = [ + 'relationship_type' => 'existing', + 'existing_contact_id' => $partner->id, + 'relationship_type_id' => $relationshipType->id, + ]; + + $response = $this->post('/people/'.$contact->hashID().'/relationships', $params); + + $response->assertStatus(302); + + $this->assertDatabaseHas('relationships', [ + 'account_id' => $user->account_id, + 'contact_is' => $contact->id, + 'of_contact' => $partner->id, + 'relationship_type_id' => $relationshipType->id, + ]); + } + + public function test_user_can_add_a_relationship_new_user_birthdate_unknown() + { + $user = $this->signIn(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $relationshipType = factory(RelationshipType::class)->create([ + 'account_id' => $user->account_id, + ]); + $gender = factory(Gender::class)->create([ + 'account_id' => $user->account_id, + ]); + + $params = [ + 'relationship_type' => 'new', + 'relationship_type_id' => $relationshipType->id, + 'first_name' => 'Arnold', + 'last_name' => 'Schwarzenegger', + 'gender_id' => $gender->id, + 'birthdate' => 'unknown', + 'realContact' => true, + ]; + + $response = $this->post('/people/'.$contact->hashID().'/relationships', $params); + + $response->assertStatus(302); + + $this->assertDatabaseHas('contacts', [ + 'account_id' => $user->account_id, + 'first_name' => 'Arnold', + 'last_name' => 'Schwarzenegger', + 'gender_id' => $gender->id, + 'is_partial' => false, + ]); + $this->assertDatabaseHas('relationships', [ + 'account_id' => $user->account_id, + 'contact_is' => $contact->id, + 'relationship_type_id' => $relationshipType->id, + ]); + } + + public function test_user_can_add_a_relationship_new_user_partial() + { + $user = $this->signIn(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $relationshipType = factory(RelationshipType::class)->create([ + 'account_id' => $user->account_id, + ]); + $gender = factory(Gender::class)->create([ + 'account_id' => $user->account_id, + ]); + + $params = [ + 'relationship_type' => 'new', + 'relationship_type_id' => $relationshipType->id, + 'first_name' => 'Arnold', + 'last_name' => 'Schwarzenegger', + 'gender_id' => $gender->id, + 'birthdate' => 'unknown', + 'realContact' => false, + ]; + + $response = $this->post('/people/'.$contact->hashID().'/relationships', $params); + + $response->assertStatus(302); + + $this->assertDatabaseHas('contacts', [ + 'account_id' => $user->account_id, + 'first_name' => 'Arnold', + 'last_name' => 'Schwarzenegger', + 'gender_id' => $gender->id, + 'is_partial' => true, + ]); + $this->assertDatabaseHas('relationships', [ + 'account_id' => $user->account_id, + 'contact_is' => $contact->id, + 'relationship_type_id' => $relationshipType->id, + ]); + } + + public function test_user_can_add_a_relationship_new_user_birthdate_known() + { + $user = $this->signIn(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $relationshipType = factory(RelationshipType::class)->create([ + 'account_id' => $user->account_id, + ]); + $gender = factory(Gender::class)->create([ + 'account_id' => $user->account_id, + ]); + + $params = [ + 'relationship_type' => 'new', + 'relationship_type_id' => $relationshipType->id, + 'first_name' => 'Arnold', + 'last_name' => 'Schwarzenegger', + 'gender_id' => $gender->id, + 'birthdate' => 'exact', + 'birthdayDate' => '1947-07-30', + 'realContact' => true, + ]; + + $response = $this->post('/people/'.$contact->hashID().'/relationships', $params); + + $response->assertStatus(302); + + $this->assertDatabaseHas('contacts', [ + 'account_id' => $user->account_id, + 'first_name' => 'Arnold', + 'last_name' => 'Schwarzenegger', + 'gender_id' => $gender->id, + 'is_partial' => false, + ]); + $this->assertDatabaseHas('special_dates', [ + 'account_id' => $user->account_id, + 'date' => '1947-07-30', + 'is_age_based' => false, + 'is_year_unknown' => false, + ]); + $this->assertDatabaseHas('relationships', [ + 'account_id' => $user->account_id, + 'contact_is' => $contact->id, + 'relationship_type_id' => $relationshipType->id, + ]); + } + + public function test_edit_a_relationship() + { + $user = $this->signIn(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $partner = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + 'first_name' => 'Homer', + 'last_name' => 'Simpson', + ]); + $relationship = factory(Relationship::class)->create([ + 'account_id' => $user->account_id, + 'contact_is' => $contact->id, + 'of_contact' => $partner->id, + ]); + + $response = $this->get('/people/'.$contact->hashID().'/relationships/'.$relationship->id.'/edit'); + + $response->assertStatus(200); + + $response->assertSee('Homer Simpson is…'); + } + + public function test_user_can_update_a_relationship() + { + $user = $this->signIn(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $partner = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $relationship = factory(Relationship::class)->create([ + 'account_id' => $user->account_id, + 'contact_is' => $contact->id, + 'of_contact' => $partner->id, + ]); + $relationshipType = factory(RelationshipType::class)->create([ + 'account_id' => $user->account_id, + ]); + + $params = [ + 'relationship_id' => $relationship->id, + 'relationship_type_id' => $relationshipType->id, + ]; + + $response = $this->put('/people/'.$contact->hashID().'/relationships/'.$relationship->id, $params); + + $response->assertStatus(302); + + $this->assertDatabaseHas('relationships', [ + 'id' => $relationship->id, + 'account_id' => $user->account_id, + 'contact_is' => $contact->id, + 'of_contact' => $partner->id, + 'relationship_type_id' => $relationshipType->id, + ]); + } + + public function test_user_can_update_a_relationship_partial_user() + { + $user = $this->signIn(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $partner = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + 'is_partial' => true, + ]); + $relationship = factory(Relationship::class)->create([ + 'account_id' => $user->account_id, + 'contact_is' => $contact->id, + 'of_contact' => $partner->id, + ]); + $relationshipType = factory(RelationshipType::class)->create([ + 'account_id' => $user->account_id, + ]); + + $params = [ + 'relationship_id' => $relationship->id, + 'relationship_type_id' => $relationshipType->id, + 'first_name' => 'Arnold', + 'last_name' => 'Schwarzenegger', + 'gender_id' => $partner->gender_id, + 'birthdate' => 'exact', + 'birthdayDate' => '1947-07-30', + ]; + + $response = $this->put('/people/'.$contact->hashID().'/relationships/'.$relationship->id, $params); + + $response->assertStatus(302); + + $this->assertDatabaseHas('contacts', [ + 'account_id' => $user->account_id, + 'first_name' => 'Arnold', + 'last_name' => 'Schwarzenegger', + 'is_partial' => true, + ]); + $this->assertDatabaseHas('special_dates', [ + 'account_id' => $user->account_id, + 'date' => '1947-07-30', + 'is_age_based' => false, + 'is_year_unknown' => false, + ]); + $this->assertDatabaseHas('relationships', [ + 'id' => $relationship->id, + 'account_id' => $user->account_id, + 'contact_is' => $contact->id, + 'of_contact' => $partner->id, + 'relationship_type_id' => $relationshipType->id, + ]); + } + + public function test_user_can_destroy_a_relationship() + { + $user = $this->signIn(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $partner = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $relationship = factory(Relationship::class)->create([ + 'account_id' => $user->account_id, + 'contact_is' => $contact->id, + 'of_contact' => $partner->id, + ]); + + $response = $this->delete('/people/'.$contact->hashID().'/relationships/'.$relationship->id); + + $response->assertStatus(302); + + $this->assertDatabaseMissing('relationships', [ + 'id' => $relationship->id, + 'account_id' => $user->account_id, + 'contact_is' => $contact->id, + 'of_contact' => $partner->id, + ]); + } +} diff --git a/tests/Feature/ReminderRuleTest.php b/tests/Feature/ReminderRuleTest.php new file mode 100644 index 0000000..b88f71f --- /dev/null +++ b/tests/Feature/ReminderRuleTest.php @@ -0,0 +1,57 @@ +signIn(); + + $reminderRule = factory(ReminderRule::class)->create([ + 'account_id' => $user->account_id, + 'active' => true, + ]); + + return [$user, $reminderRule]; + } + + /** @test */ + public function reminder_rule_index() + { + [$user, $reminderRule] = $this->fetchUser(); + + $response = $this->get('/settings/personalization/reminderrules'); + + $response->assertJsonFragment([ + 'id' => $reminderRule->id, + 'active' => true, + ]); + } + + /** @test */ + public function reminder_rule_toggle() + { + [$user, $reminderRule] = $this->fetchUser(); + + $response = $this->post('/settings/personalization/reminderrules/'.$reminderRule->id); + + $this->assertDatabaseHas('reminder_rules', [ + 'id' => $reminderRule->id, + 'active' => 0, + ]); + $response->assertJsonFragment([ + 'id' => $reminderRule->id, + 'active' => false, + ]); + } +} diff --git a/tests/Feature/SettingsTest.php b/tests/Feature/SettingsTest.php new file mode 100644 index 0000000..8eec07f --- /dev/null +++ b/tests/Feature/SettingsTest.php @@ -0,0 +1,126 @@ +signIn(); + + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + return [$user, $contact]; + } + + public function test_user_can_access_settings_page() + { + [$user, $contact] = $this->fetchUser(); + + $response = $this->get('/settings'); + + $response->assertStatus(200); + + $response->assertSee(trans('settings.sidebar_settings')); + } + + public function test_user_can_export_account() + { + [$user, $contact] = $this->fetchUser(); + + $response = $this->get('/settings/export'); + + $response->assertStatus(200); + + $response->assertSee(trans('settings.export_title')); + + Carbon::setTestNow(Carbon::create(2021, 11, 25, 7, 0, 0)); + + $response = $this->post(route('settings.export.store.sql')); + + $response->assertStatus(302); + // $this->assertTrue($response->headers->get('content-disposition') == 'attachment; filename=monica-export.2021-11-25.sql'); + } + + public function test_user_can_delete_account() + { + [$user, $contact] = $this->fetchUser(); + + $response = $this->followingRedirects() + ->post(route('settings.delete')); + + $response->assertStatus(200); + + $response->assertSee('Login'); + } + + public function test_it_updates_the_default_profile_view() + { + $user = $this->signin(); + + $response = $this->json('POST', '/settings/updateDefaultProfileView', [ + 'name' => 'life-events', + ]); + + $response->assertStatus(200); + + $this->assertDatabaseHas('users', [ + 'profile_active_tab' => 'life-events', + 'id' => $user->id, + ]); + + $response = $this->json('POST', '/settings/updateDefaultProfileView', [ + 'name' => 'notes', + ]); + + $response->assertStatus(200); + + $this->assertDatabaseHas('users', [ + 'profile_active_tab' => 'notes', + 'id' => $user->id, + ]); + + $response = $this->json('POST', '/settings/updateDefaultProfileView', [ + 'name' => 'nawak', + ]); + + $response->assertStatus(200); + } + + public function test_user_see_webauthnkeys() + { + $user = $this->signin(); + $webauthnKey = factory(WebauthnKey::class)->create([ + 'user_id' => $user->id, + 'updated_at' => '2019-04-01 09:18:35', + ]); + + $this->session([ + 'webauthn_auth' => true, + ]); + + $response = $this->followingRedirects() + ->get(route('settings.security.index')); + + $response->assertStatus(200); + + $response->assertSee($webauthnKey->name); + $response->assertSee('2019-04-01T09:18:35Z'); + } +} diff --git a/tests/Feature/StorageControllerTest.php b/tests/Feature/StorageControllerTest.php new file mode 100644 index 0000000..0101a45 --- /dev/null +++ b/tests/Feature/StorageControllerTest.php @@ -0,0 +1,355 @@ +signIn(); + + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + return [$user, $contact]; + } + + /** @test */ + public function it_get_photo_content() + { + config(['filesystems.default' => 'local']); + + [$user, $contact] = $this->fetchUser(); + + $file = $this->storeImage($contact); + + $response = $this->get('/store/'.$file); + + $response->assertStatus(200); + $response->assertHeader('Last-Modified', 'Sat, 19 Jun 2021 07:00:00 GMT'); + $response->assertHeader('Cache-Control', 'max-age=2628000, private'); + $response->assertHeader('etag', '"'.sha1('/store/'.$file).'"'); + } + + /** @test */ + public function it_get_avatar_content() + { + config(['filesystems.default' => 'local']); + + [$user, $contact] = $this->fetchUser(); + + $file = $this->storeAvatar($contact); + + $response = $this->get('/store/'.$file); + + $response->assertStatus(200); + $response->assertHeader('Last-Modified', 'Sat, 19 Jun 2021 07:00:00 GMT'); + $response->assertHeader('Cache-Control', 'max-age=2628000, private'); + $response->assertHeader('etag', '"'.sha1('/store/'.$file).'"'); + } + + /** @test */ + public function it_get_document_content() + { + config(['filesystems.default' => 'local']); + + [$user, $contact] = $this->fetchUser(); + + $file = $this->storeDocument($contact); + + $response = $this->get('/store/'.$file); + + $response->assertStatus(200); + $response->assertHeader('Last-Modified', 'Sat, 19 Jun 2021 07:00:00 GMT'); + $response->assertHeader('Cache-Control', 'max-age=2628000, private'); + $response->assertHeader('etag', '"'.sha1('/store/'.$file).'"'); + } + + /** @test */ + public function it_returns_404_if_avatar_not_exist() + { + config(['filesystems.default' => 'local']); + + [$user, $contact] = $this->fetchUser(); + + $response = $this->get('/store/avatars/test'); + + $response->assertStatus(404); + } + + /** @test */ + public function it_returns_404_if_folder_unknown() + { + config(['filesystems.default' => 'local']); + + [$user, $contact] = $this->fetchUser(); + + $response = $this->get('/store/xxx/test'); + + $response->assertStatus(404); + } + + /** @test */ + public function it_returns_200_if_modified_after_IfModifiedSince() + { + config(['filesystems.default' => 'local']); + + [$user, $contact] = $this->fetchUser(); + + $file = $this->storeImage($contact); + + $response = $this->get('/store/'.$file, [ + 'If-Modified-Since' => 'Sat, 12 Jun 2021 07:00:00 GMT', + ]); + + $response->assertStatus(200); + $response->assertHeader('Last-Modified', 'Sat, 19 Jun 2021 07:00:00 GMT'); + $response->assertHeader('Cache-Control', 'max-age=2628000, private'); + $response->assertHeader('etag', '"'.sha1('/store/'.$file).'"'); + } + + /** @test */ + public function it_returns_304_if_not_modified_since_IfModifiedSince() + { + config(['filesystems.default' => 'local']); + + [$user, $contact] = $this->fetchUser(); + + $file = $this->storeImage($contact); + + $response = $this->get('/store/'.$file, [ + 'If-Modified-Since' => 'Sat, 26 Jun 2021 07:00:00 GMT', + ]); + + $response->assertNoContent(304); + $response->assertHeaderMissing('Last-Modified'); + $response->assertHeader('Cache-Control', 'max-age=2628000, private'); + $response->assertHeader('etag', '"'.sha1('/store/'.$file).'"'); + } + + /** @test */ + public function it_returns_200_if_not_modified_after_IfUnmodifiedSince() + { + config(['filesystems.default' => 'local']); + + [$user, $contact] = $this->fetchUser(); + + $file = $this->storeImage($contact); + + $response = $this->get('/store/'.$file, [ + 'If-Unmodified-Since' => 'Sat, 26 Jun 2021 07:00:00 GMT', + ]); + + $response->assertStatus(200); + $response->assertHeader('Last-Modified', 'Sat, 19 Jun 2021 07:00:00 GMT'); + $response->assertHeader('Cache-Control', 'max-age=2628000, private'); + $response->assertHeader('etag', '"'.sha1('/store/'.$file).'"'); + } + + /** @test */ + public function it_returns_412_if_modified_after_IfUnmodifiedSince() + { + config(['filesystems.default' => 'local']); + + [$user, $contact] = $this->fetchUser(); + + $file = $this->storeImage($contact); + + $response = $this->get('/store/'.$file, [ + 'If-Unmodified-Since' => 'Sat, 12 Jun 2021 07:00:00 GMT', + ]); + + $response->assertStatus(412); + } + + /** @test */ + public function it_fails_if_file_not_found() + { + config(['filesystems.default' => 'local']); + + [$user, $contact] = $this->fetchUser(); + + $response = $this->get('/store/photos/fail.png'); + + $response->assertStatus(404); + } + + /** @test */ + public function it_fails_if_file_not_exist() + { + config(['filesystems.default' => 'local']); + + [$user, $contact] = $this->fetchUser(); + + $photo = factory(Photo::class)->create([ + 'account_id' => $contact->account_id, + 'original_filename' => 'avatar.png', + 'filesize' => 0, + 'mime_type' => '', + 'new_filename' => 'avatar.png', + ]); + + $contact->photos()->syncWithoutDetaching([$photo->id]); + + $response = $this->get('/store/photos/avatar.png'); + + $response->assertStatus(404); + } + + /** @test */ + public function it_fails_if_file_not_owned_by_user() + { + config(['filesystems.default' => 'local']); + + [$user, $contact] = $this->fetchUser(); + + $file = $this->storeImage($contact); + + $this->signIn(); + + $response = $this->get('/store/'.$file, [ + 'If-Unmodified-Since' => 'Sat, 12 Jun 2021 07:00:00 GMT', + ]); + + $response->assertStatus(404); + } + + /** @test */ + public function it_returns_200_if_matching_IfMatch() + { + config(['filesystems.default' => 'local']); + + [$user, $contact] = $this->fetchUser(); + + $file = $this->storeImage($contact); + + $response = $this->get('/store/'.$file, [ + 'If-Match' => '"'.sha1('/store/'.$file).'"', + ]); + + $response->assertNoContent(200); + $response->assertHeader('Last-Modified', 'Sat, 19 Jun 2021 07:00:00 GMT'); + $response->assertHeader('Cache-Control', 'max-age=2628000, private'); + $response->assertHeader('etag', '"'.sha1('/store/'.$file).'"'); + } + + /** @test */ + public function it_returns_200_with_none_matching_IfNoneMatch() + { + config(['filesystems.default' => 'local']); + + [$user, $contact] = $this->fetchUser(); + + $file = $this->storeImage($contact); + + $response = $this->get('/store/'.$file, [ + 'If-None-Match' => '"test"', + ]); + + $response->assertNoContent(200); + $response->assertHeader('Last-Modified', 'Sat, 19 Jun 2021 07:00:00 GMT'); + $response->assertHeader('Cache-Control', 'max-age=2628000, private'); + $response->assertHeader('etag', '"'.sha1('/store/'.$file).'"'); + } + + /** @test */ + public function it_returns_304_if_matching_IfNoneMatch() + { + config(['filesystems.default' => 'local']); + + [$user, $contact] = $this->fetchUser(); + + $file = $this->storeImage($contact); + + $response = $this->get('/store/'.$file, [ + 'If-None-Match' => '"'.sha1('/store/'.$file).'"', + ]); + + $response->assertNoContent(304); + $response->assertHeaderMissing('Last-Modified'); + $response->assertHeader('Cache-Control', 'max-age=2628000, private'); + $response->assertHeader('etag', '"'.sha1('/store/'.$file).'"'); + } + + public function storeImage(Contact $contact) + { + Storage::fake('local'); + $image = File::createWithContent('avatar.png', file_get_contents(base_path('public/img/favicon.png'))); + + $file = Storage::putFile('/photos', $image, [ + 'disk' => 'local', + ]); + + $photo = factory(Photo::class)->create([ + 'account_id' => $contact->account_id, + 'original_filename' => 'avatar.png', + 'filesize' => $image->getSize(), + 'mime_type' => $image->getMimeType(), + 'new_filename' => $file, + ]); + + $contact->photos()->syncWithoutDetaching([$photo->id]); + + touch(StorageHelper::disk('local')->path($file), Carbon::create(2021, 6, 19, 7, 0, 0, 'UTC')->timestamp); + + return $file; + } + + public function storeDocument(Contact $contact) + { + Storage::fake('local'); + $image = File::createWithContent('file.png', file_get_contents(base_path('public/img/favicon.png'))); + + $file = Storage::putFile('/documents', $image, [ + 'disk' => 'local', + ]); + + factory(Document::class)->create([ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'original_filename' => 'file.png', + 'new_filename' => $file, + ]); + + touch(StorageHelper::disk('local')->path($file), Carbon::create(2021, 6, 19, 7, 0, 0, 'UTC')->timestamp); + + return $file; + } + + public function storeAvatar(Contact $contact) + { + $disk = Storage::fake('local'); + $image = File::createWithContent('avatar.png', file_get_contents(base_path('public/img/favicon.png'))); + + $file = Storage::putFile('/avatars', $image, [ + 'disk' => 'local', + ]); + + $contact->avatar_source = 'default'; + $contact->avatar_default_url = $file.'?123'; + $contact->save(); + + touch(StorageHelper::disk('local')->path($file), Carbon::create(2021, 6, 19, 7, 0, 0, 'UTC')->timestamp); + + return $file; + } +} diff --git a/tests/Feature/TaskTest.php b/tests/Feature/TaskTest.php new file mode 100644 index 0000000..52ab288 --- /dev/null +++ b/tests/Feature/TaskTest.php @@ -0,0 +1,55 @@ +signIn(); + + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + return [$user, $contact]; + } + + public function test_user_can_add_a_task() + { + [$user, $contact] = $this->fetchUser(); + + $taskTitle = $this->faker->realText(); + $taskDescription = $this->faker->realText(); + + $params = [ + 'title' => $taskTitle, + 'description' => $taskDescription, + 'completed' => 0, + 'contact_id' => $contact->id, + ]; + + $response = $this->post('/tasks', $params); + + // Assert the note has been added for the correct user. + $params['account_id'] = $user->account_id; + $params['contact_id'] = $contact->id; + $params['title'] = $taskTitle; + $params['description'] = $taskDescription; + + $this->assertDatabaseHas('tasks', $params); + } +} diff --git a/tests/FeatureTestCase.php b/tests/FeatureTestCase.php new file mode 100644 index 0000000..ba6f857 --- /dev/null +++ b/tests/FeatureTestCase.php @@ -0,0 +1,14 @@ +buffer = collect([]); + } + + public function exec($command, $message, $commandline): void + { + $this->buffer->push(['message' => $message, 'command' => $commandline]); + } + + public function artisan($command, $message, $commandline, array $arguments = []): void + { + $info = ''; + foreach ($arguments as $key => $value) { + $info = $info.' '.$key.'='.$value; + } + $this->buffer->push(['message' =>$message, 'command' => 'php artisan '.$commandline.$info]); + } + + /** + * Assert the command identified by a message has been launched. + */ + public function assertContainsMessage(string $message): void + { + $messages = $this->buffer->map(function ($line) { + return $line['message']; + }); + Assert::assertContains($message, $messages); + } +} diff --git a/tests/Helpers/DavTester.php b/tests/Helpers/DavTester.php new file mode 100644 index 0000000..74e45f8 --- /dev/null +++ b/tests/Helpers/DavTester.php @@ -0,0 +1,362 @@ +current = 0; + $this->baseUri = $baseUri; + $this->responses = []; + } + + public function client(): DavClient + { + return (new DavClient())->setBaseUri($this->baseUri); + } + + public function fake() + { + Http::fake(function ($request) { + return $this->responses[$this->current++]['response']; + }); + + return $this; + } + + public function assert() + { + Http::assertSentInOrder(array_map(function ($data) { + return function (Request $request, Response $response) use ($data) { + $srequest = $request->method().' '.$request->url(); + $this->assertEquals($data['method'], $request->method(), "method for request $srequest differs"); + $this->assertEquals($data['uri'], $request->url(), "uri for request $srequest differs"); + if (isset($data['body'])) { + $this->assertEquals($data['body'], $request->body(), "body for request $srequest differs"); + } + if (isset($data['headers'])) { + foreach ($data['headers'] as $key => $value) { + $this->assertArrayHasKey($key, $request->headers(), "header $key for request $srequest is missing"); + $this->assertEquals($value, $request->header($key), "header $key for request $srequest differs"); + } + } + + return true; + }; + }, $this->responses)); + + // $this->assertCount(count($this->responses), $this->container, 'the number of response do not match the number of requests'); + // foreach ($this->container as $index => $request) { + // $srequest = $request->getMethod().' '.(string) $request->getUri(); + // $this->assertEquals($this->responses[$index]['method'], $request->getMethod(), "method for request $srequest differs"); + // $this->assertEquals($this->responses[$index]['uri'], (string) $request->getUri(), "uri for request $srequest differs"); + // if (isset($this->responses[$index]['body'])) { + // $this->assertEquals($this->responses[$index]['body'], (string) $request->getBody(), "body for request $srequest differs"); + // } + // if (isset($this->responses[$index]['headers'])) { + // foreach ($this->responses[$index]['headers'] as $key => $value) { + // $this->assertArrayHasKey($key, $request->getHeaders(), "header $key for request $srequest is missing"); + // $this->assertEquals($value, $request->getHeaderLine($key), "header $key for request $srequest differs"); + // } + // } + // } + } + + public function addressBookBaseUri() + { + return $this->userPrincipal('https://test') + ->optionsOk('https://test/dav/principals/user@test.com/') + ->userPrincipal('https://test/dav/principals/user@test.com/') + ->addressbookHome() + ->resourceTypeAddressBook() + ->optionsOk('https://test/dav/addressbooks/user@test.com/contacts/'); + } + + public function capabilities() + { + return $this->supportedReportSet() + ->supportedAddressData(); + } + + public function addResponse(string $uri, PromiseInterface $response, string $body = null, string $method = 'PROPFIND', array $headers = null) + { + $this->responses[] = [ + 'uri' => $uri, + 'response' => $response, + 'method' => $method, + 'body' => $body, + 'headers' => $headers, + ]; + + return $this; + } + + public function serviceUrl() + { + return $this->addResponse('https://test/.well-known/carddav', Http::response(null, 301, ['Location' => $this->baseUri.'/dav/']), null, 'GET'); + } + + public function nonStandardServiceUrl() + { + return $this->addResponse('https://test/.well-known/carddav', Http::response(null, 301, ['Location' => '/dav/']), null, 'PROPFIND'); + } + + public function optionsOk(string $url = 'https://test/dav/') + { + return $this->addResponse($url, Http::response(null, 200, ['Dav' => '1, 3, addressbook']), null, 'OPTIONS'); + } + + public function optionsFail() + { + return $this->addResponse('https://test/dav/', Http::response(null, 200, ['Dav' => 'bad']), null, 'OPTIONS'); + } + + public function userPrincipal(string $url = 'https://test/dav/') + { + return $this->addResponse($url, Http::response($this->multistatusHeader(). + ''. + '/dav/'. + ''. + ''. + ''. + '/dav/principals/user@test.com/'. + ''. + ''. + 'HTTP/1.1 200 OK'. + ''. + ''. + '')); + } + + public function userPrincipalEmpty() + { + return $this->addResponse('https://test/dav/', Http::response($this->multistatusHeader(). + ''. + '/dav/'. + ''. + ''. + ''. + ''. + 'HTTP/1.1 200 OK'. + ''. + ''. + '')); + } + + public function addressbookHome() + { + return $this->addResponse('https://test/dav/principals/user@test.com/', Http::response($this->multistatusHeader(). + ''. + '/dav/principals/user@test.com/'. + ''. + ''. + ''. + '/dav/addressbooks/user@test.com/'. + ''. + ''. + 'HTTP/1.1 200 OK'. + ''. + ''. + '')); + } + + public function addressbookEmpty() + { + return $this->addResponse('https://test/dav/principals/user@test.com/', Http::response($this->multistatusHeader(). + ''. + '/dav/principals/user@test.com/'. + ''. + ''. + ''. + ''. + 'HTTP/1.1 200 OK'. + ''. + ''. + '')); + } + + public function resourceTypeAddressBook() + { + return $this->addResponse('https://test/dav/addressbooks/user@test.com/', Http::response($this->multistatusHeader(). + ''. + '/dav/addressbooks/user@test.com/contacts/'. + ''. + ''. + ''. + ''. + ''. + ''. + 'HTTP/1.1 200 OK'. + ''. + ''. + '')); + } + + public function resourceTypeHomeOnly() + { + return $this->addResponse('https://test/dav/addressbooks/user@test.com/', Http::response($this->multistatusHeader(). + ''. + '/dav/addressbooks/user@test.com/'. + ''. + ''. + ''. + ''. + 'HTTP/1.1 200 OK'. + ''. + ''. + '')); + } + + public function resourceTypeEmpty() + { + return $this->addResponse('https://test/dav/addressbooks/user@test.com/contacts/', Http::response($this->multistatusHeader(). + ''. + '/dav/addressbooks/user@test.com/contacts/'. + ''. + ''. + ''. + ''. + 'HTTP/1.1 200 OK'. + ''. + ''. + '')); + } + + public function supportedReportSet(array $reportSet = ['card:addressbook-multiget', 'card:addressbook-query', 'd:sync-collection']) + { + return $this->addResponse('https://test/dav/addressbooks/user@test.com/contacts/', Http::response($this->multistatusHeader(). + ''. + '/dav/addressbooks/user@test.com/contacts/'. + ''. + ''. + ''. + implode('', array_map(function ($report) { + return "<$report/>"; + }, $reportSet)). + ''. + ''. + 'HTTP/1.1 200 OK'. + ''. + ''. + '')); + } + + public function supportedAddressData(array $list = ['card:address-data-type content-type="text/vcard" version="4.0"']) + { + return $this->addResponse('https://test/dav/addressbooks/user@test.com/contacts/', Http::response($this->multistatusHeader(). + ''. + '/dav/addressbooks/user@test.com/contacts/'. + ''. + ''. + ''. + implode('', array_map(function ($item) { + return "<$item/>"; + }, $list)). + ''. + ''. + 'HTTP/1.1 200 OK'. + ''. + ''. + '')); + } + + public function displayName(string $name = 'Test') + { + return $this->addResponse('https://test/dav/addressbooks/user@test.com/contacts/', Http::response($this->multistatusHeader(). + ''. + '/dav/addressbooks/user@test.com/contacts/'. + ''. + ''. + "$name". + ''. + 'HTTP/1.1 200 OK'. + ''. + ''. + '')); + } + + public function getSynctoken(string $synctoken = '"test"') + { + return $this->addResponse('https://test/dav/addressbooks/user@test.com/contacts/', Http::response($this->multistatusHeader(). + ''. + '/dav/addressbooks/user@test.com/contacts/'. + ''. + ''. + "$synctoken". + ''. + 'HTTP/1.1 200 OK'. + ''. + ''. + '')); + } + + public function getSyncCollection(string $synctoken = 'token', string $etag = '"etag"') + { + return $this->addResponse('https://test/dav/addressbooks/user@test.com/contacts/', Http::response($this->multistatusHeader(). + ''. + 'https://test/dav/addressbooks/user@test.com/contacts/uuid'. + ''. + ''. + "$etag". + 'text/vcard'. + ''. + 'HTTP/1.1 200 OK'. + ''. + ''. + "$synctoken". + ''), null, 'REPORT'); + } + + public function addressMultiGet($etag, $card, $url) + { + return $this->addResponse('https://test/dav/addressbooks/user@test.com/contacts/', Http::response($this->multistatusHeader(). + ''. + 'https://test/dav/addressbooks/user@test.com/contacts/uuid'. + ''. + ''. + "$etag". + "$card". + ''. + 'HTTP/1.1 200 OK'. + ''. + ''. + ''), ''."\n". + ''. + ''. + ''. + ''. + ''. + "$url". + "\n", 'REPORT'); + } + + public static function multistatusHeader() + { + return ''; + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..9a84970 --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,78 @@ +getMethod($methodName); + $method->setAccessible(true); + + return $method->invokeArgs($object, $parameters); + } + + /** + * Set protected/private property of a class. + * + * @param object &$object + * @param string $propertyName + * @param mixed $value + * @return void + */ + public function setPrivateValue(&$object, string $propertyName, $value) + { + $reflection = new \ReflectionClass(get_class($object)); + $property = $reflection->getProperty($propertyName); + $property->setAccessible(true); + + $property->setValue($object, $value); + } + + /** + * Get protected/private property of a class. + * + * @param object &$object + * @param string $propertyName + * @return mixed + */ + public function getPrivateValue(&$object, string $propertyName) + { + $reflection = new \ReflectionClass(get_class($object)); + $property = $reflection->getProperty($propertyName); + $property->setAccessible(true); + + return $property->getValue($object); + } + + /** + * Test that the response contains an ObjectDeleted response. + * + * @param TestResponse $response + * @param int $id + */ + public function expectObjectDeleted(TestResponse $response, int $id) + { + $response->assertStatus(200); + + $response->assertJson([ + 'deleted' => true, + 'id' => $id, + ]); + } +} diff --git a/tests/Traits/ApiSignIn.php b/tests/Traits/ApiSignIn.php new file mode 100644 index 0000000..e5e00dd --- /dev/null +++ b/tests/Traits/ApiSignIn.php @@ -0,0 +1,23 @@ +create(); + Passport::actingAs($user); + + return $user; + } +} diff --git a/tests/Traits/Asserts.php b/tests/Traits/Asserts.php new file mode 100644 index 0000000..1e15bd5 --- /dev/null +++ b/tests/Traits/Asserts.php @@ -0,0 +1,78 @@ +assertStatus(404); + + $response->assertJson([ + 'error' => [ + 'message' => 'The resource has not been found', + 'error_code' => 31, + ], + ]); + } + + /** + * Test that the response contains a not authorized notification. + * + * @param TestResponse $response + */ + public function expectNotAuthorized(TestResponse $response) + { + $response->assertStatus(401); + + $response->assertJson([ + 'error' => [ + 'message' => 'Not authorized', + 'error_code' => 42, + ], + ]); + } + + /** + * Test that the response contains a data error notification. + * + * @param TestResponse $response + * @param string|array $message + */ + public function expectDataError(TestResponse $response, $message = '') + { + $response->assertStatus(422); + + $response->assertJson([ + 'error' => [ + 'message' => $message, + 'error_code' => 32, + ], + ]); + } + + /** + * Test that the response contains an invalid parameter notification. + * + * @param TestResponse $response + * @param string|array $message + */ + public function expectInvalidParameter(TestResponse $response, $message = '') + { + $response->assertStatus(422); + + $response->assertJson([ + 'error' => [ + 'message' => $message, + 'error_code' => 41, + ], + ]); + } +} diff --git a/tests/Traits/CreatesApplication.php b/tests/Traits/CreatesApplication.php new file mode 100644 index 0000000..ee28555 --- /dev/null +++ b/tests/Traits/CreatesApplication.php @@ -0,0 +1,30 @@ +make(Kernel::class)->bootstrap(); + + App::setLocale('en'); + + // set the bcrypt hashing rounds (the minimum allowed). + // this reduces the amount of cycles needed to manage users. + Hash::setRounds(4); + + return $app; + } +} diff --git a/tests/Traits/SignIn.php b/tests/Traits/SignIn.php new file mode 100644 index 0000000..d29482a --- /dev/null +++ b/tests/Traits/SignIn.php @@ -0,0 +1,33 @@ +create(); + $user->account->populateDefaultFields(); + app(AcceptPolicy::class)->execute([ + 'account_id' => $user->account_id, + 'user_id' => $user->id, + 'ip_address' => null, + ]); + } + + $this->be($user); + + return $user; + } +} diff --git a/tests/Unit/Controllers/Account/LifeEvent/LifeEventCategoriesControllerTest.php b/tests/Unit/Controllers/Account/LifeEvent/LifeEventCategoriesControllerTest.php new file mode 100644 index 0000000..3eb315f --- /dev/null +++ b/tests/Unit/Controllers/Account/LifeEvent/LifeEventCategoriesControllerTest.php @@ -0,0 +1,21 @@ +signin(); + + $response = $this->get('settings/personalization/lifeeventcategories'); + + $response->assertStatus(200); + } +} diff --git a/tests/Unit/Controllers/Auth/PasswordResetTest.php b/tests/Unit/Controllers/Auth/PasswordResetTest.php new file mode 100644 index 0000000..753fb52 --- /dev/null +++ b/tests/Unit/Controllers/Auth/PasswordResetTest.php @@ -0,0 +1,31 @@ +create(); + + $this->post('/password/email', ['email' => $user->email]); + + NotificationFacade::assertSentTo($user, ResetPassword::class); + + $notifications = NotificationFacade::sent($user, ResetPassword::class); + $message = $notifications[0]->toMail($user); + + $this->assertStringContainsString('You are receiving this email because we received a password reset request for your account.', implode('', $message->introLines)); + } +} diff --git a/tests/Unit/Controllers/Contact/ContactAuditLogControllerTest.php b/tests/Unit/Controllers/Contact/ContactAuditLogControllerTest.php new file mode 100644 index 0000000..4359f28 --- /dev/null +++ b/tests/Unit/Controllers/Contact/ContactAuditLogControllerTest.php @@ -0,0 +1,26 @@ +signin(); + + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->get("/people/{$contact->hashID()}/auditlogs"); + + $response->assertStatus(200); + } +} diff --git a/tests/Unit/Controllers/Contact/LifeEventsControllerTest.php b/tests/Unit/Controllers/Contact/LifeEventsControllerTest.php new file mode 100644 index 0000000..c908138 --- /dev/null +++ b/tests/Unit/Controllers/Contact/LifeEventsControllerTest.php @@ -0,0 +1,26 @@ +signin(); + + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->get("/people/{$contact->hashID()}/lifeevents"); + + $response->assertStatus(200); + } +} diff --git a/tests/Unit/Controllers/Settings/AuditLogControllerTest.php b/tests/Unit/Controllers/Settings/AuditLogControllerTest.php new file mode 100644 index 0000000..7a65e76 --- /dev/null +++ b/tests/Unit/Controllers/Settings/AuditLogControllerTest.php @@ -0,0 +1,26 @@ +signin(); + + factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $response = $this->get('/settings/auditlogs'); + + $response->assertStatus(200); + } +} diff --git a/tests/Unit/Controllers/Settings/GendersControllerTest.php b/tests/Unit/Controllers/Settings/GendersControllerTest.php new file mode 100644 index 0000000..6b08d44 --- /dev/null +++ b/tests/Unit/Controllers/Settings/GendersControllerTest.php @@ -0,0 +1,202 @@ +signin(); + + $response = $this->json('GET', '/settings/personalization/genders'); + + $response->assertStatus(200); + + $response->assertJsonStructure([ + '*' => $this->jsonStructure, + ]); + + $this->assertCount( + 3, + $response->decodeResponseJson() + ); + } + + /** @test */ + public function it_gets_the_list_of_genderTypes() + { + $user = $this->signin(); + + $response = $this->json('GET', '/settings/personalization/genderTypes'); + + $response->assertStatus(200); + + $response->assertJsonStructure([ + '*' => $this->typesJsonStructure, + ]); + + $this->assertCount( + 5, + $response->decodeResponseJson() + ); + } + + /** @test */ + public function it_stores_a_new_gender() + { + $user = $this->signin(); + + $response = $this->json('POST', '/settings/personalization/genders', [ + 'name' => 'gender', + 'type' => 'O', + ]); + + $response->assertStatus(200); + $response->assertJsonStructure($this->jsonStructure); + + $this->assertDataBaseHas('genders', [ + 'account_id' => $user->account_id, + 'name' => 'gender', + 'type' => 'O', + ]); + } + + /** @test */ + public function it_stores_a_new_default_gender() + { + $user = $this->signin(); + + $this->assertNull($user->account->default_gender_id); + + $response = $this->json('POST', '/settings/personalization/genders', [ + 'name' => 'new-default-gender', + 'type' => 'O', + 'isDefault' => 'true', + ]); + + $this->assertEquals($response->getData()->id, $user->account->default_gender_id); + } + + /** @test */ + public function it_updates_a_gender() + { + $user = $this->signin(); + + $gender = $user->account->genders()->first(); + + $response = $this->json('PUT', '/settings/personalization/genders/'.$gender->id, [ + 'name' => 'gender', + 'type' => 'U', + ]); + + $response->assertStatus(200); + $response->assertJsonStructure($this->jsonStructure); + + $this->assertDataBaseHas('genders', [ + 'account_id' => $user->account_id, + 'id' => $gender->id, + 'name' => 'gender', + ]); + } + + /** @test */ + public function it_replaces_a_gender() + { + $user = $this->signin(); + + $genders = $user->account->genders()->get(); + $gender1 = $genders[0]->id; + $gender2 = $genders[1]->id; + + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + 'gender_id' => $gender1, + ]); + + $response = $this->json('DELETE', '/settings/personalization/genders/'.$gender1.'/replaceby/'.$gender2); + + $this->expectObjectDeleted($response, $genders[0]->id); + + $this->assertDataBaseMissing('genders', [ + 'account_id' => $user->account_id, + 'id' => $genders[0]->id, + ]); + $this->assertDataBaseHas('contacts', [ + 'account_id' => $user->account_id, + 'id' => $contact->id, + 'gender_id' => $gender2, + ]); + } + + /** @test */ + public function it_replaces_a_gender_with_error() + { + $user = $this->signin(); + $gender1 = factory(Gender::class)->create([ + 'account_id' => $user->account_id, + ]); + $gender2 = factory(Gender::class)->create(); + + $response = $this->json('DELETE', '/settings/personalization/genders/'.$gender1->id.'/replaceby/'.$gender2->id); + + $response->assertStatus(403); + $response->assertJson([ + 'message' => 'Please choose a gender from the list.', + ]); + } + + /** @test */ + public function it_destroys_a_gender() + { + $user = $this->signin(); + + $gender = $user->account->genders()->first(); + + $response = $this->json('DELETE', '/settings/personalization/genders/'.$gender->id); + + $this->expectObjectDeleted($response, $gender->id); + + $this->assertDataBaseMissing('genders', [ + 'account_id' => $user->account_id, + 'id' => $gender->id, + ]); + } + + /** @test */ + public function it_updates_the_default_gender() + { + $user = $this->signin(); + + $gender = $user->account->genders()->first(); + + $this->assertNull($user->account->default_gender_id); + + $response = $this->json('PUT', '/settings/personalization/genders/default/'.$gender->id); + + $response->assertStatus(200); + $response->assertJsonStructure($this->jsonStructure); + + $this->assertEquals($gender->id, $user->account->default_gender_id); + } +} diff --git a/tests/Unit/Events/Google2faEventListenerTest.php b/tests/Unit/Events/Google2faEventListenerTest.php new file mode 100644 index 0000000..9126129 --- /dev/null +++ b/tests/Unit/Events/Google2faEventListenerTest.php @@ -0,0 +1,64 @@ +startSession(); + + $request = new FakeRequest(); + $request->session = $this->app['session']; + + Google2FA::setRequest($request); + app('pragmarx.google2fa')->setStateless(false); + } + + /** @test */ + public function it_listens_recovery_event() + { + $user = $this->signIn(); + $user->google2fa_secret = 'x'; + + Event::dispatch(new RecoveryLogin($user)); + + $this->assertTrue($this->app['session']->get('google2fa.auth_passed')); + } + + /** @test */ + public function it_listens_login_remember_event() + { + $user = $this->signIn(); + $user->google2fa_secret = 'x'; + + $guard = app(AuthManager::class)->guard(); + $this->setPrivateValue($guard, 'viaRemember', true); + + Event::dispatch(new Login('guard', $user, true)); + + $this->assertTrue($this->app['session']->get('google2fa.auth_passed')); + } +} + +class FakeRequest +{ + public $session; + + public function session() + { + return $this->session; + } +} diff --git a/tests/Unit/Helpers/AccountHelperTest.php b/tests/Unit/Helpers/AccountHelperTest.php new file mode 100644 index 0000000..e359c4f --- /dev/null +++ b/tests/Unit/Helpers/AccountHelperTest.php @@ -0,0 +1,264 @@ +make([ + 'has_access_to_paid_version_for_free' => true, + ]); + + $this->assertFalse(AccountHelper::hasLimitations($account)); + + // Check that if the ENV variable REQUIRES_SUBSCRIPTION has an effect + $account = factory(Account::class)->make([ + 'has_access_to_paid_version_for_free' => false, + ]); + + config(['monica.requires_subscription' => false]); + + $this->assertFalse(AccountHelper::hasLimitations($account)); + } + + /** @test */ + public function account_has_reached_contact_limit_on_free_plan(): void + { + $account = factory(Account::class)->create(); + factory(Contact::class, 2)->create([ + 'account_id' => $account->id, + ]); + + config(['monica.number_of_allowed_contacts_free_account' => 1]); + $this->assertTrue(AccountHelper::hasReachedContactLimit($account)); + $this->assertFalse(AccountHelper::isBelowContactLimit($account)); + + factory(Contact::class)->state('partial')->create([ + 'account_id' => $account->id, + ]); + + config(['monica.number_of_allowed_contacts_free_account' => 3]); + $this->assertFalse(AccountHelper::hasReachedContactLimit($account)); + $this->assertTrue(AccountHelper::isBelowContactLimit($account)); + + config(['monica.number_of_allowed_contacts_free_account' => 100]); + $this->assertFalse(AccountHelper::hasReachedContactLimit($account)); + $this->assertTrue(AccountHelper::isBelowContactLimit($account)); + + $account = factory(Account::class)->create(); + factory(Contact::class, 2)->create([ + 'account_id' => $account->id, + 'is_active' => false, + ]); + factory(Contact::class, 3)->create([ + 'account_id' => $account->id, + 'is_active' => true, + ]); + + config(['monica.number_of_allowed_contacts_free_account' => 3]); + $this->assertTrue(AccountHelper::hasReachedContactLimit($account)); + $this->assertTrue(AccountHelper::isBelowContactLimit($account)); + } + + /** @test */ + public function user_can_downgrade_with_only_one_user_and_no_pending_invitations_and_under_contact_limit(): void + { + config(['monica.number_of_allowed_contacts_free_account' => 1]); + $contact = factory(Contact::class)->create(); + + factory(User::class)->create([ + 'account_id' => $contact->account_id, + ]); + + $this->assertTrue(AccountHelper::canDowngrade($contact->account)); + } + + /** @test */ + public function user_cant_downgrade_with_two_users(): void + { + $contact = factory(Contact::class)->create(); + + factory(User::class, 3)->create([ + 'account_id' => $contact->account_id, + ]); + + $this->assertFalse(AccountHelper::canDowngrade($contact->account)); + } + + /** @test */ + public function user_cant_downgrade_with_pending_invitations(): void + { + $account = factory(Account::class)->create(); + + factory(Invitation::class)->create([ + 'account_id' => $account->id, + ]); + + $this->assertFalse(AccountHelper::canDowngrade($account)); + } + + /** @test */ + public function user_cant_downgrade_with_too_many_contacts(): void + { + config(['monica.number_of_allowed_contacts_free_account' => 1]); + $account = factory(Account::class)->create(); + + factory(Contact::class, 2)->create([ + 'account_id' => $account->id, + ]); + + $this->assertFalse(AccountHelper::canDowngrade($account)); + } + + /** @test */ + public function it_gets_the_default_gender_for_the_account(): void + { + $account = factory(Account::class)->create(); + + $this->assertEquals(Gender::UNKNOWN, AccountHelper::getDefaultGender($account)); + + $gender = factory(Gender::class)->create([ + 'account_id' => $account->id, + ]); + $account->default_gender_id = $gender->id; + $account->save(); + + $this->assertEquals($gender->type, AccountHelper::getDefaultGender($account)); + } + + /** @test */ + public function get_reminders_for_month_returns_no_reminders(): void + { + $account = factory(Account::class)->create(); + $user = factory(User::class)->create([ + 'account_id' => $account->id, + ]); + + Carbon::setTestNow(Carbon::create(2017, 1, 1)); + factory(Reminder::class, 3)->create([ + 'account_id' => $account->id, + ]); + + // check if there are reminders for the month of March + $this->actingAs($user)->assertCount(0, AccountHelper::getUpcomingRemindersForMonth($account, 3)); + } + + /** @test */ + public function get_reminders_for_month_returns_reminders_for_given_month(): void + { + $account = factory(Account::class)->create(); + $user = factory(User::class)->create([ + 'account_id' => $account->id, + ]); + + Carbon::setTestNow(Carbon::create(2017, 1, 1)); + + // add 3 reminders for the month of March + for ($i = 0; $i < 3; $i++) { + $reminder = factory(Reminder::class)->create([ + 'account_id' => $account->id, + 'initial_date' => '2017-03-03 00:00:00', + ]); + + $reminder->schedule($user); + } + + $this->actingAs($user)->assertCount(3, AccountHelper::getUpcomingRemindersForMonth($account, 2)); + } + + /** @test */ + public function get_reminders_for_month_returns_reminders_for_current_user_only(): void + { + $account = factory(Account::class)->create(); + $user1 = factory(User::class)->create([ + 'account_id' => $account->id, + ]); + $user2 = factory(User::class)->create([ + 'account_id' => $account->id, + ]); + + Carbon::setTestNow(Carbon::create(2017, 1, 1)); + + // add 3 reminders for the month of March + for ($i = 0; $i < 3; $i++) { + $reminder = factory(Reminder::class)->create([ + 'account_id' => $account->id, + 'initial_date' => '2017-03-03 00:00:00', + ]); + + $reminder->schedule($user1); + $reminder->schedule($user2); + } + + $this->actingAs($user1)->assertCount(3, AccountHelper::getUpcomingRemindersForMonth($account, 2)); + } + + /** @test */ + public function it_retrieves_yearly_activities_statistics(): void + { + $account = factory(Account::class)->create(); + factory(Activity::class, 4)->create([ + 'account_id' => $account->id, + 'happened_at' => '2018-03-02', + ]); + + factory(Activity::class, 2)->create([ + 'account_id' => $account->id, + 'happened_at' => '1992-03-02', + ]); + + $statistics = AccountHelper::getYearlyActivitiesStatistics($account); + + $this->assertEquals( + [ + 1992 => 2, + 2018 => 4, + ], + $statistics->toArray() + ); + } + + /** @test */ + public function it_retrieves_yearly_call_statistics(): void + { + $contact = factory(Contact::class)->create(); + factory(Call::class, 4)->create([ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'called_at' => '2018-03-02', + ]); + + factory(Call::class, 2)->create([ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'called_at' => '1992-03-02', + ]); + + $statistics = AccountHelper::getYearlyCallStatistics($contact->account); + + $this->assertEquals( + [ + 1992 => 2, + 2018 => 4, + ], + $statistics->toArray() + ); + } +} diff --git a/tests/Unit/Helpers/AuditLogHelperTest.php b/tests/Unit/Helpers/AuditLogHelperTest.php new file mode 100644 index 0000000..7d30c2b --- /dev/null +++ b/tests/Unit/Helpers/AuditLogHelperTest.php @@ -0,0 +1,65 @@ +create([]); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + 'first_name' => 'roger', + 'last_name' => 'moore', + ]); + + factory(AuditLog::class, 2)->create([ + 'account_id' => $user->account_id, + 'about_contact_id' => $contact->id, + 'objects' => '{"contact_name":"'.$contact->name.'","contact_id":'.$contact->id.'}', + ]); + + $logs = $user->account->auditLogs; + $collection = AuditLogHelper::getCollectionOfAudits($logs); + + $this->assertEquals( + 2, + $collection->count() + ); + } + + /** @test */ + public function it_prepares_a_collection_of_audit_logs_without_likns_for_the_settings_page() + { + $user = factory(User::class)->create([]); + + factory(AuditLog::class, 2)->create([ + 'account_id' => $user->account_id, + 'about_contact_id' => null, + 'objects' => '{"contact_name":"roger moore","contact_id":123456789}', + ]); + + $logs = $user->account->auditLogs; + $collection = AuditLogHelper::getCollectionOfAudits($logs); + + $this->assertEquals( + 2, + $collection->count() + ); + + $this->assertEquals( + 'logs.settings_log_account_created_with_name', + $collection[0]['description'] + ); + } +} diff --git a/tests/Unit/Helpers/CollectionHelperTest.php b/tests/Unit/Helpers/CollectionHelperTest.php new file mode 100644 index 0000000..19e834e --- /dev/null +++ b/tests/Unit/Helpers/CollectionHelperTest.php @@ -0,0 +1,168 @@ + 'a'], + ['name' => 'c'], + ['name' => 'b'], + ]); + $collection = CollectionHelper::sortByCollator($collection, 'name'); + + $this->assertEquals( + [ + ['name' => 'a'], + ['name' => 'b'], + ['name' => 'c'], + ], + array_values($collection->toArray()) + ); + } + + /** @test */ + public function sortByCollator_macro() + { + $collection = collect([ + ['name' => 'a'], + ['name' => 'c'], + ['name' => 'b'], + ]); + $collection = $collection->sortByCollator('name'); + + $this->assertEquals( + [ + ['name' => 'a'], + ['name' => 'b'], + ['name' => 'c'], + ], + array_values($collection->toArray()) + ); + } + + /** @test */ + public function sortByCollator_callback() + { + $collection = collect([ + ['name' => 'a'], + ['name' => 'c'], + ['name' => 'b'], + ]); + $collection = $collection->sortByCollator(function ($item) { + return $item['name']; + }); + + $this->assertEquals( + [ + ['name' => 'a'], + ['name' => 'b'], + ['name' => 'c'], + ], + array_values($collection->toArray()) + ); + } + + /** @test */ + public function sortByCollator_default_collation() + { + App::setLocale('en'); + + $collection = collect([ + ['name' => 'cote'], + ['name' => 'côté'], + ['name' => 'coté'], + ['name' => 'côte'], + ]); + $collection = CollectionHelper::sortByCollator($collection, 'name'); + + $this->assertEquals( + [ + ['name' => 'cote'], + ['name' => 'coté'], + ['name' => 'côte'], + ['name' => 'côté'], + ], + array_values($collection->toArray()) + ); + } + + /** @test */ + public function sortByCollator_french_collation() + { + App::setLocale('fr'); + + $collection = collect([ + ['name' => 'cote'], + ['name' => 'côté'], + ['name' => 'coté'], + ['name' => 'côte'], + ]); + $collection = CollectionHelper::sortByCollator($collection, 'name'); + + $this->assertEquals( + [ + ['name' => 'cote'], + ['name' => 'côte'], + ['name' => 'coté'], + ['name' => 'côté'], + ], + array_values($collection->toArray()) + ); + } + + /** @test */ + public function getCollator_french_collation() + { + $collator = CollectionHelper::getCollator('fr'); + + $this->assertEquals($collator->getAttribute(\Collator::FRENCH_COLLATION), \Collator::ON); + $this->assertEquals($collator->getLocale(\Locale::VALID_LOCALE), 'fr'); + } + + /** @test */ + public function group_by_items_property() + { + $object1 = (object) ['name' => 'John']; + $object2 = (object) ['name' => 'Jack']; + $object3 = (object) ['name' => 'John']; + + $collection = collect([ + $object1, + $object2, + $object3, + ]); + + $collection = CollectionHelper::groupByItemsProperty($collection, 'name'); + + $this->assertEquals( + [ + 'John' => [$object1, $object3], + 'Jack' => [$object2], + ], + $collection->toArray() + ); + } + + /** @test */ + public function it_maps_uuid() + { + $collection = collect(); + for ($i = 1; $i <= 3; $i++) { + $uuid = new \stdClass(); + $uuid->uuid = $i; + $collection->push($uuid); + } + + $uuids = $collection->mapUuid(); + + $this->assertEquals([1, 2, 3], $uuids); + } +} diff --git a/tests/Unit/Helpers/ComplianceHelperTest.php b/tests/Unit/Helpers/ComplianceHelperTest.php new file mode 100644 index 0000000..3b48830 --- /dev/null +++ b/tests/Unit/Helpers/ComplianceHelperTest.php @@ -0,0 +1,44 @@ +create([]); + $term = factory(Term::class)->create([]); + $this->assertFalse(ComplianceHelper::hasSignedGivenTerm($user, $term)); + + $term = factory(Term::class)->create([]); + $user->terms()->sync([$term->id => ['account_id' => $user->account_id]]); + + $this->assertTrue(ComplianceHelper::hasSignedGivenTerm($user, $term)); + } + + /** @test */ + public function it_checks_if_the_user_has_signed_the_latest_term() + { + $user = factory(User::class)->create([]); + $term = factory(Term::class)->create([ + 'created_at' => '1990-02-07 02:26:07', + ]); + $this->assertFalse(ComplianceHelper::isCompliantWithCurrentTerm($user)); + + $term = factory(Term::class)->create([ + 'created_at' => '2020-02-07 02:26:07', + ]); + $user->terms()->syncWithoutDetaching([$term->id => ['account_id' => $user->account_id]]); + + $this->assertTrue(ComplianceHelper::isCompliantWithCurrentTerm($user)); + } +} diff --git a/tests/Unit/Helpers/CountryHelperTest.php b/tests/Unit/Helpers/CountryHelperTest.php new file mode 100644 index 0000000..590cf1f --- /dev/null +++ b/tests/Unit/Helpers/CountryHelperTest.php @@ -0,0 +1,125 @@ +getMethod('getDefaultCountryFromLocale'); + $method->setAccessible(true); + + $country = $method->invokeArgs(null, [$locale]); + + $this->assertEquals( + $expect, + $country + ); + } + + public function countryDefaultCountryFromLocaleProvider() + { + return [ + ['en', 'US'], + ['En', 'US'], + ['EN', 'US'], + ['cs', 'CZ'], + ['he', 'IL'], + ['zh', 'CN'], + ['de', 'DE'], + ['es', 'ES'], + ['fr', 'FR'], + ['hr', 'HR'], + ['it', 'IT'], + ['nl', 'NL'], + ['pt', 'PT'], + ['ru', 'RU'], + ['tr', 'TR'], + ['ja', null], + ]; + } + + /** + * @dataProvider countryCountryFromLocaleProvider + */ + public function test_country_getCountryFromLocale($locale, $expect) + { + $country = CountriesHelper::getCountryFromLocale($locale); + + $this->assertNotNull($country); + $this->assertEquals( + $expect, + $country->getIsoAlpha2() + ); + } + + public function countryCountryFromLocaleProvider() + { + return [ + ['en', 'US'], + ['En', 'US'], + ['EN', 'US'], + ['en-US', 'US'], + ['cs', 'CZ'], + ['he', 'IL'], + ['zh', 'CN'], + ['de', 'DE'], + ['es', 'ES'], + ['fr', 'FR'], + ['hr', 'HR'], + ['id', 'ID'], + ['it', 'IT'], + ['nl', 'NL'], + ['pt', 'PT'], + ['ru', 'RU'], + ['tr', 'TR'], + ['ja', 'JP'], + ['pt-BR', 'BR'], + ['fr-BE', 'BE'], + ]; + } + + /** + * @dataProvider timezoneFromLocaleProvider + * @test + */ + public function it_get_default_timezone($locale, $expect) + { + $country = CountriesHelper::getCountryFromLocale($locale); + $timezone = CountriesHelper::getDefaultTimezone($country); + + $this->assertNotNull($timezone); + $this->assertEquals( + $expect, + $timezone + ); + } + + public function timezoneFromLocaleProvider() + { + return [ + ['en', 'America/Chicago'], + ['cs', 'Europe/Prague'], + ['he', 'Asia/Jerusalem'], + ['zh', 'Asia/Shanghai'], + ['de', 'Europe/Berlin'], + ['es', 'Europe/Madrid'], + ['fr', 'Europe/Paris'], + ['hr', 'Europe/Zagreb'], + ['id', 'Asia/Jakarta'], + ['it', 'Europe/Rome'], + ['nl', 'Europe/Amsterdam'], + ['pt', 'Europe/Lisbon'], + ['ru', 'Europe/Moscow'], + ['tr', 'Europe/Istanbul'], + ['ja', 'Asia/Tokyo'], + ]; + } +} diff --git a/tests/Unit/Helpers/DateHelperTest.php b/tests/Unit/Helpers/DateHelperTest.php new file mode 100644 index 0000000..9a4e0c7 --- /dev/null +++ b/tests/Unit/Helpers/DateHelperTest.php @@ -0,0 +1,761 @@ +assertEquals( + 'Jan 22, 2017', + DateHelper::getShortDate($date) + ); + } + + public function testGetShortDateWithFrenchLocale() + { + $date = Carbon::parse('2017-01-22 17:56:03'); + App::setLocale('fr'); + + $this->assertEquals( + '22 janv. 2017', + DateHelper::getShortDate($date) + ); + } + + public function testGetShortDateWithUnknownLocale() + { + $date = Carbon::parse('2017-01-22 17:56:03'); + App::setLocale('jp'); + + $this->assertEquals( + 'Jan 22, 2017', + DateHelper::getShortDate($date) + ); + } + + public function testGetFullDateWithEnglishLocale() + { + $date = Carbon::parse('2017-01-22 17:56:03'); + App::setLocale('en'); + + $this->assertEquals( + 'January 22, 2017', + DateHelper::getFullDate($date) + ); + } + + public function testGetFullDateWithFrenchLocale() + { + $date = Carbon::parse('2017-01-22 17:56:03'); + App::setLocale('fr'); + + $this->assertEquals( + '22 janvier 2017', + DateHelper::getFullDate($date) + ); + } + + public function testGetFullDateWithUnknownLocale() + { + $date = Carbon::parse('2017-01-22 17:56:03'); + App::setLocale('jp'); + + $this->assertEquals( + 'January 22, 2017', + DateHelper::getFullDate($date) + ); + } + + public function testGetShortDateWithTimeWithEnglishLocale() + { + $date = Carbon::parse('2017-01-22 17:56:03'); + App::setLocale('en'); + + $this->assertEquals( + 'Jan 22, 2017 17:56', + DateHelper::getShortDateWithTime($date) + ); + } + + public function testGetShortDateWithTimeWithFrenchLocale() + { + $date = Carbon::parse('2017-01-22 17:56:03'); + App::setLocale('fr'); + + $this->assertEquals( + '22 janv. 2017 17:56', + DateHelper::getShortDateWithTime($date) + ); + } + + public function testGetShortDateWithTimeWithUnknownLocale() + { + $date = Carbon::parse('2017-01-22 17:56:03'); + App::setLocale('jp'); + + $this->assertEquals( + 'Jan 22, 2017 17:56', + DateHelper::getShortDateWithTime($date) + ); + } + + public function test_get_short_date_without_year_returns_a_date() + { + $date = Carbon::parse('2017-01-22 17:56:03'); + App::setLocale('en'); + + $this->assertEquals( + 'Jan 22', + DateHelper::getShortDateWithoutYear($date) + ); + + App::setLocale('fr'); + + $this->assertEquals( + '22 janv.', + DateHelper::getShortDateWithoutYear($date) + ); + } + + public function test_it_returns_the_default_short_date() + { + $date = Carbon::parse('2017-01-22 17:56:03'); + App::setLocale(null); + + $this->assertEquals( + 'Jan 22', + DateHelper::getShortDateWithoutYear($date) + ); + } + + public function test_add_time_according_to_frequency_type_returns_the_right_value() + { + $date = '2017-01-22 17:56:03'; + + $testDate = DateHelper::parseDateTime($date); + $this->assertEquals( + '2017-01-29', + DateHelper::addTimeAccordingToFrequencyType($testDate, 'week', 1)->toDateString() + ); + + $testDate = DateHelper::parseDateTime($date); + $this->assertEquals( + '2017-02-22', + DateHelper::addTimeAccordingToFrequencyType($testDate, 'month', 1)->toDateString() + ); + + $testDate = DateHelper::parseDateTime($date); + $this->assertEquals( + '2018-01-22', + DateHelper::addTimeAccordingToFrequencyType($testDate, 'year', 1)->toDateString() + ); + } + + public function test_parse_dateTime() + { + $testDate = DateHelper::parseDateTime(null); + + $this->assertNull($testDate); + + $date = '2017-01-22 17:56:03'; + + $testDate = DateHelper::parseDateTime($date); + + $this->assertInstanceOf(Carbon::class, $testDate); + } + + public function test_parse_dateTime_bad() + { + $date = 'xF 2017'; + + $testDate = DateHelper::parseDateTime($date); + + $this->assertNull($testDate); + } + + public function test_parse_parseDate_bad() + { + $date = 'xF 2017'; + + $testDate = DateHelper::parseDate($date); + + $this->assertNull($testDate); + } + + public function test_parse_dateTime_format() + { + $date = '20190120T232144Z'; + + $testDate = DateHelper::parseDateTime($date); + + $this->assertEquals(2019, $testDate->year); + $this->assertEquals(1, $testDate->month); + $this->assertEquals(20, $testDate->day); + $this->assertEquals(23, $testDate->hour); + $this->assertEquals(21, $testDate->minute); + $this->assertEquals(44, $testDate->second); + $this->assertEquals('UTC', $testDate->timezone->getName()); + + $this->assertEquals( + '2019-01-20', + $testDate->toDateString() + ); + $this->assertEquals( + '2019-01-20T23:21:44Z', + DateHelper::getTimestamp($testDate) + ); + } + + public function test_parse_dateTime_utc() + { + $date = '2017-01-22 17:56:03'; + + $testDate = DateHelper::parseDateTime($date); + + $this->assertEquals(2017, $testDate->year); + $this->assertEquals(1, $testDate->month); + $this->assertEquals(22, $testDate->day); + $this->assertEquals(17, $testDate->hour); + $this->assertEquals(56, $testDate->minute); + $this->assertEquals(03, $testDate->second); + $this->assertEquals('UTC', $testDate->timezone->getName()); + + $this->assertEquals( + '2017-01-22', + $testDate->toDateString() + ); + $this->assertEquals( + '2017-01-22T17:56:03Z', + DateHelper::getTimestamp($testDate) + ); + } + + public function test_parse_dateTime_new_york() + { + $date = '2017-01-22 17:56:03'; + $timezone = 'America/New_York'; + + $testDate = DateHelper::parseDateTime($date, $timezone); + + $this->assertEquals(2017, $testDate->year); + $this->assertEquals(1, $testDate->month); + $this->assertEquals(22, $testDate->day); + $this->assertEquals(22, $testDate->hour); + $this->assertEquals(56, $testDate->minute); + $this->assertEquals(03, $testDate->second); + $this->assertEquals('UTC', $testDate->timezone->getName()); + + $this->assertEquals( + '2017-01-22', + $testDate->toDateString() + ); + $this->assertEquals( + '2017-01-22T22:56:03Z', + DateHelper::getTimestamp($testDate) + ); + } + + public function test_parse_dateTime_paris() + { + $date = '2019-01-01 00:56:03'; + $timezone = 'Europe/Paris'; + + $testDate = DateHelper::parseDateTime($date, $timezone); + + $this->assertEquals(2018, $testDate->year); + $this->assertEquals(12, $testDate->month); + $this->assertEquals(31, $testDate->day); + $this->assertEquals(23, $testDate->hour); + $this->assertEquals(56, $testDate->minute); + $this->assertEquals(03, $testDate->second); + $this->assertEquals('UTC', $testDate->timezone->getName()); + + $this->assertEquals( + '2018-12-31', + $testDate->toDateString() + ); + $this->assertEquals( + '2018-12-31T23:56:03Z', + DateHelper::getTimestamp($testDate) + ); + } + + public function test_parse_dateTime_carbon() + { + $date = new Carbon('2019-01-01 00:56:03', 'Europe/Paris'); + + $testDate = DateHelper::parseDateTime($date); + + $this->assertEquals(2018, $testDate->year); + $this->assertEquals(12, $testDate->month); + $this->assertEquals(31, $testDate->day); + $this->assertEquals(23, $testDate->hour); + $this->assertEquals(56, $testDate->minute); + $this->assertEquals(03, $testDate->second); + $this->assertEquals('UTC', $testDate->timezone->getName()); + + $this->assertEquals( + '2018-12-31', + $testDate->toDateString() + ); + $this->assertEquals( + '2018-12-31T23:56:03Z', + DateHelper::getTimestamp($testDate) + ); + } + + public function test_parse_dateTime_dateTimeObject() + { + $date = new \DateTime('2019-01-01 00:56:03', new \DateTimeZone('Europe/Paris')); + + $testDate = DateHelper::parseDateTime($date); + + $this->assertEquals(2018, $testDate->year); + $this->assertEquals(12, $testDate->month); + $this->assertEquals(31, $testDate->day); + $this->assertEquals(23, $testDate->hour); + $this->assertEquals(56, $testDate->minute); + $this->assertEquals(03, $testDate->second); + $this->assertEquals('UTC', $testDate->timezone->getName()); + + $this->assertEquals( + '2018-12-31', + $testDate->toDateString() + ); + $this->assertEquals( + '2018-12-31T23:56:03Z', + DateHelper::getTimestamp($testDate) + ); + } + + public function testGetShortMonthWithEnglishLocale() + { + $date = Carbon::parse('2017-01-22 17:56:03'); + App::setLocale('en'); + + $this->assertEquals( + 'Jan', + DateHelper::getShortMonth($date) + ); + } + + public function testGetShortMonthWithFrenchLocale() + { + $date = Carbon::parse('2017-01-22 17:56:03'); + App::setLocale('fr'); + + $this->assertEquals( + 'janv.', + DateHelper::getShortMonth($date) + ); + } + + public function testGetShortMonthWithUnknownLocale() + { + $date = Carbon::parse('2017-01-22 17:56:03'); + App::setLocale('jp'); + + $this->assertEquals( + 'Jan', + DateHelper::getShortMonth($date) + ); + } + + public function testGetFullMonthAndDateWithEnglishLocale() + { + $date = Carbon::parse('2017-01-22 17:56:03'); + App::setLocale('en'); + + $this->assertEquals( + 'January 2017', + DateHelper::getFullMonthAndDate($date) + ); + } + + public function testGetFullMonthAndDateWithFrenchLocale() + { + $date = Carbon::parse('2017-01-22 17:56:03'); + App::setLocale('fr'); + + $this->assertEquals( + 'janvier 2017', + DateHelper::getFullMonthAndDate($date) + ); + } + + public function testGetFullMonthAndDateWithUnknownLocale() + { + $date = Carbon::parse('2017-01-22 17:56:03'); + App::setLocale('jp'); + + $this->assertEquals( + 'January 2017', + DateHelper::getFullMonthAndDate($date) + ); + } + + public function testGetShortDayWithEnglishLocale() + { + $date = Carbon::parse('2017-01-22 17:56:03'); + App::setLocale('en'); + + $this->assertEquals( + 'Sun', + DateHelper::getShortDay($date) + ); + } + + public function testGetShortDayWithFrenchLocale() + { + $date = Carbon::parse('2017-01-22 17:56:03'); + App::setLocale('fr'); + + $this->assertEquals( + 'dim.', + DateHelper::getShortDay($date) + ); + } + + public function testGetShortDayWithUnknownLocale() + { + $date = Carbon::parse('2017-01-22 17:56:03'); + App::setLocale('jp'); + + $this->assertEquals( + 'Sun', + DateHelper::getShortDay($date) + ); + } + + public function test_get_month_and_year() + { + Carbon::setTestNow(Carbon::create(2017, 1, 1)); + + $this->assertEquals( + 'Jul 2017', + DateHelper::getMonthAndYear(6) + ); + } + + public function test_it_gets_date_one_month_from_now() + { + Carbon::setTestNow(Carbon::create(2017, 1, 1)); + + $this->assertEquals( + '2017-02-01', + DateHelper::getNextTheoriticalBillingDate('monthly')->toDateString() + ); + } + + public function test_it_gets_date_one_year_from_now() + { + Carbon::setTestNow(Carbon::create(2017, 1, 1)); + + $this->assertEquals( + '2018-01-01', + DateHelper::getNextTheoriticalBillingDate('yearly')->toDateString() + ); + } + + public function test_it_returns_a_list_with_years() + { + $user = $this->signIn(); + $user->locale = 'en'; + $user->save(); + + $this->assertCount( + 3, + DateHelper::getListOfYears(2) + ); + + $this->assertEquals( + now()->year, + DateHelper::getListOfYears(2)->first()['name'] + ); + $this->assertEquals( + now()->subYears(2)->year, + DateHelper::getListOfYears(2)->last()['name'] + ); + $this->assertEquals( + now()->subYears(-2)->year, + DateHelper::getListOfYears(2, -2)->first()['name'] + ); + $this->assertEquals( + now()->year, + DateHelper::getListOfYears(2, -2)[2]['name'] + ); + } + + public function test_it_returns_a_list_with_twelve_months() + { + $user = $this->signIn(); + $user->locale = 'en'; + $user->save(); + + $this->assertCount( + 12, + DateHelper::getListOfMonths() + ); + } + + public function test_it_returns_a_list_of_months_in_english() + { + $user = $this->signIn(); + $user->locale = 'en'; + $user->save(); + + $months = DateHelper::getListOfMonths(); + + $this->assertEquals( + 'January', + $months[0]['name'] + ); + } + + public function test_it_returns_a_list_with_thirty_one_days() + { + $user = $this->signIn(); + $user->locale = 'en'; + $user->save(); + + $this->assertCount( + 31, + DateHelper::getListOfDays() + ); + } + + public function test_it_returns_a_list_with_twenty_four_hours() + { + $this->assertCount( + 24, + DateHelper::getListOfHours() + ); + } + + public function test_it_returns_a_list_of_hours() + { + $hours = DateHelper::getListOfHours(); + + $this->assertEquals( + '01.00 AM', + $hours[0]['name'] + ); + + $this->assertEquals( + '01:00', + $hours[0]['id'] + ); + + $this->assertEquals( + '02.00 PM', + $hours[13]['name'] + ); + + $this->assertEquals( + '14:00', + $hours[13]['id'] + ); + } + + public function test_it_returns_a_list_of_hours_French() + { + App::setLocale('fr'); + $hours = DateHelper::getListOfHours(); + + $this->assertEquals( + '01:00', + $hours[0]['name'] + ); + + $this->assertEquals( + '01:00', + $hours[0]['id'] + ); + + $this->assertEquals( + '14:00', + $hours[13]['name'] + ); + + $this->assertEquals( + '14:00', + $hours[13]['id'] + ); + } + + public function test_old_timezones_exists() + { + // These are all currently used timezone in monica + $oldTimezones = [ + 'US/Eastern', + 'US/Central', + 'America/Los_Angeles', + 'Pacific/Midway', + 'Pacific/Samoa', + 'Pacific/Honolulu', + 'US/Alaska', + 'America/Tijuana', + 'US/Arizona', + 'America/Chihuahua', + 'America/Chihuahua', + 'America/Mazatlan', + 'US/Mountain', + 'America/Managua', + 'US/Central', + 'America/Mexico_City', + 'America/Mexico_City', + 'America/Monterrey', + 'Canada/Saskatchewan', + 'America/Bogota', + 'US/Eastern', + 'US/East-Indiana', + 'America/Lima', + 'America/Bogota', + 'Canada/Atlantic', + 'America/Caracas', + 'America/La_Paz', + 'America/Santiago', + 'Canada/Newfoundland', + 'America/Sao_Paulo', + 'America/Argentina/Buenos_Aires', + 'America/Noronha', + 'Atlantic/Azores', + 'Atlantic/Cape_Verde', + 'Africa/Casablanca', + 'Europe/London', + 'Etc/Greenwich', + 'Europe/Lisbon', + 'Europe/London', + 'Africa/Monrovia', + 'UTC', + 'Europe/Amsterdam', + 'Europe/Belgrade', + 'Europe/Berlin', + 'Europe/Bratislava', + 'Europe/Brussels', + 'Europe/Budapest', + 'Europe/Copenhagen', + 'Europe/Ljubljana', + 'Europe/Madrid', + 'Europe/Paris', + 'Europe/Prague', + 'Europe/Rome', + 'Europe/Sarajevo', + 'Europe/Skopje', + 'Europe/Stockholm', + 'Europe/Vienna', + 'Europe/Warsaw', + 'Africa/Lagos', + 'Europe/Zagreb', + 'Europe/Zurich', + 'Europe/Athens', + 'Europe/Bucharest', + 'Africa/Cairo', + 'Africa/Harare', + 'Europe/Helsinki', + 'Europe/Istanbul', + 'Asia/Jerusalem', + 'Europe/Helsinki', + 'Africa/Johannesburg', + 'Europe/Riga', + 'Europe/Sofia', + 'Europe/Tallinn', + 'Europe/Vilnius', + 'Asia/Baghdad', + 'Asia/Kuwait', + 'Europe/Minsk', + 'Africa/Nairobi', + 'Asia/Riyadh', + 'Europe/Volgograd', + 'Asia/Tehran', + 'Asia/Muscat', + 'Asia/Baku', + 'Europe/Moscow', + 'Asia/Muscat', + 'Europe/Moscow', + 'Asia/Tbilisi', + 'Asia/Yerevan', + 'Asia/Kabul', + 'Asia/Karachi', + 'Asia/Karachi', + 'Asia/Tashkent', + 'Asia/Calcutta', + 'Asia/Kolkata', + 'Asia/Calcutta', + 'Asia/Calcutta', + 'Asia/Calcutta', + 'Asia/Katmandu', + 'Asia/Almaty', + 'Asia/Dhaka', + 'Asia/Dhaka', + 'Asia/Yekaterinburg', + 'Asia/Rangoon', + 'Asia/Bangkok', + 'Asia/Bangkok', + 'Asia/Jakarta', + 'Asia/Novosibirsk', + 'Asia/Hong_Kong', + 'Asia/Chongqing', + 'Asia/Hong_Kong', + 'Asia/Krasnoyarsk', + 'Asia/Kuala_Lumpur', + 'Australia/Perth', + 'Asia/Singapore', + 'Asia/Taipei', + 'Asia/Ulan_Bator', + 'Asia/Urumqi', + 'Asia/Irkutsk', + 'Asia/Tokyo', + 'Asia/Tokyo', + 'Asia/Seoul', + 'Asia/Tokyo', + 'Australia/Adelaide', + 'Australia/Darwin', + 'Australia/Brisbane', + 'Australia/Canberra', + 'Pacific/Guam', + 'Australia/Hobart', + 'Australia/Melbourne', + 'Pacific/Port_Moresby', + 'Australia/Sydney', + 'Asia/Yakutsk', + 'Asia/Vladivostok', + 'Pacific/Auckland', + 'Pacific/Fiji', + 'Pacific/Kwajalein', + 'Asia/Kamchatka', + 'Asia/Magadan', + 'Pacific/Fiji', + 'Asia/Magadan', + 'Asia/Magadan', + 'Pacific/Auckland', + 'Pacific/Tongatapu', + ]; + + $list = TimezoneHelper::getListOfTimezones(); + $list = collect($list); + + $missed = ''; + foreach ($oldTimezones as $timezone) { + $timezone = TimezoneHelper::adjustEquivalentTimezone($timezone); + if ($list->firstWhere('timezone', $timezone) == null) { + $missed .= $timezone.','; + } + } + + $this->assertTrue(empty($missed), 'Missed timezones : '.$missed); + } +} diff --git a/tests/Unit/Helpers/FormHelperTest.php b/tests/Unit/Helpers/FormHelperTest.php new file mode 100644 index 0000000..8c4490a --- /dev/null +++ b/tests/Unit/Helpers/FormHelperTest.php @@ -0,0 +1,54 @@ +create([]); + $user->name_order = 'firstname_lastname'; + $this->assertEquals( + 'firstname', + FormHelper::getNameOrderForForms($user) + ); + + $user->name_order = 'firstname_lastname_nickname'; + $this->assertEquals( + 'firstname', + FormHelper::getNameOrderForForms($user) + ); + + $user->name_order = 'firstname_nickname_lastname'; + $this->assertEquals( + 'firstname', + FormHelper::getNameOrderForForms($user) + ); + + $user->name_order = 'lastname_firstname'; + $this->assertEquals( + 'lastname', + FormHelper::getNameOrderForForms($user) + ); + + $user->name_order = 'lastname_firstname_nickname'; + $this->assertEquals( + 'lastname', + FormHelper::getNameOrderForForms($user) + ); + + $user->name_order = 'lastname_nickname_firstname'; + $this->assertEquals( + 'lastname', + FormHelper::getNameOrderForForms($user) + ); + } +} diff --git a/tests/Unit/Helpers/GenderHelperTest.php b/tests/Unit/Helpers/GenderHelperTest.php new file mode 100644 index 0000000..654c1b6 --- /dev/null +++ b/tests/Unit/Helpers/GenderHelperTest.php @@ -0,0 +1,54 @@ +signIn(); + + $genders = GenderHelper::getGendersInput(); + + $this->assertCount(4, $genders); + $this->assertEquals([ + 'id' => '', + 'name' => 'No gender', + ], $genders[0]); + } + + /** @test */ + public function it_replaces_gender_with_another_gender() + { + $account = factory(Account::class)->create(); + $male = factory(Gender::class)->create([ + 'account_id' => $account->id, + ]); + $female = factory(Gender::class)->create([ + 'account_id' => $account->id, + ]); + + factory(Contact::class, 2)->create([ + 'account_id' => $account->id, + 'gender_id' => $male, + ]); + factory(Contact::class)->create([ + 'account_id' => $account->id, + 'gender_id' => $female, + ]); + + GenderHelper::replace($account, $male, $female); + + $this->assertEquals( + 3, + $female->contacts->count() + ); + } +} diff --git a/tests/Unit/Helpers/InstanceHelperTest.php b/tests/Unit/Helpers/InstanceHelperTest.php new file mode 100644 index 0000000..be1cfb9 --- /dev/null +++ b/tests/Unit/Helpers/InstanceHelperTest.php @@ -0,0 +1,184 @@ +create(['stripe_id' => 'id292839']); + factory(Account::class)->create(); + factory(Account::class)->create(['stripe_id' => 'id2sdf92839']); + + $this->assertEquals( + 2, + InstanceHelper::getNumberOfPaidSubscribers() + ); + } + + /** @test */ + public function it_fetches_the_monthly_plan_information() + { + config(['monica.paid_plan_monthly_friendly_name' => 'Monthly']); + config(['monica.paid_plan_monthly_id' => 'monthly']); + config(['monica.paid_plan_monthly_price' => 1000]); + + $this->assertEquals( + 'monthly', + InstanceHelper::getPlanInformationFromConfig('monthly')['type'] + ); + + $this->assertEquals( + 'Monthly', + InstanceHelper::getPlanInformationFromConfig('monthly')['name'] + ); + + $this->assertEquals( + 'monthly', + InstanceHelper::getPlanInformationFromConfig('monthly')['id'] + ); + + $this->assertEquals( + 1000, + InstanceHelper::getPlanInformationFromConfig('monthly')['price'] + ); + + $this->assertEquals( + '$10.00', + InstanceHelper::getPlanInformationFromConfig('monthly')['friendlyPrice'] + ); + } + + /** @test */ + public function it_fetches_the_annually_plan_information() + { + config(['monica.paid_plan_annual_friendly_name' => 'Annual']); + config(['monica.paid_plan_annual_id' => 'annual']); + config(['monica.paid_plan_annual_price' => 1000]); + + $this->assertEquals( + 'annual', + InstanceHelper::getPlanInformationFromConfig('annual')['type'] + ); + + $this->assertEquals( + 'Annual', + InstanceHelper::getPlanInformationFromConfig('annual')['name'] + ); + + $this->assertEquals( + 'annual', + InstanceHelper::getPlanInformationFromConfig('annual')['id'] + ); + + $this->assertEquals( + 1000, + InstanceHelper::getPlanInformationFromConfig('annual')['price'] + ); + + $this->assertEquals( + '$10.00', + InstanceHelper::getPlanInformationFromConfig('annual')['friendlyPrice'] + ); + } + + /** @test */ + public function it_fetches_subscription_information() + { + $stripeSubscription = (object) [ + 'plan' => (object) [ + 'currency' => 'USD', + 'amount' => 500, + 'interval' => 'month', + 'id' => 'monthly', + ], + 'current_period_end' => 1629976560, + ]; + + $subscription = Mockery::mock('\Laravel\Cashier\Subscription'); + $subscription->shouldReceive('asStripeSubscription') + ->andReturn($stripeSubscription); + $subscription->shouldReceive('getAttribute') + ->with('name') + ->andReturn('Monthly'); + + $this->assertEquals( + 'monthly', + InstanceHelper::getPlanInformationFromSubscription($subscription)['type'] + ); + + $this->assertEquals( + 'Monthly', + InstanceHelper::getPlanInformationFromSubscription($subscription)['name'] + ); + + $this->assertEquals( + 'monthly', + InstanceHelper::getPlanInformationFromSubscription($subscription)['id'] + ); + + $this->assertEquals( + 500, + InstanceHelper::getPlanInformationFromSubscription($subscription)['price'] + ); + + $this->assertEquals( + '$5.00', + InstanceHelper::getPlanInformationFromSubscription($subscription)['friendlyPrice'] + ); + } + + /** @test */ + public function it_returns_null_when_fetching_an_unknown_plan_information() + { + $account = new Account; + + $this->assertNull( + InstanceHelper::getPlanInformationFromConfig('unknown_plan') + ); + } + + /** @test */ + public function it_gets_latest_changelog_entries() + { + $json = public_path('changelog.json'); + $changelogs = json_decode(file_get_contents($json), true)['entries']; + $count = count($changelogs); + + $this->assertCount( + $count, + InstanceHelper::getChangelogEntries() + ); + + $this->assertCount( + 3, + InstanceHelper::getChangelogEntries(3) + ); + } + + /** @test */ + public function it_checks_if_the_instance_has_at_least_one_account() + { + DB::table('accounts')->delete(); + + $this->assertFalse( + InstanceHelper::hasAtLeastOneAccount() + ); + + factory(Account::class)->create(); + $this->assertTrue( + InstanceHelper::hasAtLeastOneAccount() + ); + } +} diff --git a/tests/Unit/Helpers/JournalHelperTest.php b/tests/Unit/Helpers/JournalHelperTest.php new file mode 100644 index 0000000..5f37cba --- /dev/null +++ b/tests/Unit/Helpers/JournalHelperTest.php @@ -0,0 +1,37 @@ +create([]); + $user = factory(User::class)->create(['account_id' => $account->id]); + + $this->assertFalse(JournalHelper::hasAlreadyRatedToday($user)); + } + + /** @test */ + public function you_cant_vote_if_you_have_already_voted_today() + { + $account = factory(Account::class)->create([]); + $user = factory(User::class)->create(['account_id' => $account->id]); + factory(Day::class)->create([ + 'account_id' => $account->id, + 'date' => now(), + ]); + + $this->assertTrue(JournalHelper::hasAlreadyRatedToday($user)); + } +} diff --git a/tests/Unit/Helpers/LocaleHelperTest.php b/tests/Unit/Helpers/LocaleHelperTest.php new file mode 100644 index 0000000..4d440eb --- /dev/null +++ b/tests/Unit/Helpers/LocaleHelperTest.php @@ -0,0 +1,162 @@ +assertEquals( + 'en', + LocaleHelper::getLocale() + ); + } + + /** @test */ + public function get_locale_returns_right_locale_if_user_logged() + { + $user = $this->signIn(); + $user->locale = 'fr'; + $user->save(); + + $this->assertEquals( + 'fr', + LocaleHelper::getLocale() + ); + } + + /** @test */ + public function get_direction_default() + { + $this->assertEquals( + 'ltr', + LocaleHelper::getDirection() + ); + } + + /** @test */ + public function get_direction_french() + { + App::setLocale('fr'); + + $this->assertEquals( + 'ltr', + LocaleHelper::getDirection() + ); + } + + /** @test */ + public function get_direction_hebrew() + { + App::setLocale('he'); + + $this->assertEquals( + 'rtl', + LocaleHelper::getDirection() + ); + } + + /** @test */ + public function format_telephone_by_iso() + { + $tel = LocaleHelper::formatTelephoneNumberByISO('202-555-0191', 'gb'); + + $this->assertEquals( + '+44 20 2555 0191', + $tel + ); + } + + /** + * @dataProvider localeHelperGetLangProvider + */ + public function test_locale_get_lang($locale, $expect) + { + $lang = LocaleHelper::getLang($locale); + + $this->assertEquals( + $expect, + $lang + ); + } + + public function localeHelperGetLangProvider() + { + return [ + ['en', 'en'], + ['En', 'en'], + ['EN', 'en'], + ['en-US', 'en'], + ['en-us', 'en'], + ['en_US', 'en'], + ['pt-BR', 'pt'], + ['xx-YY', 'xx'], + ]; + } + + /** + * @dataProvider localeHelperGetCountryProvider + */ + public function test_locale_get_country($locale, $expect) + { + $country = LocaleHelper::getCountry($locale); + + $this->assertEquals( + $expect, + $country + ); + } + + public function localeHelperGetCountryProvider() + { + return [ + ['en', 'US'], + ['en-us', 'US'], + ['en-US', 'US'], + ['en_US', 'US'], + ['pt-BR', 'BR'], + ['xx-YY', 'YY'], + ]; + } + + /** + * @dataProvider localeHelperExtractCountryProvider + */ + public function test_locale_extract_country($locale, $expect) + { + $country = LocaleHelper::extractCountry($locale); + + $this->assertEquals( + $expect, + $country + ); + + App::setLocale($locale); + + $country = LocaleHelper::extractCountry(); + + $this->assertEquals( + $expect, + $country + ); + } + + public function localeHelperExtractCountryProvider() + { + return [ + ['en', null], + ['fr', null], + ['en-US', 'US'], + ['pt-BR', 'BR'], + ['xx-YY', 'YY'], + ]; + } +} diff --git a/tests/Unit/Helpers/MoneyHelperTest.php b/tests/Unit/Helpers/MoneyHelperTest.php new file mode 100644 index 0000000..3909538 --- /dev/null +++ b/tests/Unit/Helpers/MoneyHelperTest.php @@ -0,0 +1,114 @@ +iso = 'EUR'; + + $this->assertEquals('€500.00', MoneyHelper::format(50000, $currency)); + $this->assertEquals('€5,038.29', MoneyHelper::format(503829, $currency)); + $this->assertEquals('500.00', MoneyHelper::getValue(50000, $currency)); + $this->assertEquals('5038.29', MoneyHelper::getValue(503829, $currency)); + $this->assertEquals(500, MoneyHelper::exchangeValue(50000, $currency)); + $this->assertEquals(5038.29, MoneyHelper::exchangeValue(503829, $currency)); + } + + /** @test */ + public function it_returns_the_amount_with_the_currency_symbol_in_the_right_locale() + { + App::setLocale('fr'); + + $currency = new Currency(); + $currency->iso = 'EUR'; + + $this->assertEquals('500,00 €', MoneyHelper::format(50000, $currency)); + $this->assertEquals('5 038,29 €', MoneyHelper::format(503829, $currency)); + $this->assertEquals('500,00', MoneyHelper::getValue(50000, $currency)); + $this->assertEquals('5038,29', MoneyHelper::getValue(503829, $currency)); + $this->assertEquals(500, MoneyHelper::exchangeValue(50000, $currency)); + $this->assertEquals(5038.29, MoneyHelper::exchangeValue(503829, $currency)); + } + + /** @test */ + public function it_returns_the_amount_with_the_currency_symbol_with_the_right_punctuation() + { + $currency = new Currency(); + $currency->iso = 'JPY'; // minorUnit value is zero "0" + + $this->assertEquals('¥500', MoneyHelper::format(500, $currency)); + $this->assertEquals('¥5,038', MoneyHelper::format(5038, $currency)); + $this->assertEquals('500', MoneyHelper::getValue(500, $currency)); + $this->assertEquals('5038', MoneyHelper::getValue(5038, $currency)); + $this->assertEquals(500, MoneyHelper::exchangeValue(500, $currency)); + $this->assertEquals(5038, MoneyHelper::exchangeValue(5038, $currency)); + } + + /** @test */ + public function it_formats_the_currency_with_the_right_locale() + { + $currency = Currency::where('iso', 'GBP')->first(); + $user = factory(User::class)->create([ + 'currency_id' => $currency->id, + ]); + $this->actingAs($user); + + $this->assertEquals('£75.00', MoneyHelper::format(7500, $currency)); + $this->assertEquals('£2,734.12', MoneyHelper::format(273412, $currency)); + $this->assertEquals('75.00', MoneyHelper::getValue(7500, $currency)); + $this->assertEquals('2734.12', MoneyHelper::getValue(273412, $currency)); + $this->assertEquals(75, MoneyHelper::exchangeValue(7500, $currency)); + $this->assertEquals(2734.12, MoneyHelper::exchangeValue(273412, $currency)); + } + + /** @test */ + public function it_returns_the_amount_without_the_currency_symbol_if_not_provided() + { + $this->assertEquals('500', MoneyHelper::format(500)); + $this->assertEquals('5,000', MoneyHelper::format(5000)); + } + + /** @test */ + public function it_returns_zero_if_amount_is_null() + { + $this->assertEquals('0', MoneyHelper::format(null)); + } + + /** @test */ + public function it_covers_brazilian_currency() + { + $currency = Currency::where('iso', 'BRL')->first(); + + $user = factory(User::class)->create([ + 'currency_id' => $currency->id, + ]); + $this->actingAs($user); + + $this->assertEquals('R$12,345.67', MoneyHelper::format(1234567, $currency)); + $this->assertEquals('12345.67', MoneyHelper::getValue(1234567, $currency)); + $this->assertEquals(12345.67, MoneyHelper::exchangeValue(1234567, $currency)); + } + + /** @test */ + public function it_parse_an_input_value() + { + $currency = new Currency(); + $currency->iso = 'EUR'; + + $this->assertEquals(50000, MoneyHelper::parseInput('500.00', $currency)); + $this->assertEquals(503829, MoneyHelper::parseInput('5038.29', $currency)); + } +} diff --git a/tests/Unit/Helpers/RequestHelperTest.php b/tests/Unit/Helpers/RequestHelperTest.php new file mode 100644 index 0000000..8c5d6ed --- /dev/null +++ b/tests/Unit/Helpers/RequestHelperTest.php @@ -0,0 +1,106 @@ +headers->set('Cf-Connecting-Ip', '1.2.3.4'); + + $this->assertEquals( + '1.2.3.4', + RequestHelper::ip() + ); + } + + /** @test */ + public function get_server_ip() + { + Request::instance()->server->set('REMOTE_ADDR', '1.2.3.4'); + + $this->assertEquals( + '1.2.3.4', + RequestHelper::ip() + ); + } + + /** @test */ + public function get_country_from_cf() + { + Request::instance()->headers->set('Cf-Ipcountry', 'XX'); + + $this->assertEquals( + 'XX', + RequestHelper::country('1.2.3.4') + ); + } + + /** @test */ + public function get_country_from_ip() + { + $driver = $this->mock(\Stevebauman\Location\Drivers\Driver::class, function (MockInterface $mock) { + $mock->shouldReceive('get') + ->with('123.45.67.89') + ->andReturn(tap(new \Stevebauman\Location\Position(), function ($position) { + $position->countryCode = 'TEST'; + })); + }); + Location::setDriver($driver); + + Request::instance()->server->set('REMOTE_ADDR', '123.45.67.89'); + + $this->assertEquals( + 'TEST', + RequestHelper::country(null) + ); + } + + /** @test */ + public function get_infos_from_ip() + { + config(['location.ipdata.token' => 'test']); + + $body = file_get_contents(base_path('tests/Fixtures/Helpers/ipdata.json')); + Http::fake([ + 'https://api.ipdata.co/*' => Http::response($body, 200), + ]); + + $this->assertEquals( + [ + 'country' => 'FR', + 'currency' => 'EUR', + 'timezone' => 'Europe/Paris', + ], + RequestHelper::infos('test') + ); + } + + /** @test */ + public function get_infos_from_ip_fail() + { + config(['location.ipdata.token' => 'test']); + + Http::fake([ + 'https://api.ipdata.co/*' => Http::response(null, 500), + ]); + Request::instance()->headers->set('Cf-Ipcountry', 'XX'); + + $this->assertEquals( + [ + 'country' => 'XX', + 'currency' => null, + 'timezone' => null, + ], + RequestHelper::infos('test') + ); + } +} diff --git a/tests/Unit/Helpers/SearchHelperTest.php b/tests/Unit/Helpers/SearchHelperTest.php new file mode 100644 index 0000000..0be70c5 --- /dev/null +++ b/tests/Unit/Helpers/SearchHelperTest.php @@ -0,0 +1,81 @@ +signin(); + + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $searchResults = SearchHelper::searchContacts($contact->first_name, 'created_at') + ->paginate(1); + + $this->assertNotNull($searchResults); + $this->assertInstanceOf('Illuminate\Pagination\LengthAwarePaginator', $searchResults); + $this->assertCount(1, $searchResults); + } + + /** disabled for now */ + public function searching_with_notes() + { + $user = $this->signin(); + + $note = factory(Note::class)->create([ + 'account_id' => $user->account_id, + 'body' => 'we met on github and talked about monica', + ]); + + $searchResults = SearchHelper::searchContacts('monica', 'created_at') + ->paginate(1); + + $this->assertNotNull($searchResults); + $this->assertInstanceOf('Illuminate\Pagination\LengthAwarePaginator', $searchResults); + $this->assertCount(1, $searchResults); + } + + /** disabled for now */ + public function searching_with_introduction_information() + { + $user = $this->signin(); + + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + 'first_met_additional_info' => 'github', + ]); + $searchResults = SearchHelper::searchContacts($contact->first_met_additional_info, 'created_at') + ->paginate(1); + + $this->assertNotNull($searchResults); + $this->assertInstanceOf('Illuminate\Pagination\LengthAwarePaginator', $searchResults); + $this->assertCount(1, $searchResults); + } + + /** @test */ + public function searching_with_wrong_search_field() + { + $user = $this->signin(); + + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $searchResults = SearchHelper::searchContacts('wrongsearchfield:1', 'created_at') + ->paginate(1); + + $this->assertNotNull($searchResults); + $this->assertInstanceOf('Illuminate\Pagination\LengthAwarePaginator', $searchResults); + $this->assertCount(0, $searchResults); + } +} diff --git a/tests/Unit/Helpers/StorageHelperTest.php b/tests/Unit/Helpers/StorageHelperTest.php new file mode 100644 index 0000000..c269400 --- /dev/null +++ b/tests/Unit/Helpers/StorageHelperTest.php @@ -0,0 +1,68 @@ +create([]); + + factory(Document::class)->create([ + 'filesize' => 1000000, + 'account_id' => $account->id, + ]); + + factory(Photo::class)->create([ + 'filesize' => 1000000, + 'account_id' => $account->id, + ]); + + $this->assertEquals( + 2000000, + StorageHelper::getAccountStorageSize($account) + ); + } + + /** @test */ + public function it_tests_account_storage_limit(): void + { + config(['monica.requires_subscription' => true]); + $account = factory(Account::class)->create([]); + + factory(Document::class)->create([ + 'filesize' => 1000000, + 'account_id' => $account->id, + ]); + + config(['monica.max_storage_size' => 0.1]); + $this->assertTrue(StorageHelper::hasReachedAccountStorageLimit($account)); + + config(['monica.max_storage_size' => 1]); + $this->assertFalse(StorageHelper::hasReachedAccountStorageLimit($account)); + + factory(Photo::class)->create([ + 'filesize' => 1000000, + 'account_id' => $account->id, + ]); + + config(['monica.max_storage_size' => 2]); + $this->assertFalse(StorageHelper::hasReachedAccountStorageLimit($account)); + + config(['monica.max_storage_size' => 1]); + $this->assertTrue(StorageHelper::hasReachedAccountStorageLimit($account)); + + config(['monica.requires_subscription' => false]); + $this->assertFalse(StorageHelper::hasReachedAccountStorageLimit($account)); + } +} diff --git a/tests/Unit/Helpers/VCardHelperTest.php b/tests/Unit/Helpers/VCardHelperTest.php new file mode 100644 index 0000000..3d4ccaa --- /dev/null +++ b/tests/Unit/Helpers/VCardHelperTest.php @@ -0,0 +1,26 @@ + '202-555-0191', + 'ADR' => ['', '', '17 Shakespeare Ave.', 'Southampton', '', 'SO17 2HB', 'United Kingdom'], + ]); + + $iso = VCardHelper::getCountryISOFromSabreVCard($vcard); + + $this->assertEquals( + 'GB', + $iso + ); + } +} diff --git a/tests/Unit/Helpers/WeatherHelperTest.php b/tests/Unit/Helpers/WeatherHelperTest.php new file mode 100644 index 0000000..d3af88d --- /dev/null +++ b/tests/Unit/Helpers/WeatherHelperTest.php @@ -0,0 +1,48 @@ +create([]); + $this->assertNull(WeatherHelper::getWeatherForAddress($contact->addresses()->first())); + } + + /** @test */ + public function it_dispatch_batch_with_get_coordinates() + { + config(['monica.enable_geolocation' => true]); + config(['monica.location_iq_api_key' => 'test']); + config(['monica.enable_weather' => true]); + config(['monica.weatherapi_key' => 'test']); + + $fake = Bus::fake(); + + $address = factory(Address::class)->create(); + + WeatherHelper::getWeatherForAddress($address); + + $fake->assertBatched(function (PendingBatch $pendingBatch) { + $this->assertCount(2, $pendingBatch->jobs); + $this->assertInstanceOf(GetGPSCoordinate::class, $pendingBatch->jobs[0]); + $this->assertInstanceOf(GetWeatherInformation::class, $pendingBatch->jobs[1]); + + return true; + }); + } +} diff --git a/tests/Unit/Jobs/CreateAvatarsForExistingContactsTest.php b/tests/Unit/Jobs/CreateAvatarsForExistingContactsTest.php new file mode 100644 index 0000000..16c96a7 --- /dev/null +++ b/tests/Unit/Jobs/CreateAvatarsForExistingContactsTest.php @@ -0,0 +1,35 @@ +create([ + 'avatar_adorable_url' => null, + ]); + + (new CreateAvatarsForExistingContacts)->handle(); + + Queue::assertPushed(GenerateDefaultAvatar::class, function ($job) use ($contact) { + return $job->contact->id === $contact->id; + }); + Queue::assertPushed(GetAvatarsFromInternet::class, function ($job) use ($contact) { + return $job->contact->id === $contact->id; + }); + } +} diff --git a/tests/Unit/Jobs/Dav/DeleteMultipleVCardTest.php b/tests/Unit/Jobs/Dav/DeleteMultipleVCardTest.php new file mode 100644 index 0000000..9045134 --- /dev/null +++ b/tests/Unit/Jobs/Dav/DeleteMultipleVCardTest.php @@ -0,0 +1,52 @@ +create(); + $addressBookSubscription = AddressBookSubscription::factory()->create([ + 'account_id' => $user->account_id, + 'user_id' => $user->id, + ]); + + $pendingBatch = $fake->batch([ + $job = new DeleteMultipleVCard($addressBookSubscription, ['https://test/dav/uri']), + ]); + $batch = $pendingBatch->dispatch(); + + $fake->assertBatched(function (PendingBatch $pendingBatch) { + $this->assertCount(1, $pendingBatch->jobs); + $this->assertInstanceOf(DeleteMultipleVCard::class, $pendingBatch->jobs->first()); + + return true; + }); + + $batch = app(DatabaseBatchRepository::class)->store($pendingBatch); + $job->withBatchId($batch->id)->handle(); + + $fake->assertDispatched(function (DeleteVCard $updateVCard) { + $uri = $this->getPrivateValue($updateVCard, 'uri'); + $this->assertEquals('https://test/dav/uri', $uri); + + return true; + }); + } +} diff --git a/tests/Unit/Jobs/Dav/DeleteVCardTest.php b/tests/Unit/Jobs/Dav/DeleteVCardTest.php new file mode 100644 index 0000000..743c4b6 --- /dev/null +++ b/tests/Unit/Jobs/Dav/DeleteVCardTest.php @@ -0,0 +1,53 @@ +create(); + $addressBookSubscription = AddressBookSubscription::factory()->create([ + 'account_id' => $user->account_id, + 'user_id' => $user->id, + ]); + + Http::fake(function (Request $request) { + $this->assertEquals('https://test/dav/uri', $request->url()); + $this->assertEquals('DELETE', $request->method()); + + return Http::response(null, 204); + }); + + $pendingBatch = $fake->batch([ + $job = new DeleteVCard($addressBookSubscription, 'https://test/dav/uri'), + ]); + $batch = $pendingBatch->dispatch(); + + $fake->assertBatched(function (PendingBatch $pendingBatch) { + $this->assertCount(1, $pendingBatch->jobs); + $this->assertInstanceOf(DeleteVCard::class, $pendingBatch->jobs->first()); + + return true; + }); + + $batch = app(DatabaseBatchRepository::class)->store($pendingBatch); + $job->withBatchId($batch->id)->handle(); + } +} diff --git a/tests/Unit/Jobs/Dav/GetMultipleVCardTest.php b/tests/Unit/Jobs/Dav/GetMultipleVCardTest.php new file mode 100644 index 0000000..b05122d --- /dev/null +++ b/tests/Unit/Jobs/Dav/GetMultipleVCardTest.php @@ -0,0 +1,176 @@ +create(); + $addressBookSubscription = AddressBookSubscription::factory()->create([ + 'account_id' => $user->account_id, + 'user_id' => $user->id, + ]); + + $contact = new Contact(); + $contact->forceFill([ + 'first_name' => 'Test', + 'uuid' => 'affacde9-b2fe-4371-9acb-6612aaee6971', + 'updated_at' => now(), + ]); + $card = $this->getCard($contact); + $etag = $this->getEtag($contact, true); + + $this->mock(DavClient::class, function (MockInterface $mock) use ($card, $etag) { + $mock->shouldReceive('setBaseUri')->once()->andReturn($mock); + $mock->shouldReceive('setCredentials')->once()->andReturn($mock); + $mock->shouldReceive('addressbookMultiget') + ->once() + ->withArgs(function ($properties, $contacts) { + $this->assertEquals([ + '{DAV:}getetag', + [ + 'name' => '{'.CardDAVPlugin::NS_CARDDAV.'}address-data', + 'value' => null, + 'attributes' => [ + 'content-type' => 'text/vcard', + 'version' => '4.0', + ], + ], + ], $properties); + $this->assertEquals(['https://test/dav/uri'], $contacts); + + return true; + }) + ->andReturn([ + 'https://test/dav/uri' => [ + 200 => [ + '{'.CardDAVPlugin::NS_CARDDAV.'}address-data' => $card, + '{DAV:}getetag' => $etag, + ], + ], + ]); + }); + + $pendingBatch = $fake->batch([ + $job = new GetMultipleVCard($addressBookSubscription, ['https://test/dav/uri']), + ]); + $batch = $pendingBatch->dispatch(); + + $fake->assertBatched(function (PendingBatch $pendingBatch) { + $this->assertCount(1, $pendingBatch->jobs); + $this->assertInstanceOf(GetMultipleVCard::class, $pendingBatch->jobs->first()); + + return true; + }); + + $batch = app(DatabaseBatchRepository::class)->store($pendingBatch); + $job->withBatchId($batch->id)->handle(); + + $fake->assertDispatched(function (UpdateVCard $updateVCard) use ($etag, $card) { + $dto = $this->getPrivateValue($updateVCard, 'contact'); + $this->assertEquals('https://test/dav/uri', $dto->uri); + $this->assertEquals($etag, $dto->etag); + $this->assertEquals($card, $dto->card); + + return true; + }); + } + + /** @test */ + public function it_get_cards_mock_http() + { + $fake = Bus::fake(); + + $user = factory(User::class)->create(); + $addressBookSubscription = AddressBookSubscription::factory()->create([ + 'account_id' => $user->account_id, + 'user_id' => $user->id, + ]); + + $contact = new Contact(); + $contact->forceFill([ + 'first_name' => 'Test', + 'uuid' => 'affacde9-b2fe-4371-9acb-6612aaee6971', + 'updated_at' => now(), + ]); + $card = $this->getCard($contact); + $etag = $this->getEtag($contact, true); + + $this->mock(DavClient::class, function (MockInterface $mock) use ($card, $etag) { + $mock->shouldReceive('setBaseUri')->once()->andReturn($mock); + $mock->shouldReceive('setCredentials')->once()->andReturn($mock); + $mock->shouldReceive('addressbookMultiget') + ->once() + ->withArgs(function ($properties, $contacts) { + $this->assertEquals([ + '{DAV:}getetag', + [ + 'name' => '{'.CardDAVPlugin::NS_CARDDAV.'}address-data', + 'value' => null, + 'attributes' => [ + 'content-type' => 'text/vcard', + 'version' => '4.0', + ], + ], + ], $properties); + $this->assertEquals(['https://test/dav/uri'], $contacts); + + return true; + }) + ->andReturn([ + 'https://test/dav/uri' => [ + 200 => [ + '{'.CardDAVPlugin::NS_CARDDAV.'}address-data' => $card, + '{DAV:}getetag' => $etag, + ], + ], + ]); + }); + + $pendingBatch = $fake->batch([ + $job = new GetMultipleVCard($addressBookSubscription, ['https://test/dav/uri']), + ]); + $batch = $pendingBatch->dispatch(); + + $fake->assertBatched(function (PendingBatch $pendingBatch) { + $this->assertCount(1, $pendingBatch->jobs); + $this->assertInstanceOf(GetMultipleVCard::class, $pendingBatch->jobs->first()); + + return true; + }); + + $batch = app(DatabaseBatchRepository::class)->store($pendingBatch); + $job->withBatchId($batch->id)->handle(); + + $fake->assertDispatched(function (UpdateVCard $updateVCard) use ($etag, $card) { + $dto = $this->getPrivateValue($updateVCard, 'contact'); + $this->assertEquals('https://test/dav/uri', $dto->uri); + $this->assertEquals($etag, $dto->etag); + $this->assertEquals($card, $dto->card); + + return true; + }); + } +} diff --git a/tests/Unit/Jobs/Dav/GetVCardTest.php b/tests/Unit/Jobs/Dav/GetVCardTest.php new file mode 100644 index 0000000..b9edbd1 --- /dev/null +++ b/tests/Unit/Jobs/Dav/GetVCardTest.php @@ -0,0 +1,72 @@ +create(); + $addressBookSubscription = AddressBookSubscription::factory()->create([ + 'account_id' => $user->account_id, + 'user_id' => $user->id, + ]); + + $contact = new Contact(); + $contact->forceFill([ + 'first_name' => 'Test', + 'uuid' => 'affacde9-b2fe-4371-9acb-6612aaee6971', + 'updated_at' => now(), + ]); + $card = $this->getCard($contact); + $etag = $this->getEtag($contact, true); + + Http::fake([ + 'https://test/dav/uri' => Http::response($card, 200), + ]); + + $pendingBatch = $fake->batch([ + $job = new GetVCard($addressBookSubscription, new ContactDto('https://test/dav/uri', $etag)), + ]); + $batch = $pendingBatch->dispatch(); + + $fake->assertBatched(function (PendingBatch $pendingBatch) { + $this->assertCount(1, $pendingBatch->jobs); + $this->assertInstanceOf(GetVCard::class, $pendingBatch->jobs->first()); + + return true; + }); + + $batch = app(DatabaseBatchRepository::class)->store($pendingBatch); + $job->withBatchId($batch->id)->handle(); + + $fake->assertDispatched(function (UpdateVCard $updateVCard) use ($etag, $card) { + $dto = $this->getPrivateValue($updateVCard, 'contact'); + $this->assertEquals('https://test/dav/uri', $dto->uri); + $this->assertEquals($etag, $dto->etag); + $this->assertEquals($card, $dto->card); + + return true; + }); + } +} diff --git a/tests/Unit/Jobs/Dav/PushVCardTest.php b/tests/Unit/Jobs/Dav/PushVCardTest.php new file mode 100644 index 0000000..bd21016 --- /dev/null +++ b/tests/Unit/Jobs/Dav/PushVCardTest.php @@ -0,0 +1,85 @@ +create(); + $addressBookSubscription = AddressBookSubscription::factory()->create([ + 'account_id' => $user->account_id, + 'user_id' => $user->id, + 'uri' => 'https://test/dav', + ]); + + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + 'first_name' => 'John', + 'last_name' => 'Doe', + 'uuid' => 'affacde9-b2fe-4371-9acb-6612aaee6971', + 'updated_at' => '2021-09-01', + ]); + $card = $this->getCard($contact); + $etag = $this->getEtag($contact, true); + + if ($ifmatch == ['etag']) { + $ifmatch = [$etag]; + } + + Http::fake(function (Request $request, $options) use ($card, $ifmatch) { + $this->assertEquals('https://test/dav/uri', $request->url()); + $this->assertEquals('PUT', $request->method()); + $this->assertEquals($ifmatch, $request->header('If-Match')); + + return Http::response($card, 200); + }); + + $pendingBatch = $fake->batch([ + $job = new PushVCard($addressBookSubscription, new ContactPushDto('https://test/dav/uri', $etag, $card, $contact->id, $mode)), + ]); + $batch = $pendingBatch->dispatch(); + + $fake->assertBatched(function (PendingBatch $pendingBatch) { + $this->assertCount(1, $pendingBatch->jobs); + $this->assertInstanceOf(PushVCard::class, $pendingBatch->jobs->first()); + + return true; + }); + + $batch = app(DatabaseBatchRepository::class)->store($pendingBatch); + $job->withBatchId($batch->id)->handle(); + } + + public function modes(): array + { + return [ + [0, []], + [1, ['etag']], + [2, ['*']], + ]; + } +} diff --git a/tests/Unit/Jobs/Dav/UpdateVCardTest.php b/tests/Unit/Jobs/Dav/UpdateVCardTest.php new file mode 100644 index 0000000..92438f4 --- /dev/null +++ b/tests/Unit/Jobs/Dav/UpdateVCardTest.php @@ -0,0 +1,62 @@ +create(); + $addressBook = AddressBook::factory()->create([ + 'account_id' => $user->account_id, + 'user_id' => $user->id, + ]); + + $contact = new Contact(); + $contact->forceFill([ + 'first_name' => 'Test', + 'uuid' => 'affacde9-b2fe-4371-9acb-6612aaee6971', + 'updated_at' => now(), + ]); + $card = $this->getCard($contact); + $etag = $this->getEtag($contact, true); + + $pendingBatch = $fake->batch([ + $job = new UpdateVCard($user, $addressBook->name, new ContactUpdateDto('https://test/dav/uricontact1', $etag, $card)), + ]); + $batch = $pendingBatch->dispatch(); + + $fake->assertBatched(function (PendingBatch $pendingBatch) { + $this->assertCount(1, $pendingBatch->jobs); + $this->assertInstanceOf(UpdateVCard::class, $pendingBatch->jobs->first()); + + return true; + }); + + $batch = app(DatabaseBatchRepository::class)->store($pendingBatch); + $job->withBatchId($batch->id)->handle(); + + $this->assertDatabaseHas('contacts', [ + 'first_name' => 'Test', + 'uuid' => 'affacde9-b2fe-4371-9acb-6612aaee6971', + ]); + } +} diff --git a/tests/Unit/Jobs/GetWeatherInformationTest.php b/tests/Unit/Jobs/GetWeatherInformationTest.php new file mode 100644 index 0000000..1476bed --- /dev/null +++ b/tests/Unit/Jobs/GetWeatherInformationTest.php @@ -0,0 +1,57 @@ +create([ + 'latitude' => '34.112456', + 'longitude' => '-118.4270732', + ]); + + $this->mock(GetWeatherInformationService::class, function (MockInterface $mock) use ($place) { + $mock->shouldReceive('execute') + ->once() + ->withArgs(function ($data) use ($place) { + $this->assertEquals([ + 'account_id' => $place->account_id, + 'place_id' => $place->id, + ], $data); + + return true; + }); + }); + + $pendingBatch = $fake->batch([ + $job = new GetWeatherInformation($place), + ]); + $batch = $pendingBatch->dispatch(); + + $fake->assertBatched(function (PendingBatch $pendingBatch) { + $this->assertCount(1, $pendingBatch->jobs); + $this->assertInstanceOf(GetWeatherInformation::class, $pendingBatch->jobs->first()); + + return true; + }); + + $batch = app(DatabaseBatchRepository::class)->store($pendingBatch); + $job->withBatchId($batch->id)->handle(); + } +} diff --git a/tests/Unit/Jobs/Reminder/NotifyUserAboutReminderTest.php b/tests/Unit/Jobs/Reminder/NotifyUserAboutReminderTest.php new file mode 100644 index 0000000..f90a2ac --- /dev/null +++ b/tests/Unit/Jobs/Reminder/NotifyUserAboutReminderTest.php @@ -0,0 +1,274 @@ +create([ + 'default_time_reminder_is_sent' => '07:00', + ]); + $contact = factory(Contact::class)->create(['account_id' => $account->id]); + $user = factory(User::class)->create(['account_id' => $account->id]); + $reminder = factory(Reminder::class)->create([ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + 'initial_date' => '2017-01-01', + 'title' => 'fake text saying nothing', + 'frequency_type' => 'year', + 'frequency_number' => 1, + ]); + $reminderOutbox = factory(ReminderOutbox::class)->create([ + 'account_id' => $user->account_id, + 'reminder_id' => $reminder->id, + 'user_id' => $user->id, + 'planned_date' => '2017-01-01', + 'nature' => 'reminder', + ]); + + NotifyUserAboutReminder::dispatch($reminderOutbox); + + // Assert the notification has been sent to the user with the right + // reminderoutbox id and the right email content + Notification::assertSentTo( + $user, + UserReminded::class, + function ($notification, $channels) use ($reminderOutbox, $reminder, $user, $contact) { + $mailData = $notification->toMail($user)->toArray(); + $this->assertEquals("Reminder for {$contact->name}", $mailData['subject']); + $this->assertEquals("Hi {$user->first_name}", $mailData['greeting']); + $this->assertStringContainsString("You wanted to be reminded of {$reminderOutbox->reminder->title}", $mailData['introLines'][0]); + + return $notification->reminder->id === $reminder->id; + } + ); + } + + /** @test */ + public function it_sends_a_notification_to_a_user() + { + Notification::fake(); + + Carbon::setTestNow(Carbon::create(2017, 1, 1, 7, 0, 0)); + + $account = factory(Account::class)->create([ + 'default_time_reminder_is_sent' => '07:00', + ]); + $contact = factory(Contact::class)->create(['account_id' => $account->id]); + $user = factory(User::class)->create(['account_id' => $account->id]); + $reminder = factory(Reminder::class)->create([ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + 'initial_date' => '2017-01-01', + 'title' => 'fake text saying nothing', + 'frequency_type' => 'year', + 'frequency_number' => '1', + ]); + $reminderOutbox = factory(ReminderOutbox::class)->create([ + 'account_id' => $user->account_id, + 'reminder_id' => $reminder->id, + 'user_id' => $user->id, + 'planned_date' => '2017-01-01', + 'nature' => 'notification', + ]); + + NotifyUserAboutReminder::dispatch($reminderOutbox); + + // Assert the notification has been sent to the user with the right + // reminderoutbox id and the right email content + Notification::assertSentTo( + $user, + UserNotified::class, + function ($notification, $channels) use ($reminder, $user, $contact) { + $mailData = $notification->toMail($user)->toArray(); + $this->assertEquals("Reminder for {$contact->name}", $mailData['subject']); + $this->assertEquals("Hi {$user->first_name}", $mailData['greeting']); + $this->assertStringContainsString('In days (on Jan 01, 2018), the following event will happen:', $mailData['introLines'][0]); + + return $notification->reminder->id === $reminder->id; + } + ); + } + + /** @test */ + public function it_doesnt_notify_a_user_if_he_is_on_the_free_plan() + { + Notification::fake(); + + Carbon::setTestNow(Carbon::create(2017, 1, 1, 7, 0, 0)); + config(['monica.requires_subscription' => true]); + + $account = factory(Account::class)->create([ + 'default_time_reminder_is_sent' => '07:00', + 'has_access_to_paid_version_for_free' => false, + ]); + $contact = factory(Contact::class)->create(['account_id' => $account->id]); + $user = factory(User::class)->create(['account_id' => $account->id]); + $reminder = factory(Reminder::class)->create([ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + 'initial_date' => '2017-01-01', + 'title' => 'fake text saying nothing', + 'frequency_type' => 'year', + 'frequency_number' => 1, + ]); + $reminderOutbox = factory(ReminderOutbox::class)->create([ + 'account_id' => $user->account_id, + 'reminder_id' => $reminder->id, + 'user_id' => $user->id, + 'planned_date' => '2017-01-01', + ]); + + NotifyUserAboutReminder::dispatch($reminderOutbox); + + Notification::assertNotSentTo( + $user, + UserReminded::class + ); + } + + /** @test */ + public function it_doesnt_notify_a_user_if_contact_deleted() + { + Notification::fake(); + + Carbon::setTestNow(Carbon::create(2017, 1, 1, 7, 0, 0)); + + $account = factory(Account::class)->create([ + 'default_time_reminder_is_sent' => '07:00', + ]); + $contact = factory(Contact::class)->create(['account_id' => $account->id]); + $user = factory(User::class)->create(['account_id' => $account->id]); + $reminder = factory(Reminder::class)->create([ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + 'initial_date' => '2017-01-01', + 'title' => 'fake text saying nothing', + 'frequency_type' => 'year', + 'frequency_number' => '1', + ]); + $reminderOutbox = factory(ReminderOutbox::class)->create([ + 'account_id' => $user->account_id, + 'reminder_id' => $reminder->id, + 'user_id' => $user->id, + 'planned_date' => '2017-01-01', + 'nature' => 'notification', + ]); + + $contact->delete(); + + NotifyUserAboutReminder::dispatch($reminderOutbox); + + Notification::assertNotSentTo( + $user, + UserReminded::class + ); + } + + /** @test */ + public function it_marks_the_one_time_reminder_has_inactive_once_it_is_sent() + { + Notification::fake(); + + Carbon::setTestNow(Carbon::create(2017, 1, 1, 7, 0, 0)); + + $account = factory(Account::class)->create([ + 'default_time_reminder_is_sent' => '07:00', + ]); + $contact = factory(Contact::class)->create(['account_id' => $account->id]); + $user = factory(User::class)->create(['account_id' => $account->id]); + $reminder = factory(Reminder::class)->create([ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + 'initial_date' => '2017-01-01', + 'title' => 'fake text saying nothing', + 'frequency_type' => 'one_time', + ]); + $reminderOutbox = factory(ReminderOutbox::class)->create([ + 'account_id' => $user->account_id, + 'reminder_id' => $reminder->id, + 'user_id' => $user->id, + 'planned_date' => '2017-01-01', + ]); + + NotifyUserAboutReminder::dispatch($reminderOutbox); + + $this->assertDatabaseMissing('reminder_outbox', [ + 'account_id' => $user->account_id, + 'id' => $reminderOutbox->id, + ]); + + $this->assertDatabaseHas('reminders', [ + 'account_id' => $user->account_id, + 'id' => $reminder->id, + 'inactive' => true, + ]); + } + + /** @test */ + public function it_reschedule_a_recurring_reminder_once_it_is_sent() + { + Notification::fake(); + + Carbon::setTestNow(Carbon::create(2017, 1, 1, 7, 0, 0)); + + $account = factory(Account::class)->create([ + 'default_time_reminder_is_sent' => '07:00', + ]); + $contact = factory(Contact::class)->create(['account_id' => $account->id]); + $user = factory(User::class)->create(['account_id' => $account->id]); + $reminder = factory(Reminder::class)->create([ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + 'initial_date' => '2017-01-01', + 'title' => 'fake text saying nothing', + 'frequency_type' => 'year', + 'frequency_number' => 1, + ]); + $reminderOutbox = factory(ReminderOutbox::class)->create([ + 'account_id' => $user->account_id, + 'reminder_id' => $reminder->id, + 'user_id' => $user->id, + 'planned_date' => '2017-01-01', + ]); + + NotifyUserAboutReminder::dispatch($reminderOutbox); + + $this->assertDatabaseMissing('reminder_outbox', [ + 'account_id' => $user->account_id, + 'id' => $reminderOutbox->id, + ]); + + $this->assertDatabaseHas('reminders', [ + 'account_id' => $user->account_id, + 'id' => $reminder->id, + 'inactive' => false, + ]); + + $this->assertDatabaseHas('reminder_outbox', [ + 'account_id' => $user->account_id, + 'reminder_id' => $reminder->id, + ]); + } +} diff --git a/tests/Unit/Jobs/ScheduleStayInTouchTest.php b/tests/Unit/Jobs/ScheduleStayInTouchTest.php new file mode 100644 index 0000000..2a059c0 --- /dev/null +++ b/tests/Unit/Jobs/ScheduleStayInTouchTest.php @@ -0,0 +1,125 @@ +create([ + 'default_time_reminder_is_sent' => '07:00', + 'has_access_to_paid_version_for_free' => 1, + ]); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + 'stay_in_touch_trigger_date' => '2017-01-01 07:00:00', + 'stay_in_touch_frequency' => 5, + ]); + $user = factory(User::class)->create([ + 'account_id' => $account->id, + 'email' => 'john@doe.com', + 'timezone' => 'America/New_York', + ]); + + ScheduleStayInTouch::dispatch($contact); + + NotificationFacade::assertSentTo($user, StayInTouchEmail::class, + function ($notification, $channels) use ($contact) { + return $channels[0] == 'mail' + && $notification->assertSentFor($contact); + } + ); + + $notifications = NotificationFacade::sent($user, StayInTouchEmail::class); + $message = $notifications[0]->toMail($user); + + $this->assertStringContainsString('You asked to be reminded to stay in touch with John Doe every 5 days.', implode('', $message->introLines)); + + $this->assertDatabaseHas('contacts', [ + 'stay_in_touch_trigger_date' => '2017-01-06 07:00:00', + ]); + } + + /** @test */ + public function it_doesnt_dispatches_an_email_if_free_account() + { + NotificationFacade::fake(); + + Carbon::setTestNow(Carbon::create(2017, 1, 1, 5, 0, 0)); + + config(['monica.requires_subscription' => true]); + + $account = factory(Account::class)->create([ + 'default_time_reminder_is_sent' => '07:00', + 'has_access_to_paid_version_for_free' => 0, + ]); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + 'stay_in_touch_trigger_date' => '2017-01-01 07:00:00', + 'stay_in_touch_frequency' => 5, + ]); + $user = factory(User::class)->create([ + 'account_id' => $account->id, + 'email' => 'john@doe.com', + 'timezone' => 'America/New_York', + ]); + + ScheduleStayInTouch::dispatch($contact); + + NotificationFacade::assertNotSentTo($user, StayInTouchEmail::class); + NotificationFacade::assertNothingSent(); + + $this->assertDatabaseHas('contacts', [ + 'stay_in_touch_trigger_date' => '2017-01-01 07:00:00', + ]); + } + + /** @test */ + public function it_reschedule_missed_stayintouch() + { + NotificationFacade::fake(); + + Carbon::setTestNow(Carbon::create(2019, 1, 1, 5, 0, 0)); + + $account = factory(Account::class)->create([ + 'default_time_reminder_is_sent' => '07:00', + 'has_access_to_paid_version_for_free' => 0, + ]); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + 'stay_in_touch_trigger_date' => '2018-01-01 07:00:00', + 'stay_in_touch_frequency' => 30, + ]); + $user = factory(User::class)->create([ + 'account_id' => $account->id, + 'email' => 'john@doe.com', + 'timezone' => 'America/New_York', + ]); + + ScheduleStayInTouch::dispatch($contact); + + NotificationFacade::assertNotSentTo($user, StayInTouchEmail::class); + NotificationFacade::assertNothingSent(); + + $this->assertDatabaseHas('contacts', [ + 'stay_in_touch_trigger_date' => '2019-01-26 07:00:00', + ]); + } +} diff --git a/tests/Unit/Jobs/ServiceQueueTest.php b/tests/Unit/Jobs/ServiceQueueTest.php new file mode 100644 index 0000000..8b1023b --- /dev/null +++ b/tests/Unit/Jobs/ServiceQueueTest.php @@ -0,0 +1,54 @@ + 'sync']); + + ServiceQueueTester::dispatch(); + + $this->assertTrue(ServiceQueueTester::$executed); + $this->assertFalse(ServiceQueueTester::$failed); + } + + /** @test */ + public function it_run_a_service_sync(): void + { + ServiceQueueTester::dispatchSync(); + + $this->assertTrue(ServiceQueueTester::$executed); + $this->assertFalse(ServiceQueueTester::$failed); + } + + /** @test */ + public function it_run_a_service_which_failed(): void + { + $this->expectException(\Exception::class); + try { + ServiceQueueTester::dispatchSync(['throw' => true]); + } finally { + $this->assertTrue(ServiceQueueTester::$executed); + $this->assertTrue(ServiceQueueTester::$failed); + } + } + + /** @test */ + public function service_is_not_run_if_queue_set(): void + { + config(['queue.default' => 'database']); + + ServiceQueueTester::dispatch(['throw' => true]); + + $this->assertFalse(ServiceQueueTester::$executed); + $this->assertFalse(ServiceQueueTester::$failed); + } +} diff --git a/tests/Unit/Jobs/ServiceQueueTester.php b/tests/Unit/Jobs/ServiceQueueTester.php new file mode 100644 index 0000000..e71428f --- /dev/null +++ b/tests/Unit/Jobs/ServiceQueueTester.php @@ -0,0 +1,55 @@ +obj)) { + // variable can be touch + } + } +} diff --git a/tests/Unit/Jobs/SynchronizeAddressBooksTest.php b/tests/Unit/Jobs/SynchronizeAddressBooksTest.php new file mode 100644 index 0000000..92778a0 --- /dev/null +++ b/tests/Unit/Jobs/SynchronizeAddressBooksTest.php @@ -0,0 +1,39 @@ +create(); + + $this->mock(SynchronizeAddressBook::class, function ($mock) use ($subscription) { + $mock->shouldReceive('execute') + ->once() + ->with([ + 'account_id' => $subscription->account_id, + 'addressbook_subscription_id' => $subscription->id, + 'force' => false, + ]); + }); + + (new SynchronizeAddressBooks($subscription)) + ->handle(); + + $subscription->refresh(); + $this->assertEquals(Carbon::create(2021, 9, 1, 10, 0, 0), $subscription->last_synchronized_at); + } +} diff --git a/tests/Unit/Jobs/UpdateAllGravatarsTest.php b/tests/Unit/Jobs/UpdateAllGravatarsTest.php new file mode 100644 index 0000000..5d5bf26 --- /dev/null +++ b/tests/Unit/Jobs/UpdateAllGravatarsTest.php @@ -0,0 +1,33 @@ +create([ + 'avatar_source' => 'gravatar', + ]); + + (new UpdateAllGravatars)->handle(); + + foreach ($contacts as $contact) { + Queue::assertPushed(UpdateGravatar::class, function ($job) use ($contact) { + return $job->contact->id === $contact->id; + }); + } + } +} diff --git a/tests/Unit/Jobs/UpdateGravatarTest.php b/tests/Unit/Jobs/UpdateGravatarTest.php new file mode 100644 index 0000000..fda153a --- /dev/null +++ b/tests/Unit/Jobs/UpdateGravatarTest.php @@ -0,0 +1,38 @@ +create(); + $contactFieldType = factory(ContactFieldType::class)->create([ + 'account_id' => $contact->account->id, + ]); + factory(ContactField::class)->create([ + 'contact_id' => $contact->id, + 'account_id' => $contact->account->id, + 'contact_field_type_id' => $contactFieldType->id, + 'data' => 'matt@wordpress.com', + ]); + + (new UpdateGravatarJob($contact))->handle(); + + $contact->refresh(); + + $this->assertNotNull( + $contact->avatar_gravatar_url + ); + } +} diff --git a/tests/Unit/Jobs/UpdateLastConsultedDateTest.php b/tests/Unit/Jobs/UpdateLastConsultedDateTest.php new file mode 100644 index 0000000..831921f --- /dev/null +++ b/tests/Unit/Jobs/UpdateLastConsultedDateTest.php @@ -0,0 +1,32 @@ +create([ + 'number_of_views' => 1, + ]); + + UpdateLastConsultedDate::dispatch($contact); + + $this->assertDatabaseHas('contacts', [ + 'id' => $contact->id, + 'last_consulted_at' => '2017-01-01 07:00:00', + 'number_of_views' => 2, + ]); + } +} diff --git a/tests/Unit/Models/AccountTest.php b/tests/Unit/Models/AccountTest.php new file mode 100644 index 0000000..db66526 --- /dev/null +++ b/tests/Unit/Models/AccountTest.php @@ -0,0 +1,669 @@ +create(); + $gender = factory(Gender::class)->create([ + 'account_id' => $account->id, + 'name' => 'test', + ]); + $gender = factory(Gender::class)->create([ + 'account_id' => $account->id, + 'name' => 'test', + ]); + + $this->assertTrue($account->genders()->exists()); + } + + /** @test */ + public function it_has_many_relationship_types() + { + $account = factory(Account::class)->create(); + $relationshipType = factory(RelationshipType::class)->create([ + 'account_id' => $account->id, + ]); + $relationshipType = factory(RelationshipType::class)->create([ + 'account_id' => $account->id, + ]); + + $this->assertTrue($account->relationshipTypes()->exists()); + } + + /** @test */ + public function it_has_many_relationship_type_groups() + { + $contact = factory(Contact::class)->create(); + $account = $contact->account; + $relationshipTypeGroup = factory(RelationshipTypeGroup::class)->create([ + 'account_id' => $account->id, + ]); + $relationshipTypeGroup = factory(RelationshipTypeGroup::class)->create([ + 'account_id' => $account->id, + ]); + + $this->assertTrue($account->relationshipTypeGroups()->exists()); + } + + /** @test */ + public function it_has_many_modules() + { + $contact = factory(Contact::class)->create(); + $account = $contact->account; + $module = factory(Module::class)->create([ + 'account_id' => $account->id, + ]); + $module = factory(Module::class)->create([ + 'account_id' => $account->id, + ]); + + $this->assertTrue($account->modules()->exists()); + } + + /** @test */ + public function it_has_many_activity_types() + { + $account = factory(Account::class)->create(); + $activityType = factory(ActivityType::class)->create([ + 'account_id' => $account->id, + ]); + + $this->assertTrue($account->activityTypes()->exists()); + } + + /** @test */ + public function it_has_many_activity_type_categories() + { + $account = factory(Account::class)->create(); + $ActivityTypeCategory = factory(ActivityTypeCategory::class)->create([ + 'account_id' => $account->id, + ]); + + $this->assertTrue($account->activityTypeCategories()->exists()); + } + + /** @test */ + public function it_has_many_conversations() + { + $account = factory(Account::class)->create([]); + $conversation = factory(Conversation::class, 2)->create([ + 'account_id' => $account->id, + ]); + + $this->assertTrue($account->conversations()->exists()); + } + + /** @test */ + public function it_has_many_messages() + { + $account = factory(Account::class)->create([]); + $conversation = factory(Conversation::class)->create([ + 'account_id' => $account->id, + ]); + $message = factory(Message::class, 2)->create([ + 'account_id' => $account->id, + 'conversation_id' => $conversation->id, + ]); + + $this->assertTrue($account->messages()->exists()); + } + + /** @test */ + public function it_has_many_life_event_categories() + { + $account = factory(Account::class)->create([]); + $lifeEventCategory = factory(LifeEventCategory::class)->create([ + 'account_id' => $account->id, + ]); + + $this->assertTrue($account->lifeEventCategories()->exists()); + } + + /** @test */ + public function it_has_many_reminder_outboxes() + { + $reminderOutbox = factory(ReminderOutbox::class)->create([]); + $this->assertTrue($reminderOutbox->account->reminderOutboxes()->exists()); + } + + /** @test */ + public function it_has_many_life_event_types() + { + $account = factory(Account::class)->create([]); + $lifeEventType = factory(LifeEventType::class)->create([ + 'account_id' => $account->id, + ]); + + $this->assertTrue($account->lifeEventTypes()->exists()); + } + + /** @test */ + public function it_has_many_life_events() + { + $account = factory(Account::class)->create([]); + $lifeEvent = factory(LifeEvent::class)->create([ + 'account_id' => $account->id, + ]); + + $this->assertTrue($account->lifeEvents()->exists()); + } + + /** @test */ + public function it_has_many_documents() + { + $account = factory(Account::class)->create([]); + $document = factory(Document::class)->create([ + 'account_id' => $account->id, + ]); + + $this->assertTrue($account->documents()->exists()); + } + + /** @test */ + public function it_has_many_photos() + { + $account = factory(Account::class)->create([]); + $photo = factory(Photo::class)->create([ + 'account_id' => $account->id, + ]); + $this->assertTrue($account->photos()->exists()); + } + + /** @test */ + public function it_has_many_weathers() + { + $weather = factory(Weather::class)->create([]); + $this->assertTrue($weather->account->weathers()->exists()); + } + + /** @test */ + public function it_has_many_places() + { + $account = factory(Account::class)->create([]); + $places = factory(Place::class)->create([ + 'account_id' => $account->id, + ]); + $this->assertTrue($account->places()->exists()); + } + + /** @test */ + public function it_has_many_addresses() + { + $account = factory(Account::class)->create([]); + $addresses = factory(Address::class)->create([ + 'account_id' => $account->id, + ]); + $this->assertTrue($account->addresses()->exists()); + } + + /** @test */ + public function it_has_many_companies() + { + $account = factory(Account::class)->create([]); + $companies = factory(Company::class)->create([ + 'account_id' => $account->id, + ]); + $this->assertTrue($account->companies()->exists()); + } + + /** @test */ + public function it_has_many_occupations() + { + $account = factory(Account::class)->create([]); + $occupations = factory(Occupation::class)->create([ + 'account_id' => $account->id, + ]); + $this->assertTrue($account->occupations()->exists()); + } + + /** @test */ + public function it_has_many_logs() + { + $account = factory(Account::class)->create([]); + factory(AuditLog::class)->create([ + 'account_id' => $account->id, + ]); + $this->assertTrue($account->auditLogs()->exists()); + } + + /** @test */ + public function user_is_subscribed_if_user_can_access_to_paid_version_for_free() + { + $account = factory(Account::class)->make([ + 'has_access_to_paid_version_for_free' => true, + ]); + + $this->assertTrue( + $account->isSubscribed() + ); + } + + /** @test */ + public function user_is_subscribed_returns_false_if_not_subcribed() + { + $account = factory(Account::class)->make([ + 'has_access_to_paid_version_for_free' => false, + ]); + + $this->assertFalse( + $account->isSubscribed() + ); + } + + /** @test */ + public function user_is_subscribed_returns_true_if_monthly_plan_is_set() + { + $account = factory(Account::class)->create(); + + $plan = factory(\Laravel\Cashier\Subscription::class)->create([ + 'account_id' => $account->id, + 'stripe_price' => 'chandler_5', + 'stripe_id' => 'sub_C0R444pbxddhW7', + 'name' => 'fakePlan', + ]); + + config(['monica.paid_plan_monthly_friendly_name' => 'fakePlan']); + + $this->assertTrue( + $account->isSubscribed() + ); + } + + /** @test */ + public function user_is_subscribed_returns_true_if_annual_plan_is_set() + { + $account = factory(Account::class)->create(); + + $plan = factory(\Laravel\Cashier\Subscription::class)->create([ + 'account_id' => $account->id, + 'stripe_price' => 'chandler_annual', + 'stripe_id' => 'sub_C0R444pbxddhW7', + 'name' => 'annualPlan', + ]); + + config(['monica.paid_plan_annual_friendly_name' => 'annualPlan']); + + $this->assertTrue( + $account->isSubscribed() + ); + } + + /** @test */ + public function user_is_subscribed_returns_false_if_no_plan_is_set() + { + $account = factory(Account::class)->create(); + + $this->assertFalse( + $account->isSubscribed() + ); + } + + /** @test */ + public function has_invoices_returns_true_if_a_plan_exists() + { + $account = factory(Account::class)->create(); + + $plan = factory(\Laravel\Cashier\Subscription::class)->create([ + 'account_id' => $account->id, + 'stripe_price' => 'chandler_5', + 'stripe_id' => 'sub_C0R444pbxddhW7', + 'name' => 'fakePlan', + ]); + + $this->assertTrue($account->hasInvoices()); + } + + /** @test */ + public function has_invoices_returns_false_if_a_plan_does_not_exist() + { + $account = factory(Account::class)->create(); + + $this->assertFalse($account->hasInvoices()); + } + + /** @test */ + public function it_gets_the_id_of_the_subscribed_plan() + { + config([ + 'monica.paid_plan_annual_friendly_name' => 'fakePlan', + 'monica.paid_plan_annual_id' => 'chandler_5', + ]); + + $user = $this->signIn(); + + $account = $user->account; + + $plan = factory(\Laravel\Cashier\Subscription::class)->create([ + 'account_id' => $account->id, + 'stripe_price' => 'chandler_5', + 'stripe_id' => 'sub_C0R444pbxddhW7', + 'name' => 'fakePlan', + ]); + + $this->assertEquals( + 'chandler_5', + $account->getSubscribedPlanId() + ); + } + + /** @test */ + public function it_gets_the_friendly_name_of_the_subscribed_plan() + { + config([ + 'monica.paid_plan_annual_friendly_name' => 'fakePlan', + 'monica.paid_plan_annual_id' => 'chandler_5', + ]); + + $user = $this->signIn(); + + $account = $user->account; + + $plan = factory(\Laravel\Cashier\Subscription::class)->create([ + 'account_id' => $account->id, + 'stripe_price' => 'chandler_5', + 'stripe_id' => 'sub_C0R444pbxddhW7', + 'name' => 'fakePlan', + ]); + + $this->assertEquals( + 'fakePlan', + $account->getSubscribedPlanName() + ); + } + + /** @test */ + public function it_populates_the_account_with_three_default_genders() + { + $account = factory(Account::class)->create(); + $account->populateDefaultGendersTable(); + + $this->assertEquals( + 3, + $account->genders->count() + ); + } + + /** @test */ + public function it_populates_the_account_with_the_right_default_genders() + { + $account = factory(Account::class)->create(); + $account->populateDefaultGendersTable(); + + $this->assertDatabaseHas( + 'genders', + ['name' => 'Man'] + ); + + $this->assertDatabaseHas( + 'genders', + ['name' => 'Woman'] + ); + + $this->assertDatabaseHas( + 'genders', + ['name' => 'Rather not say'] + ); + } + + /** @test */ + public function it_gets_default_time_reminder_is_sent_attribute() + { + $account = factory(Account::class)->create(['default_time_reminder_is_sent' => '14:00']); + + $this->assertEquals( + '14:00', + $account->default_time_reminder_is_sent + ); + } + + /** @test */ + public function it_sets_default_time_reminder_is_sent_attribute() + { + $account = new Account; + $account->default_time_reminder_is_sent = '14:00'; + + $this->assertEquals( + '14:00', + $account->default_time_reminder_is_sent + ); + } + + /** @test */ + public function it_populates_the_account_with_two_default_reminder_rules() + { + $account = factory(Account::class)->create(); + $account->populateDefaultReminderRulesTable(); + + $this->assertEquals( + 2, + $account->reminderRules->count() + ); + } + + /** @test */ + public function it_populates_the_account_with_the_right_default_reminder_rules() + { + $account = factory(Account::class)->create(); + $account->populateDefaultReminderRulesTable(); + + $this->assertDatabaseHas( + 'reminder_rules', + ['number_of_days_before' => 7] + ); + + $this->assertDatabaseHas( + 'reminder_rules', + ['number_of_days_before' => 30] + ); + } + + /** @test */ + public function it_gets_the_relationship_type_object_matching_a_given_name() + { + $account = factory(Account::class)->create(); + $relationshipType = factory(RelationshipType::class)->create([ + 'account_id' => $account->id, + 'name' => 'partner', + ]); + + $this->assertInstanceOf(RelationshipType::class, $account->getRelationshipTypeByType('partner')); + } + + /** @test */ + public function it_gets_the_relationship_type_group_object_matching_a_given_name() + { + $account = factory(Account::class)->create(); + $relationshipTypeGroup = factory(RelationshipTypeGroup::class)->create([ + 'account_id' => $account->id, + 'name' => 'love', + ]); + + $this->assertInstanceOf(RelationshipTypeGroup::class, $account->getRelationshipTypeGroupByType('love')); + } + + /** @test */ + public function it_populates_default_relationship_type_groups_table_if_tables_havent_been_migrated_yet() + { + $account = factory(Account::class)->create(); + + // Love type + $id = DB::table('default_relationship_type_groups')->insertGetId([ + 'name' => 'friend_and_family', + ]); + + $account->populateRelationshipTypeGroupsTable(); + + $this->assertDatabaseHas('relationship_type_groups', [ + 'name' => 'friend_and_family', + ]); + } + + /** @test */ + public function it_skips_default_relationship_type_groups_table_for_types_already_migrated() + { + $account = factory(Account::class)->create(); + $id = DB::table('default_relationship_type_groups')->insertGetId([ + 'name' => 'friend_and_family', + 'migrated' => 1, + ]); + + $account->populateRelationshipTypeGroupsTable(true); + + $this->assertDatabaseMissing('relationship_type_groups', [ + 'name' => 'friend_and_family', + ]); + } + + /** @test */ + public function it_populates_default_relationship_types_table_if_tables_havent_been_migrated_yet() + { + $account = factory(Account::class)->create(); + $id = DB::table('default_relationship_type_groups')->insertGetId([ + 'name' => 'friend_and_family', + ]); + + DB::table('default_relationship_types')->insert([ + 'name' => 'fuckfriend', + 'relationship_type_group_id' => $id, + ]); + + $account->populateRelationshipTypeGroupsTable(); + $account->populateRelationshipTypesTable(); + + $this->assertDatabaseHas('relationship_types', [ + 'name' => 'fuckfriend', + ]); + } + + /** @test */ + public function it_skips_default_relationship_types_table_for_types_already_migrated() + { + $account = factory(Account::class)->create(); + $id = DB::table('default_relationship_type_groups')->insertGetId([ + 'name' => 'friend_and_family', + ]); + + DB::table('default_relationship_types')->insert([ + 'name' => 'fuckfriend', + 'relationship_type_group_id' => $id, + 'migrated' => 1, + ]); + + $account->populateRelationshipTypeGroupsTable(); + $account->populateRelationshipTypesTable(true); + + $this->assertDatabaseMissing('relationship_types', [ + 'name' => 'fuckfriend', + ]); + } + + /** @test */ + public function it_create_default_account() + { + $account = Account::createDefault('John', 'Doe', 'john@doe.com', 'password'); + + $this->assertDatabaseHas('accounts', [ + 'id' => $account->id, + ]); + $this->assertDatabaseHas('users', [ + 'account_id' => $account->id, + ]); + } + + /** @test */ + public function it_throw_an_exception_if_user_already_exist() + { + $account = Account::createDefault('John', 'Doe', 'john@doe.com', 'password'); + + $this->assertDatabaseHas('accounts', [ + 'id' => $account->id, + ]); + $this->assertDatabaseHas('users', [ + 'account_id' => $account->id, + ]); + + $this->expectException(\Illuminate\Validation\ValidationException::class); + $account = Account::createDefault('John', 'Doe', 'john@doe.com', 'password'); + } + + /** @test */ + public function it_gets_first_user_locale() + { + $account = factory(Account::class)->create(); + $user = factory(User::class)->create([ + 'account_id' => $account->id, + 'locale' => 'fr', + ]); + $user = factory(User::class)->create([ + 'account_id' => $account->id, + 'locale' => 'en', + ]); + + $this->assertEquals( + 'fr', + $account->getFirstLocale() + ); + } + + /** @test */ + public function getting_first_locale_returns_null_if_user_doesnt_exist() + { + $account = factory(Account::class)->create(); + + $this->assertNull($account->getFirstLocale()); + } + + /** @test */ + public function it_populates_default_life_event_tables_upon_creation() + { + $account = factory(Account::class)->create(); + $user = factory(User::class)->create([ + 'account_id' => $account->id, + ]); + + $account->populateDefaultFields(); + + $this->assertEquals( + 5, + DB::table('life_event_categories')->where('account_id', $account->id)->get()->count() + ); + + $this->assertEquals( + 43, + DB::table('life_event_types')->where('account_id', $account->id)->get()->count() + ); + } +} diff --git a/tests/Unit/Models/ActivityTest.php b/tests/Unit/Models/ActivityTest.php new file mode 100644 index 0000000..d721458 --- /dev/null +++ b/tests/Unit/Models/ActivityTest.php @@ -0,0 +1,65 @@ +make(); + + $this->assertInstanceOf( + Carbon::class, + $activity->happened_at + ); + } + + /** @test */ + public function it_returns_a_title() + { + $type = factory(ActivityType::class)->create(); + + $activity = factory(Activity::class)->create([ + 'activity_type_id' => $type->id, + ]); + + $this->assertEquals( + $type->translation_key, + $activity->getTitle() + ); + } + + /** @test */ + public function it_gets_info_for_journal_entry() + { + $activity = factory(Activity::class)->create(); + + $data = [ + 'type' => 'activity', + 'id' => $activity->id, + 'activity_type' => (! is_null($activity->type) ? $activity->type->name : null), + 'summary' => $activity->summary, + 'description' => $activity->description, + 'day' => $activity->happened_at->day, + 'day_name' => $activity->happened_at->format('D'), + 'month' => $activity->happened_at->month, + 'month_name' => strtoupper($activity->happened_at->format('M')), + 'year' => $activity->happened_at->year, + 'attendees' => $activity->getContactsForAPI(), + ]; + + $this->assertEquals( + $data, + $activity->getInfoForJournalEntry() + ); + } +} diff --git a/tests/Unit/Models/ActivityTypeCategoryTest.php b/tests/Unit/Models/ActivityTypeCategoryTest.php new file mode 100644 index 0000000..9fb228c --- /dev/null +++ b/tests/Unit/Models/ActivityTypeCategoryTest.php @@ -0,0 +1,56 @@ +create([]); + + $this->assertTrue($activityTypeCategory->account()->exists()); + } + + /** @test */ + public function it_has_many_activity_types() + { + $activityTypeCategory = factory(ActivityTypeCategory::class)->create([]); + $activityType = factory(ActivityType::class, 10)->create([ + 'activity_type_category_id' => $activityTypeCategory->id, + ]); + + $this->assertTrue($activityTypeCategory->activityTypes()->exists()); + } + + /** @test */ + public function it_gets_the_name_attribute() + { + $activityTypeCategory = factory(ActivityTypeCategory::class)->create([ + 'translation_key' => 'awesome_key', + 'name' => null, + ]); + + $this->assertEquals( + 'people.activity_type_category_awesome_key', + $activityTypeCategory->name + ); + + $activityTypeCategory = factory(ActivityTypeCategory::class)->create([ + 'translation_key' => null, + 'name' => 'awesome_name', + ]); + + $this->assertEquals( + 'awesome_name', + $activityTypeCategory->name + ); + } +} diff --git a/tests/Unit/Models/ActivityTypeTest.php b/tests/Unit/Models/ActivityTypeTest.php new file mode 100644 index 0000000..be190b4 --- /dev/null +++ b/tests/Unit/Models/ActivityTypeTest.php @@ -0,0 +1,98 @@ +create([]); + + $this->assertTrue($activityType->account()->exists()); + } + + /** @test */ + public function it_belongs_to_a_category() + { + $activityType = factory(ActivityType::class)->create([]); + + $this->assertTrue($activityType->category()->exists()); + } + + /** @test */ + public function it_has_many_activities() + { + $account = factory(Account::class)->create(); + $activityType = factory(ActivityType::class)->create([ + 'account_id' => $account->id, + ]); + $activity = factory(Activity::class, 2)->create([ + 'account_id' => $account->id, + 'activity_type_id' => $activityType->id, + ]); + + $this->assertTrue($account->activities()->exists()); + } + + /** @test */ + public function it_gets_the_name_attribute() + { + $activityType = factory(ActivityType::class)->create([ + 'translation_key' => 'awesome_key', + 'name' => null, + ]); + + $this->assertEquals( + 'people.activity_type_awesome_key', + $activityType->name + ); + + $activityType = factory(ActivityType::class)->create([ + 'translation_key' => null, + 'name' => 'awesome_name', + ]); + + $this->assertEquals( + 'awesome_name', + $activityType->name + ); + } + + /** @test */ + public function it_resets_the_associated_activities() + { + $activityType = factory(ActivityType::class)->create([]); + $activity = factory(Activity::class, 10)->create([ + 'activity_type_id' => $activityType->id, + ]); + + $this->assertEquals( + 10, + $activityType->activities()->count() + ); + + $this->assertDatabaseHas('activities', [ + 'activity_type_id' => $activityType->id, + ]); + + $activityType->resetAssociationWithActivities(); + + $this->assertDatabaseMissing('activities', [ + 'activity_type_id' => $activityType->id, + ]); + + $this->assertEquals( + 0, + $activityType->activities()->count() + ); + } +} diff --git a/tests/Unit/Models/AddressBookSubscriptionTest.php b/tests/Unit/Models/AddressBookSubscriptionTest.php new file mode 100644 index 0000000..fd1ed12 --- /dev/null +++ b/tests/Unit/Models/AddressBookSubscriptionTest.php @@ -0,0 +1,74 @@ +create(); + $user = factory(User::class)->create(['account_id' => $account->id]); + $addressBookSubscription = AddressBookSubscription::factory()->create([ + 'account_id' => $account->id, + 'user_id' => $user->id, + ]); + + $this->assertTrue($addressBookSubscription->account()->exists()); + } + + /** @test */ + public function it_belongs_to_a_user() + { + $user = factory(User::class)->create(); + $addressBookSubscription = AddressBookSubscription::factory()->create([ + 'user_id' => $user->id, + ]); + + $this->assertTrue($addressBookSubscription->user()->exists()); + } + + /** @test */ + public function it_belongs_to_an_addressbook() + { + $addressBook = AddressBook::factory()->create(); + $addressBookSubscription = AddressBookSubscription::factory()->create([ + 'address_book_id' => $addressBook->id, + ]); + + $this->assertTrue($addressBookSubscription->addressBook()->exists()); + } + + /** @test */ + public function it_saves_capabilities() + { + $addressBookSubscription = new AddressBookSubscription(); + + $addressBookSubscription->capabilities = [ + 'test' => true, + ]; + + $this->assertIsArray($addressBookSubscription->capabilities); + $this->assertEquals([ + 'test' => true, + ], $addressBookSubscription->capabilities); + } + + /** @test */ + public function it_saves_password() + { + $addressBookSubscription = new AddressBookSubscription(); + + $addressBookSubscription->password = 'test'; + $this->assertEquals('test', $addressBookSubscription->password); + } +} diff --git a/tests/Unit/Models/AddressBookTest.php b/tests/Unit/Models/AddressBookTest.php new file mode 100644 index 0000000..e125fc3 --- /dev/null +++ b/tests/Unit/Models/AddressBookTest.php @@ -0,0 +1,38 @@ +create(); + $user = factory(User::class)->create(['account_id' => $account->id]); + $addressBook = AddressBook::factory()->create([ + 'account_id' => $account->id, + 'user_id' => $user->id, + ]); + + $this->assertTrue($addressBook->account()->exists()); + } + + /** @test */ + public function it_belongs_to_a_user() + { + $user = factory(User::class)->create(); + $addressBook = AddressBook::factory()->create([ + 'user_id' => $user->id, + ]); + + $this->assertTrue($addressBook->user()->exists()); + } +} diff --git a/tests/Unit/Models/AddressTest.php b/tests/Unit/Models/AddressTest.php new file mode 100644 index 0000000..736d9fc --- /dev/null +++ b/tests/Unit/Models/AddressTest.php @@ -0,0 +1,33 @@ +create([]); + $this->assertTrue($address->account()->exists()); + } + + /** @test */ + public function it_belongs_to_a_contact() + { + $address = factory(Address::class)->create([]); + $this->assertTrue($address->contact()->exists()); + } + + /** @test */ + public function it_belongs_to_a_place() + { + $address = factory(Address::class)->create([]); + $this->assertTrue($address->place()->exists()); + } +} diff --git a/tests/Unit/Models/AuditLogTest.php b/tests/Unit/Models/AuditLogTest.php new file mode 100644 index 0000000..69bfc00 --- /dev/null +++ b/tests/Unit/Models/AuditLogTest.php @@ -0,0 +1,47 @@ +create([]); + $this->assertTrue($auditLog->account()->exists()); + } + + /** @test */ + public function it_belongs_to_a_user(): void + { + $auditLog = factory(AuditLog::class)->create([]); + $this->assertTrue($auditLog->author()->exists()); + } + + /** @test */ + public function it_belongs_to_a_contact(): void + { + $contact = factory(Contact::class)->create([]); + $auditLog = factory(AuditLog::class)->create([ + 'about_contact_id' => $contact->id, + ]); + $this->assertTrue($auditLog->contact()->exists()); + } + + /** @test */ + public function it_returns_the_object_attribute(): void + { + $auditLog = factory(AuditLog::class)->create([]); + $this->assertEquals( + 1, + $auditLog->object->{'user'} + ); + } +} diff --git a/tests/Unit/Models/CompanyTest.php b/tests/Unit/Models/CompanyTest.php new file mode 100644 index 0000000..cfceb7c --- /dev/null +++ b/tests/Unit/Models/CompanyTest.php @@ -0,0 +1,35 @@ +create([]); + $company = factory(Company::class)->create([ + 'account_id' => $account->id, + ]); + + $this->assertTrue($company->account()->exists()); + } + + /** @test */ + public function it_has_many_occupations() + { + $company = factory(Company::class)->create([]); + $occupations = factory(Occupation::class)->create([ + 'company_id' => $company->id, + ]); + $this->assertTrue($company->occupations()->exists()); + } +} diff --git a/tests/Unit/Models/ContactFieldTest.php b/tests/Unit/Models/ContactFieldTest.php new file mode 100644 index 0000000..328b949 --- /dev/null +++ b/tests/Unit/Models/ContactFieldTest.php @@ -0,0 +1,24 @@ +data = 'this is a test'; + + $this->assertEquals( + 'this is a test', + $contactField->data + ); + } +} diff --git a/tests/Unit/Models/ContactFieldTypeTest.php b/tests/Unit/Models/ContactFieldTypeTest.php new file mode 100644 index 0000000..fd4df25 --- /dev/null +++ b/tests/Unit/Models/ContactFieldTypeTest.php @@ -0,0 +1,39 @@ +create([]); + $conversation = factory(Conversation::class, 3)->create([ + 'account_id' => $contactFieldType->account_id, + 'contact_field_type_id' => $contactFieldType->id, + ]); + + $this->assertTrue($contactFieldType->conversations()->exists()); + } + + /** @test */ + public function it_belongs_to_an_account() + { + $account = factory(Account::class)->create([]); + $contactFieldType = factory(ContactFieldType::class)->create([]); + $conversation = factory(Conversation::class, 3)->create([ + 'account_id' => $account->id, + 'contact_field_type_id' => $contactFieldType->id, + ]); + + $this->assertTrue($contactFieldType->account()->exists()); + } +} diff --git a/tests/Unit/Models/ContactTest.php b/tests/Unit/Models/ContactTest.php new file mode 100644 index 0000000..4b27467 --- /dev/null +++ b/tests/Unit/Models/ContactTest.php @@ -0,0 +1,1094 @@ +create([]); + $gender = factory(Gender::class)->create([ + 'account_id' => $account->id, + ]); + + $contact = factory(Contact::class)->create(['gender_id' => $gender->id]); + + $this->assertTrue($contact->gender()->exists()); + } + + /** @test */ + public function it_has_many_relationships() + { + $account = factory(Account::class)->create([]); + $contact = factory(Contact::class)->create(['account_id' => $account->id]); + $relationship = factory(Relationship::class, 2)->create([ + 'account_id' => $account->id, + 'contact_is' => $contact->id, + ]); + + $this->assertTrue($contact->relationships()->exists()); + } + + /** @test */ + public function it_has_many_conversations() + { + $account = factory(Account::class)->create([]); + $contact = factory(Contact::class)->create(['account_id' => $account->id]); + $conversation = factory(Conversation::class, 2)->create([ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + ]); + + $this->assertTrue($contact->conversations()->exists()); + } + + /** @test */ + public function it_has_many_messages() + { + $account = factory(Account::class)->create([]); + $contact = factory(Contact::class)->create(['account_id' => $account->id]); + $messages = factory(Message::class, 2)->create([ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + ]); + + $this->assertTrue($contact->messages()->exists()); + } + + /** @test */ + public function it_has_many_documents() + { + $account = factory(Account::class)->create([]); + $contact = factory(Contact::class)->create(['account_id' => $account->id]); + $documents = factory(Document::class, 2)->create([ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + ]); + + $this->assertTrue($contact->documents()->exists()); + } + + /** @test */ + public function it_has_many_photos() + { + $account = factory(Account::class)->create([]); + $contact = factory(Contact::class)->create(['account_id' => $account->id]); + $photo = factory(Photo::class)->create([ + 'account_id' => $account->id, + ]); + + $contact->photos()->sync([$photo->id]); + + $this->assertTrue($contact->photos()->exists()); + } + + /** @test */ + public function it_has_many_life_events() + { + $account = factory(Account::class)->create([]); + $contact = factory(Contact::class)->create(['account_id' => $account->id]); + $lifeEvents = factory(LifeEvent::class, 2)->create([ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + ]); + $this->assertTrue($contact->lifeEvents()->exists()); + } + + /** @test */ + public function it_has_many_occupations() + { + $account = factory(Account::class)->create([]); + $contact = factory(Contact::class)->create(['account_id' => $account->id]); + $occupations = factory(Occupation::class, 2)->create([ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + ]); + $this->assertTrue($contact->occupations()->exists()); + } + + /** @test */ + public function it_has_many_logs() + { + $contact = factory(Contact::class)->create(); + factory(AuditLog::class, 2)->create([ + 'about_contact_id' => $contact->id, + ]); + $this->assertTrue($contact->logs()->exists()); + } + + /** @test */ + public function it_gets_the_nickname() + { + $contact = new Contact; + $contact->nickname = 'Peter'; + + $this->assertEquals( + 'Peter', + $contact->nickname + ); + } + + /** @test */ + public function it_sets_the_nickname() + { + $contact = new Contact; + $contact->nickname = ' Peter '; + + $this->assertEquals( + 'Peter', + $contact->nickname + ); + } + + /** @test */ + public function name_attribute_returns_name_in_the_right_order() + { + $contact = new Contact; + $contact->first_name = 'Peter'; + $contact->middle_name = 'H'; + $contact->last_name = 'Gregory'; + $contact->is_dead = false; + + $this->assertEquals( + 'Peter H Gregory', + $contact->name + ); + + $contact = new Contact; + $contact->first_name = 'Peter'; + $contact->middle_name = null; + $contact->last_name = 'Gregory'; + $this->assertEquals( + 'Peter Gregory', + $contact->name + ); + + $contact = new Contact; + $contact->first_name = 'Peter'; + $contact->middle_name = null; + $contact->last_name = null; + $this->assertEquals( + 'Peter', + $contact->name + ); + + $contact = new Contact; + $contact->first_name = 'Peter'; + $contact->middle_name = 'H'; + $contact->last_name = 'Gregory'; + $contact->nickname = 'Rambo'; + $contact->is_dead = true; + $this->assertEquals( + 'Peter H Gregory ⚰', + $contact->name + ); + + $contact = new Contact; + $contact->first_name = 'Peter'; + $contact->middle_name = 'H'; + $contact->last_name = 'Gregory'; + $contact->nickname = 'Rambo'; + $contact->nameOrder('lastname_firstname'); + $this->assertEquals( + 'Gregory H Peter', + $contact->name + ); + + $contact = new Contact; + $contact->first_name = 'Peter'; + $contact->middle_name = 'H'; + $contact->last_name = 'Gregory'; + $contact->nickname = 'Rambo'; + $contact->nameOrder('firstname_lastname_nickname'); + $this->assertEquals( + 'Peter H Gregory (Rambo)', + $contact->name + ); + + $contact = new Contact; + $contact->first_name = 'Peter'; + $contact->middle_name = 'H'; + $contact->last_name = 'Gregory'; + $contact->nickname = 'Rambo'; + $contact->nameOrder('firstname_nickname_lastname'); + $this->assertEquals( + 'Peter H (Rambo) Gregory', + $contact->name + ); + + $contact = new Contact; + $contact->first_name = 'Peter'; + $contact->middle_name = 'H'; + $contact->last_name = 'Gregory'; + $contact->nickname = 'Rambo'; + $contact->nameOrder('lastname_firstname_nickname'); + $this->assertEquals( + 'Gregory Peter H (Rambo)', + $contact->name + ); + + $contact = new Contact; + $contact->first_name = 'Peter'; + $contact->middle_name = 'H'; + $contact->last_name = 'Gregory'; + $contact->nickname = 'Rambo'; + $contact->nameOrder('lastname_nickname_firstname'); + $this->assertEquals( + 'Gregory (Rambo) Peter H', + $contact->name + ); + + $contact = new Contact; + $contact->first_name = 'Peter'; + $contact->middle_name = 'H'; + $contact->last_name = 'Gregory'; + $contact->nickname = 'Rambo'; + $contact->nameOrder('nickname_firstname_lastname'); + $this->assertEquals( + 'Rambo (Peter H Gregory)', + $contact->name + ); + + $contact = new Contact; + $contact->first_name = 'Peter'; + $contact->middle_name = 'H'; + $contact->last_name = null; + $contact->nickname = 'Rambo'; + $contact->nameOrder('nickname_firstname_lastname'); + $this->assertEquals( + 'Rambo (Peter H)', + $contact->name + ); + + $contact = new Contact; + $contact->first_name = 'Peter'; + $contact->middle_name = null; + $contact->last_name = 'Gregory'; + $contact->nickname = 'Rambo'; + $contact->nameOrder('nickname_firstname_lastname'); + $this->assertEquals( + 'Rambo (Peter Gregory)', + $contact->name + ); + + $contact = new Contact; + $contact->first_name = 'Peter'; + $contact->middle_name = 'H'; + $contact->last_name = 'Gregory'; + $contact->nickname = 'Rambo'; + $contact->nameOrder('nickname_lastname_firstname'); + $this->assertEquals( + 'Rambo (Gregory Peter H)', + $contact->name + ); + + $contact = new Contact; + $contact->first_name = 'Peter'; + $contact->middle_name = 'H'; + $contact->last_name = null; + $contact->nickname = 'Rambo'; + $contact->nameOrder('nickname_lastname_firstname'); + $this->assertEquals( + 'Rambo (Peter H)', + $contact->name + ); + + $contact = new Contact; + $contact->first_name = 'Peter'; + $contact->middle_name = null; + $contact->last_name = 'Gregory'; + $contact->nickname = 'Rambo'; + $contact->nameOrder('nickname_lastname_firstname'); + $this->assertEquals( + 'Rambo (Gregory Peter)', + $contact->name + ); + + $contact = new Contact; + $contact->first_name = 'Peter'; + $contact->middle_name = 'H'; + $contact->last_name = 'Gregory'; + $contact->nickname = 'Rambo'; + $contact->nameOrder('nickname_bracketed_firstname_lastname'); + $this->assertEquals( + 'Rambo (Peter H) Gregory', + $contact->name + ); + + $contact = new Contact; + $contact->first_name = 'Peter'; + $contact->middle_name = 'H'; + $contact->last_name = null; + $contact->nickname = 'Rambo'; + $contact->nameOrder('nickname_bracketed_firstname_lastname'); + $this->assertEquals( + 'Rambo (Peter H)', + $contact->name + ); + + $contact = new Contact; + $contact->first_name = 'Peter'; + $contact->middle_name = null; + $contact->last_name = 'Gregory'; + $contact->nickname = 'Rambo'; + $contact->nameOrder('nickname_bracketed_firstname_lastname'); + $this->assertEquals( + 'Rambo (Peter) Gregory', + $contact->name + ); + + $contact = new Contact; + $contact->first_name = 'Peter'; + $contact->middle_name = 'H'; + $contact->last_name = 'Gregory'; + $contact->nickname = 'Rambo'; + $contact->nameOrder('nickname'); + $this->assertEquals( + 'Rambo', + $contact->name + ); + + $contact = new Contact; + $contact->first_name = 'Peter'; + $contact->last_name = 'Gregory'; + $contact->nameOrder('nickname'); + $this->assertEquals( + 'Peter Gregory', + $contact->name + ); + + $contact = new Contact; + $contact->first_name = 'Peter'; + $contact->last_name = null; + $contact->nameOrder('nickname'); + $this->assertEquals( + 'Peter', + $contact->name + ); + } + + /** @test */ + public function it_returns_the_initials() + { + $contact = new Contact; + $contact->first_name = 'Peter'; + $contact->middle_name = 'H'; + $contact->last_name = 'Gregory'; + + $this->assertEquals( + 'PHG', + $contact->getInitials() + ); + + $contact = new Contact; + $contact->first_name = 'Peter'; + $contact->middle_name = null; + $contact->last_name = 'Gregory'; + + $this->assertEquals( + 'PG', + $contact->getInitials() + ); + + $contact = new Contact; + $contact->first_name = 'Peter'; + $contact->middle_name = 'H'; + $contact->last_name = null; + + $this->assertEquals( + 'PH', + $contact->getInitials() + ); + + $contact = new Contact; + $contact->first_name = 'Peter'; + $contact->middle_name = null; + $contact->last_name = null; + + $this->assertEquals( + 'P', + $contact->getInitials() + ); + } + + /** @test */ + public function get_initials_returns_order_thanks_to_user_preferences() + { + $contact = new Contact; + $contact->first_name = 'Peter'; + $contact->middle_name = null; + $contact->last_name = 'Gregory'; + $contact->nameOrder('lastname_firstname'); + + $this->assertEquals( + 'GP', + $contact->getInitials() + ); + } + + /** @test */ + public function get_initials_with_special_chars() + { + $user = $this->signIn(); + $user->locale = 'de'; + $user->save(); + + $contact = new Contact; + $contact->first_name = 'Änders'; + $contact->middle_name = null; + $contact->last_name = 'Ürgen'; + $contact->nameOrder('lastname_firstname'); + + $this->assertEquals( + 'AU', + $contact->getInitials() + ); + } + + /** @test */ + public function it_returns_the_last_activity_date_for_multiple_activities() + { + $contact = factory(Contact::class)->create(); + + $activity1 = factory(Activity::class)->create([ + 'happened_at' => '2015-10-29', + 'account_id' => $contact->account_id, + ]); + $contact->activities()->attach($activity1, ['account_id' => $contact->account_id]); + + $activity2 = factory(Activity::class)->create([ + 'happened_at' => '2010-10-29', + 'account_id' => $contact->account_id, + ]); + $contact->activities()->attach($activity2, ['account_id' => $contact->account_id]); + + $activity3 = factory(Activity::class)->create([ + 'happened_at' => '1981-10-29', + 'account_id' => $contact->account_id, + ]); + $contact->activities()->attach($activity3, ['account_id' => $contact->account_id]); + + $this->assertEquals( + '2015-10-29', + DateHelper::getDate($contact->getLastActivityDate()) + ); + } + + /** @test */ + public function it_returns_the_last_activity_date_for_one_activity() + { + $contact = factory(Contact::class)->create(); + + $activity1 = factory(Activity::class)->create([ + 'happened_at' => '2015-10-29', + 'account_id' => $contact->account_id, + ]); + $contact->activities()->attach($activity1, ['account_id' => $contact->account_id]); + + $this->assertEquals( + '2015-10-29', + DateHelper::getDate($contact->getLastActivityDate()) + ); + } + + /** @test */ + public function it_returns_the_last_activity_date_for_no_activity() + { + $contact = new Contact; + $contact->account_id = 1; + $contact->id = 1; + + $this->assertNull( + $contact->getLastActivityDate() + ); + } + + /** @test */ + public function it_sets_a_default_avatar_color() + { + $contact = factory(Contact::class)->create([]); + $contact->setAvatarColor(); + + $this->assertEquals( + 7, + strlen($contact->default_avatar_color) + ); + } + + /** @test */ + public function it_returns_the_url_of_the_avatar() + { + // default + $contact = factory(Contact::class)->create([ + 'avatar_default_url' => 'defaultURL', + 'avatar_source' => 'default', + ]); + + $this->assertStringContainsString( + 'store/defaultURL', + $contact->getAvatarURL() + ); + + // adorable + $contact = factory(Contact::class)->create([ + 'avatar_adorable_uuid' => 'uuid', + 'avatar_source' => 'adorable', + ]); + + $this->mock(LaravelAdorable::class, function (MockInterface $mock) { + $mock->shouldReceive('get')->andReturn('adorableURL'); + }); + + $this->assertEquals( + 'adorableURL', + $contact->getAvatarURL() + ); + + // gravatar + $contact = factory(Contact::class)->create([ + 'avatar_gravatar_url' => 'gravatarURL', + 'avatar_source' => 'gravatar', + ]); + + $this->assertEquals( + 'gravatarURL', + $contact->getAvatarURL() + ); + + // photo + $photo = factory(Photo::class)->create([ + 'account_id' => $contact->account_id, + ]); + $contact->avatar_photo_id = $photo->id; + $contact->avatar_source = 'photo'; + $contact->save(); + + $this->assertEquals( + config('app.url').'/store/'.$photo->new_filename, + $contact->getAvatarURL() + ); + } + + /** @test */ + public function it_indicates_that_it_has_not_debts() + { + $contact = new Contact; + + $this->assertFalse( + $contact->hasDebt() + ); + } + + /** @test */ + public function a_contact_is_owned_money() + { + /** @var Contact $contact */ + $contact = factory(Contact::class)->create(); + + $contact->debts()->save(new Debt([ + 'in_debt' => 'no', + 'amount' => 100, + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + ])); + + $this->assertTrue($contact->isOwedMoney()); + } + + /** @test */ + public function a_contact_is_not_owned_money() + { + /** @var Contact $contact */ + $contact = factory(Contact::class)->create(); + + $contact->debts()->save(new Debt([ + 'in_debt' => 'yes', + 'amount' => 100, + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + ])); + + $this->assertFalse($contact->isOwedMoney()); + } + + /** @test */ + public function it_returns_the_amount_of_money_due() + { + /** @var Contact $contact */ + $contact = factory(Contact::class)->create(); + + $contact->debts()->save(new Debt([ + 'in_debt' => 'no', + 'amount' => 100, + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + ])); + $contact->debts()->save(new Debt([ + 'in_debt' => 'no', + 'amount' => 100, + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + ])); + + $this->assertEquals(20000, $contact->totalOutstandingDebtAmount()); + + $contact->debts()->save(new Debt([ + 'in_debt' => 'yes', + 'amount' => 100, + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + ])); + + $this->assertEquals(10000, $contact->totalOutstandingDebtAmount()); + } + + /** @test */ + public function set_special_date_with_age_creates_a_date_and_saves_the_id() + { + $contact = factory(Contact::class)->create(); + + $this->assertNull($contact->setSpecialDateFromAge(null, 33)); + + $this->assertNull($contact->birthday_special_date_id); + + $specialDate = $contact->setSpecialDateFromAge('birthdate', 33); + $this->assertNotNull($contact->birthday_special_date_id); + } + + /** @test */ + public function has_first_met_information_returns_false_if_no_information_is_present() + { + $contact = factory(Contact::class)->create(); + + $this->assertFalse($contact->hasFirstMetInformation()); + } + + /** @test */ + public function has_first_met_information_returns_true_if_at_least_one_info_is_present() + { + $contact = factory(Contact::class)->create(); + + $contact->first_met_additional_info = 'data'; + $this->assertTrue($contact->hasFirstMetInformation()); + } + + /** @test */ + public function it_returns_an_unknown_birthday_state() + { + $contact = factory(Contact::class)->create(); + + $this->assertEquals( + 'unknown', + $contact->getBirthdayState() + ); + } + + /** @test */ + public function it_returns_an_approximate_birthday_state() + { + $contact = factory(Contact::class)->create(); + $specialDate = factory(SpecialDate::class)->create([ + 'is_age_based' => 1, + ]); + $contact->birthday_special_date_id = $specialDate->id; + $contact->save(); + + $specialDate->contact_id = $contact->id; + $specialDate->save(); + + $this->assertEquals( + 'approximate', + $contact->getBirthdayState() + ); + } + + /** @test */ + public function it_returns_an_almost_birthday_state() + { + $contact = factory(Contact::class)->create(); + $specialDate = factory(SpecialDate::class)->create([ + 'is_age_based' => 0, + 'is_year_unknown' => 1, + ]); + $contact->birthday_special_date_id = $specialDate->id; + $contact->save(); + + $specialDate->contact_id = $contact->id; + $specialDate->save(); + + $this->assertEquals( + 'almost', + $contact->getBirthdayState() + ); + } + + /** @test */ + public function it_returns_an_exact_birthday_state() + { + $contact = factory(Contact::class)->create(); + $specialDate = factory(SpecialDate::class)->create(); + $contact->birthday_special_date_id = $specialDate->id; + $contact->save(); + + $specialDate->contact_id = $contact->id; + $specialDate->save(); + + $this->assertEquals( + 'exact', + $contact->getBirthdayState() + ); + } + + /** @test */ + public function set_name_returns_false_if_given_an_empty_firstname() + { + $contact = factory(Contact::class)->create(); + + $this->assertFalse($contact->setName('', 'Test', 'Test')); + } + + /** @test */ + public function set_name_returns_true() + { + $contact = factory(Contact::class)->create(); + $this->assertTrue($contact->setName('John', 'Doe', 'Jr')); + $contact->save(); + + $this->assertDatabaseHas( + 'contacts', + [ + 'first_name' => 'John', + 'last_name' => 'Doe', + 'middle_name' => 'Jr', + ] + ); + } + + /** @test */ + public function it_gets_related_relationships_of_a_certain_relationshiptype_group_name() + { + $account = factory(Account::class)->create([]); + $contact = factory(Contact::class)->create(['account_id' => $account->id]); + $relatedContact = factory(Contact::class)->create(['account_id' => $account->id]); + $otherRelatedContact = factory(Contact::class)->create(['account_id' => $account->id]); + $relationshipTypeGroup = factory(RelationshipTypeGroup::class)->create([ + 'account_id' => $account->id, + 'name' => 'friend', + ]); + $relationshipType = factory(RelationshipType::class)->create([ + 'account_id' => $account->id, + 'relationship_type_group_id' => $relationshipTypeGroup->id, + ]); + $relationship = factory(Relationship::class)->create([ + 'account_id' => $account->id, + 'relationship_type_id' => $relationshipType->id, + 'contact_is' => $contact->id, + 'of_contact' => $relatedContact->id, + ]); + $relationship = factory(Relationship::class)->create([ + 'account_id' => $account->id, + 'relationship_type_id' => $relationshipType->id, + 'contact_is' => $contact->id, + 'of_contact' => $otherRelatedContact->id, + ]); + + $this->assertEquals( + 2, + $contact->getRelationshipsByRelationshipTypeGroup('friend')->count() + ); + + $this->assertNull($contact->getRelationshipsByRelationshipTypeGroup('love')); + } + + /** @test */ + public function it_gets_the_right_number_of_birthdays_about_related_contacts() + { + $user = $this->signIn(); + + $contact = factory(Contact::class)->create(['account_id' => $user->account_id]); + $specialDate = factory(SpecialDate::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + ]); + $reminder = factory(Reminder::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contact->id, + ]); + $contact->birthday_special_date_id = $specialDate->id; + $contact->birthday_reminder_id = $reminder->id; + $contact->save(); + + $contactB = factory(Contact::class)->create(['account_id' => $user->account_id]); + $specialDate = factory(SpecialDate::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contactB->id, + ]); + $reminder = factory(Reminder::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contactB->id, + ]); + $contactB->birthday_special_date_id = $specialDate->id; + $contactB->birthday_reminder_id = $reminder->id; + $contactB->save(); + + $contactC = factory(Contact::class)->create(['account_id' => $user->account_id]); + $specialDate = factory(SpecialDate::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contactC->id, + ]); + $reminder = factory(Reminder::class)->create([ + 'account_id' => $user->account_id, + 'contact_id' => $contactC->id, + ]); + $contactC->birthday_special_date_id = $specialDate->id; + $contactC->birthday_reminder_id = $reminder->id; + $contactC->save(); + + $relationshipType = factory(RelationshipType::class)->create([ + 'account_id' => $user->account_id, + ]); + $relationship = factory(Relationship::class)->create([ + 'account_id' => $user->account_id, + 'relationship_type_id' => $relationshipType->id, + 'contact_is' => $contact->id, + 'of_contact' => $contactB->id, + ]); + $relationship = factory(Relationship::class)->create([ + 'account_id' => $user->account_id, + 'relationship_type_id' => $relationshipType->id, + 'contact_is' => $contact->id, + 'of_contact' => $contactC->id, + ]); + + $this->assertEquals( + 2, + $contact->getBirthdayRemindersAboutRelatedContacts()->count() + ); + } + + /** @test */ + public function it_fetches_the_partial_contact_who_belongs_to_a_real_contact() + { + $user = $this->signIn(); + + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + 'is_partial' => false, + ]); + $otherContact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + 'is_partial' => true, + ]); + + $relationshipType = factory(RelationshipType::class)->create([ + 'account_id' => $user->account_id, + ]); + $relationship = factory(Relationship::class)->create([ + 'account_id' => $user->account_id, + 'relationship_type_id' => $relationshipType->id, + 'contact_is' => $otherContact->id, + 'of_contact' => $contact->id, + ]); + + $foundContact = $otherContact->getRelatedRealContact(); + + $this->assertInstanceOf(Contact::class, $foundContact); + + $this->assertEquals( + $contact->id, + $foundContact->id + ); + } + + /** @test */ + public function it_updates_stay_in_touch_frequency() + { + $account = factory(Account::class)->create([]); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + 'stay_in_touch_frequency' => null, + ]); + + $result = $contact->updateStayInTouchFrequency(3); + + $this->assertTrue($result); + + $this->assertDatabaseHas('contacts', [ + 'id' => $contact->id, + 'stay_in_touch_frequency' => 3, + ]); + } + + /** @test */ + public function it_resets_stay_in_touch_frequency_if_set_to_0() + { + $account = factory(Account::class)->create([]); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + 'stay_in_touch_frequency' => 3, + ]); + + $result = $contact->updateStayInTouchFrequency(0); + + $this->assertTrue($result); + + $this->assertDatabaseHas('contacts', [ + 'id' => $contact->id, + 'stay_in_touch_frequency' => null, + ]); + } + + /** @test */ + public function it_returns_false_if_frequency_is_not_an_integer() + { + $account = factory(Account::class)->create([]); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + + $result = $contact->updateStayInTouchFrequency('not an integer'); + + $this->assertFalse($result); + } + + /** @test */ + public function it_updates_the_stay_in_touch_trigger_date() + { + Carbon::setTestNow(Carbon::create(2017, 1, 1)); + + $account = factory(Account::class)->create([]); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + + $this->assertNull($contact->stay_in_touch_trigger_date); + + $contact->setStayInTouchTriggerDate(3); + + $this->assertNotNull($contact->stay_in_touch_trigger_date); + + $this->assertEquals( + '2017-01-04', + $contact->stay_in_touch_trigger_date->toDateString() + ); + } + + public function it_resets_the_stay_in_touch_trigger_date() + { + Carbon::setTestNow(Carbon::create(2017, 1, 1)); + + $account = factory(Account::class)->create([]); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + 'stay_in_touch_trigger_date' => '2018-03-03 00:00:00', + ]); + + $contact->setStayInTouchTriggerDate(0); + + $this->assertNull($contact->stay_in_touch_trigger_date); + } + + /** @test */ + public function it_sends_the_stay_in_touch_email() + { + config(['monica.requires_subscription' => false]); + NotificationFacade::fake(); + + Carbon::setTestNow(Carbon::create(2017, 1, 1, 15, 0, 0)); + + $account = factory(Account::class)->create([ + 'default_time_reminder_is_sent' => '10:00', + ]); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + 'stay_in_touch_frequency' => 3, + 'stay_in_touch_trigger_date' => '2017-01-01 00:00:00', + ]); + $user = factory(User::class)->create([ + 'account_id' => $account->id, + 'email' => 'john@doe.com', + 'timezone' => 'America/New_York', + ]); + + ScheduleStayInTouch::dispatch($contact); + + NotificationFacade::assertSentTo($user, StayInTouchEmail::class, + function ($notification, $channels) use ($contact) { + return $channels[0] == 'mail' + && $notification->assertSentFor($contact); + } + ); + } + + /** @test */ + public function it_gets_the_age_at_death() + { + $contact = factory(Contact::class)->create(); + + $specialDate = $contact->setSpecialDate('birthdate', 1980, 10, 10); + $specialDate = $contact->setSpecialDate('deceased_date', 2010, 10, 10); + + $this->assertEquals( + 30, + $contact->getAgeAtDeath() + ); + } + + /** @test */ + public function getting_age_at_death_returns_null() + { + $contact = factory(Contact::class)->create(); + + $specialDate = $contact->setSpecialDate('birthdate', 1980, 10, 10); + + $this->assertNull( + $contact->getAgeAtDeath() + ); + } + + /** @test */ + public function it_gets_the_default_avatar_url_attribute() + { + $contact = factory(Contact::class)->create([ + 'avatar_default_url' => 'avatars/image.jpg', + ]); + + config(['filesystems.default' => 'public']); + + $this->assertStringContainsString( + 'avatars/image.jpg', + $contact->avatar_default_url + ); + } +} diff --git a/tests/Unit/Models/ConversationTest.php b/tests/Unit/Models/ConversationTest.php new file mode 100644 index 0000000..2e8029c --- /dev/null +++ b/tests/Unit/Models/ConversationTest.php @@ -0,0 +1,64 @@ +create([]); + $conversation = factory(Conversation::class)->create([ + 'account_id' => $account->id, + ]); + + $this->assertTrue($conversation->account()->exists()); + } + + /** @test */ + public function it_belongs_to_a_contact() + { + $contact = factory(Contact::class)->create(); + $conversation = factory(Conversation::class)->create([ + 'contact_id' => $contact->id, + ]); + + $this->assertTrue($conversation->contact()->exists()); + } + + /** @test */ + public function it_belongs_to_a_contact_field_type() + { + $account = factory(Account::class)->create([]); + $contactFieldType = factory(ContactFieldType::class)->create([ + 'account_id' => $account->id, + ]); + $conversation = factory(Conversation::class)->create([ + 'contact_field_type_id' => $contactFieldType->id, + 'account_id' => $account->id, + ]); + + $this->assertTrue($conversation->contactFieldType()->exists()); + } + + /** @test */ + public function it_has_many_messages() + { + $conversation = factory(Conversation::class)->create(); + $message = factory(Message::class)->create([ + 'conversation_id' => $conversation->id, + ]); + + $this->assertTrue($conversation->messages()->exists()); + } +} diff --git a/tests/Unit/Models/DayTest.php b/tests/Unit/Models/DayTest.php new file mode 100644 index 0000000..f084b47 --- /dev/null +++ b/tests/Unit/Models/DayTest.php @@ -0,0 +1,77 @@ +make(); + $day->id = 1; + $day->rate = 1; + $day->comment = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur. Donec ut libero sed arcu vehicula ultricies a non tortor.'; + $day->date = '2017-01-01 00:00:00'; + $day->created_at = '2017-01-01 00:00:00'; + $day->save(); + + $data = [ + 'type' => 'day', + 'id' => 1, + 'rate' => 1, + 'comment' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur. Donec ut libero sed arcu vehicula ultricies a non tortor.', + 'day' => 1, + 'day_name' => 'Sun', + 'month' => 1, + 'month_name' => 'JAN', + 'year' => 2017, + 'happens_today' => false, + 'date' => Carbon::parse('2017-01-01 00:00:00'), + ]; + + $this->assertEquals( + $data, + $day->getInfoForJournalEntry() + ); + } + + /** @test */ + public function get_info_for_journal_entry_that_happen_today() + { + $date = now(); + + $day = factory(Day::class)->make(); + $day->id = 1; + $day->rate = 1; + $day->comment = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur. Donec ut libero sed arcu vehicula ultricies a non tortor.'; + $day->date = $date; + $day->created_at = '2017-01-01 00:00:00'; + $day->save(); + + $data = [ + 'type' => 'day', + 'id' => 1, + 'rate' => 1, + 'comment' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur. Donec ut libero sed arcu vehicula ultricies a non tortor.', + 'day' => $date->day, + 'day_name' => $date->format('D'), + 'month' => $date->month, + 'month_name' => strtoupper($date->format('M')), + 'year' => $date->year, + 'happens_today' => true, + 'date' => $date->addMicroseconds(-1 * $date->microsecond), + ]; + + $this->assertEquals( + $data, + $day->getInfoForJournalEntry() + ); + } +} diff --git a/tests/Unit/Models/DocumentTest.php b/tests/Unit/Models/DocumentTest.php new file mode 100644 index 0000000..6ba09d6 --- /dev/null +++ b/tests/Unit/Models/DocumentTest.php @@ -0,0 +1,47 @@ +create([]); + $document = factory(Document::class)->create([ + 'account_id' => $account->id, + ]); + + $this->assertTrue($document->account()->exists()); + } + + /** @test */ + public function it_belongs_to_a_contact() + { + $contact = factory(Contact::class)->create(); + $document = factory(Document::class)->create([ + 'contact_id' => $contact->id, + ]); + + $this->assertTrue($document->contact()->exists()); + } + + /** @test */ + public function it_gets_the_download_link() + { + $document = factory(Document::class)->create(); + + $this->assertEquals( + config('app.url').'/store/'.$document->new_filename, + $document->getDownloadLink() + ); + } +} diff --git a/tests/Unit/Models/EmotionTest.php b/tests/Unit/Models/EmotionTest.php new file mode 100644 index 0000000..fe6d9da --- /dev/null +++ b/tests/Unit/Models/EmotionTest.php @@ -0,0 +1,48 @@ +create([]); + + $this->assertTrue($emotion->primary->exists()); + + $this->assertTrue($emotion->secondary->exists()); + } + + /** @test */ + public function secondary_emotion_belongs_to_a_primary_emotion() + { + $secondaryEmotion = factory(SecondaryEmotion::class)->create([]); + + $this->assertTrue($secondaryEmotion->primary->exists()); + } + + /** @test */ + public function a_primary_emotion_has_multiple_emotions() + { + $primaryEmotion = factory(PrimaryEmotion::class)->create([]); + $secondaryEmotion = factory(SecondaryEmotion::class)->create([ + 'emotion_primary_id' => $primaryEmotion->id, + ]); + factory(Emotion::class, 3)->create([ + 'emotion_primary_id' => $primaryEmotion->id, + 'emotion_secondary_id' => $secondaryEmotion->id, + ]); + + $this->assertTrue($primaryEmotion->secondaries()->exists()); + $this->assertTrue($primaryEmotion->emotions()->exists()); + } +} diff --git a/tests/Unit/Models/EntryTest.php b/tests/Unit/Models/EntryTest.php new file mode 100644 index 0000000..86336d8 --- /dev/null +++ b/tests/Unit/Models/EntryTest.php @@ -0,0 +1,42 @@ +make([ + 'id' => 1, + 'title' => 'This is the title', + 'post' => 'this is a post', + 'created_at' => '2017-01-01 00:00:00', + ]); + + $data = [ + 'type' => 'entry', + 'id' => 1, + 'title' => 'This is the title', + 'post' => 'this is a post', + 'day' => 1, + 'day_name' => 'Sun', + 'month' => 1, + 'month_name' => 'JAN', + 'year' => 2017, + 'date' => '2017-01-01 00:00:00', + 'created_at' => 'Jan 01, 2017 00:00', + ]; + + $this->assertEquals( + $data, + $entry->getInfoForJournalEntry() + ); + } +} diff --git a/tests/Unit/Models/GenderTest.php b/tests/Unit/Models/GenderTest.php new file mode 100644 index 0000000..de48cc7 --- /dev/null +++ b/tests/Unit/Models/GenderTest.php @@ -0,0 +1,68 @@ +create([]); + $gender = factory(Gender::class)->create([ + 'account_id' => $account->id, + ]); + + $this->assertTrue($gender->account()->exists()); + } + + /** @test */ + public function it_belongs_to_many_contacts() + { + $account = factory(Account::class)->create([]); + $gender = factory(Gender::class)->create([ + 'account_id' => $account->id, + ]); + $contact = factory(Contact::class)->create(['account_id' => $account->id, 'gender_id' => $gender->id]); + $contact = factory(Contact::class)->create(['account_id' => $account->id, 'gender_id' => $gender->id]); + + $this->assertTrue($gender->contacts()->exists()); + } + + /** @test */ + public function it_gets_the_gender_name() + { + $gender = new Gender; + $gender->name = 'Woman'; + + $this->assertEquals( + 'Woman', + $gender->name + ); + } + + /** @test */ + public function it_gets_the_default_gender() + { + $account = factory(Account::class)->create(); + $gender = Gender::create([ + 'account_id' => $account->id, + 'name' => 'Woman', + ]); + + $this->assertFalse($gender->isDefault()); + + $account->default_gender_id = $gender->id; + $account->save(); + $gender->refresh(); + + $this->assertTrue($gender->isDefault()); + } +} diff --git a/tests/Unit/Models/GiftTest.php b/tests/Unit/Models/GiftTest.php new file mode 100644 index 0000000..e15b832 --- /dev/null +++ b/tests/Unit/Models/GiftTest.php @@ -0,0 +1,120 @@ +make(); + + $this->assertFalse( + $gift->hasParticularRecipient() + ); + } + + /** @test */ + public function has_particular_recipient_returns_true_if_it_s_for_a_specific_recipient() + { + $gift = factory(Gift::class)->make([ + 'is_for' => 1, + ]); + + $this->assertTrue( + $gift->hasParticularRecipient() + ); + } + + /** @test */ + public function it_sets_is_for_attribute() + { + $gift = factory(Gift::class)->make([ + 'is_for' => 1, + ]); + + $this->assertEquals( + 1, + $gift->is_for + ); + } + + /** @test */ + public function it_gets_the_recipient_name() + { + $contact = factory(Contact::class)->create(['first_name' => 'Regis']); + $gift = factory(Gift::class)->make([ + 'account_id' => $contact->account_id, + 'is_for' => $contact->id, + 'contact_id' => $contact->id, + ]); + + $this->assertEquals( + 'Regis', + $gift->recipient_name + ); + } + + /** @test */ + public function it_gets_the_gift_name() + { + $gift = factory(Gift::class)->make([ + 'name' => 'Maison de folie', + ]); + + $this->assertEquals( + 'Maison de folie', + $gift->name + ); + } + + /** @test */ + public function it_gets_the_gift_url() + { + $gift = factory(Gift::class)->make([ + 'url' => 'https://facebook.com', + ]); + + $this->assertEquals( + 'https://facebook.com', + $gift->url + ); + } + + /** @test */ + public function it_gets_the_comment() + { + $gift = factory(Gift::class)->make([ + 'comment' => 'This is just a comment', + ]); + + $this->assertEquals( + 'This is just a comment', + $gift->comment + ); + } + + /** @test */ + public function it_gets_the_value() + { + $user = factory(User::class)->create(); + $this->be($user); + $gift = factory(Gift::class)->make([ + 'account_id' => $user->account_id, + 'amount' => 100, + ]); + + $this->assertEquals( + '100.00', + $gift->amount + ); + } +} diff --git a/tests/Unit/Models/Google2FATest.php b/tests/Unit/Models/Google2FATest.php new file mode 100644 index 0000000..aa7780d --- /dev/null +++ b/tests/Unit/Models/Google2FATest.php @@ -0,0 +1,68 @@ +generateSecretKey(32); + + $result = $google2fa->verifyGoogle2FA($secret, 'aaaaaa'); + + $this->assertFalse($result); + } + + /** @test */ + public function it_tests_a_correct_key_for_Google2fa() + { + $google2fa = app('pragmarx.google2fa'); + + $secret = $google2fa->generateSecretKey(32); + $one_time_password = $google2fa->getCurrentOtp($secret); + + $result = $google2fa->verifyGoogle2FA($secret, $one_time_password); + + $this->assertTrue($result); + } + + /** @test */ + public function it_logs_in_with_Google2Fa() + { + config(['google2fa.enabled' => true]); + + $google2fa = app('pragmarx.google2fa')->setStateless(false); + $secret = $google2fa->generateSecretKey(32); + + $user = factory(User::class)->create(); + $user->google2fa_secret = $secret; + $this->actingAs($user); + + $request = $this->app['request']; + // Avoid "Session store not set on request." - Exception! + $request->setLaravelSession(new Store('test', new NullSessionHandler)); + $request->getSession()->start(); + + $authenticator = new \PragmaRX\Google2FALaravel\Support\Authenticator($request); + + $this->assertFalse($authenticator->isAuthenticated()); + + $this->assertTrue($google2fa->isActivated()); + + $google2fa->login(); + + $this->assertTrue($authenticator->isAuthenticated()); + } +} diff --git a/tests/Unit/Models/IdHasherTest.php b/tests/Unit/Models/IdHasherTest.php new file mode 100644 index 0000000..ed2b9a5 --- /dev/null +++ b/tests/Unit/Models/IdHasherTest.php @@ -0,0 +1,65 @@ +encodeId($test_id); + + $value = substr($test_hash, 0, 1); + + $this->assertEquals('h', $value); + } + + /** @test */ + public function it_returns_the_id_back() + { + $idHasher = new IdHasher(); + + $test_id = rand(); + + $test_hash = $idHasher->encodeId($test_id); + + $result_id = $idHasher->decodeId($test_hash); + + $this->assertEquals($test_id, $result_id); + } + + /** @test */ + public function it_gets_an_exception_when_the_id_is_not_valid() + { + $idHasher = new IdHasher(); + + $test_id = rand(); + + $this->expectException(\App\Exceptions\WrongIdException::class); + + $idHasher->decodeId($test_id); + } + + /** @test */ + public function it_decodes_the_hash_and_returns_the_right_id() + { + $idHasher = new IdHasher(); + + $contact = factory(Contact::class)->create(); + + $value = $idHasher->decodeId($contact->hashID()); + + $this->assertEquals($contact->id, $value); + } +} diff --git a/tests/Unit/Models/ImportJobTest.php b/tests/Unit/Models/ImportJobTest.php new file mode 100644 index 0000000..2563b75 --- /dev/null +++ b/tests/Unit/Models/ImportJobTest.php @@ -0,0 +1,347 @@ +create(); + + $this->assertTrue($importJob->user()->exists()); + } + + /** @test */ + public function it_belongs_to_an_account() + { + $account = factory(Account::class)->create([]); + $user = factory(User::class)->create([ + 'account_id' => $account->id, + ]); + $importJob = factory(ImportJob::class)->create([ + 'account_id' => $account->id, + 'user_id' => $user->id, + ]); + + $this->assertTrue($importJob->account()->exists()); + } + + /** @test */ + public function it_belongs_to_many_reports() + { + $account = factory(Account::class)->create([]); + $user = factory(User::class)->create([ + 'account_id' => $account->id, + ]); + $importJob = factory(ImportJob::class)->create([ + 'account_id' => $account->id, + 'user_id' => $user->id, + ]); + factory(ImportJobReport::class, 100)->create([ + 'import_job_id' => $importJob->id, + 'account_id' => $account->id, + 'user_id' => $user->id, + ]); + + $this->assertTrue($importJob->importJobReports()->exists()); + } + + /** @test */ + public function it_initiates_the_job() + { + $importJob = factory(ImportJob::class)->make([]); + + $this->assertNull($importJob->started_at); + + $this->invokePrivateMethod($importJob, 'initJob'); + + $this->assertNotNull($importJob->started_at); + } + + /** @test */ + public function it_finalizes_the_job() + { + $importJob = factory(ImportJob::class)->make([]); + + $this->assertNull($importJob->ended_at); + + $this->invokePrivateMethod($importJob, 'endJob'); + + $this->assertNotNull($importJob->ended_at); + } + + /** @test */ + public function it_fails_and_throws_an_exception() + { + $importJob = factory(ImportJob::class)->create([]); + $this->invokePrivateMethod($importJob, 'fail', [ + 'reason', + ]); + + $this->assertTrue($importJob->failed); + $this->assertEquals( + 'reason', + $importJob->failed_reason + ); + } + + /** @test */ + public function it_gets_the_physical_file() + { + Storage::fake('public'); + $importJob = factory(ImportJob::class)->create([ + 'filename' => 'testfile.vcf', + ]); + + Storage::disk('public')->put( + 'testfile.vcf', + 'fakeContent' + ); + + Storage::disk('public')->assertExists($importJob->filename); + + $this->assertNull($importJob->physicalFile); + $this->invokePrivateMethod($importJob, 'getPhysicalFile'); + + $this->assertIsResource($importJob->physicalFile); + } + + /** @test */ + public function it_throws_an_exception_if_file_doesnt_exist() + { + Storage::fake('public', [ + 'throw' => true, + ]); + $importJob = factory(ImportJob::class)->create([ + 'filename' => 'testfile.vcf', + ]); + + $this->invokePrivateMethod($importJob, 'getPhysicalFile'); + + $this->assertEquals( + trans('settings.import_vcard_file_not_found'), + $importJob->failed_reason + ); + } + + /** @test */ + public function it_deletes_the_file() + { + Storage::fake('public'); + $importJob = factory(ImportJob::class)->create([ + 'filename' => 'testfile.vcf', + ]); + + Storage::disk('public')->put( + 'testfile.vcf', + 'fakeContent' + ); + + $this->invokePrivateMethod($importJob, 'deletePhysicalFile'); + + Storage::disk('public')->assertMissing($importJob->filename); + } + + /** @test */ + public function it_calculates_how_many_entries_there_are_and_populate_the_entries_array() + { + Storage::fake('public'); + $importJob = $this->createImportJob(); + $importJob->filename = 'testfile.vcf'; + + Storage::disk('public')->put( + 'testfile.vcf', + $this->vcfContent + ); + + $this->invokePrivateMethod($importJob, 'getPhysicalFile'); + $this->invokePrivateMethod($importJob, 'getEntries'); + $this->invokePrivateMethod($importJob, 'processEntries'); + $this->assertJobSuccess($importJob); + + $this->assertEquals( + 3, + $importJob->contacts_found + ); + } + + /** @test */ + public function it_doesnt_process_an_entry_if_import_is_not_feasible() + { + $importJob = $this->createImportJob(); + + $vcard = new VCard([ + 'TEL' => '+1 555 34567 455', + 'N' => ['', '', '', '', ''], + ]); + $this->invokePrivateMethod($importJob, 'processSingleEntry', [ + $vcard->serialize(), + ]); + $this->assertJobSuccess($importJob); + $this->assertEquals( + 1, + $importJob->contacts_skipped + ); + } + + /** @test */ + public function it_doesnt_process_an_entry_if_contact_already_exists() + { + $importJob = $this->createImportJob(); + $contact = factory(Contact::class)->create([ + 'account_id' => $importJob->account_id, + ]); + $contactFieldType = factory(ContactFieldType::class)->create([ + 'account_id' => $importJob->account_id, + 'type' => 'email', + ]); + $contactField = factory(ContactField::class)->create([ + 'account_id' => $importJob->account_id, + 'contact_id' => $contact->id, + 'contact_field_type_id' => $contactFieldType->id, + 'data' => 'john@doe.com', + ]); + + $vcard = new VCard([ + 'N' => ['John', 'Doe', '', '', ''], + 'EMAIL' => 'john@doe.com', + ]); + + $this->invokePrivateMethod($importJob, 'processSingleEntry', [$vcard]); + $this->assertJobSuccess($importJob); + $this->assertEquals( + 1, + $importJob->contacts_skipped + ); + } + + /** @test */ + public function skipping_entries_increments_counter_and_file_job_report() + { + $importJob = $this->createImportJob(); + + $this->invokePrivateMethod($importJob, 'skipEntry', [ + 'John Doe', + ]); + + $this->assertEquals( + 1, + $importJob->contacts_skipped + ); + + $this->assertDatabaseHas('import_job_reports', [ + 'account_id' => $importJob->account_id, + 'import_job_id' => $importJob->id, + ]); + } + + /** @test */ + public function it_files_an_import_job_report() + { + $importJob = $this->createImportJob(); + $vcard = new VCard([ + 'N' => ['John', 'Doe', '', '', ''], + 'EMAIL' => 'john@doe.com', + ]); + + $this->invokePrivateMethod($importJob, 'fileImportJobReport', [ + 'Doe John john@doe.com', + $importJob::VCARD_SKIPPED, + ]); + $this->assertDatabaseHas('import_job_reports', [ + 'account_id' => $importJob->account_id, + 'user_id' => $importJob->user_id, + 'import_job_id' => $importJob->id, + 'contact_information' => 'Doe John john@doe.com', + 'skipped' => 1, + 'skip_reason' => null, + ]); + + $this->invokePrivateMethod($importJob, 'fileImportJobReport', [ + 'Doe John john@doe.com', + $importJob::VCARD_IMPORTED, + ]); + $this->assertDatabaseHas('import_job_reports', [ + 'account_id' => $importJob->account_id, + 'user_id' => $importJob->user_id, + 'import_job_id' => $importJob->id, + 'contact_information' => 'Doe John john@doe.com', + 'skipped' => 0, + 'skip_reason' => null, + ]); + + $this->invokePrivateMethod($importJob, 'fileImportJobReport', [ + 'Doe John john@doe.com', + $importJob::VCARD_SKIPPED, + 'the reason why', + ]); + $this->assertDatabaseHas('import_job_reports', [ + 'account_id' => $importJob->account_id, + 'user_id' => $importJob->user_id, + 'import_job_id' => $importJob->id, + 'contact_information' => 'Doe John john@doe.com', + 'skipped' => 1, + 'skip_reason' => 'the reason why', + ]); + } + + private function createImportJob() + { + $account = factory(Account::class)->create([]); + $user = factory(User::class)->create([ + 'account_id' => $account->id, + ]); + + return factory(ImportJob::class)->create([ + 'account_id' => $account->id, + 'user_id' => $user->id, + 'failed' => false, + ]); + } + + private function assertJobSuccess($importJob) + { + $this->assertFalse($importJob->failed, 'Job has failed, reason: '.$importJob->failed_reason); + } +} diff --git a/tests/Unit/Models/JournalEntryTest.php b/tests/Unit/Models/JournalEntryTest.php new file mode 100644 index 0000000..71a41d8 --- /dev/null +++ b/tests/Unit/Models/JournalEntryTest.php @@ -0,0 +1,127 @@ +create([]); + $task = factory(JournalEntry::class)->create([ + 'account_id' => $account->id, + ]); + + $this->assertTrue($task->account()->exists()); + } + + /** @test */ + public function it_has_polymorphic_relations() + { + $activity = factory(Activity::class)->create(); + $journalEntry = JournalEntry::add($activity); + $activity->refresh(); + + $this->assertNotNull($journalEntry->journalable); + $this->assertEquals($activity->id, $journalEntry->journalable_id); + $this->assertNotNull($activity->journalEntry); + $this->assertEquals($journalEntry->id, $activity->journalEntry->id); + } + + /** @test */ + public function it_has_polymorphic_relations2() + { + $entry = factory(Entry::class)->create(); + $entry->date = '2018-01-01'; + $journalEntry = JournalEntry::add($entry); + $entry->refresh(); + + $this->assertNotNull($journalEntry->journalable); + $this->assertEquals($entry->id, $journalEntry->journalable_id); + $this->assertNotNull($entry->journalEntry); + $this->assertEquals($journalEntry->id, $entry->journalEntry->id); + } + + /** @test */ + public function get_add_adds_data_of_the_right_type() + { + $activity = factory(Activity::class)->create(); + $date = $activity->happened_at; + + $journalEntry = JournalEntry::add($activity); + + $this->assertDatabaseHas('journal_entries', [ + 'account_id' => $activity->account_id, + 'date' => $date, + 'journalable_id' => $activity->id, + 'journalable_type' => 'App\Models\Account\Activity', + ]); + } + + /** @test */ + public function get_object_data_returns_an_object() + { + $activity = factory(Activity::class)->create(); + + $journalEntry = JournalEntry::add($activity); + + $data = [ + 'type' => 'activity', + 'id' => $activity->id, + 'activity_type' => (! is_null($activity->type) ? $activity->type->name : null), + 'summary' => $activity->summary, + 'description' => $activity->description, + 'day' => $activity->happened_at->day, + 'day_name' => $activity->happened_at->format('D'), + 'month' => $activity->happened_at->month, + 'month_name' => strtoupper($activity->happened_at->format('M')), + 'year' => $activity->happened_at->year, + 'attendees' => $activity->getContactsForAPI(), + ]; + + $this->assertEquals( + $data, + $journalEntry->getObjectData() + ); + } + + /** @test */ + public function get_edit_journal_entry() + { + Carbon::setTestNow(Carbon::create(2017, 1, 1, 0, 0, 0)); + + $entry = factory(Entry::class)->create([ + 'title' => 'This is the title', + 'post' => 'this is a post', + ]); + $entry->date = '2017-01-01'; + $journalEntry = JournalEntry::add($entry); + + $this->assertDatabaseHas('journal_entries', [ + 'account_id' => $entry->account_id, + 'date' => '2017-01-01 00:00:00', + 'journalable_id' => $entry->id, + 'journalable_type' => 'App\Models\Journal\Entry', + ]); + + $entry->date = '2018-01-01'; + $journalEntry->edit($entry); + + $this->assertDatabaseHas('journal_entries', [ + 'account_id' => $entry->account_id, + 'date' => '2018-01-01 00:00:00', + 'journalable_id' => $entry->id, + 'journalable_type' => 'App\Models\Journal\Entry', + ]); + } +} diff --git a/tests/Unit/Models/LIfeEventTypeTest.php b/tests/Unit/Models/LIfeEventTypeTest.php new file mode 100644 index 0000000..a39f0c8 --- /dev/null +++ b/tests/Unit/Models/LIfeEventTypeTest.php @@ -0,0 +1,59 @@ +create([]); + + $this->assertTrue($lifeEventType->account()->exists()); + } + + /** @test */ + public function it_belongs_to_a_category() + { + $lifeEventType = factory(LifeEventType::class)->create([]); + + $this->assertTrue($lifeEventType->lifeEventCategory()->exists()); + } + + /** @test */ + public function it_has_many_life_events() + { + $account = factory(Account::class)->create([]); + $contact = factory(Contact::class)->create(['account_id' => $account->id]); + $lifeEventType = factory(LifeEventType::class)->create([]); + $lifeEvents = factory(LifeEvent::class, 2)->create([ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + 'life_event_type_id' => $lifeEventType->id, + ]); + + $this->assertTrue($lifeEventType->lifeEvents()->exists()); + } + + /** @test */ + public function it_gets_the_name_attribute() + { + $lifeEventType = factory(LifeEventType::class)->create([ + 'name' => 'Fake name', + ]); + + $this->assertEquals( + 'Fake name', + $lifeEventType->name + ); + } +} diff --git a/tests/Unit/Models/LifeEventCategoryTest.php b/tests/Unit/Models/LifeEventCategoryTest.php new file mode 100644 index 0000000..5c375db --- /dev/null +++ b/tests/Unit/Models/LifeEventCategoryTest.php @@ -0,0 +1,49 @@ +create([]); + $lifeEventCategory = factory(LifeEventCategory::class)->create([ + 'account_id' => $account->id, + ]); + + $this->assertTrue($lifeEventCategory->account()->exists()); + } + + /** @test */ + public function it_has_many_life_event_types() + { + $lifeEventCategory = factory(LifeEventCategory::class)->create(); + factory(LifeEventType::class)->create([ + 'life_event_category_id' => $lifeEventCategory->id, + ]); + + $this->assertTrue($lifeEventCategory->lifeEventTypes()->exists()); + } + + /** @test */ + public function it_gets_name_attribute() + { + $lifeEventCategory = factory(LifeEventCategory::class)->create([ + 'name' => 'Fake name', + ]); + + $this->assertEquals( + 'Fake name', + $lifeEventCategory->name + ); + } +} diff --git a/tests/Unit/Models/LifeEventTest.php b/tests/Unit/Models/LifeEventTest.php new file mode 100644 index 0000000..08e7620 --- /dev/null +++ b/tests/Unit/Models/LifeEventTest.php @@ -0,0 +1,76 @@ +create([]); + + $this->assertTrue($lifeEvent->account()->exists()); + } + + /** @test */ + public function it_belongs_to_a_contact() + { + $lifeEvent = factory(LifeEvent::class)->create([]); + + $this->assertTrue($lifeEvent->contact()->exists()); + } + + /** @test */ + public function it_belongs_to_a_type() + { + $lifeEvent = factory(LifeEvent::class)->create([]); + + $this->assertTrue($lifeEvent->lifeEventType()->exists()); + } + + /** @test */ + public function it_has_a_reminder() + { + $lifeEvent = factory(LifeEvent::class)->create([]); + $reminder = factory(Reminder::class)->create([ + 'account_id' => $lifeEvent->account_id, + ]); + $lifeEvent->reminder_id = $reminder->id; + $lifeEvent->save(); + + $this->assertTrue($lifeEvent->reminder()->exists()); + } + + /** @test */ + public function it_gets_the_name_attribute() + { + $lifeEvent = factory(LifeEvent::class)->create([ + 'name' => 'Fake name', + ]); + + $this->assertEquals( + 'Fake name', + $lifeEvent->name + ); + } + + /** @test */ + public function it_gets_the_note_attribute() + { + $lifeEvent = factory(LifeEvent::class)->create([ + 'note' => 'Fake note', + ]); + + $this->assertEquals( + 'Fake note', + $lifeEvent->note + ); + } +} diff --git a/tests/Unit/Models/MessageTest.php b/tests/Unit/Models/MessageTest.php new file mode 100644 index 0000000..cc76ce6 --- /dev/null +++ b/tests/Unit/Models/MessageTest.php @@ -0,0 +1,61 @@ +create([]); + $message = factory(Message::class)->create([ + 'account_id' => $account->id, + ]); + + $this->assertTrue($message->account()->exists()); + } + + /** @test */ + public function it_belongs_to_a_contact() + { + $contact = factory(Contact::class)->create(); + $message = factory(Message::class)->create([ + 'contact_id' => $contact->id, + ]); + + $this->assertTrue($message->contact()->exists()); + } + + /** @test */ + public function it_belongs_to_a_conversation() + { + $conversation = factory(Conversation::class)->create(); + $message = factory(Message::class)->create([ + 'conversation_id' => $conversation->id, + ]); + + $this->assertTrue($message->conversation()->exists()); + } + + /** @test */ + public function it_gets_the_content_attribute() + { + $message = factory(Message::class)->create([ + 'content' => 'This is a text', + ]); + + $this->assertEquals( + 'This is a text', + $message->content + ); + } +} diff --git a/tests/Unit/Models/NoteTest.php b/tests/Unit/Models/NoteTest.php new file mode 100644 index 0000000..b5a07ed --- /dev/null +++ b/tests/Unit/Models/NoteTest.php @@ -0,0 +1,96 @@ +create([]); + $contact = factory(Contact::class)->create(['account_id' => $account->id]); + $note = factory(Note::class)->create([ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + ]); + + $this->assertTrue($note->account()->exists()); + } + + /** @test */ + public function it_belongs_to_a_contact() + { + $contact = factory(Contact::class)->create([]); + $note = factory(Note::class)->create([ + 'contact_id' => $contact->id, + ]); + + $this->assertTrue($note->contact()->exists()); + } + + /** @test */ + public function it_filters_by_favorited_notes() + { + $note = factory(Note::class)->create(['is_favorited' => true]); + $note = factory(Note::class)->create(['is_favorited' => true]); + $note = factory(Note::class)->create(['is_favorited' => false]); + $note = factory(Note::class)->create(['is_favorited' => true]); + + $this->assertEquals( + 3, + Note::favorited()->count() + ); + } + + public function testGetBodyReturnsNullIfUndefined() + { + $note = new Note; + + $this->assertNull($note->getBody()); + } + + public function testGetBodyReturnsTextIfDefined() + { + $note = new Note; + $note->body = 'This is a text'; + + $this->assertEquals( + 'This is a text', + $note->getBody() + ); + } + + public function testGetCreatedAtReturnsAFormattedDate() + { + $note = new Note; + $note->created_at = '2017-01-22 17:56:03'; + + $this->assertEquals( + 'Jan 22, 2017', + $note->getCreatedAt() + ); + } + + public function testGetCreatedAtReturnsAString() + { + $note = new Note; + $note->created_at = '2017-01-22 17:56:03'; + + $this->assertIsString($note->getCreatedAt()); + } + + public function testGetContentReturnsAString() + { + $note = factory(Note::class)->make(); + + $this->assertIsString($note->getContent()); + } +} diff --git a/tests/Unit/Models/OccupationTest.php b/tests/Unit/Models/OccupationTest.php new file mode 100644 index 0000000..b480bea --- /dev/null +++ b/tests/Unit/Models/OccupationTest.php @@ -0,0 +1,46 @@ +create([]); + $occupation = factory(Occupation::class)->create([ + 'account_id' => $account->id, + ]); + + $this->assertTrue($occupation->account()->exists()); + } + + /** @test */ + public function it_belongs_to_a_contact() + { + $contact = factory(Contact::class)->create([]); + $occupation = factory(Occupation::class)->create([ + 'contact_id' => $contact->id, + ]); + $this->assertTrue($occupation->contact()->exists()); + } + + /** @test */ + public function it_belongs_to_a_company() + { + $company = factory(Company::class)->create([]); + $occupation = factory(Occupation::class)->create([ + 'company_id' => $company->id, + ]); + $this->assertTrue($occupation->company()->exists()); + } +} diff --git a/tests/Unit/Models/PetCategoryTest.php b/tests/Unit/Models/PetCategoryTest.php new file mode 100644 index 0000000..7a3bd1b --- /dev/null +++ b/tests/Unit/Models/PetCategoryTest.php @@ -0,0 +1,35 @@ +assertEquals( + 3, + $petCategory->common()->count() + ); + } + + /** @test */ + public function it_gets_pet_category_name() + { + $petCategory = new PetCategory; + $petCategory->name = 'Rgis'; + + $this->assertEquals( + 'Rgis', + $petCategory->name + ); + } +} diff --git a/tests/Unit/Models/PetTest.php b/tests/Unit/Models/PetTest.php new file mode 100644 index 0000000..ae4c16c --- /dev/null +++ b/tests/Unit/Models/PetTest.php @@ -0,0 +1,64 @@ +create([]); + $contact = factory(Contact::class)->create(['account_id' => $account->id]); + $pet = factory(Pet::class)->create([ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + ]); + + $this->assertTrue($pet->account()->exists()); + } + + /** @test */ + public function it_belongs_to_a_contact() + { + $contact = factory(Contact::class)->create([]); + $pet = factory(Pet::class)->create([ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + ]); + + $this->assertTrue($pet->contact()->exists()); + } + + /** @test */ + public function it_belongs_to_a_pet_category() + { + $petCategory = factory(PetCategory::class)->create([]); + $pet = factory(Pet::class)->create([ + 'pet_category_id' => $petCategory->id, + ]); + + $this->assertTrue($pet->petCategory()->exists()); + } + + /** @test */ + public function it_sets_name() + { + $pet = new Pet; + $this->assertNull($pet->name); + + $pet->name = 'henri'; + $this->assertEquals( + 'henri', + $pet->name + ); + } +} diff --git a/tests/Unit/Models/PhotoTest.php b/tests/Unit/Models/PhotoTest.php new file mode 100644 index 0000000..782f159 --- /dev/null +++ b/tests/Unit/Models/PhotoTest.php @@ -0,0 +1,47 @@ +create([]); + $photo = factory(Photo::class)->create([ + 'account_id' => $account->id, + ]); + $this->assertTrue($photo->account()->exists()); + } + + /** @test */ + public function it_belongs_to_many_contacts() + { + $contact = factory(Contact::class)->create(); + $photo = factory(Photo::class)->create(); + $contact->photos()->sync([$photo->id]); + + $photo = factory(Photo::class)->create(); + $contact->photos()->sync([$photo->id]); + + $this->assertTrue($photo->contacts()->exists()); + } + + /** @test */ + public function it_gets_the_url() + { + $photo = factory(Photo::class)->create(); + $this->assertEquals( + config('app.url').'/store/'.$photo->new_filename, + $photo->url() + ); + } +} diff --git a/tests/Unit/Models/PlaceTest.php b/tests/Unit/Models/PlaceTest.php new file mode 100644 index 0000000..4ba74ba --- /dev/null +++ b/tests/Unit/Models/PlaceTest.php @@ -0,0 +1,71 @@ +create([]); + $this->assertTrue($place->account()->exists()); + } + + /** @test */ + public function it_has_many_weathers() + { + $weather = factory(Weather::class)->create([]); + $this->assertTrue($weather->place->weathers()->exists()); + } + + /** @test */ + public function it_returns_the_full_address_as_a_string() + { + $place = factory(Place::class)->create([]); + $this->assertEquals( + '12 beverly hills 90210 United States', + $place->getAddressAsString() + ); + } + + /** @test */ + public function it_returns_country_name() + { + $place = factory(Place::class)->create([]); + $this->assertEquals( + 'United States', + $place->getCountryName() + ); + } + + /** @test */ + public function it_returns_a_link_to_google_maps() + { + $place = factory(Place::class)->create([]); + + $this->assertEquals( + 'https://www.google.com/maps/place/'.urlencode($place->getAddressAsString()), + $place->getGoogleMapAddress() + ); + } + + /** @test */ + public function it_returns_a_google_map_url_with_latitude_longitude() + { + $place = new Place; + $place->latitude = 24.197611; + $place->longitude = 120.780512; + + $this->assertEquals( + 'http://maps.google.com/maps?q=24.197611,120.780512', + $place->getGoogleMapsAddressWithLatitude() + ); + } +} diff --git a/tests/Unit/Models/RelationshipTest.php b/tests/Unit/Models/RelationshipTest.php new file mode 100644 index 0000000..ddb5584 --- /dev/null +++ b/tests/Unit/Models/RelationshipTest.php @@ -0,0 +1,146 @@ +create([]); + $relationship = factory(Relationship::class)->create([ + 'account_id' => $account->id, + ]); + + $this->assertTrue($relationship->account()->exists()); + } + + /** @test */ + public function it_belongs_to_a_contact() + { + $contact = factory(Contact::class)->create([]); + $relationship = factory(Relationship::class)->create([ + 'contact_is' => $contact->id, + ]); + + $this->assertTrue($relationship->contactIs()->exists()); + } + + /** @test */ + public function it_belongs_to_another_contact() + { + $contact = factory(Contact::class)->create([]); + $relationship = factory(Relationship::class)->create([ + 'of_contact' => $contact->id, + ]); + + $this->assertTrue($relationship->ofContact()->exists()); + } + + /** @test */ + public function it_belongs_to_a_relationship_type() + { + $account = factory(Account::class)->create([]); + $relationshipType = factory(RelationshipType::class)->create([ + 'account_id' => $account->id, + ]); + $relationship = factory(Relationship::class)->create([ + 'account_id' => $account->id, + 'relationship_type_id' => $relationshipType->id, + ]); + + $this->assertTrue($relationship->relationshipType()->exists()); + } + + /** @test */ + public function it_belongs_to_a_contact_through_with_contact_field() + { + $contact = factory(Contact::class)->create([]); + $relationship = factory(Relationship::class)->create([ + 'of_contact' => $contact->id, + ]); + + $this->assertTrue($relationship->ofContact()->exists()); + } + + /** @test */ + public function it_gets_the_reverse_relationship() + { + $account = factory(Account::class)->create(); + $contactA = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + $contactB = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + $relationshipTypeA = factory(RelationshipType::class)->create([ + 'account_id' => $account->id, + 'name' => 'uncle', + 'name_reverse_relationship' => 'nephew', + ]); + $relationshipA = factory(Relationship::class)->create([ + 'account_id' => $account->id, + 'relationship_type_id' => $relationshipTypeA->id, + 'contact_is' => $contactA->id, + 'of_contact' => $contactB->id, + ]); + $relationshipTypeB = factory(RelationshipType::class)->create([ + 'account_id' => $account->id, + 'name' => 'nephew', + 'name_reverse_relationship' => 'uncle', + ]); + $relationshipB = factory(Relationship::class)->create([ + 'account_id' => $account->id, + 'relationship_type_id' => $relationshipTypeB->id, + 'contact_is' => $contactB->id, + 'of_contact' => $contactA->id, + ]); + + $reverseRelationship = $relationshipA->reverseRelationship(); + $this->assertEquals( + $relationshipB->id, + $reverseRelationship->id + ); + + $reverseReverseRelationship = $reverseRelationship->reverseRelationship(); + $this->assertEquals( + $relationshipA->id, + $reverseReverseRelationship->id + ); + } + + /** @test */ + public function it_not_gets_the_reverse_relationship() + { + $account = factory(Account::class)->create(); + $contactA = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + $contactB = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + $relationshipTypeA = factory(RelationshipType::class)->create([ + 'account_id' => $account->id, + 'name' => 'uncle', + ]); + $relationshipA = factory(Relationship::class)->create([ + 'account_id' => $account->id, + 'relationship_type_id' => $relationshipTypeA->id, + 'contact_is' => $contactA->id, + 'of_contact' => $contactB->id, + ]); + + $reverseRelationship = $relationshipA->reverseRelationship(); + + $this->assertNull($reverseRelationship); + } +} diff --git a/tests/Unit/Models/RelationshipTypeGroupTest.php b/tests/Unit/Models/RelationshipTypeGroupTest.php new file mode 100644 index 0000000..3b16fdb --- /dev/null +++ b/tests/Unit/Models/RelationshipTypeGroupTest.php @@ -0,0 +1,24 @@ +create([]); + $relationshipType = factory(RelationshipType::class)->create([ + 'account_id' => $account->id, + ]); + + $this->assertTrue($relationshipType->account()->exists()); + } +} diff --git a/tests/Unit/Models/RelationshipTypeTest.php b/tests/Unit/Models/RelationshipTypeTest.php new file mode 100644 index 0000000..770974c --- /dev/null +++ b/tests/Unit/Models/RelationshipTypeTest.php @@ -0,0 +1,182 @@ +create([]); + $relationshipType = factory(RelationshipType::class)->create([ + 'account_id' => $account->id, + ]); + + $this->assertTrue($relationshipType->account()->exists()); + } + + /** @test */ + public function it_belongs_to_an_relationship_type_group() + { + $account = factory(Account::class)->create([]); + $relationshipTypeGroup = factory(RelationshipTypeGroup::class)->create([ + 'account_id' => $account->id, + ]); + + $this->assertTrue($relationshipTypeGroup->account()->exists()); + } + + /** @test */ + public function it_gets_the_masculine_short_name_of_the_relationship_type() + { + $account = factory(Account::class)->create([]); + $relationshipType = factory(RelationshipType::class)->create([ + 'account_id' => $account->id, + 'name' => 'uncle', + 'name_reverse_relationship' => 'nephew', + ]); + + $this->assertEquals( + 'uncle', + $relationshipType->getLocalizedName() + ); + } + + /** @test */ + public function it_gets_the_feminine_short_name_of_the_relationship_type() + { + $account = factory(Account::class)->create([]); + $relationshipType = factory(RelationshipType::class)->create([ + 'account_id' => $account->id, + 'name' => 'uncle', + 'name_reverse_relationship' => 'nephew', + ]); + + $this->assertEquals( + 'aunt', + $relationshipType->getLocalizedName(null, false, 'F') + ); + } + + /** @test */ + public function it_gets_the_masculine_name_of_the_relationship_type_with_the_name_of_the_contact() + { + $account = factory(Account::class)->create([]); + $relationshipType = factory(RelationshipType::class)->create([ + 'account_id' => $account->id, + 'name' => 'uncle', + 'name_reverse_relationship' => 'nephew', + ]); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + 'first_name' => 'Mark', + 'last_name' => 'Twain', + ]); + + $this->assertEquals( + 'Mark Twain’s uncle', + $relationshipType->getLocalizedName($contact, false, 'M') + ); + } + + /** @test */ + public function it_gets_the_feminine_name_of_the_relationship_type_with_the_name_of_the_contact() + { + $account = factory(Account::class)->create([]); + $relationshipType = factory(RelationshipType::class)->create([ + 'account_id' => $account->id, + 'name' => 'uncle', + 'name_reverse_relationship' => 'nephew', + ]); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + 'first_name' => 'Mark', + 'last_name' => 'Twain', + ]); + + $this->assertEquals( + 'Mark Twain’s aunt', + $relationshipType->getLocalizedName($contact, false, 'F') + ); + } + + /** @test */ + public function it_gets_both_names_of_the_relationship_type_with_the_name_of_the_contact_and_the_opposite_version() + { + $account = factory(Account::class)->create([]); + $relationshipType = factory(RelationshipType::class)->create([ + 'account_id' => $account->id, + 'name' => 'uncle', + 'name_reverse_relationship' => 'nephew', + ]); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + 'first_name' => 'Mark', + 'last_name' => 'Twain', + ]); + + $this->assertEquals( + 'Mark Twain’s uncle/aunt', + $relationshipType->getLocalizedName($contact, true) + ); + } + + /** @test */ + public function it_gets_only_one_name_of_the_relationship_type_if_name_and_name_reverse_are_similar() + { + $account = factory(Account::class)->create([]); + $relationshipType = factory(RelationshipType::class)->create([ + 'account_id' => $account->id, + 'name' => 'partner', + 'name_reverse_relationship' => 'partner', + ]); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + 'first_name' => 'Mark', + 'last_name' => 'Twain', + ]); + + $this->assertEquals( + 'Mark Twain’s significant other', + $relationshipType->getLocalizedName($contact, true) + ); + } + + /** @test */ + public function it_gets_the_reverse_relationship_type() + { + $account = factory(Account::class)->create([]); + $relationshipTypeA = factory(RelationshipType::class)->create([ + 'account_id' => $account->id, + 'name' => 'uncle', + 'name_reverse_relationship' => 'nephew', + ]); + $relationshipTypeB = factory(RelationshipType::class)->create([ + 'account_id' => $account->id, + 'name' => 'nephew', + 'name_reverse_relationship' => 'uncle', + ]); + + $reverseRelationshipType = $relationshipTypeA->reverseRelationshipType(); + + $this->assertEquals( + $relationshipTypeB->id, + $reverseRelationshipType->id + ); + + $reverseReverseRelationshipType = $reverseRelationshipType->reverseRelationshipType(); + $this->assertEquals( + $relationshipTypeA->id, + $reverseReverseRelationshipType->id + ); + } +} diff --git a/tests/Unit/Models/ReminderOutboxTest.php b/tests/Unit/Models/ReminderOutboxTest.php new file mode 100644 index 0000000..1d34b41 --- /dev/null +++ b/tests/Unit/Models/ReminderOutboxTest.php @@ -0,0 +1,33 @@ +create([]); + $this->assertTrue($reminderOutbox->account()->exists()); + } + + /** @test */ + public function it_belongs_to_a_reminder() + { + $reminderOutbox = factory(ReminderOutbox::class)->create([]); + $this->assertTrue($reminderOutbox->reminder()->exists()); + } + + /** @test */ + public function it_belongs_to_a_user() + { + $reminderOutbox = factory(ReminderOutbox::class)->create([]); + $this->assertTrue($reminderOutbox->user()->exists()); + } +} diff --git a/tests/Unit/Models/ReminderRuleTest.php b/tests/Unit/Models/ReminderRuleTest.php new file mode 100644 index 0000000..8d161ea --- /dev/null +++ b/tests/Unit/Models/ReminderRuleTest.php @@ -0,0 +1,45 @@ +create([]); + $reminderRule = factory(ReminderRule::class)->create(['account_id' => $account->id]); + + $this->assertTrue($reminderRule->account()->exists()); + } + + /** @test */ + public function it_gets_number_of_days_before_attribute() + { + $reminderRule = factory(ReminderRule::class)->create(['number_of_days_before' => '14']); + + $this->assertEquals( + 14, + $reminderRule->number_of_days_before + ); + } + + /** @test */ + public function it_sets_number_of_days_before_attribute() + { + $reminderRule = new ReminderRule; + $reminderRule->number_of_days_before = '14'; + + $this->assertEquals( + 14, + $reminderRule->number_of_days_before + ); + } +} diff --git a/tests/Unit/Models/ReminderTest.php b/tests/Unit/Models/ReminderTest.php new file mode 100644 index 0000000..4ce2f28 --- /dev/null +++ b/tests/Unit/Models/ReminderTest.php @@ -0,0 +1,254 @@ +create([]); + $reminder = factory(Reminder::class)->create([ + 'account_id' => $account->id, + ]); + + $this->assertTrue($reminder->account()->exists()); + } + + /** @test */ + public function it_belongs_to_a_contact() + { + $contact = factory(Contact::class)->create([]); + $reminder = factory(Reminder::class)->create([ + 'contact_id' => $contact->id, + ]); + + $this->assertTrue($reminder->contact()->exists()); + } + + /** @test */ + public function it_has_many_reminder_outbox() + { + $user = factory(User::class)->create([]); + $reminder = factory(Reminder::class)->create(['account_id' => $user->account_id]); + factory(ReminderOutbox::class, 3)->create([ + 'account_id' => $user->account_id, + 'reminder_id' => $reminder->id, + 'user_id' => $user->id, + ]); + + $this->assertTrue($reminder->reminderOutboxes()->exists()); + } + + /** @test */ + public function it_gets_the_title_attribute() + { + $reminder = factory(Reminder::class)->create([ + 'title' => 'Fake name', + ]); + + $this->assertEquals( + 'Fake name', + $reminder->title + ); + } + + /** @test */ + public function it_gets_the_description_attribute() + { + $reminder = factory(Reminder::class)->create([ + 'description' => 'Fake name', + ]); + + $this->assertEquals( + 'Fake name', + $reminder->description + ); + } + + /** @test */ + public function it_calculates_next_expected_date() + { + $timezone = 'UTC'; + $reminder = new Reminder; + $reminder->initial_date = '1980-01-01 10:10:10'; + $reminder->frequency_number = 1; + + Carbon::setTestNow(Carbon::create(1980, 1, 1)); + $reminder->frequency_type = 'week'; + $this->assertEquals( + '1980-01-08', + $reminder->calculateNextExpectedDate()->toDateString() + ); + + Carbon::setTestNow(Carbon::create(2017, 1, 1)); + // from 1980, incrementing one week will lead to Jan 03, 2017 + $reminder->frequency_type = 'week'; + $this->assertEquals( + '2017-01-03', + $reminder->calculateNextExpectedDate()->toDateString() + ); + + $reminder->frequency_type = 'month'; + $reminder->initial_date = '1980-01-01 10:10:10'; + $this->assertEquals( + '2017-02-01', + $reminder->calculateNextExpectedDate()->toDateString() + ); + + $reminder->frequency_type = 'year'; + $reminder->initial_date = '1980-01-01 10:10:10'; + $this->assertEquals( + '2018-01-01', + $reminder->calculateNextExpectedDate()->toDateString() + ); + + Carbon::setTestNow(Carbon::create(2017, 1, 1)); + $reminder->initial_date = '2016-12-25 10:10:10'; + $reminder->frequency_type = 'week'; + $this->assertEquals( + '2017-01-08', + $reminder->calculateNextExpectedDate()->toDateString() + ); + + Carbon::setTestNow(Carbon::create(2017, 1, 1)); + $reminder->initial_date = '2017-02-02 10:10:10'; + $reminder->frequency_type = 'week'; + $this->assertEquals( + '2017-02-02', + $reminder->calculateNextExpectedDate()->toDateString() + ); + } + + /** @test */ + public function it_calculates_next_expected_date_in_timezone() + { + config(['app.timezone' => 'Europe/Paris']); + + $reminder = new Reminder; + $reminder->initial_date = '1980-05-01'; + $reminder->frequency_type = 'year'; + $reminder->frequency_number = 1; + + Carbon::setTestNow(Carbon::create(2000, 4, 30, 21, 59, 59)); + $this->assertEquals( + '2000-05-01', + $reminder->calculateNextExpectedDateOnTimezone()->toDateString() + ); + + Carbon::setTestNow(Carbon::create(2000, 4, 30, 22, 00, 00)); + $this->assertEquals( + '2001-05-01', + $reminder->calculateNextExpectedDateOnTimezone()->toDateString() + ); + } + + /** @test */ + public function it_schedules_a_reminder_for_one_user() + { + Carbon::setTestNow(Carbon::create(2017, 2, 1)); + $user = factory(User::class)->create([]); + $reminder = factory(Reminder::class)->create([ + 'account_id' => $user->account_id, + 'initial_date' => '2017-01-01', + 'frequency_type' => 'year', + 'frequency_number' => 1, + ]); + + $reminder->schedule($user); + + $this->assertDatabaseHas('reminder_outbox', [ + 'reminder_id' => $reminder->id, + 'planned_date' => '2018-01-01', + 'nature' => 'reminder', + 'user_id' => $user->id, + ]); + } + + /** @test */ + public function scheduling_a_reminder_also_schedules_notifications_for_one_user() + { + Carbon::setTestNow(Carbon::create(2017, 2, 1)); + $user = factory(User::class)->create([]); + $reminder = factory(Reminder::class)->create([ + 'account_id' => $user->account_id, + 'initial_date' => '2017-01-01', + 'frequency_type' => 'year', + 'frequency_number' => 1, + ]); + $reminderRule = factory(ReminderRule::class)->create([ + 'account_id' => $reminder->account_id, + 'number_of_days_before' => 30, + 'active' => 1, + ]); + $reminderRule = factory(ReminderRule::class)->create([ + 'account_id' => $reminder->account_id, + 'number_of_days_before' => 7, + 'active' => 1, + ]); + + $reminder->schedule($user); + + $this->assertDatabaseHas('reminder_outbox', [ + 'reminder_id' => $reminder->id, + 'planned_date' => '2017-12-02', + 'nature' => 'notification', + 'notification_number_days_before' => 30, + ]); + + $this->assertDatabaseHas('reminder_outbox', [ + 'reminder_id' => $reminder->id, + 'planned_date' => '2017-12-25', + 'nature' => 'notification', + 'notification_number_days_before' => 7, + 'user_id' => $user->id, + ]); + + $this->assertEquals( + 3, + $reminder->reminderOutboxes()->count() + ); + } + + /** @test */ + public function it_doesnt_schedule_a_notification_if_date_is_too_close_to_present_date() + { + Carbon::setTestNow(Carbon::create(2017, 2, 1)); + $user = factory(User::class)->create([]); + $reminder = factory(Reminder::class)->create([ + 'account_id' => $user->account_id, + 'initial_date' => '2017-01-01', + 'frequency_type' => 'week', + 'frequency_number' => 1, + ]); + $reminderRule = factory(ReminderRule::class)->create([ + 'account_id' => $reminder->account_id, + 'number_of_days_before' => 7, + 'active' => 1, + ]); + + $reminder->schedule($user); + + $this->assertDatabaseMissing('reminder_outbox', [ + 'reminder_id' => $reminder->id, + 'nature' => 'notification', + ]); + + $this->assertEquals( + 1, + $reminder->reminderOutboxes()->count() + ); + } +} diff --git a/tests/Unit/Models/SpecialDateTest.php b/tests/Unit/Models/SpecialDateTest.php new file mode 100644 index 0000000..0f52932 --- /dev/null +++ b/tests/Unit/Models/SpecialDateTest.php @@ -0,0 +1,196 @@ +create([]); + $specialDate = factory(SpecialDate::class)->create([ + 'account_id' => $account->id, + ]); + + $this->assertTrue($specialDate->account()->exists()); + } + + /** @test */ + public function it_belongs_to_a_contact() + { + $account = factory(Account::class)->create([]); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + $specialDate = factory(SpecialDate::class)->create([ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + ]); + + $this->assertTrue($specialDate->contact()->exists()); + } + + /** @test */ + public function get_age_returns_null_if_no_date_is_set() + { + $specialDate = new SpecialDate; + $this->assertNull($specialDate->getAge()); + } + + /** @test */ + public function get_age_returns_null_if_year_is_unknown() + { + $specialDate = factory(SpecialDate::class)->make(); + $specialDate->is_year_unknown = 1; + $specialDate->save(); + + $this->assertNull($specialDate->getAge()); + } + + /** @test */ + public function get_age_returns_age() + { + Carbon::setTestNow(Carbon::create(2020, 2, 17, 17, 0, 0)); + + $specialDate = factory(SpecialDate::class)->make(); + $specialDate->is_year_unknown = 0; + $specialDate->date = now()->subYears(5); + $specialDate->save(); + + $this->assertEquals( + 5, + $specialDate->getAge() + ); + } + + /** @test */ + public function create_from_age_sets_the_right_date() + { + $specialDate = factory(SpecialDate::class)->make(); + + $specialDate->createFromAge(100); + + $this->assertTrue( + $specialDate->is_age_based + ); + + $this->assertEquals( + 1, + $specialDate->date->day + ); + + $this->assertEquals( + 1, + $specialDate->date->month + ); + } + + /** @test */ + public function create_from_date_creates_an_approximate_date() + { + $specialDate = factory(SpecialDate::class)->make(); + + $specialDate->createFromDate(0, 10, 10); + + $this->assertTrue( + $specialDate->is_year_unknown + ); + + $this->assertEquals( + 10, + $specialDate->date->day + ); + + $this->assertEquals( + 10, + $specialDate->date->month + ); + + $this->assertEquals( + now()->year, + $specialDate->date->year + ); + } + + /** @test */ + public function create_from_date_creates_an_exact_date() + { + $specialDate = factory(SpecialDate::class)->make(); + + $specialDate->createFromDate(2019, 10, 10); + + $this->assertFalse( + $specialDate->is_year_unknown + ); + + $this->assertEquals( + 10, + $specialDate->date->day + ); + + $this->assertEquals( + 10, + $specialDate->date->month + ); + + $this->assertEquals( + 2019, + $specialDate->date->year + ); + } + + /** @test */ + public function set_contact_sets_the_contact_information() + { + $specialDate = factory(SpecialDate::class)->make(); + + $contact = factory(Contact::class)->create(); + + $specialDate->setToContact($contact); + + $this->assertEquals( + $contact->account_id, + $specialDate->account_id + ); + + $this->assertEquals( + $contact->id, + $specialDate->contact_id + ); + } + + /** @test */ + public function to_short_string_returns_date_with_year() + { + $specialDate = new SpecialDate; + $specialDate->is_year_unknown = false; + $specialDate->date = Carbon::create(2001, 5, 21); + + $this->assertEquals( + 'May 21, 2001', + $specialDate->toShortString() + ); + } + + /** @test */ + public function to_short_string_returns_date_without_year() + { + $specialDate = new SpecialDate; + $specialDate->is_year_unknown = true; + $specialDate->date = Carbon::create(2001, 5, 21); + + $this->assertEquals( + 'May 21', + $specialDate->toShortString() + ); + } +} diff --git a/tests/Unit/Models/TagTest.php b/tests/Unit/Models/TagTest.php new file mode 100644 index 0000000..b465db8 --- /dev/null +++ b/tests/Unit/Models/TagTest.php @@ -0,0 +1,41 @@ +create([]); + $contact = factory(Contact::class)->create(['account_id' => $account->id]); + $tag = factory(Tag::class)->create([ + 'account_id' => $account->id, + ]); + + $this->assertTrue($tag->account()->exists()); + } + + /** @test */ + public function it_belongs_to_many_contacts() + { + $account = factory(Account::class)->create([]); + $contact = factory(Contact::class)->create(['account_id' => $account->id]); + $tag = factory(Tag::class)->create(['account_id' => $account->id]); + $contact->tags()->sync([$tag->id => ['account_id' => $account->id]]); + + $contact = factory(Contact::class)->create(['account_id' => $account->id]); + $tag = factory(Tag::class)->create(['account_id' => $account->id]); + $contact->tags()->sync([$tag->id => ['account_id' => $account->id]]); + + $this->assertTrue($tag->contacts()->exists()); + } +} diff --git a/tests/Unit/Models/TaskTest.php b/tests/Unit/Models/TaskTest.php new file mode 100644 index 0000000..cb8e920 --- /dev/null +++ b/tests/Unit/Models/TaskTest.php @@ -0,0 +1,68 @@ +create([]); + $contact = factory(Contact::class)->create(['account_id' => $account->id]); + $task = factory(Task::class)->create([ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + ]); + + $this->assertTrue($task->account()->exists()); + } + + /** @test */ + public function it_belongs_to_a_contact() + { + $account = factory(Account::class)->create([]); + $contact = factory(Contact::class)->create(['account_id' => $account->id]); + $task = factory(Task::class)->create([ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + ]); + + $this->assertTrue($task->contact()->exists()); + } + + /** @test */ + public function it_filters_by_completed_items() + { + $task = factory(Task::class)->create(['completed' => true]); + $task = factory(Task::class)->create(['completed' => true]); + $task = factory(Task::class)->create(['completed' => false]); + $task = factory(Task::class)->create(['completed' => true]); + + $this->assertEquals( + 3, + Task::completed()->count() + ); + } + + /** @test */ + public function it_filters_by_incomplete_items() + { + $task = factory(Task::class)->create(['completed' => false]); + $task = factory(Task::class)->create(['completed' => true]); + $task = factory(Task::class)->create(['completed' => true]); + $task = factory(Task::class)->create(['completed' => true]); + + $this->assertEquals( + 1, + Task::inProgress()->count() + ); + } +} diff --git a/tests/Unit/Models/TermTest.php b/tests/Unit/Models/TermTest.php new file mode 100644 index 0000000..0fb26de --- /dev/null +++ b/tests/Unit/Models/TermTest.php @@ -0,0 +1,29 @@ +create([]); + $user = factory(User::class)->create(['account_id' => $account->id]); + $term = factory(Term::class)->create([]); + $term->users()->sync([$user->id => ['account_id' => $account->id]]); + + $user = factory(User::class)->create(['account_id' => $account->id]); + $term = factory(Term::class)->create([]); + $term->users()->sync([$user->id => ['account_id' => $account->id]]); + + $this->assertTrue($term->users()->exists()); + } +} diff --git a/tests/Unit/Models/UserTest.php b/tests/Unit/Models/UserTest.php new file mode 100644 index 0000000..0c02a31 --- /dev/null +++ b/tests/Unit/Models/UserTest.php @@ -0,0 +1,453 @@ +execute([ + 'account_id' => $account_id, + 'first_name' => $first_name, + 'last_name' => $last_name, + 'email' => $email, + 'password' => $password, + ]); + } + + /** @test */ + public function it_belongs_to_account() + { + $account = factory(Account::class)->create([]); + $user = factory(User::class)->create(['account_id' => $account->id]); + + $this->assertTrue($user->account()->exists()); + } + + /** @test */ + public function it_belongs_to_many_terms() + { + $account = factory(Account::class)->create([]); + $user = factory(User::class)->create(['account_id' => $account->id]); + $term = factory(Term::class)->create(); + $user->terms()->sync([$term->id => ['account_id' => $account->id]]); + + $user = factory(User::class)->create(['account_id' => $account->id]); + $term = factory(Term::class)->create(); + $user->terms()->sync([$term->id => ['account_id' => $account->id]]); + + $this->assertTrue($user->terms()->exists()); + } + + /** @test */ + public function name_accessor_returns_name_in_the_user_preferred_way() + { + $user = new User; + $user->first_name = 'John'; + $user->last_name = 'Doe'; + $user->name_order = 'firstname_lastname'; + + $this->assertEquals( + $user->name, + 'John Doe' + ); + + $user->name_order = 'lastname_firstname'; + + $this->assertEquals( + $user->name, + 'Doe John' + ); + } + + /** @test */ + public function it_gets_2fa_secret_attribute() + { + $user = new User; + + $this->assertNull($user->getGoogle2faSecretAttribute(null)); + + $string = 'pass1234'; + + $this->assertEquals( + $string, + $user->getGoogle2faSecretAttribute(encrypt($string)) + ); + } + + /** @test */ + public function it_gets_fluid_layout() + { + $user = new User; + $user->fluid_container = true; + + $this->assertEquals( + 'container-fluid', + $user->getFluidLayout() + ); + + $user->fluid_container = false; + + $this->assertEquals( + 'container', + $user->getFluidLayout() + ); + } + + /** @test */ + public function it_gets_the_locale() + { + $user = new User; + $user->locale = 'en'; + + $this->assertEquals( + 'en', + $user->locale + ); + } + + /** @test */ + public function user_should_not_be_reminded_because_dates_are_different() + { + Carbon::setTestNow(Carbon::create(2017, 1, 1)); + $account = factory(Account::class)->create(); + $user = factory(User::class)->create(['account_id' => $account->id]); + $reminder = factory(Reminder::class)->create([ + 'account_id' => $account->id, + 'initial_date' => '2018-02-01', + ]); + + $this->assertFalse($user->isTheRightTimeToBeReminded($reminder->initial_date)); + } + + /** @test */ + public function user_should_not_be_reminded_because_hours_are_different() + { + Carbon::setTestNow(Carbon::create(2017, 1, 1, 7, 0, 0)); + $account = factory(Account::class)->create(['default_time_reminder_is_sent' => '08:00']); + $user = factory(User::class)->create(['account_id' => $account->id]); + $reminder = factory(Reminder::class)->create([ + 'account_id' => $account->id, + 'initial_date' => '2017-01-01', + ]); + + $this->assertFalse($user->isTheRightTimeToBeReminded($reminder->initial_date)); + } + + /** @test */ + public function user_should_not_be_reminded_because_timezone_is_different() + { + Carbon::setTestNow(Carbon::create(2017, 1, 1, 7, 0, 0)); + $account = factory(Account::class)->create(['default_time_reminder_is_sent' => '07:00']); + $user = factory(User::class)->create([ + 'account_id' => $account->id, + 'timezone' => 'Europe/Paris', + ]); + $reminder = factory(Reminder::class)->create([ + 'account_id' => $account->id, + 'initial_date' => '2017-01-01', + ]); + + $this->assertFalse($user->isTheRightTimeToBeReminded($reminder->initial_date)); + } + + /** @test */ + public function user_should_be_reminded() + { + Carbon::setTestNow(Carbon::create(2017, 1, 1, 7, 32, 12)); + $account = factory(Account::class)->create(['default_time_reminder_is_sent' => '07:00']); + $user = factory(User::class)->create(['account_id' => $account->id]); + $reminder = factory(Reminder::class)->create([ + 'account_id' => $account->id, + 'initial_date' => '2017-01-01', + ]); + + $this->assertTrue($user->isTheRightTimeToBeReminded($reminder->initial_date)); + } + + /** @test */ + public function it_creates_default_user_en() + { + App::setLocale('en'); + + $account = factory(Account::class)->create([]); + $user = $this->createUser($account->id, 'John', 'Doe', 'john@doe.com', 'password'); + $currency = Currency::where('iso', 'USD')->first(); + + $this->assertDatabaseHas('users', [ + 'id' => $user->id, + 'account_id' => $account->id, + 'first_name' => 'John', + 'last_name' => 'Doe', + 'email' => 'john@doe.com', + 'locale' => 'en', + 'timezone' => 'America/Chicago', + 'currency_id' => $currency->id, + 'temperature_scale' => 'fahrenheit', + ]); + } + + /** @test */ + public function it_creates_default_user_fr() + { + App::setLocale('fr'); + + $account = factory(Account::class)->create([]); + $user = $this->createUser($account->id, 'John', 'Doe', 'john@doe.com', 'password'); + $currency = Currency::where('iso', 'EUR')->first(); + + $this->assertDatabaseHas('users', [ + 'id' => $user->id, + 'account_id' => $account->id, + 'first_name' => 'John', + 'last_name' => 'Doe', + 'email' => 'john@doe.com', + 'locale' => 'fr', + 'timezone' => 'Europe/Paris', + 'currency_id' => $currency->id, + 'temperature_scale' => 'celsius', + ]); + } + + /** @test */ + public function it_creates_default_user_cs() + { + App::setLocale('cs'); + + $account = factory(Account::class)->create([]); + $user = $this->createUser($account->id, 'John', 'Doe', 'john@doe.com', 'password'); + $currency = Currency::where('iso', 'CZK')->first(); + + $this->assertDatabaseHas('users', [ + 'id' => $user->id, + 'account_id' => $account->id, + 'first_name' => 'John', + 'last_name' => 'Doe', + 'email' => 'john@doe.com', + 'locale' => 'cs', + 'timezone' => 'Europe/Prague', + 'currency_id' => $currency->id, + 'temperature_scale' => 'celsius', + ]); + } + + /** @test */ + public function it_creates_default_user_de() + { + App::setLocale('de'); + + $account = factory(Account::class)->create([]); + $user = $this->createUser($account->id, 'John', 'Doe', 'john@doe.com', 'password'); + $currency = Currency::where('iso', 'EUR')->first(); + + $this->assertDatabaseHas('users', [ + 'id' => $user->id, + 'account_id' => $account->id, + 'first_name' => 'John', + 'last_name' => 'Doe', + 'email' => 'john@doe.com', + 'locale' => 'de', + 'timezone' => 'Europe/Berlin', + 'currency_id' => $currency->id, + 'temperature_scale' => 'celsius', + ]); + } + + /** @test */ + public function it_creates_default_user_es() + { + App::setLocale('es'); + + $account = factory(Account::class)->create([]); + $user = $this->createUser($account->id, 'John', 'Doe', 'john@doe.com', 'password'); + $currency = Currency::where('iso', 'EUR')->first(); + + $this->assertDatabaseHas('users', [ + 'id' => $user->id, + 'account_id' => $account->id, + 'first_name' => 'John', + 'last_name' => 'Doe', + 'email' => 'john@doe.com', + 'locale' => 'es', + 'timezone' => 'Europe/Madrid', + 'currency_id' => $currency->id, + 'temperature_scale' => 'celsius', + ]); + } + + /** @test */ + public function it_creates_default_user_he() + { + App::setLocale('he'); + + $account = factory(Account::class)->create([]); + $user = $this->createUser($account->id, 'John', 'Doe', 'john@doe.com', 'password'); + $currency = Currency::where('iso', 'ILS')->first(); + + $this->assertDatabaseHas('users', [ + 'id' => $user->id, + 'account_id' => $account->id, + 'first_name' => 'John', + 'last_name' => 'Doe', + 'email' => 'john@doe.com', + 'locale' => 'he', + 'timezone' => 'Asia/Jerusalem', + 'currency_id' => $currency->id, + 'temperature_scale' => 'celsius', + ]); + } + + /** @test */ + public function it_creates_default_user_it() + { + App::setLocale('it'); + + $account = factory(Account::class)->create([]); + $user = $this->createUser($account->id, 'John', 'Doe', 'john@doe.com', 'password'); + $currency = Currency::where('iso', 'EUR')->first(); + + $this->assertDatabaseHas('users', [ + 'id' => $user->id, + 'account_id' => $account->id, + 'first_name' => 'John', + 'last_name' => 'Doe', + 'email' => 'john@doe.com', + 'locale' => 'it', + 'timezone' => 'Europe/Rome', + 'currency_id' => $currency->id, + 'temperature_scale' => 'celsius', + ]); + } + + /** @test */ + public function it_creates_default_user_nl() + { + App::setLocale('nl'); + + $account = factory(Account::class)->create([]); + $user = $this->createUser($account->id, 'John', 'Doe', 'john@doe.com', 'password'); + $currency = Currency::where('iso', 'EUR')->first(); + + $this->assertDatabaseHas('users', [ + 'id' => $user->id, + 'account_id' => $account->id, + 'first_name' => 'John', + 'last_name' => 'Doe', + 'email' => 'john@doe.com', + 'locale' => 'nl', + 'timezone' => 'Europe/Amsterdam', + 'currency_id' => $currency->id, + 'temperature_scale' => 'celsius', + ]); + } + + /** @test */ + public function it_creates_default_user_pt() + { + App::setLocale('pt'); + + $account = factory(Account::class)->create([]); + $user = $this->createUser($account->id, 'John', 'Doe', 'john@doe.com', 'password'); + $currency = Currency::where('iso', 'EUR')->first(); + + $this->assertDatabaseHas('users', [ + 'id' => $user->id, + 'account_id' => $account->id, + 'first_name' => 'John', + 'last_name' => 'Doe', + 'email' => 'john@doe.com', + 'locale' => 'pt', + 'timezone' => 'Europe/Lisbon', + 'currency_id' => $currency->id, + 'temperature_scale' => 'celsius', + ]); + } + + /** @test */ + public function it_creates_default_user_ru() + { + App::setLocale('ru'); + + $account = factory(Account::class)->create([]); + $user = $this->createUser($account->id, 'John', 'Doe', 'john@doe.com', 'password'); + $currency = Currency::where('iso', 'RUB')->first(); + + $this->assertDatabaseHas('users', [ + 'id' => $user->id, + 'account_id' => $account->id, + 'first_name' => 'John', + 'last_name' => 'Doe', + 'email' => 'john@doe.com', + 'locale' => 'ru', + 'timezone' => 'Europe/Moscow', + 'currency_id' => $currency->id, + 'temperature_scale' => 'celsius', + ]); + } + + /** @test */ + public function it_creates_default_user_zh() + { + App::setLocale('zh'); + + $account = factory(Account::class)->create([]); + $user = $this->createUser($account->id, 'John', 'Doe', 'john@doe.com', 'password'); + $currency = Currency::where('iso', 'CNY')->first(); + + $this->assertDatabaseHas('users', [ + 'id' => $user->id, + 'account_id' => $account->id, + 'first_name' => 'John', + 'last_name' => 'Doe', + 'email' => 'john@doe.com', + 'locale' => 'zh', + 'timezone' => 'Asia/Shanghai', + 'currency_id' => $currency->id, + 'temperature_scale' => 'celsius', + ]); + } + + /** @test */ + public function it_sends_a_verification_email() + { + config(['monica.signup_double_optin' => true]); + Notification::fake(); + + // Creating a fake account + factory(Account::class)->create(); + + $user = factory(User::class)->create([]); + $user->sendEmailVerificationNotification(); + + Notification::assertSentTo( + [$user], VerifyEmail::class + ); + } + + /** @test */ + public function it_doesnt_send_a_verification_email_if_the_double_optin_is_disabled_at_the_instance_level() + { + config(['monica.signup_double_optin' => false]); + Notification::fake(); + + $user = factory(User::class)->create([]); + $user->sendEmailVerificationNotification(); + + Notification::assertNothingSent(); + } +} diff --git a/tests/Unit/Models/WeatherTest.php b/tests/Unit/Models/WeatherTest.php new file mode 100644 index 0000000..b35ddcc --- /dev/null +++ b/tests/Unit/Models/WeatherTest.php @@ -0,0 +1,96 @@ +create([]); + $weather = factory(Weather::class)->create([ + 'account_id' => $account->id, + ]); + $this->assertTrue($weather->account()->exists()); + } + + /** @test */ + public function it_belongs_to_a_place() + { + $weather = factory(Weather::class)->create([]); + $this->assertTrue($weather->place()->exists()); + } + + /** @test */ + public function it_gets_current_temperature() + { + $weather = factory(Weather::class)->create(); + + $this->assertEquals( + 13, + $weather->temperature() + ); + } + + /** @test */ + public function it_gets_current_temperature_in_celsius() + { + $weather = factory(Weather::class)->create(); + + $this->assertEquals( + 13, + $weather->temperature('celsius') + ); + } + + /** @test */ + public function it_gets_current_temperature_in_fahrenheit() + { + $weather = factory(Weather::class)->create(); + + $this->assertEquals( + 55.4, + $weather->temperature('fahrenheit') + ); + } + + /** @test */ + public function it_gets_current_summary() + { + $weather = factory(Weather::class)->create(); + + $this->assertEquals( + 'Partly cloudy', + $weather->summary + ); + } + + /** @test */ + public function it_gets_current_code() + { + $weather = factory(Weather::class)->create(); + + $this->assertEquals( + 'partly-cloudy-night', + $weather->summary_code + ); + } + + /** @test */ + public function it_gets_weather_emoji() + { + $weather = factory(Weather::class)->create(); + + $this->assertEquals( + '🎑', + $weather->emoji + ); + } +} diff --git a/tests/Unit/Services/Account/Activity/ActivityStatisticServiceTest.php b/tests/Unit/Services/Account/Activity/ActivityStatisticServiceTest.php new file mode 100644 index 0000000..691ef00 --- /dev/null +++ b/tests/Unit/Services/Account/Activity/ActivityStatisticServiceTest.php @@ -0,0 +1,249 @@ +create(); + + for ($i = 0; $i <= 2; $i++) { + $activity = factory(Activity::class)->create([ + 'happened_at' => now()->subMonth(), + 'account_id' => $contact->account_id, + ]); + $contact->activities()->attach($activity, ['account_id' => $contact->account_id]); + } + + $this->assertCount( + 3, + $service->activitiesWithContactInTimeRange($contact, now()->subMonths(2), now()) + ); + + $this->assertInstanceOf( + Activity::class, + $service->activitiesWithContactInTimeRange($contact, now()->subMonths(2), now())[1] + ); + } + + /** @test */ + public function it_gets_an_empty_list_of_activities() + { + $service = new ActivityStatisticService; + $contact = factory(Contact::class)->create(); + + for ($i = 0; $i <= 2; $i++) { + $activity = factory(Activity::class)->create([ + 'happened_at' => now()->subYears(2), + 'account_id' => $contact->account_id, + ]); + $contact->activities()->attach($activity, ['account_id' => $contact->account_id]); + } + + $this->assertCount( + 0, + $service->activitiesWithContactInTimeRange($contact, now()->subMonths(2), now()) + ); + } + + /** @test */ + public function it_gets_a_list_of_unique_activity_types() + { + $service = new ActivityStatisticService; + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + + // creation of 3 activities with a given activity type + $activityType = factory(ActivityType::class)->create([ + 'account_id' => $account->id, + ]); + + for ($i = 0; $i <= 2; $i++) { + $activity = factory(Activity::class)->create([ + 'happened_at' => now(), + 'account_id' => $account->id, + 'activity_type_id' => $activityType->id, + ]); + $contact->activities()->attach($activity, ['account_id' => $contact->account_id]); + } + + // creation of 1 activity with a given activity type + $activityType = factory(ActivityType::class)->create([ + 'account_id' => $account->id, + ]); + + $activity = factory(Activity::class)->create([ + 'happened_at' => now(), + 'account_id' => $account->id, + 'activity_type_id' => $activityType->id, + ]); + $contact->activities()->attach($activity, ['account_id' => $contact->account_id]); + + // here we should have 2 uniques activity types, one with 3 and the other with 1 occurence + $response = $service->uniqueActivityTypesInTimeRange($contact, now()->subMonths(2), now()); + + $this->assertCount( + 2, + $response + ); + + $this->assertInstanceOf( + ActivityType::class, + $response[0]['object'] + ); + + $this->assertEquals( + 3, + $response[0]['occurences'] + ); + + $this->assertInstanceOf( + ActivityType::class, + $response[1]['object'] + ); + + $this->assertEquals( + 1, + $response[1]['occurences'] + ); + } + + /** @test */ + public function it_gets_the_breakdown_of_activities_per_year() + { + $service = new ActivityStatisticService; + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + + for ($i = 0; $i <= 2; $i++) { + $activity = factory(Activity::class)->create([ + 'happened_at' => now()->subYears(2), + 'account_id' => $account->id, + ]); + $contact->activities()->attach($activity, ['account_id' => $contact->account_id]); + } + + for ($i = 0; $i <= 5; $i++) { + $activity = factory(Activity::class)->create([ + 'happened_at' => now(), + 'account_id' => $account->id, + ]); + $contact->activities()->attach($activity, ['account_id' => $contact->account_id]); + } + + $activityStatistic = $contact->activityStatistics()->make(); + $activityStatistic->account_id = $contact->account_id; + $activityStatistic->contact_id = $contact->id; + $activityStatistic->year = now()->year; + $activityStatistic->count = 6; + $activityStatistic->save(); + + $activityStatistic = $contact->activityStatistics()->make(); + $activityStatistic->account_id = $contact->account_id; + $activityStatistic->contact_id = $contact->id; + $activityStatistic->year = now()->subYears(2)->year; + $activityStatistic->count = 3; + $activityStatistic->save(); + + $response = $service->activitiesPerYearWithContact($contact); + + $this->assertCount( + 2, + $response + ); + + $this->assertEquals( + 6, + $response[0]->count + ); + + $this->assertEquals( + 3, + $response[1]->count + ); + + $this->assertInstanceOf( + ActivityStatistic::class, + $response[0] + ); + } + + /** @test */ + public function it_gets_a_list_of_activities_per_month_for_given_year() + { + $service = new ActivityStatisticService; + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + + Carbon::setTestNow(Carbon::create(2017, 1, 1)); + + for ($i = 0; $i <= 2; $i++) { + $activity = factory(Activity::class)->create([ + 'happened_at' => '2017-01-02', + 'account_id' => $account->id, + ]); + $contact->activities()->attach($activity, ['account_id' => $contact->account_id]); + } + + for ($i = 0; $i <= 5; $i++) { + $activity = factory(Activity::class)->create([ + 'happened_at' => '2017-02-01', + 'account_id' => $account->id, + ]); + $contact->activities()->attach($activity, ['account_id' => $contact->account_id]); + } + + $response = $service->activitiesPerMonthForYear($contact, 2017); + + $this->assertCount( + 12, + $response + ); + + $this->assertEquals( + 1, + $response[0]['month'] + ); + + $this->assertEquals( + 3, + $response[0]['occurences'] + ); + + $this->assertEquals( + 2, + $response[1]['month'] + ); + + $this->assertEquals( + 6, + $response[1]['occurences'] + ); + + $this->assertInstanceOf( + Activity::class, + $response[1]['activities'][0] + ); + } +} diff --git a/tests/Unit/Services/Account/Activity/ActivityType/CreateActivityTypeTest.php b/tests/Unit/Services/Account/Activity/ActivityType/CreateActivityTypeTest.php new file mode 100644 index 0000000..2feb1eb --- /dev/null +++ b/tests/Unit/Services/Account/Activity/ActivityType/CreateActivityTypeTest.php @@ -0,0 +1,76 @@ +create([]); + $activityTypeCategory = factory(ActivityTypeCategory::class)->create([ + 'account_id' => $account->id, + ]); + + $request = [ + 'account_id' => $account->id, + 'activity_type_category_id' => $activityTypeCategory->id, + 'name' => 'central perk', + 'translation_key' => 'central_perk', + ]; + + $activityType = app(CreateActivityType::class)->execute($request); + + $this->assertDatabaseHas('activity_types', [ + 'id' => $activityType->id, + 'account_id' => $account->id, + 'activity_type_category_id' => $activityTypeCategory->id, + 'name' => 'central perk', + 'translation_key' => 'central_perk', + ]); + + $this->assertInstanceOf( + ActivityType::class, + $activityType + ); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $request = [ + 'name' => '199 Lafayette Street', + ]; + + $this->expectException(ValidationException::class); + app(CreateActivityType::class)->execute($request); + } + + /** @test */ + public function it_fails_if_activity_type_category_is_not_linked_to_account() + { + $account = factory(Account::class)->create([]); + $activityTypeCategory = factory(ActivityTypeCategory::class)->create([]); + + $request = [ + 'account_id' => $account->id, + 'activity_type_category_id' => $activityTypeCategory->id, + 'name' => 'central perk', + 'translation_key' => 'central_perk', + ]; + + $this->expectException(ModelNotFoundException::class); + app(CreateActivityType::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Account/Activity/ActivityType/DestroyActivityTypeTest.php b/tests/Unit/Services/Account/Activity/ActivityType/DestroyActivityTypeTest.php new file mode 100644 index 0000000..cd5ab0d --- /dev/null +++ b/tests/Unit/Services/Account/Activity/ActivityType/DestroyActivityTypeTest.php @@ -0,0 +1,60 @@ +create([]); + + $request = [ + 'account_id' => $activityType->account_id, + 'activity_type_id' => $activityType->id, + ]; + + app(DestroyActivityType::class)->execute($request); + + $this->assertDatabaseMissing('activity_types', [ + 'id' => $activityType->id, + ]); + } + + /** @test */ + public function it_throws_an_exception_if_account_is_not_linked_to_activity_type() + { + $account = factory(Account::class)->create([]); + $activityType = factory(ActivityType::class)->create([]); + + $request = [ + 'account_id' => $account->id, + 'activity_type_id' => $activityType->id, + ]; + + $this->expectException(ModelNotFoundException::class); + app(DestroyActivityType::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_ids_do_not_exist() + { + $request = [ + 'account_id' => 11111111, + 'activity_type_id' => 11111111, + ]; + + $this->expectException(ValidationException::class); + app(DestroyActivityType::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Account/Activity/ActivityType/UpdateActivityTypeTest.php b/tests/Unit/Services/Account/Activity/ActivityType/UpdateActivityTypeTest.php new file mode 100644 index 0000000..452a0fc --- /dev/null +++ b/tests/Unit/Services/Account/Activity/ActivityType/UpdateActivityTypeTest.php @@ -0,0 +1,86 @@ +create([]); + $activityTypeCategory = factory(ActivityTypeCategory::class)->create([ + 'account_id' => $activityType->account_id, + ]); + + $request = [ + 'account_id' => $activityType->account_id, + 'activity_type_id' => $activityType->id, + 'activity_type_category_id' => $activityTypeCategory->id, + 'name' => 'Chandler House', + 'translation_key' => 'https://centralperk.com', + ]; + + $activityType = app(UpdateActivityType::class)->execute($request); + + $this->assertDatabaseHas('activity_types', [ + 'id' => $activityType->id, + 'account_id' => $activityType->account_id, + 'activity_type_category_id' => $activityTypeCategory->id, + 'name' => 'Chandler House', + 'translation_key' => 'https://centralperk.com', + ]); + + $this->assertInstanceOf( + ActivityType::class, + $activityType + ); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $activityType = factory(ActivityType::class)->create([]); + $activityTypeCategory = factory(ActivityTypeCategory::class)->create([ + 'account_id' => $activityType->account_id, + ]); + + $request = [ + 'account_id' => $activityType->account_id, + 'activity_type_category_id' => $activityTypeCategory->id, + 'name' => 'Chandler House', + 'translation_key' => 'https://centralperk.com', + ]; + + $this->expectException(ValidationException::class); + app(UpdateActivityType::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_activity_is_not_linked_to_account() + { + $account = factory(Account::class)->create([]); + $activityType = factory(ActivityType::class)->create([]); + + $request = [ + 'account_id' => $account->id, + 'activity_type_id' => $activityType->id, + 'activity_type_category_id' => $activityType->activity_type_category_id, + 'name' => 'Chandler House', + 'translation_key' => 'https://centralperk.com', + ]; + + $this->expectException(ModelNotFoundException::class); + app(UpdateActivityType::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Account/Activity/ActivityTypeCategory/CreateActivityTypeCategoryTest.php b/tests/Unit/Services/Account/Activity/ActivityTypeCategory/CreateActivityTypeCategoryTest.php new file mode 100644 index 0000000..c3cd8f4 --- /dev/null +++ b/tests/Unit/Services/Account/Activity/ActivityTypeCategory/CreateActivityTypeCategoryTest.php @@ -0,0 +1,52 @@ +create([]); + + $request = [ + 'account_id' => $account->id, + 'name' => 'central perk', + 'translation_key' => 'central_perk', + ]; + + $activityTypeCategory = app(CreateActivityTypeCategory::class)->execute($request); + + $this->assertDatabaseHas('activity_type_categories', [ + 'id' => $activityTypeCategory->id, + 'account_id' => $account->id, + 'name' => 'central perk', + 'translation_key' => 'central_perk', + ]); + + $this->assertInstanceOf( + ActivityTypeCategory::class, + $activityTypeCategory + ); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $request = [ + 'name' => '199 Lafayette Street', + ]; + + $this->expectException(ValidationException::class); + app(CreateActivityTypeCategory::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Account/Activity/ActivityTypeCategory/DestroyActivityTypeCategoryTest.php b/tests/Unit/Services/Account/Activity/ActivityTypeCategory/DestroyActivityTypeCategoryTest.php new file mode 100644 index 0000000..5ac5f7d --- /dev/null +++ b/tests/Unit/Services/Account/Activity/ActivityTypeCategory/DestroyActivityTypeCategoryTest.php @@ -0,0 +1,60 @@ +create([]); + + $request = [ + 'account_id' => $activityTypeCategory->account_id, + 'activity_type_category_id' => $activityTypeCategory->id, + ]; + + app(DestroyActivityTypeCategory::class)->execute($request); + + $this->assertDatabaseMissing('activity_type_categories', [ + 'id' => $activityTypeCategory->id, + ]); + } + + /** @test */ + public function it_throws_an_exception_if_account_is_not_linked_to_activity_type_category() + { + $account = factory(Account::class)->create([]); + $activityTypeCategory = factory(ActivityTypeCategory::class)->create([]); + + $request = [ + 'account_id' => $account->id, + 'activity_type_category_id' => $activityTypeCategory->id, + ]; + + $this->expectException(ModelNotFoundException::class); + app(DestroyActivityTypeCategory::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_ids_do_not_exist() + { + $request = [ + 'account_id' => 11111111, + 'activity_type_category_id' => 11111111, + ]; + + $this->expectException(ValidationException::class); + app(DestroyActivityTypeCategory::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Account/Activity/ActivityTypeCategory/UpdateActivityTypeCategoryTest.php b/tests/Unit/Services/Account/Activity/ActivityTypeCategory/UpdateActivityTypeCategoryTest.php new file mode 100644 index 0000000..12f4513 --- /dev/null +++ b/tests/Unit/Services/Account/Activity/ActivityTypeCategory/UpdateActivityTypeCategoryTest.php @@ -0,0 +1,72 @@ +create([]); + + $request = [ + 'account_id' => $activityTypeCategory->account_id, + 'activity_type_category_id' => $activityTypeCategory->id, + 'name' => 'Chandler House', + 'translation_key' => 'https://centralperk.com', + ]; + + $activityTypeCategory = app(UpdateActivityTypeCategory::class)->execute($request); + + $this->assertDatabaseHas('activity_type_categories', [ + 'id' => $activityTypeCategory->id, + 'account_id' => $activityTypeCategory->account_id, + 'name' => 'Chandler House', + 'translation_key' => 'https://centralperk.com', + ]); + + $this->assertInstanceOf( + ActivityTypeCategory::class, + $activityTypeCategory + ); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $activityTypeCategory = factory(ActivityTypeCategory::class)->create([]); + + $request = [ + 'name' => '199 Lafayette Street', + ]; + + $this->expectException(ValidationException::class); + app(UpdateActivityTypeCategory::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_activity_is_not_linked_to_account() + { + $account = factory(Account::class)->create([]); + $activityTypeCategory = factory(ActivityTypeCategory::class)->create([]); + + $request = [ + 'account_id' => $account->id, + 'activity_type_category_id' => $activityTypeCategory->id, + 'name' => '199 Lafayette Street', + ]; + + $this->expectException(ModelNotFoundException::class); + app(UpdateActivityTypeCategory::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Account/Activity/AttachContactToActivityTest.php b/tests/Unit/Services/Account/Activity/AttachContactToActivityTest.php new file mode 100644 index 0000000..db96306 --- /dev/null +++ b/tests/Unit/Services/Account/Activity/AttachContactToActivityTest.php @@ -0,0 +1,99 @@ +create([]); + $contactA = factory(Contact::class)->create([ + 'account_id' => $activity->account_id, + ]); + $contactB = factory(Contact::class)->create([ + 'account_id' => $activity->account_id, + ]); + $contactC = factory(Contact::class)->create([ + 'account_id' => $activity->account_id, + ]); + + $request = [ + 'account_id' => $activity->account_id, + 'activity_id' => $activity->id, + 'contacts' => [$contactA->id, $contactB->id, $contactC->id], + ]; + + $activity = app(AttachContactToActivity::class)->execute($request); + + $this->assertDatabaseHas('activity_contact', [ + 'activity_id' => $activity->id, + 'contact_id' => $contactA->id, + 'account_id' => $activity->account_id, + ]); + + $this->assertDatabaseHas('activity_contact', [ + 'activity_id' => $activity->id, + 'contact_id' => $contactB->id, + 'account_id' => $activity->account_id, + ]); + + $this->assertDatabaseHas('activity_contact', [ + 'activity_id' => $activity->id, + 'contact_id' => $contactC->id, + 'account_id' => $activity->account_id, + ]); + + $this->assertInstanceOf( + Activity::class, + $activity + ); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $activity = factory(Activity::class)->create([]); + $contactA = factory(Contact::class)->create([ + 'account_id' => $activity->account_id, + ]); + + $request = [ + 'activity_id' => $activity->id, + 'contacts' => [$contactA->id], + ]; + + $this->expectException(ValidationException::class); + app(AttachContactToActivity::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_contact_is_not_linked_to_account() + { + $activity = factory(Activity::class)->create([]); + $account = factory(Account::class)->create([]); + $contactA = factory(Contact::class)->create([ + 'account_id' => $activity->account_id, + ]); + + $request = [ + 'activity_id' => $activity->id, + 'account_id' => $account->id, + 'contacts' => [$contactA->id], + ]; + + $this->expectException(ModelNotFoundException::class); + app(AttachContactToActivity::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Account/Activity/CreateActivityTest.php b/tests/Unit/Services/Account/Activity/CreateActivityTest.php new file mode 100644 index 0000000..f127b81 --- /dev/null +++ b/tests/Unit/Services/Account/Activity/CreateActivityTest.php @@ -0,0 +1,149 @@ +create(); + $contacts = factory(Contact::class, 3)->create([ + 'account_id' => $account->id, + ]); + $activityType = factory(ActivityType::class)->create([ + 'account_id' => $account->id, + ]); + + $request = [ + 'account_id' => $account->id, + 'activity_type_id' => $activityType->id, + 'summary' => 'we went to central perk', + 'description' => 'it was awesome', + 'happened_at' => '2009-09-09', + 'contacts' => $contacts->map(function ($contact) { + return $contact->id; + })->toArray(), + ]; + + $activity = app(CreateActivity::class)->execute($request); + + $this->assertDatabaseHas('activities', [ + 'id' => $activity->id, + 'account_id' => $account->id, + 'summary' => 'we went to central perk', + 'description' => 'it was awesome', + 'happened_at' => '2009-09-09', + ]); + + foreach ($contacts as $contact) { + $this->assertDatabaseHas('activity_contact', [ + 'account_id' => $account->id, + 'activity_id' => $activity->id, + 'contact_id' => $contact->id, + ]); + } + + $this->assertInstanceOf( + Activity::class, + $activity + ); + + $this->assertDatabaseHas('journal_entries', [ + 'account_id' => $account->id, + 'journalable_id' => $activity->id, + 'journalable_type' => get_class($activity), + ]); + } + + /** @test */ + public function it_adds_emotions() + { + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + $emotion = factory(Emotion::class)->create([]); + $emotion2 = factory(Emotion::class)->create([]); + + $emotionArray = []; + $emotionArray[] = $emotion->id; + $emotionArray[] = $emotion2->id; + + $activityType = factory(ActivityType::class)->create([ + 'account_id' => $account->id, + ]); + + $request = [ + 'account_id' => $account->id, + 'activity_type_id' => $activityType->id, + 'summary' => 'we went to central perk', + 'description' => 'it was awesome', + 'happened_at' => '2009-09-09', + 'emotions' => $emotionArray, + 'contacts' => [$contact->id], + ]; + + $activity = app(CreateActivity::class)->execute($request); + + $this->assertDatabaseHas('emotion_activity', [ + 'account_id' => $account->id, + 'activity_id' => $activity->id, + 'emotion_id' => $emotion->id, + ]); + + $this->assertDatabaseHas('emotion_activity', [ + 'account_id' => $account->id, + 'activity_id' => $activity->id, + 'emotion_id' => $emotion2->id, + ]); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $account = factory(Account::class)->create([]); + + $request = [ + 'account_id' => $account->id, + ]; + + $this->expectException(ValidationException::class); + app(CreateActivity::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_activity_type_is_not_linked_to_account() + { + $account = factory(Account::class)->create([]); + $activityType = factory(ActivityType::class)->create([]); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + + $request = [ + 'account_id' => $account->id, + 'activity_type_id' => $activityType->id, + 'summary' => 'we went to central perk', + 'description' => 'it was awesome', + 'happened_at' => '2009-09-09', + 'contacts' => [$contact->id], + ]; + + $this->expectException(ModelNotFoundException::class); + app(CreateActivity::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Account/Activity/DestroyActivityTest.php b/tests/Unit/Services/Account/Activity/DestroyActivityTest.php new file mode 100644 index 0000000..83624b0 --- /dev/null +++ b/tests/Unit/Services/Account/Activity/DestroyActivityTest.php @@ -0,0 +1,94 @@ +create([]); + + $request = [ + 'account_id' => $activity->account_id, + 'activity_id' => $activity->id, + ]; + + $this->assertDatabaseHas('activities', [ + 'id' => $activity->id, + ]); + + app(DestroyActivity::class)->execute($request); + + $this->assertDatabaseMissing('activities', [ + 'id' => $activity->id, + ]); + } + + /** @test */ + public function it_removes_the_journal_entry_when_destroying_the_activity() + { + $account = factory(Account::class)->create([]); + $activityType = factory(ActivityType::class)->create([ + 'account_id' => $account->id, + ]); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + + $request = [ + 'account_id' => $account->id, + 'activity_type_id' => $activityType->id, + 'summary' => 'we went to central perk', + 'description' => 'it was awesome', + 'happened_at' => '2009-09-09', + 'contacts' => [$contact->id], + ]; + + $activity = app(CreateActivity::class)->execute($request); + + $this->assertDatabaseHas('activities', [ + 'id' => $activity->id, + ]); + $this->assertDatabaseHas('activity_contact', [ + 'activity_id' => $activity->id, + 'contact_id' => $contact->id, + ]); + + $this->assertDatabaseHas('journal_entries', [ + 'account_id' => $account->id, + 'journalable_id' => $activity->id, + 'journalable_type' => get_class($activity), + ]); + + $request = [ + 'account_id' => $activity->account_id, + 'activity_id' => $activity->id, + ]; + app(DestroyActivity::class)->execute($request); + + $this->assertDatabaseMissing('activities', [ + 'id' => $activity->id, + ]); + $this->assertDatabaseMissing('activity_contact', [ + 'activity_id' => $activity->id, + ]); + + $this->assertDatabaseMissing('journal_entries', [ + 'account_id' => $account->id, + 'journalable_id' => $activity->id, + 'journalable_type' => get_class($activity), + ]); + } +} diff --git a/tests/Unit/Services/Account/Activity/UpdateActivityTest.php b/tests/Unit/Services/Account/Activity/UpdateActivityTest.php new file mode 100644 index 0000000..494cd5e --- /dev/null +++ b/tests/Unit/Services/Account/Activity/UpdateActivityTest.php @@ -0,0 +1,191 @@ +create([]); + $contact = factory(Contact::class)->create([ + 'account_id' => $activity->account_id, + ]); + + $request = [ + 'account_id' => $activity->account_id, + 'activity_id' => $activity->id, + 'activity_type_id' => $activity->activity_type_id, + 'summary' => 'we went to central perk', + 'description' => 'it was awesome', + 'happened_at' => '2009-09-09', + 'contacts' => [$contact->id], + ]; + + app(UpdateActivity::class)->execute($request); + + $this->assertDatabaseHas('activities', [ + 'id' => $activity->id, + 'account_id' => $activity->account_id, + 'summary' => 'we went to central perk', + 'description' => 'it was awesome', + ]); + + $this->assertInstanceOf( + Activity::class, + $activity + ); + } + + /** @test */ + public function it_removes_old_associated_contacts() + { + $activity = factory(Activity::class)->create(); + $contacts = factory(Contact::class, 3)->create([ + 'account_id' => $activity->account_id, + ]); + foreach ($contacts as $contact) { + $activity->contacts()->syncWithoutDetaching([$contact->id => [ + 'account_id' => $activity->account_id, + ]]); + } + + foreach ($contacts as $contact) { + $this->assertDatabaseHas('activity_contact', [ + 'account_id' => $activity->account_id, + 'activity_id' => $activity->id, + 'contact_id' => $contact->id, + ]); + } + + $newContact = factory(Contact::class)->create([ + 'account_id' => $activity->account_id, + ]); + + $request = [ + 'account_id' => $activity->account_id, + 'activity_id' => $activity->id, + 'activity_type_id' => $activity->activity_type_id, + 'summary' => 'we went to central perk', + 'description' => 'it was awesome', + 'happened_at' => '2009-09-09', + 'contacts' => [$newContact->id], + ]; + + app(UpdateActivity::class)->execute($request); + + $this->assertDatabaseHas('activity_contact', [ + 'account_id' => $activity->account_id, + 'activity_id' => $activity->id, + 'contact_id' => $newContact->id, + ]); + foreach ($contacts as $contact) { + $this->assertDatabaseMissing('activity_contact', [ + 'account_id' => $activity->account_id, + 'activity_id' => $activity->id, + 'contact_id' => $contact->id, + ]); + } + } + + /** @test */ + public function it_removes_old_associated_contacts_and_keep_previous_one() + { + $activity = factory(Activity::class)->create(); + $contacts = factory(Contact::class, 3)->create([ + 'account_id' => $activity->account_id, + ]); + foreach ($contacts as $contact) { + $activity->contacts()->syncWithoutDetaching([$contact->id => [ + 'account_id' => $activity->account_id, + ]]); + } + + foreach ($contacts as $contact) { + $this->assertDatabaseHas('activity_contact', [ + 'account_id' => $activity->account_id, + 'activity_id' => $activity->id, + 'contact_id' => $contact->id, + ]); + } + + $request = [ + 'account_id' => $activity->account_id, + 'activity_id' => $activity->id, + 'activity_type_id' => $activity->activity_type_id, + 'summary' => 'we went to central perk', + 'description' => 'it was awesome', + 'happened_at' => '2009-09-09', + 'contacts' => [$contacts[0]->id, $contacts[1]->id], + ]; + + app(UpdateActivity::class)->execute($request); + + $this->assertDatabaseHas('activity_contact', [ + 'account_id' => $activity->account_id, + 'activity_id' => $activity->id, + 'contact_id' => $contacts[0]->id, + ]); + $this->assertDatabaseHas('activity_contact', [ + 'account_id' => $activity->account_id, + 'activity_id' => $activity->id, + 'contact_id' => $contacts[1]->id, + ]); + $this->assertDatabaseMissing('activity_contact', [ + 'account_id' => $activity->account_id, + 'activity_id' => $activity->id, + 'contact_id' => $contacts[2]->id, + ]); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $activity = factory(Activity::class)->create([]); + + $request = [ + 'activity_id' => $activity->id, + 'activity_type_id' => $activity->activity_type_id, + 'summary' => 'we went to central perk', + 'description' => 'it was awesome', + 'happened_at' => '2009-09-09', + ]; + + $this->expectException(ValidationException::class); + app(UpdateActivity::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_contact_is_not_linked_to_account() + { + $activity = factory(Activity::class)->create([]); + $account = factory(Account::class)->create([]); + $contact = factory(Contact::class)->create([ + 'account_id' => $activity->account_id, + ]); + + $request = [ + 'account_id' => $account->id, + 'activity_id' => $activity->id, + 'activity_type_id' => $activity->activity_type_id, + 'summary' => 'we went to central perk', + 'description' => 'it was awesome', + 'happened_at' => '2009-09-09', + 'contacts' => [$contact->id], + ]; + + $this->expectException(ModelNotFoundException::class); + app(UpdateActivity::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Account/Company/CreateCompanyTest.php b/tests/Unit/Services/Account/Company/CreateCompanyTest.php new file mode 100644 index 0000000..0ca1b9e --- /dev/null +++ b/tests/Unit/Services/Account/Company/CreateCompanyTest.php @@ -0,0 +1,76 @@ +create([]); + $user = factory(User::class)->create([ + 'account_id' => $account->id, + ]); + + $request = [ + 'account_id' => $account->id, + 'author_id' => $user->id, + 'name' => 'central perk', + 'website' => 'https://centralperk.com', + 'number_of_employees' => 3, + ]; + + $company = app(CreateCompany::class)->execute($request); + + $this->assertDatabaseHas('companies', [ + 'id' => $company->id, + 'account_id' => $account->id, + 'name' => 'central perk', + 'website' => 'https://centralperk.com', + 'number_of_employees' => 3, + ]); + + $this->assertInstanceOf( + Company::class, + $company + ); + + Queue::assertPushed(LogAccountAudit::class, function ($job) use ($user) { + return $job->auditLog['action'] === 'company_created' && + $job->auditLog['author_id'] === $user->id && + $job->auditLog['about_contact_id'] === null && + $job->auditLog['should_appear_on_dashboard'] === true && + $job->auditLog['objects'] === json_encode([ + 'name' => 'central perk', + ]); + }); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $account = factory(Account::class)->create([]); + + $request = [ + 'street' => '199 Lafayette Street', + ]; + + $this->expectException(ValidationException::class); + app(CreateCompany::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Account/Company/DestroyCompanyTest.php b/tests/Unit/Services/Account/Company/DestroyCompanyTest.php new file mode 100644 index 0000000..89d48ad --- /dev/null +++ b/tests/Unit/Services/Account/Company/DestroyCompanyTest.php @@ -0,0 +1,60 @@ +create([]); + + $request = [ + 'account_id' => $company->account_id, + 'company_id' => $company->id, + ]; + + app(DestroyCompany::class)->execute($request); + + $this->assertDatabaseMissing('companies', [ + 'id' => $company->id, + ]); + } + + /** @test */ + public function it_throws_an_exception_if_account_is_not_linked_to_company() + { + $account = factory(Account::class)->create([]); + $company = factory(Company::class)->create([]); + + $request = [ + 'account_id' => $account->id, + 'company_id' => $company->id, + ]; + + $this->expectException(ModelNotFoundException::class); + app(DestroyCompany::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_ids_do_not_exist() + { + $request = [ + 'account_id' => 11111111, + 'company_id' => 11111111, + ]; + + $this->expectException(ValidationException::class); + app(DestroyCompany::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Account/Company/UpdateCompanyTest.php b/tests/Unit/Services/Account/Company/UpdateCompanyTest.php new file mode 100644 index 0000000..30debee --- /dev/null +++ b/tests/Unit/Services/Account/Company/UpdateCompanyTest.php @@ -0,0 +1,74 @@ +create([]); + + $request = [ + 'account_id' => $company->account_id, + 'company_id' => $company->id, + 'name' => 'Chandler House', + 'website' => 'https://centralperk.com', + 'number_of_employees' => 300, + ]; + + app(UpdateCompany::class)->execute($request); + + $this->assertDatabaseHas('companies', [ + 'id' => $company->id, + 'account_id' => $company->account_id, + 'name' => 'Chandler House', + 'website' => 'https://centralperk.com', + 'number_of_employees' => 300, + ]); + + $this->assertInstanceOf( + Company::class, + $company + ); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $company = factory(Company::class)->create([]); + + $request = [ + 'name' => '199 Lafayette Street', + ]; + + $this->expectException(ValidationException::class); + app(UpdateCompany::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_place_is_not_linked_to_account() + { + $account = factory(Account::class)->create([]); + $company = factory(Company::class)->create([]); + + $request = [ + 'account_id' => $account->id, + 'company_id' => $company->id, + 'name' => '199 Lafayette Street', + ]; + + $this->expectException(ModelNotFoundException::class); + app(UpdateCompany::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Account/Gender/CreateGenderTest.php b/tests/Unit/Services/Account/Gender/CreateGenderTest.php new file mode 100644 index 0000000..c788753 --- /dev/null +++ b/tests/Unit/Services/Account/Gender/CreateGenderTest.php @@ -0,0 +1,55 @@ +create([]); + + $request = [ + 'account_id' => $account->id, + 'name' => 'man', + 'type' => 'M', + ]; + + $gender = app(CreateGender::class)->execute($request); + + $this->assertDatabaseHas('genders', [ + 'id' => $gender->id, + 'account_id' => $account->id, + 'name' => 'man', + 'type' => 'M', + ]); + + $this->assertInstanceOf( + Gender::class, + $gender + ); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $account = factory(Account::class)->create([]); + + $request = [ + 'name' => 'man', + 'type' => 'X', + ]; + + $this->expectException(ValidationException::class); + app(CreateGender::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Account/Gender/DestroyGenderTest.php b/tests/Unit/Services/Account/Gender/DestroyGenderTest.php new file mode 100644 index 0000000..a0bbf1a --- /dev/null +++ b/tests/Unit/Services/Account/Gender/DestroyGenderTest.php @@ -0,0 +1,60 @@ +create([]); + + $request = [ + 'account_id' => $gender->account_id, + 'gender_id' => $gender->id, + ]; + + app(DestroyGender::class)->execute($request); + + $this->assertDatabaseMissing('genders', [ + 'id' => $gender->id, + ]); + } + + /** @test */ + public function it_throws_an_exception_if_account_is_not_linked_to_gender() + { + $account = factory(Account::class)->create([]); + $gender = factory(Gender::class)->create([]); + + $request = [ + 'account_id' => $account->id, + 'gender_id' => $gender->id, + ]; + + $this->expectException(ModelNotFoundException::class); + app(DestroyGender::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_ids_do_not_exist() + { + $request = [ + 'account_id' => 11111111, + 'gender_id' => 11111111, + ]; + + $this->expectException(ValidationException::class); + app(DestroyGender::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Account/Gender/UpdateGenderTest.php b/tests/Unit/Services/Account/Gender/UpdateGenderTest.php new file mode 100644 index 0000000..a3cc4b0 --- /dev/null +++ b/tests/Unit/Services/Account/Gender/UpdateGenderTest.php @@ -0,0 +1,74 @@ +create([]); + + $request = [ + 'account_id' => $gender->account_id, + 'gender_id' => $gender->id, + 'name' => 'man', + 'type' => 'M', + ]; + + $gender = app(UpdateGender::class)->execute($request); + + $this->assertDatabaseHas('genders', [ + 'id' => $gender->id, + 'account_id' => $gender->account_id, + 'name' => 'man', + 'type' => 'M', + ]); + + $this->assertInstanceOf( + Gender::class, + $gender + ); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $gender = factory(Gender::class)->create([]); + + $request = [ + 'name' => 'man', + 'type' => 'X', + ]; + + $this->expectException(ValidationException::class); + app(UpdateGender::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_place_is_not_linked_to_account() + { + $account = factory(Account::class)->create([]); + $gender = factory(Gender::class)->create([]); + + $request = [ + 'account_id' => $account->id, + 'gender_id' => $gender->id, + 'name' => 'man', + 'type' => 'M', + ]; + + $this->expectException(ModelNotFoundException::class); + app(UpdateGender::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Account/LifeEvent/LifeEventType/CreateLifeEventTypeTest.php b/tests/Unit/Services/Account/LifeEvent/LifeEventType/CreateLifeEventTypeTest.php new file mode 100644 index 0000000..c53ce62 --- /dev/null +++ b/tests/Unit/Services/Account/LifeEvent/LifeEventType/CreateLifeEventTypeTest.php @@ -0,0 +1,73 @@ +create([]); + $lifeEventCategory = factory(LifeEventCategory::class)->create([ + 'account_id' => $account->id, + ]); + + $request = [ + 'account_id' => $account->id, + 'life_event_category_id' => $lifeEventCategory->id, + 'name' => 'Had a major health problem', + ]; + + $lifeEventType = app(CreateLifeEventType::class)->execute($request); + + $this->assertDatabaseHas('life_event_types', [ + 'id' => $lifeEventType->id, + 'account_id' => $account->id, + 'life_event_category_id' => $lifeEventCategory->id, + 'name' => 'Had a major health problem', + ]); + + $this->assertInstanceOf( + LifeEventType::class, + $lifeEventType + ); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $request = [ + 'name' => 'Had a major health problem', + ]; + + $this->expectException(ValidationException::class); + app(CreateLifeEventType::class)->execute($request); + } + + /** @test */ + public function it_fails_if_life_event_category_is_not_linked_to_account() + { + $account = factory(Account::class)->create([]); + $lifeEventCategory = factory(LifeEventCategory::class)->create([]); + + $request = [ + 'account_id' => $account->id, + 'life_event_category_id' => $lifeEventCategory->id, + 'name' => 'Had a major health problem', + ]; + + $this->expectException(ModelNotFoundException::class); + app(CreateLifeEventType::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Account/LifeEvent/LifeEventType/DestroyLifeEventTypeTest.php b/tests/Unit/Services/Account/LifeEvent/LifeEventType/DestroyLifeEventTypeTest.php new file mode 100644 index 0000000..f52bf97 --- /dev/null +++ b/tests/Unit/Services/Account/LifeEvent/LifeEventType/DestroyLifeEventTypeTest.php @@ -0,0 +1,60 @@ +create([]); + + $request = [ + 'account_id' => $lifeEventType->account_id, + 'life_event_type_id' => $lifeEventType->id, + ]; + + app(DestroyLifeEventType::class)->execute($request); + + $this->assertDatabaseMissing('life_event_types', [ + 'id' => $lifeEventType->id, + ]); + } + + /** @test */ + public function it_throws_an_exception_if_account_is_not_linked_to_life_event_type() + { + $account = factory(Account::class)->create([]); + $lifeEventType = factory(LifeEventType::class)->create([]); + + $request = [ + 'account_id' => $account->id, + 'life_event_type_id' => $lifeEventType->id, + ]; + + $this->expectException(ModelNotFoundException::class); + app(DestroyLifeEventType::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_ids_do_not_exist() + { + $request = [ + 'account_id' => 11111111, + 'life_event_type_id' => 11111111, + ]; + + $this->expectException(ValidationException::class); + app(DestroyLifeEventType::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Account/LifeEvent/LifeEventType/UpdateLifeEventTypeTest.php b/tests/Unit/Services/Account/LifeEvent/LifeEventType/UpdateLifeEventTypeTest.php new file mode 100644 index 0000000..ed332d9 --- /dev/null +++ b/tests/Unit/Services/Account/LifeEvent/LifeEventType/UpdateLifeEventTypeTest.php @@ -0,0 +1,82 @@ +create([]); + $lifeEventCategory = factory(LifeEventCategory::class)->create([ + 'account_id' => $lifeEventType->account_id, + ]); + + $request = [ + 'account_id' => $lifeEventType->account_id, + 'life_event_type_id' => $lifeEventType->id, + 'life_event_category_id' => $lifeEventCategory->id, + 'name' => 'Had a major health problem', + ]; + + $lifeEventType = app(UpdateLifeEventType::class)->execute($request); + + $this->assertDatabaseHas('life_event_types', [ + 'id' => $lifeEventType->id, + 'account_id' => $lifeEventType->account_id, + 'life_event_category_id' => $lifeEventCategory->id, + 'name' => 'Had a major health problem', + ]); + + $this->assertInstanceOf( + LifeEventType::class, + $lifeEventType + ); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $lifeEventType = factory(LifeEventType::class)->create([]); + $lifeEventCategory = factory(LifeEventCategory::class)->create([ + 'account_id' => $lifeEventType->account_id, + ]); + + $request = [ + 'account_id' => $lifeEventType->account_id, + 'life_event_category_id' => $lifeEventCategory->id, + 'name' => 'Had a major health problem', + ]; + + $this->expectException(ValidationException::class); + app(UpdateLifeEventType::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_life_event_is_not_linked_to_account() + { + $account = factory(Account::class)->create([]); + $lifeEventType = factory(LifeEventType::class)->create([]); + + $request = [ + 'account_id' => $account->id, + 'life_event_type_id' => $lifeEventType->id, + 'life_event_category_id' => $lifeEventType->life_event_category_id, + 'name' => 'Had a major health problem', + ]; + + $this->expectException(ModelNotFoundException::class); + app(UpdateLifeEventType::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Account/Photo/DestroyPhotoTest.php b/tests/Unit/Services/Account/Photo/DestroyPhotoTest.php new file mode 100644 index 0000000..8f30f08 --- /dev/null +++ b/tests/Unit/Services/Account/Photo/DestroyPhotoTest.php @@ -0,0 +1,85 @@ +create([]); + $photo = $this->uploadPhoto($contact); + + $request = [ + 'account_id' => $photo->account_id, + 'photo_id' => $photo->id, + ]; + + $this->assertDatabaseHas('photos', [ + 'id' => $photo->id, + ]); + + app(DestroyPhoto::class)->execute($request); + + $this->assertDatabaseMissing('photos', [ + 'id' => $photo->id, + ]); + + Storage::disk('photos')->assertMissing('photo.png'); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $request = [ + 'photo_id' => 2, + ]; + + $this->expectException(ValidationException::class); + + app(DestroyPhoto::class)->execute($request); + } + + /** @test */ + public function it_throws_a_photo_doesnt_exist() + { + $account = factory(Account::class)->create([]); + $photo = factory(Photo::class)->create([]); + + $request = [ + 'account_id' => $account->id, + 'photo_id' => $photo->id, + ]; + + $this->expectException(ModelNotFoundException::class); + + app(DestroyPhoto::class)->execute($request); + } + + private function uploadPhoto($contact) + { + Storage::fake('photos'); + + $request = [ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'photo' => UploadedFile::fake()->image('photo.png'), + ]; + + return app(UploadPhoto::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Account/Photo/UploadPhotoTest.php b/tests/Unit/Services/Account/Photo/UploadPhotoTest.php new file mode 100644 index 0000000..3101fda --- /dev/null +++ b/tests/Unit/Services/Account/Photo/UploadPhotoTest.php @@ -0,0 +1,74 @@ +create([]); + + $file = UploadedFile::fake()->image('imag.png'); + + $request = [ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'photo' => $file, + ]; + + $photo = app(UploadPhoto::class)->execute($request); + + $this->assertDatabaseHas('photos', [ + 'id' => $photo->id, + 'account_id' => $contact->account_id, + 'mime_type' => 'image/png', + ]); + + $this->assertInstanceOf( + Photo::class, + $photo + ); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $request = [ + 'account_id' => 'wrong', + ]; + + $this->expectException(ValidationException::class); + + app(UploadPhoto::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_account_does_not_exist() + { + Storage::fake('photos'); + + $request = [ + 'account_id' => 0, + 'contact_id' => 0, + 'photo' => UploadedFile::fake()->image('document.pdf'), + ]; + + $this->expectException(ValidationException::class); + + app(UploadPhoto::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Account/Place/CreatePlaceTest.php b/tests/Unit/Services/Account/Place/CreatePlaceTest.php new file mode 100644 index 0000000..0421415 --- /dev/null +++ b/tests/Unit/Services/Account/Place/CreatePlaceTest.php @@ -0,0 +1,95 @@ +create([]); + + $request = [ + 'account_id' => $account->id, + 'street' => '199 Lafayette Street', + 'city' => 'New York City', + 'province' => '', + 'postal_code' => '', + 'country' => 'USA', + 'latitude' => '10', + 'longitude' => '10', + ]; + + $place = app(CreatePlace::class)->execute($request); + + $this->assertDatabaseHas('places', [ + 'id' => $place->id, + 'account_id' => $account->id, + 'street' => '199 Lafayette Street', + 'latitude' => 10, + ]); + + $this->assertInstanceOf( + Place::class, + $place + ); + } + + /** @test */ + public function it_stores_a_place_and_fetch_geolocation_information() + { + config(['monica.enable_geolocation' => true]); + config(['monica.location_iq_api_key' => 'test']); + + $body = file_get_contents(base_path('tests/Fixtures/Services/Account/Place/CreatePlaceSampleResponse.json')); + Http::fake([ + 'us1.locationiq.com/v1/*' => Http::response($body, 200), + ]); + + $account = factory(Account::class)->create([]); + + $request = [ + 'account_id' => $account->id, + 'street' => '12', + 'city' => 'beverly hills', + 'province' => '', + 'postal_code' => '90210', + 'country' => 'US', + 'latitude' => '', + 'longitude' => '', + ]; + + $place = app(CreatePlace::class)->execute($request); + + $this->assertDatabaseHas('places', [ + 'id' => $place->id, + 'account_id' => $account->id, + 'street' => '12', + 'latitude' => 34.0736204, + 'longitude' => -118.4003563, + ]); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + factory(Account::class)->create([]); + + $request = [ + 'street' => '199 Lafayette Street', + ]; + + $this->expectException(ValidationException::class); + app(CreatePlace::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Account/Place/DestroyPlaceTest.php b/tests/Unit/Services/Account/Place/DestroyPlaceTest.php new file mode 100644 index 0000000..873a107 --- /dev/null +++ b/tests/Unit/Services/Account/Place/DestroyPlaceTest.php @@ -0,0 +1,60 @@ +create([]); + + $request = [ + 'account_id' => $place->account_id, + 'place_id' => $place->id, + ]; + + app(DestroyPlace::class)->execute($request); + + $this->assertDatabaseMissing('places', [ + 'id' => $place->id, + ]); + } + + /** @test */ + public function it_throws_an_exception_if_account_is_not_linked_to_places() + { + $account = factory(Account::class)->create([]); + $place = factory(Place::class)->create([]); + + $request = [ + 'account_id' => $account->id, + 'place_id' => $place->id, + ]; + + $this->expectException(ModelNotFoundException::class); + app(DestroyPlace::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_ids_do_not_exist() + { + $request = [ + 'account_id' => 11111111, + 'place_id' => 11111111, + ]; + + $this->expectException(ValidationException::class); + app(DestroyPlace::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Account/Place/UpdatePlaceTest.php b/tests/Unit/Services/Account/Place/UpdatePlaceTest.php new file mode 100644 index 0000000..16caba8 --- /dev/null +++ b/tests/Unit/Services/Account/Place/UpdatePlaceTest.php @@ -0,0 +1,113 @@ +create([]); + + $request = [ + 'account_id' => $place->account_id, + 'place_id' => $place->id, + 'street' => '199 Lafayette Street', + 'city' => 'New York City', + 'province' => '', + 'postal_code' => '', + 'country' => 'USA', + 'latitude' => '10', + 'longitude' => '10', + ]; + + $place = app(UpdatePlace::class)->execute($request); + + $this->assertDatabaseHas('places', [ + 'id' => $place->id, + 'account_id' => $place->account_id, + 'latitude' => 10, + 'city' => 'New York City', + ]); + + $this->assertInstanceOf( + Place::class, + $place + ); + } + + /** @test */ + public function it_updates_a_place_and_fetch_geolocation_information() + { + config(['monica.enable_geolocation' => true]); + config(['monica.location_iq_api_key' => 'test']); + + $body = file_get_contents(base_path('tests/Fixtures/Services/Account/Place/UpdatePlaceSampleResponse.json')); + Http::fake([ + 'us1.locationiq.com/v1/*' => Http::response($body, 200), + ]); + + $place = factory(Place::class)->create([]); + + $request = [ + 'account_id' => $place->account_id, + 'place_id' => $place->id, + 'street' => '12', + 'city' => 'beverly hills', + 'province' => '', + 'postal_code' => '90210', + 'country' => 'US', + 'latitude' => '', + 'longitude' => '', + ]; + + $place = app(UpdatePlace::class)->execute($request); + + $this->assertDatabaseHas('places', [ + 'id' => $place->id, + 'account_id' => $place->account_id, + 'street' => '12', + 'latitude' => 34.0736204, + 'longitude' => -118.4003563, + ]); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $place = factory(Place::class)->create([]); + + $request = [ + 'street' => '199 Lafayette Street', + ]; + + $this->expectException(ValidationException::class); + app(UpdatePlace::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_place_is_not_linked_to_account() + { + $account = factory(Account::class)->create([]); + $place = factory(Place::class)->create([]); + + $request = [ + 'account_id' => $account->id, + 'place_id' => $place->id, + ]; + + $this->expectException(ModelNotFoundException::class); + app(UpdatePlace::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Account/Settings/ArchiveAllContactsTest.php b/tests/Unit/Services/Account/Settings/ArchiveAllContactsTest.php new file mode 100644 index 0000000..19eb443 --- /dev/null +++ b/tests/Unit/Services/Account/Settings/ArchiveAllContactsTest.php @@ -0,0 +1,46 @@ +create([]); + factory(Contact::class, 3)->create([ + 'account_id' => $user->account_id, + ]); + + $request = [ + 'account_id' => $user->account_id, + ]; + + $result = app(ArchiveAllContacts::class)->execute($request); + + $this->assertTrue($result); + + $this->assertDatabaseHas('contacts', [ + 'is_active' => 0, + ]); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $request = []; + + $this->expectException(ValidationException::class); + app(DestroyAccount::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Account/Settings/DestroyAccountTest.php b/tests/Unit/Services/Account/Settings/DestroyAccountTest.php new file mode 100644 index 0000000..b56ef0f --- /dev/null +++ b/tests/Unit/Services/Account/Settings/DestroyAccountTest.php @@ -0,0 +1,47 @@ +create([]); + factory(Contact::class, 3)->create([ + 'account_id' => $user->account_id, + ]); + + $request = [ + 'account_id' => $user->account_id, + ]; + + app(DestroyAccount::class)->execute($request); + + $this->assertDatabaseMissing('contacts', [ + 'account_id' => $user->account_id, + 'deleted_at' => null, + ]); + $this->assertDatabaseMissing('accounts', [ + 'id' => $user->account_id, + ]); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $request = []; + + $this->expectException(ValidationException::class); + app(DestroyAccount::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Account/Settings/DestroyAllDocumentsTest.php b/tests/Unit/Services/Account/Settings/DestroyAllDocumentsTest.php new file mode 100644 index 0000000..ef5602e --- /dev/null +++ b/tests/Unit/Services/Account/Settings/DestroyAllDocumentsTest.php @@ -0,0 +1,70 @@ +create([]); + + $documents = []; + for ($i = 0; $i < 2; $i++) { + $documents[] = $this->uploadDocument($contact); + } + + $request = [ + 'account_id' => $contact->account_id, + ]; + + app(DestroyAllDocuments::class)->execute($request); + + $this->assertDatabaseMissing('documents', [ + 'account_id' => $contact->account_id, + ]); + + foreach ($documents as $document) { + Storage::disk('public')->assertMissing($document->new_filename); + } + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $request = [ + ]; + + $this->expectException(ValidationException::class); + + app(DestroyAllDocuments::class)->execute($request); + } + + private function uploadDocument($contact) + { + $request = [ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'document' => UploadedFile::fake()->image('document.pdf'), + ]; + + $document = app(UploadDocument::class)->execute($request); + + Storage::disk('public')->assertExists($document->new_filename); + + return $document; + } +} diff --git a/tests/Unit/Services/Account/Settings/DestroyAllPhotosTest.php b/tests/Unit/Services/Account/Settings/DestroyAllPhotosTest.php new file mode 100644 index 0000000..24cc72b --- /dev/null +++ b/tests/Unit/Services/Account/Settings/DestroyAllPhotosTest.php @@ -0,0 +1,69 @@ +create([]); + + $photos = []; + for ($i = 0; $i < 2; $i++) { + $photos[] = $this->uploadPhoto($contact); + } + + $request = [ + 'account_id' => $contact->account_id, + ]; + + app(DestroyAllPhotos::class)->execute($request); + + $this->assertDatabaseMissing('photos', [ + 'account_id' => $contact->account_id, + ]); + + foreach ($photos as $photo) { + Storage::disk('public')->assertMissing($photo->new_filename); + } + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $request = []; + + $this->expectException(ValidationException::class); + + app(DestroyAllPhotos::class)->execute($request); + } + + private function uploadPhoto($contact) + { + $request = [ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'photo' => UploadedFile::fake()->image('imag.png'), + ]; + + $photo = app(UploadPhoto::class)->execute($request); + + Storage::disk('public')->assertExists($photo->new_filename); + + return $photo; + } +} diff --git a/tests/Unit/Services/Account/Settings/ExportAccountTest.php b/tests/Unit/Services/Account/Settings/ExportAccountTest.php new file mode 100644 index 0000000..fee693f --- /dev/null +++ b/tests/Unit/Services/Account/Settings/ExportAccountTest.php @@ -0,0 +1,363 @@ +put('temp/test.json', 'null'); + + $job = ExportJob::factory()->create(); + + $this->mock(JsonExportAccount::class, function (MockInterface $mock) use ($job) { + $mock->shouldReceive('execute') + ->once() + ->with([ + 'account_id' => $job->account_id, + 'user_id' => $job->user_id, + ]) + ->andReturn('temp/test.json'); + }); + + ExportAccount::dispatchSync($job); + $job->refresh(); + + Storage::disk('public')->assertExists($job->filename); + + Notification::assertSentTo( + [$job->user], ExportAccountDone::class + ); + } + + /** @test */ + public function it_exports_account_sql() + { + Notification::fake(); + + Storage::fake(); + $fake = Storage::fake('local'); + $fake->put('temp/test.sql', 'null'); + + $job = ExportJob::factory()->create([ + 'type' => 'sql', + ]); + + $this->mock(SqlExportAccount::class, function (MockInterface $mock) use ($job) { + $mock->shouldReceive('execute') + ->once() + ->with([ + 'account_id' => $job->account_id, + 'user_id' => $job->user_id, + ]) + ->andReturn('temp/test.sql'); + }); + + ExportAccount::dispatchSync($job); + $job->refresh(); + + Storage::disk('public')->assertExists($job->filename); + + Notification::assertSentTo( + [$job->user], ExportAccountDone::class + ); + } + + /** @test */ + public function it_exports_account_file() + { + Storage::fake(); + Storage::fake('local'); + + $job = ExportJob::factory()->create(); + ExportAccount::dispatchSync($job); + + $job->refresh(); + + $this->assertStringStartsWith('exports/', $job->filename); + $this->assertStringEndsWith('.json', $job->filename); + Storage::disk('public')->assertExists($job->filename); + } + + /** @test */ + public function it_exports_account_file_sql() + { + Storage::fake(); + Storage::fake('local'); + + $job = ExportJob::factory()->create([ + 'type' => 'sql', + ]); + ExportAccount::dispatchSync($job); + + $job->refresh(); + + $this->assertStringStartsWith('exports/', $job->filename); + $this->assertStringEndsWith('.sql', $job->filename); + Storage::disk('public')->assertExists($job->filename); + } + + /** @test */ + public function it_exports_json_file() + { + Storage::fake(); + Storage::fake('local'); + + $job = ExportJob::factory()->create(); + ExportAccount::dispatchSync($job); + + $job->refresh(); + + $this->assertStringStartsWith('exports/', $job->filename); + $this->assertStringEndsWith('.json', $job->filename); + Storage::disk('public')->assertExists($job->filename); + + $json = Storage::disk('public')->get($job->filename); + $test = new AssertableJsonString($json); + + $test->assertStructure([ + 'account' => [ + 'uuid', + 'created_at', + 'updated_at', + 'data' => [ + '*' => [ + 'count', + 'type', + 'values' => [ + '*' => [ + 'uuid', + 'created_at', + 'updated_at', + 'properties', + ], + ], + ], + ], + 'properties' => [ + 'modules' => [ + '*' => [ + 'key', + 'translation_key', + 'created_at', + 'updated_at', + 'properties', + ], + ], + 'reminder_rules' => [ + '*' => [ + 'number_of_days_before', + 'created_at', + 'updated_at', + 'properties', + ], + ], + ], + 'instance' => [ + 'activity_types' => [ + '*' => [ + 'uuid', + 'created_at', + 'updated_at', + 'properties' => [ + 'name', + 'translation_key', + 'category', + ], + ], + ], + 'activity_type_categories' => [ + '*' => [ + 'uuid', + 'created_at', + 'updated_at', + 'properties' => [ + 'name', + 'translation_key', + ], + ], + ], + 'life_event_types' => [ + '*' => [ + 'uuid', + 'created_at', + 'updated_at', + 'properties' => [ + 'translation_key', + 'core_monica_data', + 'category', + ], + ], + ], + 'life_event_categories' => [ + '*' => [ + 'uuid', + 'created_at', + 'updated_at', + 'properties' => [ + 'translation_key', + 'core_monica_data', + ], + ], + ], + 'contact_field_types' => [ + '*' => [ + 'uuid', + 'created_at', + 'updated_at', + 'properties' => [ + 'name', + 'fontawesome_icon', + 'delible', + ], + ], + ], + ], + ], + ]); + } + + /** @test */ + public function it_exports_json_file_contacts() + { + Storage::fake(); + Storage::fake('local'); + + $job = ExportJob::factory()->create(); + factory(Contact::class, 5)->create([ + 'account_id' => $job->account->id, + ]); + + ExportAccount::dispatchSync($job); + + $job->refresh(); + + $this->assertStringStartsWith('exports/', $job->filename); + $this->assertStringEndsWith('.json', $job->filename); + Storage::disk('public')->assertExists($job->filename); + + $json = Storage::disk('public')->get($job->filename); + $test = new AssertableJsonString($json); + + $test->assertStructure([ + 'account' => [ + 'uuid', + 'created_at', + 'updated_at', + 'data' => [ + '*' => [ + 'count', + 'type', + 'values' => [ + '*' => [ + 'uuid', + 'created_at', + 'updated_at', + 'properties', + ], + ], + ], + ], + 'properties' => [ + 'modules' => [ + '*' => [ + 'key', + 'translation_key', + 'created_at', + 'updated_at', + 'properties', + ], + ], + 'reminder_rules' => [ + '*' => [ + 'number_of_days_before', + 'created_at', + 'updated_at', + 'properties', + ], + ], + ], + 'instance' => [ + 'activity_types' => [ + '*' => [ + 'uuid', + 'created_at', + 'updated_at', + 'properties' => [ + 'name', + 'translation_key', + 'category', + ], + ], + ], + 'activity_type_categories' => [ + '*' => [ + 'uuid', + 'created_at', + 'updated_at', + 'properties' => [ + 'name', + 'translation_key', + ], + ], + ], + 'life_event_types' => [ + '*' => [ + 'uuid', + 'created_at', + 'updated_at', + 'properties' => [ + 'translation_key', + 'core_monica_data', + 'category', + ], + ], + ], + 'life_event_categories' => [ + '*' => [ + 'uuid', + 'created_at', + 'updated_at', + 'properties' => [ + 'translation_key', + 'core_monica_data', + ], + ], + ], + 'contact_field_types' => [ + '*' => [ + 'uuid', + 'created_at', + 'updated_at', + 'properties' => [ + 'name', + 'fontawesome_icon', + 'delible', + ], + ], + ], + ], + ], + ]); + } +} diff --git a/tests/Unit/Services/Account/Settings/ResetAccountTest.php b/tests/Unit/Services/Account/Settings/ResetAccountTest.php new file mode 100644 index 0000000..6da12a0 --- /dev/null +++ b/tests/Unit/Services/Account/Settings/ResetAccountTest.php @@ -0,0 +1,67 @@ +create(); + $contacts = factory(Contact::class, 3)->create([ + 'account_id' => $user->account_id, + ]); + + $activityType = factory(ActivityType::class)->create([ + 'account_id' => $user->account_id, + ]); + + $request = [ + 'account_id' => $user->account_id, + 'activity_type_id' => $activityType->id, + 'summary' => 'we went to central perk', + 'description' => 'it was awesome', + 'happened_at' => '2009-09-09', + 'contacts' => $contacts->map(function ($contact) { + return $contact->id; + })->toArray(), + ]; + + app(CreateActivity::class)->execute($request); + + $request = [ + 'account_id' => $user->account_id, + ]; + + app(ResetAccount::class)->handle($request); + + $this->assertDatabaseMissing('contacts', [ + 'account_id' => $user->account_id, + 'deleted_at' => null, + ]); + $this->assertDatabaseMissing('activities', [ + 'account_id' => $user->account_id, + ]); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $request = []; + + $this->expectException(ValidationException::class); + app(ResetAccount::class)->handle($request); + } +} diff --git a/tests/Unit/Services/Account/Settings/SqlExportAccountTest.php b/tests/Unit/Services/Account/Settings/SqlExportAccountTest.php new file mode 100644 index 0000000..60bfde1 --- /dev/null +++ b/tests/Unit/Services/Account/Settings/SqlExportAccountTest.php @@ -0,0 +1,43 @@ +create([]); + + $request = [ + 'account_id' => $user->account_id, + 'user_id' => $user->id, + ]; + + $filename = app(SqlExportAccount::class)->execute($request); + + $this->assertStringStartsWith('temp/', $filename); + $this->assertStringEndsWith('.sql', $filename); + Storage::disk('local')->assertExists($filename); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $request = []; + + $this->expectException(ValidationException::class); + app(SqlExportAccount::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Auth/PopulateContactFieldTypesTableTest.php b/tests/Unit/Services/Auth/PopulateContactFieldTypesTableTest.php new file mode 100644 index 0000000..6a4b4fd --- /dev/null +++ b/tests/Unit/Services/Auth/PopulateContactFieldTypesTableTest.php @@ -0,0 +1,94 @@ + 1, + ]; + + $this->expectException(\Exception::class); + app(PopulateContactFieldTypesTable::class)->execute($request); + + $request = [ + 'migrate_existing_data' => false, + ]; + + $this->expectException(ValidationException::class); + app(PopulateContactFieldTypesTable::class)->execute($request); + } + + /** @test */ + public function it_populate_contact_field_types_tables() + { + $account = factory(Account::class)->create([]); + $user = factory(User::class)->create([ + 'account_id' => $account->id, + ]); + + $number = DB::table('contact_field_types') + ->where('account_id', $account->id) + ->count(); + + DB::table('default_contact_field_types') + ->where('name', 'Phone') + ->update(['migrated' => 0]); + + $request = [ + 'account_id' => $account->id, + 'migrate_existing_data' => false, + ]; + + app(PopulateContactFieldTypesTable::class)->execute($request); + + $this->assertEquals( + $number + 1, + DB::table('contact_field_types')->where('account_id', $account->id)->count() + ); + } + + /** @test */ + public function it_only_populates_partially() + { + $account = factory(Account::class)->create([]); + $user = factory(User::class)->create([ + 'account_id' => $account->id, + ]); + + $numberOfDefault = DB::table('default_contact_field_types') + ->count(); + + DB::table('default_contact_field_types') + ->update(['migrated' => 0]); + + $numberOfContactFieldTypesAssociatedWithAccount = DB::table('contact_field_types') + ->where('account_id', $account->id) + ->count(); + + $request = [ + 'account_id' => $account->id, + 'migrate_existing_data' => true, + ]; + + app(PopulateContactFieldTypesTable::class)->execute($request); + + $this->assertEquals( + $numberOfContactFieldTypesAssociatedWithAccount + $numberOfDefault, + DB::table('contact_field_types')->where('account_id', $account->id)->get()->count() + ); + } +} diff --git a/tests/Unit/Services/Auth/PopulateLifeEventsTableTest.php b/tests/Unit/Services/Auth/PopulateLifeEventsTableTest.php new file mode 100644 index 0000000..49b5fad --- /dev/null +++ b/tests/Unit/Services/Auth/PopulateLifeEventsTableTest.php @@ -0,0 +1,122 @@ + 1, + ]; + + $this->expectException(\Exception::class); + + app(PopulateLifeEventsTable::class)->execute($request); + + $request = [ + 'migrate_existing_data' => false, + ]; + + $this->expectException(ValidationException::class); + + app(PopulateLifeEventsTable::class)->execute($request); + } + + /** @test */ + public function it_populate_life_event_tables() + { + $account = factory(Account::class)->create([]); + $user = factory(User::class)->create([ + 'account_id' => $account->id, + ]); + + DB::table('default_life_event_categories') + ->where('translation_key', 'work_education') + ->update(['migrated' => 0]); + + $request = [ + 'account_id' => $account->id, + 'migrate_existing_data' => 1, + ]; + + app(PopulateLifeEventsTable::class)->execute($request); + + $this->assertEquals( + 5, + DB::table('life_event_categories')->where('account_id', $account->id)->get()->count() + ); + + $this->assertEquals( + 43, + DB::table('life_event_types')->where('account_id', $account->id)->get()->count() + ); + + // make sure tables have been set to migrated = 1 + $this->assertDatabaseMissing('default_life_event_categories', [ + 'migrated' => 0, + ]); + + $this->assertDatabaseMissing('default_life_event_types', [ + 'migrated' => 0, + ]); + } + + /** @test */ + public function it_refuses_to_populate_table_if_account_doesnt_have_locale() + { + $account = factory(Account::class)->create([]); + + $request = [ + 'account_id' => $account->id, + 'migrate_existing_data' => 0, + ]; + + $this->assertFalse(app(PopulateLifeEventsTable::class)->execute($request)); + } + + /** @test */ + public function it_only_populates_life_event_tables_partially() + { + $account = factory(Account::class)->create([]); + $user = factory(User::class)->create([ + 'account_id' => $account->id, + ]); + + DB::table('default_life_event_categories') + ->update(['migrated' => 0]); + + DB::table('default_life_event_categories') + ->where('translation_key', 'work_education') + ->update(['migrated' => 1]); + + // we will only migrate the ones that haven't been populated yet + $request = [ + 'account_id' => $account->id, + 'migrate_existing_data' => 0, + ]; + + app(PopulateLifeEventsTable::class)->execute($request); + + $this->assertEquals( + 4, + DB::table('life_event_categories')->where('account_id', $account->id)->get()->count() + ); + + $this->assertEquals( + 36, + DB::table('life_event_types')->where('account_id', $account->id)->get()->count() + ); + } +} diff --git a/tests/Unit/Services/Auth/PopulateModulesTableTest.php b/tests/Unit/Services/Auth/PopulateModulesTableTest.php new file mode 100644 index 0000000..bbb5e4f --- /dev/null +++ b/tests/Unit/Services/Auth/PopulateModulesTableTest.php @@ -0,0 +1,100 @@ + 1, + ]; + + $this->expectException(\Exception::class); + + $populateModulesService = new PopulateModulesTable; + $populateModulesService->execute($request); + + $request = [ + 'migrate_existing_data' => false, + ]; + + $this->expectException(ValidationException::class); + + app(PopulateModulesTable::class)->execute($request); + } + + /** @test */ + public function it_populate_modules_tables() + { + $account = factory(Account::class)->create([]); + $user = factory(User::class)->create([ + 'account_id' => $account->id, + ]); + + DB::table('default_contact_modules') + ->where('key', 'work_education') + ->update(['migrated' => 0]); + + $request = [ + 'account_id' => $account->id, + 'migrate_existing_data' => 1, + ]; + + app(PopulateModulesTable::class)->execute($request); + + // by defauult there is 18 columns in the default table. + // therefore, we need 18 entries for the new account. + $this->assertEquals( + 18, + DB::table('modules')->where('account_id', $account->id)->get()->count() + ); + + // make sure tables have been set to migrated = 1 + $this->assertDatabaseMissing('default_contact_modules', [ + 'migrated' => 0, + ]); + } + + /** @test */ + public function it_only_populates_module_tables_partially() + { + $account = factory(Account::class)->create([]); + $user = factory(User::class)->create([ + 'account_id' => $account->id, + ]); + + DB::table('default_contact_modules') + ->update(['migrated' => 0]); + + DB::table('default_contact_modules') + ->where('key', 'love_relationships') + ->update(['migrated' => 1]); + + // we will only migrate the ones that haven't been populated yet + $request = [ + 'account_id' => $account->id, + 'migrate_existing_data' => 0, + ]; + + app(PopulateModulesTable::class)->execute($request); + + // by defauult there is 18 columns in the default table. + // therefore, we need 17 entries for the new account. + $this->assertEquals( + 17, + DB::table('modules')->where('account_id', $account->id)->get()->count() + ); + } +} diff --git a/tests/Unit/Services/BaseServiceTest.php b/tests/Unit/Services/BaseServiceTest.php new file mode 100644 index 0000000..f08ba1e --- /dev/null +++ b/tests/Unit/Services/BaseServiceTest.php @@ -0,0 +1,121 @@ +getMockForAbstractClass(BaseService::class); + + $this->assertIsArray( + $stub->rules() + ); + } + + /** @test */ + public function it_validates_rules(): void + { + $rules = [ + 'street' => 'nullable|string|max:255', + ]; + + $stub = $this->getMockForAbstractClass(BaseService::class); + $stub->rules([$rules]); + + $this->assertTrue( + $stub->validate([ + 'street' => 'la rue du bonheur', + ]) + ); + } + + /** @test */ + public function it_returns_null_or_the_actual_value(): void + { + $stub = $this->getMockForAbstractClass(BaseService::class); + $array = [ + 'value' => 'this', + ]; + + $this->assertEquals( + 'this', + $stub->nullOrValue($array, 'value') + ); + + $array = [ + 'otherValue' => '', + ]; + + $this->assertNull( + $stub->nullOrValue($array, 'otherValue') + ); + + $array = []; + + $this->assertNull( + $stub->nullOrValue($array, 'value') + ); + } + + /** @test */ + public function it_returns_null_or_the_actual_date(): void + { + $stub = $this->getMockForAbstractClass(BaseService::class); + $array = [ + 'value' => '1990-01-01', + ]; + + $this->assertInstanceOf( + Carbon::class, + $stub->nullOrDate($array, 'value') + ); + + $array = [ + 'otherValue' => '', + ]; + + $this->assertNull( + $stub->nullOrDate($array, 'otherValue') + ); + + $array = []; + + $this->assertNull( + $stub->nullOrDate($array, 'value') + ); + } + + /** @test */ + public function it_returns_the_default_value_or_the_given_value(): void + { + $stub = $this->getMockForAbstractClass(BaseService::class); + $array = [ + 'value' => true, + ]; + + $this->assertTrue( + $stub->valueOrFalse($array, 'value') + ); + + $array = [ + 'value' => false, + ]; + + $this->assertFalse( + $stub->valueOrFalse($array, 'value') + ); + + $this->assertFalse( + $stub->valueOrFalse([], 'value') + ); + } +} diff --git a/tests/Unit/Services/Contact/Address/CreateAddressTest.php b/tests/Unit/Services/Contact/Address/CreateAddressTest.php new file mode 100644 index 0000000..3143ad7 --- /dev/null +++ b/tests/Unit/Services/Contact/Address/CreateAddressTest.php @@ -0,0 +1,112 @@ +create([]); + + $request = [ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'name' => 'work address', + 'street' => '199 Lafayette Street', + 'city' => 'New York City', + 'province' => '', + 'postal_code' => '', + 'country' => 'USA', + 'latitude' => '', + 'longitude' => '', + ]; + + $address = app(CreateAddress::class)->execute($request); + + $this->assertDatabaseHas('addresses', [ + 'id' => $address->id, + 'account_id' => $contact->account_id, + 'name' => 'work address', + ]); + + $this->assertEquals( + '199 Lafayette Street', + $address->place->street + ); + + $this->assertInstanceOf( + Address::class, + $address + ); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $account = factory(Account::class)->create([]); + + $request = [ + 'name' => '199 Lafayette Street', + ]; + + $this->expectException(ValidationException::class); + app(CreateAddress::class)->execute($request); + } + + /** @test */ + public function it_fails_if_contact_is_archived() + { + $contact = factory(Contact::class)->state('archived')->create(); + + $request = [ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'name' => 'work address', + 'street' => '199 Lafayette Street', + 'city' => 'New York City', + 'province' => '', + 'postal_code' => '', + 'country' => 'USA', + 'latitude' => '', + 'longitude' => '', + ]; + + $this->expectException(ValidationException::class); + app(CreateAddress::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_contact_is_not_linked_to_account() + { + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create(); + + $request = [ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + 'name' => 'work address', + 'street' => '199 Lafayette Street', + 'city' => 'New York City', + 'province' => '', + 'postal_code' => '', + 'country' => 'USA', + 'latitude' => '', + 'longitude' => '', + ]; + + $this->expectException(ModelNotFoundException::class); + app(CreateAddress::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Contact/Address/DestroyAddressTest.php b/tests/Unit/Services/Contact/Address/DestroyAddressTest.php new file mode 100644 index 0000000..d511a60 --- /dev/null +++ b/tests/Unit/Services/Contact/Address/DestroyAddressTest.php @@ -0,0 +1,78 @@ +create([]); + + $request = [ + 'account_id' => $address->account_id, + 'address_id' => $address->id, + ]; + + app(DestroyAddress::class)->execute($request); + + $this->assertDatabaseMissing('addresses', [ + 'id' => $address->id, + ]); + } + + /** @test */ + public function it_throws_an_exception_if_account_is_not_linked_to_address() + { + $contact = factory(Contact::class)->create([]); + $address = factory(Address::class)->create([]); + + $request = [ + 'account_id' => $contact->account_id, + 'address_id' => $address->id, + ]; + + $this->expectException(ModelNotFoundException::class); + app(DestroyAddress::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_ids_do_not_exist() + { + $request = [ + 'account_id' => 11111111, + 'address_id' => 11111111, + ]; + + $this->expectException(ValidationException::class); + app(DestroyAddress::class)->execute($request); + } + + /** @test */ + public function it_fails_if_contact_is_archived() + { + $contact = factory(Contact::class)->state('archived')->create(); + $address = factory(Address::class)->create([ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + ]); + + $request = [ + 'account_id' => $contact->account_id, + 'address_id' => $address->id, + ]; + + $this->expectException(ValidationException::class); + app(DestroyAddress::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Contact/Address/UpdateAddressTest.php b/tests/Unit/Services/Contact/Address/UpdateAddressTest.php new file mode 100644 index 0000000..68810ac --- /dev/null +++ b/tests/Unit/Services/Contact/Address/UpdateAddressTest.php @@ -0,0 +1,112 @@ +create([]); + + $request = [ + 'account_id' => $address->account_id, + 'contact_id' => $address->contact_id, + 'address_id' => $address->id, + 'name' => 'this is a test', + 'street' => '1990 Lafayette Street', + 'city' => 'New York City', + 'province' => '', + 'postal_code' => '', + 'country' => 'USA', + 'latitude' => '', + 'longitude' => '', + ]; + + $address = app(UpdateAddress::class)->execute($request); + + $this->assertDatabaseHas('addresses', [ + 'id' => $address->id, + 'account_id' => $address->account_id, + 'name' => 'this is a test', + ]); + + $this->assertEquals( + '1990 Lafayette Street', + $address->place->street + ); + + $this->assertInstanceOf( + Address::class, + $address + ); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $address = factory(Address::class)->create([]); + + $request = [ + 'street' => '199 Lafayette Street', + ]; + + $this->expectException(ValidationException::class); + app(UpdateAddress::class)->execute($request); + } + + /** @test */ + public function it_fails_if_contact_is_archived() + { + $contact = factory(Contact::class)->state('archived')->create(); + $address = factory(Address::class)->create([ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + ]); + + $request = [ + 'account_id' => $address->account_id, + 'contact_id' => $address->contact_id, + 'address_id' => $address->id, + 'name' => 'this is a test', + 'street' => '1990 Lafayette Street', + 'city' => 'New York City', + 'province' => '', + 'postal_code' => '', + 'country' => 'USA', + 'latitude' => '', + 'longitude' => '', + ]; + + $this->expectException(ValidationException::class); + app(UpdateAddress::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_address_is_not_linked_to_account() + { + $account = factory(Account::class)->create([]); + $contact = factory(Contact::class)->create([]); + $address = factory(Address::class)->create([]); + + $request = [ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + 'address_id' => $address->id, + ]; + + $this->expectException(ModelNotFoundException::class); + app(UpdateAddress::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Contact/Avatar/GenerateDefaultAvatarTest.php b/tests/Unit/Services/Contact/Avatar/GenerateDefaultAvatarTest.php new file mode 100644 index 0000000..076f857 --- /dev/null +++ b/tests/Unit/Services/Contact/Avatar/GenerateDefaultAvatarTest.php @@ -0,0 +1,67 @@ +create([ + 'default_avatar_color' => '#000', + ]); + + $request = [ + 'contact_id' => $contact->id, + ]; + + $contact = app(GenerateDefaultAvatar::class)->execute($request); + + $this->assertStringContainsString( + 'avatars/', + $contact->avatar_default_url + ); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $request = []; + + $this->expectException(ValidationException::class); + app(GenerateDefaultAvatar::class)->execute($request); + } + + /** @test */ + public function it_replaces_existing_default_avatar() + { + $file = UploadedFile::fake()->image('image.png'); + + $contact = factory(Contact::class)->create([ + 'default_avatar_color' => '#fff', + 'avatar_default_url' => $file->getPathname(), + ]); + + $this->assertFileExists($file->getPathname()); + + $request = [ + 'contact_id' => $contact->id, + ]; + + $contact = app(GenerateDefaultAvatar::class)->execute($request); + + $this->assertStringContainsString( + 'avatars/', + $contact->avatar_default_url + ); + } +} diff --git a/tests/Unit/Services/Contact/Avatar/GetAdorableAvatarTest.php b/tests/Unit/Services/Contact/Avatar/GetAdorableAvatarTest.php new file mode 100644 index 0000000..440ae5e --- /dev/null +++ b/tests/Unit/Services/Contact/Avatar/GetAdorableAvatarTest.php @@ -0,0 +1,56 @@ + 'matt@wordpress.com', + 'size' => 400, + ]; + + $url = app(GetAdorableAvatarURL::class)->execute($request); + + $this->assertEquals( + '400/matt@wordpress.com.png', + $url + ); + } + + /** @test */ + public function it_returns_an_url_with_a_default_avatar_size() + { + $request = [ + 'uuid' => 'matt@wordpress.com', + ]; + + $url = app(GetAdorableAvatarURL::class)->execute($request); + + // should return an avatar of 200 px wide + $this->assertEquals( + '200/matt@wordpress.com.png', + $url + ); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $request = [ + 'size' => 200, + ]; + + $this->expectException(ValidationException::class); + app(GetAdorableAvatarURL::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Contact/Avatar/GetAvatarsFromInternetTest.php b/tests/Unit/Services/Contact/Avatar/GetAvatarsFromInternetTest.php new file mode 100644 index 0000000..51fd103 --- /dev/null +++ b/tests/Unit/Services/Contact/Avatar/GetAvatarsFromInternetTest.php @@ -0,0 +1,113 @@ +create([]); + $contactFieldType = factory(ContactFieldType::class)->create([ + 'account_id' => $contact->account_id, + ]); + factory(ContactField::class)->create([ + 'contact_id' => $contact->id, + 'account_id' => $contact->account_id, + 'contact_field_type_id' => $contactFieldType->id, + 'data' => 'matt@wordpress.com', + ]); + + $request = [ + 'contact_id' => $contact->id, + ]; + + $contact = app(GetAvatarsFromInternet::class)->execute($request); + + $this->assertInstanceOf( + Contact::class, + $contact + ); + + $this->assertNotNull( + $contact->avatar_adorable_url + ); + + $this->assertNotNull( + $contact->avatar_gravatar_url + ); + } + + /** @test */ + public function gravatar_is_null_if_contact_doesnt_have_an_email() + { + $contact = factory(Contact::class)->create([]); + + $request = [ + 'contact_id' => $contact->id, + ]; + + $contact = app(GetAvatarsFromInternet::class)->execute($request); + + $this->assertNull( + $contact->avatar_gravatar_url + ); + } + + /** @test */ + public function avatar_source_is_reset_and_set_to_adorable_if_gravatar_doesnt_exist_anymore() + { + $contact = factory(Contact::class)->create([ + 'avatar_source' => 'gravatar', + ]); + $contactFieldType = factory(ContactFieldType::class)->create([ + 'account_id' => $contact->account_id, + ]); + $contactField = factory(ContactField::class)->create([ + 'contact_id' => $contact->id, + 'account_id' => $contact->account_id, + 'contact_field_type_id' => $contactFieldType->id, + 'data' => 'matt@wordpress.com', + ]); + + $request = [ + 'contact_id' => $contact->id, + ]; + + $contact = app(GetAvatarsFromInternet::class)->execute($request); + + // now we call the service again to reset the gravatar url + $contactField->delete(); + $contact = app(GetAvatarsFromInternet::class)->execute($request); + + $this->assertNull( + $contact->avatar_gravatar_url + ); + + $this->assertEquals( + 'adorable', + $contact->avatar_source + ); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $request = [ + 'size' => 200, + ]; + + $this->expectException(ValidationException::class); + app(GetAvatarsFromInternet::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Contact/Avatar/GetGravatarTest.php b/tests/Unit/Services/Contact/Avatar/GetGravatarTest.php new file mode 100644 index 0000000..f21207e --- /dev/null +++ b/tests/Unit/Services/Contact/Avatar/GetGravatarTest.php @@ -0,0 +1,145 @@ +create(); + $contactFieldType = factory(ContactFieldType::class)->create([ + 'account_id' => $contact->account->id, + ]); + factory(ContactField::class)->create([ + 'contact_id' => $contact->id, + 'account_id' => $contact->account->id, + 'contact_field_type_id' => $contactFieldType->id, + 'data' => 'matt@wordpress.com', + ]); + + $request = [ + 'contact_id' => $contact->id, + ]; + + $contact = app(GetGravatar::class)->execute($request); + + $this->assertNotNull( + $contact->avatar_gravatar_url + ); + } + + /** @test */ + public function it_get_gravatar_of_real_email() + { + $contact = factory(Contact::class)->create(); + $contactFieldType = factory(ContactFieldType::class)->create([ + 'account_id' => $contact->account->id, + ]); + factory(ContactField::class)->create([ + 'contact_id' => $contact->id, + 'account_id' => $contact->account->id, + 'contact_field_type_id' => $contactFieldType->id, + 'data' => 'bademail', + ]); + factory(ContactField::class)->create([ + 'contact_id' => $contact->id, + 'account_id' => $contact->account->id, + 'contact_field_type_id' => $contactFieldType->id, + 'data' => 'matt@wordpress.com', + ]); + + $request = [ + 'contact_id' => $contact->id, + ]; + + $contact = app(GetGravatar::class)->execute($request); + + $this->assertNotNull( + $contact->avatar_gravatar_url + ); + } + + /** @test */ + public function it_returns_an_url() + { + $request = [ + 'email' => 'matt@wordpress.com', + 'size' => 400, + ]; + + $url = app(GetGravatarURL::class)->execute($request); + + $this->assertEquals( + 'https://www.gravatar.com/avatar/5bbc9048a99ec78cdbc227770e707efb.jpg?s=400&d=404&r=g', + $url + ); + } + + /** @test */ + public function it_returns_an_url_with_a_small_avatar_size() + { + $request = [ + 'email' => 'matt@wordpress.com', + 'size' => 80, + ]; + + $url = app(GetGravatarURL::class)->execute($request); + + $this->assertEquals( + 'https://www.gravatar.com/avatar/5bbc9048a99ec78cdbc227770e707efb.jpg?s=80&d=404&r=g', + $url + ); + } + + /** @test */ + public function it_returns_an_url_with_a_default_avatar_size() + { + $request = [ + 'email' => 'matt@wordpress.com', + ]; + + $url = app(GetGravatarURL::class)->execute($request); + + // should return an avatar of 200 px wide + $this->assertEquals( + 'https://www.gravatar.com/avatar/5bbc9048a99ec78cdbc227770e707efb.jpg?s=200&d=404&r=g', + $url + ); + } + + /** @test */ + public function it_returns_null_if_no_avatar_is_found() + { + $request = [ + 'email' => 'jlskjdfl@dskfjlsd.com', + ]; + + // should return an avatar of 200 px wide + $this->assertNull( + app(GetGravatarURL::class)->execute($request) + ); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $request = [ + 'size' => 200, + ]; + + $this->expectException(ValidationException::class); + $url = app(GetGravatarURL::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Contact/Avatar/UpdateAvatarTest.php b/tests/Unit/Services/Contact/Avatar/UpdateAvatarTest.php new file mode 100644 index 0000000..2f0ae44 --- /dev/null +++ b/tests/Unit/Services/Contact/Avatar/UpdateAvatarTest.php @@ -0,0 +1,192 @@ +create([]); + + $request = [ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'source' => 'gravatar', + ]; + + $contact = app(UpdateAvatar::class)->execute($request); + + $this->assertDatabaseHas('contacts', [ + 'id' => $contact->id, + 'avatar_source' => 'gravatar', + ]); + + $this->assertInstanceOf( + Contact::class, + $contact + ); + } + + /** @test */ + public function it_updates_the_avatar_with_default_avatar() + { + $contact = factory(Contact::class)->create([]); + + $request = [ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'source' => 'default', + ]; + + $contact = app(UpdateAvatar::class)->execute($request); + + $this->assertDatabaseHas('contacts', [ + 'id' => $contact->id, + 'avatar_source' => 'default', + ]); + + $this->assertInstanceOf( + Contact::class, + $contact + ); + } + + /** @test */ + public function it_updates_the_avatar_with_adorable() + { + $contact = factory(Contact::class)->create([]); + + $request = [ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'source' => 'adorable', + ]; + + $contact = app(UpdateAvatar::class)->execute($request); + + $this->assertDatabaseHas('contacts', [ + 'id' => $contact->id, + 'avatar_source' => 'adorable', + ]); + + $this->assertInstanceOf( + Contact::class, + $contact + ); + } + + /** @test */ + public function it_updates_the_avatar_with_existing_photo() + { + $contact = factory(Contact::class)->create([]); + $photo = factory(Photo::class)->create([ + 'account_id' => $contact->account_id, + ]); + + $request = [ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'source' => 'photo', + 'photo_id' => $photo->id, + ]; + + $contact = app(UpdateAvatar::class)->execute($request); + + $this->assertDatabaseHas('contacts', [ + 'id' => $contact->id, + 'avatar_source' => 'photo', + 'avatar_photo_id' => $photo->id, + ]); + + $this->assertInstanceOf( + Contact::class, + $contact + ); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $contact = factory(Contact::class)->create([]); + + $request = [ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + ]; + + $this->expectException(ValidationException::class); + app(UpdateAvatar::class)->execute($request); + } + + /** @test */ + public function it_fails_if_contact_is_archived() + { + $contact = factory(Contact::class)->state('archived')->create([]); + + $request = [ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'source' => 'gravatar', + ]; + + $this->expectException(ValidationException::class); + app(UpdateAvatar::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_contact_not_linked_to_account() + { + $account = factory(Account::class)->create([]); + $contact = factory(Contact::class)->create([]); + + $request = [ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + 'source' => 'adorable', + ]; + + $this->expectException(ModelNotFoundException::class); + app(UpdateAvatar::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_photo_not_linked_to_account() + { + // Case: photo doesn't exist + $contact = factory(Contact::class)->create([]); + + $request = [ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'source' => 'photo', + 'photo_id' => 0, + ]; + + $this->expectException(ValidationException::class); + $contact = app(UpdateAvatar::class)->execute($request); + + // Case: photo exists but belongs to another account + $photo = factory(Photo::class)->create(); + $request = [ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'source' => 'photo', + 'photo_id' => $photo->id, + ]; + + $this->expectException(ModelNotFoundException::class); + app(UpdateAvatar::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Contact/Call/CreateCallTest.php b/tests/Unit/Services/Contact/Call/CreateCallTest.php new file mode 100644 index 0000000..ec16bd2 --- /dev/null +++ b/tests/Unit/Services/Contact/Call/CreateCallTest.php @@ -0,0 +1,250 @@ +create([]); + + $request = [ + 'contact_id' => $contact->id, + 'account_id' => $contact->account_id, + 'called_at' => now(), + 'content' => 'this is the content', + ]; + + $call = app(CreateCall::class)->execute($request); + + $this->assertDatabaseHas('calls', [ + 'id' => $call->id, + 'contact_id' => $contact->id, + 'account_id' => $contact->account_id, + 'content' => 'this is the content', + 'contact_called' => 0, + ]); + + $this->assertInstanceOf( + Call::class, + $call + ); + } + + /** @test */ + public function it_stores_a_call_and_who_called_information() + { + $contact = factory(Contact::class)->create([]); + + $request = [ + 'contact_id' => $contact->id, + 'account_id' => $contact->account_id, + 'called_at' => now(), + 'content' => 'this is the content', + 'contact_called' => true, + ]; + + $call = app(CreateCall::class)->execute($request); + + $this->assertDatabaseHas('calls', [ + 'id' => $call->id, + 'contact_id' => $contact->id, + 'account_id' => $contact->account_id, + 'content' => 'this is the content', + 'contact_called' => 1, + ]); + } + + /** @test */ + public function it_adds_emotions() + { + $contact = factory(Contact::class)->create([]); + $emotion = factory(Emotion::class)->create([]); + $emotion2 = factory(Emotion::class)->create([]); + + $emotionArray = []; + $emotionArray[] = $emotion->id; + $emotionArray[] = $emotion2->id; + + $request = [ + 'contact_id' => $contact->id, + 'account_id' => $contact->account_id, + 'called_at' => now(), + 'content' => 'this is the content', + 'contact_called' => true, + 'emotions' => $emotionArray, + ]; + + $call = app(CreateCall::class)->execute($request); + + $this->assertDatabaseHas('calls', [ + 'id' => $call->id, + 'contact_id' => $contact->id, + 'account_id' => $contact->account_id, + 'content' => 'this is the content', + 'contact_called' => 1, + ]); + + $this->assertDatabaseHas('emotion_call', [ + 'contact_id' => $contact->id, + 'account_id' => $contact->account_id, + 'call_id' => $call->id, + 'emotion_id' => $emotion->id, + ]); + + $this->assertDatabaseHas('emotion_call', [ + 'contact_id' => $contact->id, + 'account_id' => $contact->account_id, + 'call_id' => $call->id, + 'emotion_id' => $emotion2->id, + ]); + } + + /** @test */ + public function it_fails_adding_emotions_when_emotion_is_unknown() + { + $contact = factory(Contact::class)->create([]); + $emotionArray = []; + $emotionArray[] = 1111111; + + $request = [ + 'contact_id' => $contact->id, + 'account_id' => $contact->account_id, + 'called_at' => now(), + 'content' => 'this is the content', + 'contact_called' => true, + 'emotions' => $emotionArray, + ]; + + $this->expectException(ModelNotFoundException::class); + + app(CreateCall::class)->execute($request); + } + + /** @test */ + public function it_stores_a_call_without_the_content() + { + $contact = factory(Contact::class)->create([]); + + $request = [ + 'contact_id' => $contact->id, + 'account_id' => $contact->account_id, + 'called_at' => now(), + ]; + + $call = app(CreateCall::class)->execute($request); + + $this->assertDatabaseHas('calls', [ + 'id' => $call->id, + 'contact_id' => $contact->id, + 'account_id' => $contact->account_id, + 'content' => null, + ]); + } + + /** @test */ + public function it_updates_the_last_call_info() + { + $contact = factory(Contact::class)->create([ + 'last_talked_to' => '1900-01-01 00:00:00', + ]); + + $date = now(); + + $request = [ + 'contact_id' => $contact->id, + 'account_id' => $contact->account_id, + 'called_at' => $date, + ]; + + app(CreateCall::class)->execute($request); + + $this->assertDatabaseHas('contacts', [ + 'id' => $contact->id, + 'last_talked_to' => $date->toDateString(), + ]); + } + + /** @test */ + public function it_doesnt_update_the_last_call_info() + { + $contact = factory(Contact::class)->create([ + 'last_talked_to' => '2200-01-01 00:00:00', + ]); + + $date = now(); + + $request = [ + 'contact_id' => $contact->id, + 'account_id' => $contact->account_id, + 'called_at' => $date, + ]; + + app(CreateCall::class)->execute($request); + + $this->assertDatabaseHas('contacts', [ + 'id' => $contact->id, + 'last_talked_to' => '2200-01-01', + ]); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $contact = factory(Contact::class)->create([]); + + $request = [ + 'contact_id' => $contact->id, + 'called_at' => now(), + ]; + + $this->expectException(ValidationException::class); + app(CreateCall::class)->execute($request); + } + + /** @test */ + public function it_fails_if_contact_is_archived() + { + $contact = factory(Contact::class)->state('archived')->create([]); + + $request = [ + 'contact_id' => $contact->id, + 'account_id' => $contact->account_id, + 'called_at' => now(), + 'content' => 'this is the content', + ]; + + $this->expectException(ValidationException::class); + app(CreateCall::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_contact_is_not_linked_to_account() + { + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create(); + + $request = [ + 'contact_id' => $contact->id, + 'account_id' => $account->id, + 'called_at' => now(), + 'content' => 'this is the content', + ]; + + $this->expectException(ModelNotFoundException::class); + app(CreateCall::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Contact/Call/DestroyCallTest.php b/tests/Unit/Services/Contact/Call/DestroyCallTest.php new file mode 100644 index 0000000..1d79c78 --- /dev/null +++ b/tests/Unit/Services/Contact/Call/DestroyCallTest.php @@ -0,0 +1,148 @@ +create([]); + $call = factory(Call::class)->create([ + 'contact_id' => $contact->id, + 'called_at' => '2008-01-01', + ]); + + $request = [ + 'account_id' => $call->account_id, + 'call_id' => $call->id, + ]; + + $this->assertDatabaseHas('calls', [ + 'id' => $call->id, + ]); + + app(DestroyCall::class)->execute($request); + + $this->assertDatabaseMissing('calls', [ + 'id' => $call->id, + ]); + } + + /** @test */ + public function it_removes_emotions() + { + $contact = factory(Contact::class)->create([]); + $call = factory(Call::class)->create([ + 'contact_id' => $contact->id, + ]); + + $emotion = factory(Emotion::class)->create([]); + + DB::table('emotion_call')->insert([ + 'account_id' => $call->account_id, + 'contact_id' => $call->contact_id, + 'call_id' => $call->id, + 'emotion_id' => $emotion->id, + ]); + + $request = [ + 'account_id' => $call->account_id, + 'call_id' => $call->id, + ]; + + app(DestroyCall::class)->execute($request); + + $this->assertDatabaseMissing('emotion_call', [ + 'contact_id' => $call->contact_id, + 'account_id' => $call->account_id, + 'call_id' => $call->id, + 'emotion_id' => $emotion->id, + ]); + } + + /** @test */ + public function it_updates_the_last_talked_to_information() + { + $contact = factory(Contact::class)->create([ + 'last_talked_to' => '2008-01-01', + ]); + $call = factory(Call::class)->create([ + 'contact_id' => $contact->id, + 'called_at' => '2008-01-01', + ]); + $call2 = factory(Call::class)->create([ + 'contact_id' => $contact->id, + 'called_at' => '1990-01-01', + ]); + $call3 = factory(Call::class)->create([ + 'contact_id' => $contact->id, + 'called_at' => '1980-01-01', + ]); + + $request = [ + 'account_id' => $call->account_id, + 'call_id' => $call->id, + ]; + + app(DestroyCall::class)->execute($request); + + $this->assertDatabaseHas('contacts', [ + 'id' => $contact->id, + 'last_talked_to' => '1990-01-01', + ]); + } + + /** @test */ + public function it_doesnt_update_the_last_talked_to_information() + { + $contact = factory(Contact::class)->create([ + 'last_talked_to' => '2008-01-01', + ]); + $call = factory(Call::class)->create([ + 'contact_id' => $contact->id, + 'called_at' => '2008-01-01', + ]); + + $request = [ + 'account_id' => $call->account_id, + 'call_id' => $call->id, + ]; + + app(DestroyCall::class)->execute($request); + + $this->assertDatabaseHas('contacts', [ + 'id' => $contact->id, + 'last_talked_to' => null, + ]); + } + + /** @test */ + public function it_fails_if_contact_is_archived() + { + $contact = factory(Contact::class)->state('archived')->create([]); + $call = factory(Call::class)->create([ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + ]); + + $request = [ + 'account_id' => $call->account_id, + 'call_id' => $call->id, + ]; + + $this->expectException(ValidationException::class); + app(DestroyCall::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Contact/Call/UpdateCallTest.php b/tests/Unit/Services/Contact/Call/UpdateCallTest.php new file mode 100644 index 0000000..d3a624a --- /dev/null +++ b/tests/Unit/Services/Contact/Call/UpdateCallTest.php @@ -0,0 +1,333 @@ +create([]); + $call = factory(Call::class)->create([ + 'contact_id' => $contact, + 'account_id' => $contact->account_id, + ]); + + $request = [ + 'account_id' => $call->account_id, + 'call_id' => $call->id, + 'called_at' => now(), + 'content' => 'this is the content', + ]; + + $call = app(UpdateCall::class)->execute($request); + + $this->assertDatabaseHas('calls', [ + 'id' => $call->id, + 'contact_id' => $call->contact_id, + 'account_id' => $call->contact->account_id, + 'content' => 'this is the content', + ]); + + $this->assertInstanceOf( + Call::class, + $call + ); + } + + /** @test */ + public function it_updates_a_call_and_who_called_info() + { + $contact = factory(Contact::class)->create([]); + $call = factory(Call::class)->create([ + 'contact_id' => $contact, + 'account_id' => $contact->account_id, + 'contact_called' => 0, + ]); + + $request = [ + 'account_id' => $call->account_id, + 'call_id' => $call->id, + 'called_at' => now(), + 'content' => 'this is the content', + 'contact_called' => 1, + ]; + + $call = app(UpdateCall::class)->execute($request); + + $this->assertDatabaseHas('calls', [ + 'id' => $call->id, + 'contact_id' => $call->contact_id, + 'account_id' => $call->contact->account_id, + 'content' => 'this is the content', + 'contact_called' => 1, + ]); + } + + /** @test */ + public function it_updates_a_call_without_the_content() + { + $contact = factory(Contact::class)->create([]); + $call = factory(Call::class)->create([ + 'contact_id' => $contact, + 'account_id' => $contact->account_id, + ]); + + $request = [ + 'account_id' => $call->account_id, + 'call_id' => $call->id, + 'called_at' => now(), + ]; + + $call = app(UpdateCall::class)->execute($request); + + $this->assertDatabaseHas('calls', [ + 'id' => $call->id, + 'contact_id' => $call->contact_id, + 'account_id' => $call->contact->account_id, + 'content' => null, + ]); + } + + /** + * Checks that it adds new emotions. + */ + + /** @test */ + public function it_updates_emotions() + { + $contact = factory(Contact::class)->create([]); + $call = factory(Call::class)->create([ + 'contact_id' => $contact->id, + ]); + $emotion = factory(Emotion::class)->create([]); + $emotion2 = factory(Emotion::class)->create([]); + + DB::table('emotion_call')->insert([ + 'account_id' => $call->account_id, + 'contact_id' => $call->contact_id, + 'call_id' => $call->id, + 'emotion_id' => $emotion->id, + ]); + + $emotionArray = []; + $emotionArray[] = $emotion->id; + $emotionArray[] = $emotion2->id; + + $request = [ + 'account_id' => $call->account_id, + 'call_id' => $call->id, + 'called_at' => now(), + 'content' => 'this is the content', + 'contact_called' => 1, + 'emotions' => $emotionArray, + ]; + + $call = app(UpdateCall::class)->execute($request); + + $this->assertDatabaseHas('emotion_call', [ + 'contact_id' => $call->contact_id, + 'account_id' => $call->account_id, + 'call_id' => $call->id, + 'emotion_id' => $emotion->id, + ]); + + $this->assertDatabaseHas('emotion_call', [ + 'contact_id' => $call->contact_id, + 'account_id' => $call->account_id, + 'call_id' => $call->id, + 'emotion_id' => $emotion2->id, + ]); + } + + /** + * Checks that it removes old emotion and add new emotions. + */ + + /** @test */ + public function it_deletes_and_updates_emotions() + { + $contact = factory(Contact::class)->create([]); + $call = factory(Call::class)->create([ + 'contact_id' => $contact->id, + ]); + $emotion = factory(Emotion::class)->create([]); + $emotion2 = factory(Emotion::class)->create([]); + + DB::table('emotion_call')->insert([ + 'account_id' => $call->account_id, + 'contact_id' => $call->contact_id, + 'call_id' => $call->id, + 'emotion_id' => $emotion->id, + ]); + + DB::table('emotion_call')->insert([ + 'account_id' => $call->account_id, + 'contact_id' => $call->contact_id, + 'call_id' => $call->id, + 'emotion_id' => $emotion2->id, + ]); + + $emotion3 = factory(Emotion::class)->create([]); + $emotion4 = factory(Emotion::class)->create([]); + $emotionArray = []; + $emotionArray[] = $emotion3->id; + $emotionArray[] = $emotion4->id; + + $request = [ + 'account_id' => $call->account_id, + 'call_id' => $call->id, + 'called_at' => now(), + 'content' => 'this is the content', + 'contact_called' => 1, + 'emotions' => $emotionArray, + ]; + + $call = app(UpdateCall::class)->execute($request); + + $this->assertDatabaseHas('emotion_call', [ + 'contact_id' => $call->contact_id, + 'account_id' => $call->account_id, + 'call_id' => $call->id, + 'emotion_id' => $emotion3->id, + ]); + + $this->assertDatabaseHas('emotion_call', [ + 'contact_id' => $call->contact_id, + 'account_id' => $call->account_id, + 'call_id' => $call->id, + 'emotion_id' => $emotion4->id, + ]); + + $this->assertDatabaseMissing('emotion_call', [ + 'contact_id' => $call->contact_id, + 'account_id' => $call->account_id, + 'call_id' => $call->id, + 'emotion_id' => $emotion->id, + ]); + + $this->assertDatabaseMissing('emotion_call', [ + 'contact_id' => $call->contact_id, + 'account_id' => $call->account_id, + 'call_id' => $call->id, + 'emotion_id' => $emotion2->id, + ]); + } + + /** @test */ + public function it_updates_the_last_call_info() + { + $contact = factory(Contact::class)->create([ + 'last_talked_to' => '1900-01-01 00:00:00', + ]); + $call = factory(Call::class)->create([ + 'contact_id' => $contact, + 'account_id' => $contact->account_id, + ]); + + $date = now(); + + $request = [ + 'account_id' => $call->account_id, + 'call_id' => $call->id, + 'called_at' => now(), + ]; + + app(UpdateCall::class)->execute($request); + + $this->assertDatabaseHas('contacts', [ + 'id' => $contact->id, + 'last_talked_to' => $date->toDateString(), + ]); + } + + /** @test */ + public function it_doesnt_update_the_last_call_info() + { + $contact = factory(Contact::class)->create([ + 'last_talked_to' => '2200-01-01 00:00:00', + ]); + $call = factory(Call::class)->create([ + 'contact_id' => $contact, + 'account_id' => $contact->account_id, + ]); + + $date = now(); + + $request = [ + 'account_id' => $call->account_id, + 'call_id' => $call->id, + 'called_at' => now(), + ]; + + app(UpdateCall::class)->execute($request); + + $this->assertDatabaseHas('contacts', [ + 'id' => $contact->id, + 'last_talked_to' => '2200-01-01', + ]); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $contact = factory(Contact::class)->create([]); + + $request = [ + 'contact_id' => $contact->id, + 'called_at' => now(), + ]; + + $this->expectException(ValidationException::class); + app(UpdateCall::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_call_is_not_linked_to_account() + { + $account = factory(Account::class)->create(); + $call = factory(Call::class)->create(); + + $request = [ + 'account_id' => $account->id, + 'call_id' => $call->id, + 'called_at' => now(), + ]; + + $this->expectException(ModelNotFoundException::class); + app(UpdateCall::class)->execute($request); + } + + /** @test */ + public function it_fails_if_contact_is_archived() + { + $contact = factory(Contact::class)->state('archived')->create([]); + $call = factory(Call::class)->create([ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + ]); + + $request = [ + 'account_id' => $call->account_id, + 'call_id' => $call->id, + 'called_at' => now(), + 'content' => 'this is the content', + ]; + + $this->expectException(ValidationException::class); + app(UpdateCall::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Contact/Contact/CreateContactTest.php b/tests/Unit/Services/Contact/Contact/CreateContactTest.php new file mode 100644 index 0000000..fd27956 --- /dev/null +++ b/tests/Unit/Services/Contact/Contact/CreateContactTest.php @@ -0,0 +1,232 @@ +create([]); + $user = factory(User::class)->create([ + 'account_id' => $account->id, + ]); + $gender = factory(Gender::class)->create([ + 'account_id' => $account->id, + ]); + + $request = [ + 'account_id' => $account->id, + 'author_id' => $user->id, + 'first_name' => 'john', + 'middle_name' => 'franck', + 'last_name' => 'doe', + 'gender_id' => $gender->id, + 'description' => 'this is a test', + 'is_partial' => false, + 'is_birthdate_known' => false, + 'is_deceased' => false, + 'is_deceased_date_known' => false, + ]; + + $contact = app(CreateContact::class)->execute($request); + + $this->assertDatabaseHas('contacts', [ + 'id' => $contact->id, + 'account_id' => $contact->account_id, + 'first_name' => 'john', + ]); + + // check that a default color has been set + $this->assertNotNull($contact->default_avatar_color); + + // check that the default avatar has been generated + $this->assertNotNull($contact->avatar_adorable_uuid); + $this->assertNotNull($contact->avatar_adorable_url); + $this->assertNotNull($contact->avatar_default_url); + $this->assertInstanceOf( + Contact::class, + $contact + ); + } + + /** @test */ + public function it_stores_a_contact_with_email() + { + $account = factory(Account::class)->create([]); + $user = factory(User::class)->create([ + 'account_id' => $account->id, + ]); + + factory(ContactFieldType::class)->create([ + 'account_id' => $account->id, + 'type' => 'email', + ]); + + $request = [ + 'account_id' => $account->id, + 'author_id' => $user->id, + 'first_name' => 'john', + 'last_name' => 'doe', + 'email' => 'email@example.com', + + 'is_birthdate_known' => false, + 'is_deceased' => false, + 'is_deceased_date_known' => false, + ]; + + $contact = app(CreateContact::class)->execute($request); + + $this->assertDatabaseHas('contacts', [ + 'id' => $contact->id, + 'account_id' => $contact->account_id, + 'first_name' => 'john', + ]); + + $this->assertDatabaseHas('contact_fields', [ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + 'data' => 'email@example.com', + ]); + } + + /** @test */ + public function it_stores_a_contact_and_triggers_an_audit_log() + { + Queue::fake(); + + $account = factory(Account::class)->create([]); + $user = factory(User::class)->create([ + 'account_id' => $account->id, + ]); + $gender = factory(Gender::class)->create([ + 'account_id' => $account->id, + ]); + + $request = [ + 'account_id' => $account->id, + 'author_id' => $user->id, + 'first_name' => 'john', + 'middle_name' => 'franck', + 'last_name' => 'doe', + 'gender_id' => $gender->id, + 'description' => 'this is a test', + 'is_partial' => false, + 'is_birthdate_known' => false, + 'is_deceased' => false, + 'is_deceased_date_known' => false, + ]; + + $contact = app(CreateContact::class)->execute($request); + + Queue::assertPushed(LogAccountAudit::class, function ($job) use ($contact, $user) { + return $job->auditLog['action'] === 'contact_created' && + $job->auditLog['author_id'] === $user->id && + $job->auditLog['about_contact_id'] === $contact->id && + $job->auditLog['should_appear_on_dashboard'] === true && + $job->auditLog['objects'] === json_encode([ + 'contact_name' => $contact->name, + 'contact_id' => $contact->id, + ]); + }); + } + + /** @test */ + public function it_stores_a_contact_without_gender() + { + $account = factory(Account::class)->create([]); + $user = factory(User::class)->create([ + 'account_id' => $account->id, + ]); + $gender = factory(Gender::class)->create([ + 'account_id' => $account->id, + ]); + + $request = [ + 'account_id' => $account->id, + 'author_id' => $user->id, + 'first_name' => 'john', + 'middle_name' => 'franck', + 'last_name' => 'doe', + 'description' => 'this is a test', + 'is_partial' => false, + 'is_birthdate_known' => false, + 'is_deceased' => false, + 'is_deceased_date_known' => false, + ]; + + $contact = app(CreateContact::class)->execute($request); + + $this->assertDatabaseHas('contacts', [ + 'id' => $contact->id, + 'account_id' => $contact->account_id, + 'first_name' => 'john', + 'gender_id' => null, + ]); + + $this->assertInstanceOf( + Contact::class, + $contact + ); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $account = factory(Account::class)->create([]); + $gender = factory(Gender::class)->create([ + 'account_id' => $account->id, + ]); + + $request = [ + 'account_id' => $account->id, + 'middle_name' => 'franck', + 'last_name' => 'doe', + 'gender_id' => $gender->id, + 'description' => 'this is a test', + 'is_partial' => false, + 'is_birthdate_known' => false, + 'is_deceased' => false, + 'is_deceased_date_known' => false, + ]; + + $this->expectException(ValidationException::class); + app(CreateContact::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_account_doesnt_exist() + { + $gender = factory(Gender::class)->create([]); + + $request = [ + 'account_id' => 111111111, + 'middle_name' => 'franck', + 'last_name' => 'doe', + 'gender_id' => $gender->id, + 'description' => 'this is a test', + 'is_partial' => false, + 'is_birthdate_known' => false, + 'is_deceased' => false, + 'is_deceased_date_known' => false, + ]; + + $this->expectException(ValidationException::class); + app(CreateContact::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Contact/Contact/DeleteMeContactTest.php b/tests/Unit/Services/Contact/Contact/DeleteMeContactTest.php new file mode 100644 index 0000000..1b0dc5c --- /dev/null +++ b/tests/Unit/Services/Contact/Contact/DeleteMeContactTest.php @@ -0,0 +1,70 @@ +create(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account->id, + ]); + $user->me_contact_id = $contact->id; + $user->save(); + + $request = [ + 'account_id' => $user->account->id, + 'user_id' => $user->id, + ]; + + $user = app(DeleteMeContact::class)->execute($request); + + $this->assertDatabaseHas('users', [ + 'id' => $user->id, + 'account_id' => $user->account->id, + 'me_contact_id' => null, + ]); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $account = factory(Account::class)->create(); + + $request = [ + 'account_id' => $account->id, + 'user_id' => 0, + ]; + + $this->expectException(ValidationException::class); + app(DeleteMeContact::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_contact_not_found() + { + $account = factory(Account::class)->create(); + $user = factory(User::class)->create(); + + $request = [ + 'account_id' => $account->id, + 'user_id' => $user->id, + ]; + + $this->expectException(ModelNotFoundException::class); + app(DeleteMeContact::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Contact/Contact/DestroyContactTest.php b/tests/Unit/Services/Contact/Contact/DestroyContactTest.php new file mode 100644 index 0000000..86a5908 --- /dev/null +++ b/tests/Unit/Services/Contact/Contact/DestroyContactTest.php @@ -0,0 +1,76 @@ +create([]); + + $request = [ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + ]; + + app(DestroyContact::class)->handle($request); + + $this->assertDatabaseMissing('contacts', [ + 'id' => $contact->id, + 'deleted_at' => null, + ]); + } + + /** @test */ + public function it_fails_if_contact_is_archived() + { + $contact = factory(Contact::class)->state('archived')->create([]); + + $request = [ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + ]; + + $this->expectException(ValidationException::class); + app(DestroyContact::class)->handle($request); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $contact = factory(Contact::class)->create([]); + + $request = [ + 'account_id' => $contact->account_id, + ]; + + $this->expectException(ValidationException::class); + app(DestroyContact::class)->handle($request); + } + + /** @test */ + public function it_throws_an_exception_if_contact_doesnt_exist() + { + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create([]); + + $request = [ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + ]; + + $this->expectException(ModelNotFoundException::class); + app(DestroyContact::class)->handle($request); + } +} diff --git a/tests/Unit/Services/Contact/Contact/SetMeContactTest.php b/tests/Unit/Services/Contact/Contact/SetMeContactTest.php new file mode 100644 index 0000000..9334397 --- /dev/null +++ b/tests/Unit/Services/Contact/Contact/SetMeContactTest.php @@ -0,0 +1,73 @@ +create(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $request = [ + 'account_id' => $user->account_id, + 'user_id' => $user->id, + 'contact_id' => $contact->id, + ]; + + $user = app(SetMeContact::class)->execute($request); + + $this->assertDatabaseHas('users', [ + 'id' => $user->id, + 'account_id' => $user->account_id, + 'me_contact_id' => $contact->id, + ]); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $user = factory(User::class)->create(); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $request = [ + 'account_id' => $user->account_id, + 'user_id' => $user->id, + 'contact_id' => 0, + ]; + + $this->expectException(ValidationException::class); + app(SetMeContact::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_contact_not_found() + { + $user = factory(User::class)->create(); + $contact = factory(Contact::class)->create(); + + $request = [ + 'account_id' => $user->account_id, + 'user_id' => $user->id, + 'contact_id' => $contact->id, + ]; + + $this->expectException(ModelNotFoundException::class); + app(SetMeContact::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Contact/Contact/UpdateBirthdayInformationTest.php b/tests/Unit/Services/Contact/Contact/UpdateBirthdayInformationTest.php new file mode 100644 index 0000000..4bc5555 --- /dev/null +++ b/tests/Unit/Services/Contact/Contact/UpdateBirthdayInformationTest.php @@ -0,0 +1,197 @@ +create([]); + + $request = [ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'is_date_known' => true, + 'day' => 10, + 'month' => 10, + 'year' => 1980, + 'is_age_based' => false, + 'age' => 0, + 'add_reminder' => true, + 'is_deceased' => false, + ]; + + app(UpdateBirthdayInformation::class)->execute($request); + + $specialDate = SpecialDate::where('contact_id', $contact->id)->first(); + + $this->assertDatabaseHas('contacts', [ + 'id' => $contact->id, + 'account_id' => $contact->account_id, + 'birthday_special_date_id' => $specialDate->id, + ]); + + $this->assertDatabaseHas('special_dates', [ + 'id' => $specialDate->id, + 'account_id' => $contact->account_id, + 'is_age_based' => false, + ]); + + // then we update it again + $request = [ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'is_date_known' => false, + ]; + + $contact = app(UpdateBirthdayInformation::class)->execute($request); + + $this->assertDatabaseHas('contacts', [ + 'id' => $contact->id, + 'account_id' => $contact->account_id, + 'birthday_special_date_id' => null, + ]); + } + + /** @test */ + public function it_sets_a_date_if_age_is_provided() + { + $contact = factory(Contact::class)->create([]); + + $request = [ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'is_date_known' => true, + 'is_age_based' => true, + 'age' => 10, + ]; + + $contact = app(UpdateBirthdayInformation::class)->execute($request); + + $specialDate = SpecialDate::where('contact_id', $contact->id)->first(); + + $this->assertDatabaseHas('contacts', [ + 'id' => $contact->id, + 'account_id' => $contact->account_id, + 'birthday_special_date_id' => $specialDate->id, + ]); + + $this->assertDatabaseHas('special_dates', [ + 'id' => $specialDate->id, + 'account_id' => $contact->account_id, + 'is_age_based' => true, + ]); + } + + /** @test */ + public function it_sets_a_complete_date() + { + $contact = factory(Contact::class)->create([]); + + $request = [ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'is_date_known' => true, + 'day' => 10, + 'month' => 10, + 'year' => 1980, + 'is_age_based' => false, + 'add_reminder' => false, + ]; + + $contact = app(UpdateBirthdayInformation::class)->execute($request); + + $specialDate = SpecialDate::where('contact_id', $contact->id)->first(); + + $this->assertDatabaseHas('contacts', [ + 'id' => $contact->id, + 'account_id' => $contact->account_id, + 'birthday_special_date_id' => $specialDate->id, + ]); + + $this->assertDatabaseHas('special_dates', [ + 'id' => $specialDate->id, + 'account_id' => $contact->account_id, + 'is_age_based' => false, + 'is_year_unknown' => false, + ]); + } + + /** @test */ + public function it_sets_a_complete_date_and_sets_a_reminder() + { + $contact = factory(Contact::class)->create([]); + + $request = [ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'is_date_known' => true, + 'day' => 10, + 'month' => 10, + 'year' => 1980, + 'is_age_based' => false, + 'add_reminder' => true, + 'is_deceased' => false, + ]; + + $contact = app(UpdateBirthdayInformation::class)->execute($request); + + $specialDate = SpecialDate::where('contact_id', $contact->id)->first(); + + $this->assertNotNull($contact->birthday_reminder_id); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $contact = factory(Contact::class)->create([]); + + $request = [ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'day' => 10, + 'month' => 10, + 'year' => 1980, + 'is_age_based' => false, + 'add_reminder' => false, + ]; + + $this->expectException(ValidationException::class); + + app(UpdateBirthdayInformation::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_contact_and_account_are_not_linked() + { + $contact = factory(Contact::class)->create([]); + + $request = [ + 'account_id' => 11111111, + 'contact_id' => $contact->id, + 'is_date_known' => true, + 'day' => 10, + 'month' => 10, + 'year' => 1980, + 'is_age_based' => false, + 'add_reminder' => false, + ]; + + $this->expectException(ValidationException::class); + + app(UpdateBirthdayInformation::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Contact/Contact/UpdateContactTest.php b/tests/Unit/Services/Contact/Contact/UpdateContactTest.php new file mode 100644 index 0000000..cad0749 --- /dev/null +++ b/tests/Unit/Services/Contact/Contact/UpdateContactTest.php @@ -0,0 +1,162 @@ +create([]); + $user = factory(User::class)->create([]); + + $request = [ + 'account_id' => $contact->account_id, + 'author_id' => $user->id, + 'contact_id' => $contact->id, + 'first_name' => 'john', + 'middle_name' => 'franck', + 'last_name' => 'doe', + 'gender_id' => $contact->gender_id, + 'description' => 'this is a test', + 'is_partial' => false, + 'is_birthdate_known' => true, + 'birthdate_day' => 10, + 'birthdate_month' => 10, + 'birthdate_year' => 1980, + 'birthdate_is_age_based' => false, + 'birthdate_age' => 0, + 'birthdate_add_reminder' => false, + 'is_deceased' => true, + 'is_deceased_date_known' => true, + 'deceased_date_day' => 10, + 'deceased_date_month' => 10, + 'deceased_date_year' => 1980, + 'deceased_date_add_reminder' => true, + ]; + + $contact = app(UpdateContact::class)->execute($request); + + $this->assertDatabaseHas('contacts', [ + 'id' => $contact->id, + 'account_id' => $contact->account_id, + 'first_name' => 'john', + ]); + + $this->assertInstanceOf( + Contact::class, + $contact + ); + } + + /** @test */ + public function it_fails_if_contact_is_archived() + { + $contact = factory(Contact::class)->state('archived')->create([]); + $user = factory(User::class)->create([]); + + $request = [ + 'account_id' => $contact->account_id, + 'author_id' => $user->id, + 'contact_id' => $contact->id, + 'first_name' => 'john', + 'middle_name' => 'franck', + 'last_name' => 'doe', + 'gender_id' => $contact->gender_id, + 'description' => 'this is a test', + 'is_partial' => false, + 'is_birthdate_known' => true, + 'birthdate_day' => 10, + 'birthdate_month' => 10, + 'birthdate_year' => 1980, + 'birthdate_is_age_based' => false, + 'birthdate_age' => 0, + 'birthdate_add_reminder' => false, + 'is_deceased' => true, + 'is_deceased_date_known' => true, + 'deceased_date_day' => 10, + 'deceased_date_month' => 10, + 'deceased_date_year' => 1980, + 'deceased_date_add_reminder' => true, + ]; + + $this->expectException(ValidationException::class); + app(UpdateContact::class)->execute($request); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $contact = factory(Contact::class)->create([]); + + $request = [ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'middle_name' => 'franck', + 'last_name' => 'doe', + 'gender_id' => $contact->gender_id, + 'description' => 'this is a test', + 'is_partial' => false, + 'is_birthdate_known' => true, + 'birthdate_day' => 10, + 'birthdate_month' => 10, + 'birthdate_year' => 1980, + 'birthdate_is_age_based' => false, + 'birthdate_age' => 0, + 'birthdate_add_reminder' => false, + 'is_deceased' => true, + 'is_deceased_date_known' => true, + 'deceased_date_day' => 10, + 'deceased_date_month' => 10, + 'deceased_date_year' => 1980, + 'deceased_date_add_reminder' => true, + ]; + + $this->expectException(ValidationException::class); + app(UpdateContact::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_account_doesnt_exist() + { + $contact = factory(Contact::class)->create([]); + $user = factory(User::class)->create([]); + + $request = [ + 'account_id' => 11111, + 'author_id' => $user->id, + 'contact_id' => $contact->id, + 'first_name' => 'john', + 'middle_name' => 'franck', + 'last_name' => 'doe', + 'gender_id' => $contact->gender_id, + 'description' => 'this is a test', + 'is_partial' => false, + 'is_birthdate_known' => true, + 'birthdate_day' => 10, + 'birthdate_month' => 10, + 'birthdate_year' => 1980, + 'birthdate_is_age_based' => false, + 'birthdate_age' => 0, + 'birthdate_add_reminder' => false, + 'is_deceased' => true, + 'is_deceased_date_known' => true, + 'deceased_date_day' => 10, + 'deceased_date_month' => 10, + 'deceased_date_year' => 1980, + 'deceased_date_add_reminder' => true, + ]; + + $this->expectException(ValidationException::class); + app(UpdateContact::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Contact/Contact/UpdateDeceasedInformationTest.php b/tests/Unit/Services/Contact/Contact/UpdateDeceasedInformationTest.php new file mode 100644 index 0000000..330e4c4 --- /dev/null +++ b/tests/Unit/Services/Contact/Contact/UpdateDeceasedInformationTest.php @@ -0,0 +1,249 @@ +create([]); + + $request = [ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'is_deceased' => true, + 'is_date_known' => false, + 'add_reminder' => false, + ]; + + app(UpdateDeceasedInformation::class)->execute($request); + + $this->assertDatabaseHas('contacts', [ + 'id' => $contact->id, + 'account_id' => $contact->account_id, + 'is_dead' => 1, + ]); + + // now set the contact as not dead anymore (a zombie, basically) + $request = [ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'is_deceased' => false, + 'is_date_known' => false, + 'add_reminder' => false, + ]; + + app(UpdateDeceasedInformation::class)->execute($request); + + $this->assertDatabaseHas('contacts', [ + 'id' => $contact->id, + 'account_id' => $contact->account_id, + 'is_dead' => 0, + 'deceased_special_date_id' => null, + 'deceased_reminder_id' => null, + ]); + } + + /** @test */ + public function it_sets_a_complete_date() + { + $contact = factory(Contact::class)->create([]); + + $request = [ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'is_deceased' => true, + 'is_date_known' => true, + 'day' => 10, + 'month' => 10, + 'year' => 1980, + 'add_reminder' => false, + ]; + + $contact = app(UpdateDeceasedInformation::class)->execute($request); + + $specialDate = SpecialDate::where('contact_id', $contact->id)->first(); + + $this->assertDatabaseHas('contacts', [ + 'id' => $contact->id, + 'account_id' => $contact->account_id, + 'deceased_special_date_id' => $specialDate->id, + ]); + + $this->assertDatabaseHas('special_dates', [ + 'id' => $specialDate->id, + 'account_id' => $contact->account_id, + 'is_age_based' => false, + 'is_year_unknown' => false, + ]); + } + + /** @test */ + public function it_sets_a_complete_date_with_unknown_year() + { + $contact = factory(Contact::class)->create([]); + + $request = [ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'is_deceased' => true, + 'is_date_known' => true, + 'day' => 10, + 'month' => 10, + 'year' => 0, + 'add_reminder' => false, + ]; + + $contact = app(UpdateDeceasedInformation::class)->execute($request); + + $specialDate = SpecialDate::where('contact_id', $contact->id)->first(); + + $this->assertDatabaseHas('contacts', [ + 'id' => $contact->id, + 'account_id' => $contact->account_id, + 'deceased_special_date_id' => $specialDate->id, + ]); + + $this->assertDatabaseHas('special_dates', [ + 'id' => $specialDate->id, + 'account_id' => $contact->account_id, + 'is_age_based' => false, + 'is_year_unknown' => true, + ]); + } + + /** @test */ + public function it_sets_a_complete_date_and_sets_a_reminder() + { + $contact = factory(Contact::class)->create([]); + + $request = [ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'is_deceased' => true, + 'is_date_known' => true, + 'day' => 10, + 'month' => 10, + 'year' => 1980, + 'add_reminder' => true, + ]; + + $contact = app(UpdateDeceasedInformation::class)->execute($request); + + $specialDate = SpecialDate::where('contact_id', $contact->id)->first(); + $reminder = Reminder::where('contact_id', $contact->id)->first(); + + $this->assertDatabaseHas('contacts', [ + 'id' => $contact->id, + 'account_id' => $contact->account_id, + 'deceased_special_date_id' => $specialDate->id, + 'deceased_reminder_id' => $reminder->id, + ]); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $contact = factory(Contact::class)->create([]); + + $request = [ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'is_date_known' => true, + 'day' => 10, + 'month' => 10, + 'year' => 1980, + 'add_reminder' => false, + ]; + + $this->expectException(ValidationException::class); + app(UpdateDeceasedInformation::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_contact_and_account_are_not_linked() + { + $contact = factory(Contact::class)->create([]); + + $request = [ + 'account_id' => 11111111, + 'contact_id' => $contact->id, + 'is_deceased' => true, + 'is_date_known' => true, + 'day' => 10, + 'month' => 10, + 'year' => 1980, + 'add_reminder' => false, + ]; + + $this->expectException(ValidationException::class); + app(UpdateDeceasedInformation::class)->execute($request); + } + + /** @test */ + public function it_removes_deceased_reminder() + { + $reminder = factory(Reminder::class)->create([]); + $contact = factory(Contact::class)->create([ + 'account_id' => $reminder->account_id, + 'deceased_reminder_id' => $reminder->id, + ]); + + $request = [ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'is_deceased' => true, + 'is_date_known' => true, + 'day' => 10, + 'month' => 10, + 'year' => 1980, + 'add_reminder' => true, + ]; + + app(UpdateDeceasedInformation::class)->execute($request); + + $this->assertDatabaseMissing('reminders', [ + 'id' => $reminder->id, + ]); + } + + /** @test */ + public function it_removes_deceased_special_date() + { + $special_date = factory(SpecialDate::class)->create(); + $contact = factory(Contact::class)->create([ + 'account_id' => $special_date->account_id, + 'deceased_special_date_id' => $special_date->id, + ]); + + $request = [ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'is_deceased' => true, + 'is_date_known' => true, + 'day' => 10, + 'month' => 10, + 'year' => 1980, + 'add_reminder' => true, + ]; + + app(UpdateDeceasedInformation::class)->execute($request); + + $this->assertDatabaseMissing('special_dates', [ + 'id' => $special_date->id, + ]); + } +} diff --git a/tests/Unit/Services/Contact/Contact/UpdateWorkInformationTest.php b/tests/Unit/Services/Contact/Contact/UpdateWorkInformationTest.php new file mode 100644 index 0000000..40d0534 --- /dev/null +++ b/tests/Unit/Services/Contact/Contact/UpdateWorkInformationTest.php @@ -0,0 +1,114 @@ +create([]); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $request = [ + 'account_id' => $user->account_id, + 'author_id' => $user->id, + 'contact_id' => $contact->id, + 'job' => 'Dunder', + ]; + + $contact = app(UpdateWorkInformation::class)->execute($request); + + $this->assertDatabaseHas('contacts', [ + 'id' => $contact->id, + 'account_id' => $contact->account_id, + 'job' => 'Dunder', + 'company' => null, + ]); + + $this->assertInstanceOf( + Contact::class, + $contact + ); + + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $request = [ + 'account_id' => $user->account_id, + 'author_id' => $user->id, + 'contact_id' => $contact->id, + 'company' => 'Sales', + ]; + + $contact = app(UpdateWorkInformation::class)->execute($request); + + $this->assertDatabaseHas('contacts', [ + 'id' => $contact->id, + 'account_id' => $contact->account_id, + 'job' => null, + 'company' => 'Sales', + ]); + + Queue::assertPushed(LogAccountAudit::class, function ($job) use ($contact, $user) { + return $job->auditLog['action'] === 'contact_work_updated' && + $job->auditLog['author_id'] === $user->id && + $job->auditLog['about_contact_id'] === $contact->id && + $job->auditLog['should_appear_on_dashboard'] === true && + $job->auditLog['objects'] === json_encode([ + 'contact_name' => $contact->name, + 'contact_id' => $contact->id, + ]); + }); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $user = factory(User::class)->create([]); + + $request = [ + 'account_id' => $user->account_id, + ]; + + $this->expectException(ValidationException::class); + app(UpdateWorkInformation::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_account_doesnt_exist() + { + $user = factory(User::class)->create([]); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $request = [ + 'account_id' => 111111111, + 'author_id' => $user->id, + 'contact_id' => $contact->id, + 'job' => 'Dunder', + ]; + + $this->expectException(ValidationException::class); + app(CreateContact::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Contact/ContactField/CreateContactFieldTest.php b/tests/Unit/Services/Contact/ContactField/CreateContactFieldTest.php new file mode 100644 index 0000000..8b215aa --- /dev/null +++ b/tests/Unit/Services/Contact/ContactField/CreateContactFieldTest.php @@ -0,0 +1,117 @@ +create(); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + $contactFieldType = factory(ContactFieldType::class)->create([ + 'account_id' => $account->id, + ]); + + $contactField = app(CreateContactField::class)->execute([ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + 'contact_field_type_id' => $contactFieldType->id, + 'data' => 'john@doe.com', + ]); + + $this->assertDatabaseHas('contact_fields', [ + 'id' => $contactField->id, + 'account_id' => $account->id, + 'data' => 'john@doe.com', + ]); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + $contactFieldType = factory(ContactFieldType::class)->create([ + 'account_id' => $account->id, + ]); + + $this->expectException(ValidationException::class); + app(CreateContactField::class)->execute([ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + 'contact_field_type_id' => $contactFieldType->id, + 'data' => '', + ]); + } + + /** @test */ + public function it_throws_an_exception_if_account_doesnt_exist() + { + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + $contactFieldType = factory(ContactFieldType::class)->create([ + 'account_id' => $account->id, + ]); + + $this->expectException(ValidationException::class); + app(CreateContactField::class)->execute([ + 'account_id' => -1, + 'contact_id' => $contact->id, + 'contact_field_type_id' => $contactFieldType->id, + 'data' => 'john@doe.com', + ]); + } + + /** @test */ + public function it_throws_an_exception_if_contact_use_wrong_account() + { + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create(); + $contactFieldType = factory(ContactFieldType::class)->create([ + 'account_id' => $account->id, + ]); + + $this->expectException(ValidationException::class); + app(CreateContactField::class)->execute([ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + 'contact_field_type_id' => $contactFieldType->id, + 'data' => '', + ]); + } + + /** @test */ + public function it_throws_an_exception_if_contact_field_use_wrong_account() + { + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + $contactFieldType = factory(ContactFieldType::class)->create(); + + $this->expectException(ValidationException::class); + app(CreateContactField::class)->execute([ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + 'contact_field_type_id' => $contactFieldType->id, + 'data' => '', + ]); + } +} diff --git a/tests/Unit/Services/Contact/ContactField/DestroyContactFieldTest.php b/tests/Unit/Services/Contact/ContactField/DestroyContactFieldTest.php new file mode 100644 index 0000000..c5b6996 --- /dev/null +++ b/tests/Unit/Services/Contact/ContactField/DestroyContactFieldTest.php @@ -0,0 +1,76 @@ +create(); + + $request = [ + 'account_id' => $contactField->account_id, + 'contact_field_id' => $contactField->id, + ]; + + app(DestroyContactField::class)->execute($request); + + $this->assertDatabaseMissing('contact_fields', [ + 'id' => $contactField->id, + ]); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $account = factory(Account::class)->create(); + + $request = [ + 'account_id' => $account->id, + ]; + + $this->expectException(ValidationException::class); + + app(DestroyContactField::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_contact_field_doesnt_exist() + { + $account = factory(Account::class)->create(); + + $request = [ + 'account_id' => $account->id, + 'contact_field_id' => -1, + ]; + + $this->expectException(ValidationException::class); + app(DestroyContactField::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_contact_field_use_wrong_account() + { + $account = factory(Account::class)->create(); + $contactField = factory(ContactField::class)->create(); + + $request = [ + 'account_id' => $account->id, + 'contact_field_id' => $contactField->id, + ]; + + $this->expectException(ModelNotFoundException::class); + app(DestroyContactField::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Contact/ContactField/UpdateContactFieldTest.php b/tests/Unit/Services/Contact/ContactField/UpdateContactFieldTest.php new file mode 100644 index 0000000..9471f31 --- /dev/null +++ b/tests/Unit/Services/Contact/ContactField/UpdateContactFieldTest.php @@ -0,0 +1,107 @@ +create(); + + $request = [ + 'account_id' => $contactField->account_id, + 'contact_field_id' => $contactField->id, + 'contact_id' => $contactField->contact_id, + 'contact_field_type_id' => $contactField->contactFieldType->id, + 'data' => 'mark@twain.com', + ]; + + $contactField = app(UpdateContactField::class)->execute($request); + + $this->assertDatabaseHas('contact_fields', [ + 'id' => $contactField->id, + 'account_id' => $contactField->account_id, + 'data' => 'mark@twain.com', + ]); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $contactField = factory(ContactField::class)->create(); + + $request = [ + 'account_id' => $contactField->account_id, + 'contact_field_id' => $contactField->id, + 'contact_id' => $contactField->contact_id, + 'contact_field_type_id' => $contactField->contactFieldType->id, + 'data' => null, + ]; + + $this->expectException(ValidationException::class); + app(UpdateContactField::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_account_doesnt_exist() + { + $contactField = factory(ContactField::class)->create([]); + + $request = [ + 'account_id' => -1, + 'contact_field_id' => $contactField->id, + 'contact_id' => $contactField->contact_id, + 'contact_field_type_id' => $contactField->contactFieldType->id, + 'data' => 'mark@twain.com', + ]; + + $this->expectException(ValidationException::class); + app(UpdateContactField::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_contact_field_doesnt_exist() + { + $contactField = factory(ContactField::class)->create(); + + $request = [ + 'account_id' => $contactField->account_id, + 'contact_field_id' => -1, + 'contact_id' => $contactField->contact_id, + 'contact_field_type_id' => $contactField->contactFieldType->id, + 'data' => 'mark@twain.com', + ]; + + $this->expectException(ValidationException::class); + app(UpdateContactField::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_contact_field_type_is_wrong_account() + { + $account = factory(Account::class)->create(); + $contactField = factory(ContactField::class)->create(); + + $request = [ + 'account_id' => $account->id, + 'contact_field_id' => $contactField->id, + 'contact_id' => $contactField->contact_id, + 'contact_field_type_id' => $contactField->contactFieldType->id, + 'data' => 'mark@twain.com', + ]; + + $this->expectException(ModelNotFoundException::class); + app(UpdateContactField::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Contact/Conversation/AddMessageToConversationTest.php b/tests/Unit/Services/Contact/Conversation/AddMessageToConversationTest.php new file mode 100644 index 0000000..0702094 --- /dev/null +++ b/tests/Unit/Services/Contact/Conversation/AddMessageToConversationTest.php @@ -0,0 +1,110 @@ + 1, + 'happened_at' => now(), + ]; + + $this->expectException(ValidationException::class); + + app(AddMessageToConversation::class)->execute($request); + } + + /** @test */ + public function it_stores_a_message() + { + $conversation = factory(Conversation::class)->create([]); + + $request = [ + 'account_id' => $conversation->account_id, + 'contact_id' => $conversation->contact_id, + 'conversation_id' => $conversation->id, + 'written_by_me' => true, + 'written_at' => now(), + 'content' => 'lorem ipsum', + ]; + + $message = app(AddMessageToConversation::class)->execute($request); + + $this->assertDatabaseHas('messages', [ + 'id' => $message->id, + 'conversation_id' => $conversation->id, + 'contact_id' => $message->contact_id, + 'account_id' => $message->account_id, + 'written_by_me' => true, + 'content' => 'lorem ipsum', + ]); + + $this->assertInstanceOf( + Message::class, + $message + ); + } + + /** @test */ + public function it_throws_an_exception_if_contact_is_not_found() + { + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + $conversation = factory(Conversation::class)->create([ + 'account_id' => $account->id, + ]); + $request = [ + 'conversation_id' => $conversation->id, + 'contact_id' => $contact->id, + 'account_id' => $account->id, + 'written_by_me' => true, + 'written_at' => now(), + 'content' => 'lorem ipsum', + ]; + + $this->expectException(ModelNotFoundException::class); + + app(AddMessageToConversation::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_conversation_is_not_found2() + { + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + $conversation = factory(Conversation::class)->create([ + 'contact_id' => $contact->id, + ]); + $request = [ + 'conversation_id' => $conversation->id, + 'contact_id' => $contact->id, + 'account_id' => $account->id, + 'written_by_me' => true, + 'written_at' => now(), + 'content' => 'lorem ipsum', + ]; + + $this->expectException(ModelNotFoundException::class); + + app(AddMessageToConversation::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Contact/Conversation/CreateConversationTest.php b/tests/Unit/Services/Contact/Conversation/CreateConversationTest.php new file mode 100644 index 0000000..3a3f657 --- /dev/null +++ b/tests/Unit/Services/Contact/Conversation/CreateConversationTest.php @@ -0,0 +1,102 @@ +create([]); + $contactFieldType = factory(ContactFieldType::class)->create([ + 'account_id' => $contact->account_id, + ]); + + $request = [ + 'contact_id' => $contact->id, + 'account_id' => $contact->account_id, + 'happened_at' => now(), + 'contact_field_type_id' => $contactFieldType->id, + ]; + + $conversation = app(CreateConversation::class)->execute($request); + + $this->assertDatabaseHas('conversations', [ + 'id' => $conversation->id, + 'contact_id' => $contact->id, + 'account_id' => $contact->account_id, + 'contact_field_type_id' => $contactFieldType->id, + ]); + + $this->assertInstanceOf( + Conversation::class, + $conversation + ); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $contact = factory(Contact::class)->create([]); + + $request = [ + 'contact_id' => $contact->id, + 'happened_at' => now(), + ]; + + $this->expectException(ValidationException::class); + + app(CreateConversation::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_contact_is_not_linked_to_account() + { + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create(); + $contactFieldType = factory(ContactFieldType::class)->create([ + 'account_id' => $contact->account_id, + ]); + + $request = [ + 'contact_id' => $contact->id, + 'account_id' => $account->id, + 'happened_at' => now(), + 'contact_field_type_id' => $contactFieldType->id, + ]; + + $this->expectException(ModelNotFoundException::class); + + app(CreateConversation::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_contactfieldtype_is_not_linked_to_account() + { + $contact = factory(Contact::class)->create([]); + $contactFieldType = factory(ContactFieldType::class)->create([]); + + $request = [ + 'contact_id' => $contact->id, + 'account_id' => $contact->account_id, + 'happened_at' => now(), + 'contact_field_type_id' => $contactFieldType->id, + ]; + + $this->expectException(ModelNotFoundException::class); + + app(CreateConversation::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Contact/Conversation/DestroyConversationTest.php b/tests/Unit/Services/Contact/Conversation/DestroyConversationTest.php new file mode 100644 index 0000000..c2f3531 --- /dev/null +++ b/tests/Unit/Services/Contact/Conversation/DestroyConversationTest.php @@ -0,0 +1,104 @@ +create([ + 'happened_at' => '2008-01-01', + ]); + + $request = [ + 'account_id' => $conversation->account_id, + 'conversation_id' => $conversation->id, + ]; + + $this->assertDatabaseHas('conversations', [ + 'id' => $conversation->id, + ]); + + app(DestroyConversation::class)->execute($request); + + $this->assertDatabaseMissing('conversations', [ + 'id' => $conversation->id, + ]); + } + + /** @test */ + public function destroying_a_conversation_destroys_corresponding_messages() + { + $conversation = factory(Conversation::class)->create([ + 'happened_at' => '2008-01-01', + ]); + + $message = factory(Message::class)->create([ + 'conversation_id' => $conversation->id, + 'account_id' => $conversation->account_id, + 'contact_id' => $conversation->contact_id, + 'content' => 'tititi', + 'written_at' => '2009-01-01', + 'written_by_me' => false, + ]); + + $this->assertDatabaseHas('messages', [ + 'id' => $message->id, + ]); + + $request = [ + 'account_id' => $conversation->account_id, + 'conversation_id' => $conversation->id, + ]; + + app(DestroyConversation::class)->execute($request); + + $this->assertDatabaseMissing('messages', [ + 'id' => $message->id, + ]); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $conversation = factory(Conversation::class)->create([ + 'happened_at' => '2008-01-01', + ]); + + $request = [ + 'account_id' => $conversation->account_id, + ]; + + $this->expectException(ValidationException::class); + + app(DestroyConversation::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_conversation_doesnt_exist() + { + $account = factory(Account::class)->create(); + $conversation = factory(Conversation::class)->create([]); + + $request = [ + 'account_id' => $account->id, + 'conversation_id' => $conversation->id, + ]; + + $this->expectException(ModelNotFoundException::class); + + app(DestroyConversation::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Contact/Conversation/DestroyMessageTest.php b/tests/Unit/Services/Contact/Conversation/DestroyMessageTest.php new file mode 100644 index 0000000..491266b --- /dev/null +++ b/tests/Unit/Services/Contact/Conversation/DestroyMessageTest.php @@ -0,0 +1,77 @@ +create([]); + + $message = factory(Message::class)->create([ + 'conversation_id' => $conversation->id, + 'account_id' => $conversation->account_id, + 'contact_id' => $conversation->contact_id, + 'content' => 'tititi', + 'written_at' => '2009-01-01', + 'written_by_me' => false, + ]); + + $request = [ + 'account_id' => $conversation->account_id, + 'conversation_id' => $conversation->id, + 'message_id' => $message->id, + ]; + + $this->assertDatabaseHas('messages', [ + 'id' => $message->id, + ]); + + app(DestroyMessage::class)->execute($request); + + $this->assertDatabaseMissing('messages', [ + 'id' => $message->id, + ]); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $request = [ + 'conversation_id' => 2, + 'message_id' => 3, + ]; + + $this->expectException(ValidationException::class); + + app(DestroyMessage::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_message_doesnt_exist() + { + $conversation = factory(Conversation::class)->create([]); + $message = factory(Message::class)->create([]); + + $request = [ + 'account_id' => $conversation->account_id, + 'conversation_id' => $conversation->id, + 'message_id' => $message->id, + ]; + + $this->expectException(ModelNotFoundException::class); + + app(DestroyMessage::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Contact/Conversation/UpdateConversationTest.php b/tests/Unit/Services/Contact/Conversation/UpdateConversationTest.php new file mode 100644 index 0000000..946a432 --- /dev/null +++ b/tests/Unit/Services/Contact/Conversation/UpdateConversationTest.php @@ -0,0 +1,85 @@ +create([ + 'happened_at' => '2008-01-01', + ]); + $contactFieldType = factory(ContactFieldType::class)->create([ + 'account_id' => $conversation->account_id, + ]); + + $request = [ + 'account_id' => $conversation->account_id, + 'conversation_id' => $conversation->id, + 'happened_at' => '2010-02-02', + 'contact_field_type_id' => $contactFieldType->id, + ]; + + $conversation = app(UpdateConversation::class)->execute($request); + + $this->assertDatabaseHas('conversations', [ + 'id' => $conversation->id, + 'happened_at' => '2010-02-02 00:00:00', + 'contact_field_type_id' => $contactFieldType->id, + ]); + + $this->assertInstanceOf( + Conversation::class, + $conversation + ); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $contact = factory(Contact::class)->create([]); + + $request = [ + 'contact_id' => $contact->id, + 'happened_at' => now(), + ]; + + $this->expectException(ValidationException::class); + + app(UpdateConversation::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_conversation_doesnt_exist() + { + $account = factory(Account::class)->create(); + $conversation = factory(Conversation::class)->create([]); + $contactFieldType = factory(ContactFieldType::class)->create([ + 'account_id' => $conversation->account_id, + ]); + + $request = [ + 'account_id' => $account->id, + 'conversation_id' => $conversation->id, + 'happened_at' => '2010-02-02', + 'contact_field_type_id' => $contactFieldType->id, + ]; + + $this->expectException(ModelNotFoundException::class); + + app(UpdateConversation::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Contact/Conversation/UpdateMessageTest.php b/tests/Unit/Services/Contact/Conversation/UpdateMessageTest.php new file mode 100644 index 0000000..8629720 --- /dev/null +++ b/tests/Unit/Services/Contact/Conversation/UpdateMessageTest.php @@ -0,0 +1,104 @@ +create([]); + + $message = factory(Message::class)->create([ + 'conversation_id' => $conversation->id, + 'account_id' => $conversation->account_id, + 'contact_id' => $conversation->contact_id, + 'content' => 'tititi', + 'written_at' => '2009-01-01', + 'written_by_me' => false, + ]); + + $request = [ + 'account_id' => $conversation->account_id, + 'contact_id' => $conversation->contact_id, + 'conversation_id' => $conversation->id, + 'message_id' => $message->id, + 'written_at' => now(), + 'written_by_me' => true, + 'content' => 'lorem', + ]; + + $message = app(UpdateMessage::class)->execute($request); + + $this->assertDatabaseHas('messages', [ + 'id' => $message->id, + 'account_id' => $conversation->account_id, + 'contact_id' => $conversation->contact_id, + 'conversation_id' => $conversation->id, + 'written_by_me' => true, + 'content' => 'lorem', + ]); + + $this->assertInstanceOf( + Message::class, + $message + ); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $request = [ + 'account_id' => 1, + 'conversation_id' => 2, + 'message_id' => 3, + 'written_at' => now(), + 'written_by_me' => true, + 'content' => 'lorem', + ]; + + $this->expectException(ValidationException::class); + + app(UpdateMessage::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_message_does_not_exist() + { + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + $conversation = factory(Conversation::class)->create([ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + ]); + $message = factory(Message::class)->create([]); + + $request = [ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + 'conversation_id' => $conversation->id, + 'message_id' => $message->id, + 'written_at' => now(), + 'written_by_me' => true, + 'content' => 'lorem', + ]; + + $this->expectException(ModelNotFoundException::class); + + app(UpdateMessage::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Contact/Description/ClearPersonalDescriptionTest.php b/tests/Unit/Services/Contact/Description/ClearPersonalDescriptionTest.php new file mode 100644 index 0000000..51e6770 --- /dev/null +++ b/tests/Unit/Services/Contact/Description/ClearPersonalDescriptionTest.php @@ -0,0 +1,71 @@ +create([]); + $user = factory(User::class)->create([ + 'account_id' => $contact->account_id, + ]); + + $request = [ + 'account_id' => $contact->account_id, + 'author_id' => $user->id, + 'contact_id' => $contact->id, + ]; + + $contact = (new ClearPersonalDescription)->execute($request); + + $this->assertDatabaseHas('contacts', [ + 'account_id' => $contact->account_id, + 'id' => $contact->id, + 'description' => null, + ]); + + $this->assertInstanceOf( + Contact::class, + $contact + ); + + // check that a job has been triggered to create an auditlog + Queue::assertPushed(LogAccountAudit::class, function ($job) use ($contact, $user) { + return $job->auditLog['action'] === 'contact_description_cleared' && + $job->auditLog['author_id'] === $user->id && + $job->auditLog['about_contact_id'] === $contact->id && + $job->auditLog['should_appear_on_dashboard'] === true && + $job->auditLog['objects'] === json_encode([ + 'contact_name' => $contact->name, + 'contact_id' => $contact->id, + ]); + }); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given(): void + { + $request = [ + 'first_name' => 'Dwight', + ]; + + $this->expectException(ValidationException::class); + (new ClearPersonalDescription)->execute($request); + } +} diff --git a/tests/Unit/Services/Contact/Description/SetPersonalDescriptionTest.php b/tests/Unit/Services/Contact/Description/SetPersonalDescriptionTest.php new file mode 100644 index 0000000..969ff24 --- /dev/null +++ b/tests/Unit/Services/Contact/Description/SetPersonalDescriptionTest.php @@ -0,0 +1,72 @@ +create([]); + $user = factory(User::class)->create([ + 'account_id' => $contact->account_id, + ]); + + $request = [ + 'account_id' => $contact->account_id, + 'author_id' => $user->id, + 'contact_id' => $contact->id, + 'description' => 'This is just great', + ]; + + $contact = (new SetPersonalDescription)->execute($request); + + $this->assertDatabaseHas('contacts', [ + 'account_id' => $contact->account_id, + 'id' => $contact->id, + 'description' => 'This is just great', + ]); + + $this->assertInstanceOf( + Contact::class, + $contact + ); + + // check that a job has been triggered to create an auditlog + Queue::assertPushed(LogAccountAudit::class, function ($job) use ($contact, $user) { + return $job->auditLog['action'] === 'contact_description_updated' && + $job->auditLog['author_id'] === $user->id && + $job->auditLog['about_contact_id'] === $contact->id && + $job->auditLog['should_appear_on_dashboard'] === true && + $job->auditLog['objects'] === json_encode([ + 'contact_name' => $contact->name, + 'contact_id' => $contact->id, + ]); + }); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given(): void + { + $request = [ + 'first_name' => 'Dwight', + ]; + + $this->expectException(ValidationException::class); + (new SetPersonalDescription)->execute($request); + } +} diff --git a/tests/Unit/Services/Contact/Document/DestroyDocumentTest.php b/tests/Unit/Services/Contact/Document/DestroyDocumentTest.php new file mode 100644 index 0000000..8908de0 --- /dev/null +++ b/tests/Unit/Services/Contact/Document/DestroyDocumentTest.php @@ -0,0 +1,87 @@ +create([]); + $document = $this->uploadDocument($contact); + + $request = [ + 'account_id' => $document->account_id, + 'document_id' => $document->id, + ]; + + $this->assertDatabaseHas('documents', [ + 'id' => $document->id, + ]); + + app(DestroyDocument::class)->execute($request); + + $this->assertDatabaseMissing('documents', [ + 'id' => $document->id, + ]); + + Storage::disk('public')->assertMissing($document->new_filename); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $request = [ + 'document_id' => 2, + ]; + + $this->expectException(ValidationException::class); + + app(DestroyDocument::class)->execute($request); + } + + /** @test */ + public function it_throws_a_document_doesnt_exist() + { + $document = factory(Document::class)->create([]); + + $request = [ + 'account_id' => $document->account_id, + 'document_id' => 3, + ]; + + $this->expectException(ModelNotFoundException::class); + + app(DestroyDocument::class)->execute($request); + } + + private function uploadDocument($contact) + { + $request = [ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'document' => UploadedFile::fake()->image('document.pdf'), + ]; + + $document = app(UploadDocument::class)->execute($request); + + Storage::disk('public')->assertExists($document->new_filename); + + return $document; + } +} diff --git a/tests/Unit/Services/Contact/Document/UploadDocumentTest.php b/tests/Unit/Services/Contact/Document/UploadDocumentTest.php new file mode 100644 index 0000000..d48af61 --- /dev/null +++ b/tests/Unit/Services/Contact/Document/UploadDocumentTest.php @@ -0,0 +1,83 @@ +create([]); + + $file = UploadedFile::fake()->image('document.pdf'); + + $request = [ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'document' => $file, + ]; + + $document = app(UploadDocument::class)->execute($request); + + $this->assertDatabaseHas('documents', [ + 'id' => $document->id, + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'type' => 'pdf', + ]); + + $this->assertInstanceOf( + Document::class, + $document + ); + + Storage::disk('public')->assertExists($document->new_filename); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $request = [ + 'account_id' => 1, + 'contact_id' => 2, + ]; + + $this->expectException(ValidationException::class); + + app(UploadDocument::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_contact_does_not_exist() + { + Storage::fake(); + + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create([]); + + $request = [ + 'account_id' => $account->id, + 'contact_id' => 2, + 'document' => UploadedFile::fake()->image('document.pdf'), + ]; + + $this->expectException(ModelNotFoundException::class); + + $document = app(UploadDocument::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Contact/Gift/AssociateGiftToPhotoTest.php b/tests/Unit/Services/Contact/Gift/AssociateGiftToPhotoTest.php new file mode 100644 index 0000000..1ff3eb6 --- /dev/null +++ b/tests/Unit/Services/Contact/Gift/AssociateGiftToPhotoTest.php @@ -0,0 +1,66 @@ +create(); + $photo = factory(Photo::class)->create([ + 'account_id' => $gift->account_id, + ]); + + $giftUpdated = app(AssociatePhotoToGift::class)->execute([ + 'account_id' => $gift->account_id, + 'gift_id' => $gift->id, + 'photo_id' => $photo->id, + ]); + + $this->assertInstanceOf(Gift::class, $giftUpdated); + $this->assertEquals($gift->id, $giftUpdated->id); + + $this->assertDatabaseHas('gift_photo', [ + 'gift_id' => $gift->id, + 'photo_id' => $photo->id, + ]); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $this->expectException(ValidationException::class); + + app(AssociatePhotoToGift::class)->execute([ + 'account_id' => -1, + 'gift_id' => -1, + 'photo_id' => -1, + ]); + } + + /** @test */ + public function it_fails_if_photo_is_wrong_account() + { + $gift = factory(Gift::class)->create(); + $photo = factory(Photo::class)->create(); + + $this->expectException(ModelNotFoundException::class); + + app(AssociatePhotoToGift::class)->execute([ + 'account_id' => $gift->account_id, + 'gift_id' => $gift->id, + 'photo_id' => $photo->id, + ]); + } +} diff --git a/tests/Unit/Services/Contact/Gift/CreateGiftTest.php b/tests/Unit/Services/Contact/Gift/CreateGiftTest.php new file mode 100644 index 0000000..69aac10 --- /dev/null +++ b/tests/Unit/Services/Contact/Gift/CreateGiftTest.php @@ -0,0 +1,72 @@ +create(); + + $gift = app(CreateGift::class)->execute([ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'name' => 'Book', + 'status' => 'idea', + ]); + + $this->assertDatabaseHas('gifts', [ + 'id' => $gift->id, + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'name' => 'Book', + 'status' => 'idea', + ]); + + $this->assertInstanceOf( + Gift::class, + $gift + ); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $this->expectException(ValidationException::class); + + app(CreateGift::class)->execute([ + 'account_id' => -1, + 'contact_id' => -1, + 'name' => 'Book', + 'status' => 'idea', + ]); + } + + /** @test */ + public function it_fails_if_contact_is_wrong_account() + { + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create(); + + $this->expectException(ModelNotFoundException::class); + + $gift = app(CreateGift::class)->execute([ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + 'name' => 'Book', + 'status' => 'idea', + ]); + } +} diff --git a/tests/Unit/Services/Contact/Gift/DestroyGiftTest.php b/tests/Unit/Services/Contact/Gift/DestroyGiftTest.php new file mode 100644 index 0000000..f88b823 --- /dev/null +++ b/tests/Unit/Services/Contact/Gift/DestroyGiftTest.php @@ -0,0 +1,61 @@ +create(); + + $this->assertDatabaseHas('gifts', [ + 'account_id' => $gift->account_id, + 'contact_id' => $gift->contact_id, + 'id' => $gift->id, + ]); + + app(DestroyGift::class)->execute([ + 'account_id' => $gift->account_id, + 'gift_id' => $gift->id, + ]); + + $this->assertDatabaseMissing('gifts', [ + 'id' => $gift->id, + ]); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $this->expectException(ValidationException::class); + + app(DestroyGift::class)->execute([ + 'account_id' => -1, + ]); + } + + /** @test */ + public function it_fails_if_gift_is_wrong_account() + { + $account = factory(Account::class)->create(); + $gift = factory(Gift::class)->create(); + + $this->expectException(ModelNotFoundException::class); + + app(DestroyGift::class)->execute([ + 'account_id' => $account->id, + 'gift_id' => $gift->id, + ]); + } +} diff --git a/tests/Unit/Services/Contact/Gift/UpdateGiftTest.php b/tests/Unit/Services/Contact/Gift/UpdateGiftTest.php new file mode 100644 index 0000000..ebf2737 --- /dev/null +++ b/tests/Unit/Services/Contact/Gift/UpdateGiftTest.php @@ -0,0 +1,69 @@ +create(); + + $gift = app(UpdateGift::class)->execute([ + 'account_id' => $gift->account_id, + 'gift_id' => $gift->id, + 'contact_id' => $gift->contact_id, + 'name' => 'Book', + 'status' => 'offered', + ]); + + $this->assertDatabaseHas('gifts', [ + 'id' => $gift->id, + 'name' => 'Book', + 'status' => 'offered', + ]); + + $this->assertInstanceOf( + Gift::class, + $gift + ); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $this->expectException(ValidationException::class); + + app(UpdateGift::class)->execute([ + 'account_id' => -1, + 'gift_id' => -1, + ]); + } + + /** @test */ + public function it_throws_an_exception_if_gift_wrong_account() + { + $account = factory(Account::class)->create(); + $gift = factory(Gift::class)->create(); + + $this->expectException(ModelNotFoundException::class); + + app(UpdateGift::class)->execute([ + 'account_id' => $account->id, + 'gift_id' => $gift->id, + 'contact_id' => $gift->contact_id, + 'name' => 'Book', + 'status' => 'offered', + ]); + } +} diff --git a/tests/Unit/Services/Contact/Label/UpdateAddessLabelTest.php b/tests/Unit/Services/Contact/Label/UpdateAddessLabelTest.php new file mode 100644 index 0000000..d7454bc --- /dev/null +++ b/tests/Unit/Services/Contact/Label/UpdateAddessLabelTest.php @@ -0,0 +1,228 @@ +create([]); + $address = factory(Address::class)->create([ + 'account_id' => $account->id, + ]); + + app(UpdateAddressLabels::class)->execute([ + 'account_id' => $account->id, + 'address_id' => $address->id, + 'labels' => ['home'], + ]); + + $this->assertDatabaseHas('contact_field_labels', [ + 'account_id' => $account->id, + 'label_i18n' => 'home', + ]); + + $contactFieldLabel = ContactFieldLabel::where([ + 'account_id' => $account->id, + 'label_i18n' => 'home', + ])->first(); + $this->assertDatabaseHas('address_contact_field_label', [ + 'account_id' => $account->id, + 'address_id' => $address->id, + 'contact_field_label_id' => $contactFieldLabel->id, + ]); + } + + /** @test */ + public function it_creates_contact_field_multiple_labels() + { + $account = factory(Account::class)->create([]); + $address = factory(Address::class)->create([ + 'account_id' => $account->id, + ]); + + app(UpdateAddressLabels::class)->execute([ + 'account_id' => $account->id, + 'address_id' => $address->id, + 'labels' => ['home', 'main', 'cell'], + ]); + + $this->assertDatabaseHas('contact_field_labels', [ + 'account_id' => $account->id, + 'label_i18n' => 'home', + ]); + $this->assertDatabaseHas('contact_field_labels', [ + 'account_id' => $account->id, + 'label_i18n' => 'main', + ]); + $this->assertDatabaseHas('contact_field_labels', [ + 'account_id' => $account->id, + 'label_i18n' => 'cell', + ]); + + $homeLabel = ContactFieldLabel::where([ + 'account_id' => $account->id, + 'label_i18n' => 'home', + ])->first(); + $this->assertDatabaseHas('address_contact_field_label', [ + 'account_id' => $account->id, + 'address_id' => $address->id, + 'contact_field_label_id' => $homeLabel->id, + ]); + $mainLabel = ContactFieldLabel::where([ + 'account_id' => $account->id, + 'label_i18n' => 'main', + ])->first(); + $this->assertDatabaseHas('address_contact_field_label', [ + 'account_id' => $account->id, + 'address_id' => $address->id, + 'contact_field_label_id' => $mainLabel->id, + ]); + $cellLabel = ContactFieldLabel::where([ + 'account_id' => $account->id, + 'label_i18n' => 'cell', + ])->first(); + $this->assertDatabaseHas('address_contact_field_label', [ + 'account_id' => $account->id, + 'address_id' => $address->id, + 'contact_field_label_id' => $cellLabel->id, + ]); + } + + /** @test */ + public function it_adds_contact_field_labels() + { + $account = factory(Account::class)->create([]); + $address = factory(Address::class)->create([ + 'account_id' => $account->id, + ]); + $homeLabel = factory(ContactFieldLabel::class)->create([ + 'account_id' => $account->id, + 'label_i18n' => 'home', + ]); + $cellLabel = factory(ContactFieldLabel::class)->create([ + 'account_id' => $account->id, + 'label_i18n' => 'cell', + ]); + $address->labels()->sync([$homeLabel->id => ['account_id' => $account->id]]); + + $this->assertDatabaseHas('address_contact_field_label', [ + 'account_id' => $account->id, + 'address_id' => $address->id, + 'contact_field_label_id' => $homeLabel->id, + ]); + + app(UpdateAddressLabels::class)->execute([ + 'account_id' => $account->id, + 'address_id' => $address->id, + 'labels' => ['home', 'cell'], + ]); + + $this->assertDatabaseHas('address_contact_field_label', [ + 'account_id' => $account->id, + 'address_id' => $address->id, + 'contact_field_label_id' => $homeLabel->id, + ]); + $this->assertDatabaseHas('address_contact_field_label', [ + 'account_id' => $account->id, + 'address_id' => $address->id, + 'contact_field_label_id' => $cellLabel->id, + ]); + } + + /** @test */ + public function it_removes_contact_field_labels() + { + $account = factory(Account::class)->create([]); + $address = factory(Address::class)->create([ + 'account_id' => $account->id, + ]); + $homeLabel = factory(ContactFieldLabel::class)->create([ + 'account_id' => $account->id, + 'label_i18n' => 'home', + ]); + $cellLabel = factory(ContactFieldLabel::class)->create([ + 'account_id' => $account->id, + 'label_i18n' => 'cell', + ]); + $address->labels()->sync([$homeLabel->id => ['account_id' => $account->id]]); + + $this->assertDatabaseHas('address_contact_field_label', [ + 'account_id' => $account->id, + 'address_id' => $address->id, + 'contact_field_label_id' => $homeLabel->id, + ]); + + app(UpdateAddressLabels::class)->execute([ + 'account_id' => $account->id, + 'address_id' => $address->id, + 'labels' => ['cell'], + ]); + + $this->assertDatabaseMissing('address_contact_field_label', [ + 'account_id' => $account->id, + 'address_id' => $address->id, + 'contact_field_label_id' => $homeLabel->id, + ]); + $this->assertDatabaseHas('address_contact_field_label', [ + 'account_id' => $account->id, + 'address_id' => $address->id, + 'contact_field_label_id' => $cellLabel->id, + ]); + } + + /** @test */ + public function it_throws_an_exception_if_account_doesnt_exist() + { + $address = factory(Address::class)->create(); + + $this->expectException(ValidationException::class); + + app(UpdateAddressLabels::class)->execute([ + 'account_id' => -1, + 'address_id' => $address->id, + 'labels' => ['cell'], + ]); + } + + /** @test */ + public function it_throws_an_exception_if_contact_field_doesnt_exist() + { + $account = factory(Account::class)->create([]); + + $this->expectException(ValidationException::class); + + app(UpdateAddressLabels::class)->execute([ + 'account_id' => $account->id, + 'address_id' => -1, + 'labels' => ['cell'], + ]); + } + + /** @test */ + public function it_throws_an_exception_if_contact_field_is_wrong_account() + { + $account = factory(Account::class)->create([]); + $address = factory(Address::class)->create(); + + $this->expectException(ModelNotFoundException::class); + + app(UpdateAddressLabels::class)->execute([ + 'account_id' => $account->id, + 'address_id' => $address->id, + 'labels' => ['cell'], + ]); + } +} diff --git a/tests/Unit/Services/Contact/Label/UpdateContactFieldLabelTest.php b/tests/Unit/Services/Contact/Label/UpdateContactFieldLabelTest.php new file mode 100644 index 0000000..b6eb96a --- /dev/null +++ b/tests/Unit/Services/Contact/Label/UpdateContactFieldLabelTest.php @@ -0,0 +1,258 @@ +create([]); + $contactField = factory(ContactField::class)->create([ + 'account_id' => $account->id, + ]); + + app(UpdateContactFieldLabels::class)->execute([ + 'account_id' => $account->id, + 'contact_field_id' => $contactField->id, + 'labels' => ['HOME'], + ]); + + $this->assertDatabaseHas('contact_field_labels', [ + 'account_id' => $account->id, + 'label_i18n' => 'home', + ]); + + $contactFieldLabel = ContactFieldLabel::where([ + 'account_id' => $account->id, + 'label_i18n' => 'home', + ])->first(); + $this->assertDatabaseHas('contact_field_contact_field_label', [ + 'account_id' => $account->id, + 'contact_field_id' => $contactField->id, + 'contact_field_label_id' => $contactFieldLabel->id, + ]); + } + + /** @test */ + public function it_creates_personal_contact_field_labels() + { + $account = factory(Account::class)->create([]); + $contactField = factory(ContactField::class)->create([ + 'account_id' => $account->id, + ]); + + app(UpdateContactFieldLabels::class)->execute([ + 'account_id' => $account->id, + 'contact_field_id' => $contactField->id, + 'labels' => ['Family'], + ]); + + $this->assertDatabaseHas('contact_field_labels', [ + 'account_id' => $account->id, + 'label' => 'Family', + ]); + + $contactFieldLabel = ContactFieldLabel::where([ + 'account_id' => $account->id, + 'label' => 'Family', + ])->first(); + $this->assertDatabaseHas('contact_field_contact_field_label', [ + 'account_id' => $account->id, + 'contact_field_id' => $contactField->id, + 'contact_field_label_id' => $contactFieldLabel->id, + ]); + } + + /** @test */ + public function it_creates_contact_field_multiple_labels() + { + $account = factory(Account::class)->create([]); + $contactField = factory(ContactField::class)->create([ + 'account_id' => $account->id, + ]); + + app(UpdateContactFieldLabels::class)->execute([ + 'account_id' => $account->id, + 'contact_field_id' => $contactField->id, + 'labels' => ['home', 'main', 'cell'], + ]); + + $this->assertDatabaseHas('contact_field_labels', [ + 'account_id' => $account->id, + 'label_i18n' => 'home', + ]); + $this->assertDatabaseHas('contact_field_labels', [ + 'account_id' => $account->id, + 'label_i18n' => 'main', + ]); + $this->assertDatabaseHas('contact_field_labels', [ + 'account_id' => $account->id, + 'label_i18n' => 'cell', + ]); + + $homeLabel = ContactFieldLabel::where([ + 'account_id' => $account->id, + 'label_i18n' => 'home', + ])->first(); + $this->assertDatabaseHas('contact_field_contact_field_label', [ + 'account_id' => $account->id, + 'contact_field_id' => $contactField->id, + 'contact_field_label_id' => $homeLabel->id, + ]); + $mainLabel = ContactFieldLabel::where([ + 'account_id' => $account->id, + 'label_i18n' => 'main', + ])->first(); + $this->assertDatabaseHas('contact_field_contact_field_label', [ + 'account_id' => $account->id, + 'contact_field_id' => $contactField->id, + 'contact_field_label_id' => $mainLabel->id, + ]); + $cellLabel = ContactFieldLabel::where([ + 'account_id' => $account->id, + 'label_i18n' => 'cell', + ])->first(); + $this->assertDatabaseHas('contact_field_contact_field_label', [ + 'account_id' => $account->id, + 'contact_field_id' => $contactField->id, + 'contact_field_label_id' => $cellLabel->id, + ]); + } + + /** @test */ + public function it_adds_contact_field_labels() + { + $account = factory(Account::class)->create([]); + $contactField = factory(ContactField::class)->create([ + 'account_id' => $account->id, + ]); + $homeLabel = factory(ContactFieldLabel::class)->create([ + 'account_id' => $account->id, + 'label_i18n' => 'home', + ]); + $cellLabel = factory(ContactFieldLabel::class)->create([ + 'account_id' => $account->id, + 'label_i18n' => 'cell', + ]); + $contactField->labels()->sync([$homeLabel->id => ['account_id' => $account->id]]); + + $this->assertDatabaseHas('contact_field_contact_field_label', [ + 'account_id' => $account->id, + 'contact_field_id' => $contactField->id, + 'contact_field_label_id' => $homeLabel->id, + ]); + + app(UpdateContactFieldLabels::class)->execute([ + 'account_id' => $account->id, + 'contact_field_id' => $contactField->id, + 'labels' => ['home', 'cell'], + ]); + + $this->assertDatabaseHas('contact_field_contact_field_label', [ + 'account_id' => $account->id, + 'contact_field_id' => $contactField->id, + 'contact_field_label_id' => $homeLabel->id, + ]); + $this->assertDatabaseHas('contact_field_contact_field_label', [ + 'account_id' => $account->id, + 'contact_field_id' => $contactField->id, + 'contact_field_label_id' => $cellLabel->id, + ]); + } + + /** @test */ + public function it_removes_contact_field_labels() + { + $account = factory(Account::class)->create([]); + $contactField = factory(ContactField::class)->create([ + 'account_id' => $account->id, + ]); + $homeLabel = factory(ContactFieldLabel::class)->create([ + 'account_id' => $account->id, + 'label_i18n' => 'home', + ]); + $cellLabel = factory(ContactFieldLabel::class)->create([ + 'account_id' => $account->id, + 'label_i18n' => 'cell', + ]); + $contactField->labels()->sync([$homeLabel->id => ['account_id' => $account->id]]); + + $this->assertDatabaseHas('contact_field_contact_field_label', [ + 'account_id' => $account->id, + 'contact_field_id' => $contactField->id, + 'contact_field_label_id' => $homeLabel->id, + ]); + + app(UpdateContactFieldLabels::class)->execute([ + 'account_id' => $account->id, + 'contact_field_id' => $contactField->id, + 'labels' => ['cell'], + ]); + + $this->assertDatabaseMissing('contact_field_contact_field_label', [ + 'account_id' => $account->id, + 'contact_field_id' => $contactField->id, + 'contact_field_label_id' => $homeLabel->id, + ]); + $this->assertDatabaseHas('contact_field_contact_field_label', [ + 'account_id' => $account->id, + 'contact_field_id' => $contactField->id, + 'contact_field_label_id' => $cellLabel->id, + ]); + } + + /** @test */ + public function it_throws_an_exception_if_account_doesnt_exist() + { + $contactField = factory(ContactField::class)->create(); + + $this->expectException(ValidationException::class); + + app(UpdateContactFieldLabels::class)->execute([ + 'account_id' => -1, + 'contact_field_id' => $contactField->id, + 'labels' => ['cell'], + ]); + } + + /** @test */ + public function it_throws_an_exception_if_contact_field_doesnt_exist() + { + $account = factory(Account::class)->create([]); + + $this->expectException(ValidationException::class); + + app(UpdateContactFieldLabels::class)->execute([ + 'account_id' => $account->id, + 'contact_field_id' => -1, + 'labels' => ['cell'], + ]); + } + + /** @test */ + public function it_throws_an_exception_if_contact_field_is_wrong_account() + { + $account = factory(Account::class)->create([]); + $contactField = factory(ContactField::class)->create(); + + $this->expectException(ModelNotFoundException::class); + + app(UpdateContactFieldLabels::class)->execute([ + 'account_id' => $account->id, + 'contact_field_id' => $contactField->id, + 'labels' => ['cell'], + ]); + } +} diff --git a/tests/Unit/Services/Contact/LifeEvent/CreateLifeEventTest.php b/tests/Unit/Services/Contact/LifeEvent/CreateLifeEventTest.php new file mode 100644 index 0000000..045ad1f --- /dev/null +++ b/tests/Unit/Services/Contact/LifeEvent/CreateLifeEventTest.php @@ -0,0 +1,148 @@ +create([]); + $lifeEventType = factory(LifeEventType::class)->create([ + 'account_id' => $contact->account_id, + ]); + + $request = [ + 'contact_id' => $contact->id, + 'account_id' => $contact->account_id, + 'life_event_type_id' => $lifeEventType->id, + 'happened_at' => now(), + 'name' => 'This is a name', + 'note' => 'This is a note', + 'has_reminder' => false, + 'happened_at_day_unknown' => false, + 'happened_at_month_unknown' => false, + ]; + + $lifeEvent = app(CreateLifeEvent::class)->execute($request); + + $this->assertDatabaseHas('life_events', [ + 'id' => $lifeEvent->id, + 'contact_id' => $contact->id, + 'account_id' => $contact->account_id, + 'life_event_type_id' => $lifeEventType->id, + 'name' => 'This is a name', + 'note' => 'This is a note', + 'reminder_id' => null, + ]); + + $this->assertInstanceOf( + LifeEvent::class, + $lifeEvent + ); + } + + /** @test */ + public function it_stores_a_life_event_and_set_a_reminder() + { + $contact = factory(Contact::class)->create([]); + $lifeEventType = factory(LifeEventType::class)->create([ + 'account_id' => $contact->account_id, + ]); + + $request = [ + 'contact_id' => $contact->id, + 'account_id' => $contact->account_id, + 'life_event_type_id' => $lifeEventType->id, + 'happened_at' => now(), + 'name' => 'This is a name', + 'note' => 'This is a note', + 'has_reminder' => true, + 'happened_at_day_unknown' => false, + 'happened_at_month_unknown' => false, + ]; + + $lifeEvent = app(CreateLifeEvent::class)->execute($request); + + $this->assertDatabaseHas('reminders', [ + 'id' => $lifeEvent->reminder->id, + ]); + + $this->assertDatabaseHas('life_events', [ + 'reminder_id' => $lifeEvent->reminder->id, + ]); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $contact = factory(Contact::class)->create([]); + + $request = [ + 'contact_id' => $contact->id, + 'happened_at' => now(), + ]; + + $this->expectException(ValidationException::class); + + app(CreateLifeEvent::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_contact_is_not_linked_to_account() + { + $account = factory(Account::class)->create(); + $lifeEvent = factory(LifeEvent::class)->create([]); + + $request = [ + 'contact_id' => $lifeEvent->contact_id, + 'account_id' => $account->id, + 'life_event_type_id' => $lifeEvent->lifeEventType->id, + 'name' => 'This is a name', + 'note' => 'This is a note', + 'has_reminder' => false, + 'happened_at_day_unknown' => false, + 'happened_at_month_unknown' => false, + 'happened_at' => now(), + ]; + + $this->expectException(ModelNotFoundException::class); + + app(CreateLifeEvent::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_life_event_type_is_not_linked_to_account() + { + $contact = factory(Contact::class)->create([]); + $lifeEventType = factory(LifeEventType::class)->create([]); + + $request = [ + 'contact_id' => $contact->id, + 'account_id' => $contact->account_id, + 'life_event_type_id' => $lifeEventType->id, + 'name' => 'This is a name', + 'note' => 'This is a note', + 'has_reminder' => false, + 'happened_at_day_unknown' => false, + 'happened_at_month_unknown' => false, + 'happened_at' => now(), + ]; + + $this->expectException(ModelNotFoundException::class); + + app(CreateLifeEvent::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Contact/LifeEvent/DestroyLifeEventTest.php b/tests/Unit/Services/Contact/LifeEvent/DestroyLifeEventTest.php new file mode 100644 index 0000000..8b0e759 --- /dev/null +++ b/tests/Unit/Services/Contact/LifeEvent/DestroyLifeEventTest.php @@ -0,0 +1,88 @@ +create([]); + + $request = [ + 'account_id' => $lifeEvent->account_id, + 'life_event_id' => $lifeEvent->id, + ]; + + $this->assertDatabaseHas('life_events', [ + 'id' => $lifeEvent->id, + ]); + + app(DestroyLifeEvent::class)->execute($request); + + $this->assertDatabaseMissing('life_events', [ + 'id' => $lifeEvent->id, + ]); + } + + /** @test */ + public function it_destroys_a_life_event_and_associated_reminder() + { + $lifeEvent = factory(LifeEvent::class)->create([]); + $reminder = factory(Reminder::class)->create([ + 'account_id' => $lifeEvent->account_id, + ]); + $lifeEvent->reminder_id = $reminder->id; + $lifeEvent->save(); + + $request = [ + 'account_id' => $lifeEvent->account_id, + 'life_event_id' => $lifeEvent->id, + ]; + + app(DestroyLifeEvent::class)->execute($request); + + $this->assertDatabaseMissing('reminders', [ + 'id' => $reminder->id, + ]); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $request = [ + 'account_id' => 1, + ]; + + $this->expectException(ValidationException::class); + + app(DestroyLifeEvent::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_life_event_doesnt_exist() + { + $account = factory(Account::class)->create(); + $lifeEvent = factory(LifeEvent::class)->create([]); + + $request = [ + 'account_id' => $account->id, + 'life_event_id' => $lifeEvent->id, + ]; + + $this->expectException(ModelNotFoundException::class); + + app(DestroyLifeEvent::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Contact/LifeEvent/UpdateLifeEventTest.php b/tests/Unit/Services/Contact/LifeEvent/UpdateLifeEventTest.php new file mode 100644 index 0000000..c571f86 --- /dev/null +++ b/tests/Unit/Services/Contact/LifeEvent/UpdateLifeEventTest.php @@ -0,0 +1,94 @@ +create([ + 'happened_at' => '2008-01-01', + ]); + $lifeEventType = factory(LifeEventType::class)->create([ + 'account_id' => $lifeEvent->account_id, + ]); + + $request = [ + 'life_event_id' => $lifeEvent->id, + 'account_id' => $lifeEvent->account_id, + 'life_event_type_id' => $lifeEventType->id, + 'happened_at' => '2018-01-01', + 'name' => 'This is a name', + 'note' => 'This is a note', + ]; + + $lifeEvent = app(UpdateLifeEvent::class)->execute($request); + + $this->assertDatabaseHas('life_events', [ + 'id' => $lifeEvent->id, + 'happened_at' => '2018-01-01 00:00:00', + 'life_event_type_id' => $lifeEventType->id, + 'contact_id' => $lifeEvent->contact_id, + 'account_id' => $lifeEvent->account_id, + 'name' => 'This is a name', + 'note' => 'This is a note', + ]); + + $this->assertInstanceOf( + LifeEvent::class, + $lifeEvent + ); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $contact = factory(Contact::class)->create([]); + + $request = [ + 'contact_id' => $contact->id, + 'happened_at' => now(), + ]; + + $this->expectException(ValidationException::class); + + app(UpdateLifeEvent::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_life_type_doesnt_exist() + { + $account = factory(Account::class)->create(); + $lifeEvent = factory(LifeEvent::class)->create([]); + $lifeEventType = factory(LifeEventType::class)->create([ + 'account_id' => $lifeEvent->account_id, + ]); + + $request = [ + 'account_id' => $account->id, + 'contact_id' => $lifeEvent->contact_id, + 'life_event_id' => $lifeEvent->id, + 'happened_at' => '2010-02-02', + 'life_event_type_id' => $lifeEventType->id, + 'name' => 'This is a name', + 'note' => 'This is a note', + ]; + + $this->expectException(ModelNotFoundException::class); + + app(UpdateLifeEvent::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Contact/Occupation/CreateOccupationTest.php b/tests/Unit/Services/Contact/Occupation/CreateOccupationTest.php new file mode 100644 index 0000000..a36fd62 --- /dev/null +++ b/tests/Unit/Services/Contact/Occupation/CreateOccupationTest.php @@ -0,0 +1,63 @@ +create([]); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + $company = factory(Company::class)->create([ + 'account_id' => $account->id, + ]); + + $request = [ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + 'company_id' => $company->id, + 'title' => 'Waiter', + ]; + + $occupation = app(CreateOccupation::class)->execute($request); + + $this->assertDatabaseHas('occupations', [ + 'id' => $occupation->id, + 'account_id' => $account->id, + 'title' => 'Waiter', + 'description' => null, + ]); + + $this->assertInstanceOf( + Occupation::class, + $occupation + ); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $account = factory(Account::class)->create([]); + + $request = [ + 'street' => '199 Lafayette Street', + ]; + + $this->expectException(ValidationException::class); + app(CreateOccupation::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Contact/Occupation/DestroyOccupationTest.php b/tests/Unit/Services/Contact/Occupation/DestroyOccupationTest.php new file mode 100644 index 0000000..47885b9 --- /dev/null +++ b/tests/Unit/Services/Contact/Occupation/DestroyOccupationTest.php @@ -0,0 +1,34 @@ +create([]); + + $request = [ + 'account_id' => $occupation->account_id, + 'occupation_id' => $occupation->id, + ]; + + $this->assertDatabaseHas('occupations', [ + 'id' => $occupation->id, + ]); + + app(DestroyOccupation::class)->execute($request); + + $this->assertDatabaseMissing('occupations', [ + 'id' => $occupation->id, + ]); + } +} diff --git a/tests/Unit/Services/Contact/Occupation/UpdateOccupationTest.php b/tests/Unit/Services/Contact/Occupation/UpdateOccupationTest.php new file mode 100644 index 0000000..813816d --- /dev/null +++ b/tests/Unit/Services/Contact/Occupation/UpdateOccupationTest.php @@ -0,0 +1,79 @@ +create([]); + + $request = [ + 'account_id' => $occupation->account_id, + 'contact_id' => $occupation->contact_id, + 'company_id' => $occupation->company_id, + 'occupation_id' => $occupation->id, + 'title' => 'Fashion girl', + 'description' => null, + 'salary' => '30000', + ]; + + $occupation = app(UpdateOccupation::class)->execute($request); + + $this->assertDatabaseHas('occupations', [ + 'id' => $occupation->id, + 'account_id' => $occupation->account_id, + 'contact_id' => $occupation->contact_id, + 'company_id' => $occupation->company_id, + 'title' => 'Fashion girl', + 'salary' => 30000, + ]); + + $this->assertInstanceOf( + Occupation::class, + $occupation + ); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $occupation = factory(Occupation::class)->create([]); + + $request = [ + 'name' => '199 Lafayette Street', + ]; + + $this->expectException(ValidationException::class); + app(UpdateOccupation::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_occupation_is_not_linked_to_account() + { + $account = factory(Account::class)->create([]); + $occupation = factory(Occupation::class)->create([]); + + $request = [ + 'account_id' => $account->id, + 'contact_id' => $occupation->contact_id, + 'company_id' => $occupation->company_id, + 'occupation_id' => $occupation->id, + 'title' => 'Fashion', + ]; + + $this->expectException(ModelNotFoundException::class); + app(UpdateOccupation::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Contact/Relationship/CreateRelationshipTest.php b/tests/Unit/Services/Contact/Relationship/CreateRelationshipTest.php new file mode 100644 index 0000000..5e8adc3 --- /dev/null +++ b/tests/Unit/Services/Contact/Relationship/CreateRelationshipTest.php @@ -0,0 +1,163 @@ +create(); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + $otherContact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + $relationshipType = factory(RelationshipType::class)->create([ + 'account_id' => $account->id, + ]); + + $request = [ + 'contact_is' => $contact->id, + 'of_contact' => $otherContact->id, + 'account_id' => $account->id, + 'relationship_type_id' => $relationshipType->id, + ]; + + $relationship = app(CreateRelationship::class)->execute($request); + + $this->assertDatabaseHas('relationships', [ + 'id' => $relationship->id, + 'account_id' => $account->id, + 'relationship_type_id' => $relationshipType->id, + 'contact_is' => $contact->id, + 'of_contact' => $otherContact->id, + ]); + } + + /** @test */ + public function it_fails_adding_relationship_when_relationship_type_is_unknown() + { + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + $otherContact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + $relationshipType = factory(RelationshipType::class)->create(); + + $request = [ + 'contact_is' => $contact->id, + 'of_contact' => $otherContact->id, + 'account_id' => $account->id, + 'relationship_type_id' => $relationshipType->id, + ]; + + $this->expectException(ModelNotFoundException::class); + + app(CreateRelationship::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_contact_is_not_linked_to_account() + { + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create(); + $otherContact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + $relationshipType = factory(RelationshipType::class)->create([ + 'account_id' => $account->id, + ]); + + $request = [ + 'contact_is' => $contact->id, + 'of_contact' => $otherContact->id, + 'account_id' => $account->id, + 'relationship_type_id' => $relationshipType->id, + ]; + + $this->expectException(ModelNotFoundException::class); + app(CreateRelationship::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_other_contact_is_not_linked_to_account() + { + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + $otherContact = factory(Contact::class)->create(); + $relationshipType = factory(RelationshipType::class)->create([ + 'account_id' => $account->id, + ]); + + $request = [ + 'contact_is' => $contact->id, + 'of_contact' => $otherContact->id, + 'account_id' => $account->id, + 'relationship_type_id' => $relationshipType->id, + ]; + + $this->expectException(ModelNotFoundException::class); + + app(CreateRelationship::class)->execute($request); + } + + /** @test */ + public function it_creates_a_relationship_and_reverse() + { + $account = factory(Account::class)->create(); + $contactA = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + $contactB = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + $relationshipTypeA = factory(RelationshipType::class)->create([ + 'account_id' => $account->id, + 'name' => 'uncle', + 'name_reverse_relationship' => 'nephew', + ]); + $relationshipTypeB = factory(RelationshipType::class)->create([ + 'account_id' => $account->id, + 'name' => 'nephew', + 'name_reverse_relationship' => 'uncle', + ]); + + $request = [ + 'account_id' => $account->id, + 'contact_is' => $contactA->id, + 'of_contact' => $contactB->id, + 'relationship_type_id' => $relationshipTypeA->id, + ]; + + app(CreateRelationship::class)->execute($request); + + $this->assertDatabaseHas('relationships', [ + 'account_id' => $account->id, + 'contact_is' => $contactA->id, + 'of_contact' => $contactB->id, + 'relationship_type_id' => $relationshipTypeA->id, + ]); + $this->assertDatabaseHas('relationships', [ + 'account_id' => $account->id, + 'contact_is' => $contactB->id, + 'of_contact' => $contactA->id, + 'relationship_type_id' => $relationshipTypeB->id, + ]); + } +} diff --git a/tests/Unit/Services/Contact/Relationship/DestroyRelationshipTest.php b/tests/Unit/Services/Contact/Relationship/DestroyRelationshipTest.php new file mode 100644 index 0000000..55ca0a4 --- /dev/null +++ b/tests/Unit/Services/Contact/Relationship/DestroyRelationshipTest.php @@ -0,0 +1,262 @@ +create([]); + $contactB = factory(Contact::class)->create([ + 'account_id' => $contactA->account_id, + ]); + + $relationship = factory(Relationship::class)->create([ + 'account_id' => $contactA->account_id, + 'contact_is' => $contactA, + 'of_contact' => $contactB, + ]); + + $request = [ + 'account_id' => $contactA->account_id, + 'relationship_id' => $relationship->id, + ]; + + app(DestroyRelationship::class)->execute($request); + + $this->assertDatabaseMissing('relationships', [ + 'id' => $relationship->id, + ]); + } + + /** @test */ + public function it_destroys_a_relationship_and_reverse() + { + $contactA = factory(Contact::class)->create([]); + $contactB = factory(Contact::class)->create([ + 'account_id' => $contactA->account_id, + ]); + + $relationshipTypeA = factory(RelationshipType::class)->create([ + 'account_id' => $contactA->account_id, + 'name' => 'uncle', + 'name_reverse_relationship' => 'nephew', + ]); + $relationshipA = factory(Relationship::class)->create([ + 'account_id' => $contactA->account_id, + 'contact_is' => $contactA, + 'of_contact' => $contactB, + 'relationship_type_id' => $relationshipTypeA->id, + ]); + + $relationshipTypeB = factory(RelationshipType::class)->create([ + 'account_id' => $contactA->account_id, + 'name' => 'nephew', + 'name_reverse_relationship' => 'uncle', + ]); + $relationshipB = factory(Relationship::class)->create([ + 'account_id' => $contactA->account_id, + 'contact_is' => $contactB, + 'of_contact' => $contactA, + 'relationship_type_id' => $relationshipTypeB->id, + ]); + + $request = [ + 'account_id' => $contactA->account_id, + 'relationship_id' => $relationshipA->id, + ]; + + $this->assertDatabaseHas('relationships', [ + 'id' => $relationshipA->id, + ]); + $this->assertDatabaseHas('relationships', [ + 'id' => $relationshipB->id, + ]); + + app(DestroyRelationship::class)->execute($request); + + $this->assertDatabaseMissing('relationships', [ + 'id' => $relationshipA->id, + ]); + $this->assertDatabaseMissing('relationships', [ + 'id' => $relationshipB->id, + ]); + } + + /** @test */ + public function it_destroys_a_relationship_and_reverse_and_partial_contact() + { + $contactA = factory(Contact::class)->create([]); + $contactB = factory(Contact::class)->create([ + 'account_id' => $contactA->account_id, + 'is_partial' => true, + ]); + + $relationshipTypeA = factory(RelationshipType::class)->create([ + 'account_id' => $contactA->account_id, + 'name' => 'uncle', + 'name_reverse_relationship' => 'nephew', + ]); + $relationshipA = factory(Relationship::class)->create([ + 'account_id' => $contactA->account_id, + 'contact_is' => $contactA, + 'of_contact' => $contactB, + 'relationship_type_id' => $relationshipTypeA->id, + ]); + + $relationshipTypeB = factory(RelationshipType::class)->create([ + 'account_id' => $contactA->account_id, + 'name' => 'nephew', + 'name_reverse_relationship' => 'uncle', + ]); + $relationshipB = factory(Relationship::class)->create([ + 'account_id' => $contactA->account_id, + 'contact_is' => $contactB, + 'of_contact' => $contactA, + 'relationship_type_id' => $relationshipTypeB->id, + ]); + + $request = [ + 'account_id' => $contactA->account_id, + 'relationship_id' => $relationshipA->id, + ]; + + $this->assertDatabaseHas('relationships', [ + 'id' => $relationshipA->id, + ]); + $this->assertDatabaseHas('relationships', [ + 'id' => $relationshipB->id, + ]); + + app(DestroyRelationship::class)->execute($request); + + $this->assertDatabaseMissing('relationships', [ + 'id' => $relationshipA->id, + ]); + $this->assertDatabaseMissing('relationships', [ + 'id' => $relationshipB->id, + ]); + $this->assertDatabaseMissing('contacts', [ + 'id' => $contactB->id, + 'deleted_at' => null, + ]); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $account = factory(Account::class)->create([]); + + $request = [ + 'account_id' => $account->id, + ]; + + $this->expectException(ValidationException::class); + + app(DestroyRelationship::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_relationship_is_not_linked_to_account() + { + $account = factory(Account::class)->create(); + $relationship = factory(Relationship::class)->create([]); + + $request = [ + 'account_id' => $account->id, + 'relationship_id' => $relationship->id, + ]; + + $this->expectException(ModelNotFoundException::class); + + app(DestroyRelationship::class)->execute($request); + } + + /** @test */ + public function it_deletes_relationship_between_two_contacts_and_deletes_the_contact() + { + $account = factory(Account::class)->create([]); + $contact = factory(Contact::class)->create(['account_id' => $account->id]); + $partner = factory(Contact::class)->create([ + 'account_id' => $account->id, + 'is_partial' => true, + ]); + $relationshipType = factory(RelationshipType::class)->create([ + 'account_id' => $account->id, + ]); + $relationship = factory(Relationship::class)->create([ + 'account_id' => $account->id, + 'contact_is' => $contact->id, + 'of_contact' => $partner->id, + 'relationship_type_id' => $relationshipType->id, + ]); + + app(DestroyRelationship::class)->execute([ + 'account_id' => $account->id, + 'relationship_id' => $relationship->id, + ]); + + $this->assertDatabaseMissing( + 'relationships', + [ + 'contact_is' => $contact->id, + 'of_contact' => $partner->id, + 'relationship_type_id' => $relationshipType->id, + ] + ); + } + + /** @test */ + public function it_deletes_relationship_between_two_contacts_and_doesnt_delete_the_contact() + { + $account = factory(Account::class)->create([]); + $contact = factory(Contact::class)->create(['account_id' => $account->id]); + $partner = factory(Contact::class)->create([ + 'account_id' => $account->id, + 'is_partial' => false, + ]); + $relationshipType = factory(RelationshipType::class)->create([ + 'account_id' => $account->id, + ]); + $relationship = factory(Relationship::class)->create([ + 'account_id' => $account->id, + 'contact_is' => $contact->id, + 'of_contact' => $partner->id, + 'relationship_type_id' => $relationshipType->id, + ]); + + app(DestroyRelationship::class)->execute([ + 'account_id' => $account->id, + 'relationship_id' => $relationship->id, + ]); + + $this->assertDatabaseMissing( + 'relationships', + [ + 'contact_is' => $contact->id, + 'of_contact' => $partner->id, + 'relationship_type_id' => $relationshipType->id, + ] + ); + + $this->assertDatabaseHas( + 'contacts', + [ + 'id' => $partner->id, + ] + ); + } +} diff --git a/tests/Unit/Services/Contact/Relationship/UpdateRelationshipTest.php b/tests/Unit/Services/Contact/Relationship/UpdateRelationshipTest.php new file mode 100644 index 0000000..c58c806 --- /dev/null +++ b/tests/Unit/Services/Contact/Relationship/UpdateRelationshipTest.php @@ -0,0 +1,157 @@ +create(); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + $otherContact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + $relationshipType0 = factory(RelationshipType::class)->create([ + 'account_id' => $account->id, + 'name' => 'son', + 'name_reverse_relationship' => 'father', + ]); + $relationshipType = factory(RelationshipType::class)->create([ + 'account_id' => $account->id, + 'name' => $relationshipType0->name_reverse_relationship, + 'name_reverse_relationship' => $relationshipType0->name, + ]); + $request = [ + 'contact_is' => $contact->id, + 'of_contact' => $otherContact->id, + 'account_id' => $account->id, + 'relationship_type_id' => $relationshipType->id, + ]; + + $relationship = app(CreateRelationship::class)->execute($request); + + $relationshipType0 = factory(RelationshipType::class)->create([ + 'account_id' => $account->id, + 'name' => 'uncle', + 'name_reverse_relationship' => 'nephew', + ]); + $relationshipType = factory(RelationshipType::class)->create([ + 'account_id' => $account->id, + 'name' => $relationshipType0->name_reverse_relationship, + 'name_reverse_relationship' => $relationshipType0->name, + ]); + + $request = [ + 'account_id' => $account->id, + 'relationship_id' => $relationship->id, + 'relationship_type_id' => $relationshipType->id, + ]; + + $newRelationship = app(UpdateRelationship::class)->execute($request); + + $this->assertDatabaseHas('relationships', [ + 'id' => $newRelationship->id, + 'account_id' => $account->id, + 'relationship_type_id' => $relationshipType->id, + 'contact_is' => $contact->id, + 'of_contact' => $otherContact->id, + ]); + } + + /** @test */ + public function it_updates_a_partial_relationship() + { + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + $otherContact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + $relationship = factory(Relationship::class)->create([ + 'account_id' => $account->id, + 'contact_is' => $contact->id, + 'of_contact' => $otherContact->id, + ]); + + $relationshipType0 = factory(RelationshipType::class)->create([ + 'account_id' => $account->id, + 'name' => 'name', + ]); + $relationshipType = factory(RelationshipType::class)->create([ + 'account_id' => $account->id, + 'name_reverse_relationship' => $relationshipType0->name, + ]); + + $request = [ + 'account_id' => $account->id, + 'relationship_id' => $relationship->id, + 'relationship_type_id' => $relationshipType->id, + ]; + + $newRelationship = app(UpdateRelationship::class)->execute($request); + + $this->assertDatabaseHas('relationships', [ + 'id' => $newRelationship->id, + 'account_id' => $account->id, + 'relationship_type_id' => $relationshipType->id, + 'contact_is' => $contact->id, + 'of_contact' => $otherContact->id, + ]); + } + + /** @test */ + public function it_throws_an_exception_if_relationship_is_not_linked_to_account() + { + $account = factory(Account::class)->create(); + $relationship = factory(Relationship::class)->create(); + $relationshipType = factory(RelationshipType::class)->create([ + 'account_id' => $account->id, + ]); + + $request = [ + 'account_id' => $account->id, + 'relationship_id' => $relationship->id, + 'relationship_type_id' => $relationshipType->id, + ]; + + $this->expectException(ModelNotFoundException::class); + + app(UpdateRelationship::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_relationship_type_is_not_linked_to_account() + { + $account = factory(Account::class)->create(); + $relationship = factory(Relationship::class)->create([ + 'account_id' => $account->id, + ]); + $relationshipType = factory(RelationshipType::class)->create(); + + $request = [ + 'account_id' => $account->id, + 'relationship_id' => $relationship->id, + 'relationship_type_id' => $relationshipType->id, + ]; + + $this->expectException(ModelNotFoundException::class); + + app(UpdateRelationship::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Contact/Reminder/CreateReminderTest.php b/tests/Unit/Services/Contact/Reminder/CreateReminderTest.php new file mode 100644 index 0000000..e3dae6d --- /dev/null +++ b/tests/Unit/Services/Contact/Reminder/CreateReminderTest.php @@ -0,0 +1,228 @@ +create([]); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $request = [ + 'contact_id' => $contact->id, + 'account_id' => $contact->account_id, + 'initial_date' => '2017-02-01', + 'frequency_type' => 'year', + 'frequency_number' => 1, + 'title' => 'title', + 'description' => 'description', + ]; + + $reminder = app(CreateReminder::class)->execute($request); + + $this->assertDatabaseHas('reminders', [ + 'id' => $reminder->id, + 'contact_id' => $contact->id, + 'account_id' => $contact->account_id, + ]); + + $this->assertInstanceOf( + Reminder::class, + $reminder + ); + + $this->assertDatabaseHas('reminder_outbox', [ + 'reminder_id' => $reminder->id, + 'account_id' => $contact->account_id, + 'planned_date' => '2017-02-01', + 'nature' => 'reminder', + ]); + } + + /** @test */ + public function it_stores_a_one_time_reminder() + { + Carbon::setTestNow(Carbon::create(2017, 1, 1)); + $user = factory(User::class)->create([]); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + + $request = [ + 'contact_id' => $contact->id, + 'account_id' => $contact->account_id, + 'initial_date' => '2017-02-01', + 'frequency_type' => 'one_time', + 'frequency_number' => 1, + 'title' => 'title', + 'description' => 'description', + ]; + + $reminder = app(CreateReminder::class)->execute($request); + + $this->assertDatabaseHas('reminders', [ + 'id' => $reminder->id, + 'contact_id' => $contact->id, + 'account_id' => $contact->account_id, + ]); + + $this->assertInstanceOf( + Reminder::class, + $reminder + ); + + $this->assertDatabaseHas('reminder_outbox', [ + 'reminder_id' => $reminder->id, + 'account_id' => $contact->account_id, + 'planned_date' => '2017-02-01', + 'nature' => 'reminder', + ]); + } + + /** @test */ + public function it_stores_a_reminder_for_each_user_of_an_account() + { + Carbon::setTestNow(Carbon::create(2017, 1, 1)); + $account = factory(Account::class)->create([]); + $userA = factory(User::class)->create([ + 'account_id' => $account->id, + ]); + $userB = factory(User::class)->create([ + 'account_id' => $account->id, + ]); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + + $request = [ + 'contact_id' => $contact->id, + 'account_id' => $contact->account_id, + 'initial_date' => '2017-02-01', + 'frequency_type' => 'one_time', + 'frequency_number' => 1, + 'title' => 'title', + 'description' => 'description', + ]; + + $reminder = app(CreateReminder::class)->execute($request); + + $this->assertDatabaseHas('reminders', [ + 'id' => $reminder->id, + 'contact_id' => $contact->id, + 'account_id' => $contact->account_id, + ]); + + $this->assertInstanceOf( + Reminder::class, + $reminder + ); + + $this->assertDatabaseHas('reminder_outbox', [ + 'reminder_id' => $reminder->id, + 'account_id' => $contact->account_id, + 'planned_date' => '2017-02-01', + 'nature' => 'reminder', + 'user_id' => $userA->id, + ]); + $this->assertDatabaseHas('reminder_outbox', [ + 'reminder_id' => $reminder->id, + 'account_id' => $contact->account_id, + 'planned_date' => '2017-02-01', + 'nature' => 'reminder', + 'user_id' => $userB->id, + ]); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $contact = factory(Contact::class)->create([]); + + $request = [ + 'contact_id' => $contact->id, + 'initial_date' => now(), + ]; + + $this->expectException(ValidationException::class); + + $reminderService = app(CreateReminder::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_ids_are_not_found() + { + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create([]); + + $request = [ + 'contact_id' => $contact->id, + 'account_id' => $account->id, + 'initial_date' => '2017-02-02', + 'frequency_type' => 'year', + 'frequency_number' => 1, + 'title' => 'title', + 'description' => 'description', + ]; + + $this->expectException(ModelNotFoundException::class); + + $reminder = app(CreateReminder::class)->execute($request); + + $request = [ + 'contact_id' => $contact->id, + 'account_id' => $contact->account_id, + 'initial_date' => '2017-02-02', + 'frequency_type' => 'year', + 'frequency_number' => 1, + 'title' => 'title', + 'description' => 'description', + ]; + + $this->expectException(ModelNotFoundException::class); + + $reminderService = app(CreateReminder::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_frequency_type_is_not_right() + { + $contact = factory(Contact::class)->create([]); + + $request = [ + 'contact_id' => $contact->id, + 'account_id' => $contact->account_id, + 'initial_date' => '2017-02-02', + 'frequency_type' => 'blabla', + 'frequency_number' => 1, + 'title' => 'title', + 'description' => 'description', + ]; + + $this->expectException(ValidationException::class); + + try { + $reminderService = app(CreateReminder::class)->execute($request); + } catch (ValidationException $e) { + $this->assertEquals(['The selected frequency type is invalid.'], $e->validator->errors()->all()); + throw $e; + } + } +} diff --git a/tests/Unit/Services/Contact/Reminder/DestroyReminderTest.php b/tests/Unit/Services/Contact/Reminder/DestroyReminderTest.php new file mode 100644 index 0000000..19790c1 --- /dev/null +++ b/tests/Unit/Services/Contact/Reminder/DestroyReminderTest.php @@ -0,0 +1,89 @@ +create([ + 'initial_date' => '2017-02-02', + 'frequency_type' => 'year', + 'frequency_number' => 1, + 'title' => 'title', + 'description' => 'description', + ]); + + $request = [ + 'account_id' => $reminder->account_id, + 'reminder_id' => $reminder->id, + ]; + + $this->assertDatabaseHas('reminders', [ + 'id' => $reminder->id, + ]); + + app(DestroyReminder::class)->execute($request); + + $this->assertDatabaseMissing('reminders', [ + 'id' => $reminder->id, + ]); + } + + /** @test */ + public function it_destroys_scheduled_reminders() + { + // prepare a reminder and schedule some notifications + Carbon::setTestNow(Carbon::create(2017, 2, 1)); + $user = factory(User::class)->create([]); + $reminder = factory(Reminder::class)->create([ + 'account_id' => $user->account_id, + 'initial_date' => '2017-02-02', + 'frequency_type' => 'year', + 'frequency_number' => 1, + 'title' => 'title', + 'description' => 'description', + ]); + $reminderRule = factory(ReminderRule::class)->create([ + 'account_id' => $reminder->account_id, + 'number_of_days_before' => 30, + 'active' => 1, + ]); + + $reminder->schedule($user); + + $this->assertDatabaseHas('reminder_outbox', [ + 'reminder_id' => $reminder->id, + ]); + + $request = [ + 'account_id' => $reminder->account_id, + 'reminder_id' => $reminder->id, + ]; + + $this->assertDatabaseHas('reminders', [ + 'id' => $reminder->id, + ]); + + app(DestroyReminder::class)->execute($request); + + $this->assertDatabaseMissing('reminders', [ + 'id' => $reminder->id, + ]); + + $this->assertDatabaseMissing('reminder_outbox', [ + 'reminder_id' => $reminder->id, + ]); + } +} diff --git a/tests/Unit/Services/Contact/Reminder/UpdateReminderTest.php b/tests/Unit/Services/Contact/Reminder/UpdateReminderTest.php new file mode 100644 index 0000000..5b289c6 --- /dev/null +++ b/tests/Unit/Services/Contact/Reminder/UpdateReminderTest.php @@ -0,0 +1,110 @@ +create([]); + $reminder = factory(Reminder::class)->create([ + 'account_id' => $user->account_id, + 'initial_date' => '2017-02-02', + 'frequency_type' => 'year', + 'frequency_number' => 1, + 'title' => 'title', + 'description' => 'description', + ]); + + $request = [ + 'contact_id' => $reminder->contact_id, + 'account_id' => $reminder->contact->account_id, + 'reminder_id' => $reminder->id, + 'initial_date' => '2017-10-01', + 'frequency_type' => 'month', + 'frequency_number' => 1, + 'title' => 'title', + ]; + + $reminder = app(UpdateReminder::class)->execute($request); + + $this->assertDatabaseHas('reminders', [ + 'id' => $reminder->id, + 'contact_id' => $reminder->contact_id, + 'account_id' => $reminder->contact->account_id, + 'initial_date' => '2017-10-01', + ]); + + $this->assertInstanceOf( + Reminder::class, + $reminder + ); + + $this->assertDatabaseHas('reminder_outbox', [ + 'reminder_id' => $reminder->id, + 'account_id' => $reminder->contact->account_id, + 'planned_date' => '2017-10-01', + 'nature' => 'reminder', + ]); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $contact = factory(Contact::class)->create([]); + + $request = [ + 'contact_id' => $contact->id, + 'initial_date' => now(), + ]; + + $this->expectException(ValidationException::class); + + app(UpdateReminder::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_frequency_type_is_not_right() + { + $reminder = factory(Reminder::class)->create([ + 'initial_date' => '2017-02-02', + 'frequency_type' => 'year', + 'frequency_number' => 1, + 'title' => 'title', + 'description' => 'description', + ]); + + $request = [ + 'contact_id' => $reminder->contact_id, + 'account_id' => $reminder->contact->account_id, + 'reminder_id' => $reminder->id, + 'initial_date' => '2017-02-02', + 'frequency_type' => 'blabla', + 'frequency_number' => 1, + 'title' => 'title', + 'description' => 'description', + ]; + + $this->expectException(ValidationException::class); + + try { + app(UpdateReminder::class)->execute($request); + } catch (ValidationException $e) { + $this->assertEquals(['The selected frequency type is invalid.'], $e->validator->errors()->all()); + throw $e; + } + } +} diff --git a/tests/Unit/Services/Contact/Tag/AssociateTagTest.php b/tests/Unit/Services/Contact/Tag/AssociateTagTest.php new file mode 100644 index 0000000..0339928 --- /dev/null +++ b/tests/Unit/Services/Contact/Tag/AssociateTagTest.php @@ -0,0 +1,154 @@ +create([]); + + $request = [ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'name' => '朋友', + ]; + + $tag = app(AssociateTag::class)->execute($request); + + $this->assertDatabaseHas('tags', [ + 'account_id' => $contact->account_id, + 'name' => '朋友', + 'name_slug' => '朋友', + ]); + + $this->assertDatabaseHas('contact_tag', [ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'tag_id' => $tag->id, + ]); + + $this->assertInstanceOf( + Tag::class, + $tag + ); + } + + /** @test */ + public function it_sets_a_tag_to_a_contact_when_tag_doesnt_exist_yet() + { + $contact = factory(Contact::class)->create([]); + + $request = [ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'name' => 'Central Perk', + ]; + + $tag = app(AssociateTag::class)->execute($request); + + $this->assertDatabaseHas('tags', [ + 'account_id' => $contact->account_id, + 'name' => 'Central Perk', + 'name_slug' => 'central-perk', + ]); + + $this->assertDatabaseHas('contact_tag', [ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'tag_id' => $tag->id, + ]); + + $this->assertInstanceOf( + Tag::class, + $tag + ); + } + + /** @test */ + public function it_sets_a_tag_to_a_contact_when_tag_does_exist_yet() + { + $contact = factory(Contact::class)->create([]); + $tag = factory(Tag::class)->create([ + 'account_id' => $contact->account_id, + ]); + + $this->assertDatabaseHas('tags', [ + 'account_id' => $contact->account_id, + 'name' => $tag->name, + 'name_slug' => $tag->name_slug, + ]); + + $this->assertDatabaseMissing('contact_tag', [ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'tag_id' => $tag->id, + ]); + + $request = [ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'name' => 'Central Perk', + ]; + + $tag = app(AssociateTag::class)->execute($request); + + $this->assertDatabaseHas('tags', [ + 'account_id' => $contact->account_id, + 'name' => 'Central Perk', + 'name_slug' => 'central-perk', + ]); + + $this->assertDatabaseHas('contact_tag', [ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'tag_id' => $tag->id, + ]); + + $this->assertInstanceOf( + Tag::class, + $tag + ); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $request = [ + 'account_id' => 1, + 'contact_id' => 2, + ]; + + $this->expectException(ValidationException::class); + + app(AssociateTag::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_contact_does_not_exist() + { + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create(); + + $request = [ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + 'name' => 'Central Perk', + ]; + + $this->expectException(ModelNotFoundException::class); + app(AssociateTag::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Contact/Tag/CreateTagTest.php b/tests/Unit/Services/Contact/Tag/CreateTagTest.php new file mode 100644 index 0000000..17222b7 --- /dev/null +++ b/tests/Unit/Services/Contact/Tag/CreateTagTest.php @@ -0,0 +1,51 @@ +create([]); + + $request = [ + 'account_id' => $tag->account_id, + 'name' => 'Central Perk', + ]; + + $tag = app(CreateTag::class)->execute($request); + + $this->assertDatabaseHas('tags', [ + 'id' => $tag->id, + 'name' => 'Central Perk', + 'name_slug' => 'central-perk', + ]); + + $this->assertInstanceOf( + Tag::class, + $tag + ); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $request = [ + 'account_id' => 1, + 'tag_id' => 2, + ]; + + $this->expectException(ValidationException::class); + + app(CreateTag::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Contact/Tag/DestroyTagTest.php b/tests/Unit/Services/Contact/Tag/DestroyTagTest.php new file mode 100644 index 0000000..6c709c2 --- /dev/null +++ b/tests/Unit/Services/Contact/Tag/DestroyTagTest.php @@ -0,0 +1,83 @@ +create([]); + + $tag = factory(Tag::class)->create([ + 'account_id' => $contact->account_id, + ]); + + $contact->tags()->syncWithoutDetaching([ + $tag->id => [ + 'account_id' => $contact->account_id, + ], + ]); + + $this->assertDatabaseHas('contact_tag', [ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'tag_id' => $tag->id, + ]); + + $request = [ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'tag_id' => $tag->id, + ]; + + app(DestroyTag::class)->execute($request); + + $this->assertDatabaseMissing('contact_tag', [ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + ]); + + $this->assertDatabaseMissing('tags', [ + 'account_id' => $contact->account_id, + 'id' => $tag->id, + ]); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $request = [ + 'account_id' => 1, + ]; + + $this->expectException(ValidationException::class); + + app(DestroyTag::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_tag_does_not_exist() + { + $account = factory(Account::class)->create(); + + $request = [ + 'account_id' => $account->id, + 'tag_id' => 123232, + ]; + + $this->expectException(ModelNotFoundException::class); + app(DestroyTag::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Contact/Tag/DetachTagTest.php b/tests/Unit/Services/Contact/Tag/DetachTagTest.php new file mode 100644 index 0000000..0a5a9ad --- /dev/null +++ b/tests/Unit/Services/Contact/Tag/DetachTagTest.php @@ -0,0 +1,82 @@ +create([]); + + $tag = factory(Tag::class)->create([ + 'account_id' => $contact->account_id, + ]); + + $contact->tags()->syncWithoutDetaching([ + $tag->id => [ + 'account_id' => $contact->account_id, + ], + ]); + + $this->assertDatabaseHas('contact_tag', [ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'tag_id' => $tag->id, + ]); + + $request = [ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'tag_id' => $tag->id, + ]; + + app(DetachTag::class)->execute($request); + + $this->assertDatabaseMissing('contact_tag', [ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + ]); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $request = [ + 'account_id' => 1, + ]; + + $this->expectException(ValidationException::class); + + app(DetachTag::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_contact_does_not_exist() + { + $account = factory(Account::class)->create(); + $tag = factory(Tag::class)->create([ + 'account_id' => $account->id, + ]); + + $request = [ + 'account_id' => $account->id, + 'contact_id' => 12322, + 'tag_id' => $tag->id, + ]; + + $this->expectException(ModelNotFoundException::class); + app(DetachTag::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Contact/Tag/UpdateTagTest.php b/tests/Unit/Services/Contact/Tag/UpdateTagTest.php new file mode 100644 index 0000000..eba9e70 --- /dev/null +++ b/tests/Unit/Services/Contact/Tag/UpdateTagTest.php @@ -0,0 +1,69 @@ +create([]); + + $request = [ + 'account_id' => $tag->account_id, + 'tag_id' => $tag->id, + 'name' => 'Central Perk', + ]; + + $tag = app(UpdateTag::class)->execute($request); + + $this->assertDatabaseHas('tags', [ + 'id' => $tag->id, + 'name' => 'Central Perk', + 'name_slug' => 'central-perk', + ]); + + $this->assertInstanceOf( + Tag::class, + $tag + ); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $request = [ + 'account_id' => 1, + 'tag_id' => 2, + ]; + + $this->expectException(ValidationException::class); + + app(UpdateTag::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_tag_does_not_exist() + { + $account = factory(Account::class)->create(); + + $request = [ + 'account_id' => $account->id, + 'tag_id' => 1232322, + 'name' => 'Central Perk', + ]; + + $this->expectException(ModelNotFoundException::class); + app(UpdateTag::class)->execute($request); + } +} diff --git a/tests/Unit/Services/DavClient/AddAddressBookTest.php b/tests/Unit/Services/DavClient/AddAddressBookTest.php new file mode 100644 index 0000000..098f5e0 --- /dev/null +++ b/tests/Unit/Services/DavClient/AddAddressBookTest.php @@ -0,0 +1,129 @@ +create([]); + + $this->mock(AddressBookGetter::class, function (MockInterface $mock) { + $mock->shouldReceive('execute') + ->once() + ->andReturn($this->mockReturn()); + }); + + $request = [ + 'account_id' => $user->account_id, + 'user_id' => $user->id, + 'base_uri' => 'https://test', + 'username' => 'test', + 'password' => 'test', + ]; + + $addressBookSubscription = (new CreateAddressBookSubscription())->execute($request); + + $this->assertDatabaseHas('addressbooks', [ + 'id' => $addressBookSubscription->address_book_id, + 'account_id' => $user->account_id, + 'user_id' => $user->id, + 'name' => 'contacts1', + ]); + $this->assertDatabaseHas('addressbook_subscriptions', [ + 'id' => $addressBookSubscription->id, + 'account_id' => $user->account_id, + 'user_id' => $user->id, + 'address_book_id' => $addressBookSubscription->address_book_id, + 'capabilities' => json_encode([ + 'addressbookMultiget' => true, + 'addressbookQuery' => true, + 'syncCollection' => true, + 'addressData' => [ + 'content-type' => 'text/vcard', + 'version' => '4.0', + ], + ]), + ]); + + $this->assertInstanceOf( + AddressBookSubscription::class, + $addressBookSubscription + ); + } + + /** @test */ + public function it_creates_next_addressbook() + { + $user = factory(User::class)->create([]); + AddressBook::factory()->create([ + 'account_id' => $user->account_id, + 'user_id' => $user->id, + 'name' => 'contacts5', + ]); + + $this->mock(AddressBookGetter::class, function (MockInterface $mock) { + $mock->shouldReceive('execute') + ->once() + ->andReturn($this->mockReturn()); + }); + + $request = [ + 'account_id' => $user->account_id, + 'user_id' => $user->id, + 'base_uri' => 'https://test', + 'username' => 'test', + 'password' => 'test', + ]; + + $addressBookSubscription = app(CreateAddressBookSubscription::class)->execute($request); + + $this->assertDatabaseHas('addressbooks', [ + 'id' => $addressBookSubscription->address_book_id, + 'account_id' => $user->account_id, + 'user_id' => $user->id, + 'name' => 'contacts6', + ]); + $this->assertDatabaseHas('addressbook_subscriptions', [ + 'id' => $addressBookSubscription->id, + 'account_id' => $user->account_id, + 'user_id' => $user->id, + 'address_book_id' => $addressBookSubscription->address_book_id, + ]); + + $this->assertInstanceOf( + AddressBookSubscription::class, + $addressBookSubscription + ); + } + + private function mockReturn(): array + { + return [ + 'uri' => 'https://test/dav', + 'capabilities' => [ + 'addressbookMultiget' => true, + 'addressbookQuery' => true, + 'syncCollection' => true, + 'addressData' => [ + 'content-type' => 'text/vcard', + 'version' => '4.0', + ], + ], + 'name' => 'Test', + ]; + } +} diff --git a/tests/Unit/Services/DavClient/SynchronizeAddressBookTest.php b/tests/Unit/Services/DavClient/SynchronizeAddressBookTest.php new file mode 100644 index 0000000..c629959 --- /dev/null +++ b/tests/Unit/Services/DavClient/SynchronizeAddressBookTest.php @@ -0,0 +1,62 @@ +mock(AddressBookSynchronizer::class, function (MockInterface $mock) { + $mock->shouldReceive('execute') + ->once() + ->withArgs(function ($sync, $force) { + $this->assertFalse($force); + + return true; + }); + }); + + $subscription = AddressBookSubscription::factory()->create(); + + $request = [ + 'account_id' => $subscription->account_id, + 'addressbook_subscription_id' => $subscription->id, + ]; + + (new SynchronizeAddressBook())->execute($request); + } + + /** @test */ + public function it_runs_sync_force() + { + $this->mock(AddressBookSynchronizer::class, function (MockInterface $mock) { + $mock->shouldReceive('execute') + ->once() + ->withArgs(function ($sync, $force) { + $this->assertTrue($force); + + return true; + }); + }); + + $subscription = AddressBookSubscription::factory()->create(); + + $request = [ + 'account_id' => $subscription->account_id, + 'addressbook_subscription_id' => $subscription->id, + 'force' => true, + ]; + + (new SynchronizeAddressBook())->execute($request); + } +} diff --git a/tests/Unit/Services/DavClient/UpdateSubscriptionLocalSyncTokenTest.php b/tests/Unit/Services/DavClient/UpdateSubscriptionLocalSyncTokenTest.php new file mode 100644 index 0000000..1c31d40 --- /dev/null +++ b/tests/Unit/Services/DavClient/UpdateSubscriptionLocalSyncTokenTest.php @@ -0,0 +1,78 @@ +create([ + 'name' => 'contacts1', + ]); + $token = factory(SyncToken::class)->create([ + 'account_id' => $subscription->account_id, + 'user_id' => $subscription->user_id, + 'name' => 'contacts1', + 'timestamp' => now()->addDays(-1), + ]); + + $this->mock(CardDAVBackend::class, function (MockInterface $mock) use ($token) { + $mock->shouldReceive('init')->andReturn($mock); + $mock->shouldReceive('getCurrentSyncToken') + ->withArgs(function ($name) { + $this->assertEquals($name, 'contacts1'); + + return true; + }) + ->andReturn($token); + }); + + (new UpdateSubscriptionLocalSyncToken())->execute([ + 'account_id' => $subscription->account_id, + 'addressbook_subscription_id' => $subscription->id, + ]); + + $subscription->refresh(); + + $this->assertEquals($token->id, $subscription->localSyncToken); + } + + /** @test */ + public function it_wont_update_null_token() + { + $subscription = AddressBookSubscription::factory()->create([ + 'name' => 'contacts1', + ]); + + $this->mock(CardDAVBackend::class, function (MockInterface $mock) { + $mock->shouldReceive('init')->andReturn($mock); + $mock->shouldReceive('getCurrentSyncToken') + ->withArgs(function ($name) { + $this->assertEquals($name, 'contacts1'); + + return true; + }) + ->andReturn(null); + }); + + (new UpdateSubscriptionLocalSyncToken())->execute([ + 'account_id' => $subscription->account_id, + 'addressbook_subscription_id' => $subscription->id, + ]); + + $subscription->refresh(); + + $this->assertNull($subscription->localSyncToken); + } +} diff --git a/tests/Unit/Services/DavClient/Utils/AddressBookContactsPushMissedTest.php b/tests/Unit/Services/DavClient/Utils/AddressBookContactsPushMissedTest.php new file mode 100644 index 0000000..9f3a2f4 --- /dev/null +++ b/tests/Unit/Services/DavClient/Utils/AddressBookContactsPushMissedTest.php @@ -0,0 +1,87 @@ +create(); + $token = factory(SyncToken::class)->create([ + 'account_id' => $subscription->account_id, + 'user_id' => $subscription->user_id, + 'name' => 'contacts1', + 'timestamp' => now()->addDays(-1), + ]); + $subscription->localSyncToken = $token->id; + $subscription->save(); + + $contact = factory(Contact::class)->create([ + 'account_id' => $subscription->account_id, + 'first_name' => 'Test', + 'uuid' => 'affacde9-b2fe-4371-9acb-6612aaee6971', + ]); + $card = $this->getCard($contact); + $etag = $this->getEtag($contact, true); + + $this->mock(CardDAVBackend::class, function (MockInterface $mock) use ($card, $etag, $contact) { + $mock->shouldReceive('init')->andReturn($mock); + $mock->shouldReceive('getUuid') + ->once() + ->withArgs(function ($uri) { + $this->assertEquals('uuid6', $uri); + + return true; + }) + ->andReturn('uuid3'); + $mock->shouldReceive('prepareCard') + ->once() + ->withArgs(function ($c) use ($contact) { + $this->assertEquals($contact, $c); + + return true; + }) + ->andReturn([ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'carddata' => $card, + 'uri' => 'uuid3', + 'etag' => $etag, + ]); + }); + + $client = (new DavTester())->fake()->client(); + + $batchs = (new AddressBookContactsPushMissed()) + ->execute(new SyncDto($subscription, $client), [], collect([ + 'uuid6' => new ContactDto('uuid6', $etag), + ]), collect([$contact])); + + $this->assertCount(1, $batchs); + $batch = $batchs->first(); + $this->assertInstanceOf(PushVCard::class, $batch); + $dto = $this->getPrivateValue($batch, 'contact'); + $this->assertInstanceOf(ContactPushDto::class, $dto); + $this->assertEquals('uuid3', $dto->uri); + $this->assertEquals(2, $dto->mode); + } +} diff --git a/tests/Unit/Services/DavClient/Utils/AddressBookContactsPushTest.php b/tests/Unit/Services/DavClient/Utils/AddressBookContactsPushTest.php new file mode 100644 index 0000000..b70b0f0 --- /dev/null +++ b/tests/Unit/Services/DavClient/Utils/AddressBookContactsPushTest.php @@ -0,0 +1,170 @@ +create(); + $token = factory(SyncToken::class)->create([ + 'account_id' => $subscription->account_id, + 'user_id' => $subscription->user_id, + 'name' => 'contacts1', + 'timestamp' => now()->addDays(-1), + ]); + $subscription->localSyncToken = $token->id; + $subscription->save(); + + $contact = factory(Contact::class)->create([ + 'account_id' => $subscription->account_id, + 'first_name' => 'Test', + 'uuid' => 'affacde9-b2fe-4371-9acb-6612aaee6971', + ]); + $card = $this->getCard($contact); + $etag = $this->getEtag($contact, true); + + $this->mock(CardDAVBackend::class, function (MockInterface $mock) use ($contact, $card, $etag) { + $mock->shouldReceive('init')->andReturn($mock); + $mock->shouldReceive('getCard') + ->withArgs(function ($name, $uri) { + $this->assertEquals($uri, 'uricontact2'); + + return true; + }) + ->andReturn([ + 'contact_id' => $contact->id, + 'carddata' => $card, + 'etag' => $etag, + 'distant_etag' => $etag, + ]); + $mock->shouldReceive('getUuid') + ->withArgs(function ($uri) { + $this->assertEquals($uri, 'https://test/dav/uricontact1'); + + return true; + }) + ->andReturn('uricontact1'); + }); + + $client = (new DavTester())->fake()->client(); + + $batchs = (new AddressBookContactsPush()) + ->execute(new SyncDto($subscription, $client), collect([ + 'https://test/dav/uricontact1' => new ContactDto('https://test/dav/uricontact1', $etag), + ]), [ + 'added' => ['uricontact2'], + ]); + + $this->assertCount(1, $batchs); + $batch = $batchs->first(); + $this->assertInstanceOf(PushVCard::class, $batch); + $dto = $this->getPrivateValue($batch, 'contact'); + $this->assertInstanceOf(ContactPushDto::class, $dto); + $this->assertEquals('uricontact2', $dto->uri); + $this->assertEquals(ContactPushDto::MODE_MATCH_NONE, $dto->mode); + } + + /** @test */ + public function it_push_contacts_modified() + { + $subscription = AddressBookSubscription::factory()->create(); + $token = factory(SyncToken::class)->create([ + 'account_id' => $subscription->account_id, + 'user_id' => $subscription->user_id, + 'name' => 'contacts1', + 'timestamp' => now()->addDays(-1), + ]); + $subscription->localSyncToken = $token->id; + $subscription->save(); + + $contact = factory(Contact::class)->create([ + 'account_id' => $subscription->account_id, + 'first_name' => 'Test', + 'uuid' => 'affacde9-b2fe-4371-9acb-6612aaee6971', + ]); + $card = $this->getCard($contact); + $etag = $this->getEtag($contact, true); + + $this->mock(CardDAVBackend::class, function (MockInterface $mock) use ($contact, $card, $etag) { + $mock->shouldReceive('init')->andReturn($mock); + $mock->shouldReceive('getUuid') + ->withArgs(function ($uri) { + $this->assertStringContainsString('uricontact', $uri); + + return true; + }) + ->andReturnUsing(function ($uri) { + return Str::contains($uri, 'uricontact1') ? 'uricontact1' : 'uricontact2'; + }); + $mock->shouldReceive('getCard') + ->withArgs(function ($name, $uri) { + $this->assertEquals($uri, 'uricontact2'); + + return true; + }) + ->andReturn([ + 'contact_id' => $contact->id, + 'carddata' => $card, + 'etag' => $etag, + 'distant_etag' => $etag, + ]); + }); + + $client = (new DavTester())->fake()->client(); + + $batchs = (new AddressBookContactsPush()) + ->execute(new SyncDto($subscription, $client), collect([ + 'https://test/dav/uricontact1' => new ContactDto('https://test/dav/uricontact1', $etag), + ]), [ + 'modified' => ['uricontact2'], + ]); + + $this->assertCount(1, $batchs); + $batch = $batchs->first(); + $this->assertInstanceOf(PushVCard::class, $batch); + $dto = $this->getPrivateValue($batch, 'contact'); + $this->assertInstanceOf(ContactPushDto::class, $dto); + $this->assertEquals('uricontact2', $dto->uri); + $this->assertEquals(1, $dto->mode); + } + + /** @test */ + public function it_delete_contacts_removed() + { + $subscription = AddressBookSubscription::factory()->create(); + $client = (new DavTester())->fake()->client(); + + $batchs = (new AddressBookContactsPush()) + ->execute(new SyncDto($subscription, $client), collect(), [ + 'deleted' => ['uricontact2'], + ]); + + $this->assertCount(1, $batchs); + $batch = $batchs->first(); + $this->assertInstanceOf(DeleteVCard::class, $batch); + $uri = $this->getPrivateValue($batch, 'uri'); + $this->assertEquals('uricontact2', $uri); + } +} diff --git a/tests/Unit/Services/DavClient/Utils/AddressBookContactsUpdaterMissedTest.php b/tests/Unit/Services/DavClient/Utils/AddressBookContactsUpdaterMissedTest.php new file mode 100644 index 0000000..34fefc7 --- /dev/null +++ b/tests/Unit/Services/DavClient/Utils/AddressBookContactsUpdaterMissedTest.php @@ -0,0 +1,81 @@ +create(); + $token = factory(SyncToken::class)->create([ + 'account_id' => $subscription->account_id, + 'user_id' => $subscription->user_id, + 'name' => 'contacts1', + 'timestamp' => now()->addDays(-1), + ]); + $subscription->localSyncToken = $token->id; + $subscription->save(); + + $contact = new Contact(); + $contact->forceFill([ + 'first_name' => 'Test', + 'uuid' => 'affacde9-b2fe-4371-9acb-6612aaee6971', + 'updated_at' => now(), + ]); + $card = $this->getCard($contact); + $etag = $this->getEtag($contact, true); + + $this->mock(CardDAVBackend::class, function (MockInterface $mock) use ($card, $etag) { + $mock->shouldReceive('init')->andReturn($mock); + $mock->shouldReceive('getUuid') + ->withArgs(function ($uri) { + $this->assertEquals($uri, 'https://test/dav/uuid2'); + + return true; + }) + ->andReturn('uuid2'); + $mock->shouldReceive('updateCard') + ->withArgs(function ($addressBookId, $cardUri, $cardData) use ($card) { + $this->assertEquals($card, $cardData); + + return true; + }) + ->andReturn($etag); + }); + + $client = (new DavTester())->fake()->client(); + + $batchs = (new AddressBookContactsUpdaterMissed()) + ->execute(new SyncDto($subscription, $client), collect([ + [ + 'uuid' => 'uuid1', + ], + ]), collect([ + 'https://test/dav/uuid2' => new ContactDto('https://test/dav/uuid2', $etag), + ])); + + $this->assertCount(2, $batchs); + $batch = $batchs->first(); + $this->assertInstanceOf(GetMultipleVCard::class, $batch); + $hrefs = $this->getPrivateValue($batch, 'hrefs'); + $this->assertEquals(['https://test/dav/uuid2'], $hrefs); + } +} diff --git a/tests/Unit/Services/DavClient/Utils/AddressBookContactsUpdaterTest.php b/tests/Unit/Services/DavClient/Utils/AddressBookContactsUpdaterTest.php new file mode 100644 index 0000000..d3704a4 --- /dev/null +++ b/tests/Unit/Services/DavClient/Utils/AddressBookContactsUpdaterTest.php @@ -0,0 +1,208 @@ +create(); + $token = factory(SyncToken::class)->create([ + 'account_id' => $subscription->account_id, + 'user_id' => $subscription->user_id, + 'name' => 'contacts1', + 'timestamp' => now()->addDays(-1), + ]); + $subscription->localSyncToken = $token->id; + $subscription->save(); + + $contact = new Contact(); + $contact->forceFill([ + 'first_name' => 'Test', + 'uuid' => 'affacde9-b2fe-4371-9acb-6612aaee6971', + 'updated_at' => now(), + ]); + $card = $this->getCard($contact); + $etag = $this->getEtag($contact, true); + + $this->mock(CardDAVBackend::class, function (MockInterface $mock) use ($card, $etag) { + $mock->shouldReceive('updateCard') + ->withArgs(function ($addressBookId, $cardUri, $cardData) use ($card) { + $this->assertEquals($card, $cardData); + + return true; + }) + ->andReturn($etag); + }); + + $client = (new DavTester())->fake()->client(); + + $batchs = (new AddressBookContactsUpdater()) + ->execute(new SyncDto($subscription, $client), collect([ + 'https://test/dav/uuid2' => new ContactDto('https://test/dav/uuid2', $etag), + ])); + + $this->assertCount(2, $batchs); + $batch = $batchs->first(); + $this->assertInstanceOf(GetMultipleVCard::class, $batch); + $hrefs = $this->getPrivateValue($batch, 'hrefs'); + $this->assertEquals(['https://test/dav/uuid2'], $hrefs); + } + + /** @test */ + public function it_sync_deleted_multiget() + { + $subscription = AddressBookSubscription::factory()->create(); + $token = factory(SyncToken::class)->create([ + 'account_id' => $subscription->account_id, + 'user_id' => $subscription->user_id, + 'name' => 'contacts1', + 'timestamp' => now()->addDays(-1), + ]); + $subscription->localSyncToken = $token->id; + $subscription->save(); + + $client = (new DavTester())->fake()->client(); + + $batchs = (new AddressBookContactsUpdater()) + ->execute(new SyncDto($subscription, $client), collect([ + 'https://test/dav/uuid2' => new ContactDeleteDto('https://test/dav/uuid2'), + ])); + + $this->assertCount(2, $batchs); + $batch = $batchs->first(); + $this->assertInstanceOf(GetMultipleVCard::class, $batch); + $hrefs = $this->getPrivateValue($batch, 'hrefs'); + $this->assertEquals([], $hrefs); + + $batch = $batchs[1]; + $this->assertInstanceOf(DeleteMultipleVCard::class, $batch); + $hrefs = $this->getPrivateValue($batch, 'hrefs'); + $this->assertEquals(['https://test/dav/uuid2'], $hrefs); + } + + /** @test */ + public function it_sync_changes_simple() + { + $subscription = AddressBookSubscription::factory()->create([ + 'capabilities' => [ + 'addressbookMultiget' => false, + 'addressbookQuery' => true, + 'syncCollection' => true, + 'addressData' => [ + 'content-type' => 'text/vcard', + 'version' => '4.0', + ], + ], + ]); + $token = factory(SyncToken::class)->create([ + 'account_id' => $subscription->account_id, + 'user_id' => $subscription->user_id, + 'name' => 'contacts1', + 'timestamp' => now()->addDays(-1), + ]); + $subscription->localSyncToken = $token->id; + $subscription->save(); + + $contact = new Contact(); + $contact->forceFill([ + 'first_name' => 'Test', + 'uuid' => 'affacde9-b2fe-4371-9acb-6612aaee6971', + 'updated_at' => now(), + ]); + $card = $this->getCard($contact); + $etag = $this->getEtag($contact, true); + + $this->mock(CardDAVBackend::class, function (MockInterface $mock) use ($card, $etag) { + $mock->shouldReceive('updateCard') + ->withArgs(function ($addressBookId, $cardUri, $cardData) use ($card) { + $this->assertTrue(is_resource($cardData)); + + $data = ''; + while (! feof($cardData)) { + $data .= fgets($cardData); + } + + fclose($cardData); + + $this->assertEquals($card, $data); + + return true; + }) + ->andReturn($etag); + }); + + $client = (new DavTester())->fake()->client(); + + $batchs = (new AddressBookContactsUpdater()) + ->execute(new SyncDto($subscription, $client), collect([ + 'https://test/dav/uuid2' => new ContactDto('https://test/dav/uuid2', $etag), + ])); + + $this->assertCount(1, $batchs); + $batch = $batchs->first(); + $this->assertInstanceOf(GetVCard::class, $batch); + $dto = $this->getPrivateValue($batch, 'contact'); + $this->assertInstanceOf(ContactDto::class, $dto); + $this->assertEquals('https://test/dav/uuid2', $dto->uri); + } + + /** @test */ + public function it_sync_deleted_simple() + { + $subscription = AddressBookSubscription::factory()->create([ + 'capabilities' => [ + 'addressbookMultiget' => false, + 'addressbookQuery' => true, + 'syncCollection' => true, + 'addressData' => [ + 'content-type' => 'text/vcard', + 'version' => '4.0', + ], + ], + ]); + $token = factory(SyncToken::class)->create([ + 'account_id' => $subscription->account_id, + 'user_id' => $subscription->user_id, + 'name' => 'contacts1', + 'timestamp' => now()->addDays(-1), + ]); + $subscription->localSyncToken = $token->id; + $subscription->save(); + + $client = (new DavTester())->fake()->client(); + + $batchs = (new AddressBookContactsUpdater()) + ->execute(new SyncDto($subscription, $client), collect([ + 'https://test/dav/uuid2' => new ContactDeleteDto('https://test/dav/uuid2'), + ])); + + $this->assertCount(1, $batchs); + $batch = $batchs->first(); + $this->assertInstanceOf(DeleteVCard::class, $batch); + $uri = $this->getPrivateValue($batch, 'uri'); + $this->assertEquals('https://test/dav/uuid2', $uri); + } +} diff --git a/tests/Unit/Services/DavClient/Utils/AddressBookGetterTest.php b/tests/Unit/Services/DavClient/Utils/AddressBookGetterTest.php new file mode 100644 index 0000000..323e2af --- /dev/null +++ b/tests/Unit/Services/DavClient/Utils/AddressBookGetterTest.php @@ -0,0 +1,110 @@ +addressBookBaseUri() + ->capabilities() + ->displayName() + ->fake(); + $client = $tester->client(); + $result = (new AddressBookGetter()) + ->execute($client); + + $tester->assert(); + $this->assertEquals([ + 'uri' => 'https://test/dav/addressbooks/user@test.com/contacts/', + 'capabilities' => [ + 'addressbookMultiget' => true, + 'addressbookQuery' => true, + 'syncCollection' => true, + 'addressData' => [ + 'content-type' => 'text/vcard', + 'version' => '4.0', + ], + ], + 'name' => 'Test', + ], $result); + } + + /** @test */ + public function it_fails_on_server_not_compliant() + { + $tester = (new DavTester()) + ->userPrincipalEmpty() + ->serviceUrl() + ->optionsFail() + ->fake(); + $client = $tester->client(); + + $this->expectException(DavServerNotCompliantException::class); + (new AddressBookGetter()) + ->execute($client); + } + + /** @test */ + public function it_fails_if_no_userprincipal() + { + $tester = (new DavTester()) + ->userPrincipalEmpty() + ->serviceUrl() + ->optionsOk() + ->userPrincipalEmpty() + ->fake(); + $client = $tester->client(); + + $this->expectException(DavServerNotCompliantException::class); + (new AddressBookGetter()) + ->execute($client); + } + + /** @test */ + public function it_fails_if_no_addressbook() + { + $tester = (new DavTester()) + ->userPrincipalEmpty() + ->serviceUrl() + ->optionsOk() + ->userPrincipal() + ->addressbookEmpty() + ->fake(); + $client = $tester->client(); + + $this->expectException(DavServerNotCompliantException::class); + (new AddressBookGetter()) + ->execute($client); + } + + /** @test */ + public function it_fails_if_no_addressbook_url() + { + $tester = (new DavTester()) + ->userPrincipalEmpty() + ->serviceUrl() + ->optionsOk() + ->userPrincipal() + ->addressbookHome() + ->resourceTypeHomeOnly() + ->optionsOk() + ->fake(); + $client = $tester->client(); + + $this->expectException(DavClientException::class); + (new AddressBookGetter()) + ->execute($client); + } +} diff --git a/tests/Unit/Services/DavClient/Utils/AddressBookSynchronizerTest.php b/tests/Unit/Services/DavClient/Utils/AddressBookSynchronizerTest.php new file mode 100644 index 0000000..ca0c468 --- /dev/null +++ b/tests/Unit/Services/DavClient/Utils/AddressBookSynchronizerTest.php @@ -0,0 +1,370 @@ +mock(AddressBookContactsUpdater::class, function (MockInterface $mock) { + $mock->shouldReceive('execute') + ->once() + ->andReturn(collect()); + }); + + $subscription = $this->getSubscription(); + + $tester = (new DavTester('https://test/dav/addressbooks/user@test.com/contacts/')) + ->getSynctoken($subscription->syncToken) + ->fake(); + $client = $tester->client(); + + (new AddressBookSynchronizer()) + ->execute(new SyncDto($subscription, $client)); + + $tester->assert(); + } + + /** @test */ + public function it_sync_no_changes() + { + Bus::fake(); + + $this->mock(AddressBookContactsUpdater::class, function (MockInterface $mock) { + $mock->shouldReceive('execute') + ->once() + ->andReturn(collect()); + }); + + $subscription = $this->getSubscription(); + + $tester = (new DavTester('https://test/dav/addressbooks/user@test.com/contacts/')); + $tester->getSynctoken('"test21"') + ->getSyncCollection('test20') + ->fake(); + + $client = $tester->client(); + + (new AddressBookSynchronizer()) + ->execute(new SyncDto($subscription, $client)); + + $tester->assert(); + } + + /** @test */ + public function it_sync_changes_added_local_contact() + { + Bus::fake(); + + $subscription = $this->getSubscription(); + + $contact = factory(Contact::class)->create([ + 'account_id' => $subscription->account_id, + 'address_book_id' => $subscription->address_book_id, + 'uuid' => 'd403af1c-8492-4e9b-9833-cf18c795dfa9', + ]); + + $tester = (new DavTester('https://test/dav/addressbooks/user@test.com/contacts/')); + $tester->getSynctoken('"token"') + ->getSyncCollection('token', '"test2"') + ->fake(); + + $client = $tester->client(); + + $sync = new SyncDto($subscription, $client); + $this->mock(AddressBookContactsUpdater::class, function (MockInterface $mock) use ($sync) { + $mock->shouldReceive('execute') + ->once() + ->withArgs(function ($localSync, $contacts) use ($sync) { + $this->assertEquals($sync, $localSync); + $this->assertEquals('https://test/dav/addressbooks/user@test.com/contacts/uuid', $contacts->first()->uri); + $this->assertEquals('"test2"', $contacts->first()->etag); + + return true; + }) + ->andReturn(collect()); + }); + + (new AddressBookSynchronizer()) + ->execute($sync); + + $tester->assert(); + } + + /** @test */ + public function it_sync_changes_added_local_contact_batched() + { + Bus::fake(); + + $subscription = $this->getSubscription(); + + factory(Contact::class)->create([ + 'account_id' => $subscription->account_id, + 'address_book_id' => $subscription->address_book_id, + 'uuid' => 'd403af1c-8492-4e9b-9833-cf18c795dfa9', + ]); + + $tester = (new DavTester('https://test/dav/addressbooks/user@test.com/contacts/')); + $tester->getSynctoken('"token"') + ->getSyncCollection('token', '"test2"') + ->fake(); + + $client = $tester->client(); + + $sync = new SyncDto($subscription, $client); + + (new AddressBookSynchronizer()) + ->execute($sync); + + $tester->assert(); + + Bus::assertBatched(function (PendingBatch $batch) { + $this->assertCount(2, $batch->jobs); + $job = $batch->jobs[0]; + $this->assertInstanceOf(GetMultipleVCard::class, $job); + $this->assertEquals(['https://test/dav/addressbooks/user@test.com/contacts/uuid'], $this->getPrivateValue($job, 'hrefs')); + + return true; + }); + } + + /** @test */ + public function it_sync_changes_deleted_contact_batched() + { + Bus::fake(); + + $subscription = $this->getSubscription(); + + $tester = (new DavTester('https://test/dav/addressbooks/user@test.com/contacts/')); + $tester->getSynctoken('"token"') + ->addResponse('https://test/dav/addressbooks/user@test.com/contacts/', Http::response(DavTester::multistatusHeader(). + ''. + 'HTTP/1.1 404 Not Found'. + 'https://test/dav/addressbooks/user@test.com/contacts/uuid'. + ''. + ''. + 'HTTP/1.1 418 I\'m a teapot'. + ''. + ''. + 'token'. + ''), null, 'REPORT') + ->fake(); + + $client = $tester->client(); + + $sync = new SyncDto($subscription, $client); + + (new AddressBookSynchronizer()) + ->execute($sync); + + $tester->assert(); + + Bus::assertBatched(function (PendingBatch $batch) { + $this->assertCount(2, $batch->jobs); + $job = $batch->jobs[1]; + $this->assertInstanceOf(DeleteMultipleVCard::class, $job); + $this->assertEquals(['https://test/dav/addressbooks/user@test.com/contacts/uuid'], $this->getPrivateValue($job, 'hrefs')); + + return true; + }); + } + + /** @test */ + public function it_forcesync_changes_added_local_contact() + { + Bus::fake(); + + $subscription = $this->getSubscription(); + + $contact = factory(Contact::class)->create([ + 'account_id' => $subscription->account_id, + 'address_book_id' => $subscription->address_book_id, + 'uuid' => 'd403af1c-8492-4e9b-9833-cf18c795dfa9', + ]); + $etag = $this->getEtag($contact, true); + + $tester = (new DavTester('https://test/dav/addressbooks/user@test.com/contacts/')) + ->fake(); + $tester->addResponse('https://test/dav/addressbooks/user@test.com/contacts/', Http::response(DavTester::multistatusHeader(). + ''. + 'https://test/dav/uuid1'. + ''. + ''. + "$etag". + ''. + 'HTTP/1.1 200 OK'. + ''. + ''. + ''), ''."\n". + ''. + ''. + ''. + ''. + "\n", 'REPORT'); + + $client = $tester->client(); + + $sync = new SyncDto($subscription, $client); + $this->mock(AddressBookContactsUpdaterMissed::class, function (MockInterface $mock) use ($sync, $contact, $etag) { + $mock->shouldReceive('execute') + ->once() + ->withArgs(function ($localSync, $localContacts, $distContacts) use ($sync, $contact, $etag) { + $this->assertEquals($sync, $localSync); + $this->assertEquals($contact->id, $localContacts->first()->id); + $this->assertEquals('https://test/dav/uuid1', $distContacts->first()->uri); + $this->assertEquals($etag, $distContacts->first()->etag); + + return true; + }) + ->andReturn(collect()); + }); + $this->mock(AddressBookContactsPushMissed::class, function (MockInterface $mock) { + $mock->shouldReceive('execute') + ->once() + ->andReturn(collect()); + }); + + (new AddressBookSynchronizer()) + ->execute($sync, true); + + $tester->assert(); + } + + /** @test */ + public function it_forcesync_changes_added_local_contact_batched() + { + Bus::fake(); + + $subscription = $this->getSubscription(); + + $contact = factory(Contact::class)->create([ + 'account_id' => $subscription->account_id, + 'address_book_id' => $subscription->address_book_id, + 'uuid' => 'd403af1c-8492-4e9b-9833-cf18c795dfa9', + ]); + $etag = $this->getEtag($contact, true); + + $tester = (new DavTester('https://test/dav/addressbooks/user@test.com/contacts/')) + ->fake(); + $tester->addResponse('https://test/dav/addressbooks/user@test.com/contacts/', Http::response(DavTester::multistatusHeader(). + ''. + 'https://test/dav/uuid1'. + ''. + ''. + "$etag". + ''. + 'HTTP/1.1 200 OK'. + ''. + ''. + ''), ''."\n". + ''. + ''. + ''. + ''. + "\n", 'REPORT'); + + $client = $tester->client(); + + $sync = new SyncDto($subscription, $client); + + (new AddressBookSynchronizer()) + ->execute($sync, true); + + $tester->assert(); + + Bus::assertBatched(function (PendingBatch $batch) { + $this->assertCount(2, $batch->jobs); + $job = $batch->jobs[0]; + $this->assertInstanceOf(GetMultipleVCard::class, $job); + $this->assertEquals(['https://test/dav/uuid1'], $this->getPrivateValue($job, 'hrefs')); + + return true; + }); + } + + /** @test */ + public function it_forcesync_changes_deleted_contact_batched() + { + Bus::fake(); + + $subscription = $this->getSubscription(); + + $tester = (new DavTester('https://test/dav/addressbooks/user@test.com/contacts/')) + ->fake(); + $tester->addResponse('https://test/dav/addressbooks/user@test.com/contacts/', Http::response(DavTester::multistatusHeader(). + ''. + 'HTTP/1.1 404 Not Found'. + 'https://test/dav/uuid1'. + ''. + ''. + 'HTTP/1.1 418 I\'m a teapot'. + ''. + ''. + ''), ''."\n". + ''. + ''. + ''. + ''. + "\n", 'REPORT'); + + $client = $tester->client(); + + $sync = new SyncDto($subscription, $client); + + (new AddressBookSynchronizer()) + ->execute($sync, true); + + $tester->assert(); + + Bus::assertBatched(function (PendingBatch $batch) { + $this->assertCount(2, $batch->jobs); + $job = $batch->jobs[1]; + $this->assertInstanceOf(DeleteMultipleVCard::class, $job); + $this->assertEquals(['https://test/dav/uuid1'], $this->getPrivateValue($job, 'hrefs')); + + return true; + }); + } + + private function getSubscription() + { + $subscription = AddressBookSubscription::factory()->create([ + 'uri' => 'https://test/dav/addressbooks/user@test.com/contacts/', + ]); + $token = factory(SyncToken::class)->create([ + 'account_id' => $subscription->account_id, + 'user_id' => $subscription->user_id, + 'name' => 'contacts1', + 'timestamp' => now()->addDays(-1), + ]); + $subscription->localSyncToken = $token->id; + $subscription->save(); + + return $subscription; + } +} diff --git a/tests/Unit/Services/DavClient/Utils/Dav/DavClientTest.php b/tests/Unit/Services/DavClient/Utils/Dav/DavClientTest.php new file mode 100644 index 0000000..590acee --- /dev/null +++ b/tests/Unit/Services/DavClient/Utils/Dav/DavClientTest.php @@ -0,0 +1,470 @@ +addResponse('https://test', Http::response(), null, 'OPTIONS') + ->addResponse('https://test', Http::response(null, 200, ['Dav' => 'test']), null, 'OPTIONS') + ->addResponse('https://test', Http::response(null, 200, ['Dav' => ' test ']), null, 'OPTIONS') + ->fake(); + $client = $tester->client(); + + $result = $client->options(); + $this->assertEquals([], $result); + + $result = $client->options(); + $this->assertEquals(['test'], $result); + + $result = $client->options(); + $this->assertEquals(['test'], $result); + + $tester->assert(); + } + + /** @test */ + public function it_get_serviceurl() + { + $tester = (new DavTester()) + ->serviceUrl() + ->fake(); + $client = $tester->client(); + + $result = $client->getServiceUrl(); + + $tester->assert(); + $this->assertEquals('https://test/dav/', $result); + } + + /** @test */ + public function it_get_non_standard_serviceurl() + { + $tester = (new DavTester()) + ->addResponse('https://test/.well-known/carddav', Http::response(), null, 'GET') + ->addResponse('https://test/.well-known/carddav', Http::response(), null, 'GET') + ->nonStandardServiceUrl() + ->fake(); + $client = $tester->client(); + + $result = $client->getServiceUrl(); + + $tester->assert(); + $this->assertEquals('https://test/dav/', $result); + } + + /** @test */ + public function it_get_non_standard_serviceurl2() + { + $tester = (new DavTester()) + ->addResponse('https://test/.well-known/carddav', Http::response(null, 404), null, 'GET') + ->addResponse('https://test/.well-known/carddav', Http::response(null, 404), null, 'GET') + ->nonStandardServiceUrl() + ->fake(); + $client = $tester->client(); + + $result = $client->getServiceUrl(); + + $tester->assert(); + $this->assertEquals('https://test/dav/', $result); + } + + /** @test */ + public function it_fail_non_standard() + { + $tester = (new DavTester()) + ->addResponse('https://test/.well-known/carddav', Http::response(null, 500), null, 'GET') + ->fake(); + $client = $tester->client(); + + $this->expectException(RequestException::class); + $client->getServiceUrl(); + } + + /** @test */ + public function it_get_base_uri() + { + $tester = (new DavTester()) + ->fake(); + $client = $tester->client(); + + $result = $client->path(); + + $this->assertEquals('https://test', $result); + + $result = $client->path('xxx'); + + $this->assertEquals('https://test/xxx', $result); + } + + /** @test */ + public function it_set_base_uri() + { + $tester = (new DavTester()) + ->fake(); + $client = $tester->client(); + + $result = $client->setBaseUri('https://new') + ->path(); + + $this->assertEquals('https://new', $result); + } + + /** @test */ + public function it_call_propfind() + { + $tester = (new DavTester()) + ->addResponse('https://test', Http::response(DavTester::multistatusHeader(). + ''. + 'href'. + ''. + ''. + 'value'. + ''. + 'HTTP/1.1 200 OK'. + ''. + ''. + ''), ''."\n". + ''. + ''. + ''. + ''. + "\n", 'PROPFIND') + ->fake(); + + $client = $tester->client(); + + $result = $client->propFind(['{DAV:}test']); + + $tester->assert(); + $this->assertEquals([ + '{DAV:}test' => 'value', + ], $result); + } + + /** @test */ + public function it_get_property() + { + $tester = (new DavTester()) + ->addResponse('https://test/test', Http::response(DavTester::multistatusHeader(). + ''. + 'href'. + ''. + ''. + 'value'. + ''. + 'HTTP/1.1 200 OK'. + ''. + ''. + ''), ''."\n". + ''. + ''. + ''. + ''. + "\n", 'PROPFIND') + ->fake(); + + $client = $tester->client(); + + $result = $client->getProperty('{DAV:}test', 'https://test/test'); + + $tester->assert(); + $this->assertEquals('value', $result); + } + + /** @test */ + public function it_get_supported_report() + { + $tester = (new DavTester('https://test/dav')) + ->addResponse('https://test/dav', Http::response(DavTester::multistatusHeader(). + ''. + '/dav'. + ''. + ''. + ''. + ''. + ''. + ''. + ''. + ''. + ''. + ''. + ''. + 'HTTP/1.1 200 OK'. + ''. + ''. + ''), ''."\n". + ''. + ''. + ''. + ''. + "\n", 'PROPFIND') + ->fake(); + + $client = $tester->client(); + + $result = $client->getSupportedReportSet(); + + $tester->assert(); + $this->assertEquals(['{DAV:}test1', '{DAV:}test2'], $result); + } + + /** @test */ + public function it_sync_collection() + { + $tester = (new DavTester()) + ->addResponse('https://test', Http::response(DavTester::multistatusHeader(). + ''. + 'href'. + ''. + ''. + '"00001-abcd1"'. + 'value'. + ''. + 'HTTP/1.1 200 OK'. + ''. + ''. + '"00001-abcd1"'. + ''), ''."\n". + ''. + ''. + '1'. + ''. + ''. + ''. + "\n", 'REPORT') + ->fake(); + + $client = $tester->client(); + + $result = $client->syncCollection(['{DAV:}test'], ''); + + $tester->assert(); + $this->assertEquals([ + 'href' => [ + 'properties' => [ + 200 => [ + '{DAV:}getetag' => '"00001-abcd1"', + '{DAV:}test' => 'value', + ], + ], + 'status' => '200', + ], + 'synctoken' => '"00001-abcd1"', + ], $result); + } + + /** @test */ + public function it_sync_collection_with_synctoken() + { + $tester = (new DavTester()) + ->addResponse('https://test', Http::response(DavTester::multistatusHeader(). + ''. + 'href'. + ''. + ''. + '"00001-abcd1"'. + 'value'. + ''. + 'HTTP/1.1 200 OK'. + ''. + ''. + '"00001-abcd1"'. + ''), ''."\n". + ''. + '"00000-abcd0"'. + '1'. + ''. + ''. + ''. + "\n", 'REPORT') + ->fake(); + + $client = $tester->client(); + + $result = $client->syncCollection(['{DAV:}test'], '"00000-abcd0"'); + + $tester->assert(); + $this->assertEquals([ + 'href' => [ + 'properties' => [ + 200 => [ + '{DAV:}getetag' => '"00001-abcd1"', + '{DAV:}test' => 'value', + ], + ], + 'status' => '200', + ], + 'synctoken' => '"00001-abcd1"', + ], $result); + } + + /** @test */ + public function it_run_addressbook_multiget_report() + { + $tester = (new DavTester()) + ->addResponse('https://test', Http::response(DavTester::multistatusHeader(). + ''. + 'href'. + ''. + ''. + '"00001-abcd1"'. + 'value'. + ''. + 'HTTP/1.1 200 OK'. + ''. + ''. + ''), ''."\n". + ''. + ''. + ''. + ''. + 'https://test/contacts/1'. + "\n", 'REPORT') + ->fake(); + + $client = $tester->client(); + + $result = $client->addressbookMultiget(['{DAV:}test'], ['https://test/contacts/1']); + + $this->assertEquals([ + 'href' => [ + 'properties' => [ + 200 => [ + '{DAV:}getetag' => '"00001-abcd1"', + '{DAV:}test' => 'value', + ], + ], + 'status' => '200', + ], + ], $result); + + $tester->assert(); + } + + /** @test */ + public function it_run_addressbook_query_report() + { + $tester = (new DavTester()) + ->addResponse('https://test', Http::response(DavTester::multistatusHeader(). + ''. + 'href'. + ''. + ''. + '"00001-abcd1"'. + 'value'. + ''. + 'HTTP/1.1 200 OK'. + ''. + ''. + ''), ''."\n". + ''. + ''. + ''. + ''. + "\n", 'REPORT') + ->fake(); + + $client = $tester->client(); + + $result = $client->addressbookQuery(['{DAV:}test']); + + $tester->assert(); + $this->assertEquals([ + 'href' => [ + 'properties' => [ + 200 => [ + '{DAV:}getetag' => '"00001-abcd1"', + '{DAV:}test' => 'value', + ], + ], + 'status' => '200', + ], + ], $result); + } + + /** @test */ + public function it_run_proppatch() + { + $tester = (new DavTester()) + ->addResponse('https://test', Http::response(DavTester::multistatusHeader(). + ''. + 'href'. + ''. + ''. + 'value'. + ''. + 'HTTP/1.1 200 OK'. + ''. + ''. + '', 207), ''."\n". + ''."\n". + ' '."\n". + ' '."\n". + ' value'."\n". + ' '."\n". + ' '."\n". + "\n", 'PROPPATCH') + ->fake(); + + $client = $tester->client(); + + $result = $client->propPatch(['{DAV:}test' => 'value']); + + $tester->assert(); + $this->assertTrue($result); + } + + /** @test */ + public function it_run_proppatch_error() + { + $tester = (new DavTester()) + ->addResponse('https://test', Http::response(DavTester::multistatusHeader(). + ''. + 'href'. + ''. + ''. + 'x'. + ''. + 'HTTP/1.1 405 OK'. + ''. + ''. + ''. + 'x'. + ''. + 'HTTP/1.1 500 OK'. + ''. + ''. + '', 207), ''."\n". + ''."\n". + ' '."\n". + ' '."\n". + ' value'."\n". + ' value'."\n". + ' '."\n". + ' '."\n". + "\n", 'PROPPATCH') + ->fake(); + + $client = $tester->client(); + + $this->expectException(DavClientException::class); + $this->expectExceptionMessage('PROPPATCH failed. The following properties errored: {DAV:}test (405), {DAV:}excerpt (500)'); + $client->propPatch([ + '{DAV:}test' => 'value', + '{DAV:}excerpt' => 'value', + ]); + } +} diff --git a/tests/Unit/Services/DavClient/Utils/Model/ContactUpdateDtoTest.php b/tests/Unit/Services/DavClient/Utils/Model/ContactUpdateDtoTest.php new file mode 100644 index 0000000..02ac692 --- /dev/null +++ b/tests/Unit/Services/DavClient/Utils/Model/ContactUpdateDtoTest.php @@ -0,0 +1,31 @@ +assertEquals('uri', $dto->uri); + $this->assertEquals('etag', $dto->etag); + $this->assertEquals('card', $dto->card); + } + + /** @test */ + public function it_create_dto_resource() + { + $resource = fopen(__DIR__.'/stub.vcf', 'r'); + $dto = new ContactUpdateDto('uri', 'etag', $resource); + $this->assertEquals('uri', $dto->uri); + $this->assertEquals('etag', $dto->etag); + $this->assertEquals('card', $dto->card); + } +} diff --git a/tests/Unit/Services/DavClient/Utils/Model/stub.vcf b/tests/Unit/Services/DavClient/Utils/Model/stub.vcf new file mode 100644 index 0000000..8c7c282 --- /dev/null +++ b/tests/Unit/Services/DavClient/Utils/Model/stub.vcf @@ -0,0 +1 @@ +card \ No newline at end of file diff --git a/tests/Unit/Services/Instance/AuditLog/LogAccountActionTest.php b/tests/Unit/Services/Instance/AuditLog/LogAccountActionTest.php new file mode 100644 index 0000000..9ff1db8 --- /dev/null +++ b/tests/Unit/Services/Instance/AuditLog/LogAccountActionTest.php @@ -0,0 +1,104 @@ +create([]); + + $date = Carbon::now(); + + $request = [ + 'account_id' => $michael->account_id, + 'action' => 'account_created', + 'author_id' => $michael->id, + 'author_name' => $michael->name, + 'audited_at' => $date, + 'objects' => '{"user": 1}', + ]; + + $auditLog = (new LogAccountAction)->execute($request); + + $this->assertDatabaseHas('audit_logs', [ + 'id' => $auditLog->id, + 'account_id' => $michael->account_id, + 'about_contact_id' => null, + 'action' => 'account_created', + 'author_id' => $michael->id, + 'author_name' => $michael->name, + 'audited_at' => $date, + 'should_appear_on_dashboard' => false, + 'objects' => '{"user": 1}', + ]); + + $this->assertInstanceOf( + AuditLog::class, + $auditLog + ); + } + + /** @test */ + public function it_logs_an_action_about_a_contact(): void + { + $michael = factory(User::class)->create([]); + $contact = factory(Contact::class)->create([ + 'account_id' => $michael->account_id, + ]); + + $date = Carbon::now(); + + $request = [ + 'account_id' => $michael->account_id, + 'action' => 'account_created', + 'about_contact_id' => $contact->id, + 'author_id' => $michael->id, + 'author_name' => $michael->name, + 'audited_at' => $date, + 'objects' => '{"user": 1}', + ]; + + $auditLog = (new LogAccountAction)->execute($request); + + $this->assertDatabaseHas('audit_logs', [ + 'id' => $auditLog->id, + 'account_id' => $michael->account_id, + 'about_contact_id' => $contact->id, + 'action' => 'account_created', + 'author_id' => $michael->id, + 'author_name' => $michael->name, + 'audited_at' => $date, + 'should_appear_on_dashboard' => false, + 'objects' => '{"user": 1}', + ]); + + $this->assertInstanceOf( + AuditLog::class, + $auditLog + ); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given(): void + { + $request = [ + 'action' => 'account_created', + ]; + + $this->expectException(ValidationException::class); + (new LogAccountAction)->execute($request); + } +} diff --git a/tests/Unit/Services/Instance/Geolocalization/GetGPSCoordinateTest.php b/tests/Unit/Services/Instance/Geolocalization/GetGPSCoordinateTest.php new file mode 100644 index 0000000..3d08930 --- /dev/null +++ b/tests/Unit/Services/Instance/Geolocalization/GetGPSCoordinateTest.php @@ -0,0 +1,132 @@ + false]); + + $place = factory(Place::class)->create(); + + $request = [ + 'account_id' => $place->account_id, + 'place_id' => $place->id, + ]; + + $this->expectException(MissingEnvVariableException::class); + app(GetGPSCoordinate::class)->execute($request); + } + + /** @test */ + public function it_gets_gps_coordinates() + { + config(['monica.enable_geolocation' => true]); + config(['monica.location_iq_api_key' => 'test']); + + $body = file_get_contents(base_path('tests/Fixtures/Services/Instance/Geolocalization/GetGPSCoordinateSampleResponse.json')); + Http::fake([ + 'us1.locationiq.com/v1/*' => Http::response($body, 200), + ]); + + $place = factory(Place::class)->create(); + + $request = [ + 'account_id' => $place->account_id, + 'place_id' => $place->id, + ]; + + $place = app(GetGPSCoordinate::class)->execute($request); + + $this->assertDatabaseHas('places', [ + 'id' => $place->id, + ]); + + $this->assertInstanceOf( + Place::class, + $place + ); + } + + /** @test */ + public function it_returns_null_if_address_is_garbage() + { + config(['monica.enable_geolocation' => true]); + config(['monica.location_iq_api_key' => 'test']); + + $body = file_get_contents(base_path('tests/Fixtures/Services/Instance/Geolocalization/GetGPSCoordinateGarbageResponse.json')); + Http::fake([ + 'us1.locationiq.com/v1/*' => Http::response($body, 404), + ]); + + $place = factory(Place::class)->create([ + 'country' => 'ewqr', + 'street' => '', + 'city' => 'sieklopekznqqq', + 'postal_code' => '', + ]); + + $request = [ + 'account_id' => $place->account_id, + 'place_id' => $place->id, + ]; + + $place = app(GetGPSCoordinate::class)->execute($request); + + $this->assertNull($place); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + config(['monica.enable_geolocation' => true]); + config(['monica.location_iq_api_key' => 'test']); + + $request = [ + 'account_id' => 111, + ]; + + $this->expectException(ValidationException::class); + + app(GetGPSCoordinate::class)->execute($request); + } + + /** @test */ + public function it_release_the_job_if_rate_limited_second() + { + config(['monica.enable_geolocation' => true]); + config(['monica.location_iq_api_key' => 'test']); + + Http::fake([ + 'us1.locationiq.com/v1/*' => Http::response('{"error":"Rate Limited Second"}', 429), + ]); + + $place = factory(Place::class)->create([ + 'country' => 'ewqr', + 'street' => '', + 'city' => 'sieklopekznqqq', + 'postal_code' => '', + ]); + + $request = [ + 'account_id' => $place->account_id, + 'place_id' => $place->id, + ]; + + $this->expectException(RateLimitedSecondException::class); + app(GetGPSCoordinate::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Instance/TokenCleanTest.php b/tests/Unit/Services/Instance/TokenCleanTest.php new file mode 100644 index 0000000..7960f2e --- /dev/null +++ b/tests/Unit/Services/Instance/TokenCleanTest.php @@ -0,0 +1,88 @@ +create(); + $user = factory(User::class)->create([ + 'account_id' => $account->id, + ]); + + SyncToken::create([ + 'account_id' => $account->id, + 'user_id' => $user->id, + 'name' => 'contacts', + 'timestamp' => now(), + ]); + app(TokenClean::class)->execute(['dryrun' => false]); + + $this->assertDatabaseHas('synctoken', [ + 'account_id' => $account->id, + 'user_id' => $user->id, + 'name' => 'contacts', + ]); + } + + /** @test */ + public function tokenclean_left_all_token() + { + $account = factory(Account::class)->create(); + $user = factory(User::class)->create([ + 'account_id' => $account->id, + ]); + + $s1 = SyncToken::create([ + 'account_id' => $account->id, + 'user_id' => $user->id, + 'name' => 'contacts', + 'timestamp' => now(), + ]); + $s2 = SyncToken::create([ + 'account_id' => $account->id, + 'user_id' => $user->id, + 'name' => 'contacts', + 'timestamp' => now()->addDays(-10), + ]); + $s3 = SyncToken::create([ + 'account_id' => $account->id, + 'user_id' => $user->id, + 'name' => 'contacts', + 'timestamp' => now()->addDays(-15), + ]); + $s4 = SyncToken::create([ + 'account_id' => $account->id, + 'user_id' => $user->id, + 'name' => 'contacts', + 'timestamp' => now()->addDays(-1), + ]); + app(TokenClean::class)->execute(['dryrun' => false]); + + $this->assertDatabaseHas('synctoken', [ + 'id' => $s1->id, + ]); + $this->assertDatabaseMissing('synctoken', [ + 'id' => $s2->id, + ]); + $this->assertDatabaseMissing('synctoken', [ + 'id' => $s3->id, + ]); + $this->assertDatabaseHas('synctoken', [ + 'id' => $s4->id, + ]); + } +} diff --git a/tests/Unit/Services/Instance/Weather/GetWeatherInformationTest.php b/tests/Unit/Services/Instance/Weather/GetWeatherInformationTest.php new file mode 100644 index 0000000..52f386d --- /dev/null +++ b/tests/Unit/Services/Instance/Weather/GetWeatherInformationTest.php @@ -0,0 +1,126 @@ +create([ + 'latitude' => '34.112456', + 'longitude' => '-118.4270732', + ]); + + config(['monica.enable_weather' => true]); + config(['monica.weatherapi_key' => 'test']); + + $body = file_get_contents(base_path('tests/Fixtures/Services/Instance/Weather/GetWeatherInformationSampleResponse.json')); + Http::fake([ + 'api.weatherapi.com/v1/*' => Http::response($body, 200), + ]); + + $request = [ + 'account_id' => $place->account_id, + 'place_id' => $place->id, + ]; + + $weather = app(GetWeatherInformation::class)->execute($request); + + $this->assertDatabaseHas('weather', [ + 'id' => $weather->id, + 'account_id' => $place->account_id, + 'place_id' => $place->id, + ]); + + $this->assertEquals( + 'Partly cloudy', + $weather->summary + ); + + $this->assertInstanceOf( + Weather::class, + $weather + ); + } + + /** @test */ + public function it_cant_get_weather_info_if_weather_not_enabled() + { + $place = factory(Place::class)->create([ + 'latitude' => '34.112456', + 'longitude' => '-118.4270732', + ]); + + config(['monica.enable_weather' => false]); + + $request = [ + 'place_id' => $place->id, + ]; + + $this->expectException(MissingEnvVariableException::class); + app(GetWeatherInformation::class)->execute($request); + } + + /** @test */ + public function it_cant_get_weather_info_if_weatherapi_key_not_provided() + { + $place = factory(Place::class)->create([ + 'latitude' => '34.112456', + 'longitude' => '-118.4270732', + ]); + + config(['monica.enable_weather' => true]); + config(['monica.weatherapi_key' => null]); + + $request = [ + 'account_id' => $place->account_id, + 'place_id' => $place->id, + ]; + + $this->expectException(MissingEnvVariableException::class); + app(GetWeatherInformation::class)->execute($request); + } + + /** @test */ + public function it_cant_get_weather_info_if_latitude_longitude_are_null() + { + $place = factory(Place::class)->create([]); + + config(['monica.enable_weather' => true]); + config(['monica.weatherapi_key' => 'test']); + config(['monica.enable_geolocation' => false]); + + $request = [ + 'account_id' => $place->account_id, + 'place_id' => $place->id, + ]; + + $this->expectException(NoCoordinatesException::class); + app(GetWeatherInformation::class)->execute($request); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + config(['monica.enable_weather' => true]); + config(['monica.weatherapi_key' => 'test']); + + $request = []; + + $this->expectException(ValidationException::class); + app(GetWeatherInformation::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Task/CreateTaskTest.php b/tests/Unit/Services/Task/CreateTaskTest.php new file mode 100644 index 0000000..bf5e05d --- /dev/null +++ b/tests/Unit/Services/Task/CreateTaskTest.php @@ -0,0 +1,129 @@ +create([]); + + $request = [ + 'contact_id' => $contact->id, + 'account_id' => $contact->account_id, + 'title' => 'This is a title', + 'description' => 'This is a description', + ]; + + $task = app(CreateTask::class)->execute($request); + + $this->assertDatabaseHas('tasks', [ + 'id' => $task->id, + 'contact_id' => $contact->id, + 'title' => 'This is a title', + 'description' => 'This is a description', + ]); + + $this->assertInstanceOf( + Task::class, + $task + ); + } + + /** @test */ + public function it_stores_a_task_without_contact_id() + { + $contact = factory(Contact::class)->create([]); + + $request = [ + 'account_id' => $contact->account_id, + 'title' => 'This is a title', + 'description' => 'This is a description', + ]; + + $task = app(CreateTask::class)->execute($request); + + $this->assertDatabaseHas('tasks', [ + 'id' => $task->id, + 'contact_id' => null, + 'title' => 'This is a title', + 'description' => 'This is a description', + ]); + + $this->assertInstanceOf( + Task::class, + $task + ); + } + + /** @test */ + public function it_stores_a_task_without_description() + { + $contact = factory(Contact::class)->create([]); + + $request = [ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'title' => 'This is a title', + 'description' => null, + ]; + + $task = app(CreateTask::class)->execute($request); + + $this->assertDatabaseHas('tasks', [ + 'id' => $task->id, + 'contact_id' => $contact->id, + 'title' => 'This is a title', + ]); + + $this->assertInstanceOf( + Task::class, + $task + ); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $contact = factory(Contact::class)->create([]); + + $request = [ + 'contact_id' => $contact->id, + 'account_id' => $contact->account_id, + ]; + + $this->expectException(ValidationException::class); + + app(CreateTask::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_contact_is_not_linked_to_account() + { + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create(); + + $request = [ + 'contact_id' => $contact->id, + 'account_id' => $account->id, + 'title' => 'This is a title', + 'description' => 'This is a description', + ]; + + $this->expectException(ModelNotFoundException::class); + + app(CreateTask::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Task/DestroyTaskTest.php b/tests/Unit/Services/Task/DestroyTaskTest.php new file mode 100644 index 0000000..103e6a8 --- /dev/null +++ b/tests/Unit/Services/Task/DestroyTaskTest.php @@ -0,0 +1,65 @@ +create([]); + + $this->assertDatabaseHas('tasks', [ + 'id' => $task->id, + ]); + + $request = [ + 'account_id' => $task->account_id, + 'task_id' => $task->id, + ]; + + app(DestroyTask::class)->execute($request); + + $this->assertDatabaseMissing('tasks', [ + 'id' => $task->id, + ]); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $request = [ + 'task_id' => 2, + ]; + + $this->expectException(ValidationException::class); + + app(DestroyTask::class)->execute($request); + } + + /** @test */ + public function it_throws_a_task_doesnt_exist() + { + $task = factory(Task::class)->create([]); + $account = factory(Account::class)->create([]); + + $request = [ + 'account_id' => $account->id, + 'task_id' => $task->id, + ]; + + $this->expectException(ModelNotFoundException::class); + + app(DestroyTask::class)->execute($request); + } +} diff --git a/tests/Unit/Services/Task/UpdateTaskTest.php b/tests/Unit/Services/Task/UpdateTaskTest.php new file mode 100644 index 0000000..f0ea27c --- /dev/null +++ b/tests/Unit/Services/Task/UpdateTaskTest.php @@ -0,0 +1,137 @@ +create([]); + + $request = [ + 'account_id' => $task->account_id, + 'contact_id' => $task->contact_id, + 'task_id' => $task->id, + 'title' => 'title', + 'description' => 'description', + 'completed' => true, + ]; + + $task = app(UpdateTask::class)->execute($request); + + $this->assertDatabaseHas('tasks', [ + 'id' => $task->id, + 'contact_id' => $task->contact_id, + 'title' => 'title', + 'description' => 'description', + 'completed' => 1, + ]); + + $this->assertInstanceOf( + Task::class, + $task + ); + } + + /** @test */ + public function it_updates_a_task_associated_without_a_contact() + { + $task = factory(Task::class)->create([ + 'contact_id' => null, + ]); + + $request = [ + 'account_id' => $task->account_id, + 'task_id' => $task->id, + 'title' => 'title', + 'description' => 'description', + 'completed' => true, + ]; + + $task = app(UpdateTask::class)->execute($request); + + $this->assertDatabaseHas('tasks', [ + 'id' => $task->id, + 'contact_id' => null, + 'title' => 'title', + 'description' => 'description', + 'completed' => 1, + ]); + + $this->assertInstanceOf( + Task::class, + $task + ); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $task = factory(Task::class)->create([]); + + $request = [ + 'account_id' => $task->account_id, + 'task_id' => $task->id, + 'title' => 'title', + 'description' => 'description', + ]; + + $this->expectException(ValidationException::class); + + app(UpdateTask::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_contact_is_not_linked_to_account() + { + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create(); + $task = factory(Task::class)->create([]); + + $request = [ + 'account_id' => $task->account_id, + 'contact_id' => $contact->id, + 'task_id' => $task->id, + 'title' => 'title', + 'description' => 'description', + 'completed' => false, + ]; + + $this->expectException(ModelNotFoundException::class); + + app(UpdateTask::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_task_is_not_linked_to_account() + { + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create(); + $task = factory(Task::class)->create([]); + + $request = [ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + 'task_id' => $task->id, + 'title' => 'title', + 'description' => 'description', + 'completed' => false, + ]; + + $this->expectException(ModelNotFoundException::class); + + app(UpdateTask::class)->execute($request); + } +} diff --git a/tests/Unit/Services/User/AcceptPolicyTest.php b/tests/Unit/Services/User/AcceptPolicyTest.php new file mode 100644 index 0000000..446cca7 --- /dev/null +++ b/tests/Unit/Services/User/AcceptPolicyTest.php @@ -0,0 +1,73 @@ +create([]); + $term = factory(Term::class)->create([]); + + $request = [ + 'account_id' => $user->account_id, + 'user_id' => $user->id, + 'ip_address' => '182.21.12.21', + ]; + + $term = app(AcceptPolicy::class)->execute($request); + + $this->assertDatabaseHas('term_user', [ + 'user_id' => $user->id, + 'term_id' => $term->id, + 'account_id' => $user->account_id, + 'ip_address' => '182.21.12.21', + ]); + + $this->assertInstanceOf( + Term::class, + $term + ); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $user = factory(User::class)->create([]); + + $request = [ + 'email' => 'email@email.com', + ]; + + $this->expectException(ValidationException::class); + app(AcceptPolicy::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_user_is_not_linked_to_account() + { + $account = factory(Account::class)->create(); + $user = factory(User::class)->create(); + + $request = [ + 'account_id' => $account->id, + 'user_id' => $user->id, + 'ip_address' => '182.21.12.21', + ]; + + $this->expectException(ModelNotFoundException::class); + app(AcceptPolicy::class)->execute($request); + } +} diff --git a/tests/Unit/Services/User/EmailChangeTest.php b/tests/Unit/Services/User/EmailChangeTest.php new file mode 100644 index 0000000..2870d75 --- /dev/null +++ b/tests/Unit/Services/User/EmailChangeTest.php @@ -0,0 +1,140 @@ + false]); + + $user = factory(User::class)->create([]); + + $request = [ + 'account_id' => $user->account_id, + 'user_id' => $user->id, + 'email' => 'newmail@ok.com', + ]; + + $user = app(EmailChange::class)->execute($request); + + NotificationFacade::assertNotSentTo($user, VerifyEmail::class); + NotificationFacade::assertNothingSent(); + + $this->assertDatabaseHas('users', [ + 'id' => $user->id, + 'account_id' => $user->account_id, + 'email' => 'newmail@ok.com', + ]); + + $this->assertInstanceOf( + User::class, + $user + ); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $user = factory(User::class)->create([]); + + $request = [ + 'email' => 'email@email.com', + ]; + + $this->expectException(ValidationException::class); + + app(EmailChange::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_user_is_not_linked_to_account() + { + $account = factory(Account::class)->create(); + $user = factory(User::class)->create(); + + $request = [ + 'account_id' => $account->id, + 'user_id' => $user->id, + 'email' => 'newmail@ok.com', + ]; + + $this->expectException(ModelNotFoundException::class); + + app(EmailChange::class)->execute($request); + } + + /** @test */ + public function it_updates_user_email_and_send_confirmation() + { + NotificationFacade::fake(); + config(['monica.signup_double_optin' => true]); + + // Creating a fake account + factory(Account::class)->create(); + + $user = factory(User::class)->create([]); + + $request = [ + 'account_id' => $user->account_id, + 'user_id' => $user->id, + 'email' => 'newmail@ok.com', + ]; + + $user = app(EmailChange::class)->execute($request); + + NotificationFacade::assertSentTo($user, VerifyEmail::class); + + $this->assertDatabaseHas('users', [ + 'id' => $user->id, + 'account_id' => $user->account_id, + 'email' => 'newmail@ok.com', + ]); + + $this->assertInstanceOf( + User::class, + $user + ); + } + + /** @test */ + public function it_sends_confirmation_email() + { + NotificationFacade::fake(); + config(['monica.signup_double_optin' => true]); + + // Creating a fake account + factory(Account::class)->create(); + + $user = factory(User::class)->create([]); + + $request = [ + 'account_id' => $user->account_id, + 'user_id' => $user->id, + 'email' => 'newmail@ok.com', + ]; + + $user = app(EmailChange::class)->execute($request); + + NotificationFacade::assertSentTo($user, VerifyEmail::class); + + $notifications = NotificationFacade::sent($user, VerifyEmail::class); + $message = $notifications[0]->toMail($user); + + $this->assertStringContainsString('To validate your email click on the button below', implode('', $message->introLines)); + } +} diff --git a/tests/Unit/Services/User/UpdateViewPreferenceTest.php b/tests/Unit/Services/User/UpdateViewPreferenceTest.php new file mode 100644 index 0000000..0dbc92b --- /dev/null +++ b/tests/Unit/Services/User/UpdateViewPreferenceTest.php @@ -0,0 +1,70 @@ +create([]); + + $request = [ + 'account_id' => $user->account_id, + 'user_id' => $user->id, + 'preference' => 'last_first', + ]; + + $user = app(UpdateViewPreference::class)->execute($request); + + $this->assertDatabaseHas('users', [ + 'id' => $user->id, + 'account_id' => $user->account_id, + 'contacts_sort_order' => 'last_first', + ]); + + $this->assertInstanceOf( + User::class, + $user + ); + } + + /** @test */ + public function it_fails_if_wrong_parameters_are_given() + { + $user = factory(User::class)->create([]); + + $request = [ + 'email' => 'email@email.com', + ]; + + $this->expectException(ValidationException::class); + app(UpdateViewPreference::class)->execute($request); + } + + /** @test */ + public function it_throws_an_exception_if_user_is_not_linked_to_account() + { + $account = factory(Account::class)->create(); + $user = factory(User::class)->create(); + + $request = [ + 'account_id' => $account->id, + 'user_id' => $user->id, + 'preference' => 'last_first', + ]; + + $this->expectException(ModelNotFoundException::class); + app(UpdateViewPreference::class)->execute($request); + } +} diff --git a/tests/Unit/Services/VCard/ExportVCardTest.php b/tests/Unit/Services/VCard/ExportVCardTest.php new file mode 100644 index 0000000..122c27c --- /dev/null +++ b/tests/Unit/Services/VCard/ExportVCardTest.php @@ -0,0 +1,696 @@ +create(); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + $vCard = new VCard(); + + $exportVCard = app(ExportVCard::class); + $this->invokePrivateMethod($exportVCard, 'exportNames', [$contact, $vCard]); + + $this->assertCount( + self::defaultPropsCount + 2, + $vCard->children() + ); + $this->assertStringContainsString('FN:John Doe', $vCard->serialize()); + $this->assertStringContainsString('N:Doe;John;;;', $vCard->serialize()); + } + + /** @test */ + public function vcard_add_nickname() + { + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + 'nickname' => 'the nickname', + ]); + $vCard = new VCard(); + + $exportVCard = app(ExportVCard::class); + $this->invokePrivateMethod($exportVCard, 'exportNames', [$contact, $vCard]); + + $this->assertCount( + self::defaultPropsCount + 3, + $vCard->children() + ); + $this->assertStringContainsString('FN:John Doe', $vCard->serialize()); + $this->assertStringContainsString('N:Doe;John;;;', $vCard->serialize()); + $this->assertStringContainsString('NICKNAME:the nickname', $vCard->serialize()); + } + + /** @test */ + public function vcard_add_gender() + { + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + $vCard = new VCard(); + + $exportVCard = app(ExportVCard::class); + $this->invokePrivateMethod($exportVCard, 'exportGender', [$contact, $vCard]); + + $this->assertCount( + self::defaultPropsCount + 1, + $vCard->children() + ); + $this->assertStringContainsString('GENDER:M', $vCard->serialize()); + } + + /** @test */ + public function vcard_add_gender_female() + { + $account = factory(Account::class)->create(); + $gender = factory(Gender::class)->create([ + 'account_id' => $account->id, + 'type' => 'F', + 'name' => 'Female', + ]); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + 'gender_id' => $gender->id, + ]); + $vCard = new VCard(); + + $exportVCard = new ExportVCard(); + $this->invokePrivateMethod($exportVCard, 'exportGender', [$contact, $vCard]); + + $this->assertCount( + self::defaultPropsCount + 1, + $vCard->children() + ); + $this->assertStringContainsString('GENDER:F', $vCard->serialize()); + } + + /** @test */ + public function vcard_add_gender_unknown() + { + $account = factory(Account::class)->create(); + $gender = factory(Gender::class)->create([ + 'account_id' => $account->id, + 'type' => 'U', + ]); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + 'gender_id' => $gender->id, + ]); + $vCard = new VCard(); + + $exportVCard = new ExportVCard(); + $this->invokePrivateMethod($exportVCard, 'exportGender', [$contact, $vCard]); + + $this->assertCount( + self::defaultPropsCount + 1, + $vCard->children() + ); + $this->assertStringContainsString('GENDER:U', $vCard->serialize()); + } + + /** @test */ + public function vcard_add_gender_type_null() + { + $account = factory(Account::class)->create(); + $gender = factory(Gender::class)->create([ + 'account_id' => $account->id, + 'type' => null, + 'name' => 'Something', + ]); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + 'gender_id' => $gender->id, + ]); + $vCard = new VCard(); + + $exportVCard = new ExportVCard(); + $this->invokePrivateMethod($exportVCard, 'exportGender', [$contact, $vCard]); + + $this->assertCount( + self::defaultPropsCount + 1, + $vCard->children() + ); + $this->assertStringContainsString('GENDER:O', $vCard->serialize()); + } + + /** @test */ + public function vcard_add_gender_type_null_male() + { + $account = factory(Account::class)->create(); + $gender = factory(Gender::class)->create([ + 'account_id' => $account->id, + 'type' => null, + 'name' => 'Male', + ]); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + 'gender_id' => $gender->id, + ]); + $vCard = new VCard(); + + $exportVCard = new ExportVCard(); + $this->invokePrivateMethod($exportVCard, 'exportGender', [$contact, $vCard]); + + $this->assertCount( + self::defaultPropsCount + 1, + $vCard->children() + ); + $this->assertStringContainsString('GENDER:O', $vCard->serialize()); + } + + /** @test */ + public function vcard_add_gender_type_null_female() + { + $account = factory(Account::class)->create(); + $gender = factory(Gender::class)->create([ + 'account_id' => $account->id, + 'type' => null, + 'name' => 'Woman', + ]); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + 'gender_id' => $gender->id, + ]); + $vCard = new VCard(); + + $exportVCard = new ExportVCard(); + $this->invokePrivateMethod($exportVCard, 'exportGender', [$contact, $vCard]); + + $this->assertCount( + self::defaultPropsCount + 1, + $vCard->children() + ); + $this->assertStringContainsString('GENDER:F', $vCard->serialize()); + } + + /** @test */ + public function vcard_add_photo() + { + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create(['account_id' => $account->id]); + $vCard = new VCard(); + + $contact->avatar_source = 'gravatar'; + $contact->avatar_gravatar_url = 'gravatar'; + + $exportVCard = app(ExportVCard::class); + $this->invokePrivateMethod($exportVCard, 'exportPhoto', [$contact, $vCard]); + + $this->assertCount( + self::defaultPropsCount + 1, + $vCard->children() + ); + $this->assertStringContainsString('PHOTO;VALUE=URI:gravatar', $vCard->serialize()); + } + + /** @test */ + public function vcard_add_work_org() + { + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + 'company' => 'the company', + ]); + $vCard = new VCard(); + + $exportVCard = app(ExportVCard::class); + $this->invokePrivateMethod($exportVCard, 'exportWorkInformation', [$contact, $vCard]); + + $this->assertCount( + self::defaultPropsCount + 1, + $vCard->children() + ); + $this->assertStringContainsString('ORG:the company', $vCard->serialize()); + } + + /** @test */ + public function vcard_add_work_title() + { + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + 'job' => 'job position', + ]); + $vCard = new VCard(); + + $exportVCard = app(ExportVCard::class); + $this->invokePrivateMethod($exportVCard, 'exportWorkInformation', [$contact, $vCard]); + + $this->assertCount( + self::defaultPropsCount + 1, + $vCard->children() + ); + $this->assertStringContainsString('TITLE:job position', $vCard->serialize()); + } + + /** @test */ + public function vcard_add_work_information() + { + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + 'company' => 'the company', + 'job' => 'job position', + ]); + $vCard = new VCard(); + + $exportVCard = app(ExportVCard::class); + $this->invokePrivateMethod($exportVCard, 'exportWorkInformation', [$contact, $vCard]); + + $this->assertCount( + self::defaultPropsCount + 2, + $vCard->children() + ); + $this->assertStringContainsString('ORG:the company', $vCard->serialize()); + $this->assertStringContainsString('TITLE:job position', $vCard->serialize()); + } + + /** @test */ + public function vcard_add_birthday() + { + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create(['account_id' => $account->id]); + $contact->setSpecialDate('birthdate', 2000, 10, 5); + $vCard = new VCard(); + + $exportVCard = app(ExportVCard::class); + $this->invokePrivateMethod($exportVCard, 'exportBirthday', [$contact, $vCard]); + + $this->assertCount( + self::defaultPropsCount + 1, + $vCard->children() + ); + $this->assertStringContainsString('BDAY:20001005', $vCard->serialize()); + } + + /** @test */ + public function vcard_add_birthday_with_unknown_year() + { + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create(['account_id' => $account->id]); + $contact->setSpecialDate('birthdate', 0, 10, 5); + $vCard = new VCard(); + + $exportVCard = app(ExportVCard::class); + $this->invokePrivateMethod($exportVCard, 'exportBirthday', [$contact, $vCard]); + + $this->assertCount( + self::defaultPropsCount + 1, + $vCard->children() + ); + $this->assertStringContainsString('BDAY:--1005', $vCard->serialize()); + } + + /** @test */ + public function vcard_add_contact_fields_empty() + { + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create(['account_id' => $account->id]); + $vCard = new VCard(); + + $exportVCard = app(ExportVCard::class); + $this->invokePrivateMethod($exportVCard, 'exportContactFields', [$contact, $vCard]); + + $this->assertCount( + self::defaultPropsCount, + $vCard->children() + ); + } + + /** @test */ + public function vcard_add_contact_fields() + { + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create(['account_id' => $account->id]); + $vCard = new VCard(); + + $contactFieldType = factory(ContactFieldType::class)->create(['account_id' => $account->id]); + factory(ContactField::class)->create([ + 'contact_id' => $contact->id, + 'account_id' => $account->id, + 'contact_field_type_id' => $contactFieldType->id, + ]); + + $exportVCard = app(ExportVCard::class); + $this->invokePrivateMethod($exportVCard, 'exportContactFields', [$contact, $vCard]); + + $this->assertCount( + self::defaultPropsCount + 1, + $vCard->children() + ); + $this->assertStringContainsString('EMAIL:john@doe.com', $vCard->serialize()); + } + + /** @test */ + public function vcard_add_contact_fields_email_labels() + { + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create(['account_id' => $account->id]); + $vCard = new VCard(); + + $contactFieldType = factory(ContactFieldType::class)->create(['account_id' => $account->id]); + $contactField = factory(ContactField::class)->create([ + 'contact_id' => $contact->id, + 'account_id' => $account->id, + 'contact_field_type_id' => $contactFieldType->id, + ]); + $contactFieldLabel = factory(ContactFieldLabel::class)->create([ + 'account_id' => $account->id, + ]); + $contactField->labels()->attach($contactFieldLabel->id, ['account_id' => $account->id]); + + $exportVCard = app(ExportVCard::class); + $this->invokePrivateMethod($exportVCard, 'exportContactFields', [$contact, $vCard]); + + $this->assertCount( + self::defaultPropsCount + 1, + $vCard->children() + ); + $this->assertStringContainsString('EMAIL;TYPE=WORK:john@doe.com', $vCard->serialize()); + } + + /** @test */ + public function vcard_add_contact_fields_tel_labels() + { + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create(['account_id' => $account->id]); + $vCard = new VCard(); + + $contactFieldType = factory(ContactFieldType::class)->create([ + 'account_id' => $account->id, + 'type' => 'phone', + ]); + $contactField = factory(ContactField::class)->create([ + 'contact_id' => $contact->id, + 'account_id' => $account->id, + 'data' => '0123456789', + 'contact_field_type_id' => $contactFieldType->id, + ]); + $contactFieldLabel = factory(ContactFieldLabel::class)->create([ + 'account_id' => $account->id, + ]); + $contactField->labels()->attach($contactFieldLabel->id, ['account_id' => $account->id]); + + $exportVCard = app(ExportVCard::class); + $this->invokePrivateMethod($exportVCard, 'exportContactFields', [$contact, $vCard]); + + $this->assertCount( + self::defaultPropsCount + 1, + $vCard->children() + ); + $this->assertStringContainsString('TEL;TYPE=WORK:0123456789', $vCard->serialize()); + } + + /** @test */ + public function vcard_add_contact_fields_personal_labels() + { + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create(['account_id' => $account->id]); + $vCard = new VCard(); + + $contactField = factory(ContactField::class)->create([ + 'contact_id' => $contact->id, + 'account_id' => $account->id, + ]); + $contactFieldLabel = factory(ContactFieldLabel::class)->create([ + 'account_id' => $account->id, + 'label_i18n' => null, + 'label' => 'Something', + ]); + $contactField->labels()->attach($contactFieldLabel->id, ['account_id' => $account->id]); + + $exportVCard = app(ExportVCard::class); + $this->invokePrivateMethod($exportVCard, 'exportContactFields', [$contact, $vCard]); + + $this->assertCount( + self::defaultPropsCount + 1, + $vCard->children() + ); + $this->assertStringContainsString('EMAIL;TYPE=Something:john@doe.com', $vCard->serialize()); + } + + /** + * @test + * @dataProvider socialProfileProvider + */ + public function vcard_add_social_profile($name, $type, $data, $result) + { + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create(['account_id' => $account->id]); + $vCard = new VCard(); + + $contactFieldType = factory(ContactFieldType::class)->create([ + 'account_id' => $account->id, + 'name' => $name, + 'type' => $type, + ]); + factory(ContactField::class)->create([ + 'contact_id' => $contact->id, + 'account_id' => $account->id, + 'contact_field_type_id' => $contactFieldType->id, + 'data' => $data, + ]); + + $exportVCard = app(ExportVCard::class); + $this->invokePrivateMethod($exportVCard, 'exportContactFields', [$contact, $vCard]); + + $this->assertCount( + self::defaultPropsCount + 1, + $vCard->children() + ); + $this->assertStringContainsString($result, $vCard->serialize()); + } + + public function socialProfileProvider() + { + return [ + ['Facebook', 'Facebook', 'test', 'SOCIALPROFILE;TYPE=facebook:https://www.facebook.com/test'], + ['Twitter', 'Twitter', 'test', 'SOCIALPROFILE;TYPE=twitter:https://twitter.com/test'], + ['Whatsapp', 'Whatsapp', 'test', 'SOCIALPROFILE;TYPE=whatsapp:https://wa.me/test'], + ['Telegram', 'Telegram', 'test', 'SOCIALPROFILE;TYPE=telegram:http://t.me/test'], + ['LinkedIn', 'LinkedIn', 'test', 'SOCIALPROFILE;TYPE=linkedin:http://www.linkedin.com/in/test'], + ]; + } + + /** + * @test + * @dataProvider contactUrlProvider + */ + public function vcard_add_contact_url($name, $protocol, $data, $result) + { + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create(['account_id' => $account->id]); + $vCard = new VCard(); + + $contactFieldType = factory(ContactFieldType::class)->create([ + 'account_id' => $account->id, + 'name' => $name, + 'protocol' => $protocol, + 'type' => 'URL', + ]); + factory(ContactField::class)->create([ + 'contact_id' => $contact->id, + 'account_id' => $account->id, + 'contact_field_type_id' => $contactFieldType->id, + 'data' => $data, + ]); + + $exportVCard = app(ExportVCard::class); + $this->invokePrivateMethod($exportVCard, 'exportContactFields', [$contact, $vCard]); + $this->assertStringContainsString($result, $vCard->serialize()); + } + + public function contactUrlProvider() + { + return [ + ['Discord', 'https://www.discord.app/user/', 'test123', 'URL;VALUE=URI:https://www.discord.app/user/test123'], + ['Facebook Profile', 'https://www.facebook.com/', 'test123', 'URL;VALUE=URI:https://www.facebook.com/test123'], + ]; + } + + /** @test */ + public function vcard_add_addresses_empty() + { + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create(['account_id' => $account->id]); + $vCard = new VCard(); + + $exportVCard = app(ExportVCard::class); + $this->invokePrivateMethod($exportVCard, 'exportAddress', [$contact, $vCard]); + + $this->assertCount( + self::defaultPropsCount, + $vCard->children() + ); + } + + /** @test */ + public function vcard_add_addresses() + { + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create(['account_id' => $account->id]); + $vCard = new VCard(); + + factory(Address::class)->create([ + 'contact_id' => $contact->id, + 'name' => 'Home', + 'account_id' => $account->id, + ]); + factory(Address::class)->create([ + 'contact_id' => $contact->id, + 'name' => 'Home', + 'account_id' => $account->id, + ]); + + $exportVCard = app(ExportVCard::class); + $this->invokePrivateMethod($exportVCard, 'exportAddress', [$contact, $vCard]); + + $this->assertCount( + self::defaultPropsCount + 2, + $vCard->children() + ); + $this->assertStringContainsString('ADR:;;12;beverly hills;;90210;US', $vCard->serialize()); + $this->assertStringContainsString('ADR:;;12;beverly hills;;90210;US', $vCard->serialize()); + } + + /** @test */ + public function vcard_add_addresses_with_labels() + { + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create(['account_id' => $account->id]); + $vCard = new VCard(); + + $address = factory(Address::class)->create([ + 'contact_id' => $contact->id, + 'name' => 'Home', + 'account_id' => $account->id, + ]); + + $contactFieldLabel = factory(ContactFieldLabel::class)->create([ + 'account_id' => $account->id, + ]); + $address->labels()->attach($contactFieldLabel->id, ['account_id' => $account->id]); + + $exportVCard = app(ExportVCard::class); + $this->invokePrivateMethod($exportVCard, 'exportAddress', [$contact, $vCard]); + + $this->assertCount( + self::defaultPropsCount + 1, + $vCard->children() + ); + $this->assertStringContainsString('ADR;TYPE=WORK:;;12;beverly hills;;90210;US', $vCard->serialize()); + } + + /** @test */ + public function vcard_prepares_an_almost_empty_vcard() + { + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create(['account_id' => $account->id])->refresh(); + + $exportVCard = app(ExportVCard::class); + $vCard = $this->invokePrivateMethod($exportVCard, 'export', [$contact]); + + $this->assertCount( + self::defaultPropsCount + 6, + $vCard->children() + ); + + $this->assertVObjectEqualsVObject($this->getCard($contact), $vCard); + } + + /** @test */ + public function vcard_prepares_a_complete_vcard() + { + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create(['account_id' => $account->id]); + + factory(Address::class)->create([ + 'contact_id' => $contact->id, + 'name' => 'Home', + 'account_id' => $account->id, + ]); + + factory(Address::class)->create([ + 'contact_id' => $contact->id, + 'name' => 'Home', + 'account_id' => $account->id, + ]); + + $contactFieldType = factory(ContactFieldType::class)->create(['account_id' => $account->id]); + factory(ContactField::class)->create([ + 'contact_id' => $contact->id, + 'account_id' => $account->id, + 'contact_field_type_id' => $contactFieldType->id, + ]); + + $exportVCard = app(ExportVCard::class); + $contact = $contact->refresh(); + $vCard = $this->invokePrivateMethod($exportVCard, 'export', [$contact]); + + $this->assertCount( + self::defaultPropsCount + 9, + $vCard->children() + ); + + $this->assertVObjectEqualsVObject($this->getCard($contact), $vCard); + } + + /** @test */ + public function vcard_with_tags() + { + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + + app(AssociateTag::class)->execute([ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'name' => 'tag1', + ]); + app(AssociateTag::class)->execute([ + 'account_id' => $contact->account_id, + 'contact_id' => $contact->id, + 'name' => 'tag2', + ]); + + $exportVCard = app(ExportVCard::class); + $contact = $contact->refresh(); + $vCard = $this->invokePrivateMethod($exportVCard, 'export', [$contact]); + + $this->assertCount( + self::defaultPropsCount + 7, + $vCard->children() + ); + + $this->assertVObjectEqualsVObject($this->getCard($contact), $vCard); + } +} diff --git a/tests/Unit/Services/VCard/GetEtagTest.php b/tests/Unit/Services/VCard/GetEtagTest.php new file mode 100644 index 0000000..b67cf30 --- /dev/null +++ b/tests/Unit/Services/VCard/GetEtagTest.php @@ -0,0 +1,49 @@ +create(); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + 'vcard' => 'test', + ]); + + $etag = app(GetEtag::class)->execute([ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + ]); + + $this->assertEquals('"a94a8fe5ccb19ba61c4c0873d391e987982fbbd3"', $etag); + } + + /** @test */ + public function it_get_etag_distant_contact() + { + $account = factory(Account::class)->create(); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + 'vcard' => 'test', + 'distant_etag' => '"test"', + ]); + + $etag = app(GetEtag::class)->execute([ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + ]); + + $this->assertEquals('"test"', $etag); + } +} diff --git a/tests/Unit/Services/VCard/ImportVCardTest.php b/tests/Unit/Services/VCard/ImportVCardTest.php new file mode 100644 index 0000000..0df1717 --- /dev/null +++ b/tests/Unit/Services/VCard/ImportVCardTest.php @@ -0,0 +1,1382 @@ +create([]); + $importVCard = new ImportVCard; + + $vcard = new VCard([]); + + $this->assertFalse($this->invokePrivateMethod($importVCard, 'canImportCurrentEntry', [$vcard])); + } + + /** @test */ + public function it_can_not_import_because_no_firstname_in_vcard() + { + $account = factory(Account::class)->create([]); + $importVCard = new ImportVCard; + + $vcard = new VCard([ + 'N' => ['John', '', '', '', ''], + ]); + + $this->assertFalse($this->invokePrivateMethod($importVCard, 'canImportCurrentEntry', [$vcard])); + } + + /** @test */ + public function it_can_not_import_because_empty_firstname_in_vcard() + { + $account = factory(Account::class)->create([]); + $importVCard = new ImportVCard; + + $vcard = new VCard([ + 'N' => ';;;;', + ]); + + $this->assertFalse($this->invokePrivateMethod($importVCard, 'canImportCurrentEntry', [$vcard])); + } + + /** @test */ + public function it_can_not_import_vcard() + { + $account = factory(Account::class)->create([]); + $importVCard = new ImportVCard; + + $vcard = Reader::read(' +BEGIN:VCARD +VERSION:3.0 +N:;;;; +FN: +ORG:; +EMAIL;TYPE=home;TYPE=pref:mail@example.org +NOTE: +NICKNAME: +TITLE: +REV:20210900T000102Z +END:VCARD', Reader::OPTION_FORGIVING + Reader::OPTION_IGNORE_INVALID_LINES); + + $this->assertFalse($this->invokePrivateMethod($importVCard, 'canImportCurrentEntry', [$vcard])); + } + + /** @test */ + public function it_can_not_import_because_empty_nickname_in_vcard() + { + $account = factory(Account::class)->create([]); + $importVCard = new ImportVCard; + + $vcard = new VCard([ + 'NICKNAME' => '', + ]); + + $this->assertFalse($this->invokePrivateMethod($importVCard, 'canImportCurrentEntry', [$vcard])); + } + + /** @test */ + public function it_can_not_import_because_empty_fullname_in_vcard() + { + $account = factory(Account::class)->create([]); + $importVCard = new ImportVCard; + + $vcard = new VCard([ + 'FN' => '', + ]); + + $this->assertFalse($this->invokePrivateMethod($importVCard, 'canImportCurrentEntry', [$vcard])); + } + + /** @test */ + public function it_can_import_firstname() + { + $account = factory(Account::class)->create([]); + $importVCard = new ImportVCard; + + $vcard = new VCard([ + 'N' => ['', 'John', '', '', ''], + ]); + + $this->assertTrue($this->invokePrivateMethod($importVCard, 'canImportCurrentEntry', [$vcard])); + } + + /** @test */ + public function it_can_import_nickname() + { + $account = factory(Account::class)->create([]); + $importVCard = new ImportVCard; + + $vcard = new VCard([ + 'NICKNAME' => 'John', + ]); + + $this->assertTrue($this->invokePrivateMethod($importVCard, 'canImportCurrentEntry', [$vcard])); + } + + /** @test */ + public function it_can_import_fullname() + { + $account = factory(Account::class)->create([]); + $importVCard = new ImportVCard; + + $vcard = new VCard([ + 'FN' => 'John Doe', + ]); + + $this->assertTrue($this->invokePrivateMethod($importVCard, 'canImportCurrentEntry', [$vcard])); + } + + /** @test */ + public function it_validates_email() + { + $account = factory(Account::class)->create([]); + $importVCard = new ImportVCard; + + $invalidEmail = 'test@'; + + $this->assertFalse($this->invokePrivateMethod($importVCard, 'isValidEmail', [$invalidEmail])); + + $validEmail = 'john@doe.com'; + + $this->assertTrue($this->invokePrivateMethod($importVCard, 'isValidEmail', [$validEmail])); + } + + /** @test */ + public function it_checks_if_a_contact_exists() + { + $account = factory(Account::class)->create([]); + $importVCard = new ImportVCard; + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + $contactFieldType = factory(ContactFieldType::class)->create([ + 'account_id' => $account->id, + 'type' => 'email', + ]); + $contactField = factory(ContactField::class)->create([ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + 'contact_field_type_id' => $contactFieldType->id, + 'data' => 'john@doe.com', + ]); + + $vcard = new VCard([ + 'N' => ['John', 'Doe', '', '', ''], + 'EMAIL' => 'john@', + ]); + + $contact = $this->invokePrivateMethod($importVCard, 'getExistingContact', [$vcard]); + $this->assertNull($contact); + } + + /** @test */ + public function it_returns_an_unknown_name_if_no_name_is_in_entry() + { + $account = factory(Account::class)->create([]); + $importVCard = new ImportVCard; + + $vcard = new VCard([ + 'EMAIL' => 'john@', + ]); + + $this->assertEquals( + trans('settings.import_vcard_unknown_entry'), + $this->invokePrivateMethod($importVCard, 'name', [$vcard]) + ); + } + + /** @test */ + public function it_returns_a_name_for_N() + { + $account = factory(Account::class)->create([]); + $importVCard = new ImportVCard; + + $vcard = new VCard([ + 'N' => ['John', 'Doe', '', '', ''], + 'EMAIL' => 'john@doe.com', + ]); + + $this->assertEquals('Doe John john@doe.com', $this->invokePrivateMethod($importVCard, 'name', [$vcard])); + } + + /** @test */ + public function it_returns_a_name_for_N_incomplete() + { + $account = factory(Account::class)->create([]); + $importVCard = new ImportVCard; + + $vcard = new VCard([ + 'N' => ['John', 'Doe'], + 'EMAIL' => 'john@doe.com', + ]); + + $this->assertEquals('Doe John john@doe.com', $this->invokePrivateMethod($importVCard, 'name', [$vcard])); + } + + /** @test */ + public function it_returns_a_name_for_NICKNAME() + { + $account = factory(Account::class)->create([]); + $importVCard = new ImportVCard; + + $vcard = new VCard([ + 'NICKNAME' => 'John', + 'EMAIL' => 'john@doe.com', + ]); + + $this->assertEquals('John john@doe.com', $this->invokePrivateMethod($importVCard, 'name', [$vcard])); + } + + /** @test */ + public function it_returns_a_name_for_FN() + { + $account = factory(Account::class)->create([]); + $importVCard = new ImportVCard; + + $vcard = new VCard([ + 'FN' => 'John Doe', + 'EMAIL' => 'john@doe.com', + ]); + + $this->assertEquals('John Doe john@doe.com', $this->invokePrivateMethod($importVCard, 'name', [$vcard])); + } + + /** @test */ + public function it_formats_value() + { + $account = factory(Account::class)->create([]); + $importVCard = new ImportVCard; + + $result = $this->invokePrivateMethod($importVCard, 'formatValue', ['']); + $this->assertNull($result); + + $result = $this->invokePrivateMethod($importVCard, 'formatValue', ['This is a value']); + $this->assertEquals( + 'This is a value', + $result + ); + } + + /** @test */ + public function it_creates_a_contact() + { + $user = factory(User::class)->create([]); + $importVCard = new ImportVCard; + $importVCard->accountId = $user->account_id; + $importVCard->userId = $user->id; + + $vcard = new VCard([ + 'N' => ['John', 'Doe', '', '', ''], + 'EMAIL' => 'john@doe.com', + ]); + factory(ContactFieldType::class)->create([ + 'account_id' => $user->account_id, + 'type' => 'email', + ]); + + $contact = $this->invokePrivateMethod($importVCard, 'importEntry', [null, $vcard, $vcard->serialize(), null]); + + $this->assertTrue($contact->exists); + } + + /** @test */ + public function it_creates_a_contact_in_address_book() + { + $user = factory(User::class)->create([]); + $addressBook = AddressBook::factory()->create([ + 'account_id' => $user->account_id, + 'user_id' => $user->id, + 'name' => 'contacts', + ]); + + $importVCard = new ImportVCard; + $importVCard->accountId = $user->account_id; + $importVCard->userId = $user->id; + + $this->setPrivateValue($importVCard, 'addressBook', $addressBook); + + $vcard = new VCard([ + 'N' => ['John', 'Doe', '', '', ''], + 'EMAIL' => 'john@doe.com', + ]); + factory(ContactFieldType::class)->create([ + 'account_id' => $user->account_id, + 'type' => 'email', + ]); + + $contact = $this->invokePrivateMethod($importVCard, 'importEntry', [null, $vcard, $vcard->serialize(), null]); + + $this->assertTrue($contact->exists); + $this->assertEquals($addressBook->id, $contact->address_book_id); + } + + /** @test */ + public function it_update_a_contact_with_birthdate() + { + $user = factory(User::class)->create([]); + $importVCard = new ImportVCard; + $importVCard->accountId = $user->account_id; + $importVCard->userId = $user->id; + + $vcard = new VCard([ + 'N' => ['John', 'Doe', '', '', ''], + ]); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $contact->setSpecialDate('birthdate', 2001, 04, 01); + + $newContact = $this->invokePrivateMethod($importVCard, 'importGeneralInformation', [$contact, $vcard]); + + $this->assertEquals('John', $newContact->last_name); + $this->assertEquals('Doe', $newContact->first_name); + $this->assertNotNull($newContact->birthdate); + $this->assertEquals('2001-04-01', $newContact->birthdate->date->format('Y-m-d')); + } + + /** @test */ + public function it_update_a_contact_with_birthdate_age_based() + { + Carbon::setTestNow(Carbon::create(2021, 8, 25, 7, 0, 0)); + + $user = factory(User::class)->create([]); + $importVCard = new ImportVCard; + $importVCard->accountId = $user->account_id; + $importVCard->userId = $user->id; + + $vcard = new VCard([ + 'N' => ['John', 'Doe', '', '', ''], + ]); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $contact->setSpecialDateFromAge('birthdate', 19); + + $newContact = $this->invokePrivateMethod($importVCard, 'importGeneralInformation', [$contact, $vcard]); + + $this->assertEquals('John', $newContact->last_name); + $this->assertEquals('Doe', $newContact->first_name); + $this->assertNotNull($newContact->birthdate); + $this->assertTrue($newContact->birthdate->is_age_based); + $this->assertEquals('2002-01-01', $newContact->birthdate->date->format('Y-m-d')); + } + + /** @test */ + public function it_update_a_contact_with_birthdate_and_replace_it() + { + $user = factory(User::class)->create([]); + $importVCard = new ImportVCard; + $importVCard->accountId = $user->account_id; + $importVCard->userId = $user->id; + + $vcard = new VCard([ + 'N' => ['John', 'Doe', '', '', ''], + 'BDAY' => '1990-01-01', + ]); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $contact->setSpecialDate('birthdate', 2001, 04, 01); + + $newContact = $this->invokePrivateMethod($importVCard, 'importGeneralInformation', [$contact, $vcard]); + + $this->assertEquals('John', $newContact->last_name); + $this->assertEquals('Doe', $newContact->first_name); + $this->assertNotNull($newContact->birthdate); + $this->assertEquals('1990-01-01', $newContact->birthdate->date->format('Y-m-d')); + } + + /** @test */ + public function it_update_a_contact_with_deceased_date() + { + $user = factory(User::class)->create([]); + $importVCard = new ImportVCard; + $importVCard->accountId = $user->account_id; + $importVCard->userId = $user->id; + + $vcard = new VCard([ + 'N' => ['John', 'Doe', '', '', ''], + ]); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + 'is_dead' => true, + ]); + $contact->setSpecialDate('deceased_date', 2021, 07, 01); + + $newContact = $this->invokePrivateMethod($importVCard, 'importGeneralInformation', [$contact, $vcard]); + + $this->assertEquals('John', $newContact->last_name); + $this->assertEquals('Doe', $newContact->first_name); + $this->assertTrue($newContact->is_dead); + $this->assertNotNull($newContact->deceasedDate); + $this->assertEquals('2021-07-01', $newContact->deceasedDate->date->format('Y-m-d')); + } + + /** @test */ + public function it_creates_a_contact_with_process() + { + $user = factory(User::class)->create([]); + $importVCard = new ImportVCard; + $importVCard->accountId = $user->account_id; + $importVCard->userId = $user->id; + + $vcard = new VCard([ + 'N' => ['John', 'Doe', '', '', ''], + 'EMAIL' => 'john@doe.com', + ]); + factory(ContactFieldType::class)->create([ + 'account_id' => $user->account_id, + 'type' => 'email', + ]); + + $result = $this->invokePrivateMethod($importVCard, 'processEntry', [ + ['behaviour' => 'behaviour_add'], + $vcard, + $vcard->serialize(), + ]); + + $this->assertDatabaseHas('contacts', [ + 'account_id' => $user->account_id, + 'id' => $result['contact_id'], + ]); + } + + /** @test */ + public function it_updates_a_contact_with_process() + { + $user = factory(User::class)->create([]); + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $importVCard = new ImportVCard; + $importVCard->accountId = $user->account_id; + $importVCard->userId = $user->id; + + $vcard = new VCard([ + 'N' => ['Miles', 'Davis', '', '', ''], + ]); + + $this->invokePrivateMethod($importVCard, 'processEntry', [ + [ + 'behaviour' => 'behaviour_replace', + 'contact_id' => $contact->id, + ], + $vcard, + $vcard->serialize(), + ]); + + $this->assertDatabaseHas('contacts', [ + 'account_id' => $user->account_id, + 'id' => $contact->id, + ]); + $contact->refresh(); + + $this->assertEquals('Davis', $contact->first_name); + $this->assertEquals('Miles', $contact->last_name); + } + + /** @test */ + public function it_imports_names_N() + { + $importVCard = new ImportVCard; + + $vcard = new VCard([ + 'N' => ['Doe', 'John', 'Jane', '', ''], + ]); + $contact = $this->invokePrivateMethod($importVCard, 'importNames', [[], $vcard]); + + $this->assertEquals('John', $contact['first_name']); + $this->assertEquals('Doe', $contact['last_name']); + $this->assertEquals('Jane', $contact['middle_name']); + } + + /** @test */ + public function it_imports_names_NICKNAME() + { + $importVCard = new ImportVCard; + + $vcard = new VCard([ + 'NICKNAME' => 'John', + ]); + $contact = $this->invokePrivateMethod($importVCard, 'importNames', [[], $vcard]); + + $this->assertEquals('John', $contact['first_name']); + } + + /** @test */ + public function it_imports_names_FN() + { + $account = factory(Account::class)->create([]); + $user = factory(User::class)->create(['account_id' => $account->id]); + $importVCard = new ImportVCard; + $importVCard->accountId = $account->id; + $importVCard->userId = $user->id; + + $vcard = new VCard([ + 'FN' => 'John Doe', + ]); + $contact = $this->invokePrivateMethod($importVCard, 'importNames', [[], $vcard]); + + $this->assertEquals('John', $contact['first_name']); + $this->assertEquals('Doe', $contact['last_name']); + } + + /** @test */ + public function it_imports_names_FN_last() + { + $account = factory(Account::class)->create([]); + $user = factory(User::class)->create([ + 'account_id' => $account->id, + 'name_order' => 'lastname_firstname', + ]); + $importVCard = new ImportVCard; + $importVCard->accountId = $account->id; + $importVCard->userId = $user->id; + + $vcard = new VCard([ + 'FN' => 'John Doe', + ]); + $contact = $this->invokePrivateMethod($importVCard, 'importNames', [[], $vcard]); + + $this->assertEquals('Doe', $contact['first_name']); + $this->assertEquals('John', $contact['last_name']); + } + + /** @test */ + public function it_imports_names_FN_extra_space() + { + $account = factory(Account::class)->create([]); + $user = factory(User::class)->create(['account_id' => $account->id]); + $importVCard = new ImportVCard; + $importVCard->accountId = $account->id; + $importVCard->userId = $user->id; + + $vcard = new VCard([ + 'FN' => 'John Doe', + ]); + $contact = $this->invokePrivateMethod($importVCard, 'importNames', [[], $vcard]); + + $this->assertEquals('John', $contact['first_name']); + $this->assertEquals('Doe', $contact['last_name']); + } + + /** @test */ + public function it_imports_name_FN() + { + $account = factory(Account::class)->create([]); + $user = factory(User::class)->create(['account_id' => $account->id]); + $importVCard = new ImportVCard; + $importVCard->accountId = $account->id; + $importVCard->userId = $user->id; + + $vcard = new VCard([ + 'FN' => 'John', + 'N' => 'Mike;;;;', + ]); + $contact = $this->invokePrivateMethod($importVCard, 'importNames', [[], $vcard]); + + $this->assertEquals('John', $contact['first_name']); + $this->assertEquals('', Arr::get($contact, 'last_name')); + } + + /** @test */ + public function it_imports_name_FN_last() + { + $account = factory(Account::class)->create([]); + $user = factory(User::class)->create([ + 'account_id' => $account->id, + 'name_order' => 'lastname_firstname', + ]); + $importVCard = new ImportVCard; + $importVCard->accountId = $account->id; + $importVCard->userId = $user->id; + + $vcard = new VCard([ + 'FN' => 'John', + 'N' => 'Mike;;;;', + ]); + $contact = $this->invokePrivateMethod($importVCard, 'importNames', [[], $vcard]); + + $this->assertEquals('John', $contact['first_name']); + $this->assertEquals('', Arr::get($contact, 'last_name')); + } + + /** @test */ + public function it_imports_names_FN_multiple() + { + $account = factory(Account::class)->create([]); + $user = factory(User::class)->create(['account_id' => $account->id]); + $importVCard = new ImportVCard; + $importVCard->accountId = $account->id; + $importVCard->userId = $user->id; + + $vcard = new VCard([ + 'FN' => 'John Doe Marco', + 'N' => 'Mike;;;;', + ]); + $contact = $this->invokePrivateMethod($importVCard, 'importNames', [[], $vcard]); + + $this->assertEquals('John', $contact['first_name']); + $this->assertEquals('Doe Marco', $contact['last_name']); + } + + /** @test */ + public function it_imports_work_information() + { + $user = factory(User::class)->create(); + $importVCard = new ImportVCard; + $importVCard->accountId = $user->account_id; + $importVCard->userId = $user->id; + + $vcard = new VCard([ + 'ORG' => 'Company', + 'ROLE' => 'Branleur', + ]); + + $contact = factory(Contact::class)->create([ + 'account_id' => $user->account_id, + ]); + $this->invokePrivateMethod($importVCard, 'importWorkInformation', [$contact, $vcard]); + + $contact->refresh(); + $this->assertEquals( + 'Company', + $contact->company + ); + + $this->assertEquals( + 'Branleur', + $contact->job + ); + } + + /** @test */ + public function it_imports_birthday() + { + config(['monica.requires_subscription' => false]); + + $user = factory(User::class)->create(); + $importVCard = new ImportVCard; + $importVCard->accountId = $user->account_id; + $importVCard->userId = $user->id; + + $vcard = new VCard([ + 'BDAY' => '1990-01-01', + ]); + + $contact = $this->invokePrivateMethod($importVCard, 'importBirthday', [[], $vcard]); + + $this->assertEquals([ + 'is_birthdate_known' => true, + 'birthdate_is_age_based' => false, + 'birthdate_day' => 1, + 'birthdate_month' => 1, + 'birthdate_year' => 1990, + 'birthdate_add_reminder' => true, + 'is_deceased' => false, + ], $contact); + } + + /** @test */ + public function it_imports_birthday_compact_format() + { + config(['monica.requires_subscription' => false]); + + $account = factory(Account::class)->create([]); + $importVCard = new ImportVCard; + + $vcard = new VCard([ + 'BDAY' => '19900101', + ]); + + $contact = $this->invokePrivateMethod($importVCard, 'importBirthday', [[], $vcard]); + + $this->assertEquals([ + 'is_birthdate_known' => true, + 'birthdate_is_age_based' => false, + 'birthdate_day' => 1, + 'birthdate_month' => 1, + 'birthdate_year' => 1990, + 'birthdate_add_reminder' => true, + 'is_deceased' => false, + ], $contact); + } + + /** @test */ + public function it_imports_birthday_year_unknown() + { + config(['monica.requires_subscription' => false]); + + $account = factory(Account::class)->create([]); + $importVCard = new ImportVCard; + + $vcard = new VCard([ + 'BDAY' => '--05-22', + ]); + + $contact = $this->invokePrivateMethod($importVCard, 'importBirthday', [[], $vcard]); + + $this->assertEquals([ + 'is_birthdate_known' => true, + 'birthdate_is_age_based' => false, + 'birthdate_day' => 22, + 'birthdate_month' => 5, + 'birthdate_year' => null, + 'birthdate_add_reminder' => true, + 'is_deceased' => false, + ], $contact); + } + + /** @test */ + public function import_vcard_imports_address() + { + $account = factory(Account::class)->create([]); + $importVCard = new ImportVCard; + + $vcard = new VCard([ + 'ADR' => [ + '', + '', + 'street', + 'CITY', + 'province', + '10000', + 'us', + ], + ]); + + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + $this->invokePrivateMethod($importVCard, 'importAddress', [$contact, $vcard]); + + $this->assertDatabaseHas('addresses', [ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + ]); + $this->assertDatabaseHas('places', [ + 'account_id' => $account->id, + 'street' => 'street', + 'city' => 'CITY', + 'province' => 'province', + 'postal_code' => '10000', + 'country' => 'US', + ]); + } + + /** @test */ + public function import_vcard_imports_partial_address() + { + $account = factory(Account::class)->create([]); + $importVCard = new ImportVCard; + + $vcard = new VCard([ + 'ADR' => [ + '', + '', + 'street', + ], + ]); + + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + $this->invokePrivateMethod($importVCard, 'importAddress', [$contact, $vcard]); + + $this->assertDatabaseHas('addresses', [ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + ]); + $this->assertDatabaseHas('places', [ + 'account_id' => $account->id, + 'street' => 'street', + 'city' => null, + 'province' => null, + 'postal_code' => null, + 'country' => null, + ]); + } + + /** @test */ + public function import_vcard_updates_address() + { + $account = factory(Account::class)->create([]); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + $address = factory(Address::class)->create([ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + ]); + + $importVCard = new ImportVCard; + + $vcard = new VCard([ + 'ADR' => [ + '', + '', + 'street', + 'CITY', + 'province', + '10000', + 'us', + ], + ]); + + $this->invokePrivateMethod($importVCard, 'importAddress', [$contact, $vcard]); + + $this->assertDatabaseHas('addresses', [ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + 'id' => $address->id, + ]); + $this->assertDatabaseHas('places', [ + 'account_id' => $account->id, + 'street' => 'street', + 'city' => 'CITY', + 'province' => 'province', + 'postal_code' => '10000', + 'country' => 'US', + ]); + $address->refresh(); + $place = $address->place()->first(); + $this->assertEquals($place->street, 'street'); + $this->assertEquals($place->city, 'CITY'); + $this->assertEquals($place->province, 'province'); + $this->assertEquals($place->postal_code, '10000'); + $this->assertEquals($place->country, 'US'); + } + + /** @test */ + public function import_vcard_updates_and_destroy_address() + { + $account = factory(Account::class)->create([]); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + $address1 = factory(Address::class)->create([ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + ]); + $address2 = factory(Address::class)->create([ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + ]); + + $importVCard = new ImportVCard; + + $vcard = new VCard([ + 'ADR' => [ + '', + '', + 'street', + 'CITY', + 'province', + '10000', + 'us', + ], + ]); + + $this->invokePrivateMethod($importVCard, 'importAddress', [$contact, $vcard]); + + $this->assertDatabaseHas('addresses', [ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + 'id' => $address1->id, + ]); + $this->assertDatabaseMissing('addresses', [ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + 'id' => $address2->id, + ]); + $this->assertDatabaseHas('places', [ + 'account_id' => $account->id, + 'street' => 'street', + 'city' => 'CITY', + 'province' => 'province', + 'postal_code' => '10000', + 'country' => 'US', + ]); + $address1->refresh(); + $place = $address1->place()->first(); + $this->assertEquals($place->street, 'street'); + $this->assertEquals($place->city, 'CITY'); + $this->assertEquals($place->province, 'province'); + $this->assertEquals($place->postal_code, '10000'); + $this->assertEquals($place->country, 'US'); + } + + /** @test */ + public function import_vcard_imports_email() + { + $account = factory(Account::class)->create([]); + $importVCard = new ImportVCard; + $importVCard->accountId = $account->id; + + $vcard = new VCard([ + 'EMAIL' => 'john@doe.com', + ]); + + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + $contactFieldType = factory(ContactFieldType::class)->create([ + 'account_id' => $account->id, + 'type' => 'email', + ]); + $this->invokePrivateMethod($importVCard, 'importEmail', [$contact, $vcard]); + + $this->assertDatabaseHas('contact_fields', [ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + 'data' => 'john@doe.com', + ]); + } + + /** @test */ + public function import_vcard_updates_email() + { + $account = factory(Account::class)->create([]); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + $email = factory(ContactField::class)->create([ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + ]); + + $importVCard = new ImportVCard; + $importVCard->accountId = $account->id; + + $vcard = new VCard([ + 'EMAIL' => 'other@doe.com', + ]); + + $this->invokePrivateMethod($importVCard, 'importEmail', [$contact, $vcard]); + + $this->assertDatabaseHas('contact_fields', [ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + 'data' => 'other@doe.com', + ]); + $email->refresh(); + $this->assertEquals($email->data, 'other@doe.com'); + } + + /** @test */ + public function import_vcard_updates_and_detroy_email() + { + $account = factory(Account::class)->create([]); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + $email1 = factory(ContactField::class)->create([ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + ]); + factory(ContactField::class)->create([ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + 'data' => 'xxx@mail.com', + ]); + + $importVCard = new ImportVCard; + $importVCard->accountId = $account->id; + + $vcard = new VCard([ + 'EMAIL' => 'other@doe.com', + ]); + + $this->invokePrivateMethod($importVCard, 'importEmail', [$contact, $vcard]); + + $this->assertDatabaseHas('contact_fields', [ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + 'data' => 'other@doe.com', + ]); + $this->assertDatabaseMissing('contact_fields', [ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + 'data' => 'xxx@mail.com', + ]); + $email1->refresh(); + $this->assertEquals($email1->data, 'other@doe.com'); + } + + /** @test */ + public function it_imports_phone() + { + $account = factory(Account::class)->create([]); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + factory(ContactFieldType::class)->create([ + 'account_id' => $account->id, + 'type' => 'phone', + ]); + + $importVCard = new ImportVCard; + $importVCard->accountId = $account->id; + + $vcard = new VCard([ + 'TEL' => '01010101010', + ]); + + $this->invokePrivateMethod($importVCard, 'importTel', [$contact, $vcard]); + + $this->assertDatabaseHas('contact_fields', [ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + 'data' => '01010101010', + ]); + } + + /** @test */ + public function it_imports_phone_by_national_format() + { + $account = factory(Account::class)->create([]); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + factory(ContactFieldType::class)->create([ + 'account_id' => $account->id, + 'type' => 'phone', + ]); + + $importVCard = new ImportVCard; + $importVCard->accountId = $account->id; + + $vcard = new VCard([ + 'TEL' => '202-555-0191', + 'ADR' => ['', '', '17 Shakespeare Ave.', 'Southampton', '', 'SO17 2HB', 'United Kingdom'], + ]); + + $this->invokePrivateMethod($importVCard, 'importTel', [$contact, $vcard]); + + $this->assertDatabaseHas('contact_fields', [ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + 'data' => '020 2555 0191', + ]); + } + + /** @test */ + public function it_imports_phone_by_international_format() + { + $account = factory(Account::class)->create([]); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + factory(ContactFieldType::class)->create([ + 'account_id' => $account->id, + 'type' => 'phone', + ]); + + $vcard = new VCard([ + 'TEL' => '+44(0)202-555-0191', + 'ADR' => ['', '', '17 Shakespeare Ave.', 'Southampton', '', 'SO17 2HB', 'United Kingdom'], + ]); + + $importVCard = new ImportVCard; + $importVCard->accountId = $account->id; + + $this->invokePrivateMethod($importVCard, 'importTel', [$contact, $vcard]); + + $this->assertDatabaseHas('contact_fields', [ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + 'data' => '+44 20 2555 0191', + ]); + } + + /** @test */ + public function it_imports_email_labels() + { + $account = factory(Account::class)->create([]); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + $email = factory(ContactField::class)->create([ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + ]); + + $importVCard = new ImportVCard; + $importVCard->accountId = $account->id; + + $vcard = new VCard(); + $vcard->add( + 'EMAIL', + 'test@test.com', + [ + 'type' => ['WORK'], + ] + ); + + $this->invokePrivateMethod($importVCard, 'importEmail', [$contact, $vcard]); + + $this->assertDatabaseHas('contact_fields', [ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + 'data' => 'test@test.com', + ]); + $this->assertDatabaseHas('contact_field_labels', [ + 'account_id' => $account->id, + 'label_i18n' => 'work', + ]); + + $contactFieldLabel = ContactFieldLabel::where([ + 'account_id' => $account->id, + 'label_i18n' => 'work', + ])->first(); + $this->assertDatabaseHas('contact_field_contact_field_label', [ + 'account_id' => $account->id, + 'contact_field_id' => $email->id, + 'contact_field_label_id' => $contactFieldLabel->id, + ]); + $email->refresh(); + $this->assertEquals($email->data, 'test@test.com'); + } + + /** @test */ + public function it_imports_address_labels() + { + $account = factory(Account::class)->create([]); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + $email = factory(ContactField::class)->create([ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + ]); + + $importVCard = new ImportVCard; + $importVCard->accountId = $account->id; + + $vcard = new VCard(); + $vcard->add( + 'ADR', + ['', '', '5 Avenue Anatole France', 'Paris', '', '75007', 'France'], + [ + 'type' => ['HOME'], + ] + ); + + $this->invokePrivateMethod($importVCard, 'importAddress', [$contact, $vcard]); + + $this->assertDatabaseHas('addresses', [ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + ]); + $this->assertDatabaseHas('contact_field_labels', [ + 'account_id' => $account->id, + 'label_i18n' => 'home', + ]); + + $address = Address::where([ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + ])->first(); + $contactFieldLabel = ContactFieldLabel::where([ + 'account_id' => $account->id, + 'label_i18n' => 'home', + ])->first(); + $this->assertDatabaseHas('address_contact_field_label', [ + 'account_id' => $account->id, + 'address_id' => $address->id, + 'contact_field_label_id' => $contactFieldLabel->id, + ]); + } + + /** @test */ + public function it_imports_categories() + { + $account = factory(Account::class)->create(); + $importVCard = new ImportVCard; + $importVCard->accountId = $account->id; + + $tag1 = factory(Tag::class)->create([ + 'account_id' => $account->id, + 'name' => 'tag1', + 'name_slug' => Str::slug('tag1'), + ]); + $tag2 = factory(Tag::class)->create([ + 'account_id' => $account->id, + 'name' => 'tag2', + 'name_slug' => Str::slug('tag2'), + ]); + + $vcard = new VCard([ + 'CATEGORIES' => ['tag1', 'tag2'], + ]); + + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + $this->invokePrivateMethod($importVCard, 'importCategories', [$contact, $vcard]); + + $this->assertDatabaseHas('contact_tag', [ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + 'tag_id' => $tag1->id, + ]); + $this->assertDatabaseHas('contact_tag', [ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + 'tag_id' => $tag2->id, + ]); + } + + /** @test */ + public function it_imports_notes() + { + $account = factory(Account::class)->create([]); + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + + $importVCard = new ImportVCard; + $importVCard->accountId = $account->id; + + $vcard = new VCard([ + 'NOTE' => 'a great note about this contact', + ]); + + $this->invokePrivateMethod($importVCard, 'importNote', [$contact, $vcard]); + + $this->assertDatabaseHas('notes', [ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + 'body' => 'a great note about this contact', + ]); + } + + /** @test */ + public function it_imports_new_categories() + { + $account = factory(Account::class)->create(); + $importVCard = new ImportVCard; + $importVCard->accountId = $account->id; + + $tag1 = factory(Tag::class)->create([ + 'account_id' => $account->id, + 'name' => 'tag1', + 'name_slug' => Str::slug('tag1'), + ]); + $tag2 = factory(Tag::class)->create([ + 'account_id' => $account->id, + 'name' => 'tag2', + 'name_slug' => Str::slug('tag2'), + ]); + $tag3 = factory(Tag::class)->create([ + 'account_id' => $account->id, + 'name' => 'tag3', + 'name_slug' => Str::slug('tag3'), + ]); + + $vcard = new VCard([ + 'CATEGORIES' => ['tag2', 'tag3'], + ]); + + $contact = factory(Contact::class)->create([ + 'account_id' => $account->id, + ]); + $contact->tags()->sync([ + $tag1->id => ['account_id' => $contact->account_id], + $tag2->id => ['account_id' => $contact->account_id], + ]); + $this->assertDatabaseHas('contact_tag', [ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + 'tag_id' => $tag1->id, + ]); + $this->assertDatabaseHas('contact_tag', [ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + 'tag_id' => $tag2->id, + ]); + + $this->invokePrivateMethod($importVCard, 'importCategories', [$contact, $vcard]); + + $this->assertDatabaseMissing('contact_tag', [ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + 'tag_id' => $tag1->id, + ]); + $this->assertDatabaseHas('contact_tag', [ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + 'tag_id' => $tag2->id, + ]); + $this->assertDatabaseHas('contact_tag', [ + 'account_id' => $account->id, + 'contact_id' => $contact->id, + 'tag_id' => $tag3->id, + ]); + } + + /** @test */ + public function it_imports_uuid_default() + { + $account = factory(Account::class)->create(); + $importVCard = new ImportVCard; + $importVCard->accountId = $account->id; + + $vcard = new VCard([ + 'UID' => '31fdc242-c974-436e-98de-6b21624d6e34', + ]); + + $contact = []; + + $contact = $this->invokePrivateMethod($importVCard, 'importUid', [$contact, $vcard]); + + $this->assertEquals('31fdc242-c974-436e-98de-6b21624d6e34', $contact['uuid']); + } + + /** @test */ + public function it_imports_uuid_contact() + { + $user = factory(User::class)->create([]); + $importVCard = new ImportVCard; + $importVCard->accountId = $user->account_id; + $importVCard->userId = $user->id; + + $vcard = new VCard([ + 'FN' => 'John Doe', + 'UID' => '31fdc242-c974-436e-98de-6b21624d6e34', + ]); + + $contact = $this->invokePrivateMethod($importVCard, 'importEntry', [null, $vcard, $vcard->serialize(), null]); + + $this->assertDatabaseHas('contacts', [ + 'account_id' => $user->account_id, + 'id' => $contact->id, + 'uuid' => '31fdc242-c974-436e-98de-6b21624d6e34', + ]); + $this->assertEquals('31fdc242-c974-436e-98de-6b21624d6e34', $contact->uuid); + } +} diff --git a/tests/Unit/Traits/SearchableTest.php b/tests/Unit/Traits/SearchableTest.php new file mode 100644 index 0000000..7ee134a --- /dev/null +++ b/tests/Unit/Traits/SearchableTest.php @@ -0,0 +1,65 @@ +make(); + $searchResults = $contact->search($contact->first_name, $contact->account_id, 'created_at', 'desc') + ->paginate(10); + + $this->assertInstanceOf('Illuminate\Pagination\LengthAwarePaginator', $searchResults); + } + + /** @test */ + public function testSearchContactsThroughFirstNameAndResultContainsContact() + { + $contact = factory(Contact::class)->create(['first_name' => 'FirstName']); + $searchResults = $contact->search($contact->first_name, $contact->account_id, 'created_at', 'desc') + ->paginate(10); + + $this->assertTrue($searchResults->contains($contact)); + } + + /** @test */ + public function testSearchContactsThroughMiddleNameAndResultContainsContact() + { + $contact = factory(Contact::class)->create(['middle_name' => 'MiddleName']); + $searchResults = $contact->search($contact->middle_name, $contact->account_id, 'created_at', 'desc') + ->paginate(10); + + $this->assertTrue($searchResults->contains($contact)); + } + + /** @test */ + public function testSearchContactsThroughLastNameAndResultContainsContact() + { + $contact = factory(Contact::class)->create(['last_name' => 'LastName']); + $searchResults = $contact->search($contact->last_name, $contact->account_id, 'created_at', 'desc') + ->paginate(10); + + $this->assertTrue($searchResults->contains($contact)); + } + + /** + * @test + * @psalm-suppress UndefinedFunction + */ + public function testFailingSearchContacts() + { + $contact = factory(Contact::class)->create(['first_name' => 'TestShouldFail']); + $searchResults = $contact->search('TestWillSucceed', $contact->account_id, 'created_at', 'desc') + ->paginate(10); + + $this->assertFalse($searchResults->contains($contact)); + } +} diff --git a/tests/cypress/fixtures/example.json b/tests/cypress/fixtures/example.json new file mode 100644 index 0000000..da18d93 --- /dev/null +++ b/tests/cypress/fixtures/example.json @@ -0,0 +1,5 @@ +{ + "name": "Using fixtures to represent data", + "email": "hello@cypress.io", + "body": "Fixtures are a great way to mock data for responses to routes" +} \ No newline at end of file diff --git a/tests/cypress/fixtures/profile.json b/tests/cypress/fixtures/profile.json new file mode 100644 index 0000000..51a1a4f --- /dev/null +++ b/tests/cypress/fixtures/profile.json @@ -0,0 +1,5 @@ +{ + "firstname": "Jane", + "lastname": "Florentina", + "email": "jane@example.com" +} \ No newline at end of file diff --git a/tests/cypress/fixtures/users.json b/tests/cypress/fixtures/users.json new file mode 100644 index 0000000..79b699a --- /dev/null +++ b/tests/cypress/fixtures/users.json @@ -0,0 +1,232 @@ +[ + { + "id": 1, + "name": "Leanne Graham", + "username": "Bret", + "email": "Sincere@april.biz", + "address": { + "street": "Kulas Light", + "suite": "Apt. 556", + "city": "Gwenborough", + "zipcode": "92998-3874", + "geo": { + "lat": "-37.3159", + "lng": "81.1496" + } + }, + "phone": "1-770-736-8031 x56442", + "website": "hildegard.org", + "company": { + "name": "Romaguera-Crona", + "catchPhrase": "Multi-layered client-server neural-net", + "bs": "harness real-time e-markets" + } + }, + { + "id": 2, + "name": "Ervin Howell", + "username": "Antonette", + "email": "Shanna@melissa.tv", + "address": { + "street": "Victor Plains", + "suite": "Suite 879", + "city": "Wisokyburgh", + "zipcode": "90566-7771", + "geo": { + "lat": "-43.9509", + "lng": "-34.4618" + } + }, + "phone": "010-692-6593 x09125", + "website": "anastasia.net", + "company": { + "name": "Deckow-Crist", + "catchPhrase": "Proactive didactic contingency", + "bs": "synergize scalable supply-chains" + } + }, + { + "id": 3, + "name": "Clementine Bauch", + "username": "Samantha", + "email": "Nathan@yesenia.net", + "address": { + "street": "Douglas Extension", + "suite": "Suite 847", + "city": "McKenziehaven", + "zipcode": "59590-4157", + "geo": { + "lat": "-68.6102", + "lng": "-47.0653" + } + }, + "phone": "1-463-123-4447", + "website": "ramiro.info", + "company": { + "name": "Romaguera-Jacobson", + "catchPhrase": "Face to face bifurcated interface", + "bs": "e-enable strategic applications" + } + }, + { + "id": 4, + "name": "Patricia Lebsack", + "username": "Karianne", + "email": "Julianne.OConner@kory.org", + "address": { + "street": "Hoeger Mall", + "suite": "Apt. 692", + "city": "South Elvis", + "zipcode": "53919-4257", + "geo": { + "lat": "29.4572", + "lng": "-164.2990" + } + }, + "phone": "493-170-9623 x156", + "website": "kale.biz", + "company": { + "name": "Robel-Corkery", + "catchPhrase": "Multi-tiered zero tolerance productivity", + "bs": "transition cutting-edge web services" + } + }, + { + "id": 5, + "name": "Chelsey Dietrich", + "username": "Kamren", + "email": "Lucio_Hettinger@annie.ca", + "address": { + "street": "Skiles Walks", + "suite": "Suite 351", + "city": "Roscoeview", + "zipcode": "33263", + "geo": { + "lat": "-31.8129", + "lng": "62.5342" + } + }, + "phone": "(254)954-1289", + "website": "demarco.info", + "company": { + "name": "Keebler LLC", + "catchPhrase": "User-centric fault-tolerant solution", + "bs": "revolutionize end-to-end systems" + } + }, + { + "id": 6, + "name": "Mrs. Dennis Schulist", + "username": "Leopoldo_Corkery", + "email": "Karley_Dach@jasper.info", + "address": { + "street": "Norberto Crossing", + "suite": "Apt. 950", + "city": "South Christy", + "zipcode": "23505-1337", + "geo": { + "lat": "-71.4197", + "lng": "71.7478" + } + }, + "phone": "1-477-935-8478 x6430", + "website": "ola.org", + "company": { + "name": "Considine-Lockman", + "catchPhrase": "Synchronised bottom-line interface", + "bs": "e-enable innovative applications" + } + }, + { + "id": 7, + "name": "Kurtis Weissnat", + "username": "Elwyn.Skiles", + "email": "Telly.Hoeger@billy.biz", + "address": { + "street": "Rex Trail", + "suite": "Suite 280", + "city": "Howemouth", + "zipcode": "58804-1099", + "geo": { + "lat": "24.8918", + "lng": "21.8984" + } + }, + "phone": "210.067.6132", + "website": "elvis.io", + "company": { + "name": "Johns Group", + "catchPhrase": "Configurable multimedia task-force", + "bs": "generate enterprise e-tailers" + } + }, + { + "id": 8, + "name": "Nicholas Runolfsdottir V", + "username": "Maxime_Nienow", + "email": "Sherwood@rosamond.me", + "address": { + "street": "Ellsworth Summit", + "suite": "Suite 729", + "city": "Aliyaview", + "zipcode": "45169", + "geo": { + "lat": "-14.3990", + "lng": "-120.7677" + } + }, + "phone": "586.493.6943 x140", + "website": "jacynthe.com", + "company": { + "name": "Abernathy Group", + "catchPhrase": "Implemented secondary concept", + "bs": "e-enable extensible e-tailers" + } + }, + { + "id": 9, + "name": "Glenna Reichert", + "username": "Delphine", + "email": "Chaim_McDermott@dana.io", + "address": { + "street": "Dayna Park", + "suite": "Suite 449", + "city": "Bartholomebury", + "zipcode": "76495-3109", + "geo": { + "lat": "24.6463", + "lng": "-168.8889" + } + }, + "phone": "(775)976-6794 x41206", + "website": "conrad.com", + "company": { + "name": "Yost and Sons", + "catchPhrase": "Switchable contextually-based project", + "bs": "aggregate real-time technologies" + } + }, + { + "id": 10, + "name": "Clementina DuBuque", + "username": "Moriah.Stanton", + "email": "Rey.Padberg@karina.biz", + "address": { + "street": "Kattie Turnpike", + "suite": "Suite 198", + "city": "Lebsackbury", + "zipcode": "31428-2261", + "geo": { + "lat": "-38.2386", + "lng": "57.2232" + } + }, + "phone": "024-648-3804", + "website": "ambrose.net", + "company": { + "name": "Hoeger LLC", + "catchPhrase": "Centralized empowering task-force", + "bs": "target end-to-end models" + } + } +] \ No newline at end of file diff --git a/tests/cypress/integration/auth/login_spec.js b/tests/cypress/integration/auth/login_spec.js new file mode 100644 index 0000000..82d8a92 --- /dev/null +++ b/tests/cypress/integration/auth/login_spec.js @@ -0,0 +1,11 @@ +describe('Login', function () { + it('should not let user sign in with a non-existing account', function () { + cy.visit('/'); + + cy.get('input[name=email]').type('impossibru@test.com'); + cy.get('input[name=password]').type('testtest'); + cy.get('button[type=submit]').click(); + + cy.get('.alert').should('exist'); + }); +}); diff --git a/tests/cypress/integration/auth/signup_spec.js b/tests/cypress/integration/auth/signup_spec.js new file mode 100644 index 0000000..d0df0d4 --- /dev/null +++ b/tests/cypress/integration/auth/signup_spec.js @@ -0,0 +1,43 @@ +var faker = require('faker'); + +describe('Signup', function () { + // @TODO: get emails from Mailtrap with their API and click on the confirmation + // link + // + // it('should sign up and logout', function () { + // cy.visit('/register') + + // cy.get('input[name=email]').type('test@test.com') + // cy.get('input[name=first_name]').type('test') + // cy.get('input[name=last_name]').type('test') + // cy.get('input[name=password]').type('testtest') + // cy.get('input[name=password_confirmation]').type('testtest') + + // cy.get('input[name=policy]').click() + // cy.get('button[type=submit]').click() + + // cy.url().should('include', '/dashboard') + + // cy.get('[data-cy=header-link-logout]').click() + + // cy.contains('Login to your account') + // }) + + //it('should block registration if policy is not accepted', function () { + // cy.register(faker.name.firstName(), faker.name.lastName(), faker.internet.password(), faker.internet.email(), false); + // cy.get('.alert').should('exist'); + //}); + + it('should block registration if email is already used', function () { + const email = faker.internet.email(); + + // test email address + cy.register(faker.name.firstName(), faker.name.lastName(), faker.internet.password(), email, true); + cy.get('.alert').should('not.exist'); + + cy.get('[data-cy=header-link-logout]').click(); + + cy.register(faker.name.firstName(), faker.name.lastName(), faker.internet.password(), email, true); + cy.get('.alert').should('exist'); + }); +}); diff --git a/tests/cypress/integration/contacts/activities_spec.js b/tests/cypress/integration/contacts/activities_spec.js new file mode 100644 index 0000000..6fe3f17 --- /dev/null +++ b/tests/cypress/integration/contacts/activities_spec.js @@ -0,0 +1,35 @@ +describe('Activities', function () { + beforeEach(function () { + cy.login(); + cy.createContact('John', 'Doe', 'Man'); + }); + + it('lets you manage an activity', function () { + cy.url().should('include', '/people/h:'); + cy.get('[cy-name=activities-blank-state]').should('be.visible'); + + // add an activity + cy.createActivity(); + + // edit an activity + cy.get('[cy-name=activities-body]').should('be.visible') + .invoke('attr', 'cy-items').then(function (item) { + + cy.get('[cy-name=edit-activity-button-'+item+']').click(); + + cy.get('[name=summary]').clear(); + cy.get('[name=summary]').type('This is another summary'); + cy.get('[cy-name=save-activity-button]').click(); + + cy.get('[cy-name=activity-body-'+item+']').should('exist'); + cy.get('[cy-name=activity-body-'+item+']').should('contain', 'This is another summary'); + + // delete an activity + cy.get('[cy-name=delete-activity-button-'+item+']').click(); + cy.wait(10); + cy.get('[cy-name=confirm-delete-activity]').should('be.visible').click(); + cy.get('[cy-name=activities-blank-state]').should('be.visible'); + cy.get('[cy-name=activity-body-'+item+']').should('not.exist'); + }); + }); +}); diff --git a/tests/cypress/integration/contacts/calls_spec.js b/tests/cypress/integration/contacts/calls_spec.js new file mode 100644 index 0000000..fe0ed48 --- /dev/null +++ b/tests/cypress/integration/contacts/calls_spec.js @@ -0,0 +1,38 @@ +var _ = require('lodash'); + +describe('Calls', function () { + beforeEach(function () { + cy.login(); + cy.createContact('John', 'Doe', 'Man'); + }); + + it('lets you manage a call', function () { + cy.url().should('include', '/people/h:'); + cy.get('[cy-name=calls-blank-state]').should('exist'); + cy.get('[cy-name=log-call-form]').should('not.be.visible'); + cy.get('[cy-name=last-talked-to]').should('contain', 'unknown'); + + // add a call + cy.get('[cy-name=add-call-button]').click(); + cy.get('[cy-name=log-call-form]').should('be.visible'); + cy.get('[cy-name=save-call-button]').click(); + cy.get('[cy-name=calls-blank-state]').should('not.exist'); + cy.get('[cy-name=last-talked-to]').should('not.contain', 'unknown'); + + cy.get('[cy-name=calls-body]').should('be.visible') + .invoke('attr', 'cy-items').then(function (items) { + let item = _.last(items.split(',')); + + cy.get('[cy-name=call-body-'+item+']').should('exist'); + cy.get('[cy-name=call-body-'+item+']').should('contain', 'John'); + + // delete a call + cy.get('[cy-name=delete-call-button-'+item+']').should('be.visible'); + cy.get('[cy-name=delete-call-button-'+item+']').click(); + cy.get('[cy-name=delete-call-confirm-button-'+item+']').should('be.visible'); + cy.get('[cy-name=delete-call-confirm-button-'+item+']').click(); + cy.get('[cy-name=calls-blank-state]').should('exist'); + cy.get('[cy-name=last-talked-to]').should('contain', 'unknown'); + }); + }); +}); diff --git a/tests/cypress/integration/contacts/contacts_spec.js b/tests/cypress/integration/contacts/contacts_spec.js new file mode 100644 index 0000000..a70a24f --- /dev/null +++ b/tests/cypress/integration/contacts/contacts_spec.js @@ -0,0 +1,95 @@ +describe('Contacts', function () { + beforeEach(function () { + cy.login(); + }); + + it('lets you add a contact', function () { + cy.createContact('John', 'Doe', 'Man'); + cy.url().should('include', '/people/h:'); + cy.get('h1').should('contain', 'John Doe'); + }); + + it('lets you add two contacts in a row', function () { + cy.createContact('John', 'Doe', 'Man', 'save_and_add_another'); + cy.url().should('include', '/people/add'); + }); + + it('requires at least a firstname to add a contact', function () { + cy.visit('/people'); + cy.get('#button-add-contact').click(); + cy.get('button[name=save]').click(); + cy.url().should('include', '/people/add'); + + cy.get('input[name=first_name]').type('John'); + cy.get('select[name=gender]').select('Man'); + cy.get('button[name=save]').click(); + + cy.url().should('include', '/people/h:'); + cy.get('h1').should('contain', 'John'); + }); + + it('lets you edit a contact', function () { + cy.createContact('John', 'Doe', 'Man'); + + cy.get('#button-edit-contact').click(); + cy.url().should('include', '/edit'); + + cy.get('input[name=firstname]').should('have.value', 'John'); + cy.get('input[name=lastname]').should('have.value', 'Doe'); + //TO FIX cy.get('select[name=gender]').should('have.text', 'Man') + + cy.get('input[name=firstname]').clear(); + cy.get('input[name=firstname]').type('Jane'); + cy.get('input[name=lastname]').clear(); + cy.get('button[name=save]').click(); + + cy.url().should('include', '/people/h:'); + cy.get('h1').should('contain', 'Jane'); + }); + + it('lets you delete a contact', function () { + cy.createContact('John', 'Doe', 'Man'); + + cy.visit('/people'); + cy.get('.people-list-item').should('contain', 'John Doe'); + + // this gets the first content of the list + cy.get('tr.people-list-item.bg-white.pointer').click(); + + cy.get('#link-delete-contact').click(); + + // cypress auto accepts window alerts (confirm or alert) + cy.url().should('include', '/people'); + + cy.visit('/people'); + cy.get('.people-list-item').should('not.contain', 'John'); + }); + + it('lets you add a contact as favorite', function () { + cy.createContact('John', 'Doe', 'Man'); + + cy.visit('/people'); + + // this gets the first content of the list + cy.get('tr.people-list-item.bg-white.pointer').click(); + + // tests if the favorite button can be toggled + cy.get('[cy-name=set-favorite]').should('be.visible'); + cy.get('[cy-name=set-favorite]').click(); + cy.get('[cy-name=set-favorite]').should('not.be.visible'); + cy.get('[cy-name=unset-favorite]').should('be.visible'); + cy.get('[cy-name=unset-favorite]').click(); + cy.get('[cy-name=set-favorite]').should('be.visible'); + + // test to see if a contact appears on top of the contact list if favorited + cy.get('[cy-name=set-favorite]').click(); + cy.visit('/dashboard'); + cy.createContact('Abc', 'Abc', 'Man'); + cy.visit('/people'); + + cy.get('.people-list-item span').should('contain', 'John Doe'); + cy.get('.people-list-item svg').should('be.visible'); + + cy.get('.people-list-item').should('contain', 'Abc Abc'); + }); +}); diff --git a/tests/cypress/integration/contacts/conversations_spec.js b/tests/cypress/integration/contacts/conversations_spec.js new file mode 100644 index 0000000..b568111 --- /dev/null +++ b/tests/cypress/integration/contacts/conversations_spec.js @@ -0,0 +1,27 @@ +describe('Conversations', function () { + beforeEach(function () { + cy.login(); + cy.createContact('John', 'Doe', 'Man'); + }); + + it('lets you manage a conversation', function () { + cy.get('[cy-name=conversation-blank-state]').should('be.visible'); + + // add a conversation + cy.visit('/people'); + + // this gets the first content of the list + cy.get('tr.people-list-item.bg-white.pointer').click(); + + cy.get('[cy-name=add-conversation-button]').should('be.visible'); + cy.get('[cy-name=add-conversation-button]').click(); + cy.url().should('include', '/conversations/create'); + + cy.get('[name=contactFieldTypeId]').select('Phone'); + cy.get('[name=content_1]').type('This is a message'); + cy.get('[cy-name=save-conversation-button]').click(); + + cy.url().should('include', '/people/h:'); + cy.get('[cy-name=conversation-blank-state]').should('not.be.visible'); + }); +}); diff --git a/tests/cypress/integration/contacts/debts_spec.js b/tests/cypress/integration/contacts/debts_spec.js new file mode 100644 index 0000000..7b3fc7a --- /dev/null +++ b/tests/cypress/integration/contacts/debts_spec.js @@ -0,0 +1,46 @@ +describe('Debts', function () { + beforeEach(function () { + cy.login(); + cy.createContact('John', 'Doe', 'Man'); + }); + + it('lets you manage a debt', function () { + cy.url().should('include', '/people/h:'); + cy.get('[cy-name=debt-blank-state]').should('be.visible'); + + // add a debt + cy.get('[cy-name=add-debt-button]').should('be.visible'); + cy.get('[cy-name=add-debt-button]').click(); + cy.url().should('include', '/debts/create'); + + cy.get('[name=amount]').type('123'); + cy.get('[cy-name=save-debt-button]').click(); + + cy.url().should('include', '/people/h:'); + cy.get('[cy-name=debt-blank-state]').should('not.be.visible'); + + cy.get('[cy-name=debts-body]').should('be.visible') + .invoke('attr', 'cy-items').then(function (item) { + + cy.get('[cy-name=debt-item-'+item+']').should('exist'); + cy.get('[cy-name=debt-item-'+item+']').should('contain', '123'); + + // edit a debt + cy.get('[cy-name=edit-debt-button-'+item+']').click(); + cy.url().should('include', '/edit'); + + cy.get('[name=amount]').clear(); + cy.get('[name=amount]').type('234'); + cy.get('[cy-name=save-debt-button]').click(); + + cy.get('[cy-name=debt-item-'+item+']').should('exist'); + cy.get('[cy-name=debt-item-'+item+']').should('contain', '234'); + + // delete a debt + cy.get('[cy-name=delete-debt-button-'+item+']').click(); + cy.get('[cy-name=confirm-delete-debt]').should('be.visible').click(); + cy.get('[cy-name=debt-blank-state]').should('be.visible'); + cy.get('[cy-name=debt-item-'+item+']').should('not.exist'); + }); + }); +}); diff --git a/tests/cypress/integration/contacts/gifts_spec.js b/tests/cypress/integration/contacts/gifts_spec.js new file mode 100644 index 0000000..3a71ebc --- /dev/null +++ b/tests/cypress/integration/contacts/gifts_spec.js @@ -0,0 +1,50 @@ +/* + +Gift page has change recently a lot, let rewrite this test later ... + +### +describe('Gifts', function () { + beforeEach(function () { + cy.login(); + cy.createContact('John', 'Doe', 'Man'); + }); + + it('lets you manage a gift', function () { + cy.url().should('include', '/people/h:'); + + // add a gift + cy.get('[cy-name=add-gift-button]').should('be.visible'); + cy.get('[cy-name=add-gift-button]').click(); + cy.url().should('include', '/gifts/create'); + + cy.get('[name=name]').type('This is a gift'); + cy.get('[cy-name=save-gift-button]').click(); + + cy.url().should('include', '/people/h:'); + + cy.get('[cy-name=gift-ideas-body]').should('be.visible') + .invoke('attr', 'cy-items').then(function (item) { + + cy.get('[cy-name=gift-idea-item-'+item+']').should('exist'); + cy.get('[cy-name=gift-idea-item-'+item+']').should('contain', 'This is a gift'); + + // edit a gift + cy.get('[cy-name=edit-gift-button-'+item+']').click(); + cy.url().should('include', '/gifts/'+item+'/edit'); + + cy.get('[name=name]').clear(); + cy.get('[name=name]').type('This is another gift'); + cy.get('[cy-name=save-gift-button]').click(); + + cy.get('[cy-name=gift-idea-item-'+item+']').should('exist'); + cy.get('[cy-name=gift-idea-item-'+item+']').should('contain', 'This is another gift'); + + // delete an gift + cy.get('[cy-name=delete-gift-button-'+item+']').click(); + cy.get('[cy-name=modal-delete-gift-button-'+item+']').click(); + cy.get('[cy-name=activities-blank-state]').should('be.visible'); + cy.get('[cy-name=gift-idea-item-'+item+']').should('not.exist'); + }); + }); +}); +*/ diff --git a/tests/cypress/integration/contacts/introductions_spec.js b/tests/cypress/integration/contacts/introductions_spec.js new file mode 100644 index 0000000..bc27f47 --- /dev/null +++ b/tests/cypress/integration/contacts/introductions_spec.js @@ -0,0 +1,65 @@ +describe('Introduction', function () { + beforeEach(function () { + cy.login(); + cy.createContact('John', 'Doe', 'Man'); + cy.createContact('Jane', 'Doe', 'Woman'); + cy.createContact('Joe', 'Shmoe', 'Man'); + }); + + it('lets you fill first met without an introducer', function () { + cy.url().should('include', '/people/h:'); + + cy.get('.introductions a[href$="introductions/edit"]').click(); + cy.url().should('include', '/introductions/edit'); + + cy.get('textarea[name=first_met_additional_info]').type('Lorem ipsum'); + cy.get('button.btn-primary[type=submit]').click(); + + cy.url().should('include', '/people/h:'); + cy.get('.alert-success'); + cy.get('.introductions').contains('Lorem ipsum'); + }); + + it('lets you save first met', function () { + cy.url().should('include', '/people/h:'); + + cy.get('.introductions a[href$="introductions/edit"]').click(); + cy.url().should('include', '/introductions/edit'); + + cy.get('textarea[name=first_met_additional_info]').type('Lorem ipsum'); + cy.get('#metThrough > .v-select input').click(); + cy.get('#metThrough ul[role="listbox"]').contains('John Doe'); + cy.get('#metThrough ul[role="listbox"]').contains('Jane Doe'); + cy.get('#metThrough ul[role="listbox"]').contains('Joe Shmoe'); + + cy.get('#metThrough ul[role="listbox"]').contains('John Doe').click(); + + cy.get('button.btn-primary[type=submit]').click(); + + cy.url().should('include', '/people/h:'); + cy.get('.alert-success'); + cy.get('.introductions').contains('Lorem ipsum'); + cy.get('.introductions').contains('John Doe'); + }); + + it('lets you search first met', function () { + cy.url().should('include', '/people/h:'); + + cy.get('.introductions a[href$="introductions/edit"]').click(); + cy.url().should('include', '/introductions/edit'); + + cy.get('textarea[name=first_met_additional_info]').type('Lorem ipsum'); + cy.get('#metThrough input[type=search]').type('John'); + cy.get('#metThrough ul[role="listbox"]').contains('John Doe'); + cy.get('#metThrough ul[role="listbox"]').should('not.contain', 'Joe Shmoe'); + cy.get('#metThrough ul[role="listbox"]').should('not.contain', 'Jane Doe'); + + cy.get('#metThrough ul[role="listbox"]').contains('John Doe').click(); + cy.get('button.btn-primary[type=submit]').click(); + + cy.url().should('include', '/people/h:'); + cy.get('.introductions').contains('Lorem ipsum'); + cy.get('.introductions').contains('John Doe'); + }); + +}); diff --git a/tests/cypress/integration/contacts/notes_spec.js b/tests/cypress/integration/contacts/notes_spec.js new file mode 100644 index 0000000..6c7ae00 --- /dev/null +++ b/tests/cypress/integration/contacts/notes_spec.js @@ -0,0 +1,45 @@ +describe('Notes', function () { + beforeEach(function () { + cy.login(); + cy.createContact('John', 'Doe', 'Man'); + }); + + it('lets you manage a note', function () { + cy.url().should('include', '/people/h:'); + cy.get('[cy-name=add-note-button]').should('not.be.visible'); + + // add a note + cy.get('[cy-name=add-note-textarea]').click(); + cy.get('[cy-name=add-note-button]').should('be.visible'); + + cy.get('[cy-name=add-note-textarea]').type('This is a note'); + cy.get('[cy-name=add-note-button]').click(); + + cy.get('[cy-name=notes-body]').should('be.visible') + .invoke('attr', 'cy-items').then(function (item) { + + cy.get('[cy-name=note-body-'+item+']').should('contain', 'This is a note'); + + cy.get('[cy-name=edit-note-body-'+item+']').should('not.be.visible'); + + // edit a note + cy.get('[cy-name=edit-note-button-'+item+']').click(); + cy.get('[cy-name=edit-note-body-'+item+']').should('be.visible'); + + cy.get('[cy-name=edit-note-body-'+item+']').clear(); + cy.get('[cy-name=edit-note-body-'+item+']').type('This is another note'); + cy.get('[cy-name=edit-mode-note-button-'+item+']').click(); + + cy.get('[cy-name=edit-note-body-'+item+']').should('not.be.visible'); + cy.get('[cy-name=note-body-'+item+']').should('contain', 'This is another note'); + + // delete a note + cy.get('[cy-name=modal-delete-note]').should('not.be.visible'); + cy.get('[cy-name=delete-note-button-'+item+']').click(); + cy.get('[cy-name=modal-delete-note]').should('be.visible'); + cy.get('[cy-name=delete-mode-note-button-'+item+']').click(); + + cy.get('[cy-name=note-body-'+item+']').should('not.exist'); + }); + }); +}); diff --git a/tests/cypress/integration/contacts/tasks_spec.js b/tests/cypress/integration/contacts/tasks_spec.js new file mode 100644 index 0000000..b2f85aa --- /dev/null +++ b/tests/cypress/integration/contacts/tasks_spec.js @@ -0,0 +1,31 @@ +describe('Tasks', function () { + beforeEach(function () { + cy.login(); + cy.createContact('John', 'Doe', 'Man'); + }); + + it('lets you manage a task', function () { + cy.url().should('include', '/people/h:'); + cy.get('[cy-name=task-blank-state]').should('be.visible'); + + // add a task + cy.get('[cy-name=add-task-button]').click(); + cy.get('[cy-name=task-add-view]').should('be.visible'); + + cy.get('[cy-name=task-add-title]').type('This is a task'); + cy.get('[cy-name=save-task-button]').click(); + + cy.get('[cy-name=tasks-body]').should('be.visible') + .invoke('attr', 'cy-items').then(function (item) { + + cy.get('[cy-name=task-item-'+item+']').should('exist'); + cy.get('[cy-name=task-item-'+item+']').should('contain', 'This is a task'); + + // edit a task + cy.get('[cy-name=task-toggle-edit-mode]').click(); + cy.get('[cy-name=task-delete-button-'+item+']').click(); + + cy.get('[cy-name=task-blank-state]').should('be.visible'); + }); + }); +}); diff --git a/tests/cypress/integration/journal/entries_spec.js b/tests/cypress/integration/journal/entries_spec.js new file mode 100644 index 0000000..97e3e8e --- /dev/null +++ b/tests/cypress/integration/journal/entries_spec.js @@ -0,0 +1,73 @@ +describe('Journal entries', function () { + beforeEach(function () { + cy.login(); + cy.createContact('John', 'Doe', 'Man'); + }); + + it('lets you manage a journal entry', function () { + cy.visit('/journal'); + cy.get('[cy-name=journal-blank-state]').should('be.visible'); + + // add a journal entry + cy.get('[cy-name=add-entry-button]').should('be.visible'); + cy.get('[cy-name=add-entry-button]').click(); + cy.url().should('include', '/journal/add'); + + cy.get('[name=entry]').type('This is an entry'); + cy.get('[cy-name=save-entry-button]').click(); + + cy.url().should('include', '/journal'); + cy.get('[cy-name=journal-blank-state]').should('not.be.visible'); + + cy.get('[cy-name=journal-entries-body]').should('be.visible') + .invoke('attr', 'cy-items').then(function (item) { + cy.get('[cy-name=journal-entries-body]') + .invoke('attr', 'cy-object-items').then(function (objItem) { + + cy.get('[cy-name=entry-body-'+item+']').should('exist'); + cy.get('[cy-name=entry-body-'+item+']').should('contain', 'This is an entry'); + + // delete a journal entry + cy.get('[cy-name=entry-delete-button-'+objItem+']').click(); + cy.url().should('include', '/journal'); + cy.get('[cy-name=entry-body-'+item+']').should('not.exist'); + }); + }); + }); + + it('creates a journal entry when creating an activity', function () { + cy.createActivity(); + + cy.visit('/journal'); + + cy.get('[cy-name=journal-blank-state]').should('not.be.visible'); + + cy.get('[cy-name=journal-entries-body]').should('be.visible').then((entries) => { + let item = entries[0].getAttribute('cy-items'); + let objItem = entries[0].getAttribute('cy-object-items'); + + cy.get('[cy-name=entry-body-'+item+']').should('exist'); + cy.get('[cy-name=entry-body-'+item+']').should('contain', 'This is a summary'); + cy.get('[cy-name=entry-delete-button-'+objItem+']').should('not.exist'); + }); + }); + + it('lets you rate your day', function () { + cy.visit('/journal'); + + cy.get('[cy-name=journal-blank-state]').should('be.visible'); + + cy.get('[cy-name=sad-reaction-button]').click(); + cy.wait(10); + + cy.get('[cy-name=comment]').should('be.visible'); + cy.get('[cy-name=save-entry-button]').click(); + + cy.get('[cy-name=journal-entries-body]').should('be.visible').then((entries) => { + let item = entries[0].getAttribute('cy-items'); + + cy.get('[cy-name=entry-body-'+item+']').should('exist'); + cy.get('[cy-name=entry-delete-button-'+item+']').should('exist'); + }); + }); +}); diff --git a/tests/cypress/integration/settings/activity_types_spec.js b/tests/cypress/integration/settings/activity_types_spec.js new file mode 100644 index 0000000..4fa4038 --- /dev/null +++ b/tests/cypress/integration/settings/activity_types_spec.js @@ -0,0 +1,134 @@ +var _ = require('lodash'); + +describe('Settings: activity types', function () { + it('doesn\'t let you manage activity types if user is not premium', function () { + cy.login(); + cy.visit('/settings/personalization'); + cy.get('[cy-name=activity-type-premium-message]').should('be.visible'); + cy.get('[cy-name=activity-type-edit-button]').should('not.exist'); + }); + + it('lets you manage an activity type category and activity type', function () { + cy.login(); + cy.visit('/'); + + // set account as premium + cy.get('body').invoke('attr', 'data-account-id').then(function ($accountId) { + cy.setPremium($accountId); + }); + + // make sure that going premium removes restrictions + cy.visit('/settings/personalization'); + cy.get('[cy-name=activity-type-premium-message]').should('not.be.visible'); + cy.get('[cy-name=activity-types]').should('contain', 'played a sport together'); + + // add an activity type category + cy.get('[cy-name=add-activity-type-category-button]').click(); + cy.get('.sweet-modal-overlay').should('be.visible'); + cy.get('[name=add-category-name]').type('This is an activity type category'); + cy.get('[cy-name=add-activity-type-category-save-button]').click(); + cy.wait(10); + + cy.get('[cy-name=activity-types]').should('contain', 'This is an activity type category'); + cy.get('[cy-name=activity-type-categories]').invoke('attr', 'cy-items').then((items) => { + let item = _.last(items.split(',')); + + // edit an activity type category + cy.get('[cy-name=activity-type-category-edit-button-'+item+']').click(); + cy.get('.sweet-modal-overlay').should('be.visible'); + cy.get('[name=update-category-name]').clear(); + cy.get('[name=update-category-name]').type('This is still an activity type category'); + cy.get('[cy-name=update-activity-type-category-button]').click(); + cy.get('[cy-name=activity-types]').should('contain', 'This is still an activity type category'); + + // add an activity type + cy.get('[cy-name=add-activity-type-button-for-category-'+item+']').click(); + cy.get('.sweet-modal-overlay').should('be.visible'); + cy.get('[name=add-type-name]').type('This is activity type 1'); + cy.get('[cy-name=add-type-button]').click(); + cy.get('[cy-name=activity-types]').should('contain', 'This is activity type 1'); + + // edit an activity type + cy.get('[cy-name=activity-types-'+item+']').invoke('attr', 'cy-items').then((items) => { + let aitem = _.last(items.split(',')); + + cy.get('[cy-name=activity-type-edit-button-'+aitem+']').click(); + cy.get('.sweet-modal-overlay').should('be.visible'); + cy.get('[name=update-type-name]').clear(); + cy.get('[name=update-type-name]').type('This is modified activity type 1'); + cy.get('[cy-name=update-type-button]').click(); + cy.wait(10); + cy.get('[cy-name=activity-types]').should('contain', 'This is modified activity type 1'); + }); + + // delete an activity type + cy.get('[cy-name=add-activity-type-button-for-category-'+item+']').click(); + cy.get('.sweet-modal-overlay').should('be.visible'); + cy.get('[name=add-type-name]').type('This is activity type 2'); + cy.get('[cy-name=add-type-button]').click(); + cy.get('[cy-name=activity-types]').should('contain', 'This is activity type 2'); + + cy.get('[cy-name=activity-types-'+item+']').invoke('attr', 'cy-items').then((items) => { + let aitem = _.last(items.split(',')); + + cy.get('[cy-name=activity-type-delete-button-'+aitem+']').click(); + cy.get('[cy-name=delete-type-button]').click(); + cy.get('[cy-name=activity-types]').should('not.contain', 'This is activity type 2'); + }); + + // now delete the activity type category and make sure it also deletes + // the activity type that belonged to it + cy.get('[cy-name=activity-type-category-delete-button-'+item+']').click(); + cy.get('[cy-name=delete-category-button]').click(); + cy.get('[cy-name=activity-types]').should('not.contain', 'This is still an activity type category'); + cy.get('[cy-name=activity-types]').should('not.contain', 'This is modified activity type 1'); + }); + }); + + it('lets you add an activity type and use it', function () { + cy.login(); + cy.visit('/'); + + // set account as premium + cy.get('body').invoke('attr', 'data-account-id').then(function ($accountId) { + cy.setPremium($accountId); + }); + + // make sure that going premium removes restrictions + cy.visit('/settings/personalization'); + cy.get('[cy-name=activity-type-premium-message]').should('not.be.visible'); + cy.get('[cy-name=activity-types]').should('contain', 'played a sport together'); + + // add an activity type category + cy.get('[cy-name=add-activity-type-category-button]').click(); + cy.get('.sweet-modal-overlay').should('be.visible'); + cy.get('[name=add-category-name]').type('This is an activity type category'); + cy.get('[cy-name=add-activity-type-category-save-button]').click(); + + cy.get('[cy-name=activity-types]').should('contain', 'This is an activity type category'); + cy.get('[cy-name=activity-type-categories]').invoke('attr', 'cy-items').then((items) => { + let item = _.last(items.split(',')); + + // edit an activity type category + cy.get('[cy-name=activity-type-category-edit-button-'+item+']').click(); + cy.get('.sweet-modal-overlay').should('be.visible'); + cy.get('[name=update-category-name]').clear(); + cy.get('[name=update-category-name]').type('This is still an activity type category'); + cy.get('[cy-name=update-activity-type-category-button]').click(); + cy.get('[cy-name=activity-types]').should('contain', 'This is still an activity type category'); + + // add an activity type + cy.get('[cy-name=add-activity-type-button-for-category-'+item+']').click(); + cy.get('.sweet-modal-overlay').should('be.visible'); + cy.get('[name=add-type-name]').type('This is activity type 1'); + cy.get('[cy-name=add-type-button]').click(); + cy.get('[cy-name=activity-types]').should('contain', 'This is activity type 1'); + + // make sure the activity type exists on the Add activity page + cy.createContact('John', 'Doe', 'Man'); + cy.get('[cy-name=add-activity-button]').click(); + cy.get('[cy-name=activities_add_category]').click(); + cy.get('[name=activity-type-list]').should('contain', 'This is activity type 1'); + }); + }); +}); diff --git a/tests/cypress/plugins/index.js b/tests/cypress/plugins/index.js new file mode 100644 index 0000000..dffed25 --- /dev/null +++ b/tests/cypress/plugins/index.js @@ -0,0 +1,17 @@ +// *********************************************************** +// This example plugins/index.js can be used to load plugins +// +// You can change the location of this file or turn off loading +// the plugins file with the 'pluginsFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/plugins-guide +// *********************************************************** + +// This function is called when a project is opened or re-opened (e.g. due to +// the project's config changing) + +module.exports = (on, config) => { + // `on` is used to hook into various events Cypress emits + // `config` is the resolved Cypress config +}; diff --git a/tests/cypress/support/commands.js b/tests/cypress/support/commands.js new file mode 100644 index 0000000..c1f5a77 --- /dev/null +++ b/tests/cypress/support/commands.js @@ -0,0 +1,25 @@ +// *********************************************** +// This example commands.js shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** +// +// +// -- This is a parent command -- +// Cypress.Commands.add("login", (email, password) => { ... }) +// +// +// -- This is a child command -- +// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) +// +// +// -- This is a dual command -- +// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) +// +// +// -- This is will overwrite an existing command -- +// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) diff --git a/tests/cypress/support/helpers/app.js b/tests/cypress/support/helpers/app.js new file mode 100644 index 0000000..8b17716 --- /dev/null +++ b/tests/cypress/support/helpers/app.js @@ -0,0 +1,24 @@ +Cypress.Commands.add('login', () => { + cy.exec('php artisan setup:frontendtestuser').then((result) => { + cy.visit('/_dusk/login/'+result.stdout+'/'); + }); +}); + +Cypress.Commands.add('setPremium', (accountId) => { + cy.exec('php artisan account:setpremium ' + accountId); +}); + +Cypress.Commands.add('register', (firstName, lastName, password, email, policy) => { + cy.visit('/register'); + + cy.get('.alert').should('not.exist'); + cy.get('input[name=email]').type(email); + cy.get('input[name=first_name]').type(firstName); + cy.get('input[name=last_name]').type(lastName); + cy.get('input[name=password]').type(password); + cy.get('input[name=password_confirmation]').type(password); + if (policy) { + cy.get('input[name=policy]').click(); + } + cy.get('button[type=submit]').click(); +}); diff --git a/tests/cypress/support/helpers/contacts.js b/tests/cypress/support/helpers/contacts.js new file mode 100644 index 0000000..a0384e5 --- /dev/null +++ b/tests/cypress/support/helpers/contacts.js @@ -0,0 +1,34 @@ +Cypress.Commands.add('createContact', (firstname, lastname, gender, action = 'save') => { + cy.visit('/people'); + cy.get('#button-add-contact').click(); + + cy.get('input[name=first_name]').type(firstname); + cy.get('input[name=last_name]').type(lastname); + cy.get('select[name=gender]').select(gender); + + cy.get('button[name=' + action + ']').click(); +}); + +Cypress.Commands.add('createActivity', () => { + cy.visit('/people'); + + // this gets the first content of the list + cy.get('tr.clickable.people-list-item.bg-white.pointer').click(); + + cy.get('[cy-name=add-activity-button]').should('be.visible'); + cy.get('[cy-name=add-activity-button]').click(); + //cy.url().should('include', '/activities/add/h:'); + + cy.get('[name=summary]').type('This is a summary'); + cy.get('[cy-name=save-activity-button]').click(); + + cy.url().should('include', '/people/h:'); + cy.get('[cy-name=activities-blank-state]').should('not.be.visible'); + + cy.get('[cy-name=activities-body]').should('be.visible').then((activities) => { + let item = activities[0].getAttribute('cy-items'); + + cy.get('[cy-name=activity-body-'+item+']').should('exist'); + cy.get('[cy-name=activity-body-'+item+']').should('contain', 'This is a summary'); + }); +}); diff --git a/tests/cypress/support/index.js b/tests/cypress/support/index.js new file mode 100644 index 0000000..23cff87 --- /dev/null +++ b/tests/cypress/support/index.js @@ -0,0 +1,22 @@ +// *********************************************************** +// This example support/index.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands'; +import './helpers/app'; +import './helpers/contacts'; + +// Alternatively you can use CommonJS syntax: +// require('./commands') diff --git a/tests/stubs/broken_vcard_stub.vcard b/tests/stubs/broken_vcard_stub.vcard new file mode 100644 index 0000000..a2e1dc2 --- /dev/null +++ b/tests/stubs/broken_vcard_stub.vcard @@ -0,0 +1,8 @@ +BEGIN:VCAR +N:;Bono;;; +EMAIL;TYPE=INTERNET:bono@example.com +TEL:+1 202-555-0191 +ORG:U2 +BDAY:1960-05-10 +NOTE:Lorem ipsum dolor sit amet +END:VCARD \ No newline at end of file diff --git a/tests/stubs/single_contact_stub.csv b/tests/stubs/single_contact_stub.csv new file mode 100644 index 0000000..0c5f6c9 --- /dev/null +++ b/tests/stubs/single_contact_stub.csv @@ -0,0 +1,2 @@ +id,first_name,middle_name,last_name +0,Bono,Paul David,Hewson,,,,,,,,,,,1960-05-10,,,,,,,,,,,,,,bono@example.com \ No newline at end of file diff --git a/tests/stubs/single_vcard_stub.vcard b/tests/stubs/single_vcard_stub.vcard new file mode 100644 index 0000000..943f746 --- /dev/null +++ b/tests/stubs/single_vcard_stub.vcard @@ -0,0 +1,10 @@ +BEGIN:VCARD +VERSION:3.0 +FN:Bono +N:;Bono;;; +EMAIL;TYPE=INTERNET:bono@example.com +TEL:+1 202-555-0191 +ORG:U2 +BDAY:1960-05-10 +NOTE:Lorem ipsum dolor sit amet +END:VCARD \ No newline at end of file diff --git a/tests/stubs/vcard_stub.vcf b/tests/stubs/vcard_stub.vcf new file mode 100644 index 0000000..a570d39 --- /dev/null +++ b/tests/stubs/vcard_stub.vcf @@ -0,0 +1,24 @@ +BEGIN:VCARD +VERSION:3.0 +FN:Bono +N:;Bono;;; +EMAIL;TYPE=INTERNET:bono@example.com +TEL:+1 202-555-0191 +ORG:U2 +TITLE:Lead vocalist +BDAY:1960-05-10 +NOTE:Lorem ipsum dolor sit amet +END:VCARD +BEGIN:VCARD +VERSION:3.0 +FN:John Doe +N:Doe;John;;; +EMAIL;TYPE=INTERNET:john.doe@example.com +END:VCARD +BEGIN:VCARD +VERSION:3.0 +FN: +N:;;;; +NICKNAME:Johnny +ADR:;;17 Shakespeare Ave.;Southampton;;SO17 2HB;United Kingdom +END:VCARD diff --git a/webpack.mix.js b/webpack.mix.js new file mode 100644 index 0000000..990acb0 --- /dev/null +++ b/webpack.mix.js @@ -0,0 +1,63 @@ +const mix = require('laravel-mix'); +const path = require('path'); +require('laravel-mix-purgecss'); + +const MomentLocalesPlugin = require('moment-locales-webpack-plugin'); +mix.webpackConfig({ + plugins: [ + new MomentLocalesPlugin({ + localesToKeep: [ + 'en', + 'ar', + 'de', + 'el', + 'en-GB', + 'es', + 'fr', + 'he', + 'id', + 'it', + 'nl', + 'pt-BR', + 'ru', + 'sv', + 'tr', + 'vi', + 'zh-CN', + 'zh-TW', + ], + }), + ], +}); + +const purgeCssOptions = { + safelist: { + // List of regex of CSS class to not remove + standard: [/^autosuggest/, /^fa-/, /^vdp-datepicker/, /^StripeElement/, /^vgt/, /^vue-tooltip/, /^pretty/, /^sweet-/, /^vuejs-clipper-basic/, /^vs__/, /^sr-only/], + // List of regex of CSS class name whose child path CSS class will not be removed + // ex: to exclude "jane" in "mary jane": add "mary") + deep: [/^vdp-datepicker/, /^vgt/, /^vue-tooltip/, /^pretty/, /^sweet-/, /^vs-/] + } +}; + +mix.js('resources/js/app.js', 'public/js').vue() + .sass('resources/sass/app-ltr.scss', 'public/css') + .sass('resources/sass/app-rtl.scss', 'public/css') + + // stripe + .js('resources/js/stripe.js', 'public/js') + .sass('resources/sass/stripe.scss', 'public/css') + + .alias({ + vue$: path.join(__dirname, 'node_modules/vue/dist/vue.esm.js'), + }) + + // global commands + .purgeCss(purgeCssOptions) + .extract() + .sourceMaps(process.env.MIX_PROD_SOURCE_MAPS || false, 'eval-cheap-module-source-map', 'source-map') + .setResourceRoot('../'); + +if (mix.inProduction()) { + mix.version(); +} diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..3ba7d42 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,7613 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@aashutoshrathi/word-wrap@^1.2.3": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf" + integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== + +"@ampproject/remapping@^2.2.0": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630" + integrity sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg== + dependencies: + "@jridgewell/gen-mapping" "^0.3.0" + "@jridgewell/trace-mapping" "^0.3.9" + +"@babel/code-frame@7.12.11": + version "7.12.11" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f" + integrity sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw== + dependencies: + "@babel/highlight" "^7.10.4" + +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.22.13": + version "7.22.13" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e" + integrity sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w== + dependencies: + "@babel/highlight" "^7.22.13" + chalk "^2.4.2" + +"@babel/compat-data@^7.20.5", "@babel/compat-data@^7.22.6", "@babel/compat-data@^7.22.9", "@babel/compat-data@^7.23.2": + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.23.2.tgz#6a12ced93455827037bfb5ed8492820d60fc32cc" + integrity sha512-0S9TQMmDHlqAZ2ITT95irXKfxN9bncq8ZCoJhun3nHL/lLUxd2NKBJYoNGWH7S0hz6fRQwWlAWn/ILM0C70KZQ== + +"@babel/core@^7.15.8": + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.23.2.tgz#ed10df0d580fff67c5f3ee70fd22e2e4c90a9f94" + integrity sha512-n7s51eWdaWZ3vGT2tD4T7J6eJs3QoBXydv7vkUM06Bf1cbVD2Kc2UrkzhiQwobfV7NwOnQXYL7UBJ5VPU+RGoQ== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.22.13" + "@babel/generator" "^7.23.0" + "@babel/helper-compilation-targets" "^7.22.15" + "@babel/helper-module-transforms" "^7.23.0" + "@babel/helpers" "^7.23.2" + "@babel/parser" "^7.23.0" + "@babel/template" "^7.22.15" + "@babel/traverse" "^7.23.2" + "@babel/types" "^7.23.0" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + +"@babel/generator@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.0.tgz#df5c386e2218be505b34837acbcb874d7a983420" + integrity sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g== + dependencies: + "@babel/types" "^7.23.0" + "@jridgewell/gen-mapping" "^0.3.2" + "@jridgewell/trace-mapping" "^0.3.17" + jsesc "^2.5.1" + +"@babel/helper-annotate-as-pure@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz#e7f06737b197d580a01edf75d97e2c8be99d3882" + integrity sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-builder-binary-assignment-operator-visitor@^7.22.5": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz#5426b109cf3ad47b91120f8328d8ab1be8b0b956" + integrity sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw== + dependencies: + "@babel/types" "^7.22.15" + +"@babel/helper-compilation-targets@^7.20.7", "@babel/helper-compilation-targets@^7.22.15", "@babel/helper-compilation-targets@^7.22.5", "@babel/helper-compilation-targets@^7.22.6": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz#0698fc44551a26cf29f18d4662d5bf545a6cfc52" + integrity sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw== + dependencies: + "@babel/compat-data" "^7.22.9" + "@babel/helper-validator-option" "^7.22.15" + browserslist "^4.21.9" + lru-cache "^5.1.1" + semver "^6.3.1" + +"@babel/helper-create-class-features-plugin@^7.22.11", "@babel/helper-create-class-features-plugin@^7.22.5": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.15.tgz#97a61b385e57fe458496fad19f8e63b63c867de4" + integrity sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-function-name" "^7.22.5" + "@babel/helper-member-expression-to-functions" "^7.22.15" + "@babel/helper-optimise-call-expression" "^7.22.5" + "@babel/helper-replace-supers" "^7.22.9" + "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + semver "^6.3.1" + +"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.22.5": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz#5ee90093914ea09639b01c711db0d6775e558be1" + integrity sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + regexpu-core "^5.3.1" + semver "^6.3.1" + +"@babel/helper-define-polyfill-provider@^0.4.3": + version "0.4.3" + resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.3.tgz#a71c10f7146d809f4a256c373f462d9bba8cf6ba" + integrity sha512-WBrLmuPP47n7PNwsZ57pqam6G/RGo1vw/87b0Blc53tZNGZ4x7YvZ6HgQe2vo1W/FR20OgjeZuGXzudPiXHFug== + dependencies: + "@babel/helper-compilation-targets" "^7.22.6" + "@babel/helper-plugin-utils" "^7.22.5" + debug "^4.1.1" + lodash.debounce "^4.0.8" + resolve "^1.14.2" + +"@babel/helper-environment-visitor@^7.22.20", "@babel/helper-environment-visitor@^7.22.5": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167" + integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA== + +"@babel/helper-function-name@^7.22.5", "@babel/helper-function-name@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759" + integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw== + dependencies: + "@babel/template" "^7.22.15" + "@babel/types" "^7.23.0" + +"@babel/helper-hoist-variables@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb" + integrity sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-member-expression-to-functions@^7.22.15": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz#9263e88cc5e41d39ec18c9a3e0eced59a3e7d366" + integrity sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA== + dependencies: + "@babel/types" "^7.23.0" + +"@babel/helper-module-imports@^7.22.15", "@babel/helper-module-imports@^7.22.5": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz#16146307acdc40cc00c3b2c647713076464bdbf0" + integrity sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w== + dependencies: + "@babel/types" "^7.22.15" + +"@babel/helper-module-transforms@^7.22.5", "@babel/helper-module-transforms@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.23.0.tgz#3ec246457f6c842c0aee62a01f60739906f7047e" + integrity sha512-WhDWw1tdrlT0gMgUJSlX0IQvoO1eN279zrAUbVB+KpV2c3Tylz8+GnKOLllCS6Z/iZQEyVYxhZVUdPTqs2YYPw== + dependencies: + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-module-imports" "^7.22.15" + "@babel/helper-simple-access" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/helper-validator-identifier" "^7.22.20" + +"@babel/helper-optimise-call-expression@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz#f21531a9ccbff644fdd156b4077c16ff0c3f609e" + integrity sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz#dd7ee3735e8a313b9f7b05a773d892e88e6d7295" + integrity sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg== + +"@babel/helper-remap-async-to-generator@^7.22.20", "@babel/helper-remap-async-to-generator@^7.22.5": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz#7b68e1cb4fa964d2996fd063723fb48eca8498e0" + integrity sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-wrap-function" "^7.22.20" + +"@babel/helper-replace-supers@^7.22.5", "@babel/helper-replace-supers@^7.22.9": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz#e37d367123ca98fe455a9887734ed2e16eb7a793" + integrity sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw== + dependencies: + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-member-expression-to-functions" "^7.22.15" + "@babel/helper-optimise-call-expression" "^7.22.5" + +"@babel/helper-simple-access@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz#4938357dc7d782b80ed6dbb03a0fba3d22b1d5de" + integrity sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-skip-transparent-expression-wrappers@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz#007f15240b5751c537c40e77abb4e89eeaaa8847" + integrity sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-split-export-declaration@^7.22.6": + version "7.22.6" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz#322c61b7310c0997fe4c323955667f18fcefb91c" + integrity sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g== + dependencies: + "@babel/types" "^7.22.5" + +"@babel/helper-string-parser@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f" + integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw== + +"@babel/helper-validator-identifier@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" + integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== + +"@babel/helper-validator-option@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz#694c30dfa1d09a6534cdfcafbe56789d36aba040" + integrity sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA== + +"@babel/helper-wrap-function@^7.22.20": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.22.20.tgz#15352b0b9bfb10fc9c76f79f6342c00e3411a569" + integrity sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw== + dependencies: + "@babel/helper-function-name" "^7.22.5" + "@babel/template" "^7.22.15" + "@babel/types" "^7.22.19" + +"@babel/helpers@^7.23.2": + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.23.2.tgz#2832549a6e37d484286e15ba36a5330483cac767" + integrity sha512-lzchcp8SjTSVe/fPmLwtWVBFC7+Tbn8LGHDVfDp9JGxpAY5opSaEFgt8UQvrnECWOTdji2mOWMz1rOhkHscmGQ== + dependencies: + "@babel/template" "^7.22.15" + "@babel/traverse" "^7.23.2" + "@babel/types" "^7.23.0" + +"@babel/highlight@^7.10.4", "@babel/highlight@^7.22.13": + version "7.22.20" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.20.tgz#4ca92b71d80554b01427815e06f2df965b9c1f54" + integrity sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg== + dependencies: + "@babel/helper-validator-identifier" "^7.22.20" + chalk "^2.4.2" + js-tokens "^4.0.0" + +"@babel/parser@^7.1.0", "@babel/parser@^7.18.4", "@babel/parser@^7.20.7", "@babel/parser@^7.22.15", "@babel/parser@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.0.tgz#da950e622420bf96ca0d0f2909cdddac3acd8719" + integrity sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw== + +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.15.tgz#02dc8a03f613ed5fdc29fb2f728397c78146c962" + integrity sha512-FB9iYlz7rURmRJyXRKEnalYPPdn87H5no108cyuQQyMwlpJ2SJtpIUBI27kdTin956pz+LPypkPVPUTlxOmrsg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.15.tgz#2aeb91d337d4e1a1e7ce85b76a37f5301781200f" + integrity sha512-Hyph9LseGvAeeXzikV88bczhsrLrIZqDPxO+sSmAunMPaGrBGhfMWzCPYTtiW9t+HzSE2wtV8e5cc5P6r1xMDQ== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" + "@babel/plugin-transform-optional-chaining" "^7.22.15" + +"@babel/plugin-proposal-object-rest-spread@^7.15.6": + version "7.20.7" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.7.tgz#aa662940ef425779c75534a5c41e9d936edc390a" + integrity sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg== + dependencies: + "@babel/compat-data" "^7.20.5" + "@babel/helper-compilation-targets" "^7.20.7" + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-transform-parameters" "^7.20.7" + +"@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2": + version "7.21.0-placeholder-for-preset-env.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz#7844f9289546efa9febac2de4cfe358a050bd703" + integrity sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w== + +"@babel/plugin-syntax-async-generators@^7.8.4": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" + integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-class-properties@^7.12.13": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" + integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== + dependencies: + "@babel/helper-plugin-utils" "^7.12.13" + +"@babel/plugin-syntax-class-static-block@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz#195df89b146b4b78b3bf897fd7a257c84659d406" + integrity sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-dynamic-import@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" + integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-export-namespace-from@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz#028964a9ba80dbc094c915c487ad7c4e7a66465a" + integrity sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + +"@babel/plugin-syntax-import-assertions@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.22.5.tgz#07d252e2aa0bc6125567f742cd58619cb14dce98" + integrity sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-syntax-import-attributes@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.22.5.tgz#ab840248d834410b829f569f5262b9e517555ecb" + integrity sha512-KwvoWDeNKPETmozyFE0P2rOLqh39EoQHNjqizrI5B8Vt0ZNS7M56s7dAiAqbYfiAYOuIzIh96z3iR2ktgu3tEg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-syntax-import-meta@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" + integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-json-strings@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" + integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" + integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" + integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-numeric-separator@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" + integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-object-rest-spread@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" + integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-catch-binding@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" + integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-chaining@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" + integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-private-property-in-object@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz#0dc6671ec0ea22b6e94a1114f857970cd39de1ad" + integrity sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-top-level-await@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" + integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-unicode-sets-regex@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz#d49a3b3e6b52e5be6740022317580234a6a47357" + integrity sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-arrow-functions@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.22.5.tgz#e5ba566d0c58a5b2ba2a8b795450641950b71958" + integrity sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-async-generator-functions@^7.23.2": + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.23.2.tgz#054afe290d64c6f576f371ccc321772c8ea87ebb" + integrity sha512-BBYVGxbDVHfoeXbOwcagAkOQAm9NxoTdMGfTqghu1GrvadSaw6iW3Je6IcL5PNOw8VwjxqBECXy50/iCQSY/lQ== + dependencies: + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-remap-async-to-generator" "^7.22.20" + "@babel/plugin-syntax-async-generators" "^7.8.4" + +"@babel/plugin-transform-async-to-generator@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.22.5.tgz#c7a85f44e46f8952f6d27fe57c2ed3cc084c3775" + integrity sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ== + dependencies: + "@babel/helper-module-imports" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-remap-async-to-generator" "^7.22.5" + +"@babel/plugin-transform-block-scoped-functions@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.22.5.tgz#27978075bfaeb9fa586d3cb63a3d30c1de580024" + integrity sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-block-scoping@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.23.0.tgz#8744d02c6c264d82e1a4bc5d2d501fd8aff6f022" + integrity sha512-cOsrbmIOXmf+5YbL99/S49Y3j46k/T16b9ml8bm9lP6N9US5iQ2yBK7gpui1pg0V/WMcXdkfKbTb7HXq9u+v4g== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-class-properties@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.22.5.tgz#97a56e31ad8c9dc06a0b3710ce7803d5a48cca77" + integrity sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-class-static-block@^7.22.11": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.11.tgz#dc8cc6e498f55692ac6b4b89e56d87cec766c974" + integrity sha512-GMM8gGmqI7guS/llMFk1bJDkKfn3v3C4KHK9Yg1ey5qcHcOlKb0QvcMrgzvxo+T03/4szNh5lghY+fEC98Kq9g== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.22.11" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-class-static-block" "^7.14.5" + +"@babel/plugin-transform-classes@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.15.tgz#aaf4753aee262a232bbc95451b4bdf9599c65a0b" + integrity sha512-VbbC3PGjBdE0wAWDdHM9G8Gm977pnYI0XpqMd6LrKISj8/DJXEsWqgRuTYaNE9Bv0JGhTZUzHDlMk18IpOuoqw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-compilation-targets" "^7.22.15" + "@babel/helper-environment-visitor" "^7.22.5" + "@babel/helper-function-name" "^7.22.5" + "@babel/helper-optimise-call-expression" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-replace-supers" "^7.22.9" + "@babel/helper-split-export-declaration" "^7.22.6" + globals "^11.1.0" + +"@babel/plugin-transform-computed-properties@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.22.5.tgz#cd1e994bf9f316bd1c2dafcd02063ec261bb3869" + integrity sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/template" "^7.22.5" + +"@babel/plugin-transform-destructuring@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.23.0.tgz#6447aa686be48b32eaf65a73e0e2c0bd010a266c" + integrity sha512-vaMdgNXFkYrB+8lbgniSYWHsgqK5gjaMNcc84bMIOMRLH0L9AqYq3hwMdvnyqj1OPqea8UtjPEuS/DCenah1wg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-dotall-regex@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.22.5.tgz#dbb4f0e45766eb544e193fb00e65a1dd3b2a4165" + integrity sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-duplicate-keys@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.22.5.tgz#b6e6428d9416f5f0bba19c70d1e6e7e0b88ab285" + integrity sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-dynamic-import@^7.22.11": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.11.tgz#2c7722d2a5c01839eaf31518c6ff96d408e447aa" + integrity sha512-g/21plo58sfteWjaO0ZNVb+uEOkJNjAaHhbejrnBmu011l/eNDScmkbjCC3l4FKb10ViaGU4aOkFznSu2zRHgA== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + +"@babel/plugin-transform-exponentiation-operator@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.22.5.tgz#402432ad544a1f9a480da865fda26be653e48f6a" + integrity sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g== + dependencies: + "@babel/helper-builder-binary-assignment-operator-visitor" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-export-namespace-from@^7.22.11": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.11.tgz#b3c84c8f19880b6c7440108f8929caf6056db26c" + integrity sha512-xa7aad7q7OiT8oNZ1mU7NrISjlSkVdMbNxn9IuLZyL9AJEhs1Apba3I+u5riX1dIkdptP5EKDG5XDPByWxtehw== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + +"@babel/plugin-transform-for-of@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.15.tgz#f64b4ccc3a4f131a996388fae7680b472b306b29" + integrity sha512-me6VGeHsx30+xh9fbDLLPi0J1HzmeIIyenoOQHuw2D4m2SAU3NrspX5XxJLBpqn5yrLzrlw2Iy3RA//Bx27iOA== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-function-name@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.22.5.tgz#935189af68b01898e0d6d99658db6b164205c143" + integrity sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg== + dependencies: + "@babel/helper-compilation-targets" "^7.22.5" + "@babel/helper-function-name" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-json-strings@^7.22.11": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.11.tgz#689a34e1eed1928a40954e37f74509f48af67835" + integrity sha512-CxT5tCqpA9/jXFlme9xIBCc5RPtdDq3JpkkhgHQqtDdiTnTI0jtZ0QzXhr5DILeYifDPp2wvY2ad+7+hLMW5Pw== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-json-strings" "^7.8.3" + +"@babel/plugin-transform-literals@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.22.5.tgz#e9341f4b5a167952576e23db8d435849b1dd7920" + integrity sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-logical-assignment-operators@^7.22.11": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.11.tgz#24c522a61688bde045b7d9bc3c2597a4d948fc9c" + integrity sha512-qQwRTP4+6xFCDV5k7gZBF3C31K34ut0tbEcTKxlX/0KXxm9GLcO14p570aWxFvVzx6QAfPgq7gaeIHXJC8LswQ== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + +"@babel/plugin-transform-member-expression-literals@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.22.5.tgz#4fcc9050eded981a468347dd374539ed3e058def" + integrity sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-modules-amd@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.23.0.tgz#05b2bc43373faa6d30ca89214731f76f966f3b88" + integrity sha512-xWT5gefv2HGSm4QHtgc1sYPbseOyf+FFDo2JbpE25GWl5BqTGO9IMwTYJRoIdjsF85GE+VegHxSCUt5EvoYTAw== + dependencies: + "@babel/helper-module-transforms" "^7.23.0" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-modules-commonjs@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.0.tgz#b3dba4757133b2762c00f4f94590cf6d52602481" + integrity sha512-32Xzss14/UVc7k9g775yMIvkVK8xwKE0DPdP5JTapr3+Z9w4tzeOuLNY6BXDQR6BdnzIlXnCGAzsk/ICHBLVWQ== + dependencies: + "@babel/helper-module-transforms" "^7.23.0" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-simple-access" "^7.22.5" + +"@babel/plugin-transform-modules-systemjs@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.23.0.tgz#77591e126f3ff4132a40595a6cccd00a6b60d160" + integrity sha512-qBej6ctXZD2f+DhlOC9yO47yEYgUh5CZNz/aBoH4j/3NOlRfJXJbY7xDQCqQVf9KbrqGzIWER1f23doHGrIHFg== + dependencies: + "@babel/helper-hoist-variables" "^7.22.5" + "@babel/helper-module-transforms" "^7.23.0" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-validator-identifier" "^7.22.20" + +"@babel/plugin-transform-modules-umd@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.22.5.tgz#4694ae40a87b1745e3775b6a7fe96400315d4f98" + integrity sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ== + dependencies: + "@babel/helper-module-transforms" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-named-capturing-groups-regex@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz#67fe18ee8ce02d57c855185e27e3dc959b2e991f" + integrity sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-new-target@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.22.5.tgz#1b248acea54ce44ea06dfd37247ba089fcf9758d" + integrity sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-nullish-coalescing-operator@^7.22.11": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.11.tgz#debef6c8ba795f5ac67cd861a81b744c5d38d9fc" + integrity sha512-YZWOw4HxXrotb5xsjMJUDlLgcDXSfO9eCmdl1bgW4+/lAGdkjaEvOnQ4p5WKKdUgSzO39dgPl0pTnfxm0OAXcg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + +"@babel/plugin-transform-numeric-separator@^7.22.11": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.11.tgz#498d77dc45a6c6db74bb829c02a01c1d719cbfbd" + integrity sha512-3dzU4QGPsILdJbASKhF/V2TVP+gJya1PsueQCxIPCEcerqF21oEcrob4mzjsp2Py/1nLfF5m+xYNMDpmA8vffg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + +"@babel/plugin-transform-object-rest-spread@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.15.tgz#21a95db166be59b91cde48775310c0df6e1da56f" + integrity sha512-fEB+I1+gAmfAyxZcX1+ZUwLeAuuf8VIg67CTznZE0MqVFumWkh8xWtn58I4dxdVf080wn7gzWoF8vndOViJe9Q== + dependencies: + "@babel/compat-data" "^7.22.9" + "@babel/helper-compilation-targets" "^7.22.15" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-transform-parameters" "^7.22.15" + +"@babel/plugin-transform-object-super@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.22.5.tgz#794a8d2fcb5d0835af722173c1a9d704f44e218c" + integrity sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-replace-supers" "^7.22.5" + +"@babel/plugin-transform-optional-catch-binding@^7.22.11": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.11.tgz#461cc4f578a127bb055527b3e77404cad38c08e0" + integrity sha512-rli0WxesXUeCJnMYhzAglEjLWVDF6ahb45HuprcmQuLidBJFWjNnOzssk2kuc6e33FlLaiZhG/kUIzUMWdBKaQ== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + +"@babel/plugin-transform-optional-chaining@^7.22.15", "@babel/plugin-transform-optional-chaining@^7.23.0": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.23.0.tgz#73ff5fc1cf98f542f09f29c0631647d8ad0be158" + integrity sha512-sBBGXbLJjxTzLBF5rFWaikMnOGOk/BmK6vVByIdEggZ7Vn6CvWXZyRkkLFK6WE0IF8jSliyOkUN6SScFgzCM0g== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + +"@babel/plugin-transform-parameters@^7.20.7", "@babel/plugin-transform-parameters@^7.22.15": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.15.tgz#719ca82a01d177af358df64a514d64c2e3edb114" + integrity sha512-hjk7qKIqhyzhhUvRT683TYQOFa/4cQKwQy7ALvTpODswN40MljzNDa0YldevS6tGbxwaEKVn502JmY0dP7qEtQ== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-private-methods@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.22.5.tgz#21c8af791f76674420a147ae62e9935d790f8722" + integrity sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-private-property-in-object@^7.22.11": + version "7.22.11" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.11.tgz#ad45c4fc440e9cb84c718ed0906d96cf40f9a4e1" + integrity sha512-sSCbqZDBKHetvjSwpyWzhuHkmW5RummxJBVbYLkGkaiTOWGxml7SXt0iWa03bzxFIx7wOj3g/ILRd0RcJKBeSQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.22.5" + "@babel/helper-create-class-features-plugin" "^7.22.11" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + +"@babel/plugin-transform-property-literals@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.22.5.tgz#b5ddabd73a4f7f26cd0e20f5db48290b88732766" + integrity sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-regenerator@^7.22.10": + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.22.10.tgz#8ceef3bd7375c4db7652878b0241b2be5d0c3cca" + integrity sha512-F28b1mDt8KcT5bUyJc/U9nwzw6cV+UmTeRlXYIl2TNqMMJif0Jeey9/RQ3C4NOd2zp0/TRsDns9ttj2L523rsw== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + regenerator-transform "^0.15.2" + +"@babel/plugin-transform-reserved-words@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.22.5.tgz#832cd35b81c287c4bcd09ce03e22199641f964fb" + integrity sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-runtime@^7.15.8": + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.23.2.tgz#c956a3f8d1aa50816ff6c30c6288d66635c12990" + integrity sha512-XOntj6icgzMS58jPVtQpiuF6ZFWxQiJavISGx5KGjRj+3gqZr8+N6Kx+N9BApWzgS+DOjIZfXXj0ZesenOWDyA== + dependencies: + "@babel/helper-module-imports" "^7.22.15" + "@babel/helper-plugin-utils" "^7.22.5" + babel-plugin-polyfill-corejs2 "^0.4.6" + babel-plugin-polyfill-corejs3 "^0.8.5" + babel-plugin-polyfill-regenerator "^0.5.3" + semver "^6.3.1" + +"@babel/plugin-transform-shorthand-properties@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.22.5.tgz#6e277654be82b5559fc4b9f58088507c24f0c624" + integrity sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-spread@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.22.5.tgz#6487fd29f229c95e284ba6c98d65eafb893fea6b" + integrity sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-skip-transparent-expression-wrappers" "^7.22.5" + +"@babel/plugin-transform-sticky-regex@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.22.5.tgz#295aba1595bfc8197abd02eae5fc288c0deb26aa" + integrity sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-template-literals@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.22.5.tgz#8f38cf291e5f7a8e60e9f733193f0bcc10909bff" + integrity sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-typeof-symbol@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.22.5.tgz#5e2ba478da4b603af8673ff7c54f75a97b716b34" + integrity sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-unicode-escapes@^7.22.10": + version "7.22.10" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.22.10.tgz#c723f380f40a2b2f57a62df24c9005834c8616d9" + integrity sha512-lRfaRKGZCBqDlRU3UIFovdp9c9mEvlylmpod0/OatICsSfuQ9YFthRo1tpTkGsklEefZdqlEFdY4A2dwTb6ohg== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-unicode-property-regex@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.22.5.tgz#098898f74d5c1e86660dc112057b2d11227f1c81" + integrity sha512-HCCIb+CbJIAE6sXn5CjFQXMwkCClcOfPCzTlilJ8cUatfzwHlWQkbtV0zD338u9dZskwvuOYTuuaMaA8J5EI5A== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-unicode-regex@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.22.5.tgz#ce7e7bb3ef208c4ff67e02a22816656256d7a183" + integrity sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-unicode-sets-regex@^7.22.5": + version "7.22.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.22.5.tgz#77788060e511b708ffc7d42fdfbc5b37c3004e91" + integrity sha512-lhMfi4FC15j13eKrh3DnYHjpGj6UKQHtNKTbtc1igvAhRy4+kLhV07OpLcsN0VgDEw/MjAvJO4BdMJsHwMhzCg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.22.5" + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/preset-env@^7.15.8": + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.23.2.tgz#1f22be0ff0e121113260337dbc3e58fafce8d059" + integrity sha512-BW3gsuDD+rvHL2VO2SjAUNTBe5YrjsTiDyqamPDWY723na3/yPQ65X5oQkFVJZ0o50/2d+svm1rkPoJeR1KxVQ== + dependencies: + "@babel/compat-data" "^7.23.2" + "@babel/helper-compilation-targets" "^7.22.15" + "@babel/helper-plugin-utils" "^7.22.5" + "@babel/helper-validator-option" "^7.22.15" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.22.15" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.22.15" + "@babel/plugin-proposal-private-property-in-object" "7.21.0-placeholder-for-preset-env.2" + "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/plugin-syntax-class-properties" "^7.12.13" + "@babel/plugin-syntax-class-static-block" "^7.14.5" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + "@babel/plugin-syntax-import-assertions" "^7.22.5" + "@babel/plugin-syntax-import-attributes" "^7.22.5" + "@babel/plugin-syntax-import-meta" "^7.10.4" + "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + "@babel/plugin-syntax-top-level-await" "^7.14.5" + "@babel/plugin-syntax-unicode-sets-regex" "^7.18.6" + "@babel/plugin-transform-arrow-functions" "^7.22.5" + "@babel/plugin-transform-async-generator-functions" "^7.23.2" + "@babel/plugin-transform-async-to-generator" "^7.22.5" + "@babel/plugin-transform-block-scoped-functions" "^7.22.5" + "@babel/plugin-transform-block-scoping" "^7.23.0" + "@babel/plugin-transform-class-properties" "^7.22.5" + "@babel/plugin-transform-class-static-block" "^7.22.11" + "@babel/plugin-transform-classes" "^7.22.15" + "@babel/plugin-transform-computed-properties" "^7.22.5" + "@babel/plugin-transform-destructuring" "^7.23.0" + "@babel/plugin-transform-dotall-regex" "^7.22.5" + "@babel/plugin-transform-duplicate-keys" "^7.22.5" + "@babel/plugin-transform-dynamic-import" "^7.22.11" + "@babel/plugin-transform-exponentiation-operator" "^7.22.5" + "@babel/plugin-transform-export-namespace-from" "^7.22.11" + "@babel/plugin-transform-for-of" "^7.22.15" + "@babel/plugin-transform-function-name" "^7.22.5" + "@babel/plugin-transform-json-strings" "^7.22.11" + "@babel/plugin-transform-literals" "^7.22.5" + "@babel/plugin-transform-logical-assignment-operators" "^7.22.11" + "@babel/plugin-transform-member-expression-literals" "^7.22.5" + "@babel/plugin-transform-modules-amd" "^7.23.0" + "@babel/plugin-transform-modules-commonjs" "^7.23.0" + "@babel/plugin-transform-modules-systemjs" "^7.23.0" + "@babel/plugin-transform-modules-umd" "^7.22.5" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.22.5" + "@babel/plugin-transform-new-target" "^7.22.5" + "@babel/plugin-transform-nullish-coalescing-operator" "^7.22.11" + "@babel/plugin-transform-numeric-separator" "^7.22.11" + "@babel/plugin-transform-object-rest-spread" "^7.22.15" + "@babel/plugin-transform-object-super" "^7.22.5" + "@babel/plugin-transform-optional-catch-binding" "^7.22.11" + "@babel/plugin-transform-optional-chaining" "^7.23.0" + "@babel/plugin-transform-parameters" "^7.22.15" + "@babel/plugin-transform-private-methods" "^7.22.5" + "@babel/plugin-transform-private-property-in-object" "^7.22.11" + "@babel/plugin-transform-property-literals" "^7.22.5" + "@babel/plugin-transform-regenerator" "^7.22.10" + "@babel/plugin-transform-reserved-words" "^7.22.5" + "@babel/plugin-transform-shorthand-properties" "^7.22.5" + "@babel/plugin-transform-spread" "^7.22.5" + "@babel/plugin-transform-sticky-regex" "^7.22.5" + "@babel/plugin-transform-template-literals" "^7.22.5" + "@babel/plugin-transform-typeof-symbol" "^7.22.5" + "@babel/plugin-transform-unicode-escapes" "^7.22.10" + "@babel/plugin-transform-unicode-property-regex" "^7.22.5" + "@babel/plugin-transform-unicode-regex" "^7.22.5" + "@babel/plugin-transform-unicode-sets-regex" "^7.22.5" + "@babel/preset-modules" "0.1.6-no-external-plugins" + "@babel/types" "^7.23.0" + babel-plugin-polyfill-corejs2 "^0.4.6" + babel-plugin-polyfill-corejs3 "^0.8.5" + babel-plugin-polyfill-regenerator "^0.5.3" + core-js-compat "^3.31.0" + semver "^6.3.1" + +"@babel/preset-modules@0.1.6-no-external-plugins": + version "0.1.6-no-external-plugins" + resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz#ccb88a2c49c817236861fee7826080573b8a923a" + integrity sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/types" "^7.4.4" + esutils "^2.0.2" + +"@babel/regjsgen@^0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" + integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== + +"@babel/runtime@^7.15.4", "@babel/runtime@^7.21.0", "@babel/runtime@^7.8.4": + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.2.tgz#062b0ac103261d68a966c4c7baf2ae3e62ec3885" + integrity sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg== + dependencies: + regenerator-runtime "^0.14.0" + +"@babel/template@^7.22.15", "@babel/template@^7.22.5": + version "7.22.15" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38" + integrity sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w== + dependencies: + "@babel/code-frame" "^7.22.13" + "@babel/parser" "^7.22.15" + "@babel/types" "^7.22.15" + +"@babel/traverse@^7.23.2": + version "7.23.2" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.2.tgz#329c7a06735e144a506bdb2cad0268b7f46f4ad8" + integrity sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw== + dependencies: + "@babel/code-frame" "^7.22.13" + "@babel/generator" "^7.23.0" + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-function-name" "^7.23.0" + "@babel/helper-hoist-variables" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/parser" "^7.23.0" + "@babel/types" "^7.23.0" + debug "^4.1.0" + globals "^11.1.0" + +"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.22.15", "@babel/types@^7.22.19", "@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.4.4": + version "7.23.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.0.tgz#8c1f020c9df0e737e4e247c0619f58c68458aaeb" + integrity sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg== + dependencies: + "@babel/helper-string-parser" "^7.22.5" + "@babel/helper-validator-identifier" "^7.22.20" + to-fast-properties "^2.0.0" + +"@colors/colors@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" + integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== + +"@cypress/request@^2.88.5": + version "2.88.12" + resolved "https://registry.yarnpkg.com/@cypress/request/-/request-2.88.12.tgz#ba4911431738494a85e93fb04498cb38bc55d590" + integrity sha512-tOn+0mDZxASFM+cuAP9szGUGPI1HwWVSvdzm7V4cCsPdFTx6qMj29CwaQmRAMIEhORIUBFBsYROYJcveK4uOjA== + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + http-signature "~1.3.6" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + performance-now "^2.1.0" + qs "~6.10.3" + safe-buffer "^5.1.2" + tough-cookie "^4.1.3" + tunnel-agent "^0.6.0" + uuid "^8.3.2" + +"@cypress/xvfb@^1.2.4": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@cypress/xvfb/-/xvfb-1.2.4.tgz#2daf42e8275b39f4aa53c14214e557bd14e7748a" + integrity sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q== + dependencies: + debug "^3.1.0" + lodash.once "^4.1.1" + +"@discoveryjs/json-ext@^0.5.0": + version "0.5.7" + resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" + integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== + +"@eslint/eslintrc@^0.4.3": + version "0.4.3" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c" + integrity sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw== + dependencies: + ajv "^6.12.4" + debug "^4.1.1" + espree "^7.3.0" + globals "^13.9.0" + ignore "^4.0.6" + import-fresh "^3.2.1" + js-yaml "^3.13.1" + minimatch "^3.0.4" + strip-json-comments "^3.1.1" + +"@fullhuman/postcss-purgecss@^3.0.0": + version "3.1.3" + resolved "https://registry.yarnpkg.com/@fullhuman/postcss-purgecss/-/postcss-purgecss-3.1.3.tgz#47af7b87c9bfb3de4bc94a38f875b928fffdf339" + integrity sha512-kwOXw8fZ0Lt1QmeOOrd+o4Ibvp4UTEBFQbzvWldjlKv5n+G9sXfIPn1hh63IQIL8K8vbvv1oYMJiIUbuy9bGaA== + dependencies: + purgecss "^3.1.3" + +"@hokify/vuejs-datepicker@^2.0": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@hokify/vuejs-datepicker/-/vuejs-datepicker-2.0.2.tgz#17c747feb69696a70e224be450d48e9ea00f0160" + integrity sha512-IhZ6tDu29t2iIFUIfTFB/D0bjNNW5KoSszP2pI12Crs+AGYSaQCerSC0oF6TT+2Pvlahvesb2LcPSyVMoXb5Mg== + dependencies: + moment "^2.24.0" + +"@humanwhocodes/config-array@^0.5.0": + version "0.5.0" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9" + integrity sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg== + dependencies: + "@humanwhocodes/object-schema" "^1.2.0" + debug "^4.1.1" + minimatch "^3.0.4" + +"@humanwhocodes/object-schema@^1.2.0": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" + integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== + +"@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2": + version "0.3.3" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz#7e02e6eb5df901aaedb08514203b096614024098" + integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ== + dependencies: + "@jridgewell/set-array" "^1.0.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.1" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721" + integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA== + +"@jridgewell/set-array@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" + integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== + +"@jridgewell/source-map@^0.3.3": + version "0.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.5.tgz#a3bb4d5c6825aab0d281268f47f6ad5853431e91" + integrity sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ== + dependencies: + "@jridgewell/gen-mapping" "^0.3.0" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + +"@jridgewell/trace-mapping@^0.3.17", "@jridgewell/trace-mapping@^0.3.9": + version "0.3.20" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz#72e45707cf240fa6b081d0366f8265b0cd10197f" + integrity sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@leichtgewicht/ip-codec@^2.0.1": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b" + integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A== + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@snyk/protect@^1.1034.0": + version "1.1238.0" + resolved "https://registry.yarnpkg.com/@snyk/protect/-/protect-1.1238.0.tgz#39b88c4ac178e25f20f15348a9e7a44afa1350c8" + integrity sha512-5n309NbhWl2g51ylyQguWOFQ1ahUW+BLkwiKRGW15f04HCi/Mc2gdInjvyAT8131UHgoMiEubDymT6F8Kdn2lA== + +"@trysound/sax@0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" + integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== + +"@types/babel__core@^7.1.16": + version "7.20.3" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.3.tgz#d5625a50b6f18244425a1359a858c73d70340778" + integrity sha512-54fjTSeSHwfan8AyHWrKbfBWiEUrNTZsUwPTDSNaaP1QDQIZbeNUg3a59E9D+375MzUw/x1vx2/0F5LBz+AeYA== + dependencies: + "@babel/parser" "^7.20.7" + "@babel/types" "^7.20.7" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + +"@types/babel__generator@*": + version "7.6.6" + resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.6.tgz#676f89f67dc8ddaae923f70ebc5f1fa800c031a8" + integrity sha512-66BXMKb/sUWbMdBNdMvajU7i/44RkrA3z/Yt1c7R5xejt8qh84iU54yUWCtm0QwGJlDcf/gg4zd/x4mpLAlb/w== + dependencies: + "@babel/types" "^7.0.0" + +"@types/babel__template@*": + version "7.4.3" + resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.3.tgz#db9ac539a2fe05cfe9e168b24f360701bde41f5f" + integrity sha512-ciwyCLeuRfxboZ4isgdNZi/tkt06m8Tw6uGbBSBgWrnnZGNXiEyM27xc/PjXGQLqlZ6ylbgHMnm7ccF9tCkOeQ== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + +"@types/babel__traverse@*": + version "7.20.3" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.20.3.tgz#a971aa47441b28ef17884ff945d0551265a2d058" + integrity sha512-Lsh766rGEFbaxMIDH7Qa+Yha8cMVI3qAK6CHt3OR0YfxOIn5Z54iHiyDRycHrBqeIiqGa20Kpsv1cavfBKkRSw== + dependencies: + "@babel/types" "^7.20.7" + +"@types/body-parser@*": + version "1.19.4" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.4.tgz#78ad68f1f79eb851aa3634db0c7f57f6f601b462" + integrity sha512-N7UDG0/xiPQa2D/XrVJXjkWbpqHCd2sBaB32ggRF2l83RhPfamgKGF8gwwqyksS95qUS5ZYF9aF+lLPRlwI2UA== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/bonjour@^3.5.9": + version "3.5.12" + resolved "https://registry.yarnpkg.com/@types/bonjour/-/bonjour-3.5.12.tgz#49badafb988e6c433ca675a5fd769b93b7649fc8" + integrity sha512-ky0kWSqXVxSqgqJvPIkgFkcn4C8MnRog308Ou8xBBIVo39OmUFy+jqNe0nPwLCDFxUpmT9EvT91YzOJgkDRcFg== + dependencies: + "@types/node" "*" + +"@types/clean-css@^4.2.5": + version "4.2.9" + resolved "https://registry.yarnpkg.com/@types/clean-css/-/clean-css-4.2.9.tgz#aa520e8483275ef824bb1b19d378cc6ca1c41350" + integrity sha512-pjzJ4n5eAXAz/L5Zur4ZymuJUvyo0Uh0iRnRI/1kADFLs76skDky0K0dX1rlv4iXXrJXNk3sxRWVJR7CMDroWA== + dependencies: + "@types/node" "*" + source-map "^0.6.0" + +"@types/connect-history-api-fallback@^1.3.5": + version "1.5.2" + resolved "https://registry.yarnpkg.com/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.2.tgz#acf51e088b3bb6507f7b093bd2b0de20940179cc" + integrity sha512-gX2j9x+NzSh4zOhnRPSdPPmTepS4DfxES0AvIFv3jGv5QyeAJf6u6dY5/BAoAJU9Qq1uTvwOku8SSC2GnCRl6Q== + dependencies: + "@types/express-serve-static-core" "*" + "@types/node" "*" + +"@types/connect@*": + version "3.4.37" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.37.tgz#c66a96689fd3127c8772eb3e9e5c6028ec1a9af5" + integrity sha512-zBUSRqkfZ59OcwXon4HVxhx5oWCJmc0OtBTK05M+p0dYjgN6iTwIL2T/WbsQZrEsdnwaF9cWQ+azOnpPvIqY3Q== + dependencies: + "@types/node" "*" + +"@types/eslint-scope@^3.7.3": + version "3.7.6" + resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.6.tgz#585578b368ed170e67de8aae7b93f54a1b2fdc26" + integrity sha512-zfM4ipmxVKWdxtDaJ3MP3pBurDXOCoyjvlpE3u6Qzrmw4BPbfm4/ambIeTk/r/J0iq/+2/xp0Fmt+gFvXJY2PQ== + dependencies: + "@types/eslint" "*" + "@types/estree" "*" + +"@types/eslint@*": + version "8.44.6" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.44.6.tgz#60e564551966dd255f4c01c459f0b4fb87068603" + integrity sha512-P6bY56TVmX8y9J87jHNgQh43h6VVU+6H7oN7hgvivV81K2XY8qJZ5vqPy/HdUoVIelii2kChYVzQanlswPWVFw== + dependencies: + "@types/estree" "*" + "@types/json-schema" "*" + +"@types/estree@*", "@types/estree@^1.0.0": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.4.tgz#d9748f5742171b26218516cf1828b8eafaf8a9fa" + integrity sha512-2JwWnHK9H+wUZNorf2Zr6ves96WHoWDJIftkcxPKsS7Djta6Zu519LarhRNljPXkpsZR2ZMwNCPeW7omW07BJw== + +"@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.33": + version "4.17.39" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.39.tgz#2107afc0a4b035e6cb00accac3bdf2d76ae408c8" + integrity sha512-BiEUfAiGCOllomsRAZOiMFP7LAnrifHpt56pc4Z7l9K6ACyN06Ns1JLMBxwkfLOjJRlSf06NwWsT7yzfpaVpyQ== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + "@types/send" "*" + +"@types/express@*", "@types/express@^4.17.13": + version "4.17.20" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.20.tgz#e7c9b40276d29e38a4e3564d7a3d65911e2aa433" + integrity sha512-rOaqlkgEvOW495xErXMsmyX3WKBInbhG5eqojXYi3cGUaLoRDlXa5d52fkfWZT963AZ3v2eZ4MbKE6WpDAGVsw== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.33" + "@types/qs" "*" + "@types/serve-static" "*" + +"@types/glob@^7.1.1": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.2.0.tgz#bc1b5bf3aa92f25bd5dd39f35c57361bdce5b2eb" + integrity sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA== + dependencies: + "@types/minimatch" "*" + "@types/node" "*" + +"@types/http-errors@*": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.3.tgz#c54e61f79b3947d040f150abd58f71efb422ff62" + integrity sha512-pP0P/9BnCj1OVvQR2lF41EkDG/lWWnDyA203b/4Fmi2eTyORnBtcDoKDwjWQthELrBvWkMOrvSOnZ8OVlW6tXA== + +"@types/http-proxy@^1.17.8": + version "1.17.13" + resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.13.tgz#dd3a4da550580eb0557d4c7128a2ff1d1a38d465" + integrity sha512-GkhdWcMNiR5QSQRYnJ+/oXzu0+7JJEPC8vkWXK351BkhjraZF+1W13CUYARUvX9+NqIU2n6YHA4iwywsc/M6Sw== + dependencies: + "@types/node" "*" + +"@types/imagemin-gifsicle@^7.0.1": + version "7.0.3" + resolved "https://registry.yarnpkg.com/@types/imagemin-gifsicle/-/imagemin-gifsicle-7.0.3.tgz#588f0f7e1cf723c19d12e6077007ddb460f40711" + integrity sha512-GQBKOk9doOd0Xp7OvO4QDl7U0Vkwk2Ps7J0rxafdAa7wG9lu7idvZTm8TtSZiRtHENdkW88Kz8OjmjMlgeeC5w== + dependencies: + "@types/imagemin" "*" + +"@types/imagemin-mozjpeg@^8.0.1": + version "8.0.3" + resolved "https://registry.yarnpkg.com/@types/imagemin-mozjpeg/-/imagemin-mozjpeg-8.0.3.tgz#da7df35f9fe36bda5e1d010005025ea382ac548f" + integrity sha512-+U/ibETP2/oRqeuaaXa67dEpKHfzmfK0OBVC09AR4c1CIFAKjQ5xY+dxH+fjoMQRlwdcRQLkn/ALtnxSl3Xsqw== + dependencies: + "@types/imagemin" "*" + +"@types/imagemin-optipng@^5.2.1": + version "5.2.3" + resolved "https://registry.yarnpkg.com/@types/imagemin-optipng/-/imagemin-optipng-5.2.3.tgz#c26810f39d1ff0fc16ca13349744843661571c3e" + integrity sha512-Q80ANbJYn+WgKkWVfx9f7/q4LR6qun4NIiuV1eRWCg8KCAmNrU7ZH16a2hGs9kfkFqyJlhBv6oV9SDXe1vL3aQ== + dependencies: + "@types/imagemin" "*" + +"@types/imagemin-svgo@^8.0.0": + version "8.0.1" + resolved "https://registry.yarnpkg.com/@types/imagemin-svgo/-/imagemin-svgo-8.0.1.tgz#03af689b75dbdeb634c2457ba22043530a00d87e" + integrity sha512-YafkdrVAcr38U0Ln1C+L1n4SIZqC47VBHTyxCq7gTUSd1R9MdIvMcrljWlgU1M9O68WZDeQWUrKipKYfEOCOvQ== + dependencies: + "@types/imagemin" "*" + "@types/svgo" "^1" + +"@types/imagemin@*": + version "8.0.3" + resolved "https://registry.yarnpkg.com/@types/imagemin/-/imagemin-8.0.3.tgz#9357a382497d33d592afc582df900f0759d7769f" + integrity sha512-se/hpaYxu5DyvPqmUEwbupmbQSx6JNislk0dkoIgWSmArkj+Ow9pGG9pGz8MRmbQDfGNYNzqwPQKHCUy+K+jpQ== + dependencies: + "@types/node" "*" + +"@types/json-schema@*", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": + version "7.0.14" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.14.tgz#74a97a5573980802f32c8e47b663530ab3b6b7d1" + integrity sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw== + +"@types/json5@^0.0.29": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" + integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== + +"@types/mime@*": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.3.tgz#886674659ce55fe7c6c06ec5ca7c0eb276a08f91" + integrity sha512-i8MBln35l856k5iOhKk2XJ4SeAWg75mLIpZB4v6imOagKL6twsukBZGDMNhdOVk7yRFTMPpfILocMos59Q1otQ== + +"@types/mime@^1": + version "1.3.4" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.4.tgz#a4ed836e069491414bab92c31fdea9e557aca0d9" + integrity sha512-1Gjee59G25MrQGk8bsNvC6fxNiRgUlGn2wlhGf95a59DrprnnHk80FIMMFG9XHMdrfsuA119ht06QPDXA1Z7tw== + +"@types/minimatch@*": + version "5.1.2" + resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca" + integrity sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA== + +"@types/node-forge@^1.3.0": + version "1.3.8" + resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-1.3.8.tgz#044ad98354ff309a031a55a40ad122f3be1ac2bb" + integrity sha512-vGXshY9vim9CJjrpcS5raqSjEfKlJcWy2HNdgUasR66fAnVEYarrf1ULV4nfvpC1nZq/moA9qyqBcu83x+Jlrg== + dependencies: + "@types/node" "*" + +"@types/node@*": + version "20.8.10" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.8.10.tgz#a5448b895c753ae929c26ce85cab557c6d4a365e" + integrity sha512-TlgT8JntpcbmKUFzjhsyhGfP2fsiz1Mv56im6enJ905xG1DAYesxJaeSbGqQmAw8OWPdhyJGhGSQGKRNJ45u9w== + dependencies: + undici-types "~5.26.4" + +"@types/node@^14.14.31": + version "14.18.63" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.63.tgz#1788fa8da838dbb5f9ea994b834278205db6ca2b" + integrity sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ== + +"@types/parse-json@^4.0.0": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.1.tgz#27f7559836ad796cea31acb63163b203756a5b4e" + integrity sha512-3YmXzzPAdOTVljVMkTMBdBEvlOLg2cDQaDhnnhT3nT9uDbnJzjWhKlzb+desT12Y7tGqaN6d+AbozcKzyL36Ng== + +"@types/qs@*": + version "6.9.9" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.9.tgz#66f7b26288f6799d279edf13da7ccd40d2fa9197" + integrity sha512-wYLxw35euwqGvTDx6zfY1vokBFnsK0HNrzc6xNHchxfO2hpuRg74GbkEW7e3sSmPvj0TjCDT1VCa6OtHXnubsg== + +"@types/range-parser@*": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.6.tgz#7cb33992049fd7340d5b10c0098e104184dfcd2a" + integrity sha512-+0autS93xyXizIYiyL02FCY8N+KkKPhILhcUSA276HxzreZ16kl+cmwvV2qAM/PuCCwPXzOXOWhiPcw20uSFcA== + +"@types/retry@0.12.0": + version "0.12.0" + resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" + integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== + +"@types/send@*": + version "0.17.3" + resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.3.tgz#81b2ea5a3a18aad357405af2d643ccbe5a09020b" + integrity sha512-/7fKxvKUoETxjFUsuFlPB9YndePpxxRAOfGC/yJdc9kTjTeP5kRCTzfnE8kPUKCeyiyIZu0YQ76s50hCedI1ug== + dependencies: + "@types/mime" "^1" + "@types/node" "*" + +"@types/serve-index@^1.9.1": + version "1.9.3" + resolved "https://registry.yarnpkg.com/@types/serve-index/-/serve-index-1.9.3.tgz#af9403916eb6fbf7d6ec6f47b2a4c46eb3222cc9" + integrity sha512-4KG+yMEuvDPRrYq5fyVm/I2uqAJSAwZK9VSa+Zf+zUq9/oxSSvy3kkIqyL+jjStv6UCVi8/Aho0NHtB1Fwosrg== + dependencies: + "@types/express" "*" + +"@types/serve-static@*", "@types/serve-static@^1.13.10": + version "1.15.4" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.4.tgz#44b5895a68ca637f06c229119e1c774ca88f81b2" + integrity sha512-aqqNfs1XTF0HDrFdlY//+SGUxmdSUbjeRXb5iaZc3x0/vMbYmdw9qvOgHWOyyLFxSSRnUuP5+724zBgfw8/WAw== + dependencies: + "@types/http-errors" "*" + "@types/mime" "*" + "@types/node" "*" + +"@types/sinonjs__fake-timers@^6.0.2": + version "6.0.4" + resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.4.tgz#0ecc1b9259b76598ef01942f547904ce61a6a77d" + integrity sha512-IFQTJARgMUBF+xVd2b+hIgXWrZEjND3vJtRCvIelcFB5SIXfjV4bOHbHJ0eXKh+0COrBRc8MqteKAz/j88rE0A== + +"@types/sizzle@^2.3.2": + version "2.3.5" + resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.5.tgz#d93dd29cdcd5801d90be968073b09a6b370780e4" + integrity sha512-tAe4Q+OLFOA/AMD+0lq8ovp8t3ysxAOeaScnfNdZpUxaGl51ZMDEITxkvFl1STudQ58mz6gzVGl9VhMKhwRnZQ== + +"@types/sockjs@^0.3.33": + version "0.3.35" + resolved "https://registry.yarnpkg.com/@types/sockjs/-/sockjs-0.3.35.tgz#f4a568c73d2a8071944bd6ffdca0d4e66810cd21" + integrity sha512-tIF57KB+ZvOBpAQwSaACfEu7htponHXaFzP7RfKYgsOS0NoYnn+9+jzp7bbq4fWerizI3dTB4NfAZoyeQKWJLw== + dependencies: + "@types/node" "*" + +"@types/svgo@^1": + version "1.3.6" + resolved "https://registry.yarnpkg.com/@types/svgo/-/svgo-1.3.6.tgz#9db00a7ddf9b26ad2feb6b834bef1818677845e1" + integrity sha512-AZU7vQcy/4WFEuwnwsNsJnFwupIpbllH1++LXScN6uxT1Z4zPzdrWG97w4/I7eFKFTvfy/bHFStWjdBAg2Vjug== + +"@types/ws@^8.5.5": + version "8.5.8" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.8.tgz#13efec7bd439d0bdf2af93030804a94f163b1430" + integrity sha512-flUksGIQCnJd6sZ1l5dqCEG/ksaoAg/eUwiLAGTJQcfgvZJKF++Ta4bJA6A5aPSJmsr+xlseHn4KLgVlNnvPTg== + dependencies: + "@types/node" "*" + +"@types/yauzl@^2.9.1": + version "2.10.2" + resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.2.tgz#dab926ef9b41a898bc943f11bca6b0bad6d4b729" + integrity sha512-Km7XAtUIduROw7QPgvcft0lIupeG8a8rdKL8RiSyKvlE7dYY31fEn41HVuQsRFDuROA8tA4K2UVL+WdfFmErBA== + dependencies: + "@types/node" "*" + +"@ungap/promise-all-settled@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44" + integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q== + +"@vue/compiler-sfc@2.7.15": + version "2.7.15" + resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-2.7.15.tgz#62135fb2f69559fc723fd9c56b8e8b0ac7864a0b" + integrity sha512-FCvIEevPmgCgqFBH7wD+3B97y7u7oj/Wr69zADBf403Tui377bThTjBvekaZvlRr4IwUAu3M6hYZeULZFJbdYg== + dependencies: + "@babel/parser" "^7.18.4" + postcss "^8.4.14" + source-map "^0.6.1" + +"@vue/component-compiler-utils@^3.1.0": + version "3.3.0" + resolved "https://registry.yarnpkg.com/@vue/component-compiler-utils/-/component-compiler-utils-3.3.0.tgz#f9f5fb53464b0c37b2c8d2f3fbfe44df60f61dc9" + integrity sha512-97sfH2mYNU+2PzGrmK2haqffDpVASuib9/w2/noxiFi31Z54hW+q3izKQXXQZSNhtiUpAI36uSuYepeBe4wpHQ== + dependencies: + consolidate "^0.15.1" + hash-sum "^1.0.2" + lru-cache "^4.1.2" + merge-source-map "^1.1.0" + postcss "^7.0.36" + postcss-selector-parser "^6.0.2" + source-map "~0.6.1" + vue-template-es2015-compiler "^1.9.0" + optionalDependencies: + prettier "^1.18.2 || ^2.0.0" + +"@webassemblyjs/ast@1.11.6", "@webassemblyjs/ast@^1.11.5": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.6.tgz#db046555d3c413f8966ca50a95176a0e2c642e24" + integrity sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q== + dependencies: + "@webassemblyjs/helper-numbers" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + +"@webassemblyjs/floating-point-hex-parser@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz#dacbcb95aff135c8260f77fa3b4c5fea600a6431" + integrity sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw== + +"@webassemblyjs/helper-api-error@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz#6132f68c4acd59dcd141c44b18cbebbd9f2fa768" + integrity sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q== + +"@webassemblyjs/helper-buffer@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz#b66d73c43e296fd5e88006f18524feb0f2c7c093" + integrity sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA== + +"@webassemblyjs/helper-numbers@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz#cbce5e7e0c1bd32cf4905ae444ef64cea919f1b5" + integrity sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g== + dependencies: + "@webassemblyjs/floating-point-hex-parser" "1.11.6" + "@webassemblyjs/helper-api-error" "1.11.6" + "@xtuc/long" "4.2.2" + +"@webassemblyjs/helper-wasm-bytecode@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz#bb2ebdb3b83aa26d9baad4c46d4315283acd51e9" + integrity sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA== + +"@webassemblyjs/helper-wasm-section@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz#ff97f3863c55ee7f580fd5c41a381e9def4aa577" + integrity sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-buffer" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/wasm-gen" "1.11.6" + +"@webassemblyjs/ieee754@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz#bb665c91d0b14fffceb0e38298c329af043c6e3a" + integrity sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg== + dependencies: + "@xtuc/ieee754" "^1.2.0" + +"@webassemblyjs/leb128@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.6.tgz#70e60e5e82f9ac81118bc25381a0b283893240d7" + integrity sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ== + dependencies: + "@xtuc/long" "4.2.2" + +"@webassemblyjs/utf8@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.6.tgz#90f8bc34c561595fe156603be7253cdbcd0fab5a" + integrity sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA== + +"@webassemblyjs/wasm-edit@^1.11.5": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz#c72fa8220524c9b416249f3d94c2958dfe70ceab" + integrity sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-buffer" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/helper-wasm-section" "1.11.6" + "@webassemblyjs/wasm-gen" "1.11.6" + "@webassemblyjs/wasm-opt" "1.11.6" + "@webassemblyjs/wasm-parser" "1.11.6" + "@webassemblyjs/wast-printer" "1.11.6" + +"@webassemblyjs/wasm-gen@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz#fb5283e0e8b4551cc4e9c3c0d7184a65faf7c268" + integrity sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/ieee754" "1.11.6" + "@webassemblyjs/leb128" "1.11.6" + "@webassemblyjs/utf8" "1.11.6" + +"@webassemblyjs/wasm-opt@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz#d9a22d651248422ca498b09aa3232a81041487c2" + integrity sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-buffer" "1.11.6" + "@webassemblyjs/wasm-gen" "1.11.6" + "@webassemblyjs/wasm-parser" "1.11.6" + +"@webassemblyjs/wasm-parser@1.11.6", "@webassemblyjs/wasm-parser@^1.11.5": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz#bb85378c527df824004812bbdb784eea539174a1" + integrity sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@webassemblyjs/helper-api-error" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/ieee754" "1.11.6" + "@webassemblyjs/leb128" "1.11.6" + "@webassemblyjs/utf8" "1.11.6" + +"@webassemblyjs/wast-printer@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz#a7bf8dd7e362aeb1668ff43f35cb849f188eff20" + integrity sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A== + dependencies: + "@webassemblyjs/ast" "1.11.6" + "@xtuc/long" "4.2.2" + +"@webpack-cli/configtest@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-1.2.0.tgz#7b20ce1c12533912c3b217ea68262365fa29a6f5" + integrity sha512-4FB8Tj6xyVkyqjj1OaTqCjXYULB9FMkqQ8yGrZjRDrYh0nOE+7Lhs45WioWQQMV+ceFlE368Ukhe6xdvJM9Egg== + +"@webpack-cli/info@^1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-1.5.0.tgz#6c78c13c5874852d6e2dd17f08a41f3fe4c261b1" + integrity sha512-e8tSXZpw2hPl2uMJY6fsMswaok5FdlGNRTktvFk2sD8RjH0hE2+XistawJx1vmKteh4NmGmNUrp+Tb2w+udPcQ== + dependencies: + envinfo "^7.7.3" + +"@webpack-cli/serve@^1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-1.7.0.tgz#e1993689ac42d2b16e9194376cfb6753f6254db1" + integrity sha512-oxnCNGj88fL+xzV+dacXs44HcDwf1ovs3AuEzvP7mqXw7fQntqIhQ1BRmynh4qEKQSSSRSWVyXRjmTbZIX9V2Q== + +"@xtuc/ieee754@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" + integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== + +"@xtuc/long@4.2.2": + version "4.2.2" + resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" + integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== + +accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +acorn-import-assertions@^1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz#507276249d684797c84e0734ef84860334cfb1ac" + integrity sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA== + +acorn-jsx@^5.2.0, acorn-jsx@^5.3.1: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + +acorn@^7.1.1, acorn@^7.4.0: + version "7.4.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" + integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== + +acorn@^8.7.1, acorn@^8.8.2: + version "8.11.2" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.2.tgz#ca0d78b51895be5390a5903c5b3bdcdaf78ae40b" + integrity sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w== + +adjust-sourcemap-loader@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz#fc4a0fd080f7d10471f30a7320f25560ade28c99" + integrity sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A== + dependencies: + loader-utils "^2.0.0" + regex-parser "^2.2.11" + +aggregate-error@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" + integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== + dependencies: + clean-stack "^2.0.0" + indent-string "^4.0.0" + +ajv-formats@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" + integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== + dependencies: + ajv "^8.0.0" + +ajv-keywords@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" + integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== + +ajv-keywords@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz#69d4d385a4733cdbeab44964a1170a88f87f0e16" + integrity sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw== + dependencies: + fast-deep-equal "^3.1.3" + +ajv@^6.10.0, ajv@^6.12.4, ajv@^6.12.5: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ajv@^8.0.0, ajv@^8.0.1, ajv@^8.9.0: + version "8.12.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1" + integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + +animate.css@^4.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/animate.css/-/animate.css-4.1.1.tgz#614ec5a81131d7e4dc362a58143f7406abd68075" + integrity sha512-+mRmCTv6SbCmtYJCN4faJMNFVNN5EuCTTprDTAo7YzIGji2KADmakjVA3+8mVDkZ2Bf09vayB35lSQIex2+QaQ== + +ansi-colors@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" + integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== + +ansi-colors@^4.1.1: + version "4.1.3" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" + integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== + +ansi-escapes@^4.3.0: + version "4.3.2" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + +ansi-html-community@^0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/ansi-html-community/-/ansi-html-community-0.0.8.tgz#69fbc4d6ccbe383f9736934ae34c3f8290f1bf41" + integrity sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw== + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +arch@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/arch/-/arch-2.2.0.tgz#1bc47818f305764f23ab3306b0bfc086c5a29d11" + integrity sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ== + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +array-buffer-byte-length@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz#fabe8bc193fea865f317fe7807085ee0dee5aead" + integrity sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A== + dependencies: + call-bind "^1.0.2" + is-array-buffer "^3.0.1" + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + +array-flatten@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.2.tgz#24ef80a28c1a893617e2149b0c6d0d788293b099" + integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ== + +array-includes@^3.1.7: + version "3.1.7" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.7.tgz#8cd2e01b26f7a3086cbc87271593fe921c62abda" + integrity sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + get-intrinsic "^1.2.1" + is-string "^1.0.7" + +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +array.prototype.findlastindex@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.3.tgz#b37598438f97b579166940814e2c0493a4f50207" + integrity sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + es-shim-unscopables "^1.0.0" + get-intrinsic "^1.2.1" + +array.prototype.flat@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz#1476217df8cff17d72ee8f3ba06738db5b387d18" + integrity sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + es-shim-unscopables "^1.0.0" + +array.prototype.flatmap@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz#c9a7c6831db8e719d6ce639190146c24bbd3e527" + integrity sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + es-shim-unscopables "^1.0.0" + +arraybuffer.prototype.slice@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz#98bd561953e3e74bb34938e77647179dfe6e9f12" + integrity sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw== + dependencies: + array-buffer-byte-length "^1.0.0" + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + get-intrinsic "^1.2.1" + is-array-buffer "^3.0.2" + is-shared-array-buffer "^1.0.2" + +asn1.js@^5.2.0: + version "5.4.1" + resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-5.4.1.tgz#11a980b84ebb91781ce35b0fdc2ee294e3783f07" + integrity sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA== + dependencies: + bn.js "^4.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + safer-buffer "^2.1.0" + +asn1@~0.2.3: + version "0.2.6" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d" + integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ== + dependencies: + safer-buffer "~2.1.0" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + integrity sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw== + +assert@^1.1.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/assert/-/assert-1.5.1.tgz#038ab248e4ff078e7bc2485ba6e6388466c78f76" + integrity sha512-zzw1uCAgLbsKwBfFc8CX78DDg+xZeBksSO3vwVIDDN5i94eOrPsSSyiVhmsSABFDM/OcpE2aagCat9dnWQLG1A== + dependencies: + object.assign "^4.1.4" + util "^0.10.4" + +astral-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" + integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== + +async@^3.2.0: + version "3.2.4" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c" + integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ== + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +at-least-node@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" + integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== + +autoprefixer@^10.4.0: + version "10.4.16" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.16.tgz#fad1411024d8670880bdece3970aa72e3572feb8" + integrity sha512-7vd3UC6xKp0HLfua5IjZlcXvGAGy7cBAXTg2lyQ/8WpNhd6SiZ8Be+xm3FyBSYJx5GKcpRCzBh7RH4/0dnY+uQ== + dependencies: + browserslist "^4.21.10" + caniuse-lite "^1.0.30001538" + fraction.js "^4.3.6" + normalize-range "^0.1.2" + picocolors "^1.0.0" + postcss-value-parser "^4.2.0" + +available-typed-arrays@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" + integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== + +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + integrity sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA== + +aws4@^1.8.0: + version "1.12.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.12.0.tgz#ce1c9d143389679e253b314241ea9aa5cec980d3" + integrity sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg== + +axios@^0.21: + version "0.21.4" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" + integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg== + dependencies: + follow-redirects "^1.14.0" + +babel-loader@^8.2.3: + version "8.3.0" + resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.3.0.tgz#124936e841ba4fe8176786d6ff28add1f134d6a8" + integrity sha512-H8SvsMF+m9t15HNLMipppzkC+Y2Yq+v3SonZyU70RBL/h1gxPkH08Ot8pEE9Z4Kd+czyWJClmFS8qzIP9OZ04Q== + dependencies: + find-cache-dir "^3.3.1" + loader-utils "^2.0.0" + make-dir "^3.1.0" + schema-utils "^2.6.5" + +babel-plugin-polyfill-corejs2@^0.4.6: + version "0.4.6" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.6.tgz#b2df0251d8e99f229a8e60fc4efa9a68b41c8313" + integrity sha512-jhHiWVZIlnPbEUKSSNb9YoWcQGdlTLq7z1GHL4AjFxaoOUMuuEVJ+Y4pAaQUGOGk93YsVCKPbqbfw3m0SM6H8Q== + dependencies: + "@babel/compat-data" "^7.22.6" + "@babel/helper-define-polyfill-provider" "^0.4.3" + semver "^6.3.1" + +babel-plugin-polyfill-corejs3@^0.8.5: + version "0.8.6" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.6.tgz#25c2d20002da91fe328ff89095c85a391d6856cf" + integrity sha512-leDIc4l4tUgU7str5BWLS2h8q2N4Nf6lGZP6UrNDxdtfF2g69eJ5L0H7S8A5Ln/arfFAfHor5InAdZuIOwZdgQ== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.4.3" + core-js-compat "^3.33.1" + +babel-plugin-polyfill-regenerator@^0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.3.tgz#d4c49e4b44614607c13fb769bcd85c72bb26a4a5" + integrity sha512-8sHeDOmXC8csczMrYEOf0UTNa4yE2SxV5JGeT/LP1n0OYVDUUFPxG9vdk2AlDlIit4t+Kf0xCtpgXPBwnn/9pw== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.4.3" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +base64-js@^1.0.2: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +batch@0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" + integrity sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw== + +bcrypt-pbkdf@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + integrity sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w== + dependencies: + tweetnacl "^0.14.3" + +big.js@^5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" + integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== + +binary-extensions@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" + integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + +blob-util@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/blob-util/-/blob-util-2.0.2.tgz#3b4e3c281111bb7f11128518006cdc60b403a1eb" + integrity sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ== + +bluebird@^3.1.1, bluebird@^3.7.2: + version "3.7.2" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" + integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== + +bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.11.9: + version "4.12.0" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" + integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== + +bn.js@^5.0.0, bn.js@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70" + integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ== + +body-parser@1.20.1: + version "1.20.1" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668" + integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== + dependencies: + bytes "3.1.2" + content-type "~1.0.4" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.11.0" + raw-body "2.5.1" + type-is "~1.6.18" + unpipe "1.0.0" + +bonjour-service@^1.0.11: + version "1.1.1" + resolved "https://registry.yarnpkg.com/bonjour-service/-/bonjour-service-1.1.1.tgz#960948fa0e0153f5d26743ab15baf8e33752c135" + integrity sha512-Z/5lQRMOG9k7W+FkeGTNjh7htqn/2LMnfOvBZ8pynNZCM9MwkQkI3zeI4oz09uWdcgmgHugVvBqxGg4VQJ5PCg== + dependencies: + array-flatten "^2.1.2" + dns-equal "^1.0.0" + fast-deep-equal "^3.1.3" + multicast-dns "^7.2.5" + +boolbase@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== + +bootstrap@^4.6: + version "4.6.2" + resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.6.2.tgz#8e0cd61611728a5bf65a3a2b8d6ff6c77d5d7479" + integrity sha512-51Bbp/Uxr9aTuy6ca/8FbFloBUJZLHwnhTcnjIeRn2suQWsWzcuJhGjKDB5eppVte/8oCdOL3VuwxvZDUggwGQ== + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^3.0.2, braces@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +brorand@^1.0.1, brorand@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" + integrity sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w== + +browser-stdout@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" + integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== + +browserify-aes@^1.0.0, browserify-aes@^1.0.4: + version "1.2.0" + resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48" + integrity sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA== + dependencies: + buffer-xor "^1.0.3" + cipher-base "^1.0.0" + create-hash "^1.1.0" + evp_bytestokey "^1.0.3" + inherits "^2.0.1" + safe-buffer "^5.0.1" + +browserify-cipher@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/browserify-cipher/-/browserify-cipher-1.0.1.tgz#8d6474c1b870bfdabcd3bcfcc1934a10e94f15f0" + integrity sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w== + dependencies: + browserify-aes "^1.0.4" + browserify-des "^1.0.0" + evp_bytestokey "^1.0.0" + +browserify-des@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/browserify-des/-/browserify-des-1.0.2.tgz#3af4f1f59839403572f1c66204375f7a7f703e9c" + integrity sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A== + dependencies: + cipher-base "^1.0.1" + des.js "^1.0.0" + inherits "^2.0.1" + safe-buffer "^5.1.2" + +browserify-rsa@^4.0.0, browserify-rsa@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.1.0.tgz#b2fd06b5b75ae297f7ce2dc651f918f5be158c8d" + integrity sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog== + dependencies: + bn.js "^5.0.0" + randombytes "^2.0.1" + +browserify-sign@^4.0.0: + version "4.2.2" + resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.2.tgz#e78d4b69816d6e3dd1c747e64e9947f9ad79bc7e" + integrity sha512-1rudGyeYY42Dk6texmv7c4VcQ0EsvVbLwZkA+AQB7SxvXxmcD93jcHie8bzecJ+ChDlmAm2Qyu0+Ccg5uhZXCg== + dependencies: + bn.js "^5.2.1" + browserify-rsa "^4.1.0" + create-hash "^1.2.0" + create-hmac "^1.1.7" + elliptic "^6.5.4" + inherits "^2.0.4" + parse-asn1 "^5.1.6" + readable-stream "^3.6.2" + safe-buffer "^5.2.1" + +browserify-zlib@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/browserify-zlib/-/browserify-zlib-0.2.0.tgz#2869459d9aa3be245fe8fe2ca1f46e2e7f54d73f" + integrity sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA== + dependencies: + pako "~1.0.5" + +browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.21.10, browserslist@^4.21.4, browserslist@^4.21.9, browserslist@^4.22.1: + version "4.22.1" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.22.1.tgz#ba91958d1a59b87dab6fed8dfbcb3da5e2e9c619" + integrity sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ== + dependencies: + caniuse-lite "^1.0.30001541" + electron-to-chromium "^1.4.535" + node-releases "^2.0.13" + update-browserslist-db "^1.0.13" + +buffer-crc32@~0.2.3: + version "0.2.13" + resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" + integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +buffer-xor@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" + integrity sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ== + +buffer@^4.3.0: + version "4.9.2" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8" + integrity sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg== + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + isarray "^1.0.0" + +builtin-status-codes@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" + integrity sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ== + +bytes@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" + integrity sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw== + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +cachedir@^2.3.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/cachedir/-/cachedir-2.4.0.tgz#7fef9cf7367233d7c88068fe6e34ed0d355a610d" + integrity sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ== + +call-bind@^1.0.0, call-bind@^1.0.2, call-bind@^1.0.4, call-bind@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.5.tgz#6fa2b7845ce0ea49bf4d8b9ef64727a2c2e2e513" + integrity sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ== + dependencies: + function-bind "^1.1.2" + get-intrinsic "^1.2.1" + set-function-length "^1.1.1" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +camel-case@^4.1.1: + version "4.1.2" + resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-4.1.2.tgz#9728072a954f805228225a6deea6b38461e1bd5a" + integrity sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw== + dependencies: + pascal-case "^3.1.2" + tslib "^2.0.3" + +camelcase@^6.0.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + +caniuse-api@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0" + integrity sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw== + dependencies: + browserslist "^4.0.0" + caniuse-lite "^1.0.0" + lodash.memoize "^4.1.2" + lodash.uniq "^4.5.0" + +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001538, caniuse-lite@^1.0.30001541: + version "1.0.30001559" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001559.tgz#95a982440d3d314c471db68d02664fb7536c5a30" + integrity sha512-cPiMKZgqgkg5LY3/ntGeLFUpi6tzddBNS58A4tnTgQw1zON7u2sZMU7SzOeVH4tj20++9ggL+V6FDOFMTaFFYA== + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== + +chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +charenc@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" + integrity sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA== + +check-more-types@^2.24.0: + version "2.24.0" + resolved "https://registry.yarnpkg.com/check-more-types/-/check-more-types-2.24.0.tgz#1420ffb10fd444dcfc79b43891bbfffd32a84600" + integrity sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA== + +chokidar@3.5.3, "chokidar@>=3.0.0 <4.0.0", chokidar@^3.5.2, chokidar@^3.5.3: + version "3.5.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +chrome-trace-event@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" + integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg== + +ci-info@^3.2.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" + integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== + +cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" + integrity sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q== + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +clean-css@^4.2.3: + version "4.2.4" + resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.4.tgz#733bf46eba4e607c6891ea57c24a989356831178" + integrity sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A== + dependencies: + source-map "~0.6.0" + +clean-css@^5.2.4: + version "5.3.2" + resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-5.3.2.tgz#70ecc7d4d4114921f5d298349ff86a31a9975224" + integrity sha512-JVJbM+f3d3Q704rF4bqQ5UUyTtuJ0JRKNbTKVEeujCCBoMdkEi+V+e8oktO9qGQNSvHrFTM6JZRXrUvGR1czww== + dependencies: + source-map "~0.6.0" + +clean-stack@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" + integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== + +cli-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" + integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== + dependencies: + restore-cursor "^3.1.0" + +cli-table3@^0.6.0, cli-table3@~0.6.0: + version "0.6.3" + resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.6.3.tgz#61ab765aac156b52f222954ffc607a6f01dbeeb2" + integrity sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg== + dependencies: + string-width "^4.2.0" + optionalDependencies: + "@colors/colors" "1.5.0" + +cli-truncate@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7" + integrity sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg== + dependencies: + slice-ansi "^3.0.0" + string-width "^4.2.0" + +clipboard@^2.0.0: + version "2.0.11" + resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-2.0.11.tgz#62180360b97dd668b6b3a84ec226975762a70be5" + integrity sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw== + dependencies: + good-listener "^1.2.2" + select "^1.1.2" + tiny-emitter "^2.0.0" + +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + +cliui@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" + integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.1" + wrap-ansi "^7.0.0" + +clone-deep@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" + integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== + dependencies: + is-plain-object "^2.0.4" + kind-of "^6.0.2" + shallow-clone "^3.0.0" + +collect.js@^4.28.5: + version "4.36.1" + resolved "https://registry.yarnpkg.com/collect.js/-/collect.js-4.36.1.tgz#0194c52e90e01db6f136d28e7a3cf88c5687894d" + integrity sha512-jd97xWPKgHn6uvK31V6zcyPd40lUJd7gpYxbN2VOVxGWO4tyvS9Li4EpsFjXepGTo2tYcOTC4a8YsbQXMJ4XUw== + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +colord@^2.9.1: + version "2.9.3" + resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.3.tgz#4f8ce919de456f1d5c1c368c307fe20f3e59fb43" + integrity sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw== + +colorette@^2.0.10, colorette@^2.0.14, colorette@^2.0.16: + version "2.0.20" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" + integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== + +combined-stream@^1.0.6, combined-stream@~1.0.6: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +commander@^2.20.0, commander@^2.9.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +commander@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" + integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== + +commander@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" + integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== + +commander@^6.0.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" + integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== + +commander@^7.0.0, commander@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" + integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== + +common-tags@^1.8.0: + version "1.8.2" + resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.2.tgz#94ebb3c076d26032745fd54face7f688ef5ac9c6" + integrity sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA== + +commondir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" + integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg== + +compressible@~2.0.16: + version "2.0.18" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" + integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== + dependencies: + mime-db ">= 1.43.0 < 2" + +compression@^1.7.4: + version "1.7.4" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f" + integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== + dependencies: + accepts "~1.3.5" + bytes "3.0.0" + compressible "~2.0.16" + debug "2.6.9" + on-headers "~1.0.2" + safe-buffer "5.1.2" + vary "~1.1.2" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +concat@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/concat/-/concat-1.0.3.tgz#40f3353089d65467695cb1886b45edd637d8cca8" + integrity sha512-f/ZaH1aLe64qHgTILdldbvyfGiGF4uzeo9IuXUloIOLQzFmIPloy9QbZadNsuVv0j5qbKQvQb/H/UYf2UsKTpw== + dependencies: + commander "^2.9.0" + +connect-history-api-fallback@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz#647264845251a0daf25b97ce87834cace0f5f1c8" + integrity sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA== + +consola@^2.15.3: + version "2.15.3" + resolved "https://registry.yarnpkg.com/consola/-/consola-2.15.3.tgz#2e11f98d6a4be71ff72e0bdf07bd23e12cb61550" + integrity sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw== + +console-browserify@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.2.0.tgz#67063cef57ceb6cf4993a2ab3a55840ae8c49336" + integrity sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA== + +consolidate@^0.15.1: + version "0.15.1" + resolved "https://registry.yarnpkg.com/consolidate/-/consolidate-0.15.1.tgz#21ab043235c71a07d45d9aad98593b0dba56bab7" + integrity sha512-DW46nrsMJgy9kqAbPt5rKaCr7uFtpo4mSUvLHIUbJEjm0vo+aY5QLwBUq3FK4tRnJr/X0Psc0C4jf/h+HtXSMw== + dependencies: + bluebird "^3.1.1" + +constants-browserify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" + integrity sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ== + +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +convert-source-map@^1.7.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" + integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== + +convert-source-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" + integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" + integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== + +core-js-compat@^3.31.0, core-js-compat@^3.33.1: + version "3.33.2" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.33.2.tgz#3ea4563bfd015ad4e4b52442865b02c62aba5085" + integrity sha512-axfo+wxFVxnqf8RvxTzoAlzW4gRoacrHeoFlc9n0x50+7BEyZL/Rt3hicaED1/CEd7I6tPCPVUYcJwCMO5XUYw== + dependencies: + browserslist "^4.22.1" + +core-js@^3.6.4: + version "3.33.2" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.33.2.tgz#312bbf6996a3a517c04c99b9909cdd27138d1ceb" + integrity sha512-XeBzWI6QL3nJQiHmdzbAOiMYqjrb7hwU7A39Qhvd/POSa/t9E1AeZyEZx3fNvp/vtM8zXwhoL0FsiS0hD0pruQ== + +core-util-is@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ== + +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + +cosmiconfig@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.1.0.tgz#1443b9afa596b670082ea46cbd8f6a62b84635f6" + integrity sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.2.1" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.10.0" + +create-ecdh@^4.0.0: + version "4.0.4" + resolved "https://registry.yarnpkg.com/create-ecdh/-/create-ecdh-4.0.4.tgz#d6e7f4bffa66736085a0762fd3a632684dabcc4e" + integrity sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A== + dependencies: + bn.js "^4.1.0" + elliptic "^6.5.3" + +create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" + integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg== + dependencies: + cipher-base "^1.0.1" + inherits "^2.0.1" + md5.js "^1.3.4" + ripemd160 "^2.0.1" + sha.js "^2.4.0" + +create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff" + integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg== + dependencies: + cipher-base "^1.0.3" + create-hash "^1.1.0" + inherits "^2.0.1" + ripemd160 "^2.0.0" + safe-buffer "^5.0.1" + sha.js "^2.4.8" + +cross-env@^7.0: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" + integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw== + dependencies: + cross-spawn "^7.0.1" + +cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +crypt@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" + integrity sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow== + +crypto-browserify@^3.11.0: + version "3.12.0" + resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" + integrity sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg== + dependencies: + browserify-cipher "^1.0.0" + browserify-sign "^4.0.0" + create-ecdh "^4.0.0" + create-hash "^1.1.0" + create-hmac "^1.1.0" + diffie-hellman "^5.0.0" + inherits "^2.0.1" + pbkdf2 "^3.0.3" + public-encrypt "^4.0.0" + randombytes "^2.0.0" + randomfill "^1.0.3" + +css-declaration-sorter@^6.3.1: + version "6.4.1" + resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-6.4.1.tgz#28beac7c20bad7f1775be3a7129d7eae409a3a71" + integrity sha512-rtdthzxKuyq6IzqX6jEcIzQF/YqccluefyCYheovBOLhFT/drQA9zj/UbRAa9J7C0o6EG6u3E6g+vKkay7/k3g== + +css-loader@^5.2.6: + version "5.2.7" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-5.2.7.tgz#9b9f111edf6fb2be5dc62525644cbc9c232064ae" + integrity sha512-Q7mOvpBNBG7YrVGMxRxcBJZFL75o+cH2abNASdibkj/fffYD8qWbInZrD0S9ccI6vZclF3DsHE7njGlLtaHbhg== + dependencies: + icss-utils "^5.1.0" + loader-utils "^2.0.0" + postcss "^8.2.15" + postcss-modules-extract-imports "^3.0.0" + postcss-modules-local-by-default "^4.0.0" + postcss-modules-scope "^3.0.0" + postcss-modules-values "^4.0.0" + postcss-value-parser "^4.1.0" + schema-utils "^3.0.0" + semver "^7.3.5" + +css-select@^4.1.3: + version "4.3.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b" + integrity sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ== + dependencies: + boolbase "^1.0.0" + css-what "^6.0.1" + domhandler "^4.3.1" + domutils "^2.8.0" + nth-check "^2.0.1" + +css-tree@^1.1.2, css-tree@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d" + integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q== + dependencies: + mdn-data "2.0.14" + source-map "^0.6.1" + +css-what@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" + integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== + +cssesc@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" + integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== + +cssnano-preset-default@^5.2.14: + version "5.2.14" + resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-5.2.14.tgz#309def4f7b7e16d71ab2438052093330d9ab45d8" + integrity sha512-t0SFesj/ZV2OTylqQVOrFgEh5uanxbO6ZAdeCrNsUQ6fVuXwYTxJPNAGvGTxHbD68ldIJNec7PyYZDBrfDQ+6A== + dependencies: + css-declaration-sorter "^6.3.1" + cssnano-utils "^3.1.0" + postcss-calc "^8.2.3" + postcss-colormin "^5.3.1" + postcss-convert-values "^5.1.3" + postcss-discard-comments "^5.1.2" + postcss-discard-duplicates "^5.1.0" + postcss-discard-empty "^5.1.1" + postcss-discard-overridden "^5.1.0" + postcss-merge-longhand "^5.1.7" + postcss-merge-rules "^5.1.4" + postcss-minify-font-values "^5.1.0" + postcss-minify-gradients "^5.1.1" + postcss-minify-params "^5.1.4" + postcss-minify-selectors "^5.2.1" + postcss-normalize-charset "^5.1.0" + postcss-normalize-display-values "^5.1.0" + postcss-normalize-positions "^5.1.1" + postcss-normalize-repeat-style "^5.1.1" + postcss-normalize-string "^5.1.0" + postcss-normalize-timing-functions "^5.1.0" + postcss-normalize-unicode "^5.1.1" + postcss-normalize-url "^5.1.0" + postcss-normalize-whitespace "^5.1.1" + postcss-ordered-values "^5.1.3" + postcss-reduce-initial "^5.1.2" + postcss-reduce-transforms "^5.1.0" + postcss-svgo "^5.1.0" + postcss-unique-selectors "^5.1.1" + +cssnano-utils@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cssnano-utils/-/cssnano-utils-3.1.0.tgz#95684d08c91511edfc70d2636338ca37ef3a6861" + integrity sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA== + +cssnano@^5.0.8: + version "5.1.15" + resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-5.1.15.tgz#ded66b5480d5127fcb44dac12ea5a983755136bf" + integrity sha512-j+BKgDcLDQA+eDifLx0EO4XSA56b7uut3BQFH+wbSaSTuGLuiyTa/wbRYthUXX8LC9mLg+WWKe8h+qJuwTAbHw== + dependencies: + cssnano-preset-default "^5.2.14" + lilconfig "^2.0.3" + yaml "^1.10.2" + +csso@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529" + integrity sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA== + dependencies: + css-tree "^1.1.2" + +csstype@^3.1.0: + version "3.1.2" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b" + integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ== + +cypress@^7.2.0: + version "7.7.0" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-7.7.0.tgz#0839ae28e5520536f9667d6c9ae81496b3836e64" + integrity sha512-uYBYXNoI5ym0UxROwhQXWTi8JbUEjpC6l/bzoGZNxoKGsLrC1SDPgIDJMgLX/MeEdPL0UInXLDUWN/rSyZUCjQ== + dependencies: + "@cypress/request" "^2.88.5" + "@cypress/xvfb" "^1.2.4" + "@types/node" "^14.14.31" + "@types/sinonjs__fake-timers" "^6.0.2" + "@types/sizzle" "^2.3.2" + arch "^2.2.0" + blob-util "^2.0.2" + bluebird "^3.7.2" + cachedir "^2.3.0" + chalk "^4.1.0" + check-more-types "^2.24.0" + cli-cursor "^3.1.0" + cli-table3 "~0.6.0" + commander "^5.1.0" + common-tags "^1.8.0" + dayjs "^1.10.4" + debug "^4.3.2" + enquirer "^2.3.6" + eventemitter2 "^6.4.3" + execa "4.1.0" + executable "^4.1.1" + extract-zip "2.0.1" + figures "^3.2.0" + fs-extra "^9.1.0" + getos "^3.2.1" + is-ci "^3.0.0" + is-installed-globally "~0.4.0" + lazy-ass "^1.6.0" + listr2 "^3.8.3" + lodash "^4.17.21" + log-symbols "^4.0.0" + minimist "^1.2.5" + ospath "^1.2.2" + pretty-bytes "^5.6.0" + ramda "~0.27.1" + request-progress "^3.0.0" + supports-color "^8.1.1" + tmp "~0.2.1" + untildify "^4.0.0" + url "^0.11.0" + yauzl "^2.10.0" + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + integrity sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g== + dependencies: + assert-plus "^1.0.0" + +date-fns@^2.17.0: + version "2.30.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0" + integrity sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw== + dependencies: + "@babel/runtime" "^7.21.0" + +dayjs@^1.10.4: + version "1.11.10" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.10.tgz#68acea85317a6e164457d6d6947564029a6a16a0" + integrity sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ== + +de-indent@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d" + integrity sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg== + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@4.3.3: + version "4.3.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" + integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== + dependencies: + ms "2.1.2" + +debug@^3.1.0, debug@^3.2.7: + version "3.2.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== + dependencies: + ms "^2.1.1" + +debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +decamelize@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" + integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== + +deep-is@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== + +default-gateway@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-6.0.3.tgz#819494c888053bdb743edbf343d6cdf7f2943a71" + integrity sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg== + dependencies: + execa "^5.0.0" + +define-data-property@^1.0.1, define-data-property@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.1.tgz#c35f7cd0ab09883480d12ac5cb213715587800b3" + integrity sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ== + dependencies: + get-intrinsic "^1.2.1" + gopd "^1.0.1" + has-property-descriptors "^1.0.0" + +define-lazy-prop@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" + integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== + +define-properties@^1.1.3, define-properties@^1.1.4, define-properties@^1.2.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" + integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== + dependencies: + define-data-property "^1.0.1" + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + +delegate@^3.1.2: + version "3.2.0" + resolved "https://registry.yarnpkg.com/delegate/-/delegate-3.2.0.tgz#b66b71c3158522e8ab5744f720d8ca0c2af59166" + integrity sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw== + +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== + +des.js@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.1.0.tgz#1d37f5766f3bbff4ee9638e871a8768c173b81da" + integrity sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg== + dependencies: + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +detect-node@^2.0.4: + version "2.1.0" + resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" + integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== + +diff@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" + integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== + +diffie-hellman@^5.0.0: + version "5.0.3" + resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" + integrity sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg== + dependencies: + bn.js "^4.1.0" + miller-rabin "^4.0.0" + randombytes "^2.0.0" + +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + +dns-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d" + integrity sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg== + +dns-packet@^5.2.2: + version "5.6.1" + resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-5.6.1.tgz#ae888ad425a9d1478a0674256ab866de1012cf2f" + integrity sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw== + dependencies: + "@leichtgewicht/ip-codec" "^2.0.1" + +doctrine@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" + integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== + dependencies: + esutils "^2.0.2" + +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + +dom-serializer@^1.0.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30" + integrity sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag== + dependencies: + domelementtype "^2.0.1" + domhandler "^4.2.0" + entities "^2.0.0" + +domain-browser@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" + integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA== + +domelementtype@^2.0.1, domelementtype@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" + integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== + +domhandler@^3.0.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-3.3.0.tgz#6db7ea46e4617eb15cf875df68b2b8524ce0037a" + integrity sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA== + dependencies: + domelementtype "^2.0.1" + +domhandler@^4.2.0, domhandler@^4.3.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.1.tgz#8d792033416f59d68bc03a5aa7b018c1ca89279c" + integrity sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ== + dependencies: + domelementtype "^2.2.0" + +domutils@^2.0.0, domutils@^2.8.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135" + integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A== + dependencies: + dom-serializer "^1.0.1" + domelementtype "^2.2.0" + domhandler "^4.2.0" + +dot-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751" + integrity sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w== + dependencies: + no-case "^3.0.4" + tslib "^2.0.3" + +dotenv-expand@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-5.1.0.tgz#3fbaf020bfd794884072ea26b1e9791d45a629f0" + integrity sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA== + +dotenv@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81" + integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q== + +ecc-jsbn@~0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" + integrity sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw== + dependencies: + jsbn "~0.1.0" + safer-buffer "^2.1.0" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +electron-to-chromium@^1.4.535: + version "1.4.572" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.572.tgz#ed9876658998138fe9e3aa47ecfa0bf914192a86" + integrity sha512-RlFobl4D3ieetbnR+2EpxdzFl9h0RAJkPK3pfiwMug2nhBin2ZCsGIAJWdpNniLz43sgXam/CgipOmvTA+rUiA== + +elliptic@^6.5.3, elliptic@^6.5.4: + version "6.5.4" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb" + integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ== + dependencies: + bn.js "^4.11.9" + brorand "^1.1.0" + hash.js "^1.0.0" + hmac-drbg "^1.0.1" + inherits "^2.0.4" + minimalistic-assert "^1.0.1" + minimalistic-crypto-utils "^1.0.1" + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +emojis-list@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" + integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +end-of-stream@^1.1.0: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + +enhanced-resolve@^5.15.0: + version "5.15.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz#1af946c7d93603eb88e9896cee4904dc012e9c35" + integrity sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + +enquirer@^2.3.5, enquirer@^2.3.6: + version "2.4.1" + resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.4.1.tgz#93334b3fbd74fc7097b224ab4a8fb7e40bf4ae56" + integrity sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ== + dependencies: + ansi-colors "^4.1.1" + strip-ansi "^6.0.1" + +entities@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" + integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== + +envinfo@^7.7.3: + version "7.11.0" + resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.11.0.tgz#c3793f44284a55ff8c82faf1ffd91bc6478ea01f" + integrity sha512-G9/6xF1FPbIw0TtalAMaVPpiq2aDEuKLXM314jPVAO9r2fo2a4BLqMNkmRS7O/xPPZ+COAhGIz3ETvHEV3eUcg== + +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +es-abstract@^1.22.1: + version "1.22.3" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.22.3.tgz#48e79f5573198de6dee3589195727f4f74bc4f32" + integrity sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA== + dependencies: + array-buffer-byte-length "^1.0.0" + arraybuffer.prototype.slice "^1.0.2" + available-typed-arrays "^1.0.5" + call-bind "^1.0.5" + es-set-tostringtag "^2.0.1" + es-to-primitive "^1.2.1" + function.prototype.name "^1.1.6" + get-intrinsic "^1.2.2" + get-symbol-description "^1.0.0" + globalthis "^1.0.3" + gopd "^1.0.1" + has-property-descriptors "^1.0.0" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + internal-slot "^1.0.5" + is-array-buffer "^3.0.2" + is-callable "^1.2.7" + is-negative-zero "^2.0.2" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.2" + is-string "^1.0.7" + is-typed-array "^1.1.12" + is-weakref "^1.0.2" + object-inspect "^1.13.1" + object-keys "^1.1.1" + object.assign "^4.1.4" + regexp.prototype.flags "^1.5.1" + safe-array-concat "^1.0.1" + safe-regex-test "^1.0.0" + string.prototype.trim "^1.2.8" + string.prototype.trimend "^1.0.7" + string.prototype.trimstart "^1.0.7" + typed-array-buffer "^1.0.0" + typed-array-byte-length "^1.0.0" + typed-array-byte-offset "^1.0.0" + typed-array-length "^1.0.4" + unbox-primitive "^1.0.2" + which-typed-array "^1.1.13" + +es-module-lexer@^1.2.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.3.1.tgz#c1b0dd5ada807a3b3155315911f364dc4e909db1" + integrity sha512-JUFAyicQV9mXc3YRxPnDlrfBKpqt6hUYzz9/boprUJHs4e4KVr3XwOF70doO6gwXUor6EWZJAyWAfKki84t20Q== + +es-set-tostringtag@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz#11f7cc9f63376930a5f20be4915834f4bc74f9c9" + integrity sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q== + dependencies: + get-intrinsic "^1.2.2" + has-tostringtag "^1.0.0" + hasown "^2.0.0" + +es-shim-unscopables@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz#1f6942e71ecc7835ed1c8a83006d8771a63a3763" + integrity sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw== + dependencies: + hasown "^2.0.0" + +es-to-primitive@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +escape-string-regexp@4.0.0, escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + +eslint-config-standard@^16.0: + version "16.0.3" + resolved "https://registry.yarnpkg.com/eslint-config-standard/-/eslint-config-standard-16.0.3.tgz#6c8761e544e96c531ff92642eeb87842b8488516" + integrity sha512-x4fmJL5hGqNJKGHSjnLdgA6U6h1YW/G2dW9fA+cyVur4SK6lyue8+UgNKWlZtUDTXvgKDD/Oa3GQjmB5kjtVvg== + +eslint-import-resolver-node@^0.3.9: + version "0.3.9" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz#d4eaac52b8a2e7c3cd1903eb00f7e053356118ac" + integrity sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g== + dependencies: + debug "^3.2.7" + is-core-module "^2.13.0" + resolve "^1.22.4" + +eslint-module-utils@^2.8.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz#e439fee65fc33f6bba630ff621efc38ec0375c49" + integrity sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw== + dependencies: + debug "^3.2.7" + +eslint-plugin-cypress@>=2.11.2: + version "2.15.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-cypress/-/eslint-plugin-cypress-2.15.1.tgz#336afa7e8e27451afaf65aa359c9509e0a4f3a7b" + integrity sha512-eLHLWP5Q+I4j2AWepYq0PgFEei9/s5LvjuSqWrxurkg1YZ8ltxdvMNmdSf0drnsNo57CTgYY/NIHHLRSWejR7w== + dependencies: + globals "^13.20.0" + +eslint-plugin-es@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz#75a7cdfdccddc0589934aeeb384175f221c57893" + integrity sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ== + dependencies: + eslint-utils "^2.0.0" + regexpp "^3.0.0" + +eslint-plugin-import@>=2.22.1: + version "2.29.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.29.0.tgz#8133232e4329ee344f2f612885ac3073b0b7e155" + integrity sha512-QPOO5NO6Odv5lpoTkddtutccQjysJuFxoPS7fAHO+9m9udNHvTCPSAMW9zGAYj8lAIdr40I8yPCdUYrncXtrwg== + dependencies: + array-includes "^3.1.7" + array.prototype.findlastindex "^1.2.3" + array.prototype.flat "^1.3.2" + array.prototype.flatmap "^1.3.2" + debug "^3.2.7" + doctrine "^2.1.0" + eslint-import-resolver-node "^0.3.9" + eslint-module-utils "^2.8.0" + hasown "^2.0.0" + is-core-module "^2.13.1" + is-glob "^4.0.3" + minimatch "^3.1.2" + object.fromentries "^2.0.7" + object.groupby "^1.0.1" + object.values "^1.1.7" + semver "^6.3.1" + tsconfig-paths "^3.14.2" + +eslint-plugin-node@>=11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz#c95544416ee4ada26740a30474eefc5402dc671d" + integrity sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g== + dependencies: + eslint-plugin-es "^3.0.0" + eslint-utils "^2.0.0" + ignore "^5.1.1" + minimatch "^3.0.4" + resolve "^1.10.1" + semver "^6.1.0" + +eslint-plugin-promise@>=4.0.0: + version "6.1.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-6.1.1.tgz#269a3e2772f62875661220631bd4dafcb4083816" + integrity sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig== + +eslint-plugin-standard@>=4.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-standard/-/eslint-plugin-standard-5.0.0.tgz#c43f6925d669f177db46f095ea30be95476b1ee4" + integrity sha512-eSIXPc9wBM4BrniMzJRBm2uoVuXz2EPa+NXPk2+itrVt+r5SbKFERx/IgrK/HmfjddyKVz2f+j+7gBRvu19xLg== + +eslint-plugin-vue@^7.0: + version "7.20.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-7.20.0.tgz#98c21885a6bfdf0713c3a92957a5afeaaeed9253" + integrity sha512-oVNDqzBC9h3GO+NTgWeLMhhGigy6/bQaQbHS+0z7C4YEu/qK/yxHvca/2PTZtGNPsCrHwOTgKMrwu02A9iPBmw== + dependencies: + eslint-utils "^2.1.0" + natural-compare "^1.4.0" + semver "^6.3.0" + vue-eslint-parser "^7.10.0" + +eslint-scope@5.1.1, eslint-scope@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +eslint-utils@^2.0.0, eslint-utils@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27" + integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg== + dependencies: + eslint-visitor-keys "^1.1.0" + +eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" + integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== + +eslint-visitor-keys@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" + integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== + +eslint@^7.10: + version "7.32.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.32.0.tgz#c6d328a14be3fb08c8d1d21e12c02fdb7a2a812d" + integrity sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA== + dependencies: + "@babel/code-frame" "7.12.11" + "@eslint/eslintrc" "^0.4.3" + "@humanwhocodes/config-array" "^0.5.0" + ajv "^6.10.0" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.0.1" + doctrine "^3.0.0" + enquirer "^2.3.5" + escape-string-regexp "^4.0.0" + eslint-scope "^5.1.1" + eslint-utils "^2.1.0" + eslint-visitor-keys "^2.0.0" + espree "^7.3.1" + esquery "^1.4.0" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^6.0.1" + functional-red-black-tree "^1.0.1" + glob-parent "^5.1.2" + globals "^13.6.0" + ignore "^4.0.6" + import-fresh "^3.0.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + js-yaml "^3.13.1" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.4.1" + lodash.merge "^4.6.2" + minimatch "^3.0.4" + natural-compare "^1.4.0" + optionator "^0.9.1" + progress "^2.0.0" + regexpp "^3.1.0" + semver "^7.2.1" + strip-ansi "^6.0.0" + strip-json-comments "^3.1.0" + table "^6.0.9" + text-table "^0.2.0" + v8-compile-cache "^2.0.3" + +espree@^6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-6.2.1.tgz#77fc72e1fd744a2052c20f38a5b575832e82734a" + integrity sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw== + dependencies: + acorn "^7.1.1" + acorn-jsx "^5.2.0" + eslint-visitor-keys "^1.1.0" + +espree@^7.3.0, espree@^7.3.1: + version "7.3.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6" + integrity sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g== + dependencies: + acorn "^7.4.0" + acorn-jsx "^5.3.1" + eslint-visitor-keys "^1.3.0" + +esprima@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +esquery@^1.4.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b" + integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.1.0, estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +eventemitter2@^6.4.3: + version "6.4.9" + resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.9.tgz#41f2750781b4230ed58827bc119d293471ecb125" + integrity sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg== + +eventemitter3@^4.0.0: + version "4.0.7" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== + +events@^3.0.0, events@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + +evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02" + integrity sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA== + dependencies: + md5.js "^1.3.4" + safe-buffer "^5.1.1" + +execa@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-4.1.0.tgz#4e5491ad1572f2f17a77d388c6c857135b22847a" + integrity sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA== + dependencies: + cross-spawn "^7.0.0" + get-stream "^5.0.0" + human-signals "^1.1.1" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.0" + onetime "^5.1.0" + signal-exit "^3.0.2" + strip-final-newline "^2.0.0" + +execa@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" + strip-final-newline "^2.0.0" + +executable@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/executable/-/executable-4.1.1.tgz#41532bff361d3e57af4d763b70582db18f5d133c" + integrity sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg== + dependencies: + pify "^2.2.0" + +exif-js@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/exif-js/-/exif-js-2.3.0.tgz#9d10819bf571f873813e7640241255ab9ce1a814" + integrity sha512-1Og9pAzG2FZRVlaavH8bB8BTeHcjMdJhKmeQITkX+uLRCD0xPtKAdZ2clZmQdJ56p9adXtJ8+jwrGp/4505lYg== + +express@^4.17.3: + version "4.18.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" + integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.1" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.5.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.2.0" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.7" + qs "6.11.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.18.0" + serve-static "1.15.0" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +extend@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +extract-zip@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" + integrity sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg== + dependencies: + debug "^4.1.1" + get-stream "^5.1.0" + yauzl "^2.10.0" + optionalDependencies: + "@types/yauzl" "^2.9.1" + +extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + integrity sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g== + +extsprintf@^1.2.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.1.tgz#8d172c064867f235c0c84a596806d279bf4bcc07" + integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA== + +faker@^5.1: + version "5.5.3" + resolved "https://registry.yarnpkg.com/faker/-/faker-5.5.3.tgz#c57974ee484431b25205c2c8dc09fda861e51e0e" + integrity sha512-wLTv2a28wjUyWkbnX7u/ABZBkUkIF2fCd73V6P2oFqEGEktDfzWx4UxrSqtPRw0xPRAcjeAOIiJWqZm3pP4u3g== + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-glob@^3.0.3: + version "3.3.1" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4" + integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== + +fastest-levenshtein@^1.0.12: + version "1.0.16" + resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" + integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg== + +fastq@^1.6.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a" + integrity sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw== + dependencies: + reusify "^1.0.4" + +faye-websocket@^0.11.3: + version "0.11.4" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da" + integrity sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g== + dependencies: + websocket-driver ">=0.5.1" + +fd-slicer@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" + integrity sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g== + dependencies: + pend "~1.2.0" + +figures@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" + integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== + dependencies: + escape-string-regexp "^1.0.5" + +file-entry-cache@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" + integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== + dependencies: + flat-cache "^3.0.4" + +file-loader@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-6.2.0.tgz#baef7cf8e1840df325e4390b4484879480eebe4d" + integrity sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw== + dependencies: + loader-utils "^2.0.0" + schema-utils "^3.0.0" + +file-type@^12.0.0: + version "12.4.2" + resolved "https://registry.yarnpkg.com/file-type/-/file-type-12.4.2.tgz#a344ea5664a1d01447ee7fb1b635f72feb6169d9" + integrity sha512-UssQP5ZgIOKelfsaB5CuGAL+Y+q7EmONuiwF3N5HAH0t27rvrttgi6Ra9k/+DVaY9UF6+ybxu5pOXLUdA8N7Vg== + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +finalhandler@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" + integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + +find-cache-dir@^3.3.1: + version "3.3.2" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.2.tgz#b30c5b6eff0730731aea9bbd9dbecbd80256d64b" + integrity sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig== + dependencies: + commondir "^1.0.1" + make-dir "^3.0.2" + pkg-dir "^4.1.0" + +find-up@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +find-up@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +flat-cache@^3.0.4: + version "3.1.1" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.1.1.tgz#a02a15fdec25a8f844ff7cc658f03dd99eb4609b" + integrity sha512-/qM2b3LUIaIgviBQovTLvijfyOQXPtSRnRK26ksj2J7rzPIecePUIpJsZ4T02Qg+xiAEKIs5K8dsHEd+VaKa/Q== + dependencies: + flatted "^3.2.9" + keyv "^4.5.3" + rimraf "^3.0.2" + +flat@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" + integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== + +flatted@^3.2.9: + version "3.2.9" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.9.tgz#7eb4c67ca1ba34232ca9d2d93e9886e611ad7daf" + integrity sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ== + +follow-redirects@^1.0.0, follow-redirects@^1.14.0: + version "1.15.3" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a" + integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q== + +font-awesome@^4.7: + version "4.7.0" + resolved "https://registry.yarnpkg.com/font-awesome/-/font-awesome-4.7.0.tgz#8fa8cf0411a1a31afd07b06d2902bb9fc815a133" + integrity sha512-U6kGnykA/6bFmg1M/oT9EkFeIYv7JlX3bozwQJWiiLz6L0w3F5vBVPxHlwyX/vtNq1ckcpRKOB9f2Qal/VtFpg== + +for-each@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" + integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== + dependencies: + is-callable "^1.1.3" + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + integrity sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw== + +form-data@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" + integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fraction.js@^4.3.6: + version "4.3.7" + resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7" + integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew== + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + +fs-extra@^10.0.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf" + integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs-extra@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" + integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== + dependencies: + at-least-node "^1.0.0" + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs-monkey@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.0.5.tgz#fe450175f0db0d7ea758102e1d84096acb925788" + integrity sha512-8uMbBjrhzW76TYgEV27Y5E//W2f/lTFmx78P2w19FZSxarhI/798APGQyuGCwmkNxgwGRhrLfvWyLBvNtuOmew== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +function.prototype.name@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.6.tgz#cdf315b7d90ee77a4c6ee216c3c3362da07533fd" + integrity sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + functions-have-names "^1.2.3" + +functional-red-black-tree@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" + integrity sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g== + +functions-have-names@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" + integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== + +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.0, get-intrinsic@^1.2.1, get-intrinsic@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.2.tgz#281b7622971123e1ef4b3c90fd7539306da93f3b" + integrity sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA== + dependencies: + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + +get-stream@^5.0.0, get-stream@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" + integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== + dependencies: + pump "^3.0.0" + +get-stream@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + +get-symbol-description@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6" + integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.1" + +getos@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/getos/-/getos-3.2.1.tgz#0134d1f4e00eb46144c5a9c0ac4dc087cbb27dc5" + integrity sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q== + dependencies: + async "^3.2.0" + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + integrity sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng== + dependencies: + assert-plus "^1.0.0" + +glob-parent@^5.1.2, glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-to-regexp@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" + integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== + +glob@7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" + integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^7.0.0, glob@^7.1.3, glob@^7.2.0: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +global-dirs@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-3.0.1.tgz#0c488971f066baceda21447aecb1a8b911d22485" + integrity sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA== + dependencies: + ini "2.0.0" + +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +globals@^13.20.0, globals@^13.6.0, globals@^13.9.0: + version "13.23.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.23.0.tgz#ef31673c926a0976e1f61dab4dca57e0c0a8af02" + integrity sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA== + dependencies: + type-fest "^0.20.2" + +globalthis@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.3.tgz#5852882a52b80dc301b0660273e1ed082f0b6ccf" + integrity sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA== + dependencies: + define-properties "^1.1.3" + +globby@^10.0.0: + version "10.0.2" + resolved "https://registry.yarnpkg.com/globby/-/globby-10.0.2.tgz#277593e745acaa4646c3ab411289ec47a0392543" + integrity sha512-7dUi7RvCoT/xast/o/dLN53oqND4yk0nsHkhRgn9w65C4PofCLOoJ39iSOg+qVDdWQPIEj+eszMHQ+aLVwwQSg== + dependencies: + "@types/glob" "^7.1.1" + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.0.3" + glob "^7.1.3" + ignore "^5.1.1" + merge2 "^1.2.3" + slash "^3.0.0" + +good-listener@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50" + integrity sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw== + dependencies: + delegate "^3.1.2" + +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + +graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.2, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +growl@1.10.5: + version "1.10.5" + resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" + integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== + +growly@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" + integrity sha512-+xGQY0YyAWCnqy7Cd++hc2JqMYzlm0dG30Jd0beaA64sROr8C4nt8Yc9V5Ro3avlSUDTN0ulqP/VBKi1/lLygw== + +handle-thing@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" + integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg== + +has-bigints@^1.0.1, has-bigints@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" + integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-property-descriptors@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz#52ba30b6c5ec87fd89fa574bc1c39125c6f65340" + integrity sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg== + dependencies: + get-intrinsic "^1.2.2" + +has-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0" + integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg== + +has-symbols@^1.0.2, has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +has-tostringtag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" + integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ== + dependencies: + has-symbols "^1.0.2" + +hash-base@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.0.tgz#55c381d9e06e1d2997a883b4a3fddfe7f0d3af33" + integrity sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA== + dependencies: + inherits "^2.0.4" + readable-stream "^3.6.0" + safe-buffer "^5.2.0" + +hash-sum@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/hash-sum/-/hash-sum-1.0.2.tgz#33b40777754c6432573c120cc3808bbd10d47f04" + integrity sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA== + +hash.js@^1.0.0, hash.js@^1.0.3: + version "1.1.7" + resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" + integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== + dependencies: + inherits "^2.0.3" + minimalistic-assert "^1.0.1" + +hasown@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.0.tgz#f4c513d454a57b7c7e1650778de226b11700546c" + integrity sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA== + dependencies: + function-bind "^1.1.2" + +he@1.2.0, he@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + +hint.css@^2.3: + version "2.7.0" + resolved "https://registry.yarnpkg.com/hint.css/-/hint.css-2.7.0.tgz#2f7f9cd8fb07745de9800547e36a0c82ee9aedaf" + integrity sha512-kNtr/oXTOFA+qYLFAHY1dKVKZuAbavCPB22+w9kaFzqjB9ZLSFJyksxrVsJCphQiQxAYLazXZdrOaGLRe0hZMA== + +hmac-drbg@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" + integrity sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg== + dependencies: + hash.js "^1.0.3" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.1" + +hpack.js@^2.1.6: + version "2.1.6" + resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" + integrity sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ== + dependencies: + inherits "^2.0.1" + obuf "^1.0.0" + readable-stream "^2.0.1" + wbuf "^1.1.0" + +html-entities@^2.3.2: + version "2.4.0" + resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.4.0.tgz#edd0cee70402584c8c76cc2c0556db09d1f45061" + integrity sha512-igBTJcNNNhvZFRtm8uA6xMY6xYleeDwn3PeBCkDz7tHttv4F2hsDI2aPgNERWzvRcNYHNT3ymRaQzllmXj4YsQ== + +html-loader@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/html-loader/-/html-loader-1.3.2.tgz#5a72ebba420d337083497c9aba7866c9e1aee340" + integrity sha512-DEkUwSd0sijK5PF3kRWspYi56XP7bTNkyg5YWSzBdjaSDmvCufep5c4Vpb3PBf6lUL0YPtLwBfy9fL0t5hBAGA== + dependencies: + html-minifier-terser "^5.1.1" + htmlparser2 "^4.1.0" + loader-utils "^2.0.0" + schema-utils "^3.0.0" + +html-minifier-terser@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz#922e96f1f3bb60832c2634b79884096389b1f054" + integrity sha512-ZPr5MNObqnV/T9akshPKbVgyOqLmy+Bxo7juKCfTfnjNniTAMdy4hz21YQqoofMBJD2kdREaqPPdThoR78Tgxg== + dependencies: + camel-case "^4.1.1" + clean-css "^4.2.3" + commander "^4.1.1" + he "^1.2.0" + param-case "^3.0.3" + relateurl "^0.2.7" + terser "^4.6.3" + +htmlparser2@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-4.1.0.tgz#9a4ef161f2e4625ebf7dfbe6c0a2f52d18a59e78" + integrity sha512-4zDq1a1zhE4gQso/c5LP1OtrhYTncXNSpvJYtWJBtXAETPlMfi3IFNjGuQbYLuVY4ZR0QMqRVvo4Pdy9KLyP8Q== + dependencies: + domelementtype "^2.0.1" + domhandler "^3.0.0" + domutils "^2.0.0" + entities "^2.0.0" + +http-deceiver@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" + integrity sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw== + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +http-errors@~1.6.2: + version "1.6.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" + integrity sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A== + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.0" + statuses ">= 1.4.0 < 2" + +http-parser-js@>=0.5.1: + version "0.5.8" + resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.8.tgz#af23090d9ac4e24573de6f6aecc9d84a48bf20e3" + integrity sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q== + +http-proxy-middleware@^2.0.3: + version "2.0.6" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz#e1a4dd6979572c7ab5a4e4b55095d1f32a74963f" + integrity sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw== + dependencies: + "@types/http-proxy" "^1.17.8" + http-proxy "^1.18.1" + is-glob "^4.0.1" + is-plain-obj "^3.0.0" + micromatch "^4.0.2" + +http-proxy@^1.18.1: + version "1.18.1" + resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" + integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== + dependencies: + eventemitter3 "^4.0.0" + follow-redirects "^1.0.0" + requires-port "^1.0.0" + +http-signature@~1.3.6: + version "1.3.6" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.3.6.tgz#cb6fbfdf86d1c974f343be94e87f7fc128662cf9" + integrity sha512-3adrsD6zqo4GsTqtO7FyrejHNv+NgiIfAfv68+jVlFmSr9OGy7zrxONceFRLKvnnZA5jbxQBX1u9PpB6Wi32Gw== + dependencies: + assert-plus "^1.0.0" + jsprim "^2.0.2" + sshpk "^1.14.1" + +https-browserify@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" + integrity sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg== + +human-signals@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" + integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== + +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +icss-utils@^5.0.0, icss-utils@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae" + integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== + +ieee754@^1.1.4: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + +ignore@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" + integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== + +ignore@^5.1.1: + version "5.2.4" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" + integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== + +imagemin@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/imagemin/-/imagemin-7.0.1.tgz#f6441ca647197632e23db7d971fffbd530c87dbf" + integrity sha512-33AmZ+xjZhg2JMCe+vDf6a9mzWukE7l+wAtesjE7KyteqqKjzxv7aVQeWnul1Ve26mWvEQqyPwl0OctNBfSR9w== + dependencies: + file-type "^12.0.0" + globby "^10.0.0" + graceful-fs "^4.2.2" + junk "^3.1.0" + make-dir "^3.0.0" + p-pipe "^3.0.0" + replace-ext "^1.0.0" + +img-loader@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/img-loader/-/img-loader-4.0.0.tgz#f41fb0737cc8e1d6a8c242f48c29a443640e0638" + integrity sha512-UwRcPQdwdOyEHyCxe1V9s9YFwInwEWCpoO+kJGfIqDrBDqA8jZUsEZTxQ0JteNPGw/Gupmwesk2OhLTcnw6tnQ== + dependencies: + loader-utils "^1.1.0" + +immutable@^4.0.0: + version "4.3.4" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.4.tgz#2e07b33837b4bb7662f288c244d1ced1ef65a78f" + integrity sha512-fsXeu4J4i6WNWSikpI88v/PcVflZz+6kMhUfIwc5SY+poQRPnaf5V7qds6SUyUN3cVxEzuCab7QIoLOQ+DQ1wA== + +import-fresh@^3.0.0, import-fresh@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +import-local@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.1.0.tgz#b4479df8a5fd44f6cdce24070675676063c95cb4" + integrity sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg== + dependencies: + pkg-dir "^4.2.0" + resolve-cwd "^3.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw== + +ini@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ini/-/ini-2.0.0.tgz#e5fd556ecdd5726be978fa1001862eacb0a94bc5" + integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== + +internal-slot@^1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.6.tgz#37e756098c4911c5e912b8edbf71ed3aa116f930" + integrity sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg== + dependencies: + get-intrinsic "^1.2.2" + hasown "^2.0.0" + side-channel "^1.0.4" + +interpret@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" + integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw== + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +ipaddr.js@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.1.0.tgz#2119bc447ff8c257753b196fc5f1ce08a4cdf39f" + integrity sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ== + +is-array-buffer@^3.0.1, is-array-buffer@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.2.tgz#f2653ced8412081638ecb0ebbd0c41c6e0aecbbe" + integrity sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.2.0" + is-typed-array "^1.1.10" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== + +is-bigint@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" + integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== + dependencies: + has-bigints "^1.0.1" + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-boolean-object@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" + integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-buffer@~1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== + +is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" + integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== + +is-ci@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-3.0.1.tgz#db6ecbed1bd659c43dac0f45661e7674103d1867" + integrity sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ== + dependencies: + ci-info "^3.2.0" + +is-core-module@^2.13.0, is-core-module@^2.13.1: + version "2.13.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384" + integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== + dependencies: + hasown "^2.0.0" + +is-date-object@^1.0.1: + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" + integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== + dependencies: + has-tostringtag "^1.0.0" + +is-docker@^2.0.0, is-docker@^2.1.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" + integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-installed-globally@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.4.0.tgz#9a0fd407949c30f86eb6959ef1b7994ed0b7b520" + integrity sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ== + dependencies: + global-dirs "^3.0.0" + is-path-inside "^3.0.2" + +is-negative-zero@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150" + integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA== + +is-number-object@^1.0.4: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc" + integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== + dependencies: + has-tostringtag "^1.0.0" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-path-inside@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + +is-plain-obj@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-2.1.0.tgz#45e42e37fccf1f40da8e5f76ee21515840c09287" + integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== + +is-plain-obj@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-3.0.0.tgz#af6f2ea14ac5a646183a5bbdb5baabbc156ad9d7" + integrity sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA== + +is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" + +is-regex@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" + integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-shared-array-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz#8f259c573b60b6a32d4058a1a07430c0a7344c79" + integrity sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA== + dependencies: + call-bind "^1.0.2" + +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + +is-string@^1.0.5, is-string@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" + integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== + dependencies: + has-tostringtag "^1.0.0" + +is-symbol@^1.0.2, is-symbol@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" + integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== + dependencies: + has-symbols "^1.0.2" + +is-typed-array@^1.1.10, is-typed-array@^1.1.12, is-typed-array@^1.1.9: + version "1.1.12" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.12.tgz#d0bab5686ef4a76f7a73097b95470ab199c57d4a" + integrity sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg== + dependencies: + which-typed-array "^1.1.11" + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== + +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + +is-weakref@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" + integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== + dependencies: + call-bind "^1.0.2" + +is-wsl@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" + integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== + dependencies: + is-docker "^2.0.0" + +isarray@^1.0.0, isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + +isarray@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + integrity sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g== + +jest-worker@^27.4.5: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" + integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +jquery@^3.6: + version "3.7.1" + resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.7.1.tgz#083ef98927c9a6a74d05a6af02806566d16274de" + integrity sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg== + +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +js-yaml@^3.13.1: + version "3.14.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" + integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + integrity sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg== + +jsesc@^2.5.1: + version "2.5.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" + integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== + +jsesc@~0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" + integrity sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA== + +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + +json-parse-even-better-errors@^2.3.0, json-parse-even-better-errors@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + +json-schema@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5" + integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== + +json5@^1.0.1, json5@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" + integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== + dependencies: + minimist "^1.2.0" + +json5@^2.1.2, json5@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + +jsprim@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-2.0.2.tgz#77ca23dbcd4135cd364800d22ff82c2185803d4d" + integrity sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ== + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.4.0" + verror "1.10.0" + +junk@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/junk/-/junk-3.1.0.tgz#31499098d902b7e98c5d9b9c80f43457a88abfa1" + integrity sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ== + +keyv@^4.5.3: + version "4.5.4" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== + dependencies: + json-buffer "3.0.1" + +kind-of@^6.0.2: + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + +klona@^2.0.4, klona@^2.0.5: + version "2.0.6" + resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.6.tgz#85bffbf819c03b2f53270412420a4555ef882e22" + integrity sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA== + +laravel-mix-purgecss@^6.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/laravel-mix-purgecss/-/laravel-mix-purgecss-6.0.0.tgz#0e52e1e9f8ddd951c5dc354674c37790ffc8cb40" + integrity sha512-1OVy3xVVqvWrBTI+vQrr9qlrNKKqq3lFlWQpdJxKO2IeK8bMERkNab3fLtldyyOd5ApBuoMd81EqF4ew2N/NdA== + dependencies: + postcss-purgecss-laravel "^2.0.0" + +laravel-mix@^6.0: + version "6.0.49" + resolved "https://registry.yarnpkg.com/laravel-mix/-/laravel-mix-6.0.49.tgz#d718414858045df9d7467245e13fd4b45bc52c15" + integrity sha512-bBMFpFjp26XfijPvY5y9zGKud7VqlyOE0OWUcPo3vTBY5asw8LTjafAbee1dhfLz6PWNqDziz69CP78ELSpfKw== + dependencies: + "@babel/core" "^7.15.8" + "@babel/plugin-proposal-object-rest-spread" "^7.15.6" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + "@babel/plugin-transform-runtime" "^7.15.8" + "@babel/preset-env" "^7.15.8" + "@babel/runtime" "^7.15.4" + "@types/babel__core" "^7.1.16" + "@types/clean-css" "^4.2.5" + "@types/imagemin-gifsicle" "^7.0.1" + "@types/imagemin-mozjpeg" "^8.0.1" + "@types/imagemin-optipng" "^5.2.1" + "@types/imagemin-svgo" "^8.0.0" + autoprefixer "^10.4.0" + babel-loader "^8.2.3" + chalk "^4.1.2" + chokidar "^3.5.2" + clean-css "^5.2.4" + cli-table3 "^0.6.0" + collect.js "^4.28.5" + commander "^7.2.0" + concat "^1.0.3" + css-loader "^5.2.6" + cssnano "^5.0.8" + dotenv "^10.0.0" + dotenv-expand "^5.1.0" + file-loader "^6.2.0" + fs-extra "^10.0.0" + glob "^7.2.0" + html-loader "^1.3.2" + imagemin "^7.0.1" + img-loader "^4.0.0" + lodash "^4.17.21" + md5 "^2.3.0" + mini-css-extract-plugin "^1.6.2" + node-libs-browser "^2.2.1" + postcss-load-config "^3.1.0" + postcss-loader "^6.2.0" + semver "^7.3.5" + strip-ansi "^6.0.0" + style-loader "^2.0.0" + terser "^5.9.0" + terser-webpack-plugin "^5.2.4" + vue-style-loader "^4.1.3" + webpack "^5.60.0" + webpack-cli "^4.9.1" + webpack-dev-server "^4.7.3" + webpack-merge "^5.8.0" + webpack-notifier "^1.14.1" + webpackbar "^5.0.0-3" + yargs "^17.2.1" + +launch-editor@^2.6.0: + version "2.6.1" + resolved "https://registry.yarnpkg.com/launch-editor/-/launch-editor-2.6.1.tgz#f259c9ef95cbc9425620bbbd14b468fcdb4ffe3c" + integrity sha512-eB/uXmFVpY4zezmGp5XtU21kwo7GBbKB+EQ+UZeWtGb9yAM5xt/Evk+lYH3eRNAtId+ej4u7TYPFZ07w4s7rRw== + dependencies: + picocolors "^1.0.0" + shell-quote "^1.8.1" + +lazy-ass@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/lazy-ass/-/lazy-ass-1.6.0.tgz#7999655e8646c17f089fdd187d150d3324d54513" + integrity sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw== + +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + +lilconfig@^2.0.3, lilconfig@^2.0.5: + version "2.1.0" + resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52" + integrity sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ== + +lines-and-columns@^1.1.6: + version "1.2.4" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== + +list.js@^2.3: + version "2.3.1" + resolved "https://registry.yarnpkg.com/list.js/-/list.js-2.3.1.tgz#48961989ffe52b0505e352f7a521f819f51df7e7" + integrity sha512-jnmm7DYpKtH3DxtO1E2VNCC9Gp7Wrp/FWA2JxQrZUhVJ2RCQBd57pCN6W5w6jpsfWZV0PCAbTX2NOPgyFeeZZg== + dependencies: + string-natural-compare "^2.0.2" + +listr2@^3.8.3: + version "3.14.0" + resolved "https://registry.yarnpkg.com/listr2/-/listr2-3.14.0.tgz#23101cc62e1375fd5836b248276d1d2b51fdbe9e" + integrity sha512-TyWI8G99GX9GjE54cJ+RrNMcIFBfwMPxc3XTFiAYGN4s10hWROGtOg7+O6u6LE3mNkyld7RSLE6nrKBvTfcs3g== + dependencies: + cli-truncate "^2.1.0" + colorette "^2.0.16" + log-update "^4.0.0" + p-map "^4.0.0" + rfdc "^1.3.0" + rxjs "^7.5.1" + through "^2.3.8" + wrap-ansi "^7.0.0" + +loader-runner@^4.2.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1" + integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== + +loader-utils@^1.0.2, loader-utils@^1.1.0: + version "1.4.2" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.2.tgz#29a957f3a63973883eb684f10ffd3d151fec01a3" + integrity sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg== + dependencies: + big.js "^5.2.2" + emojis-list "^3.0.0" + json5 "^1.0.1" + +loader-utils@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.4.tgz#8b5cb38b5c34a9a018ee1fc0e6a066d1dfcc528c" + integrity sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw== + dependencies: + big.js "^5.2.2" + emojis-list "^3.0.0" + json5 "^2.1.2" + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash.debounce@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" + integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== + +lodash.difference@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.difference/-/lodash.difference-4.5.0.tgz#9ccb4e505d486b91651345772885a2df27fd017c" + integrity sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA== + +lodash.isequal@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" + integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ== + +lodash.memoize@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag== + +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + +lodash.once@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg== + +lodash.truncate@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" + integrity sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw== + +lodash.uniq@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" + integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ== + +lodash@^4.17, lodash@^4.17.15, lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +log-symbols@4.1.0, log-symbols@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + +log-update@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/log-update/-/log-update-4.0.0.tgz#589ecd352471f2a1c0c570287543a64dfd20e0a1" + integrity sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg== + dependencies: + ansi-escapes "^4.3.0" + cli-cursor "^3.1.0" + slice-ansi "^4.0.0" + wrap-ansi "^6.2.0" + +lower-case@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28" + integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg== + dependencies: + tslib "^2.0.3" + +lru-cache@^4.1.2: + version "4.1.5" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" + integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== + dependencies: + pseudomap "^1.0.2" + yallist "^2.1.2" + +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +make-dir@^3.0.0, make-dir@^3.0.2, make-dir@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" + integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== + dependencies: + semver "^6.0.0" + +marked@^2.0: + version "2.1.3" + resolved "https://registry.yarnpkg.com/marked/-/marked-2.1.3.tgz#bd017cef6431724fd4b27e0657f5ceb14bff3753" + integrity sha512-/Q+7MGzaETqifOMWYEA7HVMaZb4XbcRfaOzcSsHZEith83KGlvaSG33u0SKu89Mj5h+T8V2hM+8O45Qc5XTgwA== + +md5.js@^1.3.4: + version "1.3.5" + resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" + integrity sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg== + dependencies: + hash-base "^3.0.0" + inherits "^2.0.1" + safe-buffer "^5.1.2" + +md5@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/md5/-/md5-2.3.0.tgz#c3da9a6aae3a30b46b7b0c349b87b110dc3bda4f" + integrity sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g== + dependencies: + charenc "0.0.2" + crypt "0.0.2" + is-buffer "~1.1.6" + +mdn-data@2.0.14: + version "2.0.14" + resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" + integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +memfs@^3.4.3: + version "3.6.0" + resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.6.0.tgz#d7a2110f86f79dd950a8b6df6d57bc984aa185f6" + integrity sha512-EGowvkkgbMcIChjMTMkESFDbZeSh8xZ7kNSF0hAiAN4Jh6jgHCRS0Ga/+C8y6Au+oqpezRHCfPsmJ2+DwAgiwQ== + dependencies: + fs-monkey "^1.0.4" + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== + +merge-source-map@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/merge-source-map/-/merge-source-map-1.1.0.tgz#2fdde7e6020939f70906a68f2d7ae685e4c8c646" + integrity sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw== + dependencies: + source-map "^0.6.1" + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +merge2@^1.2.3, merge2@^1.3.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +micromatch@^4.0.2, micromatch@^4.0.4: + version "4.0.5" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" + integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== + dependencies: + braces "^3.0.2" + picomatch "^2.3.1" + +miller-rabin@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" + integrity sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA== + dependencies: + bn.js "^4.0.0" + brorand "^1.0.1" + +mime-db@1.52.0, "mime-db@>= 1.43.0 < 2": + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +mini-css-extract-plugin@^1.6.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-1.6.2.tgz#83172b4fd812f8fc4a09d6f6d16f924f53990ca8" + integrity sha512-WhDvO3SjGm40oV5y26GjMJYjd2UMqrLAGKy5YS2/3QKJy2F7jgynuHTir/tgUUOiNQu5saXHdc8reo7YuhhT4Q== + dependencies: + loader-utils "^2.0.0" + schema-utils "^3.0.0" + webpack-sources "^1.1.0" + +minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" + integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== + +minimalistic-crypto-utils@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" + integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg== + +minimatch@4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-4.2.1.tgz#40d9d511a46bdc4e563c22c3080cde9c0d8299b4" + integrity sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +mkdirp@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-3.0.1.tgz#e44e4c5607fb279c168241713cc6e0fea9adcb50" + integrity sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg== + +mocha-junit-reporter@^2.0.2: + version "2.2.1" + resolved "https://registry.yarnpkg.com/mocha-junit-reporter/-/mocha-junit-reporter-2.2.1.tgz#739f5595d0f051d07af9d74e32c416e13a41cde5" + integrity sha512-iDn2tlKHn8Vh8o4nCzcUVW4q7iXp7cC4EB78N0cDHIobLymyHNwe0XG8HEHHjc3hJlXm0Vy6zcrxaIhnI2fWmw== + dependencies: + debug "^4.3.4" + md5 "^2.3.0" + mkdirp "^3.0.0" + strip-ansi "^6.0.1" + xml "^1.0.1" + +mocha-multi-reporters@^1.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/mocha-multi-reporters/-/mocha-multi-reporters-1.5.1.tgz#c73486bed5519e1d59c9ce39ac7a9792600e5676" + integrity sha512-Yb4QJOaGLIcmB0VY7Wif5AjvLMUFAdV57D2TWEva1Y0kU/3LjKpeRVmlMIfuO1SVbauve459kgtIizADqxMWPg== + dependencies: + debug "^4.1.1" + lodash "^4.17.15" + +mocha@^9.1.2: + version "9.2.2" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-9.2.2.tgz#d70db46bdb93ca57402c809333e5a84977a88fb9" + integrity sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g== + dependencies: + "@ungap/promise-all-settled" "1.1.2" + ansi-colors "4.1.1" + browser-stdout "1.3.1" + chokidar "3.5.3" + debug "4.3.3" + diff "5.0.0" + escape-string-regexp "4.0.0" + find-up "5.0.0" + glob "7.2.0" + growl "1.10.5" + he "1.2.0" + js-yaml "4.1.0" + log-symbols "4.1.0" + minimatch "4.2.1" + ms "2.1.3" + nanoid "3.3.1" + serialize-javascript "6.0.0" + strip-json-comments "3.1.1" + supports-color "8.1.1" + which "2.0.2" + workerpool "6.2.0" + yargs "16.2.0" + yargs-parser "20.2.4" + yargs-unparser "2.0.0" + +moment-locales-webpack-plugin@^1.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/moment-locales-webpack-plugin/-/moment-locales-webpack-plugin-1.2.0.tgz#9af83876a44053706b868ceece5119584d10d7aa" + integrity sha512-QAi5v0OlPUP7GXviKMtxnpBAo8WmTHrUNN7iciAhNOEAd9evCOvuN0g1N7ThIg3q11GLCkjY1zQ2saRcf/43nQ== + dependencies: + lodash.difference "^4.5.0" + +moment-timezone@^0.5: + version "0.5.43" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.43.tgz#3dd7f3d0c67f78c23cd1906b9b2137a09b3c4790" + integrity sha512-72j3aNyuIsDxdF1i7CEgV2FfxM1r6aaqJyLB2vwb33mXYyoyLly+F1zbWqhA3/bVIoJ4szlUoMbUnVdid32NUQ== + dependencies: + moment "^2.29.4" + +moment@^2.24.0, moment@^2.26, moment@^2.29.4: + version "2.29.4" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.4.tgz#3dbe052889fe7c1b2ed966fcb3a77328964ef108" + integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@2.1.3, ms@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +multicast-dns@^7.2.5: + version "7.2.5" + resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-7.2.5.tgz#77eb46057f4d7adbd16d9290fa7299f6fa64cced" + integrity sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg== + dependencies: + dns-packet "^5.2.2" + thunky "^1.0.2" + +nanoid@3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.1.tgz#6347a18cac88af88f58af0b3594b723d5e99bb35" + integrity sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw== + +nanoid@^3.3.6: + version "3.3.6" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" + integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +neo-async@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + +no-case@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d" + integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg== + dependencies: + lower-case "^2.0.2" + tslib "^2.0.3" + +node-forge@^1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" + integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== + +node-libs-browser@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.2.1.tgz#b64f513d18338625f90346d27b0d235e631f6425" + integrity sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q== + dependencies: + assert "^1.1.1" + browserify-zlib "^0.2.0" + buffer "^4.3.0" + console-browserify "^1.1.0" + constants-browserify "^1.0.0" + crypto-browserify "^3.11.0" + domain-browser "^1.1.1" + events "^3.0.0" + https-browserify "^1.0.0" + os-browserify "^0.3.0" + path-browserify "0.0.1" + process "^0.11.10" + punycode "^1.2.4" + querystring-es3 "^0.2.0" + readable-stream "^2.3.3" + stream-browserify "^2.0.1" + stream-http "^2.7.2" + string_decoder "^1.0.0" + timers-browserify "^2.0.4" + tty-browserify "0.0.0" + url "^0.11.0" + util "^0.11.0" + vm-browserify "^1.0.1" + +node-notifier@^9.0.0: + version "9.0.1" + resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-9.0.1.tgz#cea837f4c5e733936c7b9005e6545cea825d1af4" + integrity sha512-fPNFIp2hF/Dq7qLDzSg4vZ0J4e9v60gJR+Qx7RbjbWqzPDdEqeVpEx5CFeDAELIl+A/woaaNn1fQ5nEVerMxJg== + dependencies: + growly "^1.3.0" + is-wsl "^2.2.0" + semver "^7.3.2" + shellwords "^0.1.1" + uuid "^8.3.0" + which "^2.0.2" + +node-releases@^2.0.13: + version "2.0.13" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.13.tgz#d5ed1627c23e3461e819b02e57b75e4899b1c81d" + integrity sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ== + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +normalize-range@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" + integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA== + +normalize-url@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a" + integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A== + +npm-run-path@^4.0.0, npm-run-path@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + +nth-check@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" + integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w== + dependencies: + boolbase "^1.0.0" + +object-inspect@^1.13.1, object-inspect@^1.9.0: + version "1.13.1" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2" + integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ== + +object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object.assign@^4.1.4: + version "4.1.4" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.4.tgz#9673c7c7c351ab8c4d0b516f4343ebf4dfb7799f" + integrity sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + has-symbols "^1.0.3" + object-keys "^1.1.1" + +object.fromentries@^2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.7.tgz#71e95f441e9a0ea6baf682ecaaf37fa2a8d7e616" + integrity sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + +object.groupby@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/object.groupby/-/object.groupby-1.0.1.tgz#d41d9f3c8d6c778d9cbac86b4ee9f5af103152ee" + integrity sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + get-intrinsic "^1.2.1" + +object.values@^1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.7.tgz#617ed13272e7e1071b43973aa1655d9291b8442a" + integrity sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + +obuf@^1.0.0, obuf@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" + integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== + +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +on-headers@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" + integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== + +once@^1.3.0, once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +onetime@^5.1.0, onetime@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + +open@^8.0.9: + version "8.4.2" + resolved "https://registry.yarnpkg.com/open/-/open-8.4.2.tgz#5b5ffe2a8f793dcd2aad73e550cb87b59cb084f9" + integrity sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ== + dependencies: + define-lazy-prop "^2.0.0" + is-docker "^2.1.1" + is-wsl "^2.2.0" + +optionator@^0.9.1: + version "0.9.3" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64" + integrity sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg== + dependencies: + "@aashutoshrathi/word-wrap" "^1.2.3" + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + +os-browserify@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27" + integrity sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A== + +ospath@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/ospath/-/ospath-1.2.2.tgz#1276639774a3f8ef2572f7fe4280e0ea4550c07b" + integrity sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA== + +p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +p-map@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" + integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== + dependencies: + aggregate-error "^3.0.0" + +p-pipe@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-pipe/-/p-pipe-3.1.0.tgz#48b57c922aa2e1af6a6404cb7c6bf0eb9cc8e60e" + integrity sha512-08pj8ATpzMR0Y80x50yJHn37NF6vjrqHutASaX5LiH5npS9XPvrUmscd9MF5R4fuYRHOxQR1FfMIlF7AzwoPqw== + +p-retry@^4.5.0: + version "4.6.2" + resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.6.2.tgz#9baae7184057edd4e17231cee04264106e092a16" + integrity sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ== + dependencies: + "@types/retry" "0.12.0" + retry "^0.13.1" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +pako@~1.0.5: + version "1.0.11" + resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" + integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== + +param-case@^3.0.3: + version "3.0.4" + resolved "https://registry.yarnpkg.com/param-case/-/param-case-3.0.4.tgz#7d17fe4aa12bde34d4a77d91acfb6219caad01c5" + integrity sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A== + dependencies: + dot-case "^3.0.4" + tslib "^2.0.3" + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parse-asn1@^5.0.0, parse-asn1@^5.1.6: + version "5.1.6" + resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.6.tgz#385080a3ec13cb62a62d39409cb3e88844cdaed4" + integrity sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw== + dependencies: + asn1.js "^5.2.0" + browserify-aes "^1.0.0" + evp_bytestokey "^1.0.0" + pbkdf2 "^3.0.3" + safe-buffer "^5.1.1" + +parse-json@^5.0.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + +parseurl@~1.3.2, parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +pascal-case@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/pascal-case/-/pascal-case-3.1.2.tgz#b48e0ef2b98e205e7c1dae747d0b1508237660eb" + integrity sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g== + dependencies: + no-case "^3.0.4" + tslib "^2.0.3" + +path-browserify@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.1.tgz#e6c4ddd7ed3aa27c68a20cc4e50e1a4ee83bbc4a" + integrity sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ== + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.0.0, path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +pbkdf2@^3.0.3: + version "3.1.2" + resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.1.2.tgz#dd822aa0887580e52f1a039dc3eda108efae3075" + integrity sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA== + dependencies: + create-hash "^1.1.2" + create-hmac "^1.1.4" + ripemd160 "^2.0.1" + safe-buffer "^5.0.1" + sha.js "^2.4.8" + +pend@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" + integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg== + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== + +picocolors@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-0.2.1.tgz#570670f793646851d1ba135996962abad587859f" + integrity sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA== + +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pify@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== + +pkg-dir@^4.1.0, pkg-dir@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + +popper.js@1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.15.0.tgz#5560b99bbad7647e9faa475c6b8056621f5a4ff2" + integrity sha512-w010cY1oCUmI+9KwwlWki+r5jxKfTFDVoadl7MSrIujHU5MJ5OR6HTDj6Xo8aoR/QsA56x8jKjA59qGH4ELtrA== + +popper.js@^1.16: + version "1.16.1" + resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.1.tgz#2a223cb3dc7b6213d740e40372be40de43e65b1b" + integrity sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ== + +postcss-calc@^8.2.3: + version "8.2.4" + resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-8.2.4.tgz#77b9c29bfcbe8a07ff6693dc87050828889739a5" + integrity sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q== + dependencies: + postcss-selector-parser "^6.0.9" + postcss-value-parser "^4.2.0" + +postcss-colormin@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-5.3.1.tgz#86c27c26ed6ba00d96c79e08f3ffb418d1d1988f" + integrity sha512-UsWQG0AqTFQmpBegeLLc1+c3jIqBNB0zlDGRWR+dQ3pRKJL1oeMzyqmH3o2PIfn9MBdNrVPWhDbT769LxCTLJQ== + dependencies: + browserslist "^4.21.4" + caniuse-api "^3.0.0" + colord "^2.9.1" + postcss-value-parser "^4.2.0" + +postcss-convert-values@^5.1.3: + version "5.1.3" + resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-5.1.3.tgz#04998bb9ba6b65aa31035d669a6af342c5f9d393" + integrity sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA== + dependencies: + browserslist "^4.21.4" + postcss-value-parser "^4.2.0" + +postcss-discard-comments@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-5.1.2.tgz#8df5e81d2925af2780075840c1526f0660e53696" + integrity sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ== + +postcss-discard-duplicates@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz#9eb4fe8456706a4eebd6d3b7b777d07bad03e848" + integrity sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw== + +postcss-discard-empty@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz#e57762343ff7f503fe53fca553d18d7f0c369c6c" + integrity sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A== + +postcss-discard-overridden@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz#7e8c5b53325747e9d90131bb88635282fb4a276e" + integrity sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw== + +postcss-load-config@^3.1.0: + version "3.1.4" + resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-3.1.4.tgz#1ab2571faf84bb078877e1d07905eabe9ebda855" + integrity sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg== + dependencies: + lilconfig "^2.0.5" + yaml "^1.10.2" + +postcss-loader@^6.2.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-6.2.1.tgz#0895f7346b1702103d30fdc66e4d494a93c008ef" + integrity sha512-WbbYpmAaKcux/P66bZ40bpWsBucjx/TTgVVzRZ9yUO8yQfVBlameJ0ZGVaPfH64hNSBh63a+ICP5nqOpBA0w+Q== + dependencies: + cosmiconfig "^7.0.0" + klona "^2.0.5" + semver "^7.3.5" + +postcss-merge-longhand@^5.1.7: + version "5.1.7" + resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-5.1.7.tgz#24a1bdf402d9ef0e70f568f39bdc0344d568fb16" + integrity sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ== + dependencies: + postcss-value-parser "^4.2.0" + stylehacks "^5.1.1" + +postcss-merge-rules@^5.1.4: + version "5.1.4" + resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-5.1.4.tgz#2f26fa5cacb75b1402e213789f6766ae5e40313c" + integrity sha512-0R2IuYpgU93y9lhVbO/OylTtKMVcHb67zjWIfCiKR9rWL3GUk1677LAqD/BcHizukdZEjT8Ru3oHRoAYoJy44g== + dependencies: + browserslist "^4.21.4" + caniuse-api "^3.0.0" + cssnano-utils "^3.1.0" + postcss-selector-parser "^6.0.5" + +postcss-minify-font-values@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-5.1.0.tgz#f1df0014a726083d260d3bd85d7385fb89d1f01b" + integrity sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-minify-gradients@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-5.1.1.tgz#f1fe1b4f498134a5068240c2f25d46fcd236ba2c" + integrity sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw== + dependencies: + colord "^2.9.1" + cssnano-utils "^3.1.0" + postcss-value-parser "^4.2.0" + +postcss-minify-params@^5.1.4: + version "5.1.4" + resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-5.1.4.tgz#c06a6c787128b3208b38c9364cfc40c8aa5d7352" + integrity sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw== + dependencies: + browserslist "^4.21.4" + cssnano-utils "^3.1.0" + postcss-value-parser "^4.2.0" + +postcss-minify-selectors@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-5.2.1.tgz#d4e7e6b46147b8117ea9325a915a801d5fe656c6" + integrity sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg== + dependencies: + postcss-selector-parser "^6.0.5" + +postcss-modules-extract-imports@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz#cda1f047c0ae80c97dbe28c3e76a43b88025741d" + integrity sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw== + +postcss-modules-local-by-default@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.3.tgz#b08eb4f083050708998ba2c6061b50c2870ca524" + integrity sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA== + dependencies: + icss-utils "^5.0.0" + postcss-selector-parser "^6.0.2" + postcss-value-parser "^4.1.0" + +postcss-modules-scope@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz#9ef3151456d3bbfa120ca44898dfca6f2fa01f06" + integrity sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg== + dependencies: + postcss-selector-parser "^6.0.4" + +postcss-modules-values@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz#d7c5e7e68c3bb3c9b27cbf48ca0bb3ffb4602c9c" + integrity sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ== + dependencies: + icss-utils "^5.0.0" + +postcss-normalize-charset@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz#9302de0b29094b52c259e9b2cf8dc0879879f0ed" + integrity sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg== + +postcss-normalize-display-values@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-normalize-display-values/-/postcss-normalize-display-values-5.1.0.tgz#72abbae58081960e9edd7200fcf21ab8325c3da8" + integrity sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-normalize-positions@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-positions/-/postcss-normalize-positions-5.1.1.tgz#ef97279d894087b59325b45c47f1e863daefbb92" + integrity sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-normalize-repeat-style@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.1.1.tgz#e9eb96805204f4766df66fd09ed2e13545420fb2" + integrity sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-normalize-string@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-normalize-string/-/postcss-normalize-string-5.1.0.tgz#411961169e07308c82c1f8c55f3e8a337757e228" + integrity sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-normalize-timing-functions@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.1.0.tgz#d5614410f8f0b2388e9f240aa6011ba6f52dafbb" + integrity sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-normalize-unicode@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-unicode/-/postcss-normalize-unicode-5.1.1.tgz#f67297fca3fea7f17e0d2caa40769afc487aa030" + integrity sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA== + dependencies: + browserslist "^4.21.4" + postcss-value-parser "^4.2.0" + +postcss-normalize-url@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-5.1.0.tgz#ed9d88ca82e21abef99f743457d3729a042adcdc" + integrity sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew== + dependencies: + normalize-url "^6.0.1" + postcss-value-parser "^4.2.0" + +postcss-normalize-whitespace@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.1.1.tgz#08a1a0d1ffa17a7cc6efe1e6c9da969cc4493cfa" + integrity sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-ordered-values@^5.1.3: + version "5.1.3" + resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-5.1.3.tgz#b6fd2bd10f937b23d86bc829c69e7732ce76ea38" + integrity sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ== + dependencies: + cssnano-utils "^3.1.0" + postcss-value-parser "^4.2.0" + +postcss-purgecss-laravel@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/postcss-purgecss-laravel/-/postcss-purgecss-laravel-2.0.0.tgz#f714a4f02a6c839a0b8afca2215693b3735c394c" + integrity sha512-vWObgEC5f0isOdumiLwzJPuZFyp7i1Go9i2Obce5qrVJWciBtCG1rrNiPEb7xp5bU3u/uk30M2P891tLL8tcQQ== + dependencies: + "@fullhuman/postcss-purgecss" "^3.0.0" + +postcss-reduce-initial@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-5.1.2.tgz#798cd77b3e033eae7105c18c9d371d989e1382d6" + integrity sha512-dE/y2XRaqAi6OvjzD22pjTUQ8eOfc6m/natGHgKFBK9DxFmIm69YmaRVQrGgFlEfc1HePIurY0TmDeROK05rIg== + dependencies: + browserslist "^4.21.4" + caniuse-api "^3.0.0" + +postcss-reduce-transforms@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-5.1.0.tgz#333b70e7758b802f3dd0ddfe98bb1ccfef96b6e9" + integrity sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5, postcss-selector-parser@^6.0.9: + version "6.0.13" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz#d05d8d76b1e8e173257ef9d60b706a8e5e99bf1b" + integrity sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + +postcss-svgo@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-5.1.0.tgz#0a317400ced789f233a28826e77523f15857d80d" + integrity sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA== + dependencies: + postcss-value-parser "^4.2.0" + svgo "^2.7.0" + +postcss-unique-selectors@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-5.1.1.tgz#a9f273d1eacd09e9aa6088f4b0507b18b1b541b6" + integrity sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA== + dependencies: + postcss-selector-parser "^6.0.5" + +postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" + integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== + +postcss@^7.0.35, postcss@^7.0.36: + version "7.0.39" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.39.tgz#9624375d965630e2e1f2c02a935c82a59cb48309" + integrity sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA== + dependencies: + picocolors "^0.2.1" + source-map "^0.6.1" + +postcss@^8.2.1, postcss@^8.2.13, postcss@^8.2.15, postcss@^8.4.14: + version "8.4.31" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d" + integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ== + dependencies: + nanoid "^3.3.6" + picocolors "^1.0.0" + source-map-js "^1.0.2" + +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + +"prettier@^1.18.2 || ^2.0.0": + version "2.8.8" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" + integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== + +pretty-bytes@^5.6.0: + version "5.6.0" + resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" + integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== + +pretty-checkbox-vue@^1.1: + version "1.1.9" + resolved "https://registry.yarnpkg.com/pretty-checkbox-vue/-/pretty-checkbox-vue-1.1.9.tgz#2d4bfc7f20c54a0e7b94b3d205641dbf8f390fb4" + integrity sha512-45HOanzF+BUTD5prwCoNrtEFYVzWtASTIIPtPQxGCajC097pFD/9mbyjEjoTsu8Tk4/rSyA7RNk6JpFWVHpLag== + dependencies: + pretty-checkbox "^3.0.3" + +pretty-checkbox@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/pretty-checkbox/-/pretty-checkbox-3.0.3.tgz#d49c8013a8fc08ee0c2d6ebde453464bfdbc428e" + integrity sha512-kCLsENsJ6h5Bcq106Q3YMSxuz2q3jtIXP7fgDB/+jZjUsZjRjAoL9Lr1TVwAEcugufVBhr5Mfd9L7P6d+SR+Yw== + +pretty-time@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/pretty-time/-/pretty-time-1.1.0.tgz#ffb7429afabb8535c346a34e41873adf3d74dd0e" + integrity sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA== + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +process@^0.11.10: + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== + +progress@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" + integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +pseudomap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" + integrity sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ== + +psl@^1.1.33: + version "1.9.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" + integrity sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag== + +public-encrypt@^4.0.0: + version "4.0.3" + resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.3.tgz#4fcc9d77a07e48ba7527e7cbe0de33d0701331e0" + integrity sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q== + dependencies: + bn.js "^4.1.0" + browserify-rsa "^4.0.0" + create-hash "^1.1.0" + parse-asn1 "^5.0.0" + randombytes "^2.0.1" + safe-buffer "^5.1.2" + +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +punycode@^1.2.4, punycode@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + integrity sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ== + +punycode@^2.1.0, punycode@^2.1.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + +purgecss@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/purgecss/-/purgecss-3.1.3.tgz#26987ec09d12eeadc318e22f6e5a9eb0be094f41" + integrity sha512-hRSLN9mguJ2lzlIQtW4qmPS2kh6oMnA9RxdIYK8sz18QYqd6ePp4GNDl18oWHA1f2v2NEQIh51CO8s/E3YGckQ== + dependencies: + commander "^6.0.0" + glob "^7.0.0" + postcss "^8.2.1" + postcss-selector-parser "^6.0.2" + +qs@6.11.0: + version "6.11.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" + +qs@^6.11.2: + version "6.11.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9" + integrity sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA== + dependencies: + side-channel "^1.0.4" + +qs@~6.10.3: + version "6.10.5" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.5.tgz#974715920a80ff6a262264acd2c7e6c2a53282b4" + integrity sha512-O5RlPh0VFtR78y79rgcgKK4wbAI0C5zGVLztOIdpWX6ep368q5Hv6XRxDvXuZ9q3C6v+e3n8UfZZJw7IIG27eQ== + dependencies: + side-channel "^1.0.4" + +querystring-es3@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" + integrity sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA== + +querystringify@^2.1.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.2.0.tgz#3345941b4153cb9d082d8eee4cda2016a9aef7f6" + integrity sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ== + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +ramda@~0.27.1: + version "0.27.2" + resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.27.2.tgz#84463226f7f36dc33592f6f4ed6374c48306c3f1" + integrity sha512-SbiLPU40JuJniHexQSAgad32hfwd+DRUdwF2PlVuI5RZD0/vahUco7R8vD86J/tcEKKF9vZrUVwgtmGCqlCKyA== + +randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +randomfill@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/randomfill/-/randomfill-1.0.4.tgz#c92196fc86ab42be983f1bf31778224931d61458" + integrity sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw== + dependencies: + randombytes "^2.0.5" + safe-buffer "^5.1.0" + +range-parser@^1.2.1, range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857" + integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.3.3, readable-stream@^2.3.6: + version "2.3.8" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.0.6, readable-stream@^3.6.0, readable-stream@^3.6.2: + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +rechoir@^0.7.0: + version "0.7.1" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.7.1.tgz#9478a96a1ca135b5e88fc027f03ee92d6c645686" + integrity sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg== + dependencies: + resolve "^1.9.0" + +regenerate-unicode-properties@^10.1.0: + version "10.1.1" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz#6b0e05489d9076b04c436f318d9b067bba459480" + integrity sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q== + dependencies: + regenerate "^1.4.2" + +regenerate@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" + integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== + +regenerator-runtime@^0.14.0: + version "0.14.0" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45" + integrity sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA== + +regenerator-transform@^0.15.2: + version "0.15.2" + resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.2.tgz#5bbae58b522098ebdf09bca2f83838929001c7a4" + integrity sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg== + dependencies: + "@babel/runtime" "^7.8.4" + +regex-parser@^2.2.11: + version "2.2.11" + resolved "https://registry.yarnpkg.com/regex-parser/-/regex-parser-2.2.11.tgz#3b37ec9049e19479806e878cabe7c1ca83ccfe58" + integrity sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q== + +regexp.prototype.flags@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz#90ce989138db209f81492edd734183ce99f9677e" + integrity sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + set-function-name "^2.0.0" + +regexpp@^3.0.0, regexpp@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" + integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== + +regexpu-core@^5.3.1: + version "5.3.2" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.3.2.tgz#11a2b06884f3527aec3e93dbbf4a3b958a95546b" + integrity sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ== + dependencies: + "@babel/regjsgen" "^0.8.0" + regenerate "^1.4.2" + regenerate-unicode-properties "^10.1.0" + regjsparser "^0.9.1" + unicode-match-property-ecmascript "^2.0.0" + unicode-match-property-value-ecmascript "^2.1.0" + +regjsparser@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.9.1.tgz#272d05aa10c7c1f67095b1ff0addae8442fc5709" + integrity sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ== + dependencies: + jsesc "~0.5.0" + +relateurl@^0.2.7: + version "0.2.7" + resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" + integrity sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog== + +replace-ext@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.1.tgz#2d6d996d04a15855d967443631dd5f77825b016a" + integrity sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw== + +request-progress@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/request-progress/-/request-progress-3.0.0.tgz#4ca754081c7fec63f505e4faa825aa06cd669dbe" + integrity sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg== + dependencies: + throttleit "^1.0.0" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== + +resolve-cwd@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" + integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== + dependencies: + resolve-from "^5.0.0" + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + +resolve-url-loader@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-url-loader/-/resolve-url-loader-4.0.0.tgz#d50d4ddc746bb10468443167acf800dcd6c3ad57" + integrity sha512-05VEMczVREcbtT7Bz+C+96eUO5HDNvdthIiMB34t7FcF8ehcu4wC0sSgPUubs3XW2Q3CNLJk/BJrCU9wVRymiA== + dependencies: + adjust-sourcemap-loader "^4.0.0" + convert-source-map "^1.7.0" + loader-utils "^2.0.0" + postcss "^7.0.35" + source-map "0.6.1" + +resolve@^1.10.1, resolve@^1.14.2, resolve@^1.22.4, resolve@^1.9.0: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +restore-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" + integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== + dependencies: + onetime "^5.1.0" + signal-exit "^3.0.2" + +retry@^0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" + integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +rfdc@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b" + integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== + +rimraf@^3.0.0, rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +ripemd160@^2.0.0, ripemd160@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" + integrity sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA== + dependencies: + hash-base "^3.0.0" + inherits "^2.0.1" + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +rx-js@^0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/rx-js/-/rx-js-0.0.0.tgz#5d99a2416971722633eddedf0baa3c672445e49f" + integrity sha512-SRel6ja0HI6B9NEHy9FOUVDEsk4FA3oLQS1DqwsWu9HJ32yL0HJzQINE0PHH0Ne3W5mIRP+zPkgmiIA40zkkWg== + +rxjs@^7.5.1: + version "7.8.1" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.1.tgz#6f6f3d99ea8044291efd92e7c7fcf562c4057543" + integrity sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg== + dependencies: + tslib "^2.1.0" + +safe-array-concat@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.0.1.tgz#91686a63ce3adbea14d61b14c99572a8ff84754c" + integrity sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.2.1" + has-symbols "^1.0.3" + isarray "^2.0.5" + +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@^5.2.1, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-regex-test@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.0.tgz#793b874d524eb3640d1873aad03596db2d4f2295" + integrity sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.3" + is-regex "^1.1.4" + +"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +sass-loader@^11.0: + version "11.1.1" + resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-11.1.1.tgz#0db441bbbe197b2af96125bebb7f4be6476b13a7" + integrity sha512-fOCp/zLmj1V1WHDZbUbPgrZhA7HKXHEqkslzB+05U5K9SbSbcmH91C7QLW31AsXikxUMaxXRhhcqWZAxUMLDyA== + dependencies: + klona "^2.0.4" + neo-async "^2.6.2" + +sass@^1.32: + version "1.69.5" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.69.5.tgz#23e18d1c757a35f2e52cc81871060b9ad653dfde" + integrity sha512-qg2+UCJibLr2LCVOt3OlPhr/dqVHWOa9XtZf2OjbLs/T4VPSJ00udtgJxH3neXZm+QqX8B+3cU7RaLqp1iVfcQ== + dependencies: + chokidar ">=3.0.0 <4.0.0" + immutable "^4.0.0" + source-map-js ">=0.6.2 <2.0.0" + +schema-utils@^2.6.5: + version "2.7.1" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7" + integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg== + dependencies: + "@types/json-schema" "^7.0.5" + ajv "^6.12.4" + ajv-keywords "^3.5.2" + +schema-utils@^3.0.0, schema-utils@^3.1.1, schema-utils@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe" + integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== + dependencies: + "@types/json-schema" "^7.0.8" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + +schema-utils@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.2.0.tgz#70d7c93e153a273a805801882ebd3bff20d89c8b" + integrity sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw== + dependencies: + "@types/json-schema" "^7.0.9" + ajv "^8.9.0" + ajv-formats "^2.1.1" + ajv-keywords "^5.1.0" + +select-hose@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" + integrity sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg== + +select@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d" + integrity sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA== + +selfsigned@^2.1.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-2.4.1.tgz#560d90565442a3ed35b674034cec4e95dceb4ae0" + integrity sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q== + dependencies: + "@types/node-forge" "^1.3.0" + node-forge "^1" + +semver@^6.0.0, semver@^6.1.0, semver@^6.3.0, semver@^6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +semver@^7.2.1, semver@^7.3.2, semver@^7.3.5: + version "7.5.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" + integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== + dependencies: + lru-cache "^6.0.0" + +send@0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" + integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +serialize-javascript@6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8" + integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag== + dependencies: + randombytes "^2.1.0" + +serialize-javascript@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.1.tgz#b206efb27c3da0b0ab6b52f48d170b7996458e5c" + integrity sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w== + dependencies: + randombytes "^2.1.0" + +serve-index@^1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239" + integrity sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw== + dependencies: + accepts "~1.3.4" + batch "0.6.1" + debug "2.6.9" + escape-html "~1.0.3" + http-errors "~1.6.2" + mime-types "~2.1.17" + parseurl "~1.3.2" + +serve-static@1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" + integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.18.0" + +set-function-length@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.1.1.tgz#4bc39fafb0307224a33e106a7d35ca1218d659ed" + integrity sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ== + dependencies: + define-data-property "^1.1.1" + get-intrinsic "^1.2.1" + gopd "^1.0.1" + has-property-descriptors "^1.0.0" + +set-function-name@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.1.tgz#12ce38b7954310b9f61faa12701620a0c882793a" + integrity sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA== + dependencies: + define-data-property "^1.0.1" + functions-have-names "^1.2.3" + has-property-descriptors "^1.0.0" + +setimmediate@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA== + +setprototypeof@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" + integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +sha.js@^2.4.0, sha.js@^2.4.8: + version "2.4.11" + resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" + integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + +shallow-clone@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" + integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== + dependencies: + kind-of "^6.0.2" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +shell-quote@^1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680" + integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== + +shellwords@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" + integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== + +side-channel@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + dependencies: + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" + +signal-exit@^3.0.2, signal-exit@^3.0.3: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +slice-ansi@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787" + integrity sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + +slice-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" + integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== + dependencies: + ansi-styles "^4.0.0" + astral-regex "^2.0.0" + is-fullwidth-code-point "^3.0.0" + +sockjs@^0.3.24: + version "0.3.24" + resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.24.tgz#c9bc8995f33a111bea0395ec30aa3206bdb5ccce" + integrity sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ== + dependencies: + faye-websocket "^0.11.3" + uuid "^8.3.2" + websocket-driver "^0.7.4" + +source-list-map@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" + integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== + +"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" + integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== + +source-map-support@~0.5.12, source-map-support@~0.5.20: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@0.6.1, source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +spdy-transport@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31" + integrity sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw== + dependencies: + debug "^4.1.0" + detect-node "^2.0.4" + hpack.js "^2.1.6" + obuf "^1.1.2" + readable-stream "^3.0.6" + wbuf "^1.7.3" + +spdy@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/spdy/-/spdy-4.0.2.tgz#b74f466203a3eda452c02492b91fb9e84a27677b" + integrity sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA== + dependencies: + debug "^4.1.0" + handle-thing "^2.0.0" + http-deceiver "^1.2.7" + select-hose "^2.0.0" + spdy-transport "^3.0.0" + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== + +sshpk@^1.14.1: + version "1.18.0" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.18.0.tgz#1663e55cddf4d688b86a46b77f0d5fe363aba028" + integrity sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ== + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + bcrypt-pbkdf "^1.0.0" + dashdash "^1.12.0" + ecc-jsbn "~0.1.1" + getpass "^0.1.1" + jsbn "~0.1.0" + safer-buffer "^2.0.2" + tweetnacl "~0.14.0" + +stable@^0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" + integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +"statuses@>= 1.4.0 < 2": + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== + +std-env@^3.0.1: + version "3.4.3" + resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.4.3.tgz#326f11db518db751c83fd58574f449b7c3060910" + integrity sha512-f9aPhy8fYBuMN+sNfakZV18U39PbalgjXG3lLB9WkaYTxijru61wb57V9wxxNthXM5Sd88ETBWi29qLAsHO52Q== + +stream-browserify@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.2.tgz#87521d38a44aa7ee91ce1cd2a47df0cb49dd660b" + integrity sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg== + dependencies: + inherits "~2.0.1" + readable-stream "^2.0.2" + +stream-http@^2.7.2: + version "2.8.3" + resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.3.tgz#b2d242469288a5a27ec4fe8933acf623de6514fc" + integrity sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw== + dependencies: + builtin-status-codes "^3.0.0" + inherits "^2.0.1" + readable-stream "^2.3.6" + to-arraybuffer "^1.0.0" + xtend "^4.0.0" + +string-natural-compare@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-2.0.3.tgz#9dbe1dd65490a5fe14f7a5c9bc686fc67cb9c6e4" + integrity sha512-4Kcl12rNjc+6EKhY8QyDVuQTAlMWwRiNbsxnVwBUKFr7dYPQuXVrtNU4sEkjF9LHY0AY6uVbB3ktbkIH4LC+BQ== + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string.prototype.trim@^1.2.8: + version "1.2.8" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz#f9ac6f8af4bd55ddfa8895e6aea92a96395393bd" + integrity sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + +string.prototype.trimend@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz#1bb3afc5008661d73e2dc015cd4853732d6c471e" + integrity sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + +string.prototype.trimstart@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz#d4cdb44b83a4737ffbac2d406e405d43d0184298" + integrity sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + +string_decoder@^1.0.0, string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== + +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + +strip-json-comments@3.1.1, strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +style-loader@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-2.0.0.tgz#9669602fd4690740eaaec137799a03addbbc393c" + integrity sha512-Z0gYUJmzZ6ZdRUqpg1r8GsaFKypE+3xAzuFeMuoHgjc9KZv3wMyCRjQIWEbhoFSq7+7yoHXySDJyyWQaPajeiQ== + dependencies: + loader-utils "^2.0.0" + schema-utils "^3.0.0" + +stylehacks@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-5.1.1.tgz#7934a34eb59d7152149fa69d6e9e56f2fc34bcc9" + integrity sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw== + dependencies: + browserslist "^4.21.4" + postcss-selector-parser "^6.0.4" + +supports-color@8.1.1, supports-color@^8.0.0, supports-color@^8.1.1: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +svgo@^2.7.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/svgo/-/svgo-2.8.0.tgz#4ff80cce6710dc2795f0c7c74101e6764cfccd24" + integrity sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg== + dependencies: + "@trysound/sax" "0.2.0" + commander "^7.2.0" + css-select "^4.1.3" + css-tree "^1.1.3" + csso "^4.2.0" + picocolors "^1.0.0" + stable "^0.1.8" + +sweet-modal-vue@^2.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/sweet-modal-vue/-/sweet-modal-vue-2.0.0.tgz#205ee28b7e5c8579e44303544b500ef7f4fcab21" + integrity sha512-1/F7G3I3dWU5O2RGnHEirf6zw2AeR/CdfxiIlcEjxeyxGqgPtQmhjHoHQbem6bZTg1rFS5asVCOXMrTOyZgeJg== + +table@^6.0.9: + version "6.8.1" + resolved "https://registry.yarnpkg.com/table/-/table-6.8.1.tgz#ea2b71359fe03b017a5fbc296204471158080bdf" + integrity sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA== + dependencies: + ajv "^8.0.1" + lodash.truncate "^4.4.2" + slice-ansi "^4.0.0" + string-width "^4.2.3" + strip-ansi "^6.0.1" + +tachyons@^4.12: + version "4.12.0" + resolved "https://registry.yarnpkg.com/tachyons/-/tachyons-4.12.0.tgz#6fdfa8360927a46a1efd996a4dcc94f04bd31df0" + integrity sha512-2nA2IrYFy3raCM9fxJ2KODRGHVSZNTW3BR0YnlGsLUf1DA3pk3YfWZ/DdfbnZK6zLZS+jUenlUGJsKcA5fUiZg== + +tapable@^2.1.1, tapable@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" + integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== + +terser-webpack-plugin@^5.2.4, terser-webpack-plugin@^5.3.7: + version "5.3.9" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz#832536999c51b46d468067f9e37662a3b96adfe1" + integrity sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA== + dependencies: + "@jridgewell/trace-mapping" "^0.3.17" + jest-worker "^27.4.5" + schema-utils "^3.1.1" + serialize-javascript "^6.0.1" + terser "^5.16.8" + +terser@^4.6.3: + version "4.8.1" + resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.1.tgz#a00e5634562de2239fd404c649051bf6fc21144f" + integrity sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw== + dependencies: + commander "^2.20.0" + source-map "~0.6.1" + source-map-support "~0.5.12" + +terser@^5.16.8, terser@^5.9.0: + version "5.24.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.24.0.tgz#4ae50302977bca4831ccc7b4fef63a3c04228364" + integrity sha512-ZpGR4Hy3+wBEzVEnHvstMvqpD/nABNelQn/z2r0fjVWGQsN3bpOLzQlqDxmb4CDZnXq5lpjnQ+mHQLAOpfM5iw== + dependencies: + "@jridgewell/source-map" "^0.3.3" + acorn "^8.8.2" + commander "^2.20.0" + source-map-support "~0.5.20" + +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== + +throttleit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c" + integrity sha512-rkTVqu6IjfQ/6+uNuuc3sZek4CEYxTJom3IktzgdSxcZqdARuebbA/f4QmAxMQIxqq9ZLEUkSYqvuk1I6VKq4g== + +through@^2.3.8: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== + +thunky@^1.0.2: + version "1.1.0" + resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" + integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== + +timers-browserify@^2.0.4: + version "2.0.12" + resolved "https://registry.yarnpkg.com/timers-browserify/-/timers-browserify-2.0.12.tgz#44a45c11fbf407f34f97bccd1577c652361b00ee" + integrity sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ== + dependencies: + setimmediate "^1.0.4" + +tiny-emitter@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423" + integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q== + +tmp@~0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" + integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ== + dependencies: + rimraf "^3.0.0" + +to-arraybuffer@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43" + integrity sha512-okFlQcoGTi4LQBG/PgSYblw9VOyptsz2KJZqc6qtgGdes8VktzUQkj4BI2blit072iS8VODNcMA+tvnS9dnuMA== + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +tough-cookie@^4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.1.3.tgz#97b9adb0728b42280aa3d814b6b999b2ff0318bf" + integrity sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw== + dependencies: + psl "^1.1.33" + punycode "^2.1.1" + universalify "^0.2.0" + url-parse "^1.5.3" + +tsconfig-paths@^3.14.2: + version "3.14.2" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz#6e32f1f79412decd261f92d633a9dc1cfa99f088" + integrity sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g== + dependencies: + "@types/json5" "^0.0.29" + json5 "^1.0.2" + minimist "^1.2.6" + strip-bom "^3.0.0" + +tslib@^2.0.3, tslib@^2.1.0: + version "2.6.2" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" + integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== + +tty-browserify@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" + integrity sha512-JVa5ijo+j/sOoHGjw0sxw734b1LhBkQ3bvUGNdxnVXDCX81Yx7TFgnZygxrIIWn23hbfTaMYLwRmAxFyDuFmIw== + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + integrity sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w== + dependencies: + safe-buffer "^5.0.1" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + integrity sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA== + +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== + +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== + +type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +typed-array-buffer@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz#18de3e7ed7974b0a729d3feecb94338d1472cd60" + integrity sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.2.1" + is-typed-array "^1.1.10" + +typed-array-byte-length@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz#d787a24a995711611fb2b87a4052799517b230d0" + integrity sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA== + dependencies: + call-bind "^1.0.2" + for-each "^0.3.3" + has-proto "^1.0.1" + is-typed-array "^1.1.10" + +typed-array-byte-offset@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz#cbbe89b51fdef9cd6aaf07ad4707340abbc4ea0b" + integrity sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + for-each "^0.3.3" + has-proto "^1.0.1" + is-typed-array "^1.1.10" + +typed-array-length@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.4.tgz#89d83785e5c4098bec72e08b319651f0eac9c1bb" + integrity sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng== + dependencies: + call-bind "^1.0.2" + for-each "^0.3.3" + is-typed-array "^1.1.9" + +unbox-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" + integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw== + dependencies: + call-bind "^1.0.2" + has-bigints "^1.0.2" + has-symbols "^1.0.3" + which-boxed-primitive "^1.0.2" + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + +unicode-canonical-property-names-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" + integrity sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ== + +unicode-match-property-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz#54fd16e0ecb167cf04cf1f756bdcc92eba7976c3" + integrity sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q== + dependencies: + unicode-canonical-property-names-ecmascript "^2.0.0" + unicode-property-aliases-ecmascript "^2.0.0" + +unicode-match-property-value-ecmascript@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz#cb5fffdcd16a05124f5a4b0bf7c3770208acbbe0" + integrity sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA== + +unicode-property-aliases-ecmascript@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz#43d41e3be698bd493ef911077c9b131f827e8ccd" + integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w== + +universalify@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" + integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg== + +universalify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" + integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +untildify@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b" + integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw== + +update-browserslist-db@^1.0.13: + version "1.0.13" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz#3c5e4f5c083661bd38ef64b6328c26ed6c8248c4" + integrity sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg== + dependencies: + escalade "^3.1.1" + picocolors "^1.0.0" + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +url-parse@^1.5.3: + version "1.5.10" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1" + integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ== + dependencies: + querystringify "^2.1.1" + requires-port "^1.0.0" + +url@^0.11.0: + version "0.11.3" + resolved "https://registry.yarnpkg.com/url/-/url-0.11.3.tgz#6f495f4b935de40ce4a0a52faee8954244f3d3ad" + integrity sha512-6hxOLGfZASQK/cijlZnZJTq8OXAkt/3YGfQX45vvMYXpZoo8NdWZcY73K108Jf759lS1Bv/8wXnHDTSz17dSRw== + dependencies: + punycode "^1.4.1" + qs "^6.11.2" + +util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +util@^0.10.4: + version "0.10.4" + resolved "https://registry.yarnpkg.com/util/-/util-0.10.4.tgz#3aa0125bfe668a4672de58857d3ace27ecb76901" + integrity sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A== + dependencies: + inherits "2.0.3" + +util@^0.11.0: + version "0.11.1" + resolved "https://registry.yarnpkg.com/util/-/util-0.11.1.tgz#3236733720ec64bb27f6e26f421aaa2e1b588d61" + integrity sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ== + dependencies: + inherits "2.0.3" + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +uuid@^8.3.0, uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + +v8-compile-cache@^2.0.3: + version "2.4.0" + resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.4.0.tgz#cdada8bec61e15865f05d097c5f4fd30e94dc128" + integrity sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw== + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + integrity sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw== + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +vm-browserify@^1.0.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" + integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== + +vue-autosuggest@^2.2: + version "2.2.0" + resolved "https://registry.yarnpkg.com/vue-autosuggest/-/vue-autosuggest-2.2.0.tgz#0202b9aaeeef6a3a4357bf401a73be2ecbe166f1" + integrity sha512-cHgEakpoRUOaqXXEo8RcRrbSTM3eAaCu9b55ZXiKbaS6IUD8ewqffQrMy/A1DXqHSQbyEEGui4oAsCbRge29Jg== + +vue-checkbox-radio@^0.6: + version "0.6.0" + resolved "https://registry.yarnpkg.com/vue-checkbox-radio/-/vue-checkbox-radio-0.6.0.tgz#616d83aaab60dbcad9ae18ffb7d9d471f4746f76" + integrity sha512-qaXzRR9Mji5onbYPvxXbXdCSHkJmauMirCWnHYG4uNLy7xXNoSBJOtetMnuE2KkVL6DbLycFe/uCLmx7LrXoNg== + +vue-clipboard2@^0.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/vue-clipboard2/-/vue-clipboard2-0.3.3.tgz#331fec85f9d4f175eb0d4feaef4d77338562af36" + integrity sha512-aNWXIL2DKgJyY/1OOeITwAQz1fHaCIGvUFHf9h8UcoQBG5a74MkdhS/xqoYe7DNZdQmZRL+TAdIbtUs9OyVjbw== + dependencies: + clipboard "^2.0.0" + +vue-directive-tooltip@^1.6: + version "1.6.3" + resolved "https://registry.yarnpkg.com/vue-directive-tooltip/-/vue-directive-tooltip-1.6.3.tgz#18d1a645b5649a45748884049c819a13f03e1ff5" + integrity sha512-aSdlBIdibctL+Tw+2j+IVUtz/fOZPLMQz0xxSoIGOA223WsPahiybSykJHx6QxtPzWq2phE/OZaCTmvMOVPyeA== + dependencies: + popper.js "1.15.0" + +vue-eslint-parser@^7.10.0: + version "7.11.0" + resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-7.11.0.tgz#214b5dea961007fcffb2ee65b8912307628d0daf" + integrity sha512-qh3VhDLeh773wjgNTl7ss0VejY9bMMa0GoDG2fQVyDzRFdiU3L7fw74tWZDHNQXdZqxO3EveQroa9ct39D2nqg== + dependencies: + debug "^4.1.1" + eslint-scope "^5.1.1" + eslint-visitor-keys "^1.1.0" + espree "^6.2.1" + esquery "^1.4.0" + lodash "^4.17.21" + semver "^6.3.0" + +vue-good-table@^2.21: + version "2.21.11" + resolved "https://registry.yarnpkg.com/vue-good-table/-/vue-good-table-2.21.11.tgz#0d92a36f4119bc91825009780c09df38d27b5a4b" + integrity sha512-OpVPdxbBTahtfq1aXxEa5P1CMy1wiLcBg4mo7k6Qs537l9v8KVrvF+fXqbnxqNrAfmd1Mw9LidcjgTErjmVU8g== + dependencies: + date-fns "^2.17.0" + lodash.isequal "^4.5.0" + +vue-hot-reload-api@^2.3.0: + version "2.3.4" + resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz#532955cc1eb208a3d990b3a9f9a70574657e08f2" + integrity sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog== + +vue-i18n@^8.24: + version "8.28.2" + resolved "https://registry.yarnpkg.com/vue-i18n/-/vue-i18n-8.28.2.tgz#913558066e274395c0a9f40b2f3393d5c2636840" + integrity sha512-C5GZjs1tYlAqjwymaaCPDjCyGo10ajUphiwA922jKt9n7KPpqR7oM1PCwYzhB/E7+nT3wfdG3oRre5raIT1rKA== + +vue-js-toggle-button@^1.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/vue-js-toggle-button/-/vue-js-toggle-button-1.3.3.tgz#d603089039e41d45e607355ad2e0478c6a52aceb" + integrity sha512-0b920oztgK+1SqlYF26MPiT28hAieL5aAQE7u21XEym5ryfzD4EMer4hLkgDC/1sWsCHb22GvV+t1Kb4AI6QFw== + +vue-loader@^15.9: + version "15.11.1" + resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-15.11.1.tgz#dee91169211276ed43c5715caef88a56b1f497b0" + integrity sha512-0iw4VchYLePqJfJu9s62ACWUXeSqM30SQqlIftbYWM3C+jpPcEHKSPUZBLjSF9au4HTHQ/naF6OGnO3Q/qGR3Q== + dependencies: + "@vue/component-compiler-utils" "^3.1.0" + hash-sum "^1.0.2" + loader-utils "^1.1.0" + vue-hot-reload-api "^2.3.0" + vue-style-loader "^4.1.0" + +vue-notification@^1.3: + version "1.3.20" + resolved "https://registry.yarnpkg.com/vue-notification/-/vue-notification-1.3.20.tgz#d85618127763b46f3e25b8962b857947d5a97cbe" + integrity sha512-vPj67Ah72p8xvtyVE8emfadqVWguOScAjt6OJDEUdcW5hW189NsqvfkOrctxHUUO9UYl9cTbIkzAEcPnHu+zBQ== + +vue-rx@^6.2: + version "6.2.0" + resolved "https://registry.yarnpkg.com/vue-rx/-/vue-rx-6.2.0.tgz#c3b86462b252626edd16417385bb9054a3a23acb" + integrity sha512-tpKUcqS5IUYS+HsdbR5TlE5LL9PK4zwlplEtmMeydnbqaUTa9ciLMplJXAtFSiQw1vuURoyEJmCXqMxaVEIloQ== + +vue-select@^3.11: + version "3.20.2" + resolved "https://registry.yarnpkg.com/vue-select/-/vue-select-3.20.2.tgz#eaa15012b032c154d4fb51ac82ebeda2beeae54b" + integrity sha512-ZSzIDzyYsWZULGUxVp1h6u3yi9IZQBWX8r6kSudUI/I5J1HQKpBjRntvkrg6pr87xmm16kdChvHCDN+W84vTKw== + +vue-style-loader@^4.1.0, vue-style-loader@^4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/vue-style-loader/-/vue-style-loader-4.1.3.tgz#6d55863a51fa757ab24e89d9371465072aa7bc35" + integrity sha512-sFuh0xfbtpRlKfm39ss/ikqs9AbKCoXZBpHeVZ8Tx650o0k0q/YCM7FRvigtxpACezfq6af+a7JeqVTWvncqDg== + dependencies: + hash-sum "^1.0.2" + loader-utils "^1.0.2" + +vue-template-compiler@^2.6: + version "2.7.15" + resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.7.15.tgz#ec88ba8ceafe0f17a528b89c57e01e02da92b0de" + integrity sha512-yQxjxMptBL7UAog00O8sANud99C6wJF+7kgbcwqkvA38vCGF7HWE66w0ZFnS/kX5gSoJr/PQ4/oS3Ne2pW37Og== + dependencies: + de-indent "^1.0.2" + he "^1.2.0" + +vue-template-es2015-compiler@^1.9.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825" + integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw== + +vue@^2.6: + version "2.7.15" + resolved "https://registry.yarnpkg.com/vue/-/vue-2.7.15.tgz#94cd34e6e9f22cd2d35a02143f96a5beac1c1f54" + integrity sha512-a29fsXd2G0KMRqIFTpRgpSbWaNBK3lpCTOLuGLEDnlHWdjB8fwl6zyYZ8xCrqkJdatwZb4mGHiEfJjnw0Q6AwQ== + dependencies: + "@vue/compiler-sfc" "2.7.15" + csstype "^3.1.0" + +vuejs-clipper@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/vuejs-clipper/-/vuejs-clipper-4.0.0.tgz#28b216f6a4d0cc42700cb8e3f94fd0259304f276" + integrity sha512-rzATt89TjeGEYaOI6kbGnjKhWCY3JKGcfuFshFh4H7tk+BQVHVWz5WhxJmH/2wVA6yAmW4EzA1LCbMnjQPhWRg== + dependencies: + core-js "^3.6.4" + exif-js "^2.3.0" + +vuelidate@^0.7: + version "0.7.7" + resolved "https://registry.yarnpkg.com/vuelidate/-/vuelidate-0.7.7.tgz#5df3930a63ddecf56fde7bdacea9dbaf0c9bf899" + integrity sha512-pT/U2lDI67wkIqI4tum7cMSIfGcAMfB+Phtqh2ttdXURwvHRBJEAQ0tVbUsW9Upg83Q5QH59bnCoXI7A9JDGnA== + +watchpack@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" + integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== + dependencies: + glob-to-regexp "^0.4.1" + graceful-fs "^4.1.2" + +wbuf@^1.1.0, wbuf@^1.7.3: + version "1.7.3" + resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df" + integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA== + dependencies: + minimalistic-assert "^1.0.0" + +webpack-cli@^4.9.1: + version "4.10.0" + resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-4.10.0.tgz#37c1d69c8d85214c5a65e589378f53aec64dab31" + integrity sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w== + dependencies: + "@discoveryjs/json-ext" "^0.5.0" + "@webpack-cli/configtest" "^1.2.0" + "@webpack-cli/info" "^1.5.0" + "@webpack-cli/serve" "^1.7.0" + colorette "^2.0.14" + commander "^7.0.0" + cross-spawn "^7.0.3" + fastest-levenshtein "^1.0.12" + import-local "^3.0.2" + interpret "^2.2.0" + rechoir "^0.7.0" + webpack-merge "^5.7.3" + +webpack-dev-middleware@^5.3.1: + version "5.3.3" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz#efae67c2793908e7311f1d9b06f2a08dcc97e51f" + integrity sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA== + dependencies: + colorette "^2.0.10" + memfs "^3.4.3" + mime-types "^2.1.31" + range-parser "^1.2.1" + schema-utils "^4.0.0" + +webpack-dev-server@^4.7.3: + version "4.15.1" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.15.1.tgz#8944b29c12760b3a45bdaa70799b17cb91b03df7" + integrity sha512-5hbAst3h3C3L8w6W4P96L5vaV0PxSmJhxZvWKYIdgxOQm8pNZ5dEOmmSLBVpP85ReeyRt6AS1QJNyo/oFFPeVA== + dependencies: + "@types/bonjour" "^3.5.9" + "@types/connect-history-api-fallback" "^1.3.5" + "@types/express" "^4.17.13" + "@types/serve-index" "^1.9.1" + "@types/serve-static" "^1.13.10" + "@types/sockjs" "^0.3.33" + "@types/ws" "^8.5.5" + ansi-html-community "^0.0.8" + bonjour-service "^1.0.11" + chokidar "^3.5.3" + colorette "^2.0.10" + compression "^1.7.4" + connect-history-api-fallback "^2.0.0" + default-gateway "^6.0.3" + express "^4.17.3" + graceful-fs "^4.2.6" + html-entities "^2.3.2" + http-proxy-middleware "^2.0.3" + ipaddr.js "^2.0.1" + launch-editor "^2.6.0" + open "^8.0.9" + p-retry "^4.5.0" + rimraf "^3.0.2" + schema-utils "^4.0.0" + selfsigned "^2.1.1" + serve-index "^1.9.1" + sockjs "^0.3.24" + spdy "^4.0.2" + webpack-dev-middleware "^5.3.1" + ws "^8.13.0" + +webpack-merge@^5.7.3, webpack-merge@^5.8.0: + version "5.10.0" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.10.0.tgz#a3ad5d773241e9c682803abf628d4cd62b8a4177" + integrity sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA== + dependencies: + clone-deep "^4.0.1" + flat "^5.0.2" + wildcard "^2.0.0" + +webpack-notifier@^1.14.1: + version "1.15.0" + resolved "https://registry.yarnpkg.com/webpack-notifier/-/webpack-notifier-1.15.0.tgz#72644a1a4ec96b3528704d28f79da5e70048e8ee" + integrity sha512-N2V8UMgRB5komdXQRavBsRpw0hPhJq2/SWNOGuhrXpIgRhcMexzkGQysUyGStHLV5hkUlgpRiF7IUXoBqyMmzQ== + dependencies: + node-notifier "^9.0.0" + strip-ansi "^6.0.0" + +webpack-sources@^1.1.0: + version "1.4.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933" + integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ== + dependencies: + source-list-map "^2.0.0" + source-map "~0.6.1" + +webpack-sources@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" + integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== + +webpack@^5.60.0: + version "5.89.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.89.0.tgz#56b8bf9a34356e93a6625770006490bf3a7f32dc" + integrity sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw== + dependencies: + "@types/eslint-scope" "^3.7.3" + "@types/estree" "^1.0.0" + "@webassemblyjs/ast" "^1.11.5" + "@webassemblyjs/wasm-edit" "^1.11.5" + "@webassemblyjs/wasm-parser" "^1.11.5" + acorn "^8.7.1" + acorn-import-assertions "^1.9.0" + browserslist "^4.14.5" + chrome-trace-event "^1.0.2" + enhanced-resolve "^5.15.0" + es-module-lexer "^1.2.1" + eslint-scope "5.1.1" + events "^3.2.0" + glob-to-regexp "^0.4.1" + graceful-fs "^4.2.9" + json-parse-even-better-errors "^2.3.1" + loader-runner "^4.2.0" + mime-types "^2.1.27" + neo-async "^2.6.2" + schema-utils "^3.2.0" + tapable "^2.1.1" + terser-webpack-plugin "^5.3.7" + watchpack "^2.4.0" + webpack-sources "^3.2.3" + +webpackbar@^5.0.0-3: + version "5.0.2" + resolved "https://registry.yarnpkg.com/webpackbar/-/webpackbar-5.0.2.tgz#d3dd466211c73852741dfc842b7556dcbc2b0570" + integrity sha512-BmFJo7veBDgQzfWXl/wwYXr/VFus0614qZ8i9znqcl9fnEdiVkdbi0TedLQ6xAK92HZHDJ0QmyQ0fmuZPAgCYQ== + dependencies: + chalk "^4.1.0" + consola "^2.15.3" + pretty-time "^1.1.0" + std-env "^3.0.1" + +websocket-driver@>=0.5.1, websocket-driver@^0.7.4: + version "0.7.4" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" + integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== + dependencies: + http-parser-js ">=0.5.1" + safe-buffer ">=5.1.0" + websocket-extensions ">=0.1.1" + +websocket-extensions@>=0.1.1: + version "0.1.4" + resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" + integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== + +which-boxed-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" + integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== + dependencies: + is-bigint "^1.0.1" + is-boolean-object "^1.1.0" + is-number-object "^1.0.4" + is-string "^1.0.5" + is-symbol "^1.0.3" + +which-typed-array@^1.1.11, which-typed-array@^1.1.13: + version "1.1.13" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.13.tgz#870cd5be06ddb616f504e7b039c4c24898184d36" + integrity sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.4" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.0" + +which@2.0.2, which@^2.0.1, which@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wildcard@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67" + integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ== + +workerpool@6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.0.tgz#827d93c9ba23ee2019c3ffaff5c27fccea289e8b" + integrity sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A== + +wrap-ansi@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" + integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +ws@^8.13.0: + version "8.14.2" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.14.2.tgz#6c249a806eb2db7a20d26d51e7709eab7b2e6c7f" + integrity sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g== + +xml@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/xml/-/xml-1.0.1.tgz#78ba72020029c5bc87b8a81a3cfcd74b4a2fc1e5" + integrity sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw== + +xtend@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yallist@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" + integrity sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A== + +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yaml@^1.10.0, yaml@^1.10.2: + version "1.10.2" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" + integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== + +yargs-parser@20.2.4: + version "20.2.4" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" + integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== + +yargs-parser@^20.2.2: + version "20.2.9" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== + +yargs-parser@^21.1.1: + version "21.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" + integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== + +yargs-unparser@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb" + integrity sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA== + dependencies: + camelcase "^6.0.0" + decamelize "^4.0.0" + flat "^5.0.2" + is-plain-obj "^2.1.0" + +yargs@16.2.0: + version "16.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + +yargs@^17.2.1: + version "17.7.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" + integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== + dependencies: + cliui "^8.0.1" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.3" + y18n "^5.0.5" + yargs-parser "^21.1.1" + +yauzl@^2.10.0: + version "2.10.0" + resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" + integrity sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g== + dependencies: + buffer-crc32 "~0.2.3" + fd-slicer "~1.1.0" + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==