Spolszczenie LOGO! Soft Comfort przy użyciu AI

LOGO! Soft Comfort to oprogramowanie służące do budowania programów działających na sterownikach LOGO!. Posiada pewną znaczącą wadę – nie posiada spolszczenia.

Gotowe spolszczenie do LOGO! Soft Comfort jest dostępne na moim GitHubie.

W końcu przydały się do czegoś tokeny na DeepSeeku.

Zrzut ekranu przedstawiający program LOGO! Soft Comfort, z polskim interfejsem.
LOGO! Soft Comfort z zainstalowanym spolszczeniem

Wstęp

LOGO! Soft Comfort to program firmy Siemens służący do programowania sterowników „LOGO!”. Niestety nie posiada polskiej wersji językowej, więc postanowiłem, że wykorzystam do czegoś te zgromadzone tokeny na DeepSeeku i zrobię spolszczenie do tego programu.

Tłumaczenie interfejsu

Nie znalazłem żadnej instrukcji dotyczącej dodawania autorskich tłumaczeń, ale nie szukałem za specjalnie (w ogóle).

Pierwszym miejscem gdzie zacząłem szukać plików językowych był główny katalog aplikacji. Długo szukać nie trzeba było, w oczy rzuciły się mi się pliki „Language_xx_XX.properties„, które zawierają teksty interfejsu.

Zrzut ekranu przedstawiający listę plików językowych w katalogu głównym aplikacji LSC.
Pliki językowe w katalogu aplikacji

Są to pliki tekstowe przestrzegające prostego formatu:

# English
#
language.en_US=English
language.version=8.1
# Date 2016-02-19
# 
# Do not remove this line! This line has to be the first line!=#
AnalogInputPanel.configAI=AI3 and AI4 setting
AnalogInputPanel.enable0AIBtn=Enable 0 AIs
AnalogInputPanel.enable2AIBtn=Enable 2 AIs
[...]
klucz.podklucz=Tekst w danym języku

Napisałem prosty parser w Pythonie, co później pozwoliło mi na wysłanie tekstów do API DeepSeeku, jednocześnie zachowując pewność, że LLM nie zmieni struktury pliku:

def parse(f: IO[AnyStr]) -> List[Token]:
    tokens = []
    line = f.readline()
    while line:
        if line.startswith(CommentToken.START_TOKEN):
            tokens.append(CommentToken.from_line(line))
        elif line.strip() == '':
            tokens.append(EmptyToken())
        else:
            tokens.append(KeyValuePairToken.from_line(line))
        line = f.readline()
    return tokens

Spolszczenie LOGO! Soft Comfort przy użyciu DeepSeeka

Do mojego autotłumacza potrzebowałem promptu systemowego, który mówi LLMowi co ma tak właściwie robić. Jestem leniwy, więc kazałem napisać prompt innemu AI. Oto rezultat:

SYSTEM_PROMPT = """
You are a system prompt for an AI whose sole job is to translate English text to Polish in bulk via JSON. Use the following instructions exactly:
You are a translation engine that converts English strings into Polish, preserving keys and JSON structure.

Input:
A JSON array of up to 200 strings, each in the form:
[
  "key1.subkey1=English text",
  "key1.subkey2=More English text",
  …
]

Behavior:
1. Parse the incoming JSON array.
2. For each element:
   a. Split at the first “=” into a key and a value.
   b. Translate the value (the English text) into Polish.
   c. Reassemble into “key=Polish text”.
3. Preserve all keys exactly (including dots and subkeys).
4. Preserve any punctuation, whitespace, and formatting in the translated text.
5. Return the result as a JSON array of the same size and order:
[
  "key1.subkey1=Polish translation",
  "key1.subkey2=Polish translation",
  …
]

Output:
A JSON array, no additional wrapping or commentary.

Example:

Input:
[
  "greeting.hello=Hello, how are you?",
  "farewell.goodbye=Goodbye and see you soon!"
]

Output:
[
  "greeting.hello=Cześć, jak się masz?",
  "farewell.goodbye=Do widzenia i do zobaczenia wkrótce!"
]

That’s all you output—just the translated JSON array.
"""

Potrzebny był jeszcze kawałek kodu, który będzie przesyłać prompt oraz teksty do tłumaczenia do API DeepSeeka:

from openai import OpenAI

SYSTEM_PROMPT = """[...]"""

class TranslationClient:
    def __init__(self, api_key: str):
        self.client = OpenAI(
            api_key=api_key, 
            base_url="https://api.deepseek.com")

    def translate_batch(self, texts: List[str]) -> List[str]:
        response = self.client.chat.completions.create(
            model='deepseek-chat',
            messages=[
                {"role": "system", "content": SYSTEM_PROMPT},
                {"role": "user", "content": json.dumps(texts)}
            ],
            stream=False
        )
        return json.loads(response.choices[0].message.content)

Pozostało tylko połączyć to wszystko w mainie:

def get_translatable(tokens: List[parser.Token]) -> List[parser.Token]:
    translatable = []
    for t in tokens:
        if t.get_token_type() != parser.KeyValuePairToken.TOKEN_TYPE:
            continue
        kp_token = cast(parser.KeyValuePairToken, t)

        # Skip language metadata keys
        if kp_token.key.startswith('language'):
            continue
        translatable.append(kp_token)
    return translatable

def main_translate():
    with open('Language_pl_PL.properties', 'r') as f:
        tokens = parser.parse(f)

    print(f'Tokens: {len(tokens)}')
    translatable = get_translatable(tokens)
    client = translation.TranslationClient(api_key=read_api_key())

    all_translated = []
    all_tokens = len(translatable)

    for batch in itertools.batched(translatable, 15):
        strings = [str(t) for t in batch]
        print(strings)
        translated = client.translate_batch(strings)
        print(translated)

        all_translated.extend(translated)
        print(f'Translated: {len(all_translated)}/{all_tokens}')

        with open('work_file', 'w') as work:
            json.dump(all_translated, work, indent=4)
            work.flush()
    print('all done')

W razie awarii, możemy bardzo łatwo wznowić pracę i nie marnować tokenów, ponieważ spolszczenia zapisywane są w trakcie pracy w formacie JSON do pliku roboczego.

Pominąłem kod zapisujący tłumaczenia do pliku .properties, ale polegał na zamienianiu wcześniej sprasowanych tokenów na stringi i zapisywaniu ich do pliku.

Pozostało skopiować wtedy stworzony plik Language_pl_PL.properties do katalogu aplikacji i zobaczyć czy w opcjach pojawił się język.

Zrzut ekranu przedstawiający menu programu LOGO! Soft Comfort. Rozwinięta lista języków, wybrany język "Polish".
Menu wyboru języka

Okazuje się, że LOGO! Soft Comfort znalazł nasze spolszczenie. Wystarczy wybrać nową pozycję, zrestartować program i… jednak coś nie działa.

Zrzut ekranu pokazujący puste białe okienko programu LOGO! Soft Comfort.

Debugowanie

Po szybkim zweryfikowaniu struktury pliku .properties, przeszedłem do debugowania LSC. Aplikacja została napisana w Javie i używa bootstrapera, który umożliwia włączenie przekierowywanie logów do konsoli.

W pliku Start.lax możemy włączyć tą funkcjonalność:

#   LAX.STDERR.REDIRECT
#   -------------------
#   leave blank for no output, "console" to send to a console window,
#   and any path to a file to save to the file

lax.stderr.redirect=console


#   LAX.STDIN.REDIRECT
#   ------------------
#   leave blank for no input, "console" to read from the console window,
#   and any path to a file to read from that file

lax.stdin.redirect=


#   LAX.STDOUT.REDIRECT
#   -------------------
#   leave blank for no output, "console" to send to a console window,
#   and any path to a file to save to the file

lax.stdout.redirect=console

Po uruchomieniu aplikacji pojawiła się konsola. W śladach stosu widać nazwy funkcji odpowiedzialnych za „help” i „HSFile”:

java.lang.NullPointerException
        at DE.siemens.ad.logo.app.Application.getActiveTabName(Application.java:2022)
        at DE.siemens.ad.logo.util.Log.getTextPane(Log.java:206)
        at DE.siemens.ad.logo.util.Log.print(Log.java:258)
        at DE.siemens.ad.logo.util.Log.println(Log.java:411)
        at DE.siemens.ad.logo.util.Log.printStartSequence(Log.java:458)
        at DE.siemens.ad.pdraw.app.LogoHelp.loadHSFile(LogoHelp.java:334)
        at DE.siemens.ad.pdraw.app.LogoHelp.initialize(LogoHelp.java:176)

Okazuje się, że LogoHelp dotyczy plików podręcznika, które znajdują się w katalogu help.

Zrzut ekranu pokazujący pliki JAR znajdujące się w katalogu help. Widoczne pliki dla 6 języków (brak polskiego).

Po skopiowaniu angielskiej wersji podręcznika pod nazwą Help_pl_PL.jar, program uruchamia się pomyślnie.

Spolszczenie podręcznika

Pliki .jar są tak naprawdę plikami .zip, zatem z łatwością możemy wypakować zawartość tych plików podręcznika.

Zrzut ekranu pokazujący rozpakowany plik podręcznika. Widoczne pliki projektu HTML Help.

Okazuje się, że w JAR-ach znajdują się zarówno skompilowane pliki podręcznika HTML (.chm) jak i źródłowe (folder 11965523851, plik projektu .hhp, plik spisu treści: toc.xml itd.).

Spolszczenie spisu treści

Pliki ndx.xml oraz toc.xml rozbiłem na dwie części i wkleiłem prosto do DeepSeeka przez interfejs webowy, jednocześnie podkreślając żeby AI nie zmieniło struktury pliku. Kawałek przetłumaczonego pliku toc.xml:

<?xml version='1.0' encoding='utf-8' ?>
<!DOCTYPE helpset PUBLIC "-//Sun Microsystems Inc.//DTD JavaHelp HelpSet Version 1.0//EN" "http://java.sun.com/products/javahelp/helpset_1_0.dtd">
<toc version="1.0">
<tocitem text="Pomoc online LOGO!Soft Comfort" target="11965523851" />
<tocitem text="LOGO!Soft Comfort V8.4" target="12109772683">
<tocitem text="Informacje o bezpieczeństwie" target="115239771915">
<tocitem text="Informacje o bezpieczeństwie" target="118270987275" />
</tocitem>
<tocitem text="Ochrona danych" target="153564199819" />
<tocitem text="Uwaga dotycząca bezpieczeństwa" target="security.note" />
<tocitem text="Witamy w LOGO!Soft Comfort V8.4!" target="Start_Screen" />
<tocitem text="Zawartość DVD" target="CD_Content" />
<tocitem text="Co nowego w LOGO!Soft Comfort?" target="25609171723">
<tocitem text="Co nowego w LOGO!Soft Comfort V8.4?" target="161886522891" />
<tocitem text="Co nowego w LOGO!Soft Comfort V8.3?" target="134013754251" />
<tocitem text="Co nowego w LOGO!Soft Comfort V8.2?" target="103892283915" />
<tocitem text="Co nowego w LOGO!Soft Comfort V8.1?" target="86268125067" />
[...]

W środku pliku nie wystąpiły żadne artefakty, ale za to LLM dodał zamykające tagi na końcu części plików. Po ich usunięciu i spakowaniu JAR-a, program wczytał polską wersją spisu treści.

Zrzut ekranu pokazujący spolszczenie spisu treści programu LOGO! Soft Comfort.

Spolszczenie treści

Ostatnią częścią do przetłumaczenia była sama treść podręcznika zawarta w plikach .htm, które zawierają kod HTML.

<div id="nstext" style="valign:bottom">
      <p class="blocktitlefirst">Introduction</p>
      <p>To give you an impression of the versatility of LOGO!, LOGO!Soft Comfort includes a small collection of applications, in addition to the service water pump application shown in the tutorial.
</p>

Zdecydowałem, że postąpię podobnie jak w przypadku spisu treści i nie będę parsować tych plików, ponieważ poprawność ich struktury zostawia trochę do życzenia, a poza tym jest to dodatkowa praca.

Stworzyłem (AI stworzyło) kolejny prompt, tym razem dotyczący plików .htm:

SYSTEM_PROMPT_HTM = """
You are a specialized HTML‑aware translator. You will be given the contents of a `.HTM` file containing English text. Your task is to:

1. Parse the input strictly as HTML.
2. Locate only these elements:
   - `<title>…</title>`
   - `<p>…</p>`
   - `<a …>…</a>` (even when nested inside `<p>`)
3. Translate **only the inner text** of those elements from English to Polish.
4. Preserve **every other part** of the document verbatim, including:
   - Tag names (`<p>`, `<a>`, `<div>`, etc.)
   - Attribute names and values (e.g. `class="foo"`, `id="bar"`)
   - Whitespace, line breaks, indentation
   - Comments, CDATA sections, scripts, styles, etc., without modification
5. Emit the result as valid `.HTM` (i.e. same file extension and structure).

**Example**

**Input**  
```html
<!DOCTYPE html>
<HTML>
<HEAD>
  <TITLE>Welcome to My Site</TITLE>
</HEAD>
<BODY>
  <div class="header">…</div>
  <p class="intro">Hello, world! <a href="about.htm">Learn more</a>.</p>
  <!-- footer below -->
  <p>Contact us at <a href="mailto:info@example.com">info@example.com</a></p>
</BODY>
</HTML>
```

**Output**
```
<!DOCTYPE html>
<HTML>
<HEAD>
  <TITLE>Witamy na mojej stronie</TITLE>
</HEAD>
<BODY>
  <div class="header">…</div>
  <p class="intro">Witaj, świecie! <a href="about.htm">Dowiedz się więcej</a>.</p>
  <!-- footer below -->
  <p>Skontaktuj się z nami pod adresem <a href="mailto:info@example.com">info@example.com</a></p>
</BODY>
</HTML>
```

Begin now. Always output only the translated .HTM content—no additional commentary.
"""

Stworzyłem także metodę wysyłającą żądanie do API:

    def translate_htm(self, htm_text: str) -> str:
        response = self.client.chat.completions.create(
            model='deepseek-chat',
            messages=[
                {"role": "system", "content": SYSTEM_PROMPT_HTM},
                {"role": "user", "content": htm_text}
            ],
            stream=False
        )
        return response.choices[0].message.content

Skrypt przeskanował wszystkie pliki w katalogu 11965523851 i każdy wysyłał do DeepSeeka (do przyspieszenia procesu wykorzystałem ThreadPoolExecutor, który umożliwił mi wysyłanie kilku plików w tym samym czasie).

Cała operacja (w tym kilka testowych uruchomień) kosztowała mnie zawrotne 0,38 USD (w momencie pisania około 1,37 zł):

Zrzut ekranu pokazujący wydatki na platformie DeepSeek. Miesięczne wydatki wynoszą 0,38 USD. Ilość zużytych tokenów to 1364675.

Kompilacja podręcznika

Po spolszczeniu zawartości trzeba było jeszcze skompilować projekt HHP (HTML help project). Do tego posłużył mi HTML Help Workshop. Naiwnie myślałem, że pobiorę go z oficjalnej strony Microsoftu, ale najwyraźniej link wygasł.

Zrzut ekranu pokazujący odpowiedź "404 nie znaleziono".

Na szczęście któryś crawler na Wayback Machine zapisał kopię instalatora:

Zrzut ekranu pokazujący zapis na Wayback Machine, z dnia 22 listopada 2016.

Po jednym przekierowaniu udało mi się pobrać instalator

Zrzut ekranu pokazujący pobrany instalator htmlhelp.exe

Po instalacji HTML Help Workshop skompilowałem projekt HHP:

Microsoft Windows [Version 10.0.19045.6093]
(c) Microsoft Corporation. Wszelkie prawa zastrzeżone.

C:\Program Files (x86)\HTML Help Workshop>hhc.exe C:\Users\bonk\Desktop\spolszczenie-logo-src\src\help\Help_pl-PL.hhp
Microsoft HTML Help Compiler 4.74.8702

Compiling c:\Users\bonk\Desktop\spolszczenie-logo-src\src\help\Help_pl-PL.chm

HHC4002: Warning: The alias "window___SplitHorizontal" is defined more then once. Only the first alias will be used.
HHC3002: Warning: 12206721547.htm : The HTML tag "p" is missing a closing angle bracket.
HHC3002: Warning: 25633462283.htm : The HTML tag "table" is missing a closing angle bracket.
HHC3002: Warning: 12019634699.htm : The HTML tag "p" is missing a closing angle bracket.
HHC3002: Warning: 164360233995.htm : The HTML tag "tr" is missing a closing angle bracket.

Compile time: 0 minutes, 20 seconds
428     Topics
2,611   Local links
10      Internet links
0       Graphics


Created c:\Users\bonk\Desktop\spolszczenie-logo-src\src\help\Help_pl-PL.chm, 9,073,586 bytes
Compression decreased file by 1,901,473 bytes.

C:\Program Files (x86)\HTML Help Workshop>

Widać, że wystąpiły pewne ostrzeżenia związane ze strukturą czterech plików. W przyszłości kiedyś je poprawię (na pewno).

Po ponownym skompresowaniu wszystkich plików do pliku JAR, program pomyślnie wczytał spolszczenie podręcznika.

Zrzut ekranu pokazujący spolszczony podręcznik LOGO! Soft Comfort - widoczny spis treści i zawartość jednej pozycji.

Skrypt budowania

Tak jak wspomniałem, jestem leniwy. Po drugiej ręcznej poprawce tłumaczenia (zmiany nazwy bloku z „LUB” na „OR”), postanowiłem, że napiszę skrypt w PowerShellu:

$hhc = "C:\Program Files (x86)\HTML Help Workshop\hhc.exe"
$zip = "C:\Program Files\7-Zip\7z.exe"
$version = "1.0.0"
$logoScVersion = "8.4"
$buildDir = ".\build"
$distDir = ".\dist"
$srcDir = ".\src"
if (Test-Path $buildDir) {
	Remove-Item -Path $buildDir
}

New-Item -ItemType Directory -Force -Path $buildDir
New-Item -ItemType Directory -Force -Path $distDir

& $hhc "$srcDir\help\Help_pl-PL.hhp"

$buildArt = "$buildDir\Help_pl_PL.zip"
$compress = @{
	Path = "$srcDir\help\*"
	CompressionLevel = "Optimal"
	DestinationPath = $buildArt
}
# Compress-Archive @compress
& $zip a $buildArt "$srcDir\help\*"

$buildDirDist = "$buildDir/dist"
New-Item -ItemType Directory -Path $buildDirDist
New-Item -ItemType Directory -Path "$buildDirDist\help"
Move-Item -Path $buildArt -Destination "$buildDirDist\help\Help_pl_PL.jar" -Force
Copy-Item -Path "$srcDir\Language_pl_PL.properties" -Destination "$buildDirDist\Language_pl_PL.properties"
Copy-Item -Path "$buildDirDist\*" -Destination "$distDir\" -Recurse -Force

$distZipName = "spolszczenie-$version-logo-$logoScVersion.zip"
$distZip = "$distDir\$distZipName"

if (Test-Path $distZip) {
	Remove-Item -Path $distZip
}

$compress = @{
	Path = $buildDirDist
	CompressionLevel = "Optimal"
	DestinationPath = $distZip
}
#Compress-Archive @compress
& $zip a $distZip $buildDirDist

Remove-Item -Path $buildDir -Recurse

Skrypt automatycznie kompiluje projekt podręcznika oraz pakuje wszystko w JAR-a. Niestety wbudowany cmdlet Compress-Archive budował archiwa niekompatybilne z wyświetlaczem podręcznika, więc musiałem użyć 7z.

Repozytorium na GitHubie

Zdecydowałem się opublikować pliki spolszczenia oraz skrypt do budowania na swoim GitHubie. Repozytorium jest dostępne tutaj. Wrzuciłem także zbudowane, gotowe do użycia spolszczenie.

Zrzut ekranu pokazujący repozytorium ze spolszczeniem na GitHubie.