Skip to content

Commit 466ec92

Browse files
committed
feat: compile-time i18n system (v0.3.0)
Zero-ceremony internationalization: string literals in UI components are automatically localizable keys. Translation files are flat JSON, all validation happens at compile time, and translations are baked into the binary as an embedded string table with near-zero runtime lookup cost. - Embedded 2D string table: translations[locale_idx * key_count + string_idx] - Native locale detection on all 6 platforms (CFLocaleCopyCurrent, GetUserDefaultLocaleName, Android system properties, LANG/LC_ALL) - {param} interpolation with runtime substitution - CLDR plural rules for 30+ locales (.one/.other/.few/.many/.zero/.two) - Format wrappers: Currency, Percent, ShortDate, LongDate, FormatNumber, FormatTime, Raw - perry i18n extract CLI for scaffolding locale files - iOS .lproj + Android values-xx/ platform resource generation - Compile-time validation: missing translations, param mismatches, plural form errors
1 parent e7acc76 commit 466ec92

29 files changed

Lines changed: 2923 additions & 49 deletions

File tree

CHANGELOG.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,47 @@
22

33
Detailed changelog for Perry. See CLAUDE.md for concise summaries.
44

5+
## v0.3.0 — Compile-Time Internationalization
6+
7+
Major release adding a complete compile-time i18n system to Perry.
8+
9+
### Core Mechanism
10+
- `[i18n]` section in perry.toml: `locales`, `default_locale`, `dynamic`, `[i18n.currencies]`
11+
- Embedded 2D string table: `translations[locale_idx * key_count + string_idx]` — all locales baked into binary
12+
- UI widget string detection: string literals in `Button`, `Text`, `Label`, `TextField`, `TextArea`, `Tab`, `NavigationTitle`, `SectionHeader`, `SecureField`, `Alert` automatically treated as localizable keys
13+
- `Expr::I18nString` HIR variant with transform pass (`perry-transform/src/i18n.rs`) and Cranelift codegen with locale branching
14+
- Compile-time validation: warns on missing translations, unused keys, parameter mismatches
15+
- Key registry: `.perry/i18n-keys.json` updated on every build
16+
17+
### Locale Detection (all 6 platforms)
18+
- macOS/iOS: `CFLocaleCopyCurrent()` (CoreFoundation) — works for GUI apps launched from Finder/SpringBoard
19+
- Windows: `GetUserDefaultLocaleName()` (Win32)
20+
- Android: `__system_property_get("persist.sys.locale")` (bionic libc)
21+
- Linux: `LANG` / `LC_ALL` / `LC_MESSAGES` env vars
22+
- Platform-native APIs tried first, env vars as fallback
23+
- Fuzzy matching: `de_DE.UTF-8` matches `de`, normalizes `_` to `-`
24+
25+
### Interpolation & Plurals
26+
- Parameterized strings: `Text("Hello, {name}!", { name: user.name })` — runtime `perry_i18n_interpolate()` does `{param}` → value substitution
27+
- CLDR plural rules for 30+ locales: `.one`/`.other`/`.few`/`.many`/`.zero`/`.two` suffixes, compile-time validation, runtime `perry_i18n_plural_category()` category selection
28+
- `perry/i18n` native module: `import { t } from "perry/i18n"` for non-UI string localization
29+
30+
### Format Wrappers
31+
- `Currency(value)`, `Percent(value)`, `ShortDate(timestamp)`, `LongDate(timestamp)`, `FormatNumber(value)`, `FormatTime(timestamp)`, `Raw(value)` — importable from `perry/i18n`
32+
- Hand-rolled formatting rules for 25+ locales: number grouping, decimal/thousands separators, currency symbol placement, date ordering (MDY/DMY/YMD), 12h vs 24h time, percent spacing
33+
- `[i18n.currencies]` config: locale → ISO 4217 code mapping
34+
35+
### CLI & Platform Output
36+
- `perry i18n extract`: scans `.ts`/`.tsx` files, generates/updates `locales/*.json` scaffolds
37+
- iOS: `{locale}.lproj/Localizable.strings` generated inside `.app` bundle
38+
- Android: `res/values-{locale}/strings.xml` generated alongside `.so`
39+
40+
### New Files
41+
- `crates/perry-transform/src/i18n.rs` — HIR transform pass
42+
- `crates/perry-runtime/src/i18n.rs` — Runtime: locale detection, interpolation, plural rules, formatters
43+
- `crates/perry/src/commands/i18n.rs` — CLI extract command
44+
- `docs/src/i18n/` — 4 documentation pages (overview, interpolation, formatting, CLI)
45+
546
## v0.2.202
647
- Fix `perry setup ios` not saving bundle_id to perry.toml — bundle ID was used for provisioning profile creation but never written to `[ios].bundle_id`; `perry publish` fell back to default `com.perry.<name>`, causing profile/bundle mismatch
748

CLAUDE.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
88

99
Perry is a native TypeScript compiler written in Rust that compiles TypeScript source code directly to native executables. It uses SWC for TypeScript parsing and Cranelift for code generation.
1010

11-
**Current Version:** 0.2.202
11+
**Current Version:** 0.3.0
1212

1313
## Workflow Requirements
1414

@@ -153,6 +153,12 @@ Projects can list npm packages to compile natively instead of routing to V8. Con
153153

154154
## Recent Changes
155155

156+
### v0.3.0
157+
- Compile-time i18n system (`perry/i18n` module): zero-ceremony localization for UI strings, `[i18n]` config in perry.toml, embedded 2D string table, native locale detection (all 6 platforms via OS APIs), `{param}` interpolation, CLDR plural rules (30+ locales), format wrappers (`Currency`, `Percent`, `ShortDate`, `LongDate`, `FormatNumber`, `FormatTime`, `Raw`), `perry i18n extract` CLI, iOS `.lproj` + Android `values-xx/` generation, compile-time validation
158+
159+
### v0.2.203
160+
- `perry setup` summary: show what's stored in global config vs project perry.toml for all platforms
161+
156162
### v0.2.202
157163
- Fix `perry setup ios` not saving bundle_id to perry.toml
158164

Cargo.lock

Lines changed: 22 additions & 22 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ opt-level = "s" # Optimize for size in stdlib
7979
opt-level = 3
8080

8181
[workspace.package]
82-
version = "0.2.202"
82+
version = "0.3.0"
8383
edition = "2021"
8484
license = "MIT"
8585
repository = "https://github.com/PerryTS/perry"

crates/perry-codegen-js/src/emit.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -696,6 +696,10 @@ impl JsEmitter {
696696
Expr::String(s) => {
697697
self.output.push_str(&self.quote_string(s));
698698
}
699+
Expr::I18nString { key, .. } => {
700+
// JS codegen: emit as regular string (i18n handled by JS runtime)
701+
self.output.push_str(&self.quote_string(key));
702+
}
699703

700704
// --- Variables ---
701705
Expr::LocalGet(id) => {

crates/perry-codegen/src/closures.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -860,7 +860,7 @@ impl crate::codegen::Compiler {
860860
}
861861
// Expressions with no inner expressions to traverse
862862
Expr::Undefined | Expr::Null | Expr::Bool(_) | Expr::Number(_) | Expr::Integer(_) |
863-
Expr::BigInt(_) | Expr::String(_) | Expr::LocalGet(_) | Expr::GlobalGet(_) |
863+
Expr::BigInt(_) | Expr::String(_) | Expr::I18nString { .. } | Expr::LocalGet(_) | Expr::GlobalGet(_) |
864864
Expr::Update { .. } | Expr::FuncRef(_) | Expr::ExternFuncRef { .. } |
865865
Expr::NativeModuleRef(_) | Expr::StaticFieldGet { .. } | Expr::This |
866866
Expr::EnumMember { .. } | Expr::ClassRef(_) | Expr::EnvGet(_) |

0 commit comments

Comments
 (0)