mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-28 00:33:06 +08:00
135 lines
7.1 KiB
Python
135 lines
7.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Generate offline UI resources from catalog.json. No network access required."""
|
|
import argparse
|
|
from collections import Counter
|
|
import hashlib
|
|
import json
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
from xml.sax.saxutils import escape
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
CATALOG = Path(__file__).with_name('catalog.json')
|
|
|
|
def quoted(value):
|
|
return json.dumps(value, ensure_ascii=False)
|
|
|
|
def resource_id(key):
|
|
return 'text_' + hashlib.sha256(key.encode()).hexdigest()[:16]
|
|
|
|
def render(data):
|
|
languages = data['languages']
|
|
strings = data['strings']
|
|
def translated(key, language):
|
|
return strings[key].get(language, strings[key].get('en', key))
|
|
outputs = {}
|
|
header = '''/*
|
|
* @Author: DI JUNKUN
|
|
* @Date: 2024-05-29
|
|
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
|
*/
|
|
|
|
#ifndef _LOCALIZATION_DATA_H_
|
|
#define _LOCALIZATION_DATA_H_
|
|
|
|
// Generated by localization/generate.py. Edit localization/catalog.json.
|
|
namespace crossdesk {
|
|
namespace localization {
|
|
namespace detail {
|
|
struct TranslationRow { const char* key; const char* text[10]; };
|
|
inline constexpr TranslationRow kTranslationRows[] = {
|
|
'''
|
|
for key in data['platforms']['desktop']:
|
|
header += ' {' + quoted(key) + ', {' + ', '.join(quoted(translated(key, lang)) for lang in languages) + '}},\n'
|
|
header += '};\n\nstruct NamedTranslation { const char* name; const char* literal; };\ninline constexpr NamedTranslation kNamedTranslations[] = {\n'
|
|
for name,key in data['desktop'].items():
|
|
header += ' {' + quoted(name) + ', ' + quoted(key) + '},\n'
|
|
header += '};\ninline constexpr const char* kSlintLiterals[] = {\n'
|
|
header += ''.join(' ' + quoted(key) + ',\n' for key in data['slint'])
|
|
header += '};\n} // namespace detail\n\n#define CROSSDESK_LOCALIZATION_ALL(X) \\\n'
|
|
header += ' \\\n'.join(' X(' + name + ')' for name in data['desktop'])
|
|
header += '\n\n} // namespace localization\n} // namespace crossdesk\n\n#endif\n'
|
|
outputs['apps/desktop/src/gui/assets/localization/localization_data.h'] = header
|
|
# Android resources participate in native locale matching and per-app language settings.
|
|
qualifiers = {'en':'values','zh-Hans':'values-b+zh+Hans','zh-Hant':'values-b+zh+Hant','pt-BR':'values-pt'}
|
|
for language in languages:
|
|
text = '<?xml version="1.0" encoding="utf-8"?>\n<!-- Generated by localization/generate.py. -->\n<resources>\n'
|
|
for key in data['platforms']['android']:
|
|
value = translated(key,language).replace('\\','\\\\').replace('\n','\\n').replace('\t','\\t').replace('"','\\"').replace("'","\\'")
|
|
text += ' <string name="'+resource_id(key)+'" formatted="false">"'+escape(value)+'"</string>\n'
|
|
text += '</resources>\n'
|
|
qualifier = qualifiers.get(language,'values-'+language)
|
|
outputs['apps/android/app/src/main/res/'+qualifier+'/strings.xml'] = text
|
|
android = '''package cn.crossdesk.mobile;
|
|
|
|
import java.util.HashMap;
|
|
import java.util.Map;
|
|
|
|
/** Generated resource keys; text and locale selection live in Android resources. */
|
|
final class LocalizationKeys {
|
|
static final Map<String, Integer> IDS = new HashMap<>();
|
|
static {
|
|
'''
|
|
android += ''.join(' IDS.put('+quoted(key)+', R.string.'+resource_id(key)+');\n' for key in data['platforms']['android'])
|
|
android += ' }\n private LocalizationKeys() {}\n}\n'
|
|
outputs['apps/android/app/src/main/java/cn/crossdesk/mobile/LocalizationKeys.java'] = android
|
|
locales = ['en','zh-Hans','zh-Hant','ja','ko','de','fr','es','pt-BR','ru']
|
|
outputs['apps/android/app/src/main/res/xml/locales_config.xml'] = '<?xml version="1.0" encoding="utf-8"?>\n<locale-config xmlns:android="http://schemas.android.com/apk/res/android">\n' + ''.join(' <locale android:name="'+code+'" />\n' for code in locales) + '</locale-config>\n'
|
|
# iOS uses Foundation/SwiftUI localization directly, including strings from models.
|
|
permission = 'CrossDesk 使用局域网发现并建立原生 P2P 远程控制连接。'
|
|
for language in languages:
|
|
directory = 'apps/ios/CrossDeskMobile/Resources/'+language+'.lproj/'
|
|
outputs[directory+'Localizable.strings'] = '// Generated by localization/generate.py.\n' + ''.join(quoted(key)+' = '+quoted(translated(key,language))+';\n' for key in data['platforms']['ios'])
|
|
outputs[directory+'InfoPlist.strings'] = '"NSLocalNetworkUsageDescription" = '+quoted(translated(permission,language))+';\n'
|
|
# A generic Portuguese fallback also serves Portuguese locales outside Brazil.
|
|
for name in ['Localizable.strings','InfoPlist.strings']:
|
|
outputs['apps/ios/CrossDeskMobile/Resources/pt.lproj/'+name] = outputs['apps/ios/CrossDeskMobile/Resources/pt-BR.lproj/'+name]
|
|
# Harmony keeps lookup pure; EntryAbility injects the system locale provider.
|
|
harmony = '// Generated by localization/generate.py.\nexport const translations: Record<string, string[]> = {\n'
|
|
for key in data['platforms']['harmony']:
|
|
harmony += ' '+quoted(key)+': ['+', '.join(quoted(translated(key,lang)) for lang in languages)+'],\n'
|
|
harmony += '};\n'
|
|
outputs['apps/harmony/entry/src/main/ets/mobile/LocalizationData.ets'] = harmony
|
|
return outputs
|
|
|
|
def main():
|
|
parser=argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('--check',action='store_true',help='check catalog completeness and generated resources without writing')
|
|
args=parser.parse_args()
|
|
data=json.loads(CATALOG.read_text())
|
|
missing=[(key,lang) for key,values in data['strings'].items() for lang in data['languages'] if lang not in values]
|
|
if missing:
|
|
print(f'{len(missing)} translations missing',file=sys.stderr)
|
|
return 1
|
|
token = re.compile(r'%(?:\d+\$)?[-+0 #]*\d*(?:\.\d+)?(?:ll|l|z)?[sduif@]|%%|\{\d+\}')
|
|
for key, values in data['strings'].items():
|
|
expected = Counter(token.findall(key))
|
|
for language, value in values.items():
|
|
if Counter(token.findall(value)) != expected:
|
|
raise ValueError(f'Format placeholders differ: {language}: {key}')
|
|
if re.search(r'ZXQ\d+QXZ|[【[]\d+[】]]', value):
|
|
raise ValueError(f'Translation marker left in {language}: {key}')
|
|
for keys in data['platforms'].values():
|
|
if len(keys) != len(set(keys)):
|
|
raise ValueError('Duplicate platform resource key')
|
|
for key in keys:
|
|
if key not in data['strings']: raise ValueError(f'Unknown resource: {key}')
|
|
mismatches=[]
|
|
for relative,content in render(data).items():
|
|
path=ROOT/relative
|
|
if args.check:
|
|
if not path.exists() or path.read_text()!=content:mismatches.append(relative)
|
|
else:
|
|
if not path.exists() or path.read_text()!=content:
|
|
path.parent.mkdir(parents=True,exist_ok=True)
|
|
path.write_text(content)
|
|
if mismatches:
|
|
print('Stale generated resources:\n'+'\n'.join(mismatches),file=sys.stderr)
|
|
return 1
|
|
print(f'{len(data["strings"])} strings, {len(data["languages"])} language variants; resources '+('verified' if args.check else 'generated'))
|
|
return 0
|
|
|
|
if __name__=='__main__':
|
|
raise SystemExit(main())
|