4
\$\begingroup\$

So i made a program, which allows you to translate from one language to another using the command prompt. I would like advice about the design, optimizations, and the tips and tricks to avoid repetition and writing effective code.

Code:

import requests
from bs4 import BeautifulSoup
import sys


class Translator:

    # Default Options
    headers = {'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:69.0) Gecko/20100101 Firefox/69.0'}
    website = 'https://context.reverso.net/translation/'
    languages = ['arabic', 'german', 'english', 'spanish', 'french', 'hebrew', 'japanese', 'dutch', 'polish', 'portuguese', 'romanian', 'russian', 'turkish']
    textfile_name = 'hello.txt'

    def __init__(self):
        """
        Initialises the following variables to be used through the course of the translator:
        1) response              : holds the response object for the link created by the input
        2) from_language         : holds the name of the language to translate the string from
        3) to_language           : holds the name of the language to translate the string to
        4) string_for_translation: holds the string to be translated from and to
        5) do_nothing            : a placeholder function, for doing nothing
        6) command_line          : holds the arguments provided in the command_line
        """
        self.response = None
        self.from_language = None
        self.to_language = None
        self.string_for_translation = None
        self.do_nothing = lambda: [None, None]
        self.command_line = sys.argv[1:]


    def verify_command_line(self):
        """
        verifies the arguments provided in the command line.

        returns True, if the arguments are valid, else exits the program and outputs an error prompt, if the arguments aren't valid.
        """
        if len(self.command_line) >= 3:
            if self.command_line[0].lower() in self.languages:
                if self.command_line[1].lower() in self.languages+["all"]:
                    if self.command_line[0] != self.command_line[1]:
                        return True

                    else:
                        print("The from_language and to_language cannot be the same!")
                else:
                    print(f"Sorry, the program doesn't support {self.command_line[1]}.")
            else:
                print(f"Sorry, the program doesn't support {self.command_line[0]}.")
        else:
            print("3 Arguments Must Be Provided In The Order: [from_language] [to_language] [string_for_translation].")
        exit()

    def parse_command_line(self):
        """
        parses the command line arguments into useful information/data for the translator.
        """
        
        self.from_language = self.command_line[0].lower()
        self.to_language = self.command_line[1].lower()
        self.string_for_translation = "+".join(self.command_line[2:])


    def get_response(self, url):
        """
        acquires the response object from the url provided.

        if the response is invalid, then an error prompt is raised based on status code or connectivity.
        """
        try:
            self.response = requests.get(url, headers=self.headers)
            if 400 < self.response.status_code < 500:
                print(f"Sorry, unable to find {self.string_for_translation}")
                exit()
        except requests.exceptions.ConnectionError:
            print("Something wrong with your internet connection")
            exit()



    @staticmethod
    def parse_translations(html):
        """
        parses the html for the translations of the string provided, which is located in a <a class='dict'> tag
        """
        return [tag.text.strip() for tag in html.find_all('a', class_='dict')]

    @staticmethod
    def parse_sentences(html):
        """
        parses the html for the example sentences of from_language and to_language, which is located in a unique css selector.
        """
        return [span.text.strip() for span in html.select("#examples-content .text")]


    def translate(self, from_language, to_language, string_for_translation):
        """
        creates the url for the translation, and calls the get_response function, to acquire the content of the page.
        that of which is parsed, using Beautiful Soup.

        returns a list of translations, and a list of example sentences alternating from the from_language and to_language
        """
        link = self.website + f'{from_language.lower()}-{to_language.lower()}/{string_for_translation}'
        self.get_response(link)

        html = BeautifulSoup(self.response.content, 'html.parser')
        translations      = self.parse_translations(html)
        example_sentences = self.parse_sentences(html)

        return translations, example_sentences
    

    def translate_to_all_into_textfile(self):
        """
        translates a string from a language to every language supported in self.languages.
        Then, writes them in a text file with the name in the variable textfile_name.
        Prints the output also in the console for each language.

        Does not print anything, in the current iteration of the languages loop, if the to_language is the same as from_language.
        """
        with open(self.textfile_name, 'w', encoding='utf-8') as text_file:
            for to_language in self.languages:
                translations, example_sentences = self.translate(self.from_language, to_language, self.string_for_translation) if to_language != self.from_language else self.do_nothing()
                self.print_format(translations, example_sentences, to_language, num_of_examples=1, output_into_textfile=text_file) if to_language != self.from_language else self.do_nothing()


    @staticmethod
    def print_format(translations, example_sentences, to_language, num_of_examples=5, output_into_textfile=False):
        """
        translations : refers to the list of translations of a string.
        example_sentences : refers to the list of example sentences alternating from from_language and to_language.
        to_language       : refers to the language to translate to.
        num_of_examples   : refers to a number defining the number of examples of translations and example_sentences to print. Default = 5.
        output_into_textfile : refers to a file object (textfile), to print the output into, if provided. Default = False.


        Prints the results in an appropriate format:
        string_used = 'Hello'

        - - - - - - - - - - - - -
        French Translations:
        bonjour
        allô
        ohé
        coucou

        French Examples:
        Hello SMS World! , Success .:
        Bonjour, monde des SMS ! ","Succès.

        Hello, Mark Dessau, please.:
        Bonjour, Mark Dessau, s#39;il vous plaît.

        Hello, I've something confidential to report.:
        Allô, j'ai quelque chose de confidentiel à révéler.

        Hello, this is Ina Müller's voicemail.:
        Allô. Vous êtes sur le répondeur d'Ina Müller. Je ne suis pas disponible.

        Hello, I'm Tommy Tuberville.:
        Bonjour. Je suis Tommy Tuberville, Université d'Auburn.

        - - - - - OR - - - - - if all languages is chosen, and num_of_examples is 1

        Arabic Translations:
        مرحبا

        Arabic Examples:
        Hello, is Alex Romero available?:
        مرحباً، هل (آليكس روميرو) متاح ""؟


        German Translations:
        hallo

        German Examples:
        Hello. Welcome to High Adventure.:
        Hallo und willkommen bei "High Adventure".


        Spanish Translations:
        hola

        Spanish Examples:
        Hola, esta es la policía de Bradfield.


        French Translations:
        bonjour

        French Examples:
        Hello SMS World! , Success .:
        Bonjour, monde des SMS ! ","Succès.


        Hebrew Translations:
        שלום

        Hebrew Examples:
        Your honor! Hello, Sheriff.:
        כבודו - .שלום, שריף - ...האישה שהתוודתה בטוחה


        Japanese Translations:
        こんにちは

        Japanese Examples:
        Hello, I am Pete Lavache from Platforms Marketing:
        こんにちは、プラットフォーム・マーケッティングのPete Lavacheです。


        Dutch Translations:
        dag

        Dutch Examples:
        Hello, darling wife. Hello, husband.:
        Dag, lief vrouwtje - Dag, mannetje.


        Polish Translations:
        cześć

        Polish Examples:
        Hello and thanks for this great plugin.:
        Cześć i dzięki za ten wspaniały plugin.


        Portuguese Translations:
        olá

        Portuguese Examples:
        Hello pedestrians, city folk... urban professionals.:
        Olá, peões, habitantes da cidade... profissionais urbanos.


        Romanian Translations:
        salut

        Romanian Examples:
        Hello and welcome to the show speaking with Charlie...:
        Salut și bine v-am găsit la show-ul "De vorba cu Charlie"...


        Russian Translations:
        привет

        Russian Examples:
        Hello, I knocked but nobody opened.:
        Привет, я стучалась, но никто не открывал.


        Turkish Translations:
        selam

        Turkish Examples:
        Hello everybody and welcome to NWA airlines.:
        Selam, millet, ve NWA Havayollarına hoş geldiniz.
        - - - - - - - - - - - - -
        """
        to_language = to_language.title()
        if output_into_textfile:
            print(f"{to_language} Translations:\n", "\n".join(translations[0:num_of_examples]), end='\n\n', file=output_into_textfile)
            print(f"{to_language} Example:\n"    , "\n\n".join([f"{example[0]}:\n{example[1]}" for example in zip(example_sentences[:num_of_examples*2:2], example_sentences[1:num_of_examples*2:2])]), end='\n', file=output_into_textfile)
            print("\n", file=output_into_textfile)

        print("\n")
        print(f"{to_language} Translations:\n", "\n".join(translations[0:num_of_examples]), end='\n\n')
        print(f"{to_language} Examples:\n"    , "\n\n".join([f"{example[0]}:\n{example[1]}" for example in zip(example_sentences[:num_of_examples*2:2], example_sentences[1:num_of_examples*2:2])]), end='\n')
        

    def main(self):
        """
        main function, for running the steps to translate in order of: -

        1) Requesting Input:
            - if to_language_number is '0', then it sets self.to_language to "All".

        2) Translating Based on Input:
            - performs multiple translations on all languages, if to_language = "All".
            
        3) Printing in Appropriate Format:
            - if to_language is "All", then it prints one example sentence, and one translation for each language, and pastes it into a text file.
            - if to language is not "All, then it prints five example sentences, and 5 translations for the to_language chosen.
        """
        if self.verify_command_line():
            self.parse_command_line()

        if self.to_language != 'all':
            translations, example_sentences = self.translate(self.from_language, self.to_language, self.string_for_translation)
            self.print_format(translations, example_sentences, self.to_language)
        else:
            self.translate_to_all_into_textfile()


if __name__ == '__main__':
    translator = Translator()
    translator.main()



Output:

Command Line Based Program - MultiLingual Online Translator

Example 1:
> python "MultiLingual Online Translator.py" English French string

French Translations:
 chaîne
corde
train
string
ficelle

French Examples:
 An string broadcast station receives message content.:
Une station de radiodiffusion de chaîne de caractères reçoit un contenu de message.

The data structures are originally described in a string.:
Les structures de données sont initialement décrites sous forme d'une chaîne.

The expanded region accommodates interval consistent outward string bend functionality.:
La région élargie comprend une fonction permettant de faire un tiré sur la corde extérieure en cohérence avec les intervalles.

An improved musical instrument string is provided.:
L'invention concerne une corde améliorée pour instruments de musique.

The drill string is not rotated.:
Le train de tiges n'est pas mis en rotation.


Example 2;
> python "MultiLingual Online Translator.py" English All string

Arabic Translations:
 سلسلة

Arabic Examples:
 That we play coy, string her along in negotiations:
أن نلعب كوي، سلسلة لها جنبا إلى جنب في المفاوضات


German Translations:
 Zeichenfolge

German Examples:
 Enter an alphanumeric string to describe a unique alternate.:
Geben Sie eine alphanumerische Zeichenfolge ein, um die Alternative eindeutig zu kennzeichnen.


Spanish Translations:
 cadena

Spanish Examples:
 The string argument enables passing additional key/value pairs with the ad request.:
El argumento de cadena permite pasar pares clave/valor adicionales con la solicitud de anuncios.


French Translations:
 chaîne

French Examples:
 An string broadcast station receives message content.:
Une station de radiodiffusion de chaîne de caractères reçoit un contenu de message.


Hebrew Translations:
 מחרוזת

Hebrew Examples:
 Microsoft Dynamics AX cannot parse the Web action item configuration string.:
ל - Microsoft Dynamics AX אין אפשרות לנתח את מחרוזת התצורה של פריט הפעולה של האינטרנט.


Japanese Translations:
 文字列

Japanese Examples:
 Unknown token in SRestriction resource string.:
SRestriction リソース 文字列に不明なトークンが含まれています。


Dutch Translations:
 string

Dutch Examples:
 Returns given section of a string.:
Geeft een bepaald deel uit een string terug.


Polish Translations:
 struna

Polish Examples:
 Another relationship between different string theories is T-duality.:
Inną relacją pomiędzy różnymi teoriami strun jest T-dualność.


Portuguese Translations:
 string

Portuguese Examples:
 Possible values: IP address string.:
Valores possíveis: endereço IP em forma de string.


Romanian Translations:
 șir

Romanian Examples:
 Real workaholic, impressive string of wins.:
Real dependent de muncă, șir impresionant de victorii.


Russian Translations:
 строка

Russian Examples:
 The icons and graphics should undergo similar checking and translation as the string text to identify any possible misinterpretations.:
Иконки и графические объекты должны проверяться и переводиться так же, как и строки текста для выявления любых возможных ошибок при толковании.


Turkish Translations:
 ip

Turkish Examples:
 Get a lot of string, slap it together...:
Bir sürü ip al, birbirine bağla...
```
\$\endgroup\$

1 Answer 1

4
\$\begingroup\$

Good implementation, easy to read and understand. Few suggestions:

  • Nested if-else in verify_command_line makes it not straightforward to understand:
    if len(self.command_line) >= 3:
        if self.command_line[0].lower() in self.languages:
            if self.command_line[1].lower() in self.languages+["all"]:
                if self.command_line[0] != self.command_line[1]:
                    return True
    
                else:
                    print("The from_language and to_language cannot be the same!")
            else:
                print(f"Sorry, the program doesn't support {self.command_line[1]}.")
        else:
            print(f"Sorry, the program doesn't support {self.command_line[0]}.")
    else:
        print("3 Arguments Must Be Provided In The Order: [from_language] [to_language] [string_for_translation].")
    exit()
    
    consider to use a deny-all logic:
    if len(self.command_line) < 3:
        print("3 Arguments Must Be Provided In The Order: [from_language] [to_language] [string_for_translation].")
    elif self.command_line[0].lower() not in self.languages:
        print(f"Sorry, the program doesn't support {self.command_line[0]}.")
    elif self.command_line[1].lower() not in self.languages + ["all"]:
        print(f"Sorry, the program doesn't support {self.command_line[1]}.")
    elif self.command_line[0] == self.command_line[1]:
        print("The from_language and to_language cannot be the same!")
    else:
        return True
    exit()
    
    Additionally, such method is not easy to test, because it prints on the console and exits in some cases. An alternative is to return a boolean and a message. For example: True,"" or False,"The from_language and to_language cannot be the same!".
  • The method print_format contains some duplicated logic:
    if output_into_textfile:
        print(f"{to_language} Translations:\n", "\n".join(translations[0:num_of_examples]), end='\n\n', file=output_into_textfile)
        print(f"{to_language} Example:\n"    , "\n\n".join([f"{example[0]}:\n{example[1]}" for example in zip(example_sentences[:num_of_examples*2:2], example_sentences[1:num_of_examples*2:2])]), end='\n', file=output_into_textfile)
        print("\n", file=output_into_textfile)
    
    print("\n")
    print(f"{to_language} Translations:\n", "\n".join(translations[0:num_of_examples]), end='\n\n')
    print(f"{to_language} Examples:\n"    , "\n\n".join([f"{example[0]}:\n{example[1]}" for example in zip(example_sentences[:num_of_examples*2:2], example_sentences[1:num_of_examples*2:2])]), end='\n')
    
    create translations and examples before and then print them:
    translations_output = # create output string
    example_output = # create output string                                                         
    if output_into_textfile:
        print(translations_output, file=output_into_textfile)
        print(example_output, file=output_into_textfile)
        print("\n", file=output_into_textfile)
    
    print("\n")
    print(translations_output)
    print(example_output)
    

Single-responsibility principle

The class Translator is in charge of parsing the input, doing the translation, formatting the output, and containing the logic of the whole program in the self.main method. Consider how you would handle these changes:

  • The input comes from a file
  • The output needs to be formatted nicely for the user
  • Provide an interactive translation
  • Use Google Translate instead of BeautifulSoup

Each option requires the class Translator to change, but SRP says "A class should have only one reason to change".

My suggestion is to distribute responsibilities into more classes and methods, for example:

class Translator:
    def __init__(self, provider):
    def translate(self, from_lang, to_lang, string_to_translate):
    def translate_to_all(self,from_lang, string_to_translate):
    def examples(self, from_lang, to_lang, string_to_translate):
    def examples_to_all(self, from_lang, string_to_translate):

The class Translator contains the supported languages, translates a string, and generates examples. Feel free to adapt the interface to your use case, the point is to make the program more modular. A provider is an adapter for the library BeautifulSoup:

class BeautifulSoupProvider:
    def __init__(self):
        pass
    # public methods
    def translate(self, from_language, to_language, sentence):
    def examples(self, from_lang, to_lang, sentence):
    # private methods
    def __get_response(self, url):
    def __parse_translations(html):
    def __parse_sentences(html):

The __main__ would be something like this:

def verify_command_line(args):
    #...

def to_console(translations, examples, to_language):
    #..

def to_file(translations, examples):
    #..

if __name__ == '__main__':
    if not verify_command_line(sys.argv[1:]):
        exit()
    # get from_language, to_language and string_to_translate
    translator = Translator(BeautifulSoupProvider())
    if to_language == 'all':
        translations = translator.translate_to_all(from_language,string_to_translate)
        examples = translator.examples_to_all(from_language, string_to_translate)
        to_file(translations,examples)
    else:
        translations = translator.translate(from_language, to_language, string_to_translate)
        example_sentences = translator.examples(from_language, to_language, string_to_translate)
        to_console(translations, example_sentences, to_language)

Now the program is more modular and each method and class has its own responsibility. For example, adding Google Translate requires just to create a new adapter class and change one line in the main function. Changes to the input and output won't affect the class Translator and finally you can easily test all the methods.

Note: the code I provided is not tested, it's just to provide some examples.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.