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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,11 @@ rm -rf .built-assets/ # Clear asset cache if needed
4. Add appropriate tests (see testing docs)
5. Run pre-commit validation commands

**Code Style:**

- **Always use symbol key syntax (`key:`) in hashes** - Use `{ name: "value" }` not `{ "name" => "value" }` unless there's a specific reason to use string keys (like when the key contains special characters or spaces)
- This applies to serializers, API responses, and all Ruby hash structures

## Reference Documentation

- `docs/context/overview.md` - Complete codebase documentation and setup guide
Expand Down
30 changes: 30 additions & 0 deletions app/assemblers/assemble_localization_glossary_entries.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
class AssembleLocalizationGlossaryEntries
include Mandate

initialize_with :user, :params

def self.keys
%i[criteria status page]
end

def call
SerializePaginatedCollection.(
glossary_entries,
serializer: SerializeLocalizationGlossaryEntries,
serializer_args: user,
meta: {
unscoped_total: Localization::GlossaryEntry.count
}
)
end

memoize
def glossary_entries
Localization::GlossaryEntry::Search.(
user,
criteria: params[:criteria],
status: params[:status],
page: params[:page]
)
end
end
54 changes: 54 additions & 0 deletions app/commands/localization/glossary_entry/search.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
class Localization::GlossaryEntry::Search
include Mandate

DEFAULT_PAGE = 1
DEFAULT_PER = 24

def self.default_per
DEFAULT_PER
end

def initialize(user, page: nil, per: nil, criteria: nil, status: nil)
@user = user

@criteria = criteria
@status = status
@page = page.present? && page.to_i.positive? ? page.to_i : DEFAULT_PAGE
@per = per.present? && per.to_i.positive? ? per.to_i : self.class.default_per
end

def call
@glossary_entries = Localization::GlossaryEntry.where(locale: locales)

filter_criteria!
filter_status!

paginated_glossary_entries = @glossary_entries.page(page).per(per)

Kaminari.paginate_array(
paginated_glossary_entries,
total_count: @glossary_entries.count
).page(page).per(per)
end

memoize
def locales = user.translator_locales - [:en]

private
attr_reader :user, :per, :page, :criteria, :status

def filter_criteria!
return if criteria.blank?

@glossary_entries = @glossary_entries.where(
"localization_glossary_entries.term LIKE ? OR localization_glossary_entries.translation LIKE ?",
"%#{criteria}%", "%#{criteria}%"
)
end

def filter_status!
return if status.blank?

@glossary_entries = @glossary_entries.where("localization_glossary_entries.status": status)
end
end
8 changes: 3 additions & 5 deletions app/commands/localization/glossary_entry_proposal/approve.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,16 @@ def handle_addition!
end

def handle_modification!
raise "No glossary entry to modify" if glossary_entry.nil?
raise "No glossary entry to modify" if proposal.glossary_entry.nil?

glossary_entry.update!(
proposal.glossary_entry.update!(
translation: proposal.translation,
llm_instructions: proposal.llm_instructions
)
end

def handle_deletion!
raise "No glossary entry to delete" if glossary_entry.nil?
raise "No glossary entry to delete" if proposal.glossary_entry.nil?

# Retrieve this before destroying else we have a race
glossary_entry = proposal.glossary_entry
Expand All @@ -48,6 +48,4 @@ def handle_deletion!
proposal.update!(glossary_entry: nil)
glossary_entry.destroy!
end

delegate :glossary_entry, to: :proposal
end
12 changes: 12 additions & 0 deletions app/commands/localization/glossary_entry_proposal/create.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
class Localization::GlossaryEntryProposal
class Create
include Mandate

initialize_with :glossary_entry, :user, :value

def call
# For existing glossary entries, create a modification proposal
CreateModification.(glossary_entry, user, value, "")
end
end
end
2 changes: 0 additions & 2 deletions app/commands/localization/glossary_entry_proposal/reject.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,4 @@ def call
)
end
end

delegate :glossary_entry, to: :proposal
end
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,23 @@ def create
Localization::GlossaryEntryProposal::Create.(@glossary_entry, current_user, params[:value])

render json: {
glossary_entry: SerializeLocalizationGlossaryEntry.(@glossary_entry)

glossary_entry: SerializeLocalizationGlossaryEntry.(@glossary_entry, current_user)
}, status: :created
end

def approve
Localization::GlossaryEntryProposal::Approve.(@glossary_entry, current_user)
Localization::GlossaryEntryProposal::Approve.(@proposal, current_user)

render json: {
glossary_entry: SerializeLocalizationGlossaryEntry.(@glossary_entry)
glossary_entry: SerializeLocalizationGlossaryEntry.(@glossary_entry, current_user)
}
end

def reject
Localization::GlossaryEntryProposal::Reject.(@glossary_entry, current_user)
Localization::GlossaryEntryProposal::Reject.(@proposal, current_user)

render json: {
glossary_entry: SerializeLocalizationGlossaryEntry.(@glossary_entry)
glossary_entry: SerializeLocalizationGlossaryEntry.(@glossary_entry, current_user)
}
end

Expand All @@ -36,7 +35,7 @@ def update
end

render json: {
glossary_entry: SerializeLocalizationGlossaryEntry.(@glossary_entry)
glossary_entry: SerializeLocalizationGlossaryEntry.(@glossary_entry, current_user)
}
end

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
class Localization::Controller < ApplicationController
class Localization::GlossaryEntriesController < ApplicationController
def index
@glossary_entries = AssembleLocalizationGlossaryEntries.(current_user, params)[:results]
@glossary_entries_params = params.permit(:criteria, :status, :page)
Expand Down
15 changes: 15 additions & 0 deletions app/models/localization/glossary_entry.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
class Localization::GlossaryEntry < ApplicationRecord
enum :status, {
generating: 0,
unchecked: 1,
proposed: 2,
checked: 3
}

has_many :proposals, class_name: "Localization::GlossaryEntryProposal", dependent: :destroy

before_create do
self.uuid = SecureRandom.uuid if uuid.blank?
self.status = :unchecked if status.blank?
end

def to_param = uuid
def status = super.to_sym
end
31 changes: 31 additions & 0 deletions app/serializers/serialize_localization_glossary_entries.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
class SerializeLocalizationGlossaryEntries
include Mandate

initialize_with :glossary_entries, :user

def call
glossary_entries_with_includes.map do |glossary_entry|
{
uuid: glossary_entry.uuid,
locale: glossary_entry.locale,
term: glossary_entry.term,
translation: glossary_entry.translation,
status: glossary_entry.status.to_s,
llm_instructions: glossary_entry.llm_instructions,
proposals_count: proposals[glossary_entry.id]&.length || 0
}
end
end

def glossary_entries_with_includes
glossary_entries.to_active_relation
end

memoize
def proposals
Localization::GlossaryEntryProposal.
where(glossary_entry: glossary_entries).
where.not(status: :rejected).
group_by(&:glossary_entry_id)
end
end
35 changes: 35 additions & 0 deletions app/serializers/serialize_localization_glossary_entry.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
class SerializeLocalizationGlossaryEntry
include Mandate

initialize_with :glossary_entry, :user, proposals: nil

def call
{
uuid: glossary_entry.uuid,
locale: glossary_entry.locale,
term: glossary_entry.term,
translation: glossary_entry.translation,
status: glossary_entry.status.to_s,
llm_instructions: glossary_entry.llm_instructions,
proposals: proposals.map do |proposal|
{
uuid: proposal.uuid,
type: proposal.type.to_s,
status: proposal.status.to_s,
term: proposal.term,
translation: proposal.translation,
llm_instructions: proposal.llm_instructions,
proposer_id: proposal.proposer&.id,
reviewer_id: proposal.reviewer&.id
}
end
}
end

memoize
def proposals
Array.new(
@proposals || @glossary_entry.proposals.where.not(status: :rejected)
)
end
end
7 changes: 7 additions & 0 deletions config/routes/api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,13 @@
patch :reject, on: :member
end
end

resources :glossary_entries, only: %i[index show] do
resources :proposals, only: %i[create update], controller: "glossary_entry_proposals" do
patch :approve, on: :member
patch :reject, on: :member
end
end
end

get "/scratchpad/:category/:title" => "scratchpad_pages#show", as: :scratchpad_page
Expand Down
2 changes: 2 additions & 0 deletions config/routes/website.rb
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,8 @@
root to: "dashboard#show"
resources :originals, only: %i[index show] do
end
resources :glossary_entries, only: %i[index show] do
end
end

resource :user_onboarding, only: %i[show create], controller: "user_onboarding"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class AddStatusToLocalizationGlossaryEntries < ActiveRecord::Migration[7.1]
def change
add_column :localization_glossary_entries, :status, :integer, default: 1, null: false
add_column :localization_glossary_entries, :uuid, :string, null: false
add_index :localization_glossary_entries, :uuid, unique: true
end
end
5 changes: 4 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema[7.1].define(version: 2025_08_28_143607) do
ActiveRecord::Schema[7.1].define(version: 2025_09_05_142739) do
create_table "active_storage_attachments", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
t.string "name", null: false
t.string "record_type", null: false
Expand Down Expand Up @@ -739,6 +739,9 @@
t.text "llm_instructions", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.integer "status", default: 1, null: false
t.string "uuid", null: false
t.index ["uuid"], name: "index_localization_glossary_entries_on_uuid", unique: true
end

create_table "localization_glossary_entry_proposals", charset: "utf8mb4", collation: "utf8mb4_unicode_ci", force: :cascade do |t|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@ class API::Localization::GlossaryEntriesControllerTest < API::BaseTestCase
test "show returns a localization glossary entry with proposals" do
setup_user
glossary_entry = create :localization_glossary_entry
translation = create :localization_translation, key: glossary_entry.key, locale: "en"
create(:localization_translation_proposal, translation:)
create :localization_glossary_entry_proposal, :modification, glossary_entry: glossary_entry, proposer: @current_user

get api_localization_glossary_entry_path(glossary_entry.uuid), headers: @headers, as: :json

Expand Down
Loading
Loading