Compare commits
19 Commits
dafb91970b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 358d72b968 | |||
| d3b130b666 | |||
| 17f9d85b99 | |||
| e761f1dc68 | |||
| 7555636306 | |||
| 943791cb07 | |||
| e763c9fbc6 | |||
| 1af1801065 | |||
| d9c907a25a | |||
| 16fb3478bb | |||
| 9468719411 | |||
| c2def7f305 | |||
| 6ec5f9afbb | |||
| bd53d1c0f5 | |||
| ff9f0e1e14 | |||
| 4436ddb8f3 | |||
| 4227d48f30 | |||
| bd5ec35ce2 | |||
| 1245333858 |
@@ -3,6 +3,7 @@ name: Deploy Docs
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: ["main"]
|
branches: ["main"]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
@@ -19,6 +20,76 @@ jobs:
|
|||||||
- name: Install MkDocs
|
- name: Install MkDocs
|
||||||
run: pip install -r requirements.txt
|
run: pip install -r requirements.txt
|
||||||
|
|
||||||
|
- name: Fetch language READMEs and update nav
|
||||||
|
env:
|
||||||
|
GITEA_URL: https://git.keshavanand.net
|
||||||
|
ORG: foreign-thon
|
||||||
|
run: |
|
||||||
|
python3 << 'PYEOF'
|
||||||
|
import urllib.request
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
gitea_url = os.environ["GITEA_URL"]
|
||||||
|
org = os.environ["ORG"]
|
||||||
|
|
||||||
|
# Read languages.yml manually (no yaml dep needed)
|
||||||
|
with open("languages.yml") as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
|
||||||
|
languages = []
|
||||||
|
current = {}
|
||||||
|
for line in lines:
|
||||||
|
line = line.strip()
|
||||||
|
if line.startswith("- code:"):
|
||||||
|
if current:
|
||||||
|
languages.append(current)
|
||||||
|
current = {"code": line.split(":", 1)[1].strip()}
|
||||||
|
elif line.startswith("name:") and current:
|
||||||
|
current["name"] = line.split(":", 1)[1].strip()
|
||||||
|
elif line.startswith("repo:") and current:
|
||||||
|
current["repo"] = line.split(":", 1)[1].strip()
|
||||||
|
if current:
|
||||||
|
languages.append(current)
|
||||||
|
|
||||||
|
nav_lines = []
|
||||||
|
|
||||||
|
for lang in languages:
|
||||||
|
code = lang["code"]
|
||||||
|
name = lang["name"]
|
||||||
|
repo = lang["repo"]
|
||||||
|
|
||||||
|
readme_url = f"{gitea_url}/{org}/{repo}/raw/branch/main/README.md"
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(readme_url) as r:
|
||||||
|
content = r.read().decode("utf-8")
|
||||||
|
out_path = f"docs/language-packs/{code}.md"
|
||||||
|
with open(out_path, "w", encoding="utf-8") as f:
|
||||||
|
f.write(content)
|
||||||
|
nav_lines.append(f" - '{name} ({code})': language-packs/{code}.md")
|
||||||
|
print(f"✓ {repo} → {out_path}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"✗ {repo}: {e}")
|
||||||
|
|
||||||
|
# Inject into mkdocs.yml between markers
|
||||||
|
with open("mkdocs.yml") as f:
|
||||||
|
yml = f.read()
|
||||||
|
|
||||||
|
nav_block = "\n".join(nav_lines)
|
||||||
|
new_yml = re.sub(
|
||||||
|
r" # LANGS_NAV_START.*?# LANGS_NAV_END",
|
||||||
|
f" # LANGS_NAV_START\n{nav_block}\n # LANGS_NAV_END",
|
||||||
|
yml,
|
||||||
|
flags=re.DOTALL
|
||||||
|
)
|
||||||
|
|
||||||
|
with open("mkdocs.yml", "w") as f:
|
||||||
|
f.write(new_yml)
|
||||||
|
|
||||||
|
print(f"Nav updated with {len(nav_lines)} languages.")
|
||||||
|
PYEOF
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: mkdocs build --strict
|
run: mkdocs build --strict
|
||||||
|
|
||||||
@@ -32,12 +103,7 @@ jobs:
|
|||||||
echo "$SSH_DEPLOY_KEY" > ~/.ssh/deploy_key
|
echo "$SSH_DEPLOY_KEY" > ~/.ssh/deploy_key
|
||||||
chmod 600 ~/.ssh/deploy_key
|
chmod 600 ~/.ssh/deploy_key
|
||||||
ssh-keyscan -H $SERVER_HOST >> ~/.ssh/known_hosts
|
ssh-keyscan -H $SERVER_HOST >> ~/.ssh/known_hosts
|
||||||
|
|
||||||
# Empties the folder safely without triggering Zsh glob errors
|
|
||||||
# and without needing root permissions on /var/www
|
|
||||||
ssh -i ~/.ssh/deploy_key $SERVER_USER@$SERVER_HOST \
|
ssh -i ~/.ssh/deploy_key $SERVER_USER@$SERVER_HOST \
|
||||||
"find /var/www/foreignthon-docs -mindepth 1 -delete"
|
"rm -rf /var/www/foreignthon-docs/* && mkdir -p /var/www/foreignthon-docs"
|
||||||
|
|
||||||
# Copy built site
|
|
||||||
scp -i ~/.ssh/deploy_key -r site/* \
|
scp -i ~/.ssh/deploy_key -r site/* \
|
||||||
$SERVER_USER@$SERVER_HOST:/var/www/foreignthon-docs/
|
$SERVER_USER@$SERVER_HOST:/var/www/foreignthon-docs/
|
||||||
|
|||||||
@@ -1 +1,117 @@
|
|||||||
# Coming soon
|
# CLI Reference
|
||||||
|
|
||||||
|
All commands support `-h` / `--help`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `fpy new`
|
||||||
|
|
||||||
|
Scaffold a new ForeignThon project.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fpy new <name> --lang <code> # create in new directory
|
||||||
|
fpy new --lang <code> # initialize current directory (must be empty)
|
||||||
|
fpy new <name> --lang <code> --no-git
|
||||||
|
fpy new <name> --lang custom # scaffold a blank language pack
|
||||||
|
```
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|---|---|
|
||||||
|
| `--lang`, `-l` | Language code — required |
|
||||||
|
| `--no-git` | Skip `git init` and initial commit |
|
||||||
|
|
||||||
|
!!! note "Using `--lang custom`"
|
||||||
|
Prompts for a language code, English name, and native name. Creates a `custom.json` with all Python keywords pre-filled as stubs and wires it up in `.foreignthon.toml` automatically.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `fpy run`
|
||||||
|
|
||||||
|
Transpile and run a source file.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fpy run script.es.py
|
||||||
|
fpy run script.py --lang es # override language detection
|
||||||
|
fpy run script.es.py --keep # also write the compiled .py to disk
|
||||||
|
```
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|---|---|
|
||||||
|
| `--lang`, `-l` | Override language detection |
|
||||||
|
| `--keep` | Write `.compiled.py` alongside source after running |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `fpy compile`
|
||||||
|
|
||||||
|
Transpile to standard Python without running.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fpy compile script.es.py # → script.compiled.py (same directory)
|
||||||
|
fpy compile script.es.py -o dist/ # → dist/script.compiled.py
|
||||||
|
fpy compile script.es.py -o output.py # → output.py
|
||||||
|
```
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|---|---|
|
||||||
|
| `--output`, `-o` | Output file or directory |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `fpy decompile`
|
||||||
|
|
||||||
|
Convert standard Python back to a foreign language. Keywords and builtins are translated — variable names are untouched.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fpy decompile script.py --lang es
|
||||||
|
fpy decompile script.py --lang es --postfix
|
||||||
|
fpy decompile script.py --lang es -o out/
|
||||||
|
```
|
||||||
|
|
||||||
|
| Flag | Description |
|
||||||
|
|---|---|
|
||||||
|
| `--lang`, `-l` | Target language — required |
|
||||||
|
| `--postfix` | Rewrite conditionals to `@@` postfix style |
|
||||||
|
| `--output`, `-o` | Output file or directory |
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
Decompile is lossy — variable names and comments are not translated back. It is useful for bootstrapping a foreign-language version of an existing Python file, not as a perfect round-trip.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `fpy check`
|
||||||
|
|
||||||
|
Validate syntax without running.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fpy check script.es.py
|
||||||
|
# ✓ script.es.py looks good.
|
||||||
|
# or
|
||||||
|
# ✗ Syntax error: invalid syntax (script.es.py, line 4)
|
||||||
|
```
|
||||||
|
|
||||||
|
Exits with code `1` on failure — useful in CI pipelines.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `fpy pack`
|
||||||
|
|
||||||
|
Validate a language pack JSON file against the required schema.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fpy pack mylang.json
|
||||||
|
# ✓ Pack 'Russian' is valid.
|
||||||
|
# or
|
||||||
|
# ✗ Missing sections: {'error_messages'}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Language detection order
|
||||||
|
|
||||||
|
When running a file, ForeignThon resolves the language in this order:
|
||||||
|
|
||||||
|
1. `--lang` flag (highest priority)
|
||||||
|
2. Shebang comment: `# foreignthon: es`
|
||||||
|
3. File extension: `.es.py` → `es`
|
||||||
|
4. `.foreignthon.toml` in the project root
|
||||||
|
|||||||
@@ -1 +1,122 @@
|
|||||||
# Coming soon
|
# Contributing to Core
|
||||||
|
|
||||||
|
`foreignthon-core` is the transpiler engine, CLI, and pack loader. Contributions that improve correctness, performance, or usability are welcome.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Repository
|
||||||
|
|
||||||
|
[https://git.keshavanand.net/foreign-thon/foreignthon-core](https://git.keshavanand.net/foreign-thon/foreignthon-core)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dev setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://git.keshavanand.net/foreign-thon/foreignthon-core
|
||||||
|
cd foreignthon-core
|
||||||
|
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate # Windows: .venv\Scripts\activate
|
||||||
|
|
||||||
|
pip install -e ".[dev]" # installs foreignthon + pytest + ruff
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify the install:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fpy --version
|
||||||
|
pytest
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Running tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pytest # all tests
|
||||||
|
pytest -v # verbose
|
||||||
|
pytest -k postfix # filter by name
|
||||||
|
```
|
||||||
|
|
||||||
|
Tests live in `tests/test_engine.py`. They use a local `test_pack.json` fixture (a copy of the Spanish pack) so no installed language pack is needed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Code style
|
||||||
|
|
||||||
|
ForeignThon uses [Ruff](https://docs.astral.sh/ruff/) for linting and formatting:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ruff check src/ # lint
|
||||||
|
ruff format src/ # format
|
||||||
|
```
|
||||||
|
|
||||||
|
The CI gate runs both. A PR that fails either will not be merged.
|
||||||
|
|
||||||
|
Line length is 88. Import order follows the `I` rule set (isort-compatible).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Project layout
|
||||||
|
|
||||||
|
```
|
||||||
|
foreignthon-core/
|
||||||
|
├── src/
|
||||||
|
│ └── foreignthon/
|
||||||
|
│ ├── __init__.py # version
|
||||||
|
│ ├── cli.py # click commands (fpy)
|
||||||
|
│ ├── errors.py # bilingual error hook
|
||||||
|
│ ├── pack.py # pack discovery and validation
|
||||||
|
│ ├── template.json # canonical list of all pack keys
|
||||||
|
│ └── transpiler.py # tokenizer-based engine
|
||||||
|
└── tests/
|
||||||
|
├── test_engine.py
|
||||||
|
└── test_pack.json # Spanish fixture (no install needed)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Adding new keywords or builtins
|
||||||
|
|
||||||
|
`template.json` is the **single source of truth** for the full set of keywords, builtins, exceptions, and stdlib names that all language packs must cover.
|
||||||
|
|
||||||
|
To add a new entry (e.g. a new builtin):
|
||||||
|
|
||||||
|
1. Add it to `template.json` with the English value as the default.
|
||||||
|
2. Add a matching entry to `test_pack.json` (Spanish values).
|
||||||
|
3. Update `_build_mapping()` in `transpiler.py` if a new section is needed.
|
||||||
|
4. Bump `foreignthon-core` version.
|
||||||
|
5. Announce to language pack maintainers so they can update their packs.
|
||||||
|
|
||||||
|
Do not add entries to `template.json` that are specific to one language pack.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Adding a new CLI command
|
||||||
|
|
||||||
|
All commands are defined in `cli.py` using [Click](https://click.palletsprojects.com/).
|
||||||
|
|
||||||
|
- Add your command as a function decorated with `@main.command()`.
|
||||||
|
- Include a docstring — Click uses it as the `--help` text.
|
||||||
|
- Add `CONTEXT_SETTINGS` to every command for consistent `-h` support.
|
||||||
|
- Write tests in `tests/test_engine.py` or a new test file.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Submitting a PR
|
||||||
|
|
||||||
|
1. Fork the repository on Gitea.
|
||||||
|
2. Create a branch: `git checkout -b feature/my-change`.
|
||||||
|
3. Make your change, add or update tests, run `pytest` and `ruff check`.
|
||||||
|
4. Open a PR with a clear description of the problem and the fix.
|
||||||
|
|
||||||
|
For large changes (new features, engine behaviour changes), open an issue first to discuss before writing code.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What not to change
|
||||||
|
|
||||||
|
- **`template.json`** keys should only grow, never shrink — removing a key is a breaking change for all existing language packs.
|
||||||
|
- The tokenizer-based approach in `transpiler.py` is intentional. Do not replace it with a regex or AST-based approach without a very strong reason — the tokenizer correctly handles strings, comments, and f-strings without any special casing.
|
||||||
|
- The bilingual error format (`[XX] ForeignName: msg` / `[EN] EnglishName: msg`) is part of the public interface. Do not change the format without a major version bump.
|
||||||
|
|||||||
@@ -1 +1,189 @@
|
|||||||
# Coming soon
|
# Contributing Language Packs
|
||||||
|
|
||||||
|
Language packs are published independently to PyPI — no access to the `foreignthon-core` repo is needed. There are more packs than the ones listed here; the ones on the official docs site are the ones hosted under the [`foreign-thon` org on Gitea](https://git.keshavanand.net/foreign-thon/).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The right starting point
|
||||||
|
|
||||||
|
**Do not start from scratch.** Fork the official language template:
|
||||||
|
|
||||||
|
```
|
||||||
|
https://git.keshavanand.net/foreign-thon/language-template.git
|
||||||
|
```
|
||||||
|
|
||||||
|
The template gives you:
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `src/foreignthon_xx/__init__.py` | Exposes `get_pack_path()` and reads version/author metadata |
|
||||||
|
| `src/foreignthon_xx/xx.json` | The pack file — all keys are English stubs to replace |
|
||||||
|
| `tests/test_pack.py` | Universal test suite shared by every official pack |
|
||||||
|
| `.gitea/workflows/ci.yml` | Runs `pytest` on every push and PR |
|
||||||
|
| `.gitea/workflows/publish.yml` | Builds and uploads to PyPI on a `v*` tag |
|
||||||
|
| `.gitea/workflows/trigger-docs.yml` | Pings the docs site to rebuild when `README.md` changes |
|
||||||
|
| `pyproject.toml` | Pre-wired entry points, hatchling build, GPL v3 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 1 — Fork and rename
|
||||||
|
|
||||||
|
Fork the template on Gitea (or GitHub mirror). Rename your fork to `foreignthon-xx` where `xx` is the [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes) two-letter code for your language (use ISO 639-2 three-letter code if no two-letter code exists).
|
||||||
|
|
||||||
|
Clone your fork locally:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://git.keshavanand.net/yourname/foreignthon-xx
|
||||||
|
cd foreignthon-xx
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 2 — Rename the template files
|
||||||
|
|
||||||
|
The template uses placeholder names. Rename:
|
||||||
|
|
||||||
|
```
|
||||||
|
src/foreignthon_xx/xx.json → src/foreignthon_fr/fr.json (example: French)
|
||||||
|
src/foreignthon_xx/ → src/foreignthon_fr/
|
||||||
|
```
|
||||||
|
|
||||||
|
Then update every occurrence of `xx` in:
|
||||||
|
|
||||||
|
- `pyproject.toml` — `name`, `entry-points` key, `packages` path, `keywords`, `description`
|
||||||
|
- `src/foreignthon_fr/__init__.py` — the `get_pack_path()` return value
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 3 — Fill in the JSON
|
||||||
|
|
||||||
|
Open `src/foreignthon_fr/fr.json`. Every value is an English Python identifier — **never change the values**. Replace the **keys** with your language's words.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"meta": {
|
||||||
|
"name": "French",
|
||||||
|
"native_name": "Français",
|
||||||
|
"code": "fr"
|
||||||
|
},
|
||||||
|
"keywords": {
|
||||||
|
"si": "if",
|
||||||
|
"sinon": "else",
|
||||||
|
...
|
||||||
|
},
|
||||||
|
"builtins": { ... },
|
||||||
|
"exceptions": { ... },
|
||||||
|
"error_messages": { ... },
|
||||||
|
"stdlib": { ... },
|
||||||
|
"postfix_keywords": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The filename must exactly match `meta.code` (e.g. `fr.json` for code `"fr"`).
|
||||||
|
|
||||||
|
Set `postfix_keywords` to a list of English keywords (e.g. `["if", "elif", "while"]`) if your language is SOV and benefits from `@@` postfix style on decompile. Set it to `[]` for SVO languages.
|
||||||
|
|
||||||
|
For a full reference on the JSON schema see [Custom Packs → Pack schema](../custom-packs.md#pack-schema).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 4 — Validate and test
|
||||||
|
|
||||||
|
Install your pack in editable mode and run the test suite:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -e .
|
||||||
|
pip install pytest
|
||||||
|
|
||||||
|
fpy pack src/foreignthon_fr/fr.json
|
||||||
|
# ✓ Pack 'French' is valid.
|
||||||
|
|
||||||
|
pytest tests/ -v
|
||||||
|
```
|
||||||
|
|
||||||
|
The shared test suite (`tests/test_pack.py`) checks:
|
||||||
|
|
||||||
|
- All required sections exist (`meta`, `keywords`, `builtins`, `exceptions`, `error_messages`, `stdlib`, `postfix_keywords`)
|
||||||
|
- Every keyword value is a real Python keyword
|
||||||
|
- Every builtin/exception value is a real Python builtin
|
||||||
|
- The filename matches `meta.code`
|
||||||
|
- All `postfix_keywords` entries have a matching translation in `keywords`
|
||||||
|
|
||||||
|
The CI (`ci.yml`) runs this same suite on every push and pull request automatically.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step 5 — Publish to PyPI
|
||||||
|
|
||||||
|
You can publish from your own fork at any time — the pack works as soon as it is on PyPI, regardless of whether it is in the official org.
|
||||||
|
|
||||||
|
Tag a release and push:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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. If you are working from your personal fork you will need to add your own `PYPI_TOKEN` secret in your fork's Gitea/GitHub settings.
|
||||||
|
|
||||||
|
Once published, anyone can install your pack:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install foreignthon-fr
|
||||||
|
fpy new myproject --lang fr
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Getting hosted under the official org
|
||||||
|
|
||||||
|
The `foreign-thon` org on Gitea is where official packs live and where the org-level CI secrets (`PYPI_TOKEN`, `GIT_RELEASE_TOKEN`, `DOCS_TRIGGER_TOKEN`) are inherited automatically.
|
||||||
|
|
||||||
|
**Nobody has direct repo-creation access in the org.** To get your pack hosted there:
|
||||||
|
|
||||||
|
1. Make sure your pack is working and published to PyPI.
|
||||||
|
2. Open an issue or PR on [foreignthon-docs](https://git.keshavanand.net/foreign-thon/foreignthon-docs) requesting inclusion, and include:
|
||||||
|
- Your language code, English name, and native name
|
||||||
|
- A link to your published PyPI package
|
||||||
|
- Your Gitea handle
|
||||||
|
3. The maintainer will transfer or mirror your repo into the org and wire up the secrets.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Getting listed on the docs site
|
||||||
|
|
||||||
|
The docs site table in [Language Packs → Overview](../language-packs/index.md) is maintained in `languages.yml` in the `foreignthon-docs` repo. Open a PR to add your entry:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- code: fr
|
||||||
|
name: French
|
||||||
|
repo: foreignthon-fr
|
||||||
|
```
|
||||||
|
|
||||||
|
The `trigger-docs.yml` workflow in your pack repo fires a rebuild of the docs site whenever `README.md` is pushed to `main`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quality bar
|
||||||
|
|
||||||
|
Before submitting for listing, your pack should satisfy:
|
||||||
|
|
||||||
|
- [ ] `fpy pack xx.json` passes
|
||||||
|
- [ ] `pytest tests/` passes
|
||||||
|
- [ ] Every foreign key is unique within the pack
|
||||||
|
- [ ] No English words used as foreign keys unless the language genuinely keeps them (e.g. `lambda`)
|
||||||
|
- [ ] `meta.code` matches the JSON filename and the entry-point key in `pyproject.toml`
|
||||||
|
- [ ] Package name is `foreignthon-xx` (hyphen), module name is `foreignthon_xx` (underscore)
|
||||||
|
- [ ] `pip install foreignthon-xx` works from PyPI
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Versioning and updates
|
||||||
|
|
||||||
|
When `foreignthon-core` releases a new version that adds entries to `template.json`, update your pack to cover them and release a new version. Update the `foreignthon` lower-bound dependency in `pyproject.toml` if your pack requires the new core:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
dependencies = ["foreignthon>=0.5.4"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Follow [Semantic Versioning](https://semver.org/). Use `0.x.y` while the keyword set is still being refined; move to `1.0.0` once stable.
|
||||||
|
|||||||
@@ -1 +1,95 @@
|
|||||||
# Coming soon
|
## Custom Packs
|
||||||
|
|
||||||
|
ForeignThon lets you extend or override any installed language pack locally — no PyPI account, no new package required. You can also scaffold a completely new language from scratch.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Local override
|
||||||
|
|
||||||
|
Create a `custom.json` in your project root with only the keys you want to change:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"builtins": {
|
||||||
|
"show": "print"
|
||||||
|
},
|
||||||
|
"keywords": {
|
||||||
|
"when": "if"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then reference it in `.foreignthon.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[foreignthon]
|
||||||
|
lang = "es"
|
||||||
|
custom_pack = "custom.json"
|
||||||
|
```
|
||||||
|
|
||||||
|
Custom keys are merged on top of the installed pack. Installed pack keys are preserved — only the keys you define in `custom.json` are overridden.
|
||||||
|
|
||||||
|
!!! tip
|
||||||
|
ForeignThon walks up the directory tree to find `.foreignthon.toml`, so you can place it at the project root and run `fpy` from any subdirectory.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scaffold a new language
|
||||||
|
|
||||||
|
If no pack exists for your language yet:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fpy new myproject --lang custom
|
||||||
|
```
|
||||||
|
|
||||||
|
You will be prompted for:
|
||||||
|
|
||||||
|
- Language code (e.g. `ru`, `fr`, `ar`)
|
||||||
|
- English name (e.g. `Russian`)
|
||||||
|
- Native name (e.g. `Русский`)
|
||||||
|
|
||||||
|
This generates a `custom.json` based on the official template — every Python keyword, builtin, exception, and stdlib module is listed with the English value as a placeholder. Replace the **keys** with your language's words.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"keywords": {
|
||||||
|
"if": "if", ← replace the key, keep the value
|
||||||
|
"for": "for",
|
||||||
|
"def": "def",
|
||||||
|
...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `.foreignthon.toml` is automatically wired to use this file.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pack schema
|
||||||
|
|
||||||
|
A standalone pack must have these top-level sections:
|
||||||
|
|
||||||
|
| Section | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `meta` | Name, code, version, authors |
|
||||||
|
| `keywords` | Python reserved words |
|
||||||
|
| `builtins` | Built-in functions |
|
||||||
|
| `exceptions` | Built-in exception classes |
|
||||||
|
| `error_messages` | Translations for bilingual error output |
|
||||||
|
| `stdlib` | Common standard library module names |
|
||||||
|
| `postfix_keywords` | English keywords to rewrite in `--postfix` output |
|
||||||
|
|
||||||
|
Validate your pack at any time:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fpy pack custom.json
|
||||||
|
# ✓ Pack 'Russian' is valid.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Publishing
|
||||||
|
|
||||||
|
Once your `custom.json` is complete and working, you can turn it into a proper `foreignthon-xx` package on PyPI so others can install it with `pip install foreignthon-xx`.
|
||||||
|
|
||||||
|
See [Contributing → Language Packs](contributing/language-packs.md) for the full guide.
|
||||||
|
|||||||
@@ -1 +1,160 @@
|
|||||||
# Coming soon
|
# Architecture
|
||||||
|
|
||||||
|
This page describes how `foreignthon-core` works internally.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pipeline
|
||||||
|
|
||||||
|
```
|
||||||
|
source.xx.py
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
_check_shebang() ← reads "# foreignthon: xx" if present
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
load_pack(lang_code) ← discovers + validates the JSON pack
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
_apply_postfix_syntax() ← rewrites "expr @@keyword:" lines
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
_swap_tokens() ← tokenizer pass: replaces NAME tokens
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
standard Python string ← ready to compile or write to disk
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Module overview
|
||||||
|
|
||||||
|
| Module | Responsibility |
|
||||||
|
|---|---|
|
||||||
|
| `transpiler.py` | The engine — postfix rewriter and tokenizer pass |
|
||||||
|
| `pack.py` | Pack discovery, loading, and validation |
|
||||||
|
| `cli.py` | Click commands (`fpy run`, `fpy compile`, etc.) |
|
||||||
|
| `errors.py` | Bilingual exception hook |
|
||||||
|
| `template.json` | Canonical set of all keywords/builtins a pack must cover |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tokenizer-based translation
|
||||||
|
|
||||||
|
ForeignThon uses Python's standard `tokenize` module rather than regex or AST manipulation.
|
||||||
|
|
||||||
|
`tokenize.generate_tokens()` splits source code into typed tokens. ForeignThon only looks at `NAME` tokens — identifiers. It replaces any `NAME` token whose string appears as a key in the active pack mapping. All other token types (strings, comments, operators, numbers) pass through unchanged.
|
||||||
|
|
||||||
|
This gives three important guarantees:
|
||||||
|
|
||||||
|
1. **Strings are safe.** A keyword inside `"..."` or `f"..."` is a `STRING` token, never a `NAME` — it is never touched.
|
||||||
|
2. **Comments are safe.** Comment tokens are passed through verbatim.
|
||||||
|
3. **Variable names are safe.** A variable like `si_condition` contains `si` only as a substring; as a `NAME` token it is `si_condition`, which is not in the mapping.
|
||||||
|
|
||||||
|
The whitespace between tokens is preserved by tracking `(row, col)` positions and copying the gaps from the original source.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pack discovery
|
||||||
|
|
||||||
|
Language packs register themselves using Python [entry points](https://packaging.python.org/en/latest/specifications/entry-points/):
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# in foreignthon-es/pyproject.toml
|
||||||
|
[project.entry-points."foreignthon.langs"]
|
||||||
|
es = "foreignthon_es"
|
||||||
|
```
|
||||||
|
|
||||||
|
`pack.py` calls `importlib.metadata.entry_points(group="foreignthon.langs")` at runtime to discover all installed packs. Installing a pack is sufficient — no configuration file needs to be edited.
|
||||||
|
|
||||||
|
Each pack module must expose:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def get_pack_path() -> Path:
|
||||||
|
return files(__name__) / "xx.json"
|
||||||
|
```
|
||||||
|
|
||||||
|
The core calls `get_pack_path()` to locate the JSON, loads it, and validates that all required sections are present.
|
||||||
|
|
||||||
|
Results are cached with `@lru_cache` so each pack is loaded at most once per process.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pack mapping
|
||||||
|
|
||||||
|
Four sections of the JSON are merged into a single flat dict for translation:
|
||||||
|
|
||||||
|
```python
|
||||||
|
mapping = {}
|
||||||
|
mapping.update(pack["keywords"])
|
||||||
|
mapping.update(pack["builtins"])
|
||||||
|
mapping.update(pack["exceptions"])
|
||||||
|
mapping.update(pack["stdlib"])
|
||||||
|
```
|
||||||
|
|
||||||
|
The merged mapping is `{ foreign_word: english_word }`. It is passed directly to `_swap_tokens()`.
|
||||||
|
|
||||||
|
If two sections define the same foreign key, later sections win (stdlib last). In practice this does not occur because pack authors ensure uniqueness.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Postfix syntax (`@@`)
|
||||||
|
|
||||||
|
The `@@` operator is a source-level pre-processing step that runs **before** tokenization.
|
||||||
|
|
||||||
|
A line like:
|
||||||
|
|
||||||
|
```
|
||||||
|
x > 0 @@si:
|
||||||
|
escribir(x)
|
||||||
|
```
|
||||||
|
|
||||||
|
is rewritten to:
|
||||||
|
|
||||||
|
```
|
||||||
|
si x > 0:
|
||||||
|
escribir(x)
|
||||||
|
```
|
||||||
|
|
||||||
|
The rewriter uses a regex that matches `(.+?)@@(<keyword>)` and moves the keyword to the front. It only operates on lines that contain `@@`, preserving indentation and line endings.
|
||||||
|
|
||||||
|
`@@` is never valid Python and never appears in the tokenizer output.
|
||||||
|
|
||||||
|
**Decompile direction:** `fpy decompile --postfix` does the reverse — it looks for lines of the form `foreign_kw expr:` where `foreign_kw` is in the pack's `postfix_keywords` list, and rewrites them to `expr @@foreign_kw:`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bilingual error hook
|
||||||
|
|
||||||
|
`errors.py` installs a custom `sys.excepthook` before running user code:
|
||||||
|
|
||||||
|
1. On exception, it looks up the exception type name in the pack's `exceptions` section (reverse map: English → foreign).
|
||||||
|
2. It looks up a translated message in `error_messages`.
|
||||||
|
3. It prints `[XX] ForeignName: translated_msg` then `[EN] EnglishName: original_msg`.
|
||||||
|
4. It always calls `traceback.print_exception()` afterwards so the full traceback is shown.
|
||||||
|
|
||||||
|
Tracebacks point to the original `.xx.py` file. This is achieved by populating `linecache.cache` with the original source before `exec()`-ing the compiled code, so Python's traceback machinery reads the right lines.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Custom pack override
|
||||||
|
|
||||||
|
When `.foreignthon.toml` declares `custom_pack = "path/to/custom.json"`:
|
||||||
|
|
||||||
|
- If the custom JSON has `meta.code` set, it is treated as a **standalone pack** and used directly.
|
||||||
|
- If `meta.code` is absent, it is treated as an **override** — it is merged on top of the installed pack, replacing only the keys it defines.
|
||||||
|
|
||||||
|
The CLI (`cli.py`) handles this in `_load_effective_pack()` by walking up the directory tree to find `.foreignthon.toml`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File naming and language detection
|
||||||
|
|
||||||
|
Language detection order (highest priority first):
|
||||||
|
|
||||||
|
1. `--lang` CLI flag
|
||||||
|
2. Shebang comment `# foreignthon: xx` on the first line
|
||||||
|
3. Double extension `.xx.py` → `xx`
|
||||||
|
4. Fallback to `"en"` (no-op — English is Python)
|
||||||
|
|
||||||
|
`_detect_lang()` and `_check_shebang()` in `transpiler.py` implement steps 3 and 2 respectively. Step 1 is handled by the `run` command in `cli.py`.
|
||||||
|
|||||||
@@ -1 +1,136 @@
|
|||||||
# Coming soon
|
# Releasing
|
||||||
|
|
||||||
|
This page covers how to release `foreignthon-core` and how language pack maintainers should respond to a new core release.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Core release (`foreignthon`)
|
||||||
|
|
||||||
|
### 1. Update the version
|
||||||
|
|
||||||
|
Version is set in `pyproject.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[project]
|
||||||
|
name = "foreignthon"
|
||||||
|
version = "0.5.4" # bump this
|
||||||
|
```
|
||||||
|
|
||||||
|
ForeignThon follows [Semantic Versioning](https://semver.org/):
|
||||||
|
|
||||||
|
| Change | Version bump |
|
||||||
|
|---|---|
|
||||||
|
| Bug fix, no API change | Patch (`0.5.3` → `0.5.4`) |
|
||||||
|
| New feature, backward compatible | Minor (`0.5.x` → `0.6.0`) |
|
||||||
|
| Breaking change (removes a key, changes CLI contract) | Major (`0.x.y` → `1.0.0`) |
|
||||||
|
|
||||||
|
### 2. Update `template.json` if needed
|
||||||
|
|
||||||
|
If the release adds new keywords, builtins, or sections, update `template.json` to include them. This is the canonical list all language packs are expected to cover. Document what changed in your commit message and PR so pack maintainers know what to add.
|
||||||
|
|
||||||
|
### 3. Run CI locally
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pytest
|
||||||
|
ruff check src/
|
||||||
|
ruff format --check src/
|
||||||
|
```
|
||||||
|
|
||||||
|
All must pass.
|
||||||
|
|
||||||
|
### 4. Tag and push
|
||||||
|
|
||||||
|
The Gitea CI publishes to PyPI on any tag matching `v*`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git tag v0.5.4
|
||||||
|
git push origin v0.5.4
|
||||||
|
```
|
||||||
|
|
||||||
|
The `publish.yml` workflow builds the wheel and uploads it using the `PYPI_TOKEN` secret configured in Gitea.
|
||||||
|
|
||||||
|
### 5. Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install --upgrade foreignthon
|
||||||
|
fpy --version
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Language pack release (`foreignthon-xx`)
|
||||||
|
|
||||||
|
Each language pack is independently versioned and released.
|
||||||
|
|
||||||
|
### Responding to a core release
|
||||||
|
|
||||||
|
When `foreignthon-core` adds new entries to `template.json`:
|
||||||
|
|
||||||
|
1. Add the new keys to your `xx.json` with translations.
|
||||||
|
2. Run `fpy pack src/foreignthon_xx/xx.json` to validate.
|
||||||
|
3. Run `pytest tests/` to confirm all checks pass.
|
||||||
|
4. Bump your pack's version in `pyproject.toml`.
|
||||||
|
5. Tag and push — the `publish.yml` workflow handles PyPI upload.
|
||||||
|
|
||||||
|
Update the `foreignthon` dependency lower bound in `pyproject.toml` if your pack requires the new core version:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
dependencies = ["foreignthon>=0.5.4"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Releasing independently
|
||||||
|
|
||||||
|
You can release a pack at any time — to fix a translation, add missing entries, or extend coverage. The process is the same:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# bump version in pyproject.toml
|
||||||
|
git add pyproject.toml src/foreignthon_xx/xx.json
|
||||||
|
git commit -m "v0.3.3: fix translation for 'return'"
|
||||||
|
git tag v0.3.3
|
||||||
|
git push origin v0.3.3
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Docs release (`foreignthon-docs`)
|
||||||
|
|
||||||
|
The docs site rebuilds automatically when a language pack fires its `trigger-docs.yml` workflow after a successful publish. No manual step is needed for routine pack releases.
|
||||||
|
|
||||||
|
To manually trigger a docs rebuild (e.g. after editing `foreignthon-docs` directly):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git push origin main
|
||||||
|
```
|
||||||
|
|
||||||
|
The `deploy.yml` workflow builds the MkDocs site and deploys it to [foreignthon.keshavanand.net](https://foreignthon.keshavanand.net).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Gitea secrets
|
||||||
|
|
||||||
|
| Secret | Used by |
|
||||||
|
|---|---|
|
||||||
|
| `PYPI_TOKEN` | `publish.yml` in core and all packs |
|
||||||
|
| `DOCS_WEBHOOK` | `trigger-docs.yml` in language packs |
|
||||||
|
|
||||||
|
Secrets are set per-repository in **Gitea → Settings → Actions → Secrets**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Checklist
|
||||||
|
|
||||||
|
### Core
|
||||||
|
- [ ] Version bumped in `pyproject.toml`
|
||||||
|
- [ ] `template.json` updated if new keys added
|
||||||
|
- [ ] `pytest` passes
|
||||||
|
- [ ] `ruff check` passes
|
||||||
|
- [ ] Tagged `v*` and pushed
|
||||||
|
- [ ] PyPI upload confirmed
|
||||||
|
|
||||||
|
### Language pack
|
||||||
|
- [ ] New `template.json` entries translated in `xx.json`
|
||||||
|
- [ ] `fpy pack xx.json` passes
|
||||||
|
- [ ] `pytest tests/` passes
|
||||||
|
- [ ] Version bumped
|
||||||
|
- [ ] `foreignthon` dependency lower bound updated if required
|
||||||
|
- [ ] Tagged and pushed
|
||||||
|
|||||||
@@ -1 +1,135 @@
|
|||||||
# Coming soon
|
# Getting Started
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Python 3.9 or later
|
||||||
|
- A language pack (`pip install foreignthon-xx`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install foreignthon
|
||||||
|
```
|
||||||
|
|
||||||
|
For global CLI access across projects, use `pipx`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pipx install foreignthon
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Create a project
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fpy new myproject --lang <code>
|
||||||
|
cd myproject
|
||||||
|
```
|
||||||
|
|
||||||
|
This scaffolds:
|
||||||
|
|
||||||
|
```
|
||||||
|
myproject/
|
||||||
|
├── .foreignthon.toml # project config
|
||||||
|
├── .gitignore
|
||||||
|
├── README.md
|
||||||
|
└── src/
|
||||||
|
└── main.<lang>.py # hello world in your language
|
||||||
|
```
|
||||||
|
|
||||||
|
The `.foreignthon.toml` stores your language and any local pack overrides:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[foreignthon]
|
||||||
|
lang = "es"
|
||||||
|
# custom_pack = "custom.json"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File naming
|
||||||
|
|
||||||
|
ForeignThon detects the language from the file extension:
|
||||||
|
|
||||||
|
```
|
||||||
|
script.es.py → Spanish
|
||||||
|
script.ta.py → Tamil
|
||||||
|
script.fr.py → French
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also declare the language at the top of the file:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# foreignthon: es
|
||||||
|
```
|
||||||
|
|
||||||
|
Or override it at runtime:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fpy run script.py --lang es
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fpy run src/main.es.py
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Compile
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fpy compile src/main.es.py
|
||||||
|
# → src/main.compiled.py
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fpy compile src/main.es.py -o dist/
|
||||||
|
# → dist/main.compiled.py
|
||||||
|
```
|
||||||
|
|
||||||
|
The compiled file is standard Python. Commit it alongside your source — anyone can run it without ForeignThon installed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Validate
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fpy check src/main.es.py
|
||||||
|
# ✓ main.es.py looks good.
|
||||||
|
```
|
||||||
|
|
||||||
|
Checks syntax without running — useful in CI.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Errors
|
||||||
|
|
||||||
|
When something goes wrong, ForeignThon shows the error in your language first, then English:
|
||||||
|
|
||||||
|
```
|
||||||
|
[ES] ErrorDeDivisionCero: Error división por cero
|
||||||
|
[EN] ZeroDivisionError: division by zero
|
||||||
|
File "src/main.es.py", line 8
|
||||||
|
```
|
||||||
|
|
||||||
|
Tracebacks point to your original source file, not any intermediate.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Variable names
|
||||||
|
|
||||||
|
Variable names are completely optional — English names work alongside foreign keywords with no issues. Only keywords and builtins are ever swapped.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next steps
|
||||||
|
|
||||||
|
- [CLI Reference](cli-reference.md) — all commands and flags
|
||||||
|
- [Language Packs](language-packs/index.md) — available languages
|
||||||
|
- [Custom Packs](custom-packs.md) — extend or override a pack locally
|
||||||
|
|||||||
@@ -1 +1,42 @@
|
|||||||
# ForeignThon
|
# ForeignThon
|
||||||
|
|
||||||
|
Write Python in any human language.
|
||||||
|
|
||||||
|
ForeignThon is a transpiler that converts `.xx.py` files into standard Python — keywords, builtins, and exceptions all translated into your language. The compiled output runs anywhere without ForeignThon installed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
```
|
||||||
|
source.es.py → fpy → source.compiled.py → Python
|
||||||
|
```
|
||||||
|
|
||||||
|
ForeignThon uses Python's `tokenize` module to swap `NAME` tokens. Strings, comments, and f-strings are never touched. The result is identical, valid Python.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install foreignthon
|
||||||
|
pip install foreignthon-es # or any other language pack
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## At a glance
|
||||||
|
|
||||||
|
| Feature | Description |
|
||||||
|
|---|---|
|
||||||
|
| Transpiler | Tokenizer-based, safe, unicode-aware |
|
||||||
|
| File format | `.xx.py` where `xx` is the language code |
|
||||||
|
| Errors | Shown in your language first, English below |
|
||||||
|
| Postfix syntax | `@@` operator for SOV languages |
|
||||||
|
| Custom packs | Local JSON override, no PyPI needed |
|
||||||
|
| CLI | `fpy run`, `fpy compile`, `fpy decompile`, `fpy new` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[Get started →](getting-started.md){ .md-button .md-button--primary }
|
||||||
|
[CLI Reference →](cli-reference.md){ .md-button }
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
# Coming soon
|
|
||||||
58
docs/language-packs/index.md
Normal file
58
docs/language-packs/index.md
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# Language Packs
|
||||||
|
|
||||||
|
Language packs are separate PyPI packages that define keyword, builtin, and exception mappings for a specific human language. Each is installed independently and discovered automatically by ForeignThon.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Install a pack
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install foreignthon-<code>
|
||||||
|
```
|
||||||
|
|
||||||
|
Multiple packs can coexist — install as many as you need.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Available packs
|
||||||
|
|
||||||
|
| Language | Code | Install |
|
||||||
|
|---|---|---|
|
||||||
|
| Spanish | `es` | `pip install foreignthon-es` |
|
||||||
|
| Tamil | `ta` | `pip install foreignthon-ta` |
|
||||||
|
| Chineese | `zh` | `pip install foreignthon-zh` |
|
||||||
|
| Telugu | `te` | `pip install foreignthon-te` |
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How discovery works
|
||||||
|
|
||||||
|
ForeignThon uses Python [entry points](https://packaging.python.org/en/latest/specifications/entry-points/) to discover installed packs. Installing a pack is sufficient — no configuration required.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# Each pack registers itself in its pyproject.toml:
|
||||||
|
[project.entry-points."foreignthon.langs"]
|
||||||
|
es = "foreignthon_es"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What a pack covers
|
||||||
|
|
||||||
|
| Section | Examples |
|
||||||
|
|---|---|
|
||||||
|
| Keywords | `if`, `for`, `def`, `class`, `return` … |
|
||||||
|
| Builtins | `print`, `range`, `len`, `type` … |
|
||||||
|
| Exceptions | `ValueError`, `TypeError`, `KeyError` … |
|
||||||
|
| Error messages | Bilingual error output translations |
|
||||||
|
| Stdlib | `math`, `sys`, `os`, `random` … |
|
||||||
|
| Postfix keywords | Which keywords to rewrite with `@@` on decompile |
|
||||||
|
|
||||||
|
Third-party library names (numpy, pandas, etc.) are intentionally out of scope.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Don't see your language?
|
||||||
|
|
||||||
|
Anyone can publish a `foreignthon-xx` pack — no core access required. See [Contributing → Language Packs](../contributing/language-packs.md) to get started, or use a [local custom pack](../custom-packs.md) in the meantime.
|
||||||
240
docs/language-packs/template.md
Normal file
240
docs/language-packs/template.md
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
# 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](../contributing/language-packs.md) 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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fpy new myproject --lang custom
|
||||||
|
```
|
||||||
|
|
||||||
|
This scaffolds a `custom.json` locally — no PyPI account needed. See [Custom Packs](../custom-packs.md) 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`
|
||||||
|
|
||||||
|
```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`
|
||||||
|
|
||||||
|
```python
|
||||||
|
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**.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"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):
|
||||||
|
|
||||||
|
```json
|
||||||
|
"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
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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](https://git.keshavanand.net/foreign-thon/foreignthon-docs) to get added to the [Language Packs overview](index.md). See [Contributing → Language Packs](../contributing/language-packs.md) for the full process.
|
||||||
@@ -1 +1,87 @@
|
|||||||
# Coming soon
|
## Postfix Syntax
|
||||||
|
|
||||||
|
Some languages are SOV — subject-object-verb — meaning the condition naturally comes before the keyword rather than after it. ForeignThon supports this with the `@@` operator.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The problem
|
||||||
|
|
||||||
|
In English-order Python, the keyword always comes first:
|
||||||
|
|
||||||
|
```python
|
||||||
|
if condition:
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
In many languages, the natural order is the opposite — the condition is stated first, then the action. Forcing English word order on these languages makes the code feel unnatural.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The solution
|
||||||
|
|
||||||
|
The `@@` operator lets you put any keyword after its expression:
|
||||||
|
|
||||||
|
```
|
||||||
|
condition @@keyword:
|
||||||
|
body
|
||||||
|
```
|
||||||
|
|
||||||
|
This is equivalent to:
|
||||||
|
|
||||||
|
```
|
||||||
|
keyword condition:
|
||||||
|
body
|
||||||
|
```
|
||||||
|
|
||||||
|
Both produce identical compiled Python. `@@` is purely a source-level syntax — it is processed before tokenization and never appears in the output.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- `@@` rewrites only the line it appears on — nothing else changes
|
||||||
|
- Indentation follows standard Python rules, unchanged
|
||||||
|
- Prefix and postfix can be mixed freely in the same file
|
||||||
|
- Works for any keyword in any language pack
|
||||||
|
- `@@` is not valid Python syntax, so it never conflicts with existing code
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Supported constructs
|
||||||
|
|
||||||
|
| Construct | Prefix | Postfix |
|
||||||
|
|---|---|---|
|
||||||
|
| if | `keyword condition:` | `condition @@keyword:` |
|
||||||
|
| elif | `keyword condition:` | `condition @@keyword:` |
|
||||||
|
| while | `keyword condition:` | `condition @@keyword:` |
|
||||||
|
| def | `keyword name(args):` | `name(args) @@keyword:` |
|
||||||
|
| class | `keyword Name:` | `Name @@keyword:` |
|
||||||
|
| for | `keyword var in iter:` | `var @@in_kw iter @@for_kw:` |
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
`for` loops with postfix require two `@@` operators and can be complex. Most users keep `for` in prefix style.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Decompile with postfix
|
||||||
|
|
||||||
|
When converting Python back to a foreign language, pass `--postfix`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
fpy decompile script.py --lang <code> --postfix
|
||||||
|
```
|
||||||
|
|
||||||
|
Which keywords get rewritten is controlled by the `postfix_keywords` list in the language pack JSON. A language that uses SVO order sets this to `[]` — postfix output is never forced on languages that don't need it.
|
||||||
|
|
||||||
|
```json
|
||||||
|
"postfix_keywords": ["if", "elif", "while", "def", "class"]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Input vs output
|
||||||
|
|
||||||
|
| Direction | Mechanism | Controlled by |
|
||||||
|
|---|---|---|
|
||||||
|
| Input (writing `.xx.py`) | `@@` in source | Always available for any keyword |
|
||||||
|
| Output (`fpy decompile --postfix`) | Pack's `postfix_keywords` | Language pack author |
|
||||||
|
|||||||
18
languages.yml
Normal file
18
languages.yml
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Languages listed here get their README fetched from their repo
|
||||||
|
# and included in the docs site. Only add stable, complete packs.
|
||||||
|
languages:
|
||||||
|
- code: es
|
||||||
|
name: Spanish
|
||||||
|
repo: foreignthon-es
|
||||||
|
|
||||||
|
- code: ta
|
||||||
|
name: Tamil
|
||||||
|
repo: foreignthon-ta
|
||||||
|
|
||||||
|
- code: zh
|
||||||
|
name: Chinese
|
||||||
|
repo: foreignthon-zh
|
||||||
|
|
||||||
|
- code: te
|
||||||
|
name: Telugu
|
||||||
|
repo: foreignthon-te
|
||||||
33
mkdocs.yml
33
mkdocs.yml
@@ -1,21 +1,34 @@
|
|||||||
site_name: ForeignThon
|
site_name: ForeignThon
|
||||||
site_url: https://foreignthon.keshavanand.net
|
site_url: https://foreignthon.keshavanand.net
|
||||||
site_description: Write Python in any human language
|
site_description: Write Python in any human language
|
||||||
repo_url: https://git.keshavanand.net/foreign-thon/foreignthon-core
|
repo_url: https://git.keshavanand.net/foreign-thon/
|
||||||
repo_name: foreign-thon/foreignthon-core
|
repo_name: foreignthon
|
||||||
edit_uri: ""
|
edit_uri: ""
|
||||||
|
|
||||||
theme:
|
theme:
|
||||||
name: material
|
name: material
|
||||||
|
|
||||||
palette:
|
palette:
|
||||||
- scheme: default
|
# Dark mode default
|
||||||
toggle:
|
|
||||||
icon: material/brightness-7
|
|
||||||
name: Switch to dark mode
|
|
||||||
- scheme: slate
|
- scheme: slate
|
||||||
|
primary: deep purple
|
||||||
|
accent: cyan
|
||||||
toggle:
|
toggle:
|
||||||
icon: material/brightness-4
|
icon: material/weather-sunny
|
||||||
name: Switch to light mode
|
name: Switch to light mode
|
||||||
|
|
||||||
|
# Light mode
|
||||||
|
- scheme: default
|
||||||
|
primary: deep purple
|
||||||
|
accent: cyan
|
||||||
|
toggle:
|
||||||
|
icon: material/weather-night
|
||||||
|
name: Switch to dark mode
|
||||||
|
|
||||||
|
font:
|
||||||
|
text: Inter
|
||||||
|
code: JetBrains Mono
|
||||||
|
|
||||||
features:
|
features:
|
||||||
- navigation.tabs
|
- navigation.tabs
|
||||||
- navigation.sections
|
- navigation.sections
|
||||||
@@ -25,6 +38,8 @@ theme:
|
|||||||
- search.suggest
|
- search.suggest
|
||||||
- content.code.copy
|
- content.code.copy
|
||||||
- content.code.annotate
|
- content.code.annotate
|
||||||
|
- content.tabs.link
|
||||||
|
- toc.follow
|
||||||
|
|
||||||
nav:
|
nav:
|
||||||
- Home: index.md
|
- Home: index.md
|
||||||
@@ -34,8 +49,8 @@ nav:
|
|||||||
- Custom Packs: custom-packs.md
|
- Custom Packs: custom-packs.md
|
||||||
- Language Packs:
|
- Language Packs:
|
||||||
- Overview: language-packs/index.md
|
- Overview: language-packs/index.md
|
||||||
- Spanish (es): language-packs/es.md
|
# LANGS_NAV_START
|
||||||
- Tamil (ta): language-packs/ta.md
|
# LANGS_NAV_END
|
||||||
- Add your language: language-packs/template.md
|
- Add your language: language-packs/template.md
|
||||||
- Contributing:
|
- Contributing:
|
||||||
- Core: contributing/core.md
|
- Core: contributing/core.md
|
||||||
|
|||||||
Reference in New Issue
Block a user