mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-14 10:34:06 +02:00
Change default language to en-US and add US language (#6621)
This commit is contained in:
@@ -13,7 +13,7 @@ Usage:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# Sample for Windows:
|
# Sample for Windows:
|
||||||
# python .github/scripts/check_language_toml.py --reference-file frontend/editor/public/locales/en-GB/translation.toml --branch "" --files frontend/editor/public/locales/de-DE/translation.toml frontend/editor/public/locales/fr-FR/translation.toml
|
# python .github/scripts/check_language_toml.py --reference-file frontend/editor/public/locales/en-US/translation.toml --branch "" --files frontend/editor/public/locales/de-DE/translation.toml frontend/editor/public/locales/fr-FR/translation.toml
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import glob
|
import glob
|
||||||
@@ -211,7 +211,7 @@ def check_for_differences(reference_file, file_list, branch, actor):
|
|||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if basename_current_file == basename_reference_file and locale_dir == "en-GB":
|
if basename_current_file == basename_reference_file and locale_dir == "en-US":
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -308,7 +308,7 @@ def check_for_differences(reference_file, file_list, branch, actor):
|
|||||||
report.append("## ❌ Overall Check Status: **_Failed_**")
|
report.append("## ❌ Overall Check Status: **_Failed_**")
|
||||||
report.append("")
|
report.append("")
|
||||||
report.append(
|
report.append(
|
||||||
f"@{actor} please check your translation if it conforms to the standard. Follow the format of [en-GB/translation.toml](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/frontend/editor/public/locales/en-GB/translation.toml)"
|
f"@{actor} please check your translation if it conforms to the standard. Follow the format of [en-US/translation.toml](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/frontend/editor/public/locales/en-US/translation.toml)"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
report.append("## ✅ Overall Check Status: **_Success_**")
|
report.append("## ✅ Overall Check Status: **_Success_**")
|
||||||
|
|||||||
@@ -293,7 +293,7 @@ jobs:
|
|||||||
SECURITY_ENABLELOGIN: "true"
|
SECURITY_ENABLELOGIN: "true"
|
||||||
SECURITY_INITIALLOGIN_USERNAME: "${{ secrets.TEST_LOGIN_USERNAME }}"
|
SECURITY_INITIALLOGIN_USERNAME: "${{ secrets.TEST_LOGIN_USERNAME }}"
|
||||||
SECURITY_INITIALLOGIN_PASSWORD: "${{ secrets.TEST_LOGIN_PASSWORD }}"
|
SECURITY_INITIALLOGIN_PASSWORD: "${{ secrets.TEST_LOGIN_PASSWORD }}"
|
||||||
SYSTEM_DEFAULTLOCALE: en-GB
|
SYSTEM_DEFAULTLOCALE: en-US
|
||||||
UI_APPNAME: "Stirling-PDF V2 PR#${{ needs.check-pr.outputs.pr_number }}"
|
UI_APPNAME: "Stirling-PDF V2 PR#${{ needs.check-pr.outputs.pr_number }}"
|
||||||
UI_HOMEDESCRIPTION: "V2 PR#${{ needs.check-pr.outputs.pr_number }} - Embedded Architecture"
|
UI_HOMEDESCRIPTION: "V2 PR#${{ needs.check-pr.outputs.pr_number }} - Embedded Architecture"
|
||||||
UI_APPNAMENAVBAR: "V2 PR#${{ needs.check-pr.outputs.pr_number }}"
|
UI_APPNAMENAVBAR: "V2 PR#${{ needs.check-pr.outputs.pr_number }}"
|
||||||
|
|||||||
@@ -388,7 +388,7 @@ jobs:
|
|||||||
environment:
|
environment:
|
||||||
DISABLE_ADDITIONAL_FEATURES: "${DISABLE_ADDITIONAL_FEATURES}"
|
DISABLE_ADDITIONAL_FEATURES: "${DISABLE_ADDITIONAL_FEATURES}"
|
||||||
SECURITY_ENABLELOGIN: "${LOGIN_SECURITY}"
|
SECURITY_ENABLELOGIN: "${LOGIN_SECURITY}"
|
||||||
SYSTEM_DEFAULTLOCALE: en-GB
|
SYSTEM_DEFAULTLOCALE: en-US
|
||||||
UI_APPNAME: "Stirling-PDF PR#${PR_NUMBER}"
|
UI_APPNAME: "Stirling-PDF PR#${PR_NUMBER}"
|
||||||
UI_HOMEDESCRIPTION: "PR#${PR_NUMBER} for Stirling-PDF Latest"
|
UI_HOMEDESCRIPTION: "PR#${PR_NUMBER} for Stirling-PDF Latest"
|
||||||
UI_APPNAMENAVBAR: "PR#${PR_NUMBER}"
|
UI_APPNAMENAVBAR: "PR#${PR_NUMBER}"
|
||||||
|
|||||||
@@ -166,16 +166,16 @@ jobs:
|
|||||||
|
|
||||||
// Determine reference file
|
// Determine reference file
|
||||||
let referenceFilePath;
|
let referenceFilePath;
|
||||||
if (changedFiles.includes("frontend/editor/public/locales/en-GB/translation.toml")) {
|
if (changedFiles.includes("frontend/editor/public/locales/en-US/translation.toml")) {
|
||||||
console.log("Using PR branch reference file.");
|
console.log("Using PR branch reference file.");
|
||||||
const { data: fileContent } = await github.rest.repos.getContent({
|
const { data: fileContent } = await github.rest.repos.getContent({
|
||||||
owner: prRepoOwner,
|
owner: prRepoOwner,
|
||||||
repo: prRepoName,
|
repo: prRepoName,
|
||||||
path: "frontend/editor/public/locales/en-GB/translation.toml",
|
path: "frontend/editor/public/locales/en-US/translation.toml",
|
||||||
ref: branch,
|
ref: branch,
|
||||||
});
|
});
|
||||||
|
|
||||||
referenceFilePath = "pr-branch-translation-en-GB.toml";
|
referenceFilePath = "pr-branch-translation-en-US.toml";
|
||||||
const content = Buffer.from(fileContent.content, "base64").toString("utf-8");
|
const content = Buffer.from(fileContent.content, "base64").toString("utf-8");
|
||||||
fs.writeFileSync(referenceFilePath, content);
|
fs.writeFileSync(referenceFilePath, content);
|
||||||
} else {
|
} else {
|
||||||
@@ -183,11 +183,11 @@ jobs:
|
|||||||
const { data: fileContent } = await github.rest.repos.getContent({
|
const { data: fileContent } = await github.rest.repos.getContent({
|
||||||
owner: repoOwner,
|
owner: repoOwner,
|
||||||
repo: repoName,
|
repo: repoName,
|
||||||
path: "frontend/editor/public/locales/en-GB/translation.toml",
|
path: "frontend/editor/public/locales/en-US/translation.toml",
|
||||||
ref: "main",
|
ref: "main",
|
||||||
});
|
});
|
||||||
|
|
||||||
referenceFilePath = "main-branch-translation-en-GB.toml";
|
referenceFilePath = "main-branch-translation-en-US.toml";
|
||||||
const content = Buffer.from(fileContent.content, "base64").toString("utf-8");
|
const content = Buffer.from(fileContent.content, "base64").toString("utf-8");
|
||||||
fs.writeFileSync(referenceFilePath, content);
|
fs.writeFileSync(referenceFilePath, content);
|
||||||
}
|
}
|
||||||
@@ -293,6 +293,6 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo "Cleaning up temporary files..."
|
echo "Cleaning up temporary files..."
|
||||||
rm -rf pr-branch
|
rm -rf pr-branch
|
||||||
rm -f pr-branch-translation-en-GB.toml main-branch-translation-en-GB.toml changed_files.txt result.txt
|
rm -f pr-branch-translation-en-US.toml main-branch-translation-en-US.toml changed_files.txt result.txt
|
||||||
echo "Cleanup complete."
|
echo "Cleanup complete."
|
||||||
continue-on-error: true # Ensure cleanup runs even if previous steps fail
|
continue-on-error: true # Ensure cleanup runs even if previous steps fail
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ jobs:
|
|||||||
environment:
|
environment:
|
||||||
DISABLE_ADDITIONAL_FEATURES: "true"
|
DISABLE_ADDITIONAL_FEATURES: "true"
|
||||||
SECURITY_ENABLELOGIN: "false"
|
SECURITY_ENABLELOGIN: "false"
|
||||||
SYSTEM_DEFAULTLOCALE: en-GB
|
SYSTEM_DEFAULTLOCALE: en-US
|
||||||
UI_APPNAME: "Stirling-PDF V2"
|
UI_APPNAME: "Stirling-PDF V2"
|
||||||
UI_HOMEDESCRIPTION: "V2 Frontend/Backend Split"
|
UI_HOMEDESCRIPTION: "V2 Frontend/Backend Split"
|
||||||
UI_APPNAMENAVBAR: "V2 Deployment"
|
UI_APPNAMENAVBAR: "V2 Deployment"
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Sync translation TOML files
|
- name: Sync translation TOML files
|
||||||
run: |
|
run: |
|
||||||
python .github/scripts/check_language_toml.py --reference-file "frontend/editor/public/locales/en-GB/translation.toml" --branch main
|
python .github/scripts/check_language_toml.py --reference-file "frontend/editor/public/locales/en-US/translation.toml" --branch main
|
||||||
|
|
||||||
- name: pre-commit run
|
- name: pre-commit run
|
||||||
run: |
|
run: |
|
||||||
@@ -100,7 +100,7 @@ jobs:
|
|||||||
This Pull Request was automatically generated to synchronize updates to translation files and documentation. Below are the details of the changes made:
|
This Pull Request was automatically generated to synchronize updates to translation files and documentation. Below are the details of the changes made:
|
||||||
|
|
||||||
#### **1. Synchronization of Translation Files**
|
#### **1. Synchronization of Translation Files**
|
||||||
- Updated translation files (`frontend/editor/public/locales/*/translation.toml`) to reflect changes in the reference file `en-GB/translation.toml`.
|
- Updated translation files (`frontend/editor/public/locales/*/translation.toml`) to reflect changes in the reference file `en-US/translation.toml`.
|
||||||
- Ensured consistency and synchronization across all supported language files.
|
- Ensured consistency and synchronization across all supported language files.
|
||||||
- Highlighted any missing or incomplete translations.
|
- Highlighted any missing or incomplete translations.
|
||||||
- **Format**: TOML
|
- **Format**: TOML
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ jobs:
|
|||||||
environment:
|
environment:
|
||||||
DISABLE_ADDITIONAL_FEATURES: "true"
|
DISABLE_ADDITIONAL_FEATURES: "true"
|
||||||
SECURITY_ENABLELOGIN: "false"
|
SECURITY_ENABLELOGIN: "false"
|
||||||
SYSTEM_DEFAULTLOCALE: en-GB
|
SYSTEM_DEFAULTLOCALE: en-US
|
||||||
UI_APPNAME: "Stirling-PDF Test"
|
UI_APPNAME: "Stirling-PDF Test"
|
||||||
UI_HOMEDESCRIPTION: "Test Deployment"
|
UI_HOMEDESCRIPTION: "Test Deployment"
|
||||||
UI_APPNAMENAVBAR: "Test"
|
UI_APPNAMENAVBAR: "Test"
|
||||||
|
|||||||
+3
-3
@@ -200,9 +200,9 @@ const [ToolName] = (props: BaseToolProps) => {
|
|||||||
```
|
```
|
||||||
|
|
||||||
## 5. Add Translations
|
## 5. Add Translations
|
||||||
Update translation files. **Important: Only update `en-GB` files** - other languages are handled separately.
|
Update translation files. **Important: Only update `en-US` files** - other languages are handled separately.
|
||||||
|
|
||||||
**File to update:** `frontend/editor/public/locales/en-GB/translation.toml`
|
**File to update:** `frontend/editor/public/locales/en-US/translation.toml`
|
||||||
|
|
||||||
**Required Translation Keys**:
|
**Required Translation Keys**:
|
||||||
```toml
|
```toml
|
||||||
@@ -251,7 +251,7 @@ Update translation files. **Important: Only update `en-GB` files** - other langu
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Translation Notes:**
|
**Translation Notes:**
|
||||||
- **Only update `en-GB/translation.toml`** - other locale files are managed separately
|
- **Only update `en-US/translation.toml`** - other locale files are managed separately
|
||||||
- Use descriptive keys that match your component's `t()` calls
|
- Use descriptive keys that match your component's `t()` calls
|
||||||
- Include tooltip translations if you created tooltip hooks
|
- Include tooltip translations if you created tooltip hooks
|
||||||
- Add `options.*` keys if your tool has settings with descriptions
|
- Add `options.*` keys if your tool has settings with descriptions
|
||||||
|
|||||||
@@ -426,7 +426,7 @@ The frontend is organized with a clear separation of concerns:
|
|||||||
|
|
||||||
## Translation Rules
|
## Translation Rules
|
||||||
|
|
||||||
- **CRITICAL**: Always update translations in `en-GB` only, never `en-US`
|
- **CRITICAL**: Always update translations in `en-US` only - all other languages (including `en-GB`) are handled separately
|
||||||
- Translation files are located in `frontend/editor/public/locales/`
|
- Translation files are located in `frontend/editor/public/locales/`
|
||||||
|
|
||||||
## Important Notes
|
## Important Notes
|
||||||
|
|||||||
+1
-1
@@ -587,7 +587,7 @@ When adding a new feature or modifying existing ones in Stirling-PDF, you'll nee
|
|||||||
Find the existing `messages.properties` files in the `stirling-pdf/src/main/resources` directory. You'll see files like:
|
Find the existing `messages.properties` files in the `stirling-pdf/src/main/resources` directory. You'll see files like:
|
||||||
|
|
||||||
- `messages.properties` (default, usually English)
|
- `messages.properties` (default, usually English)
|
||||||
- `messages_en_GB.properties`
|
- `messages_en_US.properties`
|
||||||
- `messages_fr_FR.properties`
|
- `messages_fr_FR.properties`
|
||||||
- `messages_de_DE.properties`
|
- `messages_de_DE.properties`
|
||||||
- etc.
|
- etc.
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ public class LocaleConfiguration implements WebMvcConfigurer {
|
|||||||
public LocaleResolver localeResolver() {
|
public LocaleResolver localeResolver() {
|
||||||
SessionLocaleResolver slr = new SessionLocaleResolver();
|
SessionLocaleResolver slr = new SessionLocaleResolver();
|
||||||
String appLocaleEnv = applicationProperties.getSystem().getDefaultLocale();
|
String appLocaleEnv = applicationProperties.getSystem().getDefaultLocale();
|
||||||
Locale defaultLocale = // Fallback to UK locale if environment variable is not set
|
Locale defaultLocale = // Fallback to US locale if environment variable is not set
|
||||||
Locale.UK;
|
Locale.US;
|
||||||
if (appLocaleEnv != null && !appLocaleEnv.isEmpty()) {
|
if (appLocaleEnv != null && !appLocaleEnv.isEmpty()) {
|
||||||
Locale tempLocale = Locale.forLanguageTag(appLocaleEnv);
|
Locale tempLocale = Locale.forLanguageTag(appLocaleEnv);
|
||||||
String tempLanguageTag = tempLocale.toLanguageTag();
|
String tempLanguageTag = tempLocale.toLanguageTag();
|
||||||
@@ -51,7 +51,7 @@ public class LocaleConfiguration implements WebMvcConfigurer {
|
|||||||
defaultLocale = tempLocale;
|
defaultLocale = tempLocale;
|
||||||
} else {
|
} else {
|
||||||
System.err.println(
|
System.err.println(
|
||||||
"Invalid SYSTEM_DEFAULTLOCALE environment variable value. Falling back to default en-GB.");
|
"Invalid SYSTEM_DEFAULTLOCALE environment variable value. Falling back to default en-US.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -48,7 +48,7 @@ public class AdditionalLanguageJsController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Fallback
|
// Fallback
|
||||||
return "en_GB";
|
return "en_US";
|
||||||
}
|
}
|
||||||
""");
|
""");
|
||||||
writer.flush();
|
writer.flush();
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ legal:
|
|||||||
impressum: "" # URL to the impressum of your application (e.g. https://example.com/impressum). Empty string to disable or filename to load from local file in static folder
|
impressum: "" # URL to the impressum of your application (e.g. https://example.com/impressum). Empty string to disable or filename to load from local file in static folder
|
||||||
|
|
||||||
system:
|
system:
|
||||||
defaultLocale: en-US # set the default language (e.g. 'de-DE', 'fr-FR', etc)
|
defaultLocale: "" # force a default language for new users (e.g. 'en-US', 'de-DE'). Empty string auto-detects from the browser, falling back to en-US
|
||||||
googlevisibility: false # 'true' to allow Google visibility (via robots.txt), 'false' to disallow
|
googlevisibility: false # 'true' to allow Google visibility (via robots.txt), 'false' to disallow
|
||||||
enableAlphaFunctionality: false # set to enable functionality which might need more testing before it fully goes live (this feature might make no changes)
|
enableAlphaFunctionality: false # set to enable functionality which might need more testing before it fully goes live (this feature might make no changes)
|
||||||
showUpdate: false # see when a new update is available
|
showUpdate: false # see when a new update is available
|
||||||
|
|||||||
+3
-3
@@ -23,7 +23,7 @@ class AdditionalLanguageJsControllerTest {
|
|||||||
LanguageService lang = mock(LanguageService.class);
|
LanguageService lang = mock(LanguageService.class);
|
||||||
// LinkedHashSet for deterministic order in the array
|
// LinkedHashSet for deterministic order in the array
|
||||||
when(lang.getSupportedLanguages())
|
when(lang.getSupportedLanguages())
|
||||||
.thenReturn(new LinkedHashSet<>(List.of("de_DE", "en_GB")));
|
.thenReturn(new LinkedHashSet<>(List.of("de_DE", "en_US")));
|
||||||
|
|
||||||
MockMvc mvc =
|
MockMvc mvc =
|
||||||
MockMvcBuilders.standaloneSetup(new AdditionalLanguageJsController(lang)).build();
|
MockMvcBuilders.standaloneSetup(new AdditionalLanguageJsController(lang)).build();
|
||||||
@@ -36,9 +36,9 @@ class AdditionalLanguageJsControllerTest {
|
|||||||
.string(
|
.string(
|
||||||
containsString(
|
containsString(
|
||||||
"const supportedLanguages ="
|
"const supportedLanguages ="
|
||||||
+ " [\"de_DE\",\"en_GB\"];")))
|
+ " [\"de_DE\",\"en_US\"];")))
|
||||||
.andExpect(content().string(containsString("function getDetailedLanguageCode()")))
|
.andExpect(content().string(containsString("function getDetailedLanguageCode()")))
|
||||||
.andExpect(content().string(containsString("return \"en_GB\";")));
|
.andExpect(content().string(containsString("return \"en_US\";")));
|
||||||
|
|
||||||
verify(lang, times(1)).getSupportedLanguages();
|
verify(lang, times(1)).getSupportedLanguages();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ Java forms the core of Stirling-PDF. When adding new features or handling errors
|
|||||||
2. **Use `try-with-resources`** when working with streams or other closable resources to ensure clean-up even on failure.
|
2. **Use `try-with-resources`** when working with streams or other closable resources to ensure clean-up even on failure.
|
||||||
3. **Return meaningful HTTP status codes** in controllers by throwing `ResponseStatusException` or using `@ExceptionHandler` methods.
|
3. **Return meaningful HTTP status codes** in controllers by throwing `ResponseStatusException` or using `@ExceptionHandler` methods.
|
||||||
4. **Log with context** using the project’s logging framework. Include identifiers or IDs that help trace the issue.
|
4. **Log with context** using the project’s logging framework. Include identifiers or IDs that help trace the issue.
|
||||||
5. **Internationalise messages** by placing user-facing text in `messages_en_GB.properties` and referencing them with message keys.
|
5. **Internationalise messages** by placing user-facing text in `messages_en_US.properties` and referencing them with message keys.
|
||||||
|
|
||||||
## JavaScript
|
## JavaScript
|
||||||
|
|
||||||
@@ -55,11 +55,11 @@ except Exception as err:
|
|||||||
|
|
||||||
## Internationalisation (i18n)
|
## Internationalisation (i18n)
|
||||||
|
|
||||||
All user-visible error strings should be defined in the main translation file (`messages_en_GB.properties`). Other language files will use the same keys. Refer to messages in code rather than hard-coding text.
|
All user-visible error strings should be defined in the main translation file (`messages_en_US.properties`). Other language files will use the same keys. Refer to messages in code rather than hard-coding text.
|
||||||
|
|
||||||
When creating new messages:
|
When creating new messages:
|
||||||
|
|
||||||
1. Add the English phrase to `messages_en_GB.properties`.
|
1. Add the English phrase to `messages_en_US.properties`.
|
||||||
2. Reference the message key in your Java, JavaScript, or Python code.
|
2. Reference the message key in your Java, JavaScript, or Python code.
|
||||||
3. Update other localisation files as needed.
|
3. Update other localisation files as needed.
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ Fork Stirling-PDF and create a new branch out of `main`.
|
|||||||
- Use hyphenated format: `pl-PL` (not underscore)
|
- Use hyphenated format: `pl-PL` (not underscore)
|
||||||
|
|
||||||
2. Copy the reference translation file:
|
2. Copy the reference translation file:
|
||||||
- Source: `frontend/editor/public/locales/en-GB/translation.toml`
|
- Source: `frontend/editor/public/locales/en-US/translation.toml`
|
||||||
- Destination: `frontend/editor/public/locales/pl-PL/translation.toml`
|
- Destination: `frontend/editor/public/locales/pl-PL/translation.toml`
|
||||||
|
|
||||||
3. Translate all entries in the TOML file
|
3. Translate all entries in the TOML file
|
||||||
@@ -47,10 +47,10 @@ ignore = [
|
|||||||
## Add New Translation Tags
|
## Add New Translation Tags
|
||||||
|
|
||||||
> [!IMPORTANT]
|
> [!IMPORTANT]
|
||||||
> If you add any new translation tags, they must first be added to the `en-GB/translation.toml` file. This ensures consistency across all language files.
|
> If you add any new translation tags, they must first be added to the `en-US/translation.toml` file. This ensures consistency across all language files.
|
||||||
|
|
||||||
- New translation tags **must be added** to `frontend/editor/public/locales/en-GB/translation.toml` to maintain a reference for other languages.
|
- New translation tags **must be added** to `frontend/editor/public/locales/en-US/translation.toml` to maintain a reference for other languages.
|
||||||
- After adding the new tags to `en-GB/translation.toml`, add and translate them in the respective language file (e.g., `pl-PL/translation.toml`).
|
- After adding the new tags to `en-US/translation.toml`, add and translate them in the respective language file (e.g., `pl-PL/translation.toml`).
|
||||||
- Use the scripts in `scripts/translations/` to validate and manage translations (see `scripts/translations/README.md`)
|
- Use the scripts in `scripts/translations/` to validate and manage translations (see `scripts/translations/README.md`)
|
||||||
|
|
||||||
Make sure to place the entry under the correct language section. This helps maintain the accuracy of translation progress statistics and ensures that the translation tool or scripts do not misinterpret the completion rate.
|
Make sure to place the entry under the correct language section. This helps maintain the accuracy of translation progress statistics and ensures that the translation tool or scripts do not misinterpret the completion rate.
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
The script [`scripts/counter_translation.py`](../scripts/counter_translation.py) checks the translation progress of the property files in the directory `app/core/src/main/resources/`.
|
The script [`scripts/counter_translation.py`](../scripts/counter_translation.py) checks the translation progress of the property files in the directory `app/core/src/main/resources/`.
|
||||||
It compares each `messages_*.properties` file with the English reference file `messages_en_GB.properties` and calculates a percentage of completion for each language.
|
It compares each `messages_*.properties` file with the English reference file `messages_en_US.properties` and calculates a percentage of completion for each language.
|
||||||
|
|
||||||
In addition to console output, the script automatically updates the progress badges in the project’s `README.md` and maintains the configuration file [`scripts/ignore_translation.toml`](../scripts/ignore_translation.toml), which lists translation keys to be ignored for each language.
|
In addition to console output, the script automatically updates the progress badges in the project’s `README.md` and maintains the configuration file [`scripts/ignore_translation.toml`](../scripts/ignore_translation.toml), which lists translation keys to be ignored for each language.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en-GB">
|
<html lang="en-US">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<base href="%BASE_URL%" />
|
<base href="%BASE_URL%" />
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -243,7 +243,7 @@ const LanguageSelector: React.FC<LanguageSelectorProps> = ({
|
|||||||
|
|
||||||
const currentLanguage =
|
const currentLanguage =
|
||||||
supportedLanguages[i18n.language as keyof typeof supportedLanguages] ||
|
supportedLanguages[i18n.language as keyof typeof supportedLanguages] ||
|
||||||
supportedLanguages["en-GB"] ||
|
supportedLanguages["en-US"] ||
|
||||||
"English"; // Fallback if supportedLanguages lookup fails
|
"English"; // Fallback if supportedLanguages lookup fails
|
||||||
|
|
||||||
// Hide the language selector if there's only one language option
|
// Hide the language selector if there's only one language option
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import TomlBackend from "@app/i18n/tomlBackend";
|
|||||||
|
|
||||||
// Define supported languages (based on your existing translations)
|
// Define supported languages (based on your existing translations)
|
||||||
export const supportedLanguages = {
|
export const supportedLanguages = {
|
||||||
"en-GB": "English",
|
"en-US": "English (US)",
|
||||||
|
"en-GB": "English (UK)",
|
||||||
"ar-AR": "العربية",
|
"ar-AR": "العربية",
|
||||||
"az-AZ": "Azərbaycan Dili",
|
"az-AZ": "Azərbaycan Dili",
|
||||||
"bg-BG": "Български",
|
"bg-BG": "Български",
|
||||||
@@ -72,7 +73,7 @@ i18n
|
|||||||
.use(LanguageDetector)
|
.use(LanguageDetector)
|
||||||
.use(initReactI18next)
|
.use(initReactI18next)
|
||||||
.init({
|
.init({
|
||||||
fallbackLng: "en-GB",
|
fallbackLng: "en-US",
|
||||||
supportedLngs: Object.keys(supportedLanguages),
|
supportedLngs: Object.keys(supportedLanguages),
|
||||||
load: "currentOnly",
|
load: "currentOnly",
|
||||||
nonExplicitSupportedLngs: false,
|
nonExplicitSupportedLngs: false,
|
||||||
@@ -100,8 +101,8 @@ i18n
|
|||||||
order: ["localStorage", "navigator", "htmlTag"],
|
order: ["localStorage", "navigator", "htmlTag"],
|
||||||
caches: [], // Don't cache auto-detected language - only cache when user manually selects
|
caches: [], // Don't cache auto-detected language - only cache when user manually selects
|
||||||
convertDetectedLanguage: (lng: string) => {
|
convertDetectedLanguage: (lng: string) => {
|
||||||
// Map en and en-US to en-GB
|
// Map bare en to en-US
|
||||||
if (lng === "en" || lng === "en-US") return "en-GB";
|
if (lng === "en") return "en-US";
|
||||||
return lng;
|
return lng;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -154,7 +155,7 @@ export function normalizeLanguageCode(languageCode: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert language codes to underscore format (e.g., en-GB → en_GB)
|
* Convert language codes to underscore format (e.g., en-US → en_US)
|
||||||
* Used for backend API communication which expects underscore format
|
* Used for backend API communication which expects underscore format
|
||||||
*/
|
*/
|
||||||
export function toUnderscoreFormat(languageCode: string): string {
|
export function toUnderscoreFormat(languageCode: string): string {
|
||||||
@@ -212,7 +213,7 @@ export function setUserLanguage(language: string): void {
|
|||||||
* Updates the supported languages list dynamically based on config
|
* Updates the supported languages list dynamically based on config
|
||||||
* If configLanguages is null/empty, all languages remain available
|
* If configLanguages is null/empty, all languages remain available
|
||||||
* Otherwise, only the specified languages are enabled with the first valid
|
* Otherwise, only the specified languages are enabled with the first valid
|
||||||
* option (preferring en-GB when present) used as the fallback language.
|
* option (preferring en-US when present) used as the fallback language.
|
||||||
*
|
*
|
||||||
* @param configLanguages - Optional array of language codes from server config (ui.languages)
|
* @param configLanguages - Optional array of language codes from server config (ui.languages)
|
||||||
* @param defaultLocale - Optional default language for new users (system.defaultLocale)
|
* @param defaultLocale - Optional default language for new users (system.defaultLocale)
|
||||||
@@ -248,12 +249,12 @@ export function updateSupportedLanguages(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine fallback: prefer validDefault if in the list, then en-GB, then first valid language
|
// Determine fallback: prefer validDefault if in the list, then en-US, then first valid language
|
||||||
const fallback =
|
const fallback =
|
||||||
validDefault && validLanguages.includes(validDefault)
|
validDefault && validLanguages.includes(validDefault)
|
||||||
? validDefault
|
? validDefault
|
||||||
: validLanguages.includes("en-GB")
|
: validLanguages.includes("en-US")
|
||||||
? "en-GB"
|
? "en-US"
|
||||||
: validLanguages[0];
|
: validLanguages[0];
|
||||||
|
|
||||||
i18n.options.supportedLngs = validLanguages;
|
i18n.options.supportedLngs = validLanguages;
|
||||||
|
|||||||
@@ -154,8 +154,8 @@ export async function mockAppApis(
|
|||||||
email: "[email protected]",
|
email: "[email protected]",
|
||||||
roles: ["ROLE_USER"],
|
roles: ["ROLE_USER"],
|
||||||
},
|
},
|
||||||
languages = ["en-GB"],
|
languages = ["en-US"],
|
||||||
defaultLocale = "en-GB",
|
defaultLocale = "en-US",
|
||||||
endpointsAvailability = {},
|
endpointsAvailability = {},
|
||||||
backendStatus = "UP",
|
backendStatus = "UP",
|
||||||
} = opts;
|
} = opts;
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import { parse } from "smol-toml";
|
|||||||
|
|
||||||
const REPO_ROOT = path.join(__dirname, "../../../..");
|
const REPO_ROOT = path.join(__dirname, "../../../..");
|
||||||
const SRC_ROOT = path.join(__dirname, "../..");
|
const SRC_ROOT = path.join(__dirname, "../..");
|
||||||
const EN_GB_FILE = path.join(
|
const EN_US_FILE = path.join(
|
||||||
__dirname,
|
__dirname,
|
||||||
"../../../public/locales/en-GB/translation.toml",
|
"../../../public/locales/en-US/translation.toml",
|
||||||
);
|
);
|
||||||
|
|
||||||
const IGNORED_DIRS = new Set(["tests", "__mocks__"]);
|
const IGNORED_DIRS = new Set(["tests", "__mocks__"]);
|
||||||
@@ -174,14 +174,14 @@ const extractKeys = (file: string): FoundKey[] => {
|
|||||||
|
|
||||||
describe("Missing translation coverage", () => {
|
describe("Missing translation coverage", () => {
|
||||||
test(
|
test(
|
||||||
"fails if any en-GB translation key used in source is missing",
|
"fails if any en-US translation key used in source is missing",
|
||||||
{ timeout: 10000 },
|
{ timeout: 10000 },
|
||||||
() => {
|
() => {
|
||||||
expect(fs.existsSync(EN_GB_FILE)).toBe(true);
|
expect(fs.existsSync(EN_US_FILE)).toBe(true);
|
||||||
|
|
||||||
const localeContent = fs.readFileSync(EN_GB_FILE, "utf8");
|
const localeContent = fs.readFileSync(EN_US_FILE, "utf8");
|
||||||
const enGb = parse(localeContent);
|
const enUs = parse(localeContent);
|
||||||
const availableKeys = flattenKeys(enGb);
|
const availableKeys = flattenKeys(enUs);
|
||||||
|
|
||||||
const usedKeys = listSourceFiles()
|
const usedKeys = listSourceFiles()
|
||||||
.flatMap(extractKeys)
|
.flatMap(extractKeys)
|
||||||
@@ -211,7 +211,7 @@ describe("Missing translation coverage", () => {
|
|||||||
// Output errors in GitHub Annotations format so they appear tagged in the code in CI
|
// Output errors in GitHub Annotations format so they appear tagged in the code in CI
|
||||||
for (const { key, fallback, file, line, column } of annotations) {
|
for (const { key, fallback, file, line, column } of annotations) {
|
||||||
process.stderr.write(
|
process.stderr.write(
|
||||||
`::error file=${file},line=${line},col=${column}::Missing en-GB translation for ${key} (${fallback})\n`,
|
`::error file=${file},line=${line},col=${column}::Missing en-US translation for ${key} (${fallback})\n`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ test.describe("OAuth/SSO login buttons", () => {
|
|||||||
route.fulfill({
|
route.fulfill({
|
||||||
json: {
|
json: {
|
||||||
enableLogin: true,
|
enableLogin: true,
|
||||||
languages: ["en-GB"],
|
languages: ["en-US"],
|
||||||
defaultLocale: "en-GB",
|
defaultLocale: "en-US",
|
||||||
oauth2: {
|
oauth2: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
providers: [
|
providers: [
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import { parse } from "smol-toml";
|
|||||||
|
|
||||||
const REPO_ROOT = path.join(__dirname, "../../../..");
|
const REPO_ROOT = path.join(__dirname, "../../../..");
|
||||||
const SRC_ROOT = path.join(__dirname, "../..");
|
const SRC_ROOT = path.join(__dirname, "../..");
|
||||||
const EN_GB_FILE = path.join(
|
const EN_US_FILE = path.join(
|
||||||
__dirname,
|
__dirname,
|
||||||
"../../../public/locales/en-GB/translation.toml",
|
"../../../public/locales/en-US/translation.toml",
|
||||||
);
|
);
|
||||||
|
|
||||||
const IGNORED_DIRS = new Set(["tests", "__mocks__"]);
|
const IGNORED_DIRS = new Set(["tests", "__mocks__"]);
|
||||||
@@ -151,13 +151,13 @@ const isIgnored = (key: string): boolean => {
|
|||||||
|
|
||||||
describe("Unused translation coverage", () => {
|
describe("Unused translation coverage", () => {
|
||||||
test(
|
test(
|
||||||
"fails if any en-GB translation key has no source references",
|
"fails if any en-US translation key has no source references",
|
||||||
{ timeout: 30_000 },
|
{ timeout: 30_000 },
|
||||||
() => {
|
() => {
|
||||||
expect(fs.existsSync(EN_GB_FILE)).toBe(true);
|
expect(fs.existsSync(EN_US_FILE)).toBe(true);
|
||||||
|
|
||||||
const enGb = parse(fs.readFileSync(EN_GB_FILE, "utf8"));
|
const enUs = parse(fs.readFileSync(EN_US_FILE, "utf8"));
|
||||||
const availableKeys = Array.from(flattenKeys(enGb));
|
const availableKeys = Array.from(flattenKeys(enUs));
|
||||||
expect(availableKeys.length).toBeGreaterThan(100); // sanity check
|
expect(availableKeys.length).toBeGreaterThan(100); // sanity check
|
||||||
|
|
||||||
const sourceFiles = listSourceFiles();
|
const sourceFiles = listSourceFiles();
|
||||||
@@ -184,20 +184,20 @@ describe("Unused translation coverage", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const localeRelative = path
|
const localeRelative = path
|
||||||
.relative(REPO_ROOT, EN_GB_FILE)
|
.relative(REPO_ROOT, EN_US_FILE)
|
||||||
.replace(/\\/g, "/");
|
.replace(/\\/g, "/");
|
||||||
|
|
||||||
// GitHub Annotations format so unused keys show up tagged on the
|
// GitHub Annotations format so unused keys show up tagged on the
|
||||||
// translation file in CI.
|
// translation file in CI.
|
||||||
for (const key of unused) {
|
for (const key of unused) {
|
||||||
process.stderr.write(
|
process.stderr.write(
|
||||||
`::error file=${localeRelative}::Unused en-GB translation: ${key}\n`,
|
`::error file=${localeRelative}::Unused en-US translation: ${key}\n`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
unused,
|
unused,
|
||||||
`Found ${unused.length} unused en-GB translation key(s). ` +
|
`Found ${unused.length} unused en-US translation key(s). ` +
|
||||||
`Remove them from ${localeRelative}, or (if the usage is too ` +
|
`Remove them from ${localeRelative}, or (if the usage is too ` +
|
||||||
`dynamic for the heuristic to spot) add to IGNORED_KEY_PATTERNS ` +
|
`dynamic for the heuristic to spot) add to IGNORED_KEY_PATTERNS ` +
|
||||||
`in this test.`,
|
`in this test.`,
|
||||||
|
|||||||
@@ -13,7 +13,16 @@ const languageDefinitions: LanguageDefinition[] = [
|
|||||||
{
|
{
|
||||||
ocrCode: "eng",
|
ocrCode: "eng",
|
||||||
displayName: "English",
|
displayName: "English",
|
||||||
browserCodes: ["en", "en-GB", "en-AU", "en-CA", "en-IE", "en-NZ", "en-ZA"],
|
browserCodes: [
|
||||||
|
"en",
|
||||||
|
"en-US",
|
||||||
|
"en-GB",
|
||||||
|
"en-AU",
|
||||||
|
"en-CA",
|
||||||
|
"en-IE",
|
||||||
|
"en-NZ",
|
||||||
|
"en-ZA",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
// Spanish
|
// Spanish
|
||||||
@@ -908,12 +917,12 @@ languageDefinitions.forEach((lang) => {
|
|||||||
* Maps a browser language code to an OCR language code
|
* Maps a browser language code to an OCR language code
|
||||||
* Handles exact matches and similar language fallbacks
|
* Handles exact matches and similar language fallbacks
|
||||||
*
|
*
|
||||||
* @param browserLanguage - The browser language code (e.g., 'en-GB', 'fr-FR')
|
* @param browserLanguage - The browser language code (e.g., 'en-US', 'fr-FR')
|
||||||
* @returns OCR language code if found, null if no match
|
* @returns OCR language code if found, null if no match
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* mapBrowserLanguageToOcr('de-DE') // Returns 'deu'
|
* mapBrowserLanguageToOcr('de-DE') // Returns 'deu'
|
||||||
* mapBrowserLanguageToOcr('en-GB') // Returns 'eng'
|
* mapBrowserLanguageToOcr('en-US') // Returns 'eng'
|
||||||
* mapBrowserLanguageToOcr('zh-CN') // Returns 'chi_sim'
|
* mapBrowserLanguageToOcr('zh-CN') // Returns 'chi_sim'
|
||||||
*/
|
*/
|
||||||
export function mapBrowserLanguageToOcr(
|
export function mapBrowserLanguageToOcr(
|
||||||
@@ -940,7 +949,7 @@ export function mapBrowserLanguageToOcr(
|
|||||||
if (match) return match;
|
if (match) return match;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try base language code (e.g., 'en' from 'en-GB')
|
// Try base language code (e.g., 'en' from 'en-US')
|
||||||
const baseLanguage = normalizedInput.split("-")[0];
|
const baseLanguage = normalizedInput.split("-")[0];
|
||||||
const baseMatch = browserToOcrMap.get(baseLanguage);
|
const baseMatch = browserToOcrMap.get(baseLanguage);
|
||||||
if (baseMatch) return baseMatch;
|
if (baseMatch) return baseMatch;
|
||||||
@@ -1002,7 +1011,7 @@ export function getBrowserLanguagesForOcr(ocrCode: string): string[] {
|
|||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* getAutoOcrLanguage('de-DE') // Returns ['deu']
|
* getAutoOcrLanguage('de-DE') // Returns ['deu']
|
||||||
* getAutoOcrLanguage('en-GB') // Returns ['eng']
|
* getAutoOcrLanguage('en-US') // Returns ['eng']
|
||||||
* getAutoOcrLanguage('unknown') // Returns []
|
* getAutoOcrLanguage('unknown') // Returns []
|
||||||
*/
|
*/
|
||||||
export function getAutoOcrLanguage(currentLanguage: string): string[] {
|
export function getAutoOcrLanguage(currentLanguage: string): string[] {
|
||||||
|
|||||||
+1
-1
@@ -625,7 +625,7 @@ export default function AdminGeneralSection() {
|
|||||||
data={defaultLocaleOptions}
|
data={defaultLocaleOptions}
|
||||||
searchable
|
searchable
|
||||||
clearable
|
clearable
|
||||||
placeholder="en_GB"
|
placeholder="en_US"
|
||||||
comboboxProps={{ zIndex: Z_INDEX_CONFIG_MODAL }}
|
comboboxProps={{ zIndex: Z_INDEX_CONFIG_MODAL }}
|
||||||
disabled={!loginEnabled}
|
disabled={!loginEnabled}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -26,10 +26,10 @@ vi.mock("react-i18next", () => ({
|
|||||||
// Mock i18n module to avoid initialization
|
// Mock i18n module to avoid initialization
|
||||||
vi.mock("@app/i18n", () => ({
|
vi.mock("@app/i18n", () => ({
|
||||||
updateSupportedLanguages: vi.fn(),
|
updateSupportedLanguages: vi.fn(),
|
||||||
supportedLanguages: { "en-GB": "English" },
|
supportedLanguages: { "en-US": "English (US)" },
|
||||||
rtlLanguages: [],
|
rtlLanguages: [],
|
||||||
default: {
|
default: {
|
||||||
language: "en-GB",
|
language: "en-US",
|
||||||
changeLanguage: vi.fn(),
|
changeLanguage: vi.fn(),
|
||||||
options: {},
|
options: {},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export function TrialStatusBanner() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const trialEndDate = new Date(trialStatus.trialEnd).toLocaleDateString(
|
const trialEndDate = new Date(trialStatus.trialEnd).toLocaleDateString(
|
||||||
"en-GB",
|
"en-US",
|
||||||
{
|
{
|
||||||
month: "short",
|
month: "short",
|
||||||
day: "numeric",
|
day: "numeric",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en-GB">
|
<html lang="en-US">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ A script to update language progress status in README.md based on
|
|||||||
frontend locale TOML file comparisons.
|
frontend locale TOML file comparisons.
|
||||||
|
|
||||||
This script compares the default (reference) TOML file,
|
This script compares the default (reference) TOML file,
|
||||||
`frontend/editor/public/locales/en-GB/translation.toml`, with other translation
|
`frontend/editor/public/locales/en-US/translation.toml`, with other translation
|
||||||
files in `frontend/editor/public/locales/*/translation.toml`.
|
files in `frontend/editor/public/locales/*/translation.toml`.
|
||||||
It determines how many keys are fully translated and automatically updates
|
It determines how many keys are fully translated and automatically updates
|
||||||
progress badges in the `README.md`.
|
progress badges in the `README.md`.
|
||||||
@@ -346,7 +346,7 @@ def main() -> None:
|
|||||||
# Project layout assumptions
|
# Project layout assumptions
|
||||||
cwd = os.getcwd()
|
cwd = os.getcwd()
|
||||||
locales_dir = os.path.join(cwd, "frontend", "editor", "public", "locales")
|
locales_dir = os.path.join(cwd, "frontend", "editor", "public", "locales")
|
||||||
reference_file = os.path.join(locales_dir, "en-GB", "translation.toml")
|
reference_file = os.path.join(locales_dir, "en-US", "translation.toml")
|
||||||
scripts_directory = os.path.join(cwd, "scripts")
|
scripts_directory = os.path.join(cwd, "scripts")
|
||||||
translation_state_file = os.path.join(scripts_directory, "ignore_translation.toml")
|
translation_state_file = os.path.join(scripts_directory, "ignore_translation.toml")
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Translation Management Scripts
|
# Translation Management Scripts
|
||||||
|
|
||||||
This directory contains Python scripts for managing frontend translations in Stirling PDF. These tools help analyze, merge, validate, and manage translations against the en-GB golden truth file.
|
This directory contains Python scripts for managing frontend translations in Stirling PDF. These tools help analyze, merge, validate, and manage translations against the en-US golden truth file.
|
||||||
|
|
||||||
## Current Format: TOML
|
## Current Format: TOML
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ python3 scripts/translations/auto_translate.py es-ES --no-cleanup
|
|||||||
4. Validates placeholders are preserved
|
4. Validates placeholders are preserved
|
||||||
5. Merges translated batches
|
5. Merges translated batches
|
||||||
6. Applies translations to language file
|
6. Applies translations to language file
|
||||||
7. Beautifies structure to match en-GB
|
7. Beautifies structure to match en-US
|
||||||
8. Cleans up temporary files
|
8. Cleans up temporary files
|
||||||
9. Reports final completion percentage
|
9. Reports final completion percentage
|
||||||
|
|
||||||
@@ -86,7 +86,7 @@ python scripts/translations/json_validator.py --all-batches ar_AR --quiet
|
|||||||
- Trailing commas before closing braces
|
- Trailing commas before closing braces
|
||||||
|
|
||||||
#### `validate_placeholders.py`
|
#### `validate_placeholders.py`
|
||||||
Validates that translation files have correct placeholders matching en-GB (source of truth).
|
Validates that translation files have correct placeholders matching en-US (source of truth).
|
||||||
|
|
||||||
**Usage:**
|
**Usage:**
|
||||||
```bash
|
```bash
|
||||||
@@ -105,12 +105,12 @@ python scripts/translations/validate_placeholders.py --json
|
|||||||
|
|
||||||
**Features:**
|
**Features:**
|
||||||
- Detects missing placeholders (e.g., {n}, {total}, {filename})
|
- Detects missing placeholders (e.g., {n}, {total}, {filename})
|
||||||
- Detects extra placeholders not in en-GB
|
- Detects extra placeholders not in en-US
|
||||||
- Shows exact keys and text where issues occur
|
- Shows exact keys and text where issues occur
|
||||||
- Exit code 1 if issues found (good for CI/CD)
|
- Exit code 1 if issues found (good for CI/CD)
|
||||||
|
|
||||||
#### `validate_json_structure.py`
|
#### `validate_json_structure.py`
|
||||||
Validates JSON structure and key consistency with en-GB.
|
Validates JSON structure and key consistency with en-US.
|
||||||
|
|
||||||
**Usage:**
|
**Usage:**
|
||||||
```bash
|
```bash
|
||||||
@@ -130,7 +130,7 @@ python scripts/translations/validate_json_structure.py --json
|
|||||||
**Features:**
|
**Features:**
|
||||||
- Validates JSON syntax
|
- Validates JSON syntax
|
||||||
- Detects missing keys (not translated yet)
|
- Detects missing keys (not translated yet)
|
||||||
- Detects extra keys (not in en-GB, should be removed)
|
- Detects extra keys (not in en-US, should be removed)
|
||||||
- Reports key counts and structure differences
|
- Reports key counts and structure differences
|
||||||
- Exit code 1 if issues found (good for CI/CD)
|
- Exit code 1 if issues found (good for CI/CD)
|
||||||
|
|
||||||
@@ -160,21 +160,21 @@ python scripts/translations/translation_analyzer.py --format json
|
|||||||
|
|
||||||
**Features:**
|
**Features:**
|
||||||
- Finds missing translation keys
|
- Finds missing translation keys
|
||||||
- Identifies untranslated entries (identical to en-GB and [UNTRANSLATED] markers)
|
- Identifies untranslated entries (identical to en-US and [UNTRANSLATED] markers)
|
||||||
- Shows accurate completion percentages using ignore patterns
|
- Shows accurate completion percentages using ignore patterns
|
||||||
- Identifies extra keys not in en-GB
|
- Identifies extra keys not in en-US
|
||||||
- Supports JSON and text output formats
|
- Supports JSON and text output formats
|
||||||
- Uses `scripts/ignore_translation.toml` for language-specific exclusions
|
- Uses `scripts/ignore_translation.toml` for language-specific exclusions
|
||||||
|
|
||||||
### 2. `translation_merger.py`
|
### 2. `translation_merger.py`
|
||||||
Merges missing translations from en-GB into target language files and manages translation workflows.
|
Merges missing translations from en-US into target language files and manages translation workflows.
|
||||||
|
|
||||||
**Usage:**
|
**Usage:**
|
||||||
```bash
|
```bash
|
||||||
# Operate on all locales (except en-GB) when language is omitted
|
# Operate on all locales (except en-US) when language is omitted
|
||||||
python scripts/translations/translation_merger.py add-missing
|
python scripts/translations/translation_merger.py add-missing
|
||||||
|
|
||||||
# Add missing translations from en-GB to French
|
# Add missing translations from en-US to French
|
||||||
python scripts/translations/translation_merger.py fr-FR add-missing
|
python scripts/translations/translation_merger.py fr-FR add-missing
|
||||||
|
|
||||||
# Create backups before modifying files
|
# Create backups before modifying files
|
||||||
@@ -192,19 +192,19 @@ python scripts/translations/translation_merger.py fr-FR apply-translations --tra
|
|||||||
# Override default paths if needed
|
# Override default paths if needed
|
||||||
python scripts/translations/translation_merger.py fr-FR add-missing --locales-dir ./frontend/editor/public/locales --ignore-file ./scripts/ignore_translation.toml
|
python scripts/translations/translation_merger.py fr-FR add-missing --locales-dir ./frontend/editor/public/locales --ignore-file ./scripts/ignore_translation.toml
|
||||||
|
|
||||||
# Remove unused translations not present in en-GB
|
# Remove unused translations not present in en-US
|
||||||
python scripts/translations/translation_merger.py fr-FR remove-unused
|
python scripts/translations/translation_merger.py fr-FR remove-unused
|
||||||
```
|
```
|
||||||
|
|
||||||
**Features:**
|
**Features:**
|
||||||
- Adds missing keys from en-GB (copies English text directly)
|
- Adds missing keys from en-US (copies English text directly)
|
||||||
- Runs across all locales for add-missing/remove-unused when language is omitted
|
- Runs across all locales for add-missing/remove-unused when language is omitted
|
||||||
- Extracts untranslated entries for external translation
|
- Extracts untranslated entries for external translation
|
||||||
- Creates structured templates for AI translation
|
- Creates structured templates for AI translation
|
||||||
- Applies translated content back to language files (template format or plain JSON)
|
- Applies translated content back to language files (template format or plain JSON)
|
||||||
- Supports `--backup` on mutating commands
|
- Supports `--backup` on mutating commands
|
||||||
- Automatic backup creation
|
- Automatic backup creation
|
||||||
- Removes unused translations not present in en-GB
|
- Removes unused translations not present in en-US
|
||||||
|
|
||||||
### 3. `ai_translation_helper.py`
|
### 3. `ai_translation_helper.py`
|
||||||
Specialized tool for AI-assisted translation workflows with batch processing and validation.
|
Specialized tool for AI-assisted translation workflows with batch processing and validation.
|
||||||
@@ -290,7 +290,7 @@ python3 scripts/translations/auto_translate.py es-ES --skip-verification
|
|||||||
4. **Validate**: Ensures placeholders are preserved
|
4. **Validate**: Ensures placeholders are preserved
|
||||||
5. **Merge**: Combines all translated batches
|
5. **Merge**: Combines all translated batches
|
||||||
6. **Apply**: Updates the language file
|
6. **Apply**: Updates the language file
|
||||||
7. **Beautify**: Restructures to match en-GB format
|
7. **Beautify**: Restructures to match en-US format
|
||||||
8. **Cleanup**: Removes temporary files
|
8. **Cleanup**: Removes temporary files
|
||||||
9. **Verify**: Reports final completion percentage
|
9. **Verify**: Reports final completion percentage
|
||||||
|
|
||||||
@@ -337,11 +337,11 @@ python3 scripts/translations/batch_translator.py batch.json --language it-IT --s
|
|||||||
- `gpt-5-nano`: Fastest, most economical
|
- `gpt-5-nano`: Fastest, most economical
|
||||||
|
|
||||||
### 7. `json_beautifier.py`
|
### 7. `json_beautifier.py`
|
||||||
Restructures and beautifies translation JSON files to match en-GB structure exactly.
|
Restructures and beautifies translation JSON files to match en-US structure exactly.
|
||||||
|
|
||||||
**Usage:**
|
**Usage:**
|
||||||
```bash
|
```bash
|
||||||
# Restructure single language to match en-GB structure
|
# Restructure single language to match en-US structure
|
||||||
python scripts/translations/json_beautifier.py --language de-DE
|
python scripts/translations/json_beautifier.py --language de-DE
|
||||||
|
|
||||||
# Restructure all languages
|
# Restructure all languages
|
||||||
@@ -355,7 +355,7 @@ python scripts/translations/json_beautifier.py --language de-DE --no-backup
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Features:**
|
**Features:**
|
||||||
- Restructures JSON to match en-GB nested structure exactly
|
- Restructures JSON to match en-US nested structure exactly
|
||||||
- Preserves key ordering for line-by-line comparison
|
- Preserves key ordering for line-by-line comparison
|
||||||
- Creates automatic backups before modification
|
- Creates automatic backups before modification
|
||||||
- Validates structure and key ordering
|
- Validates structure and key ordering
|
||||||
@@ -585,7 +585,7 @@ python scripts/translations/json_validator.py --all-batches ar_AR
|
|||||||
|
|
||||||
#### JSON Structure Mismatches
|
#### JSON Structure Mismatches
|
||||||
**Problem**: Flattened dot-notation keys instead of proper nested objects
|
**Problem**: Flattened dot-notation keys instead of proper nested objects
|
||||||
**Solution**: Use `json_beautifier.py` to restructure files to match en-GB exactly
|
**Solution**: Use `json_beautifier.py` to restructure files to match en-US exactly
|
||||||
|
|
||||||
## Real-World Examples
|
## Real-World Examples
|
||||||
|
|
||||||
@@ -629,7 +629,7 @@ EOF
|
|||||||
python scripts/translations/translation_merger.py ar-AR apply-translations --translations-file ar_AR_merged.json
|
python scripts/translations/translation_merger.py ar-AR apply-translations --translations-file ar_AR_merged.json
|
||||||
# Result: Applied 1088 translations
|
# Result: Applied 1088 translations
|
||||||
|
|
||||||
# Beautify to match en-GB structure
|
# Beautify to match en-US structure
|
||||||
python scripts/translations/json_beautifier.py --language ar-AR
|
python scripts/translations/json_beautifier.py --language ar-AR
|
||||||
|
|
||||||
# Check final progress
|
# Check final progress
|
||||||
@@ -711,4 +711,4 @@ These scripts integrate with the existing translation system:
|
|||||||
3. **Updating Existing Language**: Use analyzer to find gaps, then compact or batch method
|
3. **Updating Existing Language**: Use analyzer to find gaps, then compact or batch method
|
||||||
4. **Quality Assurance**: Use analyzer with `--summary` for completion metrics and issue detection
|
4. **Quality Assurance**: Use analyzer with `--summary` for completion metrics and issue detection
|
||||||
5. **External Translation Services**: Use export functionality to generate CSV files for translators
|
5. **External Translation Services**: Use export functionality to generate CSV files for translators
|
||||||
6. **Structure Maintenance**: Use json_beautifier to keep files aligned with en-GB structure
|
6. **Structure Maintenance**: Use json_beautifier to keep files aligned with en-US structure
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import tomli_w
|
|||||||
class AITranslationHelper:
|
class AITranslationHelper:
|
||||||
def __init__(self, locales_dir: str = "frontend/editor/public/locales"):
|
def __init__(self, locales_dir: str = "frontend/editor/public/locales"):
|
||||||
self.locales_dir = Path(locales_dir)
|
self.locales_dir = Path(locales_dir)
|
||||||
self.golden_truth_file = self.locales_dir / "en-GB" / "translation.toml"
|
self.golden_truth_file = self.locales_dir / "en-US" / "translation.toml"
|
||||||
|
|
||||||
def _load_translation_file(self, file_path: Path) -> Dict:
|
def _load_translation_file(self, file_path: Path) -> Dict:
|
||||||
"""Load TOML translation file."""
|
"""Load TOML translation file."""
|
||||||
@@ -47,7 +47,7 @@ class AITranslationHelper:
|
|||||||
batch_data = {
|
batch_data = {
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"created_at": datetime.now().isoformat(),
|
"created_at": datetime.now().isoformat(),
|
||||||
"source_language": "en-GB",
|
"source_language": "en-US",
|
||||||
"target_languages": languages,
|
"target_languages": languages,
|
||||||
"max_entries_per_language": max_entries_per_language,
|
"max_entries_per_language": max_entries_per_language,
|
||||||
"instructions": {
|
"instructions": {
|
||||||
@@ -320,7 +320,7 @@ class AITranslationHelper:
|
|||||||
)
|
)
|
||||||
|
|
||||||
with open(output_file, "w", newline="", encoding="utf-8") as csvfile:
|
with open(output_file, "w", newline="", encoding="utf-8") as csvfile:
|
||||||
fieldnames = ["key", "context", "en_GB"] + languages
|
fieldnames = ["key", "context", "en_US"] + languages
|
||||||
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
|
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
|
||||||
writer.writeheader()
|
writer.writeheader()
|
||||||
|
|
||||||
@@ -331,7 +331,7 @@ class AITranslationHelper:
|
|||||||
row = {
|
row = {
|
||||||
"key": key,
|
"key": key,
|
||||||
"context": self._get_key_context(key),
|
"context": self._get_key_context(key),
|
||||||
"en_GB": en_value,
|
"en_US": en_value,
|
||||||
}
|
}
|
||||||
|
|
||||||
for lang in languages:
|
for lang in languages:
|
||||||
@@ -363,7 +363,7 @@ class AITranslationHelper:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
export_data["translations"][key] = {
|
export_data["translations"][key] = {
|
||||||
"en_GB": en_value,
|
"en_US": en_value,
|
||||||
"context": self._get_key_context(key),
|
"context": self._get_key_context(key),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,14 +55,14 @@ def extract_untranslated(language_code, batch_size=500, include_existing=False):
|
|||||||
print(f"\n🔍 Extracting {mode} entries for {language_code}...")
|
print(f"\n🔍 Extracting {mode} entries for {language_code}...")
|
||||||
|
|
||||||
# Load files
|
# Load files
|
||||||
golden_path = find_translation_file(Path("frontend/editor/public/locales/en-GB"))
|
golden_path = find_translation_file(Path("frontend/editor/public/locales/en-US"))
|
||||||
lang_path = find_translation_file(
|
lang_path = find_translation_file(
|
||||||
Path(f"frontend/editor/public/locales/{language_code}")
|
Path(f"frontend/editor/public/locales/{language_code}")
|
||||||
)
|
)
|
||||||
|
|
||||||
if not golden_path:
|
if not golden_path:
|
||||||
print(
|
print(
|
||||||
"Error: Golden truth file not found in frontend/editor/public/locales/en-GB"
|
"Error: Golden truth file not found in frontend/editor/public/locales/en-US"
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -222,7 +222,7 @@ def apply_translations(merged_file, language_code):
|
|||||||
|
|
||||||
|
|
||||||
def beautify_translations(language_code):
|
def beautify_translations(language_code):
|
||||||
"""Beautify translation file to match en-GB structure."""
|
"""Beautify translation file to match en-US structure."""
|
||||||
print(f"\n✨ Beautifying {language_code} translation file...")
|
print(f"\n✨ Beautifying {language_code} translation file...")
|
||||||
|
|
||||||
cmd = f"python3 scripts/translations/toml_beautifier.py --language {language_code}"
|
cmd = f"python3 scripts/translations/toml_beautifier.py --language {language_code}"
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ def get_all_languages(locales_dir: Path) -> List[str]:
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
for lang_dir in sorted(locales_dir.iterdir()):
|
for lang_dir in sorted(locales_dir.iterdir()):
|
||||||
if lang_dir.is_dir() and lang_dir.name != "en-GB":
|
if lang_dir.is_dir() and lang_dir.name != "en-US":
|
||||||
toml_file = lang_dir / "translation.toml"
|
toml_file = lang_dir / "translation.toml"
|
||||||
if toml_file.exists():
|
if toml_file.exists():
|
||||||
languages.append(lang_dir.name)
|
languages.append(lang_dir.name)
|
||||||
@@ -57,10 +57,10 @@ def get_language_completion(locales_dir: Path, language: str) -> Optional[float]
|
|||||||
with open(toml_file, "rb") as f:
|
with open(toml_file, "rb") as f:
|
||||||
target_data = tomllib.load(f)
|
target_data = tomllib.load(f)
|
||||||
|
|
||||||
# Load en-GB reference
|
# Load en-US reference
|
||||||
en_gb_file = locales_dir / "en-GB" / "translation.toml"
|
en_us_file = locales_dir / "en-US" / "translation.toml"
|
||||||
with open(en_gb_file, "rb") as f:
|
with open(en_us_file, "rb") as f:
|
||||||
en_gb_data = tomllib.load(f)
|
en_us_data = tomllib.load(f)
|
||||||
|
|
||||||
# Flatten and count
|
# Flatten and count
|
||||||
def flatten(d, parent=""):
|
def flatten(d, parent=""):
|
||||||
@@ -73,16 +73,16 @@ def get_language_completion(locales_dir: Path, language: str) -> Optional[float]
|
|||||||
items[key] = v
|
items[key] = v
|
||||||
return items
|
return items
|
||||||
|
|
||||||
en_gb_flat = flatten(en_gb_data)
|
en_us_flat = flatten(en_us_data)
|
||||||
target_flat = flatten(target_data)
|
target_flat = flatten(target_data)
|
||||||
|
|
||||||
# Count translated (not equal to en-GB)
|
# Count translated (not equal to en-US)
|
||||||
translated = sum(
|
translated = sum(
|
||||||
1
|
1
|
||||||
for k in en_gb_flat
|
for k in en_us_flat
|
||||||
if k in target_flat and target_flat[k] != en_gb_flat[k]
|
if k in target_flat and target_flat[k] != en_us_flat[k]
|
||||||
)
|
)
|
||||||
total = len(en_gb_flat)
|
total = len(en_us_flat)
|
||||||
|
|
||||||
return (translated / total * 100) if total > 0 else 0.0
|
return (translated / total * 100) if total > 0 else 0.0
|
||||||
|
|
||||||
@@ -246,7 +246,7 @@ Note: Requires OPENAI_API_KEY environment variable or --api-key argument.
|
|||||||
print(f"Translating specified languages: {', '.join(languages)}")
|
print(f"Translating specified languages: {', '.join(languages)}")
|
||||||
else:
|
else:
|
||||||
languages = get_all_languages(locales_dir)
|
languages = get_all_languages(locales_dir)
|
||||||
print(f"Found {len(languages)} languages (excluding en-GB)")
|
print(f"Found {len(languages)} languages (excluding en-US)")
|
||||||
|
|
||||||
if not languages:
|
if not languages:
|
||||||
print("No languages to translate!")
|
print("No languages to translate!")
|
||||||
|
|||||||
@@ -19,10 +19,10 @@ class CompactTranslationExtractor:
|
|||||||
ignore_file: str = "scripts/ignore_translation.toml",
|
ignore_file: str = "scripts/ignore_translation.toml",
|
||||||
):
|
):
|
||||||
self.locales_dir = Path(locales_dir)
|
self.locales_dir = Path(locales_dir)
|
||||||
self.golden_truth_file = self.locales_dir / "en-GB" / "translation.toml"
|
self.golden_truth_file = self.locales_dir / "en-US" / "translation.toml"
|
||||||
if not self.golden_truth_file.exists():
|
if not self.golden_truth_file.exists():
|
||||||
print(
|
print(
|
||||||
f"Error: en-GB translation file not found at {self.golden_truth_file}",
|
f"Error: en-US translation file not found at {self.golden_truth_file}",
|
||||||
file=sys.stderr,
|
file=sys.stderr,
|
||||||
)
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
@@ -95,7 +95,7 @@ class CompactTranslationExtractor:
|
|||||||
# Find missing translations
|
# Find missing translations
|
||||||
missing_keys = set(golden_flat.keys()) - set(target_flat.keys()) - ignore_set
|
missing_keys = set(golden_flat.keys()) - set(target_flat.keys()) - ignore_set
|
||||||
|
|
||||||
# Find untranslated entries (identical to en-GB or marked [UNTRANSLATED])
|
# Find untranslated entries (identical to en-US or marked [UNTRANSLATED])
|
||||||
untranslated_keys = set()
|
untranslated_keys = set()
|
||||||
for key in target_flat:
|
for key in target_flat:
|
||||||
if key in golden_flat and key not in ignore_set:
|
if key in golden_flat and key not in ignore_set:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
TOML Beautifier and Structure Fixer for Stirling PDF Frontend
|
TOML Beautifier and Structure Fixer for Stirling PDF Frontend
|
||||||
Restructures translation TOML files to match en-GB structure and key order exactly.
|
Restructures translation TOML files to match en-US structure and key order exactly.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
@@ -17,7 +17,7 @@ import tomli_w
|
|||||||
class TOMLBeautifier:
|
class TOMLBeautifier:
|
||||||
def __init__(self, locales_dir: str = "frontend/editor/public/locales"):
|
def __init__(self, locales_dir: str = "frontend/editor/public/locales"):
|
||||||
self.locales_dir = Path(locales_dir)
|
self.locales_dir = Path(locales_dir)
|
||||||
self.golden_truth_file = self.locales_dir / "en-GB" / "translation.toml"
|
self.golden_truth_file = self.locales_dir / "en-US" / "translation.toml"
|
||||||
self.golden_structure = self._load_toml(self.golden_truth_file)
|
self.golden_structure = self._load_toml(self.golden_truth_file)
|
||||||
|
|
||||||
def _load_toml(self, file_path: Path) -> Dict:
|
def _load_toml(self, file_path: Path) -> Dict:
|
||||||
@@ -95,7 +95,7 @@ class TOMLBeautifier:
|
|||||||
return build_recursive(reference_structure) or OrderedDict()
|
return build_recursive(reference_structure) or OrderedDict()
|
||||||
|
|
||||||
def restructure_translation_file(self, target_file: Path) -> Dict[str, Any]:
|
def restructure_translation_file(self, target_file: Path) -> Dict[str, Any]:
|
||||||
"""Restructure a translation file to match en-GB structure exactly."""
|
"""Restructure a translation file to match en-US structure exactly."""
|
||||||
if not target_file.exists():
|
if not target_file.exists():
|
||||||
print(f"Error: Target file does not exist: {target_file}")
|
print(f"Error: Target file does not exist: {target_file}")
|
||||||
return {}
|
return {}
|
||||||
@@ -177,7 +177,7 @@ class TOMLBeautifier:
|
|||||||
}
|
}
|
||||||
|
|
||||||
def validate_key_order(self, target_file: Path) -> Dict[str, Any]:
|
def validate_key_order(self, target_file: Path) -> Dict[str, Any]:
|
||||||
"""Validate that keys appear in the same order as en-GB."""
|
"""Validate that keys appear in the same order as en-US."""
|
||||||
target_data = self._load_toml(target_file)
|
target_data = self._load_toml(target_file)
|
||||||
|
|
||||||
def get_key_order(obj: Dict, path: str = "") -> List[str]:
|
def get_key_order(obj: Dict, path: str = "") -> List[str]:
|
||||||
@@ -276,7 +276,7 @@ def main():
|
|||||||
elif args.all_languages:
|
elif args.all_languages:
|
||||||
results = []
|
results = []
|
||||||
for lang_dir in Path(args.locales_dir).iterdir():
|
for lang_dir in Path(args.locales_dir).iterdir():
|
||||||
if lang_dir.is_dir() and lang_dir.name != "en-GB":
|
if lang_dir.is_dir() and lang_dir.name != "en-US":
|
||||||
translation_file = lang_dir / "translation.toml"
|
translation_file = lang_dir / "translation.toml"
|
||||||
if translation_file.exists():
|
if translation_file.exists():
|
||||||
if args.validate_only:
|
if args.validate_only:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
Translation Analyzer for Stirling PDF Frontend
|
Translation Analyzer for Stirling PDF Frontend
|
||||||
Compares language files against en-GB golden truth file.
|
Compares language files against en-US golden truth file.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
@@ -19,7 +19,7 @@ class TranslationAnalyzer:
|
|||||||
ignore_file: str = "scripts/ignore_translation.toml",
|
ignore_file: str = "scripts/ignore_translation.toml",
|
||||||
):
|
):
|
||||||
self.locales_dir = Path(locales_dir)
|
self.locales_dir = Path(locales_dir)
|
||||||
self.golden_truth_file = self.locales_dir / "en-GB" / "translation.toml"
|
self.golden_truth_file = self.locales_dir / "en-US" / "translation.toml"
|
||||||
self.golden_truth = self._load_translation_file(self.golden_truth_file)
|
self.golden_truth = self._load_translation_file(self.golden_truth_file)
|
||||||
self.ignore_file = Path(ignore_file)
|
self.ignore_file = Path(ignore_file)
|
||||||
self.ignore_patterns = self._load_ignore_patterns()
|
self.ignore_patterns = self._load_ignore_patterns()
|
||||||
@@ -70,17 +70,17 @@ class TranslationAnalyzer:
|
|||||||
return dict(items)
|
return dict(items)
|
||||||
|
|
||||||
def get_all_language_files(self) -> List[Path]:
|
def get_all_language_files(self) -> List[Path]:
|
||||||
"""Get all translation files except en-GB."""
|
"""Get all translation files except en-US."""
|
||||||
files = []
|
files = []
|
||||||
for lang_dir in self.locales_dir.iterdir():
|
for lang_dir in self.locales_dir.iterdir():
|
||||||
if lang_dir.is_dir() and lang_dir.name != "en-GB":
|
if lang_dir.is_dir() and lang_dir.name != "en-US":
|
||||||
toml_file = lang_dir / "translation.toml"
|
toml_file = lang_dir / "translation.toml"
|
||||||
if toml_file.exists():
|
if toml_file.exists():
|
||||||
files.append(toml_file)
|
files.append(toml_file)
|
||||||
return sorted(files)
|
return sorted(files)
|
||||||
|
|
||||||
def find_missing_translations(self, target_file: Path) -> Set[str]:
|
def find_missing_translations(self, target_file: Path) -> Set[str]:
|
||||||
"""Find keys that exist in en-GB but missing in target file."""
|
"""Find keys that exist in en-US but missing in target file."""
|
||||||
target_data = self._load_translation_file(target_file)
|
target_data = self._load_translation_file(target_file)
|
||||||
|
|
||||||
golden_flat = self._flatten_dict(self.golden_truth)
|
golden_flat = self._flatten_dict(self.golden_truth)
|
||||||
@@ -94,7 +94,7 @@ class TranslationAnalyzer:
|
|||||||
return missing - ignore_set
|
return missing - ignore_set
|
||||||
|
|
||||||
def find_untranslated_entries(self, target_file: Path) -> Set[str]:
|
def find_untranslated_entries(self, target_file: Path) -> Set[str]:
|
||||||
"""Find entries that appear to be untranslated (identical to en-GB)."""
|
"""Find entries that appear to be untranslated (identical to en-US)."""
|
||||||
target_data = self._load_translation_file(target_file)
|
target_data = self._load_translation_file(target_file)
|
||||||
|
|
||||||
golden_flat = self._flatten_dict(self.golden_truth)
|
golden_flat = self._flatten_dict(self.golden_truth)
|
||||||
@@ -109,7 +109,7 @@ class TranslationAnalyzer:
|
|||||||
target_value = target_flat[key]
|
target_value = target_flat[key]
|
||||||
golden_value = golden_flat[key]
|
golden_value = golden_flat[key]
|
||||||
|
|
||||||
# Check if marked as [UNTRANSLATED] or identical to en-GB
|
# Check if marked as [UNTRANSLATED] or identical to en-US
|
||||||
if (
|
if (
|
||||||
isinstance(target_value, str)
|
isinstance(target_value, str)
|
||||||
and target_value.startswith("[UNTRANSLATED]")
|
and target_value.startswith("[UNTRANSLATED]")
|
||||||
@@ -139,7 +139,7 @@ class TranslationAnalyzer:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def find_extra_translations(self, target_file: Path) -> Set[str]:
|
def find_extra_translations(self, target_file: Path) -> Set[str]:
|
||||||
"""Find keys that exist in target file but not in en-GB."""
|
"""Find keys that exist in target file but not in en-US."""
|
||||||
target_data = self._load_translation_file(target_file)
|
target_data = self._load_translation_file(target_file)
|
||||||
|
|
||||||
golden_flat = self._flatten_dict(self.golden_truth)
|
golden_flat = self._flatten_dict(self.golden_truth)
|
||||||
@@ -174,7 +174,7 @@ class TranslationAnalyzer:
|
|||||||
if not (isinstance(value, str) and value.startswith("[UNTRANSLATED]")):
|
if not (isinstance(value, str) and value.startswith("[UNTRANSLATED]")):
|
||||||
if (
|
if (
|
||||||
key not in untranslated
|
key not in untranslated
|
||||||
): # Not identical to en-GB (unless expected)
|
): # Not identical to en-US (unless expected)
|
||||||
properly_translated += 1
|
properly_translated += 1
|
||||||
|
|
||||||
completion_rate = (
|
completion_rate = (
|
||||||
@@ -204,7 +204,7 @@ class TranslationAnalyzer:
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="Analyze translation files against en-GB golden truth"
|
description="Analyze translation files against en-US golden truth"
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--locales-dir",
|
"--locales-dir",
|
||||||
@@ -260,7 +260,7 @@ def main():
|
|||||||
print(f"Language: {lang}")
|
print(f"Language: {lang}")
|
||||||
print(f"File: {result['file']}")
|
print(f"File: {result['file']}")
|
||||||
print(f"Completion Rate: {result['completion_rate']:.1f}%")
|
print(f"Completion Rate: {result['completion_rate']:.1f}%")
|
||||||
print(f"Total Keys in en-GB: {result['total_keys']}")
|
print(f"Total Keys in en-US: {result['total_keys']}")
|
||||||
|
|
||||||
if not args.summary:
|
if not args.summary:
|
||||||
if not args.untranslated_only:
|
if not args.untranslated_only:
|
||||||
@@ -278,7 +278,7 @@ def main():
|
|||||||
print(f" ... and {len(result['untranslated_keys']) - 10} more")
|
print(f" ... and {len(result['untranslated_keys']) - 10} more")
|
||||||
|
|
||||||
if result["extra_count"] > 0:
|
if result["extra_count"] > 0:
|
||||||
print(f"\nExtra Keys Not in en-GB ({result['extra_count']}):")
|
print(f"\nExtra Keys Not in en-US ({result['extra_count']}):")
|
||||||
for key in result["extra_keys"][:5]:
|
for key in result["extra_keys"][:5]:
|
||||||
print(f" - {key}")
|
print(f" - {key}")
|
||||||
if len(result["extra_keys"]) > 5:
|
if len(result["extra_keys"]) > 5:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
Translation Merger for Stirling PDF Frontend
|
Translation Merger for Stirling PDF Frontend
|
||||||
Merges missing translations from en-GB into target language files.
|
Merges missing translations from en-US into target language files.
|
||||||
Useful for AI-assisted translation workflows.
|
Useful for AI-assisted translation workflows.
|
||||||
TOML format only.
|
TOML format only.
|
||||||
"""
|
"""
|
||||||
@@ -30,7 +30,7 @@ class TranslationMerger:
|
|||||||
),
|
),
|
||||||
):
|
):
|
||||||
self.locales_dir = Path(locales_dir)
|
self.locales_dir = Path(locales_dir)
|
||||||
self.golden_truth_file = self.locales_dir / "en-GB" / "translation.toml"
|
self.golden_truth_file = self.locales_dir / "en-US" / "translation.toml"
|
||||||
self.golden_truth = self._load_translation_file(self.golden_truth_file)
|
self.golden_truth = self._load_translation_file(self.golden_truth_file)
|
||||||
self.ignore_file = Path(ignore_file)
|
self.ignore_file = Path(ignore_file)
|
||||||
self.ignore_patterns = self._load_ignore_patterns()
|
self.ignore_patterns = self._load_ignore_patterns()
|
||||||
@@ -178,7 +178,7 @@ class TranslationMerger:
|
|||||||
save: bool = True,
|
save: bool = True,
|
||||||
backup: bool = False,
|
backup: bool = False,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Add missing translations from en-GB to target file and optionally save."""
|
"""Add missing translations from en-US to target file and optionally save."""
|
||||||
if not target_file.parent.exists():
|
if not target_file.parent.exists():
|
||||||
target_file.parent.mkdir(parents=True, exist_ok=True)
|
target_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
target_data = {}
|
target_data = {}
|
||||||
@@ -210,7 +210,7 @@ class TranslationMerger:
|
|||||||
def extract_untranslated_entries(
|
def extract_untranslated_entries(
|
||||||
self, target_file: Path, output_file: Path | None = None
|
self, target_file: Path, output_file: Path | None = None
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Extract entries marked as untranslated or identical to en-GB for AI translation."""
|
"""Extract entries marked as untranslated or identical to en-US for AI translation."""
|
||||||
if not target_file.exists():
|
if not target_file.exists():
|
||||||
print(f"Error: Target file does not exist: {target_file}")
|
print(f"Error: Target file does not exist: {target_file}")
|
||||||
return {}
|
return {}
|
||||||
@@ -335,7 +335,7 @@ class TranslationMerger:
|
|||||||
|
|
||||||
template = {
|
template = {
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"source_language": "en-GB",
|
"source_language": "en-US",
|
||||||
"target_language": target_file.parent.name,
|
"target_language": target_file.parent.name,
|
||||||
"total_entries": len(untranslated),
|
"total_entries": len(untranslated),
|
||||||
"created_at": datetime.now().isoformat(),
|
"created_at": datetime.now().isoformat(),
|
||||||
@@ -384,14 +384,14 @@ def main():
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"language",
|
"language",
|
||||||
nargs="?",
|
nargs="?",
|
||||||
help="Target language code (e.g., fr-FR). If omitted, add-missing and remove-unused run for all locales except en-GB.",
|
help="Target language code (e.g., fr-FR). If omitted, add-missing and remove-unused run for all locales except en-US.",
|
||||||
)
|
)
|
||||||
|
|
||||||
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
subparsers = parser.add_subparsers(dest="command", help="Available commands")
|
||||||
|
|
||||||
# Add missing command
|
# Add missing command
|
||||||
add_parser = subparsers.add_parser(
|
add_parser = subparsers.add_parser(
|
||||||
"add-missing", help="Add missing translations from en-GB"
|
"add-missing", help="Add missing translations from en-US"
|
||||||
)
|
)
|
||||||
add_parser.add_argument(
|
add_parser.add_argument(
|
||||||
"--backup", action="store_true", help="Create backup before modifying files"
|
"--backup", action="store_true", help="Create backup before modifying files"
|
||||||
@@ -424,7 +424,7 @@ def main():
|
|||||||
|
|
||||||
# Remove unused translations command
|
# Remove unused translations command
|
||||||
remove_parser = subparsers.add_parser(
|
remove_parser = subparsers.add_parser(
|
||||||
"remove-unused", help="Remove unused translations not present in en-GB"
|
"remove-unused", help="Remove unused translations not present in en-US"
|
||||||
)
|
)
|
||||||
remove_parser.add_argument(
|
remove_parser.add_argument(
|
||||||
"--backup", action="store_true", help="Create backup before modifying files"
|
"--backup", action="store_true", help="Create backup before modifying files"
|
||||||
@@ -449,7 +449,7 @@ def main():
|
|||||||
else:
|
else:
|
||||||
total_added = 0
|
total_added = 0
|
||||||
for lang_dir in sorted(Path(args.locales_dir).iterdir()):
|
for lang_dir in sorted(Path(args.locales_dir).iterdir()):
|
||||||
if not lang_dir.is_dir() or lang_dir.name == "en-GB":
|
if not lang_dir.is_dir() or lang_dir.name == "en-US":
|
||||||
continue
|
continue
|
||||||
target_file = lang_dir / "translation.toml"
|
target_file = lang_dir / "translation.toml"
|
||||||
print(f"Processing {lang_dir.name}...")
|
print(f"Processing {lang_dir.name}...")
|
||||||
@@ -471,7 +471,7 @@ def main():
|
|||||||
else:
|
else:
|
||||||
total_removed = 0
|
total_removed = 0
|
||||||
for lang_dir in sorted(Path(args.locales_dir).iterdir()):
|
for lang_dir in sorted(Path(args.locales_dir).iterdir()):
|
||||||
if not lang_dir.is_dir() or lang_dir.name == "en-GB":
|
if not lang_dir.is_dir() or lang_dir.name == "en-US":
|
||||||
continue
|
continue
|
||||||
target_file = lang_dir / "translation.toml"
|
target_file = lang_dir / "translation.toml"
|
||||||
print(f"Processing {lang_dir.name}...")
|
print(f"Processing {lang_dir.name}...")
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ Validate TOML structure and formatting of translation files.
|
|||||||
|
|
||||||
Checks for:
|
Checks for:
|
||||||
- Valid TOML syntax
|
- Valid TOML syntax
|
||||||
- Consistent key structure with en-GB
|
- Consistent key structure with en-US
|
||||||
- Missing keys
|
- Missing keys
|
||||||
- Extra keys not in en-GB
|
- Extra keys not in en-US
|
||||||
- Malformed entries
|
- Malformed entries
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
@@ -43,18 +43,18 @@ def validate_translation_file(file_path: Path) -> tuple[bool, str]:
|
|||||||
|
|
||||||
|
|
||||||
def validate_structure(
|
def validate_structure(
|
||||||
en_gb_keys: Set[str], lang_keys: Set[str], lang_code: str
|
en_us_keys: Set[str], lang_keys: Set[str], lang_code: str
|
||||||
) -> Dict:
|
) -> Dict:
|
||||||
"""Compare structure between en-GB and target language."""
|
"""Compare structure between en-US and target language."""
|
||||||
missing_keys = en_gb_keys - lang_keys
|
missing_keys = en_us_keys - lang_keys
|
||||||
extra_keys = lang_keys - en_gb_keys
|
extra_keys = lang_keys - en_us_keys
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"language": lang_code,
|
"language": lang_code,
|
||||||
"missing_keys": sorted(missing_keys),
|
"missing_keys": sorted(missing_keys),
|
||||||
"extra_keys": sorted(extra_keys),
|
"extra_keys": sorted(extra_keys),
|
||||||
"total_keys": len(lang_keys),
|
"total_keys": len(lang_keys),
|
||||||
"expected_keys": len(en_gb_keys),
|
"expected_keys": len(en_us_keys),
|
||||||
"missing_count": len(missing_keys),
|
"missing_count": len(missing_keys),
|
||||||
"extra_count": len(extra_keys),
|
"extra_count": len(extra_keys),
|
||||||
}
|
}
|
||||||
@@ -68,12 +68,12 @@ def print_validation_result(result: Dict, verbose: bool = False):
|
|||||||
print(f"Language: {lang}")
|
print(f"Language: {lang}")
|
||||||
print(f"{'=' * 100}")
|
print(f"{'=' * 100}")
|
||||||
print(f" Total keys: {result['total_keys']}")
|
print(f" Total keys: {result['total_keys']}")
|
||||||
print(f" Expected keys (en-GB): {result['expected_keys']}")
|
print(f" Expected keys (en-US): {result['expected_keys']}")
|
||||||
print(f" Missing keys: {result['missing_count']}")
|
print(f" Missing keys: {result['missing_count']}")
|
||||||
print(f" Extra keys: {result['extra_count']}")
|
print(f" Extra keys: {result['extra_count']}")
|
||||||
|
|
||||||
if result["missing_count"] == 0 and result["extra_count"] == 0:
|
if result["missing_count"] == 0 and result["extra_count"] == 0:
|
||||||
print(" ✅ Structure matches en-GB perfectly!")
|
print(" ✅ Structure matches en-US perfectly!")
|
||||||
else:
|
else:
|
||||||
if result["missing_count"] > 0:
|
if result["missing_count"] > 0:
|
||||||
print(f"\n ⚠️ Missing {result['missing_count']} key(s):")
|
print(f"\n ⚠️ Missing {result['missing_count']} key(s):")
|
||||||
@@ -86,7 +86,7 @@ def print_validation_result(result: Dict, verbose: bool = False):
|
|||||||
print(" (use --verbose to see all)")
|
print(" (use --verbose to see all)")
|
||||||
|
|
||||||
if result["extra_count"] > 0:
|
if result["extra_count"] > 0:
|
||||||
print(f"\n ⚠️ Extra {result['extra_count']} key(s) not in en-GB:")
|
print(f"\n ⚠️ Extra {result['extra_count']} key(s) not in en-US:")
|
||||||
if verbose or result["extra_count"] <= 20:
|
if verbose or result["extra_count"] <= 20:
|
||||||
for key in result["extra_keys"][:50]:
|
for key in result["extra_keys"][:50]:
|
||||||
print(f" - {key}")
|
print(f" - {key}")
|
||||||
@@ -120,31 +120,31 @@ def main():
|
|||||||
|
|
||||||
# Define paths
|
# Define paths
|
||||||
locales_dir = Path("frontend/editor/public/locales")
|
locales_dir = Path("frontend/editor/public/locales")
|
||||||
en_gb_path = locales_dir / "en-GB" / "translation.toml"
|
en_us_path = locales_dir / "en-US" / "translation.toml"
|
||||||
|
|
||||||
if not en_gb_path.exists():
|
if not en_us_path.exists():
|
||||||
print(f"❌ Error: en-GB translation file not found at {en_gb_path}")
|
print(f"❌ Error: en-US translation file not found at {en_us_path}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# Validate en-GB itself
|
# Validate en-US itself
|
||||||
is_valid, message = validate_translation_file(en_gb_path)
|
is_valid, message = validate_translation_file(en_us_path)
|
||||||
if not is_valid:
|
if not is_valid:
|
||||||
print(f"❌ Error in en-GB file: {message}")
|
print(f"❌ Error in en-US file: {message}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# Load en-GB structure
|
# Load en-US structure
|
||||||
en_gb = load_translation_file(en_gb_path)
|
en_us = load_translation_file(en_us_path)
|
||||||
|
|
||||||
en_gb_keys = get_all_keys(en_gb)
|
en_us_keys = get_all_keys(en_us)
|
||||||
|
|
||||||
# Get list of languages to validate
|
# Get list of languages to validate
|
||||||
if args.language:
|
if args.language:
|
||||||
languages = [args.language]
|
languages = [args.language]
|
||||||
else:
|
else:
|
||||||
# Validate all languages except en-GB
|
# Validate all languages except en-US
|
||||||
languages = []
|
languages = []
|
||||||
for d in locales_dir.iterdir():
|
for d in locales_dir.iterdir():
|
||||||
if d.is_dir() and d.name != "en-GB":
|
if d.is_dir() and d.name != "en-US":
|
||||||
if (d / "translation.toml").exists():
|
if (d / "translation.toml").exists():
|
||||||
languages.append(d.name)
|
languages.append(d.name)
|
||||||
|
|
||||||
@@ -171,7 +171,7 @@ def main():
|
|||||||
lang_data = load_translation_file(lang_path)
|
lang_data = load_translation_file(lang_path)
|
||||||
|
|
||||||
lang_keys = get_all_keys(lang_data)
|
lang_keys = get_all_keys(lang_data)
|
||||||
result = validate_structure(en_gb_keys, lang_keys, lang_code)
|
result = validate_structure(en_us_keys, lang_keys, lang_code)
|
||||||
results.append(result)
|
results.append(result)
|
||||||
|
|
||||||
# Output results
|
# Output results
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
Validate that translation files have the same placeholders as en-GB (source of truth).
|
Validate that translation files have the same placeholders as en-US (source of truth).
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
python scripts/translations/validate_placeholders.py [--language LANG] [--fix]
|
python scripts/translations/validate_placeholders.py [--language LANG] [--fix]
|
||||||
@@ -38,16 +38,16 @@ def flatten_dict(d: dict, parent_key: str = "", sep: str = ".") -> Dict[str, str
|
|||||||
|
|
||||||
|
|
||||||
def validate_language(
|
def validate_language(
|
||||||
en_gb_flat: Dict[str, str], lang_flat: Dict[str, str], lang_code: str
|
en_us_flat: Dict[str, str], lang_flat: Dict[str, str], lang_code: str
|
||||||
) -> List[Dict]:
|
) -> List[Dict]:
|
||||||
"""Validate placeholders for a language against en-GB."""
|
"""Validate placeholders for a language against en-US."""
|
||||||
issues = []
|
issues = []
|
||||||
|
|
||||||
for key in en_gb_flat:
|
for key in en_us_flat:
|
||||||
if key not in lang_flat:
|
if key not in lang_flat:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
en_placeholders = find_placeholders(en_gb_flat[key])
|
en_placeholders = find_placeholders(en_us_flat[key])
|
||||||
lang_placeholders = find_placeholders(lang_flat[key])
|
lang_placeholders = find_placeholders(lang_flat[key])
|
||||||
|
|
||||||
if en_placeholders != lang_placeholders:
|
if en_placeholders != lang_placeholders:
|
||||||
@@ -59,7 +59,7 @@ def validate_language(
|
|||||||
"key": key,
|
"key": key,
|
||||||
"missing": missing,
|
"missing": missing,
|
||||||
"extra": extra,
|
"extra": extra,
|
||||||
"en_text": en_gb_flat[key],
|
"en_text": en_us_flat[key],
|
||||||
"lang_text": lang_flat[key],
|
"lang_text": lang_flat[key],
|
||||||
}
|
}
|
||||||
issues.append(issue)
|
issues.append(issue)
|
||||||
@@ -113,26 +113,26 @@ def main():
|
|||||||
|
|
||||||
# Define paths
|
# Define paths
|
||||||
locales_dir = Path("frontend/editor/public/locales")
|
locales_dir = Path("frontend/editor/public/locales")
|
||||||
en_gb_path = locales_dir / "en-GB" / "translation.toml"
|
en_us_path = locales_dir / "en-US" / "translation.toml"
|
||||||
|
|
||||||
if not en_gb_path.exists():
|
if not en_us_path.exists():
|
||||||
print(f"❌ Error: en-GB translation file not found at {en_gb_path}")
|
print(f"❌ Error: en-US translation file not found at {en_us_path}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# Load en-GB (source of truth)
|
# Load en-US (source of truth)
|
||||||
with open(en_gb_path, "rb") as f:
|
with open(en_us_path, "rb") as f:
|
||||||
en_gb = tomllib.load(f)
|
en_us = tomllib.load(f)
|
||||||
|
|
||||||
en_gb_flat = flatten_dict(en_gb)
|
en_us_flat = flatten_dict(en_us)
|
||||||
|
|
||||||
# Get list of languages to validate
|
# Get list of languages to validate
|
||||||
if args.language:
|
if args.language:
|
||||||
languages = [args.language]
|
languages = [args.language]
|
||||||
else:
|
else:
|
||||||
# Validate all languages except en-GB
|
# Validate all languages except en-US
|
||||||
languages = []
|
languages = []
|
||||||
for d in locales_dir.iterdir():
|
for d in locales_dir.iterdir():
|
||||||
if d.is_dir() and d.name != "en-GB":
|
if d.is_dir() and d.name != "en-US":
|
||||||
if (d / "translation.toml").exists():
|
if (d / "translation.toml").exists():
|
||||||
languages.append(d.name)
|
languages.append(d.name)
|
||||||
|
|
||||||
@@ -151,7 +151,7 @@ def main():
|
|||||||
lang_data = tomllib.load(f)
|
lang_data = tomllib.load(f)
|
||||||
|
|
||||||
lang_flat = flatten_dict(lang_data)
|
lang_flat = flatten_dict(lang_data)
|
||||||
issues = validate_language(en_gb_flat, lang_flat, lang_code)
|
issues = validate_language(en_us_flat, lang_flat, lang_code)
|
||||||
all_issues.extend(issues)
|
all_issues.extend(issues)
|
||||||
|
|
||||||
# Output results
|
# Output results
|
||||||
|
|||||||
Reference in New Issue
Block a user