JustToThePoint English Website Version
JustToThePoint en español

Bible GUI. A Desktop Devotional Companion

I’d far rather be happy than right any day, Douglas Adams, The Hitchhiker’s Guide to the Galaxy

For instance, on the planet Earth, man had always assumed that he was more intelligent than dolphins because he had achieved so much—the wheel, New York, wars and so on—whilst all the dolphins had ever done was muck about in the water having a good time. But conversely, the dolphins had always believed that they were far more intelligent than man —for precisely the same reasons, The Hitchhiker’s Guide to the Galaxy

Bible GUI: A Desktop Devotional Companion

This Python script, bible_gui.py, serves as a comprehensive desktop prayer tool, built with the power of PySide6 (Qt for Python). It is designed to centralize your daily devotional workflow into a single, elegantly styled window featuring three distinct tabs.

The application is built with user experience in mind: it auto-creates configuration files if they are missing, handles missing dependencies (like bible_verse.py) with functional fallbacks, and uses a consistent, dark-themed color palette.

Separable Differentials

#!/usr/bin/env python3
"""
bible_gui.py — Prayer companion (PySide6)
─────────────────────────────────────────
A desktop application with three tabs:

Tab 1 · Verse   Daily verse from Discovery Bible API + local KJV daily plan.
                Browse all verses in daily_plan.txt, navigate prev/next.
Tab 2 · Videos  YouTube prayer videos listed in prayer_videos.txt,
                embedded in a full Chromium WebEngine view.
Tab 3 · Diary   Markdown prayer journal. One file per day stored in
                ~/dotfiles/myArch/config/prayer_diary/. Live preview toggle.

The core Bible‑verse logic is delegated to the `bible_verse` module
(located in the same project directory).  If that module cannot be
imported, the GUI falls back to a local `_load_daily_plan` helper and
hard‑coded stubs so that the application still starts (albeit with
limited functionality).

Requirements:
    pip install PySide6 requests freebible

Config files (auto-created with samples if missing):
    ~/dotfiles/myArch/config/daily_plan.txt    one verse reference per line
    ~/dotfiles/myArch/config/prayer_videos.txt  VIDEO_ID  Title per line
    ~/dotfiles/myArch/config/prayer_diary/      diary .md files

Run:
    python3 bible_gui.py
"""

import sys
import re
import datetime
import subprocess
import shutil
from pathlib import Path

# ── Qt imports ───────────────────────────────────────────────────────────────
# All GUI components come from PySide6.
from PySide6.QtWidgets import (
    QApplication, QMainWindow, QTabWidget, QWidget,
    QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
    QListWidget, QListWidgetItem, QTextEdit,
    QFrame, QScrollArea, QStatusBar, QSplitter,
    QSizePolicy,
)
from PySide6.QtWebEngineWidgets import QWebEngineView # used in Tab 2 for YouTube
from PySide6.QtCore import Qt, QUrl, QTimer
from PySide6.QtGui import QFont, QColor, QShortcut, QKeySequence


# ══════════════════════════════════════════════════════════════════════════════
# PATHS
# ══════════════════════════════════════════════════════════════════════════════

CFG         = Path.home() / "dotfiles/myArch/config"
"""Base configuration directory shared with other dotfile tools."""

PLAN_FILE   = CFG / "bible/daily_plan.txt"
"""Daily plan prayer."""

VIDEOS_FILE = CFG / "bible/prayer_videos.txt"
"""List of YouTube video IDs for prayer videos (Tab 2)."""

DIARY_DIR   = CFG / "bible/prayer_diary"
"""Directory where daily Markdown diary files are stored (Tab 3)."""
DIARY_DIR.mkdir(parents=True, exist_ok=True)  # ensure it exists on startup


# ══════════════════════════════════════════════════════════════════════════════
# PALETTE  (calm night-blue theme)
# ══════════════════════════════════════════════════════════════════════════════

# Colour constants used throughout the stylesheet.
# The theme is dark and easy on the eyes, with a blue accent.
BG      = "#1a1f2e"   # main background
PANEL   = "#222840"   # slightly lighter background for panels
BORDER  = "#2d3550"   # border / separators
ACCENT  = "#5b8fd4"   # primary accent colour (buttons, selected items)
TEXT    = "#dde4f0"   # main text colour
MUTED   = "#7a8aaa"   # less prominent text
GOLD    = "#c9a84c"   # emphasis colour (e.g. blockquote border, bold)
GREEN   = "#4ea87a"   # success / highlight

# Global stylesheet applied to the whole application.
# Qt uses a CSS‑like syntax; this string sets up consistent look and feel.
STYLE = f"""
QMainWindow, QWidget       {{ background:{BG}; color:{TEXT};
                               font:14px "Noto Sans","Segoe UI",sans-serif; }}
QTabWidget::pane           {{ border:1px solid {BORDER}; background:{PANEL}; }}
QTabBar::tab               {{ background:{BG}; color:{MUTED};
                               padding:9px 24px; font-size:13px; }}
QTabBar::tab:selected      {{ background:{PANEL}; color:{ACCENT};
                               border-bottom:2px solid {ACCENT}; }}
QPushButton                {{ background:{ACCENT}; color:#fff; border:none;
                               padding:7px 18px; border-radius:5px; font-size:13px; }}
QPushButton:hover          {{ background:#7aaee0; }}
QPushButton:pressed        {{ background:#4070b0; }}
QPushButton:disabled       {{ background:#2a3550; color:#445566; }}
QListWidget                {{ background:{BG}; border:1px solid {BORDER};
                               border-radius:5px; padding:4px; }}
QListWidget::item          {{ padding:5px 10px; border-radius:3px; }}
QListWidget::item:selected {{ background:{ACCENT}; color:#fff; }}
QListWidget::item:hover    {{ background:#252d45; }}
QTextEdit                  {{ background:{BG}; border:1px solid {BORDER};
                               border-radius:5px; padding:8px; }}
QSplitter::handle          {{ background:{BORDER}; }}
QScrollBar:vertical        {{ background:{BG}; width:8px; border-radius:4px; }}
QScrollBar::handle:vertical{{ background:{BORDER}; border-radius:4px; }}
QStatusBar                 {{ color:{MUTED}; font-size:12px; }}
"""

# Additional CSS injected into the diary Markdown preview to match the theme.
DIARY_CSS = (
    f"body{{font:15px/1.8 'Noto Sans',sans-serif;color:{TEXT};"
    f"background:{BG};max-width:740px;margin:32px auto;padding:0 16px;}}"
    f"h1,h2,h3{{color:{ACCENT};}}"
    f"blockquote{{border-left:3px solid {GOLD};padding-left:14px;"
    f"color:{MUTED};margin-left:0;}}"
    f"code{{background:{PANEL};padding:2px 6px;border-radius:3px;}}"
    f"hr{{border:none;border-top:1px solid {BORDER};}}"
    f"strong{{color:{GOLD};}}"
)

# ══════════════════════════════════════════════════════════════════════════════
# DATA HELPERS
# ══════════════════════════════════════════════════════════════════════════════

# ── Verse data — delegated entirely to bible_verse.py ────────────────────────
#
# The GUI leverages the battle‑tested logic in bible_verse.py.
# We import only those functions and constants that the tabs need:
#
#   _load_daily_plan(path)   → list[str]    references from daily_plan.txt
#   _from_daily_plan()       → dict|None    {ref, text, source} offline/online
#   _from_youversion()       → dict|None    YouVersion API
#   _from_ourmanna_api()     → dict|None    OurManna.com fallback
#   BOOK_MAP                 → dict         full name → freebible abbreviation
#   DAILY_PLAN_FILE          → Path         ~/dotfiles/myArch/config/bible/daily_plan.txt
#   FALLBACK_VERSE           → dict         hardcoded John 3:16
#
# The daily-plan path was moved to a bible/ subdirectory in a recent update.
# (Note the extra bible/ subdirectory.)

try:
    from bible_verse import (
        _load_daily_plan,
        _from_daily_plan,
        _from_youversion,
        _from_ourmanna_api,
        BOOK_MAP,
        DAILY_PLAN_FILE,
        FALLBACK_VERSE,
    )
    _BV_OK = True
except ImportError as _e:
    # If bible_verse.py is missing, we provide minimal stubs so the GUI
    # can still launch (Tab 1 will show the fallback verse only).
    _BV_OK       = False
    _BV_ERR      = str(_e)
    # Ensure DAILY_PLAN_FILE still points to the expected location
    DAILY_PLAN_FILE = Path.home() / "dotfiles/myArch/config/bible/daily_plan.txt"
    BOOK_MAP     = {}
    FALLBACK_VERSE = {
        "ref":    "John 3:16",
        "text":   "For God so loved the world, that he gave his only begotten Son, "
                  "that whosoever believeth in him should not perish, but have everlasting life.",
        "source": "KJV (hardcoded fallback)",
    }

    # Basic file reader so the plan list can still be loaded if the file exists.
    def _load_daily_plan(path):
        """
        Minimal fallback: read a plan file, skip comments and empty lines.
        Returns a list of reference strings.
        """
        if not path.exists():
            return []
        return [l.strip() for l in path.read_text("utf-8").splitlines()
                if l.strip() and not l.startswith("#")]

    # All online/offline verse fetchers return None – Tab 1 will fall back
    # to FALLBACK_VERSE.
    def _from_daily_plan():   return None
    def _from_youversion():   return None
    def _from_ourmanna_api(): return None

def _today_idx(plan: list) -> int:
    """
    Compute the 0‑based index into the daily plan for today's date.

    Uses day‑of‑year modulo the length of the plan so that the list cycles
    seamlessly (a 90‑verse plan repeats ~4 times a year).

    Args:
        plan: List of reference strings (may be empty).

    Returns:
        An integer index between 0 and len(plan)-1, or 0 if the plan is empty.
    """
    if not plan:
        return 0
    # January 1st → day‑of‑year = 1, so we subtract 1 for 0‑based indexing.
    return (datetime.date.today().timetuple().tm_yday - 1) % len(plan)

def _best_verse() -> dict:
    """
    Return a verse dict using the same cascade as `bible_verse.verse_of_the_day()`.

    The cascade order is designed so that the most reliable offline source
    is tried first, then online curated sources, and finally a hardcoded
    constant that never fails.

        1. `_from_daily_plan()`    — Offline KJV (diatheke / freebible / bible-api.com)
        2. `_from_youversion()`    — YouVersion API (curated daily verse)
        3. `_from_ourmanna_api()`  — OurManna.com (devotional daily verse)
        4. `FALLBACK_VERSE`        — John 3:16 (KJV hardcoded)

    Returns:
        A dictionary with keys 'ref', 'text', 'source'.
        Guaranteed to return a non‑None value because FALLBACK_VERSE is the
        last resort.
    """
    # Each function returns a dict or None; `or` short‑circuits on the first truthy.
    return (
        _from_daily_plan()
        or _from_youversion()
        or _from_ourmanna_api()
        or FALLBACK_VERSE
    )


def _verse_for_idx(plan: list, idx: int) -> dict:
    """
    Look up the verse at `plan[idx]` using `freebible`'s web API, with fallbacks.

    The primary lookup uses the `freebible` library's modern `bibles.web.quote()`
    function, which expects a book abbreviation (e.g. "Gen"), chapter, verse.
    If `freebible` is not installed or the lookup fails, we fall back to the
    public `bible-api.com` web service.  If that also fails, a human‑readable
    error message is returned so the GUI doesn't crash.

    Args:
        plan: A list of Bible reference strings (e.g. "John 3:16" or "Psalm 23:1‑3").
        idx:  Zero‑based index into the plan.

    Returns:
        A verse dictionary with keys:
            - 'ref':    the original reference string
            - 'text':   the verse text (or an error message)
            - 'source': a short attribution string indicating which backend succeeded

        If the index is out of range or the plan is empty, `FALLBACK_VERSE` is
        returned immediately.
    """
    # Guard against empty plan or bad index
    if not plan or not (0 <= idx < len(plan)):
        return FALLBACK_VERSE

    ref = plan[idx]

    # ── Try freebible's modern API (bibles.web.quote) ─────────────────────
    import re as _re
    # Parse the reference; supports plain ("John 3:16") and ranges ("Psalm 23:1‑3").
    m = _re.match(
        r"^(\d?\s?[A-Za-z]+(?:\s[A-Za-z]+)*)\s+(\d+):(\d+)(?:-(\d+))?$",
        ref.strip(),
    )
    if m:
        # BOOK_MAP translates full names e.g. "Genesis" → "Gen" for Sword/freebible.
        # Books not in the map are used verbatim (might work for "John", "Acts", etc.).
        raw_book    = m.group(1).strip().title()
        abbrev      = BOOK_MAP.get(raw_book, raw_book)
        chapter     = int(m.group(2))
        start_verse = int(m.group(3))
        end_verse   = int(m.group(4) or start_verse)   # if no range, start == end

        try:
            from freebible import bibles
            texts = []
            # Iterate over each verse in the (possibly multi‑verse) range.
            for vs in range(start_verse, end_verse + 1):
                obj = bibles.web.quote(abbrev, chapter, vs)
                t   = str(obj).strip()
                if t:
                    texts.append(t)
            if texts:
                return {
                    "ref":    ref,
                    "text":   " ".join(texts),
                    "source": f"KJV (freebible)  ·  Day {idx + 1}/{len(plan)}",
                }
        except Exception:
            # Any error from freebible (missing module, network, bad key) → fall through.
            pass

    # ── Fallback: bible-api.com (needs network) ───────────────────────────
    try:
        import requests
        r = requests.get(
            f"https://bible-api.com/{ref.replace(' ', '%20')}",
            timeout=8,
        )
        if r.status_code == 200:
            text = r.json().get("text", "").strip()
            if text:
                return {
                    "ref":    ref,
                    "text":   text,
                    "source": f"bible-api.com  ·  Day {idx + 1}/{len(plan)}",
                }
    except Exception:
        pass

    # ── Both lookups failed ───────────────────────────────────────────────
    # Return an informative error so the UI can show the reference and the problem.
    return {
        "ref":    ref,
        "text":   "(Verse lookup failed — install freebible or check network)",
        "source": f"Day {idx + 1}/{len(plan)}",
    }

This widget (VerseTab) represents the first tab of the Bible GUI application. In Qt, almost everything visible on screen is a QWidget (buttons, labels, lists, text areas, tabs, windows, etc). Since a tab is itself a visible region of the interface, VerseTab inherits from QWidget. This means VerseTab is a widget, and can be inserted into a QTabWidget like any other Qt widget.

class VerseTab(QWidget):
    """
    Left panel  — list of every reference in daily_plan.txt (today highlighted).
    Right panel — reference, verse text, source, Prev/Today/Next navigation.

    Startup sequence:
        1. Load the plan from DAILY_PLAN_FILE (~/dotfiles/myArch/config/bible/).
        2. Show today's verse immediately via _best_verse() — offline-first.
        3. After construction, the UI is ready; no delayed timer is needed
           because the offline lookup path is fast enough.

    Navigation (Prev/Today/Next):
        Uses _verse_for_idx() which calls bibles.web.quote() from freebible,
        matching the API that bible_verse.py now uses internally.

    If bible_verse.py cannot be imported, a yellow warning is shown in
    the list panel and the minimal fallback functions defined above are used.

    Visually, this tab is divided into two panels:

        ┌──────────────────┬──────────────────────────────────┐
        │ LEFT PANEL       │ RIGHT PANEL                      │
        │                  │                                  │
        │ Reading plan     │ Selected verse                   │
        │ ┌────────────┐   │                                  │
        │ │ John 3:16  │   │ Reference                        │
        │ │ Ps 23:1    │   │ Source                           │
        │ │ ...        │   │ -----------------------------    │
        │ └────────────┘   │ Verse text                       │
        │                  │                                  │
        │ warning labels   │ Prev Today Next                  │
        └──────────────────┴──────────────────────────────────┘


    Data flow:
    ----------
    The reading plan comes from:

        DAILY_PLAN_FILE

    Example:

        John 3:16
        Psalm 23:1
        Romans 8:28

    Each line becomes one list item on the left. The actual verse text comes from bible_verse.py.

    Startup sequence:
    -----------------

    When VerseTab() is created:

        1. Load the reading plan.
        2. Compute today's index.
        3. Build the interface.
        4. Show today's verse immediately.

    Event flow:
    -----------

    User clicks a verse:
        QListWidget emits itemClicked signal
            ↓
        _navigate()
            ↓
        _display()

    User clicks Prev/Next:
            ↓
        _navigate()
            ↓
    """

    def __init__(self):
        """
        Build the entire tab.

        In Qt, __init__ usually does two things:

            1. Create widgets
            2. Connect signals

        After __init__ finishes, the widget is ready to display.
        """
        # Initialize QWidget internals.
        #
        # Without this, the widget would not exist properly in Qt.
        super().__init__()
        # ------------------------------------------------------------------
        # Load all references from daily_plan.txt, e.g., [ "John 3:16", "Psalm 23:1", "Romans 8:28" ]
        # ------------------------------------------------------------------
        self.plan = _load_daily_plan(DAILY_PLAN_FILE)
        # Compute today's index, e.g, day_of_year = 120, len(plan) = 90, 120 % 90 = 30
        # So today's verse is self.plan[30].
        self.idx  = _today_idx(self.plan)

        # ── Root layout (horizontal) ─────────────────────────────────────
        # Layouts are invisible containers that organize widgets.
        # QHBoxLayout means: widget1 | widget2
        root = QHBoxLayout(self)

        # Add outer spacing.
        root.setContentsMargins(14, 14, 14, 14)

        # Space between left and right panels.
        root.setSpacing(14)

        # ══════════════════════════════════════════════════════════════════
        # LEFT PANEL — List of all references
        # ══════════════════════════════════════════════════════════════════
        # Vertical layout:
        #     widget
        #     widget
        #     widget
        left = QVBoxLayout()
         # Small heading above the list
        cap = QLabel("Reading Plan")
        # Qt uses CSS-like styling.
        cap.setStyleSheet(f"color:{ACCENT};font-weight:bold;font-size:12px;")

        left.addWidget(cap)

        # LIST OF REFERENCES
        #
        # QListWidget is basically a GUI list (it displays each reference):
        #
        #     John 3:16
        #     Psalm 23:1
        #     ...
        #
        self.lst = QListWidget()
        # Fixed width prevents resizing (it keeps the left panel narrow).
        self.lst.setFixedWidth(210)

        # Populate list.
        for i, ref in enumerate(self.plan):
            # Each row needs its own item object.
            item = QListWidgetItem(ref)

            # Highlight today's verse.
            if i == self.idx:
                # Today's verse gets a distinctive appearance. It is highlighted with a gold colour and bold font.
                item.setForeground(QColor(GOLD))
                f = item.font(); f.setBold(True); item.setFont(f)

            self.lst.addItem(item)
        # Pre-select today's verse.
        if self.plan:
            self.lst.setCurrentRow(self.idx) # scroll to today

        # SIGNAL/SLOT CONNECTION
        # (1) Button clicked
        # (2) Qt emits signal "itemClicked".
        # (3) When Qt detects a click event, call this function _navigate()
        #
        # Clicking an item navigates to that verse
        self.lst.itemClicked.connect(
            lambda _: self._navigate(self.lst.currentRow())
        )
        left.addWidget(self.lst)

        # If bible_verse.py is missing, show a warning below the list.
        if not _BV_OK:
            w = QLabel(
                "⚠  bible_verse.py not found\n"
                + _BV_ERR[:80] + "\n\n"
                "Place bible_verse.py in the same\n"
                "directory as bible_gui.py."
            )
            w.setStyleSheet(
                f"color:{GOLD};font-size:11px;padding:6px;"
                f"background:#2a2010;border-radius:4px;"
            )
            w.setWordWrap(True)
            left.addWidget(w)

        # Add left panel to root.
        root.addLayout(left)

        # ══════════════════════════════════════════════════════════════════
        # RIGHT PANEL — Verse display and navigation buttons
        # ══════════════════════════════════════════════════════════════════
        right = QVBoxLayout()
        right.setSpacing(8)

        # Reference label.
        # Example: John 3:16
        self.ref_lbl = QLabel()
        self.ref_lbl.setStyleSheet(
            f"color:{GOLD};font-size:20px;font-weight:bold;"
        )
        self.ref_lbl.setWordWrap(True)
        right.addWidget(self.ref_lbl)

        # Source attribution (e.g. "KJV (freebible) · Day 7/90")
        self.src_lbl = QLabel()
        self.src_lbl.setStyleSheet(f"color:{MUTED};font-size:12px;")
        right.addWidget(self.src_lbl)

        # Horizontal rule for visual separation
        hr = QFrame(); hr.setFrameShape(QFrame.HLine)
        hr.setStyleSheet(f"background:{BORDER};max-height:1px;")
        right.addWidget(hr)

        # Scrollable area for the verse text
        # Needed because some verses may be long.
        scroll = QScrollArea()
        scroll.setWidgetResizable(True) # make the child widget resize with scroll area
        scroll.setFrameShape(QFrame.NoFrame) # no border around it
        self.text_lbl = QLabel()
        self.text_lbl.setStyleSheet("font-size:15px;line-height:1.9;")
        self.text_lbl.setWordWrap(True)
        self.text_lbl.setAlignment(Qt.AlignTop) # Align text to top
        self.text_lbl.setContentsMargins(2, 10, 2, 10) # a little breathing room
        scroll.setWidget(self.text_lbl)
        right.addWidget(scroll, 1) # stretch factor 1 so it takes available space

        # Navigation buttons
        nav = QHBoxLayout()
        self.btn_prev  = QPushButton("◀  Prev")
        self.btn_today = QPushButton("Today")
        self.btn_next  = QPushButton("Next  ▶")

        # Connect each button to _navigate with appropriate index bounds
        self.btn_prev.clicked.connect(
            lambda: self._navigate(max(0, self.idx - 1))
        )
        self.btn_today.clicked.connect(
            lambda: self._navigate(_today_idx(self.plan))
        )
        self.btn_next.clicked.connect(
            lambda: self._navigate(min(len(self.plan) - 1, self.idx + 1))
        )

        for b in (self.btn_prev, self.btn_today, self.btn_next):
            nav.addWidget(b)
        right.addLayout(nav)
        root.addLayout(right, 1) # Give the right panel the remaining space

        # ── Load today's verse on startup ───────────────────────────────
        # `_best_verse()` implements the same cascade as bible_verse.verse_of_the_day():
        #   1. _from_daily_plan()   (offline KJV, diatheke/freebible/bible-api)
        #   2. _from_youversion()   (YouVersion API)
        #   3. _from_ourmanna_api() (OurManna.com)
        #   4. FALLBACK_VERSE       (hardcoded John 3:16)
        self._show_today()

    def _show_today(self) -> None:
        """
        Load today's verse via `_best_verse()` and display it.
        This is called once during startup.
        """
        # _best_verse returns a verse dict using the same cascade as bible_verse.verse_of_the_day()
        # {
        #        "ref": ...,
        #        "text": ...,
        #        "source": ...
        # }
        v = _best_verse()
        # We display the full text as-is so the user can compare sources
        self._display(v)
        # Already on today's verse, so no need to click Today.
        self.btn_today.setEnabled(False)

    def _navigate(self, idx: int) -> None:
        """
        Move to another verse.

        Parameters
        ----------
        idx:
            Position inside self.plan, e.g., idx = 5 meansing self.plan[5]

        Show the verse at plan[idx] using _verse_for_idx().
        """
        # 1. Update internal "state", that is, its index
        self.idx = idx
        # 2. Retrieve verse text (freebible lookup with fallback).
        v = _verse_for_idx(self.plan, idx)
        # 3. Update UI (refreshes the display).
        self._display(v)
        # 4. Highlights the corresponding list item/row.
        self.lst.setCurrentRow(idx)
        # 5. Enables/disables navigation buttons depending on whether we are at the beginning or end of the plan.
        # Prevent going out of bounds.
        self.btn_prev.setEnabled(idx > 0)
        self.btn_next.setEnabled(idx < len(self.plan) - 1)
        self.btn_today.setEnabled(idx != _today_idx(self.plan))

    def _display(self, v: dict) -> None:
        """
        Push verse data into visible widgets.

        Args:
            v: dict with keys 'ref', 'text', 'source'.

        Example input:
            {
                "ref": "John 3:16",
                "text": "...",
                "source": "Diatheke"
            }

        The widgets themselves already exist. We are only replacing their text.
        """
        self.ref_lbl.setText(v.get("ref", ""))
        # The text from _from_daily_plan() may have "Source:\ntext\n\n" sections.
        # We keep them as-is — newlines render as spaces in QLabel, giving a
        # clean paragraph break between Diatheke and FreeBible results.
        self.text_lbl.setText(v.get("text", ""))
        self.src_lbl.setText(v.get("source", ""))
Bitcoin donation

JustToThePoint Copyright © 2011 - 2026 Anawim. ALL RIGHTS RESERVED. Bilingual e-books, articles, and videos to help your child and your entire family succeed, develop a healthy lifestyle, and have a lot of fun. Social Issues, Join us.

This website uses cookies to improve your navigation experience.
By continuing, you are consenting to our use of cookies, in accordance with our Cookies Policy and Website Terms and Conditions of use.