How MeetMe Went International on Android

Our engineering team has been working hard to implement full internationalization (i18n), including localization (L10n) support. MeetMe sees international support as mutually beneficial for its members and the business. Through full i18n and L10n support, we provide a user experience that our current members want and some of our future members need. Internationalizing is great, and it makes the experience much better for native speakers, but it didn’t come without its own unique challenges in product, design, engineering, or quality assurance.

Preparing for internationalization

Stringification — ensuring text strings aren’t hard-coded — is one of the biggest pieces of prep-work for i18n in any project. Strings are used for user-facing text in UI components like buttons and labels. In Android, text strings are stored in a format known as string resources. Strings need to be extracted from any code and pushed into string resources. While we were working on i18n, we were fortunate enough to have the Android Lint tool which (among other things) identifies and reports hard-coded strings. If we did stringification early, that meant we could get the strings translated early (we used machine translation for development testing) and we could show our product and QA team just how badass we were. Our first pass was fairly simple: at least 90% of the application was already stringified. Supporting internationalization from the early stages saved us time in both testing and implementation. Having to refactor code is almost never a simple task, so having done it “the right way” from the beginning made it easy for us in the end.

As an example, in your application and within your layout code, let’s say you have an EditText view control with a hint. The hint may be a literal attribute value of the node. That value, android:hint="Email address" should be moved to a string resource to allow for different translations of that string. In your strings file (located in res/values/strings.xml, very likely) the node becomes <string name="email_hint">Email address</string> and your EditText’s hint attribute becomes android:hint="@string/email_hint" where @string/ denotes a string resource and email_hint is the value of the name attribute from your strings (strings.xml) file.


Translating all the strings

After we went through our initial stringification and machine translation, we needed to adopt a process for ingesting new and modified strings so they could be sent for translation without resending the already-translated strings. To do this, we endured a manual process for tracking strings changes. Each time a change was merged into our internal i18n branch, we reviewed the diff of our strings file, gathering new keys, and entering them into a file to be packaged for translation. Once translated, we had to import the new strings into localized strings files (under folders denoted with the appropriate qualifiers as per the “Configuration qualifier names” table. For Spanish (language code “es”), we placed our converted strings into /res/values-es/strings.xml. Once imported, code had to be committed, code reviewed, QA’d (to ensure different string lengths didn’t cause unexpected layout issues, which they sometimes did), and merged.

Using the aforementioned email_hint example, the original value in the strings file looks like
<string name="email_hint">Email address</string>
and then the Spanish strings (res/values-es/strings.xml) file has
<string name="email_hint">Dirección de correo electrónico</string>

Troubleshooting i18n layout issues

As previously mentioned, internationalized strings have different lengths and can sometimes cause unexpected issues related to the design and layout of an application. A Button or ListView item’s height or width may be adversely effected by a longer string, or a given layout may not implement fluidity to ensure proper layout display. Compensating for these issues typically works either through a fluid layout design or by rewording the string.

Fluid layout

The first way, introducing fluidity into a layout, can often help as it allows the view to expand or contract dependent upon the length of the string.

For example, a Button in English might be one line worth of text; in Spanish, the verbiage might be longer, creating the need to wrap the text to multiple lines. This type of layout is typically achieved by using non-static units for widths and heights, such as wrap_content (see Layout Params).


Rewording text

The second way, wording text for brevity, can be used to try to shorten the string to fit within the allotted space. Perhaps your layout has two buttons side-by-side with no additional vertical space to expand into. In that case, this technique is especially useful. For cases where we determined that this was the best solution, we marked our strings files with a custom attribute denoting that the length of the text should be as brief as possible and attempted to be fit into the existing character count of the English phrase.


Users outside of the US/UK see kilometers instead of miles

Localization refers to the process of adapting or converting for use in another language, especially the support of regional local features like timezones, currency, distance units (imperial versus metric) and formatting of dates, times, numbers (decimals, percentages, currencies) and distances. Most of this functionality is afforded to the developer on Android through classes like Calendar, Date, DateUtils, and NumberFormat (as well as its children). Distances were a special case. If we wanted to show 13 ft, 18 mi, 24 km (in cases like Feed and Locals), we needed to implement it ourselves. Our implementation was as follows: if the user’s Locale country is US or UK, use imperial units; otherwise, use metric units. This doesn’t account for the UK’s switching of imperial/metric based on type of measurement (short distances are in metric, long distances in imperial), but we didn’t feel that needed to be implemented at this juncture since we only use the units for distances traveled (as the crow flies).

Strings from API responses

Success and Errors

One of our biggest problems with our English implementation was that a number of our UX responses depended on success and error strings from our API. This was an obvious downfall to our implementation and was already being revamped to localize strings to the device prior to the internationalization process being kicked off. That revamp didn’t really do much for our existing endpoints, as most of the errors that we were localizing were for new features. To localize errors, we used a straightforward structure: errorType & errorCode. Error types are unique (Exception class names from the API side) and error codes are unique to each error type they’re sent with. This mapping allowed us to easily gather all of the exceptions being thrown by our API, and map them on the device side to have them made into user-facing strings. Stringification of errors consumed a big chunk of development time as there are a lot of possible error cases — especially when it comes to validating, sanitizing, and approving user-generated content.

On one side of the coin, this couple of APIs is a nicety for our platforms. The API gets to define a strict set of error cases and limit the data throughput which is beneficial to both the device and the server architecture. Additionally, we don’t have to count on a minor change to an error string causing internationalization problems on the device due to string comparison issues. On the flip side, coupling these values puts a constraint on the API that it must maintain both that error type and code until such a time that the device may be able to update to a newer type or code. The reason that is an issue for us is due to the nature of our code file architecture. Code is shared throughout our services in order to attempt to avoid code duplication. While that’s good, what we don’t have is that code shared with the native platform. So a change reflecting constant value on the API doesn’t automatically get reflected within the device.

Ultimately, if an API error code can’t be mapped to a known code on the device, the user will be presented with the ubiquitous, default error string: “Something went awry.”

Region names

Another hurdle in the API-to-device transition was the internationalization of regional names (countries, states, regions, counties, cities, etc). For example, in English, the display name of the country for US (ISO 3166-1) is “United States” and is “Estados Unidos” in Español. On top of that, the abbreviation for the country varies by region. For US in US it is “US” but for US in ES it is “EE. UU.” To be honest, we didn’t truly conquer this challenge yet. For regions, we chose only to translate country names. Furthermore, that translation is done dynamically through a service we’re using called “Smartling.” Smartling acts as a proxy between the client and the origin; Smartling retrieves the request from the client, mimics the request to origin (our API), parses the response, replaces English strings with the translated strings for the requested language, and responds to the client with full translated content.

Our response originally comes out as ["AFGHANISTAN","ALBANIA","ALGERIA", ...] and is translated (based on the requested language, es-es in this example) to ["AFGANISTÁN","ALBANIA","ARGELIA", ...].


Internationalizing our Android application certainly wasn’t an easy task given some of the roadblocks and caveats we ran into, but we’re confident we finalized with a solid product and implementation that will continue to give our members a quality experience from end to end.

Check out the internationalized version of the MeetMe app for Android on Google Play.

If this type of challenge interests you, consider applying to work at MeetMe.


5 thoughts on “How MeetMe Went International on Android”

  1. Lots of info, but in 2017 you don’t even show the words “km”, etc. in MeetMe. You show people from other countries even if I choose “near me”, and you never show their actual distance, just their city’s name.

Leave a Reply

Your email address will not be published. Required fields are marked *