Files
foreignthon-docs/docs/language-packs/template.md
2026-05-21 14:36:47 +00:00

6.1 KiB

Add Your Language

Anyone can publish a foreignthon-xx language pack to PyPI — no access to the core repo is needed, and the pack works for anyone who installs it the moment it is on PyPI.

The recommended path is to start from the official language template. See Contributing → Language Packs for the full contribution and review process. This page is a quick reference.


Start from the template

Fork the official language template on Gitea:

https://git.keshavanand.net/foreign-thon/language-template.git

The template gives you the full correct structure — __init__.py, JSON pack, the universal tests/test_pack.py, and all three CI workflows — pre-wired and ready to rename.

!!! tip "Why fork instead of starting fresh?" The template includes the shared test suite (tests/test_pack.py) that every official pack uses. Starting from it means your CI is already correct and your pack will pass the same checks as foreignthon-es and foreignthon-ta from day one.

Alternatively, if you just want a local pack for your own project without publishing, use:

fpy new myproject --lang custom

This scaffolds a custom.json locally — no PyPI account needed. See Custom Packs for details.


Package structure

After renaming the template for your language (e.g. French, code fr):

foreignthon-fr/
├── .gitea/workflows/
│   ├── ci.yml              # pytest on push / PR
│   ├── publish.yml         # PyPI upload on v* tag
│   └── trigger-docs.yml    # docs rebuild on README.md change
├── .gitignore
├── LICENSE                 # GPL v3
├── README.md
├── pyproject.toml
└── src/
    └── foreignthon_fr/
        ├── __init__.py
        └── fr.json

pyproject.toml

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "foreignthon-fr"
version = "0.1.0"
description = "French language pack for ForeignThon."
license = { text = "GPL v3" }
requires-python = ">=3.9"
authors = [
  { name = "Your Name", email = "you@example.com" }
]
keywords = ["foreignthon", "french", "français"]
dependencies = ["foreignthon>=0.5.3"]

[project.urls]
Homepage = "https://git.keshavanand.net/foreign-thon/foreignthon-fr"

[project.entry-points."foreignthon.langs"]
fr = "foreignthon_fr"

[tool.hatch.build.targets.wheel]
packages = ["src/foreignthon_fr"]

The [project.entry-points."foreignthon.langs"] block is what makes ForeignThon auto-discover your pack — the key is the language code, the value is the Python module name.


__init__.py

from importlib.resources import files
from importlib.metadata import version, metadata, PackageNotFoundError

try:
    package_name = (__package__ or "").replace("_", "-")
    __version__ = version(package_name)

    pkg_metadata = metadata(package_name)
    raw_authors = pkg_metadata.get_all("Author") or []
    raw_emails  = pkg_metadata.get_all("Author-email") or []

    combined = []
    for item in (raw_authors + raw_emails):
        clean_name = item.split("<")[0].strip()
        if clean_name and clean_name not in combined:
            combined.append(clean_name)

    __authors__ = combined

except PackageNotFoundError:
    __version__ = "0.0.0"
    __authors__ = []


def get_pack_path():
    return files(__name__) / "fr.json"

Change "fr.json" to match your actual filename.


The JSON pack

Keys are your language's words. Values are English Python identifiers — never change the values.

{
  "meta": {
    "name": "French",
    "native_name": "Français",
    "code": "fr"
  },
  "keywords": {
    "si": "if",
    "sinon": "else",
    "sinonsi": "elif",
    "pour": "for",
    "tantque": "while",
    "déf": "def",
    "classe": "class",
    "importer": "import",
    "depuis": "from",
    "comme": "as",
    "retourner": "return",
    "arrêter": "break",
    "continuer": "continue",
    "passer": "pass",
    "essayer": "try",
    "sauf": "except",
    "finalement": "finally",
    "lever": "raise",
    "avec": "with",
    "dans": "in",
    "est": "is",
    "et": "and",
    "ou": "or",
    "non": "not",
    "supprimer": "del",
    "global": "global",
    "nonlocal": "nonlocal",
    "affirmer": "assert",
    "générer": "yield",
    "attendre": "await",
    "asynchrone": "async",
    "lambda": "lambda",
    "Vrai": "True",
    "Faux": "False",
    "Rien": "None"
  },
  "builtins": {
    "afficher": "print",
    "saisir": "input",
    "longueur": "len",
    "intervalle": "range",
    ...
  },
  "exceptions": {
    "ErreurDeValeur": "ValueError",
    ...
  },
  "error_messages": {
    "ValueError": "Erreur de valeur",
    ...
  },
  "stdlib": {
    "mathématiques": "math",
    ...
  },
  "postfix_keywords": []
}

postfix_keywords

Set this to a list of English keywords if your language is SOV (subject-object-verb) and benefits from @@ postfix style on decompile. Example (Tamil):

"postfix_keywords": ["if", "elif", "while", "class", "with"]

Set it to [] for SVO languages like Spanish or French.

Filename rule

The JSON filename must match meta.code exactly. fr.json for code "fr".


Validate and test

pip install -e .
pip install pytest

fpy pack src/foreignthon_fr/fr.json
# ✓ Pack 'French' is valid.

pytest tests/ -v

The shared tests/test_pack.py checks that all sections exist, all values are real Python identifiers, the filename matches meta.code, and postfix_keywords entries each have a translation.


Publish

Tag and push — the CI handles the rest:

git tag v0.1.0
git push origin v0.1.0

The publish.yml workflow builds the wheel and uploads it to PyPI using the PYPI_TOKEN secret. Add your own PYPI_TOKEN to your fork's secrets if you are not yet under the official org.


Getting listed

Once your pack is working and on PyPI, open a PR or issue on foreignthon-docs to get added to the Language Packs overview. See Contributing → Language Packs for the full process.