diff --git a/DOCS.md b/DOCS.md
index 089898e..4ee18c5 100644
--- a/DOCS.md
+++ b/DOCS.md
@@ -1,29 +1,32 @@
-# Flask++ Documentation
+# Flask++ v0.3.x Documentation
-> ⚠️ Note: The documentation is currently being updated to reflect Flask++ v0.3.x.
-> Some advanced sections may still reference older defaults.
+## Core
### App Factory
-The default Flask app factory hasn't changed much. The FlaskPP class does a lot of repetitive work for you.
-But the default factory which is also written into **project_root/main.py** by the `fpp init` command looks very similar:
+The default Flask app factory hasn't changed much. But the FlaskPP class can do a lot of repetitive work for you.
+A sample factory is written into **project_root/main.py** by the `fpp init` command:
```python
from flaskpp import FlaskPP
-
-def create_app(config_name: str = "default"):
- app = FlaskPP(__name__, config_name)
+
+def create_app():
+ app = FlaskPP(__name__)
+
# TODO: Extend the Flask++ default setup with your own factory
+
return app
-app = create_app().to_asgi()
+if __name__ == "__main__":
+ app = create_app()
+ app.start()
```
-The FlaskPP class just extended Flask with basic factory tasks like setting up extensions and config.
+The FlaskPP class just extends Flask with basic factory tasks like setting up the most common extensions and its config.
### Configuration
-There are two ways of configuring your apps. The first and most important one is app configs, which you can find in project_root/app_configs.
+There are two ways of configuring your apps. The first and most important one is app configs, which you can find in **project_root/app_configs**.
They are named like that: **[app_name].conf** and used by the `fpp run` command to load your apps. (Config variables are passed as environment.)
With app configs you can control which extensions you want to use and pass secrets, defaults and every other data that you would usually write into your env files.
@@ -38,7 +41,7 @@ SECRET_KEY = supersecret
DATABASE_URL = sqlite:///appdata.db
[redis]
-REDIS_URL = redis://redis:6379
+REDIS_URL = redis://localhost:6379
[babel]
SUPPORTED_LOCALES = en;de
@@ -71,31 +74,28 @@ EXT_JWT_EXTENDED = 0
[features]
FPP_PROCESSING = 1
+FPP_I18N_FALLBACK = 1
+AUTOGENERATE_TAILWIND_CSS = 1
+FPP_MODULES = 1
FRONTEND_ENGINE = 1
[dev]
DB_AUTOUPDATE = 0
[modules]
-module_name = 1
-HOME_MODULE = module_name
+module_id_01 = 0
+module_id_02 = 0
+...
+
+MODULE_HOME = module_id
```
And can be generated and configured automatically by running `fpp setup` inside your project root.
-The second way of configuring your app, is by using config classes. You may have noticed that the Flask++ app factory takes a
-config name argument. There you can provide your own config name if you like to. We provide a registration function, so you can
-plug in your own config files with ease. But of course we also provide a default config which you could extend by your own config class:
+The second way of configuring your app is by using config classes. We provide a registration function, so you can plug in your own config classes with ease.
+They will be extended with each other to one big config class by priority (values 1 to 10). In the end it will be extended with our default config class:
```python
-import os
-
-from flaskpp.app.config import register_config
-# If you want to extend this config, you can import it:
-# from flaskpp.app.config.default import DefaultConfig
-
-
-@register_config('default')
class DefaultConfig:
# -------------------------------------------------
# Core / Flask
@@ -199,202 +199,818 @@ class DefaultConfig:
JWT_REFRESH_TOKEN_EXPIRES = 86400
```
-### Full-Stack Features & FPP Magic
+So you can overwrite config keys by priority. This is especially useful to provide specific configuration for your modules.
+
+### Lifecycle
-Inside our app.conf example you may have noticed that there are two feature switches, which are set to 1 by default.
-The first one is **FPP_PROCESSING** which brings a slightly changed app processor registration to offer you some features
-like request logging and socket default events. If you have enabled this, you would have to use the Flask++
-processing utils to overwrite our default processors:
+You may have noticed the `app.start()` method inside our autogenerated **main.py** file shown in the ["App Factory" chapter](#app-factory).
+This method is an automated lifecycle manager that replaces the `app.run()` method. It will automatically run itself inside
+a daemon thread using Uvicorn. To handle SIGINT and SIGTERM signals, it uses a `threading.Event` flag. The FlaskPP class provides
+decorators to register startup and shutdown hooks which will be executed before and after the app starts and stops. You can register
+hooks like that:
```python
-from flaskpp.app.utils.processing import before_request
+@app.on_startup
+def startup_hook():
+ # TODO: Whatever you want to do before the server thread gets started
+ pass
-@before_request
-def before_app_request():
- # TODO: Handle app request
+@app.on_shutdown
+def shutdown_hook():
+ # TODO: Whatever you want to do when the server thread has been stopped
pass
+```
+
+### ASGI Wrapper
+
+If you are running your app using the start method, you may ask how a wsgi app is running itself inside an asgi server.
+Well, therefore the FlaskPP class provides a `app.to_asgi()` wrapper. It will map itself into an asgi compatible class,
+but sensitive to the **EXT_SOCKET** switch:
+
+```python
+class FlaskPP(Flask):
+ ...
+ def to_asgi(self) -> WsgiToAsgi | ASGIApp:
+ if self._asgi_app is not None:
+ return self._asgi_app
+
+ wsgi = WsgiToAsgi(self)
+
+ if enabled("EXT_SOCKET"):
+ from flaskpp.app.extensions import socket
+ app = ASGIApp(socket, other_asgi_app=wsgi)
+ else:
+ app = wsgi
+
+ self._asgi_app = app
+ return self._asgi_app
+```
+
+## Modules
+
+Modules are the most important feature of Flask++ apps. They work like little blueprint-based Flask apps, which can be
+plugged into your main app using the module switches inside your app config files. Of course, you can turn off this feature
+by setting the **FPP_MODULES** switch to 0.
+
+### Module Setup
+
+The most easy way to create a module is to use the `fpp modules create` command. But you can also create modules manually.
+It would be way more work, but we will explain what it would look like. Not because it is recommended but to provide a basic
+understanding of how modules work.
+
+At first, you would create a new python package inside **project_root/modules**. It is recommended to use the module id as
+the package name, but you don't necessarily have to.
+
+#### Manifest
+
+For your module to be recognized by Flask++ you need to create a **manifest.json** file inside your module package. It has
+to contain at least one field named "version". The following version string formats are supported:
+
+```
+x
+x alpha
+x beta
+
+x.x
+x.x alpha
+x.x beta
+
+x.x.x
+x.x.x alpha
+x.x.x beta
+```
+
+A fully qualified manifest file would look like this:
+
+```json5
+{
+ "id": "module_id", // if not set, the package name will be used as id
+ "name": "Module Name",
+ "description": "This module does ...",
+ "version": "0.1",
+ "requires": {
+ "fpp": ">=0.3.7", // the minimum required version of Flask++
+ "modules": { // other modules that are required by this module
+ "module_id_01": "==0.2",
+ "module_id_02": "<0.7"
+ }
+ }
+}
+```
+
+#### Init File
+
+Inside your **\_\_init__.py** file, you create `module: Module` variable and optionally register a `module.on_enable` hook:
+
+```python
+from flaskpp import Module
+from flaskpp.utils import enabled
+from flaskpp.exceptions import ModuleError
+
+module = Module(
+ __file__,
+ __name__,
+
+ # Optional a list of required_extensions:
+ [
+ # "sqlalchemy",
+ # "socket",
+ # "babel",
+ # "fst",
+ # "authlib",
+ # "mailing",
+ # "cache",
+ # "api",
+ # "jwt_extended"
+ ],
+
+ # And you can optionally turn off init_routes_on_enable (default is True):
+ False
+)
+
+# Now if you need to do stuff when your module gets enabled:
+@module.on_enable
+def on_enable(app: FlaskPP):
+ # Check for required features, for example:
+ if not enabled("FPP_PROCESSING"):
+ raise ModuleError(f"Module '{module.module_name}' requires FPP_PROCESSING.")
+
+ with app.app_context():
+ # TODO: Do something that requires the app context if you need to
+ pass
+
+ # And if you disabled automated route initialization:
+ module.init_routes()
+
+ # The module has got its own context dict, which is used the modules context_processor.
+ # So you can set up default variables which will be available inside your modules templates:
+ module.context["my_variable"] = "Hello World!"
+
+ # You do not need to register your module as a blueprint, this will happen automatically
+
+```
+
+#### Handling
+
+To abstract the handling of your module from its routes, the Module class provides a `module.handle_request(handler_name: str)` function.
+You can combine this with a handling package inside your module. The handling feature is automated as well. For it to work, you need to
+create a `init_handling(mod: Module)` function inside its **\_\_init__.py** file. The default init file created by the
+`fpp modules create` command looks like this:
+
+```python
+from pathlib import Path
+from importlib import import_module
+
+from flaskpp import Module
+
+_package = Path(__file__).parent
+
+def init_handling(mod: Module):
+ for file in _package.rglob("*.py"):
+ if file.stem == "__init__" or file.stem.startswith("noinit"):
+ continue
+ handler_name = file.stem
+ handler = import_module(f"{mod.import_name}.handling.{handler_name}")
+ handle_request = getattr(handler, "handle_request", None)
+ if not handle_request:
+ continue
+ mod.handler(handler_name)(handle_request)
+```
-# Or if you are using EXT_SOCKET we provide a default handler:
-from flaskpp.app.utils.processing import socket_event_handler
+Handler files should look like this:
-@socket_event_handler # equivalent to socket.on("default_event")
-def default_event(sid: str, data):
- # TODO: Your own default handling
+```python
+def handle_request(mod: Module, *args):
+ # TODO: Handle your request here
pass
+```
+
+But you can also build your own handling structure working with the `module.handler(handler_name: str)` decorator,
+as long as you stick with the handle_request function signature shown above. The module parameter will be passed
+by a wrapper inside the `module.hander(handler_name: str)` decorator.
+
+#### Routes
+
+You will also need to create a **routes.py** file inside your module package. This file will have to contain an
+`init_routes(mod: Module)` function, which will be imported and called automatically by the `module.init_routes()` function.
+So your file should look something like this:
+
+```python
+def init_routes(mod: Module):
+ @mod.route("/my-route")
+ def my_route():
+ # Modules provide their own render template function, so you can easily target the templates of your module:
+ return mod.render_template("module_template.html")
+ # This automatically targets the templates folder inside your module package.
+
+ # Of course, you can work with your handlers, which we set up before:
+ mod.route("/handle")(
+ mod.handle_request("my_handler")
+ )
+
+ # Or you can go really crazy and do something like this, for example:
+ @mod.route("/handle//")
+ def handle_id(handler_name: str, path: str):
+ return mod.handle_request(handler_name)(path)
+```
+
+#### Config
+
+You can optionally create a **config.py** file inside your module package. There you can create your modules config class
+(like mentioned earlier in the ["Configuration" chapter](#configuration)) if you need:
-# If you decide to use FPP_PROCESSING you can register socket events with:
-from flaskpp.app.socket import default_event
+```python
+from flaskpp.app.config import register_config
-@default_event("my_event")
-def handle(data):
- # TODO: Handle your default socket event
+@register_config(
+ # Optionally set a priority (default is 1):
+ priority=2
+)
+class ModuleConfig:
+ # TODO: Overwrite default config values or provide your own
pass
```
-And of course, we do also have some JavaScript utility that matches with our socket default handlers:
+#### Data Package
-```javascript
-/**
- * To use this utility you just need to include this inside the head section of you base template:
- *
- *
- *
- * */
-
-const socketScript = document.getElementById("fppSocketScript");
-
-export function connectSocket() {
- const domain = socketScript.dataset.socketDomain;
- return io(domain, {
- transports: ['websocket'],
- reconnection: true,
- reconnectionAttempts: 5,
- reconnectionDelay: 1000,
- reconnectionDelayMax: 5000,
- timeout: 20000
- })
-}
-export let socket = connectSocket();
+If your module requires **EXT_SQLALCHEMY** you need to create a **data** package inside your module. There you can create
+your modules model classes. For this to work, you have to create an `init_models()` function inside its **\_\_init__.py** file.
+The generated default looks like this:
+```python
+from pathlib import Path
+from importlib import import_module
-export function emit(event, data=null, callback=null) {
- socket.emit('default_event', {
- event: event,
- payload: data
- }, callback);
-}
+from flaskpp import Module
+
+_package = Path(__file__).parent
+
+def init_models(mod: Module):
+ for file in _package.rglob("*.py"):
+ if file.stem == "__init__" or file.stem.startswith("noinit"):
+ continue
+ import_module(f"{mod.import_name}.data.{file.stem}")
```
-Alright, before we talk about some further switch-less Flask++ magic... Let's talk about the second feature switch in our
-app.conf file, which is **FRONTEND_ENGINE**. This switch enables you to use the built-in Vite engine. Your app and every module you may create has
-got a Vite folder inside it, which contains a main.js entrypoint. This is generated by default as a template you can use to
-integrate Vite into your Flask project. It will eiter run as `vite dev` if you run your app in debug mode or be built when starting your app
-and then integrated using the .vite/manifest.json. If you want to integrate Vite files into your template, use:
-`{{ vite_main("file.ending") }}` to integrate Vite files from your apps root and `{{ vite("file.ending") }}` inside module templates
-to use the vite files of your modules. The framework does the configuration automatically for you. You can either write JavaScript or TypeScript.
-And you can also work with Tailwind (Notice that we are using the standalone Tailwind integration to generate your CSS.
-This is done to reduce redundance. Anyway the CSS of your vite builds is generated seperately, to ensure easy export into standalone Node projects
-using the Tailwindcss Vite plugin. That's why we highly recommend keeping the autogenerated `@source not "../../vite"` part.), the default structure for
-that is autogenerated as well.
-
-Okay, but now let's come to further Flask++ magic. The biggest switch-less feature is our module system. Modules look like little Flask apps
-which can simply be plugged into your app using the app.conf file. This process can be automated, if you install or create your modules before
-running `fpp setup`. To work with modules, use the modules sub-cli:
-
-```bash
-# To install a module use:
-fpp modules install module_name --src [path/git-url]
-# And in future a module hub is planned, on which you'll be able to share your modules
-# as well as install modules from there by their name:
-fpp modules install hub_module_name
-
-# To create a module use:
-fpp modules create module_name
+### Working with Modules
+
+To simplify the work with modules, modules provide their own render_template (like mentioned above) and url_for functions.
+So when you are handling a request inside a module, we recommend using these functions instead of the global ones. Modules
+are sensitive to the **HOME_MODULE** configuration. Defining a home module is optional, but if you do so, the module will
+be registered as if it was the main app when it gets enabled.
+
+#### Context
+
+As shown earlier, modules do have their own context dict. It has a default value called "NAME", which you can use inside your
+modules templates to work resolve its templates or urls:
+
+```html
+{% extends NAME ~ "/base.html" %}
+
+{% block content %}
+
+{% endblock %}
```
-Our next feature is the i18n database (**EXT_BABEL**) with fallback to .po/.mo files for which we will offer an extra module to manage your translation keys
-using a graphical web interface. ([FPP_i18n_module](https://github.com/GrowVolution/FPP_i18n_module) – coming soon.) But you can also manage your translations
-by using our utility functions:
+## Features
+
+When you take a look at our app config example, which we have shown in the ["Configuration" chapter](#configuration), you may have noticed that
+there are some feature switches. Besides those switches, some of the extension switches are also enabling adopted Flask++ extension
+classes, which extend the underlying base extension classes. We will show you which extensions are affected, so you can turn them off
+if you would like to stick with Flask defaults.
+
+The FlaskPP class uses the `flaskpp.utils.enabled` function to check if a feature or extension switch is enabled. If so, it will
+automatically set up the specific feature or initialize the extension.
+
+### FPP_PROCESSING
+
+The first feature switch is **FPP_PROCESSING**. This will cause the app to use the Flask++ processing utils to register request processors.
+Therefore, it uses the `flaskpp.app.utils.processing` module. It provides its own decorators that replace the Flask ones, which is useful
+if you want to use some of the Flask++ processing utility. The decorators will then overwrite the Flask++ default processors.
+
+Flask++ default processors provide some useful features like extended context processing (matching to the Flask++ environment),
+request logging and error handling (rendering "404.html" on NotFound errors or else "error.html").
+
+To overwrite processors, use their matching decorators like this:
```python
-from flaskpp.app.data.babel import (add_entry,
- remove_entry, remove_entries,
- get_entry, get_entries)
+from flaskpp.app.utils.processing import (
+ # context_processor,
+ before_request #,
+ # after_request,
+ # handle_app_error
+)
-add_entry("en", "WELCOME_TEXT", "Welcome to our new website!")
-# TODO: Further work with the i18n database
+@before_request
+def before_request():
+ # TODO: Do your own handling
+ pass
```
-And if you use Flask Security Too (**EXT_FST**), you can easily modify and extend the fsqla mixin using our mixin decorators:
+The FlaskPP class uses the `set_default_handlers(app: Flask)` function its default processors. This util then uses the `get_handler(name: str) -> Callable`
+function to get the matching handler from the processing utils. Both are part of the processing module and look like this:
```python
-from flaskpp.app.data.fst_base import user_mixin, role_mixin
-from flaskpp.app.extensions import db
+def get_handler(name: str) -> Callable:
+ handler = _handlers.get(name)
+ if not handler or not callable(handler):
+ if name == "context_processor":
+ return _context_processor
+ if name == "before_request":
+ return _before_request
+ if name == "after_request":
+ return _after_request
+ if name == "handle_app_error":
+ return _handle_app_error
+ return handler
+
+def set_default_handlers(app: Flask):
+ app.context_processor(
+ lambda: get_handler("context_processor")()
+ )
+ app.before_request(
+ lambda : get_handler("before_request")()
+ )
+ app.after_request(
+ lambda response: get_handler("after_request")(response)
+ )
+ app.errorhandler(Exception)(
+ lambda error: get_handler("handle_app_error")(error)
+ )
+```
-@user_mixin
-class MyUserMixin:
- bio = db.Column(db.String(512))
+### EXT_SOCKET
- # TODO: Add your own features and functionality
+When we talk about **FPP_PROCESSING** we should also mention the **EXT_SOCKET** extension switch. It extends SocketIOs AsyncServer
+with some Flask++ specific features. Some of those features are default features of the FppSocket class, and others rely on the
+default_processing switch. The sockets default processing features will either be turned on by the **FPP_PROCESSING** switch or
+by setting the default_processing value of its init function to True.
+
+The default features of the FppSocket class are its event_context (inspired by Flasks request_context) and the enable_sid_passing
+value of its init function (default is True because it's the AsyncServers default too) or the `socket.sid_passing` switch itself
+(if you are using the socket from `flaskpp.app.extensions` and want to turn it off):
+
+```python
+from flaskpp.app.extensions import socket
+from flaskpp.utils.debugger import log
+
+@socket.on("my_event")
+async def event(
+ sid: str, # if you did not set sid_passing to False
+ payload: Any
+):
+ # here you can access:
+ ctx = socket.event_context
+ # which currently only contains the current session, which you can either access via context now:
+ log("info", f"Current session data from context: {ctx.session}")
+ # or via:
+ log("info", f"Current session data from socket: {socket.current_session}")
```
-Your mixin classes extend the user / role model, before the fsqla mixin extension is added. So be careful working with security features and utilities.
-In the future, we'll add a priority feature, which will allow you to define the priority of your mixin when you decide to publish your own modules.
+If you enable default processing, you will get access to the full power of the FppSocket class. This includes default events (a socket
+event called "default_event" or – if you initialize your own FppSocket – whatever default_event_name you pass to its init function),
+a default on_connect handler (which writes the session and lang cookie into the session), error handling, HTML injectors (A default
+event called "html". It can be used to return HTML strings that you can write into DOM blocks with your scripts.), decorators to
+replace the default processing functions with you own ones and a get_handler function to fetch default handlers:
+
+```python
+from flaskpp.app.extensions import socket
+
+@socket.on_default(
+ "my_event",
+ # namespace="my_namespace",
+ # pass_sid=True,
+ # **test_request_ctx
+ # -> Default events are additionally executed inside app.test_request_context() to use Flasks rendering utility there
+)
+async def my_event(
+ # sid: str,
+ payload: Any
+):
+ # TODO: Handle your default event
+ pass
+
+@socket.html_injector(
+ "my_html",
+ # namespace="my_namespace"
+)
+async def my_html():
+ return "
Hello World!
"
+
+# Like you can do with the Flask++ default processors,
+# you can also overwrite the sockets default processors:
-### Running / Managing your apps
+@socket.on_connect
+async def handle_connect(sid: str, environ: dict):
+ # TODO: Handle the incoming socket connection
+ pass
+
+@socket.default_handler
+def on_default(sid: str, data: dict):
+ # TODO: Handle every socket event called default_event (or whatever name you gave it)
+ pass
-Attentive readers may have also noticed the `app.to_asgi()` wrapper. (This wrapper automatically wraps your app into the correct format – so it is sensitive to the **EXT_SOCKET** switch.)
-This feature is required if you want to execute your apps with our built-in executing utility, because Flask++ is running your apps using Uvicorn to offer
-cross-platform compatibility. You've got two options to run your apps:
+@socket.html_handler
+def on_html(key: str):
+ # TODO: Handle every default_event called "html"
+ pass
-```bash
-# To run your apps standalone and straight up:
-fpp run [-a/--app] app_name [-p/--port] 5000 [-d/--debug]
+@socket.on_error
+def handle_error(error: Exception):
+ # TODO: Handle the exception
+ # Every exception that happens while executing your events lands here
+ pass
-# If you would like to run and manage multiple apps at once:
-fpp run [-i/--interactive]
+# You can retrieve default handlers using:
+handler = socket.get_handler(
+ "my_handler",
+ # namespace="my_namespace",
+ # event_type="html"
+ # -> event_type defines which type of handler should be retrieved. It defaults to "default" for default events.
+ # If the passed event_type neither matches "default" nor "html", it will try to fetch the classic BaseServer handler.
+)
+# This function escalates its fallback similar to SocketIOs BaseServer:
+# handler_dict[namespace][handler_name]
+# handler_dict[namespace]["*"]
+# handler_dict["*"][handler_name]
+# handler_dict["*"]["*"]
```
-### App Registry
+Be aware that default events should not contain any "@" inside their names, because the FppSocket resolves namespaces of
+default events using the "@" character.
+
+### EXT_BABEL & FPP_I18N_FALLBACK
-If you are a system administrator, you can also use our automated app registry (of course also cross-platform compatible):
+These two switches come together. The **FPP_I18N_FALLBACK** switch only takes effect if the **EXT_BABEL** switch is enabled too.
+This is because Flask++ also provides its own Babel class called FppBabel, which extends `flask_babelplus.Babel`. Besides that,
+Flask++ also changes the internationalization process to fit into the Flask++ environment. That's why **EXT_BABEL** requires
+the **EXT_SQLALCHEMY** switch to be enabled. The Flask++ i18n system primarily stores translations inside the database
+and only uses the message catalogs as fallback.
-```bash
-fpp registry register app_name
-fpp registry [start/stop] app_name
-fpp registry remove app_name
+It also provides its own domain resolving system, which matches with the Flask++ module system. This is also where the
+**FPP_I18N_FALLBACK** switch comes into play, because it adds a fallback domain called "flaskpp" which contains default
+translations keys providing German and English translations, that are used by the Flask++ [app utility](#further-utilities).
+
+Let's take a look at how you set up Babel for a specific module and how the fallback escalation works:
+
+```python
+# Use the on_enable hook of your module and build your modules __init__.py file like this, for example:
+from flaskpp import Module
+from flaskpp.babel import register_module
+from flaskpp.utils import enabled
+from flaskpp.exceptions import ModuleError
+
+module = Module(
+ __file__,
+ __name__,
+ [
+ "sqlalchemy",
+ "socket",
+ "babel"
+ ]
+)
+
+@module.on_enable
+def enable(app: FlaskPP):
+ # This is where the noinit feature of the default generated data/__init__.py file comes into play:
+ from .data.noinit_translations import setup_db
+ with app.app_context():
+ setup_db(module) # We will show you a quick example of how this could look like in the next code block.
+ register_module(
+ module,
+ # domain_name="custom_name"
+ # -> default is module.name
+ ) # This will register the domain_name as the modules translation domain, pass it as a variable called "DOMAIN" to the
+ # context processor and automatically cause _ and ngettext to primarily resolve translation keys from that domain.
+
+# If you now do something like this, for example:
+from flask import render_template_string
+@module.route("/example")
+def example():
+ return render_template_string(
+ "
{{ _('EXAMPLE_TITLE') }}
"
+ )
+# The EXAMPLE_TITLE translation will be resolved in this order:
+# 1. Try to find it in the modules registered domain_name
+# 2. Try to find it inside FppBabels fallback domain (set by flaskpp.babel.set_fallback_domain function)
+# 3. Try to find it inside "messages" (or whatever you set as the default domain)
+# 4. Try to find it inside the "flaskpp" domain (if FPP_I18N_FALLBACK is enabled)
+# 5. Stick with the original key
```
-On NT-based systems make sure you have pywin32 installed in your Python environment.
+And here is an example of what your **module_package/data/noinit_translations.py** file could look like:
+
+```python
+from flaskpp.app.data import commit
+from flaskpp.app.data.babel import add_entry, get_entries
+
+_msg_keys = [
+ "EXAMPLE_TITLE",
+ # ...
+]
+
+_translations_en = {
+ _msg_keys[0]: "My English Title",
+ #...
+}
+
+_translations_de = {
+ _msg_keys[0]: "Mein Deutscher Titel",
+ # ...
+}
+
+def _add_entries(key, domain):
+ add_entry("en", key, _translations_en[key], domain, False)
+ add_entry("de", key, _translations_de[key], domain, False)
+
+def setup_db(mod: Module):
+ domain = mod.name
+ entries = get_entries(domain=domain, locale="en")
+
+ if entries:
+ keys = [e.key for e in entries]
+ for key in _msg_keys:
+ if key not in keys:
+ _add_entries(key, domain)
+
+ for entry in entries:
+ if _translations_en[entry.key] != entry.text:
+ entry.text = _translations_en[entry.key]
+ else:
+ for key in _msg_keys:
+ _add_entries(key, domain)
+
+ commit()
+```
-