-
Notifications
You must be signed in to change notification settings - Fork 90
feat: support localization (#309) #861
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
d06afec
3cb34cb
92928ed
9e8d93f
529e197
d7ab57b
27dee83
aaa5ec2
bdee7ad
e939838
8ac690a
95f0f2c
5c0aa19
c292a71
4915cc8
5cbdfe9
9a3a116
696b6a5
67b15ad
68827d5
835afe2
2abc221
ab30724
861923e
509a9ce
e9136b8
20f8d4d
74e51e7
b79d59a
aa21c67
9dac59b
95c9f32
a51ca43
7e1151e
74a5553
3526de2
da69a4d
4234b04
5c09b42
9f7984e
2118b5e
31b82c4
7de7b32
60058b8
bba85aa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 |
| 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" | ||
| } | ||
| } |
| 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 |
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent ❓ Verification inconclusiveVerify 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 • Location: 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 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
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:
🤖 Prompt for AI Agents