mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-12-06 13:34:13 +03:00
164 lines
5.2 KiB
Python
164 lines
5.2 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""
|
||
|
|
Generate English translation JSON files from Pydantic config models.
|
||
|
|
|
||
|
|
This script dynamically extracts all top-level config sections from FrigateConfig
|
||
|
|
and generates JSON translation files with titles and descriptions for the web UI.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import json
|
||
|
|
import logging
|
||
|
|
import shutil
|
||
|
|
from pathlib import Path
|
||
|
|
from typing import Any, Dict, Optional, get_args, get_origin
|
||
|
|
|
||
|
|
from pydantic import BaseModel
|
||
|
|
from pydantic.fields import FieldInfo
|
||
|
|
|
||
|
|
from frigate.config.config import FrigateConfig
|
||
|
|
|
||
|
|
logging.basicConfig(level=logging.INFO)
|
||
|
|
logger = logging.getLogger(__name__)
|
||
|
|
|
||
|
|
|
||
|
|
def get_field_translations(field_info: FieldInfo) -> Dict[str, str]:
|
||
|
|
"""Extract title and description from a Pydantic field."""
|
||
|
|
translations = {}
|
||
|
|
|
||
|
|
if field_info.title:
|
||
|
|
translations["label"] = field_info.title
|
||
|
|
|
||
|
|
if field_info.description:
|
||
|
|
translations["description"] = field_info.description
|
||
|
|
|
||
|
|
return translations
|
||
|
|
|
||
|
|
|
||
|
|
def process_model_fields(model: type[BaseModel]) -> Dict[str, Any]:
|
||
|
|
"""
|
||
|
|
Recursively process a Pydantic model to extract translations.
|
||
|
|
|
||
|
|
Returns a nested dictionary structure matching the config schema,
|
||
|
|
with title and description for each field.
|
||
|
|
"""
|
||
|
|
translations = {}
|
||
|
|
|
||
|
|
model_fields = model.model_fields
|
||
|
|
|
||
|
|
for field_name, field_info in model_fields.items():
|
||
|
|
field_translations = get_field_translations(field_info)
|
||
|
|
|
||
|
|
# Get the field's type annotation
|
||
|
|
field_type = field_info.annotation
|
||
|
|
|
||
|
|
# Handle Optional types
|
||
|
|
origin = get_origin(field_type)
|
||
|
|
|
||
|
|
if origin is Optional or (
|
||
|
|
hasattr(origin, "__name__") and origin.__name__ == "UnionType"
|
||
|
|
):
|
||
|
|
args = get_args(field_type)
|
||
|
|
field_type = next(
|
||
|
|
(arg for arg in args if arg is not type(None)), field_type
|
||
|
|
)
|
||
|
|
|
||
|
|
# Handle Dict types (like Dict[str, CameraConfig])
|
||
|
|
if get_origin(field_type) is dict:
|
||
|
|
dict_args = get_args(field_type)
|
||
|
|
|
||
|
|
if len(dict_args) >= 2:
|
||
|
|
value_type = dict_args[1]
|
||
|
|
|
||
|
|
if isinstance(value_type, type) and issubclass(value_type, BaseModel):
|
||
|
|
nested_translations = process_model_fields(value_type)
|
||
|
|
|
||
|
|
if nested_translations:
|
||
|
|
field_translations["properties"] = nested_translations
|
||
|
|
elif isinstance(field_type, type) and issubclass(field_type, BaseModel):
|
||
|
|
nested_translations = process_model_fields(field_type)
|
||
|
|
if nested_translations:
|
||
|
|
field_translations["properties"] = nested_translations
|
||
|
|
|
||
|
|
if field_translations:
|
||
|
|
translations[field_name] = field_translations
|
||
|
|
|
||
|
|
return translations
|
||
|
|
|
||
|
|
|
||
|
|
def generate_section_translation(
|
||
|
|
section_name: str, field_info: FieldInfo
|
||
|
|
) -> Dict[str, Any]:
|
||
|
|
"""
|
||
|
|
Generate translation structure for a top-level config section.
|
||
|
|
"""
|
||
|
|
section_translations = get_field_translations(field_info)
|
||
|
|
field_type = field_info.annotation
|
||
|
|
origin = get_origin(field_type)
|
||
|
|
|
||
|
|
if origin is Optional or (
|
||
|
|
hasattr(origin, "__name__") and origin.__name__ == "UnionType"
|
||
|
|
):
|
||
|
|
args = get_args(field_type)
|
||
|
|
field_type = next((arg for arg in args if arg is not type(None)), field_type)
|
||
|
|
|
||
|
|
# Handle Dict types (like detectors, cameras, camera_groups)
|
||
|
|
if get_origin(field_type) is dict:
|
||
|
|
dict_args = get_args(field_type)
|
||
|
|
if len(dict_args) >= 2:
|
||
|
|
value_type = dict_args[1]
|
||
|
|
if isinstance(value_type, type) and issubclass(value_type, BaseModel):
|
||
|
|
nested = process_model_fields(value_type)
|
||
|
|
if nested:
|
||
|
|
section_translations["properties"] = nested
|
||
|
|
|
||
|
|
# If the field itself is a BaseModel, process it
|
||
|
|
elif isinstance(field_type, type) and issubclass(field_type, BaseModel):
|
||
|
|
nested = process_model_fields(field_type)
|
||
|
|
if nested:
|
||
|
|
section_translations["properties"] = nested
|
||
|
|
|
||
|
|
return section_translations
|
||
|
|
|
||
|
|
|
||
|
|
def main():
|
||
|
|
"""Main function to generate config translations."""
|
||
|
|
|
||
|
|
# Define output directory
|
||
|
|
output_dir = Path(__file__).parent / "web" / "public" / "locales" / "en" / "config"
|
||
|
|
|
||
|
|
logger.info(f"Output directory: {output_dir}")
|
||
|
|
|
||
|
|
# Clean and recreate the output directory
|
||
|
|
if output_dir.exists():
|
||
|
|
logger.info(f"Removing existing directory: {output_dir}")
|
||
|
|
shutil.rmtree(output_dir)
|
||
|
|
|
||
|
|
logger.info(f"Creating directory: {output_dir}")
|
||
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||
|
|
|
||
|
|
config_fields = FrigateConfig.model_fields
|
||
|
|
logger.info(f"Found {len(config_fields)} top-level config sections")
|
||
|
|
|
||
|
|
for field_name, field_info in config_fields.items():
|
||
|
|
if field_name.startswith("_"):
|
||
|
|
continue
|
||
|
|
|
||
|
|
logger.info(f"Processing section: {field_name}")
|
||
|
|
section_data = generate_section_translation(field_name, field_info)
|
||
|
|
|
||
|
|
if not section_data:
|
||
|
|
logger.warning(f"No translations found for section: {field_name}")
|
||
|
|
continue
|
||
|
|
|
||
|
|
output_file = output_dir / f"{field_name}.json"
|
||
|
|
with open(output_file, "w", encoding="utf-8") as f:
|
||
|
|
json.dump(section_data, f, indent=2, ensure_ascii=False)
|
||
|
|
|
||
|
|
logger.info(f"Generated: {output_file}")
|
||
|
|
|
||
|
|
logger.info("Translation generation complete!")
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
main()
|