Ver Fonte

Tire hotel: seasonal tire storage, from check-in to the invoice (#256)

* Add tire hotel: seasonal tire storage with shelf capacity tracking

Opt-in module for workshops that store customers' seasonal tires. Off by
default: the sidebar entry, routes and server actions all key off a single
tireHotel.enabled setting, so a shop that does not store tires carries none
of it.

Capacity is counted in individual tires rather than sets throughout. A shelf
that holds eight 15" tires does not hold eight 21" tires, and customers store
anything from a single spare to a five-wheel set, so counting sets would be
wrong in every market except the average one.

Storage layout
- Warehouses hold locations; a location is any addressable slot (shelf, rack
  level, pallet, floor square) with its own tire capacity.
- zone/rack/shelf/position are free text, so "Room B / Rack 4 / Shelf 2" and
  "Container 1 / Pallet 7" both fit. The label code is derived from whichever
  parts are filled in, and stays editable.
- Racks are created as a numbered run in one form submission, since setting up
  a warehouse shelf by shelf is the slowest part of adopting the module.
- The overview shows free tire slots per shelf, per warehouse and overall.

Check-in
- One pass: customer, tires, quantity, then placement. Picking the vehicle
  names its owner, which is what actually happens at the counter.
- The location picker answers "will this fit" before anything is committed:
  every row carries its free count, shelves that cannot take the set stay
  visible but disabled, and suggestions are ordered tightest-fit-first so
  storage stays dense.
- Capacity is re-checked inside the write transaction, so two technicians
  checking in at once cannot both pass and overfill a shelf.

Measurements
- Tread and pressure are stored in one unit each (mm, bar) and converted at
  the edges, so imperial workshops read 32nds and psi off the same number.
- Replacement thresholds are per-organization: legal minimums differ by
  country, and winter tires lose grip well above the summer limit.

Movements (check-in, check-out, relocate, dispose) are logged with
denormalised location codes so history still reads correctly after a shelf is
renamed or deleted.

New PermissionSubject with a separate MANAGE action for the storage layout.
Full translations across all 12 locales.

Schema changes are staged but not migrated.

* Gate tire hotel behind Pro, and fix the missing page header

Two fixes.

Pro gating. Tire hotel is a paid feature now, so it joins PLAN_FEATURES as
tireHotel: false on free and true from Pro up. Self-hosted keeps it, since
that plan already resolves to the full feature set.

Availability is a single question with two halves: does the plan include the
module, and has the workshop opted in. isTireHotelEnabled answers both, so
sidebar, routes and cron all agree, and a plan downgrade hides the module
without clearing the organization's own setting — the tires are still on the
shelves, and upgrading brings the module back as it was. Server actions
separate the two causes: no plan raises the usual upgrade error, plan but no
opt-in raises the plain disabled error. The settings page renders the shared
locked message instead of the toggle, and the settings nav carries the Pro
badge.

Page header. Both tire hotel routes rendered their client straight into the
layout, with no <PageHeader /> and no padding wrapper, so the sidebar trigger
and breadcrumbs were missing and the filter row sat flush against the top of
the viewport. They now follow the same shape as every other page, with
breadcrumb entries registered for the list, the storage layout and the
settings page across all 12 locales.

* Add the tire set detail page, and fix two UI defects

The list rows linked to /tire-hotel/[id], which did not exist — every row
led to a 404. The page is now there, and with it the two actions a stored
set actually needs.

Detail page. Latest condition leads, because that is what a customer asks
about at the counter. Readings are grouped into the rounds they were taken
in rather than listed loose, so a season's arrival and departure sit side by
side and the wear between them is readable. Alongside: the movement history,
the shelf it sits on, the tire specifics, and the owner.

Check-out offers a second measurement round on the way out — the difference
from the arrival reading is the season's wear, and the moment to mention it
is while the customer is still standing there. Relocate excludes the set's
current shelf from the picker, since space it already occupies is not space
it can move into.

Two fixes.

Scrolling. A Radix popover portals out of the dialog it sits in, and a
dialog's scroll lock swallows wheel events over everything outside itself,
which left the customer list unscrollable. The popovers are modal now, so
each gets its own lock with its own content whitelisted. This affected the
same combobox in the quote, reminder and scheduled-message dialogs too, so
all four are fixed. Also dropped a redundant max-h on the check-in dialog
that was competing with the one DialogContent already sets.

Shelf column. Codes are workshop-defined and can run long, and in a
table-fixed layout a cell without truncate spills into its neighbour — the
shelf code was overrunning the condition column. It truncates now, a little
wider, with the full code on hover.

* Add editing and deleting for a tire set

updateTireSet and deleteTireSet were written with the rest of the actions but
never given a UI, so a mistake at check-in could not be corrected — a typo in
a tire size meant living with it.

Edit covers everything recorded at check-in except placement, which stays
with Relocate so the shelf counts and the movement history are only ever
changed by the one path that keeps them in step. Raising the quantity is
checked against the shelf the set is on, so growing a set past what its shelf
holds is refused rather than silently overfilling it.

Delete only appears once a set is off the shelf. The action already refused a
stored set for the same reason, and offering a button that always errors is
worse than not offering it.

* Filter the vehicle list to the selected customer

Picking a customer narrows the vehicle dropdown to that customer's vehicles,
in both the check-in and edit dialogs. With no customer chosen the full list
stays available, so staff can still start from the plate and let the vehicle
name its owner.

A vehicle already selected for someone else is cleared when the customer
changes, since it has just dropped out of the list and leaving it selected
would submit a pairing the form no longer shows. That happens in the change
handler rather than an effect, so re-seeding the edit dialog on open cannot
wipe a pairing that was already saved.

A customer with no vehicles gets a disabled dropdown and a line saying where
to add one, rather than an empty menu that looks broken.

* Add storage agreements and invoicing

There was no way to bill for storage: the shop typed a line into a work
order by hand, with nothing tying it back to the set.

An agreement holds the commercial terms — seasonal or monthly, a price,
optional extras, a start, whether it renews — separately from the set
itself, because the terms and the physical facts have different lifetimes. A
set can sit three seasons on one agreement, or move to new terms without
leaving its shelf, and ending an agreement never disturbs the storage
history.

Each billable period becomes a charge row. That makes "what has been
invoiced" a fact rather than an inference, which is what stops a sweep that
runs twice, or runs late after downtime, from billing a customer twice for
the same season. Periods are derived from what is already recorded rather
than from a cursor on the agreement, so the arithmetic is idempotent by
construction.

Invoicing reuses the existing draft-record path rather than reimplementing
it, so storage picks up the same invoice numbering, tax defaults and
technician resolution as every other job. That helper moved out of the
counter-sale action into a Lib module so both callers share one copy.
Totals go through calculateTotals for the same reason: rolling our own
arithmetic would let a storage line and a parts line disagree about what
inclusive tax means.

Where the charge lands is the shop's call. Separate raises its own invoice;
work-order appends to an open job for the same vehicle so the customer pays
once, falling back to a separate invoice when there is no open job — being
unable to bill is a worse answer than billing on its own document.

Repricing only moves periods nobody has billed yet, and a period that has
reached an invoice can be waived only before it gets there. An invoice is a
document the customer already has.

* Add prep work tracking, and make invoicing an explicit choice

Three things, all aimed at the person who actually works the tire department.

Prep work. A set arrives needing things done to it — washing, balancing, a
TPMS service — and the department's real question is "what is left on this
one". A boolean cannot answer that once someone has started, so each job is
a row with a state, and finishing one records who did it. That is also the
answer to "was this actually washed?" at pickup, which is the complaint the
module exists to prevent.

Only work that applies is offered: a set with no rims is never asked whether
to wash them, and a set with no sensors is never offered a TPMS service.
Noise is what makes a checklist stop being read. Ticking rims on at check-in
adds the rim wash, and unticking removes it, because that is what a shop
means by the gesture.

The list page gains a Prep column and a "Needs prep" filter, which is the
tech's own worklist: everything still waiting, whatever its storage status.
Check-out warns when jobs are outstanding rather than blocking — the shop may
have good reason to release a set anyway, but nobody should do it unaware.

Invoicing. The target is now chosen per charge instead of inferred. The
shop's setting still decides what is preselected, but the right answer
changes case by case: a customer collecting during a service wants one bill,
a customer who only stores wants their own. Open jobs are listed with plate
and total so picking the right one does not mean opening each, and the list
is scoped to the paying customer so another customer's invoice is never
offered.

Columns. Vehicle took a share of the table far larger than a plate and a
model need, while the shelf code — workshop-defined and often long — was
squeezed into less than it needed and spilled into its neighbour. Vehicle is
capped, shelf is wider, and both truncate.

* Link the shelf layout from the tire hotel page

The storage layout was only reachable from Settings, which is the wrong
place to look for it while standing at the counter.

The free-space indicator becomes that link. It already answers "how much room
is left", and "where are the shelves" is the same errand, so one control does
both jobs and reads as interactive instead of as a decorative badge.

A shop with no shelves yet also had no way through: the primary action was
Check in tires, which dead-ends in an empty shelf picker with no explanation.
Until somewhere exists to put tires, the page leads with setting that up, and
the empty list says so rather than implying nothing has been stored yet.

* Add work orders, desk search and a dashboard card; drop the invoice setting

Four changes, all from the same review.

The invoice target was a setting, and it should not have been. Where a
storage charge lands is a decision about this customer on this day, not a
policy: the same shop wants a separate invoice for a storage-only customer
and one bill for someone collecting during a service. The dialog already
asked every time, which made the setting a preselect for a question the user
was answering anyway, so it is gone.

A new work order joins the choice. A plain invoice carries a customer and no
vehicle, which is right for storage-only, but it never reaches the board, the
vehicle history or a technician's day. A work order hangs off the vehicle and
does. Offered only when the set has one, since a work order is defined by the
car it is for.

Tire sets are searchable. The desk question is "where are the Hansen tires",
so sets match on plate, customer, brand, size and shelf code, and the result
leads with the shelf code because that is what someone is about to read out
loud. Stored sets sort first — a released set is history. Only searched when
the module is on, so a shop without it pays nothing for the join.

The dashboard card carries the three numbers a shop acts on: room to say yes
to another customer, work queued for the tire department, and money earned
but not yet invoiced. The prep count links straight to the tech's filtered
worklist. It renders only when the module is enabled, and sits last in the
default layout so rows compact over it for everyone else.

* Fix the dashboard's paired columns, and stop defaulting agreements to renew

Two half cards sat alone in the left column with a hole beside them. The
cause was sms and notifications holding a column each while only one of them
is ever available, which left the right column permanently one card short and
split every pair below it. They share one slot now — they cannot collide,
since only one is rendered — and inspections takes the partner position,
which re-pairs everything after it.

The tire hotel card made this worse in a second way: it returned null when
the module was off, but a card node that renders null still reserves its grid
slot, so every shop without tire storage carried an empty half. Conditional
cards are excluded from the visible set now rather than returned as null.

Pairing is load-bearing and easy to break silently, so a test asserts it
across every combination of the three things that vary — sms vs
notifications, getting-started, tire hotel — checking that no two visible
cards overlap and that no half card sits alone except the last.

Auto-renew now starts off. Billing a customer for another season they never
asked for is not a good default; the column already defaulted to false and
only the dialog disagreed.

* Allow deleting an agreement that never billed anything

Ended agreements accumulated with no way to clear them, so a couple of trial
runs sit on the record permanently.

Deletion is allowed only when no period under the agreement ever reached an
invoice. Once one has, the agreement is the reason that line exists on a
document the customer holds, and removing it would leave an unexplained
amount behind; those are ended, not deleted. Anything else is a mistake or a
trial and should be tidied away rather than read past forever.

The button follows the same rule, so it is absent exactly when the action
would be refused rather than offering something that always errors.

* Stop asking for tread at check-out, and fix a nested badge

Check-out asked for a second tread reading and told the operator the
difference from arrival was the season's wear. That is wrong: tires do not
wear on a shelf. The wear happened on the car before they arrived, which is
exactly what the check-in reading captures, so measuring again recorded the
same numbers and implied a change that cannot have occurred.

What the moment is actually good for is telling the customer what those tires
are already known to be. Check-out now shows the reading it came in with, and
leads with a line when something is at or near the replacement limit — a set
going back onto a car in that state is worth a word before they drive off,
which is also when a shop sells the replacement.

Measuring is still offered in the one case it makes sense: nothing was
recorded at check-in, and this is the last chance before the tires leave.

The prep badge nested a Badge inside AppCard's badge slot, which draws its own
pill — hence the doubled border and odd padding. The slot takes bare content,
as every other caller passes.

* Let the tech message the customer from the tire set

A set arriving worn is the highest-intent sales moment in the cycle: the
customer has just handed over tires that need replacing, and there is a full
season of lead time before they need them again. The person who knows is the
tech holding the wheel, and until now they had no way to say so without
leaving the page and retyping everything into the messages screen.

The composer opens from the condition card with the message already written,
because the value is in it being one tap from the reading rather than in the
composing. A set with a tire at or below the replacement limit also leads
with a line saying so, and the button turns primary — the prompt and the
means to act on it in the same place.

The text stays editable. Every shop has its own voice and its own view on
what it will promise, and a fixed script would only be deleted and retyped.
Defaults are translated per locale rather than templated from English, since
this is the customer reading it.

Sending reuses the existing dispatcher, so SMS, email and Telegram all work
and each lands in the customer's conversation. It writes a real message row
rather than firing and forgetting: a message that existed only as a side
effect would be invisible the moment the customer rang back about it, and a
failed send nobody can see is worse than none at all.

Channels the workshop has not configured and channels this customer has no
address for are different problems, so the picker says which — and offers a
one-off recipient rather than refusing.

* Read message templates with t.raw, not t

The composer crashed on open. Its templates carry {placeholders} that the
dialog substitutes itself, but next-intl parses a message as ICU before
handing it over, so it saw {season} and {customer_name} as arguments it had
not been given and threw.

t.raw returns the message as written, which is what a template meant for
later substitution needs. A missing one now falls back to an empty composer
rather than the key, since pasting "messaging.bodies.low_tread" into a
customer's inbox is worse than a blank box the tech fills in.

Tests cover the failure mode that survives this fix: every locale is rendered
with the full variable set and asserted to have nothing left unresolved, so a
translator writing {customer} instead of {customer_name} fails here rather
than reaching a customer as literal braces. All twelve pass today.

* Rewrite the customer message in a plain voice, and drop em dashes

The low-tread message read like marketing copy rather than a tech texting a
customer. It is shorter now, uses contractions, and ends on the one question
that matters: "Want a price on a new set?". The closing reassurance about
tires being safe until the customer decides is gone, since it padded the
message without adding anything a customer needed.

Same pass across all twelve locales, each rewritten rather than translated
from the English, so none of them reads like a form letter either.

Em dashes are out everywhere, in copy and in comments. Christian has asked
three times and they read as machine-written. Each one was rewritten rather
than swapped for a comma, which would only have traded the dash for a splice.

* Turn a stored set into a quote or straight into a work order

The core of a tire hotel is not storing tires, it is selling and fitting the
next set. That path did not exist: a customer saying yes left the shop
retyping a size into a new quote, and the resulting job said nothing about
which tires it was for.

Both routes out are offered, because both are normal. A customer asking what
it would cost gets a quote. One who has already said swap them gets the job
on the board without a quote in between, which is the common case in season
and would otherwise be a step invented by the software rather than the shop.

Tires are picked from stock rather than typed. The set knows its size and the
shop knows what it has in that size, so matching is done on the fitment
rather than the string: "225/45R17", "P225/45 ZR17 94V" and "225/45ZR17" are
one tire written three ways, and matching on text would miss most of them.
What covers the whole set sorts first and cheapest of those leads, since a
cheaper tire the shop cannot supply today is worth less to whoever is
quoting. Nothing matching is normal for an odd size, so the line is still
created at a price the operator sets.

The link back is the part that matters on the floor. Both the quote and the
job carry the set, the conversion carries it across, and the job opens with a
banner led by the shelf code, because the first thing a technician does on a
tire job is walk off to fetch them. On rims, sensors and any unfinished prep
sit next to it, since each is a second trip if it is discovered at the car.
The shelf is written into the notes as well, as neither the printed job sheet
nor the PDF renders a relation.

* Print QR labels for each tire

Finding a set meant reading the app and then guessing which of the four sets
on that shelf was the right one. Now every tire carries a sticker.

Four sizes cover what shops actually own: two Dymo roll sizes, a 62mm thermal
roll, and an A4 sheet for a shop with only an office printer. The choice is
remembered per browser, since a shop buys one kind of roll and then uses it
for years.

One label per tire, not per set. A set split across two shelves, or one tire
away for repair, is exactly the case a set-level label fails at.

The shelf code is deliberately not printed. Sets get relocated when a rack is
consolidated, and a printed shelf would then be wrong in the one place nobody
thinks to check. The QR resolves the current location live, which is the
reason it is there. The plate takes the space instead, in the largest type
the label allows, because that is what identifies a tire to someone standing
at a rack holding it.

Printing happens straight after check-in rather than as a separate errand.
The tech has the tires in hand at that moment, and the sticker has to go on
before they reach the shelf; coming back for it later is how sets end up
unlabelled. The PDF opens in a tab rather than downloading, since the next
action on a roll printer is the print dialog.

* Include the tire hotel in backup and restore

The app-level backup is a curated per-model export, so a new model is not
picked up by writing it: tire hotel was simply absent. Exporting today would
have produced a file that looked complete and silently contained no stored
sets, shelves, agreements, charges, treatments or movements, and the loss
would surface on the restore that was supposed to save someone.

Both roots are exported with everything hanging off them, and the restore
runs last, since sets point at customers and vehicles and shelves have to
exist before anything can sit on one.

Three judgement calls in the restore, all the same shape. A set whose shelf
did not come across is restored unplaced rather than dropped, because losing
the tires is worse than losing where they sat and an unplaced set is visible
and fixable. An agreement with no readable start date is dropped rather than
given an invented one, since every period is derived from it and a guess
would shift the whole billing schedule quietly instead of failing where
someone can see it. Charges keep their invoiced status but not their invoice
link, so a restored period still cannot be billed twice.

Tests read both routes and the schema, and assert that every Tire model is
covered and that the two sides agree on the key names. That is the failure
this kind of export actually has, and it is invisible until it matters.

* Show a preview when picking a label size

The formats differ mostly in how much survives on them, which a name and a
measurement cannot convey. Picking one was guesswork, and finding out meant
printing.

The dialog is two panels now: the choices on the left, and what they produce
on the right. The preview uses the real plate, size and flags rather than a
mockup, draws the label at its true proportions, and never scales up, since a
small label shown large would misrepresent how much room the text has. The QR
is a real code, so it scans off the screen.

Sizes and what gets dropped as the label shrinks moved into labelLayout,
which both the PDF and the preview read. Two hand-maintained copies of the
same numbers would drift, and that drift only shows up on a wasted roll of
labels. Tests hold the invariants that make the layout work at all: the plate
outgrows everything else, the QR fits inside the label it prints on, and
neither renderer branches on the detail level by itself.

* Rework the tire strip on a work order

It was a tall card competing with the work order's own content, and most of
what it said was already on the screen. The title is the tire description, so
repeating the brand and size below it spent the space on something the
technician had just read. Three stacked lines went to the location, the
badges were a row of four competing colours, and the prep warning had a
full-width panel to itself.

It is a strip now, roughly half the height. It carries only what the job does
not already say: which shelf, what state the tires came in, and whether
anything is still owed on them. The shelf is the one thing set at size, since
it is the only reason to look at the strip at all.

Condition only shows when it is not good, so the row is empty in the ordinary
case rather than carrying a badge that means nothing. The size stays, small
and last, purely so an edited title cannot leave the strip ambiguous. A set
that is no longer on a shelf says so where the shelf would have been, which
is the question that reader is asking anyway.

* Move the tire strip into the working column

It spanned the full width, above both the work order and the invoice
sidebar. The sidebar is billing, and which shelf the tires sit on has nothing
to do with it, so the strip was pushing the invoice down for no reason.

It sits at the top of the left column now, directly above Parts, which is
where the job actually starts. On mobile the columns stack and it stays first,
so the reading order is unchanged.

* Price prep work, and let a set join an existing job

Two things, plus a bug found on the way.

Prep now reaches the bill. The work was agreed at check-in, so washing,
balancing and the rest arrive as priced labour lines on a quote or work order
without anyone retyping them.

A treatment with no price set produces no line at all. Plenty of shops fold
washing into the storage fee, and for them a zero-priced line on every
invoice is something to delete each time. Setting a price is what opts a
treatment in, which lets both kinds of shop work without a second setting to
explain the first.

Skipped work is left off, since someone decided against doing it. Work
already done stays on, because it happened and the customer is paying for it;
ticking a job off before raising the invoice must not lose the charge.

Work orders now ask where the lines go before asking what they are. A car
already booked in for something else is the common case: the customer
mentions the swap while it is on the ramp, and a second job would split one
visit across two invoices. Only jobs for the same vehicle are offered, and
the step is skipped when there are none, since a question with one answer is
not worth asking.

The bug: the tire hotel settings page never loaded the storage prices it
renders, so both fields read zero on every visit and a saved price looked
like it had not saved. Re-totalling a record from its lines also had two
copies by this point and now has one.

* Show stored tires on the vehicle page

Opening a vehicle said nothing about whether its tires were sitting on a
shelf downstairs, which is a question the desk gets constantly.

One line per set, and only what is asked: how many and which season, the
shelf code, and a mark when the condition or the prep needs attention. Anyone
who wants more follows the line.

It renders nothing when the vehicle has no stored tires, which is most of
them. An empty state here would be a permanent row saying "no" on every
vehicle in the shop, and the vehicle page has enough to say already.

The lookup returns nothing rather than throwing when the module is off, since
the vehicle page is not a tire hotel screen and should not break over a
feature it does not depend on.

* Shrink the vehicle tire indicator to one meta item

The card was a block of its own for something nobody opened the vehicle page
to read. It is one item in the meta row now, next to the service count and
the spend, which is where a fact of that size belongs.

One set shows its shelf, since that is the useful half and it fits in the
space the label would have taken anyway. Several show a count and go to the
list, because landing on an arbitrary one of them would be a guess. Nothing
renders when there is nothing in storage.

The query dropped with it: the panel needed conditions, prep and sizes, and
a meta item needs a shelf code and a status.

* Stop the work order button claiming it makes a new one

It opens a dialog that can just as easily add the lines to a job already open
for the vehicle, so "New work order" was wrong half the time. Both buttons
now read as what they are about rather than what they do, and the dialog says
which it is going to be.

The labels came from job.quote and job.workOrder, which already held exactly
these words for unnumbered rows in the list below. The newQuote and
newWorkOrder keys are gone, so there is no longer a second key per concept
sitting next to targetNew saying something different.

* Allow unlinking a tire set from a work order

Every path set the link and none removed it, so a job attached to the wrong
set stayed attached.

The control sits on the tire strip itself, next to the link it undoes.

Only the link goes. The parts and labour lines stay, because they are what is
being charged and may have been edited since; deciding on the operator's
behalf that a billed line should disappear would be a worse surprise than
leaving it visible to remove. The confirm says so rather than leaving anyone
to find out. The shelf written into the job notes also stays, since it is
free text by now and may have been added to.

* Stop registering tiptap link and underline twice

The work order notes editor added Link and Underline alongside StarterKit,
which has bundled both since v3. Two extensions answered to each name, which
is what tiptap was warning about, and it makes the toolbar's active state
depend on which of the pair resolves first.

They are configured through StarterKit now, so the link still opens in a new
tab with rel set and autolink on, and the toolbar keeps working because the
commands come from the same extensions either way.

The shared editor in components/ui was already correct; only this one had
the duplicates.

* Let the tech choose which lines go on the job, and show unset channels

The job dialog now lists everything it is about to add, the tire line and
each priced prep item, ticked by default and each one unticked when the
customer brought their own tires or the wash is goodwill. The running total
follows the ticks, and a job with nothing on it cannot be raised.

The message composer lists every customer channel, greying out the ones the
workshop has not set up with a pointer to settings, rather than hiding them
as if they did not exist. Sending on an unconfigured channel is refused
server-side too, so a forged request cannot file a message that was never
going to leave the building.

* Let the tech search the whole catalogue for the tire to sell

The fitment matches were the only thing on offer, which assumes a shop
carries one tire per size. It carries a dozen brands, sometimes sells a
wider tire on request, and sometimes has the right one filed under a name
no size parser will recognise.

The picker now lists every match in the size rather than the cheapest six,
and a search box above it reaches the rest of the catalogue. Terms are
ANDed, so "michelin 225" means both. A hit that is not the stored size is
labelled rather than hidden, since selling something else is allowed and
should just not happen by accident. Whatever is picked stays in the list
when the search is cleared.

* Stop the job dialog scrolling sideways, and tighten the stock list

The dialog is a CSS grid, and a grid item will not shrink below its
min-content width unless it is told to. A search hit carrying two pills, a
price and a long part name was wider than the column, so the whole dialog
picked up a horizontal scrollbar and cut off the total and the buttons.

The body now carries min-w-0, and the pills moved into the meta line as
plain coloured text, which is both narrower and quieter. The list itself is
one bordered table of rows instead of a stack of cards, since a tech
comparing a dozen brands in one size wants them close enough to read at a
glance. The tick has a permanent slot so choosing a row no longer shifts
every price sideways.

* Close the stock search once a tire is picked

Choosing a row left the results open, so the prep list and the total stayed
buried under a list nobody was reading any more. Picking now clears the
search, and the chosen tire is hoisted to the top of the list so it is
still in view when the results collapse rather than somewhere down a
scrolled list.

* Show one tire line on the job, and let the tech set how many

Picking from the search left the alternatives listed underneath, so a job
with one tire on it looked like a job with several. The section now
collapses to the chosen line with a Change button, and the picker only
opens while somebody is choosing.

The quantity is editable, defaulting to the set on the shelf, since a
customer replacing two of four should not have to fix the line afterwards.
Stock is judged against that number rather than against the set, so "only 1
in stock" answers the question actually being asked.

Creating a new work order also adds the tire line now. It only ever added
the prep, so picking a tire and a price produced a work order without them.

* Hold the picker still while the tech is typing

The results panel grew and shrank with the number of hits, and the dialog
is centred, so every keystroke walked the whole thing up and down the
screen. The panel is now a fixed height that the spinner, the empty state
and the list all share, and the count caption keeps its line whether or not
it has anything to say.

* Float the tire picker instead of growing the dialog

Opening the picker in the layout pushed the prep list, the total and the
buttons down by the height of the panel, and holding that panel at a fixed
height only traded the jumping for a screenful of white space when a size
had two matches in it.

The tires row is now a combobox, the same pattern the customer picker uses.
The chosen tire is the trigger, the results float over the dialog, and the
dialog itself never changes height. Nothing picked shows the set's own
description, which is what the line will be named anyway.

* Take the tires off the shelf when they go on the job

Creating a work order wrote the part line but never moved stock, while
deleting a work order restocks every linked line it finds. Sell four tires
and delete the job and the shelf claimed eight.

Both work-order paths now go through addTireLineToRecord, which writes the
line and records the movement as one operation, in the same transaction and
through the same ledger the parts editor uses, so the two cannot drift apart
again. Quotes still move nothing; conversion is what consumes stock, as it
does everywhere else. A set that is not a catalogue item moves nothing
either, which is the rule free-text part lines already follow.

* Charge for storage without an agreement, and raise due periods nightly

Two gaps in the storage billing. Charges could only exist under an
agreement, so a shop taking a fee for the winter had to create terms that
renew nothing. And refreshCharges had no caller, so a monthly agreement
raised its first period at creation and then never again: the shop's income
depended on who happened to open the set.

A charge now belongs to a set or to an agreement, and a Charge storage
button on the billing card raises one directly. It invoices, waives and
reports through the same path as a billed season, because it is the same
row. syncCharges moved to a lib so the nightly sweep and the actions share
one answer to which periods are owed; two implementations would eventually
disagree, and that shows up as a customer billed twice for one winter.

Needs a migration: TireStorageCharge.agreementId is nullable now and
tireSetId is new.

* Store tires from the job the car came in on

Everything ran tire hotel to work order: check a set in, then go and find
the job to attach it to. That is backwards for the ordinary case, where the
car is on the ramp, the desk is halfway through writing the job, and the
winter set is standing in the corner.

A work order with no set on it now offers Store tires. It opens the same
check-in dialog with the vehicle already answered by the job, links the new
set to that work order on save, and goes straight into the labels without
leaving the job half written. Shelves load on click, since most work orders
have nothing to do with tires and the page already fetches plenty.

* Tidy an unused import and formatting

* Bring the same tires back to the same record, season after season

A tire hotel's ordinary year is the same four tires arriving twice, and the
app had no way to say so: every arrival was a new record, which split one
set's history in two and lost the one thing the shop is uniquely placed to
tell a customer, how fast their tires are actually wearing.

Check-in now offers the sets this customer has left with before. Picking
one puts the same record back on a shelf, so every season's readings land
on one history. Last season's depth sits under each field as this season's
is typed, with the wear beside it, and the measurement card totals it up:
worn 2.6 mm since April, about 1.3 mm a season, roughly one season left.

Write off retires a set that was replaced, so it stops being offered as the
same tires next year with somebody else's wear attached. Written-off sets
keep their history and their invoices but leave the default list, which is
meant to answer what is on the racks now.

* Say where the numbers come from, for anyone allowed to change them

Prices, tread limits and shelf capacities all default from settings, and
the moment a shop notices one is wrong is the moment they are looking at
it, not the moment they happen to be in settings. Prep prices, storage
prices, the replacement limit and the default capacity now each carry a
link to the setting behind them.

Gated on permission to change settings, using the same rule the settings
layout enforces. A link that only leads to a redirect reads as the app
being broken rather than as a permission somebody does not have.

* Turn the tread history into next season's buying list

A tire hotel puts a gauge on the same four tires twice a year, which makes
it the only party able to say what its own customers will need. That was
sitting in the measurement table doing nothing.

Tire hotel, Next season groups the answer by fitment and counts it in
tires, the unit a supplier quotes. Sets already at the limit are kept apart
from sets a season away, because the first is measured and the second is
extrapolated from one customer's driving over one interval, and a buyer
should be able to tell which is which. Sets with one visit produce no rate
at all; the page says how many were excluded rather than implying the list
is the whole picture.

Also points the docs link on every tire hotel page at the new manual page.

* Point at the manual from the places that raise the question

The docs button in the header now resolves for every tire hotel route,
including a set's own page. Beyond that, a Read more sits where somebody is
most likely to be stuck: the check-in dialog, the label dialog, the storage
billing card, the empty racking screen, the settings page and the forecast,
where what the numbers do not mean matters as much as what they do.

* Let a set be written off wherever it is, and stop counting scrapped prep

Writing off was offered only once a set was back on the car, but a set can
be condemned while it is still on the shelf. Making somebody fake a
check-out first records an event that never happened, so the action is now
offered in both states and frees the shelf when it applies.

The dashboard prep count and the Needs prep badge were both counting
written-off sets, so a wash nobody ticked off in 2024 stayed on the
technician's list forever. Both now match the filter itself.

Picking an owner after choosing a returning set also clears the choice, so
the form cannot show one car and file the tires against another.

* Print the storage line in the workshop's own language

It was hardcoded English, so a Norwegian shop billed its customer for
"Tire storage · 4 pcs". Read from the message files the same way the prep
lines already are, falling back to English rather than failing: a line in
the wrong language still bills correctly, a thrown error does not.

* Say what the forecast did not read

It capped at 500 sets and said nothing, so a large hotel would have been
shown a corner of its racks as the whole picture. The cap is 2000 now, the
count comes back with the query, and the page says how many of how many it
read when the two differ.

* Cover the tire hotel in the permission test

The test imports actions by hand and had none from the tire hotel, so
nothing verified that a custom role without the subject is refused. It
holds customers' property and bills them for it, which makes it exactly the
kind of module that should be checked rather than assumed.

* Let a technician skip prep work they decided against

"Skipped" was a real status the billing already respected: skipped work is
never charged, which is how a shop declines to bill for something it looked
at and left. Nothing in the app could ever produce that state, so the rule
was unreachable and the only way to clear a job you were not going to do
was to mark it done.

The card already rendered the skipped state, so this is the missing half of
something already built.

* Translate the search palette

It never called useTranslations, so every heading was English and a tire
set printed its raw status: a Norwegian workshop read "disposed" in a
Norwegian app.

Most of it needed no new strings. The sidebar already names customers,
vehicles, quotes, parts, reminders, inspections and the tire hotel in
twelve languages, and the settings sidebar already names every settings
page with a description. The palette now reads from those rather than
carrying a second copy of the same names, which is also the copy that would
have gone stale. Six strings are genuinely new.

Settings keywords stay English on purpose. They are aliases, so "vat" and
"smtp" keep working whatever language the app is in, and the translated
title and description are matched alongside them.

* Translate the roles and permissions editor

Every group name and action label was a hardcoded English string, so a
workshop working in Norwegian or Russian granted permissions from an
English matrix.

permissions.ts is imported by server code that runs outside a request and
has no translations available, so the constant keeps its English names as
the developer-facing source and the editor translates at render, keyed on
the subject and action enums it already carries. Manage on the tire hotel
means the racking rather than the module, so it keeps its own wording
rather than reading as a bare "Manage".

* Let the vehicle decide who the tires belong to

Two problems, one cause. Picking a vehicle set the customer only when the
field was still empty, and the customer picker holds its label in its own
state, so the auto-filled owner never appeared on screen. It looked unset,
so staff set one, and the form resolved the clash by silently dropping the
vehicle.

Editing a set was worse: choosing a vehicle there never touched the
customer at all, so one owner's tires could be filed against another
owner's car with nothing downstream to catch it.

A car has an owner, so once a vehicle is chosen the customer is read from
it and shown rather than asked for, and clearing the vehicle is what hands
the question back. A vehicle with nobody on file keeps the picker, since
naming a customer there is how that pairing gets made.

Both dialogs now share one component rather than two copies of the rule.
The copy that drifts is the one nobody remembers to fix.

* Point out a new sidebar link once, with a reusable hint manager

Turning on the tire hotel adds a link to the sidebar, and a link that
appears on its own is easy to miss. It now gets a one-time card pointing at
it, built as a system rather than a one-off, since more will follow.

Two rules carry it. One hint shows at a time, because a screen with three
of them is noise nobody reads, and registration order sets the queue.
Dismissals are stored against the workshop in settings rather than in the
browser, so somebody joining next year does not get a backlog of
announcements about features that have always been there, and dismissing at
the desk settles it for the tablet in the bay.

Following the usual guidance for these: one card, one line, one button,
never a tour. It never steals focus, since doing that on page load throws a
keyboard user out of whatever they were doing. It is announced politely,
closes on Escape, and clicking the link it points at counts as having found
it. The card carries an arrow into the link and rings it while open,
because a card floating beside twelve links names none of them.

* Keep the hint parser out of the server action file

A 'use server' module may only export async functions, so the synchronous
parser broke the build. It moves to a lib, where the layout and the action
can both read it.

* Name the tires in the workshop's language on the job

A Norwegian work order carried "4x Nokian winter tires" on the parts line,
because the description was built from English words in the action. The
season phrase now comes from the message files, one phrase per season
rather than a word plus a noun to join: vinterdekk is one word in Norwegian
and pneus hiver is two in French, and neither falls out of stitching
"winter" onto "tires".

The job sheet notes had the same problem and they print on the PDF, so the
shelf, the rims and the TPMS lines are translated too.

Three slightly different locale readers had grown in this feature. They are
one now, before a fourth was written differently again.

* Stop the label dialog reopening over whatever you just did

Check-in lands on the set with ?print=1 so the labels go on before the
tires reach the shelf. Nothing ever took that out of the URL, so every
later refresh on that page fired it again: writing a set off, editing it or
moving it all popped the label dialog back up.

The flag is consumed once and the URL cleaned. Labels are also no longer
offered on a written-off set, since there is nothing left to stick one on.

* Offer the jobs that are actually open when billing storage

Both tire hotel pickers filtered for status "in_progress" while the app
stores "in-progress", so a job anybody had started was invisible, and
waiting-parts and scheduled jobs were never considered at all. Nothing
threw. The picker just said there were no open jobs, which left a new
invoice as the only way to bill storage, and the same fault hid open work
orders from the tire change dialog.

Both now use one OPEN_SERVICE_STATUSES constant, matching what the work
board treats as open, and a test pins it against the values the status
select actually writes.

* One dialog bills everything a stored set is charged for

Storage was a second billing system beside the jobs card: agreements,
periods, charge rows, its own invoice picker, its own nightly sweep. Two
places to charge one customer for one visit, which is one too many.

Agreements are gone. Storage is now a line in the job dialog, opening at
the season price from settings, with an editable period that prints on the
line. Nothing raises it on a schedule, because it is charged when the tires
are billed, which is when somebody is looking at the account anyway.

The dialog is twice as wide and split: what goes on the bill on the left,
tires, prep and storage, all ticked and any of them removable; where it
lands on the right, a new work order, a job already open for this vehicle,
or a plain invoice for the customer who never brings the car in. The
two-step target picker is gone with it, and so is the rule that a set
without a vehicle cannot be billed at all.

Needs a migration: it drops tire_storage_agreements and
tire_storage_charges.

* One storage price in settings, since there is one storage line

Seasonal and monthly were the two billing models an agreement could take.
There are no agreements, so there is one number: what the storage line
opens at when a set is billed, editable on the bill.

* Say fatura, not nota, in Brazilian Portuguese

The billing dialog called an invoice a nota while the rest of the app,
navigation and billing alike, calls it a fatura. One word for one thing.

Carries the regenerated tire hotel migration that was sitting in the tree,
replacing 20260820080144 with 20260820101103.

* Label the open jobs in the destination column, and drop dead copy

A list of invoice numbers under two named options does not explain itself,
so the caption the old two-step picker used comes back above them.

Five strings had nothing left to label once the picker collapsed and the
stock list lost its pills: back, next, the needs-a-vehicle guard, the
to-order badge and the prep-lines heading.

* Grade tread against the workshop's own limits

The grader takes the limits as an optional argument and falls back to
built-in figures when nobody passes them. Nobody passed them. Every tread
reading in the app was graded against a built-in winter limit of 4 mm, so a
shop that set 3 mm saw 3.5 mm called Replace with no way to tell why.

The limits now reach check-in and check-out from all three places those
dialogs open: the tire hotel list, a set's own page, and a work order. A
test pins the reported case, and pins that the fallback is stricter than a
3 mm shop limit, which is exactly why forgetting to pass them produced a
wrong grade rather than an error.

Also from this round: storage is ticked by default, since billing it is why
the dialog is usually open. The collection date starts blank because nobody
knows when a customer will come back, and an invented date would print on
the invoice as though somebody did; a period with only a start prints "from
<date>". Both date fields now say what they are for. The destination column
scrolls once a shop has more than a handful of jobs open, and the prep list
says its prices come from settings and can still be edited afterwards.

* Let the tread be corrected from Edit, and log it as a measurement

Edit could change everything recorded at check-in except the readings. The
action parsed them and threw them away, and the dialog had no fields for
them, so a mistyped depth was permanent and every wear figure derived from
it stayed wrong.

Edit now carries the tread, seeded from the latest reading per position in
the workshop's own unit and graded against its own limits. Saving a changed
reading records a new round under a movement of its own, so the history
says when somebody measured again rather than quietly rewriting what was
recorded on arrival. Saving without touching a reading records nothing, so
the history does not fill with rounds identical to the one above them.

* Grade every reading from its depth, not from the word stored with it

Passing the workshop's limits to the entry form fixed new readings and
nothing else. Every screen printed the condition saved on the row, and that
was graded with whatever limits were in force when somebody typed it, so a
set measured before the limits were passed still read Replace at 3.5 mm
next to a 3 mm limit.

One helper now derives the badge from the millimetres against the current
limits, and the set page, the list, the work order strip and the check-out
readout all use it. Changing a limit in settings regrades the whole history,
which is the only behaviour that cannot contradict the number beside it.
The stored word is used only where there is no depth, which is a tire
somebody graded by eye.

* Actually use the derived grade on each reading row

The badge on each corner still printed the stored word. The edit that was
meant to change it never matched, because the file had been reformatted
between my reading it and my writing it, and a plain string replace fails
silently. The warning line above it was already correct, which is why the
page said two tires were at the limit while showing Replace on four.

* Hold photos and documents against a stored set, and put them on the bill

A kerbed rim is easier to show than to describe, and the argument about it
happens months later when the customer collects. A set now carries photos
and PDFs, each with a line saying what it shows and a tick deciding whether
it reaches the customer.

They are copied onto the work order or invoice when the set is billed, as
ordinary service attachments, so the invoice PDF renders them with no
changes at all: it already embeds every attachment marked for the invoice,
caption and all. The copy points at the same file rather than duplicating
the bytes, and removing one from the set later leaves any invoice that
carries it intact, which is the right way round for a document a customer
may already hold.

Uploads are images and PDFs only, capped at 15 MB, under a folder of their
own, with a generated filename so an uploaded name can never decide where
it lands. Video is refused: it cannot be embedded in an invoice, which is
where these end up.

Needs a migration: tire_set_attachments is new.

* Serve the tire hotel uploads

The file route checks the folder against a whitelist and I added an upload
folder without adding it to the list, so every photo uploaded fine, was
written to disk and recorded, then rendered broken behind a 400.

A test now reads the folder each upload route writes into, straight from
the route, and asserts the file route agrees to serve it. Removing the new
entry makes it fail with the route and folder named, which is the report
this needed and did not produce.

* Count tires by the language's own plural rule

The sticker built its quantity by replacing {count} in a raw string, so
Polish printed "4 opon" where it wants "4 opony", and every language read
"1 tires" for a single wheel. The same flat string served the on-screen
preview through next-intl, where a plural rule would have worked, so the
two were one ICU expression away from disagreeing with each other.

The label route now translates through next-intl like everything else, and
both quantity strings carry real plural forms, including the four-way ones
Polish, Russian and Lithuanian need. Caught by the Italian and Polish
documentation pass, which had to write "4 opon" into a mock-up to stay
faithful to what the app prints.

* lint
Bernt Christian Egeland há 1 mês atrás
pai
commit
8ecc7ec0ac
100 ficheiros alterados com 6594 adições e 67 exclusões
  1. 27 1
      messages/de/audit.json
  2. 2 1
      messages/de/common.json
  3. 11 1
      messages/de/dashboard.json
  4. 6 0
      messages/de/featureHints.json
  5. 8 2
      messages/de/navigation.json
  6. 27 0
      messages/de/permissions.json
  7. 9 0
      messages/de/search.json
  8. 34 1
      messages/de/settings.json
  9. 473 0
      messages/de/tireHotel.json
  10. 27 1
      messages/en/audit.json
  11. 2 1
      messages/en/common.json
  12. 11 1
      messages/en/dashboard.json
  13. 6 0
      messages/en/featureHints.json
  14. 8 2
      messages/en/navigation.json
  15. 27 0
      messages/en/permissions.json
  16. 9 0
      messages/en/search.json
  17. 34 1
      messages/en/settings.json
  18. 473 0
      messages/en/tireHotel.json
  19. 27 1
      messages/es/audit.json
  20. 2 1
      messages/es/common.json
  21. 11 1
      messages/es/dashboard.json
  22. 6 0
      messages/es/featureHints.json
  23. 8 2
      messages/es/navigation.json
  24. 27 0
      messages/es/permissions.json
  25. 9 0
      messages/es/search.json
  26. 34 1
      messages/es/settings.json
  27. 473 0
      messages/es/tireHotel.json
  28. 27 1
      messages/fr/audit.json
  29. 2 1
      messages/fr/common.json
  30. 11 1
      messages/fr/dashboard.json
  31. 6 0
      messages/fr/featureHints.json
  32. 8 2
      messages/fr/navigation.json
  33. 27 0
      messages/fr/permissions.json
  34. 9 0
      messages/fr/search.json
  35. 34 1
      messages/fr/settings.json
  36. 473 0
      messages/fr/tireHotel.json
  37. 27 1
      messages/it/audit.json
  38. 2 1
      messages/it/common.json
  39. 11 1
      messages/it/dashboard.json
  40. 6 0
      messages/it/featureHints.json
  41. 8 2
      messages/it/navigation.json
  42. 27 0
      messages/it/permissions.json
  43. 9 0
      messages/it/search.json
  44. 34 1
      messages/it/settings.json
  45. 473 0
      messages/it/tireHotel.json
  46. 27 1
      messages/lt/audit.json
  47. 2 1
      messages/lt/common.json
  48. 11 1
      messages/lt/dashboard.json
  49. 6 0
      messages/lt/featureHints.json
  50. 8 2
      messages/lt/navigation.json
  51. 27 0
      messages/lt/permissions.json
  52. 9 0
      messages/lt/search.json
  53. 34 1
      messages/lt/settings.json
  54. 473 0
      messages/lt/tireHotel.json
  55. 27 1
      messages/nb/audit.json
  56. 2 1
      messages/nb/common.json
  57. 11 1
      messages/nb/dashboard.json
  58. 6 0
      messages/nb/featureHints.json
  59. 8 2
      messages/nb/navigation.json
  60. 27 0
      messages/nb/permissions.json
  61. 9 0
      messages/nb/search.json
  62. 34 1
      messages/nb/settings.json
  63. 473 0
      messages/nb/tireHotel.json
  64. 27 1
      messages/nl/audit.json
  65. 2 1
      messages/nl/common.json
  66. 11 1
      messages/nl/dashboard.json
  67. 6 0
      messages/nl/featureHints.json
  68. 8 2
      messages/nl/navigation.json
  69. 27 0
      messages/nl/permissions.json
  70. 9 0
      messages/nl/search.json
  71. 34 1
      messages/nl/settings.json
  72. 473 0
      messages/nl/tireHotel.json
  73. 27 1
      messages/pl/audit.json
  74. 2 1
      messages/pl/common.json
  75. 11 1
      messages/pl/dashboard.json
  76. 6 0
      messages/pl/featureHints.json
  77. 8 2
      messages/pl/navigation.json
  78. 27 0
      messages/pl/permissions.json
  79. 9 0
      messages/pl/search.json
  80. 34 1
      messages/pl/settings.json
  81. 473 0
      messages/pl/tireHotel.json
  82. 27 1
      messages/pt-BR/audit.json
  83. 2 1
      messages/pt-BR/common.json
  84. 11 1
      messages/pt-BR/dashboard.json
  85. 6 0
      messages/pt-BR/featureHints.json
  86. 8 2
      messages/pt-BR/navigation.json
  87. 27 0
      messages/pt-BR/permissions.json
  88. 9 0
      messages/pt-BR/search.json
  89. 34 1
      messages/pt-BR/settings.json
  90. 473 0
      messages/pt-BR/tireHotel.json
  91. 27 1
      messages/ru/audit.json
  92. 2 1
      messages/ru/common.json
  93. 11 1
      messages/ru/dashboard.json
  94. 6 0
      messages/ru/featureHints.json
  95. 8 2
      messages/ru/navigation.json
  96. 27 0
      messages/ru/permissions.json
  97. 9 0
      messages/ru/search.json
  98. 34 1
      messages/ru/settings.json
  99. 473 0
      messages/ru/tireHotel.json
  100. 27 1
      messages/tr/audit.json

+ 27 - 1
messages/de/audit.json

@@ -109,6 +109,32 @@
     "settings_applyTaxRate": "Steuersatz angewendet",
     "settings_convertToExclusive": "Preise auf netto umgestellt",
     "settings_convertToInclusive": "Preise auf brutto umgestellt",
-    "organization_sampleDataRemove": "Beispieldaten entfernt"
+    "organization_sampleDataRemove": "Beispieldaten entfernt",
+    "tire_warehouse_create": "Reifenlagerort erstellt",
+    "tire_warehouse_update": "Reifenlagerort aktualisiert",
+    "tire_warehouse_delete": "Reifenlagerort gelöscht",
+    "tire_warehouse_archive": "Reifenlagerort archiviert",
+    "tire_location_create": "Reifenregal erstellt",
+    "tire_location_bulk_create": "Reifenregale erstellt",
+    "tire_location_update": "Reifenregal aktualisiert",
+    "tire_location_delete": "Reifenregal gelöscht",
+    "tire_set_check_in": "Reifensatz eingelagert",
+    "tire_set_check_out": "Reifensatz ausgelagert",
+    "tire_set_relocate": "Reifensatz umgelagert",
+    "tire_set_update": "Reifensatz aktualisiert",
+    "tire_set_delete": "Reifensatz gelöscht",
+    "tire_measurement_create": "Reifenmessungen erfasst",
+    "tire_treatment_update": "Vorbereitung aktualisiert",
+    "tire_treatment_mark": "Vorbereitung markiert",
+    "tire_treatment_complete_all": "Vorbereitung abgeschlossen",
+    "tire_set_message_customer": "Kunde zu einem Reifensatz benachrichtigt",
+    "tire_set_create_quote": "Angebot aus einem Reifensatz erstellt",
+    "tire_set_create_work_order": "Auftrag aus einem Reifensatz erstellt",
+    "tire_set_add_to_work_order": "Reifensatz zu einem Auftrag hinzugefügt",
+    "tire_set_unlink_work_order": "Reifensatz von einem Auftrag gelöst",
+    "tire_set_return": "Satz erneut eingelagert",
+    "tire_set_dispose": "Reifensatz ausgemustert",
+    "tire_set_attach": "Dateien zu einem Reifensatz hinzugefügt",
+    "tire_set_detach": "Datei von einem Reifensatz entfernt"
   }
 }

+ 2 - 1
messages/de/common.json

@@ -10,7 +10,8 @@
     "confirm": "Bestätigen",
     "saveChanges": "Änderungen speichern",
     "edit": "Bearbeiten",
-    "add": "Hinzufügen"
+    "add": "Hinzufügen",
+    "gotIt": "Verstanden"
   },
   "contextMenu": {
     "hint": "Tipp: Rechtsklick auf eine Zeile für Aktionen",

+ 11 - 1
messages/de/dashboard.json

@@ -54,7 +54,8 @@
     "activeJobs": "Aktive Aufträge",
     "recentObservations": "Aktuelle Beobachtungen",
     "recentActivity": "Letzte Aktivitäten",
-    "gettingStarted": "Erste Schritte"
+    "gettingStarted": "Erste Schritte",
+    "tireHotel": "Reifenhotel"
   },
   "maintenance": {
     "title": "Voraussichtlich fällige Wartung",
@@ -234,5 +235,14 @@
       "expired": "Abgelaufen",
       "converted": "Umgewandelt"
     }
+  },
+  "tireHotel": {
+    "title": "Reifenhotel",
+    "viewAll": "Reifenhotel ansehen",
+    "stored": "{count} Sätze eingelagert",
+    "capacity": "{used} / {capacity} Reifen",
+    "roomLeft": "Platz für {count} weitere",
+    "needsPrep": "Vorbereitung offen",
+    "unbilled": "Nicht berechnet ({count})"
   }
 }

+ 6 - 0
messages/de/featureHints.json

@@ -0,0 +1,6 @@
+{
+  "tire-hotel": {
+    "title": "Reifenhotel ist aktiv",
+    "body": "Saisonreifen Ihrer Kunden einlagern. Erst Regale anlegen, dann einlagern."
+  }
+}

+ 8 - 2
messages/de/navigation.json

@@ -53,7 +53,12 @@
     "allReminders": "Alle Erinnerungen",
     "auditLog": "Aktivitätsprotokoll",
     "aiAssistant": "KI-Assistent",
-    "observations": "Beobachtungen"
+    "observations": "Beobachtungen",
+    "tireHotel": "Reifenhotel",
+    "allTireSets": "Alle Reifensätze",
+    "tireStorage": "Lagerstruktur",
+    "tireSetDetails": "Reifensatz",
+    "tireForecast": "Nächste Saison"
   },
   "search": "Suchen...",
   "shortcut": "Ctrl+K",
@@ -107,6 +112,7 @@
     "signOut": "Abmelden",
     "more": "Mehr",
     "auditLog": "Aktivitätsprotokoll",
-    "aiAssistant": "KI-Assistent"
+    "aiAssistant": "KI-Assistent",
+    "tireHotel": "Reifenhotel"
   }
 }

+ 27 - 0
messages/de/permissions.json

@@ -0,0 +1,27 @@
+{
+  "subjects": {
+    "dashboard": "Dashboard",
+    "vehicles": "Fahrzeuge",
+    "customers": "Kunden",
+    "work_orders": "Aufträge",
+    "quotes": "Angebote",
+    "services": "Serviceeinträge",
+    "billing": "Abrechnung",
+    "inventory": "Lager",
+    "labor_presets": "Arbeitsvorlagen",
+    "inspections": "Prüfungen",
+    "tire_hotel": "Reifenhotel",
+    "reports": "Berichte",
+    "work_board": "Auftragstafel",
+    "ai_assistant": "KI-Assistent",
+    "settings": "Einstellungen"
+  },
+  "actions": {
+    "create": "Anlegen",
+    "read": "Ansehen",
+    "update": "Bearbeiten",
+    "delete": "Löschen",
+    "manage": "Verwalten",
+    "manage_storage": "Lagerstruktur verwalten"
+  }
+}

+ 9 - 0
messages/de/search.json

@@ -0,0 +1,9 @@
+{
+  "title": "Suche",
+  "description": "Fahrzeuge, Kunden, Aufträge und Einstellungen durchsuchen",
+  "placeholder": "Nach Name, Kennzeichen, Telefon, FIN, Rechnung suchen…",
+  "noResults": "Keine Treffer.",
+  "recentCustomers": "Zuletzt genutzte Kunden",
+  "services": "Serviceeinträge",
+  "toClose": "zum Schließen"
+}

+ 34 - 1
messages/de/settings.json

@@ -122,6 +122,10 @@
       "support": {
         "title": "Support",
         "description": "Support kontaktieren und die Support-Schaltfläche verwalten"
+      },
+      "tireHotel": {
+        "title": "Reifenhotel",
+        "description": "Saisonale Reifeneinlagerung"
       }
     },
     "pro": "PRO"
@@ -974,7 +978,9 @@
       "lastBackup": "Letztes Backup:",
       "supportPrompt": "Bei Fragen zum Schutz Ihrer Daten",
       "supportAction": "kontaktieren Sie unser Support-Team."
-    }
+    },
+    "optTireHotel": "Reifenhotel",
+    "optTireHotelDesc": "Eingelagerte Reifensätze, Regale, Vorbereitung und Lagervereinbarungen"
   },
   "license": {
     "title": "White-Label-Lizenz",
@@ -1454,5 +1460,32 @@
     "unavailable": "Dieser Browser hat keine Installation angeboten. Öffnen Sie das Browsermenü und suchen Sie nach „App installieren“ oder „Zum Home-Bildschirm“.",
     "bannerHidden": "Das Installations-Banner ist auf diesem Gerät ausgeblendet.",
     "showBanner": "Banner wieder anzeigen"
+  },
+  "tireHotel": {
+    "title": "Reifenhotel",
+    "description": "Lagern Sie die Saisonreifen Ihrer Kunden ein und behalten Sie den Überblick, wo jeder Satz steht.",
+    "cardTitle": "Reifeneinlagerung",
+    "enable": "Reifenhotel aktivieren",
+    "enableHint": "Fügt die Reifeneinlagerung zur Navigation hinzu. Später abschalten blendet sie wieder aus, ohne etwas zu löschen.",
+    "defaultCapacity": "Standardreifen pro Fach",
+    "defaultCapacityHint": "Wird beim Anlegen neuer Fächer vorausgefüllt. Pro Fach änderbar.",
+    "warnPercent": "Warnen bei Auslastung (%)",
+    "warnPercentHint": "Markiert die Lagerübersicht, sobald dieser Anteil der Gesamtkapazität belegt ist.",
+    "treadTitle": "Ersatzgrenzwerte",
+    "treadHint": "Gesetzliche Mindestwerte unterscheiden sich je Land, setzen Sie also die Grenzwerte Ihres Marktes. Profiltiefe unter dem Grenzwert wird zum Ersatz markiert.",
+    "summerLimit": "Sommergrenze ({unit})",
+    "winterLimit": "Wintergrenze ({unit})",
+    "unitMm": "mm",
+    "unit32nds": "1/32\"",
+    "setupHint": "Richten Sie als Nächstes Ihre Regale ein.",
+    "setupLink": "Lagerstruktur öffnen",
+    "save": "Reifenhotel-Einstellungen speichern",
+    "saved": "Reifenhotel-Einstellungen gespeichert",
+    "billingTitle": "Lagerabrechnung",
+    "billingHint": "Wird beim Abrechnen eines Satzes auf der Lagerposition vorgeschlagen und bleibt änderbar.",
+    "seasonalPrice": "Standard-Lagerpreis",
+    "prepTitle": "Preise für Vorbereitung",
+    "prepHint": "Was jede Art von Vorbereitung kostet. Leer bedeutet, sie wird nie berechnet, was zu Betrieben passt, die das Waschen in der Lagergebühr enthalten haben. Bepreiste werden automatisch auf Angebot und Auftrag gesetzt.",
+    "prepNotCharged": "nicht berechnet"
   }
 }

+ 473 - 0
messages/de/tireHotel.json

@@ -0,0 +1,473 @@
+{
+  "title": "Reifenhotel",
+  "description": "Saisonale Reifeneinlagerung für Ihre Kunden",
+  "common": {
+    "cancel": "Abbrechen",
+    "save": "Speichern",
+    "create": "Erstellen",
+    "delete": "Löschen",
+    "edit": "Bearbeiten"
+  },
+  "seasons": {
+    "summer": "Sommer",
+    "winter": "Winter",
+    "all_season": "Ganzjahr",
+    "other": "Sonstige"
+  },
+  "statuses": {
+    "stored": "Eingelagert",
+    "released": "Zurückgegeben",
+    "disposed": "Entsorgt"
+  },
+  "conditions": {
+    "good": "Gut",
+    "fair": "Beobachten",
+    "replace": "Ersetzen"
+  },
+  "positions": {
+    "front_left": "Vorne links",
+    "front_right": "Vorne rechts",
+    "rear_left": "Hinten links",
+    "rear_right": "Hinten rechts",
+    "spare": "Reserverad",
+    "unspecified": "Nicht angegeben"
+  },
+  "capacity": {
+    "used": "{used} von {capacity} Reifen",
+    "free": "Platz für {count}",
+    "freeShort": "{count} frei",
+    "over": "{count} über Kapazität",
+    "meterLabel": "{used} von {capacity} Reifen eingelagert"
+  },
+  "tread": {
+    "placeholderMm": "mm",
+    "placeholder32": "32stel",
+    "inputLabel": "Profiltiefe, {position}",
+    "lastTime": "war {value}",
+    "worn": "{value} abgefahren"
+  },
+  "list": {
+    "tabAll": "Alle",
+    "tabStored": "Eingelagert",
+    "tabReleased": "Zurückgegeben",
+    "search": "Nach Kennzeichen, Kunde, Marke oder Regal suchen",
+    "checkIn": "Reifen einlagern",
+    "empty": "Noch keine Reifensätze. Lagern Sie einen Satz ein, um zu starten.",
+    "unassigned": "Nicht zugeordnet",
+    "reference": "Ref",
+    "vehicle": "Fahrzeug",
+    "tires": "Reifen",
+    "location": "Regal",
+    "condition": "Zustand",
+    "status": "Status",
+    "storedSince": "Seit",
+    "tireCount": "{count, plural, one {# Reifen} other {# Reifen}}",
+    "studded": "Spikes",
+    "roomLeft": "Platz für {count}",
+    "prep": "Vorb.",
+    "tabNeedsPrep": "Vorbereitung offen",
+    "setUpShelves": "Regale einrichten",
+    "emptyNoShelves": "Noch keine Regale. Richten Sie Ihr Lager ein, bevor Sie Reifen einlagern.",
+    "tabDisposed": "Ausgemustert"
+  },
+  "checkIn": {
+    "title": "Reifen einlagern",
+    "description": "Erfassen Sie, was angekommen ist, und wählen Sie den Lagerplatz.",
+    "customer": "Kunde",
+    "selectCustomer": "Kunde auswählen",
+    "noCustomer": "Kein Kunde",
+    "vehicle": "Fahrzeug",
+    "selectVehicle": "Fahrzeug auswählen",
+    "season": "Saison",
+    "quantity": "Anzahl Reifen",
+    "quantityHint": "Einzeln gezählt, damit auch ein Paar oder fünf Räder passen.",
+    "brand": "Marke",
+    "brandPlaceholder": "Michelin, Bridgestone, Continental",
+    "model": "Modell",
+    "size": "Größe",
+    "sizePlaceholder": "225/45R17",
+    "dotCode": "DOT-Nummer",
+    "dotPlaceholder": "3623",
+    "dotHint": "Woche und Jahr der Herstellung.",
+    "withRims": "Auf Felgen",
+    "hasTpms": "RDKS-Sensoren",
+    "studded": "Spikes",
+    "location": "Lagerplatz",
+    "choosePlace": "Regal wählen",
+    "searchLocations": "Regale durchsuchen",
+    "noLocations": "Keine Regale gefunden",
+    "tooFull": "Nicht genug Platz",
+    "pickLocationFirst": "Wählen Sie ein Regal, bevor Sie den Satz einlagern",
+    "roomSummary": "{locations} Regale passen für diesen Satz, {tires} Reifenplätze frei",
+    "nothingFits": "Kein Regal hat Platz für {quantity} Reifen. Schaffen Sie Platz oder legen Sie ein Regal an.",
+    "treadTitle": "Profiltiefe bei Anlieferung",
+    "treadHint": "Optional, klärt aber später jede Diskussion über den Zustand.",
+    "notes": "Notizen",
+    "submit": "Einlagern",
+    "success": "Satz {reference} auf {code} eingelagert",
+    "failed": "Einlagerung fehlgeschlagen",
+    "noVehiclesForCustomer": "Dieser Kunde hat noch keine Fahrzeuge. Legen Sie zuerst eines auf der Kundenseite an.",
+    "treatmentsHint": "Was mit diesem Satz vor der Einlagerung oder Montage geschehen soll.",
+    "storedBefore": "Schon einmal eingelagert",
+    "storedBeforeHint": "Kommen dieselben Reifen zurück? Satz wählen, dann gehört die Saison zur Historie.",
+    "sameSet": "Derselbe Satz wie zuvor",
+    "pieces": "{count} Stk.",
+    "lastOut": "Abgeholt {date}",
+    "notStored": "Derzeit nicht eingelagert",
+    "returned": "Satz {reference} liegt wieder auf {code}",
+    "submitReturn": "Erneut einlagern",
+    "ownerFromVehicle": "Vom Fahrzeug übernommen",
+    "vehicleOwner": "Der Fahrzeughalter",
+    "clearVehicle": "Fahrzeug entfernen"
+  },
+  "storage": {
+    "title": "Regale",
+    "description": "Lager einmal einrichten, dann auf einen Blick sehen, was wohin passt.",
+    "addWarehouse": "Lagerort hinzufügen",
+    "addShelves": "Regale hinzufügen",
+    "warehouseName": "Name",
+    "warehouseNamePlaceholder": "Hauptgebäude, Container 2, Nebenraum",
+    "warehouseAddress": "Adresse",
+    "warehouseCreated": "Lagerort hinzugefügt",
+    "warehouseDeleted": "{name} gelöscht",
+    "warehouseArchived": "{name} archiviert",
+    "deleteWarehouse": "Lagerort löschen",
+    "deleteWarehouseTitle": "{name} löschen?",
+    "deleteWarehouseBody": "Damit werden der Lagerort und alle seine Regale entfernt.",
+    "deleteWarehouseBodyOccupied": "Dieser Lagerort enthält noch {count} Reifen und wird deshalb archiviert statt gelöscht.",
+    "totalCapacity": "Gesamtkapazität",
+    "totalStored": "Aktuell eingelagert",
+    "totalFree": "Freier Platz",
+    "totalLocations": "Regale",
+    "locationSummary": "{occupied} von {total} Regalen belegt",
+    "noLocations": "Noch keine Regale. Legen Sie ein Regal an, um hier Reifen zu lagern.",
+    "emptyTitle": "Noch kein Lager eingerichtet",
+    "emptyBody": "Legen Sie Ihr erstes Gebäude oder Zimmer an und füllen Sie es mit Regalen.",
+    "default": "Standard",
+    "addLocation": "Regale hinzufügen",
+    "editLocation": "Regal bearbeiten",
+    "createLocation": "Regal anlegen",
+    "createShelves": "Regale anlegen",
+    "locationDialogDescription": "Lagerplatz zu {warehouse} hinzufügen.",
+    "tabBulk": "Ein Regalsystem",
+    "tabSingle": "Ein Regal",
+    "zone": "Zone",
+    "zonePlaceholder": "A, Raum 1, Nord",
+    "rack": "Regalreihe",
+    "rackPlaceholder": "04",
+    "shelf": "Fach",
+    "shelfPlaceholder": "2",
+    "position": "Position",
+    "positionPlaceholder": "Links",
+    "shelfFrom": "Fächer von",
+    "shelfTo": "bis",
+    "code": "Code",
+    "codePlaceholder": "A-04-2",
+    "codeHint": "Steht auf dem Etikett. Wird für Sie ausgefüllt, wenn Sie es leer lassen.",
+    "capacity": "Reifen pro Fach",
+    "capacityHint": "Wie viele einzelne Reifen hier hineinpassen.",
+    "notes": "Notizen",
+    "bulkPreview": "Erstellt {count} Fächer, {first} bis {last}",
+    "bulkPreviewCapacity": "Platz für insgesamt {tires} Reifen",
+    "bulkCreated": "{count} Fächer erstellt",
+    "bulkCreatedWithSkips": "{created} Fächer erstellt, {skipped} bereits vorhandene übersprungen",
+    "bulkNothingCreated": "Diese Fächer existieren bereits",
+    "locationCreated": "Regal hinzugefügt",
+    "locationUpdated": "Regal aktualisiert",
+    "locationDeleted": "{code} gelöscht",
+    "deleteLocationTitle": "{code} löschen?",
+    "deleteLocationBody": "Das Regal muss zuerst leer sein.",
+    "saveFailed": "Speichern fehlgeschlagen",
+    "deleteFailed": "Löschen fehlgeschlagen"
+  },
+  "movements": {
+    "check_in": "Eingelagert",
+    "check_out": "Ausgelagert",
+    "relocate": "Umgelagert",
+    "dispose": "Entsorgt",
+    "measure": "Gemessen"
+  },
+  "detail": {
+    "move": "Umlagern",
+    "checkOut": "Auslagern",
+    "conditionTitle": "Zustand",
+    "noMeasurements": "Noch keine Messwerte erfasst.",
+    "historyTitle": "Verlauf",
+    "storageTitle": "Lagerung",
+    "shelf": "Fach",
+    "warehouse": "Lagerort",
+    "notStored": "Derzeit nicht eingelagert.",
+    "checkedIn": "Eingelagert",
+    "checkedOut": "Ausgelagert",
+    "tiresTitle": "Reifen",
+    "loadSpeed": "Last- und Geschwindigkeitsindex",
+    "ownerTitle": "Besitzer",
+    "phone": "Telefon",
+    "deleteTitle": "Diesen Reifensatz löschen?",
+    "deleteBody": "Der Satz und sein gesamter Verlauf werden entfernt. Das lässt sich nicht rückgängig machen.",
+    "deleted": "Reifensatz gelöscht",
+    "deleteFailed": "Satz konnte nicht gelöscht werden",
+    "belowLimit": "{count} Reifen auf oder unter Ihrer Ersatzgrenze.",
+    "belowLimitHint": "Bieten Sie dem Kunden einen Preis an, solange die Reifen noch hier sind.",
+    "dispose": "Ausmustern",
+    "disposeTitle": "Diesen Satz ausmustern?",
+    "disposeBody": "Der Eintrag und seine Historie bleiben, der Regalplatz wird frei. Er wird nächste Saison nicht mehr als dieselben Reifen vorgeschlagen.",
+    "disposed": "Satz ausgemustert",
+    "disposeFailed": "Satz konnte nicht ausgemustert werden"
+  },
+  "checkOut": {
+    "title": "Reifen auslagern",
+    "description": "Der Satz wird von {code} genommen.",
+    "descriptionNoLocation": "Dieser Satz geht an den Kunden zurück.",
+    "treadTitle": "Profiltiefe bei der Ausgabe",
+    "note": "Notiz",
+    "submit": "Auslagern",
+    "success": "Satz {reference} zurückgegeben",
+    "failed": "Auslagern fehlgeschlagen",
+    "prepPending": "{count} Vorbereitungsarbeit(en) offen",
+    "conditionTitle": "Zustand bei Anlieferung",
+    "conditionLead": {
+      "replace": "Mindestens ein Reifen liegt auf oder unter Ihrer Ersatzgrenze. Sagen Sie es dem Kunden, bevor er losfährt.",
+      "fair": "Mindestens ein Reifen nähert sich Ihrer Ersatzgrenze. Vor der nächsten Saison ansprechen."
+    },
+    "treadHintUnmeasured": "Bei der Anlieferung wurde nicht gemessen. Dies ist die letzte Gelegenheit dafür."
+  },
+  "relocate": {
+    "title": "Reifen umlagern",
+    "description": "Wählen Sie das Fach, in das der Satz umzieht.",
+    "newLocation": "Neues Fach",
+    "note": "Notiz",
+    "submit": "Umlagern",
+    "success": "Satz {reference} umgelagert",
+    "failed": "Umlagern fehlgeschlagen"
+  },
+  "edit": {
+    "title": "Reifensatz bearbeiten",
+    "description": "Korrigieren Sie, was bei der Einlagerung erfasst wurde. Das Fach ändern Sie über Umlagern.",
+    "quantityHint": "Eine Erhöhung wird gegen das Fach geprüft, in dem der Satz liegt.",
+    "rimType": "Felgentyp",
+    "rimTypePlaceholder": "Alu, Stahl",
+    "success": "Reifensatz aktualisiert",
+    "failed": "Änderungen konnten nicht gespeichert werden",
+    "treadHint": "Eine geänderte Messung wird als neue Messung in der Historie festgehalten, statt die alte zu überschreiben."
+  },
+  "treatments": {
+    "types": {
+      "wash_tires": "Reifen waschen",
+      "wash_rims": "Felgen waschen",
+      "balance": "Wuchten",
+      "tpms_service": "RDKS-Service",
+      "new_valves": "Neue Ventile",
+      "repair": "Reparatur"
+    },
+    "statuses": {
+      "pending": "Offen",
+      "done": "Erledigt",
+      "skipped": "Übersprungen"
+    },
+    "title": "Vorbereitung",
+    "none": "An diesem Satz ist nichts zu tun.",
+    "add": "Vorbereitung hinzufügen",
+    "pickHint": "Haken Sie an, was dieser Satz braucht, bevor er zurück ans Fahrzeug geht.",
+    "pendingCount": "{count} offen",
+    "allComplete": "Alles erledigt",
+    "ready": "Bereit",
+    "markDone": "Erledigt",
+    "completeAll": "Alle {count} als erledigt markieren",
+    "allDone": "Vorbereitung abgeschlossen",
+    "undo": "Zurück auf die Liste",
+    "saveFailed": "Speichern fehlgeschlagen",
+    "skip": "Überspringen"
+  },
+  "messaging": {
+    "action": "Nachricht",
+    "title": "Kunden benachrichtigen",
+    "description": "An {name}.",
+    "noCustomer": "Kein Kunde an diesem Satz.",
+    "noCustomerBody": "Verknüpfen Sie einen Kunden mit dem Satz, bevor Sie schreiben.",
+    "channel": "Senden per",
+    "channels": {
+      "email": "E-Mail",
+      "sms": "SMS",
+      "telegram": "Telegram"
+    },
+    "noAddress": "keine Adresse hinterlegt",
+    "recipient": "Senden an",
+    "recipientPlaceholder": "Telefonnummer",
+    "subject": "Betreff",
+    "message": "Nachricht",
+    "send": "Senden",
+    "sent": "Nachricht gesendet",
+    "failed": "Nachricht konnte nicht gesendet werden",
+    "smsLong": "Lange Nachrichten werden eventuell auf mehrere SMS verteilt.",
+    "subjects": {
+      "low_tread": "Ihre {season}reifen",
+      "damage": "Ihre {season}reifen",
+      "stored": "Ihre {season}reifen sind eingelagert",
+      "custom": "Ihre {season}reifen"
+    },
+    "bodies": {
+      "low_tread": "Hallo {customer_name}, Ihre {season}reifen liegen bei uns bei {shop_name}. Bei der Annahme hatten {positions} noch {tread}, das ist unter der Grenze. Eine weitere Saison halten sie nicht durch. Sollen wir Ihnen einen Preis für einen neuen Satz machen?",
+      "damage": "Hallo {customer_name}, Ihre {season}reifen liegen bei uns bei {shop_name}. Bei der Annahme haben wir an {positions} eine Beschädigung gefunden. Rufen Sie uns an, wenn es passt, dann gehen wir es zusammen durch.",
+      "stored": "Hallo {customer_name}, Ihre {season}reifen liegen jetzt bei uns bei {shop_name}. Wir melden uns, wenn der Wechsel ansteht.",
+      "custom": "Hallo {customer_name}, zu Ihren bei {shop_name} eingelagerten {season}reifen:"
+    },
+    "enableInSettings": "Noch nicht eingerichtet. In den Einstellungen aktivieren."
+  },
+  "job": {
+    "cardTitle": "Angebote und Aufträge",
+    "none": "Für diesen Satz wurde noch nichts angelegt.",
+    "quote": "Angebot",
+    "workOrder": "Auftrag",
+    "converted": "umgewandelt",
+    "quoteTitle": "Neuen Satz anbieten",
+    "workOrderTitle": "Diesen Satz abrechnen",
+    "loadingSet": "Satz wird geladen",
+    "tires": "Reifen",
+    "inStock": "{count} auf Lager",
+    "shortStock": "nur {count} auf Lager",
+    "noMatch": "Nichts auf Lager passt zu {size}. Die Position wird mit Ihrem Preis angelegt.",
+    "noSize": "Für diesen Satz ist keine Größe erfasst, daher kein Lagerabgleich. Die Position wird mit Ihrem Preis angelegt.",
+    "unitPrice": "Preis pro Reifen",
+    "createQuote": "Angebot erstellen",
+    "createWorkOrder": "Auftrag erstellen",
+    "quoteCreated": "Angebot {number} erstellt",
+    "workOrderCreated": "Auftrag erstellt",
+    "failed": "Konnte nicht angelegt werden",
+    "bannerTitle": "Holen von",
+    "openSet": "Reifensatz öffnen",
+    "prepPending": "Vor der Montage offen: {jobs}",
+    "notOnShelf": "Dieser Satz ist nicht mehr eingelagert.",
+    "targetNew": "Neuer Auftrag",
+    "targetNewHint": "Einen Auftrag nur für diese Reifen anlegen.",
+    "targetExisting": "Oder zu einem bereits offenen Auftrag für dieses Fahrzeug",
+    "addToWorkOrder": "Zum Auftrag hinzufügen",
+    "workOrderUpdated": "Zum Auftrag hinzugefügt",
+    "unlink": "Verknüpfung lösen",
+    "unlinkTitle": "Diese Reifen vom Auftrag lösen?",
+    "unlinkBody": "Der Auftrag verweist nicht mehr auf den Reifensatz. Bereits erfasste Teile und Arbeitspositionen bleiben stehen; entfernen Sie sie separat, wenn sie nicht berechnet werden sollen.",
+    "unlinked": "Reifen gelöst",
+    "unlinkFailed": "Verknüpfung konnte nicht gelöst werden",
+    "prep": "Vorbereitung",
+    "tiresSkipped": "Keine Reifenposition wird hinzugefügt.",
+    "total": "Gesamt",
+    "searchStock": "Lager nach Marke, Größe oder Artikelnummer durchsuchen",
+    "noSearchHits": "Nichts im Lager passt zu \"{query}\".",
+    "otherSize": "Andere Größe",
+    "inSize": "{count} in {size}",
+    "quantity": "Anz.",
+    "noStockItem": "Kein Lagerartikel, Preis von Hand",
+    "storeTires": "Reifen einlagern",
+    "storage": "Lagerung",
+    "storageFee": "Gebühr",
+    "storagePeriod": "Zeitraum",
+    "destination": "Wohin es geht",
+    "targetInvoice": "Neue Rechnung",
+    "targetInvoiceHint": "Direkt an den Kunden, ohne Auftrag an einem Fahrzeug.",
+    "createInvoice": "Rechnung erstellen",
+    "needsCustomer": "Verknüpfen Sie einen Kunden oder ein Fahrzeug, bevor Sie abrechnen.",
+    "storagePeriodEnd": "Abgeholt",
+    "storageOpenEnded": "Offen",
+    "storagePeriodHint": "Erscheint auf der Rechnungsposition, damit der Kunde sieht, wofür die Gebühr gilt. Enddatum offen lassen, bis der Abholtermin feststeht.",
+    "pricesHint": "Die Preise für die Vorbereitung stammen aus den Einstellungen. Jede Position lässt sich danach noch ändern.",
+    "files": "Fotos und Dateien",
+    "filesCount": "{count} auf der Rechnung"
+  },
+  "label": {
+    "action": "Etiketten",
+    "title": "Reifenetiketten drucken",
+    "description": "Aufkleber für jeden Reifen, mit QR-Code zu diesem Satz.",
+    "format": "Etikettengröße",
+    "formats": {
+      "dymo_small": "Kleine Rolle",
+      "dymo_standard": "Standardrolle",
+      "thermal_large": "Große Rolle",
+      "sheet_a4": "A4-Bogen"
+    },
+    "perSheet": "{count} pro Bogen",
+    "copies": "Wie viele",
+    "copiesHint": "einer pro Reifen ({count})",
+    "fewer": "Einer weniger",
+    "more": "Einer mehr",
+    "print": "Drucken",
+    "shelfNote": "Das Fach steht bewusst nicht auf dem Etikett. Sätze werden umgelagert, und ein gedrucktes Fach wäre dann falsch. Der QR-Code zeigt immer den aktuellen Platz.",
+    "reference": "Ref",
+    "quantity": "{count, plural, one {# Reifen} other {# Reifen}}",
+    "withRims": "Auf Felgen",
+    "tpms": "RDKS",
+    "studded": "Spikes",
+    "unassigned": "Nicht zugeordnet",
+    "preview": "Vorschau",
+    "previewScale": "In Druckgröße dargestellt. Der Code lässt sich auch vom Bildschirm scannen.",
+    "previewFailed": "Vorschau nicht möglich"
+  },
+  "vehicle": {
+    "oneOnShelf": "auf {code}",
+    "inStorage": "im Reifenhotel"
+  },
+  "wear": {
+    "summary": "{value} abgefahren zwischen {from} und {to}",
+    "perSeason": "Etwa {value} pro Saison",
+    "seasonsLeft": "{count, plural, =0 {Jetzt an der Grenze} one {Noch etwa eine Saison} other {Noch etwa # Saisons}}",
+    "since": "-{value}"
+  },
+  "settings": {
+    "change": "In den Einstellungen ändern",
+    "prepPrices": "Preise für Vorbereitung",
+    "storagePrices": "Lagerpreise in den Einstellungen",
+    "treadLimits": "Grenzwert ändern",
+    "defaultCapacity": "Standardkapazität"
+  },
+  "forecast": {
+    "title": "Nächste Saison",
+    "subtitle": "Was die eingelagerten Sätze laut Messung brauchen, nach Größe.",
+    "backToList": "Zurück zu den Sätzen",
+    "tires": "Erwartete Reifen",
+    "atLimit": "Bereits an der Grenze",
+    "expected": "Innerhalb einer Saison erwartet",
+    "orderTitle": "Nach Größe",
+    "sizes": "{count} Größen",
+    "empty": "Noch nichts nahe der Grenze gemessen. Ein Satz braucht zwei Besuche, bevor sich eine Rate ergibt.",
+    "nowCount": "{count, plural, one {# Satz an der Grenze} other {# Sätze an der Grenze}}",
+    "nextCount": "{count, plural, one {# Satz innerhalb einer Saison} other {# Sätze innerhalb einer Saison}}",
+    "tiresShort": "Reifen",
+    "lowest": "niedrigster {value}",
+    "now": "An der Grenze",
+    "next": "Innerhalb einer Saison",
+    "rate": "{value} pro Saison",
+    "caveat": "Sätze an der Grenze sind gemessen. Der Rest ist aus dem Verschleiß eines Kunden über ein Intervall hochgerechnet: gut zur Planung, nicht als Bestellung.",
+    "unmeasured": "{count, plural, one {# Satz hat zu wenig Historie und wird nicht gezählt} other {# Sätze haben zu wenig Historie und werden nicht gezählt}}",
+    "capped": "Die {shown} neuesten von {total} Sätzen ausgewertet."
+  },
+  "invoiceLine": {
+    "storage": "Reifeneinlagerung",
+    "pieces": "Stk.",
+    "fromDate": "ab {date}"
+  },
+  "setNames": {
+    "winter": "Winterreifen",
+    "summer": "Sommerreifen",
+    "all_season": "Ganzjahresreifen",
+    "other": "Reifen"
+  },
+  "jobNotes": {
+    "set": "Reifensatz {reference}",
+    "shelf": "Regal: {shelf}",
+    "notOnShelf": "nicht im Regal",
+    "onRims": "Auf Felgen",
+    "tiresOnly": "Nur Reifen",
+    "tpms": "Mit RDKS-Sensoren"
+  },
+  "files": {
+    "title": "Fotos und Dateien",
+    "add": "Dateien hinzufügen",
+    "none": "Noch nichts angehängt. Fotografieren Sie eine beschädigte Felge, solange die Reifen vor Ihnen liegen.",
+    "captionPlaceholder": "Was ist darauf zu sehen?",
+    "onInvoice": "Auf der Rechnung",
+    "invoiceHint": "Für die Rechnung markierte Dateien werden beim Abrechnen auf den Auftrag oder die Rechnung kopiert.",
+    "deleteTitle": "Diese Datei entfernen?",
+    "deleteBody": "{name} wird vom Satz entfernt. Bereits auf eine Rechnung kopierte Dateien bleiben dort.",
+    "failed": "Konnte nicht gespeichert werden",
+    "someFailed": "{count, plural, one {# Datei konnte nicht hochgeladen werden} other {# Dateien konnten nicht hochgeladen werden}}"
+  }
+}

+ 27 - 1
messages/en/audit.json

@@ -109,6 +109,32 @@
     "settings_applyTaxRate": "Applied Tax Rate",
     "settings_convertToExclusive": "Converted Prices to Tax Exclusive",
     "settings_convertToInclusive": "Converted Prices to Tax Inclusive",
-    "organization_sampleDataRemove": "Removed Sample Data"
+    "organization_sampleDataRemove": "Removed Sample Data",
+    "tire_warehouse_create": "Created tire storage location",
+    "tire_warehouse_update": "Updated tire storage location",
+    "tire_warehouse_delete": "Deleted tire storage location",
+    "tire_warehouse_archive": "Archived tire storage location",
+    "tire_location_create": "Created tire shelf",
+    "tire_location_bulk_create": "Created tire shelves",
+    "tire_location_update": "Updated tire shelf",
+    "tire_location_delete": "Deleted tire shelf",
+    "tire_set_check_in": "Checked in tire set",
+    "tire_set_check_out": "Checked out tire set",
+    "tire_set_relocate": "Moved tire set",
+    "tire_set_update": "Updated tire set",
+    "tire_set_delete": "Deleted tire set",
+    "tire_measurement_create": "Recorded tire measurements",
+    "tire_treatment_update": "Updated prep work",
+    "tire_treatment_mark": "Marked prep work",
+    "tire_treatment_complete_all": "Completed prep work",
+    "tire_set_message_customer": "Messaged a customer about a tire set",
+    "tire_set_create_quote": "Created a quote from a tire set",
+    "tire_set_create_work_order": "Created a work order from a tire set",
+    "tire_set_add_to_work_order": "Added a tire set to a work order",
+    "tire_set_unlink_work_order": "Unlinked a tire set from a work order",
+    "tire_set_return": "Stored a set again",
+    "tire_set_dispose": "Wrote off a tire set",
+    "tire_set_attach": "Added files to a tire set",
+    "tire_set_detach": "Removed a file from a tire set"
   }
 }

+ 2 - 1
messages/en/common.json

@@ -10,7 +10,8 @@
     "back": "Back",
     "confirm": "Confirm",
     "edit": "Edit",
-    "add": "Add"
+    "add": "Add",
+    "gotIt": "Got it"
   },
   "contextMenu": {
     "hint": "Tip: right-click a row for actions",

+ 11 - 1
messages/en/dashboard.json

@@ -54,7 +54,8 @@
     "activeJobs": "Active Jobs",
     "recentObservations": "Recent Observations",
     "recentActivity": "Recent Activity",
-    "gettingStarted": "Getting Started"
+    "gettingStarted": "Getting Started",
+    "tireHotel": "Tire Hotel"
   },
   "maintenance": {
     "title": "Predicted Maintenance Due",
@@ -234,5 +235,14 @@
       "expired": "Expired",
       "converted": "Converted"
     }
+  },
+  "tireHotel": {
+    "title": "Tire Hotel",
+    "viewAll": "View tire hotel",
+    "stored": "{count} sets stored",
+    "capacity": "{used} / {capacity} tires",
+    "roomLeft": "Room for {count} more",
+    "needsPrep": "Needs prep",
+    "unbilled": "Unbilled ({count})"
   }
 }

+ 6 - 0
messages/en/featureHints.json

@@ -0,0 +1,6 @@
+{
+  "tire-hotel": {
+    "title": "Tire hotel is on",
+    "body": "Store customers’ seasonal tires. Set up shelves, then check a set in."
+  }
+}

+ 8 - 2
messages/en/navigation.json

@@ -53,7 +53,12 @@
     "allReminders": "All Reminders",
     "auditLog": "Audit Log",
     "aiAssistant": "AI Assistant",
-    "observations": "Observations"
+    "observations": "Observations",
+    "tireHotel": "Tire Hotel",
+    "allTireSets": "All Tire Sets",
+    "tireStorage": "Storage Layout",
+    "tireSetDetails": "Tire Set",
+    "tireForecast": "Next season"
   },
   "search": "Search...",
   "shortcut": "Ctrl+K",
@@ -107,6 +112,7 @@
     "signOut": "Sign Out",
     "more": "More",
     "auditLog": "Audit Log",
-    "aiAssistant": "AI Assistant"
+    "aiAssistant": "AI Assistant",
+    "tireHotel": "Tire hotel"
   }
 }

+ 27 - 0
messages/en/permissions.json

@@ -0,0 +1,27 @@
+{
+  "subjects": {
+    "dashboard": "Dashboard",
+    "vehicles": "Vehicles",
+    "customers": "Customers",
+    "work_orders": "Work orders",
+    "quotes": "Quotes",
+    "services": "Services",
+    "billing": "Billing",
+    "inventory": "Inventory",
+    "labor_presets": "Labor presets",
+    "inspections": "Inspections",
+    "tire_hotel": "Tire hotel",
+    "reports": "Reports",
+    "work_board": "Work board",
+    "ai_assistant": "AI assistant",
+    "settings": "Settings"
+  },
+  "actions": {
+    "create": "Create",
+    "read": "View",
+    "update": "Edit",
+    "delete": "Delete",
+    "manage": "Manage",
+    "manage_storage": "Manage storage layout"
+  }
+}

+ 9 - 0
messages/en/search.json

@@ -0,0 +1,9 @@
+{
+  "title": "Search",
+  "description": "Search for vehicles, customers, services and settings",
+  "placeholder": "Search by name, plate, phone, VIN, invoice…",
+  "noResults": "No results found.",
+  "recentCustomers": "Recent customers",
+  "services": "Service records",
+  "toClose": "to close"
+}

+ 34 - 1
messages/en/settings.json

@@ -122,6 +122,10 @@
       "support": {
         "title": "Support",
         "description": "Contact support and manage the support button"
+      },
+      "tireHotel": {
+        "title": "Tire hotel",
+        "description": "Seasonal tire storage"
       }
     },
     "pro": "PRO"
@@ -974,7 +978,9 @@
       "lastBackup": "Last backup:",
       "supportPrompt": "If you have any questions about how your data is protected,",
       "supportAction": "contact our support team."
-    }
+    },
+    "optTireHotel": "Tire hotel",
+    "optTireHotelDesc": "Stored tire sets, shelves, prep work and storage agreements"
   },
   "license": {
     "title": "White-Label License",
@@ -1454,5 +1460,32 @@
     "unavailable": "This browser has not offered an install prompt. Open the browser menu and look for \"Install app\" or \"Add to Home Screen\".",
     "bannerHidden": "The install banner is hidden on this device.",
     "showBanner": "Show banner again"
+  },
+  "tireHotel": {
+    "title": "Tire hotel",
+    "description": "Store your customers’ seasonal tires and keep track of where every set sits.",
+    "cardTitle": "Tire storage",
+    "enable": "Enable tire hotel",
+    "enableHint": "Adds tire storage to the sidebar. Turning it off later hides it again without deleting anything.",
+    "defaultCapacity": "Default tires per shelf",
+    "defaultCapacityHint": "Prefilled when you add new shelves. You can change it per shelf.",
+    "warnPercent": "Warn when full (%)",
+    "warnPercentHint": "Flags the storage overview once this share of total capacity is in use.",
+    "treadTitle": "Replacement thresholds",
+    "treadHint": "Legal minimums differ by country, so set the limits your market uses. Tread below the limit is flagged for replacement.",
+    "summerLimit": "Summer limit ({unit})",
+    "winterLimit": "Winter limit ({unit})",
+    "unitMm": "mm",
+    "unit32nds": "32nds",
+    "setupHint": "Next, set up your shelves.",
+    "setupLink": "Open storage layout",
+    "save": "Save tire hotel settings",
+    "saved": "Tire hotel settings saved",
+    "billingTitle": "Storage billing",
+    "billingHint": "Prefilled on the storage line when you bill a set. Editable on the bill.",
+    "seasonalPrice": "Default storage price",
+    "prepTitle": "Prep pricing",
+    "prepHint": "What each kind of prep costs. Leave one blank and it is never billed, which is what a shop that includes washing in the storage fee wants. Priced ones are added to a quote or work order automatically.",
+    "prepNotCharged": "not charged"
   }
 }

+ 473 - 0
messages/en/tireHotel.json

@@ -0,0 +1,473 @@
+{
+  "title": "Tire hotel",
+  "description": "Seasonal tire storage for your customers",
+  "common": {
+    "cancel": "Cancel",
+    "save": "Save",
+    "create": "Create",
+    "delete": "Delete",
+    "edit": "Edit"
+  },
+  "seasons": {
+    "summer": "Summer",
+    "winter": "Winter",
+    "all_season": "All season",
+    "other": "Other"
+  },
+  "statuses": {
+    "stored": "In storage",
+    "released": "Returned",
+    "disposed": "Disposed"
+  },
+  "conditions": {
+    "good": "Good",
+    "fair": "Watch",
+    "replace": "Replace"
+  },
+  "positions": {
+    "front_left": "Front left",
+    "front_right": "Front right",
+    "rear_left": "Rear left",
+    "rear_right": "Rear right",
+    "spare": "Spare",
+    "unspecified": "Unspecified"
+  },
+  "capacity": {
+    "used": "{used} of {capacity} tires",
+    "free": "Room for {count}",
+    "freeShort": "{count} free",
+    "over": "{count} over capacity",
+    "meterLabel": "{used} of {capacity} tires stored"
+  },
+  "tread": {
+    "placeholderMm": "mm",
+    "placeholder32": "32nds",
+    "inputLabel": "Tread depth, {position}",
+    "lastTime": "was {value}",
+    "worn": "{value} worn"
+  },
+  "list": {
+    "tabAll": "All",
+    "tabStored": "In storage",
+    "tabReleased": "Returned",
+    "search": "Search by plate, customer, brand or shelf",
+    "checkIn": "Check in tires",
+    "empty": "No tire sets yet. Check in a set to get started.",
+    "unassigned": "Unassigned",
+    "reference": "Ref",
+    "vehicle": "Vehicle",
+    "tires": "Tires",
+    "location": "Shelf",
+    "condition": "Condition",
+    "status": "Status",
+    "storedSince": "Since",
+    "tireCount": "{count, plural, one {# tire} other {# tires}}",
+    "studded": "Studded",
+    "roomLeft": "Room for {count}",
+    "prep": "Prep",
+    "tabNeedsPrep": "Needs prep",
+    "setUpShelves": "Set up shelves",
+    "emptyNoShelves": "No shelves yet. Set up your storage layout before checking tires in.",
+    "tabDisposed": "Written off"
+  },
+  "checkIn": {
+    "title": "Check in tires",
+    "description": "Record what arrived, then choose where it goes.",
+    "customer": "Customer",
+    "selectCustomer": "Select customer",
+    "noCustomer": "No customer",
+    "vehicle": "Vehicle",
+    "selectVehicle": "Select vehicle",
+    "season": "Season",
+    "quantity": "Number of tires",
+    "quantityHint": "Counted individually, so a pair or a five-wheel set works too.",
+    "brand": "Brand",
+    "brandPlaceholder": "Michelin, Bridgestone, Nokian",
+    "model": "Model",
+    "size": "Size",
+    "sizePlaceholder": "225/45R17",
+    "dotCode": "DOT / TIN code",
+    "dotPlaceholder": "3623",
+    "dotHint": "Week and year the tire was made.",
+    "withRims": "On rims",
+    "hasTpms": "TPMS sensors",
+    "studded": "Studded",
+    "location": "Where the tires go",
+    "choosePlace": "Choose a shelf",
+    "searchLocations": "Search shelves",
+    "noLocations": "No shelves found",
+    "tooFull": "Not enough room",
+    "pickLocationFirst": "Choose a shelf before checking the set in",
+    "roomSummary": "{locations} shelves fit this set, {tires} tire slots free",
+    "nothingFits": "No shelf has room for {quantity} tires. Free up space or add another shelf.",
+    "treadTitle": "Tread depth on arrival",
+    "treadHint": "Optional, but it settles any dispute about condition later.",
+    "notes": "Notes",
+    "submit": "Check in",
+    "success": "Set {reference} stored on {code}",
+    "failed": "Could not check the set in",
+    "noVehiclesForCustomer": "This customer has no vehicles yet. Add one from their customer page first.",
+    "treatmentsHint": "What should be done to this set before it is stored or refitted.",
+    "storedBefore": "Stored here before",
+    "storedBeforeHint": "Bringing the same tires back? Pick the set so this season joins its history.",
+    "sameSet": "Same set as before",
+    "pieces": "{count} pcs",
+    "lastOut": "Left {date}",
+    "notStored": "Not currently stored",
+    "returned": "Set {reference} is back on {code}",
+    "submitReturn": "Store again",
+    "ownerFromVehicle": "Taken from the vehicle",
+    "vehicleOwner": "The vehicle owner",
+    "clearVehicle": "Clear the vehicle"
+  },
+  "storage": {
+    "title": "Shelves",
+    "description": "Set up your storage once, then see at a glance what fits where.",
+    "addWarehouse": "Add location",
+    "addShelves": "Add shelves",
+    "warehouseName": "Name",
+    "warehouseNamePlaceholder": "Main building, Container 2, Back room",
+    "warehouseAddress": "Address",
+    "warehouseCreated": "Storage location added",
+    "warehouseDeleted": "Deleted {name}",
+    "warehouseArchived": "Archived {name}",
+    "deleteWarehouse": "Delete storage location",
+    "deleteWarehouseTitle": "Delete {name}?",
+    "deleteWarehouseBody": "This removes the location and all its shelves.",
+    "deleteWarehouseBodyOccupied": "This location still holds {count} tires, so it will be archived rather than deleted.",
+    "totalCapacity": "Total capacity",
+    "totalStored": "Stored now",
+    "totalFree": "Room left",
+    "totalLocations": "Shelves",
+    "locationSummary": "{occupied} of {total} shelves in use",
+    "noLocations": "No shelves yet. Add a rack to start storing tires here.",
+    "emptyTitle": "No storage set up yet",
+    "emptyBody": "Add your first building or room, then fill it with shelves.",
+    "default": "Default",
+    "addLocation": "Add shelves",
+    "editLocation": "Edit shelf",
+    "createLocation": "Create shelf",
+    "createShelves": "Create shelves",
+    "locationDialogDescription": "Adding storage to {warehouse}.",
+    "tabBulk": "A rack of shelves",
+    "tabSingle": "One shelf",
+    "zone": "Zone",
+    "zonePlaceholder": "A, Room 1, North",
+    "rack": "Rack",
+    "rackPlaceholder": "04",
+    "shelf": "Shelf",
+    "shelfPlaceholder": "2",
+    "position": "Position",
+    "positionPlaceholder": "Left",
+    "shelfFrom": "Shelves from",
+    "shelfTo": "to",
+    "code": "Code",
+    "codePlaceholder": "A-04-2",
+    "codeHint": "Written on the label. Filled in for you if you leave it blank.",
+    "capacity": "Tires per shelf",
+    "capacityHint": "How many individual tires fit here.",
+    "notes": "Notes",
+    "bulkPreview": "Creates {count} shelves, {first} through {last}",
+    "bulkPreviewCapacity": "Room for {tires} tires in total",
+    "bulkCreated": "Created {count} shelves",
+    "bulkCreatedWithSkips": "Created {created} shelves, skipped {skipped} that already existed",
+    "bulkNothingCreated": "Those shelves already exist",
+    "locationCreated": "Shelf added",
+    "locationUpdated": "Shelf updated",
+    "locationDeleted": "Deleted {code}",
+    "deleteLocationTitle": "Delete {code}?",
+    "deleteLocationBody": "The shelf has to be empty first.",
+    "saveFailed": "Could not save",
+    "deleteFailed": "Could not delete"
+  },
+  "movements": {
+    "check_in": "Checked in",
+    "check_out": "Checked out",
+    "relocate": "Moved",
+    "dispose": "Disposed",
+    "measure": "Measured"
+  },
+  "detail": {
+    "move": "Move",
+    "checkOut": "Check out",
+    "conditionTitle": "Condition",
+    "noMeasurements": "No readings recorded yet.",
+    "historyTitle": "History",
+    "storageTitle": "Storage",
+    "shelf": "Shelf",
+    "warehouse": "Location",
+    "notStored": "Not currently in storage.",
+    "checkedIn": "Checked in",
+    "checkedOut": "Checked out",
+    "tiresTitle": "Tires",
+    "loadSpeed": "Load and speed",
+    "ownerTitle": "Owner",
+    "phone": "Phone",
+    "deleteTitle": "Delete this tire set?",
+    "deleteBody": "The set and its full history are removed. This cannot be undone.",
+    "deleted": "Tire set deleted",
+    "deleteFailed": "Could not delete the set",
+    "belowLimit": "{count} tire(s) at or below your replacement limit.",
+    "belowLimitHint": "Worth offering the customer a price while the tires are still here.",
+    "dispose": "Write off",
+    "disposeTitle": "Write this set off?",
+    "disposeBody": "The record and its history stay, and the shelf is freed. It stops being offered as the same tires next season.",
+    "disposed": "Set written off",
+    "disposeFailed": "Could not write the set off"
+  },
+  "checkOut": {
+    "title": "Check out tires",
+    "description": "Taking the set off {code}.",
+    "descriptionNoLocation": "Returning this set to the customer.",
+    "treadTitle": "Tread depth on the way out",
+    "note": "Note",
+    "submit": "Check out",
+    "success": "Set {reference} returned",
+    "failed": "Could not check the set out",
+    "prepPending": "{count} prep job(s) still outstanding",
+    "conditionTitle": "Condition on arrival",
+    "conditionLead": {
+      "replace": "One or more tires are at or below your replacement limit. Worth telling the customer before they drive off.",
+      "fair": "One or more tires are close to your replacement limit. Worth a word before next season."
+    },
+    "treadHintUnmeasured": "No reading was taken when these came in. This is the last chance to record one."
+  },
+  "relocate": {
+    "title": "Move tires",
+    "description": "Pick the shelf this set moves to.",
+    "newLocation": "New shelf",
+    "note": "Note",
+    "submit": "Move",
+    "success": "Set {reference} moved",
+    "failed": "Could not move the set"
+  },
+  "edit": {
+    "title": "Edit tire set",
+    "description": "Correct what was recorded at check-in. Use Move to change the shelf.",
+    "quantityHint": "Raising this is checked against the shelf the set sits on.",
+    "rimType": "Rim type",
+    "rimTypePlaceholder": "Alloy, steel",
+    "success": "Tire set updated",
+    "failed": "Could not save the changes",
+    "treadHint": "Changing a reading records a new measurement in the history rather than rewriting the old one."
+  },
+  "treatments": {
+    "types": {
+      "wash_tires": "Wash tires",
+      "wash_rims": "Wash rims",
+      "balance": "Balance",
+      "tpms_service": "TPMS service",
+      "new_valves": "New valves",
+      "repair": "Repair"
+    },
+    "statuses": {
+      "pending": "To do",
+      "done": "Done",
+      "skipped": "Skipped"
+    },
+    "title": "Prep work",
+    "none": "Nothing to do on this set.",
+    "add": "Add prep work",
+    "pickHint": "Tick what this set needs before it goes back on the car.",
+    "pendingCount": "{count} to do",
+    "allComplete": "All done",
+    "ready": "Ready",
+    "markDone": "Done",
+    "completeAll": "Mark all {count} done",
+    "allDone": "All prep work done",
+    "undo": "Put back on the list",
+    "saveFailed": "Could not save",
+    "skip": "Skip"
+  },
+  "messaging": {
+    "action": "Message",
+    "title": "Message the customer",
+    "description": "To {name}.",
+    "noCustomer": "No customer on this set.",
+    "noCustomerBody": "Link a customer to the set before messaging them.",
+    "channel": "Send by",
+    "channels": {
+      "email": "Email",
+      "sms": "SMS",
+      "telegram": "Telegram"
+    },
+    "noAddress": "no address on file",
+    "recipient": "Send to",
+    "recipientPlaceholder": "Phone number",
+    "subject": "Subject",
+    "message": "Message",
+    "send": "Send",
+    "sent": "Message sent",
+    "failed": "Could not send the message",
+    "smsLong": "Long messages may be split into several texts.",
+    "subjects": {
+      "low_tread": "Your {season} tires",
+      "damage": "Your {season} tires",
+      "stored": "Your {season} tires are stored",
+      "custom": "Your {season} tires"
+    },
+    "bodies": {
+      "low_tread": "Hi {customer_name}, your {season} tires are stored with us at {shop_name}. When we checked them in, the {positions} measured {tread}, which is under the limit. They won't get through another season. Want a price on a new set?",
+      "damage": "Hi {customer_name}, your {season} tires are stored with us at {shop_name}. We found damage on the {positions} when we checked them in. Give us a call when you get a chance and we'll go through it with you.",
+      "stored": "Hi {customer_name}, your {season} tires are now in storage with us at {shop_name}. We'll let you know when it's time to swap them back.",
+      "custom": "Hi {customer_name}, about your {season} tires stored with us at {shop_name}:"
+    },
+    "enableInSettings": "Not set up yet. Turn it on in Settings."
+  },
+  "job": {
+    "cardTitle": "Quotes and jobs",
+    "none": "Nothing raised for this set yet.",
+    "quote": "Quote",
+    "workOrder": "Work order",
+    "converted": "converted",
+    "quoteTitle": "Quote a new set",
+    "workOrderTitle": "Bill this set",
+    "loadingSet": "Loading the set",
+    "tires": "Tires",
+    "inStock": "{count} in stock",
+    "shortStock": "only {count} in stock",
+    "noMatch": "Nothing in stock matches {size}. The line is added at the price you set.",
+    "noSize": "This set has no size recorded, so stock cannot be matched. The line is added at the price you set.",
+    "unitPrice": "Price per tire",
+    "createQuote": "Create quote",
+    "createWorkOrder": "Create work order",
+    "quoteCreated": "Quote {number} created",
+    "workOrderCreated": "Work order created",
+    "failed": "Could not create that",
+    "bannerTitle": "Fetch from",
+    "openSet": "Open tire set",
+    "prepPending": "Still to do before fitting: {jobs}",
+    "notOnShelf": "This set is no longer in storage.",
+    "targetNew": "New work order",
+    "targetNewHint": "Raise a job just for these tires.",
+    "targetExisting": "Or add to a job already open for this vehicle",
+    "addToWorkOrder": "Add to work order",
+    "workOrderUpdated": "Added to the work order",
+    "unlink": "Unlink",
+    "unlinkTitle": "Unlink these tires from the job?",
+    "unlinkBody": "The job stops pointing at the tire set. The parts and labour lines already on it stay, so remove them separately if they should not be billed.",
+    "unlinked": "Tires unlinked",
+    "unlinkFailed": "Could not unlink the tires",
+    "prep": "Prep work",
+    "tiresSkipped": "No tire line added.",
+    "total": "Total",
+    "searchStock": "Search inventory by brand, size or part number",
+    "noSearchHits": "Nothing in inventory matches \"{query}\".",
+    "otherSize": "Other size",
+    "inSize": "{count} in {size}",
+    "quantity": "Qty",
+    "noStockItem": "No stock item, priced by hand",
+    "storeTires": "Store tires",
+    "storage": "Storage",
+    "storageFee": "Fee",
+    "storagePeriod": "Period",
+    "destination": "Where it goes",
+    "targetInvoice": "New invoice",
+    "targetInvoiceHint": "Bill the customer directly, with no job on a vehicle.",
+    "createInvoice": "Create invoice",
+    "needsCustomer": "Link a customer or a vehicle to this set before billing it.",
+    "storagePeriodEnd": "Collected",
+    "storageOpenEnded": "Open",
+    "storagePeriodHint": "Prints on the invoice line so the customer can see what the fee covers. Leave the end blank until you know when they are collecting.",
+    "pricesHint": "Prep prices come from settings. Every line can still be edited after this is created.",
+    "files": "Photos and files",
+    "filesCount": "{count} on the invoice"
+  },
+  "label": {
+    "action": "Labels",
+    "title": "Print tire labels",
+    "description": "Stickers for each tire, with a QR that opens this set.",
+    "format": "Label size",
+    "formats": {
+      "dymo_small": "Small roll",
+      "dymo_standard": "Standard roll",
+      "thermal_large": "Large roll",
+      "sheet_a4": "A4 sheet"
+    },
+    "perSheet": "{count} per sheet",
+    "copies": "How many",
+    "copiesHint": "one per tire ({count})",
+    "fewer": "One fewer",
+    "more": "One more",
+    "print": "Print",
+    "shelfNote": "The shelf is not printed on purpose. Sets get moved, and a printed shelf would then be wrong. Scanning the QR always shows where it is now.",
+    "reference": "Ref",
+    "quantity": "{count, plural, one {# tire} other {# tires}}",
+    "withRims": "On rims",
+    "tpms": "TPMS",
+    "studded": "Studded",
+    "unassigned": "Unassigned",
+    "preview": "Preview",
+    "previewScale": "Shown at print size. Scanning the code on screen works too.",
+    "previewFailed": "Could not build a preview"
+  },
+  "vehicle": {
+    "oneOnShelf": "on {code}",
+    "inStorage": "in tire hotel"
+  },
+  "wear": {
+    "summary": "Worn {value} between {from} and {to}",
+    "perSeason": "About {value} a season",
+    "seasonsLeft": "{count, plural, =0 {At the limit now} one {Roughly one more season} other {Roughly # more seasons}}",
+    "since": "-{value}"
+  },
+  "settings": {
+    "change": "Change in settings",
+    "prepPrices": "Prep prices in settings",
+    "storagePrices": "Storage prices in settings",
+    "treadLimits": "Change the limit",
+    "defaultCapacity": "Default capacity"
+  },
+  "forecast": {
+    "title": "Next season",
+    "subtitle": "What your stored sets are measured to need, by size.",
+    "backToList": "Back to sets",
+    "tires": "Tires expected",
+    "atLimit": "Already at the limit",
+    "expected": "Expected within a season",
+    "orderTitle": "By size",
+    "sizes": "{count} sizes",
+    "empty": "Nothing is measured close to its limit yet. Sets need two visits before a rate can be worked out.",
+    "nowCount": "{count, plural, one {# set at the limit} other {# sets at the limit}}",
+    "nextCount": "{count, plural, one {# set within a season} other {# sets within a season}}",
+    "tiresShort": "tires",
+    "lowest": "lowest {value}",
+    "now": "At the limit",
+    "next": "Within a season",
+    "rate": "{value} a season",
+    "caveat": "Sets already at the limit are measured. The rest is extrapolated from one customer’s wear over one interval, so treat it as demand to plan around, not an order to place.",
+    "unmeasured": "{count, plural, one {# set has too little history to judge and is not counted} other {# sets have too little history to judge and are not counted}}",
+    "capped": "Read the {shown} most recent sets of {total}."
+  },
+  "invoiceLine": {
+    "storage": "Tire storage",
+    "pieces": "pcs",
+    "fromDate": "from {date}"
+  },
+  "setNames": {
+    "winter": "winter tires",
+    "summer": "summer tires",
+    "all_season": "all-season tires",
+    "other": "tires"
+  },
+  "jobNotes": {
+    "set": "Tire set {reference}",
+    "shelf": "Shelf: {shelf}",
+    "notOnShelf": "not on a shelf",
+    "onRims": "On rims",
+    "tiresOnly": "Tires only",
+    "tpms": "Has TPMS sensors"
+  },
+  "files": {
+    "title": "Photos and files",
+    "add": "Add files",
+    "none": "Nothing attached yet. Photograph a kerbed rim or damage while the tires are in front of you.",
+    "captionPlaceholder": "What does this show?",
+    "onInvoice": "On the invoice",
+    "invoiceHint": "Files marked for the invoice are copied onto the work order or invoice when this set is billed.",
+    "deleteTitle": "Remove this file?",
+    "deleteBody": "{name} is removed from this set. Anything already copied onto an invoice stays there.",
+    "failed": "Could not save that",
+    "someFailed": "{count, plural, one {# file could not be uploaded} other {# files could not be uploaded}}"
+  }
+}

+ 27 - 1
messages/es/audit.json

@@ -109,6 +109,32 @@
     "settings_applyTaxRate": "Tipo impositivo aplicado",
     "settings_convertToExclusive": "Precios convertidos a sin impuestos",
     "settings_convertToInclusive": "Precios convertidos a con impuestos",
-    "organization_sampleDataRemove": "Datos de ejemplo eliminados"
+    "organization_sampleDataRemove": "Datos de ejemplo eliminados",
+    "tire_warehouse_create": "Ubicación de almacén de neumáticos creada",
+    "tire_warehouse_update": "Ubicación de almacén de neumáticos actualizada",
+    "tire_warehouse_delete": "Ubicación de almacén de neumáticos eliminada",
+    "tire_warehouse_archive": "Ubicación de almacén de neumáticos archivada",
+    "tire_location_create": "Estante de neumáticos creado",
+    "tire_location_bulk_create": "Estantes de neumáticos creados",
+    "tire_location_update": "Estante de neumáticos actualizado",
+    "tire_location_delete": "Estante de neumáticos eliminado",
+    "tire_set_check_in": "Juego de neumáticos recibido",
+    "tire_set_check_out": "Juego de neumáticos entregado",
+    "tire_set_relocate": "Juego de neumáticos movido",
+    "tire_set_update": "Juego de neumáticos actualizado",
+    "tire_set_delete": "Juego de neumáticos eliminado",
+    "tire_measurement_create": "Mediciones de neumáticos registradas",
+    "tire_treatment_update": "Preparación actualizada",
+    "tire_treatment_mark": "Preparación marcada",
+    "tire_treatment_complete_all": "Preparación completada",
+    "tire_set_message_customer": "Mensaje enviado al cliente sobre un juego de neumáticos",
+    "tire_set_create_quote": "Presupuesto creado desde un juego de neumáticos",
+    "tire_set_create_work_order": "Orden creada desde un juego de neumáticos",
+    "tire_set_add_to_work_order": "Juego de neumáticos añadido a una orden",
+    "tire_set_unlink_work_order": "Juego de neumáticos desvinculado de una orden",
+    "tire_set_return": "Juego almacenado de nuevo",
+    "tire_set_dispose": "Juego de neumáticos dado de baja",
+    "tire_set_attach": "Archivos añadidos a un juego de neumáticos",
+    "tire_set_detach": "Archivo quitado de un juego de neumáticos"
   }
 }

+ 2 - 1
messages/es/common.json

@@ -10,7 +10,8 @@
     "confirm": "Confirmar",
     "saveChanges": "Guardar cambios",
     "edit": "Editar",
-    "add": "Agregar"
+    "add": "Agregar",
+    "gotIt": "Entendido"
   },
   "contextMenu": {
     "hint": "Consejo: clic derecho en una fila para ver acciones",

+ 11 - 1
messages/es/dashboard.json

@@ -54,7 +54,8 @@
     "activeJobs": "Trabajos activos",
     "recentObservations": "Observaciones recientes",
     "recentActivity": "Actividad reciente",
-    "gettingStarted": "Primeros pasos"
+    "gettingStarted": "Primeros pasos",
+    "tireHotel": "Hotel de neumáticos"
   },
   "maintenance": {
     "title": "Mantenimiento previsto pendiente",
@@ -234,5 +235,14 @@
       "expired": "Caducado",
       "converted": "Convertido"
     }
+  },
+  "tireHotel": {
+    "title": "Hotel de neumáticos",
+    "viewAll": "Ver hotel de neumáticos",
+    "stored": "{count} juegos guardados",
+    "capacity": "{used} / {capacity} neumáticos",
+    "roomLeft": "Sitio para {count} más",
+    "needsPrep": "Pendiente de preparar",
+    "unbilled": "Sin facturar ({count})"
   }
 }

+ 6 - 0
messages/es/featureHints.json

@@ -0,0 +1,6 @@
+{
+  "tire-hotel": {
+    "title": "El hotel de neumáticos está activo",
+    "body": "Guarda neumáticos de temporada. Crea los estantes y recibe un juego."
+  }
+}

+ 8 - 2
messages/es/navigation.json

@@ -53,7 +53,12 @@
     "allReminders": "Todos los recordatorios",
     "auditLog": "Registro de auditoría",
     "aiAssistant": "Asistente IA",
-    "observations": "Observaciones"
+    "observations": "Observaciones",
+    "tireHotel": "Hotel de neumáticos",
+    "allTireSets": "Todos los juegos",
+    "tireStorage": "Distribución del almacén",
+    "tireSetDetails": "Juego de neumáticos",
+    "tireForecast": "La próxima temporada"
   },
   "search": "Buscar...",
   "shortcut": "Ctrl+K",
@@ -107,6 +112,7 @@
     "signOut": "Cerrar sesión",
     "more": "Más",
     "auditLog": "Registro de auditoría",
-    "aiAssistant": "Asistente IA"
+    "aiAssistant": "Asistente IA",
+    "tireHotel": "Hotel de neumáticos"
   }
 }

+ 27 - 0
messages/es/permissions.json

@@ -0,0 +1,27 @@
+{
+  "subjects": {
+    "dashboard": "Panel",
+    "vehicles": "Vehículos",
+    "customers": "Clientes",
+    "work_orders": "Órdenes de trabajo",
+    "quotes": "Presupuestos",
+    "services": "Historial de servicio",
+    "billing": "Facturación",
+    "inventory": "Inventario",
+    "labor_presets": "Plantillas de mano de obra",
+    "inspections": "Inspecciones",
+    "tire_hotel": "Hotel de neumáticos",
+    "reports": "Informes",
+    "work_board": "Tablero de trabajo",
+    "ai_assistant": "Asistente de IA",
+    "settings": "Ajustes"
+  },
+  "actions": {
+    "create": "Crear",
+    "read": "Ver",
+    "update": "Editar",
+    "delete": "Eliminar",
+    "manage": "Gestionar",
+    "manage_storage": "Gestionar la distribución del almacén"
+  }
+}

+ 9 - 0
messages/es/search.json

@@ -0,0 +1,9 @@
+{
+  "title": "Buscar",
+  "description": "Busca vehículos, clientes, trabajos y ajustes",
+  "placeholder": "Buscar por nombre, matrícula, teléfono, VIN, factura…",
+  "noResults": "Sin resultados.",
+  "recentCustomers": "Clientes recientes",
+  "services": "Historial de servicio",
+  "toClose": "para cerrar"
+}

+ 34 - 1
messages/es/settings.json

@@ -122,6 +122,10 @@
       "support": {
         "title": "Soporte",
         "description": "Contactar con soporte y gestionar el botón"
+      },
+      "tireHotel": {
+        "title": "Hotel de neumáticos",
+        "description": "Almacenamiento estacional de neumáticos"
       }
     },
     "pro": "PRO"
@@ -974,7 +978,9 @@
       "lastBackup": "Última copia de seguridad:",
       "supportPrompt": "Si tiene alguna pregunta sobre cómo se protegen sus datos,",
       "supportAction": "contacte con nuestro equipo de soporte."
-    }
+    },
+    "optTireHotel": "Hotel de neumáticos",
+    "optTireHotelDesc": "Juegos guardados, estantes, preparación y contratos de almacenaje"
   },
   "license": {
     "title": "Licencia marca blanca",
@@ -1454,5 +1460,32 @@
     "unavailable": "Este navegador no ha ofrecido la instalación. Abre el menú del navegador y busca «Instalar app» o «Añadir a pantalla de inicio».",
     "bannerHidden": "El aviso de instalación está oculto en este dispositivo.",
     "showBanner": "Mostrar el aviso de nuevo"
+  },
+  "tireHotel": {
+    "title": "Hotel de neumáticos",
+    "description": "Guarda los neumáticos de temporada de tus clientes y controla dónde está cada juego.",
+    "cardTitle": "Almacenamiento de neumáticos",
+    "enable": "Activar hotel de neumáticos",
+    "enableHint": "Añade el almacenamiento de neumáticos al menú. Desactivarlo después lo oculta sin borrar nada.",
+    "defaultCapacity": "Neumáticos por balda por defecto",
+    "defaultCapacityHint": "Se rellena al crear baldas nuevas. Se puede cambiar en cada balda.",
+    "warnPercent": "Avisar al llenarse (%)",
+    "warnPercentHint": "Marca el resumen del almacén cuando se ocupa esta parte de la capacidad total.",
+    "treadTitle": "Límites de sustitución",
+    "treadHint": "Los mínimos legales cambian según el país, así que fija los límites de tu mercado. El dibujo por debajo del límite se marca para sustituir.",
+    "summerLimit": "Límite verano ({unit})",
+    "winterLimit": "Límite invierno ({unit})",
+    "unitMm": "mm",
+    "unit32nds": "1/32\"",
+    "setupHint": "A continuación, configura tus estantes.",
+    "setupLink": "Abrir distribución del almacén",
+    "save": "Guardar ajustes del hotel de neumáticos",
+    "saved": "Ajustes del hotel de neumáticos guardados",
+    "billingTitle": "Facturación del almacenaje",
+    "billingHint": "Se rellena en la línea de almacenaje al facturar un juego. Editable en la factura.",
+    "seasonalPrice": "Precio de almacenaje por defecto",
+    "prepTitle": "Precios de preparación",
+    "prepHint": "Lo que cuesta cada tipo de preparación. Déjalo en blanco y no se factura nunca, que es lo que quiere un taller que incluye el lavado en el almacenaje. Las que tienen precio se añaden solas al presupuesto y a la orden.",
+    "prepNotCharged": "no se cobra"
   }
 }

+ 473 - 0
messages/es/tireHotel.json

@@ -0,0 +1,473 @@
+{
+  "title": "Hotel de neumáticos",
+  "description": "Almacenamiento estacional de neumáticos para tus clientes",
+  "common": {
+    "cancel": "Cancelar",
+    "save": "Guardar",
+    "create": "Crear",
+    "delete": "Eliminar",
+    "edit": "Editar"
+  },
+  "seasons": {
+    "summer": "Verano",
+    "winter": "Invierno",
+    "all_season": "Todo tiempo",
+    "other": "Otro"
+  },
+  "statuses": {
+    "stored": "En almacén",
+    "released": "Devuelto",
+    "disposed": "Desechado"
+  },
+  "conditions": {
+    "good": "Bueno",
+    "fair": "Vigilar",
+    "replace": "Sustituir"
+  },
+  "positions": {
+    "front_left": "Delantero izquierdo",
+    "front_right": "Delantero derecho",
+    "rear_left": "Trasero izquierdo",
+    "rear_right": "Trasero derecho",
+    "spare": "Rueda de repuesto",
+    "unspecified": "Sin especificar"
+  },
+  "capacity": {
+    "used": "{used} de {capacity} neumáticos",
+    "free": "Sitio para {count}",
+    "freeShort": "{count} libres",
+    "over": "{count} por encima de la capacidad",
+    "meterLabel": "{used} de {capacity} neumáticos almacenados"
+  },
+  "tread": {
+    "placeholderMm": "mm",
+    "placeholder32": "32avos",
+    "inputLabel": "Profundidad del dibujo, {position}",
+    "lastTime": "era {value}",
+    "worn": "{value} de desgaste"
+  },
+  "list": {
+    "tabAll": "Todos",
+    "tabStored": "En almacén",
+    "tabReleased": "Devueltos",
+    "search": "Buscar por matrícula, cliente, marca o estante",
+    "checkIn": "Recibir neumáticos",
+    "empty": "Aún no hay juegos de neumáticos. Recibe un juego para empezar.",
+    "unassigned": "Sin asignar",
+    "reference": "Ref",
+    "vehicle": "Vehículo",
+    "tires": "Neumáticos",
+    "location": "Estante",
+    "condition": "Estado",
+    "status": "Situación",
+    "storedSince": "Desde",
+    "tireCount": "{count, plural, one {# neumático} other {# neumáticos}}",
+    "studded": "Con clavos",
+    "roomLeft": "Sitio para {count}",
+    "prep": "Prep.",
+    "tabNeedsPrep": "Pendiente de preparar",
+    "setUpShelves": "Configurar estantes",
+    "emptyNoShelves": "Aún no hay estantes. Configura el almacén antes de recibir neumáticos.",
+    "tabDisposed": "Dados de baja"
+  },
+  "checkIn": {
+    "title": "Recibir neumáticos",
+    "description": "Registra lo que llega y elige dónde se guarda.",
+    "customer": "Cliente",
+    "selectCustomer": "Seleccionar cliente",
+    "noCustomer": "Sin cliente",
+    "vehicle": "Vehículo",
+    "selectVehicle": "Seleccionar vehículo",
+    "season": "Temporada",
+    "quantity": "Número de neumáticos",
+    "quantityHint": "Se cuentan de uno en uno, así que un par o cinco ruedas también valen.",
+    "brand": "Marca",
+    "brandPlaceholder": "Michelin, Bridgestone, Continental",
+    "model": "Modelo",
+    "size": "Medida",
+    "sizePlaceholder": "225/45R17",
+    "dotCode": "Código DOT",
+    "dotPlaceholder": "3623",
+    "dotHint": "Semana y año de fabricación del neumático.",
+    "withRims": "Con llantas",
+    "hasTpms": "Sensores TPMS",
+    "studded": "Con clavos",
+    "location": "Dónde van los neumáticos",
+    "choosePlace": "Elegir estante",
+    "searchLocations": "Buscar estantes",
+    "noLocations": "No se han encontrado estantes",
+    "tooFull": "Sin sitio suficiente",
+    "pickLocationFirst": "Elige un estante antes de recibir el juego",
+    "roomSummary": "{locations} estantes admiten este juego, {tires} huecos libres",
+    "nothingFits": "Ningún estante tiene sitio para {quantity} neumáticos. Libera espacio o añade un estante.",
+    "treadTitle": "Profundidad del dibujo a la entrada",
+    "treadHint": "Opcional, pero resuelve cualquier discusión posterior sobre el estado.",
+    "notes": "Notas",
+    "submit": "Recibir",
+    "success": "Juego {reference} guardado en {code}",
+    "failed": "No se ha podido recibir el juego",
+    "noVehiclesForCustomer": "Este cliente aún no tiene vehículos. Añade uno desde su ficha de cliente.",
+    "treatmentsHint": "Qué hay que hacerle a este juego antes de guardarlo o montarlo.",
+    "storedBefore": "Ya almacenado aquí",
+    "storedBeforeHint": "¿Vuelven los mismos neumáticos? Elige el juego y la temporada se sumará a su historial.",
+    "sameSet": "El mismo juego que antes",
+    "pieces": "{count} uds",
+    "lastOut": "Retirado el {date}",
+    "notStored": "Ahora no está almacenado",
+    "returned": "El juego {reference} vuelve a estar en {code}",
+    "submitReturn": "Almacenar de nuevo",
+    "ownerFromVehicle": "Tomado del vehículo",
+    "vehicleOwner": "El propietario del vehículo",
+    "clearVehicle": "Quitar el vehículo"
+  },
+  "storage": {
+    "title": "Estantes",
+    "description": "Configura el almacén una vez y ve de un vistazo qué cabe dónde.",
+    "addWarehouse": "Añadir ubicación",
+    "addShelves": "Añadir estantes",
+    "warehouseName": "Nombre",
+    "warehouseNamePlaceholder": "Nave principal, Contenedor 2, Trastero",
+    "warehouseAddress": "Dirección",
+    "warehouseCreated": "Ubicación de almacén añadida",
+    "warehouseDeleted": "{name} eliminada",
+    "warehouseArchived": "{name} archivada",
+    "deleteWarehouse": "Eliminar ubicación",
+    "deleteWarehouseTitle": "¿Eliminar {name}?",
+    "deleteWarehouseBody": "Esto elimina la ubicación y todos sus estantes.",
+    "deleteWarehouseBodyOccupied": "Esta ubicación todavía contiene {count} neumáticos, así que se archivará en lugar de eliminarse.",
+    "totalCapacity": "Capacidad total",
+    "totalStored": "Almacenados ahora",
+    "totalFree": "Sitio libre",
+    "totalLocations": "Estantes",
+    "locationSummary": "{occupied} de {total} estantes en uso",
+    "noLocations": "Aún no hay estantes. Añade una estantería para guardar neumáticos aquí.",
+    "emptyTitle": "Todavía no hay almacén configurado",
+    "emptyBody": "Añade tu primera nave o sala y llénala de estantes.",
+    "default": "Predeterminada",
+    "addLocation": "Añadir estantes",
+    "editLocation": "Editar estante",
+    "createLocation": "Crear estante",
+    "createShelves": "Crear estantes",
+    "locationDialogDescription": "Añadiendo almacenamiento a {warehouse}.",
+    "tabBulk": "Una estantería completa",
+    "tabSingle": "Un estante",
+    "zone": "Zona",
+    "zonePlaceholder": "A, Sala 1, Norte",
+    "rack": "Estantería",
+    "rackPlaceholder": "04",
+    "shelf": "Balda",
+    "shelfPlaceholder": "2",
+    "position": "Posición",
+    "positionPlaceholder": "Izquierda",
+    "shelfFrom": "Baldas de la",
+    "shelfTo": "a la",
+    "code": "Código",
+    "codePlaceholder": "A-04-2",
+    "codeHint": "Aparece en la etiqueta. Se rellena solo si lo dejas en blanco.",
+    "capacity": "Neumáticos por balda",
+    "capacityHint": "Cuántos neumáticos sueltos caben aquí.",
+    "notes": "Notas",
+    "bulkPreview": "Crea {count} baldas, de {first} a {last}",
+    "bulkPreviewCapacity": "Sitio para {tires} neumáticos en total",
+    "bulkCreated": "{count} baldas creadas",
+    "bulkCreatedWithSkips": "{created} baldas creadas, {skipped} omitidas por existir ya",
+    "bulkNothingCreated": "Esas baldas ya existen",
+    "locationCreated": "Estante añadido",
+    "locationUpdated": "Estante actualizado",
+    "locationDeleted": "{code} eliminado",
+    "deleteLocationTitle": "¿Eliminar {code}?",
+    "deleteLocationBody": "El estante tiene que estar vacío antes.",
+    "saveFailed": "No se ha podido guardar",
+    "deleteFailed": "No se ha podido eliminar"
+  },
+  "movements": {
+    "check_in": "Recibido",
+    "check_out": "Entregado",
+    "relocate": "Movido",
+    "dispose": "Desechado",
+    "measure": "Medido"
+  },
+  "detail": {
+    "move": "Mover",
+    "checkOut": "Entregar",
+    "conditionTitle": "Estado",
+    "noMeasurements": "Aún no hay mediciones registradas.",
+    "historyTitle": "Historial",
+    "storageTitle": "Almacenamiento",
+    "shelf": "Estante",
+    "warehouse": "Ubicación",
+    "notStored": "Ahora mismo no está en almacén.",
+    "checkedIn": "Recibido",
+    "checkedOut": "Entregado",
+    "tiresTitle": "Neumáticos",
+    "loadSpeed": "Índices de carga y velocidad",
+    "ownerTitle": "Propietario",
+    "phone": "Teléfono",
+    "deleteTitle": "¿Eliminar este juego de neumáticos?",
+    "deleteBody": "Se elimina el juego y todo su historial. Esto no se puede deshacer.",
+    "deleted": "Juego de neumáticos eliminado",
+    "deleteFailed": "No se ha podido eliminar el juego",
+    "belowLimit": "{count} neumático(s) en tu límite de sustitución o por debajo.",
+    "belowLimitHint": "Conviene ofrecer precio al cliente mientras los neumáticos siguen aquí.",
+    "dispose": "Dar de baja",
+    "disposeTitle": "¿Dar de baja este juego?",
+    "disposeBody": "La ficha y su historial se mantienen y se libera el hueco. Deja de ofrecerse como los mismos neumáticos la próxima temporada.",
+    "disposed": "Juego dado de baja",
+    "disposeFailed": "No se pudo dar de baja el juego"
+  },
+  "checkOut": {
+    "title": "Entregar neumáticos",
+    "description": "Se retira el juego de {code}.",
+    "descriptionNoLocation": "Este juego vuelve al cliente.",
+    "treadTitle": "Profundidad del dibujo a la salida",
+    "note": "Nota",
+    "submit": "Entregar",
+    "success": "Juego {reference} entregado",
+    "failed": "No se ha podido entregar el juego",
+    "prepPending": "{count} tarea(s) de preparación pendiente(s)",
+    "conditionTitle": "Estado a la entrada",
+    "conditionLead": {
+      "replace": "Uno o más neumáticos están en tu límite de sustitución o por debajo. Conviene decírselo al cliente antes de que se vaya.",
+      "fair": "Uno o más neumáticos se acercan a tu límite de sustitución. Conviene comentarlo antes de la próxima temporada."
+    },
+    "treadHintUnmeasured": "No se midió cuando entraron. Esta es la última oportunidad de registrarlo."
+  },
+  "relocate": {
+    "title": "Mover neumáticos",
+    "description": "Elige el estante al que se mueve este juego.",
+    "newLocation": "Nuevo estante",
+    "note": "Nota",
+    "submit": "Mover",
+    "success": "Juego {reference} movido",
+    "failed": "No se ha podido mover el juego"
+  },
+  "edit": {
+    "title": "Editar juego de neumáticos",
+    "description": "Corrige lo que se registró en la recepción. Usa Mover para cambiar de estante.",
+    "quantityHint": "Aumentarlo se comprueba contra el estante donde está el juego.",
+    "rimType": "Tipo de llanta",
+    "rimTypePlaceholder": "Aleación, acero",
+    "success": "Juego de neumáticos actualizado",
+    "failed": "No se han podido guardar los cambios",
+    "treadHint": "Cambiar una medición la registra como una nueva lectura en el historial en lugar de sobrescribir la anterior."
+  },
+  "treatments": {
+    "types": {
+      "wash_tires": "Lavar neumáticos",
+      "wash_rims": "Lavar llantas",
+      "balance": "Equilibrar",
+      "tpms_service": "Servicio TPMS",
+      "new_valves": "Válvulas nuevas",
+      "repair": "Reparación"
+    },
+    "statuses": {
+      "pending": "Por hacer",
+      "done": "Hecho",
+      "skipped": "Omitido"
+    },
+    "title": "Preparación",
+    "none": "No hay nada que hacer en este juego.",
+    "add": "Añadir preparación",
+    "pickHint": "Marca lo que necesita este juego antes de volver al coche.",
+    "pendingCount": "{count} por hacer",
+    "allComplete": "Todo hecho",
+    "ready": "Listo",
+    "markDone": "Hecho",
+    "completeAll": "Marcar los {count} como hechos",
+    "allDone": "Preparación terminada",
+    "undo": "Volver a la lista",
+    "saveFailed": "No se ha podido guardar",
+    "skip": "Omitir"
+  },
+  "messaging": {
+    "action": "Mensaje",
+    "title": "Escribir al cliente",
+    "description": "Para {name}.",
+    "noCustomer": "Este juego no tiene cliente.",
+    "noCustomerBody": "Vincula un cliente al juego antes de escribirle.",
+    "channel": "Enviar por",
+    "channels": {
+      "email": "Correo",
+      "sms": "SMS",
+      "telegram": "Telegram"
+    },
+    "noAddress": "sin dirección registrada",
+    "recipient": "Enviar a",
+    "recipientPlaceholder": "Número de teléfono",
+    "subject": "Asunto",
+    "message": "Mensaje",
+    "send": "Enviar",
+    "sent": "Mensaje enviado",
+    "failed": "No se ha podido enviar el mensaje",
+    "smsLong": "Los mensajes largos pueden dividirse en varios SMS.",
+    "subjects": {
+      "low_tread": "Tus neumáticos de {season}",
+      "damage": "Tus neumáticos de {season}",
+      "stored": "Tus neumáticos de {season} están guardados",
+      "custom": "Tus neumáticos de {season}"
+    },
+    "bodies": {
+      "low_tread": "Hola {customer_name}, tus neumáticos de {season} están guardados con nosotros en {shop_name}. Al recibirlos, {positions} marcaban {tread}, que está por debajo del límite. No aguantan otra temporada. ¿Te preparamos precio de un juego nuevo?",
+      "damage": "Hola {customer_name}, tus neumáticos de {season} están guardados con nosotros en {shop_name}. Al recibirlos vimos daños en {positions}. Llámanos cuando puedas y lo vemos juntos.",
+      "stored": "Hola {customer_name}, tus neumáticos de {season} ya están guardados con nosotros en {shop_name}. Te avisamos cuando toque el cambio.",
+      "custom": "Hola {customer_name}, sobre tus neumáticos de {season} guardados con nosotros en {shop_name}:"
+    },
+    "enableInSettings": "Aún no está configurado. Actívalo en Ajustes."
+  },
+  "job": {
+    "cardTitle": "Presupuestos y órdenes",
+    "none": "Aún no se ha creado nada para este juego.",
+    "quote": "Presupuesto",
+    "workOrder": "Orden",
+    "converted": "convertido",
+    "quoteTitle": "Presupuestar un juego nuevo",
+    "workOrderTitle": "Facturar este juego",
+    "loadingSet": "Cargando el juego",
+    "tires": "Neumáticos",
+    "inStock": "{count} en stock",
+    "shortStock": "solo {count} en stock",
+    "noMatch": "Nada en stock coincide con {size}. La línea se añade con el precio que pongas.",
+    "noSize": "Este juego no tiene medida, así que no se puede cruzar con el stock. La línea se añade con tu precio.",
+    "unitPrice": "Precio por neumático",
+    "createQuote": "Crear presupuesto",
+    "createWorkOrder": "Crear orden",
+    "quoteCreated": "Presupuesto {number} creado",
+    "workOrderCreated": "Orden creada",
+    "failed": "No se ha podido crear",
+    "bannerTitle": "Recoger en",
+    "openSet": "Abrir juego",
+    "prepPending": "Pendiente antes de montar: {jobs}",
+    "notOnShelf": "Este juego ya no está guardado.",
+    "targetNew": "Nueva orden",
+    "targetNewHint": "Crear una orden solo para estos neumáticos.",
+    "targetExisting": "O añadir a una orden ya abierta para este vehículo",
+    "addToWorkOrder": "Añadir a la orden",
+    "workOrderUpdated": "Añadido a la orden",
+    "unlink": "Desvincular",
+    "unlinkTitle": "¿Desvincular estos neumáticos de la orden?",
+    "unlinkBody": "La orden deja de apuntar al juego de neumáticos. Las piezas y la mano de obra que ya están se quedan; quítalas aparte si no se deben cobrar.",
+    "unlinked": "Neumáticos desvinculados",
+    "unlinkFailed": "No se han podido desvincular",
+    "prep": "Preparación",
+    "tiresSkipped": "No se añadirá ninguna línea de neumáticos.",
+    "total": "Total",
+    "searchStock": "Buscar en el inventario por marca, medida o número de pieza",
+    "noSearchHits": "Nada en el inventario coincide con \"{query}\".",
+    "otherSize": "Otra medida",
+    "inSize": "{count} en {size}",
+    "quantity": "Cant.",
+    "noStockItem": "Sin artículo de inventario, precio manual",
+    "storeTires": "Guardar neumáticos",
+    "storage": "Almacenaje",
+    "storageFee": "Importe",
+    "storagePeriod": "Periodo",
+    "destination": "Dónde va",
+    "targetInvoice": "Nueva factura",
+    "targetInvoiceHint": "Factura al cliente directamente, sin orden sobre un vehículo.",
+    "createInvoice": "Crear factura",
+    "needsCustomer": "Vincula un cliente o un vehículo al juego antes de facturar.",
+    "storagePeriodEnd": "Retirado",
+    "storageOpenEnded": "Abierto",
+    "storagePeriodHint": "Aparece en la línea de la factura para que el cliente vea qué cubre el importe. Deja el final en blanco hasta saber cuándo lo retiran.",
+    "pricesHint": "Los precios de preparación vienen de los ajustes. Cada línea se puede editar después de crearlo.",
+    "files": "Fotos y archivos",
+    "filesCount": "{count} en la factura"
+  },
+  "label": {
+    "action": "Etiquetas",
+    "title": "Imprimir etiquetas",
+    "description": "Una pegatina por neumático, con un QR que abre este juego.",
+    "format": "Tamaño",
+    "formats": {
+      "dymo_small": "Rollo pequeño",
+      "dymo_standard": "Rollo estándar",
+      "thermal_large": "Rollo grande",
+      "sheet_a4": "Hoja A4"
+    },
+    "perSheet": "{count} por hoja",
+    "copies": "Cuántas",
+    "copiesHint": "una por neumático ({count})",
+    "fewer": "Una menos",
+    "more": "Una más",
+    "print": "Imprimir",
+    "shelfNote": "El estante no se imprime a propósito. Los juegos se mueven, y un estante impreso quedaría mal. El QR siempre muestra dónde está ahora.",
+    "reference": "Ref",
+    "quantity": "{count, plural, one {# neumático} other {# neumáticos}}",
+    "withRims": "Con llantas",
+    "tpms": "TPMS",
+    "studded": "Con clavos",
+    "unassigned": "Sin asignar",
+    "preview": "Vista previa",
+    "previewScale": "Mostrado a tamaño de impresión. El código también se escanea desde la pantalla.",
+    "previewFailed": "No se ha podido generar la vista previa"
+  },
+  "vehicle": {
+    "oneOnShelf": "en {code}",
+    "inStorage": "en el hotel"
+  },
+  "wear": {
+    "summary": "{value} de desgaste entre {from} y {to}",
+    "perSeason": "Unos {value} por temporada",
+    "seasonsLeft": "{count, plural, =0 {Ya en el límite} one {Alrededor de una temporada más} other {Alrededor de # temporadas más}}",
+    "since": "-{value}"
+  },
+  "settings": {
+    "change": "Cambiar en ajustes",
+    "prepPrices": "Precios de preparación",
+    "storagePrices": "Precios de almacenaje",
+    "treadLimits": "Cambiar el límite",
+    "defaultCapacity": "Capacidad por defecto"
+  },
+  "forecast": {
+    "title": "La próxima temporada",
+    "subtitle": "Lo que los juegos almacenados necesitan según las mediciones, por medida.",
+    "backToList": "Volver a los juegos",
+    "tires": "Neumáticos previstos",
+    "atLimit": "Ya en el límite",
+    "expected": "Previsto en una temporada",
+    "orderTitle": "Por medida",
+    "sizes": "{count} medidas",
+    "empty": "Nada medido cerca del límite todavía. Un juego necesita dos visitas para calcular un ritmo.",
+    "nowCount": "{count, plural, one {# juego en el límite} other {# juegos en el límite}}",
+    "nextCount": "{count, plural, one {# juego en una temporada} other {# juegos en una temporada}}",
+    "tiresShort": "neumáticos",
+    "lowest": "mínimo {value}",
+    "now": "En el límite",
+    "next": "En una temporada",
+    "rate": "{value} por temporada",
+    "caveat": "Los juegos en el límite están medidos. El resto se extrapola del desgaste de un cliente en un intervalo: sirve para planificar, no para pedir.",
+    "unmeasured": "{count, plural, one {# juego tiene poco historial y no se cuenta} other {# juegos tienen poco historial y no se cuentan}}",
+    "capped": "Se han leído los {shown} juegos más recientes de {total}."
+  },
+  "invoiceLine": {
+    "storage": "Almacenaje de neumáticos",
+    "pieces": "uds",
+    "fromDate": "desde {date}"
+  },
+  "setNames": {
+    "winter": "neumáticos de invierno",
+    "summer": "neumáticos de verano",
+    "all_season": "neumáticos all season",
+    "other": "neumáticos"
+  },
+  "jobNotes": {
+    "set": "Juego de neumáticos {reference}",
+    "shelf": "Estante: {shelf}",
+    "notOnShelf": "sin estante",
+    "onRims": "Con llantas",
+    "tiresOnly": "Solo neumáticos",
+    "tpms": "Con sensores TPMS"
+  },
+  "files": {
+    "title": "Fotos y archivos",
+    "add": "Añadir archivos",
+    "none": "Todavía no hay nada adjunto. Fotografía una llanta rozada mientras tienes los neumáticos delante.",
+    "captionPlaceholder": "¿Qué se ve aquí?",
+    "onInvoice": "En la factura",
+    "invoiceHint": "Los archivos marcados para la factura se copian a la orden o a la factura al facturar el juego.",
+    "deleteTitle": "¿Quitar este archivo?",
+    "deleteBody": "{name} se quita del juego. Lo que ya se copió a una factura se mantiene.",
+    "failed": "No se pudo guardar",
+    "someFailed": "{count, plural, one {no se pudo subir # archivo} other {no se pudieron subir # archivos}}"
+  }
+}

+ 27 - 1
messages/fr/audit.json

@@ -109,6 +109,32 @@
     "settings_applyTaxRate": "Taux de taxe appliqué",
     "settings_convertToExclusive": "Prix convertis hors taxes",
     "settings_convertToInclusive": "Prix convertis toutes taxes comprises",
-    "organization_sampleDataRemove": "Données d'exemple supprimées"
+    "organization_sampleDataRemove": "Données d'exemple supprimées",
+    "tire_warehouse_create": "Site de stockage de pneus créé",
+    "tire_warehouse_update": "Site de stockage de pneus modifié",
+    "tire_warehouse_delete": "Site de stockage de pneus supprimé",
+    "tire_warehouse_archive": "Site de stockage de pneus archivé",
+    "tire_location_create": "Étagère à pneus créée",
+    "tire_location_bulk_create": "Étagères à pneus créées",
+    "tire_location_update": "Étagère à pneus modifiée",
+    "tire_location_delete": "Étagère à pneus supprimée",
+    "tire_set_check_in": "Jeu de pneus réceptionné",
+    "tire_set_check_out": "Jeu de pneus restitué",
+    "tire_set_relocate": "Jeu de pneus déplacé",
+    "tire_set_update": "Jeu de pneus modifié",
+    "tire_set_delete": "Jeu de pneus supprimé",
+    "tire_measurement_create": "Mesures de pneus enregistrées",
+    "tire_treatment_update": "Préparation modifiée",
+    "tire_treatment_mark": "Préparation marquée",
+    "tire_treatment_complete_all": "Préparation terminée",
+    "tire_set_message_customer": "Message envoyé au client au sujet d’un jeu de pneus",
+    "tire_set_create_quote": "Devis créé depuis un jeu de pneus",
+    "tire_set_create_work_order": "Ordre créé depuis un jeu de pneus",
+    "tire_set_add_to_work_order": "Jeu de pneus ajouté à un ordre",
+    "tire_set_unlink_work_order": "Jeu de pneus dissocié d’un ordre",
+    "tire_set_return": "Jeu de nouveau stocké",
+    "tire_set_dispose": "Jeu de pneus mis au rebut",
+    "tire_set_attach": "Fichiers ajoutés à un jeu de pneus",
+    "tire_set_detach": "Fichier retiré d’un jeu de pneus"
   }
 }

+ 2 - 1
messages/fr/common.json

@@ -10,7 +10,8 @@
     "confirm": "Confirmer",
     "saveChanges": "Enregistrer les modifications",
     "edit": "Modifier",
-    "add": "Ajouter"
+    "add": "Ajouter",
+    "gotIt": "J’ai compris"
   },
   "contextMenu": {
     "hint": "Astuce : clic droit sur une ligne pour les actions",

+ 11 - 1
messages/fr/dashboard.json

@@ -54,7 +54,8 @@
     "activeJobs": "Travaux en cours",
     "recentObservations": "Observations récentes",
     "recentActivity": "Activité récente",
-    "gettingStarted": "Premiers pas"
+    "gettingStarted": "Premiers pas",
+    "tireHotel": "Hôtel à pneus"
   },
   "maintenance": {
     "title": "Maintenance prévue à effectuer",
@@ -234,5 +235,14 @@
       "expired": "Expiré",
       "converted": "Converti"
     }
+  },
+  "tireHotel": {
+    "title": "Hôtel à pneus",
+    "viewAll": "Voir l’hôtel à pneus",
+    "stored": "{count} jeux stockés",
+    "capacity": "{used} / {capacity} pneus",
+    "roomLeft": "Place pour {count} de plus",
+    "needsPrep": "À préparer",
+    "unbilled": "Non facturé ({count})"
   }
 }

+ 6 - 0
messages/fr/featureHints.json

@@ -0,0 +1,6 @@
+{
+  "tire-hotel": {
+    "title": "L’hôtel à pneus est actif",
+    "body": "Stockez les pneus saisonniers. Créez vos étagères, puis réceptionnez un jeu."
+  }
+}

+ 8 - 2
messages/fr/navigation.json

@@ -53,7 +53,12 @@
     "allReminders": "Tous les rappels",
     "auditLog": "Journal d'audit",
     "aiAssistant": "Assistant IA",
-    "observations": "Observations"
+    "observations": "Observations",
+    "tireHotel": "Hôtel à pneus",
+    "allTireSets": "Tous les jeux de pneus",
+    "tireStorage": "Organisation du stockage",
+    "tireSetDetails": "Jeu de pneus",
+    "tireForecast": "La saison prochaine"
   },
   "search": "Rechercher…",
   "shortcut": "Ctrl+K",
@@ -107,6 +112,7 @@
     "signOut": "Se déconnecter",
     "more": "Plus",
     "auditLog": "Journal d'audit",
-    "aiAssistant": "Assistant IA"
+    "aiAssistant": "Assistant IA",
+    "tireHotel": "Hôtel à pneus"
   }
 }

+ 27 - 0
messages/fr/permissions.json

@@ -0,0 +1,27 @@
+{
+  "subjects": {
+    "dashboard": "Tableau de bord",
+    "vehicles": "Véhicules",
+    "customers": "Clients",
+    "work_orders": "Ordres de travail",
+    "quotes": "Devis",
+    "services": "Interventions",
+    "billing": "Facturation",
+    "inventory": "Stock",
+    "labor_presets": "Modèles de main-d’œuvre",
+    "inspections": "Contrôles",
+    "tire_hotel": "Hôtel à pneus",
+    "reports": "Rapports",
+    "work_board": "Tableau d’atelier",
+    "ai_assistant": "Assistant IA",
+    "settings": "Paramètres"
+  },
+  "actions": {
+    "create": "Créer",
+    "read": "Consulter",
+    "update": "Modifier",
+    "delete": "Supprimer",
+    "manage": "Gérer",
+    "manage_storage": "Gérer l’organisation du stockage"
+  }
+}

+ 9 - 0
messages/fr/search.json

@@ -0,0 +1,9 @@
+{
+  "title": "Recherche",
+  "description": "Rechercher véhicules, clients, interventions et paramètres",
+  "placeholder": "Nom, plaque, téléphone, VIN, facture…",
+  "noResults": "Aucun résultat.",
+  "recentCustomers": "Clients récents",
+  "services": "Interventions",
+  "toClose": "pour fermer"
+}

+ 34 - 1
messages/fr/settings.json

@@ -122,6 +122,10 @@
       "support": {
         "title": "Assistance",
         "description": "Contacter l'assistance et gérer le bouton"
+      },
+      "tireHotel": {
+        "title": "Hôtel à pneus",
+        "description": "Stockage saisonnier des pneus"
       }
     },
     "pro": "PRO"
@@ -974,7 +978,9 @@
       "lastBackup": "Dernière sauvegarde :",
       "supportPrompt": "Pour toute question sur la protection de vos données,",
       "supportAction": "contactez notre équipe d'assistance."
-    }
+    },
+    "optTireHotel": "Hôtel à pneus",
+    "optTireHotelDesc": "Jeux stockés, étagères, préparation et contrats de stockage"
   },
   "license": {
     "title": "Licence marque blanche",
@@ -1454,5 +1460,32 @@
     "unavailable": "Ce navigateur n'a pas proposé d'installation. Ouvrez le menu du navigateur et cherchez « Installer l'application » ou « Sur l'écran d'accueil ».",
     "bannerHidden": "La bannière d'installation est masquée sur cet appareil.",
     "showBanner": "Afficher à nouveau la bannière"
+  },
+  "tireHotel": {
+    "title": "Hôtel à pneus",
+    "description": "Stockez les pneus saisonniers de vos clients et sachez où se trouve chaque jeu.",
+    "cardTitle": "Stockage des pneus",
+    "enable": "Activer l’hôtel à pneus",
+    "enableHint": "Ajoute le stockage des pneus au menu. Le désactiver plus tard le masque sans rien supprimer.",
+    "defaultCapacity": "Pneus par niveau par défaut",
+    "defaultCapacityHint": "Prérempli à la création de nouveaux niveaux. Modifiable niveau par niveau.",
+    "warnPercent": "Alerter à partir de (%)",
+    "warnPercentHint": "Signale la vue du stockage dès que cette part de la capacité totale est occupée.",
+    "treadTitle": "Seuils de remplacement",
+    "treadHint": "Les minimums légaux varient selon les pays, réglez donc les seuils de votre marché. Une sculpture sous le seuil est signalée à remplacer.",
+    "summerLimit": "Seuil été ({unit})",
+    "winterLimit": "Seuil hiver ({unit})",
+    "unitMm": "mm",
+    "unit32nds": "1/32\"",
+    "setupHint": "Configurez ensuite vos étagères.",
+    "setupLink": "Ouvrir l’organisation du stockage",
+    "save": "Enregistrer les réglages",
+    "saved": "Réglages de l’hôtel à pneus enregistrés",
+    "billingTitle": "Facturation du stockage",
+    "billingHint": "Prérempli sur la ligne de stockage lors de la facturation d’un jeu. Modifiable sur la facture.",
+    "seasonalPrice": "Tarif de stockage par défaut",
+    "prepTitle": "Prix de la préparation",
+    "prepHint": "Ce que coûte chaque type de préparation. Laissez vide et rien n’est facturé, ce qui convient aux ateliers qui incluent le lavage dans le stockage. Les prestations chiffrées sont ajoutées automatiquement au devis et à l’ordre.",
+    "prepNotCharged": "non facturé"
   }
 }

+ 473 - 0
messages/fr/tireHotel.json

@@ -0,0 +1,473 @@
+{
+  "title": "Hôtel à pneus",
+  "description": "Stockage saisonnier des pneus de vos clients",
+  "common": {
+    "cancel": "Annuler",
+    "save": "Enregistrer",
+    "create": "Créer",
+    "delete": "Supprimer",
+    "edit": "Modifier"
+  },
+  "seasons": {
+    "summer": "Été",
+    "winter": "Hiver",
+    "all_season": "Toutes saisons",
+    "other": "Autre"
+  },
+  "statuses": {
+    "stored": "En stock",
+    "released": "Restitué",
+    "disposed": "Mis au rebut"
+  },
+  "conditions": {
+    "good": "Bon",
+    "fair": "À surveiller",
+    "replace": "À remplacer"
+  },
+  "positions": {
+    "front_left": "Avant gauche",
+    "front_right": "Avant droit",
+    "rear_left": "Arrière gauche",
+    "rear_right": "Arrière droit",
+    "spare": "Roue de secours",
+    "unspecified": "Non précisé"
+  },
+  "capacity": {
+    "used": "{used} sur {capacity} pneus",
+    "free": "Place pour {count}",
+    "freeShort": "{count} libres",
+    "over": "{count} au-delà de la capacité",
+    "meterLabel": "{used} sur {capacity} pneus stockés"
+  },
+  "tread": {
+    "placeholderMm": "mm",
+    "placeholder32": "32e",
+    "inputLabel": "Profondeur de sculpture, {position}",
+    "lastTime": "était {value}",
+    "worn": "{value} d’usure"
+  },
+  "list": {
+    "tabAll": "Tous",
+    "tabStored": "En stock",
+    "tabReleased": "Restitués",
+    "search": "Rechercher par plaque, client, marque ou étagère",
+    "checkIn": "Réceptionner des pneus",
+    "empty": "Aucun jeu de pneus pour l'instant. Réceptionnez un jeu pour commencer.",
+    "unassigned": "Non attribué",
+    "reference": "Réf",
+    "vehicle": "Véhicule",
+    "tires": "Pneus",
+    "location": "Étagère",
+    "condition": "État",
+    "status": "Statut",
+    "storedSince": "Depuis",
+    "tireCount": "{count, plural, one {# pneu} other {# pneus}}",
+    "studded": "Cloutés",
+    "roomLeft": "Place pour {count}",
+    "prep": "Prép.",
+    "tabNeedsPrep": "À préparer",
+    "setUpShelves": "Configurer les étagères",
+    "emptyNoShelves": "Aucune étagère. Configurez votre stockage avant de réceptionner des pneus.",
+    "tabDisposed": "Au rebut"
+  },
+  "checkIn": {
+    "title": "Réceptionner des pneus",
+    "description": "Enregistrez ce qui arrive, puis choisissez où le ranger.",
+    "customer": "Client",
+    "selectCustomer": "Choisir un client",
+    "noCustomer": "Aucun client",
+    "vehicle": "Véhicule",
+    "selectVehicle": "Choisir un véhicule",
+    "season": "Saison",
+    "quantity": "Nombre de pneus",
+    "quantityHint": "Comptés à l'unité, donc une paire ou cinq roues fonctionnent aussi.",
+    "brand": "Marque",
+    "brandPlaceholder": "Michelin, Bridgestone, Continental",
+    "model": "Modèle",
+    "size": "Dimension",
+    "sizePlaceholder": "225/45R17",
+    "dotCode": "Code DOT",
+    "dotPlaceholder": "3623",
+    "dotHint": "Semaine et année de fabrication du pneu.",
+    "withRims": "Sur jantes",
+    "hasTpms": "Capteurs TPMS",
+    "studded": "Cloutés",
+    "location": "Emplacement de stockage",
+    "choosePlace": "Choisir une étagère",
+    "searchLocations": "Rechercher des étagères",
+    "noLocations": "Aucune étagère trouvée",
+    "tooFull": "Place insuffisante",
+    "pickLocationFirst": "Choisissez une étagère avant de réceptionner le jeu",
+    "roomSummary": "{locations} étagères conviennent, {tires} emplacements libres",
+    "nothingFits": "Aucune étagère n'a la place pour {quantity} pneus. Libérez de l'espace ou ajoutez une étagère.",
+    "treadTitle": "Profondeur de sculpture à la réception",
+    "treadHint": "Facultatif, mais cela tranche toute contestation ultérieure sur l'état.",
+    "notes": "Notes",
+    "submit": "Réceptionner",
+    "success": "Jeu {reference} rangé en {code}",
+    "failed": "Impossible de réceptionner le jeu",
+    "noVehiclesForCustomer": "Ce client n’a pas encore de véhicule. Ajoutez-en un depuis sa fiche client.",
+    "treatmentsHint": "Ce qu’il faut faire sur ce jeu avant stockage ou remontage.",
+    "storedBefore": "Déjà stocké ici",
+    "storedBeforeHint": "Ce sont les mêmes pneus ? Choisissez le jeu, la saison rejoindra son historique.",
+    "sameSet": "Même jeu que la dernière fois",
+    "pieces": "{count} pcs",
+    "lastOut": "Repris le {date}",
+    "notStored": "Pas en stock actuellement",
+    "returned": "Le jeu {reference} est de nouveau en {code}",
+    "submitReturn": "Stocker à nouveau",
+    "ownerFromVehicle": "Repris du véhicule",
+    "vehicleOwner": "Le propriétaire du véhicule",
+    "clearVehicle": "Retirer le véhicule"
+  },
+  "storage": {
+    "title": "Étagères",
+    "description": "Configurez votre stockage une fois, puis voyez d'un coup d'œil ce qui rentre où.",
+    "addWarehouse": "Ajouter un site",
+    "addShelves": "Ajouter des étagères",
+    "warehouseName": "Nom",
+    "warehouseNamePlaceholder": "Bâtiment principal, Conteneur 2, Arrière-salle",
+    "warehouseAddress": "Adresse",
+    "warehouseCreated": "Site de stockage ajouté",
+    "warehouseDeleted": "{name} supprimé",
+    "warehouseArchived": "{name} archivé",
+    "deleteWarehouse": "Supprimer le site",
+    "deleteWarehouseTitle": "Supprimer {name} ?",
+    "deleteWarehouseBody": "Cela supprime le site et toutes ses étagères.",
+    "deleteWarehouseBodyOccupied": "Ce site contient encore {count} pneus, il sera donc archivé plutôt que supprimé.",
+    "totalCapacity": "Capacité totale",
+    "totalStored": "Stockés actuellement",
+    "totalFree": "Place restante",
+    "totalLocations": "Étagères",
+    "locationSummary": "{occupied} étagères sur {total} utilisées",
+    "noLocations": "Aucune étagère. Ajoutez un rack pour stocker des pneus ici.",
+    "emptyTitle": "Aucun stockage configuré",
+    "emptyBody": "Ajoutez votre premier bâtiment ou local, puis remplissez-le d'étagères.",
+    "default": "Par défaut",
+    "addLocation": "Ajouter des étagères",
+    "editLocation": "Modifier l'étagère",
+    "createLocation": "Créer l'étagère",
+    "createShelves": "Créer les étagères",
+    "locationDialogDescription": "Ajout de stockage à {warehouse}.",
+    "tabBulk": "Un rack d'étagères",
+    "tabSingle": "Une étagère",
+    "zone": "Zone",
+    "zonePlaceholder": "A, Local 1, Nord",
+    "rack": "Rack",
+    "rackPlaceholder": "04",
+    "shelf": "Niveau",
+    "shelfPlaceholder": "2",
+    "position": "Position",
+    "positionPlaceholder": "Gauche",
+    "shelfFrom": "Niveaux de",
+    "shelfTo": "à",
+    "code": "Code",
+    "codePlaceholder": "A-04-2",
+    "codeHint": "Figure sur l'étiquette. Rempli automatiquement si vous le laissez vide.",
+    "capacity": "Pneus par niveau",
+    "capacityHint": "Combien de pneus individuels tiennent ici.",
+    "notes": "Notes",
+    "bulkPreview": "Crée {count} niveaux, de {first} à {last}",
+    "bulkPreviewCapacity": "Place pour {tires} pneus au total",
+    "bulkCreated": "{count} niveaux créés",
+    "bulkCreatedWithSkips": "{created} niveaux créés, {skipped} déjà existants ignorés",
+    "bulkNothingCreated": "Ces niveaux existent déjà",
+    "locationCreated": "Étagère ajoutée",
+    "locationUpdated": "Étagère mise à jour",
+    "locationDeleted": "{code} supprimé",
+    "deleteLocationTitle": "Supprimer {code} ?",
+    "deleteLocationBody": "L'étagère doit d'abord être vide.",
+    "saveFailed": "Enregistrement impossible",
+    "deleteFailed": "Suppression impossible"
+  },
+  "movements": {
+    "check_in": "Réceptionné",
+    "check_out": "Restitué",
+    "relocate": "Déplacé",
+    "dispose": "Mis au rebut",
+    "measure": "Mesuré"
+  },
+  "detail": {
+    "move": "Déplacer",
+    "checkOut": "Restituer",
+    "conditionTitle": "État",
+    "noMeasurements": "Aucun relevé enregistré.",
+    "historyTitle": "Historique",
+    "storageTitle": "Stockage",
+    "shelf": "Étagère",
+    "warehouse": "Site",
+    "notStored": "Pas en stock actuellement.",
+    "checkedIn": "Réceptionné",
+    "checkedOut": "Restitué",
+    "tiresTitle": "Pneus",
+    "loadSpeed": "Indices de charge et vitesse",
+    "ownerTitle": "Propriétaire",
+    "phone": "Téléphone",
+    "deleteTitle": "Supprimer ce jeu de pneus ?",
+    "deleteBody": "Le jeu et tout son historique seront supprimés. Action irréversible.",
+    "deleted": "Jeu de pneus supprimé",
+    "deleteFailed": "Impossible de supprimer le jeu",
+    "belowLimit": "{count} pneu(s) à votre limite de remplacement ou en dessous.",
+    "belowLimitHint": "À proposer au client tant que les pneus sont encore ici.",
+    "dispose": "Mettre au rebut",
+    "disposeTitle": "Mettre ce jeu au rebut ?",
+    "disposeBody": "La fiche et son historique restent, et la place est libérée. Le jeu ne sera plus proposé la saison prochaine.",
+    "disposed": "Jeu mis au rebut",
+    "disposeFailed": "Impossible de mettre le jeu au rebut"
+  },
+  "checkOut": {
+    "title": "Restituer les pneus",
+    "description": "Le jeu est retiré de {code}.",
+    "descriptionNoLocation": "Ce jeu repart chez le client.",
+    "treadTitle": "Profondeur de sculpture à la sortie",
+    "note": "Note",
+    "submit": "Restituer",
+    "success": "Jeu {reference} restitué",
+    "failed": "Impossible de restituer le jeu",
+    "prepPending": "{count} tâche(s) de préparation en attente",
+    "conditionTitle": "État à la réception",
+    "conditionLead": {
+      "replace": "Au moins un pneu est à votre limite de remplacement ou en dessous. À signaler au client avant son départ.",
+      "fair": "Au moins un pneu approche de votre limite de remplacement. À mentionner avant la saison prochaine."
+    },
+    "treadHintUnmeasured": "Aucun relevé n’a été fait à la réception. C’est la dernière occasion d’en faire un."
+  },
+  "relocate": {
+    "title": "Déplacer les pneus",
+    "description": "Choisissez l’étagère de destination.",
+    "newLocation": "Nouvelle étagère",
+    "note": "Note",
+    "submit": "Déplacer",
+    "success": "Jeu {reference} déplacé",
+    "failed": "Impossible de déplacer le jeu"
+  },
+  "edit": {
+    "title": "Modifier le jeu de pneus",
+    "description": "Corrigez ce qui a été saisi à la réception. Utilisez Déplacer pour changer d’étagère.",
+    "quantityHint": "Une augmentation est vérifiée par rapport à l’étagère où se trouve le jeu.",
+    "rimType": "Type de jante",
+    "rimTypePlaceholder": "Alliage, tôle",
+    "success": "Jeu de pneus mis à jour",
+    "failed": "Impossible d’enregistrer les modifications",
+    "treadHint": "Une valeur modifiée est enregistrée comme un nouveau relevé dans l’historique, sans écraser l’ancien."
+  },
+  "treatments": {
+    "types": {
+      "wash_tires": "Laver les pneus",
+      "wash_rims": "Laver les jantes",
+      "balance": "Équilibrer",
+      "tpms_service": "Service TPMS",
+      "new_valves": "Valves neuves",
+      "repair": "Réparation"
+    },
+    "statuses": {
+      "pending": "À faire",
+      "done": "Fait",
+      "skipped": "Ignoré"
+    },
+    "title": "Préparation",
+    "none": "Rien à faire sur ce jeu.",
+    "add": "Ajouter une préparation",
+    "pickHint": "Cochez ce dont ce jeu a besoin avant de retourner sur le véhicule.",
+    "pendingCount": "{count} à faire",
+    "allComplete": "Tout est fait",
+    "ready": "Prêt",
+    "markDone": "Fait",
+    "completeAll": "Tout marquer fait ({count})",
+    "allDone": "Préparation terminée",
+    "undo": "Remettre dans la liste",
+    "saveFailed": "Enregistrement impossible",
+    "skip": "Ignorer"
+  },
+  "messaging": {
+    "action": "Message",
+    "title": "Écrire au client",
+    "description": "À {name}.",
+    "noCustomer": "Aucun client sur ce jeu.",
+    "noCustomerBody": "Associez un client au jeu avant de lui écrire.",
+    "channel": "Envoyer par",
+    "channels": {
+      "email": "E-mail",
+      "sms": "SMS",
+      "telegram": "Telegram"
+    },
+    "noAddress": "aucune adresse enregistrée",
+    "recipient": "Envoyer à",
+    "recipientPlaceholder": "Numéro de téléphone",
+    "subject": "Objet",
+    "message": "Message",
+    "send": "Envoyer",
+    "sent": "Message envoyé",
+    "failed": "Impossible d’envoyer le message",
+    "smsLong": "Les messages longs peuvent être découpés en plusieurs SMS.",
+    "subjects": {
+      "low_tread": "Vos pneus {season}",
+      "damage": "Vos pneus {season}",
+      "stored": "Vos pneus {season} sont stockés",
+      "custom": "Vos pneus {season}"
+    },
+    "bodies": {
+      "low_tread": "Bonjour {customer_name}, vos pneus {season} sont stockés chez nous, {shop_name}. À la réception, {positions} mesuraient {tread}, ce qui est sous la limite. Ils ne tiendront pas une saison de plus. On vous fait un prix pour un jeu neuf ?",
+      "damage": "Bonjour {customer_name}, vos pneus {season} sont stockés chez nous, {shop_name}. À la réception, nous avons trouvé un dommage sur {positions}. Appelez-nous quand vous pouvez, on regarde ça ensemble.",
+      "stored": "Bonjour {customer_name}, vos pneus {season} sont maintenant stockés chez nous, {shop_name}. On vous prévient quand ce sera le moment de les remonter.",
+      "custom": "Bonjour {customer_name}, au sujet de vos pneus {season} stockés chez nous, {shop_name} :"
+    },
+    "enableInSettings": "Pas encore configuré. Activez-le dans les Paramètres."
+  },
+  "job": {
+    "cardTitle": "Devis et ordres",
+    "none": "Rien de créé pour ce jeu.",
+    "quote": "Devis",
+    "workOrder": "Ordre",
+    "converted": "converti",
+    "quoteTitle": "Chiffrer un jeu neuf",
+    "workOrderTitle": "Facturer ce jeu",
+    "loadingSet": "Chargement du jeu",
+    "tires": "Pneus",
+    "inStock": "{count} en stock",
+    "shortStock": "seulement {count} en stock",
+    "noMatch": "Rien en stock ne correspond à {size}. La ligne est ajoutée au prix que vous saisissez.",
+    "noSize": "Ce jeu n’a pas de dimension, le stock ne peut pas être comparé. La ligne est ajoutée à votre prix.",
+    "unitPrice": "Prix par pneu",
+    "createQuote": "Créer le devis",
+    "createWorkOrder": "Créer l’ordre",
+    "quoteCreated": "Devis {number} créé",
+    "workOrderCreated": "Ordre créé",
+    "failed": "Création impossible",
+    "bannerTitle": "À récupérer en",
+    "openSet": "Ouvrir le jeu",
+    "prepPending": "À faire avant montage : {jobs}",
+    "notOnShelf": "Ce jeu n’est plus en stockage.",
+    "targetNew": "Nouvel ordre",
+    "targetNewHint": "Créer un ordre uniquement pour ces pneus.",
+    "targetExisting": "Ou ajouter à un ordre déjà ouvert pour ce véhicule",
+    "addToWorkOrder": "Ajouter à l’ordre",
+    "workOrderUpdated": "Ajouté à l’ordre",
+    "unlink": "Dissocier",
+    "unlinkTitle": "Dissocier ces pneus de l’ordre ?",
+    "unlinkBody": "L’ordre ne renvoie plus au jeu de pneus. Les pièces et la main-d’œuvre déjà saisies restent ; retirez-les séparément si elles ne doivent pas être facturées.",
+    "unlinked": "Pneus dissociés",
+    "unlinkFailed": "Impossible de dissocier les pneus",
+    "prep": "Préparation",
+    "tiresSkipped": "Aucune ligne de pneus ne sera ajoutée.",
+    "total": "Total",
+    "searchStock": "Rechercher dans le stock par marque, dimension ou référence",
+    "noSearchHits": "Rien dans le stock ne correspond à \"{query}\".",
+    "otherSize": "Autre dimension",
+    "inSize": "{count} en {size}",
+    "quantity": "Qté",
+    "noStockItem": "Pas d’article en stock, prix saisi à la main",
+    "storeTires": "Stocker les pneus",
+    "storage": "Stockage",
+    "storageFee": "Frais",
+    "storagePeriod": "Période",
+    "destination": "Destination",
+    "targetInvoice": "Nouvelle facture",
+    "targetInvoiceHint": "Facturer le client directement, sans ordre sur un véhicule.",
+    "createInvoice": "Créer la facture",
+    "needsCustomer": "Associez un client ou un véhicule à ce jeu avant de facturer.",
+    "storagePeriodEnd": "Repris",
+    "storageOpenEnded": "Ouvert",
+    "storagePeriodHint": "Apparaît sur la ligne de facture pour que le client voie ce que couvrent les frais. Laissez la fin vide tant que la date de reprise est inconnue.",
+    "pricesHint": "Les tarifs de préparation viennent des paramètres. Chaque ligne reste modifiable une fois le document créé.",
+    "files": "Photos et fichiers",
+    "filesCount": "{count} sur la facture"
+  },
+  "label": {
+    "action": "Étiquettes",
+    "title": "Imprimer les étiquettes",
+    "description": "Une étiquette par pneu, avec un QR qui ouvre ce jeu.",
+    "format": "Format",
+    "formats": {
+      "dymo_small": "Petit rouleau",
+      "dymo_standard": "Rouleau standard",
+      "thermal_large": "Grand rouleau",
+      "sheet_a4": "Planche A4"
+    },
+    "perSheet": "{count} par planche",
+    "copies": "Combien",
+    "copiesHint": "une par pneu ({count})",
+    "fewer": "Une de moins",
+    "more": "Une de plus",
+    "print": "Imprimer",
+    "shelfNote": "L’étagère n’est volontairement pas imprimée. Les jeux sont déplacés, et une étagère imprimée serait alors fausse. Le QR indique toujours l’emplacement actuel.",
+    "reference": "Réf",
+    "quantity": "{count, plural, one {# pneu} other {# pneus}}",
+    "withRims": "Sur jantes",
+    "tpms": "TPMS",
+    "studded": "Cloutés",
+    "unassigned": "Non attribué",
+    "preview": "Aperçu",
+    "previewScale": "Affiché à la taille d’impression. Le code se scanne aussi depuis l’écran.",
+    "previewFailed": "Aperçu impossible"
+  },
+  "vehicle": {
+    "oneOnShelf": "en {code}",
+    "inStorage": "à l’hôtel à pneus"
+  },
+  "wear": {
+    "summary": "{value} d’usure entre le {from} et le {to}",
+    "perSeason": "Environ {value} par saison",
+    "seasonsLeft": "{count, plural, =0 {À la limite} one {Encore une saison environ} other {Encore # saisons environ}}",
+    "since": "-{value}"
+  },
+  "settings": {
+    "change": "Modifier dans les paramètres",
+    "prepPrices": "Tarifs de préparation",
+    "storagePrices": "Tarifs de stockage",
+    "treadLimits": "Modifier la limite",
+    "defaultCapacity": "Capacité par défaut"
+  },
+  "forecast": {
+    "title": "La saison prochaine",
+    "subtitle": "Ce que les jeux stockés sont mesurés comme devant remplacer, par dimension.",
+    "backToList": "Retour aux jeux",
+    "tires": "Pneus attendus",
+    "atLimit": "Déjà à la limite",
+    "expected": "Attendu sous une saison",
+    "orderTitle": "Par dimension",
+    "sizes": "{count} dimensions",
+    "empty": "Rien de mesuré près de la limite pour l’instant. Il faut deux passages pour établir un rythme d’usure.",
+    "nowCount": "{count, plural, one {# jeu à la limite} other {# jeux à la limite}}",
+    "nextCount": "{count, plural, one {# jeu sous une saison} other {# jeux sous une saison}}",
+    "tiresShort": "pneus",
+    "lowest": "plus faible {value}",
+    "now": "À la limite",
+    "next": "Sous une saison",
+    "rate": "{value} par saison",
+    "caveat": "Les jeux déjà à la limite sont mesurés. Le reste est extrapolé de l’usure d’un client sur un intervalle : à utiliser pour planifier, pas pour commander.",
+    "unmeasured": "{count, plural, one {# jeu manque d’historique et n’est pas compté} other {# jeux manquent d’historique et ne sont pas comptés}}",
+    "capped": "Les {shown} jeux les plus récents sur {total} ont été lus."
+  },
+  "invoiceLine": {
+    "storage": "Stockage de pneus",
+    "pieces": "pcs",
+    "fromDate": "depuis le {date}"
+  },
+  "setNames": {
+    "winter": "pneus hiver",
+    "summer": "pneus été",
+    "all_season": "pneus 4 saisons",
+    "other": "pneus"
+  },
+  "jobNotes": {
+    "set": "Jeu de pneus {reference}",
+    "shelf": "Étagère : {shelf}",
+    "notOnShelf": "pas en rayon",
+    "onRims": "Sur jantes",
+    "tiresOnly": "Pneus seuls",
+    "tpms": "Capteurs TPMS"
+  },
+  "files": {
+    "title": "Photos et fichiers",
+    "add": "Ajouter des fichiers",
+    "none": "Rien de joint pour l’instant. Photographiez une jante frottée pendant que les pneus sont devant vous.",
+    "captionPlaceholder": "Que montre cette image ?",
+    "onInvoice": "Sur la facture",
+    "invoiceHint": "Les fichiers marqués pour la facture sont copiés sur l’ordre ou la facture au moment de facturer.",
+    "deleteTitle": "Retirer ce fichier ?",
+    "deleteBody": "{name} est retiré du jeu. Ce qui a déjà été copié sur une facture y reste.",
+    "failed": "Impossible d’enregistrer",
+    "someFailed": "{count, plural, one {# fichier n’a pas pu être envoyé} other {# fichiers n’ont pas pu être envoyés}}"
+  }
+}

+ 27 - 1
messages/it/audit.json

@@ -109,6 +109,32 @@
     "settings_applyTaxRate": "Aliquota applicata",
     "settings_convertToExclusive": "Prezzi convertiti in imponibile",
     "settings_convertToInclusive": "Prezzi convertiti in ivato",
-    "organization_sampleDataRemove": "Dati di esempio rimossi"
+    "organization_sampleDataRemove": "Dati di esempio rimossi",
+    "tire_warehouse_create": "Sede di deposito pneumatici creata",
+    "tire_warehouse_update": "Sede di deposito pneumatici aggiornata",
+    "tire_warehouse_delete": "Sede di deposito pneumatici eliminata",
+    "tire_warehouse_archive": "Sede di deposito pneumatici archiviata",
+    "tire_location_create": "Scaffale pneumatici creato",
+    "tire_location_bulk_create": "Scaffali pneumatici creati",
+    "tire_location_update": "Scaffale pneumatici aggiornato",
+    "tire_location_delete": "Scaffale pneumatici eliminato",
+    "tire_set_check_in": "Treno di pneumatici accettato",
+    "tire_set_check_out": "Treno di pneumatici restituito",
+    "tire_set_relocate": "Treno di pneumatici spostato",
+    "tire_set_update": "Treno di pneumatici aggiornato",
+    "tire_set_delete": "Treno di pneumatici eliminato",
+    "tire_measurement_create": "Misurazioni pneumatici registrate",
+    "tire_treatment_update": "Preparazione aggiornata",
+    "tire_treatment_mark": "Preparazione segnata",
+    "tire_treatment_complete_all": "Preparazione completata",
+    "tire_set_message_customer": "Messaggio inviato al cliente su un treno di pneumatici",
+    "tire_set_create_quote": "Preventivo creato da un treno di pneumatici",
+    "tire_set_create_work_order": "Ordine creato da un treno di pneumatici",
+    "tire_set_add_to_work_order": "Treno di pneumatici aggiunto a un ordine",
+    "tire_set_unlink_work_order": "Treno di pneumatici scollegato da un ordine",
+    "tire_set_return": "Treno di nuovo in deposito",
+    "tire_set_dispose": "Treno di pneumatici dismesso",
+    "tire_set_attach": "File aggiunti a un treno di pneumatici",
+    "tire_set_detach": "File rimosso da un treno di pneumatici"
   }
 }

+ 2 - 1
messages/it/common.json

@@ -10,7 +10,8 @@
     "confirm": "Conferma",
     "saveChanges": "Salva modifiche",
     "edit": "Modifica",
-    "add": "Aggiungi"
+    "add": "Aggiungi",
+    "gotIt": "Ho capito"
   },
   "contextMenu": {
     "hint": "Suggerimento: clic destro su una riga per le azioni",

+ 11 - 1
messages/it/dashboard.json

@@ -54,7 +54,8 @@
     "activeJobs": "Lavori attivi",
     "recentObservations": "Osservazioni recenti",
     "recentActivity": "Attività recente",
-    "gettingStarted": "Per iniziare"
+    "gettingStarted": "Per iniziare",
+    "tireHotel": "Hotel pneumatici"
   },
   "maintenance": {
     "title": "Manutenzione prevista in scadenza",
@@ -234,5 +235,14 @@
       "expired": "Scaduto",
       "converted": "Convertito"
     }
+  },
+  "tireHotel": {
+    "title": "Hotel pneumatici",
+    "viewAll": "Vedi hotel pneumatici",
+    "stored": "{count} treni in deposito",
+    "capacity": "{used} / {capacity} pneumatici",
+    "roomLeft": "Spazio per altri {count}",
+    "needsPrep": "Da preparare",
+    "unbilled": "Non fatturato ({count})"
   }
 }

+ 6 - 0
messages/it/featureHints.json

@@ -0,0 +1,6 @@
+{
+  "tire-hotel": {
+    "title": "L’hotel pneumatici è attivo",
+    "body": "Conserva gli pneumatici stagionali. Crea gli scaffali, poi accetta un treno."
+  }
+}

+ 8 - 2
messages/it/navigation.json

@@ -53,7 +53,12 @@
     "allReminders": "Tutti i promemoria",
     "auditLog": "Registro attività",
     "aiAssistant": "Assistente IA",
-    "observations": "Osservazioni"
+    "observations": "Osservazioni",
+    "tireHotel": "Hotel pneumatici",
+    "allTireSets": "Tutti i treni",
+    "tireStorage": "Disposizione del deposito",
+    "tireSetDetails": "Treno di pneumatici",
+    "tireForecast": "La prossima stagione"
   },
   "search": "Cerca...",
   "shortcut": "Ctrl+K",
@@ -107,6 +112,7 @@
     "signOut": "Esci",
     "more": "Altro",
     "auditLog": "Registro attività",
-    "aiAssistant": "Assistente IA"
+    "aiAssistant": "Assistente IA",
+    "tireHotel": "Hotel pneumatici"
   }
 }

+ 27 - 0
messages/it/permissions.json

@@ -0,0 +1,27 @@
+{
+  "subjects": {
+    "dashboard": "Dashboard",
+    "vehicles": "Veicoli",
+    "customers": "Clienti",
+    "work_orders": "Ordini di lavoro",
+    "quotes": "Preventivi",
+    "services": "Interventi",
+    "billing": "Fatturazione",
+    "inventory": "Magazzino",
+    "labor_presets": "Preset di manodopera",
+    "inspections": "Ispezioni",
+    "tire_hotel": "Hotel pneumatici",
+    "reports": "Report",
+    "work_board": "Bacheca lavori",
+    "ai_assistant": "Assistente IA",
+    "settings": "Impostazioni"
+  },
+  "actions": {
+    "create": "Creare",
+    "read": "Vedere",
+    "update": "Modificare",
+    "delete": "Eliminare",
+    "manage": "Gestire",
+    "manage_storage": "Gestire la disposizione del deposito"
+  }
+}

+ 9 - 0
messages/it/search.json

@@ -0,0 +1,9 @@
+{
+  "title": "Cerca",
+  "description": "Cerca veicoli, clienti, interventi e impostazioni",
+  "placeholder": "Cerca per nome, targa, telefono, telaio, fattura…",
+  "noResults": "Nessun risultato.",
+  "recentCustomers": "Clienti recenti",
+  "services": "Interventi",
+  "toClose": "per chiudere"
+}

+ 34 - 1
messages/it/settings.json

@@ -122,6 +122,10 @@
       "support": {
         "title": "Assistenza",
         "description": "Contatta l'assistenza e gestisci il pulsante"
+      },
+      "tireHotel": {
+        "title": "Hotel pneumatici",
+        "description": "Deposito stagionale degli pneumatici"
       }
     },
     "pro": "PRO"
@@ -974,7 +978,9 @@
       "lastBackup": "Ultimo backup:",
       "supportPrompt": "Per qualsiasi domanda sulla protezione dei tuoi dati,",
       "supportAction": "contatta il nostro team di supporto."
-    }
+    },
+    "optTireHotel": "Hotel pneumatici",
+    "optTireHotelDesc": "Treni in deposito, scaffali, preparazione e contratti di deposito"
   },
   "license": {
     "title": "Licenza White-Label",
@@ -1454,5 +1460,32 @@
     "unavailable": "Questo browser non ha proposto l'installazione. Apri il menu del browser e cerca «Installa app» o «Aggiungi a Home».",
     "bannerHidden": "Il banner di installazione è nascosto su questo dispositivo.",
     "showBanner": "Mostra di nuovo il banner"
+  },
+  "tireHotel": {
+    "title": "Hotel pneumatici",
+    "description": "Custodisci gli pneumatici stagionali dei tuoi clienti e sai sempre dove si trova ogni treno.",
+    "cardTitle": "Deposito pneumatici",
+    "enable": "Attiva hotel pneumatici",
+    "enableHint": "Aggiunge il deposito pneumatici al menu. Disattivandolo in seguito viene nascosto senza cancellare nulla.",
+    "defaultCapacity": "Pneumatici per ripiano predefiniti",
+    "defaultCapacityHint": "Precompilato quando crei nuovi ripiani. Modificabile per ogni ripiano.",
+    "warnPercent": "Avvisa quando pieno (%)",
+    "warnPercentHint": "Segnala la panoramica del deposito quando questa quota della capacità totale è occupata.",
+    "treadTitle": "Soglie di sostituzione",
+    "treadHint": "I minimi di legge variano da paese a paese, quindi imposta le soglie del tuo mercato. Un battistrada sotto la soglia viene segnalato per la sostituzione.",
+    "summerLimit": "Soglia estiva ({unit})",
+    "winterLimit": "Soglia invernale ({unit})",
+    "unitMm": "mm",
+    "unit32nds": "1/32\"",
+    "setupHint": "Poi configura i tuoi scaffali.",
+    "setupLink": "Apri la disposizione del deposito",
+    "save": "Salva impostazioni hotel pneumatici",
+    "saved": "Impostazioni hotel pneumatici salvate",
+    "billingTitle": "Fatturazione del deposito",
+    "billingHint": "Precompilato sulla riga di deposito quando fatturi un treno. Modificabile sulla fattura.",
+    "seasonalPrice": "Prezzo di deposito predefinito",
+    "prepTitle": "Prezzi della preparazione",
+    "prepHint": "Quanto costa ogni tipo di preparazione. Lasciandolo vuoto non viene mai fatturata, come serve a chi include il lavaggio nel deposito. Quelle con un prezzo finiscono da sole su preventivo e ordine.",
+    "prepNotCharged": "non addebitato"
   }
 }

+ 473 - 0
messages/it/tireHotel.json

@@ -0,0 +1,473 @@
+{
+  "title": "Hotel pneumatici",
+  "description": "Deposito stagionale degli pneumatici dei tuoi clienti",
+  "common": {
+    "cancel": "Annulla",
+    "save": "Salva",
+    "create": "Crea",
+    "delete": "Elimina",
+    "edit": "Modifica"
+  },
+  "seasons": {
+    "summer": "Estivi",
+    "winter": "Invernali",
+    "all_season": "Quattro stagioni",
+    "other": "Altro"
+  },
+  "statuses": {
+    "stored": "In deposito",
+    "released": "Restituiti",
+    "disposed": "Smaltiti"
+  },
+  "conditions": {
+    "good": "Buono",
+    "fair": "Da tenere d'occhio",
+    "replace": "Da sostituire"
+  },
+  "positions": {
+    "front_left": "Anteriore sinistro",
+    "front_right": "Anteriore destro",
+    "rear_left": "Posteriore sinistro",
+    "rear_right": "Posteriore destro",
+    "spare": "Ruota di scorta",
+    "unspecified": "Non specificato"
+  },
+  "capacity": {
+    "used": "{used} di {capacity} pneumatici",
+    "free": "Spazio per {count}",
+    "freeShort": "{count} liberi",
+    "over": "{count} oltre la capacità",
+    "meterLabel": "{used} di {capacity} pneumatici in deposito"
+  },
+  "tread": {
+    "placeholderMm": "mm",
+    "placeholder32": "32esimi",
+    "inputLabel": "Profondità del battistrada, {position}",
+    "lastTime": "era {value}",
+    "worn": "{value} di usura"
+  },
+  "list": {
+    "tabAll": "Tutti",
+    "tabStored": "In deposito",
+    "tabReleased": "Restituiti",
+    "search": "Cerca per targa, cliente, marca o scaffale",
+    "checkIn": "Accettare pneumatici",
+    "empty": "Nessun treno di pneumatici. Accetta un treno per iniziare.",
+    "unassigned": "Non assegnato",
+    "reference": "Rif",
+    "vehicle": "Veicolo",
+    "tires": "Pneumatici",
+    "location": "Scaffale",
+    "condition": "Condizione",
+    "status": "Stato",
+    "storedSince": "Dal",
+    "tireCount": "{count, plural, one {# pneumatico} other {# pneumatici}}",
+    "studded": "Chiodati",
+    "roomLeft": "Spazio per {count}",
+    "prep": "Prep.",
+    "tabNeedsPrep": "Da preparare",
+    "setUpShelves": "Configura gli scaffali",
+    "emptyNoShelves": "Nessuno scaffale. Configura il deposito prima di accettare pneumatici.",
+    "tabDisposed": "Dismessi"
+  },
+  "checkIn": {
+    "title": "Accettare pneumatici",
+    "description": "Registra ciò che è arrivato, poi scegli dove riporlo.",
+    "customer": "Cliente",
+    "selectCustomer": "Seleziona cliente",
+    "noCustomer": "Nessun cliente",
+    "vehicle": "Veicolo",
+    "selectVehicle": "Seleziona veicolo",
+    "season": "Stagione",
+    "quantity": "Numero di pneumatici",
+    "quantityHint": "Contati singolarmente, così vanno bene anche due o cinque ruote.",
+    "brand": "Marca",
+    "brandPlaceholder": "Michelin, Bridgestone, Pirelli",
+    "model": "Modello",
+    "size": "Misura",
+    "sizePlaceholder": "225/45R17",
+    "dotCode": "Codice DOT",
+    "dotPlaceholder": "3623",
+    "dotHint": "Settimana e anno di produzione dello pneumatico.",
+    "withRims": "Su cerchi",
+    "hasTpms": "Sensori TPMS",
+    "studded": "Chiodati",
+    "location": "Dove vanno gli pneumatici",
+    "choosePlace": "Scegli uno scaffale",
+    "searchLocations": "Cerca scaffali",
+    "noLocations": "Nessuno scaffale trovato",
+    "tooFull": "Spazio insufficiente",
+    "pickLocationFirst": "Scegli uno scaffale prima di accettare il treno",
+    "roomSummary": "{locations} scaffali adatti a questo treno, {tires} posti liberi",
+    "nothingFits": "Nessuno scaffale ha spazio per {quantity} pneumatici. Libera spazio o aggiungi uno scaffale.",
+    "treadTitle": "Profondità del battistrada all'arrivo",
+    "treadHint": "Facoltativo, ma risolve ogni futura discussione sulle condizioni.",
+    "notes": "Note",
+    "submit": "Accetta",
+    "success": "Treno {reference} riposto in {code}",
+    "failed": "Impossibile accettare il treno",
+    "noVehiclesForCustomer": "Questo cliente non ha ancora veicoli. Aggiungine uno dalla sua scheda cliente.",
+    "treatmentsHint": "Cosa fare su questo treno prima del deposito o del rimontaggio.",
+    "storedBefore": "Già in deposito qui",
+    "storedBeforeHint": "Tornano gli stessi pneumatici? Scegli il treno e la stagione entra nel suo storico.",
+    "sameSet": "Stesso treno di prima",
+    "pieces": "{count} pz",
+    "lastOut": "Ritirato il {date}",
+    "notStored": "Al momento non in deposito",
+    "returned": "Il treno {reference} è di nuovo su {code}",
+    "submitReturn": "Metti di nuovo in deposito",
+    "ownerFromVehicle": "Preso dal veicolo",
+    "vehicleOwner": "Il proprietario del veicolo",
+    "clearVehicle": "Rimuovi il veicolo"
+  },
+  "storage": {
+    "title": "Scaffali",
+    "description": "Configura il deposito una volta e vedi a colpo d'occhio cosa entra dove.",
+    "addWarehouse": "Aggiungi sede",
+    "addShelves": "Aggiungi scaffali",
+    "warehouseName": "Nome",
+    "warehouseNamePlaceholder": "Capannone principale, Container 2, Retro",
+    "warehouseAddress": "Indirizzo",
+    "warehouseCreated": "Sede di deposito aggiunta",
+    "warehouseDeleted": "{name} eliminata",
+    "warehouseArchived": "{name} archiviata",
+    "deleteWarehouse": "Elimina sede",
+    "deleteWarehouseTitle": "Eliminare {name}?",
+    "deleteWarehouseBody": "Questo rimuove la sede e tutti i suoi scaffali.",
+    "deleteWarehouseBodyOccupied": "Questa sede contiene ancora {count} pneumatici, quindi verrà archiviata anziché eliminata.",
+    "totalCapacity": "Capacità totale",
+    "totalStored": "In deposito ora",
+    "totalFree": "Spazio libero",
+    "totalLocations": "Scaffali",
+    "locationSummary": "{occupied} di {total} scaffali in uso",
+    "noLocations": "Nessuno scaffale. Aggiungi una scaffalatura per depositare pneumatici qui.",
+    "emptyTitle": "Nessun deposito configurato",
+    "emptyBody": "Aggiungi il primo capannone o locale e riempilo di scaffali.",
+    "default": "Predefinita",
+    "addLocation": "Aggiungi scaffali",
+    "editLocation": "Modifica scaffale",
+    "createLocation": "Crea scaffale",
+    "createShelves": "Crea scaffali",
+    "locationDialogDescription": "Aggiunta di spazio a {warehouse}.",
+    "tabBulk": "Una scaffalatura",
+    "tabSingle": "Un ripiano",
+    "zone": "Zona",
+    "zonePlaceholder": "A, Locale 1, Nord",
+    "rack": "Scaffalatura",
+    "rackPlaceholder": "04",
+    "shelf": "Ripiano",
+    "shelfPlaceholder": "2",
+    "position": "Posizione",
+    "positionPlaceholder": "Sinistra",
+    "shelfFrom": "Ripiani da",
+    "shelfTo": "a",
+    "code": "Codice",
+    "codePlaceholder": "A-04-2",
+    "codeHint": "Compare sull'etichetta. Viene compilato in automatico se lo lasci vuoto.",
+    "capacity": "Pneumatici per ripiano",
+    "capacityHint": "Quanti singoli pneumatici ci stanno qui.",
+    "notes": "Note",
+    "bulkPreview": "Crea {count} ripiani, da {first} a {last}",
+    "bulkPreviewCapacity": "Spazio per {tires} pneumatici in totale",
+    "bulkCreated": "{count} ripiani creati",
+    "bulkCreatedWithSkips": "{created} ripiani creati, {skipped} già esistenti saltati",
+    "bulkNothingCreated": "Quei ripiani esistono già",
+    "locationCreated": "Scaffale aggiunto",
+    "locationUpdated": "Scaffale aggiornato",
+    "locationDeleted": "{code} eliminato",
+    "deleteLocationTitle": "Eliminare {code}?",
+    "deleteLocationBody": "Lo scaffale deve prima essere vuoto.",
+    "saveFailed": "Salvataggio non riuscito",
+    "deleteFailed": "Eliminazione non riuscita"
+  },
+  "movements": {
+    "check_in": "Accettato",
+    "check_out": "Restituito",
+    "relocate": "Spostato",
+    "dispose": "Smaltito",
+    "measure": "Misurato"
+  },
+  "detail": {
+    "move": "Sposta",
+    "checkOut": "Restituisci",
+    "conditionTitle": "Condizione",
+    "noMeasurements": "Nessuna misurazione registrata.",
+    "historyTitle": "Cronologia",
+    "storageTitle": "Deposito",
+    "shelf": "Scaffale",
+    "warehouse": "Sede",
+    "notStored": "Al momento non in deposito.",
+    "checkedIn": "Accettato",
+    "checkedOut": "Restituito",
+    "tiresTitle": "Pneumatici",
+    "loadSpeed": "Indici di carico e velocità",
+    "ownerTitle": "Proprietario",
+    "phone": "Telefono",
+    "deleteTitle": "Eliminare questo treno di pneumatici?",
+    "deleteBody": "Il treno e tutta la sua cronologia vengono rimossi. L’operazione non è reversibile.",
+    "deleted": "Treno di pneumatici eliminato",
+    "deleteFailed": "Impossibile eliminare il treno",
+    "belowLimit": "{count} pneumatico/i al limite di sostituzione o sotto.",
+    "belowLimitHint": "Vale la pena proporre un prezzo finché gli pneumatici sono ancora qui.",
+    "dispose": "Dismetti",
+    "disposeTitle": "Dismettere questo treno?",
+    "disposeBody": "La scheda e lo storico restano e il posto si libera. Non verrà più proposto come lo stesso treno la prossima stagione.",
+    "disposed": "Treno dismesso",
+    "disposeFailed": "Impossibile dismettere il treno"
+  },
+  "checkOut": {
+    "title": "Restituire gli pneumatici",
+    "description": "Il treno viene tolto da {code}.",
+    "descriptionNoLocation": "Questo treno torna al cliente.",
+    "treadTitle": "Battistrada alla riconsegna",
+    "note": "Nota",
+    "submit": "Restituisci",
+    "success": "Treno {reference} restituito",
+    "failed": "Impossibile restituire il treno",
+    "prepPending": "{count} lavoro/i di preparazione in sospeso",
+    "conditionTitle": "Condizione all’accettazione",
+    "conditionLead": {
+      "replace": "Uno o più pneumatici sono al limite di sostituzione o sotto. Vale la pena dirlo al cliente prima che riparta.",
+      "fair": "Uno o più pneumatici si avvicinano al limite di sostituzione. Da segnalare prima della prossima stagione."
+    },
+    "treadHintUnmeasured": "Non è stata fatta alcuna misurazione all’accettazione. Questa è l’ultima occasione."
+  },
+  "relocate": {
+    "title": "Sposta gli pneumatici",
+    "description": "Scegli lo scaffale di destinazione.",
+    "newLocation": "Nuovo scaffale",
+    "note": "Nota",
+    "submit": "Sposta",
+    "success": "Treno {reference} spostato",
+    "failed": "Impossibile spostare il treno"
+  },
+  "edit": {
+    "title": "Modifica treno di pneumatici",
+    "description": "Correggi quanto registrato in accettazione. Usa Sposta per cambiare scaffale.",
+    "quantityHint": "L’aumento viene verificato rispetto allo scaffale su cui si trova il treno.",
+    "rimType": "Tipo di cerchio",
+    "rimTypePlaceholder": "Lega, acciaio",
+    "success": "Treno di pneumatici aggiornato",
+    "failed": "Impossibile salvare le modifiche",
+    "treadHint": "Una misura modificata viene registrata come nuova rilevazione nello storico, senza sovrascrivere la precedente."
+  },
+  "treatments": {
+    "types": {
+      "wash_tires": "Lavare pneumatici",
+      "wash_rims": "Lavare cerchi",
+      "balance": "Equilibrare",
+      "tpms_service": "Servizio TPMS",
+      "new_valves": "Valvole nuove",
+      "repair": "Riparazione"
+    },
+    "statuses": {
+      "pending": "Da fare",
+      "done": "Fatto",
+      "skipped": "Saltato"
+    },
+    "title": "Preparazione",
+    "none": "Su questo treno non c’è nulla da fare.",
+    "add": "Aggiungi preparazione",
+    "pickHint": "Spunta ciò che serve a questo treno prima di tornare sulla vettura.",
+    "pendingCount": "{count} da fare",
+    "allComplete": "Tutto fatto",
+    "ready": "Pronto",
+    "markDone": "Fatto",
+    "completeAll": "Segna tutti e {count} come fatti",
+    "allDone": "Preparazione completata",
+    "undo": "Rimetti in elenco",
+    "saveFailed": "Impossibile salvare",
+    "skip": "Salta"
+  },
+  "messaging": {
+    "action": "Messaggio",
+    "title": "Scrivi al cliente",
+    "description": "A {name}.",
+    "noCustomer": "Nessun cliente su questo treno.",
+    "noCustomerBody": "Collega un cliente al treno prima di scrivergli.",
+    "channel": "Invia via",
+    "channels": {
+      "email": "E-mail",
+      "sms": "SMS",
+      "telegram": "Telegram"
+    },
+    "noAddress": "nessun recapito registrato",
+    "recipient": "Invia a",
+    "recipientPlaceholder": "Numero di telefono",
+    "subject": "Oggetto",
+    "message": "Messaggio",
+    "send": "Invia",
+    "sent": "Messaggio inviato",
+    "failed": "Impossibile inviare il messaggio",
+    "smsLong": "I messaggi lunghi possono essere divisi in più SMS.",
+    "subjects": {
+      "low_tread": "I tuoi pneumatici {season}",
+      "damage": "I tuoi pneumatici {season}",
+      "stored": "I tuoi pneumatici {season} sono in deposito",
+      "custom": "I tuoi pneumatici {season}"
+    },
+    "bodies": {
+      "low_tread": "Ciao {customer_name}, i tuoi pneumatici {season} sono in deposito da noi, {shop_name}. All'accettazione {positions} misuravano {tread}, sotto il limite. Un'altra stagione non la reggono. Ti facciamo un prezzo per un treno nuovo?",
+      "damage": "Ciao {customer_name}, i tuoi pneumatici {season} sono in deposito da noi, {shop_name}. All'accettazione abbiamo trovato un danno su {positions}. Chiamaci quando puoi e lo vediamo insieme.",
+      "stored": "Ciao {customer_name}, i tuoi pneumatici {season} sono ora in deposito da noi, {shop_name}. Ti avvisiamo quando sarà il momento del cambio.",
+      "custom": "Ciao {customer_name}, riguardo ai tuoi pneumatici {season} in deposito da noi, {shop_name}:"
+    },
+    "enableInSettings": "Non ancora configurato. Attivalo nelle Impostazioni."
+  },
+  "job": {
+    "cardTitle": "Preventivi e ordini",
+    "none": "Per questo treno non è stato creato nulla.",
+    "quote": "Preventivo",
+    "workOrder": "Ordine",
+    "converted": "convertito",
+    "quoteTitle": "Preventiva un treno nuovo",
+    "workOrderTitle": "Fattura questo treno",
+    "loadingSet": "Caricamento del treno",
+    "tires": "Pneumatici",
+    "inStock": "{count} disponibili",
+    "shortStock": "solo {count} disponibili",
+    "noMatch": "Niente a magazzino corrisponde a {size}. La riga viene aggiunta al prezzo che indichi.",
+    "noSize": "Questo treno non ha misura, quindi il magazzino non si può confrontare. La riga usa il tuo prezzo.",
+    "unitPrice": "Prezzo per pneumatico",
+    "createQuote": "Crea preventivo",
+    "createWorkOrder": "Crea ordine",
+    "quoteCreated": "Preventivo {number} creato",
+    "workOrderCreated": "Ordine creato",
+    "failed": "Creazione non riuscita",
+    "bannerTitle": "Da prelevare in",
+    "openSet": "Apri il treno",
+    "prepPending": "Da fare prima del montaggio: {jobs}",
+    "notOnShelf": "Questo treno non è più in deposito.",
+    "targetNew": "Nuovo ordine",
+    "targetNewHint": "Crea un ordine solo per questi pneumatici.",
+    "targetExisting": "Oppure aggiungi a un ordine già aperto per questo veicolo",
+    "addToWorkOrder": "Aggiungi all’ordine",
+    "workOrderUpdated": "Aggiunto all’ordine",
+    "unlink": "Scollega",
+    "unlinkTitle": "Scollegare questi pneumatici dall’ordine?",
+    "unlinkBody": "L’ordine non punta più al treno di pneumatici. Ricambi e manodopera già inseriti restano: rimuovili separatamente se non vanno fatturati.",
+    "unlinked": "Pneumatici scollegati",
+    "unlinkFailed": "Impossibile scollegare gli pneumatici",
+    "prep": "Preparazione",
+    "tiresSkipped": "Nessuna riga pneumatici verrà aggiunta.",
+    "total": "Totale",
+    "searchStock": "Cerca in magazzino per marca, misura o codice articolo",
+    "noSearchHits": "Niente in magazzino corrisponde a \"{query}\".",
+    "otherSize": "Altra misura",
+    "inSize": "{count} in {size}",
+    "quantity": "Qtà",
+    "noStockItem": "Nessun articolo a magazzino, prezzo manuale",
+    "storeTires": "Deposita gli pneumatici",
+    "storage": "Deposito",
+    "storageFee": "Importo",
+    "storagePeriod": "Periodo",
+    "destination": "Dove finisce",
+    "targetInvoice": "Nuova fattura",
+    "targetInvoiceHint": "Fattura il cliente direttamente, senza un ordine su un veicolo.",
+    "createInvoice": "Crea fattura",
+    "needsCustomer": "Collega un cliente o un veicolo al treno prima di fatturare.",
+    "storagePeriodEnd": "Ritirato",
+    "storageOpenEnded": "Aperto",
+    "storagePeriodHint": "Compare sulla riga della fattura, così il cliente vede cosa copre l’importo. Lascia la fine vuota finché non sai quando ritirano.",
+    "pricesHint": "I prezzi della preparazione vengono dalle impostazioni. Ogni riga resta modificabile dopo la creazione.",
+    "files": "Foto e file",
+    "filesCount": "{count} in fattura"
+  },
+  "label": {
+    "action": "Etichette",
+    "title": "Stampa le etichette",
+    "description": "Un adesivo per pneumatico, con QR che apre questo treno.",
+    "format": "Formato",
+    "formats": {
+      "dymo_small": "Rotolo piccolo",
+      "dymo_standard": "Rotolo standard",
+      "thermal_large": "Rotolo grande",
+      "sheet_a4": "Foglio A4"
+    },
+    "perSheet": "{count} per foglio",
+    "copies": "Quante",
+    "copiesHint": "una per pneumatico ({count})",
+    "fewer": "Una in meno",
+    "more": "Una in più",
+    "print": "Stampa",
+    "shelfNote": "Lo scaffale non viene stampato di proposito. I treni vengono spostati, e uno scaffale stampato risulterebbe sbagliato. Il QR mostra sempre la posizione attuale.",
+    "reference": "Rif",
+    "quantity": "{count, plural, one {# pneumatico} other {# pneumatici}}",
+    "withRims": "Su cerchi",
+    "tpms": "TPMS",
+    "studded": "Chiodati",
+    "unassigned": "Non assegnato",
+    "preview": "Anteprima",
+    "previewScale": "Mostrata a dimensione di stampa. Il codice si scansiona anche dallo schermo.",
+    "previewFailed": "Impossibile creare l’anteprima"
+  },
+  "vehicle": {
+    "oneOnShelf": "in {code}",
+    "inStorage": "in hotel"
+  },
+  "wear": {
+    "summary": "{value} di usura tra il {from} e il {to}",
+    "perSeason": "Circa {value} a stagione",
+    "seasonsLeft": "{count, plural, =0 {Già al limite} one {Circa una stagione ancora} other {Circa # stagioni ancora}}",
+    "since": "-{value}"
+  },
+  "settings": {
+    "change": "Modifica nelle impostazioni",
+    "prepPrices": "Prezzi della preparazione",
+    "storagePrices": "Prezzi del deposito",
+    "treadLimits": "Modifica il limite",
+    "defaultCapacity": "Capacità predefinita"
+  },
+  "forecast": {
+    "title": "La prossima stagione",
+    "subtitle": "Ciò che i treni in deposito risultano dover sostituire, per misura.",
+    "backToList": "Torna ai treni",
+    "tires": "Pneumatici previsti",
+    "atLimit": "Già al limite",
+    "expected": "Previsto entro una stagione",
+    "orderTitle": "Per misura",
+    "sizes": "{count} misure",
+    "empty": "Nulla misurato vicino al limite finora. Servono due visite per ricavare un ritmo di usura.",
+    "nowCount": "{count, plural, one {# treno al limite} other {# treni al limite}}",
+    "nextCount": "{count, plural, one {# treno entro una stagione} other {# treni entro una stagione}}",
+    "tiresShort": "pneumatici",
+    "lowest": "minimo {value}",
+    "now": "Al limite",
+    "next": "Entro una stagione",
+    "rate": "{value} a stagione",
+    "caveat": "I treni al limite sono misurati. Il resto è estrapolato dall’usura di un cliente su un intervallo: utile per pianificare, non per ordinare.",
+    "unmeasured": "{count, plural, one {# treno ha poco storico e non viene conteggiato} other {# treni hanno poco storico e non vengono conteggiati}}",
+    "capped": "Letti i {shown} treni più recenti su {total}."
+  },
+  "invoiceLine": {
+    "storage": "Deposito pneumatici",
+    "pieces": "pz",
+    "fromDate": "dal {date}"
+  },
+  "setNames": {
+    "winter": "pneumatici invernali",
+    "summer": "pneumatici estivi",
+    "all_season": "pneumatici quattro stagioni",
+    "other": "pneumatici"
+  },
+  "jobNotes": {
+    "set": "Treno di pneumatici {reference}",
+    "shelf": "Scaffale: {shelf}",
+    "notOnShelf": "non a scaffale",
+    "onRims": "Su cerchi",
+    "tiresOnly": "Solo pneumatici",
+    "tpms": "Con sensori TPMS"
+  },
+  "files": {
+    "title": "Foto e file",
+    "add": "Aggiungi file",
+    "none": "Ancora nulla in allegato. Fotografa un cerchio segnato mentre hai gli pneumatici davanti.",
+    "captionPlaceholder": "Che cosa mostra?",
+    "onInvoice": "In fattura",
+    "invoiceHint": "I file marcati per la fattura vengono copiati sull’ordine o sulla fattura quando il treno viene fatturato.",
+    "deleteTitle": "Rimuovere questo file?",
+    "deleteBody": "{name} viene rimosso dal treno. Ciò che è già stato copiato su una fattura resta lì.",
+    "failed": "Impossibile salvare",
+    "someFailed": "{count, plural, one {# file non è stato caricato} other {# file non sono stati caricati}}"
+  }
+}

+ 27 - 1
messages/lt/audit.json

@@ -109,6 +109,32 @@
     "settings_applyTaxRate": "Pritaikytas mokesčio tarifas",
     "settings_convertToExclusive": "Kainos konvertuotos be mokesčio",
     "settings_convertToInclusive": "Kainos konvertuotos su mokesčiu",
-    "organization_sampleDataRemove": "Pašalinti pavyzdiniai duomenys"
+    "organization_sampleDataRemove": "Pašalinti pavyzdiniai duomenys",
+    "tire_warehouse_create": "Sukurta padangų saugykla",
+    "tire_warehouse_update": "Atnaujinta padangų saugykla",
+    "tire_warehouse_delete": "Ištrinta padangų saugykla",
+    "tire_warehouse_archive": "Archyvuota padangų saugykla",
+    "tire_location_create": "Sukurta padangų lentyna",
+    "tire_location_bulk_create": "Sukurtos padangų lentynos",
+    "tire_location_update": "Atnaujinta padangų lentyna",
+    "tire_location_delete": "Ištrinta padangų lentyna",
+    "tire_set_check_in": "Priimtas padangų komplektas",
+    "tire_set_check_out": "Atiduotas padangų komplektas",
+    "tire_set_relocate": "Perkeltas padangų komplektas",
+    "tire_set_update": "Atnaujintas padangų komplektas",
+    "tire_set_delete": "Ištrintas padangų komplektas",
+    "tire_measurement_create": "Užfiksuoti padangų matavimai",
+    "tire_treatment_update": "Atnaujintas paruošimas",
+    "tire_treatment_mark": "Pažymėtas paruošimas",
+    "tire_treatment_complete_all": "Paruošimas baigtas",
+    "tire_set_message_customer": "Klientui išsiųsta žinutė apie padangų komplektą",
+    "tire_set_create_quote": "Sukurtas pasiūlymas iš padangų komplekto",
+    "tire_set_create_work_order": "Sukurtas darbo užsakymas iš padangų komplekto",
+    "tire_set_add_to_work_order": "Padangų komplektas pridėtas prie darbo užsakymo",
+    "tire_set_unlink_work_order": "Padangų komplektas atsietas nuo darbo užsakymo",
+    "tire_set_return": "Komplektas vėl saugomas",
+    "tire_set_dispose": "Padangų komplektas nurašytas",
+    "tire_set_attach": "Prie padangų komplekto pridėta failų",
+    "tire_set_detach": "Failas pašalintas iš padangų komplekto"
   }
 }

+ 2 - 1
messages/lt/common.json

@@ -10,7 +10,8 @@
     "back": "Atgal",
     "confirm": "Patvirtinti",
     "edit": "Redaguoti",
-    "add": "Pridėti"
+    "add": "Pridėti",
+    "gotIt": "Supratau"
   },
   "contextMenu": {
     "hint": "Patarimas: spustelėkite eilutę dešiniuoju mygtuku veiksmams",

+ 11 - 1
messages/lt/dashboard.json

@@ -54,7 +54,8 @@
     "activeJobs": "Aktyvūs darbai",
     "recentObservations": "Naujausi pastebėjimai",
     "recentActivity": "Naujausia veikla",
-    "gettingStarted": "Pradžia"
+    "gettingStarted": "Pradžia",
+    "tireHotel": "Padangų viešbutis"
   },
   "maintenance": {
     "title": "Numatoma priežiūra",
@@ -234,5 +235,14 @@
       "expired": "Nebegalioja",
       "converted": "Konvertuota"
     }
+  },
+  "tireHotel": {
+    "title": "Padangų viešbutis",
+    "viewAll": "Žiūrėti padangų viešbutį",
+    "stored": "Saugoma komplektų: {count}",
+    "capacity": "{used} / {capacity} padangų",
+    "roomLeft": "Vietos dar {count}",
+    "needsPrep": "Reikia paruošti",
+    "unbilled": "Neapmokestinta ({count})"
   }
 }

+ 6 - 0
messages/lt/featureHints.json

@@ -0,0 +1,6 @@
+{
+  "tire-hotel": {
+    "title": "Padangų viešbutis įjungtas",
+    "body": "Saugokite sezonines padangas. Sukurkite lentynas ir priimkite komplektą."
+  }
+}

+ 8 - 2
messages/lt/navigation.json

@@ -53,7 +53,12 @@
     "allReminders": "Visi priminimai",
     "auditLog": "Audito žurnalas",
     "aiAssistant": "DI asistentas",
-    "observations": "Pastebėjimai"
+    "observations": "Pastebėjimai",
+    "tireHotel": "Padangų viešbutis",
+    "allTireSets": "Visi komplektai",
+    "tireStorage": "Saugyklos išdėstymas",
+    "tireSetDetails": "Padangų komplektas",
+    "tireForecast": "Kitas sezonas"
   },
   "search": "Ieškoti...",
   "shortcut": "Ctrl+K",
@@ -107,6 +112,7 @@
     "signOut": "Atsijungti",
     "more": "Daugiau",
     "auditLog": "Audito žurnalas",
-    "aiAssistant": "DI asistentas"
+    "aiAssistant": "DI asistentas",
+    "tireHotel": "Padangų viešbutis"
   }
 }

+ 27 - 0
messages/lt/permissions.json

@@ -0,0 +1,27 @@
+{
+  "subjects": {
+    "dashboard": "Skydelis",
+    "vehicles": "Transporto priemonės",
+    "customers": "Klientai",
+    "work_orders": "Darbo užsakymai",
+    "quotes": "Pasiūlymai",
+    "services": "Serviso įrašai",
+    "billing": "Atsiskaitymai",
+    "inventory": "Sandėlis",
+    "labor_presets": "Darbų šablonai",
+    "inspections": "Patikros",
+    "tire_hotel": "Padangų viešbutis",
+    "reports": "Ataskaitos",
+    "work_board": "Darbų lenta",
+    "ai_assistant": "DI asistentas",
+    "settings": "Nustatymai"
+  },
+  "actions": {
+    "create": "Kurti",
+    "read": "Peržiūrėti",
+    "update": "Redaguoti",
+    "delete": "Šalinti",
+    "manage": "Valdyti",
+    "manage_storage": "Valdyti saugyklos išdėstymą"
+  }
+}

+ 9 - 0
messages/lt/search.json

@@ -0,0 +1,9 @@
+{
+  "title": "Paieška",
+  "description": "Ieškokite transporto priemonių, klientų, darbų ir nustatymų",
+  "placeholder": "Ieškoti pagal vardą, numerį, telefoną, VIN, sąskaitą…",
+  "noResults": "Rezultatų nėra.",
+  "recentCustomers": "Naujausi klientai",
+  "services": "Serviso įrašai",
+  "toClose": "kad uždarytumėte"
+}

+ 34 - 1
messages/lt/settings.json

@@ -122,6 +122,10 @@
       "support": {
         "title": "Pagalba",
         "description": "Susisiekti su pagalba ir tvarkyti mygtuką"
+      },
+      "tireHotel": {
+        "title": "Padangų viešbutis",
+        "description": "Sezoninis padangų saugojimas"
       }
     },
     "pro": "PRO"
@@ -974,7 +978,9 @@
       "lastBackup": "Paskutinė atsarginė kopija:",
       "supportPrompt": "Jei turite klausimų apie jūsų duomenų apsaugą,",
       "supportAction": "susisiekite su mūsų pagalbos komanda."
-    }
+    },
+    "optTireHotel": "Padangų viešbutis",
+    "optTireHotelDesc": "Saugomi komplektai, lentynos, paruošimas ir saugojimo sutartys"
   },
   "license": {
     "title": "\"White-Label\" licencija",
@@ -1454,5 +1460,32 @@
     "unavailable": "Ši naršyklė nepasiūlė įdiegimo. Atidarykite naršyklės meniu ir ieškokite „Įdiegti programą“ arba „Į pradžios ekraną“.",
     "bannerHidden": "Įdiegimo juosta šiame įrenginyje paslėpta.",
     "showBanner": "Vėl rodyti juostą"
+  },
+  "tireHotel": {
+    "title": "Padangų viešbutis",
+    "description": "Saugokite klientų sezonines padangas ir žinokite, kur guli kiekvienas komplektas.",
+    "cardTitle": "Padangų saugojimas",
+    "enable": "Įjungti padangų viešbutį",
+    "enableHint": "Prideda padangų saugojimą į meniu. Vėliau išjungus, jis paslepiamas nieko neištrinant.",
+    "defaultCapacity": "Numatytas padangų skaičius lentynoje",
+    "defaultCapacityHint": "Užpildoma kuriant naujas lentynas. Galima keisti kiekvienai lentynai.",
+    "warnPercent": "Įspėti užsipildžius (%)",
+    "warnPercentHint": "Pažymi saugyklos apžvalgą, kai užimta tokia bendros talpos dalis.",
+    "treadTitle": "Keitimo ribos",
+    "treadHint": "Teisiniai minimumai skiriasi pagal šalį, tad nustatykite savo rinkos ribas. Protektorius žemiau ribos žymimas keitimui.",
+    "summerLimit": "Vasaros riba ({unit})",
+    "winterLimit": "Žiemos riba ({unit})",
+    "unitMm": "mm",
+    "unit32nds": "1/32\"",
+    "setupHint": "Toliau sutvarkykite lentynas.",
+    "setupLink": "Atverti saugyklos išdėstymą",
+    "save": "Išsaugoti padangų viešbučio nustatymus",
+    "saved": "Padangų viešbučio nustatymai išsaugoti",
+    "billingTitle": "Saugojimo apmokėjimas",
+    "billingHint": "Įrašoma į saugojimo eilutę apmokestinant komplektą. Sąskaitoje galima keisti.",
+    "seasonalPrice": "Numatytoji saugojimo kaina",
+    "prepTitle": "Paruošimo kainos",
+    "prepHint": "Kiek kainuoja kiekvienas paruošimo darbas. Palikus tuščią, jis niekada neapmokestinamas, o to ir reikia dirbtuvėms, kurios plovimą įskaičiuoja į saugojimą. Turintys kainą patys atsiranda pasiūlyme ir darbo užsakyme.",
+    "prepNotCharged": "neapmokestinama"
   }
 }

+ 473 - 0
messages/lt/tireHotel.json

@@ -0,0 +1,473 @@
+{
+  "title": "Padangų viešbutis",
+  "description": "Sezoninis klientų padangų saugojimas",
+  "common": {
+    "cancel": "Atšaukti",
+    "save": "Išsaugoti",
+    "create": "Sukurti",
+    "delete": "Ištrinti",
+    "edit": "Redaguoti"
+  },
+  "seasons": {
+    "summer": "Vasarinės",
+    "winter": "Žieminės",
+    "all_season": "Visasezonės",
+    "other": "Kita"
+  },
+  "statuses": {
+    "stored": "Saugykloje",
+    "released": "Atiduotos",
+    "disposed": "Utilizuotos"
+  },
+  "conditions": {
+    "good": "Gera",
+    "fair": "Stebėti",
+    "replace": "Keisti"
+  },
+  "positions": {
+    "front_left": "Priekinė kairė",
+    "front_right": "Priekinė dešinė",
+    "rear_left": "Galinė kairė",
+    "rear_right": "Galinė dešinė",
+    "spare": "Atsarginis ratas",
+    "unspecified": "Nenurodyta"
+  },
+  "capacity": {
+    "used": "{used} iš {capacity} padangų",
+    "free": "Vietos {count}",
+    "freeShort": "{count} laisvos",
+    "over": "{count} virš talpos",
+    "meterLabel": "Saugoma {used} iš {capacity} padangų"
+  },
+  "tread": {
+    "placeholderMm": "mm",
+    "placeholder32": "1/32 colio",
+    "inputLabel": "Protektoriaus gylis, {position}",
+    "lastTime": "buvo {value}",
+    "worn": "nusidėvėjo {value}"
+  },
+  "list": {
+    "tabAll": "Visos",
+    "tabStored": "Saugykloje",
+    "tabReleased": "Atiduotos",
+    "search": "Ieškoti pagal numerį, klientą, markę ar lentyną",
+    "checkIn": "Priimti padangas",
+    "empty": "Padangų komplektų dar nėra. Priimkite komplektą, kad pradėtumėte.",
+    "unassigned": "Nepriskirta",
+    "reference": "Nr.",
+    "vehicle": "Transporto priemonė",
+    "tires": "Padangos",
+    "location": "Lentyna",
+    "condition": "Būklė",
+    "status": "Statusas",
+    "storedSince": "Nuo",
+    "tireCount": "{count, plural, one {# padanga} few {# padangos} many {# padangos} other {# padangų}}",
+    "studded": "Su dygliais",
+    "roomLeft": "Vietos {count}",
+    "prep": "Paruoš.",
+    "tabNeedsPrep": "Reikia paruošti",
+    "setUpShelves": "Sukurti lentynas",
+    "emptyNoShelves": "Lentynų dar nėra. Sutvarkykite saugyklą prieš priimdami padangas.",
+    "tabDisposed": "Nurašyti"
+  },
+  "checkIn": {
+    "title": "Priimti padangas",
+    "description": "Užfiksuokite, kas atvyko, ir pasirinkite, kur padėti.",
+    "customer": "Klientas",
+    "selectCustomer": "Pasirinkti klientą",
+    "noCustomer": "Be kliento",
+    "vehicle": "Transporto priemonė",
+    "selectVehicle": "Pasirinkti transporto priemonę",
+    "season": "Sezonas",
+    "quantity": "Padangų skaičius",
+    "quantityHint": "Skaičiuojamos po vieną, tad tinka ir pora, ir penki ratai.",
+    "brand": "Markė",
+    "brandPlaceholder": "Michelin, Bridgestone, Nokian",
+    "model": "Modelis",
+    "size": "Matmenys",
+    "sizePlaceholder": "225/45R17",
+    "dotCode": "DOT kodas",
+    "dotPlaceholder": "3623",
+    "dotHint": "Padangos pagaminimo savaitė ir metai.",
+    "withRims": "Ant ratlankių",
+    "hasTpms": "TPMS jutikliai",
+    "studded": "Su dygliais",
+    "location": "Kur bus padėtos padangos",
+    "choosePlace": "Pasirinkite lentyną",
+    "searchLocations": "Ieškoti lentynų",
+    "noLocations": "Lentynų nerasta",
+    "tooFull": "Nepakanka vietos",
+    "pickLocationFirst": "Prieš priimdami komplektą pasirinkite lentyną",
+    "roomSummary": "Tinka {locations} lentynų, laisva {tires} vietų",
+    "nothingFits": "Nė vienoje lentynoje nėra vietos {quantity} padangoms. Atlaisvinkite vietos arba pridėkite lentyną.",
+    "treadTitle": "Protektoriaus gylis priimant",
+    "treadHint": "Neprivaloma, bet vėliau išsprendžia bet kokį ginčą dėl būklės.",
+    "notes": "Pastabos",
+    "submit": "Priimti",
+    "success": "Komplektas {reference} padėtas į {code}",
+    "failed": "Nepavyko priimti komplekto",
+    "noVehiclesForCustomer": "Šis klientas dar neturi transporto priemonių. Pirmiausia pridėkite ją kliento puslapyje.",
+    "treatmentsHint": "Ką reikia padaryti su šiuo komplektu prieš saugojimą ar montavimą.",
+    "storedBefore": "Jau buvo saugoma čia",
+    "storedBeforeHint": "Grįžta tos pačios padangos? Pasirinkite komplektą, kad sezonas prisidėtų prie jo istorijos.",
+    "sameSet": "Tas pats komplektas kaip anksčiau",
+    "pieces": "{count} vnt.",
+    "lastOut": "Atsiimta {date}",
+    "notStored": "Šiuo metu nesaugoma",
+    "returned": "Komplektas {reference} vėl ant {code}",
+    "submitReturn": "Saugoti vėl",
+    "ownerFromVehicle": "Paimta iš transporto priemonės",
+    "vehicleOwner": "Transporto priemonės savininkas",
+    "clearVehicle": "Išvalyti transporto priemonę"
+  },
+  "storage": {
+    "title": "Lentynos",
+    "description": "Sutvarkykite saugyklą kartą ir iškart matykite, kas kur telpa.",
+    "addWarehouse": "Pridėti saugyklą",
+    "addShelves": "Pridėti lentynų",
+    "warehouseName": "Pavadinimas",
+    "warehouseNamePlaceholder": "Pagrindinis pastatas, Konteineris 2, Sandėliukas",
+    "warehouseAddress": "Adresas",
+    "warehouseCreated": "Saugykla pridėta",
+    "warehouseDeleted": "{name} ištrinta",
+    "warehouseArchived": "{name} archyvuota",
+    "deleteWarehouse": "Ištrinti saugyklą",
+    "deleteWarehouseTitle": "Ištrinti {name}?",
+    "deleteWarehouseBody": "Bus pašalinta saugykla ir visos jos lentynos.",
+    "deleteWarehouseBodyOccupied": "Šioje saugykloje tebėra {count} padangų, todėl ji bus archyvuota, o ne ištrinta.",
+    "totalCapacity": "Bendra talpa",
+    "totalStored": "Dabar saugoma",
+    "totalFree": "Laisva vieta",
+    "totalLocations": "Lentynos",
+    "locationSummary": "Naudojama {occupied} iš {total} lentynų",
+    "noLocations": "Lentynų dar nėra. Pridėkite stelažą, kad čia saugotumėte padangas.",
+    "emptyTitle": "Saugykla dar nesukonfigūruota",
+    "emptyBody": "Pridėkite pirmą pastatą ar patalpą ir užpildykite ją lentynomis.",
+    "default": "Numatytoji",
+    "addLocation": "Pridėti lentynų",
+    "editLocation": "Redaguoti lentyną",
+    "createLocation": "Sukurti lentyną",
+    "createShelves": "Sukurti lentynas",
+    "locationDialogDescription": "Pridedama vietos į {warehouse}.",
+    "tabBulk": "Visas stelažas",
+    "tabSingle": "Viena lentyna",
+    "zone": "Zona",
+    "zonePlaceholder": "A, Patalpa 1, Šiaurė",
+    "rack": "Stelažas",
+    "rackPlaceholder": "04",
+    "shelf": "Lentyna",
+    "shelfPlaceholder": "2",
+    "position": "Pozicija",
+    "positionPlaceholder": "Kairė",
+    "shelfFrom": "Lentynos nuo",
+    "shelfTo": "iki",
+    "code": "Kodas",
+    "codePlaceholder": "A-04-2",
+    "codeHint": "Nurodomas etiketėje. Užpildomas automatiškai, jei paliksite tuščią.",
+    "capacity": "Padangų lentynoje",
+    "capacityHint": "Kiek atskirų padangų čia telpa.",
+    "notes": "Pastabos",
+    "bulkPreview": "Sukurs {count} lentynų, nuo {first} iki {last}",
+    "bulkPreviewCapacity": "Iš viso vietos {tires} padangoms",
+    "bulkCreated": "Sukurta {count} lentynų",
+    "bulkCreatedWithSkips": "Sukurta {created} lentynų, praleista {skipped} jau esamų",
+    "bulkNothingCreated": "Tokios lentynos jau yra",
+    "locationCreated": "Lentyna pridėta",
+    "locationUpdated": "Lentyna atnaujinta",
+    "locationDeleted": "{code} ištrinta",
+    "deleteLocationTitle": "Ištrinti {code}?",
+    "deleteLocationBody": "Pirmiausia lentyna turi būti tuščia.",
+    "saveFailed": "Nepavyko išsaugoti",
+    "deleteFailed": "Nepavyko ištrinti"
+  },
+  "movements": {
+    "check_in": "Priimta",
+    "check_out": "Atiduota",
+    "relocate": "Perkelta",
+    "dispose": "Utilizuota",
+    "measure": "Išmatuota"
+  },
+  "detail": {
+    "move": "Perkelti",
+    "checkOut": "Atiduoti",
+    "conditionTitle": "Būklė",
+    "noMeasurements": "Matavimų dar nėra.",
+    "historyTitle": "Istorija",
+    "storageTitle": "Saugojimas",
+    "shelf": "Lentyna",
+    "warehouse": "Saugykla",
+    "notStored": "Šiuo metu nesaugoma.",
+    "checkedIn": "Priimta",
+    "checkedOut": "Atiduota",
+    "tiresTitle": "Padangos",
+    "loadSpeed": "Apkrovos ir greičio indeksai",
+    "ownerTitle": "Savininkas",
+    "phone": "Telefonas",
+    "deleteTitle": "Ištrinti šį padangų komplektą?",
+    "deleteBody": "Komplektas ir visa jo istorija bus pašalinti. Atšaukti negalėsite.",
+    "deleted": "Padangų komplektas ištrintas",
+    "deleteFailed": "Nepavyko ištrinti komplekto",
+    "belowLimit": "{count} padanga (-os) ties keitimo riba arba žemiau.",
+    "belowLimitHint": "Verta pasiūlyti klientui kainą, kol padangos dar čia.",
+    "dispose": "Nurašyti",
+    "disposeTitle": "Nurašyti šį komplektą?",
+    "disposeBody": "Įrašas ir istorija lieka, vieta lentynoje atlaisvinama. Kitą sezoną jis nebebus siūlomas kaip tos pačios padangos.",
+    "disposed": "Komplektas nurašytas",
+    "disposeFailed": "Nepavyko nurašyti komplekto"
+  },
+  "checkOut": {
+    "title": "Atiduoti padangas",
+    "description": "Komplektas nuimamas nuo {code}.",
+    "descriptionNoLocation": "Šis komplektas grąžinamas klientui.",
+    "treadTitle": "Protektoriaus gylis atiduodant",
+    "note": "Pastaba",
+    "submit": "Atiduoti",
+    "success": "Komplektas {reference} atiduotas",
+    "failed": "Nepavyko atiduoti komplekto",
+    "prepPending": "Liko {count} paruošimo darbas (-ai)",
+    "conditionTitle": "Būklė priimant",
+    "conditionLead": {
+      "replace": "Viena ar daugiau padangų yra ties keitimo riba arba žemiau. Verta pasakyti klientui, kol jis neišvažiavo.",
+      "fair": "Viena ar daugiau padangų artėja prie keitimo ribos. Verta paminėti prieš kitą sezoną."
+    },
+    "treadHintUnmeasured": "Priimant matavimas nebuvo atliktas. Tai paskutinė proga jį užfiksuoti."
+  },
+  "relocate": {
+    "title": "Perkelti padangas",
+    "description": "Pasirinkite lentyną, į kurią perkeliama.",
+    "newLocation": "Nauja lentyna",
+    "note": "Pastaba",
+    "submit": "Perkelti",
+    "success": "Komplektas {reference} perkeltas",
+    "failed": "Nepavyko perkelti komplekto"
+  },
+  "edit": {
+    "title": "Redaguoti padangų komplektą",
+    "description": "Pataisykite tai, kas užfiksuota priimant. Lentyną keiskite per „Perkelti“.",
+    "quantityHint": "Padidinimas tikrinamas pagal lentyną, kurioje guli komplektas.",
+    "rimType": "Ratlankio tipas",
+    "rimTypePlaceholder": "Lydinio, plieno",
+    "success": "Padangų komplektas atnaujintas",
+    "failed": "Nepavyko išsaugoti pakeitimų",
+    "treadHint": "Pakeistas matavimas įrašomas į istoriją kaip naujas, o ne perrašo ankstesnį."
+  },
+  "treatments": {
+    "types": {
+      "wash_tires": "Nuplauti padangas",
+      "wash_rims": "Nuplauti ratlankius",
+      "balance": "Balansuoti",
+      "tpms_service": "TPMS aptarnavimas",
+      "new_valves": "Nauji ventiliai",
+      "repair": "Remontas"
+    },
+    "statuses": {
+      "pending": "Reikia atlikti",
+      "done": "Atlikta",
+      "skipped": "Praleista"
+    },
+    "title": "Paruošimas",
+    "none": "Su šiuo komplektu nieko daryti nereikia.",
+    "add": "Pridėti paruošimo darbą",
+    "pickHint": "Pažymėkite, ko šiam komplektui reikia prieš grąžinant ant automobilio.",
+    "pendingCount": "Liko {count}",
+    "allComplete": "Viskas atlikta",
+    "ready": "Paruošta",
+    "markDone": "Atlikta",
+    "completeAll": "Pažymėti visus {count} kaip atliktus",
+    "allDone": "Paruošimas baigtas",
+    "undo": "Grąžinti į sąrašą",
+    "saveFailed": "Nepavyko išsaugoti",
+    "skip": "Praleisti"
+  },
+  "messaging": {
+    "action": "Žinutė",
+    "title": "Parašyti klientui",
+    "description": "Kam: {name}.",
+    "noCustomer": "Šis komplektas neturi kliento.",
+    "noCustomerBody": "Prieš rašydami susiekite komplektą su klientu.",
+    "channel": "Siųsti per",
+    "channels": {
+      "email": "El. paštas",
+      "sms": "SMS",
+      "telegram": "Telegram"
+    },
+    "noAddress": "adreso nėra",
+    "recipient": "Siųsti į",
+    "recipientPlaceholder": "Telefono numeris",
+    "subject": "Tema",
+    "message": "Žinutė",
+    "send": "Siųsti",
+    "sent": "Žinutė išsiųsta",
+    "failed": "Nepavyko išsiųsti žinutės",
+    "smsLong": "Ilgos žinutės gali būti suskaidytos į kelias SMS.",
+    "subjects": {
+      "low_tread": "Jūsų {season} padangos",
+      "damage": "Jūsų {season} padangos",
+      "stored": "Jūsų {season} padangos saugomos",
+      "custom": "Jūsų {season} padangos"
+    },
+    "bodies": {
+      "low_tread": "Sveiki, {customer_name}. Jūsų {season} padangos saugomos pas mus, {shop_name}. Priimant {positions} rodė {tread}, tai žemiau ribos. Dar vieno sezono jos neištvers. Paskaičiuoti naujo komplekto kainą?",
+      "damage": "Sveiki, {customer_name}. Jūsų {season} padangos saugomos pas mus, {shop_name}. Priimdami radome pažeidimą ties {positions}. Paskambinkite, kai bus patogu, viską kartu apžiūrėsime.",
+      "stored": "Sveiki, {customer_name}. Jūsų {season} padangos jau saugomos pas mus, {shop_name}. Pranešime, kai ateis laikas keisti.",
+      "custom": "Sveiki, {customer_name}. Dėl jūsų {season} padangų, saugomų pas mus, {shop_name}:"
+    },
+    "enableInSettings": "Dar nesukonfigūruota. Įjunkite nustatymuose."
+  },
+  "job": {
+    "cardTitle": "Pasiūlymai ir darbai",
+    "none": "Šiam komplektui dar nieko nesukurta.",
+    "quote": "Pasiūlymas",
+    "workOrder": "Darbo užsakymas",
+    "converted": "konvertuota",
+    "quoteTitle": "Pasiūlymas naujam komplektui",
+    "workOrderTitle": "Apmokestinti šį komplektą",
+    "loadingSet": "Kraunamas komplektas",
+    "tires": "Padangos",
+    "inStock": "{count} sandėlyje",
+    "shortStock": "sandėlyje tik {count}",
+    "noMatch": "Sandėlyje nieko nėra pagal {size}. Eilutė bus pridėta su jūsų nurodyta kaina.",
+    "noSize": "Šiam komplektui nenurodytas dydis, todėl sandėlio sugretinti negalima. Eilutė naudos jūsų kainą.",
+    "unitPrice": "Kaina už padangą",
+    "createQuote": "Sukurti pasiūlymą",
+    "createWorkOrder": "Sukurti darbo užsakymą",
+    "quoteCreated": "Sukurtas pasiūlymas {number}",
+    "workOrderCreated": "Sukurtas darbo užsakymas",
+    "failed": "Nepavyko sukurti",
+    "bannerTitle": "Paimti iš",
+    "openSet": "Atverti komplektą",
+    "prepPending": "Prieš montuojant liko: {jobs}",
+    "notOnShelf": "Šis komplektas nebėra saugykloje.",
+    "targetNew": "Naujas darbo užsakymas",
+    "targetNewHint": "Sukurti darbą tik šioms padangoms.",
+    "targetExisting": "Arba pridėti prie jau atviro šios transporto priemonės darbo",
+    "addToWorkOrder": "Pridėti prie darbo užsakymo",
+    "workOrderUpdated": "Pridėta prie darbo užsakymo",
+    "unlink": "Atsieti",
+    "unlinkTitle": "Atsieti šias padangas nuo darbo?",
+    "unlinkBody": "Darbas neberodys į padangų komplektą. Jau įtrauktos dalių ir darbo eilutės lieka, tad pašalinkite jas atskirai, jei jų apmokestinti nereikia.",
+    "unlinked": "Padangos atsietos",
+    "unlinkFailed": "Nepavyko atsieti padangų",
+    "prep": "Paruošimas",
+    "tiresSkipped": "Padangų eilutė nebus pridėta.",
+    "total": "Iš viso",
+    "searchStock": "Ieškoti sandėlyje pagal markę, dydį ar dalies numerį",
+    "noSearchHits": "Sandėlyje nieko neatitinka \"{query}\".",
+    "otherSize": "Kitas dydis",
+    "inSize": "{count} dydžio {size}",
+    "quantity": "Kiekis",
+    "noStockItem": "Nėra sandėlio prekės, kaina įvedama ranka",
+    "storeTires": "Padėti padangas saugoti",
+    "storage": "Saugojimas",
+    "storageFee": "Mokestis",
+    "storagePeriod": "Laikotarpis",
+    "destination": "Kur pateks",
+    "targetInvoice": "Nauja sąskaita",
+    "targetInvoiceHint": "Apmokestinti klientą tiesiogiai, be darbo užsakymo transporto priemonei.",
+    "createInvoice": "Sukurti sąskaitą",
+    "needsCustomer": "Prieš apmokestindami susiekite komplektą su klientu arba transporto priemone.",
+    "storagePeriodEnd": "Atsiimta",
+    "storageOpenEnded": "Atvira",
+    "storagePeriodHint": "Rodoma sąskaitos eilutėje, kad klientas matytų, už ką mokestis. Palikite pabaigą tuščią, kol nežinote, kada atsiims.",
+    "pricesHint": "Paruošimo kainos imamos iš nustatymų. Kiekvieną eilutę galima redaguoti ir sukūrus dokumentą.",
+    "files": "Nuotraukos ir failai",
+    "filesCount": "{count} sąskaitoje"
+  },
+  "label": {
+    "action": "Etiketės",
+    "title": "Spausdinti etiketes",
+    "description": "Lipdukas kiekvienai padangai su QR, atveriančiu šį komplektą.",
+    "format": "Etiketės dydis",
+    "formats": {
+      "dymo_small": "Maža ritė",
+      "dymo_standard": "Standartinė ritė",
+      "thermal_large": "Didelė ritė",
+      "sheet_a4": "A4 lapas"
+    },
+    "perSheet": "{count} lape",
+    "copies": "Kiek",
+    "copiesHint": "po vieną padangai ({count})",
+    "fewer": "Viena mažiau",
+    "more": "Viena daugiau",
+    "print": "Spausdinti",
+    "shelfNote": "Lentyna sąmoningai nespausdinama. Komplektai perkeliami, o atspausdinta lentyna taptų neteisinga. QR visada rodo dabartinę vietą.",
+    "reference": "Nr.",
+    "quantity": "{count, plural, one {# padanga} few {# padangos} many {# padangos} other {# padangų}}",
+    "withRims": "Ant ratlankių",
+    "tpms": "TPMS",
+    "studded": "Su dygliais",
+    "unassigned": "Nepriskirta",
+    "preview": "Peržiūra",
+    "previewScale": "Rodoma spausdinimo dydžiu. Kodą galima nuskaityti ir nuo ekrano.",
+    "previewFailed": "Nepavyko sukurti peržiūros"
+  },
+  "vehicle": {
+    "oneOnShelf": "{code} lentynoje",
+    "inStorage": "padangų viešbutyje"
+  },
+  "wear": {
+    "summary": "Nusidėvėjo {value} nuo {from} iki {to}",
+    "perSeason": "Maždaug {value} per sezoną",
+    "seasonsLeft": "{count, plural, =0 {Jau ties riba} one {Maždaug dar vienas sezonas} other {Maždaug dar # sezonai}}",
+    "since": "-{value}"
+  },
+  "settings": {
+    "change": "Keisti nustatymuose",
+    "prepPrices": "Paruošimo kainos",
+    "storagePrices": "Saugojimo kainos",
+    "treadLimits": "Keisti ribą",
+    "defaultCapacity": "Numatytoji talpa"
+  },
+  "forecast": {
+    "title": "Kitas sezonas",
+    "subtitle": "Ko, pagal matavimus, prireiks saugomiems komplektams, pagal dydį.",
+    "backToList": "Atgal į komplektus",
+    "tires": "Laukiama padangų",
+    "atLimit": "Jau ties riba",
+    "expected": "Laukiama per sezoną",
+    "orderTitle": "Pagal dydį",
+    "sizes": "{count} dydžiai",
+    "empty": "Kol kas nieko nepamatuota arti ribos. Komplektui reikia dviejų vizitų, kad būtų galima apskaičiuoti tempą.",
+    "nowCount": "{count, plural, one {# komplektas ties riba} other {# komplektai ties riba}}",
+    "nextCount": "{count, plural, one {# komplektas per sezoną} other {# komplektai per sezoną}}",
+    "tiresShort": "padangos",
+    "lowest": "mažiausia {value}",
+    "now": "Ties riba",
+    "next": "Per sezoną",
+    "rate": "{value} per sezoną",
+    "caveat": "Komplektai ties riba yra išmatuoti. Likusi dalis apskaičiuota pagal vieno kliento nusidėvėjimą per vieną intervalą: planavimui, ne užsakymui.",
+    "unmeasured": "{count, plural, one {# komplektas turi per mažai istorijos ir neįskaičiuotas} other {# komplektai turi per mažai istorijos ir neįskaičiuoti}}",
+    "capped": "Perskaityti {shown} naujausi komplektai iš {total}."
+  },
+  "invoiceLine": {
+    "storage": "Padangų saugojimas",
+    "pieces": "vnt.",
+    "fromDate": "nuo {date}"
+  },
+  "setNames": {
+    "winter": "žieminės padangos",
+    "summer": "vasarinės padangos",
+    "all_season": "visasezonės padangos",
+    "other": "padangos"
+  },
+  "jobNotes": {
+    "set": "Padangų komplektas {reference}",
+    "shelf": "Lentyna: {shelf}",
+    "notOnShelf": "ne lentynoje",
+    "onRims": "Su ratlankiais",
+    "tiresOnly": "Tik padangos",
+    "tpms": "Su TPMS jutikliais"
+  },
+  "files": {
+    "title": "Nuotraukos ir failai",
+    "add": "Pridėti failų",
+    "none": "Kol kas nieko nepridėta. Nufotografuokite subraižytą ratlankį, kol padangos prieš jus.",
+    "captionPlaceholder": "Kas čia matyti?",
+    "onInvoice": "Sąskaitoje",
+    "invoiceHint": "Sąskaitai pažymėti failai nukopijuojami į darbo užsakymą ar sąskaitą apmokestinant komplektą.",
+    "deleteTitle": "Pašalinti šį failą?",
+    "deleteBody": "{name} pašalinamas iš komplekto. Tai, kas jau nukopijuota į sąskaitą, lieka.",
+    "failed": "Nepavyko išsaugoti",
+    "someFailed": "{count, plural, one {nepavyko įkelti # failo} other {nepavyko įkelti # failų}}"
+  }
+}

+ 27 - 1
messages/nb/audit.json

@@ -109,6 +109,32 @@
     "settings_applyTaxRate": "Brukte avgiftssats",
     "settings_convertToExclusive": "Konverterte priser til eks. avgift",
     "settings_convertToInclusive": "Konverterte priser til inkl. avgift",
-    "organization_sampleDataRemove": "Fjernet eksempeldata"
+    "organization_sampleDataRemove": "Fjernet eksempeldata",
+    "tire_warehouse_create": "Opprettet lagersted for dekk",
+    "tire_warehouse_update": "Oppdaterte lagersted for dekk",
+    "tire_warehouse_delete": "Slettet lagersted for dekk",
+    "tire_warehouse_archive": "Arkiverte lagersted for dekk",
+    "tire_location_create": "Opprettet dekkhylle",
+    "tire_location_bulk_create": "Opprettet dekkhyller",
+    "tire_location_update": "Oppdaterte dekkhylle",
+    "tire_location_delete": "Slettet dekkhylle",
+    "tire_set_check_in": "Tok inn dekksett",
+    "tire_set_check_out": "Leverte ut dekksett",
+    "tire_set_relocate": "Flyttet dekksett",
+    "tire_set_update": "Oppdaterte dekksett",
+    "tire_set_delete": "Slettet dekksett",
+    "tire_measurement_create": "Registrerte dekkmålinger",
+    "tire_treatment_update": "Oppdaterte klargjøring",
+    "tire_treatment_mark": "Merket klargjøring",
+    "tire_treatment_complete_all": "Fullførte klargjøring",
+    "tire_set_message_customer": "Sendte melding til kunde om et dekksett",
+    "tire_set_create_quote": "Opprettet tilbud fra et dekksett",
+    "tire_set_create_work_order": "Opprettet arbeidsordre fra et dekksett",
+    "tire_set_add_to_work_order": "La et dekksett til en arbeidsordre",
+    "tire_set_unlink_work_order": "Koblet et dekksett fra en arbeidsordre",
+    "tire_set_return": "Lagret et sett på nytt",
+    "tire_set_dispose": "Kasserte et dekksett",
+    "tire_set_attach": "La til filer på et dekksett",
+    "tire_set_detach": "Fjernet en fil fra et dekksett"
   }
 }

+ 2 - 1
messages/nb/common.json

@@ -10,7 +10,8 @@
     "back": "Tilbake",
     "confirm": "Bekreft",
     "edit": "Rediger",
-    "add": "Legg til"
+    "add": "Legg til",
+    "gotIt": "Skjønner"
   },
   "contextMenu": {
     "hint": "Tips: høyreklikk på en rad for handlinger",

+ 11 - 1
messages/nb/dashboard.json

@@ -54,7 +54,8 @@
     "activeJobs": "Aktive jobber",
     "recentObservations": "Nylige observasjoner",
     "recentActivity": "Nylig aktivitet",
-    "gettingStarted": "Kom i gang"
+    "gettingStarted": "Kom i gang",
+    "tireHotel": "Dekkhotell"
   },
   "maintenance": {
     "title": "Forventet vedlikehold",
@@ -234,5 +235,14 @@
       "expired": "Utløpt",
       "converted": "Konvertert"
     }
+  },
+  "tireHotel": {
+    "title": "Dekkhotell",
+    "viewAll": "Vis dekkhotell",
+    "stored": "{count} sett lagret",
+    "capacity": "{used} / {capacity} dekk",
+    "roomLeft": "Plass til {count} til",
+    "needsPrep": "Trenger klargjøring",
+    "unbilled": "Ufakturert ({count})"
   }
 }

+ 6 - 0
messages/nb/featureHints.json

@@ -0,0 +1,6 @@
+{
+  "tire-hotel": {
+    "title": "Dekkhotellet er på",
+    "body": "Lagre kundenes sesongdekk. Sett opp hyller, så tar du inn et sett."
+  }
+}

+ 8 - 2
messages/nb/navigation.json

@@ -53,7 +53,12 @@
     "allReminders": "Alle påminnelser",
     "auditLog": "Aktivitetslogg",
     "aiAssistant": "AI-assistent",
-    "observations": "Observasjoner"
+    "observations": "Observasjoner",
+    "tireHotel": "Dekkhotell",
+    "allTireSets": "Alle dekksett",
+    "tireStorage": "Lageroppsett",
+    "tireSetDetails": "Dekksett",
+    "tireForecast": "Neste sesong"
   },
   "search": "Søk...",
   "shortcut": "Ctrl+K",
@@ -107,6 +112,7 @@
     "signOut": "Logg ut",
     "more": "Mer",
     "auditLog": "Aktivitetslogg",
-    "aiAssistant": "AI-assistent"
+    "aiAssistant": "AI-assistent",
+    "tireHotel": "Dekkhotell"
   }
 }

+ 27 - 0
messages/nb/permissions.json

@@ -0,0 +1,27 @@
+{
+  "subjects": {
+    "dashboard": "Dashbord",
+    "vehicles": "Kjøretøy",
+    "customers": "Kunder",
+    "work_orders": "Arbeidsordrer",
+    "quotes": "Tilbud",
+    "services": "Servicehistorikk",
+    "billing": "Fakturering",
+    "inventory": "Lager",
+    "labor_presets": "Arbeidsmaler",
+    "inspections": "Kontroller",
+    "tire_hotel": "Dekkhotell",
+    "reports": "Rapporter",
+    "work_board": "Arbeidstavle",
+    "ai_assistant": "AI-assistent",
+    "settings": "Innstillinger"
+  },
+  "actions": {
+    "create": "Opprette",
+    "read": "Se",
+    "update": "Endre",
+    "delete": "Slette",
+    "manage": "Administrere",
+    "manage_storage": "Endre lageroppsettet"
+  }
+}

+ 9 - 0
messages/nb/search.json

@@ -0,0 +1,9 @@
+{
+  "title": "Søk",
+  "description": "Søk etter kjøretøy, kunder, jobber og innstillinger",
+  "placeholder": "Søk på navn, skilt, telefon, VIN, faktura…",
+  "noResults": "Ingen treff.",
+  "recentCustomers": "Nylige kunder",
+  "services": "Servicehistorikk",
+  "toClose": "for å lukke"
+}

+ 34 - 1
messages/nb/settings.json

@@ -122,6 +122,10 @@
       "support": {
         "title": "Support",
         "description": "Kontakt support og styr supportknappen"
+      },
+      "tireHotel": {
+        "title": "Dekkhotell",
+        "description": "Sesonglagring av dekk"
       }
     },
     "pro": "PRO"
@@ -974,7 +978,9 @@
       "lastBackup": "Siste sikkerhetskopi:",
       "supportPrompt": "Har du spørsmål om hvordan dataene dine beskyttes,",
       "supportAction": "kontakt supportteamet vårt."
-    }
+    },
+    "optTireHotel": "Dekkhotell",
+    "optTireHotelDesc": "Lagrede dekksett, hyller, klargjøring og lagringsavtaler"
   },
   "license": {
     "title": "White-label-lisens",
@@ -1454,5 +1460,32 @@
     "unavailable": "Denne nettleseren har ikke tilbudt installasjon. Åpne nettlesermenyen og se etter «Installer app» eller «Legg til på Hjem-skjerm».",
     "bannerHidden": "Installasjonsbanneret er skjult på denne enheten.",
     "showBanner": "Vis banner igjen"
+  },
+  "tireHotel": {
+    "title": "Dekkhotell",
+    "description": "Lagre kundenes sesongdekk og hold styr på hvor hvert sett står.",
+    "cardTitle": "Dekklagring",
+    "enable": "Aktiver dekkhotell",
+    "enableHint": "Legger dekklagring i menyen. Slår du det av igjen, skjules det uten at noe slettes.",
+    "defaultCapacity": "Standard dekk per hylle",
+    "defaultCapacityHint": "Fylles inn når du legger til nye hyller. Kan endres per hylle.",
+    "warnPercent": "Varsle når fullt (%)",
+    "warnPercentHint": "Markerer lageroversikten når denne andelen av total kapasitet er i bruk.",
+    "treadTitle": "Grenser for bytte",
+    "treadHint": "Lovkrav varierer mellom land, så sett grensene som gjelder i ditt marked. Mønsterdybde under grensen markeres for bytte.",
+    "summerLimit": "Sommergrense ({unit})",
+    "winterLimit": "Vintergrense ({unit})",
+    "unitMm": "mm",
+    "unit32nds": "1/32\"",
+    "setupHint": "Sett så opp hyllene dine.",
+    "setupLink": "Åpne lageroppsett",
+    "save": "Lagre innstillinger for dekkhotell",
+    "saved": "Innstillinger for dekkhotell lagret",
+    "billingTitle": "Lagerfakturering",
+    "billingHint": "Fylles inn på lagringslinjen når du fakturerer et sett. Kan endres på fakturaen.",
+    "seasonalPrice": "Standard lagringspris",
+    "prepTitle": "Priser for klargjøring",
+    "prepHint": "Hva hver type klargjøring koster. La en stå tom, så faktureres den aldri, som passer for verksteder som har vask inkludert i lagerprisen. De med pris legges automatisk på tilbud og arbeidsordre.",
+    "prepNotCharged": "ikke fakturert"
   }
 }

+ 473 - 0
messages/nb/tireHotel.json

@@ -0,0 +1,473 @@
+{
+  "title": "Dekkhotell",
+  "description": "Sesonglagring av dekk for kundene dine",
+  "common": {
+    "cancel": "Avbryt",
+    "save": "Lagre",
+    "create": "Opprett",
+    "delete": "Slett",
+    "edit": "Rediger"
+  },
+  "seasons": {
+    "summer": "Sommer",
+    "winter": "Vinter",
+    "all_season": "Helår",
+    "other": "Annet"
+  },
+  "statuses": {
+    "stored": "På lager",
+    "released": "Utlevert",
+    "disposed": "Kassert"
+  },
+  "conditions": {
+    "good": "God",
+    "fair": "Følg med",
+    "replace": "Bytt"
+  },
+  "positions": {
+    "front_left": "Foran venstre",
+    "front_right": "Foran høyre",
+    "rear_left": "Bak venstre",
+    "rear_right": "Bak høyre",
+    "spare": "Reservehjul",
+    "unspecified": "Uspesifisert"
+  },
+  "capacity": {
+    "used": "{used} av {capacity} dekk",
+    "free": "Plass til {count}",
+    "freeShort": "{count} ledig",
+    "over": "{count} over kapasitet",
+    "meterLabel": "{used} av {capacity} dekk lagret"
+  },
+  "tread": {
+    "placeholderMm": "mm",
+    "placeholder32": "32-deler",
+    "inputLabel": "Mønsterdybde, {position}",
+    "lastTime": "var {value}",
+    "worn": "{value} slitt"
+  },
+  "list": {
+    "tabAll": "Alle",
+    "tabStored": "På lager",
+    "tabReleased": "Utlevert",
+    "search": "Søk på skilt, kunde, merke eller hylle",
+    "checkIn": "Ta inn dekk",
+    "empty": "Ingen dekksett ennå. Ta inn et sett for å komme i gang.",
+    "unassigned": "Uten eier",
+    "reference": "Ref",
+    "vehicle": "Kjøretøy",
+    "tires": "Dekk",
+    "location": "Hylle",
+    "condition": "Tilstand",
+    "status": "Status",
+    "storedSince": "Siden",
+    "tireCount": "{count, plural, one {# dekk} other {# dekk}}",
+    "studded": "Piggdekk",
+    "roomLeft": "Plass til {count}",
+    "prep": "Klargjør",
+    "tabNeedsPrep": "Trenger klargjøring",
+    "setUpShelves": "Sett opp hyller",
+    "emptyNoShelves": "Ingen hyller ennå. Sett opp lageret før du tar inn dekk.",
+    "tabDisposed": "Kassert"
+  },
+  "checkIn": {
+    "title": "Ta inn dekk",
+    "description": "Registrer hva som kom inn, og velg hvor det skal stå.",
+    "customer": "Kunde",
+    "selectCustomer": "Velg kunde",
+    "noCustomer": "Ingen kunde",
+    "vehicle": "Kjøretøy",
+    "selectVehicle": "Velg kjøretøy",
+    "season": "Sesong",
+    "quantity": "Antall dekk",
+    "quantityHint": "Telles enkeltvis, så et par eller fem hjul fungerer også.",
+    "brand": "Merke",
+    "brandPlaceholder": "Michelin, Bridgestone, Nokian",
+    "model": "Modell",
+    "size": "Dimensjon",
+    "sizePlaceholder": "225/45R17",
+    "dotCode": "DOT-kode",
+    "dotPlaceholder": "3623",
+    "dotHint": "Uke og år dekket ble produsert.",
+    "withRims": "På felg",
+    "hasTpms": "TPMS-sensorer",
+    "studded": "Piggdekk",
+    "location": "Hvor dekkene skal stå",
+    "choosePlace": "Velg hylle",
+    "searchLocations": "Søk i hyller",
+    "noLocations": "Fant ingen hyller",
+    "tooFull": "Ikke nok plass",
+    "pickLocationFirst": "Velg en hylle før du tar inn settet",
+    "roomSummary": "{locations} hyller passer dette settet, {tires} dekkplasser ledige",
+    "nothingFits": "Ingen hylle har plass til {quantity} dekk. Frigjør plass eller legg til en hylle.",
+    "treadTitle": "Mønsterdybde ved innlevering",
+    "treadHint": "Valgfritt, men avgjør enhver diskusjon om tilstand senere.",
+    "notes": "Notater",
+    "submit": "Ta inn",
+    "success": "Sett {reference} lagret på {code}",
+    "failed": "Kunne ikke ta inn settet",
+    "noVehiclesForCustomer": "Denne kunden har ingen kjøretøy ennå. Legg til ett på kundesiden først.",
+    "treatmentsHint": "Hva som skal gjøres med dette settet før lagring eller montering.",
+    "storedBefore": "Lagret her før",
+    "storedBeforeHint": "Kommer de samme dekkene tilbake? Velg settet, så føyes sesongen til historikken.",
+    "sameSet": "Samme sett som før",
+    "pieces": "{count} stk",
+    "lastOut": "Hentet {date}",
+    "notStored": "Ikke på lager nå",
+    "returned": "Sett {reference} står på {code} igjen",
+    "submitReturn": "Lagre igjen",
+    "ownerFromVehicle": "Hentet fra kjøretøyet",
+    "vehicleOwner": "Eieren av kjøretøyet",
+    "clearVehicle": "Fjern kjøretøyet"
+  },
+  "storage": {
+    "title": "Hyller",
+    "description": "Sett opp lageret én gang, og se med én gang hva som får plass hvor.",
+    "addWarehouse": "Legg til lagersted",
+    "addShelves": "Legg til hyller",
+    "warehouseName": "Navn",
+    "warehouseNamePlaceholder": "Hovedbygg, Container 2, Bakrom",
+    "warehouseAddress": "Adresse",
+    "warehouseCreated": "Lagersted lagt til",
+    "warehouseDeleted": "Slettet {name}",
+    "warehouseArchived": "Arkiverte {name}",
+    "deleteWarehouse": "Slett lagersted",
+    "deleteWarehouseTitle": "Slette {name}?",
+    "deleteWarehouseBody": "Dette fjerner lagerstedet og alle hyllene der.",
+    "deleteWarehouseBodyOccupied": "Dette lagerstedet har fortsatt {count} dekk, så det blir arkivert i stedet for slettet.",
+    "totalCapacity": "Total kapasitet",
+    "totalStored": "Lagret nå",
+    "totalFree": "Ledig plass",
+    "totalLocations": "Hyller",
+    "locationSummary": "{occupied} av {total} hyller i bruk",
+    "noLocations": "Ingen hyller ennå. Legg til en reol for å lagre dekk her.",
+    "emptyTitle": "Ingen lagring satt opp ennå",
+    "emptyBody": "Legg til første bygg eller rom, og fyll det med hyller.",
+    "default": "Standard",
+    "addLocation": "Legg til hyller",
+    "editLocation": "Rediger hylle",
+    "createLocation": "Opprett hylle",
+    "createShelves": "Opprett hyller",
+    "locationDialogDescription": "Legger til lagring i {warehouse}.",
+    "tabBulk": "En reol med hyller",
+    "tabSingle": "Én hylle",
+    "zone": "Sone",
+    "zonePlaceholder": "A, Rom 1, Nord",
+    "rack": "Reol",
+    "rackPlaceholder": "04",
+    "shelf": "Hylle",
+    "shelfPlaceholder": "2",
+    "position": "Plass",
+    "positionPlaceholder": "Venstre",
+    "shelfFrom": "Hyller fra",
+    "shelfTo": "til",
+    "code": "Kode",
+    "codePlaceholder": "A-04-2",
+    "codeHint": "Står på etiketten. Fylles ut for deg hvis du lar den stå tom.",
+    "capacity": "Dekk per hylle",
+    "capacityHint": "Hvor mange enkeltdekk det er plass til her.",
+    "notes": "Notater",
+    "bulkPreview": "Oppretter {count} hyller, {first} til {last}",
+    "bulkPreviewCapacity": "Plass til {tires} dekk totalt",
+    "bulkCreated": "Opprettet {count} hyller",
+    "bulkCreatedWithSkips": "Opprettet {created} hyller, hoppet over {skipped} som allerede fantes",
+    "bulkNothingCreated": "Disse hyllene finnes allerede",
+    "locationCreated": "Hylle lagt til",
+    "locationUpdated": "Hylle oppdatert",
+    "locationDeleted": "Slettet {code}",
+    "deleteLocationTitle": "Slette {code}?",
+    "deleteLocationBody": "Hyllen må være tom først.",
+    "saveFailed": "Kunne ikke lagre",
+    "deleteFailed": "Kunne ikke slette"
+  },
+  "movements": {
+    "check_in": "Tatt inn",
+    "check_out": "Levert ut",
+    "relocate": "Flyttet",
+    "dispose": "Kassert",
+    "measure": "Målt"
+  },
+  "detail": {
+    "move": "Flytt",
+    "checkOut": "Lever ut",
+    "conditionTitle": "Tilstand",
+    "noMeasurements": "Ingen målinger registrert ennå.",
+    "historyTitle": "Historikk",
+    "storageTitle": "Lagring",
+    "shelf": "Hylle",
+    "warehouse": "Lagersted",
+    "notStored": "Ikke på lager nå.",
+    "checkedIn": "Tatt inn",
+    "checkedOut": "Levert ut",
+    "tiresTitle": "Dekk",
+    "loadSpeed": "Last og hastighet",
+    "ownerTitle": "Eier",
+    "phone": "Telefon",
+    "deleteTitle": "Slette dette dekksettet?",
+    "deleteBody": "Settet og hele historikken fjernes. Dette kan ikke angres.",
+    "deleted": "Dekksett slettet",
+    "deleteFailed": "Kunne ikke slette settet",
+    "belowLimit": "{count} dekk på eller under byttegrensen.",
+    "belowLimitHint": "Verdt å tilby kunden pris mens dekkene fortsatt står her.",
+    "dispose": "Kasser",
+    "disposeTitle": "Kassere dette settet?",
+    "disposeBody": "Oppføringen og historikken blir stående, og hyllen frigjøres. Det foreslås ikke som samme dekk neste sesong.",
+    "disposed": "Settet er kassert",
+    "disposeFailed": "Kunne ikke kassere settet"
+  },
+  "checkOut": {
+    "title": "Lever ut dekk",
+    "description": "Tar settet av {code}.",
+    "descriptionNoLocation": "Leverer dette settet tilbake til kunden.",
+    "treadTitle": "Mønsterdybde ved utlevering",
+    "note": "Notat",
+    "submit": "Lever ut",
+    "success": "Sett {reference} levert ut",
+    "failed": "Kunne ikke levere ut settet",
+    "prepPending": "{count} klargjøringsjobb(er) gjenstår",
+    "conditionTitle": "Tilstand ved innlevering",
+    "conditionLead": {
+      "replace": "Ett eller flere dekk er på eller under bytte­grensen. Verdt å si fra til kunden før de kjører.",
+      "fair": "Ett eller flere dekk nærmer seg byttegrensen. Verdt en kommentar før neste sesong."
+    },
+    "treadHintUnmeasured": "Det ble ikke tatt måling da disse kom inn. Dette er siste sjanse til å registrere en."
+  },
+  "relocate": {
+    "title": "Flytt dekk",
+    "description": "Velg hyllen settet skal flyttes til.",
+    "newLocation": "Ny hylle",
+    "note": "Notat",
+    "submit": "Flytt",
+    "success": "Sett {reference} flyttet",
+    "failed": "Kunne ikke flytte settet"
+  },
+  "edit": {
+    "title": "Rediger dekksett",
+    "description": "Rett opp det som ble registrert ved innlevering. Bruk Flytt for å endre hylle.",
+    "quantityHint": "Økning kontrolleres mot hyllen settet står på.",
+    "rimType": "Felgtype",
+    "rimTypePlaceholder": "Alu, stål",
+    "success": "Dekksett oppdatert",
+    "failed": "Kunne ikke lagre endringene",
+    "treadHint": "Endrer du en måling, registreres den som en ny måling i historikken i stedet for å overskrive den gamle."
+  },
+  "treatments": {
+    "types": {
+      "wash_tires": "Vask dekk",
+      "wash_rims": "Vask felger",
+      "balance": "Balanser",
+      "tpms_service": "TPMS-service",
+      "new_valves": "Nye ventiler",
+      "repair": "Reparasjon"
+    },
+    "statuses": {
+      "pending": "Skal gjøres",
+      "done": "Ferdig",
+      "skipped": "Hoppet over"
+    },
+    "title": "Klargjøring",
+    "none": "Ingenting å gjøre på dette settet.",
+    "add": "Legg til klargjøring",
+    "pickHint": "Kryss av for hva dette settet trenger før det skal på bilen igjen.",
+    "pendingCount": "{count} igjen",
+    "allComplete": "Alt ferdig",
+    "ready": "Klar",
+    "markDone": "Ferdig",
+    "completeAll": "Merk alle {count} som ferdig",
+    "allDone": "All klargjøring ferdig",
+    "undo": "Legg tilbake på lista",
+    "saveFailed": "Kunne ikke lagre",
+    "skip": "Hopp over"
+  },
+  "messaging": {
+    "action": "Send melding",
+    "title": "Send melding til kunden",
+    "description": "Til {name}.",
+    "noCustomer": "Ingen kunde på dette settet.",
+    "noCustomerBody": "Knytt en kunde til settet før du sender melding.",
+    "channel": "Send via",
+    "channels": {
+      "email": "E-post",
+      "sms": "SMS",
+      "telegram": "Telegram"
+    },
+    "noAddress": "ingen adresse registrert",
+    "recipient": "Send til",
+    "recipientPlaceholder": "Telefonnummer",
+    "subject": "Emne",
+    "message": "Melding",
+    "send": "Send",
+    "sent": "Melding sendt",
+    "failed": "Kunne ikke sende meldingen",
+    "smsLong": "Lange meldinger kan bli delt i flere SMS.",
+    "subjects": {
+      "low_tread": "Dekkene dine ({season})",
+      "damage": "Dekkene dine ({season})",
+      "stored": "{season}dekkene dine er lagret",
+      "custom": "Dekkene dine ({season})"
+    },
+    "bodies": {
+      "low_tread": "Hei {customer_name}, {season}dekkene dine står lagret hos oss på {shop_name}. Da vi tok dem inn målte {positions} {tread}, som er under grensen. De klarer ikke en sesong til. Vil du ha pris på et nytt sett?",
+      "damage": "Hei {customer_name}, {season}dekkene dine står lagret hos oss på {shop_name}. Vi fant skade på {positions} da vi tok dem inn. Ring oss når du har anledning, så går vi gjennom det sammen.",
+      "stored": "Hei {customer_name}, {season}dekkene dine står nå lagret hos oss på {shop_name}. Vi sier fra når det er tid for å bytte tilbake.",
+      "custom": "Hei {customer_name}, angående {season}dekkene dine som står lagret hos oss på {shop_name}:"
+    },
+    "enableInSettings": "Ikke satt opp ennå. Slå det på i Innstillinger."
+  },
+  "job": {
+    "cardTitle": "Tilbud og jobber",
+    "none": "Ingenting opprettet for dette settet ennå.",
+    "quote": "Tilbud",
+    "workOrder": "Arbeidsordre",
+    "converted": "konvertert",
+    "quoteTitle": "Gi pris på nytt sett",
+    "workOrderTitle": "Fakturer dette settet",
+    "loadingSet": "Henter settet",
+    "tires": "Dekk",
+    "inStock": "{count} på lager",
+    "shortStock": "bare {count} på lager",
+    "noMatch": "Ingenting på lager passer {size}. Linjen legges inn med prisen du setter.",
+    "noSize": "Dette settet mangler dimensjon, så lageret kan ikke matches. Linjen legges inn med prisen du setter.",
+    "unitPrice": "Pris per dekk",
+    "createQuote": "Opprett tilbud",
+    "createWorkOrder": "Opprett arbeidsordre",
+    "quoteCreated": "Tilbud {number} opprettet",
+    "workOrderCreated": "Arbeidsordre opprettet",
+    "failed": "Kunne ikke opprette",
+    "bannerTitle": "Hentes fra",
+    "openSet": "Åpne dekksett",
+    "prepPending": "Gjenstår før montering: {jobs}",
+    "notOnShelf": "Dette settet står ikke lenger på lager.",
+    "targetNew": "Ny arbeidsordre",
+    "targetNewHint": "Opprett en jobb bare for disse dekkene.",
+    "targetExisting": "Eller legg til på en jobb som allerede er åpen for dette kjøretøyet",
+    "addToWorkOrder": "Legg til på arbeidsordre",
+    "workOrderUpdated": "Lagt til på arbeidsordren",
+    "unlink": "Koble fra",
+    "unlinkTitle": "Koble disse dekkene fra jobben?",
+    "unlinkBody": "Jobben peker ikke lenger på dekksettet. Deler og arbeidslinjer som allerede ligger der blir stående, så fjern dem separat hvis de ikke skal faktureres.",
+    "unlinked": "Dekkene koblet fra",
+    "unlinkFailed": "Kunne ikke koble fra dekkene",
+    "prep": "Klargjøring",
+    "tiresSkipped": "Ingen dekklinje legges til.",
+    "total": "Totalt",
+    "searchStock": "Søk i lageret på merke, dimensjon eller varenummer",
+    "noSearchHits": "Ingenting på lageret passer \"{query}\".",
+    "otherSize": "Annen dimensjon",
+    "inSize": "{count} i {size}",
+    "quantity": "Ant.",
+    "noStockItem": "Ingen lagervare, pris settes manuelt",
+    "storeTires": "Legg dekk på hotell",
+    "storage": "Lagring",
+    "storageFee": "Gebyr",
+    "storagePeriod": "Periode",
+    "destination": "Hvor det havner",
+    "targetInvoice": "Ny faktura",
+    "targetInvoiceHint": "Fakturer kunden direkte, uten jobb på et kjøretøy.",
+    "createInvoice": "Opprett faktura",
+    "needsCustomer": "Koble en kunde eller et kjøretøy til settet før du fakturerer.",
+    "storagePeriodEnd": "Hentet",
+    "storageOpenEnded": "Åpen",
+    "storagePeriodHint": "Står på fakturalinjen så kunden ser hva gebyret dekker. La sluttdatoen stå tom til du vet når de henter.",
+    "pricesHint": "Prisene for klargjøring hentes fra innstillingene. Alle linjer kan endres etter at dette er opprettet.",
+    "files": "Bilder og filer",
+    "filesCount": "{count} på fakturaen"
+  },
+  "label": {
+    "action": "Etiketter",
+    "title": "Skriv ut dekketiketter",
+    "description": "Klistremerker til hvert dekk, med QR som åpner dette settet.",
+    "format": "Etikettstørrelse",
+    "formats": {
+      "dymo_small": "Liten rull",
+      "dymo_standard": "Standard rull",
+      "thermal_large": "Stor rull",
+      "sheet_a4": "A4-ark"
+    },
+    "perSheet": "{count} per ark",
+    "copies": "Hvor mange",
+    "copiesHint": "ett per dekk ({count})",
+    "fewer": "Én færre",
+    "more": "Én til",
+    "print": "Skriv ut",
+    "shelfNote": "Hyllen står med vilje ikke på etiketten. Sett blir flyttet, og en trykt hylle ville da vært feil. QR-koden viser alltid hvor settet står nå.",
+    "reference": "Ref",
+    "quantity": "{count, plural, one {# dekk} other {# dekk}}",
+    "withRims": "På felg",
+    "tpms": "TPMS",
+    "studded": "Pigg",
+    "unassigned": "Uten eier",
+    "preview": "Forhåndsvisning",
+    "previewScale": "Vist i utskriftsstørrelse. Koden kan skannes rett fra skjermen.",
+    "previewFailed": "Kunne ikke lage forhåndsvisning"
+  },
+  "vehicle": {
+    "oneOnShelf": "på {code}",
+    "inStorage": "på dekkhotell"
+  },
+  "wear": {
+    "summary": "Slitt {value} mellom {from} og {to}",
+    "perSeason": "Rundt {value} per sesong",
+    "seasonsLeft": "{count, plural, =0 {På grensen nå} one {Omtrent én sesong igjen} other {Omtrent # sesonger igjen}}",
+    "since": "-{value}"
+  },
+  "settings": {
+    "change": "Endre i innstillinger",
+    "prepPrices": "Priser for klargjøring",
+    "storagePrices": "Lagringspriser i innstillinger",
+    "treadLimits": "Endre grensen",
+    "defaultCapacity": "Standard kapasitet"
+  },
+  "forecast": {
+    "title": "Neste sesong",
+    "subtitle": "Hva de lagrede settene er målt til å trenge, per dimensjon.",
+    "backToList": "Tilbake til sett",
+    "tires": "Forventede dekk",
+    "atLimit": "Allerede på grensen",
+    "expected": "Ventet innen en sesong",
+    "orderTitle": "Per dimensjon",
+    "sizes": "{count} dimensjoner",
+    "empty": "Ingenting er målt nær grensen ennå. Sett trenger to besøk før en slitasjerate kan regnes ut.",
+    "nowCount": "{count, plural, one {# sett på grensen} other {# sett på grensen}}",
+    "nextCount": "{count, plural, one {# sett innen en sesong} other {# sett innen en sesong}}",
+    "tiresShort": "dekk",
+    "lowest": "laveste {value}",
+    "now": "På grensen",
+    "next": "Innen en sesong",
+    "rate": "{value} per sesong",
+    "caveat": "Sett som allerede er på grensen, er målt. Resten er beregnet ut fra én kundes slitasje over ett intervall, så bruk det til å planlegge, ikke til å bestille direkte.",
+    "unmeasured": "{count, plural, one {# sett har for lite historikk til å vurderes og telles ikke med} other {# sett har for lite historikk til å vurderes og telles ikke med}}",
+    "capped": "Leste de {shown} nyeste settene av {total}."
+  },
+  "invoiceLine": {
+    "storage": "Dekklagring",
+    "pieces": "stk",
+    "fromDate": "fra {date}"
+  },
+  "setNames": {
+    "winter": "vinterdekk",
+    "summer": "sommerdekk",
+    "all_season": "helårsdekk",
+    "other": "dekk"
+  },
+  "jobNotes": {
+    "set": "Dekksett {reference}",
+    "shelf": "Hylle: {shelf}",
+    "notOnShelf": "ikke på hylle",
+    "onRims": "På felg",
+    "tiresOnly": "Kun dekk",
+    "tpms": "Har TPMS-sensorer"
+  },
+  "files": {
+    "title": "Bilder og filer",
+    "add": "Legg til filer",
+    "none": "Ingenting lagt ved ennå. Ta bilde av en skadet felg mens dekkene står foran deg.",
+    "captionPlaceholder": "Hva viser dette?",
+    "onInvoice": "På fakturaen",
+    "invoiceHint": "Filer merket for faktura kopieres til arbeidsordren eller fakturaen når settet faktureres.",
+    "deleteTitle": "Fjerne denne filen?",
+    "deleteBody": "{name} fjernes fra settet. Det som allerede er kopiert til en faktura, blir stående.",
+    "failed": "Kunne ikke lagre",
+    "someFailed": "{count, plural, one {# fil kunne ikke lastes opp} other {# filer kunne ikke lastes opp}}"
+  }
+}

+ 27 - 1
messages/nl/audit.json

@@ -109,6 +109,32 @@
     "settings_applyTaxRate": "Btw-tarief toegepast",
     "settings_convertToExclusive": "Prijzen omgezet naar exclusief btw",
     "settings_convertToInclusive": "Prijzen omgezet naar inclusief btw",
-    "organization_sampleDataRemove": "Voorbeeldgegevens verwijderd"
+    "organization_sampleDataRemove": "Voorbeeldgegevens verwijderd",
+    "tire_warehouse_create": "Bandenopslaglocatie aangemaakt",
+    "tire_warehouse_update": "Bandenopslaglocatie bijgewerkt",
+    "tire_warehouse_delete": "Bandenopslaglocatie verwijderd",
+    "tire_warehouse_archive": "Bandenopslaglocatie gearchiveerd",
+    "tire_location_create": "Bandenstelling aangemaakt",
+    "tire_location_bulk_create": "Bandenstellingen aangemaakt",
+    "tire_location_update": "Bandenstelling bijgewerkt",
+    "tire_location_delete": "Bandenstelling verwijderd",
+    "tire_set_check_in": "Bandenset ingenomen",
+    "tire_set_check_out": "Bandenset teruggegeven",
+    "tire_set_relocate": "Bandenset verplaatst",
+    "tire_set_update": "Bandenset bijgewerkt",
+    "tire_set_delete": "Bandenset verwijderd",
+    "tire_measurement_create": "Bandenmetingen vastgelegd",
+    "tire_treatment_update": "Voorbereiding bijgewerkt",
+    "tire_treatment_mark": "Voorbereiding gemarkeerd",
+    "tire_treatment_complete_all": "Voorbereiding afgerond",
+    "tire_set_message_customer": "Klant bericht gestuurd over een bandenset",
+    "tire_set_create_quote": "Offerte aangemaakt vanuit een bandenset",
+    "tire_set_create_work_order": "Werkorder aangemaakt vanuit een bandenset",
+    "tire_set_add_to_work_order": "Bandenset aan een werkorder toegevoegd",
+    "tire_set_unlink_work_order": "Bandenset van een werkorder ontkoppeld",
+    "tire_set_return": "Set opnieuw opgeslagen",
+    "tire_set_dispose": "Bandenset afgeschreven",
+    "tire_set_attach": "Bestanden aan een bandenset toegevoegd",
+    "tire_set_detach": "Bestand van een bandenset verwijderd"
   }
 }

+ 2 - 1
messages/nl/common.json

@@ -10,7 +10,8 @@
     "confirm": "Bevestigen",
     "saveChanges": "Wijzigingen opslaan",
     "edit": "Bewerken",
-    "add": "Toevoegen"
+    "add": "Toevoegen",
+    "gotIt": "Duidelijk"
   },
   "contextMenu": {
     "hint": "Tip: rechtsklik op een rij voor acties",

+ 11 - 1
messages/nl/dashboard.json

@@ -54,7 +54,8 @@
     "activeJobs": "Actieve opdrachten",
     "recentObservations": "Recente waarnemingen",
     "recentActivity": "Recente activiteit",
-    "gettingStarted": "Aan de slag"
+    "gettingStarted": "Aan de slag",
+    "tireHotel": "Bandenhotel"
   },
   "maintenance": {
     "title": "Verwacht onderhoud gepland",
@@ -234,5 +235,14 @@
       "expired": "Verlopen",
       "converted": "Omgezet"
     }
+  },
+  "tireHotel": {
+    "title": "Bandenhotel",
+    "viewAll": "Bandenhotel bekijken",
+    "stored": "{count} sets opgeslagen",
+    "capacity": "{used} / {capacity} banden",
+    "roomLeft": "Ruimte voor {count} meer",
+    "needsPrep": "Voorbereiding nodig",
+    "unbilled": "Niet gefactureerd ({count})"
   }
 }

+ 6 - 0
messages/nl/featureHints.json

@@ -0,0 +1,6 @@
+{
+  "tire-hotel": {
+    "title": "Bandenhotel staat aan",
+    "body": "Bewaar seizoensbanden van klanten. Zet stellingen op en neem een set in."
+  }
+}

+ 8 - 2
messages/nl/navigation.json

@@ -53,7 +53,12 @@
     "allReminders": "Alle herinneringen",
     "auditLog": "Activiteitenlog",
     "aiAssistant": "AI-assistent",
-    "observations": "Waarnemingen"
+    "observations": "Waarnemingen",
+    "tireHotel": "Bandenhotel",
+    "allTireSets": "Alle bandensets",
+    "tireStorage": "Opslagindeling",
+    "tireSetDetails": "Bandenset",
+    "tireForecast": "Volgend seizoen"
   },
   "search": "Zoeken...",
   "shortcut": "Ctrl+K",
@@ -107,6 +112,7 @@
     "signOut": "Uitloggen",
     "more": "Meer",
     "auditLog": "Activiteitenlog",
-    "aiAssistant": "AI-assistent"
+    "aiAssistant": "AI-assistent",
+    "tireHotel": "Bandenhotel"
   }
 }

+ 27 - 0
messages/nl/permissions.json

@@ -0,0 +1,27 @@
+{
+  "subjects": {
+    "dashboard": "Dashboard",
+    "vehicles": "Voertuigen",
+    "customers": "Klanten",
+    "work_orders": "Werkorders",
+    "quotes": "Offertes",
+    "services": "Serviceregistraties",
+    "billing": "Facturatie",
+    "inventory": "Voorraad",
+    "labor_presets": "Werksjablonen",
+    "inspections": "Inspecties",
+    "tire_hotel": "Bandenhotel",
+    "reports": "Rapporten",
+    "work_board": "Werkbord",
+    "ai_assistant": "AI-assistent",
+    "settings": "Instellingen"
+  },
+  "actions": {
+    "create": "Aanmaken",
+    "read": "Bekijken",
+    "update": "Bewerken",
+    "delete": "Verwijderen",
+    "manage": "Beheren",
+    "manage_storage": "Opslagindeling beheren"
+  }
+}

+ 9 - 0
messages/nl/search.json

@@ -0,0 +1,9 @@
+{
+  "title": "Zoeken",
+  "description": "Zoek naar voertuigen, klanten, werkorders en instellingen",
+  "placeholder": "Zoek op naam, kenteken, telefoon, VIN, factuur…",
+  "noResults": "Geen resultaten.",
+  "recentCustomers": "Recente klanten",
+  "services": "Serviceregistraties",
+  "toClose": "om te sluiten"
+}

+ 34 - 1
messages/nl/settings.json

@@ -122,6 +122,10 @@
       "support": {
         "title": "Support",
         "description": "Contact opnemen met support en de knop beheren"
+      },
+      "tireHotel": {
+        "title": "Bandenhotel",
+        "description": "Seizoensopslag van banden"
       }
     },
     "pro": "PRO"
@@ -974,7 +978,9 @@
       "lastBackup": "Laatste back-up:",
       "supportPrompt": "Heeft u vragen over hoe uw gegevens worden beschermd,",
       "supportAction": "neem contact op met ons supportteam."
-    }
+    },
+    "optTireHotel": "Bandenhotel",
+    "optTireHotelDesc": "Opgeslagen bandensets, stellingen, voorbereiding en opslagovereenkomsten"
   },
   "license": {
     "title": "White-label licentie",
@@ -1454,5 +1460,32 @@
     "unavailable": "Deze browser heeft geen installatie aangeboden. Open het browsermenu en zoek naar 'App installeren' of 'Zet op beginscherm'.",
     "bannerHidden": "De installatiebanner is verborgen op dit apparaat.",
     "showBanner": "Banner weer tonen"
+  },
+  "tireHotel": {
+    "title": "Bandenhotel",
+    "description": "Sla de seizoensbanden van uw klanten op en houd bij waar elke set staat.",
+    "cardTitle": "Bandenopslag",
+    "enable": "Bandenhotel inschakelen",
+    "enableHint": "Voegt bandenopslag toe aan de navigatie. Later uitzetten verbergt het weer zonder iets te verwijderen.",
+    "defaultCapacity": "Standaard banden per plank",
+    "defaultCapacityHint": "Vooraf ingevuld bij nieuwe planken. Per plank aanpasbaar.",
+    "warnPercent": "Waarschuwen bij vol (%)",
+    "warnPercentHint": "Markeert het opslagoverzicht zodra dit deel van de totale capaciteit in gebruik is.",
+    "treadTitle": "Vervangingsgrenzen",
+    "treadHint": "Wettelijke minima verschillen per land, stel dus de grenzen van uw markt in. Profiel onder de grens wordt gemarkeerd voor vervanging.",
+    "summerLimit": "Zomergrens ({unit})",
+    "winterLimit": "Wintergrens ({unit})",
+    "unitMm": "mm",
+    "unit32nds": "1/32\"",
+    "setupHint": "Stel daarna uw stellingen in.",
+    "setupLink": "Opslagindeling openen",
+    "save": "Bandenhotelinstellingen opslaan",
+    "saved": "Bandenhotelinstellingen opgeslagen",
+    "billingTitle": "Opslagfacturatie",
+    "billingHint": "Wordt ingevuld op de opslagregel bij het factureren van een set. Blijft aanpasbaar.",
+    "seasonalPrice": "Standaard opslagprijs",
+    "prepTitle": "Prijzen voorbereiding",
+    "prepHint": "Wat elk soort voorbereiding kost. Laat er een leeg en die wordt nooit gefactureerd, wat past bij een bedrijf dat wassen in de opslagprijs meeneemt. Beprijsde komen automatisch op offerte en werkorder.",
+    "prepNotCharged": "niet in rekening"
   }
 }

+ 473 - 0
messages/nl/tireHotel.json

@@ -0,0 +1,473 @@
+{
+  "title": "Bandenhotel",
+  "description": "Seizoensopslag van banden voor uw klanten",
+  "common": {
+    "cancel": "Annuleren",
+    "save": "Opslaan",
+    "create": "Aanmaken",
+    "delete": "Verwijderen",
+    "edit": "Bewerken"
+  },
+  "seasons": {
+    "summer": "Zomer",
+    "winter": "Winter",
+    "all_season": "Vierseizoenen",
+    "other": "Overig"
+  },
+  "statuses": {
+    "stored": "In opslag",
+    "released": "Teruggegeven",
+    "disposed": "Afgevoerd"
+  },
+  "conditions": {
+    "good": "Goed",
+    "fair": "Let op",
+    "replace": "Vervangen"
+  },
+  "positions": {
+    "front_left": "Linksvoor",
+    "front_right": "Rechtsvoor",
+    "rear_left": "Linksachter",
+    "rear_right": "Rechtsachter",
+    "spare": "Reservewiel",
+    "unspecified": "Niet opgegeven"
+  },
+  "capacity": {
+    "used": "{used} van {capacity} banden",
+    "free": "Ruimte voor {count}",
+    "freeShort": "{count} vrij",
+    "over": "{count} boven capaciteit",
+    "meterLabel": "{used} van {capacity} banden opgeslagen"
+  },
+  "tread": {
+    "placeholderMm": "mm",
+    "placeholder32": "32sten",
+    "inputLabel": "Profieldiepte, {position}",
+    "lastTime": "was {value}",
+    "worn": "{value} versleten"
+  },
+  "list": {
+    "tabAll": "Alle",
+    "tabStored": "In opslag",
+    "tabReleased": "Teruggegeven",
+    "search": "Zoek op kenteken, klant, merk of stelling",
+    "checkIn": "Banden innemen",
+    "empty": "Nog geen bandensets. Neem een set in om te beginnen.",
+    "unassigned": "Niet toegewezen",
+    "reference": "Ref",
+    "vehicle": "Voertuig",
+    "tires": "Banden",
+    "location": "Stelling",
+    "condition": "Staat",
+    "status": "Status",
+    "storedSince": "Sinds",
+    "tireCount": "{count, plural, one {# band} other {# banden}}",
+    "studded": "Spijkerbanden",
+    "roomLeft": "Ruimte voor {count}",
+    "prep": "Voorb.",
+    "tabNeedsPrep": "Voorbereiding nodig",
+    "setUpShelves": "Stellingen instellen",
+    "emptyNoShelves": "Nog geen stellingen. Richt uw opslag in voordat u banden inneemt.",
+    "tabDisposed": "Afgeschreven"
+  },
+  "checkIn": {
+    "title": "Banden innemen",
+    "description": "Leg vast wat er binnenkwam en kies waar het komt te staan.",
+    "customer": "Klant",
+    "selectCustomer": "Klant kiezen",
+    "noCustomer": "Geen klant",
+    "vehicle": "Voertuig",
+    "selectVehicle": "Voertuig kiezen",
+    "season": "Seizoen",
+    "quantity": "Aantal banden",
+    "quantityHint": "Stuk voor stuk geteld, dus een paar of vijf wielen kan ook.",
+    "brand": "Merk",
+    "brandPlaceholder": "Michelin, Bridgestone, Vredestein",
+    "model": "Model",
+    "size": "Maat",
+    "sizePlaceholder": "225/45R17",
+    "dotCode": "DOT-code",
+    "dotPlaceholder": "3623",
+    "dotHint": "Week en jaar waarin de band is gemaakt.",
+    "withRims": "Op velgen",
+    "hasTpms": "TPMS-sensoren",
+    "studded": "Spijkerbanden",
+    "location": "Waar de banden komen",
+    "choosePlace": "Kies een stelling",
+    "searchLocations": "Zoek stellingen",
+    "noLocations": "Geen stellingen gevonden",
+    "tooFull": "Niet genoeg ruimte",
+    "pickLocationFirst": "Kies een stelling voordat u de set inneemt",
+    "roomSummary": "{locations} stellingen passen bij deze set, {tires} bandenplaatsen vrij",
+    "nothingFits": "Geen stelling heeft ruimte voor {quantity} banden. Maak plaats of voeg een stelling toe.",
+    "treadTitle": "Profieldiepte bij binnenkomst",
+    "treadHint": "Optioneel, maar beslecht later elke discussie over de staat.",
+    "notes": "Notities",
+    "submit": "Innemen",
+    "success": "Set {reference} opgeslagen op {code}",
+    "failed": "Innemen mislukt",
+    "noVehiclesForCustomer": "Deze klant heeft nog geen voertuigen. Voeg er eerst een toe op de klantpagina.",
+    "treatmentsHint": "Wat er met deze set moet gebeuren voor opslag of montage.",
+    "storedBefore": "Eerder hier opgeslagen",
+    "storedBeforeHint": "Komen dezelfde banden terug? Kies de set, dan hoort dit seizoen bij de historie.",
+    "sameSet": "Dezelfde set als eerder",
+    "pieces": "{count} st.",
+    "lastOut": "Opgehaald {date}",
+    "notStored": "Nu niet opgeslagen",
+    "returned": "Set {reference} ligt weer op {code}",
+    "submitReturn": "Opnieuw opslaan",
+    "ownerFromVehicle": "Overgenomen van het voertuig",
+    "vehicleOwner": "De eigenaar van het voertuig",
+    "clearVehicle": "Voertuig wissen"
+  },
+  "storage": {
+    "title": "Stellingen",
+    "description": "Richt uw opslag één keer in en zie meteen wat waar past.",
+    "addWarehouse": "Opslaglocatie toevoegen",
+    "addShelves": "Stellingen toevoegen",
+    "warehouseName": "Naam",
+    "warehouseNamePlaceholder": "Hoofdgebouw, Container 2, Achterruimte",
+    "warehouseAddress": "Adres",
+    "warehouseCreated": "Opslaglocatie toegevoegd",
+    "warehouseDeleted": "{name} verwijderd",
+    "warehouseArchived": "{name} gearchiveerd",
+    "deleteWarehouse": "Opslaglocatie verwijderen",
+    "deleteWarehouseTitle": "{name} verwijderen?",
+    "deleteWarehouseBody": "Dit verwijdert de locatie en al haar stellingen.",
+    "deleteWarehouseBodyOccupied": "Deze locatie bevat nog {count} banden en wordt daarom gearchiveerd in plaats van verwijderd.",
+    "totalCapacity": "Totale capaciteit",
+    "totalStored": "Nu opgeslagen",
+    "totalFree": "Vrije ruimte",
+    "totalLocations": "Stellingen",
+    "locationSummary": "{occupied} van {total} stellingen in gebruik",
+    "noLocations": "Nog geen stellingen. Voeg een rek toe om hier banden op te slaan.",
+    "emptyTitle": "Nog geen opslag ingericht",
+    "emptyBody": "Voeg uw eerste gebouw of ruimte toe en vul het met stellingen.",
+    "default": "Standaard",
+    "addLocation": "Stellingen toevoegen",
+    "editLocation": "Stelling bewerken",
+    "createLocation": "Stelling aanmaken",
+    "createShelves": "Stellingen aanmaken",
+    "locationDialogDescription": "Opslag toevoegen aan {warehouse}.",
+    "tabBulk": "Een rek met stellingen",
+    "tabSingle": "Eén stelling",
+    "zone": "Zone",
+    "zonePlaceholder": "A, Ruimte 1, Noord",
+    "rack": "Rek",
+    "rackPlaceholder": "04",
+    "shelf": "Plank",
+    "shelfPlaceholder": "2",
+    "position": "Positie",
+    "positionPlaceholder": "Links",
+    "shelfFrom": "Planken van",
+    "shelfTo": "tot",
+    "code": "Code",
+    "codePlaceholder": "A-04-2",
+    "codeHint": "Staat op het label. Wordt voor u ingevuld als u dit leeg laat.",
+    "capacity": "Banden per plank",
+    "capacityHint": "Hoeveel losse banden hier passen.",
+    "notes": "Notities",
+    "bulkPreview": "Maakt {count} planken aan, {first} tot en met {last}",
+    "bulkPreviewCapacity": "Ruimte voor {tires} banden in totaal",
+    "bulkCreated": "{count} planken aangemaakt",
+    "bulkCreatedWithSkips": "{created} planken aangemaakt, {skipped} bestaande overgeslagen",
+    "bulkNothingCreated": "Die planken bestaan al",
+    "locationCreated": "Stelling toegevoegd",
+    "locationUpdated": "Stelling bijgewerkt",
+    "locationDeleted": "{code} verwijderd",
+    "deleteLocationTitle": "{code} verwijderen?",
+    "deleteLocationBody": "De stelling moet eerst leeg zijn.",
+    "saveFailed": "Opslaan mislukt",
+    "deleteFailed": "Verwijderen mislukt"
+  },
+  "movements": {
+    "check_in": "Ingenomen",
+    "check_out": "Teruggegeven",
+    "relocate": "Verplaatst",
+    "dispose": "Afgevoerd",
+    "measure": "Gemeten"
+  },
+  "detail": {
+    "move": "Verplaatsen",
+    "checkOut": "Teruggeven",
+    "conditionTitle": "Staat",
+    "noMeasurements": "Nog geen metingen vastgelegd.",
+    "historyTitle": "Geschiedenis",
+    "storageTitle": "Opslag",
+    "shelf": "Stelling",
+    "warehouse": "Locatie",
+    "notStored": "Momenteel niet in opslag.",
+    "checkedIn": "Ingenomen",
+    "checkedOut": "Teruggegeven",
+    "tiresTitle": "Banden",
+    "loadSpeed": "Draagvermogen en snelheid",
+    "ownerTitle": "Eigenaar",
+    "phone": "Telefoon",
+    "deleteTitle": "Deze bandenset verwijderen?",
+    "deleteBody": "De set en de volledige geschiedenis worden verwijderd. Dit kan niet ongedaan worden gemaakt.",
+    "deleted": "Bandenset verwijderd",
+    "deleteFailed": "Kon de set niet verwijderen",
+    "belowLimit": "{count} band(en) op of onder uw vervangingsgrens.",
+    "belowLimitHint": "Bied de klant een prijs aan nu de banden hier nog liggen.",
+    "dispose": "Afschrijven",
+    "disposeTitle": "Deze set afschrijven?",
+    "disposeBody": "Het record en de historie blijven staan en de plek komt vrij. Volgend seizoen wordt de set niet meer voorgesteld.",
+    "disposed": "Set afgeschreven",
+    "disposeFailed": "Kon de set niet afschrijven"
+  },
+  "checkOut": {
+    "title": "Banden teruggeven",
+    "description": "De set wordt van {code} gehaald.",
+    "descriptionNoLocation": "Deze set gaat terug naar de klant.",
+    "treadTitle": "Profieldiepte bij teruggave",
+    "note": "Notitie",
+    "submit": "Teruggeven",
+    "success": "Set {reference} teruggegeven",
+    "failed": "Teruggeven mislukt",
+    "prepPending": "{count} voorbereidingsklus(sen) open",
+    "conditionTitle": "Staat bij binnenkomst",
+    "conditionLead": {
+      "replace": "Een of meer banden zitten op of onder uw vervangingsgrens. Meld het de klant voordat hij wegrijdt.",
+      "fair": "Een of meer banden naderen uw vervangingsgrens. Voor het volgende seizoen even benoemen."
+    },
+    "treadHintUnmeasured": "Bij binnenkomst is niet gemeten. Dit is de laatste kans om dat te doen."
+  },
+  "relocate": {
+    "title": "Banden verplaatsen",
+    "description": "Kies de stelling waar deze set heen gaat.",
+    "newLocation": "Nieuwe stelling",
+    "note": "Notitie",
+    "submit": "Verplaatsen",
+    "success": "Set {reference} verplaatst",
+    "failed": "Verplaatsen mislukt"
+  },
+  "edit": {
+    "title": "Bandenset bewerken",
+    "description": "Corrigeer wat bij het innemen is vastgelegd. Gebruik Verplaatsen om de stelling te wijzigen.",
+    "quantityHint": "Verhogen wordt getoetst aan de stelling waar de set op staat.",
+    "rimType": "Velgtype",
+    "rimTypePlaceholder": "Lichtmetaal, staal",
+    "success": "Bandenset bijgewerkt",
+    "failed": "Kon de wijzigingen niet opslaan",
+    "treadHint": "Een gewijzigde meting wordt als nieuwe meting in de historie vastgelegd in plaats van de oude te overschrijven."
+  },
+  "treatments": {
+    "types": {
+      "wash_tires": "Banden wassen",
+      "wash_rims": "Velgen wassen",
+      "balance": "Balanceren",
+      "tpms_service": "TPMS-service",
+      "new_valves": "Nieuwe ventielen",
+      "repair": "Reparatie"
+    },
+    "statuses": {
+      "pending": "Te doen",
+      "done": "Klaar",
+      "skipped": "Overgeslagen"
+    },
+    "title": "Voorbereiding",
+    "none": "Aan deze set hoeft niets te gebeuren.",
+    "add": "Voorbereiding toevoegen",
+    "pickHint": "Vink aan wat deze set nodig heeft voordat hij terug op de auto gaat.",
+    "pendingCount": "{count} te doen",
+    "allComplete": "Alles klaar",
+    "ready": "Klaar",
+    "markDone": "Klaar",
+    "completeAll": "Alle {count} als klaar markeren",
+    "allDone": "Alle voorbereiding klaar",
+    "undo": "Terug op de lijst",
+    "saveFailed": "Kon niet opslaan",
+    "skip": "Overslaan"
+  },
+  "messaging": {
+    "action": "Bericht",
+    "title": "Klant een bericht sturen",
+    "description": "Aan {name}.",
+    "noCustomer": "Geen klant aan deze set.",
+    "noCustomerBody": "Koppel eerst een klant aan de set.",
+    "channel": "Versturen via",
+    "channels": {
+      "email": "E-mail",
+      "sms": "Sms",
+      "telegram": "Telegram"
+    },
+    "noAddress": "geen adres bekend",
+    "recipient": "Versturen naar",
+    "recipientPlaceholder": "Telefoonnummer",
+    "subject": "Onderwerp",
+    "message": "Bericht",
+    "send": "Versturen",
+    "sent": "Bericht verstuurd",
+    "failed": "Kon het bericht niet versturen",
+    "smsLong": "Lange berichten worden mogelijk over meerdere sms’jes verdeeld.",
+    "subjects": {
+      "low_tread": "Uw {season}banden",
+      "damage": "Uw {season}banden",
+      "stored": "Uw {season}banden staan opgeslagen",
+      "custom": "Uw {season}banden"
+    },
+    "bodies": {
+      "low_tread": "Hallo {customer_name}, uw {season}banden liggen bij ons opgeslagen bij {shop_name}. Bij het innemen maten {positions} nog {tread}, dat is onder de grens. Ze halen geen seizoen meer. Zullen we een prijs maken voor een nieuwe set?",
+      "damage": "Hallo {customer_name}, uw {season}banden liggen bij ons opgeslagen bij {shop_name}. Bij het innemen vonden we schade op {positions}. Bel ons wanneer het uitkomt, dan lopen we het samen door.",
+      "stored": "Hallo {customer_name}, uw {season}banden liggen nu bij ons opgeslagen bij {shop_name}. We laten het weten wanneer het tijd is om te wisselen.",
+      "custom": "Hallo {customer_name}, over uw {season}banden die bij ons liggen bij {shop_name}:"
+    },
+    "enableInSettings": "Nog niet ingesteld. Schakel het in bij Instellingen."
+  },
+  "job": {
+    "cardTitle": "Offertes en opdrachten",
+    "none": "Voor deze set is nog niets aangemaakt.",
+    "quote": "Offerte",
+    "workOrder": "Werkorder",
+    "converted": "omgezet",
+    "quoteTitle": "Nieuwe set offreren",
+    "workOrderTitle": "Deze set factureren",
+    "loadingSet": "Set wordt geladen",
+    "tires": "Banden",
+    "inStock": "{count} op voorraad",
+    "shortStock": "maar {count} op voorraad",
+    "noMatch": "Niets op voorraad past bij {size}. De regel komt erin met de prijs die u invult.",
+    "noSize": "Deze set heeft geen maat, dus voorraad kan niet gematcht worden. De regel komt erin met uw prijs.",
+    "unitPrice": "Prijs per band",
+    "createQuote": "Offerte aanmaken",
+    "createWorkOrder": "Werkorder aanmaken",
+    "quoteCreated": "Offerte {number} aangemaakt",
+    "workOrderCreated": "Werkorder aangemaakt",
+    "failed": "Kon dit niet aanmaken",
+    "bannerTitle": "Ophalen van",
+    "openSet": "Bandenset openen",
+    "prepPending": "Nog te doen voor montage: {jobs}",
+    "notOnShelf": "Deze set ligt niet meer in opslag.",
+    "targetNew": "Nieuwe werkorder",
+    "targetNewHint": "Een opdracht alleen voor deze banden.",
+    "targetExisting": "Of toevoegen aan een opdracht die al open staat voor dit voertuig",
+    "addToWorkOrder": "Aan werkorder toevoegen",
+    "workOrderUpdated": "Toegevoegd aan de werkorder",
+    "unlink": "Ontkoppelen",
+    "unlinkTitle": "Deze banden van de opdracht ontkoppelen?",
+    "unlinkBody": "De opdracht verwijst niet langer naar de bandenset. Onderdelen en arbeidsregels die er al op staan blijven staan; verwijder ze apart als ze niet gefactureerd moeten worden.",
+    "unlinked": "Banden ontkoppeld",
+    "unlinkFailed": "Kon de banden niet ontkoppelen",
+    "prep": "Voorbereiding",
+    "tiresSkipped": "Er wordt geen bandenregel toegevoegd.",
+    "total": "Totaal",
+    "searchStock": "Zoek in voorraad op merk, maat of artikelnummer",
+    "noSearchHits": "Niets in de voorraad komt overeen met \"{query}\".",
+    "otherSize": "Andere maat",
+    "inSize": "{count} in {size}",
+    "quantity": "Aantal",
+    "noStockItem": "Geen voorraadartikel, prijs handmatig",
+    "storeTires": "Banden opslaan",
+    "storage": "Opslag",
+    "storageFee": "Kosten",
+    "storagePeriod": "Periode",
+    "destination": "Waar het heen gaat",
+    "targetInvoice": "Nieuwe factuur",
+    "targetInvoiceHint": "Factureer de klant direct, zonder werkorder op een voertuig.",
+    "createInvoice": "Factuur aanmaken",
+    "needsCustomer": "Koppel een klant of voertuig aan deze set voordat u factureert.",
+    "storagePeriodEnd": "Opgehaald",
+    "storageOpenEnded": "Open",
+    "storagePeriodHint": "Staat op de factuurregel zodat de klant ziet waarvoor de kosten gelden. Laat het einde leeg tot u weet wanneer ze ophalen.",
+    "pricesHint": "Prijzen voor voorbereiding komen uit de instellingen. Elke regel is achteraf nog aan te passen.",
+    "files": "Foto’s en bestanden",
+    "filesCount": "{count} op de factuur"
+  },
+  "label": {
+    "action": "Labels",
+    "title": "Bandenlabels printen",
+    "description": "Stickers voor elke band, met een QR die deze set opent.",
+    "format": "Labelformaat",
+    "formats": {
+      "dymo_small": "Kleine rol",
+      "dymo_standard": "Standaardrol",
+      "thermal_large": "Grote rol",
+      "sheet_a4": "A4-vel"
+    },
+    "perSheet": "{count} per vel",
+    "copies": "Hoeveel",
+    "copiesHint": "één per band ({count})",
+    "fewer": "Eén minder",
+    "more": "Eén meer",
+    "print": "Printen",
+    "shelfNote": "De stelling staat er bewust niet op. Sets worden verplaatst en een geprinte stelling klopt dan niet meer. De QR laat altijd de actuele plek zien.",
+    "reference": "Ref",
+    "quantity": "{count, plural, one {# band} other {# banden}}",
+    "withRims": "Op velgen",
+    "tpms": "TPMS",
+    "studded": "Spijkers",
+    "unassigned": "Niet toegewezen",
+    "preview": "Voorbeeld",
+    "previewScale": "Op printformaat weergegeven. De code scant ook vanaf het scherm.",
+    "previewFailed": "Kon geen voorbeeld maken"
+  },
+  "vehicle": {
+    "oneOnShelf": "op {code}",
+    "inStorage": "in bandenhotel"
+  },
+  "wear": {
+    "summary": "{value} versleten tussen {from} en {to}",
+    "perSeason": "Ongeveer {value} per seizoen",
+    "seasonsLeft": "{count, plural, =0 {Nu op de grens} one {Nog ongeveer één seizoen} other {Nog ongeveer # seizoenen}}",
+    "since": "-{value}"
+  },
+  "settings": {
+    "change": "Wijzigen in instellingen",
+    "prepPrices": "Prijzen voor voorbereiding",
+    "storagePrices": "Opslagprijzen in instellingen",
+    "treadLimits": "Grens wijzigen",
+    "defaultCapacity": "Standaardcapaciteit"
+  },
+  "forecast": {
+    "title": "Volgend seizoen",
+    "subtitle": "Wat de opgeslagen sets volgens meting nodig hebben, per maat.",
+    "backToList": "Terug naar sets",
+    "tires": "Verwachte banden",
+    "atLimit": "Al op de grens",
+    "expected": "Binnen een seizoen verwacht",
+    "orderTitle": "Per maat",
+    "sizes": "{count} maten",
+    "empty": "Nog niets gemeten dicht bij de grens. Een set heeft twee bezoeken nodig voor een slijtagetempo.",
+    "nowCount": "{count, plural, one {# set op de grens} other {# sets op de grens}}",
+    "nextCount": "{count, plural, one {# set binnen een seizoen} other {# sets binnen een seizoen}}",
+    "tiresShort": "banden",
+    "lowest": "laagste {value}",
+    "now": "Op de grens",
+    "next": "Binnen een seizoen",
+    "rate": "{value} per seizoen",
+    "caveat": "Sets op de grens zijn gemeten. De rest is doorgerekend vanuit de slijtage van één klant over één periode: bruikbaar om te plannen, niet om op te bestellen.",
+    "unmeasured": "{count, plural, one {# set heeft te weinig historie en telt niet mee} other {# sets hebben te weinig historie en tellen niet mee}}",
+    "capped": "De {shown} nieuwste van {total} sets gelezen."
+  },
+  "invoiceLine": {
+    "storage": "Bandenopslag",
+    "pieces": "st.",
+    "fromDate": "vanaf {date}"
+  },
+  "setNames": {
+    "winter": "winterbanden",
+    "summer": "zomerbanden",
+    "all_season": "vierseizoenenbanden",
+    "other": "banden"
+  },
+  "jobNotes": {
+    "set": "Bandenset {reference}",
+    "shelf": "Stelling: {shelf}",
+    "notOnShelf": "niet op een stelling",
+    "onRims": "Op velgen",
+    "tiresOnly": "Alleen banden",
+    "tpms": "Heeft TPMS-sensoren"
+  },
+  "files": {
+    "title": "Foto’s en bestanden",
+    "add": "Bestanden toevoegen",
+    "none": "Nog niets toegevoegd. Fotografeer een beschadigde velg terwijl de banden voor u liggen.",
+    "captionPlaceholder": "Wat is hierop te zien?",
+    "onInvoice": "Op de factuur",
+    "invoiceHint": "Bestanden gemarkeerd voor de factuur worden bij het factureren naar de werkorder of factuur gekopieerd.",
+    "deleteTitle": "Dit bestand verwijderen?",
+    "deleteBody": "{name} wordt van de set verwijderd. Wat al naar een factuur is gekopieerd, blijft staan.",
+    "failed": "Kon dat niet opslaan",
+    "someFailed": "{count, plural, one {# bestand kon niet worden geüpload} other {# bestanden konden niet worden geüpload}}"
+  }
+}

+ 27 - 1
messages/pl/audit.json

@@ -109,6 +109,32 @@
     "settings_applyTaxRate": "Zastosowano stawkę podatku",
     "settings_convertToExclusive": "Przeliczono ceny na netto",
     "settings_convertToInclusive": "Przeliczono ceny na brutto",
-    "organization_sampleDataRemove": "Usunięto dane przykładowe"
+    "organization_sampleDataRemove": "Usunięto dane przykładowe",
+    "tire_warehouse_create": "Utworzono lokalizację magazynu opon",
+    "tire_warehouse_update": "Zaktualizowano lokalizację magazynu opon",
+    "tire_warehouse_delete": "Usunięto lokalizację magazynu opon",
+    "tire_warehouse_archive": "Zarchiwizowano lokalizację magazynu opon",
+    "tire_location_create": "Utworzono półkę na opony",
+    "tire_location_bulk_create": "Utworzono półki na opony",
+    "tire_location_update": "Zaktualizowano półkę na opony",
+    "tire_location_delete": "Usunięto półkę na opony",
+    "tire_set_check_in": "Przyjęto komplet opon",
+    "tire_set_check_out": "Wydano komplet opon",
+    "tire_set_relocate": "Przeniesiono komplet opon",
+    "tire_set_update": "Zaktualizowano komplet opon",
+    "tire_set_delete": "Usunięto komplet opon",
+    "tire_measurement_create": "Zapisano pomiary opon",
+    "tire_treatment_update": "Zaktualizowano przygotowanie",
+    "tire_treatment_mark": "Oznaczono przygotowanie",
+    "tire_treatment_complete_all": "Ukończono przygotowanie",
+    "tire_set_message_customer": "Wysłano wiadomość do klienta o komplecie opon",
+    "tire_set_create_quote": "Utworzono wycenę z kompletu opon",
+    "tire_set_create_work_order": "Utworzono zlecenie z kompletu opon",
+    "tire_set_add_to_work_order": "Dodano komplet opon do zlecenia",
+    "tire_set_unlink_work_order": "Odłączono komplet opon od zlecenia",
+    "tire_set_return": "Ponownie przyjęto komplet",
+    "tire_set_dispose": "Spisano komplet opon",
+    "tire_set_attach": "Dodano pliki do kompletu opon",
+    "tire_set_detach": "Usunięto plik z kompletu opon"
   }
 }

+ 2 - 1
messages/pl/common.json

@@ -10,7 +10,8 @@
     "confirm": "Potwierdź",
     "saveChanges": "Zapisz zmiany",
     "edit": "Edytuj",
-    "add": "Dodaj"
+    "add": "Dodaj",
+    "gotIt": "Rozumiem"
   },
   "contextMenu": {
     "hint": "Wskazówka: kliknij wiersz prawym przyciskiem, aby zobaczyć akcje",

+ 11 - 1
messages/pl/dashboard.json

@@ -54,7 +54,8 @@
     "activeJobs": "Aktywne zlecenia",
     "recentObservations": "Ostatnie obserwacje",
     "recentActivity": "Ostatnia aktywność",
-    "gettingStarted": "Pierwsze kroki"
+    "gettingStarted": "Pierwsze kroki",
+    "tireHotel": "Hotel opon"
   },
   "maintenance": {
     "title": "Przewidywany termin konserwacji",
@@ -234,5 +235,14 @@
       "expired": "Wygasła",
       "converted": "Przekształcona"
     }
+  },
+  "tireHotel": {
+    "title": "Hotel opon",
+    "viewAll": "Zobacz hotel opon",
+    "stored": "{count} kompletów w magazynie",
+    "capacity": "{used} / {capacity} opon",
+    "roomLeft": "Miejsce na {count} więcej",
+    "needsPrep": "Do przygotowania",
+    "unbilled": "Nierozliczone ({count})"
   }
 }

+ 6 - 0
messages/pl/featureHints.json

@@ -0,0 +1,6 @@
+{
+  "tire-hotel": {
+    "title": "Hotel opon jest włączony",
+    "body": "Przechowuj opony sezonowe. Skonfiguruj półki, potem przyjmij komplet."
+  }
+}

+ 8 - 2
messages/pl/navigation.json

@@ -53,7 +53,12 @@
     "allReminders": "Wszystkie przypomnienia",
     "auditLog": "Dziennik audytu",
     "aiAssistant": "Asystent AI",
-    "observations": "Obserwacje"
+    "observations": "Obserwacje",
+    "tireHotel": "Hotel opon",
+    "allTireSets": "Wszystkie komplety",
+    "tireStorage": "Układ magazynu",
+    "tireSetDetails": "Komplet opon",
+    "tireForecast": "Następny sezon"
   },
   "search": "Szukaj...",
   "shortcut": "Ctrl+K",
@@ -107,6 +112,7 @@
     "signOut": "Wyloguj się",
     "more": "Więcej",
     "auditLog": "Dziennik audytu",
-    "aiAssistant": "Asystent AI"
+    "aiAssistant": "Asystent AI",
+    "tireHotel": "Hotel opon"
   }
 }

+ 27 - 0
messages/pl/permissions.json

@@ -0,0 +1,27 @@
+{
+  "subjects": {
+    "dashboard": "Pulpit",
+    "vehicles": "Pojazdy",
+    "customers": "Klienci",
+    "work_orders": "Zlecenia",
+    "quotes": "Oferty",
+    "services": "Historia serwisowa",
+    "billing": "Rozliczenia",
+    "inventory": "Magazyn",
+    "labor_presets": "Szablony robocizny",
+    "inspections": "Przeglądy",
+    "tire_hotel": "Hotel opon",
+    "reports": "Raporty",
+    "work_board": "Tablica pracy",
+    "ai_assistant": "Asystent AI",
+    "settings": "Ustawienia"
+  },
+  "actions": {
+    "create": "Tworzyć",
+    "read": "Podgląd",
+    "update": "Edytować",
+    "delete": "Usuwać",
+    "manage": "Zarządzać",
+    "manage_storage": "Zarządzać układem magazynu"
+  }
+}

+ 9 - 0
messages/pl/search.json

@@ -0,0 +1,9 @@
+{
+  "title": "Szukaj",
+  "description": "Szukaj pojazdów, klientów, zleceń i ustawień",
+  "placeholder": "Szukaj po nazwie, rejestracji, telefonie, VIN, fakturze…",
+  "noResults": "Brak wyników.",
+  "recentCustomers": "Ostatni klienci",
+  "services": "Historia serwisowa",
+  "toClose": "aby zamknąć"
+}

+ 34 - 1
messages/pl/settings.json

@@ -122,6 +122,10 @@
       "support": {
         "title": "Pomoc",
         "description": "Skontaktuj się z pomocą i zarządzaj przyciskiem"
+      },
+      "tireHotel": {
+        "title": "Hotel opon",
+        "description": "Sezonowe przechowywanie opon"
       }
     },
     "pro": "PRO"
@@ -974,7 +978,9 @@
       "lastBackup": "Ostatnia kopia zapasowa:",
       "supportPrompt": "Jeśli masz pytania dotyczące ochrony Twoich danych,",
       "supportAction": "skontaktuj się z naszym zespołem wsparcia."
-    }
+    },
+    "optTireHotel": "Hotel opon",
+    "optTireHotelDesc": "Przechowywane komplety, półki, przygotowanie i umowy przechowywania"
   },
   "license": {
     "title": "Licencja white-label",
@@ -1454,5 +1460,32 @@
     "unavailable": "Ta przeglądarka nie zaproponowała instalacji. Otwórz menu przeglądarki i poszukaj „Zainstaluj aplikację” lub „Dodaj do ekranu głównego”.",
     "bannerHidden": "Baner instalacji jest ukryty na tym urządzeniu.",
     "showBanner": "Pokaż baner ponownie"
+  },
+  "tireHotel": {
+    "title": "Hotel opon",
+    "description": "Przechowuj sezonowe opony klientów i wiedz, gdzie stoi każdy komplet.",
+    "cardTitle": "Magazyn opon",
+    "enable": "Włącz hotel opon",
+    "enableHint": "Dodaje magazyn opon do menu. Późniejsze wyłączenie ukrywa go bez usuwania danych.",
+    "defaultCapacity": "Domyślna liczba opon na półkę",
+    "defaultCapacityHint": "Wypełniane przy tworzeniu nowych półek. Można zmienić dla każdej półki.",
+    "warnPercent": "Ostrzegaj przy zapełnieniu (%)",
+    "warnPercentHint": "Oznacza podgląd magazynu, gdy ta część łącznej pojemności jest zajęta.",
+    "treadTitle": "Progi wymiany",
+    "treadHint": "Minimalne wartości prawne różnią się w zależności od kraju, więc ustaw progi obowiązujące na Twoim rynku. Bieżnik poniżej progu jest oznaczany do wymiany.",
+    "summerLimit": "Próg letni ({unit})",
+    "winterLimit": "Próg zimowy ({unit})",
+    "unitMm": "mm",
+    "unit32nds": "1/32\"",
+    "setupHint": "Następnie skonfiguruj półki.",
+    "setupLink": "Otwórz układ magazynu",
+    "save": "Zapisz ustawienia hotelu opon",
+    "saved": "Zapisano ustawienia hotelu opon",
+    "billingTitle": "Rozliczenie przechowywania",
+    "billingHint": "Wstawiane w pozycji przechowywania przy rozliczaniu kompletu. Można je zmienić na fakturze.",
+    "seasonalPrice": "Domyślna cena przechowywania",
+    "prepTitle": "Ceny przygotowania",
+    "prepHint": "Ile kosztuje każdy rodzaj przygotowania. Puste pole oznacza, że nigdy nie jest fakturowane, co pasuje warsztatom mającym mycie w cenie przechowywania. Wycenione trafiają automatycznie na wycenę i zlecenie.",
+    "prepNotCharged": "nie naliczane"
   }
 }

+ 473 - 0
messages/pl/tireHotel.json

@@ -0,0 +1,473 @@
+{
+  "title": "Hotel opon",
+  "description": "Sezonowe przechowywanie opon klientów",
+  "common": {
+    "cancel": "Anuluj",
+    "save": "Zapisz",
+    "create": "Utwórz",
+    "delete": "Usuń",
+    "edit": "Edytuj"
+  },
+  "seasons": {
+    "summer": "Letnie",
+    "winter": "Zimowe",
+    "all_season": "Całoroczne",
+    "other": "Inne"
+  },
+  "statuses": {
+    "stored": "W magazynie",
+    "released": "Wydane",
+    "disposed": "Zutylizowane"
+  },
+  "conditions": {
+    "good": "Dobry",
+    "fair": "Do obserwacji",
+    "replace": "Do wymiany"
+  },
+  "positions": {
+    "front_left": "Przód lewy",
+    "front_right": "Przód prawy",
+    "rear_left": "Tył lewy",
+    "rear_right": "Tył prawy",
+    "spare": "Koło zapasowe",
+    "unspecified": "Nieokreślone"
+  },
+  "capacity": {
+    "used": "{used} z {capacity} opon",
+    "free": "Miejsce na {count}",
+    "freeShort": "{count} wolnych",
+    "over": "{count} ponad pojemność",
+    "meterLabel": "{used} z {capacity} opon w magazynie"
+  },
+  "tread": {
+    "placeholderMm": "mm",
+    "placeholder32": "1/32 cala",
+    "inputLabel": "Głębokość bieżnika, {position}",
+    "lastTime": "było {value}",
+    "worn": "zużycie {value}"
+  },
+  "list": {
+    "tabAll": "Wszystkie",
+    "tabStored": "W magazynie",
+    "tabReleased": "Wydane",
+    "search": "Szukaj po rejestracji, kliencie, marce lub półce",
+    "checkIn": "Przyjmij opony",
+    "empty": "Brak kompletów opon. Przyjmij komplet, aby zacząć.",
+    "unassigned": "Nieprzypisane",
+    "reference": "Nr",
+    "vehicle": "Pojazd",
+    "tires": "Opony",
+    "location": "Półka",
+    "condition": "Stan",
+    "status": "Status",
+    "storedSince": "Od",
+    "tireCount": "{count, plural, one {# opona} few {# opony} many {# opon} other {# opony}}",
+    "studded": "Z kolcami",
+    "roomLeft": "Miejsce na {count}",
+    "prep": "Przyg.",
+    "tabNeedsPrep": "Do przygotowania",
+    "setUpShelves": "Skonfiguruj półki",
+    "emptyNoShelves": "Brak półek. Skonfiguruj magazyn, zanim przyjmiesz opony.",
+    "tabDisposed": "Spisane"
+  },
+  "checkIn": {
+    "title": "Przyjmij opony",
+    "description": "Zapisz, co przyjechało, i wybierz, gdzie ma stać.",
+    "customer": "Klient",
+    "selectCustomer": "Wybierz klienta",
+    "noCustomer": "Brak klienta",
+    "vehicle": "Pojazd",
+    "selectVehicle": "Wybierz pojazd",
+    "season": "Sezon",
+    "quantity": "Liczba opon",
+    "quantityHint": "Liczone pojedynczo, więc para lub pięć kół też zadziała.",
+    "brand": "Marka",
+    "brandPlaceholder": "Michelin, Bridgestone, Dębica",
+    "model": "Model",
+    "size": "Rozmiar",
+    "sizePlaceholder": "225/45R17",
+    "dotCode": "Kod DOT",
+    "dotPlaceholder": "3623",
+    "dotHint": "Tydzień i rok produkcji opony.",
+    "withRims": "Na felgach",
+    "hasTpms": "Czujniki TPMS",
+    "studded": "Z kolcami",
+    "location": "Gdzie trafiają opony",
+    "choosePlace": "Wybierz półkę",
+    "searchLocations": "Szukaj półek",
+    "noLocations": "Nie znaleziono półek",
+    "tooFull": "Za mało miejsca",
+    "pickLocationFirst": "Wybierz półkę przed przyjęciem kompletu",
+    "roomSummary": "{locations} półek pomieści ten komplet, {tires} wolnych miejsc",
+    "nothingFits": "Żadna półka nie ma miejsca na {quantity} opon. Zwolnij miejsce lub dodaj półkę.",
+    "treadTitle": "Głębokość bieżnika przy przyjęciu",
+    "treadHint": "Opcjonalne, ale rozstrzyga późniejsze spory o stan opon.",
+    "notes": "Notatki",
+    "submit": "Przyjmij",
+    "success": "Komplet {reference} odłożony na {code}",
+    "failed": "Nie udało się przyjąć kompletu",
+    "noVehiclesForCustomer": "Ten klient nie ma jeszcze pojazdów. Najpierw dodaj pojazd na jego stronie klienta.",
+    "treatmentsHint": "Co należy zrobić z tym kompletem przed magazynowaniem lub montażem.",
+    "storedBefore": "Już tu przechowywane",
+    "storedBeforeHint": "Wracają te same opony? Wybierz komplet, a sezon dołączy do jego historii.",
+    "sameSet": "Ten sam komplet co wcześniej",
+    "pieces": "{count} szt.",
+    "lastOut": "Odebrano {date}",
+    "notStored": "Obecnie nieprzechowywany",
+    "returned": "Komplet {reference} znów leży na {code}",
+    "submitReturn": "Przyjmij ponownie",
+    "ownerFromVehicle": "Pobrane z pojazdu",
+    "vehicleOwner": "Właściciel pojazdu",
+    "clearVehicle": "Wyczyść pojazd"
+  },
+  "storage": {
+    "title": "Półki",
+    "description": "Skonfiguruj magazyn raz, a potem od razu widzisz, co i gdzie się zmieści.",
+    "addWarehouse": "Dodaj lokalizację",
+    "addShelves": "Dodaj półki",
+    "warehouseName": "Nazwa",
+    "warehouseNamePlaceholder": "Budynek główny, Kontener 2, Zaplecze",
+    "warehouseAddress": "Adres",
+    "warehouseCreated": "Dodano lokalizację magazynową",
+    "warehouseDeleted": "Usunięto {name}",
+    "warehouseArchived": "Zarchiwizowano {name}",
+    "deleteWarehouse": "Usuń lokalizację",
+    "deleteWarehouseTitle": "Usunąć {name}?",
+    "deleteWarehouseBody": "Spowoduje to usunięcie lokalizacji i wszystkich jej półek.",
+    "deleteWarehouseBodyOccupied": "Ta lokalizacja nadal zawiera {count} opon, więc zostanie zarchiwizowana zamiast usunięta.",
+    "totalCapacity": "Łączna pojemność",
+    "totalStored": "Obecnie w magazynie",
+    "totalFree": "Wolne miejsce",
+    "totalLocations": "Półki",
+    "locationSummary": "{occupied} z {total} półek w użyciu",
+    "noLocations": "Brak półek. Dodaj regał, aby przechowywać tu opony.",
+    "emptyTitle": "Magazyn nie jest jeszcze skonfigurowany",
+    "emptyBody": "Dodaj pierwszy budynek lub pomieszczenie i wypełnij je półkami.",
+    "default": "Domyślna",
+    "addLocation": "Dodaj półki",
+    "editLocation": "Edytuj półkę",
+    "createLocation": "Utwórz półkę",
+    "createShelves": "Utwórz półki",
+    "locationDialogDescription": "Dodawanie miejsca do {warehouse}.",
+    "tabBulk": "Regał z półkami",
+    "tabSingle": "Jedna półka",
+    "zone": "Strefa",
+    "zonePlaceholder": "A, Pomieszczenie 1, Północ",
+    "rack": "Regał",
+    "rackPlaceholder": "04",
+    "shelf": "Półka",
+    "shelfPlaceholder": "2",
+    "position": "Pozycja",
+    "positionPlaceholder": "Lewa",
+    "shelfFrom": "Półki od",
+    "shelfTo": "do",
+    "code": "Kod",
+    "codePlaceholder": "A-04-2",
+    "codeHint": "Widnieje na etykiecie. Uzupełni się sam, jeśli zostawisz puste.",
+    "capacity": "Opon na półkę",
+    "capacityHint": "Ile pojedynczych opon się tu mieści.",
+    "notes": "Notatki",
+    "bulkPreview": "Utworzy {count} półek, od {first} do {last}",
+    "bulkPreviewCapacity": "Miejsce na łącznie {tires} opon",
+    "bulkCreated": "Utworzono {count} półek",
+    "bulkCreatedWithSkips": "Utworzono {created} półek, pominięto {skipped} już istniejących",
+    "bulkNothingCreated": "Te półki już istnieją",
+    "locationCreated": "Dodano półkę",
+    "locationUpdated": "Zaktualizowano półkę",
+    "locationDeleted": "Usunięto {code}",
+    "deleteLocationTitle": "Usunąć {code}?",
+    "deleteLocationBody": "Półka musi być najpierw pusta.",
+    "saveFailed": "Nie udało się zapisać",
+    "deleteFailed": "Nie udało się usunąć"
+  },
+  "movements": {
+    "check_in": "Przyjęto",
+    "check_out": "Wydano",
+    "relocate": "Przeniesiono",
+    "dispose": "Zutylizowano",
+    "measure": "Zmierzono"
+  },
+  "detail": {
+    "move": "Przenieś",
+    "checkOut": "Wydaj",
+    "conditionTitle": "Stan",
+    "noMeasurements": "Brak zapisanych pomiarów.",
+    "historyTitle": "Historia",
+    "storageTitle": "Magazyn",
+    "shelf": "Półka",
+    "warehouse": "Lokalizacja",
+    "notStored": "Obecnie nie w magazynie.",
+    "checkedIn": "Przyjęto",
+    "checkedOut": "Wydano",
+    "tiresTitle": "Opony",
+    "loadSpeed": "Indeks nośności i prędkości",
+    "ownerTitle": "Właściciel",
+    "phone": "Telefon",
+    "deleteTitle": "Usunąć ten komplet opon?",
+    "deleteBody": "Komplet i cała jego historia zostaną usunięte. Tej operacji nie można cofnąć.",
+    "deleted": "Usunięto komplet opon",
+    "deleteFailed": "Nie udało się usunąć kompletu",
+    "belowLimit": "{count} opon(y) na granicy wymiany lub poniżej.",
+    "belowLimitHint": "Warto zaproponować klientowi cenę, póki opony są jeszcze u nas.",
+    "dispose": "Spisz",
+    "disposeTitle": "Spisać ten komplet?",
+    "disposeBody": "Wpis i historia zostają, a miejsce na półce zwalnia się. Komplet nie będzie proponowany w kolejnym sezonie.",
+    "disposed": "Komplet spisany",
+    "disposeFailed": "Nie udało się spisać kompletu"
+  },
+  "checkOut": {
+    "title": "Wydaj opony",
+    "description": "Komplet zostanie zdjęty z {code}.",
+    "descriptionNoLocation": "Ten komplet wraca do klienta.",
+    "treadTitle": "Głębokość bieżnika przy wydaniu",
+    "note": "Notatka",
+    "submit": "Wydaj",
+    "success": "Komplet {reference} wydany",
+    "failed": "Nie udało się wydać kompletu",
+    "prepPending": "Pozostało {count} zadań przygotowania",
+    "conditionTitle": "Stan przy przyjęciu",
+    "conditionLead": {
+      "replace": "Co najmniej jedna opona jest na granicy wymiany lub poniżej. Warto powiedzieć klientowi, zanim odjedzie.",
+      "fair": "Co najmniej jedna opona zbliża się do granicy wymiany. Warto wspomnieć przed kolejnym sezonem."
+    },
+    "treadHintUnmeasured": "Przy przyjęciu nie wykonano pomiaru. To ostatnia okazja, żeby go zapisać."
+  },
+  "relocate": {
+    "title": "Przenieś opony",
+    "description": "Wybierz półkę docelową.",
+    "newLocation": "Nowa półka",
+    "note": "Notatka",
+    "submit": "Przenieś",
+    "success": "Komplet {reference} przeniesiony",
+    "failed": "Nie udało się przenieść kompletu"
+  },
+  "edit": {
+    "title": "Edytuj komplet opon",
+    "description": "Popraw to, co zapisano przy przyjęciu. Aby zmienić półkę, użyj Przenieś.",
+    "quantityHint": "Zwiększenie jest sprawdzane względem półki, na której stoi komplet.",
+    "rimType": "Rodzaj felgi",
+    "rimTypePlaceholder": "Aluminiowa, stalowa",
+    "success": "Zaktualizowano komplet opon",
+    "failed": "Nie udało się zapisać zmian",
+    "treadHint": "Zmiana pomiaru zapisuje go jako nowy odczyt w historii, zamiast nadpisywać poprzedni."
+  },
+  "treatments": {
+    "types": {
+      "wash_tires": "Umyć opony",
+      "wash_rims": "Umyć felgi",
+      "balance": "Wyważyć",
+      "tpms_service": "Serwis TPMS",
+      "new_valves": "Nowe zawory",
+      "repair": "Naprawa"
+    },
+    "statuses": {
+      "pending": "Do zrobienia",
+      "done": "Zrobione",
+      "skipped": "Pominięte"
+    },
+    "title": "Przygotowanie",
+    "none": "Przy tym komplecie nie ma nic do zrobienia.",
+    "add": "Dodaj przygotowanie",
+    "pickHint": "Zaznacz, czego ten komplet potrzebuje przed powrotem na auto.",
+    "pendingCount": "{count} do zrobienia",
+    "allComplete": "Wszystko gotowe",
+    "ready": "Gotowe",
+    "markDone": "Zrobione",
+    "completeAll": "Oznacz wszystkie {count} jako zrobione",
+    "allDone": "Przygotowanie zakończone",
+    "undo": "Wróć na listę",
+    "saveFailed": "Nie udało się zapisać",
+    "skip": "Pomiń"
+  },
+  "messaging": {
+    "action": "Wiadomość",
+    "title": "Napisz do klienta",
+    "description": "Do {name}.",
+    "noCustomer": "Ten komplet nie ma klienta.",
+    "noCustomerBody": "Powiąż klienta z kompletem, zanim do niego napiszesz.",
+    "channel": "Wyślij przez",
+    "channels": {
+      "email": "E-mail",
+      "sms": "SMS",
+      "telegram": "Telegram"
+    },
+    "noAddress": "brak zapisanego adresu",
+    "recipient": "Wyślij do",
+    "recipientPlaceholder": "Numer telefonu",
+    "subject": "Temat",
+    "message": "Wiadomość",
+    "send": "Wyślij",
+    "sent": "Wiadomość wysłana",
+    "failed": "Nie udało się wysłać wiadomości",
+    "smsLong": "Długie wiadomości mogą zostać podzielone na kilka SMS-ów.",
+    "subjects": {
+      "low_tread": "Twoje opony {season}",
+      "damage": "Twoje opony {season}",
+      "stored": "Twoje opony {season} są w magazynie",
+      "custom": "Twoje opony {season}"
+    },
+    "bodies": {
+      "low_tread": "Cześć {customer_name}, Twoje opony {season} leżą u nas w {shop_name}. Przy przyjęciu {positions} miały {tread}, czyli poniżej granicy. Kolejnego sezonu nie wytrzymają. Przygotować wycenę nowego kompletu?",
+      "damage": "Cześć {customer_name}, Twoje opony {season} leżą u nas w {shop_name}. Przy przyjęciu znaleźliśmy uszkodzenie na {positions}. Zadzwoń, kiedy będziesz mieć chwilę, przejdziemy przez to razem.",
+      "stored": "Cześć {customer_name}, Twoje opony {season} są już u nas w magazynie w {shop_name}. Odezwiemy się, gdy przyjdzie czas na wymianę.",
+      "custom": "Cześć {customer_name}, w sprawie Twoich opon {season}, które leżą u nas w {shop_name}:"
+    },
+    "enableInSettings": "Jeszcze nieskonfigurowane. Włącz w Ustawieniach."
+  },
+  "job": {
+    "cardTitle": "Wyceny i zlecenia",
+    "none": "Dla tego kompletu nic jeszcze nie utworzono.",
+    "quote": "Wycena",
+    "workOrder": "Zlecenie",
+    "converted": "przekształcono",
+    "quoteTitle": "Wyceń nowy komplet",
+    "workOrderTitle": "Rozlicz ten komplet",
+    "loadingSet": "Wczytywanie kompletu",
+    "tires": "Opony",
+    "inStock": "{count} na stanie",
+    "shortStock": "tylko {count} na stanie",
+    "noMatch": "Nic na stanie nie pasuje do {size}. Pozycja zostanie dodana z ceną, którą podasz.",
+    "noSize": "Ten komplet nie ma rozmiaru, więc nie da się dopasować stanu. Pozycja użyje Twojej ceny.",
+    "unitPrice": "Cena za oponę",
+    "createQuote": "Utwórz wycenę",
+    "createWorkOrder": "Utwórz zlecenie",
+    "quoteCreated": "Utworzono wycenę {number}",
+    "workOrderCreated": "Utworzono zlecenie",
+    "failed": "Nie udało się utworzyć",
+    "bannerTitle": "Odbiór z",
+    "openSet": "Otwórz komplet",
+    "prepPending": "Do zrobienia przed montażem: {jobs}",
+    "notOnShelf": "Ten komplet nie jest już w magazynie.",
+    "targetNew": "Nowe zlecenie",
+    "targetNewHint": "Utwórz zlecenie tylko dla tych opon.",
+    "targetExisting": "Albo dodaj do zlecenia już otwartego dla tego pojazdu",
+    "addToWorkOrder": "Dodaj do zlecenia",
+    "workOrderUpdated": "Dodano do zlecenia",
+    "unlink": "Odłącz",
+    "unlinkTitle": "Odłączyć te opony od zlecenia?",
+    "unlinkBody": "Zlecenie przestaje wskazywać komplet opon. Części i robocizna, które już na nim są, zostają, więc usuń je osobno, jeśli nie mają być fakturowane.",
+    "unlinked": "Odłączono opony",
+    "unlinkFailed": "Nie udało się odłączyć opon",
+    "prep": "Przygotowanie",
+    "tiresSkipped": "Pozycja z oponami nie zostanie dodana.",
+    "total": "Razem",
+    "searchStock": "Szukaj w magazynie według marki, rozmiaru lub numeru części",
+    "noSearchHits": "Nic w magazynie nie pasuje do \"{query}\".",
+    "otherSize": "Inny rozmiar",
+    "inSize": "{count} w {size}",
+    "quantity": "Ilość",
+    "noStockItem": "Brak pozycji magazynowej, cena ręczna",
+    "storeTires": "Przechowaj opony",
+    "storage": "Przechowywanie",
+    "storageFee": "Opłata",
+    "storagePeriod": "Okres",
+    "destination": "Gdzie trafia",
+    "targetInvoice": "Nowa faktura",
+    "targetInvoiceHint": "Obciąż klienta bezpośrednio, bez zlecenia na pojazd.",
+    "createInvoice": "Utwórz fakturę",
+    "needsCustomer": "Powiąż klienta lub pojazd z kompletem przed rozliczeniem.",
+    "storagePeriodEnd": "Odebrano",
+    "storageOpenEnded": "Otwarty",
+    "storagePeriodHint": "Widnieje na pozycji faktury, więc klient widzi, co obejmuje opłata. Zostaw koniec pusty, dopóki nie wiesz, kiedy odbiorą.",
+    "pricesHint": "Ceny przygotowania pochodzą z ustawień. Każdą pozycję można zmienić po utworzeniu dokumentu.",
+    "files": "Zdjęcia i pliki",
+    "filesCount": "{count} na fakturze"
+  },
+  "label": {
+    "action": "Etykiety",
+    "title": "Drukuj etykiety",
+    "description": "Naklejka na każdą oponę, z kodem QR otwierającym ten komplet.",
+    "format": "Rozmiar",
+    "formats": {
+      "dymo_small": "Mała rolka",
+      "dymo_standard": "Standardowa rolka",
+      "thermal_large": "Duża rolka",
+      "sheet_a4": "Arkusz A4"
+    },
+    "perSheet": "{count} na arkusz",
+    "copies": "Ile",
+    "copiesHint": "jedna na oponę ({count})",
+    "fewer": "O jedną mniej",
+    "more": "O jedną więcej",
+    "print": "Drukuj",
+    "shelfNote": "Półka celowo nie jest drukowana. Komplety bywają przenoszone, a wydrukowana półka byłaby wtedy błędna. Kod QR zawsze pokazuje aktualne miejsce.",
+    "reference": "Nr",
+    "quantity": "{count, plural, one {# opona} few {# opony} many {# opon} other {# opony}}",
+    "withRims": "Na felgach",
+    "tpms": "TPMS",
+    "studded": "Z kolcami",
+    "unassigned": "Nieprzypisane",
+    "preview": "Podgląd",
+    "previewScale": "Pokazany w rozmiarze wydruku. Kod można zeskanować także z ekranu.",
+    "previewFailed": "Nie udało się utworzyć podglądu"
+  },
+  "vehicle": {
+    "oneOnShelf": "na {code}",
+    "inStorage": "w hotelu opon"
+  },
+  "wear": {
+    "summary": "Zużycie {value} między {from} a {to}",
+    "perSeason": "Około {value} na sezon",
+    "seasonsLeft": "{count, plural, =0 {Już na granicy} one {Jeszcze około jednego sezonu} other {Jeszcze około # sezonów}}",
+    "since": "-{value}"
+  },
+  "settings": {
+    "change": "Zmień w ustawieniach",
+    "prepPrices": "Ceny przygotowania",
+    "storagePrices": "Ceny przechowywania",
+    "treadLimits": "Zmień limit",
+    "defaultCapacity": "Domyślna pojemność"
+  },
+  "forecast": {
+    "title": "Następny sezon",
+    "subtitle": "Czego zmierzone komplety będą potrzebować, według rozmiaru.",
+    "backToList": "Wróć do kompletów",
+    "tires": "Spodziewane opony",
+    "atLimit": "Już na granicy",
+    "expected": "Spodziewane w ciągu sezonu",
+    "orderTitle": "Według rozmiaru",
+    "sizes": "{count} rozmiarów",
+    "empty": "Nic jeszcze nie zmierzono blisko granicy. Komplet potrzebuje dwóch wizyt, by wyliczyć tempo.",
+    "nowCount": "{count, plural, one {# komplet na granicy} other {# kompletów na granicy}}",
+    "nextCount": "{count, plural, one {# komplet w ciągu sezonu} other {# kompletów w ciągu sezonu}}",
+    "tiresShort": "opon",
+    "lowest": "najniższy {value}",
+    "now": "Na granicy",
+    "next": "W ciągu sezonu",
+    "rate": "{value} na sezon",
+    "caveat": "Komplety na granicy są zmierzone. Reszta to ekstrapolacja zużycia jednego klienta w jednym okresie: do planowania, nie do zamówienia.",
+    "unmeasured": "{count, plural, one {# komplet ma za mało historii i nie jest liczony} other {# kompletów ma za mało historii i nie jest liczonych}}",
+    "capped": "Odczytano {shown} najnowszych kompletów z {total}."
+  },
+  "invoiceLine": {
+    "storage": "Przechowywanie opon",
+    "pieces": "szt.",
+    "fromDate": "od {date}"
+  },
+  "setNames": {
+    "winter": "opony zimowe",
+    "summer": "opony letnie",
+    "all_season": "opony całoroczne",
+    "other": "opony"
+  },
+  "jobNotes": {
+    "set": "Komplet opon {reference}",
+    "shelf": "Półka: {shelf}",
+    "notOnShelf": "poza półką",
+    "onRims": "Na felgach",
+    "tiresOnly": "Same opony",
+    "tpms": "Z czujnikami TPMS"
+  },
+  "files": {
+    "title": "Zdjęcia i pliki",
+    "add": "Dodaj pliki",
+    "none": "Nic jeszcze nie dołączono. Zrób zdjęcie otartej felgi, póki opony masz przed sobą.",
+    "captionPlaceholder": "Co widać na zdjęciu?",
+    "onInvoice": "Na fakturze",
+    "invoiceHint": "Pliki oznaczone do faktury są kopiowane na zlecenie lub fakturę przy rozliczaniu kompletu.",
+    "deleteTitle": "Usunąć ten plik?",
+    "deleteBody": "{name} zostanie usunięty z kompletu. To, co już trafiło na fakturę, tam zostaje.",
+    "failed": "Nie udało się zapisać",
+    "someFailed": "{count, plural, one {nie udało się przesłać # pliku} other {nie udało się przesłać # plików}}"
+  }
+}

+ 27 - 1
messages/pt-BR/audit.json

@@ -109,6 +109,32 @@
     "settings_applyTaxRate": "Alíquota aplicada",
     "settings_convertToExclusive": "Preços convertidos para sem imposto",
     "settings_convertToInclusive": "Preços convertidos para com imposto",
-    "organization_sampleDataRemove": "Dados de exemplo removidos"
+    "organization_sampleDataRemove": "Dados de exemplo removidos",
+    "tire_warehouse_create": "Local de armazenagem de pneus criado",
+    "tire_warehouse_update": "Local de armazenagem de pneus atualizado",
+    "tire_warehouse_delete": "Local de armazenagem de pneus excluído",
+    "tire_warehouse_archive": "Local de armazenagem de pneus arquivado",
+    "tire_location_create": "Prateleira de pneus criada",
+    "tire_location_bulk_create": "Prateleiras de pneus criadas",
+    "tire_location_update": "Prateleira de pneus atualizada",
+    "tire_location_delete": "Prateleira de pneus excluída",
+    "tire_set_check_in": "Jogo de pneus recebido",
+    "tire_set_check_out": "Jogo de pneus devolvido",
+    "tire_set_relocate": "Jogo de pneus movido",
+    "tire_set_update": "Jogo de pneus atualizado",
+    "tire_set_delete": "Jogo de pneus excluído",
+    "tire_measurement_create": "Medições de pneus registradas",
+    "tire_treatment_update": "Preparação atualizada",
+    "tire_treatment_mark": "Preparação marcada",
+    "tire_treatment_complete_all": "Preparação concluída",
+    "tire_set_message_customer": "Mensagem enviada ao cliente sobre um jogo de pneus",
+    "tire_set_create_quote": "Orçamento criado a partir de um jogo de pneus",
+    "tire_set_create_work_order": "Ordem criada a partir de um jogo de pneus",
+    "tire_set_add_to_work_order": "Jogo de pneus somado a uma ordem",
+    "tire_set_unlink_work_order": "Jogo de pneus desvinculado de uma ordem",
+    "tire_set_return": "Jogo armazenado novamente",
+    "tire_set_dispose": "Jogo de pneus baixado",
+    "tire_set_attach": "Arquivos adicionados a um jogo de pneus",
+    "tire_set_detach": "Arquivo removido de um jogo de pneus"
   }
 }

+ 2 - 1
messages/pt-BR/common.json

@@ -10,7 +10,8 @@
     "confirm": "Confirmar",
     "saveChanges": "Salvar alterações",
     "edit": "Editar",
-    "add": "Adicionar"
+    "add": "Adicionar",
+    "gotIt": "Entendi"
   },
   "contextMenu": {
     "hint": "Dica: clique com o botão direito em uma linha para ver ações",

+ 11 - 1
messages/pt-BR/dashboard.json

@@ -54,7 +54,8 @@
     "activeJobs": "Trabalhos Ativos",
     "recentObservations": "Observações recentes",
     "recentActivity": "Atividade recente",
-    "gettingStarted": "Primeiros passos"
+    "gettingStarted": "Primeiros passos",
+    "tireHotel": "Hotel de pneus"
   },
   "maintenance": {
     "title": "Manutenção Prevista",
@@ -234,5 +235,14 @@
       "expired": "Expirado",
       "converted": "Convertido"
     }
+  },
+  "tireHotel": {
+    "title": "Hotel de pneus",
+    "viewAll": "Ver hotel de pneus",
+    "stored": "{count} jogos armazenados",
+    "capacity": "{used} / {capacity} pneus",
+    "roomLeft": "Espaço para mais {count}",
+    "needsPrep": "A preparar",
+    "unbilled": "Não faturado ({count})"
   }
 }

+ 6 - 0
messages/pt-BR/featureHints.json

@@ -0,0 +1,6 @@
+{
+  "tire-hotel": {
+    "title": "O hotel de pneus está ativo",
+    "body": "Guarde pneus de temporada. Configure prateleiras e receba um jogo."
+  }
+}

+ 8 - 2
messages/pt-BR/navigation.json

@@ -53,7 +53,12 @@
     "allReminders": "Todos os lembretes",
     "auditLog": "Registro de auditoria",
     "aiAssistant": "Assistente IA",
-    "observations": "Observações"
+    "observations": "Observações",
+    "tireHotel": "Hotel de pneus",
+    "allTireSets": "Todos os jogos",
+    "tireStorage": "Layout da armazenagem",
+    "tireSetDetails": "Jogo de pneus",
+    "tireForecast": "Próxima temporada"
   },
   "search": "Buscar...",
   "shortcut": "Ctrl+K",
@@ -107,6 +112,7 @@
     "signOut": "Sair",
     "more": "Mais",
     "auditLog": "Registro de auditoria",
-    "aiAssistant": "Assistente IA"
+    "aiAssistant": "Assistente IA",
+    "tireHotel": "Hotel de pneus"
   }
 }

+ 27 - 0
messages/pt-BR/permissions.json

@@ -0,0 +1,27 @@
+{
+  "subjects": {
+    "dashboard": "Painel",
+    "vehicles": "Veículos",
+    "customers": "Clientes",
+    "work_orders": "Ordens de serviço",
+    "quotes": "Orçamentos",
+    "services": "Histórico de serviço",
+    "billing": "Faturamento",
+    "inventory": "Estoque",
+    "labor_presets": "Modelos de mão de obra",
+    "inspections": "Inspeções",
+    "tire_hotel": "Hotel de pneus",
+    "reports": "Relatórios",
+    "work_board": "Quadro de trabalho",
+    "ai_assistant": "Assistente de IA",
+    "settings": "Configurações"
+  },
+  "actions": {
+    "create": "Criar",
+    "read": "Ver",
+    "update": "Editar",
+    "delete": "Excluir",
+    "manage": "Gerenciar",
+    "manage_storage": "Gerenciar o layout da armazenagem"
+  }
+}

+ 9 - 0
messages/pt-BR/search.json

@@ -0,0 +1,9 @@
+{
+  "title": "Buscar",
+  "description": "Busque veículos, clientes, serviços e configurações",
+  "placeholder": "Buscar por nome, placa, telefone, chassi, nota…",
+  "noResults": "Nenhum resultado.",
+  "recentCustomers": "Clientes recentes",
+  "services": "Histórico de serviço",
+  "toClose": "para fechar"
+}

+ 34 - 1
messages/pt-BR/settings.json

@@ -122,6 +122,10 @@
       "support": {
         "title": "Suporte",
         "description": "Falar com o suporte e gerenciar o botão"
+      },
+      "tireHotel": {
+        "title": "Hotel de pneus",
+        "description": "Armazenagem sazonal de pneus"
       }
     },
     "pro": "PRO"
@@ -974,7 +978,9 @@
       "lastBackup": "Último backup:",
       "supportPrompt": "Se tiver dúvidas sobre como seus dados são protegidos,",
       "supportAction": "fale com nossa equipe de suporte."
-    }
+    },
+    "optTireHotel": "Hotel de pneus",
+    "optTireHotelDesc": "Jogos armazenados, prateleiras, preparação e contratos de armazenagem"
   },
   "license": {
     "title": "Licença marca branca",
@@ -1454,5 +1460,32 @@
     "unavailable": "Este navegador não ofereceu a instalação. Abra o menu do navegador e procure \"Instalar app\" ou \"Adicionar à Tela de Início\".",
     "bannerHidden": "O aviso de instalação está oculto neste dispositivo.",
     "showBanner": "Mostrar o aviso novamente"
+  },
+  "tireHotel": {
+    "title": "Hotel de pneus",
+    "description": "Guarde os pneus sazonais dos seus clientes e saiba onde cada jogo está.",
+    "cardTitle": "Armazenagem de pneus",
+    "enable": "Ativar hotel de pneus",
+    "enableHint": "Adiciona a armazenagem de pneus ao menu. Desativar depois oculta tudo sem excluir nada.",
+    "defaultCapacity": "Pneus por prateleira (padrão)",
+    "defaultCapacityHint": "Preenchido ao criar novas prateleiras. Ajustável em cada uma.",
+    "warnPercent": "Avisar quando cheio (%)",
+    "warnPercentHint": "Sinaliza o panorama da armazenagem quando essa fatia da capacidade total estiver ocupada.",
+    "treadTitle": "Limites de troca",
+    "treadHint": "Os mínimos legais variam por país, então defina os limites do seu mercado. Sulco abaixo do limite é sinalizado para troca.",
+    "summerLimit": "Limite verão ({unit})",
+    "winterLimit": "Limite inverno ({unit})",
+    "unitMm": "mm",
+    "unit32nds": "1/32\"",
+    "setupHint": "Em seguida, configure suas prateleiras.",
+    "setupLink": "Abrir layout da armazenagem",
+    "save": "Salvar configurações do hotel de pneus",
+    "saved": "Configurações do hotel de pneus salvas",
+    "billingTitle": "Cobrança da armazenagem",
+    "billingHint": "Preenchido na linha de guarda ao cobrar um jogo. Editável na cobrança.",
+    "seasonalPrice": "Preço padrão de guarda",
+    "prepTitle": "Preços da preparação",
+    "prepHint": "Quanto custa cada tipo de preparação. Deixe em branco e nunca é cobrado, que é o que quer a oficina que inclui a lavagem na armazenagem. Os com preço entram sozinhos no orçamento e na ordem.",
+    "prepNotCharged": "não cobrado"
   }
 }

+ 473 - 0
messages/pt-BR/tireHotel.json

@@ -0,0 +1,473 @@
+{
+  "title": "Hotel de pneus",
+  "description": "Armazenagem sazonal de pneus para seus clientes",
+  "common": {
+    "cancel": "Cancelar",
+    "save": "Salvar",
+    "create": "Criar",
+    "delete": "Excluir",
+    "edit": "Editar"
+  },
+  "seasons": {
+    "summer": "Verão",
+    "winter": "Inverno",
+    "all_season": "Todas as estações",
+    "other": "Outro"
+  },
+  "statuses": {
+    "stored": "Armazenado",
+    "released": "Devolvido",
+    "disposed": "Descartado"
+  },
+  "conditions": {
+    "good": "Bom",
+    "fair": "Observar",
+    "replace": "Substituir"
+  },
+  "positions": {
+    "front_left": "Dianteiro esquerdo",
+    "front_right": "Dianteiro direito",
+    "rear_left": "Traseiro esquerdo",
+    "rear_right": "Traseiro direito",
+    "spare": "Estepe",
+    "unspecified": "Não especificado"
+  },
+  "capacity": {
+    "used": "{used} de {capacity} pneus",
+    "free": "Espaço para {count}",
+    "freeShort": "{count} livres",
+    "over": "{count} acima da capacidade",
+    "meterLabel": "{used} de {capacity} pneus armazenados"
+  },
+  "tread": {
+    "placeholderMm": "mm",
+    "placeholder32": "32 avos",
+    "inputLabel": "Profundidade do sulco, {position}",
+    "lastTime": "era {value}",
+    "worn": "{value} de desgaste"
+  },
+  "list": {
+    "tabAll": "Todos",
+    "tabStored": "Armazenados",
+    "tabReleased": "Devolvidos",
+    "search": "Buscar por placa, cliente, marca ou prateleira",
+    "checkIn": "Receber pneus",
+    "empty": "Nenhum jogo de pneus ainda. Receba um jogo para começar.",
+    "unassigned": "Sem vínculo",
+    "reference": "Ref",
+    "vehicle": "Veículo",
+    "tires": "Pneus",
+    "location": "Prateleira",
+    "condition": "Condição",
+    "status": "Situação",
+    "storedSince": "Desde",
+    "tireCount": "{count, plural, one {# pneu} other {# pneus}}",
+    "studded": "Com cravos",
+    "roomLeft": "Espaço para {count}",
+    "prep": "Prep.",
+    "tabNeedsPrep": "A preparar",
+    "setUpShelves": "Configurar prateleiras",
+    "emptyNoShelves": "Nenhuma prateleira ainda. Configure a armazenagem antes de receber pneus.",
+    "tabDisposed": "Baixados"
+  },
+  "checkIn": {
+    "title": "Receber pneus",
+    "description": "Registre o que chegou e escolha onde guardar.",
+    "customer": "Cliente",
+    "selectCustomer": "Selecionar cliente",
+    "noCustomer": "Sem cliente",
+    "vehicle": "Veículo",
+    "selectVehicle": "Selecionar veículo",
+    "season": "Estação",
+    "quantity": "Quantidade de pneus",
+    "quantityHint": "Contados individualmente, então um par ou cinco rodas também funcionam.",
+    "brand": "Marca",
+    "brandPlaceholder": "Michelin, Bridgestone, Pirelli",
+    "model": "Modelo",
+    "size": "Medida",
+    "sizePlaceholder": "225/45R17",
+    "dotCode": "Código DOT",
+    "dotPlaceholder": "3623",
+    "dotHint": "Semana e ano de fabricação do pneu.",
+    "withRims": "Com rodas",
+    "hasTpms": "Sensores TPMS",
+    "studded": "Com cravos",
+    "location": "Onde os pneus ficam",
+    "choosePlace": "Escolher prateleira",
+    "searchLocations": "Buscar prateleiras",
+    "noLocations": "Nenhuma prateleira encontrada",
+    "tooFull": "Espaço insuficiente",
+    "pickLocationFirst": "Escolha uma prateleira antes de receber o jogo",
+    "roomSummary": "{locations} prateleiras comportam este jogo, {tires} vagas livres",
+    "nothingFits": "Nenhuma prateleira tem espaço para {quantity} pneus. Libere espaço ou adicione uma prateleira.",
+    "treadTitle": "Profundidade do sulco na entrada",
+    "treadHint": "Opcional, mas resolve qualquer discussão futura sobre a condição.",
+    "notes": "Observações",
+    "submit": "Receber",
+    "success": "Jogo {reference} guardado em {code}",
+    "failed": "Não foi possível receber o jogo",
+    "noVehiclesForCustomer": "Este cliente ainda não tem veículos. Adicione um na página do cliente primeiro.",
+    "treatmentsHint": "O que fazer neste jogo antes de armazenar ou remontar.",
+    "storedBefore": "Já guardado aqui",
+    "storedBeforeHint": "São os mesmos pneus? Escolha o jogo para esta temporada entrar no histórico dele.",
+    "sameSet": "O mesmo jogo de antes",
+    "pieces": "{count} pçs",
+    "lastOut": "Retirado em {date}",
+    "notStored": "Não está guardado agora",
+    "returned": "O jogo {reference} está de volta em {code}",
+    "submitReturn": "Guardar de novo",
+    "ownerFromVehicle": "Obtido do veículo",
+    "vehicleOwner": "O proprietário do veículo",
+    "clearVehicle": "Limpar o veículo"
+  },
+  "storage": {
+    "title": "Prateleiras",
+    "description": "Configure o estoque uma vez e veja num relance o que cabe onde.",
+    "addWarehouse": "Adicionar local",
+    "addShelves": "Adicionar prateleiras",
+    "warehouseName": "Nome",
+    "warehouseNamePlaceholder": "Galpão principal, Contêiner 2, Depósito",
+    "warehouseAddress": "Endereço",
+    "warehouseCreated": "Local de armazenagem adicionado",
+    "warehouseDeleted": "{name} excluído",
+    "warehouseArchived": "{name} arquivado",
+    "deleteWarehouse": "Excluir local",
+    "deleteWarehouseTitle": "Excluir {name}?",
+    "deleteWarehouseBody": "Isso remove o local e todas as suas prateleiras.",
+    "deleteWarehouseBodyOccupied": "Este local ainda tem {count} pneus, então será arquivado em vez de excluído.",
+    "totalCapacity": "Capacidade total",
+    "totalStored": "Armazenados agora",
+    "totalFree": "Espaço livre",
+    "totalLocations": "Prateleiras",
+    "locationSummary": "{occupied} de {total} prateleiras em uso",
+    "noLocations": "Nenhuma prateleira ainda. Adicione uma estante para armazenar pneus aqui.",
+    "emptyTitle": "Nenhuma armazenagem configurada",
+    "emptyBody": "Adicione seu primeiro galpão ou sala e preencha com prateleiras.",
+    "default": "Padrão",
+    "addLocation": "Adicionar prateleiras",
+    "editLocation": "Editar prateleira",
+    "createLocation": "Criar prateleira",
+    "createShelves": "Criar prateleiras",
+    "locationDialogDescription": "Adicionando espaço a {warehouse}.",
+    "tabBulk": "Uma estante inteira",
+    "tabSingle": "Uma prateleira",
+    "zone": "Zona",
+    "zonePlaceholder": "A, Sala 1, Norte",
+    "rack": "Estante",
+    "rackPlaceholder": "04",
+    "shelf": "Prateleira",
+    "shelfPlaceholder": "2",
+    "position": "Posição",
+    "positionPlaceholder": "Esquerda",
+    "shelfFrom": "Prateleiras de",
+    "shelfTo": "até",
+    "code": "Código",
+    "codePlaceholder": "A-04-2",
+    "codeHint": "Aparece na etiqueta. É preenchido para você se deixar em branco.",
+    "capacity": "Pneus por prateleira",
+    "capacityHint": "Quantos pneus avulsos cabem aqui.",
+    "notes": "Observações",
+    "bulkPreview": "Cria {count} prateleiras, de {first} até {last}",
+    "bulkPreviewCapacity": "Espaço para {tires} pneus no total",
+    "bulkCreated": "{count} prateleiras criadas",
+    "bulkCreatedWithSkips": "{created} prateleiras criadas, {skipped} já existentes ignoradas",
+    "bulkNothingCreated": "Essas prateleiras já existem",
+    "locationCreated": "Prateleira adicionada",
+    "locationUpdated": "Prateleira atualizada",
+    "locationDeleted": "{code} excluída",
+    "deleteLocationTitle": "Excluir {code}?",
+    "deleteLocationBody": "A prateleira precisa estar vazia antes.",
+    "saveFailed": "Não foi possível salvar",
+    "deleteFailed": "Não foi possível excluir"
+  },
+  "movements": {
+    "check_in": "Recebido",
+    "check_out": "Devolvido",
+    "relocate": "Movido",
+    "dispose": "Descartado",
+    "measure": "Medido"
+  },
+  "detail": {
+    "move": "Mover",
+    "checkOut": "Devolver",
+    "conditionTitle": "Condição",
+    "noMeasurements": "Nenhuma medição registrada.",
+    "historyTitle": "Histórico",
+    "storageTitle": "Armazenagem",
+    "shelf": "Prateleira",
+    "warehouse": "Local",
+    "notStored": "No momento não está armazenado.",
+    "checkedIn": "Recebido",
+    "checkedOut": "Devolvido",
+    "tiresTitle": "Pneus",
+    "loadSpeed": "Índices de carga e velocidade",
+    "ownerTitle": "Proprietário",
+    "phone": "Telefone",
+    "deleteTitle": "Excluir este jogo de pneus?",
+    "deleteBody": "O jogo e todo o histórico serão removidos. Não é possível desfazer.",
+    "deleted": "Jogo de pneus excluído",
+    "deleteFailed": "Não foi possível excluir o jogo",
+    "belowLimit": "{count} pneu(s) no limite de troca ou abaixo.",
+    "belowLimitHint": "Vale oferecer um preço ao cliente enquanto os pneus ainda estão aqui.",
+    "dispose": "Dar baixa",
+    "disposeTitle": "Dar baixa neste jogo?",
+    "disposeBody": "O registro e o histórico permanecem e a vaga é liberada. Ele deixa de ser sugerido como os mesmos pneus na próxima temporada.",
+    "disposed": "Jogo baixado",
+    "disposeFailed": "Não foi possível dar baixa no jogo"
+  },
+  "checkOut": {
+    "title": "Devolver pneus",
+    "description": "O jogo será retirado de {code}.",
+    "descriptionNoLocation": "Este jogo volta para o cliente.",
+    "treadTitle": "Profundidade do sulco na saída",
+    "note": "Observação",
+    "submit": "Devolver",
+    "success": "Jogo {reference} devolvido",
+    "failed": "Não foi possível devolver o jogo",
+    "prepPending": "{count} tarefa(s) de preparação pendente(s)",
+    "conditionTitle": "Condição na entrada",
+    "conditionLead": {
+      "replace": "Um ou mais pneus estão no limite de troca ou abaixo. Vale avisar o cliente antes de ele sair.",
+      "fair": "Um ou mais pneus estão perto do limite de troca. Vale comentar antes da próxima temporada."
+    },
+    "treadHintUnmeasured": "Nenhuma medição foi feita na entrada. Esta é a última chance de registrar."
+  },
+  "relocate": {
+    "title": "Mover pneus",
+    "description": "Escolha a prateleira de destino.",
+    "newLocation": "Nova prateleira",
+    "note": "Observação",
+    "submit": "Mover",
+    "success": "Jogo {reference} movido",
+    "failed": "Não foi possível mover o jogo"
+  },
+  "edit": {
+    "title": "Editar jogo de pneus",
+    "description": "Corrija o que foi registrado no recebimento. Use Mover para trocar de prateleira.",
+    "quantityHint": "Aumentar é verificado contra a prateleira onde o jogo está.",
+    "rimType": "Tipo de roda",
+    "rimTypePlaceholder": "Liga leve, aço",
+    "success": "Jogo de pneus atualizado",
+    "failed": "Não foi possível salvar as alterações",
+    "treadHint": "Alterar uma medição registra uma nova leitura no histórico em vez de sobrescrever a anterior."
+  },
+  "treatments": {
+    "types": {
+      "wash_tires": "Lavar pneus",
+      "wash_rims": "Lavar rodas",
+      "balance": "Balancear",
+      "tpms_service": "Serviço TPMS",
+      "new_valves": "Válvulas novas",
+      "repair": "Reparo"
+    },
+    "statuses": {
+      "pending": "A fazer",
+      "done": "Feito",
+      "skipped": "Pulado"
+    },
+    "title": "Preparação",
+    "none": "Nada a fazer neste jogo.",
+    "add": "Adicionar preparação",
+    "pickHint": "Marque o que este jogo precisa antes de voltar para o carro.",
+    "pendingCount": "{count} a fazer",
+    "allComplete": "Tudo feito",
+    "ready": "Pronto",
+    "markDone": "Feito",
+    "completeAll": "Marcar os {count} como feitos",
+    "allDone": "Preparação concluída",
+    "undo": "Voltar para a lista",
+    "saveFailed": "Não foi possível salvar",
+    "skip": "Pular"
+  },
+  "messaging": {
+    "action": "Mensagem",
+    "title": "Enviar mensagem ao cliente",
+    "description": "Para {name}.",
+    "noCustomer": "Nenhum cliente neste jogo.",
+    "noCustomerBody": "Vincule um cliente ao jogo antes de enviar mensagem.",
+    "channel": "Enviar por",
+    "channels": {
+      "email": "E-mail",
+      "sms": "SMS",
+      "telegram": "Telegram"
+    },
+    "noAddress": "sem endereço cadastrado",
+    "recipient": "Enviar para",
+    "recipientPlaceholder": "Número de telefone",
+    "subject": "Assunto",
+    "message": "Mensagem",
+    "send": "Enviar",
+    "sent": "Mensagem enviada",
+    "failed": "Não foi possível enviar a mensagem",
+    "smsLong": "Mensagens longas podem ser divididas em vários SMS.",
+    "subjects": {
+      "low_tread": "Seus pneus de {season}",
+      "damage": "Seus pneus de {season}",
+      "stored": "Seus pneus de {season} estão guardados",
+      "custom": "Seus pneus de {season}"
+    },
+    "bodies": {
+      "low_tread": "Olá {customer_name}, seus pneus de {season} estão guardados conosco na {shop_name}. Na entrada, {positions} mediram {tread}, abaixo do limite. Não aguentam mais uma temporada. Quer que a gente faça um orçamento de um jogo novo?",
+      "damage": "Olá {customer_name}, seus pneus de {season} estão guardados conosco na {shop_name}. Na entrada encontramos um dano em {positions}. Ligue quando puder que a gente vê isso junto.",
+      "stored": "Olá {customer_name}, seus pneus de {season} já estão guardados conosco na {shop_name}. A gente avisa quando for hora de trocar.",
+      "custom": "Olá {customer_name}, sobre seus pneus de {season} guardados conosco na {shop_name}:"
+    },
+    "enableInSettings": "Ainda não configurado. Ative em Configurações."
+  },
+  "job": {
+    "cardTitle": "Orçamentos e ordens",
+    "none": "Nada criado para este jogo ainda.",
+    "quote": "Orçamento",
+    "workOrder": "Ordem",
+    "converted": "convertido",
+    "quoteTitle": "Orçar um jogo novo",
+    "workOrderTitle": "Cobrar este jogo",
+    "loadingSet": "Carregando o jogo",
+    "tires": "Pneus",
+    "inStock": "{count} em estoque",
+    "shortStock": "apenas {count} em estoque",
+    "noMatch": "Nada no estoque corresponde a {size}. A linha entra com o preço que você definir.",
+    "noSize": "Este jogo não tem medida, então não dá para cruzar com o estoque. A linha usa seu preço.",
+    "unitPrice": "Preço por pneu",
+    "createQuote": "Criar orçamento",
+    "createWorkOrder": "Criar ordem",
+    "quoteCreated": "Orçamento {number} criado",
+    "workOrderCreated": "Ordem criada",
+    "failed": "Não foi possível criar",
+    "bannerTitle": "Buscar em",
+    "openSet": "Abrir jogo",
+    "prepPending": "A fazer antes de montar: {jobs}",
+    "notOnShelf": "Este jogo não está mais armazenado.",
+    "targetNew": "Nova ordem",
+    "targetNewHint": "Criar uma ordem só para estes pneus.",
+    "targetExisting": "Ou some a uma ordem já aberta para este veículo",
+    "addToWorkOrder": "Somar à ordem",
+    "workOrderUpdated": "Somado à ordem",
+    "unlink": "Desvincular",
+    "unlinkTitle": "Desvincular estes pneus da ordem?",
+    "unlinkBody": "A ordem deixa de apontar para o jogo de pneus. Peças e mão de obra já lançadas permanecem; remova-as à parte se não devem ser cobradas.",
+    "unlinked": "Pneus desvinculados",
+    "unlinkFailed": "Não foi possível desvincular",
+    "prep": "Preparação",
+    "tiresSkipped": "Nenhuma linha de pneus será adicionada.",
+    "total": "Total",
+    "searchStock": "Buscar no estoque por marca, medida ou código da peça",
+    "noSearchHits": "Nada no estoque corresponde a \"{query}\".",
+    "otherSize": "Outra medida",
+    "inSize": "{count} em {size}",
+    "quantity": "Qtd",
+    "noStockItem": "Sem item de estoque, preço manual",
+    "storeTires": "Guardar pneus",
+    "storage": "Guarda",
+    "storageFee": "Valor",
+    "storagePeriod": "Período",
+    "destination": "Para onde vai",
+    "targetInvoice": "Nova fatura",
+    "targetInvoiceHint": "Cobre o cliente direto, sem ordem em um veículo.",
+    "createInvoice": "Criar fatura",
+    "needsCustomer": "Vincule um cliente ou veículo ao jogo antes de cobrar.",
+    "storagePeriodEnd": "Retirado",
+    "storageOpenEnded": "Aberto",
+    "storagePeriodHint": "Aparece na linha da nota para o cliente ver o que o valor cobre. Deixe o fim em branco até saber quando vão retirar.",
+    "pricesHint": "Os preços de preparação vêm das configurações. Cada linha ainda pode ser editada depois de criado.",
+    "files": "Fotos e arquivos",
+    "filesCount": "{count} na fatura"
+  },
+  "label": {
+    "action": "Etiquetas",
+    "title": "Imprimir etiquetas",
+    "description": "Um adesivo por pneu, com QR que abre este jogo.",
+    "format": "Tamanho",
+    "formats": {
+      "dymo_small": "Rolo pequeno",
+      "dymo_standard": "Rolo padrão",
+      "thermal_large": "Rolo grande",
+      "sheet_a4": "Folha A4"
+    },
+    "perSheet": "{count} por folha",
+    "copies": "Quantas",
+    "copiesHint": "uma por pneu ({count})",
+    "fewer": "Uma a menos",
+    "more": "Uma a mais",
+    "print": "Imprimir",
+    "shelfNote": "A prateleira não é impressa de propósito. Jogos são movidos, e uma prateleira impressa ficaria errada. O QR sempre mostra onde está agora.",
+    "reference": "Ref",
+    "quantity": "{count, plural, one {# pneu} other {# pneus}}",
+    "withRims": "Com rodas",
+    "tpms": "TPMS",
+    "studded": "Com cravos",
+    "unassigned": "Sem vínculo",
+    "preview": "Prévia",
+    "previewScale": "Exibida no tamanho de impressão. O código também lê direto da tela.",
+    "previewFailed": "Não foi possível gerar a prévia"
+  },
+  "vehicle": {
+    "oneOnShelf": "em {code}",
+    "inStorage": "no hotel de pneus"
+  },
+  "wear": {
+    "summary": "{value} de desgaste entre {from} e {to}",
+    "perSeason": "Cerca de {value} por temporada",
+    "seasonsLeft": "{count, plural, =0 {Já no limite} one {Cerca de mais uma temporada} other {Cerca de mais # temporadas}}",
+    "since": "-{value}"
+  },
+  "settings": {
+    "change": "Alterar nas configurações",
+    "prepPrices": "Preços de preparação",
+    "storagePrices": "Preços de armazenagem",
+    "treadLimits": "Alterar o limite",
+    "defaultCapacity": "Capacidade padrão"
+  },
+  "forecast": {
+    "title": "Próxima temporada",
+    "subtitle": "O que os jogos guardados estão medidos a precisar, por medida.",
+    "backToList": "Voltar aos jogos",
+    "tires": "Pneus previstos",
+    "atLimit": "Já no limite",
+    "expected": "Previsto em uma temporada",
+    "orderTitle": "Por medida",
+    "sizes": "{count} medidas",
+    "empty": "Nada medido perto do limite ainda. Um jogo precisa de duas visitas para calcular um ritmo.",
+    "nowCount": "{count, plural, one {# jogo no limite} other {# jogos no limite}}",
+    "nextCount": "{count, plural, one {# jogo em uma temporada} other {# jogos em uma temporada}}",
+    "tiresShort": "pneus",
+    "lowest": "menor {value}",
+    "now": "No limite",
+    "next": "Em uma temporada",
+    "rate": "{value} por temporada",
+    "caveat": "Os jogos no limite estão medidos. O resto é extrapolado do desgaste de um cliente em um intervalo: use para planejar, não para comprar.",
+    "unmeasured": "{count, plural, one {# jogo tem pouco histórico e não é contado} other {# jogos têm pouco histórico e não são contados}}",
+    "capped": "Lidos os {shown} jogos mais recentes de {total}."
+  },
+  "invoiceLine": {
+    "storage": "Guarda de pneus",
+    "pieces": "pçs",
+    "fromDate": "desde {date}"
+  },
+  "setNames": {
+    "winter": "pneus de inverno",
+    "summer": "pneus de verão",
+    "all_season": "pneus all season",
+    "other": "pneus"
+  },
+  "jobNotes": {
+    "set": "Jogo de pneus {reference}",
+    "shelf": "Prateleira: {shelf}",
+    "notOnShelf": "sem prateleira",
+    "onRims": "Com rodas",
+    "tiresOnly": "Somente pneus",
+    "tpms": "Com sensores TPMS"
+  },
+  "files": {
+    "title": "Fotos e arquivos",
+    "add": "Adicionar arquivos",
+    "none": "Nada anexado ainda. Fotografe uma roda raspada enquanto os pneus estão na sua frente.",
+    "captionPlaceholder": "O que aparece aqui?",
+    "onInvoice": "Na fatura",
+    "invoiceHint": "Arquivos marcados para a fatura são copiados para a ordem ou a fatura quando o jogo é cobrado.",
+    "deleteTitle": "Remover este arquivo?",
+    "deleteBody": "{name} sai deste jogo. O que já foi copiado para uma fatura continua lá.",
+    "failed": "Não foi possível salvar",
+    "someFailed": "{count, plural, one {não foi possível enviar # arquivo} other {não foi possível enviar # arquivos}}"
+  }
+}

+ 27 - 1
messages/ru/audit.json

@@ -109,6 +109,32 @@
     "settings_applyTaxRate": "Применена налоговая ставка",
     "settings_convertToExclusive": "Цены переведены в без налога",
     "settings_convertToInclusive": "Цены переведены во с налогом",
-    "organization_sampleDataRemove": "Удалены демонстрационные данные"
+    "organization_sampleDataRemove": "Удалены демонстрационные данные",
+    "tire_warehouse_create": "Создан склад шин",
+    "tire_warehouse_update": "Обновлён склад шин",
+    "tire_warehouse_delete": "Удалён склад шин",
+    "tire_warehouse_archive": "Склад шин перемещён в архив",
+    "tire_location_create": "Создана полка для шин",
+    "tire_location_bulk_create": "Созданы полки для шин",
+    "tire_location_update": "Обновлена полка для шин",
+    "tire_location_delete": "Удалена полка для шин",
+    "tire_set_check_in": "Принят комплект шин",
+    "tire_set_check_out": "Выдан комплект шин",
+    "tire_set_relocate": "Перемещён комплект шин",
+    "tire_set_update": "Обновлён комплект шин",
+    "tire_set_delete": "Удалён комплект шин",
+    "tire_measurement_create": "Записаны замеры шин",
+    "tire_treatment_update": "Обновлена подготовка",
+    "tire_treatment_mark": "Отмечена подготовка",
+    "tire_treatment_complete_all": "Подготовка завершена",
+    "tire_set_message_customer": "Клиенту отправлено сообщение о комплекте шин",
+    "tire_set_create_quote": "Создана смета по комплекту шин",
+    "tire_set_create_work_order": "Создан заказ-наряд по комплекту шин",
+    "tire_set_add_to_work_order": "Комплект шин добавлен в заказ-наряд",
+    "tire_set_unlink_work_order": "Комплект шин отвязан от заказа-наряда",
+    "tire_set_return": "Комплект снова на хранении",
+    "tire_set_dispose": "Комплект шин списан",
+    "tire_set_attach": "Файлы добавлены к комплекту шин",
+    "tire_set_detach": "Файл удалён из комплекта шин"
   }
 }

+ 2 - 1
messages/ru/common.json

@@ -10,7 +10,8 @@
     "back": "Назад",
     "confirm": "Подтвердить",
     "edit": "Редактировать",
-    "add": "Добавить"
+    "add": "Добавить",
+    "gotIt": "Понятно"
   },
   "contextMenu": {
     "hint": "Подсказка: правый клик по строке — действия",

+ 11 - 1
messages/ru/dashboard.json

@@ -54,7 +54,8 @@
     "activeJobs": "Активные работы",
     "recentObservations": "Недавние наблюдения",
     "recentActivity": "Последняя активность",
-    "gettingStarted": "Первые шаги"
+    "gettingStarted": "Первые шаги",
+    "tireHotel": "Шинный отель"
   },
   "maintenance": {
     "title": "Прогнозируемое техобслуживание",
@@ -234,5 +235,14 @@
       "expired": "Истекло",
       "converted": "Преобразовано"
     }
+  },
+  "tireHotel": {
+    "title": "Шинный отель",
+    "viewAll": "Открыть шинный отель",
+    "stored": "Комплектов на хранении: {count}",
+    "capacity": "{used} / {capacity} шин",
+    "roomLeft": "Место ещё для {count}",
+    "needsPrep": "Нужна подготовка",
+    "unbilled": "Не выставлено ({count})"
   }
 }

+ 6 - 0
messages/ru/featureHints.json

@@ -0,0 +1,6 @@
+{
+  "tire-hotel": {
+    "title": "Шинный отель включён",
+    "body": "Храните сезонные шины. Настройте полки, затем примите комплект."
+  }
+}

+ 8 - 2
messages/ru/navigation.json

@@ -53,7 +53,12 @@
     "allReminders": "Все напоминания",
     "auditLog": "Журнал аудита",
     "aiAssistant": "ИИ-ассистент",
-    "observations": "Наблюдения"
+    "observations": "Наблюдения",
+    "tireHotel": "Шинный отель",
+    "allTireSets": "Все комплекты",
+    "tireStorage": "Структура склада",
+    "tireSetDetails": "Комплект шин",
+    "tireForecast": "Следующий сезон"
   },
   "search": "Поиск...",
   "shortcut": "Ctrl+K",
@@ -107,6 +112,7 @@
     "signOut": "Выйти",
     "more": "Ещё",
     "auditLog": "Журнал аудита",
-    "aiAssistant": "ИИ-ассистент"
+    "aiAssistant": "ИИ-ассистент",
+    "tireHotel": "Шинный отель"
   }
 }

+ 27 - 0
messages/ru/permissions.json

@@ -0,0 +1,27 @@
+{
+  "subjects": {
+    "dashboard": "Панель",
+    "vehicles": "Автомобили",
+    "customers": "Клиенты",
+    "work_orders": "Заказ-наряды",
+    "quotes": "Сметы",
+    "services": "История работ",
+    "billing": "Счета",
+    "inventory": "Склад",
+    "labor_presets": "Шаблоны работ",
+    "inspections": "Осмотры",
+    "tire_hotel": "Шинный отель",
+    "reports": "Отчёты",
+    "work_board": "Доска работ",
+    "ai_assistant": "ИИ-помощник",
+    "settings": "Настройки"
+  },
+  "actions": {
+    "create": "Создание",
+    "read": "Просмотр",
+    "update": "Изменение",
+    "delete": "Удаление",
+    "manage": "Управление",
+    "manage_storage": "Управление структурой склада"
+  }
+}

+ 9 - 0
messages/ru/search.json

@@ -0,0 +1,9 @@
+{
+  "title": "Поиск",
+  "description": "Поиск по автомобилям, клиентам, работам и настройкам",
+  "placeholder": "Имя, номер, телефон, VIN, счёт…",
+  "noResults": "Ничего не найдено.",
+  "recentCustomers": "Недавние клиенты",
+  "services": "История работ",
+  "toClose": "чтобы закрыть"
+}

+ 34 - 1
messages/ru/settings.json

@@ -122,6 +122,10 @@
       "support": {
         "title": "Поддержка",
         "description": "Связаться с поддержкой и настроить кнопку"
+      },
+      "tireHotel": {
+        "title": "Шинный отель",
+        "description": "Сезонное хранение шин"
       }
     },
     "pro": "PRO"
@@ -974,7 +978,9 @@
       "lastBackup": "Последняя резервная копия:",
       "supportPrompt": "Если у вас есть вопросы о защите ваших данных,",
       "supportAction": "свяжитесь с нашей службой поддержки."
-    }
+    },
+    "optTireHotel": "Шинный отель",
+    "optTireHotelDesc": "Комплекты на хранении, полки, подготовка и договоры хранения"
   },
   "license": {
     "title": "White-Label лицензия",
@@ -1454,5 +1460,32 @@
     "unavailable": "Этот браузер не предложил установку. Откройте меню браузера и найдите «Установить приложение» или «На экран «Домой»».",
     "bannerHidden": "Баннер установки скрыт на этом устройстве.",
     "showBanner": "Показать баннер снова"
+  },
+  "tireHotel": {
+    "title": "Шинный отель",
+    "description": "Храните сезонные шины клиентов и всегда знайте, где лежит каждый комплект.",
+    "cardTitle": "Хранение шин",
+    "enable": "Включить шинный отель",
+    "enableHint": "Добавляет хранение шин в меню. Отключение позже скроет раздел, ничего не удаляя.",
+    "defaultCapacity": "Шин на полке по умолчанию",
+    "defaultCapacityHint": "Подставляется при создании новых полок. Можно изменить для каждой полки.",
+    "warnPercent": "Предупреждать при заполнении (%)",
+    "warnPercentHint": "Помечает обзор склада, когда занята эта доля общей вместимости.",
+    "treadTitle": "Пороги замены",
+    "treadHint": "Законодательные минимумы различаются по странам, поэтому задайте пороги своего рынка. Протектор ниже порога помечается под замену.",
+    "summerLimit": "Летний порог ({unit})",
+    "winterLimit": "Зимний порог ({unit})",
+    "unitMm": "mm",
+    "unit32nds": "1/32\"",
+    "setupHint": "Далее настройте полки.",
+    "setupLink": "Открыть структуру склада",
+    "save": "Сохранить настройки шинного отеля",
+    "saved": "Настройки шинного отеля сохранены",
+    "billingTitle": "Оплата хранения",
+    "billingHint": "Подставляется в строку хранения при выставлении счёта за комплект. Можно изменить в счёте.",
+    "seasonalPrice": "Цена хранения по умолчанию",
+    "prepTitle": "Цены на подготовку",
+    "prepHint": "Сколько стоит каждый вид подготовки. Пустое поле означает, что она никогда не выставляется, как нужно мастерским, у которых мойка входит в хранение. Те, у которых есть цена, попадают в смету и заказ сами.",
+    "prepNotCharged": "не выставляется"
   }
 }

+ 473 - 0
messages/ru/tireHotel.json

@@ -0,0 +1,473 @@
+{
+  "title": "Шинный отель",
+  "description": "Сезонное хранение шин ваших клиентов",
+  "common": {
+    "cancel": "Отмена",
+    "save": "Сохранить",
+    "create": "Создать",
+    "delete": "Удалить",
+    "edit": "Изменить"
+  },
+  "seasons": {
+    "summer": "Летние",
+    "winter": "Зимние",
+    "all_season": "Всесезонные",
+    "other": "Другое"
+  },
+  "statuses": {
+    "stored": "На хранении",
+    "released": "Выданы",
+    "disposed": "Утилизированы"
+  },
+  "conditions": {
+    "good": "Хорошее",
+    "fair": "Под наблюдением",
+    "replace": "Под замену"
+  },
+  "positions": {
+    "front_left": "Переднее левое",
+    "front_right": "Переднее правое",
+    "rear_left": "Заднее левое",
+    "rear_right": "Заднее правое",
+    "spare": "Запаска",
+    "unspecified": "Не указано"
+  },
+  "capacity": {
+    "used": "{used} из {capacity} шин",
+    "free": "Место для {count}",
+    "freeShort": "{count} свободно",
+    "over": "Превышение на {count}",
+    "meterLabel": "{used} из {capacity} шин на хранении"
+  },
+  "tread": {
+    "placeholderMm": "мм",
+    "placeholder32": "1/32 дюйма",
+    "inputLabel": "Глубина протектора, {position}",
+    "lastTime": "было {value}",
+    "worn": "износ {value}"
+  },
+  "list": {
+    "tabAll": "Все",
+    "tabStored": "На хранении",
+    "tabReleased": "Выданы",
+    "search": "Поиск по номеру, клиенту, марке или полке",
+    "checkIn": "Принять шины",
+    "empty": "Комплектов шин пока нет. Примите комплект, чтобы начать.",
+    "unassigned": "Без владельца",
+    "reference": "Номер",
+    "vehicle": "Автомобиль",
+    "tires": "Шины",
+    "location": "Полка",
+    "condition": "Состояние",
+    "status": "Статус",
+    "storedSince": "С",
+    "tireCount": "{count, plural, one {# шина} few {# шины} many {# шин} other {# шины}}",
+    "studded": "Шипованные",
+    "roomLeft": "Место для {count}",
+    "prep": "Подгот.",
+    "tabNeedsPrep": "Нужна подготовка",
+    "setUpShelves": "Настроить полки",
+    "emptyNoShelves": "Полок пока нет. Настройте склад, прежде чем принимать шины.",
+    "tabDisposed": "Списанные"
+  },
+  "checkIn": {
+    "title": "Принять шины",
+    "description": "Запишите, что поступило, и выберите место хранения.",
+    "customer": "Клиент",
+    "selectCustomer": "Выбрать клиента",
+    "noCustomer": "Без клиента",
+    "vehicle": "Автомобиль",
+    "selectVehicle": "Выбрать автомобиль",
+    "season": "Сезон",
+    "quantity": "Количество шин",
+    "quantityHint": "Считаются поштучно, поэтому пара или пять колёс тоже подойдут.",
+    "brand": "Марка",
+    "brandPlaceholder": "Michelin, Bridgestone, Nokian",
+    "model": "Модель",
+    "size": "Размер",
+    "sizePlaceholder": "225/45R17",
+    "dotCode": "Код DOT",
+    "dotPlaceholder": "3623",
+    "dotHint": "Неделя и год выпуска шины.",
+    "withRims": "На дисках",
+    "hasTpms": "Датчики TPMS",
+    "studded": "Шипованные",
+    "location": "Где будут храниться шины",
+    "choosePlace": "Выберите полку",
+    "searchLocations": "Поиск полок",
+    "noLocations": "Полки не найдены",
+    "tooFull": "Недостаточно места",
+    "pickLocationFirst": "Выберите полку, прежде чем принимать комплект",
+    "roomSummary": "Подходит полок: {locations}, свободно мест: {tires}",
+    "nothingFits": "Ни на одной полке нет места для {quantity} шин. Освободите место или добавьте полку.",
+    "treadTitle": "Глубина протектора при приёмке",
+    "treadHint": "Необязательно, но снимает любые последующие споры о состоянии.",
+    "notes": "Заметки",
+    "submit": "Принять",
+    "success": "Комплект {reference} размещён на {code}",
+    "failed": "Не удалось принять комплект",
+    "noVehiclesForCustomer": "У этого клиента ещё нет автомобилей. Сначала добавьте его на странице клиента.",
+    "treatmentsHint": "Что нужно сделать с комплектом перед хранением или установкой.",
+    "storedBefore": "Уже хранился здесь",
+    "storedBeforeHint": "Возвращаются те же шины? Выберите комплект, и сезон попадёт в его историю.",
+    "sameSet": "Тот же комплект, что и раньше",
+    "pieces": "{count} шт.",
+    "lastOut": "Забрали {date}",
+    "notStored": "Сейчас не на хранении",
+    "returned": "Комплект {reference} снова на {code}",
+    "submitReturn": "Принять снова",
+    "ownerFromVehicle": "Взято из автомобиля",
+    "vehicleOwner": "Владелец автомобиля",
+    "clearVehicle": "Очистить автомобиль"
+  },
+  "storage": {
+    "title": "Полки",
+    "description": "Настройте склад один раз и сразу видьте, что и куда помещается.",
+    "addWarehouse": "Добавить склад",
+    "addShelves": "Добавить полки",
+    "warehouseName": "Название",
+    "warehouseNamePlaceholder": "Главное здание, Контейнер 2, Подсобка",
+    "warehouseAddress": "Адрес",
+    "warehouseCreated": "Склад добавлен",
+    "warehouseDeleted": "{name} удалён",
+    "warehouseArchived": "{name} перемещён в архив",
+    "deleteWarehouse": "Удалить склад",
+    "deleteWarehouseTitle": "Удалить {name}?",
+    "deleteWarehouseBody": "Это удалит склад и все его полки.",
+    "deleteWarehouseBodyOccupied": "На этом складе ещё {count} шин, поэтому он будет перемещён в архив, а не удалён.",
+    "totalCapacity": "Общая вместимость",
+    "totalStored": "Сейчас на хранении",
+    "totalFree": "Свободно",
+    "totalLocations": "Полки",
+    "locationSummary": "Занято полок: {occupied} из {total}",
+    "noLocations": "Полок пока нет. Добавьте стеллаж, чтобы хранить здесь шины.",
+    "emptyTitle": "Склад ещё не настроен",
+    "emptyBody": "Добавьте первое здание или помещение и заполните его полками.",
+    "default": "По умолчанию",
+    "addLocation": "Добавить полки",
+    "editLocation": "Изменить полку",
+    "createLocation": "Создать полку",
+    "createShelves": "Создать полки",
+    "locationDialogDescription": "Добавление мест хранения в {warehouse}.",
+    "tabBulk": "Стеллаж целиком",
+    "tabSingle": "Одна полка",
+    "zone": "Зона",
+    "zonePlaceholder": "A, Помещение 1, Север",
+    "rack": "Стеллаж",
+    "rackPlaceholder": "04",
+    "shelf": "Полка",
+    "shelfPlaceholder": "2",
+    "position": "Позиция",
+    "positionPlaceholder": "Левая",
+    "shelfFrom": "Полки с",
+    "shelfTo": "по",
+    "code": "Код",
+    "codePlaceholder": "A-04-2",
+    "codeHint": "Печатается на этикетке. Заполнится автоматически, если оставить пустым.",
+    "capacity": "Шин на полке",
+    "capacityHint": "Сколько отдельных шин сюда помещается.",
+    "notes": "Заметки",
+    "bulkPreview": "Создаст {count} полок, с {first} по {last}",
+    "bulkPreviewCapacity": "Место для {tires} шин в общей сложности",
+    "bulkCreated": "Создано полок: {count}",
+    "bulkCreatedWithSkips": "Создано полок: {created}, пропущено уже существующих: {skipped}",
+    "bulkNothingCreated": "Такие полки уже есть",
+    "locationCreated": "Полка добавлена",
+    "locationUpdated": "Полка обновлена",
+    "locationDeleted": "{code} удалена",
+    "deleteLocationTitle": "Удалить {code}?",
+    "deleteLocationBody": "Сначала полка должна быть пустой.",
+    "saveFailed": "Не удалось сохранить",
+    "deleteFailed": "Не удалось удалить"
+  },
+  "movements": {
+    "check_in": "Принят",
+    "check_out": "Выдан",
+    "relocate": "Перемещён",
+    "dispose": "Утилизирован",
+    "measure": "Измерено"
+  },
+  "detail": {
+    "move": "Переместить",
+    "checkOut": "Выдать",
+    "conditionTitle": "Состояние",
+    "noMeasurements": "Замеры ещё не записаны.",
+    "historyTitle": "История",
+    "storageTitle": "Хранение",
+    "shelf": "Полка",
+    "warehouse": "Склад",
+    "notStored": "Сейчас не на хранении.",
+    "checkedIn": "Принят",
+    "checkedOut": "Выдан",
+    "tiresTitle": "Шины",
+    "loadSpeed": "Индексы нагрузки и скорости",
+    "ownerTitle": "Владелец",
+    "phone": "Телефон",
+    "deleteTitle": "Удалить этот комплект шин?",
+    "deleteBody": "Комплект и вся его история будут удалены. Отменить нельзя.",
+    "deleted": "Комплект шин удалён",
+    "deleteFailed": "Не удалось удалить комплект",
+    "belowLimit": "Шин на пределе замены или ниже: {count}.",
+    "belowLimitHint": "Стоит предложить клиенту цену, пока шины ещё здесь.",
+    "dispose": "Списать",
+    "disposeTitle": "Списать этот комплект?",
+    "disposeBody": "Запись и история остаются, место на полке освобождается. В следующем сезоне он не будет предлагаться как те же шины.",
+    "disposed": "Комплект списан",
+    "disposeFailed": "Не удалось списать комплект"
+  },
+  "checkOut": {
+    "title": "Выдать шины",
+    "description": "Комплект снимается с {code}.",
+    "descriptionNoLocation": "Этот комплект возвращается клиенту.",
+    "treadTitle": "Глубина протектора при выдаче",
+    "note": "Заметка",
+    "submit": "Выдать",
+    "success": "Комплект {reference} выдан",
+    "failed": "Не удалось выдать комплект",
+    "prepPending": "Осталось задач подготовки: {count}",
+    "conditionTitle": "Состояние при приёмке",
+    "conditionLead": {
+      "replace": "Одна или несколько шин на пределе замены или ниже. Стоит сказать клиенту до того, как он уедет.",
+      "fair": "Одна или несколько шин близки к пределу замены. Стоит упомянуть перед следующим сезоном."
+    },
+    "treadHintUnmeasured": "При приёмке замер не делали. Это последняя возможность его записать."
+  },
+  "relocate": {
+    "title": "Переместить шины",
+    "description": "Выберите полку, куда переедет комплект.",
+    "newLocation": "Новая полка",
+    "note": "Заметка",
+    "submit": "Переместить",
+    "success": "Комплект {reference} перемещён",
+    "failed": "Не удалось переместить комплект"
+  },
+  "edit": {
+    "title": "Изменить комплект шин",
+    "description": "Исправьте то, что записали при приёмке. Полку меняйте через «Переместить».",
+    "quantityHint": "Увеличение проверяется по полке, на которой лежит комплект.",
+    "rimType": "Тип диска",
+    "rimTypePlaceholder": "Литой, стальной",
+    "success": "Комплект шин обновлён",
+    "failed": "Не удалось сохранить изменения",
+    "treadHint": "Изменённое измерение записывается в историю как новое, а не заменяет прежнее."
+  },
+  "treatments": {
+    "types": {
+      "wash_tires": "Помыть шины",
+      "wash_rims": "Помыть диски",
+      "balance": "Балансировка",
+      "tpms_service": "Обслуживание TPMS",
+      "new_valves": "Новые вентили",
+      "repair": "Ремонт"
+    },
+    "statuses": {
+      "pending": "Нужно сделать",
+      "done": "Готово",
+      "skipped": "Пропущено"
+    },
+    "title": "Подготовка",
+    "none": "С этим комплектом делать нечего.",
+    "add": "Добавить подготовку",
+    "pickHint": "Отметьте, что нужно сделать с комплектом перед установкой на автомобиль.",
+    "pendingCount": "Осталось: {count}",
+    "allComplete": "Всё готово",
+    "ready": "Готов",
+    "markDone": "Готово",
+    "completeAll": "Отметить все {count} как готовые",
+    "allDone": "Подготовка завершена",
+    "undo": "Вернуть в список",
+    "saveFailed": "Не удалось сохранить",
+    "skip": "Пропустить"
+  },
+  "messaging": {
+    "action": "Сообщение",
+    "title": "Написать клиенту",
+    "description": "Для {name}.",
+    "noCustomer": "У этого комплекта нет клиента.",
+    "noCustomerBody": "Привяжите клиента к комплекту, прежде чем писать.",
+    "channel": "Отправить через",
+    "channels": {
+      "email": "E-mail",
+      "sms": "SMS",
+      "telegram": "Telegram"
+    },
+    "noAddress": "адрес не записан",
+    "recipient": "Отправить на",
+    "recipientPlaceholder": "Номер телефона",
+    "subject": "Тема",
+    "message": "Сообщение",
+    "send": "Отправить",
+    "sent": "Сообщение отправлено",
+    "failed": "Не удалось отправить сообщение",
+    "smsLong": "Длинные сообщения могут быть разбиты на несколько SMS.",
+    "subjects": {
+      "low_tread": "Ваши шины ({season})",
+      "damage": "Ваши шины ({season})",
+      "stored": "Ваши шины ({season}) на хранении",
+      "custom": "Ваши шины ({season})"
+    },
+    "bodies": {
+      "low_tread": "Здравствуйте, {customer_name}. Ваши шины ({season}) лежат у нас в {shop_name}. При приёмке на {positions} было {tread}, это ниже предела. Ещё один сезон они не выходят. Посчитать вам новый комплект?",
+      "damage": "Здравствуйте, {customer_name}. Ваши шины ({season}) лежат у нас в {shop_name}. При приёмке мы нашли повреждение на {positions}. Позвоните, когда будет удобно, и мы вместе всё посмотрим.",
+      "stored": "Здравствуйте, {customer_name}. Ваши шины ({season}) теперь на хранении у нас в {shop_name}. Сообщим, когда придёт время переобуваться.",
+      "custom": "Здравствуйте, {customer_name}. По поводу ваших шин ({season}), которые лежат у нас в {shop_name}:"
+    },
+    "enableInSettings": "Ещё не настроено. Включите в настройках."
+  },
+  "job": {
+    "cardTitle": "Сметы и заказы",
+    "none": "По этому комплекту пока ничего не создано.",
+    "quote": "Смета",
+    "workOrder": "Заказ-наряд",
+    "converted": "преобразовано",
+    "quoteTitle": "Смета на новый комплект",
+    "workOrderTitle": "Выставить за этот комплект",
+    "loadingSet": "Загрузка комплекта",
+    "tires": "Шины",
+    "inStock": "{count} на складе",
+    "shortStock": "на складе только {count}",
+    "noMatch": "На складе нет ничего под размер {size}. Строка добавится с указанной вами ценой.",
+    "noSize": "У комплекта не записан размер, сопоставить со складом нельзя. Строка добавится с вашей ценой.",
+    "unitPrice": "Цена за шину",
+    "createQuote": "Создать смету",
+    "createWorkOrder": "Создать заказ-наряд",
+    "quoteCreated": "Смета {number} создана",
+    "workOrderCreated": "Заказ-наряд создан",
+    "failed": "Не удалось создать",
+    "bannerTitle": "Забрать с",
+    "openSet": "Открыть комплект",
+    "prepPending": "Сделать до установки: {jobs}",
+    "notOnShelf": "Этот комплект больше не на хранении.",
+    "targetNew": "Новый заказ-наряд",
+    "targetNewHint": "Создать заказ только для этих шин.",
+    "targetExisting": "Или добавить в уже открытый заказ по этому автомобилю",
+    "addToWorkOrder": "Добавить в заказ",
+    "workOrderUpdated": "Добавлено в заказ-наряд",
+    "unlink": "Отвязать",
+    "unlinkTitle": "Отвязать эти шины от заказа?",
+    "unlinkBody": "Заказ перестанет ссылаться на комплект шин. Уже добавленные позиции по запчастям и работам останутся, поэтому удалите их отдельно, если выставлять их не нужно.",
+    "unlinked": "Шины отвязаны",
+    "unlinkFailed": "Не удалось отвязать шины",
+    "prep": "Подготовка",
+    "tiresSkipped": "Строка с шинами не будет добавлена.",
+    "total": "Итого",
+    "searchStock": "Поиск по складу: марка, размер или артикул",
+    "noSearchHits": "На складе ничего не найдено по запросу \"{query}\".",
+    "otherSize": "Другой размер",
+    "inSize": "{count} в размере {size}",
+    "quantity": "Кол-во",
+    "noStockItem": "Нет складской позиции, цена вручную",
+    "storeTires": "Сдать шины на хранение",
+    "storage": "Хранение",
+    "storageFee": "Сумма",
+    "storagePeriod": "Период",
+    "destination": "Куда попадёт",
+    "targetInvoice": "Новый счёт",
+    "targetInvoiceHint": "Выставить клиенту напрямую, без заказ-наряда на автомобиль.",
+    "createInvoice": "Создать счёт",
+    "needsCustomer": "Свяжите комплект с клиентом или автомобилем перед выставлением счёта.",
+    "storagePeriodEnd": "Забрали",
+    "storageOpenEnded": "Открыт",
+    "storagePeriodHint": "Печатается в строке счёта, чтобы клиент видел, за что платит. Оставьте окончание пустым, пока не знаете, когда заберут.",
+    "pricesHint": "Цены на подготовку берутся из настроек. Любую строку можно изменить после создания документа.",
+    "files": "Фото и файлы",
+    "filesCount": "{count} в счёте"
+  },
+  "label": {
+    "action": "Этикетки",
+    "title": "Печать этикеток",
+    "description": "Наклейка на каждую шину, с QR-кодом на этот комплект.",
+    "format": "Размер этикетки",
+    "formats": {
+      "dymo_small": "Малый рулон",
+      "dymo_standard": "Стандартный рулон",
+      "thermal_large": "Большой рулон",
+      "sheet_a4": "Лист A4"
+    },
+    "perSheet": "{count} на лист",
+    "copies": "Сколько",
+    "copiesHint": "по одной на шину ({count})",
+    "fewer": "На одну меньше",
+    "more": "На одну больше",
+    "print": "Печать",
+    "shelfNote": "Полка намеренно не печатается. Комплекты переставляют, и напечатанная полка окажется неверной. QR-код всегда показывает текущее место.",
+    "reference": "Номер",
+    "quantity": "{count, plural, one {# шина} few {# шины} many {# шин} other {# шины}}",
+    "withRims": "На дисках",
+    "tpms": "TPMS",
+    "studded": "Шипы",
+    "unassigned": "Без владельца",
+    "preview": "Предпросмотр",
+    "previewScale": "Показан в размере печати. Код можно отсканировать прямо с экрана.",
+    "previewFailed": "Не удалось построить предпросмотр"
+  },
+  "vehicle": {
+    "oneOnShelf": "на {code}",
+    "inStorage": "в шинном отеле"
+  },
+  "wear": {
+    "summary": "Износ {value} с {from} по {to}",
+    "perSeason": "Около {value} за сезон",
+    "seasonsLeft": "{count, plural, =0 {Уже на пределе} one {Примерно ещё один сезон} other {Примерно ещё # сезона}}",
+    "since": "-{value}"
+  },
+  "settings": {
+    "change": "Изменить в настройках",
+    "prepPrices": "Цены на подготовку",
+    "storagePrices": "Цены на хранение",
+    "treadLimits": "Изменить порог",
+    "defaultCapacity": "Вместимость по умолчанию"
+  },
+  "forecast": {
+    "title": "Следующий сезон",
+    "subtitle": "Что по замерам потребуется хранящимся комплектам, по размерам.",
+    "backToList": "К списку комплектов",
+    "tires": "Ожидается шин",
+    "atLimit": "Уже на пределе",
+    "expected": "Ожидается в течение сезона",
+    "orderTitle": "По размерам",
+    "sizes": "{count} размеров",
+    "empty": "Пока ничего не измерено близко к пределу. Для расчёта износа нужны два визита.",
+    "nowCount": "{count, plural, one {# комплект на пределе} other {# комплекта на пределе}}",
+    "nextCount": "{count, plural, one {# комплект в течение сезона} other {# комплекта в течение сезона}}",
+    "tiresShort": "шин",
+    "lowest": "минимум {value}",
+    "now": "На пределе",
+    "next": "В течение сезона",
+    "rate": "{value} за сезон",
+    "caveat": "Комплекты на пределе измерены. Остальное рассчитано по износу одного клиента за один интервал: для планирования, а не для заказа.",
+    "unmeasured": "{count, plural, one {у # комплекта мало истории, он не учтён} other {у # комплектов мало истории, они не учтены}}",
+    "capped": "Прочитано {shown} самых свежих комплектов из {total}."
+  },
+  "invoiceLine": {
+    "storage": "Хранение шин",
+    "pieces": "шт.",
+    "fromDate": "с {date}"
+  },
+  "setNames": {
+    "winter": "зимние шины",
+    "summer": "летние шины",
+    "all_season": "всесезонные шины",
+    "other": "шины"
+  },
+  "jobNotes": {
+    "set": "Комплект шин {reference}",
+    "shelf": "Полка: {shelf}",
+    "notOnShelf": "не на полке",
+    "onRims": "На дисках",
+    "tiresOnly": "Только шины",
+    "tpms": "С датчиками TPMS"
+  },
+  "files": {
+    "title": "Фото и файлы",
+    "add": "Добавить файлы",
+    "none": "Пока ничего не прикреплено. Сфотографируйте задетый диск, пока шины перед вами.",
+    "captionPlaceholder": "Что на снимке?",
+    "onInvoice": "В счёте",
+    "invoiceHint": "Файлы, отмеченные для счёта, копируются в заказ-наряд или счёт при выставлении за комплект.",
+    "deleteTitle": "Удалить этот файл?",
+    "deleteBody": "{name} убирается из комплекта. То, что уже скопировано в счёт, остаётся там.",
+    "failed": "Не удалось сохранить",
+    "someFailed": "{count, plural, one {не удалось загрузить # файл} other {не удалось загрузить # файлов}}"
+  }
+}

+ 27 - 1
messages/tr/audit.json

@@ -109,6 +109,32 @@
     "settings_applyTaxRate": "Vergi oranı uygulandı",
     "settings_convertToExclusive": "Fiyatlar vergi hariç yapıldı",
     "settings_convertToInclusive": "Fiyatlar vergi dahil yapıldı",
-    "organization_sampleDataRemove": "Örnek veriler kaldırıldı"
+    "organization_sampleDataRemove": "Örnek veriler kaldırıldı",
+    "tire_warehouse_create": "Lastik depo yeri oluşturuldu",
+    "tire_warehouse_update": "Lastik depo yeri güncellendi",
+    "tire_warehouse_delete": "Lastik depo yeri silindi",
+    "tire_warehouse_archive": "Lastik depo yeri arşivlendi",
+    "tire_location_create": "Lastik rafı oluşturuldu",
+    "tire_location_bulk_create": "Lastik rafları oluşturuldu",
+    "tire_location_update": "Lastik rafı güncellendi",
+    "tire_location_delete": "Lastik rafı silindi",
+    "tire_set_check_in": "Lastik takımı kabul edildi",
+    "tire_set_check_out": "Lastik takımı teslim edildi",
+    "tire_set_relocate": "Lastik takımı taşındı",
+    "tire_set_update": "Lastik takımı güncellendi",
+    "tire_set_delete": "Lastik takımı silindi",
+    "tire_measurement_create": "Lastik ölçümleri kaydedildi",
+    "tire_treatment_update": "Hazırlık güncellendi",
+    "tire_treatment_mark": "Hazırlık işaretlendi",
+    "tire_treatment_complete_all": "Hazırlık tamamlandı",
+    "tire_set_message_customer": "Lastik takımı hakkında müşteriye mesaj gönderildi",
+    "tire_set_create_quote": "Lastik takımından teklif oluşturuldu",
+    "tire_set_create_work_order": "Lastik takımından iş emri oluşturuldu",
+    "tire_set_add_to_work_order": "Lastik takımı bir iş emrine eklendi",
+    "tire_set_unlink_work_order": "Lastik takımı bir iş emrinden ayrıldı",
+    "tire_set_return": "Takım yeniden depolandı",
+    "tire_set_dispose": "Lastik takımı hurdaya ayrıldı",
+    "tire_set_attach": "Lastik takımına dosya eklendi",
+    "tire_set_detach": "Lastik takımından dosya kaldırıldı"
   }
 }

Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff