Skip to content

Localization

Working with localized strings and resources.

Resolving Localized Labels

typescript
import { Apk } from "node-apk";

const apk = new Apk("app.apk");

// Get default label
const defaultLabel = await apk.getLabel();
console.log(`Default: ${defaultLabel}`);

// Get specific locale
const frenchLabel = await apk.getLabel({ locale: "fr" });
console.log(`French: ${frenchLabel}`);

const germanLabel = await apk.getLabel({ locale: "de" });
console.log(`German: ${germanLabel}`);

Manual Resource Resolution

For more control over localization:

typescript
import { Apk } from "node-apk";

const apk = new Apk("app.apk");
const manifest = await apk.getManifestInfo();
const resources = await apk.getResources();

const labelId = manifest.applicationLabel;

if (typeof labelId === "number") {
  const resolved = resources.resolve(labelId);
  
  for (const res of resolved) {
    const lang = res.locale?.language ?? "default";
    const country = res.locale?.country ?? "";
    const locale = country ? `${lang}-${country}` : lang;
    
    console.log(`${locale}: ${res.value}`);
  }
}

Building a Locale Map

typescript
function buildLocaleMap(
  resources: Resources, 
  id: number
): Map<string, string> {
  const resolved = resources.resolve(id);
  const map = new Map<string, string>();
  
  for (const res of resolved) {
    if (typeof res.value === "string") {
      // Use language code as key
      const key = res.locale?.language ?? "default";
      map.set(key, res.value);
    }
  }
  
  return map;
}

// Usage
const apk = new Apk("app.apk");
const manifest = await apk.getManifestInfo();
const resources = await apk.getResources();

const labels = buildLocaleMap(resources, manifest.applicationLabel as number);

console.log("Available translations:");
for (const [locale, label] of labels) {
  console.log(`  ${locale}: ${label}`);
}

Fallback Chain

Implement a localization fallback strategy:

typescript
function getLocalizedValue(
  resources: Resources,
  id: number,
  preferredLocales: string[]
): string | undefined {
  const resolved = resources.resolve(id);
  
  // Try each preferred locale in order
  for (const locale of preferredLocales) {
    // Try exact match (e.g., "en-US")
    const exact = resolved.find(r => {
      const lang = r.locale?.language;
      const country = r.locale?.country;
      const parts = locale.split("-");
      return lang === parts[0] && country === parts[1];
    });
    if (exact?.value) return String(exact.value);
    
    // Try language match (e.g., "en")
    const langMatch = resolved.find(r => 
      r.locale?.language === locale.split("-")[0]
    );
    if (langMatch?.value) return String(langMatch.value);
  }
  
  // Fallback to default (no locale)
  const default_ = resolved.find(r => !r.locale);
  if (default_?.value) return String(default_.value);
  
  // Last resort: first available
  return resolved[0]?.value ? String(resolved[0].value) : undefined;
}

// Usage
const label = getLocalizedValue(resources, labelId, [
  "fr-FR",  // Try French France first
  "fr",     // Then any French
  "en-US",  // Then English US
  "en",     // Then any English
]);

Extracting All Localized Strings

typescript
import { Apk } from "node-apk";
import { promises as fs } from "node:fs";

async function extractAllStrings(apkPath: string, outputDir: string) {
  const apk = new Apk(apkPath);
  const manifest = await apk.getManifestInfo();
  const resources = await apk.getResources();
  
  // Get label translations
  const labelId = manifest.applicationLabel;
  if (typeof labelId === "number") {
    const resolved = resources.resolve(labelId);
    
    for (const res of resolved) {
      if (typeof res.value === "string") {
        const lang = res.locale?.language ?? "default";
        const filename = `${outputDir}/strings-${lang}.json`;
        
        // Read existing or create new
        let strings: Record<string, string> = {};
        try {
          strings = JSON.parse(await fs.readFile(filename, "utf8"));
        } catch {}
        
        strings.app_name = res.value;
        await fs.writeFile(filename, JSON.stringify(strings, null, 2));
      }
    }
  }
}

Locale Detection from Accept-Language

typescript
function parseAcceptLanguage(header: string): string[] {
  return header
    .split(",")
    .map(part => {
      const [lang, q = "1"] = part.trim().split(";q=");
      return { lang: lang.trim(), q: parseFloat(q) };
    })
    .sort((a, b) => b.q - a.q)
    .map(item => item.lang);
}

// Usage in a web server
function getAppNameForRequest(apk: Apk, acceptLanguage: string) {
  const locales = parseAcceptLanguage(acceptLanguage);
  return apk.getLabel({ locale: locales[0] });
}

// Example
const locales = parseAcceptLanguage("fr-FR;q=0.9, en;q=0.8, de;q=0.7");
console.log(locales); // ["fr-FR", "en", "de"]

Released under the MIT License.