Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
119 commits
Select commit Hold shift + click to select a range
28c060d
Add scripts to allow addons from personal repos to be synchronized wi…
nvdaes Nov 24, 2025
4669430
use a json file to store addonId, and use it to filter files to get C…
nvdaes Nov 24, 2025
b188560
Try to get files just for the current add-on
nvdaes Nov 24, 2025
7092615
Add workflow to export an add-on to Crowdin (authors would need to be…
nvdaes Nov 24, 2025
e89640d
Use buildVars, not metadata.json file
nvdaes Nov 25, 2025
4c7771b
Add userAccount to buildVars, and step to get addon-id to GitHub work…
nvdaes Nov 26, 2025
c529cee
Update files after testing exporting an add-on to Crowdin, needs refi…
nvdaes Nov 26, 2025
186b755
Add python version file
nvdaes Nov 26, 2025
f1fbf8e
Improve pyproject and update precommit config after testing that chec…
nvdaes Nov 26, 2025
b867a9a
Restore rules
nvdaes Nov 27, 2025
47ed91c
Restore pyproject
nvdaes Nov 27, 2025
402002e
Improve uv project
nvdaes Nov 27, 2025
d820711
Remove files
nvdaes Nov 27, 2025
9f6b3dc
Calculate hash of i18nSources
nvdaes Nov 29, 2025
4c938ec
Update workflow
nvdaes Nov 30, 2025
a303210
Update _l10n
nvdaes Nov 30, 2025
a0d02da
Upload md file
nvdaes Dec 1, 2025
a8d4252
Updates
nvdaes Dec 3, 2025
1a1e6fd
Update l10nUtil
nvdaes Dec 14, 2025
d2395b0
Update workflow
nvdaes Dec 14, 2025
e4dafe1
Update readme
nvdaes Dec 16, 2025
f76904e
Update readme.md
nvdaes Dec 16, 2025
aea5eba
Update _l10n/crowdinSync.py
nvdaes Dec 16, 2025
f7ccaf6
Add setOutput.py to separate Python code from yaml file
nvdaes Dec 16, 2025
0276e22
Remove bad comment
nvdaes Dec 16, 2025
253eb46
Reset pyproject to master
nvdaes Dec 16, 2025
c51e7ad
reset .pre-commit configuration to master
nvdaes Dec 16, 2025
cd4816c
Remove userAccount variable, since we use markdown, not xliff
nvdaes Dec 17, 2025
314220b
Update or add files from scratch depending on existence of hashFile
nvdaes Dec 17, 2025
f3e8b8d
Use addMd and addPotFromScratch outputs
nvdaes Dec 17, 2025
de4fa15
Update dependencies
nvdaes Dec 20, 2025
46a105a
Update setOutput
nvdaes Dec 20, 2025
053d4de
Update workflow
nvdaes Dec 20, 2025
4a3f5a0
Update lock
nvdaes Dec 20, 2025
3c1a73e
Merge branch 'master' into l10n
nvdaes Dec 21, 2025
dbe74dc
Verify uv lock
nvdaes Dec 21, 2025
e717292
Add uv to dependencies in case this is relevant to verify the lock ac…
nvdaes Dec 21, 2025
c4ed575
Remove debug statement
nvdaes Dec 21, 2025
befa647
Run pre-commit
nvdaes Dec 21, 2025
05c8161
Update dependencies
nvdaes Dec 22, 2025
9a0f62a
Deleted Pyproject to avoid conflicts
nvdaes Dec 22, 2025
4abd788
Reset pyproject to master
nvdaes Dec 22, 2025
c256364
Remove _l10n since this will be added as a submodule
nvdaes Dec 22, 2025
0505a3b
Don't run pre-commit since it requires a different token to access hooks
nvdaes Dec 22, 2025
fd2554b
Merge translations into branch
nvdaes Dec 22, 2025
b30f46f
Add project id without using vars
nvdaes Dec 22, 2025
70293c8
Schedule workflow
nvdaes Dec 22, 2025
da09c8c
Rename workflow
nvdaes Dec 22, 2025
5c52f33
Create PR
nvdaes Dec 22, 2025
d0d5e03
Don't create a PR since this n¡may need a personal access token
nvdaes Dec 22, 2025
b40f94a
Update removing permissions for PR
nvdaes Dec 22, 2025
cb7807e
Update Python version compatible with ubuntu-latest
nvdaes Dec 28, 2025
1449a01
Add dry-run
nvdaes Dec 28, 2025
697d048
Optimize workflow to test with act and docker locally
nvdaes Dec 28, 2025
8c9247b
Update uv.lock
nvdaes Dec 30, 2025
a8469fb
Merge branch 'master' into l10n
nvdaes Dec 30, 2025
6089057
Add workflow call to build the add-on, so it can be reused in other w…
nvdaes Mar 25, 2026
d8154a9
Update workflow for testing
nvdaes Mar 25, 2026
e699abc
Improve workflow to download files from Crowdin
nvdaes Apr 18, 2026
5eafa64
Update setOutputs to set just add-on id to download translations
nvdaes Apr 18, 2026
0eb3223
[crowdinL10n.yml] improve CI translation workflow
abdel792 Apr 20, 2026
a195110
Remove duplicate setOutputs.py from .github/workflows
abdel792 Apr 20, 2026
9ed44a3
Merge pull request #2 from abdel792/l10nImprovements
nvdaes Apr 20, 2026
e62bd5b
Update lock
nvdaes Apr 20, 2026
37c4d4f
Merge
nvdaes Apr 20, 2026
91995bd
Try to fix pre-commit configuration
nvdaes Apr 20, 2026
0c613be
Reset gitignore to master
nvdaes Apr 20, 2026
0b65072
Require polib 1.2.0
nvdaes Apr 20, 2026
f9ba8fe
Update lock file
nvdaes Apr 20, 2026
8e514ba
Remove sha256 file
nvdaes Apr 20, 2026
9c64247
Remove verification of lock file in pre-commit config, not present in…
nvdaes Apr 20, 2026
7589fef
Remove Crowdin client
nvdaes Apr 20, 2026
94b1a88
Update lock file
nvdaes Apr 20, 2026
65290e4
Enhance Crowdin l10n workflow with MD quality evaluation and comparis…
abdel792 Apr 22, 2026
059bb53
Merge pull request #3 from abdel792/l10nImprovements
nvdaes Apr 22, 2026
c9c3bab
Run pre-commit
nvdaes Apr 22, 2026
25dcb5d
Fix conflicts
nvdaes Apr 23, 2026
b1d2acf
Restore deleted space
nvdaes Apr 24, 2026
263b7e2
Update lock file
nvdaes Apr 24, 2026
f6e18c2
Remove score staff
nvdaes Apr 24, 2026
5573061
Add ps1 script and rename setOutputs.py
nvdaes Apr 24, 2026
87cbed4
Run ps1 script from workflow
nvdaes Apr 24, 2026
1a18717
Change python version to 3.13
nvdaes Apr 24, 2026
471a320
Update lock file
nvdaes Apr 24, 2026
e50a716
Fix checkTranslation script according to pre-commit
nvdaes Apr 24, 2026
f5150cb
Remove getAddonInfo step
nvdaes Apr 24, 2026
3c7093c
Update scripts and workflow
nvdaes Apr 25, 2026
065a16f
Fix pyright
nvdaes Apr 25, 2026
c4dd521
Add markdownTranslate
nvdaes Apr 25, 2026
00cf8cf
Update markdownTranslate
nvdaes Apr 25, 2026
480c70e
Exclude markdownTranslate from pyright, it contains many errors
nvdaes Apr 26, 2026
ce2d2da
Update source files
nvdaes Apr 26, 2026
3981ece
Updates
nvdaes Apr 26, 2026
0def05d
Fix files with pre-commit
nvdaes Apr 26, 2026
1c92aab
refactor: use Crowdin API for translation checks and optimize file se…
abdel792 Apr 26, 2026
9b5bf84
Merge pull request #4 from abdel792/l10nImprovements
nvdaes Apr 26, 2026
c5e4076
Add crowdin api client
nvdaes Apr 26, 2026
c098392
Add dependencies
nvdaes Apr 26, 2026
39416a0
Remove limit for list of files, fetch all
nvdaes Apr 26, 2026
2396864
Improvements
nvdaes Apr 26, 2026
94929e7
Updates
nvdaes Apr 26, 2026
57d46f5
Add language mappings
nvdaes Apr 27, 2026
74388d0
Fix
nvdaes Apr 27, 2026
1f8eb84
refactor: modernize translation workflow and integrate language mappings
abdel792 Apr 28, 2026
ef0a614
docs: update readme with translation workflow instructions
abdel792 Apr 28, 2026
92e1cd6
docs: fix translation mailing list address in readme.md
abdel792 Apr 28, 2026
f92a73f
Merge pull request #5 from abdel792/l10nImprovements
nvdaes Apr 28, 2026
6a884c0
Fix readme
nvdaes Apr 28, 2026
3100c81
Updates
nvdaes Apr 29, 2026
6fe4efc
Run pre-commit
nvdaes Apr 29, 2026
a3209ac
Exclude langCodes.py from pyright
nvdaes Apr 29, 2026
6c09158
Improve Crowdin language handling and script clarity
abdel792 Apr 29, 2026
50b41ab
refactor(python): update translation score threshold to 0.5
abdel792 Apr 29, 2026
4646c5e
refactor: improve XLIFF score labeling and align thresholds
abdel792 Apr 29, 2026
56ff1e0
cleanup: simplify directory mapping and remove langCodes.py
abdel792 Apr 29, 2026
79bdff2
style: apply NVDA coding standards to checkTranslation.py
abdel792 Apr 29, 2026
76ed71e
Fit to coding standards
nvdaes Apr 29, 2026
e423d6f
fix: correct Crowdin API dictionary key for language identification
abdel792 Apr 29, 2026
2d477f9
Fix pre-commit
nvdaes Apr 29, 2026
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
148 changes: 148 additions & 0 deletions .github/scripts/checkTranslation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# checkTranslation.py
# Copyright (C) 2026 NV Access Limited, Abdel
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.

import sys
import os
from crowdin_api import CrowdinClient


def findFileId(client: CrowdinClient, projectId: int, baseTarget: str, searchExt: str) -> int | None:
"""
Iterates through all project files (using pagination) to find the ID of the source file matching the target name and extension.

:param client: The Crowdin API client instance.
:param projectId: The ID of the Crowdin project.
:param base_target: The base name of the file (e.g., 'myAddon).
:param search_ext: The extension to look for (e.g., '.pot').
:return: The file ID if found, otherwise None.
"""
offset = 0
limit = 100

while True:
resp = client.source_files.list_files(
projectId=projectId,
limit=limit,
offset=offset,
)

data = resp["data"]
for f in data:
path_crowdin = f["data"]["path"].lower()
# Check if the path ends with addon_id.pot or addon_id.xliff.
if path_crowdin.endswith(f"{baseTarget}{searchExt}"):
fileId = f["data"]["id"]
print(f"DEBUG: Match found: {path_crowdin} (ID: {fileId})")
return fileId

if len(data) < limit:
break

offset += limit

return None


def getScoreFromApi(fileNameToSearch: str, langId: str) -> float:
"""
Retrieves the translation progress score for a specific language and file.
Handles pagination for both file listing and language status.

:param fileNameToSearch: The local path or name of the file to check.
:param langId: The language code (e.g., 'fr' or 'pt_BR').
:return: The translation ratio between 0.0 and 1.0.
"""
token = os.environ.get("crowdinAuthToken")
projectIdEnv = os.environ.get("CROWDIN_PROJECT_ID")

if not token or not projectIdEnv:
print("ERROR: Missing environment variables 'crowdinAuthToken' or 'CROWDIN_PROJECT_ID'.")
return 0.0

client = CrowdinClient(token=token)
projectId = int(projectIdEnv)

try:
# Clean and prepare search patterns.
# Example: 'addon/locale/fr/LC_MESSAGES/myAddon.po' -> base_target: 'myAddon'.
baseTarget = fileNameToSearch.replace("\\", "/").split("/")[-1].rsplit(".", 1)[0].lower()
extTarget = fileNameToSearch.split(".")[-1].lower()

# On Crowdin, the source for a .po file is usually a .pot file.
searchExt = ".pot" if extTarget == "po" else f".{extTarget}"

print(f"DEBUG: Searching for source file: {baseTarget}{searchExt}")

fileId = findFileId(client, projectId, baseTarget, searchExt)

if fileId is None:
print(f"WARNING: File '{baseTarget}{searchExt}' not found on Crowdin.")
return 0.0

# Pagination for translation status (Progress).
offset = 0
limit = 100

while True:
resp = client.translation_status.get_file_progress(
projectId=projectId,
fileId=fileId,
limit=limit,
offset=offset,
)

data = resp["data"]
for item in data:
langApi = item["data"]["languageId"]

# Flexible matching (e.g., 'fr' will match 'fr' or 'fr-FR' from API).
# Also handles underscore to dash conversion for Crowdin compatibility
if langApi.lower().startswith(langId.lower().replace("_", "-")):
progress = float(item["data"]["translationProgress"])
return progress / 100

# Check pagination total.
total = resp["pagination"]["totalCount"]
if offset + limit >= total:
break
offset += limit

print(f"DEBUG: Language '{langId}' not found in progress list for this file.")
return 0.0

except Exception as e:
print(f"API ERROR: {e}")
return 0.0


def main():
if len(sys.argv) < 3:
print("Usage: python checkTranslation.py <filePath> <langId>")
sys.exit(2)

input_file = sys.argv[1]
lang = sys.argv[2]

score = getScoreFromApi(input_file, lang)

# Output formatted for capture by the PowerShell script.
print(f"translationRatio={score}")

# Identify extension to provide a specific score label.
ext = input_file.lower().split(".")[-1]
if ext == "md":
print(f"mdScore={score}")
elif ext == "xliff":
print(f"xliffScore={score}")
else:
# Default to poScore for .po and other localization files.
print(f"poScore={score}")

# Exit with success (0) if there is at least 50% translated content.
sys.exit(0 if score > 0.5 else 1)


if __name__ == "__main__":
main()
173 changes: 173 additions & 0 deletions .github/scripts/crowdinSync.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
#!/usr/bin/env pwsh
$ErrorActionPreference = 'Stop'

# Git configuration for automated commits
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"

$addonId = $env:ADDON_ID.Trim()
if (-not $addonId) {
Write-Error "Failed to get addon ID."
exit 1
}
Comment on lines +8 to +12
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

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

$env:ADDON_ID.Trim() will throw if ADDON_ID isn’t set (null), so the script can terminate before reaching the intended error message. Read the env var first and only call .Trim() after verifying it’s non-null/non-empty (or use [string]::IsNullOrWhiteSpace).

Suggested change
$addonId = $env:ADDON_ID.Trim()
if (-not $addonId) {
Write-Error "Failed to get addon ID."
exit 1
}
$rawAddonId = $env:ADDON_ID
if ([string]::IsNullOrWhiteSpace($rawAddonId)) {
Write-Error "Failed to get addon ID."
exit 1
}
$addonId = $rawAddonId.Trim()

Copilot uses AI. Check for mistakes.

# --- STEP 1: PREPARATION AND SOURCE UPDATE ---

$xliffFile = "./$addonId.xliff"
$mdFile = "./readme.md"

if (Test-Path $mdFile) {
if (Test-Path $xliffFile) {
$tempXliff = [System.IO.Path]::GetTempFileName()
Copy-Item "$addonId.xliff" $tempXliff -Force
Write-Host "DEBUG: Updating XLIFF source based on readme.md..."
uv run .github/scripts/markdownTranslate.py updateXliff -m $mdFile -x $tempXliff -o $xliffFile
Comment on lines +22 to +24
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

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

A temp file is created for the previous XLIFF (GetTempFileName()), but it’s never removed. Wrap the update in a try/finally (or remove the temp file after use) to avoid leaking temp files on repeated runs.

Suggested change
Copy-Item "$addonId.xliff" $tempXliff -Force
Write-Host "DEBUG: Updating XLIFF source based on readme.md..."
uv run .github/scripts/markdownTranslate.py updateXliff -m $mdFile -x $tempXliff -o $xliffFile
try {
Copy-Item "$addonId.xliff" $tempXliff -Force
Write-Host "DEBUG: Updating XLIFF source based on readme.md..."
uv run .github/scripts/markdownTranslate.py updateXliff -m $mdFile -x $tempXliff -o $xliffFile
} finally {
if (Test-Path $tempXliff) {
Remove-Item $tempXliff -Force
}
}

Copilot uses AI. Check for mistakes.
} else {
Write-Host "DEBUG: XLIFF template not found. Creating new one from readme.md..."
uv run .github/scripts/markdownTranslate.py generateXliff -m $mdFile -o $xliffFile
Comment on lines +24 to +27
Copy link

Copilot AI Apr 29, 2026

Choose a reason for hiding this comment

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

uv run .github/scripts/markdownTranslate.py ... attempts to execute the Python file as a program. Since markdownTranslate.py isn’t an executable with a shebang, this is likely to fail under uv run. Call it explicitly via Python (e.g., uv run python .github/scripts/markdownTranslate.py ...) for both the update and generate commands.

Suggested change
uv run .github/scripts/markdownTranslate.py updateXliff -m $mdFile -x $tempXliff -o $xliffFile
} else {
Write-Host "DEBUG: XLIFF template not found. Creating new one from readme.md..."
uv run .github/scripts/markdownTranslate.py generateXliff -m $mdFile -o $xliffFile
uv run python .github/scripts/markdownTranslate.py updateXliff -m $mdFile -x $tempXliff -o $xliffFile
} else {
Write-Host "DEBUG: XLIFF template not found. Creating new one from readme.md..."
uv run python .github/scripts/markdownTranslate.py generateXliff -m $mdFile -o $xliffFile

Copilot uses AI. Check for mistakes.
}
}

# Update POT file (addon interface)
uv run scons pot
$potFile = "$addonId.pot"

# --- STEP 2: UPLOAD SOURCES TO CROWDIN ---

if (Test-Path $potFile) {
Write-Host "DEBUG: Uploading updated POT source to Crowdin..."
./l10nUtil.exe uploadSourceFile "$potFile" -c addon
}

if (Test-Path $xliffFile) {
Write-Host "DEBUG: Uploading updated XLIFF source to Crowdin..."
./l10nUtil.exe uploadSourceFile "$xliffFile" -c addon
git add "$xliffFile"
git diff --staged --quiet
if ($LASTEXITCODE -ne 0) {
git commit -m "Update $xliffFile for $addonId"
git push
}
}

# --- STEP 3: EXPORT AND PROCESS TRANSLATIONS ---

Write-Host "DEBUG: Exporting translations from Crowdin..."
./l10nUtil.exe exportTranslations -o _addonL10n -c addon

# Ensure base directories exist
New-Item -ItemType Directory -Force -Path addon/locale | Out-Null
New-Item -ItemType Directory -Force -Path addon/doc | Out-Null

# Load language mappings for Crowdin API calls
$languageMappings = Get-Content -Raw ".github/scripts/languageMappings.json" | ConvertFrom-Json

foreach ($dir in Get-ChildItem -Path "_addonL10n/$addonId" -Directory) {
$langCode = $dir.Name

if ($langCode -eq "en") { continue }

# --- Identify codes
$crowdinLang = $null

# Use the ."variable" syntax to correctly read the PSCustomObject from JSON
if ($languageMappings.PSObject.Properties.Name -contains $langCode) {
$crowdinLang = $languageMappings."$langCode"
}

# Fallback: If no mapping is found, replace underscores with dashes for Crowdin compatibility
if (-not $crowdinLang) {
$crowdinLang = $langCode.Replace('_', '-')
}

# The $langCode (folder name from Crowdin) represents the local repository language code.
# It matches the NVDA directory structure, so no extra mapping is needed.
Write-Host "--- Processing Language: $langCode (Crowdin: $crowdinLang) ---" -ForegroundColor Cyan

# Paths
$remoteMd = Join-Path $dir.FullName "$addonId.md"
$remoteXliff = Join-Path $dir.FullName "$addonId.xliff"
$remotePo = Join-Path $dir.FullName "$addonId.po"
$localMdDir = "addon/doc/$langCode"
$localMd = "$localMdDir/readme.md"
$localPoPath = "addon/locale/$langCode/LC_MESSAGES/nvda.po"

# --- 3.1 PO FILE PROCESSING ---
$poImported = $false
if (Test-Path $remotePo) {
Write-Host "DEBUG: Checking Remote PO progress for $crowdinLang..."
uv run python .github/scripts/checkTranslation.py "$addonId.po" $crowdinLang
if ($LASTEXITCODE -eq 0) {
Write-Host "SUCCESS: Remote PO is valid. Importing to $localPoPath"
New-Item -ItemType Directory -Force -Path (Split-Path $localPoPath) | Out-Null
Move-Item $remotePo $localPoPath -Force
$poImported = $true
} else {
Write-Host "WARNING: Remote PO progress is below threshold."
}
}

if (-not $poImported -and (Test-Path $localPoPath)) {
Write-Host "ACTION: Uploading local legacy PO to Crowdin ($crowdinLang) as fallback."
./l10nUtil.exe uploadTranslationFile $crowdinLang "$addonId.po" $localPoPath -c addon
}

# --- 3.2 DOCUMENTATION PROCESSING (MD & XLIFF) ---
$scoreMd = 0.0
$scoreXliff = 0.0

if (Test-Path $remoteMd) {
Write-Host "DEBUG: Evaluating Remote Markdown score..."
$res = uv run python .github/scripts/checkTranslation.py "$addonId.md" $crowdinLang
$scoreMd = [double]($res | Select-String "mdScore=").ToString().Split("=")[1]
} else {
Write-Host "DEBUG: No remote Markdown file found for this language."
}

if (Test-Path $remoteXliff) {
Write-Host "DEBUG: Evaluating Remote XLIFF score..."
$res = uv run python .github/scripts/checkTranslation.py "$addonId.xliff" $crowdinLang
$scoreXliff = [double]($res | Select-String "xliffScore=").ToString().Split("=")[1]
} else {
Write-Host "DEBUG: No remote XLIFF file found for this language."
}

Write-Host "DEBUG: Comparison Scores -> MD: $scoreMd | XLIFF: $scoreXliff"

$threshold = 0.5
$docImported = $false

if ($scoreXliff -gt $threshold -or $scoreMd -gt $threshold) {
if (!(Test-Path $localMdDir)) { New-Item -ItemType Directory -Force -Path $localMdDir | Out-Null }

if ($scoreXliff -ge $scoreMd) {
Write-Host "SUCCESS: XLIFF is better or equal. Converting XLIFF to local MD ($langCode)..."
./l10nUtil.exe xliff2md $remoteXliff $localMd
$docImported = $true
} else {
Write-Host "SUCCESS: Markdown is better. Importing Remote MD to local ($langCode)..."
Move-Item $remoteMd $localMd -Force
$docImported = $true
}
} else {
Write-Host "WARNING: Both remote MD and XLIFF scores are below threshold ($threshold)."
}

if (-not $docImported -and (Test-Path $localMd)) {
Write-Host "ACTION: Documentation quality too low. Uploading local MD to Crowdin ($crowdinLang) as fallback."
./l10nUtil.exe uploadTranslationFile $crowdinLang "$addonId.md" $localMd -c addon
}
}

# --- STEP 4: COMMIT UPDATED TRANSLATIONS ---

git add addon/locale addon/doc
git diff --staged --quiet
if ($LASTEXITCODE -ne 0) {
git commit -m "Update translations for $addonId from Crowdin (Automatic Sync)"
$branch = $env:downloadTranslationsBranch
git push -f origin "HEAD:$branch"
Write-Host "SUCCESS: Translations committed and pushed."
} else {
Write-Host "DEBUG: No changes in translations to commit."
}
14 changes: 14 additions & 0 deletions .github/scripts/languageMappings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"af_ZA": "af",
"de_CH": "de-CH",
"es": "es-ES",
"es_CO": "es-CO",
"nb_NO": "nb",
"nn_NO": "nn-NO",
"pt_PT": "pt-PT",
"pt_BR": "pt-BR",
"sr": "sr-CS",
"zh_CN": "zh-CN",
"zh_HK": "zh-HK",
"zh_TW": "zh-TW"
}
Loading