A student writes in Hindi that the same course payment was taken from her account twice. Our support team reads English more easily. A translation helps, but it must not change who paid, how much was taken or what the student asked for.
In this blog, we will learn how to translate the ticket with Scikit-LLM's GPTTranslator and the local Qwen 3.8 27B model. Then we will find out how to check the names, amounts and meaning in the English draft, and whether it still gets the same support label.
The figure below shows the plan. The Hindi ticket goes to the translator, and its name, date, amount and charge count are then checked in the English draft.

Translation Is Not Language Detection
Language detection asks which language a message uses. Translation rewrites the message in another language. These are two different jobs.
We know our ticket is in Hindi because we wrote it. A real inbox can hold mixed languages and short messages that are hard to identify. Detecting the language would be a separate step with its own check.

GPTTranslator does not report the language it saw. Its prompt tells the model to return the text unchanged when it is already in the target language. So a successful translation is not proof that our language field is right.
Keep the Original Message
We store every version of the ticket under one ticket ID. The Hindi source stays as it is. We also write our own English reference, so we know what the ticket means. This reference is for checking only; we never send it to the translator.
Let's see the record as below:
ticket_id = 'T11'
source_language = 'Hindi'
source_text = 'मैं Mina Rao हूँ। 14 सितंबर को मेरे खाते से 499 रुपये दो बार कटे। कृपया जांच करें।'
english_reference_text = 'I am Mina Rao. On 14 September, 499 rupees were charged twice to my account. Please check.'
human_label = 'billing'
The ticket is made up and has no real student's details. The label billing is our own answer for the exercise, not a model result. The ticket ID joins every version, which matters when many tickets are translated at once. A correct sentence attached to the wrong student is still a serious error.

Setup
Every model call in this series goes through the same setup block. Day 2 explains each line. We repeat it here so this article runs on its own:
import os
from skllm.config import SKLLMConfig
# Default: Qwen 3.8 27B running on our own machine through Ollama
SKLLMConfig.set_gpt_url("http://localhost:11434/v1")
SKLLMConfig.set_gpt_key("ollama")
MODEL = "custom_url::qwen3.8:27b"
# Option: the free Qwen 3.8 27B on OpenRouter (uncomment these lines)
# SKLLMConfig.set_gpt_url("https://openrouter.ai/api/v1")
# SKLLMConfig.set_gpt_key(os.environ["OPENROUTER_API_KEY"])
# MODEL = "custom_url::qwen/qwen3.8-27b:free"
With Ollama, the model runs on our own computer, so there is no API bill. The key "ollama" is only a placeholder. If the model is not downloaded yet, ollama pull qwen3.8:27b fetches it once.
Note
With Ollama, the ticket text never leaves our machine. With the OpenRouter option, it is sent to an outside service. That matters for real student messages, so check your support policy before switching.
Tip
The free OpenRouter model allows 20 requests per minute and 50 per day. Keep the key in the OPENROUTER_API_KEY environment variable and never print it.
Ask Scikit-LLM for an English Draft
GPTTranslator turns one text into another text. We pick the target language with output_language. Its prompt asks the model to output the translated text only. The library also removes stray code fences and a [Translated text:] label from the reply.
Let's see the code as below:
from skllm.models.gpt.text2text.translation import GPTTranslator
translator = GPTTranslator(
model=MODEL,
output_language='English',
)
Now, we pass a list, because the translator works on a collection of texts. We save the returned draft in a new field called translation. The source field stays untouched:
source_texts = [source_text]
translated_texts = translator.fit_transform(source_texts)
print(len(translated_texts) == len(source_texts))
record = {
'ticket_id': ticket_id,
'source_language': source_language,
'source_text': source_text,
'english_reference_text': english_reference_text,
'translation': translated_texts[0],
'human_label': human_label,
}
print(record['translation'])
This prints True when we get back one draft per source text. Then it prints the model's English draft. With many tickets, that length check makes sure no position has shifted. The official text translation guide shows the same fit_transform() call.
Check Names and Amounts
Names and amounts are good first checks, because a missing one is easy to spot with code. Let's write a small check that lists the protected items missing from a text:
protected_text = ['Mina Rao', '499']
def missing_items(text):
return [item for item in protected_text if item not in text]
print(missing_items(english_reference_text))
print(missing_items('Mina was charged twice. Please check.'))
[]
['Mina Rao', '499']
Here, we can see that our reference keeps both items. The hand-written bad draft loses the surname and the amount, and the check lists both.

Now, we run the same check on the model's draft:
print('missing:', missing_items(record['translation']))
An empty list means both items appear in the draft. Any item in the list sends the record for review.
But a number check cannot tell us what happened to the number. "499 rupees were charged twice" and "499 rupees were charged once" both pass it.

Read the Meaning in Parts
So a person who reads both languages compares the meaning. We split that review into four questions. Who is writing? What happened, and how many times? Which names, amounts and dates appear? What does the student ask us to do?
Our Hindi ticket answers all four. Mina Rao is writing. On 14 September, 499 rupees were taken from her account twice. She asks the team to check. Natural English wording is fine, as long as these answers stay the same.

Dates need extra care. Our source writes the month as a word, so 14 September is clear. But a date like 09/10/2026 can mean 9 October or September 10. A translation should not pick one meaning. The safe draft keeps the original date and flags it for the student to clarify.

Back-translation means translating the English draft back into Hindi. If the amount or the second charge changes, we have a clear reason to look closer. But a close match does not prove the first translation was right, because the second step can repeat the same mistake.

Warning
A fluent English sentence is not a correct translation. If no reviewer on the team reads the source language, show that limit in the app instead of marking the translation as checked.
Compare the Label on Both Versions
Now, we ask a separate question: does translating change our classifier's label? We classify the Hindi source and the English draft with the same labels and model. Anything unclear goes to needs_review.
from skllm.models.gpt.classification.zero_shot import ZeroShotGPTClassifier
labels = ['billing', 'login', 'cancellation']
classifier = ZeroShotGPTClassifier(
model=MODEL,
default_label='needs_review',
)
classifier.fit(None, labels)
original_prediction = classifier.predict([record['source_text']])[0]
translated_prediction = classifier.predict([record['translation']])[0]
print('human:', human_label)
print('original:', original_prediction)
print('translated:', translated_prediction)
The first line always prints human: billing. The other two lines each print one of billing, login, cancellation or needs_review.

A matching label does not prove the translation is accurate. A draft that says "charged once" could still get billing, yet the case changed. If the labels differ, the translation may have changed the issue, or the classifier may treat Hindi differently from English. The bilingual reviewer decides which one it is.
Keep Evidence Under One Ticket ID
The ticket ID joins the source, the reference, the draft and the review notes. We add the reviewer's notes as one more field, instead of overwriting anything:
record['review_note'] = 'check name, amount, charged twice, 14 September, request to check'
print(sorted(record))
['english_reference_text', 'human_label', 'review_note', 'source_language', 'source_text', 'ticket_id', 'translation']
Here, we can see all seven fields in one record. The source text is still there, so a reviewer can always go back to what the student wrote.

For a larger check, we would prepare more tickets with human reference translations and labels written before any model run. Then we count meaning errors and label changes separately. One ticket teaches the method, but it cannot tell us how well the translator works.
Conclusion
This is how checking a Scikit-LLM translation works. We kept the Hindi source and our own English reference under one ticket ID, then asked GPTTranslator for an English draft from the local Qwen model. Finally, we checked names and amounts with code, read the meaning in four parts, and compared labels as a separate test.
- Store the source and the translation in separate fields. Never overwrite the source.
- Code can catch a missing name or amount, but not a changed event such as "twice" becoming "once".
- A person who reads both languages must review the meaning.
- A matching label is a category check, not a translation check.
Next steps:
- Go back to summarizing support tickets for the same source-first checking habit.
- Continue with named-entity extraction, where we mark names, dates and amounts inside the original text.
- Revisit Zero-Shot Classification with Scikit-LLM to see how the label classifier works.