app-localizer 1.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. app_localizer-1.0.1/LICENSE +21 -0
  2. app_localizer-1.0.1/PKG-INFO +425 -0
  3. app_localizer-1.0.1/README.md +402 -0
  4. app_localizer-1.0.1/pyproject.toml +37 -0
  5. app_localizer-1.0.1/setup.cfg +4 -0
  6. app_localizer-1.0.1/src/app_localizer/__init__.py +19 -0
  7. app_localizer-1.0.1/src/app_localizer/catalog.py +597 -0
  8. app_localizer-1.0.1/src/app_localizer/chat_client.py +243 -0
  9. app_localizer-1.0.1/src/app_localizer/cldr.py +93 -0
  10. app_localizer-1.0.1/src/app_localizer/cli.py +245 -0
  11. app_localizer-1.0.1/src/app_localizer/config.py +392 -0
  12. app_localizer-1.0.1/src/app_localizer/engine.py +99 -0
  13. app_localizer-1.0.1/src/app_localizer/fastlane.py +563 -0
  14. app_localizer-1.0.1/src/app_localizer/fileio.py +55 -0
  15. app_localizer-1.0.1/src/app_localizer/lockfile.py +95 -0
  16. app_localizer-1.0.1/src/app_localizer/xcstrings.py +635 -0
  17. app_localizer-1.0.1/src/app_localizer.egg-info/PKG-INFO +425 -0
  18. app_localizer-1.0.1/src/app_localizer.egg-info/SOURCES.txt +32 -0
  19. app_localizer-1.0.1/src/app_localizer.egg-info/dependency_links.txt +1 -0
  20. app_localizer-1.0.1/src/app_localizer.egg-info/entry_points.txt +2 -0
  21. app_localizer-1.0.1/src/app_localizer.egg-info/requires.txt +3 -0
  22. app_localizer-1.0.1/src/app_localizer.egg-info/top_level.txt +1 -0
  23. app_localizer-1.0.1/tests/test_catalog.py +638 -0
  24. app_localizer-1.0.1/tests/test_chat_client.py +340 -0
  25. app_localizer-1.0.1/tests/test_cldr.py +51 -0
  26. app_localizer-1.0.1/tests/test_cli.py +724 -0
  27. app_localizer-1.0.1/tests/test_config.py +557 -0
  28. app_localizer-1.0.1/tests/test_engine.py +103 -0
  29. app_localizer-1.0.1/tests/test_fastlane.py +471 -0
  30. app_localizer-1.0.1/tests/test_fileio.py +51 -0
  31. app_localizer-1.0.1/tests/test_format_contract.py +109 -0
  32. app_localizer-1.0.1/tests/test_init.py +19 -0
  33. app_localizer-1.0.1/tests/test_lockfile.py +79 -0
  34. app_localizer-1.0.1/tests/test_xcstrings.py +1295 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kyle Hughes
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,425 @@
1
+ Metadata-Version: 2.4
2
+ Name: app-localizer
3
+ Version: 1.0.1
4
+ Summary: Translate Apple String Catalogs and Fastlane App Store metadata.
5
+ Author: Kyle Hughes
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/kylehughes/app-localizer
8
+ Project-URL: Repository, https://github.com/kylehughes/app-localizer
9
+ Project-URL: Changelog, https://github.com/kylehughes/app-localizer/blob/main/CHANGELOG.md
10
+ Keywords: localization,i18n,xcstrings,fastlane,app-store,translation,openai
11
+ Classifier: Development Status :: 5 - Production/Stable
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3 :: Only
17
+ Classifier: Topic :: Software Development :: Localization
18
+ Requires-Python: >=3.9
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: tomli>=2; python_version < "3.11"
22
+ Dynamic: license-file
23
+
24
+ # App Localizer
25
+
26
+ [![Test](https://github.com/kylehughes/app-localizer/actions/workflows/ci.yml/badge.svg)](https://github.com/kylehughes/app-localizer/actions/workflows/ci.yml)
27
+
28
+ *Translate Xcode String Catalogs (`.xcstrings`) and Fastlane App Store metadata from the command line with any OpenAI-compatible chat completions endpoint.*
29
+
30
+ ## About
31
+
32
+ App Localizer is a development and CI tool for app repositories. It edits localization inputs in place, records what it generated, and skips unchanged work on later runs.
33
+
34
+ The output is a translation draft. It still belongs in code review, especially UI, legal text, purchase flows, and App Store copy. Do not ship App Localizer in an app target, call it from app code, or put provider API keys in mobile apps.
35
+
36
+ ### Supported Inputs
37
+
38
+ Two input types are supported, configured independently:
39
+
40
+ - **String Catalogs.** Fills missing or untranslated `.xcstrings` entries. Preserves placeholders, substitutions, variation trees, translator comments, and localizations outside the current run.
41
+ - **Fastlane metadata.** Translates `fastlane/metadata/<locale>/*.txt` App Store copy, copies URL fields verbatim, and enforces App Store character limits.
42
+
43
+ ## Quick Start
44
+
45
+ ### Installation
46
+
47
+ Install the CLI from PyPI:
48
+
49
+ ```sh
50
+ uv tool install app-localizer
51
+ ```
52
+
53
+ `pipx install app-localizer` and `python3 -m pip install app-localizer` work too. Pin a version for reproducible installs:
54
+
55
+ ```sh
56
+ uv tool install "app-localizer==1.0.1"
57
+ ```
58
+
59
+ ### Configuration
60
+
61
+ Add `app-localizer.toml` to the app repository root with the sections the repository needs:
62
+
63
+ ```toml
64
+ [xcstrings]
65
+ sources = ["App/Resources/Localizable.xcstrings"]
66
+ languages = ["de", "es", "fr", "ja"]
67
+
68
+ [fastlane]
69
+ metadata_path = "fastlane/metadata"
70
+ source_locale = "en-US"
71
+ locales = ["de-DE", "es-ES", "fr-FR", "ja"]
72
+ ```
73
+
74
+ Paths resolve relative to the config file, so the same config works from CI and local shells when `--config` points at it.
75
+
76
+ ### First Run
77
+
78
+ Validate the config and file structure without a provider API key:
79
+
80
+ ```sh
81
+ app-localizer --check
82
+ ```
83
+
84
+ Preview pending translation work without writing files:
85
+
86
+ ```sh
87
+ app-localizer --dry-run
88
+ ```
89
+
90
+ Translate configured inputs:
91
+
92
+ ```sh
93
+ export OPENAI_API_KEY=...
94
+ app-localizer
95
+ ```
96
+
97
+ A run rewrites catalogs and metadata in place, then updates `.app-localizer.lock` files. Commit the config, generated localization files, metadata, and lockfiles. Do not commit API keys or `app-localizer.secrets.toml`.
98
+
99
+ ## Command-Line Interface
100
+
101
+ ### Common Commands
102
+
103
+ Run these from the app repository root unless passing `--config`:
104
+
105
+ ```sh
106
+ # Validate config, catalogs, metadata, and existing generated files.
107
+ app-localizer --check
108
+
109
+ # Show pending work without network access or file writes.
110
+ app-localizer --dry-run
111
+
112
+ # Translate every configured domain.
113
+ app-localizer
114
+
115
+ # Process one configured domain.
116
+ app-localizer --domain fastlane
117
+
118
+ # Retranslate even values already marked translated or locked.
119
+ app-localizer --force
120
+ ```
121
+
122
+ `--check` and `--dry-run` do not require an API key. `--fail-on-untranslated` makes either command exit nonzero while work is pending. It is additive: a run can enable the policy, but cannot disable `fail_on_untranslated = true` set in the config.
123
+
124
+ ### Options
125
+
126
+ `--config` selects a config file. Without it, the CLI reads `app-localizer.toml` from the working directory when that file exists.
127
+
128
+ Provider overrides are run-specific: `--base-url`, `--model`, `--reasoning-effort`, `--temperature`, and `--candidates`. `--api-key` supplies a provider key directly and wins over environment variables and the secrets file. `--secrets` selects a different TOML secrets file.
129
+
130
+ `--verbose` enables debug logging. `--version` prints the installed package version. `app-localizer --help` prints the full parser output.
131
+
132
+ ### Ad Hoc Catalog Runs
133
+
134
+ For one-off String Catalog translation without a config file, pass catalog paths and target languages directly:
135
+
136
+ ```sh
137
+ app-localizer --source Localizable.xcstrings --languages de es fr
138
+ ```
139
+
140
+ Ad hoc runs configure only the String Catalog domain. Use a config file for repeatable app-team workflows.
141
+
142
+ ## Configuration Reference
143
+
144
+ All non-secret settings live in `app-localizer.toml`. Unknown keys are rejected, so typos fail early. See [examples/app-localizer.toml](examples/app-localizer.toml) for a copyable template.
145
+
146
+ ### Provider Settings
147
+
148
+ Provider settings are top-level and shared by both domains:
149
+
150
+ | Key | Default | Description |
151
+ | --- | --- | --- |
152
+ | `base_url` | `https://api.openai.com/v1` | OpenAI-compatible chat completions endpoint. |
153
+ | `api_key_env` | `OPENAI_API_KEY` | Environment variable holding the API key. Set to `""` for keyless local endpoints. |
154
+ | `model` | `gpt-5.2` | Model identifier to request from the endpoint. |
155
+ | `reasoning_effort` | unset | Optional reasoning effort: `none`, `minimal`, `low`, `medium`, `high`, or `xhigh`. Unsupported endpoints are retried without it for the rest of the run. |
156
+ | `temperature` | `0.2` | Sampling temperature for translation requests. |
157
+ | `candidates` | `3` | Candidate translations requested per value before the decider pass chooses one. |
158
+ | `fail_on_untranslated` | `false` | Makes `--check` and `--dry-run` fail while anything is pending. |
159
+
160
+ ### String Catalogs
161
+
162
+ `[xcstrings]` enables `.xcstrings` translation:
163
+
164
+ | Key | Description |
165
+ | --- | --- |
166
+ | `sources` | Catalog paths, resolved relative to the config file. |
167
+ | `languages` | Target Xcode language tags, for example `de`, `pt-BR`, `zh-Hans`. |
168
+
169
+ ### Fastlane Metadata
170
+
171
+ `[fastlane]` enables App Store metadata translation:
172
+
173
+ | Key | Description |
174
+ | --- | --- |
175
+ | `metadata_path` | Fastlane metadata directory, resolved relative to the config file. |
176
+ | `source_locale` | Locale directory to translate from, for example `en-US`. Fastlane's `default` fallback directory also works. |
177
+ | `locales` | Target App Store Connect locales, for example `de-DE`, `fr-FR`. Typos like `de` fail validation. |
178
+ | `keywords_trim_to_fit` | Defaults to `true`. Generated `keywords.txt` translations are normalized, deduplicated case-insensitively, and trimmed from the tail until the comma-joined list fits 100 characters. Set to `false` to reject over-limit keyword translations instead. |
179
+ | `allow_unknown_locales` | Defaults to `false`. Set to `true` to accept App Store locales added after this package's release instead of failing validation. Unknown locales warn; malformed tags still fail. |
180
+
181
+ ### Secrets
182
+
183
+ API keys do not belong in `app-localizer.toml`. Resolution order is `--api-key`, then the environment variable named by `api_key_env`, then the secrets file. `--secrets` selects a different file.
184
+
185
+ Use an environment variable for individual developer machines:
186
+
187
+ ```sh
188
+ export OPENAI_API_KEY=...
189
+ app-localizer
190
+ ```
191
+
192
+ Teams can share `app-localizer.secrets.toml` beside the config file. Each entry maps an environment variable name to its value; one file can hold keys for every provider a team uses:
193
+
194
+ ```toml
195
+ OPENAI_API_KEY = "sk-..."
196
+ OPENROUTER_API_KEY = "sk-or-..."
197
+ ```
198
+
199
+ Add the secrets file to the consuming repository's `.gitignore` and distribute it out of band. See [docs/ios-integration.md](docs/ios-integration.md#security-posture) for the full key-handling policy.
200
+
201
+ ### Provider Examples
202
+
203
+ Any OpenAI-compatible chat completions endpoint works. The default targets OpenAI and reads `OPENAI_API_KEY`.
204
+
205
+ OpenRouter:
206
+
207
+ ```toml
208
+ base_url = "https://openrouter.ai/api/v1"
209
+ api_key_env = "OPENROUTER_API_KEY"
210
+ model = "anthropic/claude-sonnet-4-6"
211
+ ```
212
+
213
+ Ollama:
214
+
215
+ ```toml
216
+ base_url = "http://localhost:11434/v1"
217
+ api_key_env = ""
218
+ model = "llama3.3"
219
+ ```
220
+
221
+ LM Studio:
222
+
223
+ ```toml
224
+ base_url = "http://localhost:1234/v1"
225
+ api_key_env = ""
226
+ model = "qwen2.5-32b-instruct"
227
+ ```
228
+
229
+ Apple Foundation Models has no direct HTTP API. Point `base_url` at a local OpenAI-compatible bridge and set `api_key_env = ""` if the bridge is keyless.
230
+
231
+ Leave `reasoning_effort` unset unless the selected model and endpoint accept it. When an endpoint reports the field as unknown or unsupported, the run warns once, retries without it, and omits it for the rest of that run.
232
+
233
+ ## Outputs
234
+
235
+ ### String Catalog Behavior
236
+
237
+ For each configured catalog and target language, a run:
238
+
239
+ - Adds missing target-language localizations from the source language.
240
+ - Skips entries marked with `shouldTranslate: false`.
241
+ - Translates `stringUnit` values, nested variation trees, and substitution variations (`%#@name@` / `%arg`).
242
+ - Adds the plural categories the target language's CLDR rules require, even when the source language omits them.
243
+ - Preserves substitution metadata such as `argNum` and `formatSpecifier`, and keeps it in sync with the source language.
244
+ - Preserves localizations for languages outside the current run.
245
+ - Copies whitespace-only values verbatim instead of sending them to the provider.
246
+
247
+ Catalog writes are atomic. The writer keeps file permissions, syncs contents to disk, sorts JSON keys, and uses Xcode-style spacing before replacing the original file. Catalogs with duplicate JSON keys or unsupported catalog versions are rejected.
248
+
249
+ ### Fastlane Metadata Behavior
250
+
251
+ Files in the source locale directory are handled by name:
252
+
253
+ | File | Handling | Limit |
254
+ | --- | --- | --- |
255
+ | `name.txt` | translated | 30 |
256
+ | `subtitle.txt` | translated | 30 |
257
+ | `description.txt` | translated | 4000 |
258
+ | `keywords.txt` | translated, kept comma-separated | 100 |
259
+ | `promotional_text.txt` | translated | 170 |
260
+ | `release_notes.txt` | translated | 4000 |
261
+ | `apple_tv_privacy_policy.txt` | translated | - |
262
+ | `marketing_url.txt`, `support_url.txt`, `privacy_url.txt` | copied verbatim, never sent to the provider | - |
263
+ | `review_information/` | ignored | - |
264
+
265
+ Non-localized metadata at the root of `metadata_path` (`copyright.txt`, category files, and `review_information/`) lives outside the locale directories and is never touched.
266
+
267
+ Unrecognized files are skipped with a warning. For prose metadata files, a translation over the App Store character limit is rejected like a placeholder mismatch. If no candidate fits, the file errors and the run fails.
268
+
269
+ Generated `keywords.txt` translations differ when `keywords_trim_to_fit = true`: spaces around keywords are stripped, case-insensitive duplicates are removed, and trailing keywords are dropped until the comma-joined list fits 100 characters. Dropped terms are logged in a warning. Set `keywords_trim_to_fit = false` to reject over-limit keyword translations instead. `--check` still fails when a source file exceeds its own limit.
270
+
271
+ Metadata `.txt` files have no per-value translation state, so skip decisions come from the lockfile's per-file source hashes. `--check` validates locked target files against App Store character limits. Other hand-edited target changes with an unchanged source are not detected as pending. Use `--force` to rewrite everything.
272
+
273
+ ### Lockfiles and Skip Behavior
274
+
275
+ `.app-localizer.lock` is the skip cache. The String Catalog domain writes one beside each catalog. The Fastlane domain writes one at the metadata root.
276
+
277
+ Commit lockfiles. Without them, later runs have less information and translate more than they need to.
278
+
279
+ String Catalog lockfile section:
280
+
281
+ ```json
282
+ {
283
+ "catalogs" : {
284
+ "Localizable.xcstrings" : {
285
+ "de" : {
286
+ "baseUrl" : "https://api.openai.com/v1",
287
+ "config" : "sha256...",
288
+ "hash" : "sha256...",
289
+ "model" : "gpt-5.2",
290
+ "timestamp" : "2026-06-11T00:00:00+00:00"
291
+ }
292
+ }
293
+ },
294
+ "generatedBy" : "app-localizer",
295
+ "version" : 1
296
+ }
297
+ ```
298
+
299
+ Fastlane lockfile section:
300
+
301
+ ```json
302
+ {
303
+ "fastlane" : {
304
+ "de-DE" : {
305
+ "marketing_url.txt" : { "config" : "verbatim", "hash" : "sha256...", "..." : "..." },
306
+ "name.txt" : {
307
+ "baseUrl" : "https://api.openai.com/v1",
308
+ "config" : "sha256...",
309
+ "hash" : "sha256...",
310
+ "model" : "gpt-5.2",
311
+ "timestamp" : "2026-06-11T00:00:00+00:00"
312
+ }
313
+ }
314
+ },
315
+ "generatedBy" : "app-localizer",
316
+ "version" : 1
317
+ }
318
+ ```
319
+
320
+ For catalogs, `hash` fingerprints the source-language content per catalog. A language is skipped only when the hashes match and no units are pending.
321
+
322
+ For metadata, `hash` fingerprints each source file. Skips are per file: a file is translated again when the target is missing, the source changed, or the configuration changed.
323
+
324
+ `config` fingerprints the endpoint, model, reasoning effort, temperature, candidate count, prompts, and domain-specific validation policy. For metadata, it also includes the character limit and keyword trim policy. `baseUrl` and `model` are informational fields that record what produced an entry.
325
+
326
+ Unreadable, unsupported, or foreign lockfiles are discarded with a warning and rebuilt on the next run. Sections a domain does not own are preserved.
327
+
328
+ ### Limits and Review
329
+
330
+ Translations are validated before writing:
331
+
332
+ - Catalog translations must preserve printf placeholders, Swift interpolation placeholders, and String Catalog substitution tokens.
333
+ - Fastlane metadata translations must fit App Store character limits.
334
+ - Config files must not contain unknown keys, duplicate sources or languages, malformed language tags, or invalid App Store locale codes.
335
+
336
+ Current limits:
337
+
338
+ - The catalog lockfile is catalog-level per language, not per key. Per-unit state lives in the catalog itself.
339
+ - Plural variations are expanded to the categories the target language's CLDR rules require (Russian `few` and `many`, for example), translated from the source's `other` form. Languages outside the built-in CLDR table fall back to mirroring the source's categories.
340
+ - Output is a draft. Require human review for high-risk UI, marketing copy, legal text, purchase flows, and short ambiguous strings.
341
+
342
+ ## Team Workflow
343
+
344
+ ### CI
345
+
346
+ Copy [examples/github-actions/app-localizer.yml](examples/github-actions/app-localizer.yml) into the app repository. Pull requests run `--check` with no API key. Manual workflow dispatch runs translation from a repository secret and opens a draft PR with the generated changes.
347
+
348
+ If the app repository needs credentials to install App Localizer, update the workflow install step to use the repository's git credential or package source.
349
+
350
+ For release branches, set `fail_on_untranslated = true` or pass `--fail-on-untranslated` so checks fail while configured targets still have untranslated content.
351
+
352
+ ### App-Team Workflow
353
+
354
+ See [docs/ios-integration.md](docs/ios-integration.md) for the recommended app-team workflow: API key handling, developer checks, CI translation PRs, review policy, and release-branch behavior.
355
+
356
+ Run translation before `fastlane deliver` so uploaded metadata is current.
357
+
358
+ ## Development
359
+
360
+ ### Local Development
361
+
362
+ Install the package editable and run the test suite from the repository root:
363
+
364
+ ```sh
365
+ python3 -m venv .venv
366
+ . .venv/bin/activate
367
+ python3 -m pip install -e .
368
+ PYTHONPATH=src python3 -m unittest discover -s tests
369
+ ```
370
+
371
+ CI runs the same suite on every push and pull request against the oldest and newest supported Python versions.
372
+
373
+ ### Releasing
374
+
375
+ Releases are plain git tags; there is no package registry.
376
+
377
+ 1. Update `version` in `pyproject.toml` and commit to `main`.
378
+ 2. Tag the same version with a leading `v`.
379
+ 3. Push `main` and the tag.
380
+
381
+ ```sh
382
+ git tag vX.Y.Z
383
+ git push origin main vX.Y.Z
384
+ ```
385
+
386
+ Pushing the tag runs the release workflow. It verifies the tag matches the package version, runs the test suite, builds the sdist and wheel, publishes to PyPI via trusted publishing, and publishes a GitHub Release with generated notes and the build files attached.
387
+
388
+ Consumers install a pinned release from PyPI:
389
+
390
+ ```sh
391
+ python3 -m pip install "app-localizer==X.Y.Z"
392
+ ```
393
+
394
+ ### Compatibility
395
+
396
+ Versioning follows semantic versioning as of 1.0.0. The stable surface is the CLI flag set, the `app-localizer.toml` config schema, and the `.app-localizer.lock` format. A major release is required to break any of them.
397
+
398
+ The default model (`gpt-5.2`) is not part of the stable surface and may change in a minor release; pin `model` in the config to control it. Translation output is model-defined and never guaranteed to be stable. The lockfile is a disposable cache: an unreadable or future-version lockfile is discarded and rebuilt, so its format can advance without a breaking change.
399
+
400
+ See [CHANGELOG.md](CHANGELOG.md) for release history.
401
+
402
+ ## Contributions
403
+
404
+ App Localizer is not accepting source contributions at this time. Bug reports will be considered.
405
+
406
+ ## Author
407
+
408
+ [Kyle Hughes](https://kylehugh.es)
409
+
410
+ [![Bluesky][bluesky_image]][bluesky_url]
411
+ [![LinkedIn][linkedin_image]][linkedin_url]
412
+ [![Mastodon][mastodon_image]][mastodon_url]
413
+
414
+ [bluesky_image]: https://img.shields.io/badge/Bluesky-0285FF?logo=bluesky&logoColor=fff
415
+ [bluesky_url]: https://bsky.app/profile/kylehugh.es
416
+ [linkedin_image]: https://img.shields.io/badge/LinkedIn-0A66C2?logo=linkedin&logoColor=fff
417
+ [linkedin_url]: https://www.linkedin.com/in/kyle-hughes
418
+ [mastodon_image]: https://img.shields.io/mastodon/follow/109356914477272810?domain=https%3A%2F%2Fmister.computer&style=social
419
+ [mastodon_url]: https://mister.computer/@kyle
420
+
421
+ ## License
422
+
423
+ App Localizer is available under the MIT license.
424
+
425
+ See `LICENSE` for details.