Files
TREK/client/scripts/generate-android-strings.mjs
T
Maurice 1e4f1922e8 feat(android): bring the Android app level with the iOS app
Everything the iOS shell gained, built natively for Android 16 with Material
You, and back to Android 7:

- Home-screen widgets in Jetpack Glance: trip, today, bookings, to-dos,
  packing, world map and travel group, from the same snapshot the iOS widgets
  read. Wallpaper colours on Android 12+, resizable in three layouts, a trip
  picker per widget (reconfigurable), live previews in Android 15's picker
  and preview images before it.
- The travel day as a Live Update: a promoted ongoing notification with the
  next stop as the status-bar chip, a countdown and the day's stops as
  segments (ProgressStyle), a quiet ongoing notification before Android 16.
- A share sheet of its own: a Material bottom sheet with the trip picker for
  files, mails, pages and links shared from any app, and the coming trips as
  launcher and direct-share shortcuts.
- Push: channels per kind of notification (chat, trip updates, invitations,
  general) by the event the server now names, stacked per channel.
- Location for navigator.geolocation from the fused provider, a store for
  Mapbox's map resources (30 days, 250 MB), Google Play Billing behind the
  StoreKit methods, and Google Wallet save links.
- Large screens: every orientation and window size without a restart,
  the window's size classes and a foldable's posture and hinge for the UI
  (windowLayout), the UI's keyboard shortcuts as the app's (keyCommands),
  predictive back, per-app language.
- The native strings in the 28 languages of the iOS app, generated from its
  strings files (scripts/generate-android-strings.mjs).

Debug builds carry a widget gallery that draws every widget at every size.
2026-11-07 14:16:04 +01:00

77 lines
3.2 KiB
JavaScript

/**
* 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, <lang>.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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
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]) => ` <string name="${name}">${escape(value)}</string>`).join('\n');
writeFileSync(
join(folder, 'strings_native.xml'),
'<?xml version="1.0" encoding="utf-8"?>\n' +
'<!-- Generated by client/scripts/generate-android-strings.mjs from the iOS widget and share\n' +
' sheet strings and scripts/android-strings.json. Edit those and run it again. -->\n' +
`<resources>\n${body}\n</resources>\n`
);
}
console.log(`wrote ${languages.length} languages`);