Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
da2e16a
Add basic translations code/logic
iHiD Aug 11, 2025
f74ee3a
WIP
iHiD Aug 11, 2025
7456b2a
Update migrations
iHiD Aug 12, 2025
aee729b
Update migrations
iHiD Aug 12, 2025
465e5c4
Add i18n js backend file loader (#8099)
dem4ron Aug 12, 2025
7033d33
Start work on things
iHiD Aug 12, 2025
a2cd0a6
Add approval of LLM version
iHiD Aug 12, 2025
74ac6dd
Improve UI
iHiD Aug 12, 2025
925e51c
Add more endpoints
iHiD Aug 13, 2025
f984d4e
Return data from API
iHiD Aug 13, 2025
88d1e4f
Fix
iHiD Aug 13, 2025
990c09f
Improve searching
iHiD Aug 13, 2025
32540f5
Add LLM verification
iHiD Aug 13, 2025
6725277
Tweaks
iHiD Aug 13, 2025
6deccf6
Add localization generating flow
iHiD Aug 14, 2025
a1752fd
Reword lots
iHiD Aug 14, 2025
3b620f6
Add locales as URL params
iHiD Aug 14, 2025
a6fc75c
Fix default vs constraint
iHiD Aug 14, 2025
2c53ee2
Fix Zeitwerk
iHiD Aug 15, 2025
a6b374d
Redirect logged-in users to correct locale
iHiD Aug 18, 2025
0cea687
Check routing
iHiD Aug 18, 2025
e83ba13
Redirect users to their selected locale
iHiD Aug 18, 2025
5b2a579
Correctly render things
iHiD Aug 18, 2025
af4eebd
Fix rebase
iHiD Aug 18, 2025
aa52617
Add locales to footer
iHiD Aug 18, 2025
927a709
Tidy
iHiD Aug 18, 2025
2e050d8
Improve locale support
iHiD Aug 18, 2025
cd1a40c
Improve locale support
iHiD Aug 18, 2025
cea05f0
WIP
iHiD Aug 20, 2025
e0a4523
Rename migration
iHiD Aug 21, 2025
68b975b
Further progress
iHiD Aug 21, 2025
62bc71c
Add test
iHiD Aug 21, 2025
0ea24a8
Add test
iHiD Aug 21, 2025
f2587b4
Add tests
iHiD Aug 21, 2025
3c44888
Rework prompts
iHiD Aug 22, 2025
525076a
Sync things up
iHiD Aug 25, 2025
e0d4ece
Add Translation Placeholder (#8149)
dem4ron Aug 25, 2025
06103db
Add React Translation UI pages
dem4ron Aug 15, 2025
3602c48
Fix bad filename
iHiD Aug 26, 2025
7893c4c
Fix LLM verification
iHiD Aug 26, 2025
24c0f89
Add title to originals
iHiD Aug 26, 2025
6aa7961
Fix serialzier
iHiD Aug 26, 2025
bcb9cdd
Remove stray file
iHiD Aug 26, 2025
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
4 changes: 4 additions & 0 deletions .cursor/rules/general.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
alwaysApply: true
---
Always start by reading .github/copilot-instructions.md and any docs/llm-support that are required.
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ gem 'octokit' # GitHub
gem 'mandate', '~> 2.0'
gem 'kaminari'
gem 'oj', '~> 3.14.0'
gem 'i18n', '>= 0.5.0'

# Setup dependencies
gem 'exercism-config', '>= 0.130.0'
Expand Down
1 change: 1 addition & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,7 @@ DEPENDENCIES
haml_lint
hamlit
humanize
i18n (>= 0.5.0)
image_processing (~> 1.2)
jsbundling-rails
kaminari
Expand Down
2 changes: 1 addition & 1 deletion Procfile.dev
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
server: bin/rails server -p 3020
anycable: bundle exec anycable
sidekiq: bundle exec sidekiq
#sidekiq: bundle exec sidekiq
ws: anycable-go --host='local.exercism.io' --rpc_host='local.exercism.io:50051' --port=3334
css: yarn build:css --watch
js: yarn build --watch
Expand Down
30 changes: 30 additions & 0 deletions app/assemblers/assemble_localization_originals.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
class AssembleLocalizationOriginals
include Mandate

initialize_with :user, :params

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

def call
SerializePaginatedCollection.(
translations,
serializer: SerializeLocalizationOriginals,
serializer_args: user,
meta: {
unscoped_total: Localization::Original.count
}
)
end

memoize
def translations
Localization::Original::Search.(
user,
criteria: params[:criteria],
status: params[:status],
page: params[:page]
)
end
end
22 changes: 22 additions & 0 deletions app/channels/localization_translation_channel.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
class LocalizationTranslationChannel < ApplicationCable::Channel
def self.broadcast!(translation)
ActionCable.server.broadcast(
channel_name(translation.locale),
{
key: translation.key,
locale: translation.locale,
value: translation.value
}
)
end

def subscribed
stream_from self.class.channel_name(params[:locale])
end

def unsubscribed; end

def self.channel_name(locale)
"localization_translations_#{locale}"
end
end
3 changes: 2 additions & 1 deletion app/commands/cache/key_for_footer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ def call
parts << ::Track.num_active
parts << user_part
parts << stripe_version
parts << "v3"
parts << I18n.available_locales.count
parts << "3" # Basic expiry key

parts.join(':')
end
Expand Down
30 changes: 27 additions & 3 deletions app/commands/generic_exercise/create_or_update.rb
Original file line number Diff line number Diff line change
@@ -1,20 +1,44 @@
class GenericExercise::CreateOrUpdate
include Mandate

initialize_with :slug, :title, :blurb, :source, :source_url, :deep_dive_youtube_id, :deep_dive_blurb, :status
initialize_with :repo_exercise

def call
create!.tap do |exercise|
exercise.update!(attributes)

localize!(:generic_exercise_instructions, exercise.instructions, exercise)
localize!(:generic_exercise_introduction, exercise.introduction, exercise)
localize!(:generic_exercise_title, exercise.title, exercise)
localize!(:generic_exercise_blurb, exercise.blurb, exercise)
localize!(:generic_exercise_source, exercise.source, exercise)
end
end

private
def create!
GenericExercise.find_create_or_find_by!(slug:) do |exercise|
GenericExercise.find_create_or_find_by!(slug: repo_exercise[:slug]) do |exercise|
exercise.attributes = attributes
end
end

def attributes = { title:, blurb:, source:, source_url:, deep_dive_youtube_id:, deep_dive_blurb:, status: }
def localize!(type, content, exercise)
return unless content.present?

Localization::Text::AddToLocalization.defer(type, content, exercise)
end

memoize
def attributes
{
slug: repo_exercise[:slug],
title: repo_exercise[:title],
blurb: repo_exercise[:blurb],
source: repo_exercise[:source],
source_url: repo_exercise[:source_url],
deep_dive_youtube_id: repo_exercise[:deep_dive_youtube_id],
deep_dive_blurb: repo_exercise[:deep_dive_blurb],
status: repo_exercise[:deprecated] ? :deprecated : :active
}
end
end
15 changes: 13 additions & 2 deletions app/commands/git/sync_blog.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,19 @@ def create_or_update_post(data)
end

post.update!(attributes)
rescue StandardError => e
Github::Issue::OpenForBlogSyncFailure.(e, repo.head_commit.oid)

localize!(:post_title, post.title, post.id)
localize!(:post_description, post.description, post.id)
localize!(:post_content, post.content, post.id)

# rescue StandardError => e
# Github::Issue::OpenForBlogSyncFailure.(e, repo.head_commit.oid)
end

def localize!(type, content, post_id)
return unless content.present?

Localization::Text::AddToLocalization.defer(type, content, post_id)
end

def create_or_update_story(data)
Expand Down
11 changes: 11 additions & 0 deletions app/commands/git/sync_concept.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,22 @@ def call

Git::SyncConceptAuthors.(concept)
Git::SyncConceptContributors.(concept)

localize!(:concept_name, concept.name)
localize!(:concept_blurb, concept.blurb)
localize!(:concept_introduction, concept.introduction)
localize!(:concept_about, concept.about)
end

private
attr_reader :concept, :force_sync

def localize!(type, content)
return unless content.present?

Localization::Text::AddToLocalization.defer(type, content, concept.id)
end

def concept_needs_updating?
track_config_concept_modified? || concept_config_modified?
end
Expand Down
12 changes: 12 additions & 0 deletions app/commands/git/sync_concept_exercise.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,23 @@ def call
Git::SyncExerciseArticles.(exercise)
::Exercise::UpdateHasApproaches.(exercise)
SiteUpdates::ProcessNewExerciseUpdate.(exercise)

localize!(:exercise_instructions, exercise.instructions, exercise)
localize!(:exercise_introduction, exercise.introduction, exercise)
localize!(:exercise_title, exercise.title, exercise)
localize!(:exercise_blurb, exercise.blurb, exercise)
localize!(:exercise_source, exercise.source, exercise)
end

private
attr_reader :exercise, :force_sync

def localize!(type, content, exercise)
return unless content.present?

Localization::Text::AddToLocalization.defer(type, content, exercise)
end

def exercise_needs_updating?
track_config_exercise_modified? || exercise_config_modified? || exercise_files_modified?
end
Expand Down
12 changes: 12 additions & 0 deletions app/commands/git/sync_practice_exercise.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,23 @@ def call
Git::SyncExerciseArticles.(exercise)
::Exercise::UpdateHasApproaches.(exercise)
SiteUpdates::ProcessNewExerciseUpdate.(exercise)

localize!(:exercise_instructions, exercise.instructions, exercise)
localize!(:exercise_introduction, exercise.introduction, exercise)
localize!(:exercise_title, exercise.title, exercise)
localize!(:exercise_blurb, exercise.blurb, exercise)
localize!(:exercise_source, exercise.source, exercise)
end

private
attr_reader :exercise, :force_sync

def localize!(type, content, exercise)
return unless content.present?

Localization::Text::AddToLocalization.defer(type, content, exercise)
end

def exercise_needs_updating?
track_config_exercise_modified? || exercise_config_modified? || exercise_files_modified?
end
Expand Down
11 changes: 3 additions & 8 deletions app/commands/git/sync_problem_specifications.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,9 @@ def call
repo.update!

repo.exercises.each do |exercise|
GenericExercise::CreateOrUpdate.(
exercise.slug, exercise.title, exercise.blurb,
exercise.source, exercise.source_url,
exercise.deep_dive_youtube_id, exercise.deep_dive_blurb,
exercise.deprecated? ? :deprecated : :active
)
rescue StandardError => e
Bugsnag.notify(e)
GenericExercise::CreateOrUpdate.(exercise)
# rescue StandardError => e
# Bugsnag.notify(e)
end
end

Expand Down
26 changes: 26 additions & 0 deletions app/commands/llm/exec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
class LLM::Exec
include Mandate

initialize_with :service, :model, :prompt, :spi_endpoint, :stream_channel

def call
raise "Service cannot be nil" if service.nil?
raise "Model cannot be nil" if model.nil?
raise "Prompt cannot be nil" if prompt.nil?
raise "SPI endpoint cannot be nil" if spi_endpoint.nil?

RestClient.post(
proxy_url,
{
service: service,
model: model,
spi_endpoint: "llm_responses/#{spi_endpoint}",
stream_channel: stream_channel,
prompt: prompt
}.to_json,
{ content_type: :json, accept: :json }
)
end

def proxy_url = "http://localhost:8080/exec"
end
9 changes: 9 additions & 0 deletions app/commands/llm/exec_gemini_flash.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
class LLM::ExecGeminiFlash
include Mandate

initialize_with :prompt, :spi_endpoint, stream_channel: nil

def call
LLM::Exec.(:gemini, :flash, prompt, spi_endpoint, stream_channel)
end
end
11 changes: 11 additions & 0 deletions app/commands/localization/cache/retrieve.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class Localization::Cache::Retrieve
include Mandate

initialize_with :locale, :key

def call
return unless Localization.use_cache

Localization.translations.dig(locale.to_sym, key)
end
end
12 changes: 12 additions & 0 deletions app/commands/localization/cache/store.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
class Localization::Cache::Store
include Mandate

initialize_with :locale, :key, :value

def call
return unless Localization.use_cache

Localization.translations[locale.to_sym] ||= {}
Localization.translations[locale.to_sym][key] = value
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
class Localization::Content::TranslateExerciseInstructions
include Mandate

initialize_with :exercise, locale: nil

def call
# Exit early if someone is actively requesting the English
# version so that we don't go through extra lookups etc.
return instructions if locale == :en

# Look this up here, so we don't do the work of creating the context etc.
existing = Localization::Translation.find_by(key: key, locale: locale)&.value.presence
return existing if existing

# If we don't have it, then translate it. We should rarely get here.
Localization::Text::Translate.(type, instructions, exercise, locale)
end

private
delegate :instructions, to: :exercise
def type = :exercise_instructions
def key = Localization::Text::GenerateKey.(instructions)
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
class Localization::Content::TranslateExerciseIntroduction
include Mandate

initialize_with :exercise, locale: nil

def call
# Exit early if someone is actively requesting the English
# version so that we don't go through extra lookups etc.
return introduction if locale == :en

# Look this up here, so we don't do the work of creating the context etc.
existing = Localization::Translation.find_by(key: key, locale: locale)&.value.presence
return existing if existing

# If we don't have it, then translate it. We should rarely get here.
Localization::Text::Translate.(type, introduction, exercise, locale)
end

private
delegate :introduction, to: :exercise
def type = :exercise_introduction
def key = Localization::Text::GenerateKey.(introduction)
end
19 changes: 19 additions & 0 deletions app/commands/localization/original/create.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
class Localization::Original::Create
include Mandate

initialize_with :type, :key, :value, :about

def call
Localization::Original.create!(
key: key,
value: value,
type: type,
about: about
).tap do |original|
original.translations.create!(
locale: "en",
value: value
)
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
class Localization::Original::Prompts::ExerciseBlurb
include Mandate

initialize_with :original, :locale

def call
Localization::Original::Prompts::GenericExerciseBlurb.(original, locale)
end
end
Loading
Loading