Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
70 changes: 68 additions & 2 deletions examples/webapp/app.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
#!/usr/bin/env python3

from flask import Flask, jsonify, make_response, redirect, render_template, request
from flask import Flask, jsonify, make_response, redirect, render_template, request, send_file
import os
import tempfile

import pseudol10nutil.transforms as xforms
from pseudol10nutil import PseudoL10nUtil
from pseudol10nutil import PseudoL10nUtil, POFileUtil

app = Flask(__name__)
appname = "pseudol10nutil"
Expand Down Expand Up @@ -106,5 +108,69 @@ def do_pseudo_ui():
return render_template("pseudolocalize_template.html", **default_options)


@app.route(ui_base_url + "po_upload", methods=["POST"])
def do_pseudo_po_upload():
input_temp_file_path = None
output_temp_file_path = None
try:
if "po_file" not in request.files:
return make_response(
jsonify({"error": "400 Error: No file part in the request."}), 400
)

file = request.files["po_file"]

if file.filename == "":
return make_response(
jsonify({"error": "400 Error: No file selected for uploading."}), 400
)

if not file.filename.endswith(".po"):
return make_response(
jsonify(
{"error": "400 Error: Invalid file type. Please upload a .po file."}
),
400,
)
Comment on lines +128 to +134

Copilot AI Feb 27, 2026

Copy link

Choose a reason for hiding this comment

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

The file type validation only checks the filename extension, which can be easily spoofed. Consider validating the actual file content by checking if polib.pofile() can successfully parse it, and wrap that in a try-except to catch invalid PO files. This would provide more robust validation than just checking the extension.

Copilot uses AI. Check for mistakes.

# Create temporary files
fd_input, input_temp_file_path = tempfile.mkstemp(suffix=".po")
os.close(fd_input) # close file descriptor as pofileutil will open and close the file
fd_output, output_temp_file_path = tempfile.mkstemp(suffix=".po")
os.close(fd_output) # close file descriptor as pofileutil will open and close the file


Copilot AI Feb 27, 2026

Copy link

Choose a reason for hiding this comment

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

There's an extra blank line here that doesn't match the spacing pattern in the rest of the file. Remove one of these blank lines for consistency.

Suggested change

Copilot uses AI. Check for mistakes.
file.save(input_temp_file_path)

pofileutil = POFileUtil()
# Apply the same transforms as the UI text input for consistency
# Default to diacritics, square brackets, and padding
# This could be made configurable in the UI later if needed
current_transforms = [
xforms.transliterate_diacritic,
xforms.pad_length,
xforms.square_brackets
]
pofileutil.transforms = current_transforms

Copilot AI Feb 27, 2026

Copy link

Choose a reason for hiding this comment

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

POFileUtil doesn't have a transforms property. The transforms should be set on the internal l10nutil object instead. Change this line to: pofileutil.l10nutil.transforms = current_transforms

Suggested change
pofileutil.transforms = current_transforms
pofileutil.l10nutil.transforms = current_transforms

Copilot uses AI. Check for mistakes.
pofileutil.pseudolocalizefile(input_temp_file_path, output_temp_file_path)

return send_file(
output_temp_file_path,
as_attachment=True,
download_name="pseudolocalized.po",
mimetype="application/x-po",
)
except Exception as e:
# Log the exception e for debugging if necessary
return make_response(
jsonify({"error": f"500 Error: Internal server error during processing. {str(e)}"}), 500
)
finally:
if input_temp_file_path and os.path.exists(input_temp_file_path):
os.remove(input_temp_file_path)
if output_temp_file_path and os.path.exists(output_temp_file_path):
os.remove(output_temp_file_path)
Comment on lines +169 to +172

Copilot AI Feb 27, 2026

Copy link

Choose a reason for hiding this comment

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

Using send_file with a temporary file that is deleted in the finally block can cause a race condition. Flask's send_file may not have finished sending the file when the finally block executes and deletes it. Instead, either: (1) Don't delete output_temp_file_path and let the OS clean up temp files, or (2) Use send_file's download_name parameter with open() and let Flask handle the cleanup, or (3) Use Flask's after_this_request to schedule cleanup after the response is sent.

Copilot uses AI. Check for mistakes.


if __name__ == "__main__":
app.run(host="0.0.0.0", debug=True)
8 changes: 8 additions & 0 deletions examples/webapp/templates/pseudolocalize_template.html
Original file line number Diff line number Diff line change
Expand Up @@ -48,5 +48,13 @@ <h3 class="centered">Pseudo-localization options:</h3>
<br />
<textarea id="pseudolocalize_output" name="pseudolocalize_output" readonly>{{ pseudolocalized_text_output }}</textarea>
</form>
<form class="centered" id="pseudolocalize_po_form" method="post" action="/pseudol10nutil/po_upload" enctype="multipart/form-data">
<h2 class="centered">Pseudolocalize PO File</h2>
<label for="po_file_input">PO file to pseudo-localize:</label>
<br />
<input type="file" id="po_file_input" name="po_file" accept=".po" />
<br />
<input type="submit" name="do_pseudolocalize_po" id="do_pseudolocalize_po" value="Pseudolocalize PO File"/>
</form>
</body>
</html>
105 changes: 94 additions & 11 deletions examples/webapp/test_app.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,112 @@
import unittest
import io
import json

import requests
from examples.webapp.app import app, ui_base_url, api_base_url
from src.pseudol10nutil.pseudol10nutil import PseudoL10nUtil
from src.pseudol10nutil import transforms as xforms

from pseudol10nutil import PseudoL10nUtil

base_url = "http://localhost:5000/pseudol10nutil/api/v1.0/"
headers = {"Accept": "application/json", "Content-Type": "application/json"}


class TestPseudoL10nUtil(unittest.TestCase):
class TestWebApp(unittest.TestCase):
def setUp(self):
self.client = app.test_client()
# Keep util if other tests directly use its methods,
# or for comparing results in API tests.
self.util = PseudoL10nUtil()
# Default transforms for PO file for assertion comparison
# This should match the defaults set in app.py's po_upload route
self.util.transforms = [
xforms.transliterate_diacritic,
xforms.pad_length,
xforms.square_brackets
]

def test_pseudo(self):
def test_pseudo_api(self):
data = {
"key1": "The quick brown fox jumps over the lazy dog.",
"key2": "The quick brown {animal1} jumps over the lazy {animal2}.",
"key3": "The quick brown %s jumps over the lazy %s.",
"key4": "The quick brown %(animal1)s jumps over the lazy %(animal2)s.",
}
request_data = {"strings": data}
resp = requests.post(base_url + "pseudo", headers=headers, json=request_data)
results = resp.json()["strings"]
# Reset transforms to default for this specific API test if needed,
# as self.util might be configured with PO file defaults in setUp.
# For this test, we use a fresh PseudoL10nUtil instance with its own default transforms.
fresh_util_for_api_test = PseudoL10nUtil()
# If the main API also uses specific non-default transforms, set them here for fresh_util_for_api_test
# For now, assuming it uses the class defaults.

resp = self.client.post(
api_base_url + "pseudo",
headers={"Content-Type": "application/json", "Accept": "application/json"},
data=json.dumps(request_data),
)
self.assertEqual(resp.status_code, 200)
results = resp.get_json()["strings"]
for k, v in results.items():
self.assertEqual(self.util.pseudolocalize(data[k]), v)
self.assertEqual(fresh_util_for_api_test.pseudolocalize(data[k]), v)

def test_po_file_upload_success(self):
po_content = 'msgid "Hello"\nmsgstr ""'
# Apply the specific transforms to "Hello" to get the expected output part
# The self.util is already configured with these transforms in setUp
expected_output_part = self.util.pseudolocalize("Hello")

data = {
'po_file': (io.BytesIO(po_content.encode('utf-8')), 'test.po')
}
resp = self.client.post(
ui_base_url + "po_upload",
content_type='multipart/form-data',
data=data
)
self.assertEqual(resp.status_code, 200)
self.assertIn('attachment', resp.headers['Content-Disposition'])
self.assertIn('filename=pseudolocalized.po', resp.headers['Content-Disposition'])
# Check if the pseudolocalized string is in the output
# The actual PO file will have more structure (headers, etc.)
# but checking for the transformed msgid is a good indicator.
self.assertIn(expected_output_part, resp.data.decode('utf-8'))


Comment on lines +70 to +71

Copilot AI Feb 27, 2026

Copy link

Choose a reason for hiding this comment

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

This test currently passes because the transforms specified in app.py (lines 149-153) happen to match the default transforms of PseudoL10nUtil. However, due to the bug on line 154 where transforms are incorrectly set on pofileutil instead of pofileutil.l10nutil, the transforms aren't actually being applied as intended. Once that bug is fixed, this test will continue to work correctly. Consider adding a test case with non-default transforms to catch such issues in the future.

Suggested change
def test_po_file_upload_with_non_default_transforms(self):
"""
Ensure that the po_upload route correctly applies a non-default set of
transforms to the PO file contents. This test would fail if the route
forgets to configure its PseudoL10nUtil instance and instead relies on
class defaults.
"""
po_content = 'msgid "Hello"\nmsgstr ""'
# Configure a util instance with a non-default ordering/subset of transforms.
# Using a different order and subset ensures that the expected output
# diverges from the class defaults if the route does not configure
# its own transforms.
custom_util = PseudoL10nUtil()
custom_util.transforms = [
xforms.square_brackets,
xforms.transliterate_diacritic,
]
expected_output_part = custom_util.pseudolocalize("Hello")
data = {
'po_file': (io.BytesIO(po_content.encode('utf-8')), 'test_custom.po')
}
resp = self.client.post(
ui_base_url + "po_upload",
content_type='multipart/form-data',
data=data
)
self.assertEqual(resp.status_code, 200)
self.assertIn('attachment', resp.headers['Content-Disposition'])
self.assertIn('filename=pseudolocalized.po', resp.headers['Content-Disposition'])
# With non-default transforms applied, the customized pseudolocalized
# string for "Hello" should appear in the returned PO file.
self.assertIn(expected_output_part, resp.data.decode('utf-8'))

Copilot uses AI. Check for mistakes.
def test_po_file_upload_no_file(self):
resp = self.client.post(
ui_base_url + "po_upload",
content_type='multipart/form-data',
data={}
)
self.assertEqual(resp.status_code, 400)
json_data = resp.get_json()
self.assertIn("error", json_data)
self.assertIn("No file part", json_data["error"])

def test_po_file_upload_wrong_file_type(self):
data = {
'po_file': (io.BytesIO(b"this is not a po file"), 'test.txt')
}
resp = self.client.post(
ui_base_url + "po_upload",
content_type='multipart/form-data',
data=data
)
self.assertEqual(resp.status_code, 400)
json_data = resp.get_json()
self.assertIn("error", json_data)
self.assertIn("Invalid file type", json_data["error"])

def test_po_file_upload_empty_filename(self):
data = {
'po_file': (io.BytesIO(b"some content"), '') # Empty filename
}
resp = self.client.post(
ui_base_url + "po_upload",
content_type='multipart/form-data',
data=data
)
self.assertEqual(resp.status_code, 400)
json_data = resp.get_json()
self.assertIn("error", json_data)
self.assertIn("No file selected", json_data["error"])


if __name__ == "__main__":
Expand Down