Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]

### Fixed
- `lingo sync` backfills a missing `target_hash` from the current target value,
so hand-added translations (written straight into the YAML, never passing
through `translate` or `accept-edits`) get a baseline and the `manual_edits`
validator can watch them. An existing `target_hash` is still never
recomputed — that would silently absorb hand-edit drift.

### Fixed (0.4.0)
- `lingo sync` no longer destroys hand-edit protection: it now only refreshes
each entry's `source_hash` and preserves `target_hash` and `manual: true`
(the pre-extraction `bin/translate` behavior). Previously a single sync wiped
Expand Down
7 changes: 4 additions & 3 deletions docs/app/views/docs/pages/commands.rb
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,10 @@ def sync
Refreshes the `.i18n-state/` drift state from the current locale files —
for initial setup on an existing app, or after editing source strings, so
`validate` doesn't report spurious "outdated" keys. It updates each key's
`source_hash` and prunes entries whose keys were removed; `target_hash`
and `manual` flags are always preserved, so hand-edit protection never
depends on when sync last ran.
`source_hash`, backfills a `target_hash` baseline for entries that lack
one (hand-added translations), and prunes entries whose keys were
removed. An existing `target_hash` and all `manual` flags are always
preserved, so hand-edit protection never depends on when sync last ran.
MD
end
end
Expand Down
10 changes: 6 additions & 4 deletions docs/app/views/docs/pages/drift_state.rb
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,12 @@ def syncing
md <<~'MD'
Adopting locallingo on an existing app, or editing a batch of source
strings, can leave the state out of step with the files. `lingo sync`
refreshes each key's `source_hash` from the current translations and
prunes entries for removed keys, so nothing reads as spuriously outdated.
It never touches `target_hash` or `manual` — hand-edit drift stays
visible until you resolve it with `accept-edits`.
refreshes each key's `source_hash` from the current translations, records
a `target_hash` baseline for entries that have none (translations added
straight to the YAML by hand), and prunes entries for removed keys, so
nothing reads as spuriously outdated. It never rewrites an existing
`target_hash` or any `manual` flag — hand-edit drift stays visible until
you resolve it with `accept-edits`.
MD
DocsUI::Callout(:note) do
plain "Run "
Expand Down
17 changes: 11 additions & 6 deletions lib/locallingo/manager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -134,11 +134,12 @@ def source_hash
format("%08x", Zlib.crc32(load_source_translations.to_json))
end

# Refresh source hashes from the current translation files and prune state
# for keys that no longer exist. Existing `target_hash`/`manual` fields are
# preserved — hand-edit protection is never dropped by a sync; use
# `accept_edits!` to resolve hand-edit drift explicitly. Returns the
# combined state.
# Refresh source hashes from the current translation files, backfill a
# `target_hash` baseline for entries that lack one, and prune state for
# keys that no longer exist. An existing `target_hash` and the `manual`
# flag are never touched — hand-edit protection is never dropped by a
# sync; use `accept_edits!` to resolve hand-edit drift explicitly.
# Returns the combined state.
def sync_state!
source = load_source_translations

Expand Down Expand Up @@ -171,12 +172,16 @@ def sync_locale_state(source, locale)
target = load_locale_translations(locale)
locale_state = @state.load(locale)

target.each_key do |key|
target.each do |key, value|
next unless source[key]

existing = locale_state[key]
entry = existing.is_a?(Hash) ? existing.dup : {}
entry["source_hash"] = @state.hash(source[key])
# Backfill a baseline for hand-added translations so the manual-edits
# validator can watch them; an existing target_hash is never recomputed —
# that would silently absorb hand-edit drift.
entry["target_hash"] ||= @state.hash(value)
locale_state[key] = entry
end
locale_state.each_key { |key| locale_state.delete(key) unless target.key?(key) }
Expand Down
25 changes: 23 additions & 2 deletions spec/locallingo/manager_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@
end
end

it "leaves bare entries bare (no target_hash or manual invented)" do
it "backfills a missing target_hash from the current value without inventing manual" do
with_app(
config: { "target_locales" => %w[de] },
locales: {
Expand All @@ -186,7 +186,28 @@
described_class.new(config: config_for(root)).sync_state!

entry = read_state(root, "greeting.de.json").fetch("greeting.hi")
expect(entry).to eq("source_hash" => Locallingo::StateStore.hash("Hello"))
expect(entry).to eq(
"source_hash" => Locallingo::StateStore.hash("Hello"),
"target_hash" => Locallingo::StateStore.hash("Hallo")
)
end
end

it "records a full entry for keys with no state at all" do
with_app(
config: { "target_locales" => %w[de] },
locales: {
"en" => { "greeting" => { "hi" => "Hello" } },
"de" => { "greeting" => { "hi" => "Hallo" } }
}
) do |root|
described_class.new(config: config_for(root)).sync_state!

entry = read_state(root, "greeting.de.json").fetch("greeting.hi")
expect(entry).to eq(
"source_hash" => Locallingo::StateStore.hash("Hello"),
"target_hash" => Locallingo::StateStore.hash("Hallo")
)
end
end

Expand Down
Loading