Skip to content

No way to generate valid Android BCP 47 resource directories for locales with numeric region codes (e.g. es-419) #182

@ntsparis

Description

Summary

Phrase provides no mechanism to produce valid Android BCP 47 resource directory names for locales that use numeric UN M.49 region codes, such as es-419 (Latin America & Caribbean). This makes it impossible to use Phrase's pull config to deliver Latin American Spanish — or any other numeric-region locale — to an Android project.

Background: Android locale directory naming

Android supports two naming schemes for locale resource directories:

Legacy (deprecated):
values-fr-rCA — language + -r prefix + ISO 3166-1 alpha-2 region code. Limited to two-letter region codes only.

BCP 47 (recommended, required for numeric regions):
values-b+es+419b+ prefix with + as separators. This is the only valid syntax for numeric region subtags and is what Android Studio itself generates (see screenshot below).

Per the Android documentation: "To use a BCP 47 language tag, combine the language tag with the b+ prefix and replace each subtag separator with +."

The two Phrase placeholders and where each one fails

<locale_name> — works for simple regions, breaks for es-419

Since Phrase allows you to set a custom locale name, you can rename fr-CAfr-rCA and get a valid (if deprecated) values-fr-rCA directory. This is a known workaround.

However, Phrase does not allow + characters in locale names, and the legacy -r format cannot represent numeric region codes at all. There is no name you can set in Phrase that produces values-b+es+419.

<locale_code> — substitutes the raw BCP 47 code directly

Phrase substitutes es-419 as-is, producing values-es-419, which is not a valid Android qualifier and is silently ignored at runtime.

Summary of workarounds and why they all fail for es-419

Approach fr-CA es-419
<locale_name> renamed to fr-rCA ✅ Deprecated but works ❌ Impossible — + not allowed in names, -r can't express numeric regions
<locale_code> substitution ❌ Produces values-fr-CA (invalid) ❌ Produces values-es-419 (invalid)
Manual per-locale file: entries ✅ Works but doesn't scale ❌ Still no way to express b+es+419 as an output path

Expected behaviour

Phrase should provide a placeholder — e.g. <locale_android_bcp47> — that converts the locale ID into a valid Android BCP 47 directory qualifier:

Locale ID Expected output path
es-419 values-b+es+419
fr-CA values-b+fr+CA
da-DK values-b+da+DK

Impact

Any Android project targeting Latin American Spanish — a major market — is completely blocked. There is no workaround available regardless of how the locale is configured in Phrase.

Environment

  • Platform: Android
  • Pull config placeholder: <locale_name> / <locale_code>
  • Affected locales: all UN M.49 numeric region codes (es-419, en-001, etc.)
  • Resource type: values XML

References

phrase:                                                                       
    project_id: <REDACTED>
                                                                                                                                                                                
    pull:
      targets:                                                                                                                                                                  
                  
        # ── ENGLISH (base locale) ──────────────────────────────────────                                                                                                       
        # Path: values/  (no locale suffix)
        # Pulls the English master strings back into the source tree.                                                                                                           
        - file: ./app/src/main/res/values/strings.xml                                                                                                                           
          params:                                                                                                                                                               
            tags: app                                                                                                                                                           
            file_format: xml
            locale_id: en                          # single locale                                                                                                              
            translation_key_prefix: app_
            filter_by_prefix: true                                                                                                                                              
            include_empty_translations: true       # keep untranslated keys
            include_unverified_translations: false                                                                                                                              
            format_options:
              enclose_in_cdata: true                                                                                                                                            
                  
        # ── OTHER LANGUAGES ───────────────────────────────────────────                                                                                                        
        # Path: values-<locale_name>/  (Phrase substitutes the locale)
        # One entry covers all non-English locales at once.                                                                                                                     
        - file: ./app/src/main/res/values-<locale_name>/strings.xml                                                                                                             
          params:
            tags: app                                                                                                                                                           
            file_format: xml
            locale_ids:                            # list of target locales                                                                                                     
              - da-DK
              - el                                                                                                                                                              
              - es
              - es-419
              - fr
              - fr-CA
              - nl-NL
            translation_key_prefix: app_                                                                                                                                        
            filter_by_prefix: true
            include_empty_translations: false      # skip missing translations                                                                                                  
            include_unverified_translations: false
            format_options:
              enclose_in_cdata: true 

The same two-entry pattern repeats for every module (plurals.xml + strings.xml × each module).

Image

We currently rely on a custom GitHub Action to handle path resolution as a workaround. Are we missing a more native or built-in solution?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions