From 0308c5dc94f3c689eac49410acb6fec27519b25d Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sat, 31 May 2025 01:10:14 +0000 Subject: [PATCH] feat: Add .po file pseudolocalization to example web app This update enhances the example web application by adding support for pseudolocalizing Gettext PO (.po) files. Key changes include: - I modified the web UI (`pseudolocalize_template.html`) to include a new form for uploading `.po` files. - I updated the Flask application (`app.py`) with a new endpoint (`/pseudol10nutil/po_upload`) that: - Accepts `.po` file uploads. - Uses `POFileUtil` from the `pseudol10nutil` library to perform pseudolocalization on the file content. - Returns the processed `.po` file to you for download. - Includes error handling for invalid file types or processing issues. - I added comprehensive unit tests (`test_app.py`) for the new file upload functionality, covering success cases and various error scenarios. Existing tests were also refactored to use Flask's test client. This addresses the TODO item "Add support for pseudolocalizing files through the web app" by leveraging the `POFileUtil` class already present in the `pseudol10nutil` library. --- examples/webapp/app.py | 70 +++++++++++- .../templates/pseudolocalize_template.html | 8 ++ examples/webapp/test_app.py | 105 ++++++++++++++++-- 3 files changed, 170 insertions(+), 13 deletions(-) diff --git a/examples/webapp/app.py b/examples/webapp/app.py index 839e197..9daf556 100644 --- a/examples/webapp/app.py +++ b/examples/webapp/app.py @@ -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" @@ -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, + ) + + # 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 + + + 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 + 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) + + if __name__ == "__main__": app.run(host="0.0.0.0", debug=True) diff --git a/examples/webapp/templates/pseudolocalize_template.html b/examples/webapp/templates/pseudolocalize_template.html index c8c9517..8ec96e0 100644 --- a/examples/webapp/templates/pseudolocalize_template.html +++ b/examples/webapp/templates/pseudolocalize_template.html @@ -48,5 +48,13 @@

Pseudo-localization options:


+
+

Pseudolocalize PO File

+ +
+ +
+ +
diff --git a/examples/webapp/test_app.py b/examples/webapp/test_app.py index cdcd380..9d2bc99 100644 --- a/examples/webapp/test_app.py +++ b/examples/webapp/test_app.py @@ -1,18 +1,27 @@ 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}.", @@ -20,10 +29,84 @@ def test_pseudo(self): "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')) + + + 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__":