Merge branch 'master' into improve_history_localization

This commit is contained in:
Alexis Métaireau 2021-04-11 17:44:29 +02:00
commit b6820095d7
54 changed files with 4786 additions and 3628 deletions

View file

@ -4,6 +4,9 @@ python:
- "3.6" - "3.6"
- "3.7" - "3.7"
- "3.8" - "3.8"
- "3.9"
script: tox script: tox
before_install:
- python -m pip install --upgrade pip virtualenv
install: install:
- pip install tox-travis - pip install tox-travis

View file

@ -15,7 +15,9 @@ ENV NIGHTLY="" \
ACTIVATE_DEMO_PROJECT="True" \ ACTIVATE_DEMO_PROJECT="True" \
ADMIN_PASSWORD="" \ ADMIN_PASSWORD="" \
ALLOW_PUBLIC_PROJECT_CREATION="True" \ ALLOW_PUBLIC_PROJECT_CREATION="True" \
ACTIVATE_ADMIN_DASHBOARD="False" ACTIVATE_ADMIN_DASHBOARD="False" \
BABEL_DEFAULT_TIMEZONE="UTC" \
GREENLET_TEST_CPP="no"
RUN apk update && apk add git gcc libc-dev libffi-dev openssl-dev wget &&\ RUN apk update && apk add git gcc libc-dev libffi-dev openssl-dev wget &&\
mkdir -p /etc/ihatemoney &&\ mkdir -p /etc/ihatemoney &&\

View file

@ -46,7 +46,7 @@ test: install-dev ## Run the tests
.PHONY: black .PHONY: black
black: install-dev ## Run the tests black: install-dev ## Run the tests
$(VENV)/bin/black --target-version=py34 . $(VENV)/bin/black --target-version=py36 .
.PHONY: isort .PHONY: isort
isort: install-dev ## Run the tests isort: install-dev ## Run the tests

View file

@ -9,6 +9,10 @@ I hate money
:target: https://hosted.weblate.org/engage/i-hate-money/?utm_source=widget :target: https://hosted.weblate.org/engage/i-hate-money/?utm_source=widget
:alt: Translation status from Weblate :alt: Translation status from Weblate
.. image:: https://img.shields.io/liberapay/receives/IHateMoney.svg?logo=liberapay
:target: https://liberapay.com/IHateMoney/donate
:alt: Donate
*I hate money* is a web application made to ease shared budget management. *I hate money* is a web application made to ease shared budget management.
It keeps track of who bought what, when, and for whom; and helps to settle the It keeps track of who bought what, when, and for whom; and helps to settle the
bills. bills.
@ -25,7 +29,7 @@ encouraged to do so.
Requirements Requirements
============ ============
* **Python**: 3.6, 3.7, 3.8. * **Python**: version 3.6 to 3.9.
* **Backends**: MySQL, PostgreSQL, SQLite, Memory. * **Backends**: MySQL, PostgreSQL, SQLite, Memory.
Contributing Contributing
@ -35,6 +39,8 @@ Do you wish to contribute to IHateMoney? Fantastic! There's a lot of very
useful help on the official `contributing useful help on the official `contributing
<https://ihatemoney.readthedocs.io/en/latest/contributing.html>`_ page. <https://ihatemoney.readthedocs.io/en/latest/contributing.html>`_ page.
You can also `donate some money <https://liberapay.com/IHateMoney/donate>`_. All funds will be used to maintain the `hosted version <https://ihatemoney.org>`_.
Translation status Translation status
================== ==================

View file

@ -20,6 +20,7 @@ ACTIVATE_DEMO_PROJECT = $ACTIVATE_DEMO_PROJECT
ADMIN_PASSWORD = '$ADMIN_PASSWORD' ADMIN_PASSWORD = '$ADMIN_PASSWORD'
ALLOW_PUBLIC_PROJECT_CREATION = $ALLOW_PUBLIC_PROJECT_CREATION ALLOW_PUBLIC_PROJECT_CREATION = $ALLOW_PUBLIC_PROJECT_CREATION
ACTIVATE_ADMIN_DASHBOARD = $ACTIVATE_ADMIN_DASHBOARD ACTIVATE_ADMIN_DASHBOARD = $ACTIVATE_ADMIN_DASHBOARD
BABEL_DEFAULT_TIMEZONE = "$BABEL_DEFAULT_TIMEZONE"
EOF EOF
if [ "$NIGHTLY" == "True" -o "$NIGHTLY" == "true" ]; then if [ "$NIGHTLY" == "True" -o "$NIGHTLY" == "true" ]; then

View file

@ -31,6 +31,7 @@ connection string. This will look like::
SQLALCHEMY_DATABASE_URI = 'postgresql://myuser:mypass@localhost/dbname?client_encoding=utf8' SQLALCHEMY_DATABASE_URI = 'postgresql://myuser:mypass@localhost/dbname?client_encoding=utf8'
.. _the SQLAlchemy documentation: http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls
`SECRET_KEY` `SECRET_KEY`
------------ ------------
@ -96,7 +97,26 @@ if set to ``"somestring"``, it will be served from a "folder"
- **Default value:** ``""`` (empty string) - **Default value:** ``""`` (empty string)
.. _the SQLAlchemy documentation: http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls `BABEL_DEFAULT_TIMEZONE`
------------------------
The timezone that will be used to convert date and time when displaying them
to the user (all times are always stored in UTC internally).
If not set, it will default to the timezone configured on the Operating System
of the server running ihatemoney, which may or may not be what you want.
- **Default value:** *unset* (use the timezone of the server Operating System)
- **Production value:** Set to the timezone of your expected users, with a
format such as ``"Europe/Paris"``. See `this list of TZ database names`_
for a complete list.
Note: this setting is actually interpreted by Flask-Babel, see the
`Flask-Babel guide for formatting dates`_ for details.
.. _this list of TZ database name: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List
.. _Flask-Babel guide for formatting dates: https://pythonhosted.org/Flask-Babel/#formatting-dates
Configuring emails sending Configuring emails sending
-------------------------- --------------------------

View file

@ -19,7 +19,7 @@ Requirements
«Ihatemoney» depends on: «Ihatemoney» depends on:
* **Python**: either 3.6, 3.7 or 3.8 will work. * **Python**: version 3.6 to 3.9 included will work.
* **A Backend**: to choose among MySQL, PostgreSQL, SQLite or Memory. * **A Backend**: to choose among MySQL, PostgreSQL, SQLite or Memory.
* **Virtual environment** (recommended): `python3-venv` package under Debian/Ubuntu. * **Virtual environment** (recommended): `python3-venv` package under Debian/Ubuntu.

View file

@ -1,2 +1,2 @@
Sphinx==3.3.0 Sphinx==3.5.3
docutils==0.16 docutils==0.17

View file

@ -34,3 +34,7 @@ ALLOW_PUBLIC_PROJECT_CREATION = True
# If set to True, an administration dashboard is available. # If set to True, an administration dashboard is available.
ACTIVATE_ADMIN_DASHBOARD = False ACTIVATE_ADMIN_DASHBOARD = False
# You can change the timezone used to display time. By default it will be
#derived from the server OS.
#BABEL_DEFAULT_TIMEZONE = "Europe/Paris"

View file

@ -14,7 +14,7 @@ class Singleton(type):
class CurrencyConverter(object, metaclass=Singleton): class CurrencyConverter(object, metaclass=Singleton):
# Get exchange rates # Get exchange rates
no_currency = "XXX" no_currency = "XXX"
api_url = "https://api.exchangeratesapi.io/latest?base=USD" api_url = "https://api.exchangerate.host/latest?base=USD"
def __init__(self): def __init__(self):
pass pass

View file

@ -18,10 +18,12 @@ SUPPORTED_LANGUAGES = [
"nb_NO", "nb_NO",
"nl", "nl",
"pl", "pl",
"pt",
"pt_BR", "pt_BR",
"ru", "ru",
"ta", "ta",
"tr", "tr",
"uk", "uk",
"zh_Hans", "zh_Hans",
"ja",
] ]

View file

@ -80,7 +80,7 @@ def get_history(project, human_readable_names=True):
object_str = describe_version(version) object_str = describe_version(version)
common_properties = { common_properties = {
"time": version.transaction.issued_at.strftime("%Y-%m-%dT%H:%M:%SZ"), "time": version.transaction.issued_at,
"operation_type": version.operation_type, "operation_type": version.operation_type,
"object_type": object_type, "object_type": object_type,
"object_desc": object_str, "object_desc": object_str,

View file

@ -77,7 +77,7 @@ def run_migrations_online():
target_metadata=target_metadata, target_metadata=target_metadata,
include_object=include_object, include_object=include_object,
process_revision_directives=process_revision_directives, process_revision_directives=process_revision_directives,
**current_app.extensions["migrate"].configure_args **current_app.extensions["migrate"].configure_args,
) )
try: try:

View file

@ -96,7 +96,7 @@ class PatchedRelationShipBuilder(RelationshipBuilder):
association_col association_col
== self.association_version_table.c[association_col.name] == self.association_version_table.c[association_col.name]
for association_col in association_cols for association_col in association_cols
] ],
) )
) )
.group_by(*association_cols) .group_by(*association_cols)

View file

@ -2,6 +2,7 @@ import os
import os.path import os.path
import warnings import warnings
from babel.dates import LOCALTZ
from flask import Flask, g, render_template, request, session from flask import Flask, g, render_template, request, session
from flask_babel import Babel, format_currency from flask_babel import Babel, format_currency
from flask_mail import Mail from flask_mail import Mail
@ -152,8 +153,10 @@ def create_app(
app.jinja_env.filters["minimal_round"] = minimal_round app.jinja_env.filters["minimal_round"] = minimal_round
app.jinja_env.filters["localize_list"] = localize_list app.jinja_env.filters["localize_list"] = localize_list
# Translations # Translations and time zone (used to display dates). The timezone is
babel = Babel(app) # taken from the BABEL_DEFAULT_TIMEZONE settings, and falls back to
# the local timezone of the server OS by using LOCALTZ.
babel = Babel(app, default_timezone=str(LOCALTZ))
# Undocumented currencyformat filter from flask_babel is forwarding to Babel format_currency # Undocumented currencyformat filter from flask_babel is forwarding to Babel format_currency
# We overwrite it to remove the currency sign ¤ when there is no currency # We overwrite it to remove the currency sign ¤ when there is no currency
@ -169,7 +172,7 @@ def create_app(
number, number,
currency if currency != CurrencyConverter.no_currency else "", currency if currency != CurrencyConverter.no_currency else "",
*args, *args,
**kwargs **kwargs,
).strip() ).strip()
app.jinja_env.filters["currency"] = currency app.jinja_env.filters["currency"] = currency

View file

@ -0,0 +1,13 @@
.get-it-from {
width: 100px;
min-height: 110px;
border: 5px solid floralwhite;
background-color: white; margin: 10px;
}
.get-it-from:hover {
opacity: 80%;
}
main {
background: linear-gradient(150deg, #abe128 0%, #43ca61 100%);
font-family: 'Comfortaa', arial, serif;
}

View file

@ -0,0 +1,129 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="US_UK_Download_on_the" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
x="0px" y="0px" width="135px" height="40px" viewBox="0 0 135 40" enable-background="new 0 0 135 40" xml:space="preserve">
<g>
<path fill="#A6A6A6" d="M130.197,40H4.729C2.122,40,0,37.872,0,35.267V4.726C0,2.12,2.122,0,4.729,0h125.468
C132.803,0,135,2.12,135,4.726v30.541C135,37.872,132.803,40,130.197,40L130.197,40z"/>
<path d="M134.032,35.268c0,2.116-1.714,3.83-3.834,3.83H4.729c-2.119,0-3.839-1.714-3.839-3.83V4.725
c0-2.115,1.72-3.835,3.839-3.835h125.468c2.121,0,3.834,1.72,3.834,3.835L134.032,35.268L134.032,35.268z"/>
<g>
<g>
<path fill="#FFFFFF" d="M30.128,19.784c-0.029-3.223,2.639-4.791,2.761-4.864c-1.511-2.203-3.853-2.504-4.676-2.528
c-1.967-0.207-3.875,1.177-4.877,1.177c-1.022,0-2.565-1.157-4.228-1.123c-2.14,0.033-4.142,1.272-5.24,3.196
c-2.266,3.923-0.576,9.688,1.595,12.859c1.086,1.553,2.355,3.287,4.016,3.226c1.625-0.067,2.232-1.036,4.193-1.036
c1.943,0,2.513,1.036,4.207,0.997c1.744-0.028,2.842-1.56,3.89-3.127c1.255-1.78,1.759-3.533,1.779-3.623
C33.507,24.924,30.161,23.647,30.128,19.784z"/>
<path fill="#FFFFFF" d="M26.928,10.306c0.874-1.093,1.472-2.58,1.306-4.089c-1.265,0.056-2.847,0.875-3.758,1.944
c-0.806,0.942-1.526,2.486-1.34,3.938C24.557,12.205,26.016,11.382,26.928,10.306z"/>
</g>
</g>
<g>
<path fill="#FFFFFF" d="M53.645,31.504h-2.271l-1.244-3.909h-4.324l-1.185,3.909h-2.211l4.284-13.308h2.646L53.645,31.504z
M49.755,25.955L48.63,22.48c-0.119-0.355-0.342-1.191-0.671-2.507h-0.04c-0.131,0.566-0.342,1.402-0.632,2.507l-1.105,3.475
H49.755z"/>
<path fill="#FFFFFF" d="M64.662,26.588c0,1.632-0.441,2.922-1.323,3.869c-0.79,0.843-1.771,1.264-2.942,1.264
c-1.264,0-2.172-0.454-2.725-1.362h-0.04v5.055h-2.132V25.067c0-1.026-0.027-2.079-0.079-3.159h1.875l0.119,1.521h0.04
c0.711-1.146,1.79-1.718,3.238-1.718c1.132,0,2.077,0.447,2.833,1.342C64.284,23.949,64.662,25.127,64.662,26.588z M62.49,26.666
c0-0.934-0.21-1.704-0.632-2.31c-0.461-0.632-1.08-0.948-1.856-0.948c-0.526,0-1.004,0.176-1.431,0.523
c-0.428,0.35-0.708,0.807-0.839,1.373c-0.066,0.264-0.099,0.48-0.099,0.65v1.6c0,0.698,0.214,1.287,0.642,1.768
s0.984,0.721,1.668,0.721c0.803,0,1.428-0.31,1.875-0.928C62.266,28.496,62.49,27.68,62.49,26.666z"/>
<path fill="#FFFFFF" d="M75.699,26.588c0,1.632-0.441,2.922-1.324,3.869c-0.789,0.843-1.77,1.264-2.941,1.264
c-1.264,0-2.172-0.454-2.724-1.362H68.67v5.055h-2.132V25.067c0-1.026-0.027-2.079-0.079-3.159h1.875l0.119,1.521h0.04
c0.71-1.146,1.789-1.718,3.238-1.718c1.131,0,2.076,0.447,2.834,1.342C75.32,23.949,75.699,25.127,75.699,26.588z M73.527,26.666
c0-0.934-0.211-1.704-0.633-2.31c-0.461-0.632-1.078-0.948-1.855-0.948c-0.527,0-1.004,0.176-1.432,0.523
c-0.428,0.35-0.707,0.807-0.838,1.373c-0.065,0.264-0.099,0.48-0.099,0.65v1.6c0,0.698,0.214,1.287,0.64,1.768
c0.428,0.48,0.984,0.721,1.67,0.721c0.803,0,1.428-0.31,1.875-0.928C73.303,28.496,73.527,27.68,73.527,26.666z"/>
<path fill="#FFFFFF" d="M88.039,27.772c0,1.132-0.393,2.053-1.182,2.764c-0.867,0.777-2.074,1.165-3.625,1.165
c-1.432,0-2.58-0.276-3.449-0.829l0.494-1.777c0.936,0.566,1.963,0.85,3.082,0.85c0.803,0,1.428-0.182,1.877-0.544
c0.447-0.362,0.67-0.848,0.67-1.454c0-0.54-0.184-0.995-0.553-1.364c-0.367-0.369-0.98-0.712-1.836-1.029
c-2.33-0.869-3.494-2.142-3.494-3.816c0-1.094,0.408-1.991,1.225-2.689c0.814-0.699,1.9-1.048,3.258-1.048
c1.211,0,2.217,0.211,3.02,0.632l-0.533,1.738c-0.75-0.408-1.598-0.612-2.547-0.612c-0.75,0-1.336,0.185-1.756,0.553
c-0.355,0.329-0.533,0.73-0.533,1.205c0,0.526,0.203,0.961,0.611,1.303c0.355,0.316,1,0.658,1.936,1.027
c1.145,0.461,1.986,1,2.527,1.618C87.77,26.081,88.039,26.852,88.039,27.772z"/>
<path fill="#FFFFFF" d="M95.088,23.508h-2.35v4.659c0,1.185,0.414,1.777,1.244,1.777c0.381,0,0.697-0.033,0.947-0.099l0.059,1.619
c-0.42,0.157-0.973,0.236-1.658,0.236c-0.842,0-1.5-0.257-1.975-0.77c-0.473-0.514-0.711-1.376-0.711-2.587v-4.837h-1.4v-1.6h1.4
v-1.757l2.094-0.632v2.389h2.35V23.508z"/>
<path fill="#FFFFFF" d="M105.691,26.627c0,1.475-0.422,2.686-1.264,3.633c-0.883,0.975-2.055,1.461-3.516,1.461
c-1.408,0-2.529-0.467-3.365-1.401s-1.254-2.113-1.254-3.534c0-1.487,0.43-2.705,1.293-3.652c0.861-0.948,2.023-1.422,3.484-1.422
c1.408,0,2.541,0.467,3.396,1.402C105.283,24.021,105.691,25.192,105.691,26.627z M103.479,26.696
c0-0.885-0.189-1.644-0.572-2.277c-0.447-0.766-1.086-1.148-1.914-1.148c-0.857,0-1.508,0.383-1.955,1.148
c-0.383,0.634-0.572,1.405-0.572,2.317c0,0.885,0.189,1.644,0.572,2.276c0.461,0.766,1.105,1.148,1.936,1.148
c0.814,0,1.453-0.39,1.914-1.168C103.281,28.347,103.479,27.58,103.479,26.696z"/>
<path fill="#FFFFFF" d="M112.621,23.783c-0.211-0.039-0.436-0.059-0.672-0.059c-0.75,0-1.33,0.283-1.738,0.85
c-0.355,0.5-0.533,1.132-0.533,1.895v5.035h-2.131l0.02-6.574c0-1.106-0.027-2.113-0.08-3.021h1.857l0.078,1.836h0.059
c0.225-0.631,0.58-1.139,1.066-1.52c0.475-0.343,0.988-0.514,1.541-0.514c0.197,0,0.375,0.014,0.533,0.039V23.783z"/>
<path fill="#FFFFFF" d="M122.156,26.252c0,0.382-0.025,0.704-0.078,0.967h-6.396c0.025,0.948,0.334,1.673,0.928,2.173
c0.539,0.447,1.236,0.671,2.092,0.671c0.947,0,1.811-0.151,2.588-0.454l0.334,1.48c-0.908,0.396-1.98,0.593-3.217,0.593
c-1.488,0-2.656-0.438-3.506-1.313c-0.848-0.875-1.273-2.05-1.273-3.524c0-1.447,0.395-2.652,1.186-3.613
c0.828-1.026,1.947-1.539,3.355-1.539c1.383,0,2.43,0.513,3.141,1.539C121.873,24.047,122.156,25.055,122.156,26.252z
M120.123,25.699c0.014-0.632-0.125-1.178-0.414-1.639c-0.369-0.593-0.936-0.889-1.699-0.889c-0.697,0-1.264,0.289-1.697,0.869
c-0.355,0.461-0.566,1.014-0.631,1.658H120.123z"/>
</g>
<g>
<g>
<path fill="#FFFFFF" d="M49.05,10.009c0,1.177-0.353,2.063-1.058,2.658c-0.653,0.549-1.581,0.824-2.783,0.824
c-0.596,0-1.106-0.026-1.533-0.078V6.982c0.557-0.09,1.157-0.136,1.805-0.136c1.145,0,2.008,0.249,2.59,0.747
C48.723,8.156,49.05,8.961,49.05,10.009z M47.945,10.038c0-0.763-0.202-1.348-0.606-1.756c-0.404-0.407-0.994-0.611-1.771-0.611
c-0.33,0-0.611,0.022-0.844,0.068v4.889c0.129,0.02,0.365,0.029,0.708,0.029c0.802,0,1.421-0.223,1.857-0.669
S47.945,10.892,47.945,10.038z"/>
<path fill="#FFFFFF" d="M54.909,11.037c0,0.725-0.207,1.319-0.621,1.785c-0.434,0.479-1.009,0.718-1.727,0.718
c-0.692,0-1.243-0.229-1.654-0.689c-0.41-0.459-0.615-1.038-0.615-1.736c0-0.73,0.211-1.329,0.635-1.794s0.994-0.698,1.712-0.698
c0.692,0,1.248,0.229,1.669,0.688C54.708,9.757,54.909,10.333,54.909,11.037z M53.822,11.071c0-0.435-0.094-0.808-0.281-1.119
c-0.22-0.376-0.533-0.564-0.94-0.564c-0.421,0-0.741,0.188-0.961,0.564c-0.188,0.311-0.281,0.69-0.281,1.138
c0,0.435,0.094,0.808,0.281,1.119c0.227,0.376,0.543,0.564,0.951,0.564c0.4,0,0.714-0.191,0.94-0.574
C53.725,11.882,53.822,11.506,53.822,11.071z"/>
<path fill="#FFFFFF" d="M62.765,8.719l-1.475,4.714h-0.96l-0.611-2.047c-0.155-0.511-0.281-1.019-0.379-1.523h-0.019
c-0.091,0.518-0.217,1.025-0.379,1.523l-0.649,2.047h-0.971l-1.387-4.714h1.077l0.533,2.241c0.129,0.53,0.235,1.035,0.32,1.513
h0.019c0.078-0.394,0.207-0.896,0.389-1.503l0.669-2.25h0.854l0.641,2.202c0.155,0.537,0.281,1.054,0.378,1.552h0.029
c0.071-0.485,0.178-1.002,0.32-1.552l0.572-2.202H62.765z"/>
<path fill="#FFFFFF" d="M68.198,13.433H67.15v-2.7c0-0.832-0.316-1.248-0.95-1.248c-0.311,0-0.562,0.114-0.757,0.343
c-0.193,0.229-0.291,0.499-0.291,0.808v2.796h-1.048v-3.366c0-0.414-0.013-0.863-0.038-1.349h0.921l0.049,0.737h0.029
c0.122-0.229,0.304-0.418,0.543-0.569c0.284-0.176,0.602-0.265,0.95-0.265c0.44,0,0.806,0.142,1.097,0.427
c0.362,0.349,0.543,0.87,0.543,1.562V13.433z"/>
<path fill="#FFFFFF" d="M71.088,13.433h-1.047V6.556h1.047V13.433z"/>
<path fill="#FFFFFF" d="M77.258,11.037c0,0.725-0.207,1.319-0.621,1.785c-0.434,0.479-1.01,0.718-1.727,0.718
c-0.693,0-1.244-0.229-1.654-0.689c-0.41-0.459-0.615-1.038-0.615-1.736c0-0.73,0.211-1.329,0.635-1.794s0.994-0.698,1.711-0.698
c0.693,0,1.248,0.229,1.67,0.688C77.057,9.757,77.258,10.333,77.258,11.037z M76.17,11.071c0-0.435-0.094-0.808-0.281-1.119
c-0.219-0.376-0.533-0.564-0.939-0.564c-0.422,0-0.742,0.188-0.961,0.564c-0.188,0.311-0.281,0.69-0.281,1.138
c0,0.435,0.094,0.808,0.281,1.119c0.227,0.376,0.543,0.564,0.951,0.564c0.4,0,0.713-0.191,0.939-0.574
C76.074,11.882,76.17,11.506,76.17,11.071z"/>
<path fill="#FFFFFF" d="M82.33,13.433h-0.941l-0.078-0.543h-0.029c-0.322,0.433-0.781,0.65-1.377,0.65
c-0.445,0-0.805-0.143-1.076-0.427c-0.246-0.258-0.369-0.579-0.369-0.96c0-0.576,0.24-1.015,0.723-1.319
c0.482-0.304,1.16-0.453,2.033-0.446V10.3c0-0.621-0.326-0.931-0.979-0.931c-0.465,0-0.875,0.117-1.229,0.349l-0.213-0.688
c0.438-0.271,0.979-0.407,1.617-0.407c1.232,0,1.85,0.65,1.85,1.95v1.736C82.262,12.78,82.285,13.155,82.33,13.433z
M81.242,11.813v-0.727c-1.156-0.02-1.734,0.297-1.734,0.95c0,0.246,0.066,0.43,0.201,0.553c0.135,0.123,0.307,0.184,0.512,0.184
c0.23,0,0.445-0.073,0.641-0.218c0.197-0.146,0.318-0.331,0.363-0.558C81.236,11.946,81.242,11.884,81.242,11.813z"/>
<path fill="#FFFFFF" d="M88.285,13.433h-0.93l-0.049-0.757h-0.029c-0.297,0.576-0.803,0.864-1.514,0.864
c-0.568,0-1.041-0.223-1.416-0.669s-0.562-1.025-0.562-1.736c0-0.763,0.203-1.381,0.611-1.853c0.395-0.44,0.879-0.66,1.455-0.66
c0.633,0,1.076,0.213,1.328,0.64h0.02V6.556h1.049v5.607C88.248,12.622,88.26,13.045,88.285,13.433z M87.199,11.445v-0.786
c0-0.136-0.01-0.246-0.029-0.33c-0.059-0.252-0.186-0.464-0.379-0.635c-0.195-0.171-0.43-0.257-0.701-0.257
c-0.391,0-0.697,0.155-0.922,0.466c-0.223,0.311-0.336,0.708-0.336,1.193c0,0.466,0.107,0.844,0.322,1.135
c0.227,0.31,0.533,0.465,0.916,0.465c0.344,0,0.619-0.129,0.828-0.388C87.1,12.069,87.199,11.781,87.199,11.445z"/>
<path fill="#FFFFFF" d="M97.248,11.037c0,0.725-0.207,1.319-0.621,1.785c-0.434,0.479-1.008,0.718-1.727,0.718
c-0.691,0-1.242-0.229-1.654-0.689c-0.41-0.459-0.615-1.038-0.615-1.736c0-0.73,0.211-1.329,0.635-1.794s0.994-0.698,1.713-0.698
c0.691,0,1.248,0.229,1.668,0.688C97.047,9.757,97.248,10.333,97.248,11.037z M96.162,11.071c0-0.435-0.094-0.808-0.281-1.119
c-0.221-0.376-0.533-0.564-0.941-0.564c-0.42,0-0.74,0.188-0.961,0.564c-0.188,0.311-0.281,0.69-0.281,1.138
c0,0.435,0.094,0.808,0.281,1.119c0.227,0.376,0.543,0.564,0.951,0.564c0.4,0,0.715-0.191,0.941-0.574
C96.064,11.882,96.162,11.506,96.162,11.071z"/>
<path fill="#FFFFFF" d="M102.883,13.433h-1.047v-2.7c0-0.832-0.316-1.248-0.951-1.248c-0.311,0-0.562,0.114-0.756,0.343
s-0.291,0.499-0.291,0.808v2.796h-1.049v-3.366c0-0.414-0.012-0.863-0.037-1.349h0.92l0.049,0.737h0.029
c0.123-0.229,0.305-0.418,0.543-0.569c0.285-0.176,0.602-0.265,0.951-0.265c0.439,0,0.805,0.142,1.096,0.427
c0.363,0.349,0.543,0.87,0.543,1.562V13.433z"/>
<path fill="#FFFFFF" d="M109.936,9.504h-1.154v2.29c0,0.582,0.205,0.873,0.611,0.873c0.188,0,0.344-0.016,0.467-0.049
l0.027,0.795c-0.207,0.078-0.479,0.117-0.814,0.117c-0.414,0-0.736-0.126-0.969-0.378c-0.234-0.252-0.35-0.676-0.35-1.271V9.504
h-0.689V8.719h0.689V7.855l1.027-0.31v1.173h1.154V9.504z"/>
<path fill="#FFFFFF" d="M115.484,13.433h-1.049v-2.68c0-0.845-0.316-1.268-0.949-1.268c-0.486,0-0.818,0.245-1,0.735
c-0.031,0.103-0.049,0.229-0.049,0.377v2.835h-1.047V6.556h1.047v2.841h0.02c0.33-0.517,0.803-0.775,1.416-0.775
c0.434,0,0.793,0.142,1.078,0.427c0.355,0.355,0.533,0.883,0.533,1.581V13.433z"/>
<path fill="#FFFFFF" d="M121.207,10.853c0,0.188-0.014,0.346-0.039,0.475h-3.143c0.014,0.466,0.164,0.821,0.455,1.067
c0.266,0.22,0.609,0.33,1.029,0.33c0.465,0,0.889-0.074,1.271-0.223l0.164,0.728c-0.447,0.194-0.973,0.291-1.582,0.291
c-0.73,0-1.305-0.215-1.721-0.645c-0.418-0.43-0.625-1.007-0.625-1.731c0-0.711,0.193-1.303,0.582-1.775
c0.406-0.504,0.955-0.756,1.648-0.756c0.678,0,1.193,0.252,1.541,0.756C121.068,9.77,121.207,10.265,121.207,10.853z
M120.207,10.582c0.008-0.311-0.061-0.579-0.203-0.805c-0.182-0.291-0.459-0.437-0.834-0.437c-0.342,0-0.621,0.142-0.834,0.427
c-0.174,0.227-0.277,0.498-0.311,0.815H120.207z"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -0,0 +1,95 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="layer" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 652 652" style="enable-background:new 0 0 652 652;" xml:space="preserve">
<style type="text/css">
.st0{fill:#4D4D4D;}
.st1{fill:#8AB000;stroke:#769616;stroke-width:9.9598;stroke-linecap:round;}
.st2{fill:#FFFFFF;fill-opacity:0.298;}
.st3{fill:#263238;fill-opacity:0.2;}
.st4{fill:#8AB000;}
.st5{fill:#AEEA00;}
.st6{fill:#1976D2;}
.st7{fill:#FFFFFF;fill-opacity:0.2;}
.st8{fill:#0D47A1;}
.st9{fill:none;stroke:#0D47A1;stroke-width:7.5695;stroke-linecap:round;}
.st10{fill:#FFFFFF;}
.st11{fill:url(#SVGID_1_);}
</style>
<g>
<path class="st0" d="M238.2,358.3l8.1-1.6v-54.9l-8.1-1.6v-9.3h60.9v21h-11.8l-0.7-9.1h-25v21.1h26.8v11.8h-26.8v21l8.2,1.6v9.2
h-31.7L238.2,358.3z M338.2,340.7h-29.6v-11.8h29.6L338.2,340.7z M381.9,290.9c9.4,0,17.1,3,23.1,9c6.1,6,9.1,13.6,9.1,23v12.7
c0,9.4-3,17.1-9.1,23c-6.1,5.9-13.8,8.9-23.1,8.9h-34.4v-9.2l8.1-1.6v-54.9l-8.1-1.6v-9.3h8.1H381.9z M371,302.8v52.9h10.1
c5.5,0,9.8-1.9,12.9-5.6c3.1-3.7,4.7-8.6,4.7-14.6v-12.8c0-5.9-1.6-10.7-4.7-14.5c-3.1-3.7-7.4-5.6-12.9-5.6H371z M423.3,358.3
l7.4-1.6v-35.3l-8.1-1.6v-9.3h22.4l0.7,8.3c1.3-2.9,3-5.2,5.2-6.9c2.1-1.6,4.5-2.5,7.3-2.5c0.8,0,1.6,0.1,2.5,0.2
c0.9,0.1,1.7,0.2,2.3,0.4l-1.6,14l-6.3-0.2c-2.2,0-4.1,0.4-5.6,1.3c-1.5,0.9-2.6,2.1-3.4,3.7v27.6l7.4,1.6v9.2h-30.1L423.3,358.3z
M466.5,338.5c0-8.5,2.4-15.4,7.1-20.8c4.8-5.4,11.3-8.1,19.7-8.1c8.4,0,15,2.7,19.7,8.1c4.8,5.4,7.2,12.4,7.2,20.9v1.1
c0,8.6-2.4,15.5-7.2,20.9c-4.7,5.4-11.3,8-19.6,8c-8.4,0-15.1-2.7-19.8-8c-4.7-5.4-7.1-12.4-7.1-20.9V338.5z M481.8,339.6
c0,5.2,0.9,9.4,2.7,12.5c1.8,3.1,4.8,4.7,8.8,4.7c4,0,6.9-1.6,8.7-4.7c1.9-3.2,2.8-7.3,2.8-12.5v-1.1c0-5-0.9-9.2-2.8-12.4
c-1.9-3.2-4.8-4.8-8.8-4.8c-4,0-6.9,1.6-8.7,4.8c-1.8,3.2-2.7,7.3-2.7,12.4V339.6z M526.3,358.3l7.4-1.6v-35.3l-8.2-1.6v-9.3h23.5
v46.1l7.4,1.6v9.2h-30.1V358.3z M549.1,297h-15.3v-11.5h15.3V297z M598.5,360.5c-1.8,2.6-3.9,4.6-6.4,6c-2.4,1.4-5.2,2-8.3,2
c-7,0-12.5-2.6-16.3-7.7c-3.9-5.1-5.8-11.9-5.8-20.3v-1.1c0-9,1.9-16.2,5.8-21.7c3.9-5.5,9.4-8.2,16.4-8.2c2.8,0,5.4,0.6,7.7,1.9
c2.3,1.2,4.3,3,6,5.3v-20.4l-8.2-1.6v-9.3h23.5v71.2l7.4,1.6v9.2h-20.7L598.5,360.5z M577,340.6c0,5,0.8,8.9,2.5,11.8
c1.7,2.8,4.4,4.3,8.2,4.3c2.2,0,4.2-0.5,5.8-1.4c1.6-0.9,3-2.2,4-3.9v-24.1c-1-1.8-2.4-3.2-4-4.2c-1.6-1-3.5-1.5-5.7-1.5
c-3.7,0-6.5,1.7-8.2,5c-1.7,3.3-2.6,7.7-2.6,13V340.6z"/>
</g>
<g>
<path class="st1" d="M195.9,236.9L179,258.8"/>
<path class="st2" d="M195.8,231.9c1.8,0,3,0.8,3.9,1.8c-8.1,9.6-9.4,11.1-20.5,24.8c-4.1,5.3-8.3,2.5-4.2-2.7l16.9-21.9
C192.9,232.6,194.3,232,195.8,231.9L195.8,231.9z"/>
<path class="st3" d="M199.8,233.8c0.5,0.6,2.2,3.2,0.1,6.2l-16.9,21.9c-4.1,5.3-4.6-2.4-4.6-2.4S193.2,241.5,199.8,233.8z"/>
<path class="st4" d="M196.9,232.9c1.7,0,3.8,0.6,3.3,3.2c-0.4,2-18.6,24.2-18.6,24.2c-4.1,5.3-9.9,2.7-5.9-2.6l16.9-21.8
C193.7,234.7,195.1,232.9,196.9,232.9z"/>
</g>
<g>
<path class="st1" d="M25.3,236.9l16.9,21.9"/>
<path class="st2" d="M25.4,231.9c-1.8,0-3,0.7-3.9,1.8c8.1,9.6,9.4,11.1,20.5,24.8c4.1,5.3,8.3,2.5,4.2-2.7l-16.9-21.9
C28.3,232.7,26.9,231.9,25.4,231.9z"/>
<path class="st3" d="M21.5,233.8c-1.5,1.8-1.5,4.4-0.1,6.2l16.9,21.9c4.1,5.3,4.6-2.4,4.6-2.4S28,241.5,21.5,233.8z"/>
<path class="st4" d="M24.3,232.9c-1.7,0-3.8,0.6-3.3,3.2c0.4,2,18.6,24.2,18.6,24.2c4.1,5.3,9.9,2.7,5.9-2.6l-16.9-21.8
C27.6,234.7,26.2,232.9,24.3,232.9z"/>
</g>
<g transform="translate(40.672 -1004.938)">
<path class="st5" d="M6.2,1255.8h127.5c6.6,0,12,5.4,12,12v27.9c0,6.6-5.4,12-12,12H6.2c-6.6,0-12-5.4-12-12v-27.9
C-5.7,1261.1-0.4,1255.8,6.2,1255.8z"/>
<path class="st3" d="M6.2,1267.7h127.5c6.6,0,12,5.4,12,12v15.9c0,6.6-5.4,12-12,12H6.2c-6.6,0-12-5.4-12-12v-15.9
C-5.7,1273.1-0.4,1267.7,6.2,1267.7z"/>
<path class="st2" d="M6.2,1255.8h127.5c6.6,0,12,5.4,12,12v15.9c0,6.6-5.4,12-12,12H6.2c-6.6,0-12-5.4-12-12v-15.9
C-5.7,1261.1-0.4,1255.8,6.2,1255.8z"/>
<path class="st5" d="M6.2,1259.8h127.5c6.6,0,12,4.5,12,10.1v23.6c0,5.6-5.4,10.1-12,10.1H6.2c-6.6,0-12-4.5-12-10.1v-23.6
C-5.7,1264.3-0.4,1259.8,6.2,1259.8z"/>
</g>
<g transform="translate(-1.328 -1004.938)">
<path class="st6" d="M48.2,1311.6h127.5c6.6,0,12,5.4,12,12v79.7c0,6.6-5.4,12-12,12H48.2c-6.6,0-12-5.4-12-12v-79.7
C36.3,1316.9,41.6,1311.6,48.2,1311.6z"/>
<path class="st3" d="M48.2,1363.4h127.5c6.6,0,12,5.4,12,12v27.9c0,6.6-5.4,12-12,12H48.2c-6.6,0-12-5.4-12-12v-27.9
C36.3,1368.7,41.6,1363.4,48.2,1363.4z"/>
<path class="st7" d="M48.2,1311.6h127.5c6.6,0,12,5.4,12,12v27.9c0,6.6-5.4,12-12,12H48.2c-6.6,0-12-5.4-12-12v-27.9
C36.3,1316.9,41.6,1311.6,48.2,1311.6z"/>
<path class="st6" d="M48.2,1315.5h127.5c6.6,0,12,4.9,12,11v73.6c0,6.1-5.4,11-12,11H48.2c-6.6,0-12-4.9-12-11v-73.6
C36.3,1320.5,41.6,1315.5,48.2,1315.5z"/>
</g>
<g transform="translate(-1.328 8.425)">
<path class="st8" d="M112,325.1c-11.5,0-21.2,7.9-24,18.5h12.8c2.3-4.1,6.6-6.6,11.2-6.6c7.1,0,12.9,5.7,12.9,12.8
c0,0.1,0,0.1,0,0.2c0,7.1-5.7,12.9-12.8,12.9c-0.1,0-0.1,0-0.2,0c-5,0-9.5-2.9-11.7-7.4H87.7c2.6,11,12.5,19.3,24.2,19.3
c13.7,0,24.9-11.2,24.9-24.9C136.9,336.3,125.6,325.1,112,325.1z"/>
<circle class="st9" cx="112" cy="350" r="38"/>
</g>
<g transform="translate(-1.328 -1004.438)">
<ellipse class="st3" cx="73.6" cy="1281.7" rx="13.4" ry="15.4"/>
<circle class="st10" cx="73.6" cy="1283.7" r="13.4"/>
</g>
<g transform="translate(18.172 -1004.438)">
<ellipse class="st3" cx="131.8" cy="1281.7" rx="13.4" ry="15.4"/>
<circle class="st10" cx="131.8" cy="1283.7" r="13.4"/>
</g>
<radialGradient id="SVGID_1_" cx="139.5457" cy="505.1199" r="22.671" gradientTransform="matrix(0 7.8673 8.4349 0 -4240.3135 -865.9722)" gradientUnits="userSpaceOnUse">
<stop offset="0" style="stop-color:#FFFFFF;stop-opacity:9.800000e-002"/>
<stop offset="1" style="stop-color:#FFFFFF;stop-opacity:0"/>
</radialGradient>
<path class="st11" d="M25.4,231.9c-2.7-0.1-5,2.1-5.1,4.8c0,1.2,0.4,2.3,1.1,3.2l14.4,18.6c-0.5,1.4-0.8,2.8-0.8,4.3v27.9
c0,6.6,5.3,12,12,12h127.5c6.6,0,12-5.3,12-12v-27.9c0-1.5-0.3-2.9-0.8-4.3l14.4-18.6c1.7-2.2,1.4-5.3-0.8-7c-1-0.8-2.2-1.1-3.4-1.1
c-1.5,0.1-2.8,0.8-3.7,2l-13.7,17.7c-1.3-0.5-2.6-0.7-3.9-0.7H46.9c-1.4,0-2.7,0.3-3.9,0.7l-13.7-17.7
C28.4,232.7,26.9,231.9,25.4,231.9L25.4,231.9z M46.9,306.6c-0.8,0-1.6,0.1-2.4,0.2c-5.5,1.1-9.5,5.9-9.5,11.6v79.8
c0,6.6,5.3,12,12,12h127.5c6.6,0,12-5.3,12-12v-79.8c0-5.6-4-10.5-9.5-11.6c-0.8-0.2-1.6-0.2-2.4-0.2L46.9,306.6L46.9,306.6z"/>
</svg>

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

View file

@ -0,0 +1,26 @@
{% extends "layout.html" %}
{% block css %}
<link rel=stylesheet type=text/css href="{{ url_for("static", filename='css/download_mobile_app.css') }}">
{% endblock %}
{% block body %}
<main class="row py-5 justify-content-center text-center">
<div class=" col-10 text-center bg-dark text-light">
<h1 class="my-2">{{_("Download Mobile Application")}}</h1>
</div>
<h3 class="col-12 my-2 text-dark">{{_("Get it on")}}</h1>
<div class="col-12">
<a target="_blank" rel="noopener" href="https://play.google.com/store/apps/details?id=net.eneiluj.moneybuster&hl=en_US" class="text-decoration-none">
<img src="{{ url_for("static", filename='images/google-play.png') }}" class="get-it-from"/>
</a>
<a target="_blank" rel="noopener" href="https://f-droid.org/en/packages/net.eneiluj.moneybuster/" class="text-decoration-none">
<img src="{{ url_for("static", filename='images/f-droid.svg') }}" class="get-it-from" />
</a>
<a target="__blank" rel="noopener" href="https://apps.apple.com/us/app/payforme/id1500428306" class="text-decoration-none">
<img src="{{ url_for("static", filename='images/app-store.svg') }}" class="get-it-from" />
</a>
</div>
</main>
{% endblock %}

View file

@ -162,7 +162,7 @@
<tbody> <tbody>
{% for event in history %} {% for event in history %}
<tr> <tr>
<td><script>document.write(localizeTime("{{ event.time }}"));</script></td> <td>{{ event.time|datetimeformat("medium") }}</td>
<td > <td >
<div class="history_icon"> <div class="history_icon">
<i {% if event.operation_type == OperationType.INSERT %} <i {% if event.operation_type == OperationType.INSERT %}

View file

@ -6,6 +6,7 @@
<meta http-equiv="content-type" content="text/html; charset=utf-8"> <meta http-equiv="content-type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel=stylesheet type=text/css href="{{ url_for("static", filename='css/main.css') }}"> <link rel=stylesheet type=text/css href="{{ url_for("static", filename='css/main.css') }}">
{% block css %}{% endblock %}
<script src="{{ url_for("static", filename="js/jquery-3.1.1.min.js") }}"></script> <script src="{{ url_for("static", filename="js/jquery-3.1.1.min.js") }}"></script>
<script src="{{ url_for("static", filename="js/ihatemoney.js") }}"></script> <script src="{{ url_for("static", filename="js/ihatemoney.js") }}"></script>
<script src="{{ url_for("static", filename="js/tether.min.js") }}"></script> <script src="{{ url_for("static", filename="js/tether.min.js") }}"></script>
@ -144,14 +145,14 @@
<a target="_blank" rel="noopener" data-toggle="tooltip" data-placement="top" title="{{ _('Code') }}" href="https://github.com/spiral-project/ihatemoney"> <a target="_blank" rel="noopener" data-toggle="tooltip" data-placement="top" title="{{ _('Code') }}" href="https://github.com/spiral-project/ihatemoney">
<i class="icon git">{{ static_include("images/git.svg") | safe }}</i> <i class="icon git">{{ static_include("images/git.svg") | safe }}</i>
</a> </a>
<a target="_blank" rel="noopener" data-toggle="tooltip" data-placement="top" title="{{ _('Mobile Application') }}" href="https://gitlab.com/eneiluj/moneybuster#moneybuster-for-android"> <a data-toggle="tooltip" data-placement="top" title="{{ _('Mobile Application') }}" href="{{url_for('main.mobile')}}">
<i class="icon mobile">{{ static_include("images/mobile-alt.svg") | safe }}</i> <i class="icon mobile">{{ static_include("images/mobile-alt.svg") | safe }}</i>
</a> </a>
<a target="_blank" rel="noopener" data-toggle="tooltip" data-placement="top" title="{{ _('Documentation') }}" href="https://ihatemoney.readthedocs.io/en/latest/"> <a target="_blank" rel="noopener" data-toggle="tooltip" data-placement="top" title="{{ _('Documentation') }}" href="https://ihatemoney.readthedocs.io/en/latest/">
<i class="icon book">{{ static_include("images/book.svg") | safe }}</i> <i class="icon book">{{ static_include("images/book.svg") | safe }}</i>
</a> </a>
{% if g.show_admin_dashboard_link %} {% if g.show_admin_dashboard_link %}
<a target="_blank" rel="noopener" data-toggle="tooltip" data-placement="top" title="{{ _('Administation Dashboard') }}" href="{{ url_for("main.dashboard") }}"> <a target="_blank" rel="noopener" data-toggle="tooltip" data-placement="top" title="{{ _('Administation Dashboard') }}" href="{{ url_for('main.dashboard') }}">
<i class="icon admin">{{ static_include("images/cog.svg") | safe }}</i> <i class="icon admin">{{ static_include("images/cog.svg") | safe }}</i>
</a> </a>
{% endif %} {% endif %}

View file

@ -110,7 +110,7 @@
<tr owers="{{bill.owers|join(',','id')}}" payer="{{bill.payer.id}}"> <tr owers="{{bill.owers|join(',','id')}}" payer="{{bill.payer.id}}">
<td> <td>
<span data-toggle="tooltip" data-placement="top" <span data-toggle="tooltip" data-placement="top"
title="{{ _('Added on %(date)s', date=bill.creation_date if bill.creation_date else bill.date) }}"> title="{{ _('Added on %(date)s', date=bill.creation_date|dateformat("long") if bill.creation_date else bill.date|dateformat("long")) }}">
{{ bill.date }} {{ bill.date }}
</span> </span>
</td> </td>

View file

@ -0,0 +1,748 @@
import base64
import datetime
import json
import unittest
from ihatemoney.tests.common.help_functions import em_surround
from ihatemoney.tests.common.ihatemoney_testcase import IhatemoneyTestCase
class APITestCase(IhatemoneyTestCase):
"""Tests the API"""
def api_create(self, name, id=None, password=None, contact=None):
id = id or name
password = password or name
contact = contact or f"{name}@notmyidea.org"
return self.client.post(
"/api/projects",
data={
"name": name,
"id": id,
"password": password,
"contact_email": contact,
"default_currency": "USD",
},
)
def api_add_member(self, project, name, weight=1):
self.client.post(
f"/api/projects/{project}/members",
data={"name": name, "weight": weight},
headers=self.get_auth(project),
)
def get_auth(self, username, password=None):
password = password or username
base64string = (
base64.encodebytes(f"{username}:{password}".encode("utf-8"))
.decode("utf-8")
.replace("\n", "")
)
return {"Authorization": f"Basic {base64string}"}
def test_cors_requests(self):
# Create a project and test that CORS headers are present if requested.
resp = self.api_create("raclette")
self.assertStatus(201, resp)
# Try to do an OPTIONS requests and see if the headers are correct.
resp = self.client.options(
"/api/projects/raclette", headers=self.get_auth("raclette")
)
self.assertEqual(resp.headers["Access-Control-Allow-Origin"], "*")
def test_basic_auth(self):
# create a project
resp = self.api_create("raclette")
self.assertStatus(201, resp)
# try to do something on it being unauth should return a 401
resp = self.client.get("/api/projects/raclette")
self.assertStatus(401, resp)
# PUT / POST / DELETE / GET on the different resources
# should also return a 401
for verb in ("post",):
for resource in ("/raclette/members", "/raclette/bills"):
url = "/api/projects" + resource
self.assertStatus(401, getattr(self.client, verb)(url), verb + resource)
for verb in ("get", "delete", "put"):
for resource in ("/raclette", "/raclette/members/1", "/raclette/bills/1"):
url = "/api/projects" + resource
self.assertStatus(401, getattr(self.client, verb)(url), verb + resource)
def test_project(self):
# wrong email should return an error
resp = self.client.post(
"/api/projects",
data={
"name": "raclette",
"id": "raclette",
"password": "raclette",
"contact_email": "not-an-email",
"default_currency": "USD",
},
)
self.assertTrue(400, resp.status_code)
self.assertEqual(
'{"contact_email": ["Invalid email address."]}\n', resp.data.decode("utf-8")
)
# create it
resp = self.api_create("raclette")
self.assertTrue(201, resp.status_code)
# create it twice should return a 400
resp = self.api_create("raclette")
self.assertTrue(400, resp.status_code)
self.assertIn("id", json.loads(resp.data.decode("utf-8")))
# get information about it
resp = self.client.get(
"/api/projects/raclette", headers=self.get_auth("raclette")
)
self.assertTrue(200, resp.status_code)
expected = {
"members": [],
"name": "raclette",
"contact_email": "raclette@notmyidea.org",
"default_currency": "USD",
"id": "raclette",
"logging_preference": 1,
}
decoded_resp = json.loads(resp.data.decode("utf-8"))
self.assertDictEqual(decoded_resp, expected)
# edit should work
resp = self.client.put(
"/api/projects/raclette",
data={
"contact_email": "yeah@notmyidea.org",
"default_currency": "USD",
"password": "raclette",
"name": "The raclette party",
"project_history": "y",
},
headers=self.get_auth("raclette"),
)
self.assertEqual(200, resp.status_code)
resp = self.client.get(
"/api/projects/raclette", headers=self.get_auth("raclette")
)
self.assertEqual(200, resp.status_code)
expected = {
"name": "The raclette party",
"contact_email": "yeah@notmyidea.org",
"default_currency": "USD",
"members": [],
"id": "raclette",
"logging_preference": 1,
}
decoded_resp = json.loads(resp.data.decode("utf-8"))
self.assertDictEqual(decoded_resp, expected)
# password change is possible via API
resp = self.client.put(
"/api/projects/raclette",
data={
"contact_email": "yeah@notmyidea.org",
"default_currency": "USD",
"password": "tartiflette",
"name": "The raclette party",
},
headers=self.get_auth("raclette"),
)
self.assertEqual(200, resp.status_code)
resp = self.client.get(
"/api/projects/raclette", headers=self.get_auth("raclette", "tartiflette")
)
self.assertEqual(200, resp.status_code)
# delete should work
resp = self.client.delete(
"/api/projects/raclette", headers=self.get_auth("raclette", "tartiflette")
)
# get should return a 401 on an unknown resource
resp = self.client.get(
"/api/projects/raclette", headers=self.get_auth("raclette")
)
self.assertEqual(401, resp.status_code)
def test_token_creation(self):
"""Test that token of project is generated"""
# Create project
resp = self.api_create("raclette")
self.assertTrue(201, resp.status_code)
# Get token
resp = self.client.get(
"/api/projects/raclette/token", headers=self.get_auth("raclette")
)
self.assertEqual(200, resp.status_code)
decoded_resp = json.loads(resp.data.decode("utf-8"))
# Access with token
resp = self.client.get(
"/api/projects/raclette/token",
headers={"Authorization": f"Basic {decoded_resp['token']}"},
)
self.assertEqual(200, resp.status_code)
def test_token_login(self):
resp = self.api_create("raclette")
# Get token
resp = self.client.get(
"/api/projects/raclette/token", headers=self.get_auth("raclette")
)
decoded_resp = json.loads(resp.data.decode("utf-8"))
resp = self.client.get("/authenticate?token={}".format(decoded_resp["token"]))
# Test that we are redirected.
self.assertEqual(302, resp.status_code)
def test_member(self):
# create a project
self.api_create("raclette")
# get the list of members (should be empty)
req = self.client.get(
"/api/projects/raclette/members", headers=self.get_auth("raclette")
)
self.assertStatus(200, req)
self.assertEqual("[]\n", req.data.decode("utf-8"))
# add a member
req = self.client.post(
"/api/projects/raclette/members",
data={"name": "Zorglub"},
headers=self.get_auth("raclette"),
)
# the id of the new member should be returned
self.assertStatus(201, req)
self.assertEqual("1\n", req.data.decode("utf-8"))
# the list of members should contain one member
req = self.client.get(
"/api/projects/raclette/members", headers=self.get_auth("raclette")
)
self.assertStatus(200, req)
self.assertEqual(len(json.loads(req.data.decode("utf-8"))), 1)
# Try to add another member with the same name.
req = self.client.post(
"/api/projects/raclette/members",
data={"name": "Zorglub"},
headers=self.get_auth("raclette"),
)
self.assertStatus(400, req)
# edit the member
req = self.client.put(
"/api/projects/raclette/members/1",
data={"name": "Fred", "weight": 2},
headers=self.get_auth("raclette"),
)
self.assertStatus(200, req)
# get should return the new name
req = self.client.get(
"/api/projects/raclette/members/1", headers=self.get_auth("raclette")
)
self.assertStatus(200, req)
self.assertEqual("Fred", json.loads(req.data.decode("utf-8"))["name"])
self.assertEqual(2, json.loads(req.data.decode("utf-8"))["weight"])
# edit this member with same information
# (test PUT idemopotence)
req = self.client.put(
"/api/projects/raclette/members/1",
data={"name": "Fred"},
headers=self.get_auth("raclette"),
)
self.assertStatus(200, req)
# de-activate the user
req = self.client.put(
"/api/projects/raclette/members/1",
data={"name": "Fred", "activated": False},
headers=self.get_auth("raclette"),
)
self.assertStatus(200, req)
req = self.client.get(
"/api/projects/raclette/members/1", headers=self.get_auth("raclette")
)
self.assertStatus(200, req)
self.assertEqual(False, json.loads(req.data.decode("utf-8"))["activated"])
# re-activate the user
req = self.client.put(
"/api/projects/raclette/members/1",
data={"name": "Fred", "activated": True},
headers=self.get_auth("raclette"),
)
req = self.client.get(
"/api/projects/raclette/members/1", headers=self.get_auth("raclette")
)
self.assertStatus(200, req)
self.assertEqual(True, json.loads(req.data.decode("utf-8"))["activated"])
# delete a member
req = self.client.delete(
"/api/projects/raclette/members/1", headers=self.get_auth("raclette")
)
self.assertStatus(200, req)
# the list of members should be empty
req = self.client.get(
"/api/projects/raclette/members", headers=self.get_auth("raclette")
)
self.assertStatus(200, req)
self.assertEqual("[]\n", req.data.decode("utf-8"))
def test_bills(self):
# create a project
self.api_create("raclette")
# add members
self.api_add_member("raclette", "zorglub")
self.api_add_member("raclette", "fred")
self.api_add_member("raclette", "quentin")
# get the list of bills (should be empty)
req = self.client.get(
"/api/projects/raclette/bills", headers=self.get_auth("raclette")
)
self.assertStatus(200, req)
self.assertEqual("[]\n", req.data.decode("utf-8"))
# add a bill
req = self.client.post(
"/api/projects/raclette/bills",
data={
"date": "2011-08-10",
"what": "fromage",
"payer": "1",
"payed_for": ["1", "2"],
"amount": "25",
"external_link": "https://raclette.fr",
},
headers=self.get_auth("raclette"),
)
# should return the id
self.assertStatus(201, req)
self.assertEqual(req.data.decode("utf-8"), "1\n")
# get this bill details
req = self.client.get(
"/api/projects/raclette/bills/1", headers=self.get_auth("raclette")
)
# compare with the added info
self.assertStatus(200, req)
expected = {
"what": "fromage",
"payer_id": 1,
"owers": [
{"activated": True, "id": 1, "name": "zorglub", "weight": 1},
{"activated": True, "id": 2, "name": "fred", "weight": 1},
],
"amount": 25.0,
"date": "2011-08-10",
"id": 1,
"converted_amount": 25.0,
"original_currency": "USD",
"external_link": "https://raclette.fr",
}
got = json.loads(req.data.decode("utf-8"))
self.assertEqual(
datetime.date.today(),
datetime.datetime.strptime(got["creation_date"], "%Y-%m-%d").date(),
)
del got["creation_date"]
self.assertDictEqual(expected, got)
# the list of bills should length 1
req = self.client.get(
"/api/projects/raclette/bills", headers=self.get_auth("raclette")
)
self.assertStatus(200, req)
self.assertEqual(1, len(json.loads(req.data.decode("utf-8"))))
# edit with errors should return an error
req = self.client.put(
"/api/projects/raclette/bills/1",
data={
"date": "201111111-08-10", # not a date
"what": "fromage",
"payer": "1",
"payed_for": ["1", "2"],
"amount": "25",
"external_link": "https://raclette.fr",
},
headers=self.get_auth("raclette"),
)
self.assertStatus(400, req)
self.assertEqual(
'{"date": ["This field is required."]}\n', req.data.decode("utf-8")
)
# edit a bill
req = self.client.put(
"/api/projects/raclette/bills/1",
data={
"date": "2011-09-10",
"what": "beer",
"payer": "2",
"payed_for": ["1", "2"],
"amount": "25",
"external_link": "https://raclette.fr",
},
headers=self.get_auth("raclette"),
)
# check its fields
req = self.client.get(
"/api/projects/raclette/bills/1", headers=self.get_auth("raclette")
)
creation_date = datetime.datetime.strptime(
json.loads(req.data.decode("utf-8"))["creation_date"], "%Y-%m-%d"
).date()
expected = {
"what": "beer",
"payer_id": 2,
"owers": [
{"activated": True, "id": 1, "name": "zorglub", "weight": 1},
{"activated": True, "id": 2, "name": "fred", "weight": 1},
],
"amount": 25.0,
"date": "2011-09-10",
"external_link": "https://raclette.fr",
"converted_amount": 25.0,
"original_currency": "USD",
"id": 1,
}
got = json.loads(req.data.decode("utf-8"))
self.assertEqual(
creation_date,
datetime.datetime.strptime(got["creation_date"], "%Y-%m-%d").date(),
)
del got["creation_date"]
self.assertDictEqual(expected, got)
# delete a bill
req = self.client.delete(
"/api/projects/raclette/bills/1", headers=self.get_auth("raclette")
)
self.assertStatus(200, req)
# getting it should return a 404
req = self.client.get(
"/api/projects/raclette/bills/1", headers=self.get_auth("raclette")
)
self.assertStatus(404, req)
def test_bills_with_calculation(self):
# create a project
self.api_create("raclette")
# add members
self.api_add_member("raclette", "zorglub")
self.api_add_member("raclette", "fred")
# valid amounts
input_expected = [
("((100 + 200.25) * 2 - 100) / 2", 250.25),
("3/2", 1.5),
("2 + 1 * 5 - 2 / 1", 5),
]
for i, pair in enumerate(input_expected):
input_amount, expected_amount = pair
id = i + 1
req = self.client.post(
"/api/projects/raclette/bills",
data={
"date": "2011-08-10",
"what": "fromage",
"payer": "1",
"payed_for": ["1", "2"],
"amount": input_amount,
},
headers=self.get_auth("raclette"),
)
# should return the id
self.assertStatus(201, req)
self.assertEqual(req.data.decode("utf-8"), "{}\n".format(id))
# get this bill's details
req = self.client.get(
"/api/projects/raclette/bills/{}".format(id),
headers=self.get_auth("raclette"),
)
# compare with the added info
self.assertStatus(200, req)
expected = {
"what": "fromage",
"payer_id": 1,
"owers": [
{"activated": True, "id": 1, "name": "zorglub", "weight": 1},
{"activated": True, "id": 2, "name": "fred", "weight": 1},
],
"amount": expected_amount,
"date": "2011-08-10",
"id": id,
"external_link": "",
"original_currency": "USD",
"converted_amount": expected_amount,
}
got = json.loads(req.data.decode("utf-8"))
self.assertEqual(
datetime.date.today(),
datetime.datetime.strptime(got["creation_date"], "%Y-%m-%d").date(),
)
del got["creation_date"]
self.assertDictEqual(expected, got)
# should raise errors
erroneous_amounts = [
"lambda ", # letters
"(20 + 2", # invalid expression
"20/0", # invalid calc
"9999**99999999999999999", # exponents
"2" * 201, # greater than 200 chars,
]
for amount in erroneous_amounts:
req = self.client.post(
"/api/projects/raclette/bills",
data={
"date": "2011-08-10",
"what": "fromage",
"payer": "1",
"payed_for": ["1", "2"],
"amount": amount,
},
headers=self.get_auth("raclette"),
)
self.assertStatus(400, req)
def test_statistics(self):
# create a project
self.api_create("raclette")
# add members
self.api_add_member("raclette", "zorglub")
self.api_add_member("raclette", "fred")
# add a bill
req = self.client.post(
"/api/projects/raclette/bills",
data={
"date": "2011-08-10",
"what": "fromage",
"payer": "1",
"payed_for": ["1", "2"],
"amount": "25",
},
headers=self.get_auth("raclette"),
)
# get the list of bills (should be empty)
req = self.client.get(
"/api/projects/raclette/statistics", headers=self.get_auth("raclette")
)
self.assertStatus(200, req)
self.assertEqual(
[
{
"balance": 12.5,
"member": {
"activated": True,
"id": 1,
"name": "zorglub",
"weight": 1.0,
},
"paid": 25.0,
"spent": 12.5,
},
{
"balance": -12.5,
"member": {
"activated": True,
"id": 2,
"name": "fred",
"weight": 1.0,
},
"paid": 0,
"spent": 12.5,
},
],
json.loads(req.data.decode("utf-8")),
)
def test_username_xss(self):
# create a project
# self.api_create("raclette")
self.post_project("raclette")
self.login("raclette")
# add members
self.api_add_member("raclette", "<script>")
result = self.client.get("/raclette/")
self.assertNotIn("<script>", result.data.decode("utf-8"))
def test_weighted_bills(self):
# create a project
self.api_create("raclette")
# add members
self.api_add_member("raclette", "zorglub")
self.api_add_member("raclette", "freddy familly", 4)
self.api_add_member("raclette", "quentin")
# add a bill
req = self.client.post(
"/api/projects/raclette/bills",
data={
"date": "2011-08-10",
"what": "fromage",
"payer": "1",
"payed_for": ["1", "2"],
"amount": "25",
},
headers=self.get_auth("raclette"),
)
# get this bill details
req = self.client.get(
"/api/projects/raclette/bills/1", headers=self.get_auth("raclette")
)
creation_date = datetime.datetime.strptime(
json.loads(req.data.decode("utf-8"))["creation_date"], "%Y-%m-%d"
).date()
# compare with the added info
self.assertStatus(200, req)
expected = {
"what": "fromage",
"payer_id": 1,
"owers": [
{"activated": True, "id": 1, "name": "zorglub", "weight": 1},
{"activated": True, "id": 2, "name": "freddy familly", "weight": 4},
],
"amount": 25.0,
"date": "2011-08-10",
"id": 1,
"external_link": "",
"converted_amount": 25.0,
"original_currency": "USD",
}
got = json.loads(req.data.decode("utf-8"))
self.assertEqual(
creation_date,
datetime.datetime.strptime(got["creation_date"], "%Y-%m-%d").date(),
)
del got["creation_date"]
self.assertDictEqual(expected, got)
# getting it should return a 404
req = self.client.get(
"/api/projects/raclette", headers=self.get_auth("raclette")
)
expected = {
"members": [
{
"activated": True,
"id": 1,
"name": "zorglub",
"weight": 1.0,
"balance": 20.0,
},
{
"activated": True,
"id": 2,
"name": "freddy familly",
"weight": 4.0,
"balance": -20.0,
},
{
"activated": True,
"id": 3,
"name": "quentin",
"weight": 1.0,
"balance": 0,
},
],
"contact_email": "raclette@notmyidea.org",
"id": "raclette",
"name": "raclette",
"logging_preference": 1,
"default_currency": "USD",
}
self.assertStatus(200, req)
decoded_req = json.loads(req.data.decode("utf-8"))
self.assertDictEqual(decoded_req, expected)
def test_log_created_from_api_call(self):
# create a project
self.api_create("raclette")
self.login("raclette")
# add members
self.api_add_member("raclette", "zorglub")
resp = self.client.get("/raclette/history", follow_redirects=True)
self.assertEqual(resp.status_code, 200)
self.assertIn(
f"Participant {em_surround('zorglub')} added", resp.data.decode("utf-8")
)
self.assertIn(
f"Project {em_surround('raclette')} added", resp.data.decode("utf-8")
)
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 2)
self.assertNotIn("127.0.0.1", resp.data.decode("utf-8"))
if __name__ == "__main__":
unittest.main()

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,5 @@
def em_surround(string, regex_escape=False):
if regex_escape:
return r'<em class="font-italic">%s<\/em>' % string
else:
return '<em class="font-italic">%s</em>' % string

View file

@ -0,0 +1,70 @@
from flask_testing import TestCase
from werkzeug.security import generate_password_hash
from ihatemoney import models
from ihatemoney.run import create_app, db
class BaseTestCase(TestCase):
SECRET_KEY = "TEST SESSION"
def create_app(self):
# Pass the test object as a configuration.
return create_app(self)
def setUp(self):
db.create_all()
def tearDown(self):
# clean after testing
db.session.remove()
db.drop_all()
def login(self, project, password=None, test_client=None):
password = password or project
return self.client.post(
"/authenticate",
data=dict(id=project, password=password),
follow_redirects=True,
)
def post_project(self, name, follow_redirects=True):
"""Create a fake project"""
# create the project
return self.client.post(
"/create",
data={
"name": name,
"id": name,
"password": name,
"contact_email": f"{name}@notmyidea.org",
"default_currency": "USD",
},
follow_redirects=follow_redirects,
)
def create_project(self, name):
project = models.Project(
id=name,
name=str(name),
password=generate_password_hash(name),
contact_email=f"{name}@notmyidea.org",
default_currency="USD",
)
models.db.session.add(project)
models.db.session.commit()
class IhatemoneyTestCase(BaseTestCase):
SQLALCHEMY_DATABASE_URI = "sqlite://"
TESTING = True
WTF_CSRF_ENABLED = False # Simplifies the tests.
def assertStatus(self, expected, resp, url=""):
return self.assertEqual(
expected,
resp.status_code,
f"{url} expected {expected}, got {resp.status_code}",
)

View file

@ -0,0 +1,599 @@
import unittest
from ihatemoney import history, models
from ihatemoney.tests.common.help_functions import em_surround
from ihatemoney.tests.common.ihatemoney_testcase import IhatemoneyTestCase
from ihatemoney.versioning import LoggingMode
class HistoryTestCase(IhatemoneyTestCase):
def setUp(self):
super().setUp()
self.post_project("demo")
self.login("demo")
def test_simple_create_logentry_no_ip(self):
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertIn(f"Project {em_surround('demo')} added", resp.data.decode("utf-8"))
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 1)
self.assertNotIn("127.0.0.1", resp.data.decode("utf-8"))
def change_privacy_to(self, logging_preference):
# Change only logging_preferences
new_data = {
"name": "demo",
"contact_email": "demo@notmyidea.org",
"password": "demo",
"default_currency": "USD",
}
if logging_preference != LoggingMode.DISABLED:
new_data["project_history"] = "y"
if logging_preference == LoggingMode.RECORD_IP:
new_data["ip_recording"] = "y"
# Disable History
resp = self.client.post("/demo/edit", data=new_data, follow_redirects=True)
self.assertEqual(resp.status_code, 200)
self.assertNotIn("danger", resp.data.decode("utf-8"))
resp = self.client.get("/demo/edit")
self.assertEqual(resp.status_code, 200)
if logging_preference == LoggingMode.DISABLED:
self.assertIn('<input id="project_history"', resp.data.decode("utf-8"))
else:
self.assertIn(
'<input checked id="project_history"', resp.data.decode("utf-8")
)
if logging_preference == LoggingMode.RECORD_IP:
self.assertIn('<input checked id="ip_recording"', resp.data.decode("utf-8"))
else:
self.assertIn('<input id="ip_recording"', resp.data.decode("utf-8"))
def assert_empty_history_logging_disabled(self):
resp = self.client.get("/demo/history")
self.assertIn(
"This project has history disabled. New actions won't appear below. ",
resp.data.decode("utf-8"),
)
self.assertIn("Nothing to list", resp.data.decode("utf-8"))
self.assertNotIn(
"The table below reflects actions recorded prior to disabling project history.",
resp.data.decode("utf-8"),
)
self.assertNotIn(
"Some entries below contain IP addresses,", resp.data.decode("utf-8")
)
self.assertNotIn("127.0.0.1", resp.data.decode("utf-8"))
self.assertNotIn("<td> -- </td>", resp.data.decode("utf-8"))
self.assertNotIn(
f"Project {em_surround('demo')} added", resp.data.decode("utf-8")
)
def test_project_edit(self):
new_data = {
"name": "demo2",
"contact_email": "demo2@notmyidea.org",
"password": "123456",
"project_history": "y",
"default_currency": "USD",
}
resp = self.client.post("/demo/edit", data=new_data, follow_redirects=True)
self.assertEqual(resp.status_code, 200)
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertIn(f"Project {em_surround('demo')} added", resp.data.decode("utf-8"))
self.assertIn(
f"Project contact email changed to {em_surround('demo2@notmyidea.org')}",
resp.data.decode("utf-8"),
)
self.assertIn("Project private code changed", resp.data.decode("utf-8"))
self.assertIn(
f"Project renamed to {em_surround('demo2')}", resp.data.decode("utf-8")
)
self.assertLess(
resp.data.decode("utf-8").index("Project renamed "),
resp.data.decode("utf-8").index("Project contact email changed to "),
)
self.assertLess(
resp.data.decode("utf-8").index("Project renamed "),
resp.data.decode("utf-8").index("Project private code changed"),
)
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 4)
self.assertNotIn("127.0.0.1", resp.data.decode("utf-8"))
def test_project_privacy_edit(self):
resp = self.client.get("/demo/edit")
self.assertEqual(resp.status_code, 200)
self.assertIn(
'<input checked id="project_history" name="project_history" type="checkbox" value="y">',
resp.data.decode("utf-8"),
)
self.change_privacy_to(LoggingMode.DISABLED)
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertIn("Disabled Project History\n", resp.data.decode("utf-8"))
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 2)
self.assertNotIn("127.0.0.1", resp.data.decode("utf-8"))
self.change_privacy_to(LoggingMode.RECORD_IP)
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertIn(
"Enabled Project History & IP Address Recording", resp.data.decode("utf-8")
)
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 2)
self.assertEqual(resp.data.decode("utf-8").count("127.0.0.1"), 1)
self.change_privacy_to(LoggingMode.ENABLED)
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertIn("Disabled IP Address Recording\n", resp.data.decode("utf-8"))
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 2)
self.assertEqual(resp.data.decode("utf-8").count("127.0.0.1"), 2)
def test_project_privacy_edit2(self):
self.change_privacy_to(LoggingMode.RECORD_IP)
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertIn("Enabled IP Address Recording\n", resp.data.decode("utf-8"))
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 1)
self.assertEqual(resp.data.decode("utf-8").count("127.0.0.1"), 1)
self.change_privacy_to(LoggingMode.DISABLED)
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertIn(
"Disabled Project History & IP Address Recording", resp.data.decode("utf-8")
)
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 1)
self.assertEqual(resp.data.decode("utf-8").count("127.0.0.1"), 2)
self.change_privacy_to(LoggingMode.ENABLED)
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertIn("Enabled Project History\n", resp.data.decode("utf-8"))
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 2)
self.assertEqual(resp.data.decode("utf-8").count("127.0.0.1"), 2)
def do_misc_database_operations(self, logging_mode):
new_data = {
"name": "demo2",
"contact_email": "demo2@notmyidea.org",
"password": "123456",
"default_currency": "USD",
}
# Keep privacy settings where they were
if logging_mode != LoggingMode.DISABLED:
new_data["project_history"] = "y"
if logging_mode == LoggingMode.RECORD_IP:
new_data["ip_recording"] = "y"
resp = self.client.post("/demo/edit", data=new_data, follow_redirects=True)
self.assertEqual(resp.status_code, 200)
# adds a member to this project
resp = self.client.post(
"/demo/members/add", data={"name": "zorglub"}, follow_redirects=True
)
self.assertEqual(resp.status_code, 200)
user_id = models.Person.query.one().id
# create a bill
resp = self.client.post(
"/demo/add",
data={
"date": "2011-08-10",
"what": "fromage à raclette",
"payer": user_id,
"payed_for": [user_id],
"amount": "25",
},
follow_redirects=True,
)
self.assertEqual(resp.status_code, 200)
bill_id = models.Bill.query.one().id
# edit the bill
resp = self.client.post(
f"/demo/edit/{bill_id}",
data={
"date": "2011-08-10",
"what": "fromage à raclette",
"payer": user_id,
"payed_for": [user_id],
"amount": "10",
},
follow_redirects=True,
)
self.assertEqual(resp.status_code, 200)
# delete the bill
resp = self.client.get(f"/demo/delete/{bill_id}", follow_redirects=True)
self.assertEqual(resp.status_code, 200)
# delete user using POST method
resp = self.client.post(
f"/demo/members/{user_id}/delete", follow_redirects=True
)
self.assertEqual(resp.status_code, 200)
def test_disable_clear_no_new_records(self):
# Disable logging
self.change_privacy_to(LoggingMode.DISABLED)
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertIn(
"This project has history disabled. New actions won't appear below. ",
resp.data.decode("utf-8"),
)
self.assertIn(
"The table below reflects actions recorded prior to disabling project history.",
resp.data.decode("utf-8"),
)
self.assertNotIn("Nothing to list", resp.data.decode("utf-8"))
self.assertNotIn(
"Some entries below contain IP addresses,", resp.data.decode("utf-8")
)
# Clear Existing Entries
resp = self.client.post("/demo/erase_history", follow_redirects=True)
self.assertEqual(resp.status_code, 200)
self.assert_empty_history_logging_disabled()
# Do lots of database operations & check that there's still no history
self.do_misc_database_operations(LoggingMode.DISABLED)
self.assert_empty_history_logging_disabled()
def test_clear_ip_records(self):
# Enable IP Recording
self.change_privacy_to(LoggingMode.RECORD_IP)
# Do lots of database operations to generate IP address entries
self.do_misc_database_operations(LoggingMode.RECORD_IP)
# Disable IP Recording
self.change_privacy_to(LoggingMode.ENABLED)
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertNotIn(
"This project has history disabled. New actions won't appear below. ",
resp.data.decode("utf-8"),
)
self.assertNotIn(
"The table below reflects actions recorded prior to disabling project history.",
resp.data.decode("utf-8"),
)
self.assertNotIn("Nothing to list", resp.data.decode("utf-8"))
self.assertIn(
"Some entries below contain IP addresses,", resp.data.decode("utf-8")
)
self.assertEqual(resp.data.decode("utf-8").count("127.0.0.1"), 10)
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 1)
# Generate more operations to confirm additional IP info isn't recorded
self.do_misc_database_operations(LoggingMode.ENABLED)
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data.decode("utf-8").count("127.0.0.1"), 10)
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 6)
# Clear IP Data
resp = self.client.post("/demo/strip_ip_addresses", follow_redirects=True)
self.assertEqual(resp.status_code, 200)
self.assertNotIn(
"This project has history disabled. New actions won't appear below. ",
resp.data.decode("utf-8"),
)
self.assertNotIn(
"The table below reflects actions recorded prior to disabling project history.",
resp.data.decode("utf-8"),
)
self.assertNotIn("Nothing to list", resp.data.decode("utf-8"))
self.assertNotIn(
"Some entries below contain IP addresses,", resp.data.decode("utf-8")
)
self.assertEqual(resp.data.decode("utf-8").count("127.0.0.1"), 0)
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 16)
def test_logs_for_common_actions(self):
# adds a member to this project
resp = self.client.post(
"/demo/members/add", data={"name": "zorglub"}, follow_redirects=True
)
self.assertEqual(resp.status_code, 200)
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertIn(
f"Participant {em_surround('zorglub')} added", resp.data.decode("utf-8")
)
# create a bill
resp = self.client.post(
"/demo/add",
data={
"date": "2011-08-10",
"what": "fromage à raclette",
"payer": 1,
"payed_for": [1],
"amount": "25",
},
follow_redirects=True,
)
self.assertEqual(resp.status_code, 200)
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertIn(
f"Bill {em_surround('fromage à raclette')} added",
resp.data.decode("utf-8"),
)
# edit the bill
resp = self.client.post(
"/demo/edit/1",
data={
"date": "2011-08-10",
"what": "new thing",
"payer": 1,
"payed_for": [1],
"amount": "10",
},
follow_redirects=True,
)
self.assertEqual(resp.status_code, 200)
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertIn(
f"Bill {em_surround('fromage à raclette')} added",
resp.data.decode("utf-8"),
)
self.assertRegex(
resp.data.decode("utf-8"),
r"Bill %s:\s* amount changed\s* from %s\s* to %s"
% (
em_surround("fromage à raclette", regex_escape=True),
em_surround("25.0", regex_escape=True),
em_surround("10.0", regex_escape=True),
),
)
self.assertIn(
"Bill %s renamed to %s"
% (em_surround("fromage à raclette"), em_surround("new thing")),
resp.data.decode("utf-8"),
)
self.assertLess(
resp.data.decode("utf-8").index(
f"Bill {em_surround('fromage à raclette')} renamed to"
),
resp.data.decode("utf-8").index("amount changed"),
)
# delete the bill
resp = self.client.get("/demo/delete/1", follow_redirects=True)
self.assertEqual(resp.status_code, 200)
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertIn(
f"Bill {em_surround('new thing')} removed", resp.data.decode("utf-8")
)
# edit user
resp = self.client.post(
"/demo/members/1/edit",
data={"weight": 2, "name": "new name"},
follow_redirects=True,
)
self.assertEqual(resp.status_code, 200)
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertRegex(
resp.data.decode("utf-8"),
r"Participant %s:\s* weight changed\s* from %s\s* to %s"
% (
em_surround("zorglub", regex_escape=True),
em_surround("1.0", regex_escape=True),
em_surround("2.0", regex_escape=True),
),
)
self.assertIn(
"Participant %s renamed to %s"
% (em_surround("zorglub"), em_surround("new name")),
resp.data.decode("utf-8"),
)
self.assertLess(
resp.data.decode("utf-8").index(
f"Participant {em_surround('zorglub')} renamed"
),
resp.data.decode("utf-8").index("weight changed"),
)
# delete user using POST method
resp = self.client.post("/demo/members/1/delete", follow_redirects=True)
self.assertEqual(resp.status_code, 200)
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertIn(
f"Participant {em_surround('new name')} removed", resp.data.decode("utf-8")
)
def test_double_bill_double_person_edit_second(self):
# add two members
self.client.post("/demo/members/add", data={"name": "User 1"})
self.client.post("/demo/members/add", data={"name": "User 2"})
# add two bills
self.client.post(
"/demo/add",
data={
"date": "2020-04-13",
"what": "Bill 1",
"payer": 1,
"payed_for": [1, 2],
"amount": "25",
},
)
self.client.post(
"/demo/add",
data={
"date": "2020-04-13",
"what": "Bill 2",
"payer": 1,
"payed_for": [1, 2],
"amount": "20",
},
)
# Should be 5 history entries at this point
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 5)
self.assertNotIn("127.0.0.1", resp.data.decode("utf-8"))
# Edit ONLY the amount on the first bill
self.client.post(
"/demo/edit/1",
data={
"date": "2020-04-13",
"what": "Bill 1",
"payer": 1,
"payed_for": [1, 2],
"amount": "88",
},
)
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertRegex(
resp.data.decode("utf-8"),
r"Bill {}:\s* amount changed\s* from {}\s* to {}".format(
em_surround("Bill 1", regex_escape=True),
em_surround("25.0", regex_escape=True),
em_surround("88.0", regex_escape=True),
),
)
self.assertNotRegex(
resp.data.decode("utf-8"),
r"Removed\s* {}\s* and\s* {}\s* from\s* owers list".format(
em_surround("User 1", regex_escape=True),
em_surround("User 2", regex_escape=True),
),
resp.data.decode("utf-8"),
)
# Should be 6 history entries at this point
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 6)
self.assertNotIn("127.0.0.1", resp.data.decode("utf-8"))
def test_bill_add_remove_add(self):
# add two members
self.client.post("/demo/members/add", data={"name": "User 1"})
self.client.post("/demo/members/add", data={"name": "User 2"})
# add 1 bill
self.client.post(
"/demo/add",
data={
"date": "2020-04-13",
"what": "Bill 1",
"payer": 1,
"payed_for": [1, 2],
"amount": "25",
},
)
# delete the bill
self.client.get("/demo/delete/1", follow_redirects=True)
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 5)
self.assertNotIn("127.0.0.1", resp.data.decode("utf-8"))
self.assertIn(f"Bill {em_surround('Bill 1')} added", resp.data.decode("utf-8"))
self.assertIn(
f"Bill {em_surround('Bill 1')} removed", resp.data.decode("utf-8")
)
# Add a new bill
self.client.post(
"/demo/add",
data={
"date": "2020-04-13",
"what": "Bill 2",
"payer": 1,
"payed_for": [1, 2],
"amount": "20",
},
)
resp = self.client.get("/demo/history")
self.assertEqual(resp.status_code, 200)
self.assertEqual(resp.data.decode("utf-8").count("<td> -- </td>"), 6)
self.assertNotIn("127.0.0.1", resp.data.decode("utf-8"))
self.assertIn(f"Bill {em_surround('Bill 1')} added", resp.data.decode("utf-8"))
self.assertEqual(
resp.data.decode("utf-8").count(f"Bill {em_surround('Bill 1')} added"), 1
)
self.assertIn(f"Bill {em_surround('Bill 2')} added", resp.data.decode("utf-8"))
self.assertIn(
f"Bill {em_surround('Bill 1')} removed", resp.data.decode("utf-8")
)
def test_double_bill_double_person_edit_second_no_web(self):
u1 = models.Person(project_id="demo", name="User 1")
u2 = models.Person(project_id="demo", name="User 1")
models.db.session.add(u1)
models.db.session.add(u2)
models.db.session.commit()
b1 = models.Bill(what="Bill 1", payer_id=u1.id, owers=[u2], amount=10)
b2 = models.Bill(what="Bill 2", payer_id=u2.id, owers=[u2], amount=11)
# This db commit exposes the "spurious owers edit" bug
models.db.session.add(b1)
models.db.session.commit()
models.db.session.add(b2)
models.db.session.commit()
history_list = history.get_history(models.Project.query.get("demo"))
self.assertEqual(len(history_list), 5)
# Change just the amount
b1.amount = 5
models.db.session.commit()
history_list = history.get_history(models.Project.query.get("demo"))
for entry in history_list:
if "prop_changed" in entry:
self.assertNotIn("owers", entry["prop_changed"])
self.assertEqual(len(history_list), 6)
if __name__ == "__main__":
unittest.main()

View file

@ -0,0 +1,265 @@
import io
import os
import smtplib
import socket
import unittest
from unittest.mock import MagicMock, patch
from sqlalchemy import orm
from ihatemoney import models
from ihatemoney.currency_convertor import CurrencyConverter
from ihatemoney.manage import DeleteProject, GenerateConfig, GeneratePasswordHash
from ihatemoney.run import load_configuration
from ihatemoney.tests.common.ihatemoney_testcase import BaseTestCase, IhatemoneyTestCase
# Unset configuration file env var if previously set
os.environ.pop("IHATEMONEY_SETTINGS_FILE_PATH", None)
__HERE__ = os.path.dirname(os.path.abspath(__file__))
class ConfigurationTestCase(BaseTestCase):
def test_default_configuration(self):
"""Test that default settings are loaded when no other configuration file is specified"""
self.assertFalse(self.app.config["DEBUG"])
self.assertEqual(
self.app.config["SQLALCHEMY_DATABASE_URI"], "sqlite:////tmp/ihatemoney.db"
)
self.assertFalse(self.app.config["SQLALCHEMY_TRACK_MODIFICATIONS"])
self.assertEqual(
self.app.config["MAIL_DEFAULT_SENDER"],
("Budget manager", "budget@notmyidea.org"),
)
def test_env_var_configuration_file(self):
"""Test that settings are loaded from the specified configuration file"""
os.environ["IHATEMONEY_SETTINGS_FILE_PATH"] = os.path.join(
__HERE__, "ihatemoney_envvar.cfg"
)
load_configuration(self.app)
self.assertEqual(self.app.config["SECRET_KEY"], "lalatra")
# Test that the specified configuration file is loaded
# even if the default configuration file ihatemoney.cfg exists
os.environ["IHATEMONEY_SETTINGS_FILE_PATH"] = os.path.join(
__HERE__, "ihatemoney_envvar.cfg"
)
self.app.config.root_path = __HERE__
load_configuration(self.app)
self.assertEqual(self.app.config["SECRET_KEY"], "lalatra")
os.environ.pop("IHATEMONEY_SETTINGS_FILE_PATH", None)
def test_default_configuration_file(self):
"""Test that settings are loaded from the default configuration file"""
self.app.config.root_path = __HERE__
load_configuration(self.app)
self.assertEqual(self.app.config["SECRET_KEY"], "supersecret")
class ServerTestCase(IhatemoneyTestCase):
def test_homepage(self):
# See https://github.com/spiral-project/ihatemoney/pull/358
self.app.config["APPLICATION_ROOT"] = "/"
req = self.client.get("/")
self.assertStatus(200, req)
def test_unprefixed(self):
self.app.config["APPLICATION_ROOT"] = "/"
req = self.client.get("/foo/")
self.assertStatus(303, req)
def test_prefixed(self):
self.app.config["APPLICATION_ROOT"] = "/foo"
req = self.client.get("/foo/")
self.assertStatus(200, req)
class CommandTestCase(BaseTestCase):
def test_generate_config(self):
"""Simply checks that all config file generation
- raise no exception
- produce something non-empty
"""
cmd = GenerateConfig()
for config_file in cmd.get_options()[0].kwargs["choices"]:
with patch("sys.stdout", new=io.StringIO()) as stdout:
cmd.run(config_file)
print(stdout.getvalue())
self.assertNotEqual(len(stdout.getvalue().strip()), 0)
def test_generate_password_hash(self):
cmd = GeneratePasswordHash()
with patch("sys.stdout", new=io.StringIO()) as stdout, patch(
"getpass.getpass", new=lambda prompt: "secret"
): # NOQA
cmd.run()
print(stdout.getvalue())
self.assertEqual(len(stdout.getvalue().strip()), 189)
def test_demo_project_deletion(self):
self.create_project("demo")
self.assertEquals(models.Project.query.get("demo").name, "demo")
cmd = DeleteProject()
cmd.run("demo")
self.assertEqual(len(models.Project.query.all()), 0)
class ModelsTestCase(IhatemoneyTestCase):
def test_bill_pay_each(self):
self.post_project("raclette")
# add members
self.client.post("/raclette/members/add", data={"name": "zorglub", "weight": 2})
self.client.post("/raclette/members/add", data={"name": "fred"})
self.client.post("/raclette/members/add", data={"name": "tata"})
# Add a member with a balance=0 :
self.client.post("/raclette/members/add", data={"name": "pépé"})
# create bills
self.client.post(
"/raclette/add",
data={
"date": "2011-08-10",
"what": "fromage à raclette",
"payer": 1,
"payed_for": [1, 2, 3],
"amount": "10.0",
},
)
self.client.post(
"/raclette/add",
data={
"date": "2011-08-10",
"what": "red wine",
"payer": 2,
"payed_for": [1],
"amount": "20",
},
)
self.client.post(
"/raclette/add",
data={
"date": "2011-08-10",
"what": "delicatessen",
"payer": 1,
"payed_for": [1, 2],
"amount": "10",
},
)
project = models.Project.query.get_by_name(name="raclette")
zorglub = models.Person.query.get_by_name(name="zorglub", project=project)
zorglub_bills = models.Bill.query.options(
orm.subqueryload(models.Bill.owers)
).filter(models.Bill.owers.contains(zorglub))
for bill in zorglub_bills.all():
if bill.what == "red wine":
pay_each_expected = 20 / 2
self.assertEqual(bill.pay_each(), pay_each_expected)
if bill.what == "fromage à raclette":
pay_each_expected = 10 / 4
self.assertEqual(bill.pay_each(), pay_each_expected)
if bill.what == "delicatessen":
pay_each_expected = 10 / 3
self.assertEqual(bill.pay_each(), pay_each_expected)
class EmailFailureTestCase(IhatemoneyTestCase):
def test_creation_email_failure_smtp(self):
self.login("raclette")
with patch.object(
self.app.mail, "send", MagicMock(side_effect=smtplib.SMTPException)
):
resp = self.post_project("raclette")
# Check that an error message is displayed
self.assertIn(
"We tried to send you an reminder email, but there was an error",
resp.data.decode("utf-8"),
)
# Check that we were redirected to the home page anyway
self.assertIn(
'You probably want to <a href="/raclette/members/add"',
resp.data.decode("utf-8"),
)
def test_creation_email_failure_socket(self):
self.login("raclette")
with patch.object(self.app.mail, "send", MagicMock(side_effect=socket.error)):
resp = self.post_project("raclette")
# Check that an error message is displayed
self.assertIn(
"We tried to send you an reminder email, but there was an error",
resp.data.decode("utf-8"),
)
# Check that we were redirected to the home page anyway
self.assertIn(
'You probably want to <a href="/raclette/members/add"',
resp.data.decode("utf-8"),
)
def test_password_reset_email_failure(self):
self.create_project("raclette")
for exception in (smtplib.SMTPException, socket.error):
with patch.object(self.app.mail, "send", MagicMock(side_effect=exception)):
resp = self.client.post(
"/password-reminder", data={"id": "raclette"}, follow_redirects=True
)
# Check that an error message is displayed
self.assertIn(
"there was an error while sending you an email",
resp.data.decode("utf-8"),
)
# Check that we were not redirected to the success page
self.assertNotIn(
"A link to reset your password has been sent to you",
resp.data.decode("utf-8"),
)
def test_invitation_email_failure(self):
self.login("raclette")
self.post_project("raclette")
for exception in (smtplib.SMTPException, socket.error):
with patch.object(self.app.mail, "send", MagicMock(side_effect=exception)):
resp = self.client.post(
"/raclette/invite",
data={"emails": "toto@notmyidea.org"},
follow_redirects=True,
)
# Check that an error message is displayed
self.assertIn(
"there was an error while trying to send the invitation emails",
resp.data.decode("utf-8"),
)
# Check that we are still on the same page (no redirection)
self.assertIn(
"Invite people to join this project", resp.data.decode("utf-8")
)
class TestCurrencyConverter(unittest.TestCase):
converter = CurrencyConverter()
mock_data = {"USD": 1, "EUR": 0.8115}
converter.get_rates = MagicMock(return_value=mock_data)
def test_only_one_instance(self):
one = id(CurrencyConverter())
two = id(CurrencyConverter())
self.assertEqual(one, two)
def test_get_currencies(self):
self.assertCountEqual(self.converter.get_currencies(), ["USD", "EUR"])
def test_exchange_currency(self):
result = self.converter.exchange_currency(100, "USD", "EUR")
self.assertEqual(result, 81.15)
if __name__ == "__main__":
unittest.main()

File diff suppressed because it is too large Load diff

View file

@ -1,163 +1,167 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-05-30 21:50+0200\n" "POT-Creation-Date: 2020-05-30 21:50+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: 2021-01-08 14:32+0000\n"
"Last-Translator: Automatically generated\n" "Last-Translator: Oliver Klimt <klimt.oliver@gmail.com>\n"
"Language-Team: Czech <https://hosted.weblate.org/projects/i-hate-money/"
"i-hate-money/cs/>\n"
"Language: cs\n" "Language: cs\n"
"Language-Team: none\n"
"Plural-Forms: nplurals=3; plural=((n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2)\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n" "Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n"
"X-Generator: Weblate 4.4.1-dev\n"
"Generated-By: Babel 2.8.0\n" "Generated-By: Babel 2.8.0\n"
msgid "" msgid ""
"Not a valid amount or expression. Only numbers and + - * / operators are " "Not a valid amount or expression. Only numbers and + - * / operators are "
"accepted." "accepted."
msgstr "" msgstr ""
"Neplatná částka nebo výraz. Pouze čísla a operátory + - * / jsou přípustná"
msgid "Project name" msgid "Project name"
msgstr "" msgstr "Název projektu"
msgid "Private code" msgid "Private code"
msgstr "" msgstr "Přístupový kód"
msgid "Email" msgid "Email"
msgstr "" msgstr "Email"
msgid "Enable project history" msgid "Enable project history"
msgstr "" msgstr "Povolit historii projektu"
msgid "Use IP tracking for project history" msgid "Use IP tracking for project history"
msgstr "" msgstr "Záznam IP adres pro historii projektu"
msgid "Default Currency" msgid "Default Currency"
msgstr "" msgstr "Výchozí měna"
msgid "Import previously exported JSON file" msgid "Import previously exported JSON file"
msgstr "" msgstr "Import exportovaného JSON souboru"
msgid "Import" msgid "Import"
msgstr "" msgstr "Import"
msgid "Project identifier" msgid "Project identifier"
msgstr "" msgstr "Identifikátor projektu"
msgid "Create the project" msgid "Create the project"
msgstr "" msgstr "Vytvořit projekt"
#, python-format #, python-format
msgid "" msgid ""
"A project with this identifier (\"%(project)s\") already exists. Please " "A project with this identifier (\"%(project)s\") already exists. Please "
"choose a new identifier" "choose a new identifier"
msgstr "" msgstr ""
"Projekt s tímto identifikátorem (\"%(project)s\") již existuje, zvolte nový "
"identifikátor"
msgid "Get in" msgid "Get in"
msgstr "" msgstr "Vstoupit"
msgid "Admin password" msgid "Admin password"
msgstr "" msgstr "Administrátorské heslo"
msgid "Send me the code by email" msgid "Send me the code by email"
msgstr "" msgstr "Poslat kód na e-mail"
msgid "This project does not exists" msgid "This project does not exists"
msgstr "" msgstr "Tento projekt neexistuje"
msgid "Password mismatch" msgid "Password mismatch"
msgstr "" msgstr "Heslo nesouhlasí"
msgid "Password" msgid "Password"
msgstr "" msgstr "Heslo"
msgid "Password confirmation" msgid "Password confirmation"
msgstr "" msgstr "Potvrzení hesla"
msgid "Reset password" msgid "Reset password"
msgstr "" msgstr "Obnova hesla"
msgid "Date" msgid "Date"
msgstr "" msgstr "Datum"
msgid "What?" msgid "What?"
msgstr "" msgstr "Co?"
msgid "Payer" msgid "Payer"
msgstr "" msgstr "Platící"
msgid "Amount paid" msgid "Amount paid"
msgstr "" msgstr "Zaplacená částka"
msgid "Currency" msgid "Currency"
msgstr "" msgstr "Měna"
msgid "External link" msgid "External link"
msgstr "" msgstr "Externí odkaz"
msgid "A link to an external document, related to this bill" msgid "A link to an external document, related to this bill"
msgstr "" msgstr "Externí odkaz k této účtence"
msgid "For whom?" msgid "For whom?"
msgstr "" msgstr "Od koho?"
msgid "Submit" msgid "Submit"
msgstr "" msgstr "Odeslat"
msgid "Submit and add a new one" msgid "Submit and add a new one"
msgstr "" msgstr "Odeslat a přidat nový"
#, python-format #, python-format
msgid "Project default: %(currency)s" msgid "Project default: %(currency)s"
msgstr "" msgstr "Výchozí měna projektu: %(currency)s"
msgid "Bills can't be null" msgid "Bills can't be null"
msgstr "" msgstr "Účtenka nemůže být nulová"
msgid "Name" msgid "Name"
msgstr "" msgstr "Jméno"
msgid "Weights should be positive" msgid "Weights should be positive"
msgstr "" msgstr ""
msgid "Weight" msgid "Weight"
msgstr "" msgstr "Váha"
msgid "Add" msgid "Add"
msgstr "" msgstr "Přidat"
msgid "User name incorrect" msgid "User name incorrect"
msgstr "" msgstr "Nesprávné uživatelské jméno"
msgid "This project already have this member" msgid "This project already have this member"
msgstr "" msgstr "Projekt již obsahuje tohoto člena"
msgid "People to notify" msgid "People to notify"
msgstr "" msgstr "Osoby k informování"
msgid "Send invites" msgid "Send invites"
msgstr "" msgstr "Poslat pozvánky"
#, python-format #, python-format
msgid "The email %(email)s is not valid" msgid "The email %(email)s is not valid"
msgstr "" msgstr "Toto (%(email)s) není validní e-mail"
msgid "Participant" msgid "Participant"
msgstr "" msgstr "Účastník"
msgid "Bill" msgid "Bill"
msgstr "" msgstr "Účet"
msgid "Project" msgid "Project"
msgstr "" msgstr "Projekt"
msgid "No Currency" msgid "No Currency"
msgstr "" msgstr "Žádná měna"
msgid "Too many failed login attempts, please retry later." msgid "Too many failed login attempts, please retry later."
msgstr "" msgstr "Příliš mnoho pokusů, zkuste to později."
#, python-format #, python-format
msgid "This admin password is not the right one. Only %(num)d attempts left." msgid "This admin password is not the right one. Only %(num)d attempts left."
@ -783,4 +787,3 @@ msgstr ""
#~ "is by the server, so don\\'t reuse" #~ "is by the server, so don\\'t reuse"
#~ " a personal password!" #~ " a personal password!"
#~ msgstr "" #~ msgstr ""

View file

@ -1,17 +1,18 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-05-30 21:50+0200\n" "POT-Creation-Date: 2020-05-30 21:50+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: 2021-02-23 14:50+0000\n"
"Last-Translator: Automatically generated\n" "Last-Translator: Michalis <michalisntovas@yahoo.gr>\n"
"Language-Team: Greek <https://hosted.weblate.org/projects/i-hate-money/"
"i-hate-money/el/>\n"
"Language: el\n" "Language: el\n"
"Language-Team: none\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n" "Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.5\n"
"Generated-By: Babel 2.8.0\n" "Generated-By: Babel 2.8.0\n"
msgid "" msgid ""
@ -62,37 +63,37 @@ msgid "Admin password"
msgstr "" msgstr ""
msgid "Send me the code by email" msgid "Send me the code by email"
msgstr "" msgstr "Στείλε μου τον κωδικό μέσω εμάιλ"
msgid "This project does not exists" msgid "This project does not exists"
msgstr "" msgstr "Το πρότζεκτ αυτό δεν υπάρχει"
msgid "Password mismatch" msgid "Password mismatch"
msgstr "" msgstr "Ο κωδικός δεν ταιριάζει"
msgid "Password" msgid "Password"
msgstr "" msgstr "Κωδικός"
msgid "Password confirmation" msgid "Password confirmation"
msgstr "" msgstr "Επιβεβαίωση κωδικού"
msgid "Reset password" msgid "Reset password"
msgstr "" msgstr "Επαναφορά κωδικού"
msgid "Date" msgid "Date"
msgstr "" msgstr "Ημερομηνία"
msgid "What?" msgid "What?"
msgstr "" msgstr "Τι?"
msgid "Payer" msgid "Payer"
msgstr "" msgstr "Πληρωτής"
msgid "Amount paid" msgid "Amount paid"
msgstr "" msgstr "Πληρωμένο ποσό"
msgid "Currency" msgid "Currency"
msgstr "" msgstr "Νόμισμα"
msgid "External link" msgid "External link"
msgstr "" msgstr ""
@ -101,13 +102,13 @@ msgid "A link to an external document, related to this bill"
msgstr "" msgstr ""
msgid "For whom?" msgid "For whom?"
msgstr "" msgstr "Για ποιόν?"
msgid "Submit" msgid "Submit"
msgstr "" msgstr "Υποβολή"
msgid "Submit and add a new one" msgid "Submit and add a new one"
msgstr "" msgstr "Υποβολή και προσθήκη νέου"
#, python-format #, python-format
msgid "Project default: %(currency)s" msgid "Project default: %(currency)s"
@ -117,51 +118,53 @@ msgid "Bills can't be null"
msgstr "" msgstr ""
msgid "Name" msgid "Name"
msgstr "" msgstr "Όνομα"
msgid "Weights should be positive" msgid "Weights should be positive"
msgstr "" msgstr "Τα βάρη πρέπει να είναι θετικά"
msgid "Weight" msgid "Weight"
msgstr "" msgstr "Βάρος"
msgid "Add" msgid "Add"
msgstr "" msgstr "Προσθήκη"
msgid "User name incorrect" msgid "User name incorrect"
msgstr "" msgstr "Λανθασμένο όνομα χρήστη"
msgid "This project already have this member" msgid "This project already have this member"
msgstr "" msgstr "Το πρότζεκτ έχει ήδη αυτό το μέλος"
msgid "People to notify" msgid "People to notify"
msgstr "" msgstr "Άτομα να ειδοποιήσεις"
msgid "Send invites" msgid "Send invites"
msgstr "" msgstr "Στείλε πρόσκληση"
#, python-format #, python-format
msgid "The email %(email)s is not valid" msgid "The email %(email)s is not valid"
msgstr "" msgstr ""
msgid "Participant" msgid "Participant"
msgstr "" msgstr "Συμμέτοχος"
msgid "Bill" msgid "Bill"
msgstr "" msgstr "Λογαριασμός"
msgid "Project" msgid "Project"
msgstr "" msgstr "Πρότζεκτ"
msgid "No Currency" msgid "No Currency"
msgstr "" msgstr "Κανένα νόμισμα"
msgid "Too many failed login attempts, please retry later." msgid "Too many failed login attempts, please retry later."
msgstr "" msgstr "Πάρα πολλές αποτυχημένες προσπάθειες σύνδεσης, δοκιμάστε ξανά αργότερα."
#, python-format #, python-format
msgid "This admin password is not the right one. Only %(num)d attempts left." msgid "This admin password is not the right one. Only %(num)d attempts left."
msgstr "" msgstr ""
"Αυτός ο κωδικός διαχειριστή δεν είναι σωστός. Μένουν μόνο %(num)d "
"προσπάθειες."
msgid "You either provided a bad token or no project identifier." msgid "You either provided a bad token or no project identifier."
msgstr "" msgstr ""
@ -174,12 +177,14 @@ msgid "You have just created '%(project)s' to share your expenses"
msgstr "" msgstr ""
msgid "A reminder email has just been sent to you" msgid "A reminder email has just been sent to you"
msgstr "" msgstr "Ένα εμάιλ υπενθύμισης μόλις σας στάλθηκε"
msgid "" msgid ""
"We tried to send you an reminder email, but there was an error. You can " "We tried to send you an reminder email, but there was an error. You can "
"still use the project normally." "still use the project normally."
msgstr "" msgstr ""
"Προσπαθήσαμε να σας στείλουμε ένα εμάιλ υπενθύμισης, αλλά υπήρξε ένα λάθος. "
"Μπορείτε ακόμα να χρησιμοποιήσετε το πρότζεκτ κανονικά."
#, python-format #, python-format
msgid "The project identifier is %(project)s" msgid "The project identifier is %(project)s"
@ -198,10 +203,10 @@ msgid "Invalid token"
msgstr "" msgstr ""
msgid "Unknown project" msgid "Unknown project"
msgstr "" msgstr "Άγνωστο πρότζεκτ"
msgid "Password successfully reset." msgid "Password successfully reset."
msgstr "" msgstr "Επαναφορά του κωδικού επιτυχώς."
msgid "Project successfully uploaded" msgid "Project successfully uploaded"
msgstr "" msgstr ""
@ -210,14 +215,14 @@ msgid "Invalid JSON"
msgstr "" msgstr ""
msgid "Project successfully deleted" msgid "Project successfully deleted"
msgstr "" msgstr "Το πρότζεκτ διαγράφηκε επιτυχώς"
#, python-format #, python-format
msgid "You have been invited to share your expenses for %(project)s" msgid "You have been invited to share your expenses for %(project)s"
msgstr "" msgstr ""
msgid "Your invitations have been sent" msgid "Your invitations have been sent"
msgstr "" msgstr "Οι προσκλήσεις έχουν σταλθεί"
msgid "" msgid ""
"Sorry, there was an error while trying to send the invitation emails. " "Sorry, there was an error while trying to send the invitation emails. "
@ -248,7 +253,7 @@ msgid "User '%(name)s' has been edited"
msgstr "" msgstr ""
msgid "The bill has been added" msgid "The bill has been added"
msgstr "" msgstr "Ο λογαριασμός προστέθηκε"
msgid "The bill has been deleted" msgid "The bill has been deleted"
msgstr "" msgstr ""
@ -783,4 +788,3 @@ msgstr ""
#~ "is by the server, so don\\'t reuse" #~ "is by the server, so don\\'t reuse"
#~ " a personal password!" #~ " a personal password!"
#~ msgstr "" #~ msgstr ""

View file

@ -3,49 +3,53 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-09-22 14:01+0200\n" "POT-Creation-Date: 2020-09-22 14:01+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: 2020-11-11 16:28+0000\n"
"Last-Translator: Automatically generated\n" "Last-Translator: Puyma <puyma@amyup.xyz>\n"
"Language-Team: none\n" "Language-Team: Spanish <https://hosted.weblate.org/projects/i-hate-money/"
"i-hate-money/es/>\n"
"Language: es\n" "Language: es\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Generator: Translate Toolkit 3.0.0\n" "Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.4-dev\n"
msgid "" msgid ""
"Not a valid amount or expression. Only numbers and + - * / operators are " "Not a valid amount or expression. Only numbers and + - * / operators are "
"accepted." "accepted."
msgstr "" msgstr ""
"Cantidad o expresión no válida. Solo se aceptan números y los operadores + - "
"* /."
msgid "Project name" msgid "Project name"
msgstr "" msgstr "Nombre del proyecto"
msgid "Private code" msgid "Private code"
msgstr "" msgstr "Código privado"
msgid "Email" msgid "Email"
msgstr "" msgstr "Correo electrónico"
msgid "Enable project history" msgid "Enable project history"
msgstr "" msgstr "Habilitar historial del proyecto"
msgid "Use IP tracking for project history" msgid "Use IP tracking for project history"
msgstr "" msgstr ""
msgid "Default Currency" msgid "Default Currency"
msgstr "" msgstr "Moneda por defecto"
msgid "Import previously exported JSON file" msgid "Import previously exported JSON file"
msgstr "" msgstr "Importar .JSON previamente exportado"
msgid "Import" msgid "Import"
msgstr "" msgstr "Importar"
msgid "Project identifier" msgid "Project identifier"
msgstr "" msgstr "Identificador del proyecto"
msgid "Create the project" msgid "Create the project"
msgstr "" msgstr "Crear proyecto"
#, python-format #, python-format
msgid "" msgid ""
@ -57,49 +61,49 @@ msgid "Get in"
msgstr "" msgstr ""
msgid "Admin password" msgid "Admin password"
msgstr "" msgstr "Contraseña de administrador"
msgid "Send me the code by email" msgid "Send me the code by email"
msgstr "" msgstr "Envíame el código por correo electrónico"
msgid "This project does not exists" msgid "This project does not exists"
msgstr "" msgstr "Proyecto inexistente"
msgid "Password mismatch" msgid "Password mismatch"
msgstr "" msgstr "La contraseña no coincide"
msgid "Password" msgid "Password"
msgstr "" msgstr "Contraseña"
msgid "Password confirmation" msgid "Password confirmation"
msgstr "" msgstr "Confirmar contraseña"
msgid "Reset password" msgid "Reset password"
msgstr "" msgstr "Reestablecer contraseña"
msgid "Date" msgid "Date"
msgstr "" msgstr "Fecha"
msgid "What?" msgid "What?"
msgstr "" msgstr "¿Qué?"
msgid "Payer" msgid "Payer"
msgstr "" msgstr ""
msgid "Amount paid" msgid "Amount paid"
msgstr "" msgstr "Cantidad pagada"
msgid "Currency" msgid "Currency"
msgstr "" msgstr "Divisa"
msgid "External link" msgid "External link"
msgstr "" msgstr "Enlace externo"
msgid "A link to an external document, related to this bill" msgid "A link to an external document, related to this bill"
msgstr "" msgstr ""
msgid "For whom?" msgid "For whom?"
msgstr "" msgstr "¿Para quién?"
msgid "Submit" msgid "Submit"
msgstr "" msgstr ""
@ -115,7 +119,7 @@ msgid "Bills can't be null"
msgstr "" msgstr ""
msgid "Name" msgid "Name"
msgstr "" msgstr "Nombre"
msgid "Weights should be positive" msgid "Weights should be positive"
msgstr "" msgstr ""
@ -124,10 +128,10 @@ msgid "Weight"
msgstr "" msgstr ""
msgid "Add" msgid "Add"
msgstr "" msgstr "Añadir"
msgid "User name incorrect" msgid "User name incorrect"
msgstr "" msgstr "Usuario incorrecto"
msgid "This project already have this member" msgid "This project already have this member"
msgstr "" msgstr ""
@ -136,20 +140,20 @@ msgid "People to notify"
msgstr "" msgstr ""
msgid "Send invites" msgid "Send invites"
msgstr "" msgstr "Enviar invitaciones"
#, python-format #, python-format
msgid "The email %(email)s is not valid" msgid "The email %(email)s is not valid"
msgstr "" msgstr "El correo %(email)s no es válido"
msgid "Participant" msgid "Participant"
msgstr "" msgstr "Participante"
msgid "Bill" msgid "Bill"
msgstr "" msgstr "Factura"
msgid "Project" msgid "Project"
msgstr "" msgstr "Proyecto"
msgid "No Currency" msgid "No Currency"
msgstr "" msgstr ""
@ -291,16 +295,16 @@ msgid "Oldest bill"
msgstr "" msgstr ""
msgid "Actions" msgid "Actions"
msgstr "" msgstr "Acciones"
msgid "edit" msgid "edit"
msgstr "" msgstr "editar"
msgid "delete" msgid "delete"
msgstr "" msgstr "eliminar"
msgid "show" msgid "show"
msgstr "" msgstr "mostrar"
msgid "The Dashboard is currently deactivated." msgid "The Dashboard is currently deactivated."
msgstr "" msgstr ""
@ -336,49 +340,49 @@ msgid "Can't remember the password?"
msgstr "" msgstr ""
msgid "Cancel" msgid "Cancel"
msgstr "" msgstr "Cancelar"
msgid "Privacy Settings" msgid "Privacy Settings"
msgstr "" msgstr "Ajustes de privacidad"
msgid "Edit the project" msgid "Edit the project"
msgstr "" msgstr "Editar el proyecto"
msgid "Edit this bill" msgid "Edit this bill"
msgstr "" msgstr "Editar esta factura"
msgid "Add a bill" msgid "Add a bill"
msgstr "" msgstr "Añadir una factura"
msgid "Select all" msgid "Select all"
msgstr "" msgstr "Seleccionar todo"
msgid "Select none" msgid "Select none"
msgstr "" msgstr ""
msgid "Add participant" msgid "Add participant"
msgstr "" msgstr "Añadir participante"
msgid "Edit this member" msgid "Edit this member"
msgstr "" msgstr "Editar miembro"
msgid "john.doe@example.com, mary.moe@site.com" msgid "john.doe@example.com, mary.moe@site.com"
msgstr "" msgstr ""
msgid "Send the invitations" msgid "Send the invitations"
msgstr "" msgstr "Enviar invitaciones"
msgid "Download" msgid "Download"
msgstr "" msgstr "Descargar"
msgid "Disabled Project History" msgid "Disabled Project History"
msgstr "" msgstr "Historial del proyecto desactivado"
msgid "Disabled Project History & IP Address Recording" msgid "Disabled Project History & IP Address Recording"
msgstr "" msgstr ""
msgid "Enabled Project History" msgid "Enabled Project History"
msgstr "" msgstr "Historial del proyecto activado"
msgid "Disabled IP Address Recording" msgid "Disabled IP Address Recording"
msgstr "" msgstr ""
@ -396,10 +400,10 @@ msgid "changed"
msgstr "" msgstr ""
msgid "from" msgid "from"
msgstr "" msgstr "de"
msgid "to" msgid "to"
msgstr "" msgstr "para"
msgid "Confirm Remove IP Adresses" msgid "Confirm Remove IP Adresses"
msgstr "" msgstr ""
@ -412,7 +416,7 @@ msgid ""
msgstr "" msgstr ""
msgid "Close" msgid "Close"
msgstr "" msgstr "Cerrar"
msgid "Confirm Delete" msgid "Confirm Delete"
msgstr "" msgstr ""
@ -426,10 +430,10 @@ msgid ""
msgstr "" msgstr ""
msgid "Added" msgid "Added"
msgstr "" msgstr "Añadido"
msgid "Removed" msgid "Removed"
msgstr "" msgstr "Eliminado"
msgid "and" msgid "and"
msgstr "" msgstr ""
@ -477,10 +481,10 @@ msgid "Delete Stored IP Addresses"
msgstr "" msgstr ""
msgid "Time" msgid "Time"
msgstr "" msgstr "Hora"
msgid "Event" msgid "Event"
msgstr "" msgstr "Evento"
msgid "IP address recording can be enabled on the settings page" msgid "IP address recording can be enabled on the settings page"
msgstr "" msgstr ""
@ -492,7 +496,7 @@ msgid "From IP"
msgstr "" msgstr ""
msgid "added" msgid "added"
msgstr "" msgstr "añadido"
msgid "Project private code changed" msgid "Project private code changed"
msgstr "" msgstr ""
@ -519,17 +523,17 @@ msgid "External link changed to"
msgstr "" msgstr ""
msgid "Amount" msgid "Amount"
msgstr "" msgstr "Cantidad"
#, python-format #, python-format
msgid "Amount in %(currency)s" msgid "Amount in %(currency)s"
msgstr "" msgstr "Cantidad en %(currency)s"
msgid "modified" msgid "modified"
msgstr "" msgstr "modificado"
msgid "removed" msgid "removed"
msgstr "" msgstr "eliminado"
msgid "changed in a unknown way" msgid "changed in a unknown way"
msgstr "" msgstr ""
@ -556,57 +560,59 @@ msgid "Simply sharing money with others?"
msgstr "" msgstr ""
msgid "We can help!" msgid "We can help!"
msgstr "" msgstr "Podemos ayudar!"
msgid "Log in to an existing project" msgid "Log in to an existing project"
msgstr "" msgstr "Acceder a un proyecto existente"
msgid "Log in" msgid "Log in"
msgstr "" msgstr ""
msgid "can't remember your password?" msgid "can't remember your password?"
msgstr "" msgstr "¿no recuerdas tu contraseña?"
msgid "Create" msgid "Create"
msgstr "" msgstr "Crear"
msgid "" msgid ""
"Don\\'t reuse a personal password. Choose a private code and send it to " "Don\\'t reuse a personal password. Choose a private code and send it to "
"your friends" "your friends"
msgstr "" msgstr ""
"No reutilizes una contraseña personal. Escoge un código privado i envíalo a "
"tus amigos"
msgid "Account manager" msgid "Account manager"
msgstr "" msgstr ""
msgid "Bills" msgid "Bills"
msgstr "" msgstr "Facturas"
msgid "Settle" msgid "Settle"
msgstr "" msgstr ""
msgid "Statistics" msgid "Statistics"
msgstr "" msgstr "Estadísticas"
msgid "History" msgid "History"
msgstr "" msgstr "Historia"
msgid "Settings" msgid "Settings"
msgstr "" msgstr "Ajustes"
msgid "Languages" msgid "Languages"
msgstr "" msgstr "Idiomas"
msgid "Projects" msgid "Projects"
msgstr "" msgstr "Proyectos"
msgid "Start a new project" msgid "Start a new project"
msgstr "" msgstr ""
msgid "Other projects :" msgid "Other projects :"
msgstr "" msgstr "Otros proyectos :"
msgid "switch to" msgid "switch to"
msgstr "" msgstr "cambiar a"
msgid "Dashboard" msgid "Dashboard"
msgstr "" msgstr ""
@ -615,16 +621,16 @@ msgid "Logout"
msgstr "" msgstr ""
msgid "Code" msgid "Code"
msgstr "" msgstr "Código"
msgid "Mobile Application" msgid "Mobile Application"
msgstr "" msgstr "Aplicación móvil"
msgid "Documentation" msgid "Documentation"
msgstr "" msgstr "Documentación"
msgid "Administation Dashboard" msgid "Administation Dashboard"
msgstr "" msgstr "Panel de administración"
msgid "\"I hate money\" is a free software" msgid "\"I hate money\" is a free software"
msgstr "" msgstr ""
@ -652,7 +658,7 @@ msgid "Older bills"
msgstr "" msgstr ""
msgid "When?" msgid "When?"
msgstr "" msgstr "¿Cuándo?"
msgid "Who paid?" msgid "Who paid?"
msgstr "" msgstr ""
@ -675,19 +681,19 @@ msgid "Everyone but %(excluded)s"
msgstr "" msgstr ""
msgid "No bills" msgid "No bills"
msgstr "" msgstr "Sin facturas"
msgid "Nothing to list yet." msgid "Nothing to list yet."
msgstr "" msgstr "No hay nada que mostrar todavía."
msgid "You probably want to" msgid "You probably want to"
msgstr "" msgstr "Probablemente quieras"
msgid "add a bill" msgid "add a bill"
msgstr "" msgstr "añadir una factura"
msgid "add participants" msgid "add participants"
msgstr "" msgstr "añadir participantes"
msgid "Password reminder" msgid "Password reminder"
msgstr "" msgstr ""
@ -698,19 +704,19 @@ msgid ""
msgstr "" msgstr ""
msgid "Return to home page" msgid "Return to home page"
msgstr "" msgstr "Volver a la página de inicio"
msgid "Your projects" msgid "Your projects"
msgstr "" msgstr "Tus proyectos"
msgid "Reset your password" msgid "Reset your password"
msgstr "" msgstr "Reestablecer tu contraseña"
msgid "Invite people to join this project" msgid "Invite people to join this project"
msgstr "" msgstr ""
msgid "Share Identifier & code" msgid "Share Identifier & code"
msgstr "" msgstr "Compartir identificador i código"
msgid "" msgid ""
"You can share the project identifier and the private code by any " "You can share the project identifier and the private code by any "
@ -718,10 +724,10 @@ msgid ""
msgstr "" msgstr ""
msgid "Identifier:" msgid "Identifier:"
msgstr "" msgstr "Identificador:"
msgid "Share the Link" msgid "Share the Link"
msgstr "" msgstr "Compartir el enlace"
msgid "You can directly share the following link via your prefered medium" msgid "You can directly share the following link via your prefered medium"
msgstr "" msgstr ""
@ -740,28 +746,28 @@ msgid "Who pays?"
msgstr "" msgstr ""
msgid "To whom?" msgid "To whom?"
msgstr "" msgstr "¿Para quién?"
msgid "Who?" msgid "Who?"
msgstr "" msgstr "¿Quién?"
msgid "Balance" msgid "Balance"
msgstr "" msgstr ""
msgid "deactivate" msgid "deactivate"
msgstr "" msgstr "desactivar"
msgid "reactivate" msgid "reactivate"
msgstr "" msgstr "reactivar"
msgid "Paid" msgid "Paid"
msgstr "" msgstr "Pagado"
msgid "Spent" msgid "Spent"
msgstr "" msgstr "Gastado"
msgid "Expenses by Month" msgid "Expenses by Month"
msgstr "" msgstr "Gastos por mes"
msgid "Period" msgid "Period"
msgstr "" msgstr "Período"

View file

@ -3,8 +3,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-05-30 21:50+0200\n" "POT-Creation-Date: 2020-05-30 21:50+0200\n"
"PO-Revision-Date: 2020-08-30 19:59+0000\n" "PO-Revision-Date: 2020-11-11 16:28+0000\n"
"Last-Translator: Miguel Victoria Villaquiran <miguelvicvil@gmail.com>\n" "Last-Translator: Puyma <puyma@amyup.xyz>\n"
"Language-Team: Spanish (Latin America) <https://hosted.weblate.org/projects/" "Language-Team: Spanish (Latin America) <https://hosted.weblate.org/projects/"
"i-hate-money/i-hate-money/es_419/>\n" "i-hate-money/i-hate-money/es_419/>\n"
"Language: es_419\n" "Language: es_419\n"
@ -12,7 +12,7 @@ msgstr ""
"Content-Type: text/plain; charset=utf-8\n" "Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n" "Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.2.1-dev\n" "X-Generator: Weblate 4.4-dev\n"
"Generated-By: Babel 2.8.0\n" "Generated-By: Babel 2.8.0\n"
msgid "" msgid ""
@ -38,7 +38,7 @@ msgid "Use IP tracking for project history"
msgstr "Registrar la IPs para el historial del proyecto" msgstr "Registrar la IPs para el historial del proyecto"
msgid "Default Currency" msgid "Default Currency"
msgstr "moneda predeterminada" msgstr "Moneda por defecto"
msgid "Import previously exported JSON file" msgid "Import previously exported JSON file"
msgstr "Importar archivo JSON previamente exportado" msgstr "Importar archivo JSON previamente exportado"
@ -61,10 +61,10 @@ msgstr ""
"favor, elija un nuevo identificador" "favor, elija un nuevo identificador"
msgid "Get in" msgid "Get in"
msgstr "Entra" msgstr "Entrar"
msgid "Admin password" msgid "Admin password"
msgstr "Clave de Administrador" msgstr "Contraseña de Administrador"
msgid "Send me the code by email" msgid "Send me the code by email"
msgstr "Envíame el código por correo electrónico" msgstr "Envíame el código por correo electrónico"
@ -103,7 +103,7 @@ msgid "External link"
msgstr "Enlace externo" msgstr "Enlace externo"
msgid "A link to an external document, related to this bill" msgid "A link to an external document, related to this bill"
msgstr "Un enlace a un documento externo relaccionado con esta factura" msgstr "Un enlace a un documento externo relacionado con esta factura"
msgid "For whom?" msgid "For whom?"
msgstr "¿Para quién?" msgstr "¿Para quién?"

View file

@ -3,8 +3,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-05-30 21:50+0200\n" "POT-Creation-Date: 2020-05-30 21:50+0200\n"
"PO-Revision-Date: 2020-09-26 09:40+0000\n" "PO-Revision-Date: 2021-02-21 04:50+0000\n"
"Last-Translator: Fakhruddin Ahmad Darwis <fakhri.satu@gmail.com>\n" "Last-Translator: Reza Almanda <rezaalmanda27@gmail.com>\n"
"Language-Team: Indonesian <https://hosted.weblate.org/projects/i-hate-money/" "Language-Team: Indonesian <https://hosted.weblate.org/projects/i-hate-money/"
"i-hate-money/id/>\n" "i-hate-money/id/>\n"
"Language: id\n" "Language: id\n"
@ -12,14 +12,14 @@ msgstr ""
"Content-Type: text/plain; charset=utf-8\n" "Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n" "Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 4.3-dev\n" "X-Generator: Weblate 4.5\n"
"Generated-By: Babel 2.8.0\n" "Generated-By: Babel 2.8.0\n"
msgid "" msgid ""
"Not a valid amount or expression. Only numbers and + - * / operators are " "Not a valid amount or expression. Only numbers and + - * / operators are "
"accepted." "accepted."
msgstr "" msgstr ""
"Jumlah atau operator tidak valid. Hanya angka dan opertaor +-* yang " "Bukan jumlah atau operator yang valid. Hanya angka dan operator + -* / yang "
"diterima." "diterima."
msgid "Project name" msgid "Project name"
@ -116,7 +116,7 @@ msgstr "Ajukan dan tambah yang baru"
#, python-format #, python-format
msgid "Project default: %(currency)s" msgid "Project default: %(currency)s"
msgstr "" msgstr "Default proyek: %(currency)s"
msgid "Bills can't be null" msgid "Bills can't be null"
msgstr "Tagihan tidak boleh kosong" msgstr "Tagihan tidak boleh kosong"
@ -150,16 +150,16 @@ msgid "The email %(email)s is not valid"
msgstr "Surel %(email)s tidak valid" msgstr "Surel %(email)s tidak valid"
msgid "Participant" msgid "Participant"
msgstr "" msgstr "Partisipan"
msgid "Bill" msgid "Bill"
msgstr "" msgstr "Tagihan"
msgid "Project" msgid "Project"
msgstr "Proyek" msgstr "Proyek"
msgid "No Currency" msgid "No Currency"
msgstr "" msgstr "Tidak ada mata uang"
msgid "Too many failed login attempts, please retry later." msgid "Too many failed login attempts, please retry later."
msgstr "Terlalu banyak percobaan masuk, silakan coba lagi nanti." msgstr "Terlalu banyak percobaan masuk, silakan coba lagi nanti."
@ -179,22 +179,26 @@ msgid "You have just created '%(project)s' to share your expenses"
msgstr "Anda baru saja membuat %(project)s untuk membagikan harga Anda" msgstr "Anda baru saja membuat %(project)s untuk membagikan harga Anda"
msgid "A reminder email has just been sent to you" msgid "A reminder email has just been sent to you"
msgstr "" msgstr "Email pengingat baru saja dikirimkan kepada Anda"
msgid "" msgid ""
"We tried to send you an reminder email, but there was an error. You can " "We tried to send you an reminder email, but there was an error. You can "
"still use the project normally." "still use the project normally."
msgstr "" msgstr ""
"Kami telah mengirimi Anda email pengingat, tetapi ada kesalahan. Anda masih "
"dapat menggunakan proyek secara normal."
#, python-format #, python-format
msgid "The project identifier is %(project)s" msgid "The project identifier is %(project)s"
msgstr "" msgstr "Pengidentifikasi proyek adalah %(project)s"
msgid "" msgid ""
"Sorry, there was an error while sending you an email with password reset " "Sorry, there was an error while sending you an email with password reset "
"instructions. Please check the email configuration of the server or " "instructions. Please check the email configuration of the server or "
"contact the administrator." "contact the administrator."
msgstr "" msgstr ""
"Maaf, ada galat saat mengirim email berisi instruksi pengaturan ulang kata "
"sandi. Silakan periksa konfigurasi email peladen atau hubungi administrator."
msgid "No token provided" msgid "No token provided"
msgstr "Belum ada token diberikan" msgstr "Belum ada token diberikan"
@ -209,10 +213,10 @@ msgid "Password successfully reset."
msgstr "Kata sandi berhasil diatur ulang." msgstr "Kata sandi berhasil diatur ulang."
msgid "Project successfully uploaded" msgid "Project successfully uploaded"
msgstr "" msgstr "Proyek berhasil diunggah"
msgid "Invalid JSON" msgid "Invalid JSON"
msgstr "" msgstr "JSON tidak valid"
msgid "Project successfully deleted" msgid "Project successfully deleted"
msgstr "Proyek berhasil dihapus" msgstr "Proyek berhasil dihapus"
@ -229,10 +233,12 @@ msgid ""
"Please check the email configuration of the server or contact the " "Please check the email configuration of the server or contact the "
"administrator." "administrator."
msgstr "" msgstr ""
"Maaf, ada galat saat mencoba mengirim email undangan. Silakan periksa "
"konfigurasi email peladen atau hubungi administrator."
#, python-format #, python-format
msgid "%(member)s has been added" msgid "%(member)s has been added"
msgstr "" msgstr "%(member)s telah ditambahkan"
#, python-format #, python-format
msgid "%(name)s is part of this project again" msgid "%(name)s is part of this project again"
@ -309,7 +315,7 @@ msgid "delete"
msgstr "hapus" msgstr "hapus"
msgid "show" msgid "show"
msgstr "" msgstr "tampilkan"
msgid "The Dashboard is currently deactivated." msgid "The Dashboard is currently deactivated."
msgstr "Dasbor sekarang ini sedang dinonaktifkan." msgstr "Dasbor sekarang ini sedang dinonaktifkan."
@ -321,10 +327,10 @@ msgid "Edit project"
msgstr "Ubah proyek" msgstr "Ubah proyek"
msgid "Import JSON" msgid "Import JSON"
msgstr "" msgstr "Impor JSON"
msgid "Choose file" msgid "Choose file"
msgstr "" msgstr "Pilih berkas"
msgid "Download project's data" msgid "Download project's data"
msgstr "Unduh data proyek" msgstr "Unduh data proyek"
@ -348,7 +354,7 @@ msgid "Cancel"
msgstr "Batalkan" msgstr "Batalkan"
msgid "Privacy Settings" msgid "Privacy Settings"
msgstr "" msgstr "Pengaturan Privasi"
msgid "Edit the project" msgid "Edit the project"
msgstr "Ubah proyek" msgstr "Ubah proyek"
@ -381,16 +387,16 @@ msgid "Download"
msgstr "Unduh" msgstr "Unduh"
msgid "Disabled Project History" msgid "Disabled Project History"
msgstr "" msgstr "Nonaktifkan riwayat proyek"
msgid "Disabled Project History & IP Address Recording" msgid "Disabled Project History & IP Address Recording"
msgstr "" msgstr "Nonaktifkan riwayat proyek & rekaman alamat IP"
msgid "Enabled Project History" msgid "Enabled Project History"
msgstr "" msgstr "Riwayat Proyek yang Diaktifkan"
msgid "Disabled IP Address Recording" msgid "Disabled IP Address Recording"
msgstr "" msgstr "Perekaman Alamat IP Dinonaktifkan"
msgid "Enabled Project History & IP Address Recording" msgid "Enabled Project History & IP Address Recording"
msgstr "" msgstr ""
@ -399,19 +405,19 @@ msgid "Enabled IP Address Recording"
msgstr "" msgstr ""
msgid "History Settings Changed" msgid "History Settings Changed"
msgstr "" msgstr "Setelan Riwayat Diubah"
msgid "changed" msgid "changed"
msgstr "" msgstr "diubah"
msgid "from" msgid "from"
msgstr "" msgstr "dari"
msgid "to" msgid "to"
msgstr "" msgstr "ke"
msgid "Confirm Remove IP Adresses" msgid "Confirm Remove IP Adresses"
msgstr "" msgstr "Konfirmasi Hapus Alamat IP"
msgid "" msgid ""
"Are you sure you want to delete all recorded IP addresses from this " "Are you sure you want to delete all recorded IP addresses from this "
@ -419,32 +425,37 @@ msgid ""
" The rest of the project history will be unaffected. This " " The rest of the project history will be unaffected. This "
"action cannot be undone." "action cannot be undone."
msgstr "" msgstr ""
"Anda yakin ingin menghapus semua alamat IP yang direkam dari proyek ini?\n"
" Sisa riwayat proyek tidak akan terpengaruh. Tindakan ini "
"tidak bisa dibatalkan."
msgid "Close" msgid "Close"
msgstr "" msgstr "Tutup"
msgid "Confirm Delete" msgid "Confirm Delete"
msgstr "" msgstr "Konfirmasi Hapus"
msgid "Delete Confirmation" msgid "Delete Confirmation"
msgstr "" msgstr "Hapus Konfirmasi"
msgid "" msgid ""
"Are you sure you want to erase all history for this project? This action " "Are you sure you want to erase all history for this project? This action "
"cannot be undone." "cannot be undone."
msgstr "" msgstr ""
"Anda yakin ingin menghapus semua riwayat untuk proyek ini? Tindakan ini "
"tidak bisa dibatalkan."
msgid "Added" msgid "Added"
msgstr "" msgstr "Ditambahkan"
msgid "Removed" msgid "Removed"
msgstr "" msgstr "Dihapus"
msgid "and" msgid "and"
msgstr "" msgstr "dan"
msgid "owers list" msgid "owers list"
msgstr "" msgstr "daftar owers"
#, python-format #, python-format
msgid "" msgid ""
@ -454,6 +465,12 @@ msgid ""
" <a href=\"%(url)s\">settings page</a>\n" " <a href=\"%(url)s\">settings page</a>\n"
" " " "
msgstr "" msgstr ""
"\n"
" <i> Proyek ini memiliki riwayat yang dinonaktifkan. Tindakan "
"baru tidak akan muncul di bawah ini. Anda dapat mengaktifkan riwayat pada</i>"
"\n"
" <a href=\"%(url)s\">halaman pengaturan</a>\n"
" "
msgid "" msgid ""
"\n" "\n"

View file

@ -3,8 +3,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-05-30 21:50+0200\n" "POT-Creation-Date: 2020-05-30 21:50+0200\n"
"PO-Revision-Date: 2020-10-26 19:27+0000\n" "PO-Revision-Date: 2021-03-17 15:18+0000\n"
"Last-Translator: Sylphystia <yuidirnt@gmail.com>\n" "Last-Translator: TomSolGit <Tommaso.solfa@gmail.com>\n"
"Language-Team: Italian <https://hosted.weblate.org/projects/i-hate-money/" "Language-Team: Italian <https://hosted.weblate.org/projects/i-hate-money/"
"i-hate-money/it/>\n" "i-hate-money/it/>\n"
"Language: it\n" "Language: it\n"
@ -12,7 +12,7 @@ msgstr ""
"Content-Type: text/plain; charset=utf-8\n" "Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n" "Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.3.2-dev\n" "X-Generator: Weblate 4.5.2-dev\n"
"Generated-By: Babel 2.8.0\n" "Generated-By: Babel 2.8.0\n"
msgid "" msgid ""
@ -40,7 +40,7 @@ msgid "Default Currency"
msgstr "Valuta predefinita" msgstr "Valuta predefinita"
msgid "Import previously exported JSON file" msgid "Import previously exported JSON file"
msgstr "Importare il file esportato JSON" msgstr "Importare il file JSON esportato precedentemente"
msgid "Import" msgid "Import"
msgstr "Importare" msgstr "Importare"
@ -49,7 +49,7 @@ msgid "Project identifier"
msgstr "Identificatore del progetto" msgstr "Identificatore del progetto"
msgid "Create the project" msgid "Create the project"
msgstr "Creare il progetto" msgstr "Crea il progetto"
#, python-format #, python-format
msgid "" msgid ""
@ -57,13 +57,13 @@ msgid ""
"choose a new identifier" "choose a new identifier"
msgstr "" msgstr ""
"Un progetto con questo identificatore (\"%(project)s\") esiste già. Per " "Un progetto con questo identificatore (\"%(project)s\") esiste già. Per "
"favore scegliere un identificatore nuovo" "favore scegli un identificatore nuovo"
msgid "Get in" msgid "Get in"
msgstr "Entra" msgstr "Entra"
msgid "Admin password" msgid "Admin password"
msgstr "Password di amministrazione" msgstr "Password dell'amministratore"
msgid "Send me the code by email" msgid "Send me the code by email"
msgstr "Inviami il codice per email" msgstr "Inviami il codice per email"
@ -102,7 +102,7 @@ msgid "External link"
msgstr "Link esterno" msgstr "Link esterno"
msgid "A link to an external document, related to this bill" msgid "A link to an external document, related to this bill"
msgstr "Un link a un documento esterno in relazione a questa spesa" msgstr "Un link ad un documento esterno, relativo a questa spesa"
msgid "For whom?" msgid "For whom?"
msgstr "Per chi?" msgstr "Per chi?"
@ -111,7 +111,7 @@ msgid "Submit"
msgstr "Invia" msgstr "Invia"
msgid "Submit and add a new one" msgid "Submit and add a new one"
msgstr "Inviare e aggiungerne uno nuovo" msgstr "Invia ed aggiungine uno nuovo"
#, python-format #, python-format
msgid "Project default: %(currency)s" msgid "Project default: %(currency)s"
@ -124,7 +124,7 @@ msgid "Name"
msgstr "Nome" msgstr "Nome"
msgid "Weights should be positive" msgid "Weights should be positive"
msgstr "Il peso deve essere positivo" msgstr "I pesi dovrebbero essere positivi"
msgid "Weight" msgid "Weight"
msgstr "Peso" msgstr "Peso"
@ -139,10 +139,10 @@ msgid "This project already have this member"
msgstr "Membro già presente in questo progetto" msgstr "Membro già presente in questo progetto"
msgid "People to notify" msgid "People to notify"
msgstr "Persone da notificare" msgstr "Persone da informare"
msgid "Send invites" msgid "Send invites"
msgstr "Spedisci inviti" msgstr "Invia inviti"
#, python-format #, python-format
msgid "The email %(email)s is not valid" msgid "The email %(email)s is not valid"
@ -166,8 +166,8 @@ msgstr "Troppi tentativi di accesso non riusciti. Riprova più tardi."
#, python-format #, python-format
msgid "This admin password is not the right one. Only %(num)d attempts left." msgid "This admin password is not the right one. Only %(num)d attempts left."
msgstr "" msgstr ""
"Questa password di amministrazione non è corretta. Solo %(num)d tentativi" "Questa password di amministrazione non è quella corretta. Solo %(num)d "
" rimasti." "tentativi rimasti."
msgid "You either provided a bad token or no project identifier." msgid "You either provided a bad token or no project identifier."
msgstr "" msgstr ""

View file

@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-10-22 06:40+0200\n" "POT-Creation-Date: 2020-10-22 06:40+0200\n"
"PO-Revision-Date: 2020-11-04 17:27+0000\n" "PO-Revision-Date: 2020-11-11 16:28+0000\n"
"Last-Translator: Jwen921 <yangjingwen0921@gmail.com>\n" "Last-Translator: Jwen921 <yangjingwen0921@gmail.com>\n"
"Language-Team: Japanese <https://hosted.weblate.org/projects/i-hate-money/" "Language-Team: Japanese <https://hosted.weblate.org/projects/i-hate-money/"
"i-hate-money/ja/>\n" "i-hate-money/ja/>\n"
@ -12,7 +12,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n" "Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 4.3.2-dev\n" "X-Generator: Weblate 4.4-dev\n"
msgid "" msgid ""
"Not a valid amount or expression. Only numbers and + - * / operators are " "Not a valid amount or expression. Only numbers and + - * / operators are "
@ -129,282 +129,283 @@ msgid "Add"
msgstr "追加" msgstr "追加"
msgid "User name incorrect" msgid "User name incorrect"
msgstr "" msgstr "ユーザー名が正しくない"
msgid "This project already have this member" msgid "This project already have this member"
msgstr "" msgstr "プロジェクトはすでにこのメンバーを含めています"
msgid "People to notify" msgid "People to notify"
msgstr "" msgstr "知らせたい人"
msgid "Send invites" msgid "Send invites"
msgstr "" msgstr "招待状を出す"
#, python-format #, python-format
msgid "The email %(email)s is not valid" msgid "The email %(email)s is not valid"
msgstr "" msgstr "メールアドレス%(email)sは無効"
msgid "Participant" msgid "Participant"
msgstr "" msgstr "参加者"
msgid "Bill" msgid "Bill"
msgstr "" msgstr "明細"
msgid "Project" msgid "Project"
msgstr "" msgstr "プロジェクト"
msgid "No Currency" msgid "No Currency"
msgstr "" msgstr "通貨なし"
msgid "Too many failed login attempts, please retry later." msgid "Too many failed login attempts, please retry later."
msgstr "" msgstr "何度もログインに失敗したので、時間をおいてから再度ログインして下さい。"
#, python-format #, python-format
msgid "This admin password is not the right one. Only %(num)d attempts left." msgid "This admin password is not the right one. Only %(num)d attempts left."
msgstr "" msgstr "この管理者パスワードは正しくないでウ。%(num)d試行しか残っていません。"
msgid "You either provided a bad token or no project identifier." msgid "You either provided a bad token or no project identifier."
msgstr "" msgstr "無効な入力かプロジェクト名なし。"
msgid "This private code is not the right one" msgid "This private code is not the right one"
msgstr "" msgstr "私用コードは正しくない"
#, python-format #, python-format
msgid "You have just created '%(project)s' to share your expenses" msgid "You have just created '%(project)s' to share your expenses"
msgstr "" msgstr "費用を共有するため、%(project)sが作られました"
msgid "A reminder email has just been sent to you" msgid "A reminder email has just been sent to you"
msgstr "" msgstr "催促メールがただいまあなたに送りました"
msgid "" msgid ""
"We tried to send you an reminder email, but there was an error. You can " "We tried to send you an reminder email, but there was an error. You can "
"still use the project normally." "still use the project normally."
msgstr "" msgstr "催促メールを送った時、エラーが発生しました。このプロジェクトはまだ使えます。"
#, python-format #, python-format
msgid "The project identifier is %(project)s" msgid "The project identifier is %(project)s"
msgstr "" msgstr "プロジェクト名は%(project)s"
msgid "" msgid ""
"Sorry, there was an error while sending you an email with password reset " "Sorry, there was an error while sending you an email with password reset "
"instructions. Please check the email configuration of the server or " "instructions. Please check the email configuration of the server or "
"contact the administrator." "contact the administrator."
msgstr "" msgstr ""
"申し訳ございませんが、パスワード再設定の説明メールを送った時、エラーが発生しました。メールアドレスを一度確認してまたは管理者に連絡してください。"
msgid "No token provided" msgid "No token provided"
msgstr "" msgstr "印が入力されていない"
msgid "Invalid token" msgid "Invalid token"
msgstr "" msgstr "無効な印"
msgid "Unknown project" msgid "Unknown project"
msgstr "" msgstr "未知のプロジェクト"
msgid "Password successfully reset." msgid "Password successfully reset."
msgstr "" msgstr "パスワードを再設定できました。"
msgid "Project successfully uploaded" msgid "Project successfully uploaded"
msgstr "" msgstr "プロジェクトをアップロードできました"
msgid "Invalid JSON" msgid "Invalid JSON"
msgstr "" msgstr "無効なJSON"
msgid "Project successfully deleted" msgid "Project successfully deleted"
msgstr "" msgstr "プロジェクトを削除できました"
#, python-format #, python-format
msgid "You have been invited to share your expenses for %(project)s" msgid "You have been invited to share your expenses for %(project)s"
msgstr "" msgstr "%(project)sの費用を共有すると、あなたが誘われた"
msgid "Your invitations have been sent" msgid "Your invitations have been sent"
msgstr "" msgstr "あなたの招待状が送られました"
msgid "" msgid ""
"Sorry, there was an error while trying to send the invitation emails. " "Sorry, there was an error while trying to send the invitation emails. "
"Please check the email configuration of the server or contact the " "Please check the email configuration of the server or contact the "
"administrator." "administrator."
msgstr "" msgstr "申し訳ございませんが、招待メールを送ったとき、エラーが発生しました。メールアドレスを再度チェックするかまたは管理者に連絡ください。"
#, python-format #, python-format
msgid "%(member)s has been added" msgid "%(member)s has been added"
msgstr "" msgstr "%(member)sが追加されました"
#, python-format #, python-format
msgid "%(name)s is part of this project again" msgid "%(name)s is part of this project again"
msgstr "" msgstr "%(name)sはまたこのプロジェクトの一部になりました"
#, python-format #, python-format
msgid "" msgid ""
"User '%(name)s' has been deactivated. It will still appear in the users " "User '%(name)s' has been deactivated. It will still appear in the users "
"list until its balance becomes zero." "list until its balance becomes zero."
msgstr "" msgstr "ユーザー%(name)sが無効にされました。このユーザーは残高がゼロになるまでリストに出ています。"
#, python-format #, python-format
msgid "User '%(name)s' has been removed" msgid "User '%(name)s' has been removed"
msgstr "" msgstr "ユーザー%(name)sが既に取り除かれました"
#, python-format #, python-format
msgid "User '%(name)s' has been edited" msgid "User '%(name)s' has been edited"
msgstr "" msgstr "ユーザー%(name)sが既に編集されました"
msgid "The bill has been added" msgid "The bill has been added"
msgstr "" msgstr "明細が追加されました"
msgid "The bill has been deleted" msgid "The bill has been deleted"
msgstr "" msgstr "明細が削除されました"
msgid "The bill has been modified" msgid "The bill has been modified"
msgstr "" msgstr "明細が変更されました"
msgid "Sorry, we were unable to find the page you've asked for." msgid "Sorry, we were unable to find the page you've asked for."
msgstr "" msgstr "申し訳ございませんが、求められるページが見つかりませんでした。"
msgid "The best thing to do is probably to get back to the main page." msgid "The best thing to do is probably to get back to the main page."
msgstr "" msgstr "メーンページに戻ったほうがいいかもしれません。"
msgid "Back to the list" msgid "Back to the list"
msgstr "" msgstr "リストに戻る"
msgid "Administration tasks are currently disabled." msgid "Administration tasks are currently disabled."
msgstr "" msgstr "現在管理者タスクが操作できません。"
msgid "The project you are trying to access do not exist, do you want to" msgid "The project you are trying to access do not exist, do you want to"
msgstr "" msgstr "アクセスしたいプロジェクトは存在していなくて、…したいですか"
msgid "create it" msgid "create it"
msgstr "" msgstr "作る"
msgid "?" msgid "?"
msgstr "" msgstr ""
msgid "Create a new project" msgid "Create a new project"
msgstr "" msgstr "プロジェクトを新規作成する"
msgid "Number of members" msgid "Number of members"
msgstr "" msgstr "メンバー数"
msgid "Number of bills" msgid "Number of bills"
msgstr "" msgstr "明細書数"
msgid "Newest bill" msgid "Newest bill"
msgstr "" msgstr "最新の明細"
msgid "Oldest bill" msgid "Oldest bill"
msgstr "" msgstr "一番古い明細"
msgid "Actions" msgid "Actions"
msgstr "" msgstr "操作"
msgid "edit" msgid "edit"
msgstr "" msgstr "編集"
msgid "delete" msgid "delete"
msgstr "" msgstr "削除"
msgid "show" msgid "show"
msgstr "" msgstr "表示"
msgid "The Dashboard is currently deactivated." msgid "The Dashboard is currently deactivated."
msgstr "" msgstr "現在ダッシュボードは操作できません。"
msgid "you sure?" msgid "you sure?"
msgstr "" msgstr "確認?"
msgid "Edit project" msgid "Edit project"
msgstr "" msgstr "プロジェクトを編集する"
msgid "Import JSON" msgid "Import JSON"
msgstr "" msgstr "JSONを導入する"
msgid "Choose file" msgid "Choose file"
msgstr "" msgstr "ファイルを選択する"
msgid "Download project's data" msgid "Download project's data"
msgstr "" msgstr "プロジェクトのデータをダウンロードする"
msgid "Bill items" msgid "Bill items"
msgstr "" msgstr "明細項目"
msgid "Download the list of bills with owner, amount, reason,... " msgid "Download the list of bills with owner, amount, reason,... "
msgstr "" msgstr "所有者や金額、理由などの情報を含む明細リストをダウンロードする "
msgid "Settle plans" msgid "Settle plans"
msgstr "" msgstr "解決計画"
msgid "Download the list of transactions needed to settle the current bills." msgid "Download the list of transactions needed to settle the current bills."
msgstr "" msgstr "この明細に必要な取引リストをダウンロードします。"
msgid "Can't remember the password?" msgid "Can't remember the password?"
msgstr "" msgstr "パスワードを覚えていないです?"
msgid "Cancel" msgid "Cancel"
msgstr "" msgstr "キャンセル"
msgid "Privacy Settings" msgid "Privacy Settings"
msgstr "" msgstr "プライバシーの設定"
msgid "Edit the project" msgid "Edit the project"
msgstr "" msgstr "プロジェクトを編集する"
msgid "Edit this bill" msgid "Edit this bill"
msgstr "" msgstr "明細を編集する"
msgid "Add a bill" msgid "Add a bill"
msgstr "" msgstr "新しい明細書を追加する"
msgid "Select all" msgid "Select all"
msgstr "" msgstr "全て選択"
msgid "Select none" msgid "Select none"
msgstr "" msgstr "選択解除"
msgid "Add participant" msgid "Add participant"
msgstr "" msgstr "参加者を追加する"
msgid "Edit this member" msgid "Edit this member"
msgstr "" msgstr "このメンバーを編集する"
msgid "john.doe@example.com, mary.moe@site.com" msgid "john.doe@example.com, mary.moe@site.com"
msgstr "" msgstr "john.doe@example.com, mary.moe@site.com"
msgid "Send the invitations" msgid "Send the invitations"
msgstr "" msgstr "招待状を送る"
msgid "Download" msgid "Download"
msgstr "" msgstr "ダウンロードする"
msgid "Disabled Project History" msgid "Disabled Project History"
msgstr "" msgstr "操作できないプロジェクト歴史"
msgid "Disabled Project History & IP Address Recording" msgid "Disabled Project History & IP Address Recording"
msgstr "" msgstr "操作できないプロジェクトの歴史IPアドレス記録"
msgid "Enabled Project History" msgid "Enabled Project History"
msgstr "" msgstr "操作可能なプロジェクト歴史"
msgid "Disabled IP Address Recording" msgid "Disabled IP Address Recording"
msgstr "" msgstr "操作できないIPアドレス記録"
msgid "Enabled Project History & IP Address Recording" msgid "Enabled Project History & IP Address Recording"
msgstr "" msgstr "操作可能なプロジェクト歴史IPアドレス記録"
msgid "Enabled IP Address Recording" msgid "Enabled IP Address Recording"
msgstr "" msgstr "操作可能なIPアドレス記録"
msgid "History Settings Changed" msgid "History Settings Changed"
msgstr "" msgstr "歴史設定が変更された"
msgid "changed" msgid "changed"
msgstr "" msgstr "変更された"
msgid "from" msgid "from"
msgstr "" msgstr "から"
msgid "to" msgid "to"
msgstr "" msgstr "まで"
msgid "Confirm Remove IP Adresses" msgid "Confirm Remove IP Adresses"
msgstr "" msgstr "IPアドレスの取り除きを確認する"
msgid "" msgid ""
"Are you sure you want to delete all recorded IP addresses from this " "Are you sure you want to delete all recorded IP addresses from this "
@ -412,32 +413,34 @@ msgid ""
" The rest of the project history will be unaffected. This " " The rest of the project history will be unaffected. This "
"action cannot be undone." "action cannot be undone."
msgstr "" msgstr ""
"本当にこのプロジェクトから記録されたIPアドレスを全部削除したいですか。\n"
"残りのプロジェクト歴史が影響されていません。この操作は元に戻せません。"
msgid "Close" msgid "Close"
msgstr "" msgstr "閉じる"
msgid "Confirm Delete" msgid "Confirm Delete"
msgstr "" msgstr "削除を確認する"
msgid "Delete Confirmation" msgid "Delete Confirmation"
msgstr "" msgstr "確認を削除する"
msgid "" msgid ""
"Are you sure you want to erase all history for this project? This action " "Are you sure you want to erase all history for this project? This action "
"cannot be undone." "cannot be undone."
msgstr "" msgstr "本当にこのプロジェクトの歴史をすべて消しますか。この操作は元に戻せません。"
msgid "Added" msgid "Added"
msgstr "" msgstr "追加された"
msgid "Removed" msgid "Removed"
msgstr "" msgstr "取り除かれた"
msgid "and" msgid "and"
msgstr "" msgstr ""
msgid "owers list" msgid "owers list"
msgstr "" msgstr "全員リスト"
#, python-format #, python-format
msgid "" msgid ""
@ -447,6 +450,10 @@ msgid ""
" <a href=\"%(url)s\">settings page</a>\n" " <a href=\"%(url)s\">settings page</a>\n"
" " " "
msgstr "" msgstr ""
"\n"
" <i>このプロジェクトの歴史は操作できません。新しい操作は下に出ません。</i>で歴史を操作可能にすることができます。\n"
"<a href=\"%(url)s\">設定ページ</a>\n"
" "
msgid "" msgid ""
"\n" "\n"
@ -457,279 +464,284 @@ msgid ""
"them.</i></p>\n" "them.</i></p>\n"
" " " "
msgstr "" msgstr ""
"\n"
" <i>下の表では操作不可になる前に、プロジェクトの歴史に対して記録された操作が反映されています。…できます。\n"
"<a href=\"#\" data-toggle=\"modal\" data-keyboard=\"false\" data-target"
"=\"#confirm-erase\">プロジェクトの歴史を取り除く</a> で取り除きます.</i></p>\n"
" "
msgid "" msgid ""
"Some entries below contain IP addresses, even though this project has IP " "Some entries below contain IP addresses, even though this project has IP "
"recording disabled. " "recording disabled. "
msgstr "" msgstr "このプロジェクトのIP記録が操作できない一方、下の入り口の一部がIPアドレスを含めています。 "
msgid "Delete stored IP addresses" msgid "Delete stored IP addresses"
msgstr "" msgstr "保存されたIPアドレスを削除する"
msgid "No history to erase" msgid "No history to erase"
msgstr "" msgstr "削除できる歴史はない"
msgid "Clear Project History" msgid "Clear Project History"
msgstr "" msgstr "プロジェクトの歴史を取り除く"
msgid "No IP Addresses to erase" msgid "No IP Addresses to erase"
msgstr "" msgstr "削除できるIPアドレスはない"
msgid "Delete Stored IP Addresses" msgid "Delete Stored IP Addresses"
msgstr "" msgstr "保存されたIPアドレスを削除する"
msgid "Time" msgid "Time"
msgstr "" msgstr "時間"
msgid "Event" msgid "Event"
msgstr "" msgstr "イベント"
msgid "IP address recording can be enabled on the settings page" msgid "IP address recording can be enabled on the settings page"
msgstr "" msgstr "設定ページでIPアドレス記録を編集可能にすることができる"
msgid "IP address recording can be disabled on the settings page" msgid "IP address recording can be disabled on the settings page"
msgstr "" msgstr "設定ページでIPアドレス記録を編集不可にすることができる"
msgid "From IP" msgid "From IP"
msgstr "" msgstr "IPから"
msgid "added" msgid "added"
msgstr "" msgstr "追加された"
msgid "Project private code changed" msgid "Project private code changed"
msgstr "" msgstr "プロジェクトの私用コードが変更された"
msgid "Project renamed to" msgid "Project renamed to"
msgstr "" msgstr "プロジェクト名が…に変更された"
msgid "Project contact email changed to" msgid "Project contact email changed to"
msgstr "" msgstr "プロジェクトの連絡メールが…に変更された"
msgid "Project settings modified" msgid "Project settings modified"
msgstr "" msgstr "プロジェクトの設定が修正された"
msgid "deactivated" msgid "deactivated"
msgstr "" msgstr "操作不可にされた"
msgid "reactivated" msgid "reactivated"
msgstr "" msgstr "再び変更可能にされた"
msgid "renamed to" msgid "renamed to"
msgstr "" msgstr "…という名前に変更された"
msgid "External link changed to" msgid "External link changed to"
msgstr "" msgstr "外部リンクが…に変更された"
msgid "Amount" msgid "Amount"
msgstr "" msgstr "金額"
#, python-format #, python-format
msgid "Amount in %(currency)s" msgid "Amount in %(currency)s"
msgstr "" msgstr "%(currency)sでの金額"
msgid "modified" msgid "modified"
msgstr "" msgstr "修正された"
msgid "removed" msgid "removed"
msgstr "" msgstr "取り除かれた"
msgid "changed in a unknown way" msgid "changed in a unknown way"
msgstr "" msgstr "未知の方法で変更された"
msgid "Nothing to list" msgid "Nothing to list"
msgstr "" msgstr "表示できるものがない"
msgid "Someone probably cleared the project history." msgid "Someone probably cleared the project history."
msgstr "" msgstr "プロジェクトの歴史が誰かに取り除かれたかもしれません。"
msgid "Manage your shared <br />expenses, easily" msgid "Manage your shared <br />expenses, easily"
msgstr "" msgstr "あなたが共有した<br />費用を簡単に管理する"
msgid "Try out the demo" msgid "Try out the demo"
msgstr "" msgstr "デモを試す"
msgid "You're sharing a house?" msgid "You're sharing a house?"
msgstr "" msgstr "誰かと家を共有していますか?"
msgid "Going on holidays with friends?" msgid "Going on holidays with friends?"
msgstr "" msgstr "友達と旅行していますか?"
msgid "Simply sharing money with others?" msgid "Simply sharing money with others?"
msgstr "" msgstr "ただ他人とお金を割り当てていますか?"
msgid "We can help!" msgid "We can help!"
msgstr "" msgstr "助けてあげます!"
msgid "Log in to an existing project" msgid "Log in to an existing project"
msgstr "" msgstr "存在するプロジェクトにログインする"
msgid "Log in" msgid "Log in"
msgstr "" msgstr "ログイン"
msgid "can't remember your password?" msgid "can't remember your password?"
msgstr "" msgstr "パスワードを忘れた?"
msgid "Create" msgid "Create"
msgstr "" msgstr "作る"
msgid "" msgid ""
"Don\\'t reuse a personal password. Choose a private code and send it to " "Don\\'t reuse a personal password. Choose a private code and send it to "
"your friends" "your friends"
msgstr "" msgstr "自分のパスワードを再利用しないでください。私用コードを選んで、友達に送る"
msgid "Account manager" msgid "Account manager"
msgstr "" msgstr "アカウント管理者"
msgid "Bills" msgid "Bills"
msgstr "" msgstr "明細書"
msgid "Settle" msgid "Settle"
msgstr "" msgstr "解決"
msgid "Statistics" msgid "Statistics"
msgstr "" msgstr "統計"
msgid "History" msgid "History"
msgstr "" msgstr "歴史"
msgid "Settings" msgid "Settings"
msgstr "" msgstr "設定"
msgid "Languages" msgid "Languages"
msgstr "" msgstr "言語"
msgid "Projects" msgid "Projects"
msgstr "" msgstr "プロジェクト"
msgid "Start a new project" msgid "Start a new project"
msgstr "" msgstr "新しいプロジェクトを始める"
msgid "Other projects :" msgid "Other projects :"
msgstr "" msgstr "他のプロジェクト:"
msgid "switch to" msgid "switch to"
msgstr "" msgstr "…に切り替える"
msgid "Dashboard" msgid "Dashboard"
msgstr "" msgstr "ダッシュボード"
msgid "Logout" msgid "Logout"
msgstr "" msgstr "ログアウト"
msgid "Code" msgid "Code"
msgstr "" msgstr "コード"
msgid "Mobile Application" msgid "Mobile Application"
msgstr "" msgstr "携帯アプリ"
msgid "Documentation" msgid "Documentation"
msgstr "" msgstr "書類"
msgid "Administation Dashboard" msgid "Administation Dashboard"
msgstr "" msgstr "管理ダッシュボード"
msgid "\"I hate money\" is a free software" msgid "\"I hate money\" is a free software"
msgstr "" msgstr "\"I hate money\"は無料のソフトウェアです"
msgid "you can contribute and improve it!" msgid "you can contribute and improve it!"
msgstr "" msgstr "あなたは貢献して、向上させることができます!"
#, python-format #, python-format
msgid "%(amount)s each" msgid "%(amount)s each"
msgstr "" msgstr "各自に%(amount)s"
msgid "Invite people" msgid "Invite people"
msgstr "" msgstr "人を誘う"
msgid "You should start by adding participants" msgid "You should start by adding participants"
msgstr "" msgstr "参加者を追加して始めましょう"
msgid "Add a new bill" msgid "Add a new bill"
msgstr "" msgstr "新しい明細を追加する"
msgid "Newer bills" msgid "Newer bills"
msgstr "" msgstr "もっと新しい明細"
msgid "Older bills" msgid "Older bills"
msgstr "" msgstr "もっと古い明細"
msgid "When?" msgid "When?"
msgstr "" msgstr "いつ?"
msgid "Who paid?" msgid "Who paid?"
msgstr "" msgstr "誰が支払った?"
msgid "For what?" msgid "For what?"
msgstr "" msgstr "何のため?"
msgid "How much?" msgid "How much?"
msgstr "" msgstr "いくら?"
#, python-format #, python-format
msgid "Added on %(date)s" msgid "Added on %(date)s"
msgstr "" msgstr "%(date)sに追加された"
msgid "Everyone" msgid "Everyone"
msgstr "" msgstr ""
#, python-format #, python-format
msgid "Everyone but %(excluded)s" msgid "Everyone but %(excluded)s"
msgstr "" msgstr "%(excluded)s以外にみんな"
msgid "No bills" msgid "No bills"
msgstr "" msgstr "明細なし"
msgid "Nothing to list yet." msgid "Nothing to list yet."
msgstr "" msgstr "表示できるものはありません。"
msgid "You probably want to" msgid "You probably want to"
msgstr "" msgstr "…したいかもしれない"
msgid "add a bill" msgid "add a bill"
msgstr "" msgstr "明細を追加する"
msgid "add participants" msgid "add participants"
msgstr "" msgstr "参加者を追加する"
msgid "Password reminder" msgid "Password reminder"
msgstr "" msgstr "パスワードを思いさせる"
msgid "" msgid ""
"A link to reset your password has been sent to you, please check your " "A link to reset your password has been sent to you, please check your "
"emails." "emails."
msgstr "" msgstr "パスワードを再設定するリンクを送りました。メールをチェックしてください。"
msgid "Return to home page" msgid "Return to home page"
msgstr "" msgstr "ホームページに戻る"
msgid "Your projects" msgid "Your projects"
msgstr "" msgstr "あなたのプロジェクト"
msgid "Reset your password" msgid "Reset your password"
msgstr "" msgstr "パスワードを再設定する"
msgid "Invite people to join this project" msgid "Invite people to join this project"
msgstr "" msgstr "他人をこのプロジェクトに招待する"
msgid "Share Identifier & code" msgid "Share Identifier & code"
msgstr "" msgstr "名前とコードを共有する"
msgid "" msgid ""
"You can share the project identifier and the private code by any " "You can share the project identifier and the private code by any "
"communication means." "communication means."
msgstr "" msgstr "プロジェクト名と私用コードは何の方法でも共有できます。"
msgid "Identifier:" msgid "Identifier:"
msgstr "" msgstr "名前:"
msgid "Share the Link" msgid "Share the Link"
msgstr "" msgstr "リンクを共有する"
msgid "You can directly share the following link via your prefered medium" msgid "You can directly share the following link via your prefered medium"
msgstr "" msgstr "好きの手段で以下のリンクを直接に共有できる"
msgid "Send via Emails" msgid "Send via Emails"
msgstr "" msgstr "メールで送る"
msgid "" msgid ""
"Specify a (comma separated) list of email adresses you want to notify " "Specify a (comma separated) list of email adresses you want to notify "
@ -737,33 +749,35 @@ msgid ""
" creation of this budget management project and we will " " creation of this budget management project and we will "
"send them an email for you." "send them an email for you."
msgstr "" msgstr ""
"…を知らせたいメールアドレスのリストを特定する(カンマ区切り)\n"
"彼らにこの予算管理プロジェクトの作成をメールでお知らせします。"
msgid "Who pays?" msgid "Who pays?"
msgstr "" msgstr "誰が支払った?"
msgid "To whom?" msgid "To whom?"
msgstr "" msgstr "誰まで?"
msgid "Who?" msgid "Who?"
msgstr "" msgstr "誰?"
msgid "Balance" msgid "Balance"
msgstr "" msgstr "残高"
msgid "deactivate" msgid "deactivate"
msgstr "" msgstr "操作不可にする"
msgid "reactivate" msgid "reactivate"
msgstr "" msgstr "再び調査可能にする"
msgid "Paid" msgid "Paid"
msgstr "" msgstr "支払われた"
msgid "Spent" msgid "Spent"
msgstr "" msgstr "使われた"
msgid "Expenses by Month" msgid "Expenses by Month"
msgstr "" msgstr "月別の費用"
msgid "Period" msgid "Period"
msgstr "" msgstr "期間"

View file

@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: \n" "Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2020-05-30 21:50+0200\n" "POT-Creation-Date: 2020-05-30 21:50+0200\n"
"PO-Revision-Date: 2020-09-06 15:36+0000\n" "PO-Revision-Date: 2021-02-17 02:50+0000\n"
"Last-Translator: Sander Kooijmans <weblate@gogognome.nl>\n" "Last-Translator: Sander Kooijmans <weblate@gogognome.nl>\n"
"Language-Team: Dutch <https://hosted.weblate.org/projects/i-hate-money/" "Language-Team: Dutch <https://hosted.weblate.org/projects/i-hate-money/"
"i-hate-money/nl/>\n" "i-hate-money/nl/>\n"
@ -12,13 +12,15 @@ msgstr ""
"Content-Type: text/plain; charset=utf-8\n" "Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n" "Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.3-dev\n" "X-Generator: Weblate 4.5\n"
"Generated-By: Babel 2.8.0\n" "Generated-By: Babel 2.8.0\n"
msgid "" msgid ""
"Not a valid amount or expression. Only numbers and + - * / operators are " "Not a valid amount or expression. Only numbers and + - * / operators are "
"accepted." "accepted."
msgstr "Geen geldig bedrag of expressie. Gebruik alleen getallen en + - * /" msgstr ""
"Geen geldig bedrag of expressie. Gebruik alleen getallen en + - * / "
"operatoren."
msgid "Project name" msgid "Project name"
msgstr "Projectnaam" msgstr "Projectnaam"
@ -54,7 +56,7 @@ msgstr "Project aanmaken"
msgid "" msgid ""
"A project with this identifier (\"%(project)s\") already exists. Please " "A project with this identifier (\"%(project)s\") already exists. Please "
"choose a new identifier" "choose a new identifier"
msgstr "Er is al een project genaamd (\"%(project)s\"). Kies een andere naam." msgstr "Er is al een project genaamd (\"%(project)s\"). Kies een andere naam"
msgid "Get in" msgid "Get in"
msgstr "Inloggen" msgstr "Inloggen"
@ -112,7 +114,7 @@ msgstr "Versturen en nieuwe toevoegen"
#, python-format #, python-format
msgid "Project default: %(currency)s" msgid "Project default: %(currency)s"
msgstr "" msgstr "Projectstandaard: %(currency)s"
msgid "Bills can't be null" msgid "Bills can't be null"
msgstr "Rekeningen mogen niet null zijn" msgstr "Rekeningen mogen niet null zijn"
@ -177,22 +179,27 @@ msgstr ""
"verdelen" "verdelen"
msgid "A reminder email has just been sent to you" msgid "A reminder email has just been sent to you"
msgstr "" msgstr "Een herinneringsmail is zojuist naar u verzonden"
msgid "" msgid ""
"We tried to send you an reminder email, but there was an error. You can " "We tried to send you an reminder email, but there was an error. You can "
"still use the project normally." "still use the project normally."
msgstr "" msgstr ""
"We hebben geprobeerd een herinneringsmail te versturen, maar er is iets fout "
"gegaan. Je kunt het project nog steeds normaal gebruiken."
#, python-format #, python-format
msgid "The project identifier is %(project)s" msgid "The project identifier is %(project)s"
msgstr "" msgstr "Het project-id is %(project)s"
msgid "" msgid ""
"Sorry, there was an error while sending you an email with password reset " "Sorry, there was an error while sending you an email with password reset "
"instructions. Please check the email configuration of the server or " "instructions. Please check the email configuration of the server or "
"contact the administrator." "contact the administrator."
msgstr "" msgstr ""
"Sorry, er is iets fout gegaan bij het verzenden van een e-mail met "
"instructies om je wachtwoord te herstellen. Controleer de e-mailinstellingen "
"van de server of neem contact op met de beheerder."
msgid "No token provided" msgid "No token provided"
msgstr "Geen toegangssleutel opgegeven" msgstr "Geen toegangssleutel opgegeven"
@ -207,7 +214,7 @@ msgid "Password successfully reset."
msgstr "Wachtwoord is hersteld." msgstr "Wachtwoord is hersteld."
msgid "Project successfully uploaded" msgid "Project successfully uploaded"
msgstr "" msgstr "Project succesvol geüpload"
msgid "Invalid JSON" msgid "Invalid JSON"
msgstr "Ongeldige JSON" msgstr "Ongeldige JSON"
@ -227,6 +234,9 @@ msgid ""
"Please check the email configuration of the server or contact the " "Please check the email configuration of the server or contact the "
"administrator." "administrator."
msgstr "" msgstr ""
"Sorry, er is iets fout gegaan bij het verzenden van de uitnodigingsmails. "
"Controleer de e-mailinstellingen van de server of neem contact op met de "
"beheerder."
#, python-format #, python-format
msgid "%(member)s has been added" msgid "%(member)s has been added"
@ -342,13 +352,13 @@ msgstr ""
" te schikken." " te schikken."
msgid "Can't remember the password?" msgid "Can't remember the password?"
msgstr "Ben je je wachtwoord vergeten?" msgstr "Wachtwoord vergeten?"
msgid "Cancel" msgid "Cancel"
msgstr "Annuleren" msgstr "Annuleren"
msgid "Privacy Settings" msgid "Privacy Settings"
msgstr "" msgstr "Privacy-instellingen"
msgid "Edit the project" msgid "Edit the project"
msgstr "Project bewerken" msgstr "Project bewerken"
@ -381,13 +391,13 @@ msgid "Download"
msgstr "Downloaden" msgstr "Downloaden"
msgid "Disabled Project History" msgid "Disabled Project History"
msgstr "" msgstr "Uitgeschakelde Projectgeschiedenis"
msgid "Disabled Project History & IP Address Recording" msgid "Disabled Project History & IP Address Recording"
msgstr "" msgstr "Uitgeschakelde Projecthistorie & IP-Adres Vastlegging"
msgid "Enabled Project History" msgid "Enabled Project History"
msgstr "" msgstr "Ingeschakelde Projectgeschiedenis"
msgid "Disabled IP Address Recording" msgid "Disabled IP Address Recording"
msgstr "" msgstr ""
@ -399,7 +409,7 @@ msgid "Enabled IP Address Recording"
msgstr "" msgstr ""
msgid "History Settings Changed" msgid "History Settings Changed"
msgstr "" msgstr "Geschiedenisinstellingen Gewijzigd"
msgid "changed" msgid "changed"
msgstr "gewijzigd" msgstr "gewijzigd"
@ -419,20 +429,26 @@ msgid ""
" The rest of the project history will be unaffected. This " " The rest of the project history will be unaffected. This "
"action cannot be undone." "action cannot be undone."
msgstr "" msgstr ""
"Weet je zeker dat je alle vastgelegde IP-adressen wilt verwijderen van dit "
"project?\n"
"De rest van de projectgeschiedenis blijft onveranderd. Deze actie kan niet "
"ongedaan worden gemaakt."
msgid "Close" msgid "Close"
msgstr "Sluiten" msgstr "Sluiten"
msgid "Confirm Delete" msgid "Confirm Delete"
msgstr "" msgstr "Bevestig Verwijdering"
msgid "Delete Confirmation" msgid "Delete Confirmation"
msgstr "" msgstr "Bevestiging Verwijdering"
msgid "" msgid ""
"Are you sure you want to erase all history for this project? This action " "Are you sure you want to erase all history for this project? This action "
"cannot be undone." "cannot be undone."
msgstr "" msgstr ""
"Weet je zeker dat je de gehele geschiedenis wilt verwijderen voor dit "
"project? Deze actie kan niet ongedaan worden gemaakt."
msgid "Added" msgid "Added"
msgstr "Toegevoegd" msgstr "Toegevoegd"
@ -454,6 +470,12 @@ msgid ""
" <a href=\"%(url)s\">settings page</a>\n" " <a href=\"%(url)s\">settings page</a>\n"
" " " "
msgstr "" msgstr ""
"\n"
" <i>Dit project heeft de geschiedenis uitgeschakeld. Nieuwe "
"acties zullen niet hieronder verschijnen. Je kunt de geschiedenis aanzetten "
"op de </i>\n"
" <a href=\"%(url)s\">instellingen-pagina</a>\n"
" "
msgid "" msgid ""
"\n" "\n"
@ -474,19 +496,19 @@ msgid "Delete stored IP addresses"
msgstr "" msgstr ""
msgid "No history to erase" msgid "No history to erase"
msgstr "" msgstr "Geen geschiedenis om te wissen"
msgid "Clear Project History" msgid "Clear Project History"
msgstr "" msgstr "Verwijder Projectgeschiedenis"
msgid "No IP Addresses to erase" msgid "No IP Addresses to erase"
msgstr "" msgstr "Geen IP-adressen te verwijderen"
msgid "Delete Stored IP Addresses" msgid "Delete Stored IP Addresses"
msgstr "" msgstr "Verwijder opgeslagen IP-adressen"
msgid "Time" msgid "Time"
msgstr "" msgstr "Tijd"
msgid "Event" msgid "Event"
msgstr "" msgstr ""
@ -501,44 +523,44 @@ msgid "From IP"
msgstr "" msgstr ""
msgid "added" msgid "added"
msgstr "" msgstr "toegevoegd"
msgid "Project private code changed" msgid "Project private code changed"
msgstr "" msgstr "Privécode van project gewijzigd"
msgid "Project renamed to" msgid "Project renamed to"
msgstr "" msgstr "Project hernoemd tot"
msgid "Project contact email changed to" msgid "Project contact email changed to"
msgstr "" msgstr ""
msgid "Project settings modified" msgid "Project settings modified"
msgstr "" msgstr "Projectinstellingen gewijzigd"
msgid "deactivated" msgid "deactivated"
msgstr "" msgstr "gedeactiveerd"
msgid "reactivated" msgid "reactivated"
msgstr "" msgstr "gereactiveerd"
msgid "renamed to" msgid "renamed to"
msgstr "" msgstr "hernoemd naar"
msgid "External link changed to" msgid "External link changed to"
msgstr "" msgstr ""
msgid "Amount" msgid "Amount"
msgstr "" msgstr "Hoeveelheid"
#, python-format #, python-format
msgid "Amount in %(currency)s" msgid "Amount in %(currency)s"
msgstr "" msgstr "Hoeveelheid in %(currency)s"
msgid "modified" msgid "modified"
msgstr "" msgstr "gewijzigd"
msgid "removed" msgid "removed"
msgstr "" msgstr "verwijderd"
msgid "changed in a unknown way" msgid "changed in a unknown way"
msgstr "" msgstr ""
@ -547,7 +569,7 @@ msgid "Nothing to list"
msgstr "" msgstr ""
msgid "Someone probably cleared the project history." msgid "Someone probably cleared the project history."
msgstr "" msgstr "Iemand heeft waarschijnlijk de projectgeschiedenis verwijderd."
msgid "Manage your shared <br />expenses, easily" msgid "Manage your shared <br />expenses, easily"
msgstr "Beheer eenvoudig je <br />gedeelde uitgaven" msgstr "Beheer eenvoudig je <br />gedeelde uitgaven"
@ -574,7 +596,7 @@ msgid "Log in"
msgstr "Inloggen" msgstr "Inloggen"
msgid "can't remember your password?" msgid "can't remember your password?"
msgstr "ben je je wachtwoord vergeten?" msgstr "wachtwoord vergeten?"
msgid "Create" msgid "Create"
msgstr "Maken" msgstr "Maken"
@ -583,6 +605,8 @@ msgid ""
"Don\\'t reuse a personal password. Choose a private code and send it to " "Don\\'t reuse a personal password. Choose a private code and send it to "
"your friends" "your friends"
msgstr "" msgstr ""
"Hergebruik geen persoonlijk wachtwoord. Kies een privécode en stuur het naar "
"je vrienden"
msgid "Account manager" msgid "Account manager"
msgstr "Accountbeheer" msgstr "Accountbeheer"
@ -597,7 +621,7 @@ msgid "Statistics"
msgstr "Statistieken" msgstr "Statistieken"
msgid "History" msgid "History"
msgstr "" msgstr "Geschiedenis"
msgid "Settings" msgid "Settings"
msgstr "Instellingen" msgstr "Instellingen"
@ -781,7 +805,7 @@ msgid "Expenses by Month"
msgstr "" msgstr ""
msgid "Period" msgid "Period"
msgstr "" msgstr "Periode"
#~ msgid "%(msg_compl)sThe project identifier is %(project)s" #~ msgid "%(msg_compl)sThe project identifier is %(project)s"
#~ msgstr "%(msg_compl)sDe project-id is %(project)s" #~ msgstr "%(msg_compl)sDe project-id is %(project)s"

View file

@ -676,16 +676,16 @@ msgstr "вы можете способствовать развитию и ул
#, python-format #, python-format
msgid "%(amount)s each" msgid "%(amount)s each"
msgstr "%(amount)s каждый" msgstr "%(amount)s по каждому"
msgid "Invite people" msgid "Invite people"
msgstr "Пригласить людей" msgstr "Пригласите людей"
msgid "You should start by adding participants" msgid "You should start by adding participants"
msgstr "Вам стоит начать с добавления пользователей" msgstr "Вы должны начать с добавлением пользователей"
msgid "Add a new bill" msgid "Add a new bill"
msgstr "Добавить новый счёт" msgstr "Добавите новый счёт"
msgid "Newer bills" msgid "Newer bills"
msgstr "Новые счета" msgstr "Новые счета"
@ -749,7 +749,7 @@ msgid "Reset your password"
msgstr "Восстановить пароль" msgstr "Восстановить пароль"
msgid "Invite people to join this project" msgid "Invite people to join this project"
msgstr "Пригласить людей присоединиться к этому проекту" msgstr "Пригласите людей присоединиться к этому проекту"
msgid "Share Identifier & code" msgid "Share Identifier & code"
msgstr "Поделиться идентификатором и кодом" msgstr "Поделиться идентификатором и кодом"

Binary file not shown.

View file

@ -0,0 +1,770 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-04-08 13:59+0200\n"
"PO-Revision-Date: 2021-04-09 13:26+0000\n"
"Last-Translator: Rastko Sarcevic <ralesarcevic@gmail.com>\n"
"Language-Team: Serbian <https://hosted.weblate.org/projects/i-hate-money/"
"i-hate-money/sr/>\n"
"Language: sr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
"X-Generator: Weblate 4.6-dev\n"
msgid ""
"Not a valid amount or expression. Only numbers and + - * / operators are "
"accepted."
msgstr ""
msgid "Project name"
msgstr ""
msgid "Private code"
msgstr ""
msgid "Email"
msgstr "Email"
msgid "Enable project history"
msgstr ""
msgid "Use IP tracking for project history"
msgstr ""
msgid "Default Currency"
msgstr "Podrazumevana Valuta"
msgid "Import previously exported JSON file"
msgstr ""
msgid "Import"
msgstr "Uvezi"
msgid "Project identifier"
msgstr ""
msgid "Create the project"
msgstr ""
#, python-format
msgid ""
"A project with this identifier (\"%(project)s\") already exists. Please "
"choose a new identifier"
msgstr ""
msgid "Get in"
msgstr ""
msgid "Admin password"
msgstr "Administratorska lozinka"
msgid "Send me the code by email"
msgstr "Pošalji mi kod putem email-a"
msgid "This project does not exists"
msgstr ""
msgid "Password mismatch"
msgstr "Lozinke se ne slažu"
msgid "Password"
msgstr "Lozinka"
msgid "Password confirmation"
msgstr "Potvrda lozinke"
msgid "Reset password"
msgstr "Resetuj lozinku"
msgid "Date"
msgstr "Datum"
msgid "What?"
msgstr ""
msgid "Payer"
msgstr ""
msgid "Amount paid"
msgstr "Plaćeni iznos"
msgid "Currency"
msgstr "Valuta"
msgid "External link"
msgstr ""
msgid "A link to an external document, related to this bill"
msgstr ""
msgid "For whom?"
msgstr "Za koga?"
msgid "Submit"
msgstr "Unesi"
msgid "Submit and add a new one"
msgstr ""
#, python-format
msgid "Project default: %(currency)s"
msgstr ""
msgid "Bills can't be null"
msgstr ""
msgid "Name"
msgstr "Ime"
msgid "Weights should be positive"
msgstr "Težine moraju biti pozitivne"
msgid "Weight"
msgstr "Težina"
msgid "Add"
msgstr "Dodaj"
msgid "User name incorrect"
msgstr ""
msgid "This project already have this member"
msgstr ""
msgid "People to notify"
msgstr ""
msgid "Send invites"
msgstr ""
#, python-format
msgid "The email %(email)s is not valid"
msgstr "Email %(email)s nije validan"
msgid "Participant"
msgstr "Učesnik"
msgid "Bill"
msgstr "Račun"
msgid "Project"
msgstr "Projekat"
msgid "No Currency"
msgstr ""
msgid "Too many failed login attempts, please retry later."
msgstr ""
#, python-format
msgid "This admin password is not the right one. Only %(num)d attempts left."
msgstr ""
msgid "You either provided a bad token or no project identifier."
msgstr ""
msgid "This private code is not the right one"
msgstr ""
#, python-format
msgid "You have just created '%(project)s' to share your expenses"
msgstr ""
msgid "A reminder email has just been sent to you"
msgstr ""
msgid ""
"We tried to send you an reminder email, but there was an error. You can "
"still use the project normally."
msgstr ""
#, python-format
msgid "The project identifier is %(project)s"
msgstr ""
msgid ""
"Sorry, there was an error while sending you an email with password reset "
"instructions. Please check the email configuration of the server or "
"contact the administrator."
msgstr ""
msgid "No token provided"
msgstr ""
msgid "Invalid token"
msgstr ""
msgid "Unknown project"
msgstr ""
msgid "Password successfully reset."
msgstr "Lozinka uspešno resetovana."
msgid "Project successfully uploaded"
msgstr ""
msgid "Invalid JSON"
msgstr ""
msgid "Project successfully deleted"
msgstr ""
#, python-format
msgid "You have been invited to share your expenses for %(project)s"
msgstr ""
msgid "Your invitations have been sent"
msgstr ""
msgid ""
"Sorry, there was an error while trying to send the invitation emails. "
"Please check the email configuration of the server or contact the "
"administrator."
msgstr ""
#, python-format
msgid "%(member)s has been added"
msgstr ""
#, python-format
msgid "%(name)s is part of this project again"
msgstr ""
#, python-format
msgid ""
"User '%(name)s' has been deactivated. It will still appear in the users "
"list until its balance becomes zero."
msgstr ""
#, python-format
msgid "User '%(name)s' has been removed"
msgstr "Korisnik %(name)s je uklonjen"
#, python-format
msgid "User '%(name)s' has been edited"
msgstr ""
msgid "The bill has been added"
msgstr "Račun je dodat"
msgid "The bill has been deleted"
msgstr "Račun je uklonjen"
msgid "The bill has been modified"
msgstr "Račun je izmenjen"
msgid "Sorry, we were unable to find the page you've asked for."
msgstr ""
msgid "The best thing to do is probably to get back to the main page."
msgstr ""
msgid "Back to the list"
msgstr "Nazad na listu"
msgid "Administration tasks are currently disabled."
msgstr ""
msgid "The project you are trying to access do not exist, do you want to"
msgstr ""
msgid "create it"
msgstr ""
msgid "?"
msgstr "?"
msgid "Create a new project"
msgstr ""
msgid "Number of members"
msgstr "Broj korisnika"
msgid "Number of bills"
msgstr "Broj računa"
msgid "Newest bill"
msgstr "Najnoviji račun"
msgid "Oldest bill"
msgstr "Najstariji račun"
msgid "Actions"
msgstr ""
msgid "edit"
msgstr "izmeni"
msgid "delete"
msgstr "ukloni"
msgid "show"
msgstr "prikaži"
msgid "The Dashboard is currently deactivated."
msgstr ""
msgid "you sure?"
msgstr ""
msgid "Edit project"
msgstr ""
msgid "Import JSON"
msgstr ""
msgid "Choose file"
msgstr "Izaberi fajl"
msgid "Download project's data"
msgstr ""
msgid "Bill items"
msgstr "Stavke računa"
msgid "Download the list of bills with owner, amount, reason,... "
msgstr ""
msgid "Settle plans"
msgstr ""
msgid "Download the list of transactions needed to settle the current bills."
msgstr ""
msgid "Can't remember the password?"
msgstr ""
msgid "Cancel"
msgstr "Otkaži"
msgid "Privacy Settings"
msgstr "Podešavanja Privatnosti"
msgid "Edit the project"
msgstr ""
msgid "Edit this bill"
msgstr "Izmeni ovaj račun"
msgid "Add a bill"
msgstr "Dodaj račun"
msgid "Select all"
msgstr "Izaberi sve"
msgid "Select none"
msgstr ""
msgid "Add participant"
msgstr ""
msgid "Edit this member"
msgstr ""
msgid "john.doe@example.com, mary.moe@site.com"
msgstr ""
msgid "Send the invitations"
msgstr ""
msgid "Download"
msgstr "Preuzmi"
msgid "Disabled Project History"
msgstr ""
msgid "Disabled Project History & IP Address Recording"
msgstr ""
msgid "Enabled Project History"
msgstr ""
msgid "Disabled IP Address Recording"
msgstr ""
msgid "Enabled Project History & IP Address Recording"
msgstr ""
msgid "Enabled IP Address Recording"
msgstr ""
msgid "History Settings Changed"
msgstr ""
msgid "changed"
msgstr ""
msgid "from"
msgstr ""
msgid "to"
msgstr ""
msgid "Confirm Remove IP Adresses"
msgstr ""
msgid ""
"Are you sure you want to delete all recorded IP addresses from this "
"project?\n"
" The rest of the project history will be unaffected. This "
"action cannot be undone."
msgstr ""
msgid "Close"
msgstr ""
msgid "Confirm Delete"
msgstr ""
msgid "Delete Confirmation"
msgstr ""
msgid ""
"Are you sure you want to erase all history for this project? This action "
"cannot be undone."
msgstr ""
msgid "Added"
msgstr "Dodato"
msgid "Removed"
msgstr "Uklonjeno"
msgid "and"
msgstr "i"
msgid "owers list"
msgstr ""
#, python-format
msgid ""
"\n"
" <i>This project has history disabled. New actions won't "
"appear below. You can enable history on the</i>\n"
" <a href=\"%(url)s\">settings page</a>\n"
" "
msgstr ""
msgid ""
"\n"
" <i>The table below reflects actions recorded prior to "
"disabling project history. You can\n"
" <a href=\"#\" data-toggle=\"modal\" data-keyboard=\"false\" "
"data-target=\"#confirm-erase\">clear project history</a> to remove "
"them.</i></p>\n"
" "
msgstr ""
msgid ""
"Some entries below contain IP addresses, even though this project has IP "
"recording disabled. "
msgstr ""
msgid "Delete stored IP addresses"
msgstr ""
msgid "No history to erase"
msgstr ""
msgid "Clear Project History"
msgstr ""
msgid "No IP Addresses to erase"
msgstr ""
msgid "Delete Stored IP Addresses"
msgstr ""
msgid "Time"
msgstr "Vreme"
msgid "Event"
msgstr "Događaj"
msgid "IP address recording can be enabled on the settings page"
msgstr ""
msgid "IP address recording can be disabled on the settings page"
msgstr ""
msgid "From IP"
msgstr ""
msgid "added"
msgstr ""
msgid "Project private code changed"
msgstr ""
msgid "Project renamed to"
msgstr ""
msgid "Project contact email changed to"
msgstr ""
msgid "Project settings modified"
msgstr ""
msgid "deactivated"
msgstr ""
msgid "reactivated"
msgstr ""
msgid "renamed to"
msgstr ""
msgid "External link changed to"
msgstr ""
msgid "Amount"
msgstr "Iznos"
#, python-format
msgid "Amount in %(currency)s"
msgstr "Iznos u %(currency)s"
msgid "modified"
msgstr "izmenjeno"
msgid "removed"
msgstr "uklonjeno"
msgid "changed in a unknown way"
msgstr ""
msgid "Nothing to list"
msgstr ""
msgid "Someone probably cleared the project history."
msgstr ""
msgid "Manage your shared <br />expenses, easily"
msgstr ""
msgid "Try out the demo"
msgstr "Isprobaj demo verziju"
msgid "You're sharing a house?"
msgstr ""
msgid "Going on holidays with friends?"
msgstr ""
msgid "Simply sharing money with others?"
msgstr ""
msgid "We can help!"
msgstr ""
msgid "Log in to an existing project"
msgstr ""
msgid "Log in"
msgstr ""
msgid "can't remember your password?"
msgstr ""
msgid "Create"
msgstr ""
msgid ""
"Don\\'t reuse a personal password. Choose a private code and send it to "
"your friends"
msgstr ""
msgid "Account manager"
msgstr ""
msgid "Bills"
msgstr "Računi"
msgid "Settle"
msgstr ""
msgid "Statistics"
msgstr "Statistika"
msgid "History"
msgstr "Istorija"
msgid "Settings"
msgstr "Podešavanja"
msgid "Languages"
msgstr "Jezici"
msgid "Projects"
msgstr ""
msgid "Start a new project"
msgstr ""
msgid "Other projects :"
msgstr ""
msgid "switch to"
msgstr ""
msgid "Dashboard"
msgstr ""
msgid "Logout"
msgstr ""
msgid "Code"
msgstr "Kod"
msgid "Mobile Application"
msgstr "Mobilna Aplikacija"
msgid "Documentation"
msgstr "Dokumentacija"
msgid "Administation Dashboard"
msgstr ""
msgid "\"I hate money\" is a free software"
msgstr ""
msgid "you can contribute and improve it!"
msgstr ""
#, python-format
msgid "%(amount)s each"
msgstr "%(amount)s svako"
msgid "Invite people"
msgstr ""
msgid "You should start by adding participants"
msgstr ""
msgid "Add a new bill"
msgstr "Dodaj novi račun"
msgid "Newer bills"
msgstr "Noviji računi"
msgid "Older bills"
msgstr "Stariji računi"
msgid "When?"
msgstr "Kada?"
msgid "Who paid?"
msgstr "Ko je platio?"
msgid "For what?"
msgstr "Za šta?"
msgid "How much?"
msgstr "Koliko?"
#, python-format
msgid "Added on %(date)s"
msgstr "Dodato %(date)s"
msgid "Everyone"
msgstr "Svi"
#, python-format
msgid "Everyone but %(excluded)s"
msgstr "Svi osim %(excluded)s"
msgid "No bills"
msgstr ""
msgid "Nothing to list yet."
msgstr ""
msgid "You probably want to"
msgstr ""
msgid "add a bill"
msgstr ""
msgid "add participants"
msgstr ""
msgid "Password reminder"
msgstr ""
msgid ""
"A link to reset your password has been sent to you, please check your "
"emails."
msgstr ""
msgid "Return to home page"
msgstr "Vrati se na početnu stranu"
msgid "Your projects"
msgstr ""
msgid "Reset your password"
msgstr "Resetuj lozinku"
msgid "Invite people to join this project"
msgstr ""
msgid "Share Identifier & code"
msgstr ""
msgid ""
"You can share the project identifier and the private code by any "
"communication means."
msgstr ""
msgid "Identifier:"
msgstr ""
msgid "Share the Link"
msgstr "Podelite link"
msgid "You can directly share the following link via your prefered medium"
msgstr ""
msgid "Send via Emails"
msgstr ""
msgid ""
"Specify a (comma separated) list of email adresses you want to notify "
"about the\n"
" creation of this budget management project and we will "
"send them an email for you."
msgstr ""
msgid "Who pays?"
msgstr "Ko plaća?"
msgid "To whom?"
msgstr "Kome?"
msgid "Who?"
msgstr "Ko?"
msgid "Balance"
msgstr "Stanje"
msgid "deactivate"
msgstr ""
msgid "reactivate"
msgstr ""
msgid "Paid"
msgstr "Plaćeno"
msgid "Spent"
msgstr "Potrošeno"
msgid "Expenses by Month"
msgstr "Mesečni troškovi"
msgid "Period"
msgstr "Period"

Binary file not shown.

View file

@ -12,7 +12,7 @@ import socket
from babel import Locale from babel import Locale
from babel.numbers import get_currency_name, get_currency_symbol from babel.numbers import get_currency_name, get_currency_symbol
from flask import current_app, redirect, render_template, escape from flask import current_app, escape, redirect, render_template
from flask_babel import get_locale, lazy_gettext as _ from flask_babel import get_locale, lazy_gettext as _
import jinja2 import jinja2
from werkzeug.routing import HTTPException, RoutingException from werkzeug.routing import HTTPException, RoutingException
@ -363,6 +363,7 @@ def localize_list(list, surround_with_em=True):
output_str = start.format(start_object="{start_object}", next_object=output_str) output_str = start.format(start_object="{start_object}", next_object=output_str)
return output_str.format(start_object=item_wrapper(list.pop())) return output_str.format(start_object=item_wrapper(list.pop()))
def render_localized_currency(code, detailed=True): def render_localized_currency(code, detailed=True):
if code == "XXX": if code == "XXX":
return _("No Currency") return _("No Currency")
@ -389,4 +390,3 @@ def render_localized_template(template_name_prefix, **context):
] ]
# render_template() supports a list of templates to try in order # render_template() supports a list of templates to try in order
return render_template(templates, **context) return render_template(templates, **context)

View file

@ -270,6 +270,11 @@ def home():
) )
@main.route("/mobile")
def mobile():
return render_template("download_mobile_app.html")
@main.route("/create", methods=["GET", "POST"]) @main.route("/create", methods=["GET", "POST"])
@requires_admin(bypass=("ALLOW_PUBLIC_PROJECT_CREATION", True)) @requires_admin(bypass=("ALLOW_PUBLIC_PROJECT_CREATION", True))
def create_project(): def create_project():

View file

@ -14,6 +14,7 @@ classifiers =
Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Topic :: Internet :: WWW/HTTP Topic :: Internet :: WWW/HTTP
Topic :: Internet :: WWW/HTTP :: WSGI :: Application Topic :: Internet :: WWW/HTTP :: WSGI :: Application
@ -40,6 +41,7 @@ install_requires =
Jinja2~=2.11 Jinja2~=2.11
requests~=2.22 requests~=2.22
SQLAlchemy-Continuum~=1.3 SQLAlchemy-Continuum~=1.3
SQLAlchemy~=1.3.0 # New 1.4 changes API, see #728
[options.extras_require] [options.extras_require]
dev = dev =

View file

@ -1,12 +1,12 @@
[tox] [tox]
envlist = py38,py37,py36,docs,flake8,black envlist = py39,py38,py37,py36,docs,flake8,black
skip_missing_interpreters = True skip_missing_interpreters = True
[testenv] [testenv]
commands = commands =
python --version python --version
py.test --pyargs ihatemoney.tests.tests py.test --pyargs ihatemoney.tests
deps = deps =
-e.[dev] -e.[dev]
@ -22,7 +22,7 @@ changedir = {toxinidir}
[testenv:black] [testenv:black]
commands = commands =
black --check --target-version=py34 . black --check --target-version=py36 .
isort -c -rc . isort -c -rc .
changedir = {toxinidir} changedir = {toxinidir}
@ -42,3 +42,4 @@ python =
3.6: py36 3.6: py36
3.7: py37 3.7: py37
3.8: py38, docs, black, flake8 3.8: py38, docs, black, flake8
3.9: py39