Rearrange for tidier structure (#2212)

This commit is contained in:
clsty
2025-10-16 07:19:55 +08:00
parent 13065d7e5a
commit 8b493e091d
529 changed files with 165 additions and 138 deletions
@@ -0,0 +1,285 @@
# Translation Management Tool Suite
This suite is used to manage project translation files, automatically extract translatable texts, compare differences between language files, and provide maintenance functions.
## Tool Components
### 1. `translation-manager.py` - Main Translation Manager
- Extract translatable texts
- Compare and update translation files
- Interactive addition/removal of translation keys
### 2. `translation-cleaner.py` - Translation File Maintenance Tool
- Clean unused translation keys
- Synchronize key structure across different language files
### 3. `manage-translations.sh` - Convenient Wrapper Script
- Provides a unified command-line interface
- Displays translation status
- Simplifies common operations
## Quick Start
### Using the Wrapper Script (Recommended)
```bash
# Enter the tools directory
cd .config/quickshell/translations/tools
# Show help
./manage-translations.sh --help
# Show current translation status
./manage-translations.sh status
# Extract translatable texts
./manage-translations.sh extract
# Update all translation files
./manage-translations.sh update
# Update a specific language
./manage-translations.sh update -l zh_CN
# Clean unused keys
./manage-translations.sh clean
# Synchronize keys across all language files
./manage-translations.sh sync
```
Or run from the project root:
```bash
# Run from the project root
.config/quickshell/translations/tools/manage-translations.sh status
.config/quickshell/translations/tools/manage-translations.sh update
```
## Detailed Usage
### Translation Manager (`translation-manager.py`)
Basic usage:
```bash
# Process all languages
./translation-manager.py
# Specify a particular language
./translation-manager.py --language zh_CN
# Extract translatable texts only
./translation-manager.py --extract-only
# Show extracted texts
./translation-manager.py --extract-only --show-temp
```
Parameter description:
- `--translations-dir`, `-t`: Translation files directory (default: `.config/quickshell/translations`)
- `--source-dir`, `-s`: Source code directory (default: `.config/quickshell`)
- `--language`, `-l`: Specify the language code to process
- `--extract-only`, `-e`: Only extract translatable texts
- `--show-temp`: Show the content of the temporary extraction file
### Translation Cleaner (`translation-cleaner.py`)
```bash
# Clean unused translation keys
./translation-cleaner.py --clean
# Synchronize translation keys (using en_US as the base)
./translation-cleaner.py --sync
# Specify a different source language for syncing
./translation-cleaner.py --sync --source-lang zh_CN
# Clean without creating backups
./translation-cleaner.py --clean --no-backup
```
## Workflow
### Regular Translation Update Workflow
1. **Check status**:
```bash
./manage-translations.sh status
```
2. **Update translations**:
```bash
./manage-translations.sh update
```
3. **Clean unused keys** (optional):
```bash
./manage-translations.sh clean
```
### Adding a New Language
1. **Create a new language file**:
```bash
./manage-translations.sh update -l new_lang
```
2. **Synchronize key structure**:
```bash
./manage-translations.sh sync
```
### Cleanup After Large Refactoring
1. **Backup translation files**:
```bash
cp -r .config/quickshell/translations .config/quickshell/translations.backup
```
2. **Clean unused keys**:
```bash
./manage-translations.sh clean
```
3. **Synchronize all languages**:
```bash
./manage-translations.sh sync
```
## Supported Translatable Text Formats
The tool recognizes the following formats for translatable texts:
```qml
// Basic format
Translation.tr("Hello, world!")
Translation.tr('Hello, world!')
Translation.tr(`Hello, world!`)
// With line breaks
Translation.tr("Line 1\nLine 2")
// With escape characters
Translation.tr("Say \"Hello\"")
// With parameter placeholders
Translation.tr("Hello, %1!").arg(name)
```
## Example Output
### Status Display
```
$ ./manage-translations.sh status
Analyzing translation status...
=== Current Project Status ===
166 translatable texts extracted
=== Translation File Status ===
en_US: 470 keys
zh_CN: 470 keys
```
### Update Translations
```
$ ./manage-translations.sh update -l zh_CN
Updating translation files...
==================================================
Processing language: zh_CN
==================================================
Analysis result:
Missing keys: 5
Extra keys: 20
Found 5 missing translation keys:
1. "New feature text"
2. "Another new text"
...
Add these 5 missing keys? (y/n): y
5 keys added
Found 20 extra translation keys:
1. "Removed old text" -> "已删除的旧文本"
...
Delete these 20 extra keys? (y/n): y
20 keys deleted
Translation file saved
```
### Clean Unused Keys
```
$ ./manage-translations.sh clean
Cleaning unused translation keys...
Processing language: zh_CN
Found 50 unused keys:
1. "old_unused_text"
2. "deprecated_message"
...
Delete these 50 unused keys? (y/n): y
50 keys deleted
Original key count: 470, after cleaning: 420
```
## Advanced Features
### Custom Directory Structure
```bash
# Use custom directories
./translation-manager.py \
--translations-dir /path/to/translations \
--source-dir /path/to/source
```
### Ignore Mark Feature
For dynamic resources or special texts that should not be automatically cleaned, you can add `/*keep*/` at the end of the translation value. The tool will automatically ignore these keys and will not delete them during cleaning or syncing.
Example:
```json
{
"dynamic_key": "Some dynamic value /*keep*/"
}
```
## Notes
1. **Backup is important**: The tool automatically creates backups before cleaning, but it is recommended to manually back up important files
2. **Text extraction limitations**:
- ~~Only supports static strings, not dynamically constructed strings~~
- Dynamic resources (such as variable concatenation or runtime-generated text) cannot be automatically extracted. You need to manually add them to the translation file and use the `/*keep*/` mark for ignore management.
- Must use the `Translation.tr()` format
3. **File encoding**: All files must use UTF-8 encoding
4. **Key naming conventions**: It is recommended to use English for key names and avoid special characters
## Troubleshooting
### Common Issues
**Q: Text does not appear after adding Translation.tr?**
A: You need to import the translation feature in your QML file using `import "root:/"`, otherwise the translation text will not be displayed correctly.
**Q: The number of extracted texts does not match expectations?**
A: Check whether all translatable texts use the `Translation.tr()` format and ensure there are no dynamically constructed strings.
**Q: Some translations are missing after syncing?**
A: Check whether the source language file contains all necessary keys, and consider using a different source language for syncing.
**Q: The cleaning operation deleted needed keys?**
A: Restore from the automatically created backup file and check whether `Translation.tr()` is used correctly in the source code.
### Restore Backup
```bash
# Restore a single file
cp .config/quickshell/translations/zh_CN.json.backup .config/quickshell/translations/zh_CN.json
# Restore all files
cp .config/quickshell/translations.backup/* .config/quickshell/translations/
```
@@ -0,0 +1,286 @@
# 翻译管理工具套件
这套工具用于管理项目的翻译文件,自动提取可翻译文本,比较不同语言文件之间的差异,并提供维护功能。
## 工具组成
### 1. `translation-manager.py` - 主要翻译管理器
- 提取可翻译文本
- 比较和更新翻译文件
- 交互式添加/删除翻译键
### 2. `translation-cleaner.py` - 翻译文件维护工具
- 清理不再使用的翻译键
- 同步不同语言文件的键结构
### 3. `manage-translations.sh` - 便捷包装脚本
- 提供统一的命令行界面
- 显示翻译状态
- 简化常用操作
## 快速开始
### 使用便捷脚本(推荐)
```bash
# 进入工具目录
cd .config/quickshell/translations/tools
# 查看帮助
./manage-translations.sh --help
# 显示当前翻译状态
./manage-translations.sh status
# 提取可翻译文本
./manage-translations.sh extract
# 更新所有翻译文件
./manage-translations.sh update
# 更新特定语言
./manage-translations.sh update -l zh_CN
# 清理不再使用的键
./manage-translations.sh clean
# 同步所有语言文件的键
./manage-translations.sh sync
```
或者从项目根目录运行:
```bash
# 从项目根目录运行
.config/quickshell/translations/tools/manage-translations.sh status
.config/quickshell/translations/tools/manage-translations.sh update
```
## 详细使用说明
### 翻译管理器 (`translation-manager.py`)
基本用法:
```bash
# 处理所有语言
./translation-manager.py
# 指定特定语言
./translation-manager.py --language zh_CN
# 仅提取可翻译文本
./translation-manager.py --extract-only
# 显示提取的文本
./translation-manager.py --extract-only --show-temp
```
参数说明:
- `--translations-dir`, `-t`: 翻译文件目录(默认:`.config/quickshell/translations`
- `--source-dir`, `-s`: 源代码目录(默认:`.config/quickshell`
- `--language`, `-l`: 指定要处理的语言代码
- `--extract-only`, `-e`: 仅提取可翻译文本
- `--show-temp`: 显示临时提取文件的内容
### 翻译清理器 (`translation-cleaner.py`)
```bash
# 清理不再使用的翻译键
./translation-cleaner.py --clean
# 同步翻译键(以 en_US 为基准)
./translation-cleaner.py --sync
# 指定不同的源语言进行同步
./translation-cleaner.py --sync --source-lang zh_CN
# 清理时不创建备份
./translation-cleaner.py --clean --no-backup
```
## 工作流程
### 日常翻译更新流程
1. **检查状态**
```bash
./manage-translations.sh status
```
2. **更新翻译**
```bash
./manage-translations.sh update
```
3. **清理无用键**(可选):
```bash
./manage-translations.sh clean
```
### 新增语言流程
1. **创建新语言文件**
```bash
./manage-translations.sh update -l new_lang
```
2. **同步键结构**
```bash
./manage-translations.sh sync
```
### 大规模重构后的清理流程
1. **备份翻译文件**
```bash
cp -r .config/quickshell/translations .config/quickshell/translations.backup
```
2. **清理无用键**
```bash
./manage-translations.sh clean
```
3. **同步所有语言**
```bash
./manage-translations.sh sync
```
## 支持的翻译文本格式
工具可以识别以下格式的可翻译文本:
```qml
// 基本格式
Translation.tr("Hello, world!")
Translation.tr('Hello, world!')
Translation.tr(`Hello, world!`)
// 带换行符
Translation.tr("Line 1\nLine 2")
// 带转义字符
Translation.tr("Say \"Hello\"")
// 带参数占位符
Translation.tr("Hello, %1!").arg(name)
```
## 示例输出
### 状态显示
```
$ ./manage-translations.sh status
正在分析翻译状态...
=== 当前项目状态 ===
提取到 166 个可翻译文本
=== 翻译文件状态 ===
en_US: 470 个键
zh_CN: 470 个键
```
### 更新翻译
```
$ ./manage-translations.sh update -l zh_CN
更新翻译文件...
==================================================
处理语言: zh_CN
==================================================
分析结果:
缺少的键: 5
多余的键: 20
发现 5 个缺少的翻译键:
1. "New feature text"
2. "Another new text"
...
是否添加这 5 个缺少的键? (y/n): y
已添加 5 个键
发现 20 个多余的翻译键:
1. "Removed old text" -> "已删除的旧文本"
...
是否删除这 20 个多余的键? (y/n): y
已删除 20 个键
已保存翻译文件
```
### 清理无用键
```
$ ./manage-translations.sh clean
清理不再使用的翻译键...
处理语言: zh_CN
发现 50 个不再使用的键:
1. "old_unused_text"
2. "deprecated_message"
...
是否删除这 50 个不再使用的键? (y/n): y
已删除 50 个键
原始键数: 470, 清理后: 420
```
## 高级功能
### 自定义目录结构
```bash
# 使用自定义目录
./translation-manager.py \
--translations-dir /path/to/translations \
--source-dir /path/to/source
```
## 注意事项
1. **备份重要**:在执行清理操作前,工具会自动创建备份,但建议手动备份重要文件
2. **文本提取限制**
- ~~只支持静态字符串,不支持动态构建的字符串~~
- 动态资源(如变量拼接、运行时生成的文本)无法自动提取,需要在翻译文件中手动添加,并使用 `/*keep*/` 标记进行忽略管理。
- 必须使用 `Translation.tr()` 格式
### 忽略标记功能
对于动态资源或特殊文本,如果不希望被自动清理,可在翻译值末尾添加 `/*keep*/`,工具会自动忽略这些键,不会在清理和同步时删除。
示例:
```json
{
"dynamic_key": "Some dynamic value /*keep*/"
}
```
3. **文件编码**:所有文件必须使用 UTF-8 编码
4. **键名规范**:建议使用英文作为键名,避免使用特殊字符
## 故障排除
### 常见问题
**Q: 添加了 Translation.tr 后文字不显示?**
A: 需要在 QML 文件中使用 `import "root:/"` 导入翻译功能,否则无法正常显示翻译文本。
**Q: 提取的文本数量与预期不符?**
A: 检查是否所有可翻译文本都使用了 `Translation.tr()` 格式,确保没有动态构建的字符串。
**Q: 同步后某些翻译丢失?**
A: 检查源语言文件是否包含所有必要的键,考虑使用不同的源语言进行同步。
**Q: 清理操作删除了需要的键?**
A: 从自动创建的备份文件中恢复,检查源代码中是否正确使用了 `Translation.tr()`。
### 恢复备份
```bash
# 恢复单个文件
cp .config/quickshell/translations/zh_CN.json.backup .config/quickshell/translations/zh_CN.json
# 恢复所有文件
cp .config/quickshell/translations.backup/* .config/quickshell/translations/
```
@@ -0,0 +1,285 @@
# Translation Management Tool Suite
This suite is used to manage project translation files, automatically extract translatable texts, compare differences between language files, and provide maintenance functions.
## Tool Components
### 1. `translation-manager.py` - Main Translation Manager
- Extract translatable texts
- Compare and update translation files
- Interactive addition/removal of translation keys
### 2. `translation-cleaner.py` - Translation File Maintenance Tool
- Clean unused translation keys
- Synchronize key structure across different language files
### 3. `manage-translations.sh` - Convenient Wrapper Script
- Provides a unified command-line interface
- Displays translation status
- Simplifies common operations
## Quick Start
### Using the Wrapper Script (Recommended)
```bash
# Enter the tools directory
cd .config/quickshell/translations/tools
# Show help
./manage-translations.sh --help
# Show current translation status
./manage-translations.sh status
# Extract translatable texts
./manage-translations.sh extract
# Update all translation files
./manage-translations.sh update
# Update a specific language
./manage-translations.sh update -l zh_CN
# Clean unused keys
./manage-translations.sh clean
# Synchronize keys across all language files
./manage-translations.sh sync
```
Or run from the project root:
```bash
# Run from the project root
.config/quickshell/translations/tools/manage-translations.sh status
.config/quickshell/translations/tools/manage-translations.sh update
```
## Detailed Usage
### Translation Manager (`translation-manager.py`)
Basic usage:
```bash
# Process all languages
./translation-manager.py
# Specify a particular language
./translation-manager.py --language zh_CN
# Extract translatable texts only
./translation-manager.py --extract-only
# Show extracted texts
./translation-manager.py --extract-only --show-temp
```
Parameter description:
- `--translations-dir`, `-t`: Translation files directory (default: `.config/quickshell/translations`)
- `--source-dir`, `-s`: Source code directory (default: `.config/quickshell`)
- `--language`, `-l`: Specify the language code to process
- `--extract-only`, `-e`: Only extract translatable texts
- `--show-temp`: Show the content of the temporary extraction file
### Translation Cleaner (`translation-cleaner.py`)
```bash
# Clean unused translation keys
./translation-cleaner.py --clean
# Synchronize translation keys (using en_US as the base)
./translation-cleaner.py --sync
# Specify a different source language for syncing
./translation-cleaner.py --sync --source-lang zh_CN
# Clean without creating backups
./translation-cleaner.py --clean --no-backup
```
## Workflow
### Regular Translation Update Workflow
1. **Check status**:
```bash
./manage-translations.sh status
```
2. **Update translations**:
```bash
./manage-translations.sh update
```
3. **Clean unused keys** (optional):
```bash
./manage-translations.sh clean
```
### Adding a New Language
1. **Create a new language file**:
```bash
./manage-translations.sh update -l new_lang
```
2. **Synchronize key structure**:
```bash
./manage-translations.sh sync
```
### Cleanup After Large Refactoring
1. **Backup translation files**:
```bash
cp -r .config/quickshell/translations .config/quickshell/translations.backup
```
2. **Clean unused keys**:
```bash
./manage-translations.sh clean
```
3. **Synchronize all languages**:
```bash
./manage-translations.sh sync
```
## Supported Translatable Text Formats
The tool recognizes the following formats for translatable texts:
```qml
// Basic format
Translation.tr("Hello, world!")
Translation.tr('Hello, world!')
Translation.tr(`Hello, world!`)
// With line breaks
Translation.tr("Line 1\nLine 2")
// With escape characters
Translation.tr("Say \"Hello\"")
// With parameter placeholders
Translation.tr("Hello, %1!").arg(name)
```
## Example Output
### Status Display
```
$ ./manage-translations.sh status
Analyzing translation status...
=== Current Project Status ===
166 translatable texts extracted
=== Translation File Status ===
en_US: 470 keys
zh_CN: 470 keys
```
### Update Translations
```
$ ./manage-translations.sh update -l zh_CN
Updating translation files...
==================================================
Processing language: zh_CN
==================================================
Analysis result:
Missing keys: 5
Extra keys: 20
Found 5 missing translation keys:
1. "New feature text"
2. "Another new text"
...
Add these 5 missing keys? (y/n): y
5 keys added
Found 20 extra translation keys:
1. "Removed old text" -> "已删除的旧文本"
...
Delete these 20 extra keys? (y/n): y
20 keys deleted
Translation file saved
```
### Clean Unused Keys
```
$ ./manage-translations.sh clean
Cleaning unused translation keys...
Processing language: zh_CN
Found 50 unused keys:
1. "old_unused_text"
2. "deprecated_message"
...
Delete these 50 unused keys? (y/n): y
50 keys deleted
Original key count: 470, after cleaning: 420
```
## Advanced Features
### Custom Directory Structure
```bash
# Use custom directories
./translation-manager.py \
--translations-dir /path/to/translations \
--source-dir /path/to/source
```
### Ignore Mark Feature
For dynamic resources or special texts that should not be automatically cleaned, you can add `/*keep*/` at the end of the translation value. The tool will automatically ignore these keys and will not delete them during cleaning or syncing.
Example:
```json
{
"dynamic_key": "Some dynamic value /*keep*/"
}
```
## Notes
1. **Backup is important**: The tool automatically creates backups before cleaning, but it is recommended to manually back up important files
2. **Text extraction limitations**:
- ~~Only supports static strings, not dynamically constructed strings~~
- Dynamic resources (such as variable concatenation or runtime-generated text) cannot be automatically extracted. You need to manually add them to the translation file and use the `/*keep*/` mark for ignore management.
- Must use the `Translation.tr()` format
3. **File encoding**: All files must use UTF-8 encoding
4. **Key naming conventions**: It is recommended to use English for key names and avoid special characters
## Troubleshooting
### Common Issues
**Q: Text does not appear after adding Translation.tr?**
A: You need to import the translation feature in your QML file using `import "root:/"`, otherwise the translation text will not be displayed correctly.
**Q: The number of extracted texts does not match expectations?**
A: Check whether all translatable texts use the `Translation.tr()` format and ensure there are no dynamically constructed strings.
**Q: Some translations are missing after syncing?**
A: Check whether the source language file contains all necessary keys, and consider using a different source language for syncing.
**Q: The cleaning operation deleted needed keys?**
A: Restore from the automatically created backup file and check whether `Translation.tr()` is used correctly in the source code.
### Restore Backup
```bash
# Restore a single file
cp .config/quickshell/translations/zh_CN.json.backup .config/quickshell/translations/zh_CN.json
# Restore all files
cp .config/quickshell/translations.backup/* .config/quickshell/translations/
```
@@ -0,0 +1,155 @@
#!/bin/bash
# Translation management script - convenient wrapper
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TRANSLATIONS_DIR="$(dirname "$SCRIPT_DIR")"
SOURCE_DIR="$(dirname "$(dirname "$TRANSLATIONS_DIR")")"
show_help() {
echo "Translation Management Tool - Convenient Wrapper"
echo ""
echo "Usage: $0 [options] <command>"
echo ""
echo "Commands:"
echo " extract Extract translatable texts to temporary file"
echo " update Update translation files (add missing/remove extra keys)"
echo " clean Clean unused translation keys"
echo " sync Sync keys across all language files"
echo " status Show translation status"
echo ""
echo "Options:"
echo " -l, --lang LANG Specify language (e.g.: zh_CN)"
echo " -t, --trans-dir DIR Translation files directory (default: $TRANSLATIONS_DIR)"
echo " -s, --source-dir DIR Source code directory (default: $SOURCE_DIR)"
echo " -y, --yes Skip all confirmation prompts (auto-confirm)"
echo " -h, --help Show this help message"
echo ""
echo "Examples:"
echo " $0 extract # Extract translatable texts"
echo " $0 update -l zh_CN # Update Chinese translations"
echo " $0 update # Update all translations"
echo " $0 clean # Clean unused keys"
echo " $0 sync # Sync keys across all languages"
echo " $0 status # Show translation status"
}
show_status() {
echo "Analyzing translation status..."
# Extract current text count
echo "=== Current Project Status ==="
python3 "$SCRIPT_DIR/translation-manager.py" \
--translations-dir "$TRANSLATIONS_DIR" \
--source-dir "$SOURCE_DIR" \
--extract-only | grep "Extracted"
echo ""
echo "=== Translation File Status ==="
if [ -d "$TRANSLATIONS_DIR" ]; then
for file in "$TRANSLATIONS_DIR"/*.json; do
if [ -f "$file" ]; then
lang=$(basename "$file" .json)
count=$(jq 'length' "$file" 2>/dev/null || echo "error")
echo " $lang: $count keys"
fi
done
else
echo " Translation directory does not exist: $TRANSLATIONS_DIR"
fi
}
# Parse command line arguments
LANG_CODE=""
COMMAND=""
YES_FLAG=""
while [[ $# -gt 0 ]]; do
case $1 in
-l|--lang)
LANG_CODE="$2"
shift 2
;;
-t|--trans-dir)
TRANSLATIONS_DIR="$2"
shift 2
;;
-s|--source-dir)
SOURCE_DIR="$2"
shift 2
;;
-y|--yes)
YES_FLAG="-y"
shift
;;
-h|--help)
show_help
exit 0
;;
extract|update|clean|sync|status)
if [ -n "$COMMAND" ]; then
echo "Error: Only one command can be specified"
exit 1
fi
COMMAND="$1"
shift
;;
*)
echo "Unknown option: $1"
show_help
exit 1
;;
esac
done
if [ -z "$COMMAND" ]; then
echo "Error: A command must be specified"
show_help
exit 1
fi
# Check dependencies
if ! command -v python3 >/dev/null 2>&1; then
echo "Error: python3 is required"
exit 1
fi
if [ "$COMMAND" = "status" ] && ! command -v jq >/dev/null 2>&1; then
echo "Warning: jq is not installed, status display may be incomplete"
fi
# Build base arguments
BASE_ARGS="--translations-dir $TRANSLATIONS_DIR --source-dir $SOURCE_DIR"
case $COMMAND in
extract)
echo "Extracting translatable texts..."
python3 "$SCRIPT_DIR/translation-manager.py" $BASE_ARGS $YES_FLAG --extract-only --show-temp
;;
update)
echo "Updating translation files..."
if [ -n "$LANG_CODE" ]; then
python3 "$SCRIPT_DIR/translation-manager.py" $BASE_ARGS $YES_FLAG --language "$LANG_CODE"
else
python3 "$SCRIPT_DIR/translation-manager.py" $BASE_ARGS $YES_FLAG
fi
;;
clean)
echo "Cleaning unused translation keys..."
python3 "$SCRIPT_DIR/translation-cleaner.py" $BASE_ARGS $YES_FLAG --clean
;;
sync)
echo "Syncing translation keys..."
python3 "$SCRIPT_DIR/translation-cleaner.py" $BASE_ARGS $YES_FLAG --sync
;;
status)
show_status
;;
*)
echo "Unknown command: $COMMAND"
show_help
exit 1
;;
esac
@@ -0,0 +1,210 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Translation File Maintenance Helper
Used to clean and organize translation files, removing unused keys
"""
import os
import sys
import json
import argparse
import importlib.util
from pathlib import Path
from typing import Dict, Set, List
# Import from the same directory using importlib
current_dir = os.path.dirname(os.path.abspath(__file__))
manager_path = os.path.join(current_dir, 'translation-manager.py')
spec = importlib.util.spec_from_file_location("translation_manager", manager_path)
translation_manager = importlib.util.module_from_spec(spec)
spec.loader.exec_module(translation_manager)
TranslationManager = translation_manager.TranslationManager
def clean_translation_files(translations_dir: str, source_dir: str, backup: bool = True, yes_mode: bool = False):
"""Clean translation files by removing unused keys"""
print("Starting translation file cleanup...")
# Create manager
manager = TranslationManager(translations_dir, source_dir)
# Extract currently used texts
print("Extracting currently used translatable texts...")
current_texts = manager.extract_translatable_texts()
print(f"Extracted {len(current_texts)} currently used texts")
# Get all language files
languages = manager.get_available_languages()
if not languages:
print("No translation files found")
return
print(f"Found language files: {', '.join(languages)}")
total_removed = 0
for lang in languages:
print(f"\nProcessing language: {lang}")
# Load translation file
translations = manager.load_translation_file(lang)
original_count = len(translations)
# Find unused keys, skip those whose value ends with /*keep*/
unused_keys = set()
for k in translations.keys():
v = translations[k]
if k not in current_texts:
if isinstance(v, str) and v.strip().endswith('/*keep*/'):
continue
unused_keys.add(k)
if unused_keys:
print(f"Found {len(unused_keys)} unused keys:")
for i, key in enumerate(sorted(unused_keys)[:10], 1): # Only show first 10
print(f" {i}. \"{key[:50]}{'...' if len(key) > 50 else ''}\"")
if len(unused_keys) > 10:
print(f" ... and {len(unused_keys) - 10} more keys")
if yes_mode:
response = 'y'
print(f"Delete these {len(unused_keys)} unused keys? (auto-confirmed by --yes)")
else:
response = input(f"Delete these {len(unused_keys)} unused keys? (y/n): ")
if response.lower().strip() in ['y', 'yes']:
if backup:
# Create backup only when user confirms deletion
backup_file = Path(translations_dir) / f"{lang}.json.bak"
with open(backup_file, 'w', encoding='utf-8') as f:
json.dump(translations, f, ensure_ascii=False, indent=2)
print(f"Created backup: {backup_file}")
# Delete unused keys
for key in unused_keys:
del translations[key]
# Save cleaned file
manager.save_translation_file(lang, translations)
removed_count = len(unused_keys)
total_removed += removed_count
print(f"Deleted {removed_count} keys")
else:
print("Skipped deletion")
else:
print("No unused keys found")
new_count = len(translations)
print(f"Original key count: {original_count}, after cleanup: {new_count}")
print(f"\nCleanup completed! Total deleted {total_removed} unused keys.")
def sync_translations(translations_dir: str, source_lang: str = "en_US", target_langs: List[str] = None, yes_mode: bool = False):
"""Sync translation keys to ensure all language files have the same keys"""
print(f"Starting translation key sync using {source_lang} as reference...")
translations_path = Path(translations_dir)
# Load source language file
source_file = translations_path / f"{source_lang}.json"
if not source_file.exists():
print(f"Error: Source language file does not exist: {source_file}")
return
with open(source_file, 'r', encoding='utf-8') as f:
source_translations = json.load(f)
source_keys = set(source_translations.keys())
print(f"Source language {source_lang} has {len(source_keys)} keys")
# Get target language list
if target_langs is None:
target_langs = []
for file_path in translations_path.glob("*.json"):
lang_code = file_path.stem
if lang_code != source_lang:
target_langs.append(lang_code)
if not target_langs:
print("No target language files found")
return
print(f"Target languages: {', '.join(target_langs)}")
for target_lang in target_langs:
print(f"\nSyncing language: {target_lang}")
target_file = translations_path / f"{target_lang}.json"
if target_file.exists():
with open(target_file, 'r', encoding='utf-8') as f:
target_translations = json.load(f)
else:
target_translations = {}
target_keys = set(target_translations.keys())
# Find missing and extra keys
missing_keys = source_keys - target_keys
extra_keys = target_keys - source_keys
print(f" Missing keys: {len(missing_keys)}")
print(f" Extra keys: {len(extra_keys)}")
# Add missing keys
if missing_keys:
for key in missing_keys:
# Use source language value as placeholder by default
target_translations[key] = source_translations[key]
print(f" Added {len(missing_keys)} missing keys")
# Ask whether to delete extra keys
if extra_keys:
if yes_mode:
response = 'y'
print(f" Delete {len(extra_keys)} extra keys? (auto-confirmed by --yes)")
else:
response = input(f" Delete {len(extra_keys)} extra keys? (y/n): ")
if response.lower().strip() in ['y', 'yes']:
for key in extra_keys:
del target_translations[key]
print(f" Deleted {len(extra_keys)} extra keys")
# Save file (ensure UTF-8, fix for special chars)
with open(target_file, 'w', encoding='utf-8', newline='') as f:
json.dump(target_translations, f, ensure_ascii=False, indent=2)
print(f" Saved: {target_file}")
def main():
parser = argparse.ArgumentParser(description="Translation File Maintenance Helper")
parser.add_argument("--translations-dir", "-t",
default=".config/quickshell/translations",
help="Translation files directory")
parser.add_argument("--source-dir", "-s",
default=".config/quickshell",
help="Source code directory")
parser.add_argument("--clean", "-c", action="store_true",
help="Clean unused translation keys")
parser.add_argument("--sync", action="store_true",
help="Sync translation keys")
parser.add_argument("--source-lang", default="en_US",
help="Source language for syncing (default: en_US)")
parser.add_argument("--no-backup", action="store_true",
help="Do not create backup files when cleaning")
parser.add_argument("-y", "--yes", action="store_true",
help="Skip all confirmation prompts (auto-confirm)")
args = parser.parse_args()
# Convert to absolute paths
translations_dir = os.path.abspath(args.translations_dir)
source_dir = os.path.abspath(args.source_dir)
if args.clean:
clean_translation_files(translations_dir, source_dir, backup=not args.no_backup, yes_mode=args.yes)
elif args.sync:
sync_translations(translations_dir, args.source_lang, yes_mode=args.yes)
else:
print("Please specify an operation:")
print(" --clean: Clean unused translation keys")
print(" --sync: Sync translation keys")
if __name__ == "__main__":
main()
@@ -0,0 +1,334 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Translation File Management Script
Used to update and extract translatable texts, manage JSON translation file key comparison
"""
import os
import json
import re
import sys
import argparse
from pathlib import Path
from typing import Dict, Set, List, Tuple
import tempfile
import subprocess
class TranslationManager:
def __init__(self, translations_dir: str, source_dir: str, yes_mode: bool = False):
self.translations_dir = Path(translations_dir)
self.source_dir = Path(source_dir)
self.temp_extracted_file = None
self.yes_mode = yes_mode
# Ensure translation directory exists
self.translations_dir.mkdir(parents=True, exist_ok=True)
def extract_translatable_texts(self) -> Set[str]:
"""Extract translatable texts from source code"""
translatable_texts = set()
# Search patterns: Translation.tr("text") or Translation.tr('text')
# Improved regex that handles nested quotes correctly
patterns = [
r'Translation\.tr\s*\(\s*(["\'])(((?!\1)[^\\]|\\.)*)(\1)\s*\)', # Double or single quotes with escape support
r'Translation\.tr\s*\(\s*`([^`]*(?:\\.[^`]*)*?)`\s*\)', # Backticks (template strings)
]
# Search all .qml and .js files
file_extensions = ['*.qml', '*.js']
for ext in file_extensions:
for file_path in self.source_dir.rglob(ext):
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
for pattern in patterns:
matches = re.findall(pattern, content, re.MULTILINE | re.DOTALL)
for match in matches:
# Handle different match group structures
if isinstance(match, tuple):
# For improved regex, text is in the second group
if len(match) >= 3:
text = match[1] # Second group is the text content
else:
text = match[0] if match else ""
else:
text = match
try:
if '\\u' in text or '\\x' in text:
clean_text = bytes(text, "utf-8").decode("unicode_escape")
else:
clean_text = (
text.replace('\\n', '\n')
.replace('\\t', '\t')
.replace('\\r', '\r')
.replace('\\"', '"')
.replace('\\\'', "'")
.replace('\\f', '\f')
.replace('\\b', '\b')
.replace('\\\\', '\\')
)
except Exception:
clean_text = text
# Clean text (remove extra whitespace)
clean_text = clean_text.strip()
if clean_text:
translatable_texts.add(clean_text)
except (UnicodeDecodeError, IOError) as e:
print(f"Warning: Cannot read file {file_path}: {e}")
return translatable_texts
def create_temp_translation_file(self, texts: Set[str]) -> str:
"""Create temporary JSON file containing extracted texts"""
temp_data = {}
for text in sorted(texts):
temp_data[text] = text # Key and value are the same, indicating untranslated
# Create temporary file
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False, encoding='utf-8') as f:
json.dump(temp_data, f, ensure_ascii=False, indent=2)
self.temp_extracted_file = f.name
return self.temp_extracted_file
def load_translation_file(self, lang_code: str) -> Dict[str, str]:
"""Load translation file for specified language"""
file_path = self.translations_dir / f"{lang_code}.json"
if file_path.exists():
try:
with open(file_path, 'r', encoding='utf-8') as f:
return json.load(f)
except (json.JSONDecodeError, IOError) as e:
print(f"Warning: Cannot load translation file {file_path}: {e}")
return {}
return {}
def save_translation_file(self, lang_code: str, translations: Dict[str, str]):
"""Save translation file"""
file_path = self.translations_dir / f"{lang_code}.json"
try:
with open(file_path, 'w', encoding='utf-8') as f:
json.dump(translations, f, ensure_ascii=False, indent=2)
print(f"Translation file saved: {file_path}")
except IOError as e:
print(f"Error: Cannot save translation file {file_path}: {e}")
def get_available_languages(self) -> List[str]:
"""Get list of available languages"""
languages = []
for file_path in self.translations_dir.glob("*.json"):
lang_code = file_path.stem
languages.append(lang_code)
return sorted(languages)
def compare_translations(self, extracted_texts: Set[str], target_lang: str) -> Tuple[Set[str], Set[str]]:
"""Compare extracted texts with existing translation file"""
existing_translations = self.load_translation_file(target_lang)
existing_keys = set(existing_translations.keys())
missing_keys = extracted_texts - existing_keys # Missing keys
extra_keys = existing_keys - extracted_texts # Extra keys
return missing_keys, extra_keys
def interactive_update(self, lang_code: str, missing_keys: Set[str], extra_keys: Set[str]):
"""Interactively update translation file, create backup only if updating"""
translations = self.load_translation_file(lang_code)
modified = False
backup_created = False
# Handle missing keys
if missing_keys:
print(f"\nFound {len(missing_keys)} missing translation keys:")
for i, key in enumerate(sorted(missing_keys), 1):
print(f"{i}. \"{key}\"")
if self.ask_yes_no(f"\nAdd these {len(missing_keys)} missing keys?"):
if not backup_created:
backup_file = self.translations_dir / f"{lang_code}.json.bak"
with open(backup_file, 'w', encoding='utf-8') as f:
json.dump(translations, f, ensure_ascii=False, indent=2)
print(f"Created backup: {backup_file}")
backup_created = True
for key in missing_keys:
translations[key] = key # Default value is the key itself
modified = True
print(f"Added {len(missing_keys)} keys")
# Handle extra keys
if extra_keys:
# Only show extra keys that are not marked with /*keep*/
filtered_extra_keys = [key for key in extra_keys if not (isinstance(translations.get(key, ""), str) and translations.get(key, "").strip().endswith('/*keep*/'))]
if filtered_extra_keys:
print(f"\nFound {len(filtered_extra_keys)} extra translation keys:")
for i, key in enumerate(sorted(filtered_extra_keys), 1):
print(f"{i}. \"{key}\" -> \"{translations.get(key, '')}\"")
if self.ask_yes_no(f"\nDelete these {len(filtered_extra_keys)} extra keys?"):
if not backup_created:
backup_file = self.translations_dir / f"{lang_code}.json.bak"
with open(backup_file, 'w', encoding='utf-8') as f:
json.dump(translations, f, ensure_ascii=False, indent=2)
print(f"Created backup: {backup_file}")
backup_created = True
deleted_count = 0
for key in filtered_extra_keys:
if key in translations:
del translations[key]
modified = True
deleted_count += 1
print(f"Deleted {deleted_count} keys")
# Save changes
if modified:
self.save_translation_file(lang_code, translations)
else:
print("No changes made")
def ask_yes_no(self, question: str) -> bool:
"""Ask user for confirmation, auto-confirm if yes_mode is True"""
if getattr(self, "yes_mode", False):
print(f"{question} (auto-confirmed by --yes)")
return True
while True:
response = input(f"{question} (y/n): ").lower().strip()
if response in ['y', 'yes']:
return True
elif response in ['n', 'no']:
return False
else:
print("Please enter y/yes or n/no")
def cleanup(self):
"""Clean up temporary files"""
if self.temp_extracted_file and os.path.exists(self.temp_extracted_file):
os.unlink(self.temp_extracted_file)
def main():
parser = argparse.ArgumentParser(description="Translation file management tool")
parser.add_argument("--translations-dir", "-t",
default=".config/quickshell/translations",
help="Translation files directory (default: .config/quickshell/translations)")
parser.add_argument("--source-dir", "-s",
default=".config/quickshell/ii",
help="Source code directory (default: .config/quickshell/ii)")
parser.add_argument("--language", "-l",
help="Specify language code to process (e.g., zh_CN)")
parser.add_argument("--extract-only", "-e", action="store_true",
help="Only extract translatable texts to temporary file")
parser.add_argument("--show-temp", action="store_true",
help="Show temporary extracted file content")
parser.add_argument("-y", "--yes", action="store_true",
help="Skip all confirmation prompts (auto-confirm)")
args = parser.parse_args()
# Convert to absolute paths
translations_dir = os.path.abspath(args.translations_dir)
source_dir = os.path.abspath(args.source_dir)
print(f"Translation directory: {translations_dir}")
print(f"Source code directory: {source_dir}")
# Check if directories exist
if not os.path.exists(source_dir):
print(f"Error: Source code directory does not exist: {source_dir}")
sys.exit(1)
# Create manager
manager = TranslationManager(translations_dir, source_dir, yes_mode=args.yes)
try:
# Extract translatable texts
print("\nExtracting translatable texts...")
extracted_texts = manager.extract_translatable_texts()
print(f"Extracted {len(extracted_texts)} translatable texts")
# Create temporary file
temp_file = manager.create_temp_translation_file(extracted_texts)
print(f"Created temporary file: {temp_file}")
if args.show_temp:
print("\nTemporary file contents:")
with open(temp_file, 'r', encoding='utf-8') as f:
print(f.read())
if args.extract_only:
print("Extract-only mode, program finished")
return
# Get available languages
available_languages = manager.get_available_languages()
if args.language:
target_languages = [args.language]
else:
print(f"\nAvailable languages: {', '.join(available_languages) if available_languages else 'None'}")
if not available_languages:
if manager.yes_mode:
print("No existing translation files found, auto-skipping language creation due to --yes")
return
lang_input = input("Enter language code to create (e.g.: zh_CN): ").strip()
if lang_input:
target_languages = [lang_input]
else:
print("No language specified, program finished")
return
else:
print("Choose language to process:")
for i, lang in enumerate(available_languages, 1):
print(f"{i}. {lang}")
print("a. Process all languages")
if manager.yes_mode:
choice = 'a'
print("Auto-selecting all languages due to --yes")
else:
choice = input("Please choose (enter number, language code, or 'a'): ").strip()
if choice.lower() == 'a':
target_languages = available_languages
elif choice.isdigit() and 1 <= int(choice) <= len(available_languages):
target_languages = [available_languages[int(choice) - 1]]
elif choice in available_languages:
target_languages = [choice]
else:
print("Invalid choice, program finished")
return
# Process each language
for lang in target_languages:
print(f"\n{'='*50}")
print(f"Processing language: {lang}")
print('='*50)
missing_keys, extra_keys = manager.compare_translations(extracted_texts, lang)
if not missing_keys and not extra_keys:
print(f"Translation file for language {lang} is already up to date")
continue
print(f"Analysis results:")
print(f" Missing keys: {len(missing_keys)}")
# Load translation file for current lang to get values
current_translations = manager.load_translation_file(lang)
filtered_extra_keys = [key for key in extra_keys if not (isinstance(current_translations.get(key, ""), str) and current_translations.get(key, "").strip().endswith('/*keep*/'))]
ignored_extra_keys = [key for key in extra_keys if (isinstance(current_translations.get(key, ""), str) and current_translations.get(key, "").strip().endswith('/*keep*/'))]
print(f" Extra keys: {len(filtered_extra_keys)}")
if ignored_extra_keys:
print(f" Ignored keys: {len(ignored_extra_keys)} (marked with /*keep*/)")
if missing_keys or extra_keys:
manager.interactive_update(lang, missing_keys, extra_keys)
finally:
# Clean up temporary files
manager.cleanup()
if __name__ == "__main__":
main()