feat(l10n): advertise app localizations to iOS

The per-app language picker in iOS Settings only appears when the built app
declares multiple localizations. Add the planned languages to the Xcode
project's knownRegions (so Xcode compiles their .lproj from the String
Catalog), and have `generate` keep CFBundleLocalizations in Info.plist in sync
with supportedLanguages (currently en, fr).
This commit is contained in:
2026-07-20 10:41:41 +02:00
parent 91573f381a
commit c71e1b97d9
3 changed files with 37 additions and 0 deletions

View File

@@ -2,6 +2,11 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>CFBundleLocalizations</key>
<array>
<string>en</string>
<string>fr</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key> <key>CADisableMinimumFrameDurationOnPhone</key>
<true/> <true/>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>

View File

@@ -6,6 +6,7 @@
import { mkdirSync } from "node:fs"; import { mkdirSync } from "node:fs";
import { join } from "node:path"; import { join } from "node:path";
import { import {
APPLE_INFO_PLIST,
APPLE_XCSTRINGS, APPLE_XCSTRINGS,
kmpValuesDir, kmpValuesDir,
KMP_RESOURCES, KMP_RESOURCES,
@@ -79,6 +80,29 @@ function buildAndroid(doc: StringsFile, lang: string): ParsedAndroid {
return out; return out;
} }
// ---- Apple Info.plist CFBundleLocalizations ---------------------------------
/**
* Keep the app's CFBundleLocalizations in sync with supportedLanguages so iOS
* advertises the languages (and shows the per-app language picker in Settings).
* Replaces an existing array or inserts one right after the root <dict>.
*/
async function syncInfoPlistLocalizations(langs: string[]) {
const plist = await Bun.file(APPLE_INFO_PLIST).text();
const items = langs.map((l) => `\t\t<string>${l}</string>`).join("\n");
const block = `\t<key>CFBundleLocalizations</key>\n\t<array>\n${items}\n\t</array>\n`;
const existing = /\t*<key>CFBundleLocalizations<\/key>\s*<array>[\s\S]*?<\/array>\n/;
const next = existing.test(plist)
? plist.replace(existing, block)
: plist.replace(/(<dict>\n)/, `$1${block}`);
if (next !== plist) {
await Bun.write(APPLE_INFO_PLIST, next);
console.log(`Updated CFBundleLocalizations in ${APPLE_INFO_PLIST}`);
}
}
// ---- entry ------------------------------------------------------------------ // ---- entry ------------------------------------------------------------------
export async function generate() { export async function generate() {
@@ -87,6 +111,8 @@ export async function generate() {
await Bun.write(APPLE_XCSTRINGS, renderXcstrings(buildXcstrings(doc))); await Bun.write(APPLE_XCSTRINGS, renderXcstrings(buildXcstrings(doc)));
console.log(`Wrote ${APPLE_XCSTRINGS}`); console.log(`Wrote ${APPLE_XCSTRINGS}`);
await syncInfoPlistLocalizations(doc.supportedLanguages);
for (const lang of doc.supportedLanguages) { for (const lang of doc.supportedLanguages) {
const dir = join(KMP_RESOURCES, kmpValuesDir(lang, doc.sourceLanguage)); const dir = join(KMP_RESOURCES, kmpValuesDir(lang, doc.sourceLanguage));
mkdirSync(dir, { recursive: true }); mkdirSync(dir, { recursive: true });

View File

@@ -12,6 +12,12 @@ export const APPLE_XCSTRINGS = join(
"apple/VniDrop/Resources/Localizable.xcstrings", "apple/VniDrop/Resources/Localizable.xcstrings",
); );
/** Apple app Info.plist — its CFBundleLocalizations is kept in sync with supportedLanguages. */
export const APPLE_INFO_PLIST = join(
REPO_ROOT,
"apple/VniDrop/Resources/Info.plist",
);
/** KMP / Compose Multiplatform resources root; one values[-lang]/strings.xml per language. */ /** KMP / Compose Multiplatform resources root; one values[-lang]/strings.xml per language. */
export const KMP_RESOURCES = join( export const KMP_RESOURCES = join(
REPO_ROOT, REPO_ROOT,