Compare commits

...

39 commits

Author SHA1 Message Date
Adam Brown
59a788cddd
New translations from Crowdin (#1014)
Some checks are pending
Build CI / build (push) Waiting to run
Build CI / static-analysis (push) Waiting to run
Build CI / android-instrumented-tests (push) Waiting to run
Build CI / iOS compile & test (push) Waiting to run
Build CI / iOS UI tests (push) Waiting to run
PublishInternal / publish-google-play (push) Waiting to run
2026-10-18 17:19:08 -07:00
Adam Brown
1ba66394ab
New Crowdin updates (#1013)
Some checks are pending
Build CI / build (push) Waiting to run
Build CI / static-analysis (push) Waiting to run
Build CI / android-instrumented-tests (push) Waiting to run
Build CI / iOS compile & test (push) Waiting to run
Build CI / iOS UI tests (push) Waiting to run
PublishInternal / publish-google-play (push) Waiting to run
* New translations from Crowdin

* New translations from Crowdin

* New translations from Crowdin

* New translations from Crowdin

* New translations from Crowdin

* New translations from Crowdin

* New translations from Crowdin

* New translations from Crowdin

* New translations from Crowdin
2026-10-18 00:40:53 -07:00
Adam Brown
c896edf379
Delete projects off the UI thread (#1011)
The delete dialog called deleteProject from its click handler, so the
metadata read, recursive delete and sync data write all blocked the UI
thread. Run them on the IO dispatcher.

---------

Co-authored-by: Sean Li <22557931+sean0407@users.noreply.github.com>
2026-10-17 23:34:11 -07:00
Adam Brown
073921c22b
New Crowdin updates (#998)
* New translations from Crowdin

* New translations from Crowdin

* New translations from Crowdin

* New translations from Crowdin

* New translations from Crowdin

* New translations from Crowdin

* New translations from Crowdin

* New translations from Crowdin

* New translations from Crowdin

* New translations from Crowdin

* New translations from Crowdin
2026-10-17 17:36:51 -07:00
Adam Brown
78060dfe88
Fix spell check in the Linux snap (#1003)
* Give the snap libhunspell and dictionaries so spell check works

The strict snap sees core24's /usr, which has neither, so spell check never
loaded a dictionary. Stage libhunspell, mount the hunspell-dictionaries
content snap over /usr/share/hunspell, and point JNA at the staged library.

* Test that markdown editors underline misspellings in loaded text

* Point the snap's spell check at its dictionaries through DICPATH

platform-spellcheckerkt 1.4.0 searches DICPATH, so the /usr/share/hunspell
layout is no longer needed.
2026-10-17 17:36:38 -07:00
Adam Brown
05b1608bd4
Drop the scene editors' manual spell check after loading content (#1004)
SpellCheckingTextEditor re-checks on whole-document replacement since
ComposeTextEditor 2.4.7, so the extra full check only repeated that work.
2026-10-17 17:06:23 -07:00
Adam Brown
db2131cab5
Rename wear pairing strings to _android.xml and drop [ci skip] from Crowdin commits (#996) 2026-10-14 21:39:13 -07:00
Adam Brown
daa86badef
Escape apostrophe in French wear pairing string (#994)
Some checks failed
Build CI / build (push) Has been cancelled
PublishInternal / publish-google-play (push) Has been cancelled
Build CI / static-analysis (push) Has been cancelled
Build CI / android-instrumented-tests (push) Has been cancelled
Build CI / iOS compile & test (push) Has been cancelled
Build CI / iOS UI tests (push) Has been cancelled
2026-10-12 16:00:20 -07:00
Adam Brown
9013716d0d
New Crowdin updates (#993)
* New translations strings.xml (French)

[ci skip]

* New translations strings_wear_pairing.xml (French)

[ci skip]

* New translations messages_en.properties (French)

[ci skip]

* New translations strings_scene_editor.xml (French)

[ci skip]

* New translations strings_scene_list.xml (French)

[ci skip]

* New translations strings_reader_mode.xml (French)

[ci skip]

* New translations strings_account_settings.xml (French)

[ci skip]

* New translations strings_project_home.xml (French)

[ci skip]

* New translations strings_about_app.xml (French)

[ci skip]

* New translations strings_encyclopedia.xml (French)

[ci skip]

* New translations strings_sync.xml (French)

[ci skip]

* New translations full_description.txt (French)

[ci skip]
2026-10-11 21:35:50 -07:00
Adam Brown
f7605f984d
ComposeTextEditor 3.1.0, and format bar fixes (#992)
* Format bar: a quote that saves, toggles as the editor has them, and disabled states

The quote button toggles the editor's quote block, written as "> ", where its inline
style exported as nothing. Bold, italic and strikethrough use the editor's toggle and
show active only when the whole selection has the style. In a table cell the block
buttons are disabled and the rule button does nothing; undo and redo show disabled
when there is nothing to undo or redo.
2026-10-11 21:10:57 -07:00
Adam Brown
81424d94ec
Fix invisible pagination button text on hover (#990)
Some checks failed
Build CI / build (push) Has been cancelled
Build CI / static-analysis (push) Has been cancelled
Build CI / android-instrumented-tests (push) Has been cancelled
Build CI / iOS compile & test (push) Has been cancelled
Build CI / iOS UI tests (push) Has been cancelled
PublishInternal / publish-google-play (push) Has been cancelled
base.css fills hovered pagination buttons with the accent color, which the
authors and feed outline styles didn't override. Disabled buttons also picked
up the filled hover.
2026-10-05 18:47:43 -07:00
Adam Brown
83f9497937
Find update digest sidecars in artifact subdirectories (#989)
Some checks are pending
Build CI / build (push) Waiting to run
Build CI / static-analysis (push) Waiting to run
Build CI / android-instrumented-tests (push) Waiting to run
Build CI / iOS compile & test (push) Waiting to run
Build CI / iOS UI tests (push) Waiting to run
PublishInternal / publish-google-play (push) Waiting to run
upload-artifact keeps each installer's format directory, so writeUpdateMetadata
missed every sidecar and failed the v3.12.0 release.
2026-10-05 18:23:16 -07:00
Adam Brown
366e5b6a6f Prepared for release: v3.12.0
Some checks are pending
Build CI / build (push) Waiting to run
Build CI / static-analysis (push) Waiting to run
Build CI / android-instrumented-tests (push) Waiting to run
Build CI / iOS compile & test (push) Waiting to run
Build CI / iOS UI tests (push) Waiting to run
PublishInternal / publish-google-play (push) Waiting to run
2026-10-05 08:21:53 -07:00
Adam Brown
66e9920306
Keep unsaved scene buffers when the project scope closes (#983)
* Keep unsaved scene buffers when the project scope closes

Scope close deleted every temp buffer, so an Android activity destroyed in the background lost unsaved edits. Only clean buffers are cleared now, Discard discards explicitly, export reads unsaved buffers, and the Android back handler observes dirty state.

* Discard the buffer of an archived or deleted scene without reloading

* Save the buffer of an archived or deleted scene without throwing

* Drop a scene's unsaved buffer and temp file when it is deleted
2026-10-05 08:15:52 -07:00
Adam Brown
beab5e4271
New Crowdin updates (#972)
* Update source file Messages_en.properties

[ci skip]

* New translations messages_en.properties (French)

[ci skip]

* New translations messages_en.properties (Spanish)

[ci skip]

* New translations messages_en.properties (German)

[ci skip]

* New translations messages_en.properties (Italian)

[ci skip]

* New translations messages_en.properties (Ukrainian)

[ci skip]

* New translations messages_en.properties (Portuguese, Brazilian)

[ci skip]

* New translations messages_en.properties (Turkish)

[ci skip]

* New translations strings.xml (French)

[ci skip]

* New translations strings.xml (Spanish)

[ci skip]

* New translations strings.xml (German)

[ci skip]

* New translations strings.xml (Italian)

[ci skip]

* New translations strings.xml (Ukrainian)

[ci skip]

* New translations strings.xml (Portuguese, Brazilian)

[ci skip]

* New translations strings.xml (Turkish)

[ci skip]

* Update source file strings.xml

[ci skip]

* New translations strings_scene_editor.xml (French)

[ci skip]

* New translations strings_scene_editor.xml (Spanish)

[ci skip]

* New translations strings_scene_editor.xml (German)

[ci skip]

* New translations strings_scene_editor.xml (Italian)

[ci skip]

* New translations strings_scene_editor.xml (Ukrainian)

[ci skip]

* New translations strings_scene_editor.xml (Portuguese, Brazilian)

[ci skip]

* New translations strings_scene_list.xml (Italian)

[ci skip]

* New translations strings_scene_editor.xml (Turkish)

[ci skip]

* New translations strings_scene_list.xml (Turkish)

[ci skip]

* New translations strings_scene_list.xml (French)

[ci skip]

* New translations strings_reader_mode.xml (French)

[ci skip]

* New translations strings_scene_list.xml (Spanish)

[ci skip]

* New translations strings_reader_mode.xml (Spanish)

[ci skip]

* New translations strings_scene_list.xml (German)

[ci skip]

* New translations strings_reader_mode.xml (German)

[ci skip]

* New translations strings_reader_mode.xml (Italian)

[ci skip]

* New translations strings_reader_mode.xml (Turkish)

[ci skip]

* New translations strings_scene_list.xml (Ukrainian)

[ci skip]

* New translations strings_reader_mode.xml (Ukrainian)

[ci skip]

* New translations strings_scene_list.xml (Portuguese, Brazilian)

[ci skip]

* New translations strings_reader_mode.xml (Portuguese, Brazilian)

[ci skip]

* Update source file strings_reader_mode.xml

[ci skip]

* Update source file strings_scene_editor.xml

[ci skip]

* Update source file strings_scene_list.xml

[ci skip]

* New translations strings_account_settings.xml (Italian)

[ci skip]

* New translations strings_account_settings.xml (Turkish)

[ci skip]

* New translations strings_account_settings.xml (French)

[ci skip]

* New translations strings_account_settings.xml (Spanish)

[ci skip]

* New translations strings_account_settings.xml (German)

[ci skip]

* New translations strings_account_settings.xml (Ukrainian)

[ci skip]

* New translations strings_account_settings.xml (Portuguese, Brazilian)

[ci skip]

* Update source file strings_account_settings.xml

[ci skip]

* New translations strings_project_home.xml (Italian)

[ci skip]

* New translations strings_project_home.xml (Turkish)

[ci skip]

* New translations strings_project_home.xml (French)

[ci skip]

* New translations strings_project_home.xml (Spanish)

[ci skip]

* New translations strings_project_home.xml (German)

[ci skip]

* New translations strings_project_home.xml (Ukrainian)

[ci skip]

* New translations strings_project_home.xml (Portuguese, Brazilian)

[ci skip]

* Update source file strings_account_settings.xml

[ci skip]

* Update source file strings_project_home.xml

[ci skip]

* New translations strings_account_settings.xml (Italian)

[ci skip]

* New translations strings_account_settings.xml (Turkish)

[ci skip]

* New translations strings_account_settings.xml (French)

[ci skip]

* New translations strings_account_settings.xml (Spanish)

[ci skip]

* New translations strings_account_settings.xml (German)

[ci skip]

* New translations strings_account_settings.xml (Ukrainian)

[ci skip]

* New translations strings_account_settings.xml (Portuguese, Brazilian)

[ci skip]

* Update source file strings_about_app.xml

[ci skip]

* Update source file strings_account_settings.xml

[ci skip]

* New translations strings_about_app.xml (Italian)

[ci skip]

* New translations strings_account_settings.xml (Italian)

[ci skip]

* New translations strings_about_app.xml (Turkish)

[ci skip]

* New translations strings_account_settings.xml (Turkish)

[ci skip]

* New translations strings_account_settings.xml (French)

[ci skip]

* New translations strings_account_settings.xml (Spanish)

[ci skip]

* New translations strings_account_settings.xml (German)

[ci skip]

* New translations strings_account_settings.xml (Ukrainian)

[ci skip]

* New translations strings_account_settings.xml (Portuguese, Brazilian)

[ci skip]

* New translations strings_about_app.xml (French)

[ci skip]

* New translations strings_about_app.xml (Spanish)

[ci skip]

* New translations strings_about_app.xml (German)

[ci skip]

* New translations strings_about_app.xml (Ukrainian)

[ci skip]

* New translations strings_about_app.xml (Portuguese, Brazilian)

[ci skip]
2026-10-05 08:11:46 -07:00
Adam Brown
a90b72d291
Gate sync behind the unsaved-work dialogs and make Discard really discard (#987)
* Gate sync behind the unsaved-work dialogs and make Discard really discard

Sync flushed every dirty scene buffer in prepareForSync, so an automatic sync on
close committed scenes the user had just chosen to discard, and manual syncs
silently saved everything. startProjectSync now builds the same confirmation set
as requestClose (scenes, notes, encyclopedia, timeline) and ends in Sync instead
of Complete, so the existing dialogs run before the sync modal opens.

The Discard buttons previously only cleared the confirmation; they now revert the
scene buffers to disk and drop in-progress entity edits (or pop a create screen)
through a new Router.discardUnsaved hook.

* Close the remaining ungated sync paths and harden discard

Route the Home screen's Sync action through startProjectSync so it hits the
gate, ignore startProjectSync while a close request is pending, and stop the
scene synchronizer from persisting dirty buffers in prepareForSync and
finalizeSync: upload content and hashes already come from disk, and every
interactive sync is now confirmed up front, so the flush only ever committed
buffers nobody was asked about (crash-recovery buffers during the startup
account sync, for one).

ProjectRoot.discardUnsaved is now a suspend call that reverts scene buffers off
the main thread, and the entity components check and discard across their
whole stack rather than only the active destination.
2026-10-05 08:10:43 -07:00
Adam Brown
1dfc2bf663
ComposeTextEditor 3.0.3 (#985) 2026-10-05 08:09:43 -07:00
Adam Brown
ff622e82d8
Refresh open views when their entity changes underneath them (#984)
* Refresh open views when their entity changes underneath them

Note, timeline event, encyclopedia entry and scene metadata views loaded
once and went stale after sync. They now follow their repository flows,
keeping an in-progress edit and only moving its baseline.

* Address review findings on open-view refresh

- An untouched open edit follows the synced change; only a dirty edit keeps its draft
- Entry reloads are serialized, survive I/O errors, and only run for the open entry
- A note deleted elsewhere closes its view unless a dirty edit is open
- Synced scene metadata is no longer marked dirty or written back by the panel
- Sync keeps the local draft name; reference use cases name their update source

* Catch specific exceptions in the entry refresh
2026-10-05 08:09:05 -07:00
Adam Brown
28b4df9b4a
Offer in-app updates on the GitHub desktop builds (#978)
Some checks are pending
Build CI / build (push) Waiting to run
Build CI / static-analysis (push) Waiting to run
Build CI / android-instrumented-tests (push) Waiting to run
Build CI / iOS compile & test (push) Waiting to run
Build CI / iOS UI tests (push) Waiting to run
PublishInternal / publish-google-play (push) Waiting to run
* Offer in-app updates on the GitHub desktop builds

AppUpdater in :common with a no-op for every store build, and a Nucleus
backed NucleusAppUpdater in :desktop that checks shortly after launch and
every six hours while the new automaticUpdateChecks setting allows it. One
action, update(), downloads, verifies and hands the installer over; MSI,
EXE, DEB, RPM and AppImage installs relaunch on the new version, macOS
installs are told and sent to the release page until the signed-zip work
lands. A notice strip above the project list, a status row on the About
screen, and a settings toggle, each with previews and render tests.

* Keep an offered update on screen while it is re-checked

A check only shows Checking when nothing better is known, so the six-hourly
re-check no longer blinks the banner away. The updater's shared fields are
volatile, the banner and the About row share one state-to-text mapping,
the download test hands the installer over through a CompletableDeferred,
and the desktop platform settings component gets a test for the new toggle.

* Lay the update notice on the page and allow an environment override

The banner now renders inside the project list under the masthead, inset
to the page gutter, instead of as a strip above the whole content pane
butting against the nav rail. HAMMER_UPDATE_BASE_URL stands in for the
system property so an installed build can be pointed at a local server,
which is how the update from a 3.10.99 test build to the published 3.11.0
MSI was exercised end to end. The download test cleans up its staging
directory, as the real installer script does.

* Give the Android project selection preview fake the update members
2026-10-05 03:09:55 -07:00
Adam Brown
645225e3ac
Pin the Kotlin 2.4.20 fix for responds in value position instead of expecting the old cast (#988) 2026-10-05 01:39:01 -07:00
Adam Brown
de23cbec43
Update the compose-preview plugin to 2.34.0 so previews render against Skiko 0.150 (#986) 2026-10-04 23:26:10 -07:00
Adam Brown
385a6f28d2
Fix backup restore nesting the project inside itself (#982)
* Fix backup restore nesting the project inside itself

Backup archives are rooted at the project's directory name, so unzipping
into the project directory left it without a project.toml at its root and
the project vanished. Restore now extracts to a staging directory and
swaps the unwrapped project into place, leaving the existing project
untouched if extraction fails.

The Backup Manager also lists backups of projects that no longer exist,
and reports a failed restore.

* Harden backup restore swap and missing-project listing

Move the existing project aside instead of deleting it so a failed swap
puts it back, serialize restores that share the staging directory, and
list a missing project once when it has both legacy and current backup
names.
2026-10-04 23:11:57 -07:00
Adam Brown
2b4b1698b3 Version bumps 2026-10-04 22:54:10 -07:00
Adam Brown
4f2004431f
Stamp builds with their package format and publish update metadata (#977)
* Stamp builds with their package format and publish update metadata

-Pformat=<token> is resolved in buildSrc, baked into BuildMetadata, and
printed in the startup banner next to the channel; every packaging step
passes its own. The MSI UpgradeCode is pinned to the value every installed
copy already carries. writeUpdateMetadata renders latest*.yml from the
attached installers and the release workflow uploads them to the draft
before it publishes. HammerReleaseProvider points the Nucleus updater at
releases/latest/download so partial-release tags cannot break downloads.

* Hash installers where they are built instead of re-downloading them

Each platform job now writes a <installer>.sha512 sidecar beside what it
attaches and hands the sidecars to the final job as a workflow artifact.
That job assembles latest*.yml from them and attaches the yml with the same
release action every other job uses, so nothing is downloaded back from the
draft and nothing depends on looking a draft release up by tag.
2026-10-04 22:45:07 -07:00
Adam Brown
56e6e82b82
Fix PDF export cutting off lines at page breaks (#981)
pdfkmp cannot split rich text, rows or decorated containers across
pages, so prose is wrapped into lines up front and laid out one node
per line.
2026-10-04 22:36:24 -07:00
Adam Brown
215fbf6537
Add export options for chapter numbering, font and blank lines (#980)
Three new options in the Export Story dialog, from user feedback on Word, EPUB and PDF exports.

- Number chapter titles: off by default for every format. On prefixes each title with its position in the headings and the Contents page. Word, RTF, PDF and Markdown used to number always; EPUB never did and now can.
- Font: Georgia, Times New Roman, Garamond, Arial, Verdana or Courier New for Word, RTF and EPUB. PDF keeps its built-in face.
- Keep blank lines between paragraphs: on by default. Off drops a lone blank line so the indent is the only separator; a run of two or more, and the boundary between scenes, still leaves one break.
2026-10-04 22:17:45 -07:00
Adam Brown
2ad0322067
Derive the Mac App Store build mode from the distribution channel (#975)
* Derive the Mac App Store build mode from the distribution channel

The App Store build was detected two ways: Gradle read -PmacOsAppStoreRelease
for packaging and passed -Dhammer.app.store=true so IS_APP_STORE could gate
runtime behaviour. The channel enum already knows the same fact from
-Pchannel=mac-app-store, which build-appstore.sh passed alongside.

isAppStoreRelease now resolves from the channel token, the JVM property and
IS_APP_STORE are gone, and the three runtime readers (sandbox startup, the
bookmark-backed file access binding, the in-app changelog gate) branch on
DistributionChannel.current like every other per-vehicle rule.

* Share the channel resolver across build scripts and name the App Store check

Project.distributionChannel() in buildSrc owns the -Pchannel and F-Droid
detection, so :base and :desktop cannot disagree on the channel.
DistributionChannel.isMacAppStore replaces three spelled-out comparisons,
and supportsInAppChangelog is a pure function of the channel with a test
pinning the table. Docs corrected: section 1 is done, GitHubProvider only
uses the REST API for pre-release channels, and the App Store build also
needs -PbuildNumber.
2026-10-04 22:11:24 -07:00
Adam Brown
ba28b01b82
Add OLED black theme (#979)
Dark theme with a pure black background and base surface. Applies to project color themes and Android dynamic color too.

Offer the OLED theme on Android and iOS only
2026-10-04 17:04:36 -07:00
Adam Brown
d884591bf6
Add Reader Mode (#976)
Some checks are pending
Build CI / build (push) Waiting to run
Build CI / static-analysis (push) Waiting to run
Build CI / android-instrumented-tests (push) Waiting to run
Build CI / iOS compile & test (push) Waiting to run
Build CI / iOS UI tests (push) Waiting to run
PublishInternal / publish-google-play (push) Waiting to run
Read the live story as prose, one chapter at a time, from the story
editor details pane. Opens from the scene list footer, the scene editor
top bar, or the editor menu.

- Scenes in a chapter are stitched together with an ornament between
  them; a toggle shows scene headings instead.
- While the reader is open the scene list works as a table of contents,
  and its highlight follows the scene being read.
- Reader font size is its own setting. Text is selectable.
- Chapter and scene position survive rotation and process death.

Chapter collection is now shared with export, and the outline's chapter
pieces move into the design system. MarkdownView gains opt-in
importDuringComposition and isSelectable. The footer's outline button
uses a new short "Outline" string.
2026-10-03 23:59:24 -07:00
Adam Brown
ca22ec262d
ComposeTextEditor 3.0.2 and a render-time editor text size (#973)
Some checks are pending
Build CI / build (push) Waiting to run
Build CI / static-analysis (push) Waiting to run
Build CI / android-instrumented-tests (push) Waiting to run
Build CI / iOS compile & test (push) Waiting to run
Build CI / iOS UI tests (push) Waiting to run
PublishInternal / publish-google-play (push) Waiting to run
* Update ComposeTextEditor to 3.0.2

Styles move to TextEditorState.richTextStyles, headings use the block API so
they export as headings, and shortcuts match on layoutKey. Markdown pins
ParagraphSeparator.NEWLINE to keep the 2.8 on-disk format.

* Make the editor text size a render property

Rich text styles are a fixed set with em-relative headings, so the body size
lives on the editor text style and changing it rewrites no document. The
size steps through a ladder, shows its value in the format bar menu, and
answers Ctrl+Plus, Minus and 0.
2026-10-03 13:36:53 -07:00
Adam Brown
5d363ee7c4
Add a "Surprised me" kudos reaction (#971)
Some checks are pending
Build CI / build (push) Waiting to run
Build CI / static-analysis (push) Waiting to run
Build CI / android-instrumented-tests (push) Waiting to run
Build CI / iOS compile & test (push) Waiting to run
Build CI / iOS UI tests (push) Waiting to run
PublishInternal / publish-google-play (push) Waiting to run
2026-10-02 21:47:14 -07:00
Adam Brown
4edff36e95
New Crowdin updates (#968)
* New translations messages_en.properties (Turkish)

[ci skip]

* New translations strings.xml (Turkish)

[ci skip]

* New translations strings_about_app.xml (Turkish)

[ci skip]

* New translations strings_account_settings.xml (Turkish)

[ci skip]

* New translations strings_desktop.xml (Turkish)

[ci skip]

* New translations strings_drafts.xml (Turkish)

[ci skip]

* New translations strings_encyclopedia.xml (Turkish)

[ci skip]

* New translations strings_notes.xml (Turkish)

[ci skip]

* New translations strings_project_home.xml (Turkish)

[ci skip]

* New translations strings_project_navigation.xml (Turkish)

[ci skip]

* New translations strings_project_select_navigation.xml (Turkish)

[ci skip]

* New translations strings_projects_list.xml (Turkish)

[ci skip]

* New translations strings_scene_editor.xml (Turkish)

[ci skip]

* New translations strings_scene_list.xml (Turkish)

[ci skip]

* New translations strings_sync.xml (Turkish)

[ci skip]

* New translations strings_timeline.xml (Turkish)

[ci skip]

* New translations strings_android.xml (Turkish)

[ci skip]

* New translations full_description.txt (Turkish)

[ci skip]

* New translations short_description.txt (Turkish)

[ci skip]

* New translations title.txt (Turkish)

[ci skip]

* New translations strings_globalsearch.xml (Turkish)

[ci skip]

* New translations strings_ideas.xml (Turkish)

[ci skip]

* New translations strings_wear_pairing.xml (Turkish)

[ci skip]
2026-09-29 23:24:46 -07:00
Adam Brown
569b6c0297
New Crowdin updates (#965)
* New translations strings_about_app.xml (Italian)

[ci skip]

* New translations messages_en.properties (Italian)

[ci skip]

* New translations strings_wear_pairing.xml (Italian)

[ci skip]

* New translations full_description.txt (Italian)

[ci skip]

* New translations strings_project_home.xml (Italian)

[ci skip]

* New translations strings_project_navigation.xml (Italian)

[ci skip]

* New translations strings_android.xml (Italian)

[ci skip]
2026-09-27 21:04:01 -07:00
Adam Brown
1cdcdad657
Recover from an undecryptable auth token keyset instead of crashing (#960)
Some checks failed
Build CI / build (push) Has been cancelled
Build CI / static-analysis (push) Has been cancelled
Build CI / android-instrumented-tests (push) Has been cancelled
Build CI / iOS compile & test (push) Has been cancelled
Build CI / iOS UI tests (push) Has been cancelled
PublishInternal / publish-google-play (push) Has been cancelled
* Recover from an undecryptable auth token keyset instead of crashing

Exclude the encrypted token prefs from backups, and attach the R8 mappings
to GitHub releases.

* Reset the Keystore master key along with the token prefs
2026-09-27 17:43:25 -07:00
Adam Brown
13b078f26c
Add core-splashscreen to the Android phone app (#964) 2026-09-27 17:41:58 -07:00
Adam Brown
a39b1e7fd4
Update ComposeTextEditor to 2.8.0 for Compose 1.12 IME APIs (#961)
2.6.0 was built against Compose 1.11 and lacks the IME members 1.12 made
abstract, so focusing an editor on iOS threw IrLinkageError on
ImeComposeStateAdapter.text and killed the app.
2026-09-27 16:22:06 -07:00
Wavesonics
3f038d836f
Fix keyboard options on non-prose text fields
Some checks failed
Build CI / build (push) Has been cancelled
Build CI / static-analysis (push) Has been cancelled
Build CI / android-instrumented-tests (push) Has been cancelled
Build CI / iOS compile & test (push) Has been cancelled
Build CI / iOS UI tests (push) Has been cancelled
PublishInternal / publish-google-play (push) Has been cancelled
URL, numeric, tag, word-list, and delete-confirmation fields inherited
sentence capitalization and autocorrect, which inserted a space after
'.' in server URLs.
2026-09-25 23:52:26 -07:00
Adam Brown
1a433d78f9
New Crowdin updates (#955)
* Update source file full_description.txt

[ci skip]

* New translations full_description.txt (French)

[ci skip]

* New translations full_description.txt (Spanish)

[ci skip]

* New translations full_description.txt (German)

[ci skip]

* New translations full_description.txt (Italian)

[ci skip]

* New translations full_description.txt (Ukrainian)

[ci skip]

* New translations full_description.txt (Portuguese, Brazilian)

[ci skip]

* New translations full_description.txt (French)

[ci skip]

* New translations full_description.txt (Spanish)

[ci skip]

* New translations full_description.txt (German)

[ci skip]

* New translations full_description.txt (Italian)

[ci skip]

* New translations full_description.txt (Ukrainian)

[ci skip]

* New translations full_description.txt (Portuguese, Brazilian)

[ci skip]

* Update source file full_description.txt

[ci skip]

* New translations messages_en.properties (French)

[ci skip]

* New translations messages_en.properties (Spanish)

[ci skip]

* New translations messages_en.properties (German)

[ci skip]

* New translations messages_en.properties (Italian)

[ci skip]

* New translations messages_en.properties (Ukrainian)

[ci skip]

* New translations messages_en.properties (Portuguese, Brazilian)

[ci skip]

* Update source file Messages_en.properties

[ci skip]
2026-09-25 23:40:09 -07:00
Wavesonics
04de488fb0
Treat any-script letters, marks, and digits as word characters in reference matching
Names no longer match inside words containing accented or non-Latin letters ("Ana" in "Anaïs", "Иван" in "Иванов").
2026-09-25 17:51:38 -07:00
351 changed files with 11913 additions and 1464 deletions

View file

@ -72,7 +72,7 @@ jobs:
timeout_minutes: 30
max_attempts: 3
shell: pwsh
command: ./gradlew :desktop:packageReleaseMsi -Pchannel=github
command: ./gradlew :desktop:packageReleaseMsi -Pchannel=github -Pformat=msi
- name: Rename MSI Artifact
shell: cmd
@ -104,7 +104,7 @@ jobs:
timeout_minutes: 30
max_attempts: 3
shell: pwsh
command: ./gradlew :desktop:packageReleaseExe -Pchannel=github
command: ./gradlew :desktop:packageReleaseExe -Pchannel=github -Pformat=exe
- name: Rename EXE Artifact
shell: cmd
@ -130,6 +130,17 @@ jobs:
echo Successfully renamed file to "hammer.exe"
- name: Write update digests
shell: pwsh
run: ./gradlew writeAssetDigests "-PassetFiles=desktop/build/installers/main-release/msi/hammer.msi,desktop/build/installers/main-release/exe/hammer.exe"
- name: Upload update digests
uses: actions/upload-artifact@v7
with:
name: update-digests-windows
path: desktop/build/installers/main-release/*/hammer.*.sha512
if-no-files-found: error
- name: Create Release
uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1.21.0
env:
@ -192,7 +203,7 @@ jobs:
with:
timeout_minutes: 30
max_attempts: 3
command: ./gradlew :desktop:packageReleaseDeb -Pchannel=github
command: ./gradlew :desktop:packageReleaseDeb -Pchannel=github -Pformat=deb
- name: Rename DEB Artifact
run: |
@ -216,7 +227,7 @@ jobs:
with:
timeout_minutes: 30
max_attempts: 3
command: ./gradlew :desktop:packageReleaseRpm -Pchannel=github
command: ./gradlew :desktop:packageReleaseRpm -Pchannel=github -Pformat=rpm
- name: Rename RPM Artifact
run: |
@ -240,7 +251,17 @@ jobs:
with:
timeout_minutes: 30
max_attempts: 3
command: ./gradlew buildDistAppImage -Pchannel=appimage
command: ./gradlew buildDistAppImage -Pchannel=appimage -Pformat=appimage
- name: Write update digests
run: ./gradlew writeAssetDigests "-PassetFiles=desktop/build/installers/main-release/deb/hammer.deb,desktop/build/installers/main-release/rpm/hammer.rpm,desktop/build/installers/main-release/appimage/hammer.AppImage"
- name: Upload update digests
uses: actions/upload-artifact@v7
with:
name: update-digests-linux
path: desktop/build/installers/main-release/*/hammer.*.sha512
if-no-files-found: error
- name: Create Release
uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1.21.0
@ -283,7 +304,7 @@ jobs:
with:
timeout_minutes: 30
max_attempts: 3
command: ./gradlew :desktop:packageReleaseDmg -Pchannel=github
command: ./gradlew :desktop:packageReleaseDmg -Pchannel=github -Pformat=dmg
- name: Rename DMG Artifact
run: |
@ -307,7 +328,7 @@ jobs:
with:
timeout_minutes: 30
max_attempts: 3
command: ./gradlew :desktop:packageReleasePkg -Pchannel=github
command: ./gradlew :desktop:packageReleasePkg -Pchannel=github -Pformat=pkg
- name: Rename PKG Artifact
run: |
@ -328,6 +349,16 @@ jobs:
echo "Contents of pkg directory:"
ls -la
- name: Write update digests
run: ./gradlew writeAssetDigests "-PassetFiles=desktop/build/installers/main-release/dmg/hammer.dmg,desktop/build/installers/main-release/pkg/hammer.pkg"
- name: Upload update digests
uses: actions/upload-artifact@v7
with:
name: update-digests-macos
path: desktop/build/installers/main-release/*/hammer.*.sha512
if-no-files-found: error
- name: Create Release
uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1.21.0
env:
@ -465,6 +496,9 @@ jobs:
echo "File renamed successfully."
- name: Rename R8 Mapping
run: cp "$GITHUB_WORKSPACE/android/build/outputs/mapping/release/mapping.txt" "$GITHUB_WORKSPACE/android/build/outputs/apk/release/hammer-apk-mapping.txt"
- name: Create Release
uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1.21.0
env:
@ -474,7 +508,7 @@ jobs:
prerelease: true
artifactErrorsFailBuild: true
allowUpdates: true
artifacts: "${{ github.workspace }}/android/build/outputs/apk/release/hammer.apk"
artifacts: "${{ github.workspace }}/android/build/outputs/apk/release/hammer.apk,${{ github.workspace }}/android/build/outputs/apk/release/hammer-apk-mapping.txt"
snap:
needs: tests
@ -591,6 +625,47 @@ jobs:
- name: Fetch tags
run: git fetch --depth=1 origin +refs/tags/*:refs/tags/*
- name: Set up JDK
uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: '21'
- name: Gradle caching
uses: actions/cache@v6
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
# The desktop updater reads latest*.yml from the release. Each platform job hashed
# the installer it attached and handed the digest over as a workflow artifact, so
# nothing is downloaded back and no release lookup is needed. Attached before the
# draft flips, so a published release never exists without them.
- name: Collect update digests
uses: actions/download-artifact@v7
with:
pattern: update-digests-*
merge-multiple: true
path: ${{ runner.temp }}/update-digests
- name: Write update metadata
run: ./gradlew writeUpdateMetadata "-PreleaseDigestsDir=${{ runner.temp }}/update-digests" --no-daemon
- name: Attach update metadata
uses: ncipollo/release-action@339a81892b84b4eeb0f6e744e4574d79d0d9b8dd # v1.21.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
draft: true
prerelease: true
artifactErrorsFailBuild: true
allowUpdates: true
artifacts: "${{ github.workspace }}/build/update-metadata/latest.yml,${{ github.workspace }}/build/update-metadata/latest-mac.yml,${{ github.workspace }}/build/update-metadata/latest-linux.yml"
- name: Get last tag message
run: |
TAG_NAME=${GITHUB_REF#refs/tags/}

View file

@ -89,3 +89,15 @@ jobs:
esac
echo "Running fastlane $LANE for track=$TRACK"
bundle exec fastlane "$LANE"
# Keeps the mappings for the exact Play builds next to the release, so prod
# crash traces can be retraced without the Play Console.
- name: Attach R8 mappings to the GitHub release
if: github.event_name == 'release'
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ github.event.release.tag_name }}
run: |
cp android/build/outputs/mapping/release/mapping.txt hammer-google-play-mapping.txt
cp wear/build/outputs/mapping/release/mapping.txt hammer-wear-mapping.txt
gh release upload "$TAG" hammer-google-play-mapping.txt hammer-wear-mapping.txt --clobber

View file

@ -27,7 +27,7 @@ jobs:
cache: gradle
- name: Build MSIX package
run: ./gradlew :desktop:packageMsix -Pchannel=ms-store
run: ./gradlew :desktop:packageMsix -Pchannel=ms-store -Pformat=msix
- name: Upload Gradle problems report on failure
if: failure()

View file

@ -11,6 +11,8 @@ jobs:
publish-google-play:
if: ${{ !contains(github.event.release.tag_name, '+') || contains(github.event.release.tag_name, '+google-play') }}
uses: ./.github/workflows/publish-google-play.yml
permissions:
contents: write
secrets: inherit
with:
track: production

View file

@ -1,5 +1,39 @@
# Changelog
## [3.12.0] - 2026-10-5
New features
- Reader Mode: read your story as prose, one chapter at a time, instead of through the scene editor. Open it from the Read button under the scene list, the book icon in the scene editor, or the editor menu. The scene list becomes a table of contents, and the reader has its own font size
- Export: three new options in the Export Story dialog. Number chapter titles, pick a font (Word, RTF and EPUB), and choose whether to keep blank lines between paragraphs
- Android and iOS: an OLED theme, the dark theme on a pure black background
- Desktop (Windows and Linux installs from GitHub): Hammer tells you when a newer release is out and can update itself from the project list or the About screen. macOS DMG installs are told and sent to the release page. Automatic checks can be turned off in Settings
- Text editor 3.0: Huge improvements in input and performance
- Text editor: change the text size with Ctrl/Cmd and Plus, Minus or 0, or from the format bar's overflow menu
- Text editor: Delete in the right-click menu
- Turkish translation
- Web: a fifth reader reaction on public stories, "Surprised me"
Improvements
- Syncing with unsaved work now asks Save, Discard or Cancel first, where before it silently saved every unsaved scene
- Open notes, timeline events, encyclopedia entries and the scene title, outline and notes refresh when a sync or conflict resolution changes them. An edit in progress keeps its draft
- The Backup Manager lists backups of projects that are no longer on disk, so a deleted project can be restored
- Exports no longer number chapter titles unless you turn the new option on. Word, RTF, PDF and Markdown used to number them always
- Text editor: the heading button writes real Markdown headings, and Hammer's shortcuts land on the same keys on non-QWERTY layouts
- Android: a launch splash screen on every supported version
- Translations and dependencies updated
Fixes
- Android: unsaved scene edits could be lost when the app was closed in the background. They now come back as unsaved scenes the next time the project opens [#983]
- Export: unsaved scene edits were left out of the exported file [#983]
- Restoring a backup from the Backup Manager made the project disappear from the project list
- Choosing Discard on close with automatic sync on still saved and uploaded the discarded scenes
- PDF export: long paragraphs ran off the bottom of the page or left large blank gaps
- Android: a crash on launch when the saved login could not be decrypted, for example after restoring the device from a backup. You are asked to sign in again
- iOS: a crash when tapping into a text editor
- Server URL, number, tag and word-list fields no longer autocorrect or capitalize, which put a space after each "." in a server URL
- Encyclopedia references no longer match a name inside a longer word with accented or non-Latin letters ("Ana" in "Anaïs")
## [3.11.0] - 2026-9-24
New features

View file

@ -174,6 +174,19 @@ enum on purpose: every channel-conditional branch is a greppable `when` on the t
Capability flags (`canSelfUpdate`, `isSandboxed`, …) can be introduced later, from real
duplication rather than a guess at the axes.
The build side reads the same token through `Project.distributionChannel()` in buildSrc:
`desktop/build.gradle.kts` derives its Mac App Store packaging mode (`isAppStoreRelease`:
sandbox entitlements, provisioning profiles, signing) from `-Pchannel=mac-app-store`. Together
with `-PbuildNumber=N`, that is everything `build-appstore.sh` passes.
Packaging runs carry a second stamp, `-Pformat=<token>` (`msi`, `exe`, `deb`, `rpm`, `dmg`,
`pkg`, `appimage`, `msix`, `snap`, `flatpak`), read the same way into
`BuildMetadata.PACKAGE_FORMAT` and `PackageFormat.current`. The channel says which vehicle a
build is for; the format says which installer it came out of, which is what the desktop
updater needs to pick its download. Omitting it gives `none`, an unknown token fails the
build, and each release step passes its own. Two packaging tasks in one Gradle invocation
would both carry the first task's format, so the release workflow runs one per step.
Note that every branch compiles into every build, so a store-forbidden code path is
physically present in a store binary even when unreachable. If a store ever objects to
presence rather than behaviour, promote that one case to a real source set rather than

View file

@ -40,6 +40,7 @@ dependencies {
implementation(libs.work.runtime.ktx)
implementation(libs.material)
implementation(libs.appcompat)
implementation(libs.core.splashscreen)
implementation(libs.multiplatform.settings)
androidTestImplementation(libs.androidx.junit)

View file

@ -69,7 +69,7 @@ class WearPairingActivity : ComponentActivity(), KoinComponent {
setContent {
val isDark = when (settings.uiTheme) {
UiTheme.Light -> false
UiTheme.Dark -> true
UiTheme.Dark, UiTheme.OledBlack -> true
UiTheme.FollowSystem -> isSystemInDarkTheme()
}

View file

@ -23,6 +23,7 @@
<activity
android:name=".ProjectSelectActivity"
android:theme="@style/Theme.Hammer.Starting"
android:exported="true"
android:windowSoftInputMode="adjustResize">
<intent-filter>

View file

@ -109,7 +109,7 @@ class ProjectRootActivity : AppCompatActivity() {
val settingsState by globalSettings.subscribeAsState()
val isDark = when (settingsState.uiTheme) {
UiTheme.Light -> false
UiTheme.Dark -> true
UiTheme.Dark, UiTheme.OledBlack -> true
UiTheme.FollowSystem -> isSystemInDarkTheme()
}
@ -220,7 +220,8 @@ class ProjectRootActivity : AppCompatActivity() {
// Only intercept back when at Home AND there's potential unsaved work to confirm
// Otherwise, let Android's default back behavior close the activity
BackHandler(enabled = backEnabled && (component.hasUnsavedBuffers() || shouldConfirmClose.isNotEmpty())) {
val unsavedBuffers by component.unsavedBuffers.subscribeAsState()
BackHandler(enabled = backEnabled && (unsavedBuffers || shouldConfirmClose.isNotEmpty())) {
component.requestClose()
}

View file

@ -14,6 +14,7 @@ import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.runtime.ExperimentalComposeApi
import androidx.compose.runtime.getValue
import androidx.compose.ui.platform.LocalContext
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.lifecycleScope
import com.arkivanov.decompose.extensions.compose.subscribeAsState
import com.arkivanov.decompose.retainedComponent
@ -43,6 +44,7 @@ class ProjectSelectActivity : AppCompatActivity() {
private var settingsUpdateJob: Job? = null
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
super.onCreate(savedInstanceState)
enableEdgeToEdge()
@ -61,7 +63,7 @@ class ProjectSelectActivity : AppCompatActivity() {
val settingsState by globalSettings.subscribeAsState()
val isDark = when (settingsState.uiTheme) {
UiTheme.Light -> false
UiTheme.Dark -> true
UiTheme.Dark, UiTheme.OledBlack -> true
UiTheme.FollowSystem -> isSystemInDarkTheme()
}

View file

@ -18,6 +18,7 @@ import com.darkrockstudios.apps.hammer.common.components.projectselection.projec
import com.darkrockstudios.apps.hammer.common.components.storyeditor.metadata.ProjectMetadata
import com.darkrockstudios.apps.hammer.base.http.storyideas.StoryIdea
import com.darkrockstudios.apps.hammer.common.data.ImportOptions
import com.darkrockstudios.apps.hammer.common.data.appupdate.AppUpdateState
import com.darkrockstudios.apps.hammer.common.data.Msg
import com.darkrockstudios.apps.hammer.common.data.ProjectDef
import com.darkrockstudios.apps.hammer.common.fileio.HPath
@ -95,6 +96,8 @@ val component = object : ProjectSelection {
override val changelog: Value<ProjectSelection.ChangelogState> =
MutableValue(ProjectSelection.ChangelogState())
override val appUpdate: Value<AppUpdateState> = MutableValue(AppUpdateState.Unsupported)
override fun isAtRoot() = false
override fun onBack() {}
override fun showLocation(location: ProjectSelection.Locations) {}
@ -102,6 +105,8 @@ val component = object : ProjectSelection {
override fun openLatestRelease() {}
override fun dismissChangelog() {}
override fun showChangelog() {}
override fun updateApp() {}
override fun dismissUpdate() {}
override val backHandler = dummyBackHandler
}

View file

@ -97,7 +97,7 @@ class AddNoteActivity : ComponentActivity(), KoinComponent {
val settingsState by globalSettings.subscribeAsState()
val isDark = when (settingsState.uiTheme) {
UiTheme.Light -> false
UiTheme.Dark -> true
UiTheme.Dark, UiTheme.OledBlack -> true
UiTheme.FollowSystem -> isSystemInDarkTheme()
}

View file

@ -90,7 +90,7 @@ class AddNoteWidgetConfigActivity : ComponentActivity() {
val settingsState by globalSettings.subscribeAsState()
val isDark = when (settingsState.uiTheme) {
UiTheme.Light -> false
UiTheme.Dark -> true
UiTheme.Dark, UiTheme.OledBlack -> true
UiTheme.FollowSystem -> isSystemInDarkTheme()
}

View file

@ -88,7 +88,7 @@ class StoryInfoWidgetConfigActivity : ComponentActivity() {
val settingsState by globalSettings.subscribeAsState()
val isDark = when (settingsState.uiTheme) {
UiTheme.Light -> false
UiTheme.Dark -> true
UiTheme.Dark, UiTheme.OledBlack -> true
UiTheme.FollowSystem -> isSystemInDarkTheme()
}

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="wear_pairing_masthead">APPAIRER LA MONTRE</string>
<string name="wear_pairing_title">Se connecter à %1$s sur Hammer ?</string>
<string name="wear_pairing_body">La montre sera synchronisée en tant que %1$s avec sa propre session. Vous pouvez la déconnecter depuis la montre à tout moment.</string>
<string name="wear_pairing_not_signed_in">Connectez-vous au serveur sur ce smartphone en premier, puis appairez la montre de nouveau.</string>
<string name="wear_pairing_approve">Appairer</string>
<string name="wear_pairing_decline">Refuser</string>
<string name="wear_pairing_close">Fermer</string>
<string name="wear_pairing_toast_success">Montre appairée</string>
<string name="wear_pairing_toast_failure">La montre n\'a pas pu être appairée</string>
<string name="wear_pairing_channel_name">Appairage de la montre</string>
<string name="wear_pairing_notification_title">Appairez votre montre</string>
<string name="wear_pairing_notification_text">Tapoter pour que votre montre se synchronise avec votre compte Hammer</string>
</resources>

View file

@ -46,5 +46,3 @@
<string name="story_info_widget_strip_words_unit">P</string>
<string name="story_info_widget_continue_label">CONTINUA ·</string>
</resources>

View file

@ -1,15 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="wear_pairing_masthead">PAIR WATCH</string>
<string name="wear_pairing_title">Sign in %1$s to Hammer?</string>
<string name="wear_pairing_body">The watch will sync as %1$s with a session of its own. You can sign it out from the watch at any time.</string>
<string name="wear_pairing_not_signed_in">Sign in to a sync server on this phone first, then pair the watch again.</string>
<string name="wear_pairing_approve">Pair</string>
<string name="wear_pairing_decline">Rifiuta</string>
<string name="wear_pairing_close">Chiudi</string>
<string name="wear_pairing_toast_success">Watch paired</string>
<string name="wear_pairing_toast_failure">The watch could not be paired</string>
<string name="wear_pairing_channel_name">Watch pairing</string>
<string name="wear_pairing_notification_title">Pair your watch</string>
<string name="wear_pairing_notification_text">Tap to let your watch sync with your Hammer account</string>
</resources>

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="wear_pairing_masthead">ASSOCIA OROLOGIO</string>
<string name="wear_pairing_title">Accedere come %1$s su Hammer?</string>
<string name="wear_pairing_body">L’orologio si sincronizzerà come %1$s con una propria sessione. Puoi disconnetterlo dall’orologio in qualsiasi momento.</string>
<string name="wear_pairing_not_signed_in">Accedi prima a un server di sincronizzazione su questo telefono, poi associa nuovamente l’orologio.</string>
<string name="wear_pairing_approve">Associa</string>
<string name="wear_pairing_decline">Rifiuta</string>
<string name="wear_pairing_close">Chiudi</string>
<string name="wear_pairing_toast_success">Orologio associato</string>
<string name="wear_pairing_toast_failure">Impossibile associare l’orologio</string>
<string name="wear_pairing_channel_name">Associazione orologio</string>
<string name="wear_pairing_notification_title">Associa il tuo orologio</string>
<string name="wear_pairing_notification_text">Tocca per consentire all’orologio di sincronizzarsi con il tuo account Hammer</string>
</resources>

View file

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Hammer</string>
<string name="title_activity_add_note">Not Ekle</string>
<string name="loading">Yükleniyor…</string>
<string name="title_activity_configure_note_widget">Widget\'ı Yapılandır</string>
<string name="overflow_menu_button">Diğer Menüler</string>
<string name="navdrawer_button">Gezinti Çekmecesi</string>
<string name="note_widget_dialog_title">Notu Şuraya Ekle:</string>
<string name="note_widget_dialog_save_button">Kaydet</string>
<string name="note_widget_dialog_cancel_button">İptal</string>
<string name="note_widget_dialog_projects_dropdown">Projeler</string>
<string name="note_widget_dialog_failure_bad_project">Proje bulunamadı: %1$s</string>
<string name="note_widget_confirm_cancel_title">Nottan Vazgeçilsin mi?</string>
<string name="note_widget_confirm_cancel_positive">Vazgeç</string>
<string name="note_widget_confirm_cancel_negative">İptal</string>
<string name="note_widget_button_description">Not Ekle</string>
<string name="note_widget_toast_no_projects">Proje yok</string>
<string name="note_widget_toast_success">Not Eklendi</string>
<string name="action_add_note_short_label">Not Ekle</string>
<string name="action_add_note_long_label">Hikâye Notu Ekle</string>
<string name="action_add_note_disabled">Not ekleme şu anda devre dışı</string>
<string name="title_activity_add_note_widget_config">Widget\'ı Yapılandır</string>
<string name="note_widget_config_title">Not Widget\'ını yapılandır:</string>
<string name="note_widget_config_project_checkbox">Notları belirtilen projeye ekle</string>
<string name="note_widget_config_project_checkbox_explained">Eğer seçili bir proje yoksa her seferinde bir proje seçmen istenir.</string>
<string name="stories_list_widget_description">Hammer · Kütüphane — bir hikâyeyi aç</string>
<string name="story_info_widget_description">Hammer · Hikâye — bir proje için özet istatistikler</string>
<string name="story_info_widget_config_title">Hikâye Bilgisi Widget\'ı:</string>
<string name="title_activity_story_info_widget_config">Hikâye Bilgisi Widget\'ını Yapılandır</string>
<string name="story_info_widget_unconfigured">Yapılandırmak için dokun</string>
<string name="story_info_widget_folio_header">§ %1$s · HİKÂYE</string>
<string name="story_info_widget_byline">- %1$s</string>
<string name="story_info_widget_words_label">WORDS</string>
<string name="story_info_widget_goal_summary">%1$d%% OF %2$s %3$s</string>
<string name="story_info_widget_cadence_today">TODAY</string>
<string name="story_info_widget_cadence_this_week">THIS WEEK</string>
<string name="story_info_widget_today_words">+%1$d TODAY</string>
<string name="story_info_widget_no_session_today">NO SESSION TODAY</string>
<string name="story_info_widget_streak">%1$dD STREAK</string>
<string name="story_info_widget_sessions_header">SESSIONS · 7D</string>
<string name="story_info_widget_week_total">%1$s W</string>
<string name="story_info_widget_action_open">OPEN</string>
<string name="story_info_widget_action_note">NOTE</string>
<string name="story_info_widget_strip_folio">§ %1$s</string>
<string name="story_info_widget_strip_words_unit">W</string>
<string name="story_info_widget_continue_label">CONTINUE ·</string>
</resources>

View file

@ -5,8 +5,8 @@
<string name="wear_pairing_body">The watch will sync as %1$s with a session of its own. You can sign it out from the watch at any time.</string>
<string name="wear_pairing_not_signed_in">Sign in to a sync server on this phone first, then pair the watch again.</string>
<string name="wear_pairing_approve">Pair</string>
<string name="wear_pairing_decline">Refuser</string>
<string name="wear_pairing_close">Fermer</string>
<string name="wear_pairing_decline">Decline</string>
<string name="wear_pairing_close">Close</string>
<string name="wear_pairing_toast_success">Watch paired</string>
<string name="wear_pairing_toast_failure">The watch could not be paired</string>
<string name="wear_pairing_channel_name">Watch pairing</string>

View file

@ -2,4 +2,10 @@
<resources>
<style name="Theme.Hammer" parent="Theme.AppCompat.DayNight.NoActionBar">
</style>
</resources>
<style name="Theme.Hammer.Starting" parent="Theme.SplashScreen">
<item name="windowSplashScreenBackground">@color/ic_launcher_background</item>
<item name="windowSplashScreenAnimatedIcon">@mipmap/ic_launcher_foreground</item>
<item name="postSplashScreenTheme">@style/Theme.Hammer</item>
</style>
</resources>

View file

@ -3,4 +3,5 @@
<exclude domain="file" path="HammerProjects/server.json"/>
<exclude domain="file" path="HammerProjects/sync.json"/>
<exclude domain="file" path="HammerProjects/.backups"/>
<exclude domain="sharedpref" path="hammer_auth_tokens.xml"/>
</full-backup-content>

View file

@ -4,10 +4,12 @@
<exclude domain="file" path="HammerProjects/server.json"/>
<exclude domain="file" path="HammerProjects/sync.json"/>
<exclude domain="file" path="HammerProjects/.backups"/>
<exclude domain="sharedpref" path="hammer_auth_tokens.xml"/>
</cloud-backup>
<device-transfer>
<exclude domain="file" path="HammerProjects/server.json"/>
<exclude domain="file" path="HammerProjects/sync.json"/>
<exclude domain="file" path="HammerProjects/.backups"/>
<exclude domain="sharedpref" path="hammer_auth_tokens.xml"/>
</device-transfer>
</data-extraction-rules>

View file

@ -1,4 +1,5 @@
import com.darkrockstudios.build.resolveDistributionChannel
import com.darkrockstudios.build.distributionChannel
import com.darkrockstudios.build.packageFormat
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
@ -78,12 +79,8 @@ buildConfig {
buildConfigField("String", "APP_VERSION", "\"${libs.versions.app.get()}\"")
buildConfigField("String", "DATA_VERSION", "\"${libs.versions.data.version.get()}\"")
// Mirrors the F-Droid detection in settings.gradle.kts so a plain F-Droid build reports its
// channel without every call site having to pass -Pchannel too.
val isFDroid = project.findProperty("fdroid")?.toString()?.isNotEmpty() == true ||
System.getenv("FDROID_BUILD") != null
val channel = resolveDistributionChannel(project.findProperty("channel")?.toString(), isFDroid)
buildConfigField("String", "CHANNEL", "\"${channel.token}\"")
buildConfigField("String", "CHANNEL", "\"${project.distributionChannel().token}\"")
buildConfigField("String", "PACKAGE_FORMAT", "\"${project.packageFormat().token}\"")
}
val GIT_TASK_NAME = "install-git-hooks"

View file

@ -37,5 +37,9 @@ enum class DistributionChannel(val token: String, val displayName: String) {
*/
val current: DistributionChannel =
entries.firstOrNull { it.token == BuildMetadata.CHANNEL } ?: DEV
/** The sandboxed, App Review facing desktop build. */
val isMacAppStore: Boolean
get() = current == MAC_APP_STORE
}
}

View file

@ -0,0 +1,30 @@
package com.darkrockstudios.apps.hammer.base
/**
* The installer format this build was packaged as, baked in at build time from `-Pformat=<token>`.
* The second axis next to [DistributionChannel]: the channel says which vehicle the build is for
* and whether it may update itself, the format says which installer artifact it came from. Dev
* runs and anything built without a packaging task report [NONE].
*
* Kept in step with the build-side enum in `buildSrc/src/main/java/packageFormat.kt`.
*/
enum class PackageFormat(val token: String) {
NONE("none"),
MSI("msi"),
EXE("exe"),
DEB("deb"),
RPM("rpm"),
DMG("dmg"),
PKG("pkg"),
APPIMAGE("appimage"),
MSIX("msix"),
SNAP("snap"),
FLATPAK("flatpak"),
;
companion object {
/** An unrecognised token falls back to [NONE] for the same reason [DistributionChannel.current] falls back to DEV. */
val current: PackageFormat =
entries.firstOrNull { it.token == BuildMetadata.PACKAGE_FORMAT } ?: NONE
}
}

View file

@ -32,16 +32,18 @@ object ProseHtml {
/**
* The generating providers [flavour] would use, with prose layout applied. Pass the result to
* [HtmlGenerator].
* [HtmlGenerator]. With [keepBlankLines] off, a lone blank line between paragraphs produces
* nothing and a run of two or more produces a single break.
*/
fun providers(
flavour: MarkdownFlavourDescriptor,
linkMap: LinkMap,
keepBlankLines: Boolean = true,
): Map<IElementType, GeneratingProvider> =
flavour.createHtmlGeneratingProviders(linkMap, null) + mapOf(
MarkdownElementTypes.PARAGRAPH to ProseParagraphProvider,
MarkdownElementTypes.MARKDOWN_FILE to ProseContainerProvider("body"),
MarkdownElementTypes.BLOCK_QUOTE to ProseContainerProvider("blockquote"),
MarkdownElementTypes.MARKDOWN_FILE to ProseContainerProvider("body", keepBlankLines),
MarkdownElementTypes.BLOCK_QUOTE to ProseContainerProvider("blockquote", keepBlankLines),
)
/**
@ -100,7 +102,10 @@ private object ProseParagraphProvider : TrimmingInlineHolderProvider() {
}
/** Emits a break for each blank line the author left between two paragraphs. */
private class ProseContainerProvider(tag: String) : SimpleTagProvider(tag) {
private class ProseContainerProvider(
tag: String,
private val keepBlankLines: Boolean,
) : SimpleTagProvider(tag) {
override fun processNode(
visitor: HtmlGenerator.HtmlGeneratingVisitor,
text: String,
@ -119,7 +124,12 @@ private class ProseContainerProvider(tag: String) : SimpleTagProvider(tag) {
else -> {
val isParagraph = child.type == MarkdownElementTypes.PARAGRAPH
if (afterParagraph && isParagraph) {
val breaks = (newlines - 1).coerceIn(0, ProseHtml.MAX_CONSECUTIVE_BREAKS)
val blanks = (newlines - 1).coerceIn(0, ProseHtml.MAX_CONSECUTIVE_BREAKS)
val breaks = when {
keepBlankLines -> blanks
blanks >= 2 -> 1
else -> 0
}
repeat(breaks) { visitor.consumeHtml(BREAK) }
}
child.accept(visitor)

View file

@ -0,0 +1,34 @@
package com.darkrockstudios.apps.hammer.base
import kotlin.test.Test
import kotlin.test.assertEquals
class PackageFormatTest {
/**
* Pins the tokens against the build-side enum in `buildSrc/src/main/java/packageFormat.kt`,
* the same way [DistributionChannelTest] pins the channel tokens.
*/
@Test
fun `tokens match the build side enum`() {
val expected = listOf(
"none",
"msi",
"exe",
"deb",
"rpm",
"dmg",
"pkg",
"appimage",
"msix",
"snap",
"flatpak",
)
assertEquals(expected, PackageFormat.entries.map { it.token })
}
@Test
fun `current resolves the baked in token`() {
assertEquals(BuildMetadata.PACKAGE_FORMAT, PackageFormat.current.token)
}
}

View file

@ -7,6 +7,7 @@ import com.darkrockstudios.build.formatStoreNotes
import com.darkrockstudios.build.isPlatformReleaseTag
import com.darkrockstudios.build.registerLinuxDistributionTasks
import com.darkrockstudios.build.registerPublishTasks
import com.darkrockstudios.build.registerUpdateMetadataTasks
import com.darkrockstudios.build.releaseNotesUrl
import com.darkrockstudios.build.storeNotesLength
import com.darkrockstudios.build.updateFlatpakFiles
@ -128,6 +129,7 @@ kover {
registerPublishTasks()
registerLinuxDistributionTasks(libs.versions.app.get())
registerUpdateMetadataTasks(libs.versions.app.get())
// The pre-commit hook runs Gradle, so git's hook env leaks into the daemon and Gradle
// blanks rather than unsets it later; a blank GIT_INDEX_FILE reads as an empty index.

View file

@ -1,5 +1,7 @@
package com.darkrockstudios.build
import org.gradle.api.Project
/**
* Build-side mirror of the runtime `DistributionChannel` enum in `:base`. The two lists have to
* stay in step; `DistributionChannelTest` in `:base` pins the token strings so a change to one
@ -43,3 +45,13 @@ fun resolveDistributionChannel(
DistributionChannel.entries.joinToString(", ") { it.token }
)
}
/**
* The channel this build is for. The F-Droid detection mirrors settings.gradle.kts, which cannot
* see buildSrc, so a plain F-Droid build reports its channel without passing -Pchannel too.
*/
fun Project.distributionChannel(): DistributionChannel {
val isFDroid = findProperty("fdroid")?.toString()?.isNotEmpty() == true ||
System.getenv("FDROID_BUILD") != null
return resolveDistributionChannel(findProperty("channel")?.toString(), isFDroid)
}

View file

@ -0,0 +1,45 @@
package com.darkrockstudios.build
import org.gradle.api.Project
/**
* Build-side mirror of the runtime `PackageFormat` enum in `:base`. The two lists have to stay
* in step; `PackageFormatTest` in `:base` pins the token strings so a change to one without the
* other fails a test rather than shipping a build that reports the wrong format.
*/
enum class PackageFormat(val token: String) {
NONE("none"),
MSI("msi"),
EXE("exe"),
DEB("deb"),
RPM("rpm"),
DMG("dmg"),
PKG("pkg"),
APPIMAGE("appimage"),
MSIX("msix"),
SNAP("snap"),
FLATPAK("flatpak"),
;
companion object {
fun fromToken(token: String): PackageFormat? = entries.firstOrNull { it.token == token }
}
}
/**
* Resolves the installer format for this build from `-Pformat=<token>`, defaulting to [NONE]
* for anything that is not a packaging run. Throws on an unknown token, so a typo in a release
* workflow fails the build instead of quietly shipping an installer that cannot name itself.
*/
fun resolvePackageFormat(formatProperty: String?): PackageFormat {
val requested = formatProperty?.trim()?.takeIf { it.isNotEmpty() } ?: return PackageFormat.NONE
return PackageFormat.fromToken(requested)
?: error(
"Unknown -Pformat=$requested. Valid formats: " +
PackageFormat.entries.joinToString(", ") { it.token }
)
}
/** The installer format this build is packaged as, from `-Pformat`. */
fun Project.packageFormat(): PackageFormat = resolvePackageFormat(findProperty("format")?.toString())

View file

@ -0,0 +1,145 @@
package com.darkrockstudios.build
import org.gradle.api.Project
import java.io.File
import java.security.MessageDigest
import java.time.Instant
import java.time.temporal.ChronoUnit
import java.util.Base64
/** One attached installer as the update metadata describes it. */
data class UpdateAsset(val name: String, val size: Long, val sha512: String)
/**
* The metadata files the desktop updater reads, each listing its platform's installers in
* preference order. The format is electron-builder's `latest.yml`, which Nucleus parses; the
* `url` is the bare asset name and the client prefixes the release URL.
*/
private val UPDATE_METADATA_FILES: Map<String, List<String>> = mapOf(
"latest.yml" to listOf("hammer.msi", "hammer.exe"),
"latest-mac.yml" to listOf("hammer.zip", "hammer.dmg", "hammer.pkg"),
"latest-linux.yml" to listOf("hammer.deb", "hammer.rpm", "hammer.AppImage"),
)
/** Assets a release may legitimately lack; every other listed name must have a digest. */
private val OPTIONAL_ASSETS: Set<String> = setOf("hammer.zip")
/** Suffix of the sidecar each platform job writes next to its installer: `<base64 sha512> <bytes>`. */
const val DIGEST_SUFFIX = ".sha512"
private const val SHA512_BUFFER_SIZE = 1 shl 16
fun renderUpdateMetadata(version: String, releaseDate: String, assets: List<UpdateAsset>): String =
buildString {
appendLine("version: $version")
appendLine("releaseDate: $releaseDate")
appendLine("files:")
for (asset in assets) {
appendLine(" - url: ${asset.name}")
appendLine(" sha512: ${asset.sha512}")
appendLine(" size: ${asset.size}")
}
}
/** Base64 of the SHA-512 digest, which is how Nucleus's checksum verifier compares it. */
fun sha512Base64(file: File): String {
val digest = MessageDigest.getInstance("SHA-512")
file.inputStream().use { input ->
val buffer = ByteArray(SHA512_BUFFER_SIZE)
while (true) {
val read = input.read(buffer)
if (read < 0) break
digest.update(buffer, 0, read)
}
}
return Base64.getEncoder().encodeToString(digest.digest())
}
/** The one-line sidecar content for an installer. */
fun assetDigestLine(file: File): String = "${sha512Base64(file)} ${file.length()}"
fun parseAssetDigest(name: String, line: String): UpdateAsset {
val parts = line.trim().split(' ')
require(parts.size == 2) { "Digest for $name should be '<base64 sha512> <bytes>', got '$line'" }
val size = parts[1].toLongOrNull() ?: error("Digest for $name has a non-numeric size: '$line'")
return UpdateAsset(name = name, size = size, sha512 = parts[0])
}
/**
* Builds every metadata file from the digest sidecars anywhere under [digestsDir], keyed by file
* name: upload-artifact keeps each installer's format subdirectory. A required installer with no
* sidecar fails loudly: a yml that omits it would silently leave that format with no update path.
*/
fun updateMetadataFiles(digestsDir: File, version: String, releaseDate: String): Map<String, String> {
val sidecars = digestsDir.walk()
.filter { it.isFile && it.name.endsWith(DIGEST_SUFFIX) }
.groupBy { it.name }
.mapValues { (name, found) ->
found.singleOrNull() ?: error("$digestsDir has more than one $name: $found")
}
return UPDATE_METADATA_FILES.mapValues { (metadataFile, names) ->
val assets = names.mapNotNull { name ->
val sidecar = sidecars[name + DIGEST_SUFFIX]
when {
sidecar != null -> parseAssetDigest(name, sidecar.readText())
name in OPTIONAL_ASSETS -> null
else -> error("$metadataFile needs $name$DIGEST_SUFFIX, but $digestsDir has no such file")
}
}
renderUpdateMetadata(version, releaseDate, assets)
}
}
/**
* Registers the two halves of the update metadata:
*
* - `writeAssetDigests -PassetFiles=<path,path>` runs in each platform job right after it
* packages and renames its installers, writing `<installer>.sha512` beside each one. The
* sidecars travel to the final job as a workflow artifact, so no installer is downloaded
* twice and no release lookup is needed.
* - `writeUpdateMetadata -PreleaseDigestsDir=<dir>` runs once in the final job over the
* collected sidecars and writes the yml files to `build/update-metadata/`.
*/
fun Project.registerUpdateMetadataTasks(appVersion: String) {
tasks.register("writeAssetDigests") {
group = "distribution"
description = "Writes a <installer>.sha512 sidecar beside each installer in -PassetFiles (comma separated)."
doLast {
val files = findProperty("assetFiles")?.toString()
?.split(',')
?.map { it.trim() }
?.filter { it.isNotEmpty() }
?.takeIf { it.isNotEmpty() }
?: error("Pass -PassetFiles=<installer>[,<installer>...]")
for (path in files) {
val file = File(path)
check(file.isFile) { "No installer at $path" }
val sidecar = File(file.path + DIGEST_SUFFIX)
sidecar.writeText(assetDigestLine(file))
println("Wrote $sidecar")
}
}
}
tasks.register("writeUpdateMetadata") {
group = "distribution"
description = "Writes the latest*.yml files the desktop updater reads, from the digest sidecars in -PreleaseDigestsDir."
doLast {
val digestsDir = File(
findProperty("releaseDigestsDir")?.toString()
?: error("Pass -PreleaseDigestsDir=<directory holding the *$DIGEST_SUFFIX sidecars>")
)
val outputDir = layout.buildDirectory.dir("update-metadata").get().asFile
outputDir.mkdirs()
val releaseDate = Instant.now().truncatedTo(ChronoUnit.SECONDS).toString()
for ((name, content) in updateMetadataFiles(digestsDir, appVersion, releaseDate)) {
val target = outputDir.resolve(name)
target.writeText(content)
println("Wrote $target")
}
}
}
}

View file

@ -0,0 +1,34 @@
import com.darkrockstudios.build.PackageFormat
import com.darkrockstudios.build.resolvePackageFormat
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
class PackageFormatTest {
@Test
fun `no format property means not packaged`() {
assertEquals(PackageFormat.NONE, resolvePackageFormat(null))
assertEquals(PackageFormat.NONE, resolvePackageFormat(""))
assertEquals(PackageFormat.NONE, resolvePackageFormat(" "))
}
@Test
fun `every token resolves to its own format`() {
for (format in PackageFormat.entries) {
assertEquals(format, resolvePackageFormat(format.token))
}
}
@Test
fun `an unknown token fails the build`() {
val error = assertFailsWith<IllegalStateException> { resolvePackageFormat("windows") }
assertEquals(true, error.message?.contains("Unknown -Pformat=windows"))
}
@Test
fun `tokens are unique`() {
val tokens = PackageFormat.entries.map { it.token }
assertEquals(tokens.toSet().size, tokens.size)
}
}

View file

@ -0,0 +1,140 @@
import com.darkrockstudios.build.DIGEST_SUFFIX
import com.darkrockstudios.build.UpdateAsset
import com.darkrockstudios.build.assetDigestLine
import com.darkrockstudios.build.parseAssetDigest
import com.darkrockstudios.build.renderUpdateMetadata
import com.darkrockstudios.build.sha512Base64
import com.darkrockstudios.build.updateMetadataFiles
import java.io.File
import java.nio.file.Files
import java.security.MessageDigest
import java.util.Base64
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertNull
class UpdateMetadataTest {
private val installers = listOf(
"hammer.msi", "hammer.exe", "hammer.dmg", "hammer.pkg", "hammer.deb", "hammer.rpm", "hammer.AppImage",
)
@Test
fun `renders the electron-builder layout Nucleus parses`() {
val yml = renderUpdateMetadata(
version = "3.12.0",
releaseDate = "2026-10-20T18:04:11Z",
assets = listOf(
UpdateAsset("hammer.msi", 190034708, "abc="),
UpdateAsset("hammer.exe", 190100000, "def="),
),
)
val expected = """
|version: 3.12.0
|releaseDate: 2026-10-20T18:04:11Z
|files:
| - url: hammer.msi
| sha512: abc=
| size: 190034708
| - url: hammer.exe
| sha512: def=
| size: 190100000
|""".trimMargin()
assertEquals(expected, yml)
}
@Test
fun `sha512 is base64 of the digest`() {
val file = tempDir().resolve("asset.bin").apply { writeBytes(ByteArray(100_000) { it.toByte() }) }
val expected = Base64.getEncoder().encodeToString(
MessageDigest.getInstance("SHA-512").digest(file.readBytes())
)
assertEquals(expected, sha512Base64(file))
}
@Test
fun `a digest line round-trips through the sidecar`() {
val file = tempDir().resolve("hammer.msi").apply { writeBytes(ByteArray(12_345) { (it * 3).toByte() }) }
val asset = parseAssetDigest("hammer.msi", assetDigestLine(file) + "\n")
assertEquals(UpdateAsset("hammer.msi", 12_345, sha512Base64(file)), asset)
}
@Test
fun `a malformed sidecar is rejected`() {
assertFailsWith<IllegalArgumentException> { parseAssetDigest("hammer.msi", "abc=") }
assertFailsWith<IllegalStateException> { parseAssetDigest("hammer.msi", "abc= many") }
}
@Test
fun `every platform file lists its installers from the sidecars`() {
val dir = tempDir()
installers.forEachIndexed { i, name -> dir.sidecar(name, "digest$i= ${100 + i}") }
val files = updateMetadataFiles(dir, "3.12.0", "2026-10-20T18:04:11Z")
assertEquals(setOf("latest.yml", "latest-mac.yml", "latest-linux.yml"), files.keys)
assertEquals(listOf("hammer.msi", "hammer.exe"), urlsIn(files.getValue("latest.yml")))
assertEquals(listOf("hammer.dmg", "hammer.pkg"), urlsIn(files.getValue("latest-mac.yml")))
assertEquals(
listOf("hammer.deb", "hammer.rpm", "hammer.AppImage"),
urlsIn(files.getValue("latest-linux.yml")),
)
assertEquals(" sha512: digest0=", files.getValue("latest.yml").lines()[4])
assertEquals(" size: 100", files.getValue("latest.yml").lines()[5])
}
@Test
fun `sidecars are found inside the per-format subdirectories the artifacts keep`() {
val dir = tempDir()
installers.forEach { dir.resolve(it.substringAfter('.').lowercase()).apply { mkdirs() }.sidecar(it, "x= 1") }
val files = updateMetadataFiles(dir, "1", "d")
assertEquals(listOf("hammer.msi", "hammer.exe"), urlsIn(files.getValue("latest.yml")))
assertEquals(listOf("hammer.deb", "hammer.rpm", "hammer.AppImage"), urlsIn(files.getValue("latest-linux.yml")))
}
@Test
fun `a duplicated sidecar fails instead of picking one`() {
val dir = tempDir()
installers.forEach { dir.sidecar(it, "x= 1") }
dir.resolve("msi").apply { mkdirs() }.sidecar("hammer.msi", "y= 2")
assertFailsWith<IllegalStateException> { updateMetadataFiles(dir, "1", "d") }
}
@Test
fun `the mac zip is listed first when present and skipped when absent`() {
val dir = tempDir()
installers.forEach { dir.sidecar(it, "x= 1") }
assertNull(urlsIn(updateMetadataFiles(dir, "1", "d").getValue("latest-mac.yml")).firstOrNull { it == "hammer.zip" })
dir.sidecar("hammer.zip", "z= 2")
assertEquals(
listOf("hammer.zip", "hammer.dmg", "hammer.pkg"),
urlsIn(updateMetadataFiles(dir, "1", "d").getValue("latest-mac.yml")),
)
}
@Test
fun `a missing required installer fails instead of publishing a gap`() {
val dir = tempDir()
installers.filter { it != "hammer.exe" }.forEach { dir.sidecar(it, "x= 1") }
val error = assertFailsWith<IllegalStateException> { updateMetadataFiles(dir, "1", "d") }
assertEquals(true, error.message?.startsWith("latest.yml needs hammer.exe$DIGEST_SUFFIX"))
}
private fun File.sidecar(name: String, line: String) = resolve(name + DIGEST_SUFFIX).writeText(line)
private fun urlsIn(yml: String): List<String> =
yml.lines().filter { it.trim().startsWith("- url:") }.map { it.substringAfter("- url:").trim() }
private fun tempDir(): File = Files.createTempDirectory("update-metadata-test").toFile().apply { deleteOnExit() }
}

View file

@ -0,0 +1,150 @@
package com.darkrockstudios.apps.hammer.common.data.globalsettings.datasource
import android.content.SharedPreferences
import kotlinx.serialization.json.Json
import javax.crypto.AEADBadTagException
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
class EncryptedSharedPrefsAuthTokenStoreTest {
private val disk = FakeEncryptedPrefsDisk()
private fun createStore() = EncryptedSharedPrefsAuthTokenStore(
json = Json,
openPrefs = disk::open,
resetPrefs = disk::reset,
)
@Test
fun `tokens round trip`() {
val store = createStore()
store.put(URL, USER_ID, TOKENS)
assertEquals(TOKENS, createStore().get(URL, USER_ID))
}
@Test
fun `keyset the master key cannot decrypt is treated as no tokens`() {
disk.keysetDecryptable = false
val store = createStore()
val tokens = store.get(URL, USER_ID)
assertNull(tokens)
}
@Test
fun `store is usable again after an undecryptable keyset`() {
disk.keysetDecryptable = false
createStore().get(URL, USER_ID)
createStore().put(URL, USER_ID, TOKENS)
assertEquals(TOKENS, createStore().get(URL, USER_ID))
}
@Test
fun `value that cannot be decrypted is treated as no tokens`() {
createStore().put(URL, USER_ID, TOKENS)
disk.valuesDecryptable = false
val tokens = createStore().get(URL, USER_ID)
assertNull(tokens)
}
@Test
fun `put replaces a value that cannot be decrypted`() {
createStore().put(URL, USER_ID, TOKENS)
disk.valuesDecryptable = false
val replacement = AuthTokens(bearerToken = "new-bearer", refreshToken = "new-refresh")
createStore().put(URL, USER_ID, replacement)
assertEquals(replacement, createStore().get(URL, USER_ID))
}
companion object {
private const val URL = "https://hammer.example"
private const val USER_ID = 7L
private val TOKENS = AuthTokens(bearerToken = "bearer", refreshToken = "refresh")
}
}
/**
* Models the encrypted prefs file: [open] fails like EncryptedSharedPreferences.create
* when the Keystore master key cannot decrypt the stored keyset, and [reset] removes
* the file (keyset included) so the next open generates a fresh one.
*/
private class FakeEncryptedPrefsDisk {
val values = mutableMapOf<String, String>()
var keysetDecryptable = true
var valuesDecryptable = true
fun open(): SharedPreferences {
if (!keysetDecryptable) throw AEADBadTagException()
return FakePrefs()
}
fun reset() {
values.clear()
keysetDecryptable = true
valuesDecryptable = true
}
private inner class FakePrefs : SharedPreferences {
override fun getString(key: String, defValue: String?): String? {
val stored = values[key] ?: return defValue
if (!valuesDecryptable) throw SecurityException("Could not decrypt value")
return stored
}
override fun contains(key: String): Boolean = key in values
override fun getAll(): MutableMap<String, *> = values.toMutableMap()
override fun edit(): SharedPreferences.Editor = FakeEditor()
override fun getStringSet(key: String, defValues: MutableSet<String>?) = unsupported()
override fun getInt(key: String, defValue: Int): Int = unsupported()
override fun getLong(key: String, defValue: Long): Long = unsupported()
override fun getFloat(key: String, defValue: Float): Float = unsupported()
override fun getBoolean(key: String, defValue: Boolean): Boolean = unsupported()
override fun registerOnSharedPreferenceChangeListener(
listener: SharedPreferences.OnSharedPreferenceChangeListener
) = unsupported()
override fun unregisterOnSharedPreferenceChangeListener(
listener: SharedPreferences.OnSharedPreferenceChangeListener
) = unsupported()
}
private inner class FakeEditor : SharedPreferences.Editor {
private val pending = mutableMapOf<String, String?>()
private var clearAll = false
override fun putString(key: String, value: String?) = apply { pending[key] = value }
override fun remove(key: String) = apply { pending[key] = null }
override fun clear() = apply { clearAll = true }
override fun commit(): Boolean {
if (clearAll) values.clear()
pending.forEach { (key, value) -> if (value == null) values.remove(key) else values[key] = value }
valuesDecryptable = true
return true
}
override fun apply() {
commit()
}
override fun putStringSet(key: String, values: MutableSet<String>?) = unsupported()
override fun putInt(key: String, value: Int) = unsupported()
override fun putLong(key: String, value: Long) = unsupported()
override fun putFloat(key: String, value: Float) = unsupported()
override fun putBoolean(key: String, value: Boolean) = unsupported()
}
}
private fun unsupported(): Nothing = throw UnsupportedOperationException()

View file

@ -9,31 +9,46 @@ import io.github.aakira.napier.Napier
import kotlinx.atomicfu.locks.reentrantLock
import kotlinx.atomicfu.locks.withLock
import kotlinx.serialization.json.Json
import java.io.IOException
import java.security.GeneralSecurityException
import java.security.KeyStore
/**
* [AuthTokenStore] backed by [EncryptedSharedPreferences] with an Android
* Keystore-backed master key. The account-keyed token map is stored as JSON in a
* single encrypted preferences entry.
*/
class EncryptedSharedPrefsAuthTokenStore(
context: Context,
class EncryptedSharedPrefsAuthTokenStore internal constructor(
private val json: Json,
private val openPrefs: () -> SharedPreferences,
private val resetPrefs: () -> Unit,
) : AuthTokenStore {
constructor(context: Context, json: Json) : this(
json = json,
openPrefs = { openEncryptedPrefs(context) },
resetPrefs = { resetEncryptedPrefs(context) },
)
private val lock = reentrantLock()
private val prefs: SharedPreferences by lazy {
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
try {
openPrefs()
} catch (e: GeneralSecurityException) {
recreatePrefs(e)
} catch (e: IOException) {
recreatePrefs(e)
}
}
EncryptedSharedPreferences.create(
context,
PREFS_NAME,
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
)
// The keyset lives in the prefs file but its master key lives in the Keystore, which is
// never backed up, so a restored or orphaned file is undecryptable, and some keystores
// leave the master key itself unusable. Start over with both (forces re-login).
private fun recreatePrefs(cause: Exception): SharedPreferences {
Napier.w("Auth token keyset is unreadable; recreating the store", cause)
resetPrefs()
return openPrefs()
}
override fun get(url: String, userId: Long): AuthTokens? = lock.withLock {
@ -54,11 +69,11 @@ class EncryptedSharedPrefsAuthTokenStore(
}
private fun loadMap(): Map<String, AuthTokens> {
val stored = prefs.getString(TOKENS_KEY, null) ?: return emptyMap()
return try {
val stored = prefs.getString(TOKENS_KEY, null) ?: return emptyMap()
json.decodeFromString<Map<String, AuthTokens>>(stored)
} catch (@Suppress("TooGenericExceptionCaught") e: Exception) {
Napier.w("Failed to parse encrypted auth tokens; treating as empty", e)
Napier.w("Failed to read encrypted auth tokens; treating as empty", e)
emptyMap()
}
}
@ -70,5 +85,26 @@ class EncryptedSharedPrefsAuthTokenStore(
companion object {
private const val PREFS_NAME = "hammer_auth_tokens"
private const val TOKENS_KEY = "tokens"
private const val ANDROID_KEYSTORE = "AndroidKeyStore"
private fun openEncryptedPrefs(context: Context): SharedPreferences {
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
return EncryptedSharedPreferences.create(
context,
PREFS_NAME,
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
)
}
private fun resetEncryptedPrefs(context: Context) {
context.deleteSharedPreferences(PREFS_NAME)
KeyStore.getInstance(ANDROID_KEYSTORE).apply { load(null) }
.deleteEntry(MasterKey.DEFAULT_MASTER_KEY_ALIAS)
}
}
}

View file

@ -1,5 +1,8 @@
package com.darkrockstudios.apps.hammer.common.dependencyinjection
import com.darkrockstudios.apps.hammer.common.data.appupdate.AppUpdater
import com.darkrockstudios.apps.hammer.common.data.appupdate.NoOpAppUpdater
import com.darkrockstudios.apps.hammer.common.components.projectselection.accountsettings.AndroidPlatformSettingsComponent
import com.darkrockstudios.apps.hammer.common.components.projectselection.accountsettings.PlatformSettings
import com.darkrockstudios.apps.hammer.common.components.storyeditor.focusmode.FocusModeService
@ -36,4 +39,5 @@ actual val platformModule = module {
single<AndroidShareService>()
single<BackupManagerService>()
single<GlobalSettingsDatasource> { get<GlobalSettingsFilesystemDatasource>() }
single<AppUpdater> { NoOpAppUpdater }
}

View file

@ -1,17 +1,32 @@
## [3.11.0] - 2026-9-24
## [3.12.0] - 2026-10-5
New features
- Web: signed-in readers can leave kudos at the end of a publicly published story, picking up to four craft chips and one reaction. Kudos are anonymous: authors see per-chip totals on their story page, and a chip appears on the public page once three readers have picked it. Authors can turn kudos off for any story
- Reader Mode: read your story as prose, one chapter at a time, instead of through the scene editor. Open it from the Read button under the scene list, the book icon in the scene editor, or the editor menu. The scene list becomes a table of contents, and the reader has its own font size
- Export: three new options in the Export Story dialog. Number chapter titles, pick a font (Word, RTF and EPUB), and choose whether to keep blank lines between paragraphs
- Android and iOS: an OLED theme, the dark theme on a pure black background
- Desktop (Windows and Linux installs from GitHub): Hammer tells you when a newer release is out and can update itself from the project list or the About screen. macOS DMG installs are told and sent to the release page. Automatic checks can be turned off in Settings
- Text editor 3.0: Huge improvements in input and performance
- Text editor: change the text size with Ctrl/Cmd and Plus, Minus or 0, or from the format bar's overflow menu
- Text editor: Delete in the right-click menu
- Turkish translation
- Web: a fifth reader reaction on public stories, "Surprised me"
Improvements
- Word counts no longer count standalone Markdown markers such as horizontal rules, heading marks, bullets and blockquote markers as words. The server now counts the same way, so both give the same number
- Android and Wear OS builds are now shrunk and obfuscated with R8
- Syncing with unsaved work now asks Save, Discard or Cancel first, where before it silently saved every unsaved scene
- Open notes, timeline events, encyclopedia entries and the scene title, outline and notes refresh when a sync or conflict resolution changes them. An edit in progress keeps its draft
- The Backup Manager lists backups of projects that are no longer on disk, so a deleted project can be restored
- Exports no longer number chapter titles unless you turn the new option on. Word, RTF, PDF and Markdown used to number them always
- Text editor: the heading button writes real Markdown headings, and Hammer's shortcuts land on the same keys on non-QWERTY layouts
- Android: a launch splash screen on every supported version
- Translations and dependencies updated
Fixes
- Text editor: numpad navigation keys and numpad Enter now work
- Text editor: keyboard (IME) edits are applied right away, and the keyboard is told about each one only once
- Text editor: spelling underlines stay on the right words while you keep typing
- Text editor: the formatting you picked for new text is kept after unrelated updates
- Text editor: fixed crashes and misplaced carets when drawing the cursor or moving it during a layout change
- Wear OS: pairing with the phone app works in release builds
- Android: unsaved scene edits could be lost when the app was closed in the background. They now come back as unsaved scenes the next time the project opens [#983]
- Export: unsaved scene edits were left out of the exported file [#983]
- Restoring a backup from the Backup Manager made the project disappear from the project list
- Choosing Discard on close with automatic sync on still saved and uploaded the discarded scenes
- PDF export: long paragraphs ran off the bottom of the page or left large blank gaps
- Android: a crash on launch when the saved login could not be decrypted, for example after restoring the device from a backup. You are asked to sign in again
- iOS: a crash when tapping into a text editor
- Server URL, number, tag and word-list fields no longer autocorrect or capitalize, which put a space after each "." in a server URL
- Encyclopedia references no longer match a name inside a longer word with accented or non-Latin letters ("Ana" in "Anaïs")

View file

@ -22,6 +22,7 @@
<string name="confirm_dialog_negative">Nein</string>
<string name="expand">Erweitern</string>
<string name="collapse">Einklappen</string>
<string name="markdown_format_bar_text_size">Text size</string>
<string name="markdown_format_bar_decrease_text_size">Textgröße verringern</string>
<string name="markdown_format_bar_increase_text_size">Textgröße vergrößern</string>
<string name="markdown_format_bar_reset_text_size">Textgröße zurücksetzen</string>

View file

@ -12,6 +12,17 @@
<string name="about_attribution_studio_button">Dark Rock Studios</string>
<string name="about_version_changes_button">Changes</string>
<string name="about_version_github_button">GitHub Release</string>
<string name="about_update_check_button">Check for Updates</string>
<string name="about_update_checking">Checking for updates&#8230;</string>
<string name="about_update_up_to_date">Hammer is up to date.</string>
<string name="about_update_available">Hammer %1$s is available.</string>
<string name="about_update_install_button">Update to %1$s</string>
<string name="about_update_downloading">Downloading Hammer %1$s (%2$s)</string>
<string name="about_update_installing">Installing Hammer %1$s. Hammer will restart.</string>
<string name="about_update_failed">Could not update to %1$s: %2$s</string>
<string name="about_update_retry_button">Retry</string>
<string name="about_update_later_button">Later</string>
<string name="about_update_quit_notice">Hammer will quit to install the update, then reopen.</string>
<string name="changelog_dialog_title">What's New</string>
<string name="update_dialog_release_notes_section">Versionshinweise</string>
<string name="update_dialog_release_notes_empty">Keine Versionshinweise vorhanden.</string>

View file

@ -9,6 +9,7 @@
<string name="settings_theme_label">Farbschema</string>
<string name="settings_theme_light">HELL</string>
<string name="settings_theme_dark">DUNKEL</string>
<string name="settings_theme_oled_black">OLED</string>
<string name="settings_theme_followsystem">SYSTEM</string>
<string name="settings_initial_screen_label">Anfangsbildschirm des Projekts</string>
<string name="settings_initial_screen_home">Startseite</string>
@ -55,6 +56,8 @@
<string name="merge_projects_replace_confirm_title">Delete all local projects?</string>
<string name="merge_projects_replace_confirm_message">This permanently deletes every project on this device and replaces them with the projects from the server. This cannot be undone.</string>
<string name="merge_projects_replace_confirm_button">Delete and Replace</string>
<string name="settings_automatic_update_checks">Check for updates automatically</string>
<string name="settings_automatic_update_checks_hint">Looks for a new release on GitHub shortly after launch and every few hours. Nothing is downloaded or installed until you choose to update.</string>
<string name="settings_platform_settings_title">Plattform-Einstellungen</string>
<string name="settings_platform_settings_empty">Keine plattformspezifischen Einstellungen.</string>
<string name="settings_keep_screen_on">Bildschirm nicht ausschalten</string>
@ -83,6 +86,7 @@
<string name="backup_manager_error_load_backups">Fehler beim Laden der Backups: %1$s</string>
<string name="backup_manager_error_load_project_backups">Fehler beim Laden der Backups für das Projekt: %1$s</string>
<string name="backup_manager_error_delete_backup">Fehler beim Löschen der Backups: %1$s</string>
<string name="backup_manager_error_restore_backup">Failed to restore backup: %1$s</string>
<string name="backup_restore_dialog_title">Backup wiederherstellen</string>
<string name="backup_restore_dialog_message">Warnung: Das Wiederherstellen dieses Backups ersetzt den aktuellen Inhalt des Projekts vollständig. Alle vorhandenen Dateien werden gelöscht und mit den Backup-Daten ersetzt. Diese Aktion kann nicht rückgängig gemacht werden.</string>
<string name="backup_restore_dialog_confirm">Wiederherstellen</string>

View file

@ -78,6 +78,10 @@
<string name="project_home_export_format_rtf">RTF (.rtf)</string>
<string name="project_home_export_cancel">Abbrechen</string>
<string name="project_home_export_execute">Exportieren</string>
<string name="project_home_export_number_chapters_label">Number chapter titles</string>
<string name="project_home_export_font_label">Font</string>
<string name="project_home_export_blank_lines_label">Keep blank lines between paragraphs</string>
<string name="project_home_export_blank_lines_hint">Turn off if you separate paragraphs with a blank line and do not want empty paragraphs in the export. Scene breaks and runs of two or more blank lines are kept.</string>
<string name="project_home_export_limit_scenes_label">Limit to specific scenes</string>
<string name="project_home_export_limit_scenes_hint">Only the selected scenes will be included in the export</string>
<string name="project_home_export_scenes_label">Szenen</string>

View file

@ -7,6 +7,7 @@
<string name="projects_list_create_button">Projekt erstellen</string>
<string name="projects_list_toast_sync_complete">Projekte synchronisiert</string>
<string name="projects_list_toast_sync_failed">Fehler beim Synchronisieren der Projekte</string>
<string name="projects_list_toast_delete_failed">Could not delete the project. Check that its folder is accessible and try again.</string>
<string name="projects_list_item_more_button">Mehr</string>
<string name="projects_list_card_words">%1$s Wörter</string>
<string name="projects_list_card_by_author">von %1$s</string>

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="reader_mode_section">Reader</string>
<string name="reader_mode_position">Chapter %1$d of %2$d</string>
<string name="reader_mode_edit">Edit ↗</string>
<string name="reader_mode_close">× Close</string>
<string name="reader_mode_decrease_font_size">Textgröße verringern</string>
<string name="reader_mode_increase_font_size">Textgröße vergrößern</string>
<string name="reader_mode_reset_font_size">Textgröße zurücksetzen</string>
<string name="reader_mode_toggle_scene_headings">Show scene headings</string>
<string name="reader_mode_previous_chapter">← Previous chapter</string>
<string name="reader_mode_next_chapter">Next chapter →</string>
<string name="reader_mode_empty_chapter">This chapter has no scenes yet</string>
<string name="reader_mode_empty_story">There is nothing to read yet</string>
<string name="reader_mode_end">End of story</string>
</resources>

View file

@ -24,6 +24,7 @@
<string name="scene_editor_menu_item_archive">Archivieren</string>
<string name="scene_editor_menu_item_save">Speichern</string>
<string name="scene_editor_menu_item_close">Schließen</string>
<string name="scene_editor_menu_item_read">Read</string>
<string name="scene_editor_add_to_dictionary">Add to dictionary</string>
<string name="archive_scene_dialog_title">Szene archivieren</string>
<string name="archive_scene_dialog_message">Möchten Sie '%1$s' wirklich archivieren? Die Szene wird aus der Szenenliste ausgeblendet, kann aber später wiederhergestellt werden.</string>

View file

@ -16,7 +16,8 @@
<string name="create_sceneitem_dialog_create_button">Erstellen</string>
<string name="create_sceneitem_dialog_dismiss_button">Abbrechen</string>
<string name="scene_list_outline_overview_title">Gliederung</string>
<string name="scene_list_outline_overview_button">Gliederungsübersicht</string>
<string name="scene_list_outline_button">Gliederung</string>
<string name="scene_list_reader_button">Read</string>
<string name="scene_list_outline_overview_none">&lt;keine Gliederung&gt;</string>
<string name="scene_list_outline_dismiss">Schließen</string>
<string name="scene_list_outline_overview_eyebrow">Manuskript</string>

View file

@ -22,6 +22,7 @@
<string name="confirm_dialog_negative">No</string>
<string name="expand">Expandir</string>
<string name="collapse">Contraer</string>
<string name="markdown_format_bar_text_size">Text size</string>
<string name="markdown_format_bar_decrease_text_size">Reducir el tamaño del texto</string>
<string name="markdown_format_bar_increase_text_size">Aumentar el tamaño del texto</string>
<string name="markdown_format_bar_reset_text_size">Restablecer el tamaño del texto</string>

View file

@ -12,6 +12,17 @@
<string name="about_attribution_studio_button">Dark Rock Studios</string>
<string name="about_version_changes_button">Changes</string>
<string name="about_version_github_button">GitHub Release</string>
<string name="about_update_check_button">Check for Updates</string>
<string name="about_update_checking">Checking for updates&#8230;</string>
<string name="about_update_up_to_date">Hammer is up to date.</string>
<string name="about_update_available">Hammer %1$s is available.</string>
<string name="about_update_install_button">Update to %1$s</string>
<string name="about_update_downloading">Downloading Hammer %1$s (%2$s)</string>
<string name="about_update_installing">Installing Hammer %1$s. Hammer will restart.</string>
<string name="about_update_failed">Could not update to %1$s: %2$s</string>
<string name="about_update_retry_button">Retry</string>
<string name="about_update_later_button">Later</string>
<string name="about_update_quit_notice">Hammer will quit to install the update, then reopen.</string>
<string name="changelog_dialog_title">What's New</string>
<string name="update_dialog_release_notes_section">Notas de la versión</string>
<string name="update_dialog_release_notes_empty">No se proporcionaron notas de la versión.</string>

View file

@ -9,6 +9,7 @@
<string name="settings_theme_label">Tema de la interfaz</string>
<string name="settings_theme_light">CLARO</string>
<string name="settings_theme_dark">OSCURO</string>
<string name="settings_theme_oled_black">OLED</string>
<string name="settings_theme_followsystem">SISTEMA</string>
<string name="settings_initial_screen_label">Pantalla inicial del proyecto</string>
<string name="settings_initial_screen_home">Inicio</string>
@ -55,6 +56,8 @@
<string name="merge_projects_replace_confirm_title">Delete all local projects?</string>
<string name="merge_projects_replace_confirm_message">This permanently deletes every project on this device and replaces them with the projects from the server. This cannot be undone.</string>
<string name="merge_projects_replace_confirm_button">Delete and Replace</string>
<string name="settings_automatic_update_checks">Check for updates automatically</string>
<string name="settings_automatic_update_checks_hint">Looks for a new release on GitHub shortly after launch and every few hours. Nothing is downloaded or installed until you choose to update.</string>
<string name="settings_platform_settings_title">Ajustes de la plataforma</string>
<string name="settings_platform_settings_empty">No hay ajustes específicos de la plataforma.</string>
<string name="settings_keep_screen_on">Mantener la pantalla encendida</string>
@ -83,6 +86,7 @@
<string name="backup_manager_error_load_backups">No se pudieron cargar las copias de seguridad: %1$s</string>
<string name="backup_manager_error_load_project_backups">No se pudieron cargar las copias de seguridad del proyecto: %1$s</string>
<string name="backup_manager_error_delete_backup">No se pudo eliminar la copia de seguridad: %1$s</string>
<string name="backup_manager_error_restore_backup">Failed to restore backup: %1$s</string>
<string name="backup_restore_dialog_title">Restaurar copia de seguridad</string>
<string name="backup_restore_dialog_message">Advertencia: restaurar esta copia de seguridad reemplazará por completo el contenido actual del proyecto. Todos los archivos existentes se eliminarán y se reemplazarán con los datos de la copia de seguridad. Esta acción no se puede deshacer.</string>
<string name="backup_restore_dialog_confirm">Restaurar</string>

View file

@ -78,6 +78,10 @@
<string name="project_home_export_format_rtf">RTF (.rtf)</string>
<string name="project_home_export_cancel">Cancelar</string>
<string name="project_home_export_execute">Exportar</string>
<string name="project_home_export_number_chapters_label">Number chapter titles</string>
<string name="project_home_export_font_label">Font</string>
<string name="project_home_export_blank_lines_label">Keep blank lines between paragraphs</string>
<string name="project_home_export_blank_lines_hint">Turn off if you separate paragraphs with a blank line and do not want empty paragraphs in the export. Scene breaks and runs of two or more blank lines are kept.</string>
<string name="project_home_export_limit_scenes_label">Limit to specific scenes</string>
<string name="project_home_export_limit_scenes_hint">Only the selected scenes will be included in the export</string>
<string name="project_home_export_scenes_label">Escenas</string>

View file

@ -7,6 +7,7 @@
<string name="projects_list_create_button">Crear proyecto</string>
<string name="projects_list_toast_sync_complete">Proyectos sincronizados</string>
<string name="projects_list_toast_sync_failed">No se pudieron sincronizar los proyectos</string>
<string name="projects_list_toast_delete_failed">Could not delete the project. Check that its folder is accessible and try again.</string>
<string name="projects_list_item_more_button">Más</string>
<string name="projects_list_card_words">%1$s palabras</string>
<string name="projects_list_card_by_author">por %1$s</string>

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="reader_mode_section">Reader</string>
<string name="reader_mode_position">Chapter %1$d of %2$d</string>
<string name="reader_mode_edit">Edit ↗</string>
<string name="reader_mode_close">× Close</string>
<string name="reader_mode_decrease_font_size">Reducir el tamaño del texto</string>
<string name="reader_mode_increase_font_size">Aumentar el tamaño del texto</string>
<string name="reader_mode_reset_font_size">Restablecer el tamaño del texto</string>
<string name="reader_mode_toggle_scene_headings">Show scene headings</string>
<string name="reader_mode_previous_chapter">← Previous chapter</string>
<string name="reader_mode_next_chapter">Next chapter →</string>
<string name="reader_mode_empty_chapter">This chapter has no scenes yet</string>
<string name="reader_mode_empty_story">There is nothing to read yet</string>
<string name="reader_mode_end">End of story</string>
</resources>

View file

@ -24,6 +24,7 @@
<string name="scene_editor_menu_item_archive">Archivar</string>
<string name="scene_editor_menu_item_save">Guardar</string>
<string name="scene_editor_menu_item_close">Cerrar</string>
<string name="scene_editor_menu_item_read">Read</string>
<string name="scene_editor_add_to_dictionary">Add to dictionary</string>
<string name="archive_scene_dialog_title">Archivar escena</string>
<string name="archive_scene_dialog_message">¿Seguro que quieres archivar «%1$s»? La escena se ocultará de la lista de escenas, pero podrás restaurarla más tarde.</string>

View file

@ -16,7 +16,8 @@
<string name="create_sceneitem_dialog_create_button">Crear</string>
<string name="create_sceneitem_dialog_dismiss_button">Cancelar</string>
<string name="scene_list_outline_overview_title">Esquema de la historia</string>
<string name="scene_list_outline_overview_button">Vista del esquema</string>
<string name="scene_list_outline_button">Esquema</string>
<string name="scene_list_reader_button">Read</string>
<string name="scene_list_outline_overview_none">&lt;sin esquema&gt;</string>
<string name="scene_list_outline_dismiss">Descartar</string>
<string name="scene_list_outline_overview_eyebrow">Manuscrito</string>

View file

@ -22,6 +22,7 @@
<string name="confirm_dialog_negative">Non</string>
<string name="expand">Développer</string>
<string name="collapse">Réduire</string>
<string name="markdown_format_bar_text_size">Taille du texte</string>
<string name="markdown_format_bar_decrease_text_size">Réduire la taille du texte</string>
<string name="markdown_format_bar_increase_text_size">Augmenter la taille du texte</string>
<string name="markdown_format_bar_reset_text_size">Réinitialiser la taille du texte</string>

View file

@ -12,6 +12,17 @@
<string name="about_attribution_studio_button">Dark Rock Studios</string>
<string name="about_version_changes_button">Changements</string>
<string name="about_version_github_button">Version GitHub</string>
<string name="about_update_check_button">Vérifier les mises à jour</string>
<string name="about_update_checking">Recherche de mises à jour&#8230;</string>
<string name="about_update_up_to_date">Hammer est à jour.</string>
<string name="about_update_available">Hammer %1$s est disponible.</string>
<string name="about_update_install_button">Mettre à jour ver %1$s</string>
<string name="about_update_downloading">Télécharger Hammer %1$s (%2$s)</string>
<string name="about_update_installing">Installation de Hammer %1$s. Hammer va redémarrer.</string>
<string name="about_update_failed">Impossible de mettre à jour vers la %1$s: %2$s</string>
<string name="about_update_retry_button">Réessayer</string>
<string name="about_update_later_button">Plus tard</string>
<string name="about_update_quit_notice">Hammer va se fermer pour installer la mise à jour, puis se rouvrira.</string>
<string name="changelog_dialog_title">Nouveautés</string>
<string name="update_dialog_release_notes_section">Notes de version</string>
<string name="update_dialog_release_notes_empty">Aucune note de version publiée.</string>
@ -21,9 +32,9 @@
<string name="about_logs_export_button">Exporter les journaux</string>
<string name="about_logs_no_logs">Aucun fichier journal trouvé</string>
<string name="about_logs_open_tooltip">Ouvrir le dossier des journaux</string>
<string name="about_logs_export_success">Logs exported.</string>
<string name="about_logs_export_failed">Could not export the logs. See the log directory.</string>
<string name="about_logs_copy_diagnostics_button">Copy Diagnostics</string>
<string name="about_logs_copy_diagnostics_success">Diagnostics copied to the clipboard.</string>
<string name="about_logs_copy_diagnostics_failed">Could not copy the diagnostics.</string>
<string name="about_logs_export_success">Journeaux exportés.</string>
<string name="about_logs_export_failed">Impossible d'exporter les journeaux. Voir le répertoire des journeaux.</string>
<string name="about_logs_copy_diagnostics_button">Copier les diagnostics</string>
<string name="about_logs_copy_diagnostics_success">Les diagnostics ont été copier dans le presse-papiers.</string>
<string name="about_logs_copy_diagnostics_failed">Impossible de copier les diagnostics.</string>
</resources>

View file

@ -9,6 +9,7 @@
<string name="settings_theme_label">Thème de l'interface</string>
<string name="settings_theme_light">CLAIR</string>
<string name="settings_theme_dark">SOMBRE</string>
<string name="settings_theme_oled_black">OLED</string>
<string name="settings_theme_followsystem">SYSTÈME</string>
<string name="settings_initial_screen_label">Écran initial du projet</string>
<string name="settings_initial_screen_home">Accueil</string>
@ -36,10 +37,10 @@
<string name="settings_remove_server_dialog_message">Voulez-vous vraiment supprimer le serveur ?</string>
<string name="settings_server_setup_title">Configurer le serveur</string>
<string name="settings_server_setup_url_hint">URL du serveur : hammer.ink</string>
<string name="settings_server_setup_insecure_warning">Plain HTTP: your email, password and writing travel unencrypted. Only use this on a network you trust.</string>
<string name="settings_server_setup_insecure_warning">HTTP en clair : votre email, mot de passe et vos écrits non chiffrés. Utilisez cela uniquement sur un réseau de confiance.</string>
<string name="settings_server_setup_email_hint">E-mail : utilisateur@exemple.com</string>
<string name="settings_server_setup_password_hint">Mot de passe</string>
<string name="settings_server_setup_password_requirements">%1$d-%2$d characters</string>
<string name="settings_server_setup_password_requirements">%1$d-%2$d personnages</string>
<string name="settings_server_setup_password_show">Afficher le mot de passe</string>
<string name="settings_server_setup_password_hide">Masquer le mot de passe</string>
<string name="settings_server_setup_login_button">Se connecter</string>
@ -52,9 +53,11 @@
<string name="merge_projects_explain_merge">Hammer fera correspondre vos projets locaux aux projets déjà présents sur le serveur. Les projets qui n'existent que sur le serveur seront téléchargés, et les projets présents en local seront téléversés sur le serveur.</string>
<string name="merge_projects_explain_replace">Tous vos projets locaux seront supprimés et remplacés par ceux présents sur le serveur.</string>
<string name="merge_projects_continue_button">Continuer</string>
<string name="merge_projects_replace_confirm_title">Delete all local projects?</string>
<string name="merge_projects_replace_confirm_message">This permanently deletes every project on this device and replaces them with the projects from the server. This cannot be undone.</string>
<string name="merge_projects_replace_confirm_button">Delete and Replace</string>
<string name="merge_projects_replace_confirm_title">Supprimer tous les projets locaux ?</string>
<string name="merge_projects_replace_confirm_message">Cela supprime de façon permanente tous les projets de cet appareil et les remplacera avec les projets présents sur le serveur. Cette action ne peut pas être annulé.</string>
<string name="merge_projects_replace_confirm_button">Supprimer et remplacer</string>
<string name="settings_automatic_update_checks">Vérifier automatiquement les mises à jour</string>
<string name="settings_automatic_update_checks_hint">Recherche les nouvelles versions sur GitHub peu après le lancement et toutes les heures. Rien n'est téléchargé ou installé sans votre intervention.</string>
<string name="settings_platform_settings_title">Paramètres de la plateforme</string>
<string name="settings_platform_settings_empty">Aucun paramètre spécifique à la plateforme.</string>
<string name="settings_keep_screen_on">Garder l'écran allumé</string>
@ -71,8 +74,8 @@
<string name="settings_spellcheck_notice">Si cela pose problème, désactivez-la simplement.</string>
<string name="settings_spellcheck_enable">Activer la correction orthographique</string>
<string name="settings_spellcheck_in_focus_enable">Activer en mode concentration</string>
<string name="settings_spellcheck_encyclopedia_enable">Learn encyclopedia names</string>
<string name="settings_spellcheck_encyclopedia_enable_hint">Entry names and aliases won&#8217;t be marked as misspelled while their project is open</string>
<string name="settings_spellcheck_encyclopedia_enable">Apprendre les noms de l'encyclopédie</string>
<string name="settings_spellcheck_encyclopedia_enable_hint">Les noms d'entrés et les alias ne seront pas marqués comme mal orthographiés pendant que leur projet est ouvert</string>
<string name="settings_spellcheck_dictionary">Dictionnaire</string>
<string name="settings_spellcheck_dictionary_android_info_title">Langue de correction orthographique</string>
<string name="settings_spellcheck_dictionary_android_info_message">Sur Android, la langue de correction orthographique est choisie par votre appareil, pas par l'application. Pour la modifier, changez les langues de votre système et de votre clavier dans les paramètres Android.</string>
@ -83,6 +86,7 @@
<string name="backup_manager_error_load_backups">Échec du chargement des sauvegardes : %1$s</string>
<string name="backup_manager_error_load_project_backups">Échec du chargement des sauvegardes du projet : %1$s</string>
<string name="backup_manager_error_delete_backup">Échec de la suppression de la sauvegarde : %1$s</string>
<string name="backup_manager_error_restore_backup">Échec de la suppression de la sauvegarde : %1$s</string>
<string name="backup_restore_dialog_title">Restaurer la sauvegarde</string>
<string name="backup_restore_dialog_message">Avertissement : la restauration de cette sauvegarde remplacera entièrement le contenu actuel du projet. Tous les fichiers existants seront supprimés et remplacés par les données de la sauvegarde. Cette action est irréversible.</string>
<string name="backup_restore_dialog_confirm">Restaurer</string>
@ -123,5 +127,5 @@
<string name="server_error_dns">Serveur introuvable. Vérifiez l'adresse du serveur et votre connexion Internet.</string>
<string name="server_error_connection_generic">Erreur réseau lors de la connexion à %1$s. Vérifiez votre connexion et réessayez.</string>
<string name="server_error_tls">Impossible d'établir une connection sécurisée à %1$s. Hammer se connecte uniquement via HTTPS, le serveur a besoin de son propre certificat TLS ou d'un proxy inverse qui lui en fournit un</string>
<string name="server_error_network_unavailable">Hammer could not open a network connection on this device, so the request never reached the server. Security software, a VPN, or a firewall may be blocking it.</string>
<string name="server_error_network_unavailable">Hammer ne peut pas établir une connexion réseau sur cet appareil, la requête ne peut donc pas atteindre le serveur. Un système de sécurité, un VPN ou un pare-feu peuvent être la source du problème.</string>
</resources>

View file

@ -30,7 +30,7 @@
<string name="encyclopedia_create_entry_cancel_button">Annuler</string>
<string name="encyclopedia_create_entry_toast_success">Entrée créée</string>
<string name="encyclopedia_create_entry_toast_too_long">Le nom de l'entrée est trop longue. Max %1$d</string>
<string name="encyclopedia_create_entry_toast_invalid_name">The entry name contains invalid characters</string>
<string name="encyclopedia_create_entry_toast_invalid_name">Le nom de l'entrée contient des caractères invalides</string>
<string name="encyclopedia_create_entry_toast_tag_too_long">L'Étiquette est trop longue. Max %1$d</string>
<string name="encyclopedia_create_entry_toast_alias_too_long">L'alias est trop long. Max %1$d</string>
<string name="encyclopedia_entry_aliases_label">Alias</string>

View file

@ -78,10 +78,14 @@
<string name="project_home_export_format_rtf">RTF (.rtf)</string>
<string name="project_home_export_cancel">Annuler</string>
<string name="project_home_export_execute">Exporter</string>
<string name="project_home_export_limit_scenes_label">Limit to specific scenes</string>
<string name="project_home_export_limit_scenes_hint">Only the selected scenes will be included in the export</string>
<string name="project_home_export_number_chapters_label">Nombre de titres de chapitre</string>
<string name="project_home_export_font_label">Police</string>
<string name="project_home_export_blank_lines_label">Conserver les lignes vides entre les paragraphes</string>
<string name="project_home_export_blank_lines_hint">Désactivez si vous séparez les paragraphes avec une ligne vide et ne voulez pas de paragraphes vides dans l'export. Les pauses de scène et les exécutions de deux ou plusieurs lignes vierges sont conservées.</string>
<string name="project_home_export_limit_scenes_label">Limiter aux scènes spécifiques</string>
<string name="project_home_export_limit_scenes_hint">Seules les scènes sélectionnées seront incluse dans l'export</string>
<string name="project_home_export_scenes_label">Scènes</string>
<string name="project_home_export_scenes_selected">%1$d of %2$d selected</string>
<string name="project_home_export_scenes_selected">%1$d sur %2$d sélectionnés</string>
<string name="project_home_export_scenes_select_all">Tout sélectionner</string>
<string name="project_home_export_scenes_clear_all">Tout effacer</string>
<string name="project_home_export_help_icon_description">En savoir plus sur les formats d'exportation</string>

View file

@ -7,6 +7,7 @@
<string name="projects_list_create_button">Créer un projet</string>
<string name="projects_list_toast_sync_complete">Projets synchronisés</string>
<string name="projects_list_toast_sync_failed">Échec de la synchronisation des projets</string>
<string name="projects_list_toast_delete_failed">Could not delete the project. Check that its folder is accessible and try again.</string>
<string name="projects_list_item_more_button">Plus</string>
<string name="projects_list_card_words">%1$s mots</string>
<string name="projects_list_card_by_author">par %1$s</string>

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="reader_mode_section">Lecteur</string>
<string name="reader_mode_position">Chapitre %1$d sur %2$d</string>
<string name="reader_mode_edit">Modifier</string>
<string name="reader_mode_close">× Fermer</string>
<string name="reader_mode_decrease_font_size">Réduire la taille du texte</string>
<string name="reader_mode_increase_font_size">Augmenter la taille du texte</string>
<string name="reader_mode_reset_font_size">Réinitialiser la taille du texte</string>
<string name="reader_mode_toggle_scene_headings">Afficher les en-têtes de scène</string>
<string name="reader_mode_previous_chapter">← Chapitre précédent</string>
<string name="reader_mode_next_chapter">Chapitre suivant →</string>
<string name="reader_mode_empty_chapter">Ce chapitre n'a pas encore de scène</string>
<string name="reader_mode_empty_story">Il n'y a rien à lire pour le moment</string>
<string name="reader_mode_end">Fin de l'histoire</string>
</resources>

View file

@ -24,7 +24,8 @@
<string name="scene_editor_menu_item_archive">Archiver</string>
<string name="scene_editor_menu_item_save">Enregistrer</string>
<string name="scene_editor_menu_item_close">Fermer</string>
<string name="scene_editor_add_to_dictionary">Add to dictionary</string>
<string name="scene_editor_menu_item_read">Lire</string>
<string name="scene_editor_add_to_dictionary">Ajouter au dictionnaire</string>
<string name="archive_scene_dialog_title">Archiver la scène</string>
<string name="archive_scene_dialog_message">Voulez-vous vraiment archiver « %1$s » ? La scène sera masquée de la liste des scènes mais pourra être restaurée plus tard.</string>
<string name="archive_scene_dialog_confirm">Archiver</string>

View file

@ -16,7 +16,8 @@
<string name="create_sceneitem_dialog_create_button">Créer</string>
<string name="create_sceneitem_dialog_dismiss_button">Annuler</string>
<string name="scene_list_outline_overview_title">Plan de l'histoire</string>
<string name="scene_list_outline_overview_button">Aperçu du plan</string>
<string name="scene_list_outline_button">Plan</string>
<string name="scene_list_reader_button">Lire</string>
<string name="scene_list_outline_overview_none">&lt;aucun plan&gt;</string>
<string name="scene_list_outline_dismiss">Ignorer</string>
<string name="scene_list_outline_overview_eyebrow">Manuscrit</string>

View file

@ -52,11 +52,11 @@
<string name="project_settings_hero_no_author">par (aucun auteur défini)</string>
<string name="project_settings_spellcheck_section_title">Correction orthographique</string>
<string name="project_settings_spellcheck_mismatch_caption">La vérification orthographique est désactivée : la langue du projet (%1$s) ne correspond pas à votre dictionnaire (%2$s). Changez là pour la réactiver.</string>
<string name="project_settings_spellcheck_encyclopedia_enable">Learn encyclopedia names in this project</string>
<string name="project_settings_spellcheck_encyclopedia_enable_hint">Turn off to keep this project&#8217;s entry names out of the spell check dictionary</string>
<string name="project_settings_dictionary_label">Project dictionary</string>
<string name="project_settings_spellcheck_encyclopedia_enable">Apprendre les noms du dictionnaire dans ce projet</string>
<string name="project_settings_spellcheck_encyclopedia_enable_hint">Désactivez pour garder les noms d'entrés en dehors du dictionnaire de vérification orthographique</string>
<string name="project_settings_dictionary_label">Dictionnaire du projet</string>
<string name="project_settings_dictionary_hint">↵ ajouter</string>
<string name="project_settings_dictionary_placeholder">Words the spell checker should accept in this project</string>
<string name="project_settings_dictionary_placeholder">Les mots que le correcteur orthographique devrait accepter dans ce projet</string>
<string name="project_settings_folio_caption">Paramètres du projet · %1$s</string>
<string name="project_settings_folio_section_count">§§ %1$d</string>
<string name="sync_conflict_project_data_title">Conflit de paramètres du projet</string>
@ -66,13 +66,13 @@
<string name="sync_conflict_project_data_field_word_goal">Objectif de nombre de mots</string>
<string name="sync_conflict_project_data_field_tags">Étiquettes</string>
<string name="sync_conflict_project_data_field_language">Langue</string>
<string name="sync_conflict_project_data_field_dictionary">Learn Encyclopedia Names</string>
<string name="sync_conflict_project_data_field_dictionary">Apprendre les noms de l'encyclopédie</string>
<string name="sync_conflict_project_data_value_unset">(non défini)</string>
<string name="sync_conflict_project_data_value_on">On</string>
<string name="sync_conflict_project_data_value_off">Off</string>
<string name="sync_conflict_project_data_value_on">Activer</string>
<string name="sync_conflict_project_data_value_off">Désactiver</string>
<string name="sync_conflict_project_data_cadence_day">jour</string>
<string name="sync_conflict_project_data_cadence_week">semaine</string>
<string name="sync_conflict_project_data_dictionary_merged">Project dictionary words from both sides are kept.</string>
<string name="sync_conflict_project_data_dictionary_merged">Les mots du dictionnaire des deux parties sont conservés.</string>
<string name="sync_conflict_project_data_resolve_button">Résoudre</string>
<string name="sync_conflict_encyclopedia_label_name">Nom :</string>
<string name="sync_conflict_encyclopedia_label_type">Type :</string>
@ -116,16 +116,16 @@
<string name="sync_log_account_project_rename_client_from_server_failure">Échec du renommage du projet depuis le serveur : %1$s</string>
<string name="sync_log_account_project_create_server_success">Projet créé sur le serveur : %1$s</string>
<string name="sync_log_account_project_create_server_failure">Échec de la création du projet sur le serveur : %1$s</string>
<string name="sync_log_account_project_id_save_failure">Project created on server, but failed to save its ID locally: %1$s</string>
<string name="sync_log_account_project_id_save_failure">Projet a été créé sur le serveur, mais n'a pas pu sauvegarder son ID localement : %1$s</string>
<string name="sync_log_account_project_create_client">Projet local créé : %1$s</string>
<string name="sync_log_account_project_create_client_local">Définition de l'ID serveur du projet local : %1$s</string>
<string name="sync_log_account_project_create_local_failure">Failed to create local project from server: %1$s</string>
<string name="sync_log_account_project_create_client_name_taken">Skipped server project %1$s: a local project of that name already syncs with a different server project</string>
<string name="sync_log_account_project_create_local_failure">Impossible de créer un projet local depuis le serveur : %1$s</string>
<string name="sync_log_account_project_create_client_name_taken">Projet du serveur %1$s ignoré : un projet local possédant ce nom est déjà synchronisé depuis un autre serveur</string>
<string name="sync_log_begin_account">Synchronisation du compte…</string>
<string name="sync_log_begin_projects">Synchronisation des projets…</string>
<string name="sync_log_end_projects">Synchronisation des projets terminée</string>
<string name="sync_log_begin_project">Synchronisation du projet : %1$s</string>
<string name="sync_log_project_unchanged">No changes, skipped</string>
<string name="sync_log_project_unchanged">Pas de modification, ignorée</string>
<string name="sync_log_project_failed">Échec de la synchronisation : %1$s</string>
<string name="sync_log_project_conflict">Il y a un conflit dans le projet : %1$s. Ouvrez ce projet et synchronisez pour le résoudre.</string>
<string name="sync_log_client_data_computed">Données d'entité client calculées</string>
@ -193,8 +193,7 @@
<string name="server_setup_help_patreon_link">Patreon</string>
<string name="server_setup_help_hammer_link">Hammer.Ink</string>
<string name="server_setup_help_registration_header">Inscription</string>
<string name="server_setup_help_registration_body">Servers have closed registration, meaning the server admin needs to add you to the server's allowed users list before you can create an account.
</string>
<string name="server_setup_help_registration_body">Les serveurs ont fermé leur inscription, ce qui signifie que l'administrateur du serveur doit vous ajouter à la liste des utilisateurs autorisés avant que vous ne puissiez créer un compte.</string>
<string name="server_setup_help_setup_header">Configuration</string>
<string name="server_setup_help_setup_step1">Saisissez l'URL du serveur, par exemple « hammer.ink » pour le serveur officiel.</string>
<string name="server_setup_help_setup_step2">Saisissez votre e-mail et votre mot de passe, puis appuyez sur « Créer » pour créer votre compte.</string>

View file

@ -22,6 +22,7 @@
<string name="confirm_dialog_negative">No</string>
<string name="expand">Espandi</string>
<string name="collapse">Comprimi</string>
<string name="markdown_format_bar_text_size">Text size</string>
<string name="markdown_format_bar_decrease_text_size">Riduci la dimensione del testo</string>
<string name="markdown_format_bar_increase_text_size">Aumenta la dimensione del testo</string>
<string name="markdown_format_bar_reset_text_size">Reimposta la dimensione del testo</string>

View file

@ -2,7 +2,7 @@
<resources>
<string name="about_description">Uno strumento semplice per costruire storie.</string>
<string name="about_studio_header">Studio</string>
<string name="about_community_studio_attribution">Hammer is made by Dark Rock Studios, alongside a family of other privacy-respecting, open source apps.</string>
<string name="about_community_studio_attribution">Hammer è realizzato da Dark Rock Studios, insieme a una famiglia di altre app open source che rispettano la privacy.</string>
<string name="about_community_header">Community</string>
<string name="about_community_discord_link">Discord</string>
<string name="about_community_reddit_link">Reddit</string>
@ -10,9 +10,20 @@
<string name="about_attribution_header">Attribuzione</string>
<string name="about_attribution_libraries_button">Librerie</string>
<string name="about_attribution_studio_button">Dark Rock Studios</string>
<string name="about_version_changes_button">Changes</string>
<string name="about_version_github_button">GitHub Release</string>
<string name="changelog_dialog_title">What's New</string>
<string name="about_version_changes_button">Modifiche</string>
<string name="about_version_github_button">Rilascio su GitHub</string>
<string name="about_update_check_button">Check for Updates</string>
<string name="about_update_checking">Checking for updates&#8230;</string>
<string name="about_update_up_to_date">Hammer is up to date.</string>
<string name="about_update_available">Hammer %1$s is available.</string>
<string name="about_update_install_button">Update to %1$s</string>
<string name="about_update_downloading">Downloading Hammer %1$s (%2$s)</string>
<string name="about_update_installing">Installing Hammer %1$s. Hammer will restart.</string>
<string name="about_update_failed">Could not update to %1$s: %2$s</string>
<string name="about_update_retry_button">Retry</string>
<string name="about_update_later_button">Later</string>
<string name="about_update_quit_notice">Hammer will quit to install the update, then reopen.</string>
<string name="changelog_dialog_title">Novità</string>
<string name="update_dialog_release_notes_section">Note di rilascio</string>
<string name="update_dialog_release_notes_empty">Nessuna nota di rilascio fornita.</string>
<string name="update_dialog_open_release_button">Apri la versione</string>
@ -21,9 +32,9 @@
<string name="about_logs_export_button">Esporta i log</string>
<string name="about_logs_no_logs">Nessun file di log trovato</string>
<string name="about_logs_open_tooltip">Apri la cartella dei log</string>
<string name="about_logs_export_success">Logs exported.</string>
<string name="about_logs_export_failed">Could not export the logs. See the log directory.</string>
<string name="about_logs_copy_diagnostics_button">Copy Diagnostics</string>
<string name="about_logs_copy_diagnostics_success">Diagnostics copied to the clipboard.</string>
<string name="about_logs_copy_diagnostics_failed">Could not copy the diagnostics.</string>
<string name="about_logs_export_success">Log esportati.</string>
<string name="about_logs_export_failed">Impossibile esportare i log. Controlla la cartella dei log.</string>
<string name="about_logs_copy_diagnostics_button">Copia diagnostica</string>
<string name="about_logs_copy_diagnostics_success">Diagnostica copiata negli appunti.</string>
<string name="about_logs_copy_diagnostics_failed">Impossibile copiare la diagnostica.</string>
</resources>

View file

@ -9,6 +9,7 @@
<string name="settings_theme_label">Tema dell’interfaccia</string>
<string name="settings_theme_light">CHIARO</string>
<string name="settings_theme_dark">SCURO</string>
<string name="settings_theme_oled_black">OLED</string>
<string name="settings_theme_followsystem">SISTEMA</string>
<string name="settings_initial_screen_label">Schermata iniziale del progetto</string>
<string name="settings_initial_screen_home">Home</string>
@ -55,6 +56,8 @@
<string name="merge_projects_replace_confirm_title">Delete all local projects?</string>
<string name="merge_projects_replace_confirm_message">This permanently deletes every project on this device and replaces them with the projects from the server. This cannot be undone.</string>
<string name="merge_projects_replace_confirm_button">Delete and Replace</string>
<string name="settings_automatic_update_checks">Check for updates automatically</string>
<string name="settings_automatic_update_checks_hint">Looks for a new release on GitHub shortly after launch and every few hours. Nothing is downloaded or installed until you choose to update.</string>
<string name="settings_platform_settings_title">Impostazioni della piattaforma</string>
<string name="settings_platform_settings_empty">Nessuna impostazione specifica della piattaforma.</string>
<string name="settings_keep_screen_on">Tieni lo schermo acceso</string>
@ -83,6 +86,7 @@
<string name="backup_manager_error_load_backups">Impossibile caricare i backup: %1$s</string>
<string name="backup_manager_error_load_project_backups">Impossibile caricare i backup del progetto: %1$s</string>
<string name="backup_manager_error_delete_backup">Impossibile eliminare il backup: %1$s</string>
<string name="backup_manager_error_restore_backup">Failed to restore backup: %1$s</string>
<string name="backup_restore_dialog_title">Ripristina backup</string>
<string name="backup_restore_dialog_message">Attenzione: ripristinare questo backup sostituirà completamente il contenuto attuale del progetto. Tutti i file esistenti verranno eliminati e sostituiti con i dati del backup. Questa azione non può essere annullata.</string>
<string name="backup_restore_dialog_confirm">Ripristina</string>

View file

@ -78,6 +78,10 @@
<string name="project_home_export_format_rtf">RTF (.rtf)</string>
<string name="project_home_export_cancel">Annulla</string>
<string name="project_home_export_execute">Esporta</string>
<string name="project_home_export_number_chapters_label">Number chapter titles</string>
<string name="project_home_export_font_label">Font</string>
<string name="project_home_export_blank_lines_label">Keep blank lines between paragraphs</string>
<string name="project_home_export_blank_lines_hint">Turn off if you separate paragraphs with a blank line and do not want empty paragraphs in the export. Scene breaks and runs of two or more blank lines are kept.</string>
<string name="project_home_export_limit_scenes_label">Limit to specific scenes</string>
<string name="project_home_export_limit_scenes_hint">Only the selected scenes will be included in the export</string>
<string name="project_home_export_scenes_label">Scene</string>
@ -115,7 +119,7 @@
<string name="project_home_import_large_scene_label">Large scene</string>
<string name="project_home_import_large_scene_one">One scene holds %1$s words, so chapter detection probably missed your chapter titles. Try another detection setting.</string>
<string name="project_home_import_large_scene_many">%1$d scenes hold %2$s words or more, so chapter detection probably missed some chapter titles. Try another detection setting.</string>
<string name="project_home_import_scene_word_count">%1$s W</string>
<string name="project_home_import_scene_word_count">%1$s P</string>
<string name="project_home_import_cancel">Annulla</string>
<string name="project_home_import_execute">Importa</string>
<string name="project_home_action_settings_button">Impostazioni</string>

View file

@ -7,7 +7,7 @@
<string name="project_nav_time_line">Cronologia</string>
<string name="project_nav_home_short">HOME</string>
<string name="project_nav_scene_editor_short">STOR</string>
<string name="project_nav_notes_short">NOTE</string>
<string name="project_nav_notes_short">NOTA</string>
<string name="project_nav_encyclopedia_short">ENCI</string>
<string name="project_nav_time_line_short">CRON</string>
<string name="nav_rail_expand">Espandi la navigazione</string>

View file

@ -7,6 +7,7 @@
<string name="projects_list_create_button">Crea progetto</string>
<string name="projects_list_toast_sync_complete">Progetti sincronizzati</string>
<string name="projects_list_toast_sync_failed">Impossibile sincronizzare i progetti</string>
<string name="projects_list_toast_delete_failed">Could not delete the project. Check that its folder is accessible and try again.</string>
<string name="projects_list_item_more_button">Altro</string>
<string name="projects_list_card_words">%1$s parole</string>
<string name="projects_list_card_by_author">di %1$s</string>

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="reader_mode_section">Reader</string>
<string name="reader_mode_position">Chapter %1$d of %2$d</string>
<string name="reader_mode_edit">Edit ↗</string>
<string name="reader_mode_close">× Close</string>
<string name="reader_mode_decrease_font_size">Riduci la dimensione del testo</string>
<string name="reader_mode_increase_font_size">Aumenta la dimensione del testo</string>
<string name="reader_mode_reset_font_size">Reimposta la dimensione del testo</string>
<string name="reader_mode_toggle_scene_headings">Show scene headings</string>
<string name="reader_mode_previous_chapter">← Previous chapter</string>
<string name="reader_mode_next_chapter">Next chapter →</string>
<string name="reader_mode_empty_chapter">This chapter has no scenes yet</string>
<string name="reader_mode_empty_story">There is nothing to read yet</string>
<string name="reader_mode_end">End of story</string>
</resources>

View file

@ -24,6 +24,7 @@
<string name="scene_editor_menu_item_archive">Archivia</string>
<string name="scene_editor_menu_item_save">Salva</string>
<string name="scene_editor_menu_item_close">Chiudi</string>
<string name="scene_editor_menu_item_read">Read</string>
<string name="scene_editor_add_to_dictionary">Add to dictionary</string>
<string name="archive_scene_dialog_title">Archivia scena</string>
<string name="archive_scene_dialog_message">Vuoi davvero archiviare «%1$s»? La scena verrà nascosta dall’elenco delle scene ma potrà essere ripristinata in seguito.</string>

View file

@ -16,7 +16,8 @@
<string name="create_sceneitem_dialog_create_button">Crea</string>
<string name="create_sceneitem_dialog_dismiss_button">Annulla</string>
<string name="scene_list_outline_overview_title">Scaletta della storia</string>
<string name="scene_list_outline_overview_button">Panoramica della scaletta</string>
<string name="scene_list_outline_button">Scaletta</string>
<string name="scene_list_reader_button">Read</string>
<string name="scene_list_outline_overview_none">&lt;nessuna scaletta&gt;</string>
<string name="scene_list_outline_dismiss">Ignora</string>
<string name="scene_list_outline_overview_eyebrow">Manoscritto</string>

View file

@ -22,6 +22,7 @@
<string name="confirm_dialog_negative">Não</string>
<string name="expand">Expandir</string>
<string name="collapse">Encolher</string>
<string name="markdown_format_bar_text_size">Text size</string>
<string name="markdown_format_bar_decrease_text_size">Diminuir o tamanho do texto</string>
<string name="markdown_format_bar_increase_text_size">Aumentar o tamanho do texto</string>
<string name="markdown_format_bar_reset_text_size">Redefinir o tamanho do texto</string>

View file

@ -12,6 +12,17 @@
<string name="about_attribution_studio_button">Dark Rock Studios</string>
<string name="about_version_changes_button">Changes</string>
<string name="about_version_github_button">GitHub Release</string>
<string name="about_update_check_button">Check for Updates</string>
<string name="about_update_checking">Checking for updates&#8230;</string>
<string name="about_update_up_to_date">Hammer is up to date.</string>
<string name="about_update_available">Hammer %1$s is available.</string>
<string name="about_update_install_button">Update to %1$s</string>
<string name="about_update_downloading">Downloading Hammer %1$s (%2$s)</string>
<string name="about_update_installing">Installing Hammer %1$s. Hammer will restart.</string>
<string name="about_update_failed">Could not update to %1$s: %2$s</string>
<string name="about_update_retry_button">Retry</string>
<string name="about_update_later_button">Later</string>
<string name="about_update_quit_notice">Hammer will quit to install the update, then reopen.</string>
<string name="changelog_dialog_title">What's New</string>
<string name="update_dialog_release_notes_section">Notas de versão</string>
<string name="update_dialog_release_notes_empty">Nenhuma nota de versão fornecida.</string>

View file

@ -9,6 +9,7 @@
<string name="settings_theme_label">Tema de 'interface' do usuário</string>
<string name="settings_theme_light">CLARO</string>
<string name="settings_theme_dark">ESCURO</string>
<string name="settings_theme_oled_black">OLED</string>
<string name="settings_theme_followsystem">SISTEMA</string>
<string name="settings_initial_screen_label">Tela Inicial do Projeto</string>
<string name="settings_initial_screen_home">Início</string>
@ -55,6 +56,8 @@
<string name="merge_projects_replace_confirm_title">Delete all local projects?</string>
<string name="merge_projects_replace_confirm_message">This permanently deletes every project on this device and replaces them with the projects from the server. This cannot be undone.</string>
<string name="merge_projects_replace_confirm_button">Delete and Replace</string>
<string name="settings_automatic_update_checks">Check for updates automatically</string>
<string name="settings_automatic_update_checks_hint">Looks for a new release on GitHub shortly after launch and every few hours. Nothing is downloaded or installed until you choose to update.</string>
<string name="settings_platform_settings_title">Configurações da Plataforma</string>
<string name="settings_platform_settings_empty">Nenhuma configuração específica da plataforma.</string>
<string name="settings_keep_screen_on">Manter a tela ligada</string>
@ -83,6 +86,7 @@
<string name="backup_manager_error_load_backups">Falha ao carregar 'backups': %1$s</string>
<string name="backup_manager_error_load_project_backups">Falha ao carregar 'backups' para o projeto: %1$s</string>
<string name="backup_manager_error_delete_backup">Falha ao excluir 'backup': %1$s</string>
<string name="backup_manager_error_restore_backup">Failed to restore backup: %1$s</string>
<string name="backup_restore_dialog_title">Restaurar 'Backup'</string>
<string name="backup_restore_dialog_message">Aviso: Restaurar este 'backup' substituirá completamente o conteúdo atual do projeto. Todos os arquivos existentes serão apagados e substituídos pelos dados de 'backup'. Esta ação não pode ser desfeita.</string>
<string name="backup_restore_dialog_confirm">Restaurar</string>

View file

@ -78,6 +78,10 @@
<string name="project_home_export_format_rtf">RTF (.rtf)</string>
<string name="project_home_export_cancel">Cancelar</string>
<string name="project_home_export_execute">Exportar</string>
<string name="project_home_export_number_chapters_label">Number chapter titles</string>
<string name="project_home_export_font_label">Font</string>
<string name="project_home_export_blank_lines_label">Keep blank lines between paragraphs</string>
<string name="project_home_export_blank_lines_hint">Turn off if you separate paragraphs with a blank line and do not want empty paragraphs in the export. Scene breaks and runs of two or more blank lines are kept.</string>
<string name="project_home_export_limit_scenes_label">Limit to specific scenes</string>
<string name="project_home_export_limit_scenes_hint">Only the selected scenes will be included in the export</string>
<string name="project_home_export_scenes_label">Cenas</string>

View file

@ -7,6 +7,7 @@
<string name="projects_list_create_button">Criar Projeto</string>
<string name="projects_list_toast_sync_complete">Projetos sincronizados</string>
<string name="projects_list_toast_sync_failed">Falha ao sincronizar projetos</string>
<string name="projects_list_toast_delete_failed">Could not delete the project. Check that its folder is accessible and try again.</string>
<string name="projects_list_item_more_button">Mais</string>
<string name="projects_list_card_words">%1$s palavras</string>
<string name="projects_list_card_by_author">por %1$s</string>

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="reader_mode_section">Reader</string>
<string name="reader_mode_position">Chapter %1$d of %2$d</string>
<string name="reader_mode_edit">Edit ↗</string>
<string name="reader_mode_close">× Close</string>
<string name="reader_mode_decrease_font_size">Diminuir o tamanho do texto</string>
<string name="reader_mode_increase_font_size">Aumentar o tamanho do texto</string>
<string name="reader_mode_reset_font_size">Redefinir o tamanho do texto</string>
<string name="reader_mode_toggle_scene_headings">Show scene headings</string>
<string name="reader_mode_previous_chapter">← Previous chapter</string>
<string name="reader_mode_next_chapter">Next chapter →</string>
<string name="reader_mode_empty_chapter">This chapter has no scenes yet</string>
<string name="reader_mode_empty_story">There is nothing to read yet</string>
<string name="reader_mode_end">End of story</string>
</resources>

View file

@ -24,6 +24,7 @@
<string name="scene_editor_menu_item_archive">Arquivar</string>
<string name="scene_editor_menu_item_save">Salvar</string>
<string name="scene_editor_menu_item_close">Fechar</string>
<string name="scene_editor_menu_item_read">Read</string>
<string name="scene_editor_add_to_dictionary">Add to dictionary</string>
<string name="archive_scene_dialog_title">Arquivar Cena</string>
<string name="archive_scene_dialog_message">Tem certeza que deseja arquivar '%1$s'? A cena será ocultada da lista de cenas, mas pode ser restaurada mais tarde.</string>

View file

@ -16,7 +16,8 @@
<string name="create_sceneitem_dialog_create_button">Criar</string>
<string name="create_sceneitem_dialog_dismiss_button">Cancelar</string>
<string name="scene_list_outline_overview_title">Esboço da história</string>
<string name="scene_list_outline_overview_button">Visão geral</string>
<string name="scene_list_outline_button">Esboço</string>
<string name="scene_list_reader_button">Read</string>
<string name="scene_list_outline_overview_none">&lt;sem esboço&gt;</string>
<string name="scene_list_outline_dismiss">Descartar</string>
<string name="scene_list_outline_overview_eyebrow">Manuscrito</string>

View file

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Hammer</string>
<string name="save_all_toast">Saved all Scenes</string>
<string name="unsaved_scenes_dialog_title">Unsaved Scenes</string>
<string name="unsaved_scenes_dialog_message">What should be done with unsaved scenes?</string>
<string name="unsaved_encyclopedia_dialog_title">Unsaved Encyclopedia Entry</string>
<string name="unsaved_encyclopedia_dialog_message">Discard entry?</string>
<string name="unsaved_notes_dialog_title">Unsaved Note</string>
<string name="unsaved_notes_dialog_message">Discard note?</string>
<string name="unsaved_timeline_dialog_title">Unsaved Timeline Event</string>
<string name="unsaved_timeline_dialog_message">Discard event?</string>
<string name="unsaved_dialog_negative_button">Cancel</string>
<string name="unsaved_dialog_positive_button">Discard and close</string>
<string name="unsaved_entity_dialog_negative_button">Discard</string>
<string name="unsaved_entity_dialog_neutral_button">Cancel</string>
<string name="unsaved_entity_dialog_positive_button">Save</string>
<string name="close_dialog_button">Close</string>
<string name="more_menu_button">More</string>
<string name="network_request_failure_parse_body">Failed to parse response body: %1$s</string>
<string name="confirm_dialog_positive">Yes</string>
<string name="confirm_dialog_negative">No</string>
<string name="expand">Expand</string>
<string name="collapse">Collapse</string>
<string name="markdown_format_bar_text_size">Text size</string>
<string name="markdown_format_bar_decrease_text_size">Decrease text size</string>
<string name="markdown_format_bar_increase_text_size">Increase text size</string>
<string name="markdown_format_bar_reset_text_size">Reset text size</string>
<string name="markdown_format_bar_find_replace">Find and replace</string>
<string name="markdown_format_bar_bold">Bold</string>
<string name="markdown_format_bar_italic">Italic</string>
<string name="markdown_format_bar_strikethrough">Strikethrough</string>
<string name="markdown_format_bar_heading">Heading</string>
<string name="markdown_format_bar_blockquote">Quote</string>
<string name="markdown_format_bar_bullet_list">Bulleted list</string>
<string name="markdown_format_bar_numbered_list">Numbered list</string>
<string name="markdown_format_bar_horizontal_rule">Horizontal rule</string>
<string name="markdown_format_bar_undo">Undo</string>
<string name="markdown_format_bar_redo">Redo</string>
<string name="form_dialog_keyboard_hint">ESC cancel</string>
</resources>

View file

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="about_description">A simple tool for building stories.</string>
<string name="about_studio_header">Studio</string>
<string name="about_community_studio_attribution">Hammer is made by Dark Rock Studios, alongside a family of other privacy-respecting, open source apps.</string>
<string name="about_community_header">Community</string>
<string name="about_community_discord_link">Discord</string>
<string name="about_community_reddit_link">Reddit</string>
<string name="about_community_github_link">Github</string>
<string name="about_attribution_header">Attribution</string>
<string name="about_attribution_libraries_button">Libraries</string>
<string name="about_attribution_studio_button">Dark Rock Studios</string>
<string name="about_version_changes_button">Changes</string>
<string name="about_version_github_button">GitHub Release</string>
<string name="about_update_check_button">Check for Updates</string>
<string name="about_update_checking">Checking for updates&#8230;</string>
<string name="about_update_up_to_date">Hammer is up to date.</string>
<string name="about_update_available">Hammer %1$s is available.</string>
<string name="about_update_install_button">Update to %1$s</string>
<string name="about_update_downloading">Downloading Hammer %1$s (%2$s)</string>
<string name="about_update_installing">Installing Hammer %1$s. Hammer will restart.</string>
<string name="about_update_failed">Could not update to %1$s: %2$s</string>
<string name="about_update_retry_button">Retry</string>
<string name="about_update_later_button">Later</string>
<string name="about_update_quit_notice">Hammer will quit to install the update, then reopen.</string>
<string name="changelog_dialog_title">What's New</string>
<string name="update_dialog_release_notes_section">Release notes</string>
<string name="update_dialog_release_notes_empty">No release notes provided.</string>
<string name="update_dialog_open_release_button">Open Release</string>
<string name="about_logs_header">Application Logs</string>
<string name="about_logs_directory_label">Log Directory:</string>
<string name="about_logs_export_button">Export Logs</string>
<string name="about_logs_no_logs">No log files found</string>
<string name="about_logs_open_tooltip">Open log directory</string>
<string name="about_logs_export_success">Logs exported.</string>
<string name="about_logs_export_failed">Could not export the logs. See the log directory.</string>
<string name="about_logs_copy_diagnostics_button">Copy Diagnostics</string>
<string name="about_logs_copy_diagnostics_success">Diagnostics copied to the clipboard.</string>
<string name="about_logs_copy_diagnostics_failed">Could not copy the diagnostics.</string>
</resources>

View file

@ -0,0 +1,146 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="account_settings_breadcrumb">ACCOUNT</string>
<string name="account_settings_subtitle_local_install">LOCAL INSTALL</string>
<string name="account_settings_folio_caption">ACCOUNT SETTINGS</string>
<string name="settings_backups_header">Backups</string>
<string name="settings_backups_explainations">This backup system is meant to protect against data loss and corruption. It is NOT intended as a way to keep older revisions of your stories. Please use the Drafts system for that.
</string>
<string name="settings_backups_manage_button">Manage Backups</string>
<string name="settings_theme_label">UI Theme</string>
<string name="settings_theme_light">LIGHT</string>
<string name="settings_theme_dark">DARK</string>
<string name="settings_theme_oled_black">OLED</string>
<string name="settings_theme_followsystem">SYSTEM</string>
<string name="settings_initial_screen_label">Initial Project Screen</string>
<string name="settings_initial_screen_home">Home</string>
<string name="settings_initial_screen_editor">Scene Editor</string>
<string name="settings_projects_directory">Projects Directory</string>
<string name="settings_projects_directory_description">Set the directory Hammer will use to store project data in.
</string>
<string name="settings_projects_directory_button">Select Dir</string>
<string name="settings_example_project_header">Example Project</string>
<string name="settings_example_project_description">Install an example project to see how different features are used.
</string>
<string name="settings_example_project_button">Reinstall</string>
<string name="settings_example_project_success_message">Example project created.</string>
<string name="settings_server_status_connected">CONNECTED</string>
<string name="settings_server_status_not_connected">NOT CONNECTED</string>
<string name="settings_server_url_greeble">URL</string>
<string name="settings_server_help_link">HELP ↗</string>
<string name="settings_server_description">Configure data syncing</string>
<string name="settings_server_setup_button">Set Up</string>
<string name="settings_server_auto_sync">Auto-Sync</string>
<string name="settings_server_sync_backup">Backup on sync</string>
<string name="settings_server_sync_auto_close">Close Sync Dialogs on Success</string>
<string name="settings_server_max_backups">Max Backups per Project</string>
<string name="settings_server_max_backups_error">Must be between 1 and %1$d</string>
<string name="settings_server_remove_server_button">Remove Server</string>
<string name="settings_remove_server_dialog_title">Remove Server</string>
<string name="settings_remove_server_dialog_message">Are you sure you want to remove the server?</string>
<string name="settings_server_setup_title">Set Up Server</string>
<string name="settings_server_setup_url_hint">Server URL: hammer.ink</string>
<string name="settings_server_setup_insecure_warning">Plain HTTP: your email, password and writing travel unencrypted. Only use this on a network you trust.</string>
<string name="settings_server_setup_email_hint">E-Mail: user@example.com</string>
<string name="settings_server_setup_password_hint">Password</string>
<string name="settings_server_setup_password_requirements">%1$d-%2$d characters</string>
<string name="settings_server_setup_password_show">Show password</string>
<string name="settings_server_setup_password_hide">Hide password</string>
<string name="settings_server_setup_login_button">Log In</string>
<string name="settings_server_setup_create_button">Create</string>
<string name="settings_server_setup_cancel_button">Cancel</string>
<string name="merge_projects_dialog_title">Merge existing projects with the server?</string>
<string name="merge_projects_mode_label">PROJECT DATA</string>
<string name="merge_projects_mode_merge">Merge</string>
<string name="merge_projects_mode_replace">Replace</string>
<string name="merge_projects_explain_merge">Hammer will match your local projects to projects already on the server by name. Projects that only exist on the server will be downloaded, and projects that only exist on this device will be uploaded.</string>
<string name="merge_projects_explain_replace">All of your local projects will be deleted and replaced with whatever is on the server.</string>
<string name="merge_projects_continue_button">Continue</string>
<string name="merge_projects_replace_confirm_title">Delete all local projects?</string>
<string name="merge_projects_replace_confirm_message">This permanently deletes every project on this device and replaces them with the projects from the server. This cannot be undone.</string>
<string name="merge_projects_replace_confirm_button">Delete and Replace</string>
<string name="settings_automatic_update_checks">Check for updates automatically</string>
<string name="settings_automatic_update_checks_hint">Looks for a new release on GitHub shortly after launch and every few hours. Nothing is downloaded or installed until you choose to update.</string>
<string name="settings_platform_settings_title">Platform Settings</string>
<string name="settings_platform_settings_empty">No platform-specific settings.</string>
<string name="settings_keep_screen_on">Keep screen on</string>
<string name="settings_enable_dnd_focus_mode_title">Enable Do Not Disturb when in Focus Mode</string>
<string name="settings_dnd_permission_button">Grant DND Permission</string>
<string name="settings_dnd_permission_explanation">Hammer needs permission to enable Do Not Disturb mode when you are in Focus Mode.
</string>
<string name="settings_storage_public_label">Store projects in public storage</string>
<string name="settings_storage_public_hint">Keep your projects in the shared Documents folder so other apps and your file manager can reach them.
</string>
<string name="settings_storage_location_label">Projects Location</string>
<string name="settings_storage_permission_explanation">Hammer needs file access permission to store projects in public storage.
</string>
<string name="settings_storage_permission_button">Grant File Access</string>
<string name="notice_experimental_label">Experimental</string>
<string name="settings_spellcheck_heading">Spell Checking</string>
<string name="settings_spellcheck_notice">If it is causing problems just turn it off.</string>
<string name="settings_spellcheck_enable">Enable spell checking</string>
<string name="settings_spellcheck_in_focus_enable">Enable in focus mode</string>
<string name="settings_spellcheck_encyclopedia_enable">Learn encyclopedia names</string>
<string name="settings_spellcheck_encyclopedia_enable_hint">Entry names and aliases won&#8217;t be marked as misspelled while their project is open</string>
<string name="settings_spellcheck_dictionary">Dictionary</string>
<string name="settings_spellcheck_dictionary_android_info_title">Spell check language</string>
<string name="settings_spellcheck_dictionary_android_info_message">On Android, the spell check language is chosen by your device, not the app. To change it, update your system and keyboard languages in Android Settings.</string>
<string name="settings_server_enable_sync_backup_desc">Set up a server to sync your projects across devices and create secure backups.
</string>
<string name="settings_server_sync_backup_preferences">Sync &amp; Backup Preferences</string>
<string name="settings_server_danger_zone">Danger Zone</string>
<!-- Backup Manager Dialog -->
<string name="backup_manager_error_load_backups">Failed to load backups: %1$s</string>
<string name="backup_manager_error_load_project_backups">Failed to load backups for project: %1$s</string>
<string name="backup_manager_error_delete_backup">Failed to delete backup: %1$s</string>
<string name="backup_manager_error_restore_backup">Failed to restore backup: %1$s</string>
<string name="backup_restore_dialog_title">Restore Backup</string>
<string name="backup_restore_dialog_message">Warning: Restoring this backup will completely replace the current contents of the project. All existing files will be deleted and replaced with the backup data. This action cannot be undone.
</string>
<string name="backup_restore_dialog_confirm">Restore</string>
<string name="backup_restore_dialog_cancel">Cancel</string>
<string name="backup_manager_title">Manage Backups</string>
<string name="backup_manager_no_backups">No backups found</string>
<string name="backup_manager_select_project_hint">Select a project</string>
<string name="backup_manager_no_backups_for_project">No backups for this project</string>
<string name="backup_manager_restore_button">Restore</string>
<string name="backup_manager_delete_button">Delete backup</string>
<string name="backup_manager_show_button">Show in Folder</string>
<string name="backup_manager_export_button">Export</string>
<string name="account_sync_confirm_cancel_title">Cancel Sync?</string>
<string name="account_sync_confirm_cancel_message">Are you sure you want to cancel the sync?</string>
<string name="account_sync_toast_canceled">Sync canceled</string>
<!-- Server Setup Validation Errors -->
<string name="server_setup_error_unknown">An unexpected error occurred. Please try again.</string>
<string name="server_setup_error_invalid_url">Invalid server address. Enter a URL like "hammer.ink" or an IP address.
</string>
<string name="server_setup_error_invalid_email">Please enter a valid email address.</string>
<string name="server_setup_error_password_too_short">Password must be at least 8 characters.</string>
<string name="server_setup_error_password_too_long">Password must be 64 characters or less.</string>
<string name="server_setup_error_password_no_uppercase">Password must contain at least one uppercase letter.
</string>
<string name="server_setup_error_password_no_lowercase">Password must contain at least one lowercase letter.
</string>
<string name="server_setup_error_password_no_number">Password must contain at least one number.</string>
<string name="server_setup_error_password_no_special">Password must contain at least one special character.</string>
<!-- Server Setup Toast Messages -->
<string name="settings_server_setup_toast_success">Server setup successful!</string>
<string name="settings_server_setup_toast_failure">Server setup failed: %1$s</string>
<string name="settings_server_setup_toast_failure_unknown">Server setup failed. Please check your connection and try again.
</string>
<!-- Terms of Service -->
<string name="settings_server_tos_title">Terms of Service</string>
<string name="settings_server_tos_accept">Accept</string>
<string name="settings_server_tos_decline">Decline</string>
<string name="settings_server_tos_declined">You must accept the Terms of Service to create an account.</string>
<!-- Network Errors -->
<string name="server_error_connection_timeout">Could not connect to server. Make sure the server is running and the address is correct.
</string>
<string name="server_error_timeout">The server is not responding. Check your internet connection and try again.
</string>
<string name="server_error_dns">Server not found. Check the server address and your internet connection.</string>
<string name="server_error_connection_generic">Network error connecting to %1$s. Check your connection and try again.
</string>
<string name="server_error_tls">Could not make a secure connection to %1$s. Hammer only connects over HTTPS, so the server needs a valid TLS certificate of its own or a reverse proxy that provides one.</string>
<string name="server_error_network_unavailable">Hammer could not open a network connection on this device, so the request never reached the server. Security software, a VPN, or a firewall may be blocking it.</string>
</resources>

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="account_window_title">Hammer - Project Selection</string>
<string name="project_window_title">Hammer - %1$s</string>
<string name="project_window_menu_file">File</string>
<string name="project_window_menu_item_close">Close Project</string>
<string name="project_window_menu_item_exit">Exit</string>
<string name="jump_list_recent_projects">Recent Projects</string>
<!-- Sandbox first-run projects-directory flow (Mac App Store builds) -->
<string name="sandbox_intro_title">Welcome to Hammer</string>
<string name="sandbox_intro_message">Hammer stores your projects in a folder you choose, so your writing stays in
your hands.\n\nOn the next screen, pick an existing hammer projects folder or create a new one. You can change
this later in Settings.
</string>
<string name="sandbox_picker_title">Choose where to store your Hammer projects</string>
<string name="sandbox_choose_folder_button">Choose Folder</string>
<string name="sandbox_quit_button">Quit</string>
<string name="sandbox_retry_message">Hammer needs a folder to store your projects.\nChoose a folder now, or quit.
</string>
</resources>

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="draft_compare_current_header">Merge with Current</string>
<string name="draft_compare_current_subheader">Copy parts of the draft into here</string>
<string name="draft_compare_current_accept_button">Take Merged</string>
<string name="draft_compare_draft_header">Draft: %1$s</string>
<string name="draft_compare_draft_subheader">Copy desired parts into current, or accept all of this.</string>
<string name="draft_compare_draft_accept_button">Take this whole draft</string>
<string name="draft_compare_tab_title_draft">Draft</string>
<string name="draft_compare_tab_title_current">Current</string>
<string name="draft_list_header">%1$s Drafts:</string>
<string name="draft_list_empty">No Drafts Found</string>
<string name="scene_draft_invalid_name">Invalid name</string>
</resources>

View file

@ -0,0 +1,85 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="encyclopedia_header">Encyclopedia</string>
<string name="encyclopedia_search_hint">Search</string>
<string name="encyclopedia_search_clear_button">Clear</string>
<string name="encyclopedia_category_person">Person</string>
<string name="encyclopedia_category_place">Place</string>
<string name="encyclopedia_category_thing">Thing</string>
<string name="encyclopedia_category_event">Event</string>
<string name="encyclopedia_category_idea">Idea</string>
<string name="encyclopedia_browse_list_empty">No Entries Found</string>
<string name="encyclopedia_create_button">Create Entry</string>
<string name="encyclopedia_create_entry_header">Create entry</string>
<string name="encyclopedia_create_entry_section_marker">New</string>
<string name="encyclopedia_create_entry_draft_marker">Draft</string>
<string name="encyclopedia_create_entry_name_label">Name</string>
<string name="encyclopedia_create_entry_name_placeholder">e.g. Cheshire Cat</string>
<string name="encyclopedia_create_entry_tags_label">Tags</string>
<string name="encyclopedia_create_entry_tags_hint">↵ to add</string>
<string name="encyclopedia_create_entry_tags_placeholder">ritual, magical, recurring…</string>
<string name="encyclopedia_create_entry_description_label">Description</string>
<string name="encyclopedia_create_entry_cover_art_label">Cover Art</string>
<string name="encyclopedia_create_entry_image_drop_hint">Drop image · or</string>
<string name="encyclopedia_create_entry_image_browse_button">Browse files</string>
<string name="encyclopedia_create_entry_image_attached">1 attached</string>
<string name="encyclopedia_create_entry_image_replace">Replace</string>
<string name="encyclopedia_create_entry_image_too_large">Image is too large. Maximum size is %1$d MB.</string>
<string name="encyclopedia_create_entry_image_load_failed">Could not read the selected image.</string>
<string name="encyclopedia_create_entry_create_button">Create entry</string>
<string name="encyclopedia_create_entry_cancel_button">Cancel</string>
<string name="encyclopedia_create_entry_toast_success">Entry Created</string>
<string name="encyclopedia_create_entry_toast_too_long">Entry Name was too long. Max %1$d</string>
<string name="encyclopedia_create_entry_toast_invalid_name">The entry name contains invalid characters</string>
<string name="encyclopedia_create_entry_toast_tag_too_long">Tag is too long. Max %1$d</string>
<string name="encyclopedia_create_entry_toast_alias_too_long">Alias is too long. Max %1$d</string>
<string name="encyclopedia_entry_aliases_label">Aliases</string>
<string name="encyclopedia_entry_dictionary_toggle_label">Learn in spell check</string>
<string name="encyclopedia_entry_dictionary_toggle_hint">This entry&#8217;s name and aliases won&#8217;t be marked as misspelled</string>
<string name="encyclopedia_entry_add_alias_dialog_title">Add Alias</string>
<string name="encyclopedia_entry_add_alias_button">Add</string>
<string name="encyclopedia_entry_alias_hint">Nickname or other name</string>
<string name="encyclopedia_entry_appears_in_label">Appears in scenes</string>
<string name="encyclopedia_entry_appears_in_empty">Not yet referenced in any scene</string>
<string name="encyclopedia_create_entry_toast_name_too_short">Entry name cannot be empty</string>
<string name="encyclopedia_create_entry_discard_title">Discard New Entry?</string>
<string name="encyclopedia_entry_load_error">Error: Failed to load entry!</string>
<string name="encyclopedia_entry_body_empty_label">Click to enter description</string>
<string name="encyclopedia_entry_edit_cancel_button">Cancel</string>
<string name="encyclopedia_entry_edit_save_button">Save</string>
<string name="encyclopedia_entry_edit_save_toast">Entry Saved</string>
<string name="encyclopedia_entry_discard_title">Discard Changes?</string>
<string name="encyclopedia_entry_discard_message">You will lose any changes you have made.
</string>
<string name="encyclopedia_entry_delete_title">Delete Entry?</string>
<string name="encyclopedia_entry_delete_message">This cannot be undone!</string>
<string name="encyclopedia_entry_delete_toast">Entry Deleted</string>
<string name="encyclopedia_entry_delete_image_title">Delete Image?</string>
<string name="encyclopedia_entry_delete_image_message">This cannot be undone!</string>
<string name="encyclopedia_entry_name_hint">Name</string>
<string name="encyclopedia_entry_close_button">Close Entry</string>
<string name="encyclopedia_entry_menu_group">Entry</string>
<string name="encyclopedia_entry_menu_delete">Delete Entry</string>
<string name="encyclopedia_entry_menu_remove_image">Remove Image</string>
<string name="encyclopedia_entry_menu_add_image">Add Image</string>
<string name="encyclopedia_entry_add_tags_dialog_title">Add Tags</string>
<string name="encyclopedia_entry_add_tags_button">Add Tags</string>
<string name="encyclopedia_entry_crumb_root">Encyclopedia</string>
<string name="encyclopedia_entry_edit_button">Edit</string>
<string name="encyclopedia_entry_particulars_label">Particulars</string>
<string name="encyclopedia_entry_particulars_type">Type</string>
<string name="encyclopedia_entry_particulars_tags">Tags</string>
<string name="encyclopedia_entry_particulars_aliases">Aliases</string>
<string name="encyclopedia_entry_particulars_scenes">Scenes</string>
<string name="encyclopedia_entry_tags_label">Tags</string>
<string name="encyclopedia_entry_add_tag_chip">tag</string>
<string name="encyclopedia_entry_add_alias_chip">alias</string>
<string name="encyclopedia_entry_figure_caption">Fig. I · Entry</string>
<string name="encyclopedia_entry_figure_label">Fig. I</string>
<string name="encyclopedia_entry_figure_add">+ Add</string>
<string name="encyclopedia_entry_folio_format">Fol. %1$s · Ent-%2$s</string>
<string name="encyclopedia_entry_scene_index_format">Sc.%1$s</string>
<string name="encyclopedia_entry_colophon_label">Entry · Catalogue card</string>
<string name="encyclopedia_entry_colophon_label_compact">Edited</string>
<string name="encyclopedia_entry_colophon_end">End of entry ✦</string>
</resources>

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="global_search_button">Search project</string>
<string name="global_search_title">Search project</string>
<string name="global_search_placeholder">Type to search, use #tag to filter…</string>
<string name="global_search_clear">Clear search</string>
<string name="global_search_no_results">No results</string>
<string name="global_search_too_short">Type at least 2 characters to search</string>
<string name="global_search_results_count">%1$d results</string>
<string name="global_search_source_scene">Scene</string>
<string name="global_search_source_note">Note</string>
<string name="global_search_source_encyclopedia">Encyclopedia</string>
<string name="global_search_source_timeline">Timeline</string>
<string name="global_search_filter_all">All</string>
<string name="global_search_filter_scenes">Scenes</string>
<string name="global_search_filter_notes">Notes</string>
<string name="global_search_filter_encyclopedia">Encyclopedia</string>
<string name="global_search_filter_timeline">Timeline</string>
</resources>

View file

@ -0,0 +1,83 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="project_select_nav_story_ideas">Story Ideas</string>
<string name="project_select_nav_story_ideas_short">IDEA</string>
<string name="ideas_header">Story Ideas</string>
<string name="ideas_header_meta">%1$d IDEAS · %2$d TAGS</string>
<string name="ideas_list_empty">No story ideas yet. Capture your first spark.</string>
<string name="ideas_list_empty_archived">No archived ideas.</string>
<string name="ideas_word_count_short">%1$d W</string>
<string name="ideas_tag_count_short">%1$d TAGS</string>
<string name="ideas_search_placeholder">Search ideas</string>
<string name="ideas_search_button">Search ideas</string>
<string name="ideas_search_clear">Clear search</string>
<string name="ideas_search_close">Close search</string>
<string name="ideas_filter_all">All</string>
<string name="ideas_filter_archived">Archived</string>
<string name="ideas_filter_filtered">FILTERED %1$d / %2$d</string>
<string name="ideas_filter_clear_all">Clear All</string>
<string name="ideas_sort_label">SORT</string>
<string name="ideas_sort_newest">Newest</string>
<string name="ideas_sort_oldest">Oldest</string>
<string name="ideas_sort_title_az">Title A–Z</string>
<string name="ideas_sort_glyph_date_desc">↓ DATE</string>
<string name="ideas_sort_glyph_date_asc">↑ DATE</string>
<string name="ideas_sort_glyph_title_az">↓ A–Z</string>
<string name="ideas_stamp_promoted">PROMOTED · %1$s</string>
<string name="ideas_stamp_archived">ARCHIVED · %1$s</string>
<string name="ideas_create_fab">Capture a new idea</string>
<string name="ideas_create_marker">New Idea</string>
<string name="ideas_view_header">Story Idea</string>
<string name="ideas_view_label_editing">Editing</string>
<string name="ideas_view_status_unsaved">UNSAVED</string>
<string name="ideas_view_crumb_root">STORY IDEAS</string>
<string name="ideas_view_close_button">Close idea</string>
<string name="ideas_view_action_edit">Edit</string>
<string name="ideas_title_label">Title</string>
<string name="ideas_title_placeholder">Optional title</string>
<string name="ideas_tags_label">Tags</string>
<string name="ideas_tags_hint">Enter or comma adds a tag</string>
<string name="ideas_tags_placeholder">Add tag…</string>
<string name="ideas_editor_counter">%1$d / %2$d CH</string>
<string name="ideas_create_button">Create</string>
<string name="ideas_save_button">Save</string>
<string name="ideas_cancel_button">Cancel</string>
<string name="ideas_delete_button">Delete</string>
<string name="ideas_archive_button">Archive</string>
<string name="ideas_unarchive_button">Unarchive</string>
<string name="ideas_toast_created">Idea captured</string>
<string name="ideas_toast_saved">Idea saved</string>
<string name="ideas_toast_deleted">Idea deleted</string>
<string name="ideas_toast_archived">Idea archived</string>
<string name="ideas_toast_unarchived">Idea unarchived</string>
<string name="ideas_toast_empty">Idea cannot be empty</string>
<string name="ideas_toast_too_long">Idea is too long: %1$d characters max</string>
<string name="ideas_toast_tag_too_long">Tags can be at most %1$d characters</string>
<string name="ideas_promote_button">Create Project</string>
<string name="ideas_promote_dialog_title">Create Project from Idea</string>
<string name="ideas_promote_dialog_message">A new project will be created and this idea will be added to it as its first note.</string>
<string name="ideas_toast_promoted">Project created: %1$s</string>
<string name="ideas_toast_promote_failed">Failed to create project</string>
<string name="ideas_delete_dialog_title">Delete Idea</string>
<string name="ideas_delete_dialog_message">This idea will be permanently deleted. This cannot be undone.</string>
<string name="ideas_discard_dialog_title">Discard Changes</string>
<string name="ideas_discard_dialog_message">You have unsaved changes. Discard them?</string>
<string name="sync_log_ideas_begin">Syncing story ideas</string>
<string name="sync_log_ideas_unchanged">Story ideas unchanged — skipped</string>
<string name="sync_log_ideas_complete">Story ideas sync complete</string>
<string name="sync_log_ideas_state_failed">Failed to fetch story ideas state: %1$s</string>
<string name="sync_log_ideas_deleted_local">Removed deleted idea: %1$s</string>
<string name="sync_log_ideas_deleted_server">Propagated idea deletion to server: %1$s</string>
<string name="sync_log_ideas_delete_server_failed">Failed to delete idea on server: %1$s</string>
<string name="sync_log_ideas_uploaded">Uploaded idea: %1$s</string>
<string name="sync_log_ideas_upload_failed">Failed to upload idea "%1$s": %2$s</string>
<string name="sync_log_ideas_downloaded">Downloaded idea: %1$s</string>
<string name="sync_log_ideas_download_failed">Failed to download idea %1$s: %2$s</string>
<string name="sync_log_ideas_conflict">Conflict detected for idea: %1$s</string>
<string name="sync_log_ideas_conflict_resolved">Resolved conflict for idea: %1$s</string>
<string name="sync_log_ideas_conflict_skipped">Conflict left unresolved for idea: %1$s</string>
<string name="ideas_conflict_title">Story Idea Conflict</string>
<string name="ideas_conflict_field_title">Title</string>
<string name="ideas_conflict_field_tags">Tags</string>
<string name="ideas_conflict_field_content">Content</string>
</resources>

View file

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="notes_header">Notes</string>
<string name="notes_list_empty">No Notes Found</string>
<string name="notes_create_note_button">Create Note</string>
<string name="notes_note_item_action_cancel">Cancel</string>
<string name="notes_discard_dialog_title">Discard Changes?</string>
<string name="notes_discard_dialog_message">You will lose any changes you have made.</string>
<string name="notes_create_header">Create Note</string>
<string name="notes_create_marker">III · NEW</string>
<string name="notes_create_draft_label">DRAFT</string>
<string name="notes_create_create_button">Create</string>
<string name="notes_create_cancel_button">Cancel</string>
<string name="notes_create_toast_success">Note Created</string>
<string name="notes_create_toast_too_long">Note was too long</string>
<string name="notes_create_toast_empty">Note was empty</string>
<string name="notes_create_toast_tag_too_long">Tag was too long (max %1$d)</string>
<string name="notes_create_tags_label">Tags</string>
<string name="notes_create_tags_hint">↵ to add</string>
<string name="notes_create_tags_placeholder">idea, todo, follow-up…</string>
<string name="notes_delete_title">Delete Note?</string>
<string name="notes_delete_message">This action cannot be undone!</string>
<string name="notes_delete_toast_success">Note Deleted</string>
<string name="notes_menu_group">Note</string>
<string name="notes_menu_delete">Delete</string>
<string name="notes_view_header">Note</string>
<string name="notes_view_label_editing">Editing</string>
<string name="notes_view_close_button">Close Note</string>
<string name="notes_view_crumb_root">Notes</string>
<string name="notes_view_action_edit">Edit</string>
<string name="notes_view_action_save">Save</string>
<string name="notes_view_status_unsaved">UNSAVED</string>
<string name="notes_view_status_word_char">%1$d W · %2$d CH</string>
<string name="notes_view_status_hint">ESC TO CANCEL</string>
<string name="notes_search_button">Search notes</string>
<string name="notes_search_placeholder">Search notes</string>
<string name="notes_search_close">Close search</string>
<string name="notes_search_clear">Clear search</string>
<string name="notes_header_meta">%1$d NOTES · %2$d TAGS</string>
<string name="notes_word_count_short">%1$d W</string>
<string name="notes_filter_all">ALL</string>
<string name="notes_filter_filtered">FILTERED · %1$d OF %2$d</string>
<string name="notes_filter_clear_all">CLEAR ALL</string>
<string name="notes_sort_label">SORT</string>
<string name="notes_sort_newest">Newest</string>
<string name="notes_sort_oldest">Oldest</string>
<string name="notes_sort_longest">Longest</string>
<string name="notes_sort_shortest">Shortest</string>
<string name="notes_sort_title_az">Title A–Z</string>
<string name="notes_sort_glyph_date_desc">↓ DATE</string>
<string name="notes_sort_glyph_date_asc">↑ DATE</string>
<string name="notes_sort_glyph_words_desc">↓ WORDS</string>
<string name="notes_sort_glyph_words_asc">↑ WORDS</string>
<string name="notes_sort_glyph_title_az">↓ A–Z</string>
</resources>

View file

@ -0,0 +1,127 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="project_home_stat_created">Created: %1$s</string>
<string name="project_home_loading_stats">Calculating statistics...</string>
<string name="project_home_refresh_stats_button">Refresh statistics</string>
<string name="project_home_stats_dirty_indicator">Statistics may be out of date</string>
<string name="project_home_stat_num_scenes">Scenes</string>
<string name="project_home_stat_total_words">Total Words</string>
<string name="project_home_stat_encyclopedia_entries">Encyclopedia Entries</string>
<string name="project_home_stat_chapter_words">Words in Chapters</string>
<string name="project_home_stat_chapter_words_x_axis">Chapter</string>
<string name="project_home_stat_chapter_words_tooltip">%1$s: %2$s words</string>
<string name="project_home_stat_avg_words_per_scene">Avg Words / Scene</string>
<string name="project_home_stat_num_notes">Notes</string>
<string name="project_home_stat_num_timeline_events">Timeline Events</string>
<string name="project_home_stat_longest_scene">Longest Scene</string>
<string name="project_home_stat_longest_scene_words">%1$s words</string>
<string name="project_home_stat_longest_scene_empty">No scenes yet</string>
<string name="project_home_stat_last_edited_scene">Last Edited</string>
<string name="project_home_stat_scene_median">Median</string>
<string name="project_home_stat_reading_time">≈ %1$d min reading</string>
<string name="project_home_stat_pages">%1$d pages</string>
<string name="project_home_stat_daily_goal">Daily goal</string>
<string name="project_home_stat_weekly_goal">Weekly goal</string>
<string name="project_home_stat_this_week">This week</string>
<string name="project_home_stat_week_change_flat">no change vs last week</string>
<string name="project_home_stat_week_change_new">no data last week</string>
<string name="project_home_stat_today">Today</string>
<string name="project_home_stat_daily_avg">Daily avg</string>
<string name="project_home_stat_streak">Streak</string>
<string name="project_home_stat_streak_days">%1$d days</string>
<string name="project_home_stat_longest_streak">Longest: %1$d</string>
<string name="project_home_stat_days_written">Days written</string>
<string name="project_home_stat_best_day">Best day</string>
<string name="project_home_stat_best_day_value">%1$s · %2$s</string>
<string name="project_home_stat_characters_appearances">Characters by Appearances</string>
<string name="project_home_stat_connections">%1$d connections</string>
<string name="project_home_stat_words_per_device">Words by Device</string>
<string name="project_home_stat_activity">Activity · %1$d weeks</string>
<string name="project_home_stat_activity_tooltip">%1$s · %2$s words</string>
<string name="project_home_stat_activity_tooltip_empty">%1$s · no writing</string>
<string name="project_home_stat_avg_weekday">Avg weekday</string>
<string name="project_home_stat_avg_weekend">Avg weekend</string>
<string name="project_home_stat_chapter_words_summary">σ %1$d · min %2$d · max %3$d</string>
<string name="project_home_stat_donut_entries">entries</string>
<string name="project_home_stat_donut_tooltip">%1$s · %2$s entries</string>
<string name="project_home_stat_themes_title">Themes</string>
<string name="project_home_stat_themes_summary">%1$d tags · %2$d uses</string>
<string name="project_home_stat_themes_breadth">%1$d/4</string>
<string name="project_home_stat_themes_most_connective">Most connective</string>
<string name="project_home_stat_themes_all_four">Appears in all four types · %1$d uses</string>
<string name="project_home_stat_themes_breadth_of_four">Appears in %1$d of 4 types · %2$d uses</string>
<string name="project_home_stat_themes_all_four_short">All 4 types</string>
<string name="project_home_stat_themes_breadth_of_four_short">%1$d of 4 types</string>
<string name="project_home_stat_themes_distribution">Distribution</string>
<string name="project_home_stat_themes_showing">Showing %1$d of %2$d</string>
<string name="project_home_stat_themes_columns_legend">Rank · Tag · Breadth · Uses</string>
<string name="project_home_stat_themes_source_scenes">Scenes</string>
<string name="project_home_stat_themes_source_notes">Notes</string>
<string name="project_home_stat_themes_source_encyclopedia">Encyclopedia</string>
<string name="project_home_stat_themes_source_events">Events</string>
<string name="project_home_action_export">Export Story</string>
<string name="project_home_action_sync">Sync Story</string>
<string name="project_home_action_backup">Backup</string>
<string name="project_home_action_backup_toast_success">Backup Created: %1$s</string>
<string name="project_home_action_backup_toast_failure">Backup Failed</string>
<string name="project_home_action_export_toast_success">Story Exported</string>
<string name="project_home_action_export_toast_failure">Export failed</string>
<string name="project_home_export_dialog_title">Export Story</string>
<string name="project_home_export_section">EXPORT</string>
<string name="project_home_export_close">× CLOSE</string>
<string name="project_home_export_chapters_label">Treat top-level scenes as chapters</string>
<string name="project_home_export_format_label">Format</string>
<string name="project_home_export_format_markdown">Markdown (.md)</string>
<string name="project_home_export_format_epub">EPub (.epub)</string>
<string name="project_home_export_format_pdf">PDF (.pdf)</string>
<string name="project_home_export_format_docx">Word (.docx)</string>
<string name="project_home_export_format_rtf">RTF (.rtf)</string>
<string name="project_home_export_cancel">Cancel</string>
<string name="project_home_export_execute">Export</string>
<string name="project_home_export_number_chapters_label">Number chapter titles</string>
<string name="project_home_export_font_label">Font</string>
<string name="project_home_export_blank_lines_label">Keep blank lines between paragraphs</string>
<string name="project_home_export_blank_lines_hint">Turn off if you separate paragraphs with a blank line and do not want empty paragraphs in the export. Scene breaks and runs of two or more blank lines are kept.</string>
<string name="project_home_export_limit_scenes_label">Limit to specific scenes</string>
<string name="project_home_export_limit_scenes_hint">Only the selected scenes will be included in the export</string>
<string name="project_home_export_scenes_label">Scenes</string>
<string name="project_home_export_scenes_selected">%1$d of %2$d selected</string>
<string name="project_home_export_scenes_select_all">Select all</string>
<string name="project_home_export_scenes_clear_all">Clear all</string>
<string name="project_home_export_help_icon_description">Learn about export formats</string>
<string name="project_home_export_help_title">Choosing an Export Format</string>
<string name="project_home_export_help_intro">Each format suits a different purpose. Pick the one that matches how you plan to use the exported story.</string>
<string name="project_home_export_help_format_markdown">Plain text with light formatting; ideal for version control, re-importing, or other writing tools.</string>
<string name="project_home_export_help_format_epub">The standard e-book format; best for beta readers reading on e-readers and phones with reflowable text.</string>
<string name="project_home_export_help_format_pdf">A fixed page layout that looks the same everywhere; best for printing or sharing a final, read-only copy.</string>
<string name="project_home_export_help_format_docx">Opens in Word and Google Docs; best for editors and beta readers who leave comments or track changes.</string>
<string name="project_home_export_help_format_rtf">A broadly compatible rich-text format; a safe choice when you are unsure which word processor the recipient uses.</string>
<string name="project_home_export_help_dismiss">Got it</string>
<string name="project_home_export_contents_title">Contents</string>
<string name="project_home_export_byline">by %1$s</string>
<string name="project_home_action_import_toast_success">Story Imported</string>
<string name="project_home_action_import_toast_failure">Import failed</string>
<string name="project_home_import_dialog_title">Import Story</string>
<string name="project_home_import_heading_label">Chapter detection</string>
<string name="project_home_import_heading_auto">Auto</string>
<string name="project_home_import_heading_h1">H1 (#)</string>
<string name="project_home_import_heading_h2">H2 (##)</string>
<string name="project_home_import_split_label">Split into scenes</string>
<string name="project_home_import_split_formatting">Formatting</string>
<string name="project_home_import_split_pattern">Pattern</string>
<string name="project_home_import_split_single">Single scene</string>
<string name="project_home_import_pattern_label">Chapter pattern (regex)</string>
<string name="project_home_import_create_groups_label">Create chapter groups</string>
<string name="project_home_import_preview_label">Preview</string>
<string name="project_home_import_preview_empty">Nothing to import</string>
<string name="project_home_import_preview_reading">Reading your manuscript</string>
<string name="project_home_import_preview_count">Will create %1$d scene(s)</string>
<string name="project_home_import_large_scene_label">Large scene</string>
<string name="project_home_import_large_scene_one">One scene holds %1$s words, so chapter detection probably missed your chapter titles. Try another detection setting.</string>
<string name="project_home_import_large_scene_many">%1$d scenes hold %2$s words or more, so chapter detection probably missed some chapter titles. Try another detection setting.</string>
<string name="project_home_import_scene_word_count">%1$s W</string>
<string name="project_home_import_cancel">Cancel</string>
<string name="project_home_import_execute">Import</string>
<string name="project_home_action_settings_button">Settings</string>
<string name="project_home_menu_button">Project menu</string>
</resources>

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="project_nav_home">Home</string>
<string name="project_nav_scene_editor">Story</string>
<string name="project_nav_notes">Notes</string>
<string name="project_nav_encyclopedia">Encyclopedia</string>
<string name="project_nav_time_line">Timeline</string>
<string name="project_nav_home_short">HOME</string>
<string name="project_nav_scene_editor_short">STRY</string>
<string name="project_nav_notes_short">NOTE</string>
<string name="project_nav_encyclopedia_short">ENCY</string>
<string name="project_nav_time_line_short">TIME</string>
<string name="nav_rail_expand">Expand navigation</string>
<string name="nav_rail_collapse">Collapse navigation</string>
</resources>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="project_select_nav_projects_list">Projects</string>
<string name="project_select_nav_account_settings">Settings</string>
<string name="project_select_nav_about_app">About</string>
<string name="project_select_nav_projects_list_short">PROJ</string>
<string name="project_select_nav_account_settings_short">SETT</string>
<string name="project_select_nav_about_app_short">INFO</string>
<string name="project_libraries_dialog_title">Libraries</string>
</resources>

View file

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="project_select_project_list_empty">No Projects Found</string>
<string name="project_select_card_delete_button">Delete</string>
<string name="project_select_card_rename_button">Rename</string>
<string name="projects_list_sync_button">Sync Projects</string>
<string name="projects_list_create_button">Create Project</string>
<string name="projects_list_toast_sync_complete">Projects synced</string>
<string name="projects_list_toast_sync_failed">Failed to sync projects</string>
<string name="projects_list_toast_delete_failed">Could not delete the project. Check that its folder is accessible and try again.</string>
<string name="projects_list_item_more_button">More</string>
<string name="projects_list_card_words">%1$s words</string>
<string name="projects_list_card_by_author">by %1$s</string>
<string name="projects_list_card_created">Created %1$s</string>
<string name="projects_list_card_content_description">Project %1$s</string>
<string name="projects_list_search_button">Search projects</string>
<string name="projects_list_search_close">Close search</string>
<string name="projects_list_search_placeholder">filter by name or #tag</string>
<string name="projects_list_search_clear">Clear filter</string>
<string name="projects_list_no_matches">No projects match your filter</string>
<string name="projects_list_masthead_title">Hammer · Library</string>
<string name="projects_list_column_title">Title</string>
<string name="projects_list_column_last_open">Last Open</string>
<string name="projects_list_empty_index">— Empty Index —</string>
<string name="projects_list_footer_library">Library</string>
<string name="projects_list_footer_sync_on">Sync · On</string>
<string name="projects_list_footer_local">Local</string>
<string name="projects_list_entry_count_none">0 entries</string>
<string name="projects_list_entry_count_one">1 entry</string>
<string name="projects_list_entry_count_other">%1$d entries</string>
<string name="projects_list_sort_label">SORT</string>
<string name="projects_list_sort_recently_opened">Recently Opened</string>
<string name="projects_list_sort_least_recent">Least Recent</string>
<string name="projects_list_sort_newest">Newest</string>
<string name="projects_list_sort_oldest">Oldest</string>
<string name="projects_list_sort_longest">Longest</string>
<string name="projects_list_sort_shortest">Shortest</string>
<string name="projects_list_sort_glyph_opened_desc">↓ OPENED</string>
<string name="projects_list_sort_glyph_opened_asc">↑ OPENED</string>
<string name="projects_list_sort_glyph_created_desc">↓ CREATED</string>
<string name="projects_list_sort_glyph_created_asc">↑ CREATED</string>
<string name="projects_list_sort_glyph_words_desc">↓ WORDS</string>
<string name="projects_list_sort_glyph_words_asc">↑ WORDS</string>
<string name="account_sync_dialog_header">Account Sync</string>
<string name="account_sync_log_title">Sync Log</string>
<string name="account_sync_log_filter_label">Project</string>
<string name="account_sync_log_filter_all">All</string>
<string name="account_sync_dialog_status_pending">Pending</string>
<string name="account_sync_dialog_status_error">Error</string>
<string name="account_sync_dialog_status_conflict">Conflict</string>
<string name="account_sync_dialog_status_complete">Sync Complete</string>
<string name="account_sync_dialog_status_canceled">Canceled</string>
<string name="delete_project_title">Delete Project</string>
<string name="delete_project_warning">This action cannot be undone. All project data will be permanently deleted.</string>
<string name="delete_project_confirm_hint">Type "%1$s" to confirm</string>
<string name="delete_project_confirm">Delete</string>
<string name="delete_project_cancel">Cancel</string>
<string name="rename_project_title">Rename Project</string>
<string name="rename_project_heading">New Project Name</string>
<string name="rename_project_button">Rename Project</string>
<string name="rename_project_cancel_button">Cancel</string>
<string name="create_project_title">Create Project</string>
<string name="create_project_marker">New</string>
<string name="create_project_meta">Project</string>
<string name="create_project_heading">New Project Name</string>
<string name="create_project_button">Create Project</string>
<string name="create_project_cancel_button">Cancel</string>
<string name="create_project_import_button">Import</string>
<string name="create_project_import_help_icon_description">Learn about importing a story</string>
<string name="create_project_import_help_title">Create from an Existing Story</string>
<string name="create_project_import_help_intro">Already have a manuscript? Import it to create a new project. Hammer names the project after your file and automatically splits the text into scenes.</string>
<string name="create_project_import_help_formats_header">Supported Formats</string>
<string name="create_project_import_help_format_markdown">Markdown (.md)</string>
<string name="create_project_import_help_format_rtf">Rich Text (.rtf)</string>
<string name="create_project_import_help_dismiss">Got it</string>
<string name="create_project_error_already_exists">Project already exists</string>
<string name="create_project_error_null_filename">Missing Project Name</string>
<string name="create_project_error_too_long">Project name too long</string>
<string name="create_project_error_invalid_characters">The project name contains invalid characters</string>
<string name="create_project_error_blank">The project name was left blank</string>
</resources>

Some files were not shown because too many files have changed in this diff Show more