strx-precommit 0.1.1__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.
- strx_precommit-0.1.1/PKG-INFO +17 -0
- strx_precommit-0.1.1/README.md +0 -0
- strx_precommit-0.1.1/pyproject.toml +44 -0
- strx_precommit-0.1.1/setup.cfg +4 -0
- strx_precommit-0.1.1/setup.py +49 -0
- strx_precommit-0.1.1/src/strx_precommit/__init__.py +5 -0
- strx_precommit-0.1.1/src/strx_precommit/strx_convert_string.py +172 -0
- strx_precommit-0.1.1/src/strx_precommit/strx_translate_po.py +179 -0
- strx_precommit-0.1.1/src/strx_precommit/strx_update_po.py +450 -0
- strx_precommit-0.1.1/src/strx_precommit.egg-info/PKG-INFO +17 -0
- strx_precommit-0.1.1/src/strx_precommit.egg-info/SOURCES.txt +11 -0
- strx_precommit-0.1.1/src/strx_precommit.egg-info/dependency_links.txt +1 -0
- strx_precommit-0.1.1/src/strx_precommit.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: strx-precommit
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Pre-commit hooks para conversión de strings y traducción de archivos PO
|
|
5
|
+
Home-page: https://github.com/straconxsa/strx_tools/
|
|
6
|
+
Author: Tu Nombre
|
|
7
|
+
Author-email: Initium Team <info@straconx.com>
|
|
8
|
+
Project-URL: Homepage, https://github.com/straconxsa/strx_tools/
|
|
9
|
+
Project-URL: Bug Tracker, https://github.com/straconxsa/strx_tools/issues
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Requires-Python: >=3.6
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
Dynamic: author
|
|
16
|
+
Dynamic: home-page
|
|
17
|
+
Dynamic: requires-python
|
|
File without changes
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=42", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "strx-precommit"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "Pre-commit hooks para conversión de strings y traducción de archivos PO"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.6"
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "Initium Team", email = "info@straconx.com" },
|
|
13
|
+
]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.urls]
|
|
21
|
+
"Homepage" = "https://github.com/straconxsa/strx_tools/"
|
|
22
|
+
"Bug Tracker" = "https://github.com/straconxsa/strx_tools/issues"
|
|
23
|
+
|
|
24
|
+
[tool.black]
|
|
25
|
+
line-length = 88
|
|
26
|
+
target-version = ["py36", "py37", "py38", "py39"]
|
|
27
|
+
include = '\.pyi?$'
|
|
28
|
+
|
|
29
|
+
[tool.isort]
|
|
30
|
+
profile = "black"
|
|
31
|
+
multi_line_output = 3
|
|
32
|
+
|
|
33
|
+
[tool.mypy]
|
|
34
|
+
python_version = "3.6"
|
|
35
|
+
warn_return_any = true
|
|
36
|
+
warn_unused_configs = true
|
|
37
|
+
mypy_path = "src"
|
|
38
|
+
|
|
39
|
+
[tool.pytest.ini_options]
|
|
40
|
+
minversion = "6.0"
|
|
41
|
+
addopts = "-ra -q"
|
|
42
|
+
testpaths = [
|
|
43
|
+
"tests",
|
|
44
|
+
]
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Estructura de directorios:
|
|
2
|
+
# strx_precommit/
|
|
3
|
+
# ├── src/
|
|
4
|
+
# │ └── strx_precommit/
|
|
5
|
+
# │ ├── __init__.py
|
|
6
|
+
# │ ├── strx_convert_string.py
|
|
7
|
+
# │ └── strx_translate_po.py
|
|
8
|
+
# ├── setup.py
|
|
9
|
+
# ├── pyproject.toml
|
|
10
|
+
# └── README.md
|
|
11
|
+
|
|
12
|
+
# setup.py
|
|
13
|
+
from setuptools import setup, find_packages
|
|
14
|
+
|
|
15
|
+
setup(
|
|
16
|
+
name="strx-precommit",
|
|
17
|
+
version="0.1.1",
|
|
18
|
+
packages=find_packages(where="src"),
|
|
19
|
+
package_dir={"": "src"},
|
|
20
|
+
entry_points={
|
|
21
|
+
"console_scripts": [
|
|
22
|
+
"strx-precommit=strx_precommit.strx_convert_string:main",
|
|
23
|
+
"strx-convert-string=strx_precommit.strx_convert_string:main",
|
|
24
|
+
"strx-translate-po=strx_precommit.strx_translate_po:main",
|
|
25
|
+
"strx-update-po=strx_precommit.strx_update_po:main",
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
install_requires=[
|
|
29
|
+
"pre-commit",
|
|
30
|
+
"polib",
|
|
31
|
+
"translate",
|
|
32
|
+
"GitPython",
|
|
33
|
+
],
|
|
34
|
+
author="Tu Nombre",
|
|
35
|
+
author_email="tu@email.com",
|
|
36
|
+
description="Pre-commit hooks para conversión de strings y traducción de archivos PO",
|
|
37
|
+
long_description=open("README.md").read(),
|
|
38
|
+
long_description_content_type="text/markdown",
|
|
39
|
+
url="https://github.com/straconxsa/strx_tools/",
|
|
40
|
+
project_urls={
|
|
41
|
+
"Bug Tracker": "https://github.com/straconxsa/strx_tools/issues",
|
|
42
|
+
},
|
|
43
|
+
classifiers=[
|
|
44
|
+
"Programming Language :: Python :: 3",
|
|
45
|
+
"License :: OSI Approved :: MIT License",
|
|
46
|
+
"Operating System :: OS Independent",
|
|
47
|
+
],
|
|
48
|
+
python_requires=">=3.6",
|
|
49
|
+
)
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
import logging
|
|
5
|
+
import argparse
|
|
6
|
+
import subprocess
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import List, Dict, Set, Optional
|
|
9
|
+
|
|
10
|
+
_logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
# Pattern for multiline string formatting (e.g., "hello %s" % var)
|
|
13
|
+
STRING_MODULO_PATTERN = r'(\_\()?\s*(["\'])(.*?%s.*?)\2\)?\s*%\s*([^)\n,]+?|\([^)]+\))(?=\n|\)|$)'
|
|
14
|
+
|
|
15
|
+
# Default blacklist for files and directories
|
|
16
|
+
DEFAULT_BLACKLIST = {
|
|
17
|
+
"venv",
|
|
18
|
+
"__pycache__",
|
|
19
|
+
"migrations",
|
|
20
|
+
"test_*.py",
|
|
21
|
+
"setup.py",
|
|
22
|
+
".*env.*",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class FileChange:
|
|
27
|
+
"""Stores information about a single string format change."""
|
|
28
|
+
file_path: str
|
|
29
|
+
original_line: str
|
|
30
|
+
modified_line: str
|
|
31
|
+
line_number: int
|
|
32
|
+
|
|
33
|
+
class ChangeTracker:
|
|
34
|
+
"""Tracks changes made during the conversion process."""
|
|
35
|
+
def __init__(self):
|
|
36
|
+
self.changes: Dict[str, List[FileChange]] = {}
|
|
37
|
+
self.files_reviewed: Set[str] = set()
|
|
38
|
+
self.files_modified: Set[str] = set()
|
|
39
|
+
self.files_skipped: Set[str] = set()
|
|
40
|
+
self.total_modifications = 0
|
|
41
|
+
|
|
42
|
+
def add_change(self, file_path: str, original: str, modified: str, line_number: int):
|
|
43
|
+
if file_path not in self.changes:
|
|
44
|
+
self.changes[file_path] = []
|
|
45
|
+
self.changes[file_path].append(FileChange(file_path, original, modified, line_number))
|
|
46
|
+
self.files_modified.add(file_path)
|
|
47
|
+
self.total_modifications += 1
|
|
48
|
+
|
|
49
|
+
def add_reviewed_file(self, file_path: str):
|
|
50
|
+
self.files_reviewed.add(file_path)
|
|
51
|
+
|
|
52
|
+
class StringFormatConverter:
|
|
53
|
+
"""Class for converting string formats from %s to f-strings or .format()."""
|
|
54
|
+
def __init__(self, blacklist: Optional[Set[str]] = None, use_fstrings: bool = True):
|
|
55
|
+
self.blacklist = blacklist or DEFAULT_BLACKLIST
|
|
56
|
+
self.use_fstrings = use_fstrings
|
|
57
|
+
self.tracker = ChangeTracker()
|
|
58
|
+
|
|
59
|
+
def convert_to_fstring(self, string_content: str, variable: str) -> str:
|
|
60
|
+
"""Convert a string with %s and its variable to an f-string."""
|
|
61
|
+
# Clean variable if it's a tuple (var,)
|
|
62
|
+
clean_var = variable.strip()
|
|
63
|
+
if clean_var.startswith('(') and clean_var.endswith(')'):
|
|
64
|
+
clean_var = clean_var[1:-1].strip().rstrip(',')
|
|
65
|
+
|
|
66
|
+
# Replace %s with {var}
|
|
67
|
+
# Note: This handles single %s.
|
|
68
|
+
if string_content.count('%s') == 1:
|
|
69
|
+
# Check if variable is simple enough for f-string (no complex logic)
|
|
70
|
+
# Actually f-strings handle expressions, so it's mostly fine
|
|
71
|
+
return string_content.replace('%s', '{' + clean_var + '}')
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
def process_file(self, file_path: str) -> None:
|
|
75
|
+
try:
|
|
76
|
+
self.tracker.add_reviewed_file(file_path)
|
|
77
|
+
with open(file_path, "r", encoding="utf-8") as file:
|
|
78
|
+
content = file.read()
|
|
79
|
+
|
|
80
|
+
def replace_logic(match):
|
|
81
|
+
has_translation = bool(match.group(1))
|
|
82
|
+
quote = match.group(2)
|
|
83
|
+
string_content = match.group(3)
|
|
84
|
+
variable = match.group(4).strip()
|
|
85
|
+
|
|
86
|
+
line_number = content[:match.start()].count("\n") + 1
|
|
87
|
+
|
|
88
|
+
# If it's a translation, we MUST use .format() because f-strings are not translatable
|
|
89
|
+
if has_translation:
|
|
90
|
+
# Note: Simplified variable name mapping for .format()
|
|
91
|
+
# We use the variable name itself as the key if it's simple
|
|
92
|
+
safe_var = re.sub(r'[^a-zA-Z0-9_]', '_', variable.split('.')[-1].strip('()'))
|
|
93
|
+
if not safe_var or safe_var[0].isdigit(): safe_var = "var_" + safe_var
|
|
94
|
+
|
|
95
|
+
new_str = string_content.replace('%s', '{' + safe_var + '}')
|
|
96
|
+
result = f"_({quote}{new_str}{quote}).format({safe_var}={variable})"
|
|
97
|
+
else:
|
|
98
|
+
# If not translated, try f-string
|
|
99
|
+
f_content = self.convert_to_fstring(string_content, variable)
|
|
100
|
+
if f_content and self.use_fstrings:
|
|
101
|
+
# Ensure we don't duplicate f prefix if already there (though regex doesn't match it)
|
|
102
|
+
result = f"f{quote}{f_content}{quote}"
|
|
103
|
+
else:
|
|
104
|
+
# Fallback to .format() if f-string logic fails or disabled
|
|
105
|
+
safe_var = re.sub(r'[^a-zA-Z0-9_]', '_', variable.split('.')[-1].strip('()'))
|
|
106
|
+
if not safe_var or safe_var[0].isdigit(): safe_var = "var_" + safe_var
|
|
107
|
+
new_str = string_content.replace('%s', '{' + safe_var + '}')
|
|
108
|
+
result = f"{quote}{new_str}{quote}.format({safe_var}={variable})"
|
|
109
|
+
|
|
110
|
+
self.tracker.add_change(file_path, match.group(0), result, line_number)
|
|
111
|
+
return result
|
|
112
|
+
|
|
113
|
+
new_content = re.sub(STRING_MODULO_PATTERN, replace_logic, content, flags=re.MULTILINE | re.DOTALL)
|
|
114
|
+
|
|
115
|
+
if new_content != content:
|
|
116
|
+
with open(file_path, "w", encoding="utf-8") as file:
|
|
117
|
+
file.write(new_content)
|
|
118
|
+
except Exception as e:
|
|
119
|
+
_logger.error(f"Error processing {file_path}: {e}")
|
|
120
|
+
|
|
121
|
+
def run(self, directory: str):
|
|
122
|
+
directory = os.path.abspath(directory)
|
|
123
|
+
for root, dirs, files in os.walk(directory):
|
|
124
|
+
# Apply blacklist to directories
|
|
125
|
+
dirs[:] = [d for d in dirs if not any(re.match(b.replace('*', '.*'), d) for b in self.blacklist)]
|
|
126
|
+
|
|
127
|
+
for file in files:
|
|
128
|
+
if file.endswith(".py"):
|
|
129
|
+
file_path = os.path.join(root, file)
|
|
130
|
+
if any(re.match(b.replace('*', '.*'), file) for b in self.blacklist):
|
|
131
|
+
continue
|
|
132
|
+
self.process_file(file_path)
|
|
133
|
+
|
|
134
|
+
def main():
|
|
135
|
+
parser = argparse.ArgumentParser(description="Convert Python string formats from %s to f-strings.")
|
|
136
|
+
group = parser.add_mutually_exclusive_group()
|
|
137
|
+
group.add_argument("--all", action="store_true", help="Procesar todo el repositorio (global)")
|
|
138
|
+
group.add_argument("--path", "-p", help="Procesar un directorio específico")
|
|
139
|
+
|
|
140
|
+
args = parser.parse_args()
|
|
141
|
+
|
|
142
|
+
# Determine base directory
|
|
143
|
+
if args.all:
|
|
144
|
+
try:
|
|
145
|
+
root = subprocess.run(["git", "rev-parse", "--show-toplevel"],
|
|
146
|
+
capture_output=True, text=True, check=True).stdout.strip()
|
|
147
|
+
directory = root
|
|
148
|
+
except:
|
|
149
|
+
directory = os.getcwd()
|
|
150
|
+
print(f"Modo GLOBAL: Procesando desde {directory}")
|
|
151
|
+
elif args.path:
|
|
152
|
+
directory = os.path.abspath(args.path)
|
|
153
|
+
print(f"Modo DIRECTORIO: Procesando {directory}")
|
|
154
|
+
else:
|
|
155
|
+
directory = os.getcwd()
|
|
156
|
+
print(f"Modo LOCAL: Procesando directorio actual {directory}")
|
|
157
|
+
|
|
158
|
+
if not os.path.exists(directory):
|
|
159
|
+
print(f"Error: La ruta no existe: {directory}")
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
converter = StringFormatConverter()
|
|
163
|
+
converter.run(directory)
|
|
164
|
+
|
|
165
|
+
summary = converter.tracker
|
|
166
|
+
print(f"\nResumen de Conversión (a f-strings):")
|
|
167
|
+
print(f" - Archivos revisados: {len(summary.files_reviewed)}")
|
|
168
|
+
print(f" - Archivos modificados: {len(summary.files_modified)}")
|
|
169
|
+
print(f" - Total cambios realizados: {summary.total_modifications}")
|
|
170
|
+
|
|
171
|
+
if __name__ == "__main__":
|
|
172
|
+
main()
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
import glob
|
|
5
|
+
import polib
|
|
6
|
+
import logging
|
|
7
|
+
import sys
|
|
8
|
+
import subprocess
|
|
9
|
+
import argparse
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from babel.messages.catalog import Catalog
|
|
12
|
+
from babel.core import UnknownLocaleError, Locale
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Set, Dict, List, Optional, Tuple
|
|
15
|
+
from time import sleep
|
|
16
|
+
from translate import Translator
|
|
17
|
+
|
|
18
|
+
# Configuración de logging
|
|
19
|
+
logging.basicConfig(
|
|
20
|
+
level=logging.INFO, format="%(message)s", datefmt="%Y-%m-%d %H:%M:%S"
|
|
21
|
+
)
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
class OdooPoTranslator:
|
|
25
|
+
"""Gestiona la traducción de archivos PO en módulos Odoo"""
|
|
26
|
+
|
|
27
|
+
ODOO_LANGUAGE_CODES = {
|
|
28
|
+
"es": "es_ES",
|
|
29
|
+
"es_ES": "es_ES",
|
|
30
|
+
"es_CO": "es_CO",
|
|
31
|
+
"en": "en_US",
|
|
32
|
+
"en_US": "en_US",
|
|
33
|
+
"fr": "fr_FR",
|
|
34
|
+
"pt": "pt_PT",
|
|
35
|
+
"pt_BR": "pt_BR",
|
|
36
|
+
"de": "de_DE",
|
|
37
|
+
"it": "it_IT",
|
|
38
|
+
"nl": "nl_NL",
|
|
39
|
+
"pl": "pl_PL",
|
|
40
|
+
"ru": "ru_RU",
|
|
41
|
+
"zh": "zh_CN",
|
|
42
|
+
"ja": "ja_JP",
|
|
43
|
+
"ko": "ko_KR",
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
def __init__(self, source_dir: str, locale: str = "es"):
|
|
47
|
+
self.source_dir = os.path.abspath(source_dir)
|
|
48
|
+
self.locale = self._normalize_locale(locale)
|
|
49
|
+
|
|
50
|
+
if not self.locale:
|
|
51
|
+
raise ValueError(f"Idioma no soportado: {locale}")
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
self.catalog = Catalog(locale=self.locale)
|
|
55
|
+
except UnknownLocaleError:
|
|
56
|
+
base_locale = self.locale.split("_")[0]
|
|
57
|
+
self.catalog = Catalog(locale=base_locale)
|
|
58
|
+
|
|
59
|
+
self.current_strings = set()
|
|
60
|
+
self.translation_markers = [r'_\([\'"](.+?)[\'"]\)', r'_lt\([\'"](.+?)[\'"]\)', r'_t\([\'"](.+?)[\'"]\)']
|
|
61
|
+
self.xml_patterns = [r'string="([^"]+)"', r'help="([^"]+)"', r'placeholder="([^"]+)"', r'title="([^"]+)"']
|
|
62
|
+
|
|
63
|
+
def _normalize_locale(self, locale: str) -> Optional[str]:
|
|
64
|
+
locale = locale.lower()
|
|
65
|
+
if locale in self.ODOO_LANGUAGE_CODES.values(): return locale
|
|
66
|
+
if locale in self.ODOO_LANGUAGE_CODES: return self.ODOO_LANGUAGE_CODES[locale]
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
def scan_directory(self) -> None:
|
|
70
|
+
logger.info(f"🔍 Escaneando: {self.source_dir}")
|
|
71
|
+
for root, _, files in os.walk(self.source_dir):
|
|
72
|
+
for file in files:
|
|
73
|
+
filepath = os.path.join(root, file)
|
|
74
|
+
if file.endswith(".py"): self.scan_python_file(filepath)
|
|
75
|
+
elif file.endswith(".xml"): self.scan_xml_file(filepath)
|
|
76
|
+
|
|
77
|
+
def scan_python_file(self, filepath: str) -> None:
|
|
78
|
+
try:
|
|
79
|
+
with open(filepath, "r", encoding="utf-8") as f:
|
|
80
|
+
content = f.read()
|
|
81
|
+
for pattern in self.translation_markers:
|
|
82
|
+
for match in re.finditer(pattern, content):
|
|
83
|
+
text = match.group(1)
|
|
84
|
+
if text and not text.isspace():
|
|
85
|
+
line_no = content[:match.start()].count("\n") + 1
|
|
86
|
+
self.catalog.add(text, locations=[(filepath, line_no)])
|
|
87
|
+
self.current_strings.add(text)
|
|
88
|
+
except: pass
|
|
89
|
+
|
|
90
|
+
def scan_xml_file(self, filepath: str) -> None:
|
|
91
|
+
try:
|
|
92
|
+
with open(filepath, "r", encoding="utf-8") as f:
|
|
93
|
+
content = f.read()
|
|
94
|
+
for pattern in self.xml_patterns:
|
|
95
|
+
for match in re.finditer(pattern, content):
|
|
96
|
+
text = match.group(1).strip()
|
|
97
|
+
if text and not text.isspace() and len(text) > 1:
|
|
98
|
+
line_no = content[:match.start()].count("\n") + 1
|
|
99
|
+
self.catalog.add(text, locations=[(filepath, line_no)])
|
|
100
|
+
self.current_strings.add(text)
|
|
101
|
+
except: pass
|
|
102
|
+
|
|
103
|
+
def translate_pending_strings(self) -> None:
|
|
104
|
+
untranslated = [m.id for m in self.catalog if not m.string]
|
|
105
|
+
if not untranslated: return
|
|
106
|
+
|
|
107
|
+
logger.info(f"🔄 Traduciendo {len(untranslated)} strings...")
|
|
108
|
+
translator = Translator(to_lang=self.locale.split("_")[0], from_lang="en")
|
|
109
|
+
|
|
110
|
+
for text in untranslated:
|
|
111
|
+
try:
|
|
112
|
+
translation = translator.translate(text)
|
|
113
|
+
if translation and translation != text:
|
|
114
|
+
self.catalog.get(text).string = translation
|
|
115
|
+
logger.info(f" ✅ {text} -> {translation}")
|
|
116
|
+
sleep(0.1)
|
|
117
|
+
except: pass
|
|
118
|
+
|
|
119
|
+
def save(self):
|
|
120
|
+
po_path = os.path.join(self.source_dir, "i18n", f"{self.locale}.po")
|
|
121
|
+
os.makedirs(os.path.dirname(po_path), exist_ok=True)
|
|
122
|
+
|
|
123
|
+
po = polib.POFile()
|
|
124
|
+
po.metadata = {"Content-Type": "text/plain; charset=UTF-8"}
|
|
125
|
+
for msg in self.catalog:
|
|
126
|
+
entry = polib.POEntry(msgid=msg.id, msgstr=msg.string or "", occurrences=msg.locations)
|
|
127
|
+
po.append(entry)
|
|
128
|
+
po.save(po_path)
|
|
129
|
+
logger.info(f"💾 Guardado: {po_path}")
|
|
130
|
+
|
|
131
|
+
def main():
|
|
132
|
+
parser = argparse.ArgumentParser(description="Traducción automática de módulos Odoo.")
|
|
133
|
+
group = parser.add_mutually_exclusive_group()
|
|
134
|
+
group.add_argument("--all", action="store_true", help="Procesar todo el repositorio (global)")
|
|
135
|
+
group.add_argument("--path", "-p", help="Procesar un directorio específico")
|
|
136
|
+
parser.add_argument("--locale", default="es", help="Idioma (default: es)")
|
|
137
|
+
|
|
138
|
+
args = parser.parse_args()
|
|
139
|
+
|
|
140
|
+
if args.all:
|
|
141
|
+
try:
|
|
142
|
+
root = subprocess.run(["git", "rev-parse", "--show-toplevel"], capture_output=True, text=True, check=True).stdout.strip()
|
|
143
|
+
directory = root
|
|
144
|
+
except: directory = os.getcwd()
|
|
145
|
+
elif args.path: directory = os.path.abspath(args.path)
|
|
146
|
+
else: directory = os.getcwd()
|
|
147
|
+
|
|
148
|
+
if not os.path.exists(directory):
|
|
149
|
+
print(f"Error: {directory} no existe")
|
|
150
|
+
return
|
|
151
|
+
|
|
152
|
+
# Si es global, buscamos módulos (carpetas con __manifest__.py)
|
|
153
|
+
modules = []
|
|
154
|
+
if args.all:
|
|
155
|
+
for root, dirs, files in os.walk(directory):
|
|
156
|
+
if "__manifest__.py" in files:
|
|
157
|
+
modules.append(root)
|
|
158
|
+
dirs[:] = [] # No buscar sub-módulos
|
|
159
|
+
else:
|
|
160
|
+
if os.path.exists(os.path.join(directory, "__manifest__.py")):
|
|
161
|
+
modules.append(directory)
|
|
162
|
+
else:
|
|
163
|
+
# Quizás es un directorio de módulos
|
|
164
|
+
for d in os.listdir(directory):
|
|
165
|
+
dpath = os.path.join(directory, d)
|
|
166
|
+
if os.path.isdir(dpath) and os.path.exists(os.path.join(dpath, "__manifest__.py")):
|
|
167
|
+
modules.append(dpath)
|
|
168
|
+
|
|
169
|
+
for mod in modules:
|
|
170
|
+
try:
|
|
171
|
+
t = OdooPoTranslator(mod, args.locale)
|
|
172
|
+
t.scan_directory()
|
|
173
|
+
t.translate_pending_strings()
|
|
174
|
+
t.save()
|
|
175
|
+
except Exception as e:
|
|
176
|
+
logger.error(f"Error en {mod}: {e}")
|
|
177
|
+
|
|
178
|
+
if __name__ == "__main__":
|
|
179
|
+
main()
|
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import os
|
|
3
|
+
import git
|
|
4
|
+
import sys
|
|
5
|
+
import logging
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Set, Dict, List, Optional, Tuple
|
|
8
|
+
from .strx_translate_po import OdooPoTranslator
|
|
9
|
+
import argparse
|
|
10
|
+
|
|
11
|
+
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
DEFAULT_DIR = "/home/initium16/odoo16/initium"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class GitModuleDetector:
|
|
18
|
+
"""Detecta módulos Odoo modificados en git usando comandos directos"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, repo_path: str = "."):
|
|
21
|
+
self.repo_path = os.path.abspath(repo_path) if repo_path != "." else DEFAULT_DIR
|
|
22
|
+
try:
|
|
23
|
+
self.repo = git.Repo(self.repo_path)
|
|
24
|
+
os.chdir(self.repo_path)
|
|
25
|
+
except git.InvalidGitRepositoryError:
|
|
26
|
+
logger.debug(f"No es un repositorio git válido: {repo_path}")
|
|
27
|
+
self.repo = None
|
|
28
|
+
|
|
29
|
+
def _is_odoo_module(self, path: str) -> bool:
|
|
30
|
+
"""Verifica si una ruta es un módulo Odoo válido"""
|
|
31
|
+
manifest_path = os.path.join(
|
|
32
|
+
path, "__manifest__.py"
|
|
33
|
+
) # Nombre correcto del manifest
|
|
34
|
+
return os.path.isfile(manifest_path)
|
|
35
|
+
|
|
36
|
+
def get_modified_files(self) -> Set[str]:
|
|
37
|
+
"""Obtiene lista de archivos modificados usando comandos git directos"""
|
|
38
|
+
if not self.repo:
|
|
39
|
+
return set()
|
|
40
|
+
|
|
41
|
+
changed_files = set()
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
# Obtener cambios usando git status
|
|
45
|
+
status_output = self.repo.git.status("--porcelain", "--untracked-files=all")
|
|
46
|
+
|
|
47
|
+
if status_output:
|
|
48
|
+
for line in status_output.split("\n"):
|
|
49
|
+
if line:
|
|
50
|
+
status = line[:2]
|
|
51
|
+
file_path = line[3:]
|
|
52
|
+
if status.strip() != "D": # Ignorar archivos eliminados
|
|
53
|
+
changed_files.add(file_path)
|
|
54
|
+
logger.debug(f"Archivo detectado ({status}): {file_path}")
|
|
55
|
+
|
|
56
|
+
# Si no hay cambios en el status, verificar el último commit
|
|
57
|
+
if not changed_files and self.repo.head.is_valid():
|
|
58
|
+
try:
|
|
59
|
+
last_commit = self.repo.head.commit
|
|
60
|
+
if last_commit.parents:
|
|
61
|
+
changed_files.update(
|
|
62
|
+
item.a_path
|
|
63
|
+
for item in last_commit.diff(last_commit.parents[0])
|
|
64
|
+
)
|
|
65
|
+
except Exception as e:
|
|
66
|
+
logger.debug(f"Error verificando último commit: {e}")
|
|
67
|
+
|
|
68
|
+
except Exception as e:
|
|
69
|
+
logger.error(f"Error obteniendo archivos modificados: {e}")
|
|
70
|
+
|
|
71
|
+
return changed_files
|
|
72
|
+
|
|
73
|
+
def find_module_from_file(self, file_path: str) -> Optional[str]:
|
|
74
|
+
"""
|
|
75
|
+
Encuentra el módulo Odoo que contiene un archivo, buscando __manifest__.py
|
|
76
|
+
"""
|
|
77
|
+
try:
|
|
78
|
+
current_path = os.path.dirname(os.path.join(self.repo_path, file_path))
|
|
79
|
+
|
|
80
|
+
while (
|
|
81
|
+
current_path
|
|
82
|
+
and os.path.commonpath([current_path, self.repo_path]) == self.repo_path
|
|
83
|
+
):
|
|
84
|
+
if self._is_odoo_module(current_path):
|
|
85
|
+
logger.debug(f"Módulo Odoo encontrado en: {current_path}")
|
|
86
|
+
return current_path
|
|
87
|
+
parent_path = os.path.dirname(current_path)
|
|
88
|
+
if parent_path == current_path: # Llegamos a la raíz
|
|
89
|
+
break
|
|
90
|
+
current_path = parent_path
|
|
91
|
+
|
|
92
|
+
except Exception as e:
|
|
93
|
+
logger.debug(f"Error buscando módulo para {file_path}: {e}")
|
|
94
|
+
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
def get_modified_modules(self) -> Dict[str, Dict]:
|
|
98
|
+
"""Obtiene información de todos los módulos Odoo modificados"""
|
|
99
|
+
modules = {}
|
|
100
|
+
changed_files = self.get_modified_files()
|
|
101
|
+
|
|
102
|
+
logger.debug(f"Archivos modificados encontrados: {len(changed_files)}")
|
|
103
|
+
|
|
104
|
+
for file_path in changed_files:
|
|
105
|
+
# Solo procesar archivos Python
|
|
106
|
+
if not file_path.endswith(".py"):
|
|
107
|
+
continue
|
|
108
|
+
|
|
109
|
+
logger.debug(f"Procesando archivo: {file_path}")
|
|
110
|
+
module_path = self.find_module_from_file(file_path)
|
|
111
|
+
|
|
112
|
+
if module_path and module_path not in modules:
|
|
113
|
+
module_info = self._get_module_info(module_path)
|
|
114
|
+
if module_info:
|
|
115
|
+
modules[module_path] = module_info
|
|
116
|
+
logger.info(
|
|
117
|
+
f"✓ Módulo detectado: {module_info['name']} ({module_info['technical_name']})"
|
|
118
|
+
)
|
|
119
|
+
logger.debug(f" Ruta: {module_path}")
|
|
120
|
+
|
|
121
|
+
return modules
|
|
122
|
+
|
|
123
|
+
def _get_module_info(self, module_path: str) -> Optional[Dict]:
|
|
124
|
+
"""Lee la información del módulo desde __manifest__.py"""
|
|
125
|
+
manifest_path = os.path.join(module_path, "__manifest__.py")
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
if not os.path.isfile(manifest_path):
|
|
129
|
+
logger.debug(f"No se encontró __manifest__.py en: {manifest_path}")
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
with open(manifest_path, "r", encoding="utf-8") as f:
|
|
133
|
+
manifest_content = f.read()
|
|
134
|
+
manifest_dict = eval(manifest_content)
|
|
135
|
+
|
|
136
|
+
module_info = {
|
|
137
|
+
"name": manifest_dict.get("name", os.path.basename(module_path)),
|
|
138
|
+
"version": manifest_dict.get("version", "1.0"),
|
|
139
|
+
"technical_name": os.path.basename(module_path),
|
|
140
|
+
"path": module_path,
|
|
141
|
+
"manifest_path": manifest_path,
|
|
142
|
+
"summary": manifest_dict.get("summary", ""),
|
|
143
|
+
"category": manifest_dict.get("category", "Uncategorized"),
|
|
144
|
+
"author": manifest_dict.get("author", ""),
|
|
145
|
+
"depends": manifest_dict.get("depends", []),
|
|
146
|
+
"website": manifest_dict.get("website", ""),
|
|
147
|
+
"license": manifest_dict.get("license", "LGPL-3"),
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
logger.debug(f"Información del módulo leída: {module_info['name']}")
|
|
151
|
+
return module_info
|
|
152
|
+
|
|
153
|
+
except Exception as e:
|
|
154
|
+
logger.error(f"Error leyendo __manifest__.py en {manifest_path}: {e}")
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
def print_module_summary(self, modules: Dict[str, Dict]):
|
|
158
|
+
"""Imprime un resumen de los módulos encontrados"""
|
|
159
|
+
if not modules:
|
|
160
|
+
logger.info("No se encontraron módulos modificados")
|
|
161
|
+
return
|
|
162
|
+
|
|
163
|
+
logger.info("\n📦 Módulos modificados encontrados:")
|
|
164
|
+
for module_path, info in modules.items():
|
|
165
|
+
logger.info(f"\n▶ {info['name']} ({info['technical_name']})")
|
|
166
|
+
logger.info(f" 📍 Ruta: {module_path}")
|
|
167
|
+
if info["version"]:
|
|
168
|
+
logger.info(f" 📌 Versión: {info['version']}")
|
|
169
|
+
if info["depends"]:
|
|
170
|
+
logger.info(f" 🔗 Dependencias: {', '.join(info['depends'])}")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class ModuleDetector:
|
|
174
|
+
"""Detecta módulos Odoo en un directorio o en cambios de git"""
|
|
175
|
+
|
|
176
|
+
def __init__(self, default_dir: Optional[str] = None, repo_path: str = "."):
|
|
177
|
+
self.default_dir = os.path.abspath(default_dir) if default_dir else None
|
|
178
|
+
self.repo_path = os.path.abspath(repo_path)
|
|
179
|
+
self.git_detector = GitModuleDetector(repo_path)
|
|
180
|
+
|
|
181
|
+
def _is_odoo_module(self, path: str) -> bool:
|
|
182
|
+
"""Verifica si una ruta es un módulo Odoo válido"""
|
|
183
|
+
manifest_path = os.path.join(path, "__manifest__.py")
|
|
184
|
+
return os.path.isfile(manifest_path)
|
|
185
|
+
|
|
186
|
+
def _read_manifest(self, manifest_path: str) -> Optional[Dict]:
|
|
187
|
+
"""Lee y retorna la información del manifest"""
|
|
188
|
+
try:
|
|
189
|
+
with open(manifest_path, "r", encoding="utf-8") as f:
|
|
190
|
+
manifest_content = f.read()
|
|
191
|
+
return eval(manifest_content)
|
|
192
|
+
except Exception as e:
|
|
193
|
+
logger.debug(f"Error leyendo manifest {manifest_path}: {e}")
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
def _get_module_info(self, module_path: str) -> Optional[Dict]:
|
|
197
|
+
"""Obtiene información detallada de un módulo"""
|
|
198
|
+
manifest_path = os.path.join(module_path, "__manifest__.py")
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
if not os.path.isfile(manifest_path):
|
|
202
|
+
logger.debug(f"No se encontró __manifest__.py en: {manifest_path}")
|
|
203
|
+
return None
|
|
204
|
+
|
|
205
|
+
manifest_data = self._read_manifest(manifest_path)
|
|
206
|
+
if not manifest_data:
|
|
207
|
+
return None
|
|
208
|
+
|
|
209
|
+
module_info = {
|
|
210
|
+
"name": manifest_data.get("name", os.path.basename(module_path)),
|
|
211
|
+
"version": manifest_data.get("version", "1.0"),
|
|
212
|
+
"technical_name": os.path.basename(module_path),
|
|
213
|
+
"path": module_path,
|
|
214
|
+
"manifest_path": manifest_path,
|
|
215
|
+
"summary": manifest_data.get("summary", ""),
|
|
216
|
+
"author": manifest_data.get("author", ""),
|
|
217
|
+
"depends": manifest_data.get("depends", []),
|
|
218
|
+
"category": manifest_data.get("category", "Uncategorized"),
|
|
219
|
+
"website": manifest_data.get("website", ""),
|
|
220
|
+
"license": manifest_data.get("license", "LGPL-3"),
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
logger.debug(f"Información del módulo leída: {module_info['name']}")
|
|
224
|
+
return module_info
|
|
225
|
+
|
|
226
|
+
except Exception as e:
|
|
227
|
+
logger.error(f"Error procesando módulo en {module_path}: {e}")
|
|
228
|
+
if logger.getEffectiveLevel() == logging.DEBUG:
|
|
229
|
+
import traceback
|
|
230
|
+
|
|
231
|
+
logger.debug(traceback.format_exc())
|
|
232
|
+
return None
|
|
233
|
+
|
|
234
|
+
def find_modules(self) -> Dict[str, Dict]:
|
|
235
|
+
"""Encuentra módulos Odoo basado en directorio predeterminado y/o cambios git"""
|
|
236
|
+
modules = {}
|
|
237
|
+
|
|
238
|
+
# 1. Buscar en directorio predeterminado si está configurado
|
|
239
|
+
if self.default_dir and os.path.exists(self.default_dir):
|
|
240
|
+
logger.debug(f"Buscando en directorio predeterminado: {self.default_dir}")
|
|
241
|
+
for item in os.listdir(self.default_dir):
|
|
242
|
+
module_path = os.path.join(self.default_dir, item)
|
|
243
|
+
if os.path.isdir(module_path) and self._is_odoo_module(module_path):
|
|
244
|
+
module_info = self._get_module_info(module_path)
|
|
245
|
+
if module_info:
|
|
246
|
+
modules[module_path] = module_info
|
|
247
|
+
logger.debug(
|
|
248
|
+
f"Módulo encontrado en directorio: {module_info['name']}"
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
# 2. Buscar en cambios git
|
|
252
|
+
logger.debug("Buscando módulos en cambios git...")
|
|
253
|
+
git_modules = self.git_detector.get_modified_modules()
|
|
254
|
+
|
|
255
|
+
# Combinar resultados, priorizando información de git para módulos duplicados
|
|
256
|
+
modules.update(git_modules)
|
|
257
|
+
|
|
258
|
+
if modules:
|
|
259
|
+
self._print_module_summary(modules)
|
|
260
|
+
else:
|
|
261
|
+
logger.info("No se encontraron módulos para procesar")
|
|
262
|
+
|
|
263
|
+
return modules
|
|
264
|
+
|
|
265
|
+
def _print_module_summary(self, modules: Dict[str, Dict]):
|
|
266
|
+
"""Imprime un resumen de los módulos encontrados"""
|
|
267
|
+
logger.info("\n📦 Módulos encontrados:")
|
|
268
|
+
|
|
269
|
+
for module_path, info in modules.items():
|
|
270
|
+
logger.info(f"\n▶ {info['name']} ({info['technical_name']})")
|
|
271
|
+
logger.info(f" 📍 Ruta: {module_path}")
|
|
272
|
+
|
|
273
|
+
if info["version"]:
|
|
274
|
+
logger.info(f" 📌 Versión: {info['version']}")
|
|
275
|
+
|
|
276
|
+
if info["category"] != "Uncategorized":
|
|
277
|
+
logger.info(f" 📑 Categoría: {info['category']}")
|
|
278
|
+
|
|
279
|
+
if info["author"]:
|
|
280
|
+
logger.info(f" 👤 Autor: {info['author']}")
|
|
281
|
+
|
|
282
|
+
if info["depends"]:
|
|
283
|
+
logger.info(f" 🔗 Dependencias: {', '.join(info['depends'])}")
|
|
284
|
+
|
|
285
|
+
if info["website"]:
|
|
286
|
+
logger.info(f" 🌐 Website: {info['website']}")
|
|
287
|
+
|
|
288
|
+
logger.info(f" ⚖️ Licencia: {info['license']}")
|
|
289
|
+
|
|
290
|
+
def get_module_dependencies(self, module_path: str) -> List[str]:
|
|
291
|
+
"""Obtiene la lista de dependencias de un módulo"""
|
|
292
|
+
module_info = self._get_module_info(module_path)
|
|
293
|
+
if module_info and module_info["depends"]:
|
|
294
|
+
return module_info["depends"]
|
|
295
|
+
return []
|
|
296
|
+
|
|
297
|
+
def is_module_installable(self, module_path: str) -> bool:
|
|
298
|
+
"""Verifica si un módulo está marcado como instalable"""
|
|
299
|
+
manifest_path = os.path.join(module_path, "__manifest__.py")
|
|
300
|
+
if os.path.isfile(manifest_path):
|
|
301
|
+
manifest_data = self._read_manifest(manifest_path)
|
|
302
|
+
return manifest_data.get("installable", True) if manifest_data else False
|
|
303
|
+
return False
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
class TranslationChecker:
|
|
307
|
+
"""Verifica traducciones en módulos Odoo"""
|
|
308
|
+
|
|
309
|
+
def __init__(self, lang: str = "es", default_dir: Optional[str] = None):
|
|
310
|
+
self.lang = lang
|
|
311
|
+
self.module_detector = ModuleDetector(default_dir)
|
|
312
|
+
|
|
313
|
+
def check_translations(self) -> bool:
|
|
314
|
+
"""Verifica y actualiza traducciones en módulos"""
|
|
315
|
+
modules = self.module_detector.find_modules()
|
|
316
|
+
|
|
317
|
+
if not modules:
|
|
318
|
+
logger.info("✅ No se encontraron módulos para verificar")
|
|
319
|
+
return True
|
|
320
|
+
|
|
321
|
+
logger.info(f"\n🔍 Verificando traducciones ({self.lang}):")
|
|
322
|
+
|
|
323
|
+
all_ok = True
|
|
324
|
+
modules_with_issues = {}
|
|
325
|
+
new_modules = []
|
|
326
|
+
|
|
327
|
+
for module_path, module_info in modules.items():
|
|
328
|
+
logger.info(
|
|
329
|
+
f"\n📦 Verificando módulo: {module_info['name']} ({module_info['technical_name']})"
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
po_path = os.path.join(module_path, "i18n", f"{self.lang}.po")
|
|
333
|
+
|
|
334
|
+
if not os.path.exists(po_path):
|
|
335
|
+
new_modules.append(module_info)
|
|
336
|
+
all_ok = False
|
|
337
|
+
logger.error(f"❌ No se encontró archivo {self.lang}.po")
|
|
338
|
+
continue
|
|
339
|
+
|
|
340
|
+
# Crear instancia del traductor
|
|
341
|
+
translator = OdooPoTranslator(module_path, self.lang)
|
|
342
|
+
|
|
343
|
+
# Escanear y procesar el módulo
|
|
344
|
+
translator.scan_directory()
|
|
345
|
+
|
|
346
|
+
# Verificar strings sin traducir antes de la traducción
|
|
347
|
+
untranslated_before = translator.get_untranslated_strings()
|
|
348
|
+
|
|
349
|
+
if untranslated_before:
|
|
350
|
+
logger.info(
|
|
351
|
+
f"Encontrados {len(untranslated_before)} strings sin traducir"
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
# Realizar la traducción
|
|
355
|
+
logger.info("Iniciando proceso de traducción...")
|
|
356
|
+
translator.translate_pending_strings()
|
|
357
|
+
|
|
358
|
+
# Guardar las traducciones
|
|
359
|
+
translator.save_po_file()
|
|
360
|
+
|
|
361
|
+
# Verificar strings sin traducir después de la traducción
|
|
362
|
+
untranslated_after = translator.get_untranslated_strings()
|
|
363
|
+
|
|
364
|
+
if untranslated_after:
|
|
365
|
+
all_ok = False
|
|
366
|
+
modules_with_issues[module_path] = untranslated_after
|
|
367
|
+
logger.error("❌ Strings que no se pudieron traducir:")
|
|
368
|
+
for string in sorted(untranslated_after):
|
|
369
|
+
logger.error(f" - {string}")
|
|
370
|
+
else:
|
|
371
|
+
logger.info("✅ Todas las cadenas fueron traducidas exitosamente")
|
|
372
|
+
|
|
373
|
+
if not all_ok:
|
|
374
|
+
self._show_error_summary(new_modules, modules_with_issues)
|
|
375
|
+
return False
|
|
376
|
+
|
|
377
|
+
logger.info("\n✅ Todas las traducciones están actualizadas")
|
|
378
|
+
return True
|
|
379
|
+
|
|
380
|
+
def _show_error_summary(self, new_modules: List[Dict], modules_with_issues: Dict):
|
|
381
|
+
"""Muestra resumen de errores encontrados"""
|
|
382
|
+
logger.error("\n❌ Se encontraron problemas de traducción")
|
|
383
|
+
|
|
384
|
+
if new_modules:
|
|
385
|
+
logger.error("\n📋 Módulos que requieren traducción inicial:")
|
|
386
|
+
for module in new_modules:
|
|
387
|
+
logger.error(f" - {module['name']} ({module['technical_name']})")
|
|
388
|
+
logger.error("\nPara generar traducciones iniciales desde Odoo:")
|
|
389
|
+
logger.error("1. Instala el módulo en Odoo")
|
|
390
|
+
logger.error("2. Ve a Ajustes > Traducciones > Cargar una traducción")
|
|
391
|
+
logger.error(f"3. Selecciona el idioma ({self.lang})")
|
|
392
|
+
logger.error("4. Marca 'Sobreescribir términos existentes'")
|
|
393
|
+
logger.error("5. Selecciona los módulos listados arriba")
|
|
394
|
+
|
|
395
|
+
if modules_with_issues:
|
|
396
|
+
logger.error("\n📝 Resumen de strings que no se pudieron traducir:")
|
|
397
|
+
for module_path, strings in modules_with_issues.items():
|
|
398
|
+
module_name = os.path.basename(module_path)
|
|
399
|
+
logger.error(f"\nMódulo: {module_name}")
|
|
400
|
+
for string in sorted(strings):
|
|
401
|
+
logger.error(f" - {string}")
|
|
402
|
+
|
|
403
|
+
logger.error("\n⚠️ Por favor, verifica las traducciones pendientes manualmente")
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def main():
|
|
407
|
+
|
|
408
|
+
parser = argparse.ArgumentParser(
|
|
409
|
+
description="Verifica traducciones en módulos Odoo"
|
|
410
|
+
)
|
|
411
|
+
parser.add_argument("--lang", default="es", help="Código de idioma (default: es)")
|
|
412
|
+
parser.add_argument(
|
|
413
|
+
"--dir", dest="default_dir", help="Directorio predeterminado de módulos Odoo"
|
|
414
|
+
)
|
|
415
|
+
parser.add_argument(
|
|
416
|
+
"--git-only",
|
|
417
|
+
action="store_true",
|
|
418
|
+
help="Solo verificar módulos con cambios en git",
|
|
419
|
+
)
|
|
420
|
+
parser.add_argument("--debug", action="store_true", help="Activa logs de debug")
|
|
421
|
+
parser.add_argument(
|
|
422
|
+
"--no-translate", action="store_true", help="No realizar traducción automática"
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
args = parser.parse_args()
|
|
426
|
+
|
|
427
|
+
if args.debug:
|
|
428
|
+
logger.setLevel(logging.DEBUG)
|
|
429
|
+
|
|
430
|
+
try:
|
|
431
|
+
# Si se especifica --git-only, no usar directorio predeterminado
|
|
432
|
+
GIT_ONLY = args.git_only or True
|
|
433
|
+
default_dir = DEFAULT_DIR if GIT_ONLY else args.default_dir
|
|
434
|
+
|
|
435
|
+
checker = TranslationChecker(lang=args.lang, default_dir=default_dir)
|
|
436
|
+
|
|
437
|
+
if not checker.check_translations():
|
|
438
|
+
sys.exit(1)
|
|
439
|
+
|
|
440
|
+
except Exception as e:
|
|
441
|
+
logger.error(f"\n❌ Error: {e}")
|
|
442
|
+
if args.debug:
|
|
443
|
+
import traceback
|
|
444
|
+
|
|
445
|
+
logger.debug(traceback.format_exc())
|
|
446
|
+
sys.exit(1)
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
if __name__ == "__main__":
|
|
450
|
+
main()
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: strx-precommit
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Pre-commit hooks para conversión de strings y traducción de archivos PO
|
|
5
|
+
Home-page: https://github.com/straconxsa/strx_tools/
|
|
6
|
+
Author: Tu Nombre
|
|
7
|
+
Author-email: Initium Team <info@straconx.com>
|
|
8
|
+
Project-URL: Homepage, https://github.com/straconxsa/strx_tools/
|
|
9
|
+
Project-URL: Bug Tracker, https://github.com/straconxsa/strx_tools/issues
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Requires-Python: >=3.6
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
Dynamic: author
|
|
16
|
+
Dynamic: home-page
|
|
17
|
+
Dynamic: requires-python
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
setup.py
|
|
4
|
+
src/strx_precommit/__init__.py
|
|
5
|
+
src/strx_precommit/strx_convert_string.py
|
|
6
|
+
src/strx_precommit/strx_translate_po.py
|
|
7
|
+
src/strx_precommit/strx_update_po.py
|
|
8
|
+
src/strx_precommit.egg-info/PKG-INFO
|
|
9
|
+
src/strx_precommit.egg-info/SOURCES.txt
|
|
10
|
+
src/strx_precommit.egg-info/dependency_links.txt
|
|
11
|
+
src/strx_precommit.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
strx_precommit
|