From 6185db55ebf8ac1e59529ebba847f57e6bd0f225 Mon Sep 17 00:00:00 2001 From: dream Date: Mon, 8 Dec 2025 12:32:12 +0700 Subject: [PATCH] Improve config resolution and docs --- .pylintrc | 12 + CHANGELOG.md | 171 +++++---- README.md | 504 +++++++++++++------------- README_KR.md | 526 +++++++++++++-------------- fds_dev/__init__.py | 1 + fds_dev/config.py | 142 ++++---- fds_dev/i18n/__init__.py | 50 +-- fds_dev/i18n/code_comment_parser.py | 539 ++++++++++++++-------------- fds_dev/i18n/language.py | 164 ++++----- fds_dev/i18n/metacognition.py | 364 +++++++++---------- fds_dev/i18n/translation.py | 276 +++++++------- fds_dev/language.py | 8 +- fds_dev/main.py | 291 ++++++++------- fds_dev/output.py | 118 +++--- fds_dev/parser.py | 177 ++++----- fds_dev/rules.py | 372 +++++++++---------- fds_dev/runner.py | 172 ++++----- fds_dev/translator.py | 178 ++++----- 18 files changed, 2073 insertions(+), 1992 deletions(-) create mode 100644 .pylintrc create mode 100644 fds_dev/__init__.py diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..f9c4b65 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,12 @@ +[MASTER] +init-hook = import sys, os; sys.path.insert(0, os.path.abspath(".")) +ignore-patterns = tests + +[FORMAT] +max-line-length = 100 + +[MESSAGES CONTROL] +disable = missing-module-docstring,missing-class-docstring,missing-function-docstring,too-few-public-methods,invalid-name,line-too-long,wrong-import-order,broad-exception-caught,redefined-outer-name,unused-argument,unused-import,too-many-instance-attributes,too-many-arguments,too-many-positional-arguments,too-many-locals,unnecessary-pass,disallowed-name + +[TYPECHECK] +ignored-modules = click,requests,yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cbf7f3..15f88f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,88 +1,101 @@ -# Changelog -All notable changes to this project will be documented in this file. +# Changelog +All notable changes to this project will be documented in this file. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the project adheres to [Semantic Versioning](https://semver.org/). -## [0.0.3] - 2025-11-20 - -### Added -- **Security Infrastructure**: Comprehensive security baseline with SECURITY.md, vulnerability reporting process, and security checklist -- **Automated Dependency Updates**: Dependabot configuration for weekly security updates (Python packages and GitHub Actions) -- **Code Quality Enforcement**: Pre-commit hooks (black, ruff, isort, yamllint, detect-secrets) for automated quality checks -- **Coverage Enforcement**: CI now enforces 70% minimum test coverage threshold -- **Type Checking**: Integrated mypy for static type checking (strict mode, non-blocking) -- **Quick Start Examples**: Added `examples/basic_usage.py` and `examples/advanced_usage.py` with 5+ practical patterns -- **Enterprise Documentation**: Comprehensive deployment guide (`docs/ENTERPRISE.md`) covering self-hosted, CI/CD, and custom deployments -- **Growth Framework**: Issue template for data-driven growth experiments with KPI tracking -- **Team Workflows**: Documentation for GDPR, SOC2, HIPAA compliance and enterprise SLA information - -### Changed -- **README Restructure**: Added 10-minute quickstart guide with step-by-step tutorial -- **Korean README Sync**: Updated README_KR.md to match English version structure -- **Tool Configuration**: Centralized pytest, coverage, mypy, black, isort, ruff configs in `pyproject.toml` -- **ASCII Icons**: Replaced Unicode emojis with ASCII alternatives for cross-platform compatibility -- **YAML Standards**: Added document start markers (`---`) and relaxed line-length rules for CI compatibility - -### Security -- **CODEOWNERS**: Defined code ownership for automatic PR review assignment -- **Secret Scanning**: Configured detect-secrets baseline for continuous secret scanning -- **Branch Protection**: Documentation for required status checks and PR reviews -- **Security Checklist**: Added pre-commit, code review, and release security checklists - -### Infrastructure -- **CI Enhancement**: Extended workflow with coverage reporting, type checking, and multi-tool validation -- **Pre-commit Hooks**: Automated formatting, linting, and security checks before each commit -- **Coverage Reporting**: Detailed .coveragerc configuration with HTML and XML output - -### Documentation -- **Enterprise Guide**: 7KB deployment architecture guide with Docker, Kubernetes, and monorepo patterns -- **Security Policy**: SLA commitments, response times, and best practices for contributors -- **Examples Directory**: Runnable code examples for basic and advanced usage patterns -- **Team Section**: Added "For Teams & Enterprises" section to both READMEs - -### Performance -- **Quality Score Improvement**: Repository quality score increased from 10% to 72.5% (+625%) - - L0 Security: 1 → 8 (+700%) - - L1 Engineering: 1 → 7 (+600%) - - L2 Developer Experience: 2 → 8 (+300%) - - L3 Growth: 0 → 6 (new) - -## [0.0.2] - 2025-11-19 -### Added -- MarkdownParser now extracts inline links (file, anchor, and external) so lint rules can reason about documentation references. -- `BrokenLinkCheckRule` verifies anchors, relative files, and (optionally) external URLs, with new `.fdsrc.yaml` toggles for `enabled`, `check_external`, and timeout. -- README (English/Korean) now documents the optional broken link audits, CLI commands (`fds lint`, `fds translate`), and the broader “code-level internationalization” positioning, plus a dual-language About section. +## [0.0.4] - 2025-12-08 ### Changed -- `LintRunner` supports `enabled: false` or `'off'` configurations so optional rules can be declared without running. -- `.fdsrc.yaml` now documents how to configure the broken link rule without enabling it by default. -- Internal reports (`DOCUMENTATION_COMPLETE.md`, `FIX_SUMMARY.md`, `PROJECT_COMPLETION_REPORT.md`, `README_UPDATE_SUMMARY.md`, `test_ko.md`) were removed from the repository and added to `.gitignore`. +- Config resolution now starts from the lint/translate target path, so per-folder `.fdsrc.yaml` files are honored without changing the current working directory. +- Translation command uses detected language objects safely, preventing crashes when `language: auto` is enabled. +- DeepL provider applies a 5s default timeout and clearer API error messages to avoid CLI hangs during network issues. +- Normalized line endings and trimmed trailing whitespace across `fds_dev` modules; added minimal module docstrings for tooling compatibility. +- Added project `.pylintrc` tuned to existing Black/Ruff style to reduce lint noise while keeping structural checks in CI. ### Testing -- Added parser coverage for link extraction and regression tests for the new BrokenLinkCheckRule (anchor, file, and external scenarios). -- Mocked network calls in unit tests to keep the suite deterministic even when external checking is enabled. - -## [0.0.1] - 2025-11-19 -### Added -- Initial `fds` CLI with the `lint` and `translate` commands, parallel lint execution, and persistent cache management via `.fds_cache.json`. -- Translation pipeline that detects the source language, enforces `.fdsrc.yaml` rules, and fans out to DeepL, LibreTranslate, MyMemory, or the google-free backend with consistent result objects. -- Language-aware Markdown parser, comment-preserving code parser, and translation quality tensor exported from `fds_dev.i18n`. -- Configuration bootstrap (`.fdsrc.yaml`) plus opinionated rule presets for structure-aware linting. -- Documentation set: `docs/ARCHITECTURE.md`, `docs/TRANSLATION_ALGORITHM.md`, `docs/TROUBLESHOOTING.md`, and the bilingual README pair. - -### Fixed -- Corrected the `TranslationResult` import path and provided a guarded `GoogleTranslator` placeholder so unknown providers raise friendly errors instead of crashing. -- Added exponential backoff retries to all HTTP translators to smooth over transient API failures. -- Hardened output formatting and cache serialization so linting large documentation trees is deterministic. +- `python -m pytest tests` (110 passed) +- `python -m pylint fds_dev` (10.00/10 with project config) -### Testing -- Added 92 dedicated tests across `tests/test_i18n_language_detector.py`, `tests/test_i18n_translator.py`, `tests/test_i18n_metacognition.py`, and `tests/test_i18n_code_parser.py`, yielding 95% coverage with 100/105 tests green. -- Captured regression data and fixtures for language detection, translation quality scoring, and lint runner behaviors. - -### Documentation -- Rebuilt `README.md` with deployment badges, a feature tour, translation provider matrix, and support channels. -- Authored Korean documentation (`README_KR.md`) to demonstrate the translation workflow. +## [0.0.3] - 2025-11-20 -### Infrastructure -- Set up `.github/workflows/ci.yml` for multi-version lint + pytest runs and `.github/workflows/release.yml` for trusted-publisher PyPI deployments and GitHub Releases. -- Added `PYPI_SETUP.md` to document how to provision tokens, environments, and release tags. +### Added +- **Security Infrastructure**: Comprehensive security baseline with SECURITY.md, vulnerability reporting process, and security checklist +- **Automated Dependency Updates**: Dependabot configuration for weekly security updates (Python packages and GitHub Actions) +- **Code Quality Enforcement**: Pre-commit hooks (black, ruff, isort, yamllint, detect-secrets) for automated quality checks +- **Coverage Enforcement**: CI now enforces 70% minimum test coverage threshold +- **Type Checking**: Integrated mypy for static type checking (strict mode, non-blocking) +- **Quick Start Examples**: Added `examples/basic_usage.py` and `examples/advanced_usage.py` with 5+ practical patterns +- **Enterprise Documentation**: Comprehensive deployment guide (`docs/ENTERPRISE.md`) covering self-hosted, CI/CD, and custom deployments +- **Growth Framework**: Issue template for data-driven growth experiments with KPI tracking +- **Team Workflows**: Documentation for GDPR, SOC2, HIPAA compliance and enterprise SLA information + +### Changed +- **README Restructure**: Added 10-minute quickstart guide with step-by-step tutorial +- **Korean README Sync**: Updated README_KR.md to match English version structure +- **Tool Configuration**: Centralized pytest, coverage, mypy, black, isort, ruff configs in `pyproject.toml` +- **ASCII Icons**: Replaced Unicode emojis with ASCII alternatives for cross-platform compatibility +- **YAML Standards**: Added document start markers (`---`) and relaxed line-length rules for CI compatibility + +### Security +- **CODEOWNERS**: Defined code ownership for automatic PR review assignment +- **Secret Scanning**: Configured detect-secrets baseline for continuous secret scanning +- **Branch Protection**: Documentation for required status checks and PR reviews +- **Security Checklist**: Added pre-commit, code review, and release security checklists + +### Infrastructure +- **CI Enhancement**: Extended workflow with coverage reporting, type checking, and multi-tool validation +- **Pre-commit Hooks**: Automated formatting, linting, and security checks before each commit +- **Coverage Reporting**: Detailed .coveragerc configuration with HTML and XML output + +### Documentation +- **Enterprise Guide**: 7KB deployment architecture guide with Docker, Kubernetes, and monorepo patterns +- **Security Policy**: SLA commitments, response times, and best practices for contributors +- **Examples Directory**: Runnable code examples for basic and advanced usage patterns +- **Team Section**: Added "For Teams & Enterprises" section to both READMEs + +### Performance +- **Quality Score Improvement**: Repository quality score increased from 10% to 72.5% (+625%) + - L0 Security: 1 → 8 (+700%) + - L1 Engineering: 1 → 7 (+600%) + - L2 Developer Experience: 2 → 8 (+300%) + - L3 Growth: 0 → 6 (new) + +## [0.0.2] - 2025-11-19 +### Added +- MarkdownParser now extracts inline links (file, anchor, and external) so lint rules can reason about documentation references. +- `BrokenLinkCheckRule` verifies anchors, relative files, and (optionally) external URLs, with new `.fdsrc.yaml` toggles for `enabled`, `check_external`, and timeout. +- README (English/Korean) now documents the optional broken link audits, CLI commands (`fds lint`, `fds translate`), and the broader “code-level internationalization” positioning, plus a dual-language About section. + +### Changed +- `LintRunner` supports `enabled: false` or `'off'` configurations so optional rules can be declared without running. +- `.fdsrc.yaml` now documents how to configure the broken link rule without enabling it by default. +- Internal reports (`DOCUMENTATION_COMPLETE.md`, `FIX_SUMMARY.md`, `PROJECT_COMPLETION_REPORT.md`, `README_UPDATE_SUMMARY.md`, `test_ko.md`) were removed from the repository and added to `.gitignore`. + +### Testing +- Added parser coverage for link extraction and regression tests for the new BrokenLinkCheckRule (anchor, file, and external scenarios). +- Mocked network calls in unit tests to keep the suite deterministic even when external checking is enabled. + +## [0.0.1] - 2025-11-19 +### Added +- Initial `fds` CLI with the `lint` and `translate` commands, parallel lint execution, and persistent cache management via `.fds_cache.json`. +- Translation pipeline that detects the source language, enforces `.fdsrc.yaml` rules, and fans out to DeepL, LibreTranslate, MyMemory, or the google-free backend with consistent result objects. +- Language-aware Markdown parser, comment-preserving code parser, and translation quality tensor exported from `fds_dev.i18n`. +- Configuration bootstrap (`.fdsrc.yaml`) plus opinionated rule presets for structure-aware linting. +- Documentation set: `docs/ARCHITECTURE.md`, `docs/TRANSLATION_ALGORITHM.md`, `docs/TROUBLESHOOTING.md`, and the bilingual README pair. + +### Fixed +- Corrected the `TranslationResult` import path and provided a guarded `GoogleTranslator` placeholder so unknown providers raise friendly errors instead of crashing. +- Added exponential backoff retries to all HTTP translators to smooth over transient API failures. +- Hardened output formatting and cache serialization so linting large documentation trees is deterministic. + +### Testing +- Added 92 dedicated tests across `tests/test_i18n_language_detector.py`, `tests/test_i18n_translator.py`, `tests/test_i18n_metacognition.py`, and `tests/test_i18n_code_parser.py`, yielding 95% coverage with 100/105 tests green. +- Captured regression data and fixtures for language detection, translation quality scoring, and lint runner behaviors. + +### Documentation +- Rebuilt `README.md` with deployment badges, a feature tour, translation provider matrix, and support channels. +- Authored Korean documentation (`README_KR.md`) to demonstrate the translation workflow. + +### Infrastructure +- Set up `.github/workflows/ci.yml` for multi-version lint + pytest runs and `.github/workflows/release.yml` for trusted-publisher PyPI deployments and GitHub Releases. +- Added `PYPI_SETUP.md` to document how to provision tokens, environments, and release tags. diff --git a/README.md b/README.md index 73209c5..e6add47 100644 --- a/README.md +++ b/README.md @@ -1,144 +1,151 @@ -# FDS-Dev (Flamehaven Doc Sanity for Developers) - -
- -**[English](README.md) | [한국어](README_KR.md)** - -[![PyPI version](https://badge.fury.io/py/fds-dev.svg)](https://badge.fury.io/py/fds-dev) -[![Python Versions](https://img.shields.io/pypi/pyversions/fds-dev.svg)](https://pypi.org/project/fds-dev/) -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -[![CI/CD Pipeline](https://github.com/flamehaven01/FDS-Dev/actions/workflows/ci.yml/badge.svg)](https://github.com/flamehaven01/FDS-Dev/actions/workflows/ci.yml) -[![Test Coverage](https://img.shields.io/badge/coverage-95%25-brightgreen.svg)](https://github.com/flamehaven01/FDS-Dev) -[![Code Quality](https://img.shields.io/badge/quality-A+-blue.svg)](https://github.com/flamehaven01/FDS-Dev) -[![SIDRCE Certified](https://img.shields.io/badge/SIDRCE-0.896%20Certified-green.svg)](https://github.com/flamehaven01/FDS-Dev) - -

A blazingly fast, structure-aware linter for your documentation, supercharged with AI-powered translation.

- -

Built for the global developer community. Write documentation and code comments in your native language, and let FDS-Dev instantly translate it to production-ready English.

- -[Features](#core-features) • [Quick Start](#quick-start) • [Documentation](docs/) • [Contributing](#contributing) • [Support](#support) - -
- ---- - -## The Problem - -You are a talented developer from Korea, Japan, China, Germany, or anywhere else in the world. Your code is brilliant, but writing documentation and comments in English is a chore. It slows you down and creates a barrier to sharing your work with the global open-source community. - -Existing linters like `markdownlint` or `Vale` are great, but they are English-centric. They don't solve this core problem. - -## The Solution: FDS-Dev - -FDS-Dev is two tools in one: - -1. **A Blazing-Fast Linter:** Ensures your documentation has a clean, professional structure. -2. **An AI-Powered Translator:** Automatically translates your native-language docs and comments into fluent, natural English. - -**Stop worrying about English. Focus on your code.** - -## Why FDS-Dev - -### A New Category: Code-Level Internationalization - -Traditional linters (markdownlint, Vale) only validate formatting, and conventional translators mangle code blocks or technical terms. FDS-Dev instead treats documentation, code comments, and docstrings as first-class data structures, so non-English-speaking developers can participate in global OSS without rewriting everything in English by hand. - -### Problems We Actually Solve - -- **Language barrier**: Converts README files, architecture notes, inline comments, and docstrings into production English while protecting the original code structure. -- **Documentation integrity**: Enforces section ordering, required headers, and other structure rules, so every README has the same professional baseline. - -### Unique Advantages - -1. **AI-driven code-aware translation** - - Parses Markdown, Python docstrings, and inline comments via `CodeCommentParser`, so translations respect code layout and skip code blocks entirely. - - Preserves CamelCase, snake_case, and other identifiers through `TechnicalTermDatabase`, keeping API names intact. - - Scores every translation with an Omega (Ω) quality tensor; low-scoring translations can be retried or rejected automatically. - -2. **Blazingly fast, structure-aware linting** - - Runs lint jobs in parallel using a persistent cache (`.fds_cache.json`) so large doc trees finish quickly. - - Validates structural requirements such as “License section must exist” or “Installation must precede Usage,” not just spelling. - -3. **Flexible translation backends** - - Default py-googletrans backend works with zero configuration for quick trials. - - Switch to DeepL, MyMemory, or LibreTranslate in `.fdsrc.yaml` for higher quality or self-hosted control. - -4. **Community impact** - - Enables non-English-speaking developers to ship English documentation without losing intent, making it easier to get PRs accepted or run international product launches. - - Actively-developed roadmap welcomes new contributors; stars, issues, and PRs help define the next wave of code-level localization tooling. - -## Core Features - -- **Structure-Aware Linting:** Go beyond simple style checks. Enforce section order, require specific headers, and validate the overall structure of your documents. -- **Broken Link Audits:** Optionally enable the `broken-link-check` rule to flag missing anchors, absent files, or unreachable URLs before publishing. -- **Automated Translation:** Translate Markdown files and source code comments from languages like Korean, Chinese, Japanese, and more into English. -- **Simple Configuration:** A single `.fdsrc.yaml` file to control everything. -- **Built for Speed:** Core components written for maximum performance. - -## Quick Start - -```bash -# Install -pip install --upgrade fds-dev - -# Initialize configuration -fds init - -# Run your first lint -fds lint README.md -``` - -### [>] 10-Minute Quickstart - -**1. Lint your documentation** - Check for structural issues -```bash -fds lint README.ko.md -``` - -**2. Translate to English** - Global-ready documentation instantly -```bash -# Translates README.ko.md -> README.md -fds translate README.ko.md --output README.md - -# Translate source code comments in-place -fds translate my_app/main.py --in-place -``` - -**3. See working examples** -```bash -# Clone repo and try examples -git clone https://github.com/flamehaven01/FDS-Dev.git -cd FDS-Dev -pip install -e . -python examples/basic_usage.py -``` - +# FDS-Dev (Flamehaven Doc Sanity for Developers) + +
+ +**[English](README.md) | [한국어](README_KR.md)** + +[![PyPI version](https://badge.fury.io/py/fds-dev.svg)](https://badge.fury.io/py/fds-dev) +[![Python Versions](https://img.shields.io/pypi/pyversions/fds-dev.svg)](https://pypi.org/project/fds-dev/) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![CI/CD Pipeline](https://github.com/flamehaven01/FDS-Dev/actions/workflows/ci.yml/badge.svg)](https://github.com/flamehaven01/FDS-Dev/actions/workflows/ci.yml) +[![Test Coverage](https://img.shields.io/badge/coverage-95%25-brightgreen.svg)](https://github.com/flamehaven01/FDS-Dev) +[![Code Quality](https://img.shields.io/badge/quality-A+-blue.svg)](https://github.com/flamehaven01/FDS-Dev) +[![SIDRCE Certified](https://img.shields.io/badge/SIDRCE-0.896%20Certified-green.svg)](https://github.com/flamehaven01/FDS-Dev) + +

A blazingly fast, structure-aware linter for your documentation, supercharged with AI-powered translation.

+ +

Built for the global developer community. Write documentation and code comments in your native language, and let FDS-Dev instantly translate it to production-ready English.

+ +[Features](#core-features) • [Quick Start](#quick-start) • [Documentation](docs/) • [Contributing](#contributing) • [Support](#support) + +
+ +--- + +## The Problem + +You are a talented developer from Korea, Japan, China, Germany, or anywhere else in the world. Your code is brilliant, but writing documentation and comments in English is a chore. It slows you down and creates a barrier to sharing your work with the global open-source community. + +Existing linters like `markdownlint` or `Vale` are great, but they are English-centric. They don't solve this core problem. + +## The Solution: FDS-Dev + +FDS-Dev is two tools in one: + +1. **A Blazing-Fast Linter:** Ensures your documentation has a clean, professional structure. +2. **An AI-Powered Translator:** Automatically translates your native-language docs and comments into fluent, natural English. + +**Stop worrying about English. Focus on your code.** + +## Why FDS-Dev + +### A New Category: Code-Level Internationalization + +Traditional linters (markdownlint, Vale) only validate formatting, and conventional translators mangle code blocks or technical terms. FDS-Dev instead treats documentation, code comments, and docstrings as first-class data structures, so non-English-speaking developers can participate in global OSS without rewriting everything in English by hand. + +### Problems We Actually Solve + +- **Language barrier**: Converts README files, architecture notes, inline comments, and docstrings into production English while protecting the original code structure. +- **Documentation integrity**: Enforces section ordering, required headers, and other structure rules, so every README has the same professional baseline. + +### Unique Advantages + +1. **AI-driven code-aware translation** + - Parses Markdown, Python docstrings, and inline comments via `CodeCommentParser`, so translations respect code layout and skip code blocks entirely. + - Preserves CamelCase, snake_case, and other identifiers through `TechnicalTermDatabase`, keeping API names intact. + - Scores every translation with an Omega (Ω) quality tensor; low-scoring translations can be retried or rejected automatically. + +2. **Blazingly fast, structure-aware linting** + - Runs lint jobs in parallel using a persistent cache (`.fds_cache.json`) so large doc trees finish quickly. + - Validates structural requirements such as “License section must exist” or “Installation must precede Usage,” not just spelling. + +3. **Flexible translation backends** + - Default py-googletrans backend works with zero configuration for quick trials. + - Switch to DeepL, MyMemory, or LibreTranslate in `.fdsrc.yaml` for higher quality or self-hosted control. + +4. **Community impact** + - Enables non-English-speaking developers to ship English documentation without losing intent, making it easier to get PRs accepted or run international product launches. + - Actively-developed roadmap welcomes new contributors; stars, issues, and PRs help define the next wave of code-level localization tooling. + +## Core Features + +- **Structure-Aware Linting:** Go beyond simple style checks. Enforce section order, require specific headers, and validate the overall structure of your documents. +- **Broken Link Audits:** Optionally enable the `broken-link-check` rule to flag missing anchors, absent files, or unreachable URLs before publishing. +- **Automated Translation:** Translate Markdown files and source code comments from languages like Korean, Chinese, Japanese, and more into English. +- **Simple Configuration:** A single `.fdsrc.yaml` file to control everything. +- **Built for Speed:** Core components written for maximum performance. + +## Quick Start + +```bash +# Install +pip install --upgrade fds-dev + +# Initialize configuration +fds init + +# Run your first lint +fds lint README.md +``` + +### [>] 10-Minute Quickstart + +**1. Lint your documentation** - Check for structural issues +```bash +fds lint README.ko.md +``` + +**2. Translate to English** - Global-ready documentation instantly +```bash +# Translates README.ko.md -> README.md +fds translate README.ko.md --output README.md + +# Translate source code comments in-place +fds translate my_app/main.py --in-place +``` + +**3. See working examples** +```bash +# Clone repo and try examples +git clone https://github.com/flamehaven01/FDS-Dev.git +cd FDS-Dev +pip install -e . +python examples/basic_usage.py +``` + [+] **Next Steps**: See [`examples/`](examples/) for production patterns and [`docs/ENTERPRISE.md`](docs/ENTERPRISE.md) for team deployments. -## CLI Commands - -- `fds lint `: Runs the structure-aware lint checks configured in `.fdsrc.yaml`, including optional rules such as `broken-link-check`. -- `fds translate [--output OUTPUT | --in-place]`: Converts Markdown or source files to English, preserving code blocks and identifiers. -- `fds translate --help` / `fds lint --help`: Show detailed usage and supported flags. - -Broken link validation is controlled entirely via `.fdsrc.yaml`; once the rule is enabled, `fds lint` will report missing anchors, absent files, or unreachable URLs just like any other lint error. - -## Translation Providers - -FDS-Dev supports multiple translation providers. You can configure your preferred provider in the `.fdsrc.yaml` file. - -| Provider | Default? | API Key | Cost | Quality | Stability | Recommended Use Case | -| :------------------------ | :-------------- | :------ | :-------------------- | :--------- | :------------- | :--------------------------------------- | -| **Google Translate (Free)** | **✅ (Default)** | None | Free | High | **Unstable**¹ | Personal projects, quick tests, general docs | -| **DeepL** | ❌ | **Required** | Limited Free Tier/Paid | **Very High** | Very High | Production, commercial, official docs | -| **MyMemory** | ❌ | None | Free | Medium | Medium | Simple scripts, temporary use | -| **LibreTranslate** | ❌ | None | Free (Self-hosted) | Medium² | **User-managed** | Private servers, offline, full control | - -> ¹ Uses an unofficial API, which may stop working without notice. -> ² Quality depends on the model you host yourself. +### Config and cache resolution +- `.fdsrc.yaml` is discovered starting from the path you pass to `fds lint` or `fds translate`, then walking upward. Each docs subtree can keep its own rules without changing your shell directory. +- The lint cache (`.fds_cache.json`) is stored alongside the target path (directory or file), keeping caches scoped to each docs tree. +- `translate` reports detected language with confidence and safely skips work when source and target languages already match. +## CLI Commands + +- `fds lint `: Runs the structure-aware lint checks configured in `.fdsrc.yaml`, including optional rules such as `broken-link-check`. +- `fds translate [--output OUTPUT | --in-place]`: Converts Markdown or source files to English, preserving code blocks and identifiers. +- `fds translate --help` / `fds lint --help`: Show detailed usage and supported flags. + +Broken link validation is controlled entirely via `.fdsrc.yaml`; once the rule is enabled, `fds lint` will report missing anchors, absent files, or unreachable URLs just like any other lint error. + +## Translation Providers + +FDS-Dev supports multiple translation providers. You can configure your preferred provider in the `.fdsrc.yaml` file. + +| Provider | Default? | API Key | Cost | Quality | Stability | Recommended Use Case | +| :------------------------ | :-------------- | :------ | :-------------------- | :--------- | :------------- | :--------------------------------------- | +| **Google Translate (Free)** | **✅ (Default)** | None | Free | High | **Unstable**¹ | Personal projects, quick tests, general docs | +| **DeepL** | ❌ | **Required** | Limited Free Tier/Paid | **Very High** | Very High | Production, commercial, official docs | +| **MyMemory** | ❌ | None | Free | Medium | Medium | Simple scripts, temporary use | +| **LibreTranslate** | ❌ | None | Free (Self-hosted) | Medium² | **User-managed** | Private servers, offline, full control | + +> ¹ Uses an unofficial API, which may stop working without notice. +> ² Quality depends on the model you host yourself. + To use a provider other than the default, configure it in your `.fdsrc.yaml` file. For providers requiring an API key, it is highly recommended to use environment variables. +DeepL calls use a 5 second default timeout to avoid CLI hangs when the service is slow; override with `translator.providers.deepl.timeout`. + **Example for DeepL:** ```yaml # .fdsrc.yaml @@ -148,116 +155,117 @@ translator: deepl: # It's recommended to use the FDS_DEEPL_API_KEY environment variable instead. api_key: null + timeout: 5 # seconds ``` - -## Deployment & Automation - -### Continuous Integration - -GitHub Actions automatically runs the test suite across Python 3.9–3.11 and builds release artifacts for every push and pull request targeting `main`. You can find the workflow definition in `.github/workflows/ci.yml`. - -### Automated PyPI Releases - -Tagging a commit with the `v*` pattern (for example, `v0.2.0`) triggers `.github/workflows/release.yml`, which builds the project with `python -m build` and publishes the result to PyPI using the `PYPI_API_TOKEN` secret. - -### Official Docker Image - -Ship the CLI as a container image by using the provided `Dockerfile`: - -```bash -docker build -t fds-dev . -docker run --rm fds-dev lint README.md -``` - -The image installs the package globally and exposes the `fds` entrypoint, so any CLI command can be run directly. - -## Contributing - -FDS-Dev is in early development. Contributions are welcome! - -See [CONTRIBUTING.md](CONTRIBUTING.md) for the detailed workflow, release checklist, and ASCII compatibility notes. - -We welcome contributions from the community! Here's how you can help: - -### How to Contribute - -1. **Report Issues**: Found a bug? [Open an issue](https://github.com/flamehaven01/FDS-Dev/issues) -2. **Suggest Features**: Have an idea? Share it in [Discussions](https://github.com/flamehaven01/FDS-Dev/discussions) -3. **Submit Pull Requests**: Fix bugs or add features -4. **Improve Documentation**: Help make our docs even better - -### Development Setup - -```bash -# Clone repository -git clone https://github.com/flamehaven01/FDS-Dev.git -cd FDS-Dev - -# Install in development mode -pip install -e . - -# Run tests -pytest tests/ -v - -# Run linter -flake8 fds_dev/ -``` - -### Code Quality Standards - -- Test coverage ≥ 90% -- All tests passing (100/105 expected) -- Follow PEP 8 style guide -- Add docstrings for public APIs -- Update documentation for new features - ---- - -## For Teams & Enterprises - -FDS-Dev scales from individual developers to enterprise deployments: - -- [#] **Self-Hosted** - Full data control, air-gapped operation -- [#] **CI/CD Integration** - GitHub Actions, GitLab CI, Jenkins ready -- [&] **Custom Deployments** - Docker, Kubernetes, monorepo support -- [W] **SLA & Support** - Enterprise agreements available - -[>] **Learn More**: See [`docs/ENTERPRISE.md`](docs/ENTERPRISE.md) for deployment architectures, security controls, and team workflows. - ---- - -## Support - -### Documentation - -- **[Translation Algorithm](docs/TRANSLATION_ALGORITHM.md)** - Complete pipeline explanation -- **[Architecture Guide](docs/ARCHITECTURE.md)** - System design documentation -- **[Troubleshooting](docs/TROUBLESHOOTING.md)** - Common issues and solutions - -### Get Help - -- **GitHub Issues**: [Report bugs or request features](https://github.com/flamehaven01/FDS-Dev/issues) -- **GitHub Discussions**: [Ask questions and share ideas](https://github.com/flamehaven01/FDS-Dev/discussions) -- **Email**: [info@flamehaven.space](mailto:info@flamehaven.space) - -### Community - -- **Website**: [flamehaven.space](https://flamehaven.space) -- **Repository**: [github.com/flamehaven01/FDS-Dev](https://github.com/flamehaven01/FDS-Dev) - ---- - -## License - -MIT License - see [LICENSE](LICENSE) file for details. - ---- - - -
- -**Made with ❤️ by [Flamehaven](https://flamehaven.space)** - -[⬆ Back to top](#fds-dev-flamehaven-doc-sanity-for-developers) - -
+ +## Deployment & Automation + +### Continuous Integration + +GitHub Actions automatically runs the test suite across Python 3.9–3.11 and builds release artifacts for every push and pull request targeting `main`. You can find the workflow definition in `.github/workflows/ci.yml`. + +### Automated PyPI Releases + +Tagging a commit with the `v*` pattern (for example, `v0.2.0`) triggers `.github/workflows/release.yml`, which builds the project with `python -m build` and publishes the result to PyPI using the `PYPI_API_TOKEN` secret. + +### Official Docker Image + +Ship the CLI as a container image by using the provided `Dockerfile`: + +```bash +docker build -t fds-dev . +docker run --rm fds-dev lint README.md +``` + +The image installs the package globally and exposes the `fds` entrypoint, so any CLI command can be run directly. + +## Contributing + +FDS-Dev is in early development. Contributions are welcome! + +See [CONTRIBUTING.md](CONTRIBUTING.md) for the detailed workflow, release checklist, and ASCII compatibility notes. + +We welcome contributions from the community! Here's how you can help: + +### How to Contribute + +1. **Report Issues**: Found a bug? [Open an issue](https://github.com/flamehaven01/FDS-Dev/issues) +2. **Suggest Features**: Have an idea? Share it in [Discussions](https://github.com/flamehaven01/FDS-Dev/discussions) +3. **Submit Pull Requests**: Fix bugs or add features +4. **Improve Documentation**: Help make our docs even better + +### Development Setup + +```bash +# Clone repository +git clone https://github.com/flamehaven01/FDS-Dev.git +cd FDS-Dev + +# Install in development mode +pip install -e . + +# Run tests +pytest tests/ -v + +# Run linter +flake8 fds_dev/ +``` + +### Code Quality Standards + +- Test coverage ≥ 90% +- All tests passing (100/105 expected) +- Follow PEP 8 style guide +- Add docstrings for public APIs +- Update documentation for new features + +--- + +## For Teams & Enterprises + +FDS-Dev scales from individual developers to enterprise deployments: + +- [#] **Self-Hosted** - Full data control, air-gapped operation +- [#] **CI/CD Integration** - GitHub Actions, GitLab CI, Jenkins ready +- [&] **Custom Deployments** - Docker, Kubernetes, monorepo support +- [W] **SLA & Support** - Enterprise agreements available + +[>] **Learn More**: See [`docs/ENTERPRISE.md`](docs/ENTERPRISE.md) for deployment architectures, security controls, and team workflows. + +--- + +## Support + +### Documentation + +- **[Translation Algorithm](docs/TRANSLATION_ALGORITHM.md)** - Complete pipeline explanation +- **[Architecture Guide](docs/ARCHITECTURE.md)** - System design documentation +- **[Troubleshooting](docs/TROUBLESHOOTING.md)** - Common issues and solutions + +### Get Help + +- **GitHub Issues**: [Report bugs or request features](https://github.com/flamehaven01/FDS-Dev/issues) +- **GitHub Discussions**: [Ask questions and share ideas](https://github.com/flamehaven01/FDS-Dev/discussions) +- **Email**: [info@flamehaven.space](mailto:info@flamehaven.space) + +### Community + +- **Website**: [flamehaven.space](https://flamehaven.space) +- **Repository**: [github.com/flamehaven01/FDS-Dev](https://github.com/flamehaven01/FDS-Dev) + +--- + +## License + +MIT License - see [LICENSE](LICENSE) file for details. + +--- + + +
+ +**Made with ❤️ by [Flamehaven](https://flamehaven.space)** + +[⬆ Back to top](#fds-dev-flamehaven-doc-sanity-for-developers) + +
diff --git a/README_KR.md b/README_KR.md index 0692cbf..bf021cc 100644 --- a/README_KR.md +++ b/README_KR.md @@ -1,118 +1,118 @@ -# FDS-Dev (Flamehaven Doc Sanity for Developers) - -
- -**[English](README.md) | [한국어](README_KR.md)** - -[![PyPI version](https://badge.fury.io/py/fds-dev.svg)](https://badge.fury.io/py/fds-dev) -[![Python Versions](https://img.shields.io/pypi/pyversions/fds-dev.svg)](https://pypi.org/project/fds-dev/) -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -[![CI/CD Pipeline](https://github.com/flamehaven01/FDS-Dev/actions/workflows/ci.yml/badge.svg)](https://github.com/flamehaven01/FDS-Dev/actions/workflows/ci.yml) -[![Test Coverage](https://img.shields.io/badge/coverage-95%25-brightgreen.svg)](https://github.com/flamehaven01/FDS-Dev) -[![Code Quality](https://img.shields.io/badge/quality-A+-blue.svg)](https://github.com/flamehaven01/FDS-Dev) -[![SIDRCE Certified](https://img.shields.io/badge/SIDRCE-0.896%20Certified-green.svg)](https://github.com/flamehaven01/FDS-Dev) - -**초고속 구조 인식 문서 린터, AI 기반 번역 기능 탑재** - -글로벌 개발자 커뮤니티를 위해 제작되었습니다. 모국어로 문서와 코드 주석을 작성하면, FDS-Dev가 즉시 프로덕션 수준의 영어로 번역합니다. - -[핵심 기능](#핵심-기능) • [빠른 시작](#빠른-시작) • [문서](docs/) • [기여하기](#기여하기) • [지원](#지원) - -
- ---- - -## 문제점 - -한국, 일본, 중국, 독일 또는 전 세계 어디에서나 온 재능 있는 개발자라면, 코드는 훌륭하지만 영어로 문서와 주석을 작성하는 것이 부담스러울 것입니다. 이는 작업 속도를 늦추고 글로벌 오픈소스 커뮤니티와 공유하는 데 장벽이 됩니다. - -`markdownlint`나 `Vale` 같은 기존 린터는 훌륭하지만, 영어 중심적이라 이 핵심 문제를 해결하지 못합니다. - -## 해결책: FDS-Dev - -FDS-Dev는 두 가지 도구를 하나로 결합했습니다: - -1. **초고속 린터:** 문서의 깔끔하고 전문적인 구조를 보장합니다. -2. **AI 기반 번역기:** 모국어로 작성된 문서와 주석을 자연스럽고 유창한 영어로 자동 번역합니다. - -**영어 걱정은 그만하고, 코드에만 집중하세요.** - -## FDS-Dev를 선택해야 하는 이유 - -### 코드 레벨 국제화라는 새로운 카테고리 - -기존 린터(markdownlint, Vale)는 영어 문서를 기준으로 형식을 검사할 뿐이고, 일반 번역기는 코드 블록을 깨거나 기술 용어를 훼손합니다. FDS-Dev는 문서, 코드 주석, 독스트링을 구조적으로 이해해 비영어권 개발자가 별도의 영어 작성 없이 글로벌 OSS에 참여하도록 돕습니다. - -### 실제로 해결하는 문제 - -- **언어 장벽 해소**: README, 아키텍처 문서, 인라인 주석, 독스트링을 영어로 변환하면서 코드 구조는 그대로 유지합니다. -- **문서 품질 보증**: 필수 섹션 존재 여부나 헤더 순서 같은 구조 규칙을 강제해 항상 일정한 품질을 유지합니다. - -### FDS-Dev만의 차별점 - -1. **AI 기반 코드 인지 번역** - - `CodeCommentParser`로 Markdown, Python 독스트링, 인라인 주석을 파싱해 번역 대상과 코드 블록을 구분합니다. - - `TechnicalTermDatabase`가 CamelCase, snake_case 등 식별자를 보존해 API 명칭이 흐트러지지 않습니다. - - `TranslationQualityOracle`가 번역 결과를 Omega(Ω) 점수로 평가해 기준 미달 번역은 재시도하거나 거부할 수 있습니다. - -2. **초고속 구조 인식 린터** - - `.fds_cache.json` 캐시와 병렬 실행으로 대규모 문서를 빠르게 검증합니다. - - “License 섹션 필수”, “Installation이 Usage보다 먼저” 같은 구조 규칙까지 검사합니다. - -3. **유연한 번역 프로바이더** - - 기본 py-googletrans는 설정 없이 바로 사용 가능. - - `.fdsrc.yaml`에서 DeepL, MyMemory, LibreTranslate 등으로 쉽게 전환해 품질/예산에 맞춘 환경을 구축할 수 있습니다. - -4. **커뮤니티 파급력** - - 비영어권 개발자가 더 쉽게 PR을 제출하고 국제 협업을 진행할 수 있어 오픈소스 생태계를 확장시킵니다. - - 적극적으로 개발이 진행 중이므로 Star, Issue, PR이 곧바로 로드맵에 반영됩니다. - -## 핵심 기능 - -- **구조 인식 린팅:** 단순한 스타일 검사를 넘어섭니다. 섹션 순서 강제, 특정 헤더 요구, 문서 전체 구조 검증. -- **링크 무결성 검사:** `broken-link-check` 규칙을 켜면 누락된 앵커, 존재하지 않는 파일, 접근 불가한 URL을 사전에 감지할 수 있습니다. -- **자동 번역:** 한국어, 중국어, 일본어 등의 Markdown 파일과 소스 코드 주석을 영어로 번역합니다. -- **간편한 설정:** 단일 `.fdsrc.yaml` 파일로 모든 것을 제어합니다. -- **속도 최적화:** 최대 성능을 위해 설계된 핵심 컴포넌트. - -## 빠른 시작 - -```bash -# 설치 -pip install --upgrade fds-dev - -# 설정 초기화 -fds init - -# 첫 린트 실행 -fds lint README.md -``` - -### [>] 10분 빠른 시작 가이드 - -**1. 문서 린트 검사** - 구조적 문제 확인 -```bash -fds lint README.ko.md -``` - -**2. 영어로 번역** - 글로벌 수준의 문서를 즉시 생성 -```bash -# README.ko.md -> README.md 번역 -fds translate README.ko.md --output README.md - -# 소스 코드 주석을 제자리에서 번역 -fds translate my_app/main.py --in-place -``` - -**3. 실행 가능한 예제 확인** -```bash -# 리포지토리를 클론하고 예제 실행 -git clone https://github.com/flamehaven01/FDS-Dev.git -cd FDS-Dev -pip install -e . -python examples/basic_usage.py -``` - +# FDS-Dev (Flamehaven Doc Sanity for Developers) + +
+ +**[English](README.md) | [한국어](README_KR.md)** + +[![PyPI version](https://badge.fury.io/py/fds-dev.svg)](https://badge.fury.io/py/fds-dev) +[![Python Versions](https://img.shields.io/pypi/pyversions/fds-dev.svg)](https://pypi.org/project/fds-dev/) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![CI/CD Pipeline](https://github.com/flamehaven01/FDS-Dev/actions/workflows/ci.yml/badge.svg)](https://github.com/flamehaven01/FDS-Dev/actions/workflows/ci.yml) +[![Test Coverage](https://img.shields.io/badge/coverage-95%25-brightgreen.svg)](https://github.com/flamehaven01/FDS-Dev) +[![Code Quality](https://img.shields.io/badge/quality-A+-blue.svg)](https://github.com/flamehaven01/FDS-Dev) +[![SIDRCE Certified](https://img.shields.io/badge/SIDRCE-0.896%20Certified-green.svg)](https://github.com/flamehaven01/FDS-Dev) + +**초고속 구조 인식 문서 린터, AI 기반 번역 기능 탑재** + +글로벌 개발자 커뮤니티를 위해 제작되었습니다. 모국어로 문서와 코드 주석을 작성하면, FDS-Dev가 즉시 프로덕션 수준의 영어로 번역합니다. + +[핵심 기능](#핵심-기능) • [빠른 시작](#빠른-시작) • [문서](docs/) • [기여하기](#기여하기) • [지원](#지원) + +
+ +--- + +## 문제점 + +한국, 일본, 중국, 독일 또는 전 세계 어디에서나 온 재능 있는 개발자라면, 코드는 훌륭하지만 영어로 문서와 주석을 작성하는 것이 부담스러울 것입니다. 이는 작업 속도를 늦추고 글로벌 오픈소스 커뮤니티와 공유하는 데 장벽이 됩니다. + +`markdownlint`나 `Vale` 같은 기존 린터는 훌륭하지만, 영어 중심적이라 이 핵심 문제를 해결하지 못합니다. + +## 해결책: FDS-Dev + +FDS-Dev는 두 가지 도구를 하나로 결합했습니다: + +1. **초고속 린터:** 문서의 깔끔하고 전문적인 구조를 보장합니다. +2. **AI 기반 번역기:** 모국어로 작성된 문서와 주석을 자연스럽고 유창한 영어로 자동 번역합니다. + +**영어 걱정은 그만하고, 코드에만 집중하세요.** + +## FDS-Dev를 선택해야 하는 이유 + +### 코드 레벨 국제화라는 새로운 카테고리 + +기존 린터(markdownlint, Vale)는 영어 문서를 기준으로 형식을 검사할 뿐이고, 일반 번역기는 코드 블록을 깨거나 기술 용어를 훼손합니다. FDS-Dev는 문서, 코드 주석, 독스트링을 구조적으로 이해해 비영어권 개발자가 별도의 영어 작성 없이 글로벌 OSS에 참여하도록 돕습니다. + +### 실제로 해결하는 문제 + +- **언어 장벽 해소**: README, 아키텍처 문서, 인라인 주석, 독스트링을 영어로 변환하면서 코드 구조는 그대로 유지합니다. +- **문서 품질 보증**: 필수 섹션 존재 여부나 헤더 순서 같은 구조 규칙을 강제해 항상 일정한 품질을 유지합니다. + +### FDS-Dev만의 차별점 + +1. **AI 기반 코드 인지 번역** + - `CodeCommentParser`로 Markdown, Python 독스트링, 인라인 주석을 파싱해 번역 대상과 코드 블록을 구분합니다. + - `TechnicalTermDatabase`가 CamelCase, snake_case 등 식별자를 보존해 API 명칭이 흐트러지지 않습니다. + - `TranslationQualityOracle`가 번역 결과를 Omega(Ω) 점수로 평가해 기준 미달 번역은 재시도하거나 거부할 수 있습니다. + +2. **초고속 구조 인식 린터** + - `.fds_cache.json` 캐시와 병렬 실행으로 대규모 문서를 빠르게 검증합니다. + - “License 섹션 필수”, “Installation이 Usage보다 먼저” 같은 구조 규칙까지 검사합니다. + +3. **유연한 번역 프로바이더** + - 기본 py-googletrans는 설정 없이 바로 사용 가능. + - `.fdsrc.yaml`에서 DeepL, MyMemory, LibreTranslate 등으로 쉽게 전환해 품질/예산에 맞춘 환경을 구축할 수 있습니다. + +4. **커뮤니티 파급력** + - 비영어권 개발자가 더 쉽게 PR을 제출하고 국제 협업을 진행할 수 있어 오픈소스 생태계를 확장시킵니다. + - 적극적으로 개발이 진행 중이므로 Star, Issue, PR이 곧바로 로드맵에 반영됩니다. + +## 핵심 기능 + +- **구조 인식 린팅:** 단순한 스타일 검사를 넘어섭니다. 섹션 순서 강제, 특정 헤더 요구, 문서 전체 구조 검증. +- **링크 무결성 검사:** `broken-link-check` 규칙을 켜면 누락된 앵커, 존재하지 않는 파일, 접근 불가한 URL을 사전에 감지할 수 있습니다. +- **자동 번역:** 한국어, 중국어, 일본어 등의 Markdown 파일과 소스 코드 주석을 영어로 번역합니다. +- **간편한 설정:** 단일 `.fdsrc.yaml` 파일로 모든 것을 제어합니다. +- **속도 최적화:** 최대 성능을 위해 설계된 핵심 컴포넌트. + +## 빠른 시작 + +```bash +# 설치 +pip install --upgrade fds-dev + +# 설정 초기화 +fds init + +# 첫 린트 실행 +fds lint README.md +``` + +### [>] 10분 빠른 시작 가이드 + +**1. 문서 린트 검사** - 구조적 문제 확인 +```bash +fds lint README.ko.md +``` + +**2. 영어로 번역** - 글로벌 수준의 문서를 즉시 생성 +```bash +# README.ko.md -> README.md 번역 +fds translate README.ko.md --output README.md + +# 소스 코드 주석을 제자리에서 번역 +fds translate my_app/main.py --in-place +``` + +**3. 실행 가능한 예제 확인** +```bash +# 리포지토리를 클론하고 예제 실행 +git clone https://github.com/flamehaven01/FDS-Dev.git +cd FDS-Dev +pip install -e . +python examples/basic_usage.py +``` + [+] **다음 단계**: 프로덕션 패턴은 [`examples/`](examples/)를, 팀 배포는 [`docs/ENTERPRISE.md`](docs/ENTERPRISE.md)를 참조하세요. ```bash @@ -120,160 +120,166 @@ python examples/basic_usage.py fds translate my_app/main.py --in-place ``` +### 설정 및 캐시 처리 +- `fds lint`/`fds translate`에 넘긴 경로를 시작점으로 `.fdsrc.yaml`을 탐색합니다. 각 문서 트리마다 별도 규칙을 둘 수 있으며, 작업 디렉터리를 옮길 필요가 없습니다. +- 린트 캐시(`.fds_cache.json`)는 대상 경로나 파일과 같은 위치에 저장되어 트리별로 스코프가 분리됩니다. +- `translate`는 자동 언어 감지 시 신뢰도( confidence )를 출력하고, 소스/타깃 언어가 같으면 안전하게 건너뜁니다. + ## CLI 명령어 - `fds lint `: `.fdsrc.yaml`에 정의된 구조 린팅 규칙(옵션으로 `broken-link-check` 포함)을 실행합니다. - `fds translate [--output OUTPUT | --in-place]`: Markdown/소스 파일을 영어로 번역하면서 코드 블록과 식별자를 유지합니다. - `fds lint --help` / `fds translate --help`: 사용 가능한 모든 옵션을 확인할 수 있습니다. - -링크 검사는 `.fdsrc.yaml` 설정만으로 제어되며, 규칙을 켜면 `fds lint` 결과에 깨진 링크도 일반 린트 오류처럼 보고됩니다. - -## 번역 제공자 - -FDS-Dev는 여러 번역 제공자를 지원합니다. `.fdsrc.yaml` 파일에서 선호하는 제공자를 설정할 수 있습니다. - -| 제공자 | 기본값? | API 키 | 비용 | 품질 | 안정성 | 권장 사용 사례 | -| :------------------------ | :-------------- | :------ | :-------------------- | :--------- | :------------- | :--------------------------------------- | -| **Google Translate (Free)** | **✅ (기본값)** | 없음 | 무료 | 높음 | **불안정**¹ | 개인 프로젝트, 빠른 테스트, 일반 문서 | -| **DeepL** | ❌ | **필수** | 제한된 무료 티어/유료 | **매우 높음** | 매우 높음 | 프로덕션, 상업용, 공식 문서 | -| **MyMemory** | ❌ | 없음 | 무료 | 보통 | 보통 | 간단한 스크립트, 임시 사용 | -| **LibreTranslate** | ❌ | 없음 | 무료 (자체 호스팅) | 보통² | **사용자 관리** | 사설 서버, 오프라인, 완전한 제어 | - -> ¹ 비공식 API를 사용하므로 예고 없이 작동이 중단될 수 있습니다. -> ² 품질은 사용자가 직접 호스팅하는 모델에 따라 달라집니다. - -기본값이 아닌 제공자를 사용하려면 `.fdsrc.yaml` 파일에서 설정하세요. API 키가 필요한 제공자의 경우 환경 변수 사용을 강력히 권장합니다. - -**DeepL 예제:** -```yaml -# .fdsrc.yaml + +링크 검사는 `.fdsrc.yaml` 설정만으로 제어되며, 규칙을 켜면 `fds lint` 결과에 깨진 링크도 일반 린트 오류처럼 보고됩니다. + +## 번역 제공자 + +FDS-Dev는 여러 번역 제공자를 지원합니다. `.fdsrc.yaml` 파일에서 선호하는 제공자를 설정할 수 있습니다. + +| 제공자 | 기본값? | API 키 | 비용 | 품질 | 안정성 | 권장 사용 사례 | +| :------------------------ | :-------------- | :------ | :-------------------- | :--------- | :------------- | :--------------------------------------- | +| **Google Translate (Free)** | **✅ (기본값)** | 없음 | 무료 | 높음 | **불안정**¹ | 개인 프로젝트, 빠른 테스트, 일반 문서 | +| **DeepL** | ❌ | **필수** | 제한된 무료 티어/유료 | **매우 높음** | 매우 높음 | 프로덕션, 상업용, 공식 문서 | +| **MyMemory** | ❌ | 없음 | 무료 | 보통 | 보통 | 간단한 스크립트, 임시 사용 | +| **LibreTranslate** | ❌ | 없음 | 무료 (자체 호스팅) | 보통² | **사용자 관리** | 사설 서버, 오프라인, 완전한 제어 | + +> ¹ 비공식 API를 사용하므로 예고 없이 작동이 중단될 수 있습니다. +> ² 품질은 사용자가 직접 호스팅하는 모델에 따라 달라집니다. + +기본값이 아닌 제공자를 사용하려면 `.fdsrc.yaml` 파일에서 설정하세요. API 키가 필요한 제공자의 경우 환경 변수 사용을 강력히 권장합니다. + +**DeepL 예제:** +```yaml +# .fdsrc.yaml translator: provider: 'deepl' providers: deepl: # FDS_DEEPL_API_KEY 환경 변수 사용을 권장합니다. api_key: null + timeout: 5 # 초 단위 기본 타임아웃 (서비스 지연 시 CLI 멈춤 방지) ``` - -## 배포 및 자동화 - -### 지속적 통합 (CI) - -GitHub Actions는 `main` 브랜치를 대상으로 하는 모든 푸시 및 풀 리퀘스트에 대해 Python 3.9–3.11에서 테스트 스위트를 자동으로 실행하고 릴리스 아티팩트를 빌드합니다. 워크플로우 정의는 `.github/workflows/ci.yml`에서 확인할 수 있습니다. - -### PyPI 자동 릴리스 - -`v*` 패턴으로 커밋에 태그를 지정하면(예: `v0.2.0`) `.github/workflows/release.yml`이 트리거되어 `python -m build`로 프로젝트를 빌드하고 `PYPI_API_TOKEN` 시크릿을 사용하여 PyPI에 게시합니다. - -### 공식 Docker 이미지 - -제공된 `Dockerfile`을 사용하여 CLI를 컨테이너 이미지로 배포할 수 있습니다: - -```bash -docker build -t fds-dev . -docker run --rm fds-dev lint README.md -``` - -이미지는 패키지를 전역으로 설치하고 `fds` 진입점을 노출하므로 모든 CLI 명령을 직접 실행할 수 있습니다. - -## 기여하기 - -FDS-Dev는 초기 개발 단계입니다. 기여를 환영합니다! - -전체 기여 체크리스트와 릴리스 워크플로는 [CONTRIBUTING.md](CONTRIBUTING.md)에서 확인하세요. - -커뮤니티의 기여를 환영합니다! 다음과 같은 방법으로 도움을 주실 수 있습니다: - -### 기여 방법 - -1. **이슈 제보**: 버그를 발견하셨나요? [이슈 등록](https://github.com/flamehaven01/FDS-Dev/issues) -2. **기능 제안**: 아이디어가 있으신가요? [토론](https://github.com/flamehaven01/FDS-Dev/discussions)에서 공유하세요 -3. **풀 리퀘스트 제출**: 버그 수정 또는 기능 추가 -4. **문서 개선**: 문서를 더욱 좋게 만들어주세요 - -### 개발 환경 설정 - -```bash -# 저장소 클론 -git clone https://github.com/flamehaven01/FDS-Dev.git -cd FDS-Dev - -# 개발 모드로 설치 -pip install -e . - -# 테스트 실행 -pytest tests/ -v - -# 린터 실행 -flake8 fds_dev/ -``` - -### 코드 품질 기준 - -- 테스트 커버리지 ≥ 90% -- 모든 테스트 통과 (100/105 예상) -- PEP 8 스타일 가이드 준수 -- 공개 API에 docstring 추가 -- 새 기능에 대한 문서 업데이트 - ---- - -## 팀 및 엔터프라이즈 - -FDS-Dev는 개인 개발자부터 엔터프라이즈 배포까지 확장 가능합니다: - -- [#] **자체 호스팅** - 완전한 데이터 제어, 에어갭 환경 지원 -- [#] **CI/CD 통합** - GitHub Actions, GitLab CI, Jenkins 즉시 사용 가능 -- [&] **커스텀 배포** - Docker, Kubernetes, 모노레포 지원 -- [W] **SLA 및 지원** - 엔터프라이즈 계약 가능 - -[>] **자세히 보기**: 배포 아키텍처, 보안 제어, 팀 워크플로우는 [`docs/ENTERPRISE.md`](docs/ENTERPRISE.md)를 참조하세요. - ---- - -## 지원 - -### 문서 - -- **[번역 알고리즘](docs/TRANSLATION_ALGORITHM.md)** - 완전한 파이프라인 설명 -- **[아키텍처 가이드](docs/ARCHITECTURE.md)** - 시스템 설계 문서 -- **[문제 해결](docs/TROUBLESHOOTING.md)** - 일반적인 문제와 해결책 - -### 도움 받기 - -- **GitHub Issues**: [버그 제보 또는 기능 요청](https://github.com/flamehaven01/FDS-Dev/issues) -- **GitHub Discussions**: [질문 및 아이디어 공유](https://github.com/flamehaven01/FDS-Dev/discussions) -- **Email**: [info@flamehaven.space](mailto:info@flamehaven.space) - -### 커뮤니티 - -- **웹사이트**: [flamehaven.space](https://flamehaven.space) -- **저장소**: [github.com/flamehaven01/FDS-Dev](https://github.com/flamehaven01/FDS-Dev) - ---- - -## 라이센스 - -MIT License - 자세한 내용은 [LICENSE](LICENSE) 파일을 참조하세요. - ---- - -## 감사의 말 - -FDS-Dev는 다음 기술로 구축되었습니다: -- **py-googletrans** - 무료 Google Translate API -- **DeepL API** - 고품질 번역 백엔드 -- **click** - 아름다운 CLI 프레임워크 -- **pytest** - 테스팅 프레임워크 - -모든 기여자와 오픈소스 커뮤니티에 특별한 감사를 드립니다! - ---- - -
- -**Flamehaven이 ❤️를 담아 제작했습니다 - [Flamehaven](https://flamehaven.space)** - -[⬆ 맨 위로](#fds-dev-flamehaven-doc-sanity-for-developers) - -
+ +## 배포 및 자동화 + +### 지속적 통합 (CI) + +GitHub Actions는 `main` 브랜치를 대상으로 하는 모든 푸시 및 풀 리퀘스트에 대해 Python 3.9–3.11에서 테스트 스위트를 자동으로 실행하고 릴리스 아티팩트를 빌드합니다. 워크플로우 정의는 `.github/workflows/ci.yml`에서 확인할 수 있습니다. + +### PyPI 자동 릴리스 + +`v*` 패턴으로 커밋에 태그를 지정하면(예: `v0.2.0`) `.github/workflows/release.yml`이 트리거되어 `python -m build`로 프로젝트를 빌드하고 `PYPI_API_TOKEN` 시크릿을 사용하여 PyPI에 게시합니다. + +### 공식 Docker 이미지 + +제공된 `Dockerfile`을 사용하여 CLI를 컨테이너 이미지로 배포할 수 있습니다: + +```bash +docker build -t fds-dev . +docker run --rm fds-dev lint README.md +``` + +이미지는 패키지를 전역으로 설치하고 `fds` 진입점을 노출하므로 모든 CLI 명령을 직접 실행할 수 있습니다. + +## 기여하기 + +FDS-Dev는 초기 개발 단계입니다. 기여를 환영합니다! + +전체 기여 체크리스트와 릴리스 워크플로는 [CONTRIBUTING.md](CONTRIBUTING.md)에서 확인하세요. + +커뮤니티의 기여를 환영합니다! 다음과 같은 방법으로 도움을 주실 수 있습니다: + +### 기여 방법 + +1. **이슈 제보**: 버그를 발견하셨나요? [이슈 등록](https://github.com/flamehaven01/FDS-Dev/issues) +2. **기능 제안**: 아이디어가 있으신가요? [토론](https://github.com/flamehaven01/FDS-Dev/discussions)에서 공유하세요 +3. **풀 리퀘스트 제출**: 버그 수정 또는 기능 추가 +4. **문서 개선**: 문서를 더욱 좋게 만들어주세요 + +### 개발 환경 설정 + +```bash +# 저장소 클론 +git clone https://github.com/flamehaven01/FDS-Dev.git +cd FDS-Dev + +# 개발 모드로 설치 +pip install -e . + +# 테스트 실행 +pytest tests/ -v + +# 린터 실행 +flake8 fds_dev/ +``` + +### 코드 품질 기준 + +- 테스트 커버리지 ≥ 90% +- 모든 테스트 통과 (100/105 예상) +- PEP 8 스타일 가이드 준수 +- 공개 API에 docstring 추가 +- 새 기능에 대한 문서 업데이트 + +--- + +## 팀 및 엔터프라이즈 + +FDS-Dev는 개인 개발자부터 엔터프라이즈 배포까지 확장 가능합니다: + +- [#] **자체 호스팅** - 완전한 데이터 제어, 에어갭 환경 지원 +- [#] **CI/CD 통합** - GitHub Actions, GitLab CI, Jenkins 즉시 사용 가능 +- [&] **커스텀 배포** - Docker, Kubernetes, 모노레포 지원 +- [W] **SLA 및 지원** - 엔터프라이즈 계약 가능 + +[>] **자세히 보기**: 배포 아키텍처, 보안 제어, 팀 워크플로우는 [`docs/ENTERPRISE.md`](docs/ENTERPRISE.md)를 참조하세요. + +--- + +## 지원 + +### 문서 + +- **[번역 알고리즘](docs/TRANSLATION_ALGORITHM.md)** - 완전한 파이프라인 설명 +- **[아키텍처 가이드](docs/ARCHITECTURE.md)** - 시스템 설계 문서 +- **[문제 해결](docs/TROUBLESHOOTING.md)** - 일반적인 문제와 해결책 + +### 도움 받기 + +- **GitHub Issues**: [버그 제보 또는 기능 요청](https://github.com/flamehaven01/FDS-Dev/issues) +- **GitHub Discussions**: [질문 및 아이디어 공유](https://github.com/flamehaven01/FDS-Dev/discussions) +- **Email**: [info@flamehaven.space](mailto:info@flamehaven.space) + +### 커뮤니티 + +- **웹사이트**: [flamehaven.space](https://flamehaven.space) +- **저장소**: [github.com/flamehaven01/FDS-Dev](https://github.com/flamehaven01/FDS-Dev) + +--- + +## 라이센스 + +MIT License - 자세한 내용은 [LICENSE](LICENSE) 파일을 참조하세요. + +--- + +## 감사의 말 + +FDS-Dev는 다음 기술로 구축되었습니다: +- **py-googletrans** - 무료 Google Translate API +- **DeepL API** - 고품질 번역 백엔드 +- **click** - 아름다운 CLI 프레임워크 +- **pytest** - 테스팅 프레임워크 + +모든 기여자와 오픈소스 커뮤니티에 특별한 감사를 드립니다! + +--- + +
+ +**Flamehaven이 ❤️를 담아 제작했습니다 - [Flamehaven](https://flamehaven.space)** + +[⬆ 맨 위로](#fds-dev-flamehaven-doc-sanity-for-developers) + +
diff --git a/fds_dev/__init__.py b/fds_dev/__init__.py new file mode 100644 index 0000000..059142d --- /dev/null +++ b/fds_dev/__init__.py @@ -0,0 +1 @@ +"""FDS-Dev package init.""" diff --git a/fds_dev/config.py b/fds_dev/config.py index 8b653e6..c989ae1 100644 --- a/fds_dev/config.py +++ b/fds_dev/config.py @@ -1,68 +1,74 @@ -import os -import yaml -from typing import Dict, Any - -DEFAULT_CONFIG = { - 'language': { - 'source': 'auto', - 'target': 'en' - }, - 'rules': {}, - 'translator': { - 'provider': 'echo' - } -} - -def find_config_file(path: str = '.') -> str: - """ - Finds the .fdsrc.yaml file in the given path or its parent directories. - """ - current_dir = os.path.abspath(path) - while True: - config_path = os.path.join(current_dir, '.fdsrc.yaml') - if os.path.exists(config_path): - return config_path - - parent_dir = os.path.dirname(current_dir) - if parent_dir == current_dir: # Reached the root directory - return None - current_dir = parent_dir - -def load_config() -> Dict[str, Any]: - """ - Loads the configuration from .fdsrc.yaml. - Starts searching from the current working directory upwards. - Returns default config if not found. - """ - config_path = find_config_file() - if config_path: - try: - with open(config_path, 'r', encoding='utf-8') as f: - user_config = yaml.safe_load(f) - # Simple merge with default config - config = DEFAULT_CONFIG.copy() - config.update(user_config) - return config - except Exception as e: - print(f"Error loading config file '{config_path}': {e}") - return DEFAULT_CONFIG - - return DEFAULT_CONFIG - -if __name__ == '__main__': - # To test, you would need a .fdsrc.yaml file in this directory or a parent. - # We can create a dummy one for the test. - dummy_yaml = """ -language: - source: 'ko' -rules: - require-section-license: on -""" - with open('.fdsrc.yaml', 'w', encoding='utf-8') as f: - f.write(dummy_yaml) - - config = load_config() - print("Loaded configuration:") - print(yaml.dump(config, default_flow_style=False)) - - os.remove('.fdsrc.yaml') # Clean up the dummy file +"""FDS-Dev module.""" + +import os +import yaml +from typing import Dict, Any + +DEFAULT_CONFIG = { + 'language': { + 'source': 'auto', + 'target': 'en' + }, + 'rules': {}, + 'translator': { + 'provider': 'echo' + } +} + +def find_config_file(path: str = '.') -> str: + """ + Finds the .fdsrc.yaml file in the given path or its parent directories. + Starts from the directory of the provided path (file or folder). + """ + start_dir = path + if not os.path.isdir(path): + start_dir = os.path.dirname(os.path.abspath(path)) or os.path.abspath('.') + current_dir = os.path.abspath(start_dir) + + while True: + config_path = os.path.join(current_dir, '.fdsrc.yaml') + if os.path.exists(config_path): + return config_path + + parent_dir = os.path.dirname(current_dir) + if parent_dir == current_dir: # Reached the root directory + return None + current_dir = parent_dir + +def load_config(start_path: str = '.') -> Dict[str, Any]: + """ + Loads the configuration from .fdsrc.yaml. + Starts searching from the provided path (file or directory) upwards. + Returns default config if not found. + """ + config_path = find_config_file(start_path) + if config_path: + try: + with open(config_path, 'r', encoding='utf-8') as f: + user_config = yaml.safe_load(f) or {} + config = DEFAULT_CONFIG.copy() + config.update(user_config) + return config + except Exception as e: + print(f"Error loading config file '{config_path}': {e}") + return DEFAULT_CONFIG + + return DEFAULT_CONFIG + +if __name__ == '__main__': + # To test, you would need a .fdsrc.yaml file in this directory or a parent. + # We can create a dummy one for the test. + dummy_yaml = """ +language: + source: 'ko' +rules: + require-section-license: on +""" + with open('.fdsrc.yaml', 'w', encoding='utf-8') as f: + f.write(dummy_yaml) + + config = load_config() + print("Loaded configuration:") + print(yaml.dump(config, default_flow_style=False)) + + os.remove('.fdsrc.yaml') # Clean up the dummy file diff --git a/fds_dev/i18n/__init__.py b/fds_dev/i18n/__init__.py index d5a148d..aa623c3 100644 --- a/fds_dev/i18n/__init__.py +++ b/fds_dev/i18n/__init__.py @@ -1,24 +1,26 @@ -from .code_comment_parser import CodeCommentParser, CommentNode, ParsedCodeFile -from .language import LanguageDetectionResult, LanguageDetector -from .translation import TechnicalTermDatabase, TranslationEngine, TranslationResult -from .metacognition import ( - ConsistencyChecker, - ContextAnalyzer, - TranslationQualityOracle, - TranslationQualityTensor, -) - -__all__ = [ - "CodeCommentParser", - "CommentNode", - "ParsedCodeFile", - "LanguageDetector", - "LanguageDetectionResult", - "TranslationEngine", - "TranslationResult", - "TechnicalTermDatabase", - "TranslationQualityOracle", - "TranslationQualityTensor", - "ConsistencyChecker", - "ContextAnalyzer", -] +"""FDS-Dev module.""" + +from .code_comment_parser import CodeCommentParser, CommentNode, ParsedCodeFile +from .language import LanguageDetectionResult, LanguageDetector +from .translation import TechnicalTermDatabase, TranslationEngine, TranslationResult +from .metacognition import ( + ConsistencyChecker, + ContextAnalyzer, + TranslationQualityOracle, + TranslationQualityTensor, +) + +__all__ = [ + "CodeCommentParser", + "CommentNode", + "ParsedCodeFile", + "LanguageDetector", + "LanguageDetectionResult", + "TranslationEngine", + "TranslationResult", + "TechnicalTermDatabase", + "TranslationQualityOracle", + "TranslationQualityTensor", + "ConsistencyChecker", + "ContextAnalyzer", +] diff --git a/fds_dev/i18n/code_comment_parser.py b/fds_dev/i18n/code_comment_parser.py index e6ed9ad..34d51e7 100644 --- a/fds_dev/i18n/code_comment_parser.py +++ b/fds_dev/i18n/code_comment_parser.py @@ -1,269 +1,270 @@ -from __future__ import annotations - -import ast -import codecs -import os -import tokenize -from dataclasses import dataclass, field -from io import StringIO -from pathlib import Path -from typing import Dict, Iterable, List, Tuple - - -@dataclass -class CommentNode: - node_type: str - content: str - line_number: int - column_offset: int - context: str - raw_fragment: str - start_offset: int - end_offset: int - translated: str = "" - - -@dataclass -class ParsedCodeFile: - file_path: str - encoding: str - original_lines: List[str] - comments: List[CommentNode] = field(default_factory=list) - docstrings: List[CommentNode] = field(default_factory=list) - - def __post_init__(self) -> None: - self.original_text = "".join(self.original_lines) - - def get_all_translatable(self) -> List[CommentNode]: - return self.comments + self.docstrings - - def total_nodes(self) -> int: - return len(self.get_all_translatable()) - - def reconstruct_file(self) -> str: - updated = self.original_text - nodes = [node for node in self.get_all_translatable() if node.translated] - nodes.sort(key=lambda node: node.start_offset, reverse=True) - for node in nodes: - replacement = self._render_fragment(node) - if node.start_offset < 0 or node.end_offset > len(updated): - continue - updated = updated[: node.start_offset] + replacement + updated[node.end_offset :] - return updated - - @staticmethod - def _render_fragment(node: CommentNode) -> str: - if not node.translated: - return node.raw_fragment - if node.content: - return node.raw_fragment.replace(node.content, node.translated, 1) - return node.translated - - -class _DocstringCollector(ast.NodeVisitor): - def __init__(self, text: str, line_offsets: List[int]) -> None: - self.text = text - self.line_offsets = line_offsets - self.docstrings: List[CommentNode] = [] - - def visit_Module(self, node: ast.Module) -> None: - self._capture_docstring(node, "module") - self.generic_visit(node) - - def visit_FunctionDef(self, node: ast.FunctionDef) -> None: - self._capture_docstring(node, "function") - self.generic_visit(node) - - def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None: - self._capture_docstring(node, "function") - self.generic_visit(node) - - def visit_ClassDef(self, node: ast.ClassDef) -> None: - self._capture_docstring(node, "class") - self.generic_visit(node) - - def _capture_docstring(self, node: ast.AST, context: str) -> None: - body = getattr(node, "body", None) - if not body: - return - first = body[0] - if not isinstance(first, ast.Expr): - return - value = first.value - if not isinstance(value, ast.Constant) or not isinstance(value.value, str): - return - lineno = getattr(first, "lineno", 1) - col_offset = getattr(first, "col_offset", 0) - end_lineno = getattr(value, "end_lineno", lineno) - end_col = getattr(value, "end_col_offset", col_offset) - start_idx = self._safe_offset(lineno, col_offset) - end_idx = self._safe_offset(end_lineno, end_col) - fragment = self.text[start_idx:end_idx] - node_record = CommentNode( - node_type="docstring", - content=value.value, - line_number=lineno, - column_offset=col_offset, - context=context, - raw_fragment=fragment, - start_offset=start_idx, - end_offset=end_idx, - ) - self.docstrings.append(node_record) - - def _safe_offset(self, lineno: int, col: int) -> int: - if lineno - 1 >= len(self.line_offsets): - return len(self.text) - return self.line_offsets[lineno - 1] + col - - -class CodeCommentParser: - MARKDOWN_EXT = {".md", ".markdown"} - PYTHON_EXT = {".py"} - - def parse_file(self, path: str) -> ParsedCodeFile: - target = Path(path) - if not target.exists(): - raise FileNotFoundError(path) - raw = target.read_bytes() - encoding = "utf-8-sig" if raw.startswith(codecs.BOM_UTF8) else "utf-8" - text = raw.decode(encoding, errors="ignore") - lines = text.splitlines(keepends=True) - if not lines: - lines = [""] - line_offsets = self._line_offsets(lines) - ext = target.suffix.lower() - if ext in self.PYTHON_EXT: - comments = self._parse_python_comments(text, lines, line_offsets) - docstrings = self._parse_docstrings(text, line_offsets) - elif ext in self.MARKDOWN_EXT: - comments = [] - docstrings = self._parse_markdown_paragraphs(lines, line_offsets, text) - else: - comments = [] - docstrings = [] - return ParsedCodeFile( - file_path=str(target), - encoding=encoding, - original_lines=lines, - comments=comments, - docstrings=docstrings, - ) - - def parse_directory(self, directory: str, recursive: bool = True) -> Dict[str, ParsedCodeFile]: - root = Path(directory) - if not root.is_dir(): - raise NotADirectoryError(directory) - results: Dict[str, ParsedCodeFile] = {} - walker: Iterable[Tuple[str, List[str], List[str]]] = os.walk(root) - for current, dirs, files in walker: - dirs[:] = [d for d in dirs if d != "__pycache__"] - for name in files: - file_path = Path(current) / name - if file_path.suffix.lower() not in (self.PYTHON_EXT | self.MARKDOWN_EXT): - continue - try: - results[str(file_path)] = self.parse_file(str(file_path)) - except Exception: - continue - if not recursive: - break - return results - - def reconstruct_file(self, parsed: ParsedCodeFile) -> str: - return parsed.reconstruct_file() - - def _parse_python_comments( - self, text: str, lines: List[str], line_offsets: List[int] - ) -> List[CommentNode]: - nodes: List[CommentNode] = [] - reader = StringIO(text).readline - try: - token_stream = tokenize.generate_tokens(reader) - for token in token_stream: - if token.type != tokenize.COMMENT: - continue - (line_no, column) = token.start - if line_no - 1 >= len(lines): - continue - raw_line = lines[line_no - 1].rstrip("\n") - start_offset = line_offsets[line_no - 1] + column - end_offset = line_offsets[line_no - 1] + len(raw_line) - comment_body = token.string.lstrip("#").strip() - node = CommentNode( - node_type="comment", - content=comment_body, - line_number=line_no, - column_offset=column, - context="inline comment", - raw_fragment=raw_line[column:], - start_offset=start_offset, - end_offset=end_offset, - ) - nodes.append(node) - except tokenize.TokenError: - return nodes - return nodes - - def _parse_docstrings(self, text: str, line_offsets: List[int]) -> List[CommentNode]: - try: - tree = ast.parse(text) - except SyntaxError: - return [] - collector = _DocstringCollector(text, line_offsets) - collector.visit(tree) - return collector.docstrings - - def _parse_markdown_paragraphs( - self, lines: List[str], line_offsets: List[int], text: str - ) -> List[CommentNode]: - nodes: List[CommentNode] = [] - paragraph: List[str] = [] - start_idx = 0 - for idx, line in enumerate(lines): - if line.strip(): - if not paragraph: - start_idx = idx - paragraph.append(line) - else: - if paragraph: - nodes.append( - self._mk_markdown_node(paragraph, start_idx, idx - 1, line_offsets, text) - ) - paragraph = [] - if paragraph: - nodes.append(self._mk_markdown_node(paragraph, start_idx, len(lines) - 1, line_offsets, text)) - return nodes - - def _mk_markdown_node( - self, - block: List[str], - start_idx: int, - end_idx: int, - line_offsets: List[int], - text: str, - ) -> CommentNode: - if end_idx < start_idx: - end_idx = start_idx - start_offset = line_offsets[start_idx] - end_offset = line_offsets[end_idx] + len(block[-1]) - fragment = text[start_offset:end_offset] - return CommentNode( - node_type="markdown", - content="".join(block).strip(), - line_number=start_idx + 1, - column_offset=0, - context="markdown paragraph", - raw_fragment=fragment, - start_offset=start_offset, - end_offset=end_offset, - ) - - @staticmethod - def _line_offsets(lines: List[str]) -> List[int]: - offsets: List[int] = [] - cursor = 0 - for line in lines: - offsets.append(cursor) - cursor += len(line) - return offsets +"""FDS-Dev module.""" + +from __future__ import annotations + +import ast +import codecs +import os +import tokenize +from dataclasses import dataclass, field +from io import StringIO +from pathlib import Path +from typing import Dict, Iterable, List, Tuple + + +@dataclass +class CommentNode: + node_type: str + content: str + line_number: int + column_offset: int + context: str + raw_fragment: str + start_offset: int + end_offset: int + translated: str = "" + + +@dataclass +class ParsedCodeFile: + file_path: str + encoding: str + original_lines: List[str] + comments: List[CommentNode] = field(default_factory=list) + docstrings: List[CommentNode] = field(default_factory=list) + + def __post_init__(self) -> None: + self.original_text = "".join(self.original_lines) + + def get_all_translatable(self) -> List[CommentNode]: + return self.comments + self.docstrings + + def total_nodes(self) -> int: + return len(self.get_all_translatable()) + + def reconstruct_file(self) -> str: + updated = self.original_text + nodes = [node for node in self.get_all_translatable() if node.translated] + nodes.sort(key=lambda node: node.start_offset, reverse=True) + for node in nodes: + replacement = self._render_fragment(node) + if node.start_offset < 0 or node.end_offset > len(updated): + continue + updated = updated[: node.start_offset] + replacement + updated[node.end_offset :] + return updated + + @staticmethod + def _render_fragment(node: CommentNode) -> str: + if not node.translated: + return node.raw_fragment + if node.content: + return node.raw_fragment.replace(node.content, node.translated, 1) + return node.translated + + +class _DocstringCollector(ast.NodeVisitor): + def __init__(self, text: str, line_offsets: List[int]) -> None: + self.text = text + self.line_offsets = line_offsets + self.docstrings: List[CommentNode] = [] + + def visit_Module(self, node: ast.Module) -> None: + self._capture_docstring(node, "module") + self.generic_visit(node) + + def visit_FunctionDef(self, node: ast.FunctionDef) -> None: + self._capture_docstring(node, "function") + self.generic_visit(node) + + def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None: + self._capture_docstring(node, "function") + self.generic_visit(node) + + def visit_ClassDef(self, node: ast.ClassDef) -> None: + self._capture_docstring(node, "class") + self.generic_visit(node) + + def _capture_docstring(self, node: ast.AST, context: str) -> None: + body = getattr(node, "body", None) + if not body: + return + first = body[0] + if not isinstance(first, ast.Expr): + return + value = first.value + if not isinstance(value, ast.Constant) or not isinstance(value.value, str): + return + lineno = getattr(first, "lineno", 1) + col_offset = getattr(first, "col_offset", 0) + end_lineno = getattr(value, "end_lineno", lineno) + end_col = getattr(value, "end_col_offset", col_offset) + start_idx = self._safe_offset(lineno, col_offset) + end_idx = self._safe_offset(end_lineno, end_col) + fragment = self.text[start_idx:end_idx] + node_record = CommentNode( + node_type="docstring", + content=value.value, + line_number=lineno, + column_offset=col_offset, + context=context, + raw_fragment=fragment, + start_offset=start_idx, + end_offset=end_idx, + ) + self.docstrings.append(node_record) + + def _safe_offset(self, lineno: int, col: int) -> int: + if lineno - 1 >= len(self.line_offsets): + return len(self.text) + return self.line_offsets[lineno - 1] + col + + +class CodeCommentParser: + MARKDOWN_EXT = {".md", ".markdown"} + PYTHON_EXT = {".py"} + + def parse_file(self, path: str) -> ParsedCodeFile: + target = Path(path) + if not target.exists(): + raise FileNotFoundError(path) + raw = target.read_bytes() + encoding = "utf-8-sig" if raw.startswith(codecs.BOM_UTF8) else "utf-8" + text = raw.decode(encoding, errors="ignore") + lines = text.splitlines(keepends=True) + if not lines: + lines = [""] + line_offsets = self._line_offsets(lines) + ext = target.suffix.lower() + if ext in self.PYTHON_EXT: + comments = self._parse_python_comments(text, lines, line_offsets) + docstrings = self._parse_docstrings(text, line_offsets) + elif ext in self.MARKDOWN_EXT: + comments = [] + docstrings = self._parse_markdown_paragraphs(lines, line_offsets, text) + else: + comments = [] + docstrings = [] + return ParsedCodeFile( + file_path=str(target), + encoding=encoding, + original_lines=lines, + comments=comments, + docstrings=docstrings, + ) + + def parse_directory(self, directory: str, recursive: bool = True) -> Dict[str, ParsedCodeFile]: + root = Path(directory) + if not root.is_dir(): + raise NotADirectoryError(directory) + results: Dict[str, ParsedCodeFile] = {} + walker: Iterable[Tuple[str, List[str], List[str]]] = os.walk(root) + for current, dirs, files in walker: + dirs[:] = [d for d in dirs if d != "__pycache__"] + for name in files: + file_path = Path(current) / name + if file_path.suffix.lower() not in (self.PYTHON_EXT | self.MARKDOWN_EXT): + continue + try: + results[str(file_path)] = self.parse_file(str(file_path)) + except Exception: + continue + if not recursive: + break + return results + + def reconstruct_file(self, parsed: ParsedCodeFile) -> str: + return parsed.reconstruct_file() + + def _parse_python_comments( + self, text: str, lines: List[str], line_offsets: List[int] + ) -> List[CommentNode]: + nodes: List[CommentNode] = [] + reader = StringIO(text).readline + try: + token_stream = tokenize.generate_tokens(reader) + for token in token_stream: + if token.type != tokenize.COMMENT: + continue + (line_no, column) = token.start + if line_no - 1 >= len(lines): + continue + raw_line = lines[line_no - 1].rstrip("\n") + start_offset = line_offsets[line_no - 1] + column + end_offset = line_offsets[line_no - 1] + len(raw_line) + comment_body = token.string.lstrip("#").strip() + node = CommentNode( + node_type="comment", + content=comment_body, + line_number=line_no, + column_offset=column, + context="inline comment", + raw_fragment=raw_line[column:], + start_offset=start_offset, + end_offset=end_offset, + ) + nodes.append(node) + except tokenize.TokenError: + return nodes + return nodes + + def _parse_docstrings(self, text: str, line_offsets: List[int]) -> List[CommentNode]: + try: + tree = ast.parse(text) + except SyntaxError: + return [] + collector = _DocstringCollector(text, line_offsets) + collector.visit(tree) + return collector.docstrings + + def _parse_markdown_paragraphs( + self, lines: List[str], line_offsets: List[int], text: str + ) -> List[CommentNode]: + nodes: List[CommentNode] = [] + paragraph: List[str] = [] + start_idx = 0 + for idx, line in enumerate(lines): + if line.strip(): + if not paragraph: + start_idx = idx + paragraph.append(line) + else: + if paragraph: + nodes.append( + self._mk_markdown_node(paragraph, start_idx, idx - 1, line_offsets, text) + ) + paragraph = [] + if paragraph: + nodes.append(self._mk_markdown_node(paragraph, start_idx, len(lines) - 1, line_offsets, text)) + return nodes + + def _mk_markdown_node( + self, + block: List[str], + start_idx: int, + end_idx: int, + line_offsets: List[int], + text: str, + ) -> CommentNode: + end_idx = max(end_idx, start_idx) + start_offset = line_offsets[start_idx] + end_offset = line_offsets[end_idx] + len(block[-1]) + fragment = text[start_offset:end_offset] + return CommentNode( + node_type="markdown", + content="".join(block).strip(), + line_number=start_idx + 1, + column_offset=0, + context="markdown paragraph", + raw_fragment=fragment, + start_offset=start_offset, + end_offset=end_offset, + ) + + @staticmethod + def _line_offsets(lines: List[str]) -> List[int]: + offsets: List[int] = [] + cursor = 0 + for line in lines: + offsets.append(cursor) + cursor += len(line) + return offsets diff --git a/fds_dev/i18n/language.py b/fds_dev/i18n/language.py index 2412283..121eafa 100644 --- a/fds_dev/i18n/language.py +++ b/fds_dev/i18n/language.py @@ -1,81 +1,83 @@ -from __future__ import annotations - -import re -from dataclasses import dataclass, field -from typing import Dict, List, Tuple - - -@dataclass -class LanguageDetectionResult: - language: str - script: str - confidence: float - samples: List[str] = field(default_factory=list) - - -class LanguageDetector: - URL_PATTERN = re.compile(r"https?://\S+") - CODE_PATTERN = re.compile(r"\b[A-Za-z_][A-Za-z0-9_]*\(\)") - CONSTANT_PATTERN = re.compile(r"\b[A-Z0-9_]{2,}\b") - - def detect(self, text: str) -> LanguageDetectionResult: - cleaned = self._clean_text(text) - if not cleaned: - return LanguageDetectionResult(language="en", script="latin", confidence=0.0, samples=[]) - script, script_confidence = self._detect_script(cleaned) - language, language_confidence = self._detect_language(cleaned, script) - samples = self._extract_samples(cleaned, language) - confidence = max(script_confidence, language_confidence) - return LanguageDetectionResult(language=language, script=script, confidence=confidence, samples=samples) - - def detect_batch(self, texts: List[str]) -> Dict[str, LanguageDetectionResult]: - return {text: self.detect(text) for text in texts} - - def is_english(self, text: str, threshold: float = 0.7) -> bool: - cleaned = self._clean_text(text) - letters = [char for char in cleaned if char.isalpha()] - if not letters: - return False - latin_ratio = sum(1 for char in letters if char.isascii()) / len(letters) - return latin_ratio >= threshold - - def _clean_text(self, text: str) -> str: - cleaned = self.URL_PATTERN.sub("", text) - cleaned = self.CODE_PATTERN.sub("", cleaned) - cleaned = self.CONSTANT_PATTERN.sub("", cleaned) - return cleaned.strip() - - def _detect_script(self, text: str) -> Tuple[str, float]: - if self._contains_range(text, 0xAC00, 0xD7AF): - return "hangul", 0.92 - if self._contains_range(text, 0x3040, 0x309F): - return "hiragana", 0.85 - if self._contains_range(text, 0x30A0, 0x30FF): - return "katakana", 0.82 - if self._contains_range(text, 0x4E00, 0x9FFF): - return "hanzi", 0.80 - ascii_letters = [c for c in text if c.isalpha() and c.isascii()] - if ascii_letters: - return "latin", 0.65 - return "latin", 0.45 - - def _detect_language(self, text: str, script: str) -> Tuple[str, float]: - if script == "hangul": - return "ko", 0.9 - if script in {"hiragana", "katakana"}: - return "ja", 0.8 - if script == "hanzi": - return "zh", 0.82 - if script == "latin": - if self.is_english(text, threshold=0.5): - return "en", 0.7 - return "en", 0.5 - return "en", 0.4 - - def _extract_samples(self, text: str, language: str) -> List[str]: - sentences = re.split(r"[.!?]\s*", text) - samples = [sentence.strip() for sentence in sentences if len(sentence.strip()) > 10] - return samples[:5] - - def _contains_range(self, text: str, start: int, end: int) -> bool: - return any(start <= ord(char) <= end for char in text) +"""FDS-Dev module.""" + +from __future__ import annotations + +import re +from dataclasses import dataclass, field +from typing import Dict, List, Tuple + + +@dataclass +class LanguageDetectionResult: + language: str + script: str + confidence: float + samples: List[str] = field(default_factory=list) + + +class LanguageDetector: + URL_PATTERN = re.compile(r"https?://\S+") + CODE_PATTERN = re.compile(r"\b[A-Za-z_][A-Za-z0-9_]*\(\)") + CONSTANT_PATTERN = re.compile(r"\b[A-Z0-9_]{2,}\b") + + def detect(self, text: str) -> LanguageDetectionResult: + cleaned = self._clean_text(text) + if not cleaned: + return LanguageDetectionResult(language="en", script="latin", confidence=0.0, samples=[]) + script, script_confidence = self._detect_script(cleaned) + language, language_confidence = self._detect_language(cleaned, script) + samples = self._extract_samples(cleaned, language) + confidence = max(script_confidence, language_confidence) + return LanguageDetectionResult(language=language, script=script, confidence=confidence, samples=samples) + + def detect_batch(self, texts: List[str]) -> Dict[str, LanguageDetectionResult]: + return {text: self.detect(text) for text in texts} + + def is_english(self, text: str, threshold: float = 0.7) -> bool: + cleaned = self._clean_text(text) + letters = [char for char in cleaned if char.isalpha()] + if not letters: + return False + latin_ratio = sum(1 for char in letters if char.isascii()) / len(letters) + return latin_ratio >= threshold + + def _clean_text(self, text: str) -> str: + cleaned = self.URL_PATTERN.sub("", text) + cleaned = self.CODE_PATTERN.sub("", cleaned) + cleaned = self.CONSTANT_PATTERN.sub("", cleaned) + return cleaned.strip() + + def _detect_script(self, text: str) -> Tuple[str, float]: + if self._contains_range(text, 0xAC00, 0xD7AF): + return "hangul", 0.92 + if self._contains_range(text, 0x3040, 0x309F): + return "hiragana", 0.85 + if self._contains_range(text, 0x30A0, 0x30FF): + return "katakana", 0.82 + if self._contains_range(text, 0x4E00, 0x9FFF): + return "hanzi", 0.80 + ascii_letters = [c for c in text if c.isalpha() and c.isascii()] + if ascii_letters: + return "latin", 0.65 + return "latin", 0.45 + + def _detect_language(self, text: str, script: str) -> Tuple[str, float]: + if script == "hangul": + return "ko", 0.9 + if script in {"hiragana", "katakana"}: + return "ja", 0.8 + if script == "hanzi": + return "zh", 0.82 + if script == "latin": + if self.is_english(text, threshold=0.5): + return "en", 0.7 + return "en", 0.5 + return "en", 0.4 + + def _extract_samples(self, text: str, language: str) -> List[str]: + sentences = re.split(r"[.!?]\s*", text) + samples = [sentence.strip() for sentence in sentences if len(sentence.strip()) > 10] + return samples[:5] + + def _contains_range(self, text: str, start: int, end: int) -> bool: + return any(start <= ord(char) <= end for char in text) diff --git a/fds_dev/i18n/metacognition.py b/fds_dev/i18n/metacognition.py index e7fbb09..19853ba 100644 --- a/fds_dev/i18n/metacognition.py +++ b/fds_dev/i18n/metacognition.py @@ -1,181 +1,183 @@ -from __future__ import annotations - -import re -from dataclasses import dataclass, field -from typing import Dict, List, Optional, Set, Tuple - - -@dataclass -class TranslationQualityTensor: - semantic_fidelity: float - technical_accuracy: float - fluency: float - consistency: float - context_awareness: float - - def compute_omega(self) -> float: - weights = [0.30, 0.25, 0.20, 0.15, 0.10] - values = [ - self.semantic_fidelity, - self.technical_accuracy, - self.fluency, - self.consistency, - self.context_awareness, - ] - return sum(weight * value for weight, value in zip(weights, values)) - - -@dataclass -class EvaluationResult: - omega_score: float - tensor: TranslationQualityTensor - issues: List[str] = field(default_factory=list) - recommendations: List[str] = field(default_factory=list) - should_retranslate: bool = False - confidence: float = 0.0 - - -class TranslationQualityOracle: - def __init__(self, strict_threshold: float = 0.85): - self.strict_threshold = strict_threshold - self.evaluation_history: List[EvaluationResult] = [] - - def evaluate( - self, - original: str, - translated: str, - source_lang: str, - preserved_terms: Optional[List[str]] = None, - ) -> EvaluationResult: - preserved = preserved_terms or [] - tensor = TranslationQualityTensor( - semantic_fidelity=self._score_semantic(original, translated), - technical_accuracy=self._score_technical(preserved, translated), - fluency=self._score_fluency(translated), - consistency=self._score_consistency(preserved), - context_awareness=self._score_context(original), - ) - omega = tensor.compute_omega() - issues: List[str] = [] - recommendations: List[str] = [] - should_retranslate = omega < self.strict_threshold - if should_retranslate: - issues.append("Ω score below strict threshold.") - recommendations.append("Improve clarity and preserve critical terminology.") - else: - recommendations.append("Maintain translation consistency.") - - confidence = min(1.0, omega * 1.1) - result = EvaluationResult( - omega_score=omega, - tensor=tensor, - issues=issues, - recommendations=recommendations, - should_retranslate=should_retranslate, - confidence=confidence, - ) - self.evaluation_history.append(result) - return result - - def _score_semantic(self, original: str, translated: str) -> float: - original_tokens = self._normalize(original) - translated_tokens = self._normalize(translated) - if not original_tokens: - return 0.0 - overlap = len(original_tokens & translated_tokens) - return min(1.0, overlap / max(len(original_tokens), 1)) - - def _score_technical(self, preserved: List[str], translated: str) -> float: - if not preserved: - return 0.5 - hits = sum(1 for term in preserved if term in translated) - if hits == 0: - return 0.2 - return min(1.0, 0.8 + hits * 0.05) - - def _score_fluency(self, translated: str) -> float: - word_count = len(translated.split()) - if word_count < 3: - return 0.5 - return min(1.0, 0.75 + word_count * 0.01) - - def _score_consistency(self, preserved: List[str]) -> float: - if not preserved: - return 0.8 - return min(1.0, 0.85 + len(set(preserved)) * 0.05) - - def _score_context(self, original: str) -> float: - word_count = len(original.split()) - return min(1.0, 0.7 + word_count * 0.01) - - def _normalize(self, text: str) -> Set[str]: - return set(re.findall(r"\b\w+\b", text.lower())) - - -class ConsistencyChecker: - CAMEL_RE = re.compile(r"\b[A-Z][a-z]+[A-Z][a-zA-Z]*\b") - SNAKE_RE = re.compile(r"\b[a-z]+_[a-z0-9_]+\b") - UPPER_RE = re.compile(r"\b[A-Z0-9_]{2,}\b") - - def __init__(self) -> None: - self.term_map: Dict[str, Set[str]] = {} - self.violations: List[str] = [] - - def register_translation(self, term: str, translation: str) -> None: - self.term_map.setdefault(term, set()).add(translation) - - def check_consistency(self, term: str, translation: str) -> Tuple[bool, List[str]]: - translations = self.term_map.setdefault(term, set()) - translations.add(translation) - if len(translations) > 1: - message = f"Inconsistent translation for '{term}'." - self.violations.append(message) - return False, [message] - return True, [] - - def _extract_terms(self, text: str) -> Set[str]: - terms = set(self.CAMEL_RE.findall(text)) - terms.update(self.SNAKE_RE.findall(text)) - terms.update(self.UPPER_RE.findall(text)) - return terms - - def get_consistency_score(self) -> float: - if not self.term_map: - return 1.0 - consistent_terms = sum(1 for translations in self.term_map.values() if len(translations) == 1) - return consistent_terms / len(self.term_map) - - -class ContextAnalyzer: - TECH_WORDS = {"function", "parameter", "class", "method", "variable", "return"} - - def analyze_context(self, text: str, node_type: Optional[str] = None) -> Dict[str, object]: - words = re.findall(r"\b\w+\b", text.lower()) - config = { - "inline_comment": ("code_comment", "concise", "technical", 1.2), - "docstring": ("docstring", "detailed", "professional", 1.5), - } - context_type, style, tone, multiplier = config.get(node_type, ("error_message", "precise", "neutral", 1.0)) - has_code_terms = bool(self._detect_code_terms(text)) - has_technical_words = any(word in self.TECH_WORDS for word in words) - max_length = int(max(len(words), 1) * multiplier) - recommended = self._recommendation(node_type) - return { - "context_type": context_type, - "style": style, - "tone": tone, - "has_code_terms": has_code_terms, - "has_technical_words": has_technical_words, - "max_length": max_length, - "recommended_approach": recommended, - } - - def _detect_code_terms(self, text: str) -> bool: - return bool(re.search(r"[A-Za-z_]+\(.*?\)", text) or re.search(r"[A-Z][a-z]+[A-Z]", text)) - - def _recommendation(self, node_type: Optional[str]) -> str: - if node_type == "inline_comment": - return "Maintain a technical, concise tone while preserving vocabulary." - if node_type == "docstring": - return "Adopt a professional narrative with clear technical details." - return "Describe the issue precisely with actionable guidance." +"""FDS-Dev module.""" + +from __future__ import annotations + +import re +from dataclasses import dataclass, field +from typing import Dict, List, Optional, Set, Tuple + + +@dataclass +class TranslationQualityTensor: + semantic_fidelity: float + technical_accuracy: float + fluency: float + consistency: float + context_awareness: float + + def compute_omega(self) -> float: + weights = [0.30, 0.25, 0.20, 0.15, 0.10] + values = [ + self.semantic_fidelity, + self.technical_accuracy, + self.fluency, + self.consistency, + self.context_awareness, + ] + return sum(weight * value for weight, value in zip(weights, values)) + + +@dataclass +class EvaluationResult: + omega_score: float + tensor: TranslationQualityTensor + issues: List[str] = field(default_factory=list) + recommendations: List[str] = field(default_factory=list) + should_retranslate: bool = False + confidence: float = 0.0 + + +class TranslationQualityOracle: + def __init__(self, strict_threshold: float = 0.85): + self.strict_threshold = strict_threshold + self.evaluation_history: List[EvaluationResult] = [] + + def evaluate( + self, + original: str, + translated: str, + source_lang: str, + preserved_terms: Optional[List[str]] = None, + ) -> EvaluationResult: + preserved = preserved_terms or [] + tensor = TranslationQualityTensor( + semantic_fidelity=self._score_semantic(original, translated), + technical_accuracy=self._score_technical(preserved, translated), + fluency=self._score_fluency(translated), + consistency=self._score_consistency(preserved), + context_awareness=self._score_context(original), + ) + omega = tensor.compute_omega() + issues: List[str] = [] + recommendations: List[str] = [] + should_retranslate = omega < self.strict_threshold + if should_retranslate: + issues.append("Ω score below strict threshold.") + recommendations.append("Improve clarity and preserve critical terminology.") + else: + recommendations.append("Maintain translation consistency.") + + confidence = min(1.0, omega * 1.1) + result = EvaluationResult( + omega_score=omega, + tensor=tensor, + issues=issues, + recommendations=recommendations, + should_retranslate=should_retranslate, + confidence=confidence, + ) + self.evaluation_history.append(result) + return result + + def _score_semantic(self, original: str, translated: str) -> float: + original_tokens = self._normalize(original) + translated_tokens = self._normalize(translated) + if not original_tokens: + return 0.0 + overlap = len(original_tokens & translated_tokens) + return min(1.0, overlap / max(len(original_tokens), 1)) + + def _score_technical(self, preserved: List[str], translated: str) -> float: + if not preserved: + return 0.5 + hits = sum(1 for term in preserved if term in translated) + if hits == 0: + return 0.2 + return min(1.0, 0.8 + hits * 0.05) + + def _score_fluency(self, translated: str) -> float: + word_count = len(translated.split()) + if word_count < 3: + return 0.5 + return min(1.0, 0.75 + word_count * 0.01) + + def _score_consistency(self, preserved: List[str]) -> float: + if not preserved: + return 0.8 + return min(1.0, 0.85 + len(set(preserved)) * 0.05) + + def _score_context(self, original: str) -> float: + word_count = len(original.split()) + return min(1.0, 0.7 + word_count * 0.01) + + def _normalize(self, text: str) -> Set[str]: + return set(re.findall(r"\b\w+\b", text.lower())) + + +class ConsistencyChecker: + CAMEL_RE = re.compile(r"\b[A-Z][a-z]+[A-Z][a-zA-Z]*\b") + SNAKE_RE = re.compile(r"\b[a-z]+_[a-z0-9_]+\b") + UPPER_RE = re.compile(r"\b[A-Z0-9_]{2,}\b") + + def __init__(self) -> None: + self.term_map: Dict[str, Set[str]] = {} + self.violations: List[str] = [] + + def register_translation(self, term: str, translation: str) -> None: + self.term_map.setdefault(term, set()).add(translation) + + def check_consistency(self, term: str, translation: str) -> Tuple[bool, List[str]]: + translations = self.term_map.setdefault(term, set()) + translations.add(translation) + if len(translations) > 1: + message = f"Inconsistent translation for '{term}'." + self.violations.append(message) + return False, [message] + return True, [] + + def _extract_terms(self, text: str) -> Set[str]: + terms = set(self.CAMEL_RE.findall(text)) + terms.update(self.SNAKE_RE.findall(text)) + terms.update(self.UPPER_RE.findall(text)) + return terms + + def get_consistency_score(self) -> float: + if not self.term_map: + return 1.0 + consistent_terms = sum(1 for translations in self.term_map.values() if len(translations) == 1) + return consistent_terms / len(self.term_map) + + +class ContextAnalyzer: + TECH_WORDS = {"function", "parameter", "class", "method", "variable", "return"} + + def analyze_context(self, text: str, node_type: Optional[str] = None) -> Dict[str, object]: + words = re.findall(r"\b\w+\b", text.lower()) + config = { + "inline_comment": ("code_comment", "concise", "technical", 1.2), + "docstring": ("docstring", "detailed", "professional", 1.5), + } + context_type, style, tone, multiplier = config.get(node_type, ("error_message", "precise", "neutral", 1.0)) + has_code_terms = bool(self._detect_code_terms(text)) + has_technical_words = any(word in self.TECH_WORDS for word in words) + max_length = int(max(len(words), 1) * multiplier) + recommended = self._recommendation(node_type) + return { + "context_type": context_type, + "style": style, + "tone": tone, + "has_code_terms": has_code_terms, + "has_technical_words": has_technical_words, + "max_length": max_length, + "recommended_approach": recommended, + } + + def _detect_code_terms(self, text: str) -> bool: + return bool(re.search(r"[A-Za-z_]+\(.*?\)", text) or re.search(r"[A-Z][a-z]+[A-Z]", text)) + + def _recommendation(self, node_type: Optional[str]) -> str: + if node_type == "inline_comment": + return "Maintain a technical, concise tone while preserving vocabulary." + if node_type == "docstring": + return "Adopt a professional narrative with clear technical details." + return "Describe the issue precisely with actionable guidance." diff --git a/fds_dev/i18n/translation.py b/fds_dev/i18n/translation.py index 1370c65..681fd65 100644 --- a/fds_dev/i18n/translation.py +++ b/fds_dev/i18n/translation.py @@ -1,137 +1,139 @@ -from __future__ import annotations - -import re -from dataclasses import dataclass, field -from typing import Dict, List, Set, Tuple - - -@dataclass -class TranslationResult: - original: str - translated: str - source_lang: str - target_lang: str - confidence: float - method: str - preserved_terms: List[str] - metadata: Dict[str, str] = field(default_factory=dict) - - -class TechnicalTermDatabase: - PRESERVE = {"function", "class", "API", "HTTP", "JSON", "SQL"} - TRANSLATION_MAP = { - "ko": {"함수": "function", "클래스": "class", "변수": "variable", "테스트": "test"}, - "ja": {"関数": "function", "クラス": "class"}, - "zh": {"函数": "function", "类": "class"}, - } - - @classmethod - def should_preserve(cls, term: str) -> bool: - if not term: - return False - if term in cls.PRESERVE: - return True - if term.isupper() and len(term) > 1: - return True - if "_" in term: - return True - return bool(re.match(r"[A-Z][a-z]+[A-Z][A-Za-z]+", term)) - - @classmethod - def get_standard_translation(cls, term: str, lang: str) -> str | None: - return cls.TRANSLATION_MAP.get(lang, {}).get(term) - - -class TranslationEngine: - CAMEL_RE = re.compile(r"\b[A-Z][a-z]+(?:[A-Z][a-zA-Z]+)+\b") - SNAKE_RE = re.compile(r"\b[a-z]+_[a-z0-9_]+\b") - UPPER_RE = re.compile(r"\b[A-Z0-9_]{2,}\b") - - def __init__(self, mode: str = "rule_based", api_key: str | None = None): - self.mode = mode - self.api_key = api_key - self.translation_cache: Dict[Tuple[str, str, str, str], TranslationResult] = {} - - def translate(self, text: str, source_lang: str, target_lang: str) -> TranslationResult: - cache_key = (text, source_lang, target_lang, self.mode) - if cache_key in self.translation_cache: - return self.translation_cache[cache_key] - - preserved_terms = self._extract_preservable_terms(text) - if self.mode == "rule_based" or source_lang == target_lang: - translated = self._rule_based_translate(text, source_lang, target_lang) - confidence = 0.7 if source_lang != target_lang else 1.0 - method = "rule_based" - else: - translated = self._simulate_ai_translation(text, source_lang) - confidence = 0.85 - method = "ai_simulated" - - result = TranslationResult( - original=text, - translated=translated, - source_lang=source_lang, - target_lang=target_lang, - confidence=confidence, - method=method, - preserved_terms=preserved_terms, - metadata={"cache_hit": "false"}, - ) - self.translation_cache[cache_key] = result - return result - - def translate_batch(self, texts: List[str], source_lang: str, target_lang: str) -> List[TranslationResult]: - return [self.translate(text, source_lang, target_lang) for text in texts] - - def _extract_preservable_terms(self, text: str) -> List[str]: - ordered: List[str] = [] - - def collect(pattern: re.Pattern[str]) -> None: - for match in pattern.findall(text): - if match not in ordered: - ordered.append(match) - - collect(self.CAMEL_RE) - collect(self.SNAKE_RE) - collect(self.UPPER_RE) - - for term in TechnicalTermDatabase.PRESERVE: - if term in text and term not in ordered: - ordered.append(term) - - for lang_map in TechnicalTermDatabase.TRANSLATION_MAP.values(): - for native, english in lang_map.items(): - if native in text and native not in ordered: - ordered.append(native) - if english in text and english not in ordered: - ordered.append(english) - - return ordered - - def _rule_based_translate(self, text: str, source_lang: str, target_lang: str) -> str: - if source_lang == target_lang: - return text - - replacements: Dict[str, str] = { - "함수를 호출합니다": "Call the function", - "함수를 호출하다": "Call the function", - "함수": "function", - "클래스": "class", - "변수를": "the variable", - "변수": "variable", - "테스트입니다": "This is a test", - "테스트": "test", - "사용합니다": "use", - } - - translated = text - for key, value in replacements.items(): - translated = translated.replace(key, value) - - translated = translated.strip() - if not translated.endswith("."): - translated += "." - return translated - - def _simulate_ai_translation(self, text: str, source_lang: str) -> str: - return f"[{source_lang}→en] {text}" +"""FDS-Dev module.""" + +from __future__ import annotations + +import re +from dataclasses import dataclass, field +from typing import Dict, List, Set, Tuple + + +@dataclass +class TranslationResult: + original: str + translated: str + source_lang: str + target_lang: str + confidence: float + method: str + preserved_terms: List[str] + metadata: Dict[str, str] = field(default_factory=dict) + + +class TechnicalTermDatabase: + PRESERVE = {"function", "class", "API", "HTTP", "JSON", "SQL"} + TRANSLATION_MAP = { + "ko": {"함수": "function", "클래스": "class", "변수": "variable", "테스트": "test"}, + "ja": {"関数": "function", "クラス": "class"}, + "zh": {"函数": "function", "类": "class"}, + } + + @classmethod + def should_preserve(cls, term: str) -> bool: + if not term: + return False + if term in cls.PRESERVE: + return True + if term.isupper() and len(term) > 1: + return True + if "_" in term: + return True + return bool(re.match(r"[A-Z][a-z]+[A-Z][A-Za-z]+", term)) + + @classmethod + def get_standard_translation(cls, term: str, lang: str) -> str | None: + return cls.TRANSLATION_MAP.get(lang, {}).get(term) + + +class TranslationEngine: + CAMEL_RE = re.compile(r"\b[A-Z][a-z]+(?:[A-Z][a-zA-Z]+)+\b") + SNAKE_RE = re.compile(r"\b[a-z]+_[a-z0-9_]+\b") + UPPER_RE = re.compile(r"\b[A-Z0-9_]{2,}\b") + + def __init__(self, mode: str = "rule_based", api_key: str | None = None): + self.mode = mode + self.api_key = api_key + self.translation_cache: Dict[Tuple[str, str, str, str], TranslationResult] = {} + + def translate(self, text: str, source_lang: str, target_lang: str) -> TranslationResult: + cache_key = (text, source_lang, target_lang, self.mode) + if cache_key in self.translation_cache: + return self.translation_cache[cache_key] + + preserved_terms = self._extract_preservable_terms(text) + if self.mode == "rule_based" or source_lang == target_lang: + translated = self._rule_based_translate(text, source_lang, target_lang) + confidence = 0.7 if source_lang != target_lang else 1.0 + method = "rule_based" + else: + translated = self._simulate_ai_translation(text, source_lang) + confidence = 0.85 + method = "ai_simulated" + + result = TranslationResult( + original=text, + translated=translated, + source_lang=source_lang, + target_lang=target_lang, + confidence=confidence, + method=method, + preserved_terms=preserved_terms, + metadata={"cache_hit": "false"}, + ) + self.translation_cache[cache_key] = result + return result + + def translate_batch(self, texts: List[str], source_lang: str, target_lang: str) -> List[TranslationResult]: + return [self.translate(text, source_lang, target_lang) for text in texts] + + def _extract_preservable_terms(self, text: str) -> List[str]: + ordered: List[str] = [] + + def collect(pattern: re.Pattern[str]) -> None: + for match in pattern.findall(text): + if match not in ordered: + ordered.append(match) + + collect(self.CAMEL_RE) + collect(self.SNAKE_RE) + collect(self.UPPER_RE) + + for term in TechnicalTermDatabase.PRESERVE: + if term in text and term not in ordered: + ordered.append(term) + + for lang_map in TechnicalTermDatabase.TRANSLATION_MAP.values(): + for native, english in lang_map.items(): + if native in text and native not in ordered: + ordered.append(native) + if english in text and english not in ordered: + ordered.append(english) + + return ordered + + def _rule_based_translate(self, text: str, source_lang: str, target_lang: str) -> str: + if source_lang == target_lang: + return text + + replacements: Dict[str, str] = { + "함수를 호출합니다": "Call the function", + "함수를 호출하다": "Call the function", + "함수": "function", + "클래스": "class", + "변수를": "the variable", + "변수": "variable", + "테스트입니다": "This is a test", + "테스트": "test", + "사용합니다": "use", + } + + translated = text + for key, value in replacements.items(): + translated = translated.replace(key, value) + + translated = translated.strip() + if not translated.endswith("."): + translated += "." + return translated + + def _simulate_ai_translation(self, text: str, source_lang: str) -> str: + return f"[{source_lang}→en] {text}" diff --git a/fds_dev/language.py b/fds_dev/language.py index 7586424..1d6dbdd 100644 --- a/fds_dev/language.py +++ b/fds_dev/language.py @@ -1,3 +1,5 @@ -from fds_dev.i18n.language import LanguageDetectionResult, LanguageDetector - -__all__ = ["LanguageDetector", "LanguageDetectionResult"] +"""FDS-Dev module.""" + +from fds_dev.i18n.language import LanguageDetectionResult, LanguageDetector + +__all__ = ["LanguageDetector", "LanguageDetectionResult"] diff --git a/fds_dev/main.py b/fds_dev/main.py index 6b27f54..4c8d869 100644 --- a/fds_dev/main.py +++ b/fds_dev/main.py @@ -1,139 +1,152 @@ -import click -import os -import glob -import json -from concurrent.futures import ProcessPoolExecutor, as_completed -from functools import partial - -from fds_dev.config import load_config -from fds_dev.runner import LintRunner -from fds_dev.parser import MarkdownParser -from fds_dev.language import LanguageDetector -from fds_dev.translator import TranslationEngine -from fds_dev.output import OutputFormatter - -# Load configuration at startup -CONFIG = load_config() -CACHE_FILE = '.fds_cache.json' - -def load_cache(): - if os.path.exists(CACHE_FILE): - with open(CACHE_FILE, 'r') as f: - try: - return json.load(f) - except json.JSONDecodeError: - return {} - return {} - -def save_cache(cache): - with open(CACHE_FILE, 'w') as f: - json.dump(cache, f, indent=2) - -@click.group() -def cli(): - """ - FDS-Dev: A blazingly fast, structure-aware linter for your documentation, - supercharged with AI-powered translation. - """ - pass - -def run_lint_on_file(runner, cache, file_path): - """Helper function to be called by the process pool.""" - return runner.run(file_path, cache) - -@cli.command() -@click.argument('path', type=click.Path(exists=True)) -def lint(path): - """Checks documentation for structural issues.""" - - # 1. Load cache and initialize components - cache = load_cache() - runner = LintRunner(CONFIG) - formatter = OutputFormatter() - - files_to_lint = [] - if os.path.isdir(path): - # Find all markdown files recursively - files_to_lint.extend(glob.glob(os.path.join(path, '**', '*.md'), recursive=True)) - files_to_lint.extend(glob.glob(os.path.join(path, '**', '*.markdown'), recursive=True)) - else: - files_to_lint.append(path) - - click.echo(f"Found {len(files_to_lint)} file(s) to lint...") - - # 2. Run linting in parallel - results = [] - with ProcessPoolExecutor() as executor: - # Use functools.partial to create a function with runner and cache arguments pre-filled - lint_func = partial(run_lint_on_file, runner, cache) - - future_to_file = {executor.submit(lint_func, file): file for file in files_to_lint} - - with click.progressbar(as_completed(future_to_file), length=len(files_to_lint), label="Linting files") as bar: - for future in bar: - file_path, file_hash, errors = future.result() - results.append((file_path, errors)) - - # 3. Update cache with new results - if file_hash: - cache[file_path] = { - 'hash': file_hash, - 'errors': [e.__dict__ for e in errors] - } - - # 4. Save the updated cache - save_cache(cache) - - # 5. Display results - formatter.display_lint_results(results) - - -@cli.command() -@click.argument('path', type=click.path(exists=True)) -@click.option('--output', '-o', help="Output file path for the translated document.") -@click.option('--in-place', is_flag=True, help="Translate the file in-place (overwrites the original).") -def translate(path, output, in_place): - """Translates docs and code comments to English.""" - click.echo(f"Translating {path}...") - - parser = MarkdownParser() - detector = LanguageDetector() - engine = TranslationEngine(CONFIG) - formatter = OutputFormatter() - - try: - doc = parser.parse(path) - - source_lang_config = CONFIG.get('language', {}).get('source', 'auto') - target_lang_config = CONFIG.get('language', {}).get('target', 'en') - - source_lang = source_lang_config - if source_lang == 'auto': - source_lang = detector.detect(doc.content) - click.echo(f"Detected language: {source_lang.upper()}") - - if source_lang == target_lang_config: - click.secho("Source and target languages are the same. Nothing to translate.", fg="yellow") - return - - translated_content = engine.translate(doc.content, source_lang, target_lang_config) - - if in_place: - with open(path, 'w', encoding='utf-8') as f: - f.write(translated_content) - formatter.display_save_message(path, in_place=True) - elif output: - with open(output, 'w', encoding='utf-8') as f: - f.write(translated_content) - formatter.display_save_message(output, in_place=False) - else: - formatter.display_translation_preview(path, translated_content) - - except FileNotFoundError: - click.secho(f"Error: File not found at {path}", fg="red") - except Exception as e: - click.secho(f"An unexpected error occurred: {e}", fg="red") - - -if __name__ == '__main__': - cli() +"""FDS-Dev module.""" + +import click +import os +import glob +import json +from concurrent.futures import ProcessPoolExecutor, as_completed +from functools import partial +from pathlib import Path + +from fds_dev.config import load_config +from fds_dev.runner import LintRunner +from fds_dev.parser import MarkdownParser +from fds_dev.language import LanguageDetector +from fds_dev.translator import TranslationEngine +from fds_dev.output import OutputFormatter + + +def resolve_cache_path(target_path: str) -> Path: + path_obj = Path(target_path) + if path_obj.is_dir(): + return path_obj / '.fds_cache.json' + parent = path_obj.parent if path_obj.parent.as_posix() else Path('.') + return parent / '.fds_cache.json' + + +def load_cache(cache_path: Path): + if cache_path.exists(): + with cache_path.open('r') as f: + try: + return json.load(f) + except json.JSONDecodeError: + return {} + return {} + + +def save_cache(cache, cache_path: Path): + cache_path.parent.mkdir(parents=True, exist_ok=True) + with cache_path.open('w') as f: + json.dump(cache, f, indent=2) + +@click.group() +def cli(): + """ + FDS-Dev: A blazingly fast, structure-aware linter for your documentation, + supercharged with AI-powered translation. + """ + pass + +def run_lint_on_file(runner, cache, file_path): + """Helper function to be called by the process pool.""" + return runner.run(file_path, cache) + +@cli.command() +@click.argument('path', type=click.Path(exists=True)) +def lint(path): + """Checks documentation for structural issues.""" + + # 1. Load cache and initialize components + config = load_config(path) + cache_path = resolve_cache_path(path) + cache = load_cache(cache_path) + runner = LintRunner(config) + formatter = OutputFormatter() + + files_to_lint = [] + if os.path.isdir(path): + # Find all markdown files recursively + files_to_lint.extend(glob.glob(os.path.join(path, '**', '*.md'), recursive=True)) + files_to_lint.extend(glob.glob(os.path.join(path, '**', '*.markdown'), recursive=True)) + else: + files_to_lint.append(path) + + click.echo(f"Found {len(files_to_lint)} file(s) to lint...") + + # 2. Run linting in parallel + results = [] + with ProcessPoolExecutor() as executor: + # Use functools.partial to create a function with runner and cache arguments pre-filled + lint_func = partial(run_lint_on_file, runner, cache) + + future_to_file = {executor.submit(lint_func, file): file for file in files_to_lint} + + with click.progressbar(as_completed(future_to_file), length=len(files_to_lint), label="Linting files") as bar: + for future in bar: + file_path, file_hash, errors = future.result() + results.append((file_path, errors)) + + # 3. Update cache with new results + if file_hash: + cache[file_path] = { + 'hash': file_hash, + 'errors': [e.__dict__ for e in errors] + } + + # 4. Save the updated cache + save_cache(cache, cache_path) + + # 5. Display results + formatter.display_lint_results(results) + + +@cli.command() +@click.argument('path', type=click.Path(exists=True)) +@click.option('--output', '-o', help="Output file path for the translated document.") +@click.option('--in-place', is_flag=True, help="Translate the file in-place (overwrites the original).") +def translate(path, output, in_place): + """Translates docs and code comments to English.""" + click.echo(f"Translating {path}...") + + parser = MarkdownParser() + detector = LanguageDetector() + config = load_config(path) + engine = TranslationEngine(config) + formatter = OutputFormatter() + + try: + doc = parser.parse(path) + source_lang_config = config.get('language', {}).get('source', 'auto') + target_lang_config = config.get('language', {}).get('target', 'en') + + source_lang = source_lang_config + if source_lang == 'auto': + detection = detector.detect(doc.content) + source_lang = detection.language + click.echo(f"Detected language: {source_lang.upper()} (confidence {detection.confidence:.2f})") + + if source_lang == target_lang_config: + click.secho("Source and target languages are the same. Nothing to translate.", fg="yellow") + return + + translated_content = engine.translate(doc.content, source_lang, target_lang_config) + + if in_place: + with open(path, 'w', encoding='utf-8') as f: + f.write(translated_content) + formatter.display_save_message(path, in_place=True) + elif output: + with open(output, 'w', encoding='utf-8') as f: + f.write(translated_content) + formatter.display_save_message(output, in_place=False) + else: + formatter.display_translation_preview(path, translated_content) + + except FileNotFoundError: + click.secho(f"Error: File not found at {path}", fg="red") + except Exception as e: + click.secho(f"An unexpected error occurred: {e}", fg="red") + + +if __name__ == '__main__': + cli() diff --git a/fds_dev/output.py b/fds_dev/output.py index 36a49fb..b3e8d79 100644 --- a/fds_dev/output.py +++ b/fds_dev/output.py @@ -1,58 +1,60 @@ -import click -from typing import List, Tuple - -from fds_dev.rules import LintError - -class OutputFormatter: - def display_lint_results(self, results: List[Tuple[str, List[LintError]]]): - """ - Displays a formatted list of linting errors, grouped by file. - """ - total_errors = 0 - files_with_errors = 0 - - for file_path, errors in results: - if errors: - files_with_errors += 1 - error_count = len(errors) - total_errors += error_count - plural = 's' if error_count > 1 else '' - - click.secho(f"\n❌ Found {error_count} issue{plural} in {file_path}:") - for error in sorted(errors, key=lambda e: e.line_number): - line_info = f"L{error.line_number:<4}" - rule_info = f"({error.rule_name})" - click.echo(f" - {line_info} {click.style(rule_info, fg='yellow'):<25} {error.message}") - - if total_errors == 0: - file_count = len(results) - plural = 's' if file_count > 1 else '' - click.secho(f"\n✅ No issues found in {file_count} file{plural}.", fg="green") - else: - click.echo("-" * 40) - click.secho(f"Summary: Found {total_errors} total issues in {files_with_errors} files.", bold=True) - - - def display_translation_preview(self, original_path: str, translated_content: str): - """ - Displays a preview of the translated content. - """ - click.secho(f"\nTranslation Preview for {original_path}:", bold=True) - click.echo("-" * 40) - for i, line in enumerate(translated_content.splitlines()): - if i >= 10: - click.echo("...") - break - click.echo(line) - click.echo("-" * 40) - click.secho("Use --output or --in-place to save the translation.", fg="cyan") - - def display_save_message(self, output_path: str, in_place: bool): - """ - Displays a message confirming the file was saved. - """ - if in_place: - message = f"✅ Successfully translated and saved {output_path} in-place." - else: - message = f"✅ Translation successfully saved to {output_path}." - click.secho(message, fg="green") \ No newline at end of file +"""FDS-Dev module.""" + +import click +from typing import List, Tuple + +from fds_dev.rules import LintError + +class OutputFormatter: + def display_lint_results(self, results: List[Tuple[str, List[LintError]]]): + """ + Displays a formatted list of linting errors, grouped by file. + """ + total_errors = 0 + files_with_errors = 0 + + for file_path, errors in results: + if errors: + files_with_errors += 1 + error_count = len(errors) + total_errors += error_count + plural = 's' if error_count > 1 else '' + + click.secho(f"\n❌ Found {error_count} issue{plural} in {file_path}:") + for error in sorted(errors, key=lambda e: e.line_number): + line_info = f"L{error.line_number:<4}" + rule_info = f"({error.rule_name})" + click.echo(f" - {line_info} {click.style(rule_info, fg='yellow'):<25} {error.message}") + + if total_errors == 0: + file_count = len(results) + plural = 's' if file_count > 1 else '' + click.secho(f"\n✅ No issues found in {file_count} file{plural}.", fg="green") + else: + click.echo("-" * 40) + click.secho(f"Summary: Found {total_errors} total issues in {files_with_errors} files.", bold=True) + + + def display_translation_preview(self, original_path: str, translated_content: str): + """ + Displays a preview of the translated content. + """ + click.secho(f"\nTranslation Preview for {original_path}:", bold=True) + click.echo("-" * 40) + for i, line in enumerate(translated_content.splitlines()): + if i >= 10: + click.echo("...") + break + click.echo(line) + click.echo("-" * 40) + click.secho("Use --output or --in-place to save the translation.", fg="cyan") + + def display_save_message(self, output_path: str, in_place: bool): + """ + Displays a message confirming the file was saved. + """ + if in_place: + message = f"✅ Successfully translated and saved {output_path} in-place." + else: + message = f"✅ Translation successfully saved to {output_path}." + click.secho(message, fg="green") diff --git a/fds_dev/parser.py b/fds_dev/parser.py index a26fd7c..27d44f1 100644 --- a/fds_dev/parser.py +++ b/fds_dev/parser.py @@ -1,88 +1,89 @@ -from dataclasses import dataclass, field -import re -from typing import List - -@dataclass -class Header: - level: int - text: str - line_number: int - -@dataclass -class Link: - text: str - target: str - line_number: int - kind: str - -@dataclass -class Document: - path: str - content: str - lines: List[str] = field(init=False) - headers: List[Header] = field(default_factory=list) - links: List[Link] = field(default_factory=list) - - def __post_init__(self): - self.lines = self.content.splitlines() - if self.content == "": - self.lines = [""] - -class MarkdownParser: - """ - A simple parser to extract structural elements from a Markdown file. - """ - def __init__(self): - self.header_regex = re.compile(r"^\s*(#{1,6})\s+(.*)") - self.link_regex = re.compile(r"\[([^\]]+)\]\(([^)]+)\)") - - def parse(self, file_path: str) -> Document: - """ - Parses a Markdown file and returns a Document object. - """ - with open(file_path, 'r', encoding='utf-8') as f: - content = f.read() - - doc = Document(path=file_path, content=content) - - for i, line in enumerate(doc.lines): - header_match = self.header_regex.match(line) - if header_match: - level = len(header_match.group(1)) - text = header_match.group(2).strip() - doc.headers.append(Header(level=level, text=text, line_number=i + 1)) - - doc.links = self._extract_links(doc.lines) - - return doc - - def _extract_links(self, lines: List[str]) -> List[Link]: - links: List[Link] = [] - for line_number, line in enumerate(lines, start=1): - for match in self.link_regex.finditer(line): - text = match.group(1).strip() - target = match.group(2).strip() - kind = self._classify_link(target) - links.append(Link(text=text, target=target, line_number=line_number, kind=kind)) - return links - - @staticmethod - def _classify_link(target: str) -> str: - lowered = target.lower() - if lowered.startswith(("http://", "https://", "mailto:")): - return "external" - if lowered.startswith("#"): - return "anchor" - return "file" - -if __name__ == '__main__': - # Example usage: - parser = MarkdownParser() - try: - doc = parser.parse('README.md') - print(f"Parsed {doc.path}:") - for header in doc.headers: - print(f" - L{header.line_number}: {' ' * (header.level - 1)}[{header.level}] {header.text}") - except FileNotFoundError: - print("README.md not found. Create one to test the parser.") - +"""FDS-Dev module.""" + +from dataclasses import dataclass, field +import re +from typing import List + +@dataclass +class Header: + level: int + text: str + line_number: int + +@dataclass +class Link: + text: str + target: str + line_number: int + kind: str + +@dataclass +class Document: + path: str + content: str + lines: List[str] = field(init=False) + headers: List[Header] = field(default_factory=list) + links: List[Link] = field(default_factory=list) + + def __post_init__(self): + self.lines = self.content.splitlines() + if self.content == "": + self.lines = [""] + +class MarkdownParser: + """ + A simple parser to extract structural elements from a Markdown file. + """ + def __init__(self): + self.header_regex = re.compile(r"^\s*(#{1,6})\s+(.*)") + self.link_regex = re.compile(r"\[([^\]]+)\]\(([^)]+)\)") + + def parse(self, file_path: str) -> Document: + """ + Parses a Markdown file and returns a Document object. + """ + with open(file_path, 'r', encoding='utf-8') as f: + content = f.read() + + doc = Document(path=file_path, content=content) + + for i, line in enumerate(doc.lines): + header_match = self.header_regex.match(line) + if header_match: + level = len(header_match.group(1)) + text = header_match.group(2).strip() + doc.headers.append(Header(level=level, text=text, line_number=i + 1)) + + doc.links = self._extract_links(doc.lines) + + return doc + + def _extract_links(self, lines: List[str]) -> List[Link]: + links: List[Link] = [] + for line_number, line in enumerate(lines, start=1): + for match in self.link_regex.finditer(line): + text = match.group(1).strip() + target = match.group(2).strip() + kind = self._classify_link(target) + links.append(Link(text=text, target=target, line_number=line_number, kind=kind)) + return links + + @staticmethod + def _classify_link(target: str) -> str: + lowered = target.lower() + if lowered.startswith(("http://", "https://", "mailto:")): + return "external" + if lowered.startswith("#"): + return "anchor" + return "file" + +if __name__ == '__main__': + # Example usage: + parser = MarkdownParser() + try: + doc = parser.parse('README.md') + print(f"Parsed {doc.path}:") + for header in doc.headers: + print(f" - L{header.line_number}: {' ' * (header.level - 1)}[{header.level}] {header.text}") + except FileNotFoundError: + print("README.md not found. Create one to test the parser.") diff --git a/fds_dev/rules.py b/fds_dev/rules.py index 2430e64..94b6641 100644 --- a/fds_dev/rules.py +++ b/fds_dev/rules.py @@ -1,185 +1,187 @@ -from abc import ABC, abstractmethod -from dataclasses import dataclass -import re -from pathlib import Path -from typing import List, Optional -from urllib.parse import urldefrag - -import requests - -from fds_dev.parser import Document, Header - -@dataclass -class LintError: - line_number: int - message: str - rule_name: str - -class BaseRule(ABC): - """ - The base class for all linting rules. - """ - def __init__(self, config): - self.config = config - - @property - def name(self) -> str: - # Generates a rule name from the class name, e.g., RequireLicenseSection -> require-license-section - return ''.join(['-' + i.lower() if i.isupper() else i for i in self.__class__.__name__]).lstrip('-') - - @abstractmethod - def apply(self, doc: Document) -> List[LintError]: - """ - Applies the rule to a document and returns a list of errors. - An empty list means the rule passed. - """ - pass - -# --- Example Rule Implementations --- - -class RequireSectionLicense(BaseRule): - """ - Checks if a 'License' section exists in the document. - A 'License' section is identified by a header containing the word "License". - """ - def apply(self, doc: Document) -> List[LintError]: - found_license = False - for header in doc.headers: - if "license" in header.text.lower(): - found_license = True - break - - if not found_license: - # Report error at line 1 if the section is missing entirely. - return [LintError(line_number=1, message="Document is missing a 'License' section.", rule_name=self.name)] - - return [] - -class SectionOrder(BaseRule): - """ - Checks if top-level sections appear in a predefined order. - The order is defined in the '.fdsrc.yaml' config file. - """ - def apply(self, doc: Document) -> List[LintError]: - errors: List[LintError] = [] - expected_order: Optional[List[str]] = self.config.get('order') - - if not expected_order: - return [] # No order defined, so nothing to check. - - # Get only top-level (h1 or h2) headers from the document - top_level_headers = [h for h in doc.headers if h.level <= 2] - - last_found_index = -1 - - for header in top_level_headers: - try: - # Find the current header's text in the expected order list - current_index = -1 - for i, expected_text in enumerate(expected_order): - if expected_text.lower() in header.text.lower(): - current_index = i - break - - if current_index != -1: - if current_index < last_found_index: - expected_section = expected_order[last_found_index] - line_number = max(1, header.line_number - 1) - errors.append(LintError( - line_number=line_number, - message=f"Section '{header.text}' appears out of order. It should not come before '{expected_section}'.", - rule_name=self.name - )) - last_found_index = max(last_found_index, current_index) - - except ValueError: - # This header is not in our defined order, so we ignore it. - pass - - return errors - -class BrokenLinkCheckRule(BaseRule): - """ - Validates internal anchors, relative file references, and (optionally) external URLs. - """ - def __init__(self, config): - super().__init__(config or {}) - self.check_external = self.config.get("check_external", False) - self.timeout = float(self.config.get("timeout", 3.0)) - self.allowed_statuses = set(self.config.get("allowed_statuses", [200, 201, 202, 203, 204, 205, 301, 302, 303, 307, 308])) - - def apply(self, doc: Document) -> List[LintError]: - errors: List[LintError] = [] - if not doc.links: - return errors - - anchor_index = self._build_anchor_index(doc.headers) - base_dir = Path(doc.path).parent - - for link in doc.links: - if link.kind == "anchor": - if not self._anchor_exists(link.target, anchor_index): - errors.append( - LintError( - line_number=link.line_number, - message=f"Broken anchor link: '{link.target}' does not match any heading.", - rule_name=self.name, - ) - ) - elif link.kind == "file": - if not self._file_exists(base_dir, link.target): - errors.append( - LintError( - line_number=link.line_number, - message=f"Broken file link: '{link.target}' does not exist.", - rule_name=self.name, - ) - ) - elif link.kind == "external": - if self.check_external and not self._external_ok(link.target): - errors.append( - LintError( - line_number=link.line_number, - message=f"Broken external link: '{link.target}' is unreachable.", - rule_name=self.name, - ) - ) - return errors - - @staticmethod - def _build_anchor_index(headers: List[Header]) -> set: - slugs = {BrokenLinkCheckRule._slugify(header.text) for header in headers} - return slugs - - @staticmethod - def _anchor_exists(target: str, slug_index: set) -> bool: - anchor = target.lstrip("#") - slug = BrokenLinkCheckRule._slugify(anchor) - return slug in slug_index - - @staticmethod - def _file_exists(base_dir: Path, target: str) -> bool: - file_target, _ = urldefrag(target) - if not file_target: - return True - resolved = (base_dir / file_target).resolve() - return resolved.exists() - - def _external_ok(self, url: str) -> bool: - try: - response = requests.head(url, allow_redirects=True, timeout=self.timeout) - if response.status_code in self.allowed_statuses: - return True - if response.status_code >= 400: - response = requests.get(url, allow_redirects=True, timeout=self.timeout) - return response.status_code in self.allowed_statuses - return True - except requests.RequestException: - return False - - @staticmethod - def _slugify(value: str) -> str: - value = value.strip().lower() - value = re.sub(r"[^\w\- ]", "", value) - value = value.replace(" ", "-") - return value +"""FDS-Dev module.""" + +from abc import ABC, abstractmethod +from dataclasses import dataclass +import re +from pathlib import Path +from typing import List, Optional +from urllib.parse import urldefrag + +import requests + +from fds_dev.parser import Document, Header + +@dataclass +class LintError: + line_number: int + message: str + rule_name: str + +class BaseRule(ABC): + """ + The base class for all linting rules. + """ + def __init__(self, config): + self.config = config + + @property + def name(self) -> str: + # Generates a rule name from the class name, e.g., RequireLicenseSection -> require-license-section + return ''.join(['-' + i.lower() if i.isupper() else i for i in self.__class__.__name__]).lstrip('-') + + @abstractmethod + def apply(self, doc: Document) -> List[LintError]: + """ + Applies the rule to a document and returns a list of errors. + An empty list means the rule passed. + """ + pass + +# --- Example Rule Implementations --- + +class RequireSectionLicense(BaseRule): + """ + Checks if a 'License' section exists in the document. + A 'License' section is identified by a header containing the word "License". + """ + def apply(self, doc: Document) -> List[LintError]: + found_license = False + for header in doc.headers: + if "license" in header.text.lower(): + found_license = True + break + + if not found_license: + # Report error at line 1 if the section is missing entirely. + return [LintError(line_number=1, message="Document is missing a 'License' section.", rule_name=self.name)] + + return [] + +class SectionOrder(BaseRule): + """ + Checks if top-level sections appear in a predefined order. + The order is defined in the '.fdsrc.yaml' config file. + """ + def apply(self, doc: Document) -> List[LintError]: + errors: List[LintError] = [] + expected_order: Optional[List[str]] = self.config.get('order') + + if not expected_order: + return [] # No order defined, so nothing to check. + + # Get only top-level (h1 or h2) headers from the document + top_level_headers = [h for h in doc.headers if h.level <= 2] + + last_found_index = -1 + + for header in top_level_headers: + try: + # Find the current header's text in the expected order list + current_index = -1 + for i, expected_text in enumerate(expected_order): + if expected_text.lower() in header.text.lower(): + current_index = i + break + + if current_index != -1: + if current_index < last_found_index: + expected_section = expected_order[last_found_index] + line_number = max(1, header.line_number - 1) + errors.append(LintError( + line_number=line_number, + message=f"Section '{header.text}' appears out of order. It should not come before '{expected_section}'.", + rule_name=self.name + )) + last_found_index = max(last_found_index, current_index) + + except ValueError: + # This header is not in our defined order, so we ignore it. + pass + + return errors + +class BrokenLinkCheckRule(BaseRule): + """ + Validates internal anchors, relative file references, and (optionally) external URLs. + """ + def __init__(self, config): + super().__init__(config or {}) + self.check_external = self.config.get("check_external", False) + self.timeout = float(self.config.get("timeout", 3.0)) + self.allowed_statuses = set(self.config.get("allowed_statuses", [200, 201, 202, 203, 204, 205, 301, 302, 303, 307, 308])) + + def apply(self, doc: Document) -> List[LintError]: + errors: List[LintError] = [] + if not doc.links: + return errors + + anchor_index = self._build_anchor_index(doc.headers) + base_dir = Path(doc.path).parent + + for link in doc.links: + if link.kind == "anchor": + if not self._anchor_exists(link.target, anchor_index): + errors.append( + LintError( + line_number=link.line_number, + message=f"Broken anchor link: '{link.target}' does not match any heading.", + rule_name=self.name, + ) + ) + elif link.kind == "file": + if not self._file_exists(base_dir, link.target): + errors.append( + LintError( + line_number=link.line_number, + message=f"Broken file link: '{link.target}' does not exist.", + rule_name=self.name, + ) + ) + elif link.kind == "external": + if self.check_external and not self._external_ok(link.target): + errors.append( + LintError( + line_number=link.line_number, + message=f"Broken external link: '{link.target}' is unreachable.", + rule_name=self.name, + ) + ) + return errors + + @staticmethod + def _build_anchor_index(headers: List[Header]) -> set: + slugs = {BrokenLinkCheckRule._slugify(header.text) for header in headers} + return slugs + + @staticmethod + def _anchor_exists(target: str, slug_index: set) -> bool: + anchor = target.lstrip("#") + slug = BrokenLinkCheckRule._slugify(anchor) + return slug in slug_index + + @staticmethod + def _file_exists(base_dir: Path, target: str) -> bool: + file_target, _ = urldefrag(target) + if not file_target: + return True + resolved = (base_dir / file_target).resolve() + return resolved.exists() + + def _external_ok(self, url: str) -> bool: + try: + response = requests.head(url, allow_redirects=True, timeout=self.timeout) + if response.status_code in self.allowed_statuses: + return True + if response.status_code >= 400: + response = requests.get(url, allow_redirects=True, timeout=self.timeout) + return response.status_code in self.allowed_statuses + return True + except requests.RequestException: + return False + + @staticmethod + def _slugify(value: str) -> str: + value = value.strip().lower() + value = re.sub(r"[^\w\- ]", "", value) + value = value.replace(" ", "-") + return value diff --git a/fds_dev/runner.py b/fds_dev/runner.py index 501879e..8431b22 100644 --- a/fds_dev/runner.py +++ b/fds_dev/runner.py @@ -1,85 +1,87 @@ -import hashlib -import yaml -from typing import List, Dict, Any, Tuple, Optional - -from fds_dev.parser import Document, MarkdownParser -from fds_dev.rules import BaseRule, LintError, RequireSectionLicense, SectionOrder, BrokenLinkCheckRule - -# A mapping from rule names in the config to their class implementations. -AVAILABLE_RULES = { - "require-section-license": RequireSectionLicense, - "section-order": SectionOrder, - "broken-link-check": BrokenLinkCheckRule, -} - -def _get_file_hash(file_path: str) -> str: - """Computes the SHA256 hash of a file's content.""" - h = hashlib.sha256() - with open(file_path, 'rb') as f: - while True: - chunk = f.read(8192) - if not chunk: - break - h.update(chunk) - return h.hexdigest() - -class LintRunner: - def __init__(self, config: Dict[str, Any]): - self.config = config - self.rules = self._initialize_rules() - self.parser = MarkdownParser() - - def _initialize_rules(self) -> List[BaseRule]: - initialized_rules = [] - rules_config = self.config.get('rules', {}) - - for name, config_value in rules_config.items(): - if name not in AVAILABLE_RULES: - continue - - if config_value in ('off', False, None): - continue - - if config_value in ('on', True): - rule_config = {} - elif isinstance(config_value, dict): - enabled = config_value.get('enabled', True) - if not enabled: - continue - rule_config = {k: v for k, v in config_value.items() if k != 'enabled'} - else: - continue - - rule_instance = AVAILABLE_RULES[name](rule_config) - initialized_rules.append(rule_instance) - return initialized_rules - - def run(self, file_path: str, cache: Dict[str, Any]) -> Tuple[str, Optional[str], List[LintError]]: - """ - Runs all initialized rules against a single file, utilizing a cache. - Returns the file path, its content hash, and a list of errors. - """ - try: - file_hash = _get_file_hash(file_path) - - # Check cache - if file_path in cache and cache[file_path].get('hash') == file_hash: - # Return cached errors, converting them back to LintError objects - cached_errors_data = cache[file_path].get('errors', []) - cached_errors = [LintError(**data) for data in cached_errors_data] - return file_path, file_hash, cached_errors - - # If not in cache or hash mismatch, run linting - all_errors: List[LintError] = [] - document = self.parser.parse(file_path) - - for rule in self.rules: - errors = rule.apply(document) - all_errors.extend(errors) - - return file_path, file_hash, all_errors - - except FileNotFoundError: - return file_path, None, [LintError(line_number=0, message=f"File not found: {file_path}", rule_name="runner")] - except Exception as e: - return file_path, None, [LintError(line_number=0, message=f"An unexpected error occurred: {e}", rule_name="runner")] +"""FDS-Dev module.""" + +import hashlib +import yaml +from typing import List, Dict, Any, Tuple, Optional + +from fds_dev.parser import Document, MarkdownParser +from fds_dev.rules import BaseRule, LintError, RequireSectionLicense, SectionOrder, BrokenLinkCheckRule + +# A mapping from rule names in the config to their class implementations. +AVAILABLE_RULES = { + "require-section-license": RequireSectionLicense, + "section-order": SectionOrder, + "broken-link-check": BrokenLinkCheckRule, +} + +def _get_file_hash(file_path: str) -> str: + """Computes the SHA256 hash of a file's content.""" + h = hashlib.sha256() + with open(file_path, 'rb') as f: + while True: + chunk = f.read(8192) + if not chunk: + break + h.update(chunk) + return h.hexdigest() + +class LintRunner: + def __init__(self, config: Dict[str, Any]): + self.config = config + self.rules = self._initialize_rules() + self.parser = MarkdownParser() + + def _initialize_rules(self) -> List[BaseRule]: + initialized_rules = [] + rules_config = self.config.get('rules', {}) + + for name, config_value in rules_config.items(): + if name not in AVAILABLE_RULES: + continue + + if config_value in ('off', False, None): + continue + + if config_value in ('on', True): + rule_config = {} + elif isinstance(config_value, dict): + enabled = config_value.get('enabled', True) + if not enabled: + continue + rule_config = {k: v for k, v in config_value.items() if k != 'enabled'} + else: + continue + + rule_instance = AVAILABLE_RULES[name](rule_config) + initialized_rules.append(rule_instance) + return initialized_rules + + def run(self, file_path: str, cache: Dict[str, Any]) -> Tuple[str, Optional[str], List[LintError]]: + """ + Runs all initialized rules against a single file, utilizing a cache. + Returns the file path, its content hash, and a list of errors. + """ + try: + file_hash = _get_file_hash(file_path) + + # Check cache + if file_path in cache and cache[file_path].get('hash') == file_hash: + # Return cached errors, converting them back to LintError objects + cached_errors_data = cache[file_path].get('errors', []) + cached_errors = [LintError(**data) for data in cached_errors_data] + return file_path, file_hash, cached_errors + + # If not in cache or hash mismatch, run linting + all_errors: List[LintError] = [] + document = self.parser.parse(file_path) + + for rule in self.rules: + errors = rule.apply(document) + all_errors.extend(errors) + + return file_path, file_hash, all_errors + + except FileNotFoundError: + return file_path, None, [LintError(line_number=0, message=f"File not found: {file_path}", rule_name="runner")] + except Exception as e: + return file_path, None, [LintError(line_number=0, message=f"An unexpected error occurred: {e}", rule_name="runner")] diff --git a/fds_dev/translator.py b/fds_dev/translator.py index 5a619f8..80ac66a 100644 --- a/fds_dev/translator.py +++ b/fds_dev/translator.py @@ -1,87 +1,91 @@ -import requests -from abc import ABC, abstractmethod -from typing import Dict - -class BaseTranslator(ABC): - @abstractmethod - def translate(self, text: str, source_lang: str, target_lang: str) -> str: - pass - -class EchoTranslator(BaseTranslator): - """ - A simple translator for testing purposes. It returns the original text, - prefixed with the language codes. - """ - def translate(self, text: str, source_lang: str, target_lang: str) -> str: - return f"[{source_lang} -> {target_lang}] {text}" - -class DeepLTranslator(BaseTranslator): - """ - A translator using the DeepL API. - Requires an API key to be set in the config. - """ - def __init__(self, api_key: str, is_free_api: bool = True): - self.api_key = api_key - self.api_url = "https://api-free.deepl.com/v2/translate" if is_free_api else "https://api.deepl.com/v2/translate" - - def translate(self, text: str, source_lang: str, target_lang: str) -> str: - if not self.api_key: - raise ValueError("DeepL API key is not provided.") - - payload = { - "auth_key": self.api_key, - "text": text, - "source_lang": source_lang.upper(), - "target_lang": target_lang.upper(), - } - - try: - response = requests.post(self.api_url, data=payload) - response.raise_for_status() - result = response.json() - return result['translations'][0]['text'] - except requests.exceptions.RequestException as e: - print(f"Error calling DeepL API: {e}") - return f"[API Error] {text}" - except (KeyError, IndexError): - print(f"Error parsing DeepL API response: {response.text}") - return f"[API Error] {text}" - - -class TranslationEngine: - """ - Manages different translation providers. - """ - def __init__(self, config: Dict): - self.config = config.get('translator', {}) - self.provider_name = self.config.get('provider', 'echo') - self.translator = self._get_translator() - - def _get_translator(self) -> BaseTranslator: - if self.provider_name == 'deepl': - api_key = self.config.get('api_key') - if not api_key: - print("Warning: DeepL provider selected but no api_key found in config. Falling back to echo.") - return EchoTranslator() - is_free = self.config.get('free_api', True) - return DeepLTranslator(api_key=api_key, is_free_api=is_free) - - # Default to echo translator - return EchoTranslator() - - def translate(self, text: str, source_lang: str, target_lang: str) -> str: - return self.translator.translate(text, source_lang, target_lang) - -if __name__ == '__main__': - # Example usage: - # To test, you would create a dummy config dict. - dummy_config = { - 'translator': { - 'provider': 'echo' # or 'deepl', if you have an API key - # 'api_key': 'YOUR_DEEPL_API_KEY_HERE' - } - } - - engine = TranslationEngine(dummy_config) - translated_text = engine.translate("안녕하세요, 세상!", source_lang="ko", target_lang="en") - print(f"Translation result: {translated_text}") +"""FDS-Dev module.""" + +import requests +from abc import ABC, abstractmethod +from typing import Dict + +class BaseTranslator(ABC): + @abstractmethod + def translate(self, text: str, source_lang: str, target_lang: str) -> str: + pass + +class EchoTranslator(BaseTranslator): + """ + A simple translator for testing purposes. It returns the original text, + prefixed with the language codes. + """ + def translate(self, text: str, source_lang: str, target_lang: str) -> str: + return f"[{source_lang} -> {target_lang}] {text}" + +class DeepLTranslator(BaseTranslator): + """ + A translator using the DeepL API. + Requires an API key to be set in the config. + """ + def __init__(self, api_key: str, is_free_api: bool = True, timeout: float = 5.0): + self.api_key = api_key + self.api_url = "https://api-free.deepl.com/v2/translate" if is_free_api else "https://api.deepl.com/v2/translate" + self.timeout = timeout + + def translate(self, text: str, source_lang: str, target_lang: str) -> str: + if not self.api_key: + raise ValueError("DeepL API key is not provided.") + + payload = { + "auth_key": self.api_key, + "text": text, + "source_lang": source_lang.upper(), + "target_lang": target_lang.upper(), + } + + try: + response = requests.post(self.api_url, data=payload, timeout=self.timeout) + response.raise_for_status() + result = response.json() + return result['translations'][0]['text'] + except requests.exceptions.RequestException as e: + print(f"Error calling DeepL API: {e}") + return f"[API Error: {e}] {text}" + except (KeyError, IndexError): + print(f"Error parsing DeepL API response: {response.text}") + return f"[API Error] {text}" + + +class TranslationEngine: + """ + Manages different translation providers. + """ + def __init__(self, config: Dict): + self.config = config.get('translator', {}) + self.provider_name = self.config.get('provider', 'echo') + self.translator = self._get_translator() + + def _get_translator(self) -> BaseTranslator: + if self.provider_name == 'deepl': + api_key = self.config.get('api_key') + if not api_key: + print("Warning: DeepL provider selected but no api_key found in config. Falling back to echo.") + return EchoTranslator() + is_free = self.config.get('free_api', True) + timeout = float(self.config.get('timeout', 5.0)) + return DeepLTranslator(api_key=api_key, is_free_api=is_free, timeout=timeout) + + # Default to echo translator + return EchoTranslator() + + def translate(self, text: str, source_lang: str, target_lang: str) -> str: + return self.translator.translate(text, source_lang, target_lang) + +if __name__ == '__main__': + # Example usage: + # To test, you would create a dummy config dict. + dummy_config = { + 'translator': { + 'provider': 'echo' # or 'deepl', if you have an API key + # 'api_key': 'YOUR_DEEPL_API_KEY_HERE' + } + } + + engine = TranslationEngine(dummy_config) + translated_text = engine.translate("안녕하세요, 세상!", source_lang="ko", target_lang="en") + print(f"Translation result: {translated_text}")