Compare commits

...

4 Commits

Author SHA1 Message Date
6c905dc3ce feat: implement dynamic README loading based on user language
- Updated app.py to load the README content from language-specific markdown files located in the templates directory.
- Added new index.md files for English and Chinese, providing localized information about PsychoScales.Org and its resources.
2025-06-17 08:27:01 +08:00
e46d17eaf2 feat: enhance load_all_scales function for language support
- Updated load_all_scales to accept an optional language parameter, allowing for the loading of scales based on the specified language.
- Modified calls to load_all_scales in various endpoints to pass the user's detected language, ensuring consistent language handling across the application.
2025-06-17 08:23:10 +08:00
27794895c9 feat: implement multilingual template support
- Updated app.py to dynamically load Jinja2 templates based on the detected language.
- Created new template files for English and Chinese, including base, index, list, scale, and result pages.
- Modified template rendering logic to utilize the appropriate language-specific templates based on user preferences.
2025-06-17 08:08:18 +08:00
8f202262ee feat: add language detection middleware for request handling
- Implemented LanguageMiddleware to determine the user's language based on query parameters, Accept-Language header, or IP address.
- Updated the FastAPI app to include the middleware and pass the detected language to templates.
- Modified base.html to dynamically set the HTML language attribute based on the detected language.
2025-06-17 07:48:38 +08:00
13 changed files with 283 additions and 76 deletions

101
app.py
View File

@ -15,10 +15,8 @@ from datetime import datetime, UTC
import csv import csv
from io import StringIO from io import StringIO
from typing import Dict, List from typing import Dict, List
from starlette.middleware.base import BaseHTTPMiddleware
app = FastAPI() from starlette.types import ASGIApp
templates = Jinja2Templates(directory="templates")
app.mount("/static", StaticFiles(directory="static"), name="static")
# Initialize GeoIP2 reader # Initialize GeoIP2 reader
try: try:
@ -42,8 +40,62 @@ def get_location_from_ip(ip):
print(f"Error getting location for IP {ip}: {e}") print(f"Error getting location for IP {ip}: {e}")
return None return None
class LanguageMiddleware(BaseHTTPMiddleware):
def __init__(self, app: ASGIApp):
super().__init__(app)
self.default_language = "en" # Default language
self.supported_languages = ["zh", "en"] # Supported languages
async def dispatch(self, request: Request, call_next):
# Get language from query parameter
lang = request.query_params.get("lang")
# If no language in query params, try to get from Accept-Language header
if not lang:
accept_language = request.headers.get("accept-language", "")
if accept_language:
# Parse Accept-Language header and get the first language
lang = accept_language.split(",")[0].split(";")[0].strip()[:2]
# If still no language, try to detect from IP
if not lang and geoip_reader:
try:
ip = request.headers.get("X-Forwarded-For", "").split(",")[0].strip() or \
request.headers.get("X-Real-IP", "") or \
request.client.host
response = geoip_reader.city(ip)
country_to_lang = { # Map country to language
"China": "zh",
"Hong Kong": "zh",
"Taiwan": "zh",
"Macau": "zh",
"United States": "en",
"United Kingdom": "en",
}
lang = country_to_lang.get(response.country.name, self.default_language)
except:
lang = self.default_language
# Ensure language is supported
if lang not in self.supported_languages:
lang = self.default_language
# Add language to request state
request.state.language = lang
# Continue processing the request
response = await call_next(request)
return response
app = FastAPI()
app.add_middleware(LanguageMiddleware)
app.mount("/static", StaticFiles(directory="static"), name="static")
templates = {}
for lang in os.listdir("templates"):
templates[lang] = Jinja2Templates(directory="templates/"+lang)
# 加载所有问卷数据 # 加载所有问卷数据
def load_all_scales(): def load_all_scales(lang: str = None):
scales = {} scales = {}
tags = [] tags = []
for root, dirs, files in os.walk(os.path.realpath('scales')): for root, dirs, files in os.walk(os.path.realpath('scales')):
@ -52,39 +104,40 @@ def load_all_scales():
try: try:
with open(os.path.join(root, filename), 'r', encoding='utf-8') as f: with open(os.path.join(root, filename), 'r', encoding='utf-8') as f:
scale = yaml.safe_load(f) scale = yaml.safe_load(f)
scale['instructions']=markdown.markdown(scale['instructions'], extensions=['fenced_code','tables','mdx_math']) if lang is None or (scale['lang'] and scale['lang'] == lang):
scale['descriptions']=markdown.markdown(scale['descriptions'], extensions=['fenced_code','tables','mdx_math']) scale['instructions']=markdown.markdown(scale['instructions'], extensions=['fenced_code','tables','mdx_math'])
scale['abstract']=markdown.markdown(scale['abstract'], extensions=['fenced_code','tables','mdx_math']) scale['descriptions']=markdown.markdown(scale['descriptions'], extensions=['fenced_code','tables','mdx_math'])
if 'tag' not in scale: scale['abstract']=markdown.markdown(scale['abstract'], extensions=['fenced_code','tables','mdx_math'])
scale['tag']='其他' if 'tag' not in scale:
if scale['tag'] not in tags: scale['tag']='其他'
tags.append(scale['tag']) if scale['tag'] not in tags:
scale_id = os.path.splitext(filename)[0] # 使用文件名作为标识 tags.append(scale['tag'])
scales[scale_id] = scale scale_id = os.path.splitext(filename)[0] # 使用文件名作为标识
scales[scale_id] = scale
except Exception as e: except Exception as e:
print(f"Error loading scale {filename}: {e}") print(f"Error loading scale {filename}: {e}")
return tags, scales return tags, scales
@app.get("/", response_class=HTMLResponse) @app.get("/", response_class=HTMLResponse)
async def index(request: Request): async def index(request: Request):
tags, _ = load_all_scales() tags, _ = load_all_scales(request.state.language)
# 新增读取README.md的逻辑 # 新增读取README.md的逻辑
readme_content = "" readme_content = ""
try: try:
with open("README.md", "r", encoding="utf-8") as f: with open("templates/"+request.state.language+"/index.md", "r", encoding="utf-8") as f:
readme_content = markdown.markdown(f.read()) readme_content = markdown.markdown(f.read())
except FileNotFoundError: except FileNotFoundError:
pass # 如果README不存在则静默失败 pass # 如果README不存在则静默失败
return templates.TemplateResponse("index.html", { return templates[request.state.language].TemplateResponse("index.html", {
"request": request, "request": request,
"tags": tags, "tags": tags,
"readme_content": readme_content # 新增模板变量 "readme_content": readme_content
}) })
@app.get("/tag/{tag}", response_class=HTMLResponse) @app.get("/tag/{tag}", response_class=HTMLResponse)
async def list(request: Request, tag: str): async def list(request: Request, tag: str):
tags, scales = load_all_scales() tags, scales = load_all_scales(request.state.language)
return templates.TemplateResponse("list.html", { return templates[request.state.language].TemplateResponse("list.html", {
"request": request, "request": request,
"tags": tags, "tags": tags,
"scales": scales, "scales": scales,
@ -93,10 +146,10 @@ async def list(request: Request, tag: str):
@app.get("/scales/{scale_id}", response_class=HTMLResponse) @app.get("/scales/{scale_id}", response_class=HTMLResponse)
async def scale(request: Request, scale_id: str): async def scale(request: Request, scale_id: str):
tags, scales = load_all_scales() tags, scales = load_all_scales(request.state.language)
scale = scales.get(scale_id) scale = scales.get(scale_id)
if scale: if scale:
return templates.TemplateResponse("scale.html", { return templates[request.state.language].TemplateResponse("scale.html", {
"request": request, "request": request,
"scale_id": scale_id, "scale_id": scale_id,
"scale": scale, "scale": scale,
@ -107,7 +160,7 @@ async def scale(request: Request, scale_id: str):
@app.post("/scales/{scale_id}", response_class=HTMLResponse) @app.post("/scales/{scale_id}", response_class=HTMLResponse)
async def result(request: Request, scale_id: str, db: Session = Depends(get_db)): async def result(request: Request, scale_id: str, db: Session = Depends(get_db)):
form_data = await request.form() form_data = await request.form()
tags, scales = load_all_scales() tags, scales = load_all_scales(request.state.language)
scale = scales.get(scale_id) scale = scales.get(scale_id)
if scale: if scale:
responses = {} responses = {}
@ -144,7 +197,7 @@ async def result(request: Request, scale_id: str, db: Session = Depends(get_db))
db.commit() db.commit()
except Exception as e: except Exception as e:
print(e) print(e)
return templates.TemplateResponse("result.html", { return templates[request.state.language].TemplateResponse("result.html", {
"request": request, "request": request,
"responses": responses, "responses": responses,
"average": average, "average": average,

42
templates/en/base.html Normal file
View File

@ -0,0 +1,42 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="PsychoScales心理学专业量表数据库提供标准化心理测评工具包含人格测试、症状评估等2000+临床心理学量表,支持在线测评及科研数据导出。">
<meta name="keywords" content="心尺, PsychoScales, 心理学量表,专业心理测评,抑郁症测试,焦虑症自评,在线心理测评系统,心理咨询师工具, 心理测验, 报告解读, 测试题">
<meta name="baidu-site-verification" content="codeva-mPOBUr0rLS" />
<link rel="stylesheet" href="/static/styles.css">
<link rel="icon" type="image/x-icon" href="/static/favicon.ico">
{% block head_extra %}{% endblock %}
</head>
<body>
<nav>
<div class="nav-left">
<a href="/" class="logo">PsychoScales.Org</a>
</div>
<div class="menu-toggle" onclick="document.querySelector('.nav-links').classList.toggle('active')"></div>
<ul class="nav-links">
<li><a href="https://doc.psychoscales.com/"">Docs</a></li>
{% for tag in tags %}
<li><a href=" /tag/{{ tag }}"">{{ tag }}</a></li>
{% endfor %}
</ul>
</nav>
<main>
{% block main %}{% endblock %}
</main>
<!-- 添加KaTeX支持 -->
<link rel=" stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js" onload="renderMathInElement(document.body, {delimiters: [
{left: '$$', right: '$$', display: true},
{left: '$', right: '$', display: false}]});">
</script>
</body>
</html>

13
templates/en/index.html Normal file
View File

@ -0,0 +1,13 @@
{% extends 'base.html' %}
{% block head_extra %}
<title>PsychoScales.Org</title>
{% endblock %}
{% block main %}
{% if readme_content %}
<div class="readme-section">
{{ readme_content|safe }}
</div>
{% endif %}
{% endblock %}

21
templates/en/index.md Normal file
View File

@ -0,0 +1,21 @@
# PsychoScales.Org
## About Psychoscales.Org
This is an open source scale framework, built on Python.
The scale loading of this project is completely based on YAML, which strikes a balance between easy editing and easy loading.
[Open source address](https://git.mxr612.io/PsychoScales/PsychoScalesOrg)
Contact us: feedback@psychoscales.com
User community: QQ 513869177
## About PsychoScale
PsychoScale is a professional psychology resource platform that aims to provide users with comprehensive psychology measurement tools and assessment resources. We have brought together a variety of psychological measurement tools covering multiple fields such as emotions, personality, cognitive ability, and mental health to help users better understand their own psychological characteristics and conditions.
As a user of PsychoScale, you can quickly obtain detailed reports and analysis of your own psychological characteristics through simple online tests. These measurement tools are based on authoritative psychological theories and research, ensuring the accuracy and reliability of test results.
This website aims to provide free scale sharing and result calculation.
If you need analysis, you can get a comprehensive and in-depth analysis of AI results at [Psychoscales](https://www.psychoscales.com/).

16
templates/en/list.html Normal file
View File

@ -0,0 +1,16 @@
{% extends 'base.html' %}
{% block head_extra %}
<title>{{ tag }}</title>
{% endblock %}
{% block main %}
<div class="scale-list">
{% for scale_id, scale in scales.items() %}
{% if scale.tag == tag %}
<a class="title" href="/scales/{{ scale_id }}">{{ scale.get('title','Untitled') }}</a>
<p>{{ scale.abstract|safe }}</p>
{% endif %}
{% endfor %}
</div>
{% endblock %}

17
templates/en/result.html Normal file
View File

@ -0,0 +1,17 @@
{% extends 'base.html' %}
{% block head_extra %}
<title>{{scale.title}}</title>
{% endblock %}
{% block main %}
<h1>{{ scale.title }} </h1>
<ul>
{% for key, value in responses.items() %}
<li>{{ key }}: Got {{ value }} in a {{options[key][0]}} to {{options[key][1]}} scale, with {{ average[key] }} in average</li>
{% endfor %}
</ul>
<div>
{{scale.descriptions|safe}}
</div>
{% endblock %}

24
templates/en/scale.html Normal file
View File

@ -0,0 +1,24 @@
{% extends 'base.html' %}
{% block head_extra %}
<title>{{ scale.title }}</title>
{% endblock %}
{% block main %}
<h1>{{ scale.title }}</h1>
<div>
{{ scale.instructions|safe }}
</div>
<form class="scale" action="/scales/{{ scale_id }}" method="post">
{% for id, question in scale['items'].items() %}
<label for="{{ id }}">{{ id }}. {{ question }}</label>
<div class="scale-button">
{% for option, label in scale.options.items() %}
<input type="radio" id="{{ id }}_{{ option }}" name="{{ id }}" value="{{ option }}" required>
<label for="{{ id }}_{{ option }}" title="{{label}}">{{ option }}</label>
{% endfor %}
</div>
{% endfor %}
<input type="submit" value="Submit">
</form>
{% endblock %}

View File

@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<!-- <html lang="zh"> --> <html lang="zh">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">

View File

@ -1,13 +1,13 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block head_extra %} {% block head_extra %}
<title>心尺.Org</title> <title>心尺.Org</title>
{% endblock %} {% endblock %}
{% block main %} {% block main %}
{% if readme_content %} {% if readme_content %}
<div class="readme-section"> <div class="readme-section">
{{ readme_content|safe }} {{ readme_content|safe }}
</div> </div>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

21
templates/zh/index.md Normal file
View File

@ -0,0 +1,21 @@
# 心尺.Org
## 关于心尺.Org
这个一个开源的量表框架基于Python搭建。
本项目的量表加载完全基于YAML在易于编辑和易于加载中间达到了平衡。
[开源地址](https://git.mxr612.io/PsychoScales/PsychoScalesOrg)
联系我们feedback@psychoscales.com
用户社区QQ 513869177
## 关于心尺
心尺是一个专业的心理学资源平台,旨在为用户提供全面的心理学测量工具和评估资源。我们汇集了各种心理测量工具,涵盖了情绪、人格、认知能力、心理健康等多个领域,帮助用户更好地了解自己的心理特点和状况。
作为心尺的用户,您可以通过简单的在线测试,快速获取关于自己心理特征的详细报告和分析。这些测量工具基于权威的心理学理论和研究,确保了测试结果的准确性和可靠性。
本网站旨在提供免费的量表分享和结果计算。
如果您需要解析,您可以在[心尺](https://www.psychoscales.com/)得到全面、深入的AI结果解析。

View File

@ -1,17 +1,17 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block head_extra %} {% block head_extra %}
<title>{{scale.title}}</title> <title>{{scale.title}}</title>
{% endblock %} {% endblock %}
{% block main %} {% block main %}
<h1>{{ scale.title }} </h1> <h1>{{ scale.title }} </h1>
<ul> <ul>
{% for key, value in responses.items() %} {% for key, value in responses.items() %}
<li>{{ key }}: 在 {{options[key][0]}} 到 {{options[key][1]}} 的量表中得分 {{ value }},均分 {{ average[key] }}</li> <li>{{ key }}: 在 {{options[key][0]}} 到 {{options[key][1]}} 的量表中得分 {{ value }},均分 {{ average[key] }}</li>
{% endfor %} {% endfor %}
</ul> </ul>
<div> <div>
{{scale.descriptions|safe}} {{scale.descriptions|safe}}
</div> </div>
{% endblock %} {% endblock %}

View File

@ -1,24 +1,24 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% block head_extra %} {% block head_extra %}
<title>{{ scale.title }}</title> <title>{{ scale.title }}</title>
{% endblock %} {% endblock %}
{% block main %} {% block main %}
<h1>{{ scale.title }}</h1> <h1>{{ scale.title }}</h1>
<div> <div>
{{ scale.instructions|safe }} {{ scale.instructions|safe }}
</div> </div>
<form class="scale" action="/scales/{{ scale_id }}" method="post"> <form class="scale" action="/scales/{{ scale_id }}" method="post">
{% for id, question in scale['items'].items() %} {% for id, question in scale['items'].items() %}
<label for="{{ id }}">{{ id }}. {{ question }}</label> <label for="{{ id }}">{{ id }}. {{ question }}</label>
<div class="scale-button"> <div class="scale-button">
{% for option, label in scale.options.items() %} {% for option, label in scale.options.items() %}
<input type="radio" id="{{ id }}_{{ option }}" name="{{ id }}" value="{{ option }}" required> <input type="radio" id="{{ id }}_{{ option }}" name="{{ id }}" value="{{ option }}" required>
<label for="{{ id }}_{{ option }}" title="{{label}}">{{ option }}</label> <label for="{{ id }}_{{ option }}" title="{{label}}">{{ option }}</label>
{% endfor %} {% endfor %}
</div> </div>
{% endfor %} {% endfor %}
<input type="submit" value="提交"> <input type="submit" value="提交">
</form> </form>
{% endblock %} {% endblock %}