mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 15:51:00 +00:00
1e4f1922e8
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.
77 lines
3.2 KiB
JavaScript
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, '&').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]) => ` <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`);
|