Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
d06afec
feat: support skse translation (#309)
Sieluna Dec 14, 2024
3cb34cb
fix: reorder loading order and improve utils
Sieluna Dec 14, 2024
92928ed
refactor: test more translations
Sieluna Dec 15, 2024
9e8d93f
style: 🎨 apply clang-format changes
Sieluna Dec 15, 2024
529e197
refactor: modified general setting
Sieluna Dec 15, 2024
d7ab57b
Merge branch 'localization' of https://github.com/Sieluna/skyrim-comm…
Sieluna Dec 15, 2024
27dee83
refactor: improve translation by Pentalimbed's advice
Sieluna Dec 15, 2024
aaa5ec2
refactor: small fix and more translations
Sieluna Dec 16, 2024
bdee7ad
feat: create font subset feature
Sieluna Dec 17, 2024
e939838
style: 🎨 apply clang-format changes
Sieluna Dec 17, 2024
8ac690a
feat: dynamic resolve args and better ci
Sieluna Dec 17, 2024
95f0f2c
style: 🎨 apply clang-format changes
Sieluna Dec 17, 2024
5c0aa19
fix: disable few configs and make more translations
Sieluna Dec 17, 2024
c292a71
fix: force build all latin
Sieluna Dec 17, 2024
4915cc8
fix: improve styles and fix string format
Sieluna Dec 18, 2024
5cbdfe9
fix: improve and add missing translations
Sieluna Dec 18, 2024
9a3a116
fix: improve japanese translations
Sieluna Dec 18, 2024
696b6a5
chore: test working-tree-encoding
Sieluna Dec 18, 2024
67b15ad
chore: bake utf8
Sieluna Dec 18, 2024
68827d5
fix: fix typos
Sieluna Dec 18, 2024
835afe2
feat: wip dynamic load font
Sieluna Dec 25, 2024
2abc221
fix: fix few mistakes
Sieluna Dec 25, 2024
ab30724
refactor: load font good
Sieluna Dec 25, 2024
861923e
style: 🎨 apply clang-format changes
Sieluna Dec 25, 2024
509a9ce
refactor: move warning closer to input
Sieluna Dec 25, 2024
e9136b8
style: 🎨 apply clang-format changes
Sieluna Dec 25, 2024
20f8d4d
ci: prepare translation json
Sieluna Dec 25, 2024
74e51e7
ci: wip loaklise integrate
Sieluna Dec 25, 2024
b79d59a
fix: fix a mistake
Sieluna Dec 25, 2024
aa21c67
ci: save cache in branch
Sieluna Dec 25, 2024
9dac59b
fix: try allow pr
Sieluna Dec 25, 2024
95c9f32
ci: force lokalise not create pr
Sieluna Dec 25, 2024
a51ca43
fix: incorrect iso mapping
Sieluna Dec 25, 2024
7e1151e
fix: bad fix
Sieluna Dec 25, 2024
74a5553
fix: none translates path
Sieluna Dec 26, 2024
3526de2
ci: wip drop the fucking action
Sieluna Dec 26, 2024
da69a4d
ci: use esm
Sieluna Dec 26, 2024
4234b04
ci: es module fix
Sieluna Dec 26, 2024
5c09b42
fix: number parse issue
Sieluna Dec 26, 2024
9f7984e
revert: address the issue
Sieluna Dec 26, 2024
2118b5e
fix: to executable
Sieluna Dec 26, 2024
31b82c4
ci: use relative path
Sieluna Dec 26, 2024
7de7b32
Merge remote-tracking branch 'upstream/dev' into localization
Sieluna Jul 14, 2025
60058b8
fix: few small fix
Sieluna Jul 14, 2025
bba85aa
chore: check translation failure
alandtse Jul 20, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Auto detect text files and perform LF normalization
* text=auto

**/Translations/*.txt text working-tree-encoding=UTF-16LE-BOM eol=crlf
59 changes: 59 additions & 0 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

env:
TRANSLATION_FILES: package/Interface/Translations/

jobs:
check-changes:
name: Check for changes in PRs
Expand Down Expand Up @@ -100,6 +103,56 @@ jobs:
echo "should-build=true" >> $GITHUB_OUTPUT
echo "hlsl-should-build=true" >> $GITHUB_OUTPUT

translate:
name: translate plugin
runs-on: ubuntu-latest
env:
ISO: .github/workflows/iso.json
TRANSLATION_CACHE: locales/
WHITELIST_FILES: CommunityShaders_english.txt
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- uses: actions/setup-node@v4
with:
node-version: 20

- name: pull files from Lokalise
env:
LOKALISE_TOKEN: ${{ secrets.LOKALISE_API_TOKEN }}
PROJECT_ID: ${{ vars.PROJECT_ID }}
FILE_FORMAT: json
GITHUB_REF_NAME: ${{ github.ref_name }}
run: |
node .github/workflows/lokalise_download.mjs

- name: convert json to utf-16-le-bom txt
run: |
for file in $TRANSLATION_CACHE/*.json; do
if [[ -f "$file" ]]; then
iso_code=$(basename "$file" .json)
language=$(jq -r --arg iso "$iso_code" '.iso_lang[$iso]' $ISO)
if [[ -z "$language" || "$language" == "null" ]]; then
echo "Warning: ISO code '$iso_code' not found. Skipping $file." >&2
continue
fi
output_file="$TRANSLATION_FILES/CommunityShaders_${language}.txt"
if [[ "$(basename "$output_file")" != "$WHITELIST_FILES" ]]; then
printf "\xFF\xFE" > "$output_file"
jq -r 'to_entries | .[] | "\(.key)\t\(.value)"' "$file" \
| sed 's/$/\r/' | iconv -f UTF-8 -t UTF-16LE >> "$output_file"
fi
fi
done

Comment on lines +130 to +149
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add error handling for missing ISO codes.

The current implementation logs a warning but continues processing, which could lead to incomplete translation files.

Consider collecting all missing ISO codes and failing the job if critical translations are missing:

+              missing_codes=()
               for file in $TRANSLATION_CACHE/*.json; do
                 if [[ -f "$file" ]]; then
                   iso_code=$(basename "$file" .json)
                   language=$(jq -r --arg iso "$iso_code" '.iso_lang[$iso]' $ISO)
                   if [[ -z "$language" || "$language" == "null" ]]; then
                     echo "Warning: ISO code '$iso_code' not found. Skipping $file." >&2
+                    missing_codes+=("$iso_code")
                     continue
                   fi
                   output_file="$TRANSLATION_FILES/CommunityShaders_${language}.txt"
                   if [[ "$(basename "$output_file")" != "$WHITELIST_FILES" ]]; then
                     printf "\xFF\xFE" > "$output_file"
                     jq -r 'to_entries | .[] | "\(.key)\t\(.value)"' "$file" \
                       | sed 's/$/\r/' | iconv -f UTF-8 -t UTF-16LE >> "$output_file"
                   fi
                 fi
               done
+              if [ ${#missing_codes[@]} -gt 0 ]; then
+                echo "Error: Missing ISO codes: ${missing_codes[*]}"
+                exit 1
+              fi
🤖 Prompt for AI Agents
In .github/workflows/build.yaml between lines 130 and 149, the script logs a
warning when an ISO code is missing but continues processing, risking incomplete
translation files. Modify the script to collect all missing ISO codes during the
loop and after processing all files, check if any are missing. If there are
missing ISO codes, output an error message listing them and exit the job with a
failure status to prevent incomplete translations.

- name: Upload translations
uses: actions/upload-artifact@v4
with:
name: translations
path: ${{ env.TRANSLATION_FILES }}

cpp-build:
needs: [check-changes]
if: >
Expand Down Expand Up @@ -128,6 +181,12 @@ jobs:
with:
arch: x64

- name: Download translations
uses: actions/download-artifact@v4
with:
name: translations
path: ${{ env.TRANSLATION_FILES }}

- name: Get MSVC version
id: msvc_version
shell: pwsh
Expand Down
24 changes: 24 additions & 0 deletions .github/workflows/iso.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"lang_iso": {
"english": "en",
"french": "fr",
"italian": "it",
"german": "de",
"spanish": "es",
"polish": "pl",
"chinese": "zh_CN",
"russian": "ru",
"japanese": "ja"
},
"iso_lang": {
"en": "english",
"fr": "french",
"it": "italian",
"de": "german",
"es": "spanish",
"pl": "polish",
"zh_CN": "chinese",
"ru": "russian",
"ja": "japanese"
}
}
45 changes: 45 additions & 0 deletions .github/workflows/lokalise.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: push to Lokalise
on:
workflow_dispatch:

jobs:
build:
runs-on: ubuntu-latest
env:
ISO: .github/workflows/iso.json
TRANSLATION_FILES: package/Interface/Translations/
TRANSLATION_CACHE: locales/
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: convert utf-16-le-bom txt to json
run: |
mkdir -p $TRANSLATION_CACHE
for file in "$TRANSLATION_FILES"/*.txt; do
if [[ -f "$file" ]]; then
language=$(basename "$file" | sed -E 's/CommunityShaders_(.*)\.txt/\1/')
iso_code=$(jq -r --arg lang "$language" '.lang_iso[$lang]' "$ISO")
if [[ -z "$iso_code" || "$iso_code" == "null" ]]; then
echo "Warning: Language '$language' not found. Skipping $file." >&2
continue
fi
iconv -f UTF-16LE -t UTF-8 "$file" \
| sed '1s/^\xEF\xBB\xBF//' \
| grep -vE '^(;|\s*$)' \
| jq -Rn '[ inputs | split("\t") | { (.[0]): .[1] } ] | add' \
> "$TRANSLATION_CACHE/$iso_code.json"
fi
done

- name: push files to Lokalise
uses: lokalise/[email protected]
with:
api_token: ${{ secrets.LOKALISE_API_TOKEN }}
project_id: ${{ vars.PROJECT_ID }}
file_format: json
flat_naming: true
translations_path: |
${{ env.TRANSLATION_CACHE }}
base_lang: en
117 changes: 117 additions & 0 deletions .github/workflows/lokalise_download.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { execFile } from 'node:child_process';
import { setTimeout } from 'node:timers/promises';
import { promisify } from 'node:util';

const execFileAsync = promisify(execFile);

const DEFAULT_MAX_RETRIES = 5;
const DEFAULT_SLEEP = 1; // in seconds
const MAX_SLEEP = 60; // exponential backoff limit
const MAX_TOTAL = 300; // total time limit in seconds
const DEFAULT_TIMEOUT = 120; // download command timeout (sec)

const envOr = (name, def) => process.env[name] ?? def;
const parseIntEnv = (value, def) => isNaN(parseInt(value)) ? def : parseInt(value);
const parseBoolEnv = (value) => ['true','1','yes'].includes(String(value).toLowerCase());

const config = {
token: envOr('LOKALISE_TOKEN', ''),
projectId: envOr('PROJECT_ID', ''),
fileFormat: envOr('FILE_FORMAT', ''),
refName: envOr('GITHUB_REF_NAME',''),
skipIncludeTags: parseBoolEnv(envOr('SKIP_INCLUDE_TAGS', false)),
maxRetries: parseIntEnv(envOr('MAX_RETRIES', DEFAULT_MAX_RETRIES), DEFAULT_MAX_RETRIES),
sleepTime: parseIntEnv(envOr('SLEEP_TIME', DEFAULT_SLEEP), DEFAULT_SLEEP),
downloadTimeout: parseIntEnv(envOr('DOWNLOAD_TIMEOUT', DEFAULT_TIMEOUT), DEFAULT_TIMEOUT),
otherParams: envOr('CLI_ADD_PARAMS', '').trim(),
};

if (!config.token) {
console.error("Error: Missing LOKALISE_TOKEN env var.");
process.exit(1);
}
if (!config.projectId) {
console.error("Error: Missing PROJECT_ID env var.");
process.exit(1);
}
if (!config.fileFormat) {
console.error("Error: Missing FILE_FORMAT env var.");
process.exit(1);
}

const isRateLimit = (txt) => txt.includes("API request error 429");
const isNoKeysError = (txt) => txt.includes("API request error 406");

async function installLokaliseCLI(timeoutSec) {
console.log("Installing Lokalise CLI using the official installer script...");
const installerUrl = "https://raw.githubusercontent.com/lokalise/lokalise-cli-2-go/master/install.sh";

await execFileAsync('sh', ['-c', `curl -sfL ${installerUrl} | sh`], { timeout: timeoutSec * 1000 });

console.log("Lokalise CLI installed successfully.");
}
Comment on lines +45 to +52
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

❓ Verification inconclusive

Verify the security of the installer script execution.

The code downloads and executes a shell script from a remote URL. While this is the official Lokalise installer, it's a security risk to execute remote scripts without verification.

Consider adding integrity verification:

 async function installLokaliseCLI(timeoutSec) {
     console.log("Installing Lokalise CLI using the official installer script...");
     const installerUrl = "https://raw.githubusercontent.com/lokalise/lokalise-cli-2-go/master/install.sh";
 
-    await execFileAsync('sh', ['-c', `curl -sfL ${installerUrl} | sh`], { timeout: timeoutSec * 1000 });
+    // Download and verify the script first
+    const { stdout: script } = await execFileAsync('curl', ['-sfL', installerUrl], { timeout: timeoutSec * 1000 });
+    // Optionally verify script content or checksum here
+    await execFileAsync('sh', ['-c', script], { timeout: timeoutSec * 1000 });
 
     console.log("Lokalise CLI installed successfully.");
 }

Alternatively, consider pinning to a specific commit hash in the URL for better stability and security.


Enforce integrity verification when installing the Lokalise CLI

To prevent executing unverified code from master, pin the installer to a specific commit or release and verify its checksum before running.

• Location: .github/workflows/lokalise_download.mjsinstallLokaliseCLI()
• Issue: unpinned URL (…/master/install.sh) and no checksum/signature check

Proposed change:

 async function installLokaliseCLI(timeoutSec) {
     console.log("Installing Lokalise CLI using the official installer script...");
-    const installerUrl = "https://raw.githubusercontent.com/lokalise/lokalise-cli-2-go/master/install.sh";
+    // Pin to a known-good commit or tag for stability
+    const installerUrl = "https://raw.githubusercontent.com/lokalise/lokalise-cli-2-go/abc1234/install.sh";
 
-    await execFileAsync('sh', ['-c', `curl -sfL ${installerUrl} | sh`], { timeout: timeoutSec * 1000 });
+    // 1) Download script
+    const { stdout: script } = await execFileAsync(
+      'curl', ['-sfL', installerUrl], { timeout: timeoutSec * 1000 }
+    );
+    // 2) Verify checksum (replace <EXPECTED_SHA256> with actual hash)
+    const hash = crypto.createHash('sha256').update(script).digest('hex');
+    if (hash !== '<EXPECTED_SHA256>') {
+      throw new Error(`Lokalise installer checksum mismatch: ${hash}`);
+    }
+    // 3) Execute verified script
+    await execFileAsync('sh', ['-c', script], { timeout: timeoutSec * 1000 });
 
     console.log("Lokalise CLI installed successfully.");
 }

– Replace abc1234 and <EXPECTED_SHA256> with the approved commit/tag and its checksum.
– Alternatively, fetch a signed release artifact from GitHub Releases if available.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function installLokaliseCLI(timeoutSec) {
console.log("Installing Lokalise CLI using the official installer script...");
const installerUrl = "https://raw.githubusercontent.com/lokalise/lokalise-cli-2-go/master/install.sh";
await execFileAsync('sh', ['-c', `curl -sfL ${installerUrl} | sh`], { timeout: timeoutSec * 1000 });
console.log("Lokalise CLI installed successfully.");
}
async function installLokaliseCLI(timeoutSec) {
console.log("Installing Lokalise CLI using the official installer script...");
// Pin to a known-good commit or tag for stability
const installerUrl = "https://raw.githubusercontent.com/lokalise/lokalise-cli-2-go/abc1234/install.sh";
// 1) Download script
const { stdout: script } = await execFileAsync(
'curl', ['-sfL', installerUrl], { timeout: timeoutSec * 1000 }
);
// 2) Verify checksum (replace <EXPECTED_SHA256> with actual hash)
const hash = crypto.createHash('sha256').update(script).digest('hex');
if (hash !== '<EXPECTED_SHA256>') {
throw new Error(`Lokalise installer checksum mismatch: ${hash}`);
}
// 3) Execute verified script
await execFileAsync('sh', ['-c', script], { timeout: timeoutSec * 1000 });
console.log("Lokalise CLI installed successfully.");
}
🤖 Prompt for AI Agents
In .github/workflows/lokalise_download.mjs around lines 45 to 52, the
installLokaliseCLI function uses an unpinned URL to fetch the installer script
from the master branch without verifying its integrity. To fix this, update the
installerUrl to point to a specific commit hash or release tag instead of
master, then download the script separately, compute its SHA256 checksum, and
compare it against a predefined expected value before executing it. This ensures
the script is verified and prevents running untrusted code.


async function executeDownload(cmdPath, args, timeoutSec) {
const { stdout, stderr } = await execFileAsync(cmdPath, args, { timeout: timeoutSec * 1000 });
const output = stdout + stderr;
if (isNoKeysError(output)) throw new Error(output);
if (isRateLimit(output)) throw new Error(output);
return output;
}

async function downloadFiles(cfg) {
console.log("Starting download from Lokalise...");

const args = [
`--token=${cfg.token}`,
`--project-id=${cfg.projectId}`,
'file', 'download',
`--format=${cfg.fileFormat}`,
'--original-filenames=true',
`--directory-prefix=/`,
];

Comment on lines +72 to +73
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Use fmt::make_format_args for better type safety.

The current approach of manually constructing format arguments using internal fmt APIs is fragile and may break with fmt library updates.

🤖 Prompt for AI Agents
In .github/workflows/lokalise_download.mjs around lines 72 to 73, the code
manually constructs format arguments using internal fmt APIs, which is fragile
and risks breaking with future fmt library updates. Replace this manual
construction with fmt::make_format_args to ensure better type safety and
maintainability. Update the code to use fmt::make_format_args for creating
format arguments instead of the current approach.

if (!config.skipIncludeTags && config.refName) {
args.push(`--include-tags=${config.refName}`);
}

let attempt = 1, sleepTime = cfg.sleepTime;
const startTime = Date.now();

while (attempt <= cfg.maxRetries) {
console.log(`Attempt ${attempt} of ${cfg.maxRetries}...`);

try {
await executeDownload('./bin/lokalise2', args, cfg.downloadTimeout);
return;
} catch (err) {
const msg = err.message || String(err);
if (isNoKeysError(msg)) {
throw new Error("No keys found for export with current settings.");
}
if (isRateLimit(msg)) {
const elapsed = (Date.now() - startTime) / 1000;
if (elapsed >= MAX_TOTAL) {
throw new Error(`Max total time exceeded after ${attempt} attempts.`);
}
console.warn(`Rate-limited. Retrying in ${sleepTime}s...`);
await setTimeout(sleepTime * 1000);
sleepTime = Math.min(sleepTime * 2, MAX_SLEEP);
} else {
console.error("Unexpected error:", msg);
}
}
attempt++;
}
throw new Error(`Failed to download files after ${cfg.maxRetries} attempts.`);
}

try {
await installLokaliseCLI(DEFAULT_TIMEOUT);
await downloadFiles(config);
console.log("Successfully downloaded files.");
process.exit(0);
} catch (err) {
console.error("Error:", err.message || err);
process.exit(1);
}
Loading
Loading