4
\$\begingroup\$

Our application uses some singletons for localized strings. These calls used to be long and dirty. Therefore some co-worker created extensions for the int and string type to ease the usage of translations.

What does the code do: Find a translation string based on the key (the int value). If it cannot be found it falls back to the first provided string. The latter is a params[] string used in underlying string.Format() method as parameters.

const var warrantyDescription = "14";
var text = 4426.Translate("Garantietermijn {0} mnd", warrantyDescription);

However, I think this approach is bad because:

  • Bad SoC. Int and string have nothing todo with translations
  • This will result in unreadable and unmaintainable code from the perspective of other developers, encountering a extended int with specific functionality. Imagine a case where not only translations but xx other functionalities extend the int as it is a unique identifier.

Trying to provide him with a better approach, all I could think of was a struct using implicit operators as below. However, I think this casting is bad practise as well..

const string warrantyDescription = "14";
var text = ((TranslateId) 4426).Translate( "Garantietermijn {0} mnd", warrantyDescription );

To make this question complete, here is my code for the struct type.

public struct TranslateId
{
    private readonly int m_Value;

    public TranslateId (int value)
    {
        m_Value = value;
    }

    public int Value 
    {
        get { return m_Value; }
    }

    public string Translate(string defaultString, params object[] parameters)
    {
        // TODO: Use the actual singleton to retrieve translation
        string translationResult = defaultString;

        // Return the formatted retrieved value
        return string.Format(translationResult, parameters);
    }

    public static implicit operator TranslateId(int value)
    {
        return new TranslateId(value);
    }

    public static implicit operator int(TranslateId id)
    {
        return id.Value;
    }
}

Any thoughts?

\$\endgroup\$
6
  • 5
    \$\begingroup\$ Why don't you use the built-in localization using resources? Something like string.Format(Localization.WarrantyLength, warrantyDescription) (where Localization is the resource class) seems much cleaner to me. \$\endgroup\$ – svick Apr 6 '12 at 12:48
  • \$\begingroup\$ @svick you could have posted that as an answer. But my point is: maybe he has hundreds of resources to translate, like I do. :( Localization.WarrantyLength looks good in principle, but creating and maintaining resources with 500-700 translation strings in 6 langs (en fr es ru ar zh) sounds hellish. I can't really see a good workflow to get the translations to the translators, and then back into the code. Is it XML-based? Perhaps a converter script. Hopefully the process is simpler than I can think of. \$\endgroup\$ – ANeves thinks SE is evil Apr 10 '12 at 13:02
  • \$\begingroup\$ @Aphelion What are the ints? Just random, irrelevant to the translation? Or do they add context? Are they language ids, object ids, anything? \$\endgroup\$ – ANeves thinks SE is evil Apr 10 '12 at 13:04
  • \$\begingroup\$ @ANeves, yes, the resources are XML based, so it shouldn't be a problem to get them to translators, I think. I have never done it in practice, though. \$\endgroup\$ – svick Apr 10 '12 at 13:31
  • \$\begingroup\$ @ANeves they ints are Pkeys known in our Translation Management application (and the underlying datasources). We currently have about 9k of translations (over 3 languages). They already are undoubled and splitted. \$\endgroup\$ – Myrtle Apr 11 '12 at 7:03
5
\$\begingroup\$

This is a horrible misuse of extension methods. The only thing worse (and I'm sure you'll see it) is (n+5).Translate(...). You're absolutely correct - the starting point for the mistake is in thinking of the integer as the base object for the translation. There are a number of well-understood patterns for handling localization, and this isn't any of them. Since we're talking C#, there's the obvious resource technique, but there are others as well. The GNU Project pioneered the _("blah blah blah") mechanism, where the default string serves as a key in a locale-specific table of replacements (along with the Windows OS and MS Office, this is one of the most prevalent and successful translation systems). And then there's the tried-and-true dictionary-indexed-by-a-named-constant (often an enum member), often encapsulated behind a Language class with a Translate(...) method.

\$\endgroup\$
2
  • \$\begingroup\$ This is indeed a good strategy in case the strings are unique. However, the kind of translations we use cause duplicates in the key language (english) as translations to for example dutch and french use other words or grammar. We cannot use the English text as a key to the translations. \$\endgroup\$ – Myrtle Apr 11 '12 at 7:05
  • \$\begingroup\$ @Aphelion That's just one of the reasons why I prefer the named-constant approach. Another is a sneaking suspicion that, when the key is the default language, there are a lot of untranslated strings lurking in the product that will leak out to your users. When the key is Klingon or Esperanto, not so much :-) \$\endgroup\$ – Ross Patterson Apr 11 '12 at 14:56
4
\$\begingroup\$

One unobtrusive method might be to introduce a class called "OutputString" or "TranslatedString" and use it in presentation with explicit conversion from string.

The service then can translate the string using language id and string itself as key.

public class OutputString {
    public static explicit operator OutputString(string b) 
    {
        return new OutputString(b);
    }

    private readonly string innerString;
    public OutputString(string innerString) 
    {
        this.innerString = innerString;
    }

    public override string ToString() 
    {
        LongAndDirtyTranslationClass.EvenLongerAndDirtierMethod(this.innerString)
    }
}

To use;

public class ShowProfileViewModel 
{
    public string Name { get; set; }
    public OutputString Ranking { get; set; }
}

public class ProfileController 
{
    // ...
    public ActionResult Details(int id) 
    {
        var profile = new ShowProfileViewModel {
           Name = this.someService.getName(id),
           Ranking = this.someOtherService.getRanking(id) //returns string
        };

        return View(profile);
    }
}
\$\endgroup\$
1
  • \$\begingroup\$ This is a pretty common and decent answer to the problem. Personally, I much prefer having the key be a named constant or enum instrad of the untranslated string, because that makes it a first-class object that the IDE can help you with. But as long as every input string is passed through translation, rather than having it be the default output (a.k.a., the "English is our default language" scenario), it works well. \$\endgroup\$ – Ross Patterson Apr 8 '12 at 21:38

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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