aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--extra/i18n/babel.cfg4
-rw-r--r--extra/i18n/web_interface.pot27
-rw-r--r--fatcat_scholar/hacks.py51
-rw-r--r--fatcat_scholar/templates/home.html4
-rw-r--r--fatcat_scholar/translations/de/LC_MESSAGES/messages.mobin0 -> 635 bytes
-rw-r--r--fatcat_scholar/translations/de/LC_MESSAGES/messages.po28
-rw-r--r--fatcat_scholar/translations/zh/LC_MESSAGES/messages.mobin0 -> 621 bytes
-rw-r--r--fatcat_scholar/translations/zh/LC_MESSAGES/messages.po28
-rw-r--r--fatcat_scholar/web.py94
9 files changed, 220 insertions, 16 deletions
diff --git a/extra/i18n/babel.cfg b/extra/i18n/babel.cfg
new file mode 100644
index 0000000..ff50c82
--- /dev/null
+++ b/extra/i18n/babel.cfg
@@ -0,0 +1,4 @@
+[python 1="**.py" language=":"][/python]
+[jinja2: **/templates/**.html]
+encoding = utf-8
+extensions = jinja2.ext.autoescape,jinja2.ext.with_
diff --git a/extra/i18n/web_interface.pot b/extra/i18n/web_interface.pot
new file mode 100644
index 0000000..b9b0b7f
--- /dev/null
+++ b/extra/i18n/web_interface.pot
@@ -0,0 +1,27 @@
+# Translations template for PROJECT.
+# Copyright (C) 2020 ORGANIZATION
+# This file is distributed under the same license as the PROJECT project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2020.
+#
+#, fuzzy
+msgid ""
+msgstr ""
+"Project-Id-Version: PROJECT VERSION\n"
+"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
+"POT-Creation-Date: 2020-05-12 14:16-0700\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.6.0\n"
+
+#: fatcat_scholar/templates/home.html:8
+msgid "This is a longer paragraph, all of which should be translated."
+msgstr ""
+
+#: fatcat_scholar/templates/home.html:10
+msgid "this is a quick"
+msgstr ""
+
diff --git a/fatcat_scholar/hacks.py b/fatcat_scholar/hacks.py
new file mode 100644
index 0000000..fc1dacd
--- /dev/null
+++ b/fatcat_scholar/hacks.py
@@ -0,0 +1,51 @@
+
+import typing
+import jinja2
+
+from starlette.background import BackgroundTask
+from starlette.templating import _TemplateResponse
+
+class Jinja2Templates:
+ """
+ This is a patched version of starlette.templating.Jinja2Templates that
+ supports extensions (list of strings) passed to jinja2.Environment
+ """
+
+ def __init__(self, directory: str, extensions: typing.List[str] = []) -> None:
+ assert jinja2 is not None, "jinja2 must be installed to use Jinja2Templates"
+ self.env = self.get_env(directory, extensions)
+
+ def get_env(self, directory: str, extensions: typing.List[str] = []) -> "jinja2.Environment":
+ @jinja2.contextfunction
+ def url_for(context: dict, name: str, **path_params: typing.Any) -> str:
+ request = context["request"]
+ return request.url_for(name, **path_params)
+
+ loader = jinja2.FileSystemLoader(directory)
+ env = jinja2.Environment(loader=loader, extensions=extensions, autoescape=True)
+ env.globals["url_for"] = url_for
+ return env
+
+ def get_template(self, name: str) -> "jinja2.Template":
+ return self.env.get_template(name)
+
+ def TemplateResponse(
+ self,
+ name: str,
+ context: dict,
+ status_code: int = 200,
+ headers: dict = None,
+ media_type: str = None,
+ background: BackgroundTask = None,
+ ) -> _TemplateResponse:
+ if "request" not in context:
+ raise ValueError('context must include a "request" key')
+ template = self.get_template(name)
+ return _TemplateResponse(
+ template,
+ context,
+ status_code=status_code,
+ headers=headers,
+ media_type=media_type,
+ background=background,
+ )
diff --git a/fatcat_scholar/templates/home.html b/fatcat_scholar/templates/home.html
index 82eb69d..e6e09b5 100644
--- a/fatcat_scholar/templates/home.html
+++ b/fatcat_scholar/templates/home.html
@@ -4,5 +4,9 @@
</head>
<body>
<h1>The Start</h1>
+
+<p>{% trans %}This is a longer paragraph, all of which should be translated.{% endtrans %}
+
+<p>and {{ _("this is a quick") }} thing to translate.
</body>
</html>
diff --git a/fatcat_scholar/translations/de/LC_MESSAGES/messages.mo b/fatcat_scholar/translations/de/LC_MESSAGES/messages.mo
new file mode 100644
index 0000000..8abbad1
--- /dev/null
+++ b/fatcat_scholar/translations/de/LC_MESSAGES/messages.mo
Binary files differ
diff --git a/fatcat_scholar/translations/de/LC_MESSAGES/messages.po b/fatcat_scholar/translations/de/LC_MESSAGES/messages.po
new file mode 100644
index 0000000..6356808
--- /dev/null
+++ b/fatcat_scholar/translations/de/LC_MESSAGES/messages.po
@@ -0,0 +1,28 @@
+# German translations for PROJECT.
+# Copyright (C) 2020 ORGANIZATION
+# This file is distributed under the same license as the PROJECT project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2020.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PROJECT VERSION\n"
+"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
+"POT-Creation-Date: 2020-05-12 14:16-0700\n"
+"PO-Revision-Date: 2020-05-12 13:45-0700\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.6.0\n"
+
+#: fatcat_scholar/templates/home.html:8
+msgid "This is a longer paragraph, all of which should be translated."
+msgstr "Dies ist ein längerer Absatz, der alle übersetzt werden sollte"
+
+#: fatcat_scholar/templates/home.html:10
+msgid "this is a quick"
+msgstr "das ist schnell"
+
diff --git a/fatcat_scholar/translations/zh/LC_MESSAGES/messages.mo b/fatcat_scholar/translations/zh/LC_MESSAGES/messages.mo
new file mode 100644
index 0000000..16166b3
--- /dev/null
+++ b/fatcat_scholar/translations/zh/LC_MESSAGES/messages.mo
Binary files differ
diff --git a/fatcat_scholar/translations/zh/LC_MESSAGES/messages.po b/fatcat_scholar/translations/zh/LC_MESSAGES/messages.po
new file mode 100644
index 0000000..2ffaacf
--- /dev/null
+++ b/fatcat_scholar/translations/zh/LC_MESSAGES/messages.po
@@ -0,0 +1,28 @@
+# Chinese translations for PROJECT.
+# Copyright (C) 2020 ORGANIZATION
+# This file is distributed under the same license as the PROJECT project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2020.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PROJECT VERSION\n"
+"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
+"POT-Creation-Date: 2020-05-12 14:16-0700\n"
+"PO-Revision-Date: 2020-05-12 13:45-0700\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language: zh\n"
+"Language-Team: zh <LL@li.org>\n"
+"Plural-Forms: nplurals=1; plural=0\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 2.6.0\n"
+
+#: fatcat_scholar/templates/home.html:8
+msgid "This is a longer paragraph, all of which should be translated."
+msgstr "这是一个较长的段落,所有段落均应翻译"
+
+#: fatcat_scholar/templates/home.html:10
+msgid "this is a quick"
+msgstr "这是一个快速"
+
diff --git a/fatcat_scholar/web.py b/fatcat_scholar/web.py
index dfebe01..8725ce0 100644
--- a/fatcat_scholar/web.py
+++ b/fatcat_scholar/web.py
@@ -1,17 +1,24 @@
-
"""
-This file is the FastAPI web application.
+This contains the FastAPI web application and RESTful API.
+
+So far there are few endpoints, so we just put them all here!
"""
from enum import Enum
-from fastapi import FastAPI, APIRouter, Request, Depends
+import babel.support
+from fastapi import FastAPI, APIRouter, Request, Depends, Header
from fastapi.staticfiles import StaticFiles
-from fastapi.templating import Jinja2Templates
+from fatcat_scholar.hacks import Jinja2Templates
from fastapi.responses import HTMLResponse
+from pydantic import BaseModel
I18N_LANG_DEFAULT = "en"
-I18N_LANG_OPTIONS = ["en", "de", "zh"]
+I18N_LANG_TRANSLATIONS = ["de", "zh"]
+I18N_LANG_OPTIONS = I18N_LANG_TRANSLATIONS + [I18N_LANG_DEFAULT,]
+
+class SearchParams(BaseModel):
+ q: str = ""
class LangPrefix:
"""
@@ -46,34 +53,89 @@ class ContentNegotiation:
api = APIRouter()
@api.get("/", operation_id="get_home")
-async def home(request: Request):
+async def home():
return {"endpoints": {"/": "this", "/search": "fulltext search"}}
@api.get("/search", operation_id="get_search")
-async def search(request: Request):
+async def search(query: SearchParams = Depends(SearchParams)):
return {"message": "search results would go here, I guess"}
web = APIRouter()
-templates = Jinja2Templates(directory="fatcat_scholar/templates")
+
+def locale_gettext(translations):
+ def gt(s):
+ return translations.ugettext(s)
+ return gt
+
+def locale_ngettext(translations):
+ def ngt(s, n):
+ return translations.ungettext(s)
+ return ngt
+
+def load_i18n_templates():
+ """
+ This is a hack to work around lack of per-request translation
+ (babel/gettext) locale switching in FastAPI and Starlette. Flask (and
+ presumably others) get around this using global context (eg, in
+ Flask-Babel).
+
+ See related issues:
+
+ - https://github.com/encode/starlette/issues/279
+ - https://github.com/aio-libs/aiohttp-jinja2/issues/187
+ """
+
+ d = dict()
+ for lang_opt in I18N_LANG_OPTIONS:
+ translations = babel.support.Translations.load(
+ dirname="fatcat_scholar/translations",
+ locales=[lang_opt],
+ )
+ templates = Jinja2Templates(
+ directory="fatcat_scholar/templates",
+ extensions=["jinja2.ext.i18n"],
+ )
+ templates.env.install_gettext_translations(translations, newstyle=True)
+ templates.env.install_gettext_callables(
+ locale_gettext(translations),
+ locale_ngettext(translations),
+ newstyle=True,
+ )
+ d[lang_opt] = templates
+ return d
+
+i18n_templates = load_i18n_templates()
@web.get("/", include_in_schema=False)
async def web_home(request: Request, lang: LangPrefix = Depends(LangPrefix), content: ContentNegotiation = Depends(ContentNegotiation)):
if content.mimetype == "application/json":
- return await api_home(request)
- return templates.TemplateResponse("home.html", {"request": request})
+ return await home()
+ return i18n_templates[lang.code].TemplateResponse("home.html", {"request": request, "locale": lang.code, "lang_prefix": lang.prefix})
@web.get("/search", include_in_schema=False)
-async def web_search(request: Request):
+async def web_search(request: Request, query: SearchParams = Depends(SearchParams), lang: LangPrefix = Depends(LangPrefix), content: ContentNegotiation = Depends(ContentNegotiation)):
if content.mimetype == "application/json":
- return await api_search(request)
- return templates.TemplateResponse("search.html", {"request": request})
-
-app = FastAPI()
-
+ return await search(query)
+ return i18n_templates[lang.code].TemplateResponse("search.html", {"request": request})
+
+app = FastAPI(
+ title="Fatcat Scholar",
+ description="Fulltext search interface for scholarly web content in the Fatcat catalog. An Internet Archive project.",
+ version="0.1.0-dev",
+ openapi_url="/api/openapi.json",
+ redoc_url="/api/redoc",
+ docs_url="/api/docs",
+)
app.include_router(web)
for lang_option in I18N_LANG_OPTIONS:
app.include_router(web, prefix=f"/{lang_option}")
+
+# Becasue we are mounting 'api' after 'web', the web routes will take
+# precedence. Requests get passed through the API handlers based on content
+# negotiation. This is counter-intuitive here in the code, but does seem to
+# work, and results in the OpenAPI docs looking correct.
app.include_router(api)
+
app.mount("/static", StaticFiles(directory="fatcat_scholar/static"), name="static")