/** * Writes the Android app's native strings in every language the iOS app has * (android/app/src/main/res/values-*\/strings_native.xml). * * The widget gallery and the share sheet say the same on both platforms, so * their texts come from the iOS strings files (ios/App/TREKWidgets and * ShareExtension, .lproj/Localizable.strings); what only Android has (the * notification channels) lives in android-strings.json next to this script. * Run `node scripts/generate-android-strings.mjs` from client/ after changing either. */ import { mkdirSync, readFileSync, readdirSync, writeFileSync } from 'fs'; import { dirname, join } from 'path'; import { fileURLToPath } from 'url'; const clientDir = join(dirname(fileURLToPath(import.meta.url)), '..'); const ios = join(clientDir, 'ios', 'App'); const res = join(clientDir, 'android', 'app', 'src', 'main', 'res'); const androidOnly = JSON.parse(readFileSync(join(clientDir, 'scripts', 'android-strings.json'), 'utf8')); /** Android's resource folder for an iOS language, where the two differ. */ const FOLDER = { en: 'values', he: 'values-iw', id: 'values-in', 'pt-BR': 'values-pt-rBR', 'zh-Hans': 'values-b+zh+Hans', 'zh-Hant': 'values-b+zh+Hant', }; /** iOS keys are dotted camelCase, Android names snake_case. */ const androidName = (key) => key .replace(/([a-z])([A-Z])/g, '$1_$2') .toLowerCase() .replace(/\./g, '_'); function parseStrings(file) { const out = []; const text = readFileSync(file, 'utf8'); for (const m of text.matchAll(/^"([^"]+)"\s*=\s*"((?:[^"\\]|\\.)*)";/gm)) out.push([m[1], m[2].replace(/\\"/g, '"')]); return out; } function escape(value) { let v = value.replace(/&/g, '&').replace(//g, '>'); v = v.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/"/g, '\\"'); v = v.replace(/%@/g, '%1$s').replace(/%d/g, '%1$d'); return /^[@?]/.test(v) ? `\\${v}` : v; } const languages = readdirSync(join(ios, 'TREKWidgets')) .filter((d) => d.endsWith('.lproj')) .map((d) => d.slice(0, -'.lproj'.length)) .sort(); for (const lang of languages) { const widgets = parseStrings(join(ios, 'TREKWidgets', `${lang}.lproj`, 'Localizable.strings')); const share = parseStrings(join(ios, 'ShareExtension', `${lang}.lproj`, 'Localizable.strings')); const extra = androidOnly[lang]; if (!extra) throw new Error(`android-strings.json has no ${lang}`); const rows = [...widgets, ...share].map(([key, value]) => [androidName(key), value]); // A widget with nothing to show yet says what the share sheet says without trips. rows.push(['widget_empty', Object.fromEntries(share)['share.noTrips']]); rows.push(...Object.entries(extra)); const folder = join(res, FOLDER[lang] ?? `values-${lang}`); mkdirSync(folder, { recursive: true }); const body = rows.map(([name, value]) => ` ${escape(value)}`).join('\n'); writeFileSync( join(folder, 'strings_native.xml'), '\n' + '\n' + `\n${body}\n\n` ); } console.log(`wrote ${languages.length} languages`);