auto-i18n-lib 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- auto_i18n_lib-0.1.0/LICENSE +21 -0
- auto_i18n_lib-0.1.0/PKG-INFO +141 -0
- auto_i18n_lib-0.1.0/README.md +130 -0
- auto_i18n_lib-0.1.0/pyproject.toml +12 -0
- auto_i18n_lib-0.1.0/setup.cfg +4 -0
- auto_i18n_lib-0.1.0/src/auto_i18n_lib.egg-info/PKG-INFO +141 -0
- auto_i18n_lib-0.1.0/src/auto_i18n_lib.egg-info/SOURCES.txt +13 -0
- auto_i18n_lib-0.1.0/src/auto_i18n_lib.egg-info/dependency_links.txt +1 -0
- auto_i18n_lib-0.1.0/src/auto_i18n_lib.egg-info/top_level.txt +1 -0
- auto_i18n_lib-0.1.0/src/autoi18n/__init__.py +2 -0
- auto_i18n_lib-0.1.0/src/autoi18n/cache.py +0 -0
- auto_i18n_lib-0.1.0/src/autoi18n/cli.py +0 -0
- auto_i18n_lib-0.1.0/src/autoi18n/glossary.py +0 -0
- auto_i18n_lib-0.1.0/src/autoi18n/translator.py +206 -0
- auto_i18n_lib-0.1.0/src/autoi18n/utils.py +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 BONA
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: auto-i18n-lib
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Dynamic HTML translation with OpenAI
|
|
5
|
+
Author-email: Your Name <you@example.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Dynamic: license-file
|
|
11
|
+
|
|
12
|
+
# autoi18n
|
|
13
|
+
|
|
14
|
+
Runtime HTML i18n with OpenAI: translate the page on first request, cache results to JSON, reuse on subsequent requests, and auto-sync when the source changes.
|
|
15
|
+
|
|
16
|
+
**PyPI name:** `autoi18n` | **Import:** `autoi18n`
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Features
|
|
21
|
+
|
|
22
|
+
- 🔁 On-the-fly translation of raw HTML strings
|
|
23
|
+
- 🗂️ JSON cache per target language (on disk)
|
|
24
|
+
- 🔄 Auto-sync: stale entries removed when source changes
|
|
25
|
+
- 🧩 Chunking for model context limits (long pages split safely)
|
|
26
|
+
- 🛡️ Preserves `<script>`/`<style>` as-is
|
|
27
|
+
- 🎛️ Concise UI translation (e.g., buttons)
|
|
28
|
+
- 🌐 Helpers: detect browser lang, pick alternate lang
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install autoi18n
|
|
36
|
+
```
|
|
37
|
+
## Quick start
|
|
38
|
+
```
|
|
39
|
+
from autoi18n import Translator
|
|
40
|
+
|
|
41
|
+
html_in = "<h1>Добро пожаловать</h1><p>Это тест.</p>"
|
|
42
|
+
tr = Translator(
|
|
43
|
+
source_lang="ru",
|
|
44
|
+
cache_dir="./translations", # JSON cache folder
|
|
45
|
+
# api_key="sk-..." # or use OPENAI_API_KEY env var
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
html_out = tr.translate_html(html_in, target_lang="en", page_name="page")
|
|
49
|
+
print(html_out)
|
|
50
|
+
First call translates via OpenAI and writes ./translations/page.en.json.
|
|
51
|
+
Next calls reuse the cache and only translate new/changed strings.
|
|
52
|
+
```
|
|
53
|
+
## Environment
|
|
54
|
+
```
|
|
55
|
+
OPENAI_API_KEY — used if api_key not passed to Translator.
|
|
56
|
+
```
|
|
57
|
+
## API
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
Translator(source_lang="ru", cache_dir="./translations", api_key=None)
|
|
61
|
+
translate_html(html: str, target_lang: str, page_name: str = "page") -> str
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Translates visible text nodes, preserves script/style, chunks long text, updates cache JSON.
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
translate_text(text: str, target_lang: str, page_name: str = "page") -> str
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Low-level single-string translation with caching.
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
detect_browser_lang(accept_language_header: str) -> str
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Parses Accept-Language → short code like ru, en, fr.
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
get_alternative_lang(current: str, browser_lang: str) -> str
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Returns alternate code for a language toggle.
|
|
84
|
+
Cache layout
|
|
85
|
+
|
|
86
|
+
translations/
|
|
87
|
+
└─ page.en.json # UTF-8 JSON: { "source": "translated", ... }
|
|
88
|
+
Example:
|
|
89
|
+
|
|
90
|
+
``` json (example)
|
|
91
|
+
{
|
|
92
|
+
"Добро пожаловать": "Welcome",
|
|
93
|
+
"Выберите язык:": "Choose a language:"
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
## Minimal FastAPI wiring (example) python
|
|
97
|
+
```
|
|
98
|
+
from fastapi import FastAPI, Request
|
|
99
|
+
from autoi18n import Translator
|
|
100
|
+
from pathlib import Path
|
|
101
|
+
|
|
102
|
+
app = FastAPI()
|
|
103
|
+
tr = Translator(source_lang="ru", cache_dir="./translations")
|
|
104
|
+
HTML_PATH = Path("index.html")
|
|
105
|
+
|
|
106
|
+
@app.get("/")
|
|
107
|
+
def home():
|
|
108
|
+
return HTML_PATH.read_text(encoding="utf-8")
|
|
109
|
+
|
|
110
|
+
@app.get("/detect_lang")
|
|
111
|
+
def detect_lang(request: Request):
|
|
112
|
+
return {"lang": tr.detect_browser_lang(request.headers.get("accept-language", ""))}
|
|
113
|
+
|
|
114
|
+
@app.get("/alt_lang")
|
|
115
|
+
def alt_lang(request: Request, current: str = "ru"):
|
|
116
|
+
browser = tr.detect_browser_lang(request.headers.get("accept-language", ""))
|
|
117
|
+
return {"lang": tr.get_alternative_lang(current, browser)}
|
|
118
|
+
|
|
119
|
+
@app.get("/translate")
|
|
120
|
+
def translate(lang: str = "en"):
|
|
121
|
+
html = HTML_PATH.read_text(encoding="utf-8")
|
|
122
|
+
return tr.translate_html(html, target_lang=lang, page_name="page")
|
|
123
|
+
```
|
|
124
|
+
# Notes & limits
|
|
125
|
+
You control OpenAI usage/billing.
|
|
126
|
+
|
|
127
|
+
Chunking keeps requests within model limits; HTML structure preserved.
|
|
128
|
+
|
|
129
|
+
Short labels (buttons) use a concise prompt to avoid verbose text.
|
|
130
|
+
|
|
131
|
+
This package is for runtime translation with caching; static catalogs are out of scope.
|
|
132
|
+
|
|
133
|
+
# Versioning
|
|
134
|
+
Semantic Versioning: MAJOR.MINOR.PATCH.
|
|
135
|
+
|
|
136
|
+
# License
|
|
137
|
+
MIT © BONA
|
|
138
|
+
|
|
139
|
+
# Support
|
|
140
|
+
Issues & feature requests → GitHub Issues (see project URLs in metadata).
|
|
141
|
+
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# autoi18n
|
|
2
|
+
|
|
3
|
+
Runtime HTML i18n with OpenAI: translate the page on first request, cache results to JSON, reuse on subsequent requests, and auto-sync when the source changes.
|
|
4
|
+
|
|
5
|
+
**PyPI name:** `autoi18n` | **Import:** `autoi18n`
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- 🔁 On-the-fly translation of raw HTML strings
|
|
12
|
+
- 🗂️ JSON cache per target language (on disk)
|
|
13
|
+
- 🔄 Auto-sync: stale entries removed when source changes
|
|
14
|
+
- 🧩 Chunking for model context limits (long pages split safely)
|
|
15
|
+
- 🛡️ Preserves `<script>`/`<style>` as-is
|
|
16
|
+
- 🎛️ Concise UI translation (e.g., buttons)
|
|
17
|
+
- 🌐 Helpers: detect browser lang, pick alternate lang
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install autoi18n
|
|
25
|
+
```
|
|
26
|
+
## Quick start
|
|
27
|
+
```
|
|
28
|
+
from autoi18n import Translator
|
|
29
|
+
|
|
30
|
+
html_in = "<h1>Добро пожаловать</h1><p>Это тест.</p>"
|
|
31
|
+
tr = Translator(
|
|
32
|
+
source_lang="ru",
|
|
33
|
+
cache_dir="./translations", # JSON cache folder
|
|
34
|
+
# api_key="sk-..." # or use OPENAI_API_KEY env var
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
html_out = tr.translate_html(html_in, target_lang="en", page_name="page")
|
|
38
|
+
print(html_out)
|
|
39
|
+
First call translates via OpenAI and writes ./translations/page.en.json.
|
|
40
|
+
Next calls reuse the cache and only translate new/changed strings.
|
|
41
|
+
```
|
|
42
|
+
## Environment
|
|
43
|
+
```
|
|
44
|
+
OPENAI_API_KEY — used if api_key not passed to Translator.
|
|
45
|
+
```
|
|
46
|
+
## API
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
Translator(source_lang="ru", cache_dir="./translations", api_key=None)
|
|
50
|
+
translate_html(html: str, target_lang: str, page_name: str = "page") -> str
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Translates visible text nodes, preserves script/style, chunks long text, updates cache JSON.
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
translate_text(text: str, target_lang: str, page_name: str = "page") -> str
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Low-level single-string translation with caching.
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
detect_browser_lang(accept_language_header: str) -> str
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Parses Accept-Language → short code like ru, en, fr.
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
get_alternative_lang(current: str, browser_lang: str) -> str
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Returns alternate code for a language toggle.
|
|
73
|
+
Cache layout
|
|
74
|
+
|
|
75
|
+
translations/
|
|
76
|
+
└─ page.en.json # UTF-8 JSON: { "source": "translated", ... }
|
|
77
|
+
Example:
|
|
78
|
+
|
|
79
|
+
``` json (example)
|
|
80
|
+
{
|
|
81
|
+
"Добро пожаловать": "Welcome",
|
|
82
|
+
"Выберите язык:": "Choose a language:"
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
## Minimal FastAPI wiring (example) python
|
|
86
|
+
```
|
|
87
|
+
from fastapi import FastAPI, Request
|
|
88
|
+
from autoi18n import Translator
|
|
89
|
+
from pathlib import Path
|
|
90
|
+
|
|
91
|
+
app = FastAPI()
|
|
92
|
+
tr = Translator(source_lang="ru", cache_dir="./translations")
|
|
93
|
+
HTML_PATH = Path("index.html")
|
|
94
|
+
|
|
95
|
+
@app.get("/")
|
|
96
|
+
def home():
|
|
97
|
+
return HTML_PATH.read_text(encoding="utf-8")
|
|
98
|
+
|
|
99
|
+
@app.get("/detect_lang")
|
|
100
|
+
def detect_lang(request: Request):
|
|
101
|
+
return {"lang": tr.detect_browser_lang(request.headers.get("accept-language", ""))}
|
|
102
|
+
|
|
103
|
+
@app.get("/alt_lang")
|
|
104
|
+
def alt_lang(request: Request, current: str = "ru"):
|
|
105
|
+
browser = tr.detect_browser_lang(request.headers.get("accept-language", ""))
|
|
106
|
+
return {"lang": tr.get_alternative_lang(current, browser)}
|
|
107
|
+
|
|
108
|
+
@app.get("/translate")
|
|
109
|
+
def translate(lang: str = "en"):
|
|
110
|
+
html = HTML_PATH.read_text(encoding="utf-8")
|
|
111
|
+
return tr.translate_html(html, target_lang=lang, page_name="page")
|
|
112
|
+
```
|
|
113
|
+
# Notes & limits
|
|
114
|
+
You control OpenAI usage/billing.
|
|
115
|
+
|
|
116
|
+
Chunking keeps requests within model limits; HTML structure preserved.
|
|
117
|
+
|
|
118
|
+
Short labels (buttons) use a concise prompt to avoid verbose text.
|
|
119
|
+
|
|
120
|
+
This package is for runtime translation with caching; static catalogs are out of scope.
|
|
121
|
+
|
|
122
|
+
# Versioning
|
|
123
|
+
Semantic Versioning: MAJOR.MINOR.PATCH.
|
|
124
|
+
|
|
125
|
+
# License
|
|
126
|
+
MIT © BONA
|
|
127
|
+
|
|
128
|
+
# Support
|
|
129
|
+
Issues & feature requests → GitHub Issues (see project URLs in metadata).
|
|
130
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "auto-i18n-lib"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Dynamic HTML translation with OpenAI"
|
|
9
|
+
authors = [{name = "Your Name", email = "you@example.com"}]
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
requires-python = ">=3.9"
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: auto-i18n-lib
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Dynamic HTML translation with OpenAI
|
|
5
|
+
Author-email: Your Name <you@example.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Dynamic: license-file
|
|
11
|
+
|
|
12
|
+
# autoi18n
|
|
13
|
+
|
|
14
|
+
Runtime HTML i18n with OpenAI: translate the page on first request, cache results to JSON, reuse on subsequent requests, and auto-sync when the source changes.
|
|
15
|
+
|
|
16
|
+
**PyPI name:** `autoi18n` | **Import:** `autoi18n`
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Features
|
|
21
|
+
|
|
22
|
+
- 🔁 On-the-fly translation of raw HTML strings
|
|
23
|
+
- 🗂️ JSON cache per target language (on disk)
|
|
24
|
+
- 🔄 Auto-sync: stale entries removed when source changes
|
|
25
|
+
- 🧩 Chunking for model context limits (long pages split safely)
|
|
26
|
+
- 🛡️ Preserves `<script>`/`<style>` as-is
|
|
27
|
+
- 🎛️ Concise UI translation (e.g., buttons)
|
|
28
|
+
- 🌐 Helpers: detect browser lang, pick alternate lang
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install autoi18n
|
|
36
|
+
```
|
|
37
|
+
## Quick start
|
|
38
|
+
```
|
|
39
|
+
from autoi18n import Translator
|
|
40
|
+
|
|
41
|
+
html_in = "<h1>Добро пожаловать</h1><p>Это тест.</p>"
|
|
42
|
+
tr = Translator(
|
|
43
|
+
source_lang="ru",
|
|
44
|
+
cache_dir="./translations", # JSON cache folder
|
|
45
|
+
# api_key="sk-..." # or use OPENAI_API_KEY env var
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
html_out = tr.translate_html(html_in, target_lang="en", page_name="page")
|
|
49
|
+
print(html_out)
|
|
50
|
+
First call translates via OpenAI and writes ./translations/page.en.json.
|
|
51
|
+
Next calls reuse the cache and only translate new/changed strings.
|
|
52
|
+
```
|
|
53
|
+
## Environment
|
|
54
|
+
```
|
|
55
|
+
OPENAI_API_KEY — used if api_key not passed to Translator.
|
|
56
|
+
```
|
|
57
|
+
## API
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
Translator(source_lang="ru", cache_dir="./translations", api_key=None)
|
|
61
|
+
translate_html(html: str, target_lang: str, page_name: str = "page") -> str
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Translates visible text nodes, preserves script/style, chunks long text, updates cache JSON.
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
translate_text(text: str, target_lang: str, page_name: str = "page") -> str
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Low-level single-string translation with caching.
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
detect_browser_lang(accept_language_header: str) -> str
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Parses Accept-Language → short code like ru, en, fr.
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
get_alternative_lang(current: str, browser_lang: str) -> str
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Returns alternate code for a language toggle.
|
|
84
|
+
Cache layout
|
|
85
|
+
|
|
86
|
+
translations/
|
|
87
|
+
└─ page.en.json # UTF-8 JSON: { "source": "translated", ... }
|
|
88
|
+
Example:
|
|
89
|
+
|
|
90
|
+
``` json (example)
|
|
91
|
+
{
|
|
92
|
+
"Добро пожаловать": "Welcome",
|
|
93
|
+
"Выберите язык:": "Choose a language:"
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
## Minimal FastAPI wiring (example) python
|
|
97
|
+
```
|
|
98
|
+
from fastapi import FastAPI, Request
|
|
99
|
+
from autoi18n import Translator
|
|
100
|
+
from pathlib import Path
|
|
101
|
+
|
|
102
|
+
app = FastAPI()
|
|
103
|
+
tr = Translator(source_lang="ru", cache_dir="./translations")
|
|
104
|
+
HTML_PATH = Path("index.html")
|
|
105
|
+
|
|
106
|
+
@app.get("/")
|
|
107
|
+
def home():
|
|
108
|
+
return HTML_PATH.read_text(encoding="utf-8")
|
|
109
|
+
|
|
110
|
+
@app.get("/detect_lang")
|
|
111
|
+
def detect_lang(request: Request):
|
|
112
|
+
return {"lang": tr.detect_browser_lang(request.headers.get("accept-language", ""))}
|
|
113
|
+
|
|
114
|
+
@app.get("/alt_lang")
|
|
115
|
+
def alt_lang(request: Request, current: str = "ru"):
|
|
116
|
+
browser = tr.detect_browser_lang(request.headers.get("accept-language", ""))
|
|
117
|
+
return {"lang": tr.get_alternative_lang(current, browser)}
|
|
118
|
+
|
|
119
|
+
@app.get("/translate")
|
|
120
|
+
def translate(lang: str = "en"):
|
|
121
|
+
html = HTML_PATH.read_text(encoding="utf-8")
|
|
122
|
+
return tr.translate_html(html, target_lang=lang, page_name="page")
|
|
123
|
+
```
|
|
124
|
+
# Notes & limits
|
|
125
|
+
You control OpenAI usage/billing.
|
|
126
|
+
|
|
127
|
+
Chunking keeps requests within model limits; HTML structure preserved.
|
|
128
|
+
|
|
129
|
+
Short labels (buttons) use a concise prompt to avoid verbose text.
|
|
130
|
+
|
|
131
|
+
This package is for runtime translation with caching; static catalogs are out of scope.
|
|
132
|
+
|
|
133
|
+
# Versioning
|
|
134
|
+
Semantic Versioning: MAJOR.MINOR.PATCH.
|
|
135
|
+
|
|
136
|
+
# License
|
|
137
|
+
MIT © BONA
|
|
138
|
+
|
|
139
|
+
# Support
|
|
140
|
+
Issues & feature requests → GitHub Issues (see project URLs in metadata).
|
|
141
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/auto_i18n_lib.egg-info/PKG-INFO
|
|
5
|
+
src/auto_i18n_lib.egg-info/SOURCES.txt
|
|
6
|
+
src/auto_i18n_lib.egg-info/dependency_links.txt
|
|
7
|
+
src/auto_i18n_lib.egg-info/top_level.txt
|
|
8
|
+
src/autoi18n/__init__.py
|
|
9
|
+
src/autoi18n/cache.py
|
|
10
|
+
src/autoi18n/cli.py
|
|
11
|
+
src/autoi18n/glossary.py
|
|
12
|
+
src/autoi18n/translator.py
|
|
13
|
+
src/autoi18n/utils.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
autoi18n
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
from html.parser import HTMLParser
|
|
4
|
+
from openai import OpenAI
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SimpleHTMLTranslator(HTMLParser):
|
|
8
|
+
def __init__(self, translate_callback):
|
|
9
|
+
super().__init__()
|
|
10
|
+
self.result = []
|
|
11
|
+
self.translate_callback = translate_callback
|
|
12
|
+
self._current_tag = None
|
|
13
|
+
self._inside_skip = False
|
|
14
|
+
|
|
15
|
+
def handle_starttag(self, tag, attrs):
|
|
16
|
+
self._current_tag = tag
|
|
17
|
+
self._inside_skip = False
|
|
18
|
+
|
|
19
|
+
if tag in ("script", "style"):
|
|
20
|
+
self._inside_skip = True
|
|
21
|
+
|
|
22
|
+
# Кнопка langSwitch: пропускаем содержимое, но сам тег сохраняем
|
|
23
|
+
for attr, value in attrs:
|
|
24
|
+
if attr == "id" and value == "langSwitch":
|
|
25
|
+
self._inside_skip = True
|
|
26
|
+
|
|
27
|
+
self.result.append(self.get_starttag_text())
|
|
28
|
+
|
|
29
|
+
def handle_endtag(self, tag):
|
|
30
|
+
if tag in ("script", "style") and self._inside_skip:
|
|
31
|
+
self._inside_skip = False
|
|
32
|
+
if tag == "button" and self._inside_skip:
|
|
33
|
+
self._inside_skip = False
|
|
34
|
+
self.result.append(f"</{tag}>")
|
|
35
|
+
|
|
36
|
+
def handle_data(self, data):
|
|
37
|
+
if self._inside_skip:
|
|
38
|
+
# ⚡ Сохраняем содержимое <script> и <style> без изменений
|
|
39
|
+
self.result.append(data)
|
|
40
|
+
elif self._current_tag == "button":
|
|
41
|
+
# Переводим обычные кнопки
|
|
42
|
+
translated = self.translate_callback(data, prompt_type="button")
|
|
43
|
+
self.result.append(translated)
|
|
44
|
+
else:
|
|
45
|
+
# Переводим обычный текст
|
|
46
|
+
translated = self.translate_callback(data)
|
|
47
|
+
self.result.append(translated)
|
|
48
|
+
|
|
49
|
+
def get_html(self):
|
|
50
|
+
return "".join(self.result)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class Translator:
|
|
56
|
+
def __init__(self, cache_dir="./translations", api_key=None):
|
|
57
|
+
# ⚡ Берём язык исходника из .env
|
|
58
|
+
self.source_lang = os.getenv("SOURCE_LANG", "ru")
|
|
59
|
+
self.cache_dir = cache_dir
|
|
60
|
+
self.client = OpenAI(api_key=api_key)
|
|
61
|
+
self._current_file = None
|
|
62
|
+
self._cache = {}
|
|
63
|
+
|
|
64
|
+
def _file_path(self, page_name: str, lang: str) -> str:
|
|
65
|
+
filename = f"{page_name}.{lang}.json"
|
|
66
|
+
return os.path.join(self.cache_dir, filename)
|
|
67
|
+
|
|
68
|
+
def _load_storage(self, page_name: str, lang: str):
|
|
69
|
+
path = self._file_path(page_name, lang)
|
|
70
|
+
if os.path.exists(path):
|
|
71
|
+
print(f"[CACHE] Загружаем переводы из {path}")
|
|
72
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
73
|
+
self._cache = json.load(f)
|
|
74
|
+
else:
|
|
75
|
+
print(f"[CACHE] Файл {path} не найден — создаём новый")
|
|
76
|
+
self._cache = {}
|
|
77
|
+
self._current_file = path
|
|
78
|
+
|
|
79
|
+
def _save_storage(self):
|
|
80
|
+
if self._current_file:
|
|
81
|
+
# ⚡ Создаём папку только при сохранении
|
|
82
|
+
os.makedirs(self.cache_dir, exist_ok=True)
|
|
83
|
+
with open(self._current_file, "w", encoding="utf-8") as f:
|
|
84
|
+
json.dump(self._cache, f, ensure_ascii=False, indent=2)
|
|
85
|
+
print(f"[CACHE] Переводы сохранены в {self._current_file}")
|
|
86
|
+
|
|
87
|
+
def translate_text(self, text: str, target_lang: str, page_name="page", prompt_type="normal") -> str:
|
|
88
|
+
text = text.strip()
|
|
89
|
+
if not text:
|
|
90
|
+
return text
|
|
91
|
+
|
|
92
|
+
if target_lang == self.source_lang:
|
|
93
|
+
return text
|
|
94
|
+
|
|
95
|
+
if not self._current_file:
|
|
96
|
+
self._load_storage(page_name, target_lang)
|
|
97
|
+
|
|
98
|
+
if text in self._cache:
|
|
99
|
+
return self._cache[text]
|
|
100
|
+
|
|
101
|
+
# ⚡ Разные подсказки для кнопок и для обычного текста
|
|
102
|
+
if prompt_type == "button":
|
|
103
|
+
prompt = f"Translate this button label briefly from {self.source_lang} to {target_lang}:\n\n{text}"
|
|
104
|
+
else:
|
|
105
|
+
prompt = f"Translate this text from {self.source_lang} to {target_lang}:\n\n{text}"
|
|
106
|
+
|
|
107
|
+
response = self.client.chat.completions.create(
|
|
108
|
+
model="gpt-4o-mini",
|
|
109
|
+
messages=[{"role": "user", "content": prompt}],
|
|
110
|
+
)
|
|
111
|
+
translated = response.choices[0].message.content.strip()
|
|
112
|
+
|
|
113
|
+
self._cache[text] = translated
|
|
114
|
+
self._save_storage()
|
|
115
|
+
|
|
116
|
+
return translated
|
|
117
|
+
|
|
118
|
+
def translate_html(self, html: str, target_lang: str, page_name="page") -> str:
|
|
119
|
+
# ⚡ Если язык совпадает с исходным — возвращаем как есть
|
|
120
|
+
if target_lang == self.source_lang:
|
|
121
|
+
print(f"[SKIP] {self.source_lang} → {target_lang}: язык совпадает, возвращаем исходную страницу")
|
|
122
|
+
return html
|
|
123
|
+
|
|
124
|
+
# Загружаем кэш
|
|
125
|
+
self._load_storage(page_name, target_lang)
|
|
126
|
+
|
|
127
|
+
current_texts = [] # все тексты из текущей HTML-страницы
|
|
128
|
+
|
|
129
|
+
def translate_with_chunks(text: str, prompt_type: str = "normal") -> str:
|
|
130
|
+
text = text.strip()
|
|
131
|
+
if not text:
|
|
132
|
+
return text
|
|
133
|
+
|
|
134
|
+
current_texts.append(text)
|
|
135
|
+
|
|
136
|
+
# Проверяем кэш
|
|
137
|
+
if text in self._cache:
|
|
138
|
+
print(f"[CACHE-HIT] {self.source_lang} → {target_lang}: «{text[:40]}...»")
|
|
139
|
+
return self._cache[text]
|
|
140
|
+
|
|
141
|
+
# Если текст длинный — режем
|
|
142
|
+
if len(text) > 2000:
|
|
143
|
+
print(f"[SPLIT] Текст длинный ({len(text)} символов), режем на чанки")
|
|
144
|
+
step = 1500
|
|
145
|
+
chunks = [text[i:i + step] for i in range(0, len(text), step)]
|
|
146
|
+
else:
|
|
147
|
+
chunks = [text]
|
|
148
|
+
|
|
149
|
+
translated_parts = []
|
|
150
|
+
for chunk in chunks:
|
|
151
|
+
if prompt_type == "button":
|
|
152
|
+
prompt = f"Translate this button label to {target_lang}. Keep it short, return only the translated label:\n\n{chunk}"
|
|
153
|
+
else:
|
|
154
|
+
prompt = f"Translate to {target_lang}. Return only the translated text, no explanations:\n\n{chunk}"
|
|
155
|
+
|
|
156
|
+
print(f"[OPENAI] {self.source_lang} → {target_lang}: «{chunk[:40]}...»")
|
|
157
|
+
response = self.client.chat.completions.create(
|
|
158
|
+
model="gpt-4o-mini",
|
|
159
|
+
messages=[{"role": "user", "content": prompt}],
|
|
160
|
+
)
|
|
161
|
+
translated_parts.append(response.choices[0].message.content.strip())
|
|
162
|
+
|
|
163
|
+
translated_text = " ".join(translated_parts)
|
|
164
|
+
|
|
165
|
+
self._cache[text] = translated_text
|
|
166
|
+
self._save_storage()
|
|
167
|
+
print(f"[OPENAI-RESULT] «{translated_text[:60]}...»")
|
|
168
|
+
|
|
169
|
+
return translated_text
|
|
170
|
+
|
|
171
|
+
# Парсим HTML
|
|
172
|
+
parser = SimpleHTMLTranslator(
|
|
173
|
+
translate_callback=translate_with_chunks
|
|
174
|
+
)
|
|
175
|
+
parser.feed(html)
|
|
176
|
+
|
|
177
|
+
# ⚡ Синхронизация — удаляем устаревшие строки
|
|
178
|
+
removed = []
|
|
179
|
+
for old_key in list(self._cache.keys()):
|
|
180
|
+
if old_key not in current_texts:
|
|
181
|
+
removed.append(old_key)
|
|
182
|
+
self._cache.pop(old_key)
|
|
183
|
+
|
|
184
|
+
if removed:
|
|
185
|
+
print(f"[SYNC] Удалены устаревшие строки: {len(removed)}")
|
|
186
|
+
self._save_storage()
|
|
187
|
+
|
|
188
|
+
return parser.get_html()
|
|
189
|
+
|
|
190
|
+
def _collect_and_translate(self, text, target_lang, page_name, current_texts):
|
|
191
|
+
text = text.strip()
|
|
192
|
+
if text:
|
|
193
|
+
current_texts.append(text)
|
|
194
|
+
return self.translate_text(text, target_lang, page_name)
|
|
195
|
+
|
|
196
|
+
def detect_browser_lang(self, accept_language: str) -> str:
|
|
197
|
+
"""Определяем язык браузера из заголовка Accept-Language"""
|
|
198
|
+
if not accept_language:
|
|
199
|
+
return self.source_lang
|
|
200
|
+
return accept_language.split(",")[0].split("-")[0]
|
|
201
|
+
|
|
202
|
+
def get_alternative_lang(self, current_lang: str, browser_lang: str) -> str:
|
|
203
|
+
"""Определяем какой язык должен быть на кнопке"""
|
|
204
|
+
if current_lang == browser_lang:
|
|
205
|
+
return "en" # если мы на языке браузера → кнопка EN
|
|
206
|
+
return browser_lang # если мы на EN → кнопка = язык браузера
|
|
File without changes
|