Compare commits
	
		
			10 commits
		
	
	
		
			8aba6f5129
			...
			e8ea813896
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| e8ea813896 | |||
| 602ddc7143 | |||
| 19da1cf430 | |||
| 4b14d042d7 | |||
| 585fffd7ac | |||
| 30c06da254 | |||
| 2966c33ea2 | |||
| e2c218102e | |||
| 4d83464963 | |||
| 4a0a5bac3f | 
							
								
								
									
										21
									
								
								.vscode/launch.json
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,21 @@
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    "version": "0.2.0",
 | 
				
			||||||
 | 
					    "configurations": [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            "name": "Python: Remote Attach",
 | 
				
			||||||
 | 
					            "type": "python",
 | 
				
			||||||
 | 
					            "request": "attach",
 | 
				
			||||||
 | 
					            "connect": {
 | 
				
			||||||
 | 
					                "host": "192.168.10.31",
 | 
				
			||||||
 | 
					                "port": 5678
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            "pathMappings": [
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    "localRoot": "${workspaceFolder}",
 | 
				
			||||||
 | 
					                    "remoteRoot": "."
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            "justMyCode": true
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										0
									
								
								app.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -2,7 +2,7 @@ FROM python:3.10-slim
 | 
				
			||||||
 | 
					
 | 
				
			||||||
SHELL ["/bin/bash", "-c"]
 | 
					SHELL ["/bin/bash", "-c"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
RUN apt-get update && apt-get install -y --no-install-recommends wget     && mkdir -p /app/static     && wget -O /app/static/logo.png "https://git.nocci.it/nocci/GiftGamesDB/raw/branch/main/steam-gift-manager/static/logo.png"     && wget -O /app/static/logo_small.png "https://git.nocci.it/nocci/GiftGamesDB/raw/branch/main/steam-gift-manager/static/logo_small.png"     && wget -O /app/static/forgejo.svg "https://git.nocci.it/nocci/GiftGamesDB/raw/branch/main/steam-gift-manager/static/forgejo.svg"     && rm -rf /var/lib/apt/lists/*
 | 
					RUN apt-get update && apt-get install -y --no-install-recommends wget     && mkdir -p /app/static     && wget -O /app/static/logo.webp "https://drop.nocadmin.net/logo.webp"     && wget -O /app/static/logo_small.webp "https://drop.nocadmin.net/logo_small.webp"     && wget -O /app/static/forgejo.webp "https://drop.nocadmin.net/forgejo.webp"     && wget -O /app/static/gog_logo.webp "https://drop.nocadmin.net/gog_logo.webp"     && wget -O /app/static/logo_small_maskable.webp  "https://drop.nocadmin.net/logo_small_maskable.webp"     && rm -rf /var/lib/apt/lists/*
 | 
				
			||||||
 | 
					
 | 
				
			||||||
RUN mkdir -p /app/data &&     chown -R 1000:1000 /app/data 
 | 
					RUN mkdir -p /app/data &&     chown -R 1000:1000 /app/data 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,16 +1,15 @@
 | 
				
			||||||
import os
 | 
					import os
 | 
				
			||||||
import logging
 | 
					 | 
				
			||||||
import warnings
 | 
					import warnings
 | 
				
			||||||
from sqlalchemy.exc import LegacyAPIWarning
 | 
					from sqlalchemy.exc import LegacyAPIWarning
 | 
				
			||||||
warnings.simplefilter("ignore", category=LegacyAPIWarning)
 | 
					warnings.simplefilter("ignore", category=LegacyAPIWarning)
 | 
				
			||||||
from flask import Flask, render_template, request, redirect, url_for, flash, make_response, session, abort, send_file, jsonify
 | 
					from flask import Flask, render_template, request, redirect, url_for, flash, make_response, session, abort, send_file, jsonify
 | 
				
			||||||
from flask_sqlalchemy import SQLAlchemy
 | 
					from flask_sqlalchemy import SQLAlchemy
 | 
				
			||||||
from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user
 | 
					from flask_login import LoginManager, UserMixin, login_user, logout_user, login_required, current_user
 | 
				
			||||||
from flask_babel import Babel, _
 | 
					 | 
				
			||||||
from werkzeug.security import generate_password_hash, check_password_hash
 | 
					from werkzeug.security import generate_password_hash, check_password_hash
 | 
				
			||||||
from datetime import datetime, timedelta
 | 
					from datetime import datetime, timedelta
 | 
				
			||||||
from flask_wtf import CSRFProtect
 | 
					from flask_wtf import CSRFProtect
 | 
				
			||||||
from flask import abort
 | 
					from flask import abort
 | 
				
			||||||
 | 
					from flask import request, redirect
 | 
				
			||||||
import io
 | 
					import io
 | 
				
			||||||
import warnings
 | 
					import warnings
 | 
				
			||||||
import re
 | 
					import re
 | 
				
			||||||
| 
						 | 
					@ -41,8 +40,39 @@ from reportlab.lib.utils import ImageReader
 | 
				
			||||||
from reportlab.lib.units import cm, inch, mm
 | 
					from reportlab.lib.units import cm, inch, mm
 | 
				
			||||||
from io import BytesIO
 | 
					from io import BytesIO
 | 
				
			||||||
import reportlab.lib
 | 
					import reportlab.lib
 | 
				
			||||||
 | 
					import logging
 | 
				
			||||||
 | 
					logging.basicConfig()
 | 
				
			||||||
app = Flask(__name__)
 | 
					app = Flask(__name__)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Load Languages
 | 
				
			||||||
 | 
					import os
 | 
				
			||||||
 | 
					import json
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					TRANSLATION_DIR = os.path.join(os.path.dirname(__file__), 'translations')
 | 
				
			||||||
 | 
					SUPPORTED_LANGUAGES = ['de', 'en']
 | 
				
			||||||
 | 
					TRANSLATIONS = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					for lang in SUPPORTED_LANGUAGES:
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        with open(os.path.join(TRANSLATION_DIR, f'{lang}.json'), encoding='utf-8') as f:
 | 
				
			||||||
 | 
					            TRANSLATIONS[lang] = json.load(f)
 | 
				
			||||||
 | 
					    except Exception:
 | 
				
			||||||
 | 
					        TRANSLATIONS[lang] = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def translate(key, lang=None, **kwargs):
 | 
				
			||||||
 | 
					    if not lang:
 | 
				
			||||||
 | 
					        lang = session.get('lang', 'en')
 | 
				
			||||||
 | 
					    value = TRANSLATIONS.get(lang, {}).get(key)
 | 
				
			||||||
 | 
					    if value is None and lang != 'en':
 | 
				
			||||||
 | 
					        value = TRANSLATIONS.get('en', {}).get(key, key)
 | 
				
			||||||
 | 
					    else:
 | 
				
			||||||
 | 
					        value = value or key
 | 
				
			||||||
 | 
					    return value.format(**kwargs) if kwargs and isinstance(value, str) else value
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## DEBUG Translations
 | 
				
			||||||
 | 
					if app.debug:
 | 
				
			||||||
 | 
					    print(f"Loaded translations for 'de': {TRANSLATIONS.get('de', {})}")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
csrf = CSRFProtect(app)
 | 
					csrf = CSRFProtect(app)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
convention = {
 | 
					convention = {
 | 
				
			||||||
| 
						 | 
					@ -62,14 +92,16 @@ load_dotenv(override=True)
 | 
				
			||||||
# App-Configuration
 | 
					# App-Configuration
 | 
				
			||||||
app.config.update(
 | 
					app.config.update(
 | 
				
			||||||
    SECRET_KEY=os.getenv('SECRET_KEY'),
 | 
					    SECRET_KEY=os.getenv('SECRET_KEY'),
 | 
				
			||||||
    SQLALCHEMY_DATABASE_URI=('sqlite:////app/data/games.db'),
 | 
					    SQLALCHEMY_DATABASE_URI='sqlite:////app/data/games.db',
 | 
				
			||||||
    SQLALCHEMY_TRACK_MODIFICATIONS=False,
 | 
					    SQLALCHEMY_TRACK_MODIFICATIONS=False,
 | 
				
			||||||
    BABEL_DEFAULT_LOCALE=os.getenv('BABEL_DEFAULT_LOCALE'),
 | 
					    SESSION_COOKIE_SECURE=os.getenv('SESSION_COOKIE_SECURE', 'False') == 'True',
 | 
				
			||||||
    BABEL_SUPPORTED_LOCALES=os.getenv('BABEL_SUPPORTED_LOCALES').split(','),
 | 
					    SESSION_COOKIE_SAMESITE='Lax',
 | 
				
			||||||
    BABEL_TRANSLATION_DIRECTORIES=os.getenv('BABEL_TRANSLATION_DIRECTORIES'),
 | 
					    PERMANENT_SESSION_LIFETIME=timedelta(days=30),
 | 
				
			||||||
    SESSION_COOKIE_SECURE=os.getenv('SESSION_COOKIE_SECURE') == 'True',
 | 
					    SESSION_REFRESH_EACH_REQUEST=False,
 | 
				
			||||||
    WTF_CSRF_ENABLED=os.getenv('CSRF_ENABLED') == 'True',
 | 
					    WTF_CSRF_ENABLED=os.getenv('CSRF_ENABLED', 'True') == 'True',
 | 
				
			||||||
    REGISTRATION_ENABLED=os.getenv('REGISTRATION_ENABLED', 'True').lower() == 'true'
 | 
					    REGISTRATION_ENABLED=os.getenv('REGISTRATION_ENABLED', 'True').lower() == 'true',
 | 
				
			||||||
 | 
					    SEND_FILE_MAX_AGE_DEFAULT=int(os.getenv('SEND_FILE_MAX_AGE_DEFAULT', 0)),
 | 
				
			||||||
 | 
					    TEMPLATES_AUTO_RELOAD=os.getenv('TEMPLATES_AUTO_RELOAD', 'True') == 'True'
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interval_hours = int(os.getenv('CHECK_EXPIRING_KEYS_INTERVAL_HOURS', 12))
 | 
					interval_hours = int(os.getenv('CHECK_EXPIRING_KEYS_INTERVAL_HOURS', 12))
 | 
				
			||||||
| 
						 | 
					@ -79,24 +111,28 @@ db = SQLAlchemy(app, metadata=metadata)
 | 
				
			||||||
migrate = Migrate(app, db)
 | 
					migrate = Migrate(app, db)
 | 
				
			||||||
login_manager = LoginManager(app)
 | 
					login_manager = LoginManager(app)
 | 
				
			||||||
login_manager.login_view = 'login'
 | 
					login_manager.login_view = 'login'
 | 
				
			||||||
babel = Babel(app)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Logging
 | 
					# Logging
 | 
				
			||||||
app.logger.addHandler(logging.StreamHandler())
 | 
					app.logger.addHandler(logging.StreamHandler())
 | 
				
			||||||
app.logger.setLevel(logging.INFO)
 | 
					app.logger.setLevel(logging.INFO)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@babel.localeselector
 | 
					
 | 
				
			||||||
def get_locale():
 | 
					@app.before_request
 | 
				
			||||||
    if 'lang' in session and session['lang'] in app.config['BABEL_SUPPORTED_LOCALES']:
 | 
					def enforce_https():
 | 
				
			||||||
        return session['lang']
 | 
					    if os.getenv('FORCE_HTTPS', 'False').lower() == 'true':
 | 
				
			||||||
    return request.accept_languages.best_match(app.config['BABEL_SUPPORTED_LOCALES'])
 | 
					        if request.headers.get('X-Forwarded-Proto', 'http') != 'https' and not request.is_secure:
 | 
				
			||||||
 | 
					            url = request.url.replace('http://', 'https://', 1)
 | 
				
			||||||
 | 
					            app.logger.info(f"Redirecting to HTTPS: {url}")
 | 
				
			||||||
 | 
					            return redirect(url, code=301)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@app.context_processor
 | 
					@app.context_processor
 | 
				
			||||||
def inject_template_vars():
 | 
					def inject_template_vars():
 | 
				
			||||||
    return dict(
 | 
					    def _(key, **kwargs):
 | 
				
			||||||
        get_locale=get_locale,
 | 
					        lang = session.get('lang', 'en')
 | 
				
			||||||
        theme='dark' if request.cookies.get('dark_mode') == 'true' else 'light'
 | 
					        return translate(key, lang, **kwargs)
 | 
				
			||||||
    )
 | 
					    theme = request.cookies.get('theme', 'light')
 | 
				
			||||||
 | 
					    return dict(_=_, theme=theme)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# DB Models
 | 
					# DB Models
 | 
				
			||||||
class User(db.Model, UserMixin):
 | 
					class User(db.Model, UserMixin):
 | 
				
			||||||
| 
						 | 
					@ -163,14 +199,15 @@ def index():
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@app.route('/set-lang/<lang>')
 | 
					@app.route('/set-lang/<lang>')
 | 
				
			||||||
def set_lang(lang):
 | 
					def set_lang(lang):
 | 
				
			||||||
    if lang in app.config['BABEL_SUPPORTED_LOCALES']:
 | 
					    if lang in SUPPORTED_LANGUAGES:
 | 
				
			||||||
        session['lang'] = lang
 | 
					        session['lang'] = lang
 | 
				
			||||||
    return redirect(request.referrer or url_for('index'))
 | 
					    return redirect(request.referrer or url_for('index'))
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
@app.route('/set-theme/<theme>')
 | 
					@app.route('/set-theme/<theme>')
 | 
				
			||||||
def set_theme(theme):
 | 
					def set_theme(theme):
 | 
				
			||||||
    resp = make_response('', 204)
 | 
					    resp = make_response('', 204)
 | 
				
			||||||
    resp.set_cookie('dark_mode', 'true' if theme == 'dark' else 'false', max_age=60*60*24*365)
 | 
					    # Von 'dark_mode' zu 'theme' ändern
 | 
				
			||||||
 | 
					    resp.set_cookie('theme', theme, max_age=60*60*24*365)
 | 
				
			||||||
    return resp
 | 
					    return resp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@app.route('/login', methods=['GET', 'POST'])
 | 
					@app.route('/login', methods=['GET', 'POST'])
 | 
				
			||||||
| 
						 | 
					@ -190,7 +227,7 @@ def login():
 | 
				
			||||||
@app.route('/register', methods=['GET', 'POST'])
 | 
					@app.route('/register', methods=['GET', 'POST'])
 | 
				
			||||||
def register():
 | 
					def register():
 | 
				
			||||||
    if not app.config['REGISTRATION_ENABLED']:
 | 
					    if not app.config['REGISTRATION_ENABLED']:
 | 
				
			||||||
        flash(_('Registrierungen sind deaktiviert'), 'danger')
 | 
					        flash(_('No new registrations. They are deactivated!'), 'danger')
 | 
				
			||||||
        return redirect(url_for('login'))
 | 
					        return redirect(url_for('login'))
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
    if request.method == 'POST':
 | 
					    if request.method == 'POST':
 | 
				
			||||||
| 
						 | 
					@ -224,16 +261,16 @@ def change_password():
 | 
				
			||||||
        confirm_password = request.form['confirm_password']
 | 
					        confirm_password = request.form['confirm_password']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if not check_password_hash(current_user.password, current_password):
 | 
					        if not check_password_hash(current_user.password, current_password):
 | 
				
			||||||
            flash(_('Aktuelles Passwort ist falsch'), 'danger')
 | 
					            flash(_('Current passwort is wrong'), 'danger')
 | 
				
			||||||
            return redirect(url_for('change_password'))
 | 
					            return redirect(url_for('change_password'))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if new_password != confirm_password:
 | 
					        if new_password != confirm_password:
 | 
				
			||||||
            flash(_('Neue Passwörter stimmen nicht überein'), 'danger')
 | 
					            flash(_('New Passwords are not matching'), 'danger')
 | 
				
			||||||
            return redirect(url_for('change_password'))
 | 
					            return redirect(url_for('change_password'))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        current_user.password = generate_password_hash(new_password)
 | 
					        current_user.password = generate_password_hash(new_password)
 | 
				
			||||||
        db.session.commit()
 | 
					        db.session.commit()
 | 
				
			||||||
        flash(_('Passwort erfolgreich geändert'), 'success')
 | 
					        flash(_('Password changed successfully'), 'success')
 | 
				
			||||||
        return redirect(url_for('index'))
 | 
					        return redirect(url_for('index'))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return render_template('change_password.html')
 | 
					    return render_template('change_password.html')
 | 
				
			||||||
| 
						 | 
					@ -421,6 +458,12 @@ def export_pdf():
 | 
				
			||||||
                img = Image(img_data, width=3*cm, height=img_height)
 | 
					                img = Image(img_data, width=3*cm, height=img_height)
 | 
				
			||||||
            except Exception:
 | 
					            except Exception:
 | 
				
			||||||
                img = Paragraph('', styles['Normal'])
 | 
					                img = Paragraph('', styles['Normal'])
 | 
				
			||||||
 | 
					        elif game.url and 'gog.com' in game.url:
 | 
				
			||||||
 | 
					            try:
 | 
				
			||||||
 | 
					                img_path = os.path.join(app.root_path, 'static', 'gog_logo.webp')
 | 
				
			||||||
 | 
					                img = Image(img_path, width=3*cm, height=img_height)
 | 
				
			||||||
 | 
					            except Exception:
 | 
				
			||||||
 | 
					                img = Paragraph('', styles['Normal'])
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        data.append([
 | 
					        data.append([
 | 
				
			||||||
            img or '',
 | 
					            img or '',
 | 
				
			||||||
| 
						 | 
					@ -429,7 +472,7 @@ def export_pdf():
 | 
				
			||||||
            game.redeem_date.strftime('%d.%m.%y') if game.redeem_date else ''
 | 
					            game.redeem_date.strftime('%d.%m.%y') if game.redeem_date else ''
 | 
				
			||||||
        ])
 | 
					        ])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Table format
 | 
					    # Table format (korrekte Einrückung)
 | 
				
			||||||
    table = Table(data, colWidths=col_widths, repeatRows=1)
 | 
					    table = Table(data, colWidths=col_widths, repeatRows=1)
 | 
				
			||||||
    table.setStyle(TableStyle([
 | 
					    table.setStyle(TableStyle([
 | 
				
			||||||
        ('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
 | 
					        ('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
 | 
				
			||||||
| 
						 | 
					@ -452,6 +495,7 @@ def export_pdf():
 | 
				
			||||||
        download_name=f'game_export_{datetime.now().strftime("%Y%m%d")}.pdf'
 | 
					        download_name=f'game_export_{datetime.now().strftime("%Y%m%d")}.pdf'
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@app.route('/import', methods=['GET', 'POST'])
 | 
					@app.route('/import', methods=['GET', 'POST'])
 | 
				
			||||||
@login_required
 | 
					@login_required
 | 
				
			||||||
def import_games():
 | 
					def import_games():
 | 
				
			||||||
| 
						 | 
					@ -491,15 +535,15 @@ def import_games():
 | 
				
			||||||
                    
 | 
					                    
 | 
				
			||||||
                    db.session.commit()
 | 
					                    db.session.commit()
 | 
				
			||||||
                
 | 
					                
 | 
				
			||||||
                flash(_('%(new)d neue Spiele importiert, %(dup)d Duplikate übersprungen', new=new_games, dup=duplicates), 'success')
 | 
					                flash(_('%(new)d new games imported, %(dup)d skipped duplicates', new=new_games, dup=duplicates), 'success')
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            except Exception as e:
 | 
					            except Exception as e:
 | 
				
			||||||
                db.session.rollback()
 | 
					                db.session.rollback()
 | 
				
			||||||
                flash(_('Importfehler: %(error)s', error=str(e)), 'danger')
 | 
					                flash(_('Import error: %(error)s', error=str(e)), 'danger')
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            return redirect(url_for('index'))
 | 
					            return redirect(url_for('index'))
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        flash(_('Bitte eine gültige CSV-Datei hochladen.'), 'danger')
 | 
					        flash(_('Please upload a valid CSV file.'), 'danger')
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    return render_template('import.html')
 | 
					    return render_template('import.html')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -557,81 +601,29 @@ def redeem_page(token):
 | 
				
			||||||
                         redeem_token=redeem_token,
 | 
					                         redeem_token=redeem_token,
 | 
				
			||||||
                         platform_link='https://store.steampowered.com/account/registerkey?key=' if game.steam_appid else 'https://www.gog.com/redeem')
 | 
					                         platform_link='https://store.steampowered.com/account/registerkey?key=' if game.steam_appid else 'https://www.gog.com/redeem')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Benachrichtigungsfunktionen
 | 
					# Apprise Notifications
 | 
				
			||||||
def send_pushover_notification(user, game):
 | 
					import apprise
 | 
				
			||||||
    """Sendet Pushover-Benachrichtigung für ablaufenden Key"""
 | 
					
 | 
				
			||||||
    if not app.config['PUSHOVER_APP_TOKEN'] or not app.config['PUSHOVER_USER_KEY']:
 | 
					def send_apprise_notification(user, game):
 | 
				
			||||||
 | 
					    apprise_urls = os.getenv('APPRISE_URLS', '').strip()
 | 
				
			||||||
 | 
					    if not apprise_urls:
 | 
				
			||||||
 | 
					        app.logger.error("No APPRISE_URLS configured")
 | 
				
			||||||
        return False
 | 
					        return False
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    payload = {
 | 
					    apobj = apprise.Apprise()
 | 
				
			||||||
        "token": os.getenv('PUSHOVER_APP_TOKEN'),
 | 
					    for url in apprise_urls.replace(',', '\n').splitlines():
 | 
				
			||||||
        "user": os.getenv('PUSHOVER_USER_KEY'),
 | 
					        if url.strip():
 | 
				
			||||||
        "title": "Steam-Key läuft ab!",
 | 
					            apobj.add(url.strip())
 | 
				
			||||||
        "message": f"Dein Key für '{game.name}' läuft in weniger als 48 Stunden ab!",
 | 
					 | 
				
			||||||
        "url": url_for('edit_game', game_id=game.id, _external=True),
 | 
					 | 
				
			||||||
        "url_title": "Zum Spiel",
 | 
					 | 
				
			||||||
        "priority": 1
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    try:
 | 
					    edit_url = url_for('edit_game', game_id=game.id, _external=True)
 | 
				
			||||||
        response = requests.post(
 | 
					    result = apobj.notify(
 | 
				
			||||||
            'https://api.pushover.net/1/messages.json', 
 | 
					        title="Steam-Key läuft ab!",
 | 
				
			||||||
            data=payload
 | 
					        body=f"Dein Key für '{game.name}' läuft in weniger als 48 Stunden ab!\n\nLink: {edit_url}",
 | 
				
			||||||
        )
 | 
					    )
 | 
				
			||||||
        return response.status_code == 200
 | 
					    return result
 | 
				
			||||||
    except Exception as e:
 | 
					 | 
				
			||||||
        app.logger.error(f"Pushover error: {str(e)}")
 | 
					 | 
				
			||||||
        return False
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def send_gotify_notification(user, game):
 | 
					 | 
				
			||||||
    """Sendet Gotify-Benachrichtigung für ablaufenden Key"""
 | 
					 | 
				
			||||||
    if not GOTIFY_URL or not GOTIFY_TOKEN:
 | 
					 | 
				
			||||||
        return False
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
    payload = {
 | 
					 | 
				
			||||||
        "title": "Steam-Key läuft ab!",
 | 
					 | 
				
			||||||
        "message": f"Dein Key für '{game.name}' läuft in weniger als 48 Stunden ab!",
 | 
					 | 
				
			||||||
        "priority": 5
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    try:
 | 
					 | 
				
			||||||
        response = requests.post(
 | 
					 | 
				
			||||||
            f"{GOTIFY_URL}/message?token={GOTIFY_TOKEN}", 
 | 
					 | 
				
			||||||
            json=payload
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        return response.status_code == 200
 | 
					 | 
				
			||||||
    except Exception as e:
 | 
					 | 
				
			||||||
        app.logger.error(f"Gotify error: {str(e)}")
 | 
					 | 
				
			||||||
        return False
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def send_matrix_notification(user, game):
 | 
					 | 
				
			||||||
    """Sendet Matrix-Benachrichtigung für ablaufenden Key"""
 | 
					 | 
				
			||||||
    if not MATRIX_HOMESERVER or not MATRIX_ACCESS_TOKEN or not MATRIX_ROOM_ID:
 | 
					 | 
				
			||||||
        return False
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    try:
 | 
					 | 
				
			||||||
        from matrix_client.client import MatrixClient
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        client = MatrixClient(MATRIX_HOMESERVER, token=MATRIX_ACCESS_TOKEN)
 | 
					 | 
				
			||||||
        room = client.join_room(MATRIX_ROOM_ID)
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        message = f"🎮 Dein Key für '{game.name}' läuft in weniger als 48 Stunden ab!"
 | 
					 | 
				
			||||||
        room.send_text(message)
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        return True
 | 
					 | 
				
			||||||
    except Exception as e:
 | 
					 | 
				
			||||||
        app.logger.error(f"Matrix error: {str(e)}")
 | 
					 | 
				
			||||||
        return False
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
def send_notification(user, game):
 | 
					def send_notification(user, game):
 | 
				
			||||||
    """Sendet Benachrichtigung über den bevorzugten Dienst des Benutzers"""
 | 
					    return send_apprise_notification(user, game)
 | 
				
			||||||
    if user.notification_service == 'pushover':
 | 
					 | 
				
			||||||
        return send_pushover_notification(user, game)
 | 
					 | 
				
			||||||
    elif user.notification_service == 'gotify':
 | 
					 | 
				
			||||||
        return send_gotify_notification(user, game)
 | 
					 | 
				
			||||||
    elif user.notification_service == 'matrix':
 | 
					 | 
				
			||||||
        return send_matrix_notification(user, game)
 | 
					 | 
				
			||||||
    return False
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
def check_expiring_keys():
 | 
					def check_expiring_keys():
 | 
				
			||||||
    with app.app_context():
 | 
					    with app.app_context():
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,3 +0,0 @@
 | 
				
			||||||
[python: **.py]
 | 
					 | 
				
			||||||
[jinja2: **/templates/**.html]
 | 
					 | 
				
			||||||
extensions=jinja2.ext.autoescape,jinja2.ext.with_
 | 
					 | 
				
			||||||
| 
						 | 
					@ -8,7 +8,8 @@ services:
 | 
				
			||||||
      - TZ=
 | 
					      - TZ=
 | 
				
			||||||
    volumes:
 | 
					    volumes:
 | 
				
			||||||
      - ../data:/app/data
 | 
					      - ../data:/app/data
 | 
				
			||||||
      - ../translations:/app/translations
 | 
					      - ./translations:/app/translations:rw
 | 
				
			||||||
      - ../.env:/app/.env
 | 
					      - ../.env:/app/.env
 | 
				
			||||||
    user: "1000:1000"
 | 
					    user: "0:"
 | 
				
			||||||
    restart: unless-stopped
 | 
					    restart: unless-stopped
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,13 +5,12 @@ flask-migrate
 | 
				
			||||||
werkzeug
 | 
					werkzeug
 | 
				
			||||||
python-dotenv
 | 
					python-dotenv
 | 
				
			||||||
flask-sqlalchemy
 | 
					flask-sqlalchemy
 | 
				
			||||||
flask-babel
 | 
					 | 
				
			||||||
jinja2<3.1.0
 | 
					jinja2<3.1.0
 | 
				
			||||||
itsdangerous
 | 
					itsdangerous
 | 
				
			||||||
sqlalchemy
 | 
					sqlalchemy
 | 
				
			||||||
apscheduler
 | 
					apscheduler
 | 
				
			||||||
matrix-client
 | 
					 | 
				
			||||||
reportlab
 | 
					reportlab
 | 
				
			||||||
requests
 | 
					requests
 | 
				
			||||||
pillow
 | 
					pillow
 | 
				
			||||||
gunicorn
 | 
					gunicorn
 | 
				
			||||||
 | 
					apprise
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1 +0,0 @@
 | 
				
			||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 212 212" width="32" height="32"><style>circle,path{fill:none;stroke:#000;stroke-width:15}path{stroke-width:25}.orange{stroke:#f60}.red{stroke:#d40000}</style><g transform="translate(6 6)"><path d="M58 168V70a50 50 0 0 1 50-50h20" class="orange"/><path d="M58 168v-30a50 50 0 0 1 50-50h20" class="red"/><circle cx="142" cy="20" r="18" class="orange"/><circle cx="142" cy="88" r="18" class="red"/><circle cx="58" cy="180" r="18" class="red"/></g></svg>
 | 
					 | 
				
			||||||
| 
		 Before Width: | Height: | Size: 503 B  | 
| 
		 Before Width: | Height: | Size: 740 B  | 
| 
		 Before Width: | Height: | Size: 68 KiB  | 
| 
		 Before Width: | Height: | Size: 6.8 KiB  | 
| 
		 Before Width: | Height: | Size: 52 KiB  | 
| 
		 Before Width: | Height: | Size: 8.1 KiB  | 
| 
		 Before Width: | Height: | Size: 35 KiB  | 
| 
		 Before Width: | Height: | Size: 5.9 KiB  | 
| 
		 Before Width: | Height: | Size: 20 KiB  | 
| 
		 Before Width: | Height: | Size: 2.5 KiB  | 
							
								
								
									
										34
									
								
								steam-gift-manager/static/manifest.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,34 @@
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  "id": "/",
 | 
				
			||||||
 | 
					  "name": "Game Key Manager",
 | 
				
			||||||
 | 
					  "short_name": "GameKeys",
 | 
				
			||||||
 | 
					  "start_url": "/",
 | 
				
			||||||
 | 
					  "display": "standalone",
 | 
				
			||||||
 | 
					  "background_color": "#212529",
 | 
				
			||||||
 | 
					  "theme_color": "#212529",
 | 
				
			||||||
 | 
					  "description": "Manage Steam/GOG keys easily!",
 | 
				
			||||||
 | 
					  "orientation": "any",
 | 
				
			||||||
 | 
					  "launch_handler": {
 | 
				
			||||||
 | 
					    "client_mode": "navigate-existing"
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  "icons": [
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      "src": "/static/logo_small.webp",
 | 
				
			||||||
 | 
					      "sizes": "192x192",
 | 
				
			||||||
 | 
					      "type": "image/webp",
 | 
				
			||||||
 | 
					      "purpose": "any"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      "src": "/static/logo_small_maskable.webp",
 | 
				
			||||||
 | 
					      "sizes": "192x192",
 | 
				
			||||||
 | 
					      "type": "image/webp",
 | 
				
			||||||
 | 
					      "purpose": "maskable"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      "src": "/static/logo.webp",
 | 
				
			||||||
 | 
					      "sizes": "512x512",
 | 
				
			||||||
 | 
					      "type": "image/webp",
 | 
				
			||||||
 | 
					      "purpose": "any maskable"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  ]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										32
									
								
								steam-gift-manager/static/serviceworker.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,32 @@
 | 
				
			||||||
 | 
					const CACHE_NAME = 'game-key-manager-v2';
 | 
				
			||||||
 | 
					const ASSETS = [
 | 
				
			||||||
 | 
					  '/',
 | 
				
			||||||
 | 
					  '/static/style.css',
 | 
				
			||||||
 | 
					  '/static/logo.webp',
 | 
				
			||||||
 | 
					  '/static/logo_small.webp',
 | 
				
			||||||
 | 
					  '/static/gog_logo.webp',
 | 
				
			||||||
 | 
					  '/static/forgejo.webp'
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					self.addEventListener('install', (event) => {
 | 
				
			||||||
 | 
					  event.waitUntil(
 | 
				
			||||||
 | 
					    caches.open(CACHE_NAME)
 | 
				
			||||||
 | 
					      .then(cache => cache.addAll(ASSETS))
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					self.addEventListener('fetch', (event) => {
 | 
				
			||||||
 | 
					  event.respondWith(
 | 
				
			||||||
 | 
					    caches.match(event.request)
 | 
				
			||||||
 | 
					      .then(cachedResponse => cachedResponse || fetch(event.request))
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					self.addEventListener('activate', (event) => {
 | 
				
			||||||
 | 
					  event.waitUntil(
 | 
				
			||||||
 | 
					    caches.keys().then(keys => Promise.all(
 | 
				
			||||||
 | 
					      keys.filter(key => key !== CACHE_NAME)
 | 
				
			||||||
 | 
					        .map(key => caches.delete(key))
 | 
				
			||||||
 | 
					    ))
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
| 
						 | 
					@ -60,3 +60,76 @@ body {
 | 
				
			||||||
.table-pdf td, .table-pdf th {
 | 
					.table-pdf td, .table-pdf th {
 | 
				
			||||||
    padding: 4px 8px;
 | 
					    padding: 4px 8px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.badge.bg-warning {
 | 
				
			||||||
 | 
					    background-color: #ffcc00 !important;
 | 
				
			||||||
 | 
					    color: #222 !important;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.badge.bg-success {
 | 
				
			||||||
 | 
					    background-color: #198754 !important;
 | 
				
			||||||
 | 
					    color: #fff !important;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.game-cover {
 | 
				
			||||||
 | 
					    width: 368px;
 | 
				
			||||||
 | 
					    height: 172px;
 | 
				
			||||||
 | 
					    max-width: 100%;
 | 
				
			||||||
 | 
					    max-height: 35vw;
 | 
				
			||||||
 | 
					    object-fit: contain;
 | 
				
			||||||
 | 
					    background: #222;
 | 
				
			||||||
 | 
					    border-radius: 8px;
 | 
				
			||||||
 | 
					    display: block;
 | 
				
			||||||
 | 
					    margin: 0 auto;
 | 
				
			||||||
 | 
					    transition: width 0.2s, height 0.2s;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Responsive Cover Images */
 | 
				
			||||||
 | 
					.game-cover {
 | 
				
			||||||
 | 
					    width: 368px;
 | 
				
			||||||
 | 
					    height: 172px;
 | 
				
			||||||
 | 
					    object-fit: contain;
 | 
				
			||||||
 | 
					    background: #222;
 | 
				
			||||||
 | 
					    border-radius: 6px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@media (max-width: 1200px) {
 | 
				
			||||||
 | 
					    .game-cover {
 | 
				
			||||||
 | 
					        width: 260px;
 | 
				
			||||||
 | 
					        height: 122px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@media (max-width: 992px) {
 | 
				
			||||||
 | 
					    .game-cover {
 | 
				
			||||||
 | 
					        width: 180px;
 | 
				
			||||||
 | 
					        height: 84px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@media (max-width: 768px) {
 | 
				
			||||||
 | 
					    .game-cover {
 | 
				
			||||||
 | 
					        width: 120px;
 | 
				
			||||||
 | 
					        height: 56px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@media (max-width: 576px) {
 | 
				
			||||||
 | 
					    .game-cover {
 | 
				
			||||||
 | 
					        width: 90px;
 | 
				
			||||||
 | 
					        height: 42px;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Accessibility Improvements */
 | 
				
			||||||
 | 
					.visually-hidden {
 | 
				
			||||||
 | 
					    position: absolute;
 | 
				
			||||||
 | 
					    width: 1px;
 | 
				
			||||||
 | 
					    height: 1px;
 | 
				
			||||||
 | 
					    padding: 0;
 | 
				
			||||||
 | 
					    margin: -1px;
 | 
				
			||||||
 | 
					    overflow: hidden;
 | 
				
			||||||
 | 
					    clip: rect(0, 0, 0, 0);
 | 
				
			||||||
 | 
					    border: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,40 +2,44 @@
 | 
				
			||||||
{% block content %}
 | 
					{% block content %}
 | 
				
			||||||
<div class="card p-4 shadow-sm">
 | 
					<div class="card p-4 shadow-sm">
 | 
				
			||||||
    <h2 class="mb-4">{{ _('Add New Game') }}</h2>
 | 
					    <h2 class="mb-4">{{ _('Add New Game') }}</h2>
 | 
				
			||||||
    <form method="POST">
 | 
					    <form method="POST" aria-label="{{ _('Add New Game') }}">
 | 
				
			||||||
	<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
 | 
					        <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
 | 
				
			||||||
        <div class="row g-3">
 | 
					        <div class="row g-3">
 | 
				
			||||||
            <div class="col-md-6">
 | 
					            <div class="col-md-6">
 | 
				
			||||||
                <label class="form-label">{{ _('Name') }} *</label>
 | 
					                <label for="game_name" class="form-label">{{ _('Name') }} <span aria-hidden="true" class="text-danger">*</span></label>
 | 
				
			||||||
                <input type="text" name="name" class="form-control" required>
 | 
					                <input type="text" id="game_name" name="name" class="form-control" required aria-required="true">
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
            <div class="col-md-6">
 | 
					            <div class="col-md-6">
 | 
				
			||||||
                <label class="form-label">{{ _('Game Key') }} *</label>
 | 
					                <label for="game_key" class="form-label">{{ _('Game Key') }} <span aria-hidden="true" class="text-danger">*</span></label>
 | 
				
			||||||
                <input type="text" name="steam_key" class="form-control" required>
 | 
					                <input type="text" id="game_key" name="steam_key" class="form-control" required aria-required="true">
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
            <div class="col-md-4">
 | 
					            <div class="col-md-4">
 | 
				
			||||||
                <label class="form-label">{{ _('Status') }} *</label>
 | 
					                <label for="game_status" class="form-label">{{ _('Status') }} <span aria-hidden="true" class="text-danger">*</span></label>
 | 
				
			||||||
                <select name="status" class="form-select" required>
 | 
					                <select id="game_status" name="status" class="form-select" required aria-required="true">
 | 
				
			||||||
                    <option value="nicht eingelöst">{{ _('Not redeemed') }}</option>
 | 
					                    <option value="nicht eingelöst">{{ _('Not redeemed') }}</option>
 | 
				
			||||||
                    <option value="verschenkt">{{ _('Gifted') }}</option>
 | 
					                    <option value="verschenkt">{{ _('Gifted') }}</option>
 | 
				
			||||||
                    <option value="eingelöst">{{ _('Redeemed') }}</option>
 | 
					                    <option value="eingelöst">{{ _('Redeemed') }}</option>
 | 
				
			||||||
                </select>
 | 
					                </select>
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
            <div class="col-md-4">
 | 
					            <div class="col-md-4">
 | 
				
			||||||
                <label class="form-label">{{ _('Redeem by') }}</label>
 | 
					                <label for="game_redeem_date" class="form-label">{{ _('Redeem by') }}</label>
 | 
				
			||||||
                <input type="date" name="redeem_date" class="form-control">
 | 
					                <input type="date" id="game_redeem_date" name="redeem_date" class="form-control">
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
            <div class="col-md-4">
 | 
					            <div class="col-md-4">
 | 
				
			||||||
                <label class="form-label">{{ _('Recipient') }}</label>
 | 
					                <label for="game_recipient" class="form-label">{{ _('Recipient') }}</label>
 | 
				
			||||||
                <input type="text" name="recipient" class="form-control">
 | 
					                <input type="text" id="game_recipient" name="recipient" class="form-control">
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div class="col-md-6">
 | 
				
			||||||
 | 
					                <label for="game_appid" class="form-label">{{ _('Steam AppID (optional)') }}</label>
 | 
				
			||||||
 | 
					                <input type="text" id="game_appid" name="steam_appid" class="form-control">
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					            <div class="col-md-6">
 | 
				
			||||||
 | 
					                <label for="game_url" class="form-label">{{ _('Shop URL') }}</label>
 | 
				
			||||||
 | 
					                <input type="url" id="game_url" name="url" class="form-control">
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
            <div class="col-12">
 | 
					            <div class="col-12">
 | 
				
			||||||
                <label class="form-label">{{ _('Shop URL') }}</label>
 | 
					                <label for="game_notes" class="form-label">{{ _('Notes') }}</label>
 | 
				
			||||||
                <input type="url" name="url" class="form-control">
 | 
					                <textarea id="game_notes" name="notes" class="form-control" rows="3"></textarea>
 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
            <div class="col-12">
 | 
					 | 
				
			||||||
                <label class="form-label">{{ _('Notes') }}</label>
 | 
					 | 
				
			||||||
                <textarea name="notes" class="form-control" rows="3"></textarea>
 | 
					 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
            <div class="col-12">
 | 
					            <div class="col-12">
 | 
				
			||||||
                <button type="submit" class="btn btn-success">{{ _('Save') }}</button>
 | 
					                <button type="submit" class="btn btn-success">{{ _('Save') }}</button>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,28 +1,57 @@
 | 
				
			||||||
<!DOCTYPE html>
 | 
					<!DOCTYPE html>
 | 
				
			||||||
<html lang="{{ get_locale() }}" data-bs-theme="{{ theme }}">
 | 
					<html lang="{{ session.get('lang', 'en') }}" data-bs-theme="{{ theme }}">
 | 
				
			||||||
<head>
 | 
					<head>
 | 
				
			||||||
    <meta charset="UTF-8">
 | 
					    <meta charset="UTF-8">
 | 
				
			||||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
					    <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
				
			||||||
    <meta name="csrf-token" content="{{ csrf_token() }}">
 | 
					    <meta name="csrf-token" content="{{ csrf_token() }}">
 | 
				
			||||||
 | 
					    <meta name="description" content="Manage your Steam and GOG keys efficiently. Track redemption dates, share games, and export lists.">
 | 
				
			||||||
 | 
					    <meta name="theme-color" content="#212529">
 | 
				
			||||||
 | 
					    <link rel="manifest" href="{{ url_for('static', filename='manifest.json') }}">
 | 
				
			||||||
    <title>{{ _('Game Key Manager') }}</title>
 | 
					    <title>{{ _('Game Key Manager') }}</title>
 | 
				
			||||||
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
 | 
					    <!-- Preload Bootstrap CSS for better LCP -->
 | 
				
			||||||
 | 
					    <link rel="preload" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
 | 
				
			||||||
 | 
					    <noscript><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"></noscript>
 | 
				
			||||||
 | 
					    <!-- Eigene Styles -->
 | 
				
			||||||
    <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
 | 
					    <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
 | 
				
			||||||
 | 
					    {# LCP-Optimierung: Preload für das erste Cover-Bild, falls vorhanden #}
 | 
				
			||||||
 | 
					    {% if games and games[0].steam_appid %}
 | 
				
			||||||
 | 
					    <link rel="preload"
 | 
				
			||||||
 | 
					          as="image"
 | 
				
			||||||
 | 
					          href="https://cdn.cloudflare.steamstatic.com/steam/apps/{{ games[0].steam_appid }}/header.jpg"
 | 
				
			||||||
 | 
					          imagesrcset="https://cdn.cloudflare.steamstatic.com/steam/apps/{{ games[0].steam_appid }}/header.jpg 368w"
 | 
				
			||||||
 | 
					          fetchpriority="high"
 | 
				
			||||||
 | 
					          type="image/jpeg">
 | 
				
			||||||
 | 
					    {% endif %}
 | 
				
			||||||
</head>
 | 
					</head>
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
 | 
					(function() {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    var theme = localStorage.getItem('theme');
 | 
				
			||||||
 | 
					    if (!theme) {
 | 
				
			||||||
 | 
					      // Systempräferenz als Fallback
 | 
				
			||||||
 | 
					      theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    document.documentElement.setAttribute('data-bs-theme', theme);
 | 
				
			||||||
 | 
					  } catch(e) {}
 | 
				
			||||||
 | 
					})();
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
<body>
 | 
					<body>
 | 
				
			||||||
    <nav class="navbar navbar-expand-lg bg-body-tertiary">
 | 
					    <nav class="navbar navbar-expand-lg bg-body-tertiary">
 | 
				
			||||||
        <div class="container">
 | 
					        <div class="container">
 | 
				
			||||||
            <a class="navbar-brand d-flex align-items-center gap-2" href="/">
 | 
					            <a class="navbar-brand d-flex align-items-center gap-2" href="/">
 | 
				
			||||||
                 <img src="{{ url_for('static', filename='logo_small.png') }}" alt="Logo" width="150" height="116" style="object-fit:contain; border-radius:8px;">
 | 
					                <img src="{{ url_for('static', filename='logo_small.webp') }}" alt="Logo" width="150" height="116" style="object-fit:contain; border-radius:8px;">
 | 
				
			||||||
                 <span>Game Key Manager</span>
 | 
					                <span>Game Key Manager</span>
 | 
				
			||||||
            </a>
 | 
					            </a>
 | 
				
			||||||
            <div class="d-flex align-items-center gap-3">
 | 
					            <div class="d-flex align-items-center gap-3">
 | 
				
			||||||
                <form class="d-flex" action="{{ url_for('index') }}" method="GET">
 | 
					                <form class="d-flex" action="{{ url_for('index') }}" method="GET" role="search" aria-label="{{ _('Search games') }}">
 | 
				
			||||||
 | 
					                    <label for="searchInput" class="visually-hidden">{{ _('Search') }}</label>
 | 
				
			||||||
                    <input class="form-control me-2" 
 | 
					                    <input class="form-control me-2" 
 | 
				
			||||||
                           type="search" 
 | 
					                           type="search" 
 | 
				
			||||||
                           name="q"
 | 
					                           name="q"
 | 
				
			||||||
 | 
					                           id="searchInput"
 | 
				
			||||||
                           placeholder="{{ _('Search') }}"
 | 
					                           placeholder="{{ _('Search') }}"
 | 
				
			||||||
                           value="{{ search_query }}">
 | 
					                           value="{{ search_query }}">
 | 
				
			||||||
                    <button class="btn btn-outline-success" type="submit">🔍</button>
 | 
					                    <button class="btn btn-outline-success" type="submit" aria-label="{{ _('Search') }}">🔍</button>
 | 
				
			||||||
                </form>
 | 
					                </form>
 | 
				
			||||||
                <div class="form-check form-switch">
 | 
					                <div class="form-check form-switch">
 | 
				
			||||||
                    <input class="form-check-input"
 | 
					                    <input class="form-check-input"
 | 
				
			||||||
| 
						 | 
					@ -31,21 +60,22 @@
 | 
				
			||||||
                    <label class="form-check-label" for="darkModeSwitch">{{ _('Dark Mode') }}</label>
 | 
					                    <label class="form-check-label" for="darkModeSwitch">{{ _('Dark Mode') }}</label>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
                <div class="dropdown ms-3">
 | 
					                <div class="dropdown ms-3">
 | 
				
			||||||
 | 
					                    <div hidden id="locale-debug" data-locale="{{ session.get('lang', 'en') }}"></div>
 | 
				
			||||||
                    <button class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
 | 
					                    <button class="btn btn-outline-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
 | 
				
			||||||
                        {% if get_locale() == 'de' %} Deutsch {% elif get_locale() == 'en' %} English {% else %} Sprache {% endif %}
 | 
					                        {% if session.get('lang', 'en') == 'de' %} Deutsch {% elif session.get('lang', 'en') == 'en' %} English {% else %} Sprache {% endif %}
 | 
				
			||||||
                    </button>
 | 
					                    </button>
 | 
				
			||||||
                    <ul class="dropdown-menu">
 | 
					                    <ul class="dropdown-menu">
 | 
				
			||||||
                        <li><a class="dropdown-item {% if get_locale() == 'de' %}active{% endif %}" href="{{ url_for('set_lang', lang='de') }}">Deutsch</a></li>
 | 
					                        <li><a class="dropdown-item {% if session.get('lang', 'en') == 'de' %}active{% endif %}" href="{{ url_for('set_lang', lang='de') }}">Deutsch</a></li>
 | 
				
			||||||
                        <li><a class="dropdown-item {% if get_locale() == 'en' %}active{% endif %}" href="{{ url_for('set_lang', lang='en') }}">English</a></li>
 | 
					                        <li><a class="dropdown-item {% if session.get('lang', 'en') == 'en' %}active{% endif %}" href="{{ url_for('set_lang', lang='en') }}">English</a></li>
 | 
				
			||||||
                    </ul>
 | 
					                    </ul>
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
                {% if current_user.is_authenticated %}
 | 
					                {% if current_user.is_authenticated %}
 | 
				
			||||||
                        <li class="nav-item">
 | 
					                    <li class="nav-item">
 | 
				
			||||||
                             <a class="nav-link" href="{{ url_for('change_password') }}">🔒 {{ _('Passwort') }}</a>
 | 
					                        <a class="nav-link" href="{{ url_for('change_password') }}">🔒 {{ _('Password') }}</a>
 | 
				
			||||||
                        </li>
 | 
					                    </li>
 | 
				
			||||||
                        <li class="nav-item">
 | 
					                    <li class="nav-item">
 | 
				
			||||||
                             <a class="nav-link" href="{{ url_for('logout') }}">🚪 {{ _('Logout') }}</a>
 | 
					                        <a class="nav-link" href="{{ url_for('logout') }}">🚪 {{ _('Logout') }}</a>
 | 
				
			||||||
                        </li>
 | 
					                    </li>
 | 
				
			||||||
                {% endif %}
 | 
					                {% endif %}
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
| 
						 | 
					@ -65,16 +95,44 @@
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
 | 
					    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
 | 
				
			||||||
    <script>
 | 
					    <script>
 | 
				
			||||||
 | 
					    // Service Worker Registration for PWA
 | 
				
			||||||
 | 
					    if ('serviceWorker' in navigator) {
 | 
				
			||||||
 | 
					      window.addEventListener('load', () => {
 | 
				
			||||||
 | 
					        navigator.serviceWorker.register('{{ url_for("static", filename="serviceworker.js") }}', {scope: '/'})
 | 
				
			||||||
 | 
					          .then(registration => {
 | 
				
			||||||
 | 
					            console.log('ServiceWorker registered:', registration.scope);
 | 
				
			||||||
 | 
					          })
 | 
				
			||||||
 | 
					          .catch(error => {
 | 
				
			||||||
 | 
					            console.log('ServiceWorker registration failed:', error);
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    // Dark Mode Switch
 | 
				
			||||||
    document.addEventListener('DOMContentLoaded', function() {
 | 
					    document.addEventListener('DOMContentLoaded', function() {
 | 
				
			||||||
        const toggle = document.getElementById('darkModeSwitch')
 | 
					        const toggle = document.getElementById('darkModeSwitch');
 | 
				
			||||||
        const html = document.documentElement
 | 
					        const html = document.documentElement;
 | 
				
			||||||
 | 
					        if (toggle) {
 | 
				
			||||||
 | 
					            toggle.checked = (html.getAttribute('data-bs-theme') === 'dark')
 | 
				
			||||||
 | 
					            toggle.addEventListener('change', function() {
 | 
				
			||||||
 | 
					                const theme = this.checked ? 'dark' : 'light';
 | 
				
			||||||
 | 
					                document.cookie = "theme=" + theme + ";path=/;max-age=31536000";
 | 
				
			||||||
 | 
					                html.setAttribute('data-bs-theme', theme);
 | 
				
			||||||
 | 
					                fetch('/set-theme/' + theme);
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        // Set theme on page load
 | 
				
			||||||
 | 
					        function getThemeCookie() {
 | 
				
			||||||
 | 
					          const cookies = document.cookie.split(';');
 | 
				
			||||||
 | 
					            for (let cookie of cookies) {
 | 
				
			||||||
 | 
					              const [name, value] = cookie.trim().split('=');
 | 
				
			||||||
 | 
					              if (name === 'theme') return value;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return null;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        const savedTheme = getThemeCookie() || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
 | 
				
			||||||
 | 
					        document.documentElement.setAttribute('data-bs-theme', savedTheme);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        toggle.addEventListener('change', function() {
 | 
					 | 
				
			||||||
            const theme = this.checked ? 'dark' : 'light'
 | 
					 | 
				
			||||||
            fetch('/set-theme/' + theme)
 | 
					 | 
				
			||||||
                .then(() => html.setAttribute('data-bs-theme', theme))
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
    </script>
 | 
					    </script>
 | 
				
			||||||
{% include "footer.html" %}
 | 
					{% include "footer.html" %}
 | 
				
			||||||
</body>
 | 
					</body>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,22 +1,28 @@
 | 
				
			||||||
{% extends "base.html" %}
 | 
					{% extends "base.html" %}
 | 
				
			||||||
{% block content %}
 | 
					{% block content %}
 | 
				
			||||||
<div class="card p-4 shadow-sm">
 | 
					<div class="row justify-content-center">
 | 
				
			||||||
    <h2 class="mb-4">{{ _('Change Password') }}</h2>
 | 
					  <div class="col-md-6 col-lg-5">
 | 
				
			||||||
    <form method="POST">
 | 
					    <div class="card p-4 shadow-sm">
 | 
				
			||||||
 | 
					      <h2 class="mb-4">{{ _('Change Password') }}</h2>
 | 
				
			||||||
 | 
					      <form method="POST" aria-label="{{ _('Change password form') }}">
 | 
				
			||||||
        <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
 | 
					        <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
 | 
				
			||||||
        <div class="mb-3">
 | 
					        <div class="mb-3">
 | 
				
			||||||
            <label class="form-label">{{ _('Current Password') }}</label>
 | 
					          <label for="current_password" class="form-label">{{ _('Current Password') }} <span aria-hidden="true" class="text-danger">*</span></label>
 | 
				
			||||||
            <input type="password" name="current_password" class="form-control" required>
 | 
					          <input type="password" id="current_password" name="current_password" class="form-control" required autocomplete="current-password" aria-required="true">
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <div class="mb-3">
 | 
					        <div class="mb-3">
 | 
				
			||||||
            <label class="form-label">{{ _('New Password') }}</label>
 | 
					          <label for="new_password" class="form-label">{{ _('New Password') }} <span aria-hidden="true" class="text-danger">*</span></label>
 | 
				
			||||||
            <input type="password" name="new_password" class="form-control" required>
 | 
					          <input type="password" id="new_password" name="new_password" class="form-control" required autocomplete="new-password" aria-required="true">
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <div class="mb-3">
 | 
					        <div class="mb-3">
 | 
				
			||||||
            <label class="form-label">{{ _('Confirm New Password') }}</label>
 | 
					          <label for="confirm_password" class="form-label">{{ _('Confirm New Password') }} <span aria-hidden="true" class="text-danger">*</span></label>
 | 
				
			||||||
            <input type="password" name="confirm_password" class="form-control" required>
 | 
					          <input type="password" id="confirm_password" name="confirm_password" class="form-control" required autocomplete="new-password" aria-required="true">
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <button type="submit" class="btn btn-primary">{{ _('Change Password') }}</button>
 | 
					        <button type="submit" class="btn btn-primary">{{ _('Change Password') }}</button>
 | 
				
			||||||
    </form>
 | 
					        <a href="{{ url_for('index') }}" class="btn btn-outline-secondary ms-2">{{ _('Cancel') }}</a>
 | 
				
			||||||
 | 
					      </form>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,66 +1,67 @@
 | 
				
			||||||
{% extends "base.html" %}
 | 
					{% extends "base.html" %}
 | 
				
			||||||
{% block content %}
 | 
					{% block content %}
 | 
				
			||||||
<div class="card p-4 shadow-sm">
 | 
					<div class="card p-4 shadow-sm">
 | 
				
			||||||
    <h2 class="mb-4">{{ _('Edit Game') }}</h2>
 | 
					  <h2 class="mb-4">{{ _('Edit Game') }}</h2>
 | 
				
			||||||
    <form method="POST">
 | 
					  <form method="POST" aria-label="{{ _('Edit Game') }}">
 | 
				
			||||||
        <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
 | 
					    <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
 | 
				
			||||||
        <div class="row g-3">
 | 
					    <div class="row g-3">
 | 
				
			||||||
            <div class="col-md-6">
 | 
					      <div class="col-md-6">
 | 
				
			||||||
                <label class="form-label">{{ _('Name') }} *</label>
 | 
					        <label for="game_name" class="form-label">{{ _('Name') }} <span aria-hidden="true" class="text-danger">*</span></label>
 | 
				
			||||||
                <input type="text" name="name" class="form-control" value="{{ game.name }}" required>
 | 
					        <input type="text" id="game_name" name="name" class="form-control" value="{{ game.name }}" required aria-required="true">
 | 
				
			||||||
            </div>
 | 
					      </div>
 | 
				
			||||||
            <div class="col-md-6">
 | 
					      <div class="col-md-6">
 | 
				
			||||||
                <label class="form-label">{{ _('Game Key') }} *</label>
 | 
					        <label for="game_key" class="form-label">{{ _('Game Key') }} <span aria-hidden="true" class="text-danger">*</span></label>
 | 
				
			||||||
                <input type="text" name="steam_key" class="form-control" value="{{ game.steam_key }}" required>
 | 
					        <input type="text" id="game_key" name="steam_key" class="form-control" value="{{ game.steam_key }}" required aria-required="true">
 | 
				
			||||||
            </div>
 | 
					      </div>
 | 
				
			||||||
            <div class="col-md-6">
 | 
					      <div class="col-md-6">
 | 
				
			||||||
                <label class="form-label">{{ _('Steam AppID (optional)') }}</label>
 | 
					        <label for="game_appid" class="form-label">{{ _('Steam AppID (optional)') }}</label>
 | 
				
			||||||
                <input type="text" name="steam_appid" class="form-control" value="{{ game.steam_appid or '' }}">
 | 
					        <input type="text" id="game_appid" name="steam_appid" class="form-control" value="{{ game.steam_appid or '' }}">
 | 
				
			||||||
            </div>
 | 
					      </div>
 | 
				
			||||||
            <div class="col-md-4">
 | 
					      <div class="col-md-4">
 | 
				
			||||||
                <label class="form-label">{{ _('Status') }} *</label>
 | 
					        <label for="game_status" class="form-label">{{ _('Status') }} <span aria-hidden="true" class="text-danger">*</span></label>
 | 
				
			||||||
                <select name="status" class="form-select" required>
 | 
					        <select id="game_status" name="status" class="form-select" required aria-required="true">
 | 
				
			||||||
                    <option value="nicht eingelöst" {% if game.status == 'nicht eingelöst' %}selected{% endif %}>{{ _('Not redeemed') }}</option>
 | 
					          <option value="nicht eingelöst" {% if game.status == 'nicht eingelöst' %}selected{% endif %}>{{ _('Not redeemed') }}</option>
 | 
				
			||||||
                    <option value="verschenkt" {% if game.status == 'verschenkt' %}selected{% endif %}>{{ _('Gifted') }}</option>
 | 
					          <option value="verschenkt" {% if game.status == 'verschenkt' %}selected{% endif %}>{{ _('Gifted') }}</option>
 | 
				
			||||||
                    <option value="eingelöst" {% if game.status == 'eingelöst' %}selected{% endif %}>{{ _('Redeemed') }}</option>
 | 
					          <option value="eingelöst" {% if game.status == 'eingelöst' %}selected{% endif %}>{{ _('Redeemed') }}</option>
 | 
				
			||||||
                </select>
 | 
					        </select>
 | 
				
			||||||
            </div>
 | 
					      </div>
 | 
				
			||||||
            <div class="col-md-4">
 | 
					      <div class="col-md-4">
 | 
				
			||||||
                <label class="form-label">{{ _('Redeem by') }}</label>
 | 
					        <label for="game_redeem_date" class="form-label">{{ _('Redeem by') }}</label>
 | 
				
			||||||
                <input type="date" name="redeem_date" class="form-control" value="{{ redeem_date }}">
 | 
					        <input type="date" id="game_redeem_date" name="redeem_date" class="form-control" value="{{ redeem_date }}">
 | 
				
			||||||
            </div>
 | 
					      </div>
 | 
				
			||||||
            <div class="col-md-4">
 | 
					      <div class="col-md-4">
 | 
				
			||||||
                <label class="form-label">{{ _('Recipient') }}</label>
 | 
					        <label for="game_recipient" class="form-label">{{ _('Recipient') }}</label>
 | 
				
			||||||
                <input type="text" name="recipient" class="form-control" value="{{ game.recipient }}">
 | 
					        <input type="text" id="game_recipient" name="recipient" class="form-control" value="{{ game.recipient }}">
 | 
				
			||||||
            </div>
 | 
					      </div>
 | 
				
			||||||
            <div class="col-12">
 | 
					      <div class="col-12">
 | 
				
			||||||
                <label class="form-label">{{ _('Shop URL') }}</label>
 | 
					        <label for="game_url" class="form-label">{{ _('Shop URL') }}</label>
 | 
				
			||||||
                <input type="url" name="url" class="form-control" value="{{ game.url }}">
 | 
					        <input type="url" id="game_url" name="url" class="form-control" value="{{ game.url }}">
 | 
				
			||||||
            </div>
 | 
					      </div>
 | 
				
			||||||
            <div class="col-12">
 | 
					      <div class="col-12">
 | 
				
			||||||
                <label class="form-label">{{ _('Notes') }}</label>
 | 
					        <label for="game_notes" class="form-label">{{ _('Notes') }}</label>
 | 
				
			||||||
                <textarea name="notes" class="form-control" rows="3">{{ game.notes }}</textarea>
 | 
					        <textarea id="game_notes" name="notes" class="form-control" rows="3">{{ game.notes }}</textarea>
 | 
				
			||||||
            </div>
 | 
					      </div>
 | 
				
			||||||
            <div class="col-12">
 | 
					      <div class="col-12">
 | 
				
			||||||
                {% if redeem_url and active_redeem %}
 | 
					        {% if redeem_url and active_redeem %}
 | 
				
			||||||
                <div class="mb-3">
 | 
					        <div class="mb-3">
 | 
				
			||||||
                    <label class="form-label">{{ _('Active Redeem Link') }}</label>
 | 
					          <label for="active_redeem_link" class="form-label">{{ _('Active Redeem Link') }}</label>
 | 
				
			||||||
                    <input type="text"
 | 
					          <input type="text"
 | 
				
			||||||
                           class="form-control"
 | 
					                 id="active_redeem_link"
 | 
				
			||||||
                           value="{{ redeem_url }}"
 | 
					                 class="form-control"
 | 
				
			||||||
                           readonly
 | 
					                 value="{{ redeem_url }}"
 | 
				
			||||||
                           onclick="this.select()">
 | 
					                 readonly
 | 
				
			||||||
                    <small class="text-muted">
 | 
					                 onclick="this.select()">
 | 
				
			||||||
                        {{ _('Expires at') }}: {{ active_redeem.expires.strftime('%d.%m.%Y %H:%M') }}
 | 
					          <small class="text-muted">
 | 
				
			||||||
                    </small>
 | 
					            {{ _('Expires at') }}: {{ active_redeem.expires.strftime('%d.%m.%Y %H:%M') }}
 | 
				
			||||||
                </div>
 | 
					          </small>
 | 
				
			||||||
                {% endif %}
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
            <div class="col-12">
 | 
					 | 
				
			||||||
                <button type="submit" class="btn btn-primary">{{ _('Save') }}</button>
 | 
					 | 
				
			||||||
                <a href="{{ url_for('index') }}" class="btn btn-outline-secondary">{{ _('Cancel') }}</a>
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
    </form>
 | 
					        {% endif %}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <div class="col-12">
 | 
				
			||||||
 | 
					        <button type="submit" class="btn btn-primary">{{ _('Save') }}</button>
 | 
				
			||||||
 | 
					        <a href="{{ url_for('index') }}" class="btn btn-outline-secondary ms-2">{{ _('Cancel') }}</a>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </form>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,7 +5,7 @@
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <div class="mb-2">
 | 
					    <div class="mb-2">
 | 
				
			||||||
      <a href="https://git.nocci.it/nocci/GiftGamesDB" target="_blank" rel="noopener">
 | 
					      <a href="https://git.nocci.it/nocci/GiftGamesDB" target="_blank" rel="noopener">
 | 
				
			||||||
        <img src="{{ url_for('static', filename='forgejo.svg') }}" alt="forgejo" width="20" style="vertical-align:middle;margin-right:4px;">
 | 
					        <img src="{{ url_for('static', filename='forgejo.webp') }}" alt="forgejo" width="20" style="vertical-align:middle;margin-right:4px;">
 | 
				
			||||||
        find the source code on my Forgejo
 | 
					        find the source code on my Forgejo
 | 
				
			||||||
      </a>
 | 
					      </a>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,11 +5,11 @@
 | 
				
			||||||
    <form method="POST" enctype="multipart/form-data">
 | 
					    <form method="POST" enctype="multipart/form-data">
 | 
				
			||||||
	<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
 | 
						<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
 | 
				
			||||||
        <div class="mb-3">
 | 
					        <div class="mb-3">
 | 
				
			||||||
            <label class="form-label">{{ _('CSV-Datei auswählen') }}</label>
 | 
					            <label class="form-label">{{ _('Select CSV file') }}</label>
 | 
				
			||||||
            <input type="file" name="file" class="form-control" accept=".csv" required>
 | 
					            <input type="file" name="file" class="form-control" accept=".csv" required>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <button type="submit" class="btn btn-success">{{ _('Importieren') }}</button>
 | 
					        <button type="submit" class="btn btn-success">{{ _('Import') }}</button>
 | 
				
			||||||
        <a href="{{ url_for('index') }}" class="btn btn-outline-secondary">{{ _('Abbrechen') }}</a>
 | 
					        <a href="{{ url_for('index') }}" class="btn btn-outline-secondary">{{ _('Cancel') }}</a>
 | 
				
			||||||
    </form>
 | 
					    </form>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -31,7 +31,19 @@
 | 
				
			||||||
                <td>
 | 
					                <td>
 | 
				
			||||||
                {% if game.steam_appid %}
 | 
					                {% if game.steam_appid %}
 | 
				
			||||||
                <img src="https://cdn.cloudflare.steamstatic.com/steam/apps/{{ game.steam_appid }}/header.jpg"
 | 
					                <img src="https://cdn.cloudflare.steamstatic.com/steam/apps/{{ game.steam_appid }}/header.jpg"
 | 
				
			||||||
                     alt="Steam Header" style="height:64px;max-width:120px;object-fit:cover;">
 | 
					                     alt="Steam Header" 
 | 
				
			||||||
 | 
					                     class="game-cover"
 | 
				
			||||||
 | 
					                     {% if loop.first %}fetchpriority="high"{% endif %}
 | 
				
			||||||
 | 
					                     width="368" 
 | 
				
			||||||
 | 
					                     height="172"
 | 
				
			||||||
 | 
					                     loading="lazy">
 | 
				
			||||||
 | 
					                {% elif game.url and 'gog.com' in game.url %}
 | 
				
			||||||
 | 
					                <img src="{{ url_for('static', filename='gog_logo.webp') }}"
 | 
				
			||||||
 | 
					                     alt="GOG Logo" 
 | 
				
			||||||
 | 
					                     class="game-cover"
 | 
				
			||||||
 | 
					                     width="368"
 | 
				
			||||||
 | 
					                     height="172"
 | 
				
			||||||
 | 
					                     loading="lazy">
 | 
				
			||||||
                {% endif %}
 | 
					                {% endif %}
 | 
				
			||||||
                </td>
 | 
					                </td>
 | 
				
			||||||
                <td>{{ game.name }}</td>
 | 
					                <td>{{ game.name }}</td>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,29 +1,43 @@
 | 
				
			||||||
{% extends "base.html" %}
 | 
					{% extends "base.html" %}
 | 
				
			||||||
{% block content %}
 | 
					{% block content %}
 | 
				
			||||||
<div class="row justify-content-center mt-5">
 | 
					<div class="row justify-content-center">
 | 
				
			||||||
    <div class="col-md-6">
 | 
					  <div class="col-md-6 col-lg-4">
 | 
				
			||||||
        <div class="card shadow-sm">
 | 
					    <h1 class="mb-4">{{ _('Login') }}</h1>
 | 
				
			||||||
            <div class="card-body text-center">
 | 
					    <form method="POST" aria-label="{{ _('Login form') }}" autocomplete="on">
 | 
				
			||||||
                <img src="{{ url_for('static', filename='logo.png') }}" alt="Logo" width="266" height="206" class="mb-4" style="object-fit:contain;">
 | 
					      <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
 | 
				
			||||||
                <h2 class="card-title mb-4">{{ _('Login') }}</h2>
 | 
					      <div class="mb-3">
 | 
				
			||||||
                <form method="POST">
 | 
					        <label for="username" class="form-label">{{ _('Username') }} <span aria-hidden="true" class="text-danger">*</span></label>
 | 
				
			||||||
                    <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
 | 
					        <input type="text"
 | 
				
			||||||
                    <div class="mb-3">
 | 
					               id="username"
 | 
				
			||||||
                        <label class="form-label">{{ _('Username') }}</label>
 | 
					               name="username"
 | 
				
			||||||
                        <input type="text" name="username" class="form-control" required>
 | 
					               class="form-control"
 | 
				
			||||||
                    </div>
 | 
					               required
 | 
				
			||||||
                    <div class="mb-3">
 | 
					               autocomplete="username"
 | 
				
			||||||
                        <label class="form-label">{{ _('Password') }}</label>
 | 
					               aria-required="true"
 | 
				
			||||||
                        <input type="password" name="password" class="form-control" required>
 | 
					               autofocus>
 | 
				
			||||||
                    </div>
 | 
					      </div>
 | 
				
			||||||
                    <button type="submit" class="btn btn-primary w-100">{{ _('Login') }}</button>
 | 
					      <div class="mb-3">
 | 
				
			||||||
                </form>
 | 
					        <label for="password" class="form-label">{{ _('Password') }} <span aria-hidden="true" class="text-danger">*</span></label>
 | 
				
			||||||
                <div class="mt-3 text-center">
 | 
					        <input type="password"
 | 
				
			||||||
                    <a href="{{ url_for('register') }}">{{ _('No account yet? Register') }}</a>
 | 
					               id="password"
 | 
				
			||||||
                </div>
 | 
					               name="password"
 | 
				
			||||||
            </div>
 | 
					               class="form-control"
 | 
				
			||||||
        </div>
 | 
					               required
 | 
				
			||||||
 | 
					               autocomplete="current-password"
 | 
				
			||||||
 | 
					               aria-required="true">
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      {% if error %}
 | 
				
			||||||
 | 
					      <div class="alert alert-danger" role="alert">
 | 
				
			||||||
 | 
					        {{ error }}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      {% endif %}
 | 
				
			||||||
 | 
					      <button type="submit" class="btn btn-primary w-100">{{ _('Login') }}</button>
 | 
				
			||||||
 | 
					    </form>
 | 
				
			||||||
 | 
					    <div class="mt-3 text-center">
 | 
				
			||||||
 | 
					      <a href="{{ url_for('register') }}">{{ _('No account? Register here!') }}</a>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,24 +1,51 @@
 | 
				
			||||||
{% extends "base.html" %}
 | 
					{% extends "base.html" %}
 | 
				
			||||||
{% block content %}
 | 
					{% block content %}
 | 
				
			||||||
<div class="row justify-content-center mt-5">
 | 
					<div class="row justify-content-center">
 | 
				
			||||||
    <div class="col-md-6">
 | 
					  <div class="col-md-6 col-lg-4">
 | 
				
			||||||
        <div class="card shadow-sm">
 | 
					    <h1 class="mb-4">{{ _('Register') }}</h1>
 | 
				
			||||||
            <div class="card-body">
 | 
					    <form method="POST" aria-label="{{ _('Registration form') }}" autocomplete="on">
 | 
				
			||||||
                <h2 class="card-title mb-4">{{ _('Register') }}</h2>
 | 
					      <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
 | 
				
			||||||
                <form method="POST">
 | 
					      <div class="mb-3">
 | 
				
			||||||
                    <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
 | 
					        <label for="reg-username" class="form-label">{{ _('Username') }} <span aria-hidden="true" class="text-danger">*</span></label>
 | 
				
			||||||
                    <div class="mb-3">
 | 
					        <input type="text"
 | 
				
			||||||
                        <label class="form-label">{{ _('Username') }}</label>
 | 
					               id="reg-username"
 | 
				
			||||||
                        <input type="text" name="username" class="form-control" required>
 | 
					               name="username"
 | 
				
			||||||
                    </div>
 | 
					               class="form-control"
 | 
				
			||||||
                    <div class="mb-3">
 | 
					               required
 | 
				
			||||||
                        <label class="form-label">{{ _('Password') }}</label>
 | 
					               autocomplete="username"
 | 
				
			||||||
                        <input type="password" name="password" class="form-control" required>
 | 
					               aria-required="true">
 | 
				
			||||||
                    </div>
 | 
					      </div>
 | 
				
			||||||
                    <button type="submit" class="btn btn-primary w-100">{{ _('Register') }}</button>
 | 
					      <div class="mb-3">
 | 
				
			||||||
                </form>
 | 
					        <label for="reg-password" class="form-label">{{ _('Password') }} <span aria-hidden="true" class="text-danger">*</span></label>
 | 
				
			||||||
            </div>
 | 
					        <input type="password"
 | 
				
			||||||
        </div>
 | 
					               id="reg-password"
 | 
				
			||||||
 | 
					               name="password"
 | 
				
			||||||
 | 
					               class="form-control"
 | 
				
			||||||
 | 
					               required
 | 
				
			||||||
 | 
					               autocomplete="new-password"
 | 
				
			||||||
 | 
					               aria-required="true">
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <div class="mb-3">
 | 
				
			||||||
 | 
					        <label for="reg-password2" class="form-label">{{ _('Confirm Password') }} <span aria-hidden="true" class="text-danger">*</span></label>
 | 
				
			||||||
 | 
					        <input type="password"
 | 
				
			||||||
 | 
					               id="reg-password2"
 | 
				
			||||||
 | 
					               name="password2"
 | 
				
			||||||
 | 
					               class="form-control"
 | 
				
			||||||
 | 
					               required
 | 
				
			||||||
 | 
					               autocomplete="new-password"
 | 
				
			||||||
 | 
					               aria-required="true">
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      {% if error %}
 | 
				
			||||||
 | 
					      <div class="alert alert-danger" role="alert">
 | 
				
			||||||
 | 
					        {{ error }}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      {% endif %}
 | 
				
			||||||
 | 
					      <button type="submit" class="btn btn-primary w-100">{{ _('Register') }}</button>
 | 
				
			||||||
 | 
					    </form>
 | 
				
			||||||
 | 
					    <div class="mt-3 text-center">
 | 
				
			||||||
 | 
					      <a href="{{ url_for('login') }}">{{ _('Already have an account? Login!') }}</a>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
{% endblock %}
 | 
					{% endblock %}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										71
									
								
								steam-gift-manager/translations/de.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,71 @@
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  "": "",
 | 
				
			||||||
 | 
					  "Actions": "Aktionen",
 | 
				
			||||||
 | 
					  "Active Redeem Link": "Aktiver Einlöse-Link",
 | 
				
			||||||
 | 
					  "Add New Game": "Neues Spiel hinzufügen",
 | 
				
			||||||
 | 
					  "Already have an account? Login!": "",
 | 
				
			||||||
 | 
					  "Cancel": "Abbrechen",
 | 
				
			||||||
 | 
					  "Change Password": "Passwort ändern",
 | 
				
			||||||
 | 
					  "Change password form": "",
 | 
				
			||||||
 | 
					  "Changes saved!": "Änderungen gespeichert!",
 | 
				
			||||||
 | 
					  "Confirm New Password": "Neues Passwort bestätigen",
 | 
				
			||||||
 | 
					  "Confirm Password": "",
 | 
				
			||||||
 | 
					  "Cover": "Cover",
 | 
				
			||||||
 | 
					  "Created": "Erstellt",
 | 
				
			||||||
 | 
					  "Current Password": "Aktuelles Passwort",
 | 
				
			||||||
 | 
					  "Current passwort is wrong": "Aktuelles Passwort ist falsch",
 | 
				
			||||||
 | 
					  "Dark Mode": "Dunkler Modus",
 | 
				
			||||||
 | 
					  "Edit Game": "Spiel bearbeiten",
 | 
				
			||||||
 | 
					  "Error generating link": "Fehler beim Generieren des Links",
 | 
				
			||||||
 | 
					  "Error: ": "Fehler: ",
 | 
				
			||||||
 | 
					  "Expires at": "Ablaufdatum",
 | 
				
			||||||
 | 
					  "Export CSV": "CSV exportieren",
 | 
				
			||||||
 | 
					  "Game Key": "Spiele-Key",
 | 
				
			||||||
 | 
					  "Game Key Manager": "Game-Key-Verwaltung",
 | 
				
			||||||
 | 
					  "Game List (without Keys)": "Spieleliste (ohne Keys)",
 | 
				
			||||||
 | 
					  "Game added successfully!": "Spiel erfolgreich hinzugefügt!",
 | 
				
			||||||
 | 
					  "Generate redeem link": "Einlöse-Link generieren",
 | 
				
			||||||
 | 
					  "Gifted": "Verschenkt",
 | 
				
			||||||
 | 
					  "Import": "Importieren",
 | 
				
			||||||
 | 
					  "Import CSV": "CSV importieren",
 | 
				
			||||||
 | 
					  "Import Games": "Spiele importieren",
 | 
				
			||||||
 | 
					  "Import error: %(error)s', error=str(e)), 'danger": "",
 | 
				
			||||||
 | 
					  "Invalid credentials": "Ungültige Anmeldedaten",
 | 
				
			||||||
 | 
					  "Key": "Key",
 | 
				
			||||||
 | 
					  "Login": "Anmelden",
 | 
				
			||||||
 | 
					  "Login form": "",
 | 
				
			||||||
 | 
					  "Logout": "Abmelden",
 | 
				
			||||||
 | 
					  "My Games": "Meine Spiele",
 | 
				
			||||||
 | 
					  "Name": "Name",
 | 
				
			||||||
 | 
					  "New Password": "Neues Passwort",
 | 
				
			||||||
 | 
					  "New Passwords are not matching": "Neue Passwörter stimmen nicht überein",
 | 
				
			||||||
 | 
					  "No account? Register here!": "",
 | 
				
			||||||
 | 
					  "No games yet": "Der Kornspeicher ist leer, Sire!",
 | 
				
			||||||
 | 
					  "No new registrations. They are deactivated!": "Keine neuen Registrierungen. Sie sind deaktiviert!",
 | 
				
			||||||
 | 
					  "Not redeemed": "Nicht eingelöst",
 | 
				
			||||||
 | 
					  "Notes": "Notizen",
 | 
				
			||||||
 | 
					  "Password": "Passwort",
 | 
				
			||||||
 | 
					  "Password changed successfully": "Passwort erfolgreich geändert",
 | 
				
			||||||
 | 
					  "Please upload a valid CSV file.": "Bitte eine gültige CSV-Datei hochladen.",
 | 
				
			||||||
 | 
					  "Really delete?": "Wirklich löschen?",
 | 
				
			||||||
 | 
					  "Recipient": "Empfänger",
 | 
				
			||||||
 | 
					  "Redeem by": "Einzulösen vor",
 | 
				
			||||||
 | 
					  "Redeem link copied to clipboard!": "Einlöse-Link in die Zwischenablage kopiert!",
 | 
				
			||||||
 | 
					  "Redeem now on": "Jetzt einlösen bei",
 | 
				
			||||||
 | 
					  "Redeemed": "Eingelöst",
 | 
				
			||||||
 | 
					  "Register": "Registrieren",
 | 
				
			||||||
 | 
					  "Registration form": "",
 | 
				
			||||||
 | 
					  "Save": "Speichern",
 | 
				
			||||||
 | 
					  "Search": "Suche",
 | 
				
			||||||
 | 
					  "Search games": "",
 | 
				
			||||||
 | 
					  "Select CSV file": "CSV-Datei auswählen",
 | 
				
			||||||
 | 
					  "Shop": "Shop",
 | 
				
			||||||
 | 
					  "Shop URL": "Shop-URL",
 | 
				
			||||||
 | 
					  "Status": "Status",
 | 
				
			||||||
 | 
					  "Steam AppID (optional)": "Steam-AppID (optional)",
 | 
				
			||||||
 | 
					  "Steam Key already exists!": "Steam-Key existiert bereits!",
 | 
				
			||||||
 | 
					  "This page will expire in": "Diese Seite läuft ab in",
 | 
				
			||||||
 | 
					  "Username": "Benutzername",
 | 
				
			||||||
 | 
					  "Username already exists": "Benutzername existiert bereits",
 | 
				
			||||||
 | 
					  "Your Key:": "Dein Key:"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										72
									
								
								steam-gift-manager/translations/en.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						| 
						 | 
					@ -0,0 +1,72 @@
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					  "": "",
 | 
				
			||||||
 | 
					  "Actions": "",
 | 
				
			||||||
 | 
					  "Active Redeem Link": "",
 | 
				
			||||||
 | 
					  "Add New Game": "",
 | 
				
			||||||
 | 
					  "Already have an account? Login!": "",
 | 
				
			||||||
 | 
					  "Cancel": "",
 | 
				
			||||||
 | 
					  "Change Password": "",
 | 
				
			||||||
 | 
					  "Change password form": "",
 | 
				
			||||||
 | 
					  "Changes saved!": "",
 | 
				
			||||||
 | 
					  "Confirm New Password": "",
 | 
				
			||||||
 | 
					  "Confirm Password": "",
 | 
				
			||||||
 | 
					  "Cover": "",
 | 
				
			||||||
 | 
					  "Created": "",
 | 
				
			||||||
 | 
					  "Current Password": "",
 | 
				
			||||||
 | 
					  "Current passwort is wrong": "",
 | 
				
			||||||
 | 
					  "Dark Mode": "",
 | 
				
			||||||
 | 
					  "Edit Game": "",
 | 
				
			||||||
 | 
					  "Error: ": "",
 | 
				
			||||||
 | 
					  "Error generating link": "",
 | 
				
			||||||
 | 
					  "Expires at": "",
 | 
				
			||||||
 | 
					  "Export CSV": "",
 | 
				
			||||||
 | 
					  "Game added successfully!": "",
 | 
				
			||||||
 | 
					  "Game Key": "",
 | 
				
			||||||
 | 
					  "Game Key Manager": "",
 | 
				
			||||||
 | 
					  "Game List (without Keys)": "",
 | 
				
			||||||
 | 
					  "Generate redeem link": "",
 | 
				
			||||||
 | 
					  "Gifted": "",
 | 
				
			||||||
 | 
					  "Import": "",
 | 
				
			||||||
 | 
					  "Import CSV": "",
 | 
				
			||||||
 | 
					  "Import error: %(error)s', error=str(e)), 'danger": "",
 | 
				
			||||||
 | 
					  "Import Games": "",
 | 
				
			||||||
 | 
					  "Invalid credentials": "",
 | 
				
			||||||
 | 
					  "Key": "",
 | 
				
			||||||
 | 
					  "Login": "",
 | 
				
			||||||
 | 
					  "Login form": "",
 | 
				
			||||||
 | 
					  "Logout": "",
 | 
				
			||||||
 | 
					  "My Games": "",
 | 
				
			||||||
 | 
					  "Name": "",
 | 
				
			||||||
 | 
					  "%(new)d new games imported, %(dup)d skipped duplicates', new=new_games, dup=duplicates), 'success": "",
 | 
				
			||||||
 | 
					  "New Password": "",
 | 
				
			||||||
 | 
					  "New Passwords are not matching": "",
 | 
				
			||||||
 | 
					  "No account? Register here!": "",
 | 
				
			||||||
 | 
					  "No games yet": "",
 | 
				
			||||||
 | 
					  "No new registrations. They are deactivated!": "",
 | 
				
			||||||
 | 
					  "Notes": "",
 | 
				
			||||||
 | 
					  "Not redeemed": "",
 | 
				
			||||||
 | 
					  "Password": "",
 | 
				
			||||||
 | 
					  "Password changed successfully": "",
 | 
				
			||||||
 | 
					  "Please upload a valid CSV file.": "",
 | 
				
			||||||
 | 
					  "Really delete?": "",
 | 
				
			||||||
 | 
					  "Recipient": "",
 | 
				
			||||||
 | 
					  "Redeem by": "",
 | 
				
			||||||
 | 
					  "Redeemed": "",
 | 
				
			||||||
 | 
					  "Redeem link copied to clipboard!": "",
 | 
				
			||||||
 | 
					  "Redeem now on": "",
 | 
				
			||||||
 | 
					  "Register": "",
 | 
				
			||||||
 | 
					  "Registration form": "",
 | 
				
			||||||
 | 
					  "Save": "",
 | 
				
			||||||
 | 
					  "Search": "",
 | 
				
			||||||
 | 
					  "Search games": "",
 | 
				
			||||||
 | 
					  "Select CSV file": "",
 | 
				
			||||||
 | 
					  "Shop": "",
 | 
				
			||||||
 | 
					  "Shop URL": "",
 | 
				
			||||||
 | 
					  "Status": "",
 | 
				
			||||||
 | 
					  "Steam AppID (optional)": "",
 | 
				
			||||||
 | 
					  "Steam Key already exists!": "",
 | 
				
			||||||
 | 
					  "This page will expire in": "",
 | 
				
			||||||
 | 
					  "Username": "",
 | 
				
			||||||
 | 
					  "Username already exists": "",
 | 
				
			||||||
 | 
					  "Your Key:": ""
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										51
									
								
								translate.sh
									
										
									
									
									
								
							
							
						
						| 
						 | 
					@ -1,28 +1,41 @@
 | 
				
			||||||
#!/bin/bash
 | 
					#!/bin/bash
 | 
				
			||||||
set -e
 | 
					set -e
 | 
				
			||||||
 | 
					
 | 
				
			||||||
cd "$(dirname "$0")/steam-gift-manager"
 | 
					APP_DIR="steam-gift-manager"
 | 
				
			||||||
 | 
					TRANSLATION_DIR="$APP_DIR/translations"
 | 
				
			||||||
 | 
					LANGS=("de" "en")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
declare -A locales=(
 | 
					# Prüfe jq
 | 
				
			||||||
  ["de"]="de"
 | 
					if ! command -v jq &>/dev/null; then
 | 
				
			||||||
  ["en"]="en"
 | 
					  echo "❌ jq is required. Install with: sudo apt-get install jq"
 | 
				
			||||||
)
 | 
					  exit 1
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# create POT-file
 | 
					# 1. Lege JSON-Dateien an, falls sie fehlen
 | 
				
			||||||
docker-compose exec steam-manager pybabel extract -F babel.cfg -o translations/messages.pot .
 | 
					for lang in "${LANGS[@]}"; do
 | 
				
			||||||
 | 
					  file="$TRANSLATION_DIR/$lang.json"
 | 
				
			||||||
# Check for each language and initialize if necessary
 | 
					  if [ ! -f "$file" ]; then
 | 
				
			||||||
for lang in "${!locales[@]}"; do
 | 
					    echo "{}" > "$file"
 | 
				
			||||||
  if [ ! -f "translations/${locales[$lang]}/LC_MESSAGES/messages.po" ]; then
 | 
					    echo "Created $file"
 | 
				
			||||||
    docker-compose exec steam-manager pybabel init \
 | 
					 | 
				
			||||||
      -i translations/messages.pot \
 | 
					 | 
				
			||||||
      -d translations \
 | 
					 | 
				
			||||||
      -l "${locales[$lang]}"
 | 
					 | 
				
			||||||
  fi
 | 
					  fi
 | 
				
			||||||
done
 | 
					done
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Update and compile translations
 | 
					# 2. Extrahiere alle zu übersetzenden Strings
 | 
				
			||||||
docker-compose exec steam-manager pybabel update -i translations/messages.pot -d translations
 | 
					STRINGS=$(grep -rhoP "_\(\s*['\"](.+?)['\"]\s*\)" \
 | 
				
			||||||
docker-compose exec steam-manager pybabel compile -d translations
 | 
					  "$APP_DIR/templates" "$APP_DIR/app.py" | \
 | 
				
			||||||
 | 
					  sed -E "s/_\(\s*['\"](.+?)['\"]\s*\)/\1/" | sort | uniq)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
echo "✅ Translations updated!"
 | 
					# 3. Ergänze neue Keys in die JSON-Dateien
 | 
				
			||||||
 | 
					for lang in "${LANGS[@]}"; do
 | 
				
			||||||
 | 
					  file="$TRANSLATION_DIR/$lang.json"
 | 
				
			||||||
 | 
					  tmp="$file.tmp"
 | 
				
			||||||
 | 
					  cp "$file" "$tmp"
 | 
				
			||||||
 | 
					  while IFS= read -r key; do
 | 
				
			||||||
 | 
					    if ! jq -e --arg k "$key" 'has($k)' "$tmp" >/dev/null; then
 | 
				
			||||||
 | 
					      jq --arg k "$key" '. + {($k): ""}' "$tmp" > "$tmp.new" && mv "$tmp.new" "$tmp"
 | 
				
			||||||
 | 
					    fi
 | 
				
			||||||
 | 
					  done <<< "$STRINGS"
 | 
				
			||||||
 | 
					  mv "$tmp" "$file"
 | 
				
			||||||
 | 
					  echo "Updated $file"
 | 
				
			||||||
 | 
					done
 | 
				
			||||||
 | 
					echo "✅ JSON translation files updated. Please enter your translations!"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,274 +0,0 @@
 | 
				
			||||||
# German translations for PROJECT.
 | 
					 | 
				
			||||||
# Copyright (C) 2025 ORGANIZATION
 | 
					 | 
				
			||||||
# This file is distributed under the same license as the PROJECT project.
 | 
					 | 
				
			||||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2025.
 | 
					 | 
				
			||||||
#
 | 
					 | 
				
			||||||
msgid ""
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
"Project-Id-Version: PROJECT VERSION\n"
 | 
					 | 
				
			||||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
 | 
					 | 
				
			||||||
"POT-Creation-Date: 2025-04-29 15:53+0000\n"
 | 
					 | 
				
			||||||
"PO-Revision-Date: 2025-04-29 15:42+0000\n"
 | 
					 | 
				
			||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 | 
					 | 
				
			||||||
"Language: de\n"
 | 
					 | 
				
			||||||
"Language-Team: de <LL@li.org>\n"
 | 
					 | 
				
			||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
 | 
					 | 
				
			||||||
"MIME-Version: 1.0\n"
 | 
					 | 
				
			||||||
"Content-Type: text/plain; charset=utf-8\n"
 | 
					 | 
				
			||||||
"Content-Transfer-Encoding: 8bit\n"
 | 
					 | 
				
			||||||
"Generated-By: Babel 2.17.0\n"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: app.py:194
 | 
					 | 
				
			||||||
msgid "Invalid credentials"
 | 
					 | 
				
			||||||
msgstr "Ungültige Anmeldedaten"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: app.py:200
 | 
					 | 
				
			||||||
msgid "No new registrations. They are deactivated!"
 | 
					 | 
				
			||||||
msgstr "Keine neuen Registrierungen. Sie sind deaktiviert!"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: app.py:208
 | 
					 | 
				
			||||||
msgid "Username already exists"
 | 
					 | 
				
			||||||
msgstr "Benutzername existiert bereits"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: app.py:234
 | 
					 | 
				
			||||||
msgid "Current passwort is wrong"
 | 
					 | 
				
			||||||
msgstr "Aktuelles Passwort ist falsch"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: app.py:238
 | 
					 | 
				
			||||||
msgid "New Passwords are not matching"
 | 
					 | 
				
			||||||
msgstr "Neue Passwörter stimmen nicht überein"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: app.py:243
 | 
					 | 
				
			||||||
msgid "Password changed successfully"
 | 
					 | 
				
			||||||
msgstr "Passwort erfolgreich geändert"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: app.py:273
 | 
					 | 
				
			||||||
msgid "Game added successfully!"
 | 
					 | 
				
			||||||
msgstr "Spiel erfolgreich hinzugefügt!"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: app.py:278
 | 
					 | 
				
			||||||
msgid "Steam Key already exists!"
 | 
					 | 
				
			||||||
msgstr "Steam-Key existiert bereits!"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: app.py:281 app.py:325
 | 
					 | 
				
			||||||
msgid "Error: "
 | 
					 | 
				
			||||||
msgstr "Fehler: "
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: app.py:320
 | 
					 | 
				
			||||||
msgid "Changes saved!"
 | 
					 | 
				
			||||||
msgstr "Änderungen gespeichert!"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: app.py:408
 | 
					 | 
				
			||||||
msgid "Game List (without Keys)"
 | 
					 | 
				
			||||||
msgstr "Spieleliste (ohne Keys)"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: app.py:501
 | 
					 | 
				
			||||||
#, python-format
 | 
					 | 
				
			||||||
msgid "%(new)d new games imported, %(dup)d skipped duplicates"
 | 
					 | 
				
			||||||
msgstr "%(new)d neue Spiele importiert, %(dup)d Duplikate übersprungen"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: app.py:505
 | 
					 | 
				
			||||||
#, python-format
 | 
					 | 
				
			||||||
msgid "Import error: %(error)s"
 | 
					 | 
				
			||||||
msgstr "Importfehler: %(error)s"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: app.py:509
 | 
					 | 
				
			||||||
msgid "Please upload a valid CSV file."
 | 
					 | 
				
			||||||
msgstr "Bitte eine gültige CSV-Datei hochladen."
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/add_game.html:4 templates/index.html:9
 | 
					 | 
				
			||||||
msgid "Add New Game"
 | 
					 | 
				
			||||||
msgstr "Neues Spiel hinzufügen"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/add_game.html:9 templates/edit_game.html:9 templates/index.html:19
 | 
					 | 
				
			||||||
msgid "Name"
 | 
					 | 
				
			||||||
msgstr "Name"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/add_game.html:13 templates/edit_game.html:13
 | 
					 | 
				
			||||||
msgid "Game Key"
 | 
					 | 
				
			||||||
msgstr "Spiele-Key"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/add_game.html:17 templates/edit_game.html:21 templates/index.html:21
 | 
					 | 
				
			||||||
msgid "Status"
 | 
					 | 
				
			||||||
msgstr "Status"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/add_game.html:19 templates/edit_game.html:23 templates/index.html:41
 | 
					 | 
				
			||||||
msgid "Not redeemed"
 | 
					 | 
				
			||||||
msgstr "Nicht eingelöst"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/add_game.html:20 templates/edit_game.html:24 templates/index.html:43
 | 
					 | 
				
			||||||
msgid "Gifted"
 | 
					 | 
				
			||||||
msgstr "Verschenkt"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/add_game.html:21 templates/edit_game.html:25 templates/index.html:45
 | 
					 | 
				
			||||||
msgid "Redeemed"
 | 
					 | 
				
			||||||
msgstr "Eingelöst"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/add_game.html:25 templates/edit_game.html:29 templates/index.html:23
 | 
					 | 
				
			||||||
msgid "Redeem by"
 | 
					 | 
				
			||||||
msgstr "Einzulösen bis"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/add_game.html:29 templates/edit_game.html:33
 | 
					 | 
				
			||||||
msgid "Recipient"
 | 
					 | 
				
			||||||
msgstr "Empfänger"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/add_game.html:33 templates/edit_game.html:37
 | 
					 | 
				
			||||||
msgid "Shop URL"
 | 
					 | 
				
			||||||
msgstr "Shop-URL"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/add_game.html:37 templates/edit_game.html:41
 | 
					 | 
				
			||||||
msgid "Notes"
 | 
					 | 
				
			||||||
msgstr "Notizen"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/add_game.html:41 templates/edit_game.html:60
 | 
					 | 
				
			||||||
msgid "Save"
 | 
					 | 
				
			||||||
msgstr "Speichern"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/add_game.html:42 templates/edit_game.html:61 templates/import.html:12
 | 
					 | 
				
			||||||
msgid "Cancel"
 | 
					 | 
				
			||||||
msgstr "Abbrechen"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/base.html:7
 | 
					 | 
				
			||||||
msgid "Game Key Manager"
 | 
					 | 
				
			||||||
msgstr "Game-Key-Verwaltung"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/base.html:23
 | 
					 | 
				
			||||||
msgid "Search"
 | 
					 | 
				
			||||||
msgstr "Suche"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/base.html:31
 | 
					 | 
				
			||||||
msgid "Dark Mode"
 | 
					 | 
				
			||||||
msgstr "Dunkler Modus"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/base.html:46 templates/login.html:16 templates/register.html:15
 | 
					 | 
				
			||||||
msgid "Password"
 | 
					 | 
				
			||||||
msgstr "Passwort"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/base.html:49
 | 
					 | 
				
			||||||
msgid "Logout"
 | 
					 | 
				
			||||||
msgstr "Abmelden"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/change_password.html:4 templates/change_password.html:19
 | 
					 | 
				
			||||||
msgid "Change Password"
 | 
					 | 
				
			||||||
msgstr "Passwort ändern"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/change_password.html:8
 | 
					 | 
				
			||||||
msgid "Current Password"
 | 
					 | 
				
			||||||
msgstr "Aktuelles Passwort"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/change_password.html:12
 | 
					 | 
				
			||||||
msgid "New Password"
 | 
					 | 
				
			||||||
msgstr "Neues Passwort"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/change_password.html:16
 | 
					 | 
				
			||||||
msgid "Confirm New Password"
 | 
					 | 
				
			||||||
msgstr "Neues Passwort bestätigen"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/edit_game.html:4
 | 
					 | 
				
			||||||
msgid "Edit Game"
 | 
					 | 
				
			||||||
msgstr "Spiel bearbeiten"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/edit_game.html:17
 | 
					 | 
				
			||||||
msgid "Steam AppID (optional)"
 | 
					 | 
				
			||||||
msgstr "Steam-AppID (optional)"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/edit_game.html:47
 | 
					 | 
				
			||||||
msgid "Active Redeem Link"
 | 
					 | 
				
			||||||
msgstr "Aktiver Einlöse-Link"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/edit_game.html:54
 | 
					 | 
				
			||||||
msgid "Expires at"
 | 
					 | 
				
			||||||
msgstr "Ablaufdatum"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/import.html:4
 | 
					 | 
				
			||||||
msgid "Import Games"
 | 
					 | 
				
			||||||
msgstr "Spiele importieren"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/import.html:8
 | 
					 | 
				
			||||||
msgid "Select CSV file"
 | 
					 | 
				
			||||||
msgstr "CSV-Datei auswählen"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/import.html:11
 | 
					 | 
				
			||||||
msgid "Import"
 | 
					 | 
				
			||||||
msgstr "Importieren"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/index.html:4
 | 
					 | 
				
			||||||
msgid "My Games"
 | 
					 | 
				
			||||||
msgstr "Meine Spiele"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/index.html:6
 | 
					 | 
				
			||||||
msgid "Export CSV"
 | 
					 | 
				
			||||||
msgstr "CSV exportieren"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/index.html:8
 | 
					 | 
				
			||||||
msgid "Import CSV"
 | 
					 | 
				
			||||||
msgstr "CSV importieren"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/index.html:18
 | 
					 | 
				
			||||||
msgid "Cover"
 | 
					 | 
				
			||||||
msgstr "Cover"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/index.html:20
 | 
					 | 
				
			||||||
msgid "Key"
 | 
					 | 
				
			||||||
msgstr "Key"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/index.html:22
 | 
					 | 
				
			||||||
msgid "Created"
 | 
					 | 
				
			||||||
msgstr "Erstellt"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/index.html:24 templates/index.html:56
 | 
					 | 
				
			||||||
msgid "Shop"
 | 
					 | 
				
			||||||
msgstr "Shop"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/index.html:25
 | 
					 | 
				
			||||||
msgid "Actions"
 | 
					 | 
				
			||||||
msgstr "Aktionen"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/index.html:63
 | 
					 | 
				
			||||||
msgid "Generate redeem link"
 | 
					 | 
				
			||||||
msgstr "Einlöse-Link generieren"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/index.html:70
 | 
					 | 
				
			||||||
msgid "Really delete?"
 | 
					 | 
				
			||||||
msgstr "Wirklich löschen?"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/index.html:96
 | 
					 | 
				
			||||||
msgid "Redeem link copied to clipboard!"
 | 
					 | 
				
			||||||
msgstr "Einlöse-Link in die Zwischenablage kopiert!"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/index.html:100
 | 
					 | 
				
			||||||
msgid "Error generating link"
 | 
					 | 
				
			||||||
msgstr "Fehler beim Generieren des Links"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/index.html:106
 | 
					 | 
				
			||||||
msgid "No games yet"
 | 
					 | 
				
			||||||
msgstr "Der Kornspeicher ist leer, Sire!"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/login.html:8 templates/login.html:19
 | 
					 | 
				
			||||||
msgid "Login"
 | 
					 | 
				
			||||||
msgstr "Anmelden"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/login.html:12 templates/register.html:11
 | 
					 | 
				
			||||||
msgid "Username"
 | 
					 | 
				
			||||||
msgstr "Benutzername"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/login.html:22
 | 
					 | 
				
			||||||
msgid "No account yet? Register"
 | 
					 | 
				
			||||||
msgstr "Noch kein Konto? Jetzt registrieren"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/redeem.html:16
 | 
					 | 
				
			||||||
msgid "Your Key:"
 | 
					 | 
				
			||||||
msgstr "Dein Key:"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/redeem.html:22
 | 
					 | 
				
			||||||
msgid "Redeem now on"
 | 
					 | 
				
			||||||
msgstr "Jetzt einlösen bei"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/redeem.html:26
 | 
					 | 
				
			||||||
msgid "This page will expire in"
 | 
					 | 
				
			||||||
msgstr "Diese Seite läuft ab in"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/register.html:7 templates/register.html:18
 | 
					 | 
				
			||||||
msgid "Register"
 | 
					 | 
				
			||||||
msgstr "Registrieren"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,280 +0,0 @@
 | 
				
			||||||
# English translations for PROJECT.
 | 
					 | 
				
			||||||
# Copyright (C) 2025 ORGANIZATION
 | 
					 | 
				
			||||||
# This file is distributed under the same license as the PROJECT project.
 | 
					 | 
				
			||||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2025.
 | 
					 | 
				
			||||||
#
 | 
					 | 
				
			||||||
msgid ""
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
"Project-Id-Version: PROJECT VERSION\n"
 | 
					 | 
				
			||||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
 | 
					 | 
				
			||||||
"POT-Creation-Date: 2025-04-29 15:53+0000\n"
 | 
					 | 
				
			||||||
"PO-Revision-Date: 2025-04-29 15:42+0000\n"
 | 
					 | 
				
			||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 | 
					 | 
				
			||||||
"Language: en\n"
 | 
					 | 
				
			||||||
"Language-Team: en <LL@li.org>\n"
 | 
					 | 
				
			||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
 | 
					 | 
				
			||||||
"MIME-Version: 1.0\n"
 | 
					 | 
				
			||||||
"Content-Type: text/plain; charset=utf-8\n"
 | 
					 | 
				
			||||||
"Content-Transfer-Encoding: 8bit\n"
 | 
					 | 
				
			||||||
"Generated-By: Babel 2.17.0\n"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: app.py:194
 | 
					 | 
				
			||||||
msgid "Invalid credentials"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: app.py:200
 | 
					 | 
				
			||||||
msgid "No new registrations. They are deactivated!"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: app.py:208
 | 
					 | 
				
			||||||
msgid "Username already exists"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: app.py:234
 | 
					 | 
				
			||||||
msgid "Current passwort is wrong"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: app.py:238
 | 
					 | 
				
			||||||
msgid "New Passwords are not matching"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: app.py:243
 | 
					 | 
				
			||||||
msgid "Password changed successfully"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: app.py:273
 | 
					 | 
				
			||||||
msgid "Game added successfully!"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: app.py:278
 | 
					 | 
				
			||||||
msgid "Steam Key already exists!"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: app.py:281 app.py:325
 | 
					 | 
				
			||||||
msgid "Error: "
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: app.py:320
 | 
					 | 
				
			||||||
msgid "Changes saved!"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: app.py:408
 | 
					 | 
				
			||||||
msgid "Game List (without Keys)"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: app.py:501
 | 
					 | 
				
			||||||
#, python-format
 | 
					 | 
				
			||||||
msgid "%(new)d new games imported, %(dup)d skipped duplicates"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: app.py:505
 | 
					 | 
				
			||||||
#, python-format
 | 
					 | 
				
			||||||
msgid "Import error: %(error)s"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: app.py:509
 | 
					 | 
				
			||||||
msgid "Please upload a valid CSV file."
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/add_game.html:4 templates/index.html:9
 | 
					 | 
				
			||||||
msgid "Add New Game"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/add_game.html:9 templates/edit_game.html:9 templates/index.html:19
 | 
					 | 
				
			||||||
msgid "Name"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/add_game.html:13 templates/edit_game.html:13
 | 
					 | 
				
			||||||
msgid "Game Key"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/add_game.html:17 templates/edit_game.html:21
 | 
					 | 
				
			||||||
#: templates/index.html:21
 | 
					 | 
				
			||||||
msgid "Status"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/add_game.html:19 templates/edit_game.html:23
 | 
					 | 
				
			||||||
#: templates/index.html:41
 | 
					 | 
				
			||||||
msgid "Not redeemed"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/add_game.html:20 templates/edit_game.html:24
 | 
					 | 
				
			||||||
#: templates/index.html:43
 | 
					 | 
				
			||||||
msgid "Gifted"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/add_game.html:21 templates/edit_game.html:25
 | 
					 | 
				
			||||||
#: templates/index.html:45
 | 
					 | 
				
			||||||
msgid "Redeemed"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/add_game.html:25 templates/edit_game.html:29
 | 
					 | 
				
			||||||
#: templates/index.html:23
 | 
					 | 
				
			||||||
msgid "Redeem by"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/add_game.html:29 templates/edit_game.html:33
 | 
					 | 
				
			||||||
msgid "Recipient"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/add_game.html:33 templates/edit_game.html:37
 | 
					 | 
				
			||||||
msgid "Shop URL"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/add_game.html:37 templates/edit_game.html:41
 | 
					 | 
				
			||||||
msgid "Notes"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/add_game.html:41 templates/edit_game.html:60
 | 
					 | 
				
			||||||
msgid "Save"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/add_game.html:42 templates/edit_game.html:61
 | 
					 | 
				
			||||||
#: templates/import.html:12
 | 
					 | 
				
			||||||
msgid "Cancel"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/base.html:7
 | 
					 | 
				
			||||||
msgid "Game Key Manager"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/base.html:23
 | 
					 | 
				
			||||||
msgid "Search"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/base.html:31
 | 
					 | 
				
			||||||
msgid "Dark Mode"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/base.html:46 templates/login.html:16 templates/register.html:15
 | 
					 | 
				
			||||||
msgid "Password"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/base.html:49
 | 
					 | 
				
			||||||
msgid "Logout"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/change_password.html:4 templates/change_password.html:19
 | 
					 | 
				
			||||||
msgid "Change Password"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/change_password.html:8
 | 
					 | 
				
			||||||
msgid "Current Password"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/change_password.html:12
 | 
					 | 
				
			||||||
msgid "New Password"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/change_password.html:16
 | 
					 | 
				
			||||||
msgid "Confirm New Password"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/edit_game.html:4
 | 
					 | 
				
			||||||
msgid "Edit Game"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/edit_game.html:17
 | 
					 | 
				
			||||||
msgid "Steam AppID (optional)"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/edit_game.html:47
 | 
					 | 
				
			||||||
msgid "Active Redeem Link"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/edit_game.html:54
 | 
					 | 
				
			||||||
msgid "Expires at"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/import.html:4
 | 
					 | 
				
			||||||
msgid "Import Games"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/import.html:8
 | 
					 | 
				
			||||||
msgid "Select CSV file"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/import.html:11
 | 
					 | 
				
			||||||
msgid "Import"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/index.html:4
 | 
					 | 
				
			||||||
msgid "My Games"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/index.html:6
 | 
					 | 
				
			||||||
msgid "Export CSV"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/index.html:8
 | 
					 | 
				
			||||||
msgid "Import CSV"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/index.html:18
 | 
					 | 
				
			||||||
msgid "Cover"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/index.html:20
 | 
					 | 
				
			||||||
msgid "Key"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/index.html:22
 | 
					 | 
				
			||||||
msgid "Created"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/index.html:24 templates/index.html:56
 | 
					 | 
				
			||||||
msgid "Shop"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/index.html:25
 | 
					 | 
				
			||||||
msgid "Actions"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/index.html:63
 | 
					 | 
				
			||||||
msgid "Generate redeem link"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/index.html:70
 | 
					 | 
				
			||||||
msgid "Really delete?"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/index.html:96
 | 
					 | 
				
			||||||
msgid "Redeem link copied to clipboard!"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/index.html:100
 | 
					 | 
				
			||||||
msgid "Error generating link"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/index.html:106
 | 
					 | 
				
			||||||
msgid "No games yet"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/login.html:8 templates/login.html:19
 | 
					 | 
				
			||||||
msgid "Login"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/login.html:12 templates/register.html:11
 | 
					 | 
				
			||||||
msgid "Username"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/login.html:22
 | 
					 | 
				
			||||||
msgid "No account yet? Register"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/redeem.html:16
 | 
					 | 
				
			||||||
msgid "Your Key:"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/redeem.html:22
 | 
					 | 
				
			||||||
msgid "Redeem now on"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/redeem.html:26
 | 
					 | 
				
			||||||
msgid "This page will expire in"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/register.html:7 templates/register.html:18
 | 
					 | 
				
			||||||
msgid "Register"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,279 +0,0 @@
 | 
				
			||||||
# Translations template for PROJECT.
 | 
					 | 
				
			||||||
# Copyright (C) 2025 ORGANIZATION
 | 
					 | 
				
			||||||
# This file is distributed under the same license as the PROJECT project.
 | 
					 | 
				
			||||||
# FIRST AUTHOR <EMAIL@ADDRESS>, 2025.
 | 
					 | 
				
			||||||
#
 | 
					 | 
				
			||||||
#, fuzzy
 | 
					 | 
				
			||||||
msgid ""
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
"Project-Id-Version: PROJECT VERSION\n"
 | 
					 | 
				
			||||||
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
 | 
					 | 
				
			||||||
"POT-Creation-Date: 2025-04-29 15:53+0000\n"
 | 
					 | 
				
			||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 | 
					 | 
				
			||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 | 
					 | 
				
			||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
 | 
					 | 
				
			||||||
"MIME-Version: 1.0\n"
 | 
					 | 
				
			||||||
"Content-Type: text/plain; charset=utf-8\n"
 | 
					 | 
				
			||||||
"Content-Transfer-Encoding: 8bit\n"
 | 
					 | 
				
			||||||
"Generated-By: Babel 2.17.0\n"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: app.py:194
 | 
					 | 
				
			||||||
msgid "Invalid credentials"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: app.py:200
 | 
					 | 
				
			||||||
msgid "No new registrations. They are deactivated!"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: app.py:208
 | 
					 | 
				
			||||||
msgid "Username already exists"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: app.py:234
 | 
					 | 
				
			||||||
msgid "Current passwort is wrong"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: app.py:238
 | 
					 | 
				
			||||||
msgid "New Passwords are not matching"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: app.py:243
 | 
					 | 
				
			||||||
msgid "Password changed successfully"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: app.py:273
 | 
					 | 
				
			||||||
msgid "Game added successfully!"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: app.py:278
 | 
					 | 
				
			||||||
msgid "Steam Key already exists!"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: app.py:281 app.py:325
 | 
					 | 
				
			||||||
msgid "Error: "
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: app.py:320
 | 
					 | 
				
			||||||
msgid "Changes saved!"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: app.py:408
 | 
					 | 
				
			||||||
msgid "Game List (without Keys)"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: app.py:501
 | 
					 | 
				
			||||||
#, python-format
 | 
					 | 
				
			||||||
msgid "%(new)d new games imported, %(dup)d skipped duplicates"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: app.py:505
 | 
					 | 
				
			||||||
#, python-format
 | 
					 | 
				
			||||||
msgid "Import error: %(error)s"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: app.py:509
 | 
					 | 
				
			||||||
msgid "Please upload a valid CSV file."
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/add_game.html:4 templates/index.html:9
 | 
					 | 
				
			||||||
msgid "Add New Game"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/add_game.html:9 templates/edit_game.html:9 templates/index.html:19
 | 
					 | 
				
			||||||
msgid "Name"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/add_game.html:13 templates/edit_game.html:13
 | 
					 | 
				
			||||||
msgid "Game Key"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/add_game.html:17 templates/edit_game.html:21
 | 
					 | 
				
			||||||
#: templates/index.html:21
 | 
					 | 
				
			||||||
msgid "Status"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/add_game.html:19 templates/edit_game.html:23
 | 
					 | 
				
			||||||
#: templates/index.html:41
 | 
					 | 
				
			||||||
msgid "Not redeemed"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/add_game.html:20 templates/edit_game.html:24
 | 
					 | 
				
			||||||
#: templates/index.html:43
 | 
					 | 
				
			||||||
msgid "Gifted"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/add_game.html:21 templates/edit_game.html:25
 | 
					 | 
				
			||||||
#: templates/index.html:45
 | 
					 | 
				
			||||||
msgid "Redeemed"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/add_game.html:25 templates/edit_game.html:29
 | 
					 | 
				
			||||||
#: templates/index.html:23
 | 
					 | 
				
			||||||
msgid "Redeem by"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/add_game.html:29 templates/edit_game.html:33
 | 
					 | 
				
			||||||
msgid "Recipient"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/add_game.html:33 templates/edit_game.html:37
 | 
					 | 
				
			||||||
msgid "Shop URL"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/add_game.html:37 templates/edit_game.html:41
 | 
					 | 
				
			||||||
msgid "Notes"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/add_game.html:41 templates/edit_game.html:60
 | 
					 | 
				
			||||||
msgid "Save"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/add_game.html:42 templates/edit_game.html:61
 | 
					 | 
				
			||||||
#: templates/import.html:12
 | 
					 | 
				
			||||||
msgid "Cancel"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/base.html:7
 | 
					 | 
				
			||||||
msgid "Game Key Manager"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/base.html:23
 | 
					 | 
				
			||||||
msgid "Search"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/base.html:31
 | 
					 | 
				
			||||||
msgid "Dark Mode"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/base.html:46 templates/login.html:16 templates/register.html:15
 | 
					 | 
				
			||||||
msgid "Password"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/base.html:49
 | 
					 | 
				
			||||||
msgid "Logout"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/change_password.html:4 templates/change_password.html:19
 | 
					 | 
				
			||||||
msgid "Change Password"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/change_password.html:8
 | 
					 | 
				
			||||||
msgid "Current Password"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/change_password.html:12
 | 
					 | 
				
			||||||
msgid "New Password"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/change_password.html:16
 | 
					 | 
				
			||||||
msgid "Confirm New Password"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/edit_game.html:4
 | 
					 | 
				
			||||||
msgid "Edit Game"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/edit_game.html:17
 | 
					 | 
				
			||||||
msgid "Steam AppID (optional)"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/edit_game.html:47
 | 
					 | 
				
			||||||
msgid "Active Redeem Link"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/edit_game.html:54
 | 
					 | 
				
			||||||
msgid "Expires at"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/import.html:4
 | 
					 | 
				
			||||||
msgid "Import Games"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/import.html:8
 | 
					 | 
				
			||||||
msgid "Select CSV file"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/import.html:11
 | 
					 | 
				
			||||||
msgid "Import"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/index.html:4
 | 
					 | 
				
			||||||
msgid "My Games"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/index.html:6
 | 
					 | 
				
			||||||
msgid "Export CSV"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/index.html:8
 | 
					 | 
				
			||||||
msgid "Import CSV"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/index.html:18
 | 
					 | 
				
			||||||
msgid "Cover"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/index.html:20
 | 
					 | 
				
			||||||
msgid "Key"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/index.html:22
 | 
					 | 
				
			||||||
msgid "Created"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/index.html:24 templates/index.html:56
 | 
					 | 
				
			||||||
msgid "Shop"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/index.html:25
 | 
					 | 
				
			||||||
msgid "Actions"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/index.html:63
 | 
					 | 
				
			||||||
msgid "Generate redeem link"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/index.html:70
 | 
					 | 
				
			||||||
msgid "Really delete?"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/index.html:96
 | 
					 | 
				
			||||||
msgid "Redeem link copied to clipboard!"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/index.html:100
 | 
					 | 
				
			||||||
msgid "Error generating link"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/index.html:106
 | 
					 | 
				
			||||||
msgid "No games yet"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/login.html:8 templates/login.html:19
 | 
					 | 
				
			||||||
msgid "Login"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/login.html:12 templates/register.html:11
 | 
					 | 
				
			||||||
msgid "Username"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/login.html:22
 | 
					 | 
				
			||||||
msgid "No account yet? Register"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/redeem.html:16
 | 
					 | 
				
			||||||
msgid "Your Key:"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/redeem.html:22
 | 
					 | 
				
			||||||
msgid "Redeem now on"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/redeem.html:26
 | 
					 | 
				
			||||||
msgid "This page will expire in"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#: templates/register.html:7 templates/register.html:18
 | 
					 | 
				
			||||||
msgid "Register"
 | 
					 | 
				
			||||||
msgstr ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||